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
29 changes: 29 additions & 0 deletions packages/coding-agent/src/core/evolution-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
classifyEvolutionEvidence,
computeEvolutionContentDigest,
DeterministicProposalRanker,
DeterministicSkillDraftValidator,
EvolutionBackgroundWorker,
EvolutionControlPlane,
EvolutionControlService,
Expand Down Expand Up @@ -879,6 +880,33 @@ export async function createCodingAgentEvolutionRuntime(
artifacts: persistence,
jobs: persistence,
});
const skillValidator = new DeterministicSkillDraftValidator({
version: VERSION,
artifacts: persistence,
jobs: persistence,
environment: {
async capture(workspace, signal) {
signal?.throwIfAborted();
if (
workspace.repositoryId !== scope.repositoryId ||
workspace.directoryId !== currentDirectory.directoryId
) {
throw new Error("Skill validation workspace does not match the repository runtime");
}
const harnessVersion = options.session.activeHarnessVersion;
if (harnessVersion === undefined) throw new Error("Skill validation requires an active session harness");
return {
workspace: { repositoryId: scope.repositoryId, directoryId: currentDirectory.directoryId },
harnessVersion,
skillVersions: options.resourceLoader.getSkills().skills.map((skill) => ({
skillId: skill.skillId,
version: skill.version,
contentDigest: skill.contentDigest,
})),
};
},
},
});
const candidateWorker = new SkillCandidateBackgroundWorker({
clock,
source: new PollingSkillCandidateWorkSource({ clock, jobs: persistence, store: persistence }),
Expand All @@ -890,6 +918,7 @@ export async function createCodingAgentEvolutionRuntime(
store: persistence,
registry: candidateRegistry,
synthesizer: skillSynthesizer,
validator: skillValidator,
model,
}),
priority,
Expand Down
6 changes: 3 additions & 3 deletions packages/evolution/RECOVERY.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Recovery never treats partial output as completed work and never feeds evolution
| Successful rollback selection | The recorded immutable parent is still present and the evolved revision is current. | Select the parent with compare-and-swap and refresh on the next prompt boundary. | Existing in-flight prompts stay pinned to their admitted revision. |
| Unsupported skill or orchestration target | The target registry has no enabled writer route. | Preserve the finding as deferred evidence with no executable payload. | Disabled target kinds cannot reach filesystem mutation or activation. |
| Interrupted skill-candidate consolidation | A completed evolution job has checkpointed candidates but no completed candidate-work event. | Resume from append-only registry events with the same source-job idempotency key; repair missing scores or transitions before completion. | Candidate proposal revisions remain immutable, retries do not add recurrence, and partial registry appends cannot activate a skill. |
| Interrupted skill synthesis | An evidence-qualified group has no draft, or a draft-record event exists without its `drafted` transition. | Resume with the revision-bound synthesis idempotency key; reuse an already recorded immutable draft and repair the missing transition before completing candidate work. | Only evidence-qualified groups invoke synthesis, and a draft remains outside the active harness with no writer or activation path. |
| Interrupted skill synthesis or draft validation | An evidence-qualified group has no draft, a draft-record event lacks its `drafted` transition, or a validation-record event lacks its terminal transition. | Resume with the revision-bound idempotency keys; reuse recorded immutable artifacts and repair the missing transition before completing candidate work. | Only evidence-qualified groups invoke synthesis, and only deterministically accepted drafts reach `structurally-valid`; no draft has a writer or activation path. |

## Operational observability

Expand All @@ -37,7 +37,7 @@ These operational records remain outside materialized product-runtime evidence,

## Verification boundaries

The deterministic validator proves provenance, recurrence, immutable closure, score bounds, and structural eligibility.
Deterministic validation proves provenance, recurrence, immutable closure, score bounds, structural eligibility, workspace and version freshness, and bounded sensitive-data checks.
Candidate priority scoring separately orders evaluation work; it cannot skip later gates, accept a candidate, or activate a skill.
It does not prove that natural-language evidence semantically entails a proposal.
It does not prove semantic usefulness, that natural-language evidence entails a proposal, or the absence of all semantically unsafe guidance.
Post-activation outcome evaluation and automatic regression rollback are intentionally outside this recovery contract.
35 changes: 34 additions & 1 deletion packages/evolution/src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,27 @@ export interface SkillDraft {
readonly content: string;
}

export interface SkillDraftValidationIssue {
readonly code: string;
readonly message: string;
}

export interface SkillDraftValidationReport {
readonly artifactKind: "skill-draft-validation";
readonly metadata: EvolutionArtifactMetadata;
readonly validationId: string;
readonly groupId: string;
readonly draft: EvolutionArtifactReference;
readonly validatorVersion: string;
readonly assurance: "structure-provenance-scope-and-security-only";
readonly disposition: "accepted" | "rejected";
readonly issues: readonly SkillDraftValidationIssue[];
readonly validatedHarnessVersion: HarnessVersion;
readonly validatedSkillVersions: readonly string[];
readonly canonicalInventory: readonly string[];
readonly memoryRelationshipDigest: string;
}

export interface SkillCandidateDispositionRecord {
readonly eventId: string;
readonly from: SkillCandidateState | null;
Expand All @@ -625,7 +646,9 @@ export interface SkillCandidateGroup {
readonly evidence: SkillCandidateEvidenceContribution;
readonly priorityScore: EvolutionArtifactReference | null;
readonly draft: EvolutionArtifactReference | null;
readonly validation: EvolutionArtifactReference | null;
readonly synthesisIdempotencyKey: string;
readonly validationIdempotencyKey: string;
readonly evaluationIdempotencyKey: string;
}

Expand Down Expand Up @@ -658,6 +681,15 @@ export interface SkillCandidateDraftRecordedEvent extends SkillCandidateRegistry
readonly idempotencyKey: string;
}

export interface SkillCandidateValidationRecordedEvent extends SkillCandidateRegistryEventBase {
readonly type: "skill-candidate-validation-recorded";
readonly groupId: string;
readonly draft: EvolutionArtifactReference;
readonly validation: EvolutionArtifactReference;
readonly proposalRevisionIds: readonly string[];
readonly idempotencyKey: string;
}

export interface SkillCandidateStateTransitionedEvent extends SkillCandidateRegistryEventBase {
readonly type: "skill-candidate-state-transitioned";
readonly groupId: string;
Expand All @@ -666,7 +698,7 @@ export interface SkillCandidateStateTransitionedEvent extends SkillCandidateRegi
readonly reasonCodes: readonly string[];
}

export type SkillCandidateWorkStage = "consolidating" | "scoring" | "synthesizing" | "completed";
export type SkillCandidateWorkStage = "consolidating" | "scoring" | "synthesizing" | "validating" | "completed";

export interface SkillCandidateWorkCheckpointedEvent extends SkillCandidateRegistryEventBase {
readonly type: "skill-candidate-work-checkpointed";
Expand All @@ -692,6 +724,7 @@ export type SkillCandidateRegistryEvent =
| SkillCandidateProposalRecordedEvent
| SkillCandidatePriorityScoredEvent
| SkillCandidateDraftRecordedEvent
| SkillCandidateValidationRecordedEvent
| SkillCandidateStateTransitionedEvent
| SkillCandidateWorkCheckpointedEvent
| SkillCandidateWorkFailedEvent;
Expand Down
12 changes: 11 additions & 1 deletion packages/evolution/src/filesystem-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const SKILL_CANDIDATE_REGISTRY_EVENT_TYPES = new Set([
"skill-candidate-priority-scored",
"skill-candidate-proposal-recorded",
"skill-candidate-state-transitioned",
"skill-candidate-validation-recorded",
"skill-candidate-work-checkpointed",
"skill-candidate-work-failed",
]);
Expand All @@ -66,7 +67,7 @@ const SKILL_CANDIDATE_STATES = new Set([
"rejected",
"retired",
]);
const SKILL_CANDIDATE_WORK_STAGES = new Set(["consolidating", "scoring", "synthesizing", "completed"]);
const SKILL_CANDIDATE_WORK_STAGES = new Set(["consolidating", "scoring", "synthesizing", "validating", "completed"]);
const JOB_OUTCOMES = new Set([
"active",
"completed",
Expand Down Expand Up @@ -283,6 +284,15 @@ function isCandidateRegistryEventContent(value: Readonly<Record<string, unknown>
typeof value.idempotencyKey === "string"
);
}
if (value.type === "skill-candidate-validation-recorded") {
return (
typeof value.groupId === "string" &&
isArtifactReference(value.draft) &&
isArtifactReference(value.validation) &&
isStringArray(value.proposalRevisionIds) &&
typeof value.idempotencyKey === "string"
);
}
if (value.type === "skill-candidate-priority-scored") {
return (
typeof value.groupId === "string" &&
Expand Down
13 changes: 13 additions & 0 deletions packages/evolution/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,15 @@ export {
type SkillCandidateRegistrySnapshot,
type SkillCandidateState,
type SkillCandidateStateTransitionedEvent,
type SkillCandidateValidationRecordedEvent,
type SkillCandidateWorkCheckpointedEvent,
type SkillCandidateWorkFailedEvent,
type SkillCandidateWorkStage,
type SkillCandidateWorkspaceScope,
type SkillCuratorToolCallLedgerEvent,
type SkillDraft,
type SkillDraftValidationIssue,
type SkillDraftValidationReport,
type SkillMemoryRelationshipClassification,
type SkillTarget,
type ValidationIssue,
Expand Down Expand Up @@ -252,9 +255,14 @@ export type {
SkillCandidateRegistryConsolidationInput,
SkillCandidateRegistryConsolidationResult,
SkillCandidateRegistryStore,
SkillCandidateValidationRecordingInput,
SkillCurationResult,
SkillCurator,
SkillCuratorInput,
SkillDraftValidationEnvironment,
SkillDraftValidationEnvironmentSource,
SkillDraftValidator,
SkillDraftValidatorInput,
SkillSynthesizer,
SkillSynthesizerInput,
SuccessfulMaterialization,
Expand Down Expand Up @@ -324,6 +332,11 @@ export {
SkillCuratorToolRegistry,
type SkillCuratorToolRegistryOptions,
} from "./skill-curator-tools.ts";
export {
DeterministicSkillDraftValidator,
type DeterministicSkillDraftValidatorDependencies,
type DeterministicSkillDraftValidatorOptions,
} from "./skill-draft-validator.ts";
export {
StructuredSkillSynthesizer,
type StructuredSkillSynthesizerDependencies,
Expand Down
45 changes: 45 additions & 0 deletions packages/evolution/src/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type {
SkillCandidateRegistryEvent,
SkillCandidateRegistrySnapshot,
SkillDraft,
SkillDraftValidationReport,
ValidationReport,
} from "./contracts.ts";

Expand All @@ -59,6 +60,7 @@ export type EvolutionArtifact =
| SkillCandidate
| SkillCandidatePriorityScore
| SkillDraft
| SkillDraftValidationReport
| DeferredSkillCandidateFinding
| ValidationReport;

Expand Down Expand Up @@ -315,9 +317,20 @@ export interface SkillCandidateDraftRecordingInput {
readonly createdAt: string;
}

export interface SkillCandidateValidationRecordingInput {
readonly scope: EvolutionScope;
readonly groupId: string;
readonly draft: EvolutionArtifactReference;
readonly validation: EvolutionArtifactReference;
readonly proposalRevisionIds: readonly string[];
readonly idempotencyKey: string;
readonly createdAt: string;
}

export interface SkillCandidateRegistry {
consolidate(input: SkillCandidateRegistryConsolidationInput): Promise<SkillCandidateRegistryConsolidationResult>;
recordDraft(input: SkillCandidateDraftRecordingInput): Promise<SkillCandidateGroup>;
recordValidation(input: SkillCandidateValidationRecordingInput): Promise<SkillCandidateGroup>;
list(scope: EvolutionScope): Promise<readonly SkillCandidateGroup[]>;
}

Expand All @@ -333,6 +346,38 @@ export interface SkillSynthesizer {
synthesize(input: SkillSynthesizerInput): Promise<SkillDraft>;
}

export interface SkillDraftValidationEnvironment {
readonly workspace: {
readonly repositoryId: string;
readonly directoryId?: string;
};
readonly harnessVersion: string;
readonly skillVersions: readonly {
readonly skillId: string;
readonly version: string;
readonly contentDigest: string;
}[];
}

export interface SkillDraftValidationEnvironmentSource {
capture(
workspace: SkillCandidateGroup["profile"]["workspace"],
signal?: AbortSignal,
): Promise<SkillDraftValidationEnvironment>;
}

export interface SkillDraftValidatorInput {
readonly group: SkillCandidateGroup;
readonly draft: SkillDraft;
readonly idempotencyKey: string;
readonly createdAt: string;
readonly signal?: AbortSignal;
}

export interface SkillDraftValidator {
validate(input: SkillDraftValidatorInput): Promise<SkillDraftValidationReport>;
}

export interface EvolutionJobStore {
create(job: EvolutionJob, lease: EvolutionLeaseToken): Promise<void>;
save(job: EvolutionJob, expectedRevision: number, lease: EvolutionLeaseToken): Promise<void>;
Expand Down
Loading
Loading