Skip to content
Open
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
5 changes: 5 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ _Avoid_: reasoning text, inferred edge, model explanation
An unresolved, citation-backed Investigation Assertion ranked for follow-up after accounting for objective relevance, missing evidence, expected information gain, target importance, cost, risk, and authorization readiness.
_Avoid_: autonomous plan, agent hunch, task queue

**Product Skill Promotion**:
The reviewed transition that turns source- and Artifact-backed, target-agnostic campaign methodology into a discoverable runtime skill, while keeping eval and benchmark evidence validation-only and candidate-invisible.
_Avoid_: prompt extraction, transcript-to-skill, benchmark lesson

**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
Expand Down Expand Up @@ -156,6 +160,7 @@ _Avoid_: hidden gold, judge assertion
- A **Research Observation** may indicate several **Knowledge Concepts** through proposed, cited Investigation Assertions without becoming a **Finding**.
- 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.
- **Product Skill Promotion** requires cited reusable claims, independent campaign evidence under a published threshold, contamination review, approval-boundary review, evidence-backed validation, and an explicit deepen-versus-new decision before registry publication.
- 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.
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ The explicit Security Knowledge Graph remains in SQLite for versioned, reusable

The project Investigation Graph is an assertion layer over existing records, not another owner of Targets, Artifacts, Findings, Research Observations, Tasks, Attack Paths, Tool Runs, messages, memory, or reusable security knowledge. A Research Observation preserves measured or directly seen behavior, structured inputs and outputs, measurements, external identifiers, versioned scores, actor, time, and precise citations before interpretation. The user-facing Research Map projects canonical records, cited threads and messages, external sources, reusable-concept references, current Investigation Assertions, and Research Priorities through one coherent relational snapshot. The write model resolves canonical records through project-local Investigation Entities and stores append-only Assertions, coordinate-only role-bearing Citations, and rule-versioned Derivations with ordered inputs. Evidence state (`observed`, `derived`, `proposed`, `contradicted`, or `rejected`) stays separate from assertion lifecycle (`current`, `withdrawn`, or `superseded`). Revision is an optimistic, transactional replacement that retains the predecessor and its citations. SQLite and PostgreSQL relational queries define correctness. See [ADR 0001](./adr/0001-investigation-graph-as-assertion-layer.md).

Product skill promotion is a reviewed boundary in front of the existing `sandbox/skills` registry and Mastra workspace search. A promotion candidate records whether it deepens an existing skill, creates a genuinely separate procedure, or retires one; cites reusable claims to campaign Artifacts; applies an explicit independent-campaign threshold; and carries contamination, target-agnosticity, secret, approval-boundary, and validation reviews. Eval and benchmark rows may validate the method but are always validation-only and candidate-invisible. An eligible decision yields a scoped registry plan under one skill directory; it does not write or activate skill content. Publication remains a separate reviewed filesystem change, after which native Workspace discovery and `SkillSearchProcessor` expose the procedure on demand.

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.
Expand Down
297 changes: 297 additions & 0 deletions src/server/skills/promotion-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
export const PRODUCT_SKILL_PROMOTION_POLICY_VERSION =
"product-skill-promotion-v1";

export type ProductSkillEvidenceSource =
| "campaign"
| "eval"
| "benchmark"
| "primary-source";

export type ProductSkillPromotionCandidate = {
policyVersion: typeof PRODUCT_SKILL_PROMOTION_POLICY_VERSION;
candidateId: string;
skillId: string;
changeKind: "deepen-existing" | "new-skill" | "retire";
baseSkillId?: string;
baseSkillRevision?: string;
contentSha256: string;
evidence: Array<{
sourceId: string;
source: ProductSkillEvidenceSource;
projectId?: string;
campaignId?: string;
targetRecipeDigest?: string;
artifactIds: string[];
validatedFindingIds: string[];
candidateVisible: boolean;
purpose: "method-source" | "validation-only" | "retirement-evidence";
}>;
generalization: {
minimumIndependentCampaigns: number;
existingSkillAssessment: string;
deepenRejectedReason?: string;
targetSpecificFacts: string[];
reusableClaims: Array<{
claim: string;
sourceIds: string[];
}>;
};
review: {
reviewerId: string;
reviewedAt: string;
sourceBacked: boolean;
targetAgnostic: boolean;
secretFree: boolean;
knownAnswerFree: boolean;
approvalBoundariesPreserved: boolean;
evalMemoryExcluded: boolean;
};
validation: {
checks: Array<{
id: string;
passed: boolean;
evidenceArtifactIds: string[];
candidateRoute?: string;
judgeRoute?: string;
}>;
};
discovery: {
name: string;
description: string;
searchTags: string[];
intendedStages: string[];
relativeSkillPath: string;
};
retirement?: {
reason: string;
replacementSkillId?: string;
migrationNote: string;
};
};

export type ProductSkillPromotionDecision = {
policyVersion: typeof PRODUCT_SKILL_PROMOTION_POLICY_VERSION;
candidateId: string;
status:
| "promotion-ready"
| "retirement-ready"
| "needs-evidence"
| "rejected";
failures: string[];
candidateVisibleSourceIds: string[];
validationOnlySourceIds: string[];
registryPlan?: {
skillId: string;
relativeSkillPath: string;
reviewStatus: "reviewed";
searchTags: string[];
intendedStages: string[];
};
writesPerformed: false;
};

export function assessProductSkillPromotion(
candidate: ProductSkillPromotionCandidate,
): ProductSkillPromotionDecision {
const rejected = validateContamination(candidate);
const missing = [
...validateIdentity(candidate),
...validateEvidence(candidate),
...validateReview(candidate),
...validateDiscovery(candidate),
...validateChangeKind(candidate),
];
const candidateVisibleSourceIds = candidate.evidence
.filter((item) => item.candidateVisible)
.map((item) => item.sourceId);
const validationOnlySourceIds = candidate.evidence
.filter((item) => item.purpose === "validation-only")
.map((item) => item.sourceId);
const status = rejected.length
? "rejected"
: missing.length
? "needs-evidence"
: candidate.changeKind === "retire"
? "retirement-ready"
: "promotion-ready";

return {
policyVersion: PRODUCT_SKILL_PROMOTION_POLICY_VERSION,
candidateId: candidate.candidateId,
status,
failures: [...rejected, ...missing],
candidateVisibleSourceIds,
validationOnlySourceIds,
...(status === "promotion-ready"
? {
registryPlan: {
skillId: candidate.skillId,
relativeSkillPath: candidate.discovery.relativeSkillPath,
reviewStatus: "reviewed",
searchTags: unique(candidate.discovery.searchTags),
intendedStages: unique(candidate.discovery.intendedStages),
},
}
: {}),
writesPerformed: false,
};
}

function validateContamination(candidate: ProductSkillPromotionCandidate) {
const failures: string[] = [];
for (const evidence of candidate.evidence) {
if (evidence.source !== "primary-source" && evidence.candidateVisible) {
failures.push(
`candidate_visible_non_public_evidence:${evidence.sourceId}`,
);
}
if (
(evidence.source === "eval" || evidence.source === "benchmark") &&
evidence.candidateVisible
) {
failures.push(
`candidate_visible_evaluation_evidence:${evidence.sourceId}`,
);
}
if (
(evidence.source === "eval" || evidence.source === "benchmark") &&
evidence.purpose !== "validation-only"
) {
failures.push(
`evaluation_evidence_not_validation_only:${evidence.sourceId}`,
);
}
}
if (candidate.generalization.targetSpecificFacts.length > 0) {
failures.push("target_specific_facts_present");
}
if (!candidate.review.knownAnswerFree)
failures.push("known_answer_review_failed");
if (!candidate.review.secretFree) failures.push("secret_review_failed");
if (!candidate.review.evalMemoryExcluded)
failures.push("eval_memory_boundary_failed");
return failures;
}

function validateIdentity(candidate: ProductSkillPromotionCandidate) {
const failures: string[] = [];
if (candidate.policyVersion !== PRODUCT_SKILL_PROMOTION_POLICY_VERSION) {
failures.push("policy_version_unsupported");
}
if (!candidate.candidateId.trim()) failures.push("candidate_id_missing");
if (!/^[a-z0-9][a-z0-9-]*$/.test(candidate.skillId))
failures.push("skill_id_invalid");
if (!/^sha256:[0-9a-f]{64}$/.test(candidate.contentSha256)) {
failures.push("content_digest_invalid");
}
return failures;
}

function validateEvidence(candidate: ProductSkillPromotionCandidate) {
if (candidate.changeKind === "retire") return [];
const failures: string[] = [];
const campaignEvidence = candidate.evidence.filter(
(item) => item.source === "campaign" && item.purpose === "method-source",
);
const independentCampaigns = new Set(
campaignEvidence
.map((item) => item.campaignId)
.filter((value): value is string => Boolean(value)),
);
if (
candidate.generalization.minimumIndependentCampaigns < 1 ||
independentCampaigns.size <
candidate.generalization.minimumIndependentCampaigns
) {
failures.push("independent_campaign_evidence_insufficient");
}
if (campaignEvidence.some((item) => item.artifactIds.length === 0)) {
failures.push("campaign_artifact_evidence_missing");
}
const sourceIds = new Set(candidate.evidence.map((item) => item.sourceId));
if (candidate.generalization.reusableClaims.length === 0)
failures.push("reusable_claims_missing");
for (const claim of candidate.generalization.reusableClaims) {
if (!claim.claim.trim() || claim.sourceIds.length === 0)
failures.push("reusable_claim_uncited");
if (claim.sourceIds.some((sourceId) => !sourceIds.has(sourceId))) {
failures.push("reusable_claim_source_missing");
}
}
if (candidate.validation.checks.length === 0)
failures.push("validation_checks_missing");
if (candidate.validation.checks.some((check) => !check.passed)) {
failures.push("validation_check_failed");
}
if (
candidate.validation.checks.some(
(check) => check.evidenceArtifactIds.length === 0,
)
) {
failures.push("validation_evidence_missing");
}
return unique(failures);
}

function validateReview(candidate: ProductSkillPromotionCandidate) {
if (candidate.changeKind === "retire") return [];
const failures: string[] = [];
if (!candidate.review.reviewerId.trim()) failures.push("reviewer_missing");
if (!Number.isFinite(Date.parse(candidate.review.reviewedAt)))
failures.push("reviewed_at_invalid");
if (!candidate.review.sourceBacked)
failures.push("source_backing_review_failed");
if (!candidate.review.targetAgnostic)
failures.push("target_agnostic_review_failed");
if (!candidate.review.approvalBoundariesPreserved) {
failures.push("approval_boundary_review_failed");
}
return failures;
}

function validateDiscovery(candidate: ProductSkillPromotionCandidate) {
if (candidate.changeKind === "retire") return [];
const failures: string[] = [];
if (!candidate.discovery.name.trim()) failures.push("skill_name_missing");
if (!candidate.discovery.description.trim())
failures.push("skill_description_missing");
if (candidate.discovery.searchTags.length === 0)
failures.push("search_tags_missing");
if (candidate.discovery.intendedStages.length === 0)
failures.push("intended_stages_missing");
if (
candidate.discovery.relativeSkillPath !== `${candidate.skillId}/SKILL.md`
) {
failures.push("skill_path_not_scoped_to_registry_root");
}
return failures;
}

function validateChangeKind(candidate: ProductSkillPromotionCandidate) {
if (candidate.changeKind === "deepen-existing") {
return candidate.baseSkillId?.trim() && candidate.baseSkillRevision?.trim()
? []
: ["base_skill_identity_missing"];
}
if (candidate.changeKind === "new-skill") {
const failures: string[] = [];
if (!candidate.generalization.existingSkillAssessment.trim()) {
failures.push("existing_skill_assessment_missing");
}
if (!candidate.generalization.deepenRejectedReason?.trim()) {
failures.push("new_skill_boundary_not_justified");
}
return failures;
}
if (
!candidate.retirement?.reason.trim() ||
!candidate.retirement.migrationNote.trim()
) {
return ["retirement_plan_missing"];
}
return [];
}

function unique(values: readonly string[]) {
return [...new Set(values)];
}
Loading
Loading