From 089f4d89fadeacc75378761ebd89cb598ea23050 Mon Sep 17 00:00:00 2001 From: Jay Yu Date: Fri, 28 Aug 2026 13:49:57 -0400 Subject: [PATCH 1/3] feat(agent): share evolved skill identities --- packages/coding-agent/src/core/skills.ts | 15 ++++++--------- packages/harness-state/src/index.ts | 10 ++++++++++ packages/harness-state/test/harness-state.test.ts | 12 ++++++++++++ 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/coding-agent/src/core/skills.ts b/packages/coding-agent/src/core/skills.ts index 28d9d0bf6..b5edc25d4 100644 --- a/packages/coding-agent/src/core/skills.ts +++ b/packages/coding-agent/src/core/skills.ts @@ -1,5 +1,9 @@ import { createHash } from "node:crypto"; -import type { HarnessVersionBundle, HarnessWorkspaceIdentity } from "@earendil-works/diffex-harness-state"; +import { + computeHarnessSkillId, + type HarnessVersionBundle, + type HarnessWorkspaceIdentity, +} from "@earendil-works/diffex-harness-state"; import { existsSync, readdirSync, readFileSync, statSync } from "fs"; import ignore from "ignore"; import { basename, dirname, join, relative, resolve, sep } from "path"; @@ -215,14 +219,7 @@ export function createEvolvedSkillCatalog( content: artifact.content, sourceKind: "evolved", scope: "workspace", - skillId: `skill:${sha256( - JSON.stringify([ - "evolved", - artifact.workspace.repositoryId, - artifact.workspace.directoryId ?? null, - artifact.path, - ]), - )}`, + skillId: computeHarnessSkillId(artifact.workspace, artifact.path), contentDigest: artifact.contentHash, version: artifact.contentHash, harnessRevision: bundle.manifest.version, diff --git a/packages/harness-state/src/index.ts b/packages/harness-state/src/index.ts index 6368bb3af..87acb96dc 100644 --- a/packages/harness-state/src/index.ts +++ b/packages/harness-state/src/index.ts @@ -50,6 +50,16 @@ export interface HarnessWorkspaceIdentity { readonly directoryId?: string; } +export function computeHarnessSkillId( + workspace: HarnessWorkspaceIdentity, + path: string, +): string { + validateArtifactPath(path); + return `skill:${createHash("sha256") + .update(JSON.stringify(["evolved", workspace.repositoryId, workspace.directoryId ?? null, path])) + .digest("hex")}`; +} + export const HarnessSkillInputSchema = Type.Readonly( Type.Object( { diff --git a/packages/harness-state/test/harness-state.test.ts b/packages/harness-state/test/harness-state.test.ts index 810087240..c90349c50 100644 --- a/packages/harness-state/test/harness-state.test.ts +++ b/packages/harness-state/test/harness-state.test.ts @@ -5,6 +5,7 @@ import { basename, dirname, join } from "node:path"; import { Check } from "typebox/value"; import { afterEach, describe, expect, test } from "vitest"; import { + computeHarnessSkillId, computeHarnessVersion, HARNESS_VERSION_SCHEMA_VERSION, type HarnessArtifactManifest, @@ -210,6 +211,17 @@ describe("canonical harness versions", () => { ).not.toBe(version); }); + test("derives stable evolved skill identities from workspace and canonical path", () => { + const skillId = computeHarnessSkillId(WORKSPACE, "skills/focused-review/SKILL.md"); + + expect(skillId).toMatch(/^skill:[0-9a-f]{64}$/); + expect(computeHarnessSkillId(WORKSPACE, "skills/focused-review/SKILL.md")).toBe(skillId); + expect( + computeHarnessSkillId({ ...WORKSPACE, directoryId: "other" }, "skills/focused-review/SKILL.md"), + ).not.toBe(skillId); + expect(() => computeHarnessSkillId(WORKSPACE, "../SKILL.md")).toThrow(/artifact path/i); + }); + test("validates an in-memory skill with the same canonical rules as a bundle", () => { const content = skillContent("focused-review"); From 962ee1597a4de6b96ffb3456ca311034364b9521 Mon Sep 17 00:00:00 2001 From: Jay Yu Date: Fri, 28 Aug 2026 14:01:33 -0400 Subject: [PATCH 2/3] feat(agent): stage accepted evolved skill mutations --- packages/evolution/src/contracts.ts | 50 ++ packages/evolution/src/filesystem-store.ts | 4 + packages/evolution/src/index.ts | 12 + packages/evolution/src/ports.ts | 3 + packages/evolution/src/skill-writer.ts | 779 +++++++++++++++++++++ packages/harness-state/src/index.ts | 5 +- 6 files changed, 849 insertions(+), 4 deletions(-) create mode 100644 packages/evolution/src/skill-writer.ts diff --git a/packages/evolution/src/contracts.ts b/packages/evolution/src/contracts.ts index 5baae6c5c..46c7b7f6d 100644 --- a/packages/evolution/src/contracts.ts +++ b/packages/evolution/src/contracts.ts @@ -1090,6 +1090,56 @@ export interface MutationBatch { readonly mutations: readonly EvolutionMutation[]; } +export interface SkillMutationExistingArtifact { + readonly skillId: string; + readonly path: string; + readonly contentDigest: string; +} + +export interface SkillMutationArtifact { + readonly path: string; + readonly content: string; + readonly contentDigest: string; +} + +export interface SkillMutation { + readonly mutationId: string; + readonly groupId: string; + readonly operation: SkillCandidateOperation; + readonly proposedName: string; + readonly workspace: EvolutionSkillWorkspaceIdentity; + readonly proposalRevisionIds: readonly string[]; + readonly draft: EvolutionArtifactReference; + readonly validation: EvolutionArtifactReference; + readonly evaluation: EvolutionArtifactReference; + readonly existingSkills: readonly SkillMutationExistingArtifact[]; + readonly skill: SkillMutationArtifact | null; +} + +export interface SkillMutationBatch { + readonly metadata: EvolutionArtifactMetadata; + readonly batchId: string; + readonly targetKind: "skill"; + readonly expectedBaseVersion: HarnessVersion; + readonly workspace: EvolutionSkillWorkspaceIdentity; + readonly mutations: readonly SkillMutation[]; +} + +export type SkillMutationExactDiff = JsonObject & { + readonly removed: readonly JsonObject[]; + readonly added: readonly JsonObject[]; +}; + +export type SkillMutationWriteOutput = JsonObject & { + readonly candidateId: string; + readonly candidateDigest: string; + readonly batchHash: string; + readonly expectedBaseVersion: HarnessVersion; + readonly resultingHarnessVersion: HarnessVersion; + readonly operationIds: readonly string[]; + readonly diff: SkillMutationExactDiff; +}; + export type EvolutionMemoryExactDiff = JsonObject & { readonly start: number; readonly removed: string; diff --git a/packages/evolution/src/filesystem-store.ts b/packages/evolution/src/filesystem-store.ts index 894916c6d..f009907bf 100644 --- a/packages/evolution/src/filesystem-store.ts +++ b/packages/evolution/src/filesystem-store.ts @@ -656,6 +656,10 @@ export class FileEvolutionPersistence } } + assertHeld(lease: EvolutionLeaseToken): Promise { + return this.#withStoreLock(() => this.#assertLeaseHeld(lease)); + } + async put(artifact: EvolutionArtifact): Promise { await this.#ensureDirectories(); const content = serializeEvolutionValue(artifact); diff --git a/packages/evolution/src/index.ts b/packages/evolution/src/index.ts index 9651644e7..78d9309c7 100644 --- a/packages/evolution/src/index.ts +++ b/packages/evolution/src/index.ts @@ -166,6 +166,12 @@ export { type SkillEvaluationVerifier, type SkillEvaluationVerifierKind, type SkillMemoryRelationshipClassification, + type SkillMutation, + type SkillMutationArtifact, + type SkillMutationBatch, + type SkillMutationExactDiff, + type SkillMutationExistingArtifact, + type SkillMutationWriteOutput, type SkillRetrievalEvaluationResult, type SkillTarget, type ValidationIssue, @@ -376,6 +382,12 @@ export { StructuredSkillSynthesizer, type StructuredSkillSynthesizerDependencies, } from "./skill-synthesizer.ts"; +export { + SkillWriter, + type SkillWriterBuildInput, + type SkillWriterContext, + type SkillWriterDependencies, +} from "./skill-writer.ts"; export { EvolutionTargetAdapterRegistry, type EvolutionTargetAdapterRegistryOptions, diff --git a/packages/evolution/src/ports.ts b/packages/evolution/src/ports.ts index 5daa690c9..06970afc2 100644 --- a/packages/evolution/src/ports.ts +++ b/packages/evolution/src/ports.ts @@ -46,6 +46,7 @@ import type { SkillEvaluationPoolSkill, SkillEvaluationReport, SkillEvaluationRuntimeConfiguration, + SkillMutationBatch, SkillRetrievalEvaluationResult, ValidationReport, } from "./contracts.ts"; @@ -70,6 +71,7 @@ export type EvolutionArtifact = | SkillDraft | SkillDraftValidationReport | SkillEvaluationReport + | SkillMutationBatch | DeferredSkillCandidateFinding | ValidationReport; @@ -504,6 +506,7 @@ export interface EvolutionCursorStore { } export interface EvolutionLeaseStore { + assertHeld(lease: EvolutionLeaseToken): Promise; acquire( kind: EvolutionLeaseToken["kind"], key: string, diff --git a/packages/evolution/src/skill-writer.ts b/packages/evolution/src/skill-writer.ts new file mode 100644 index 000000000..70606daf3 --- /dev/null +++ b/packages/evolution/src/skill-writer.ts @@ -0,0 +1,779 @@ +import { link, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { basename, dirname, join, resolve } from "node:path"; +import { + computeHarnessSkillId, + type HarnessSkill, + type HarnessSkillInput, + type HarnessStateStore, + type HarnessVersionBundle, + type HarnessWorkspaceIdentity, + validateHarnessSkillContent, +} from "@earendil-works/diffex-harness-state"; +import type { + EvolutionArtifactProducer, + EvolutionArtifactReference, + EvolutionLeaseToken, + EvolutionScope, + HarnessActivationReceipt, + HarnessActivationResult, + MutationWriteReceipt, + MutationWriteResult, + SkillCandidateGroup, + SkillDraft, + SkillDraftValidationReport, + SkillEvaluationReport, + SkillMutation, + SkillMutationBatch, + SkillMutationExactDiff, + SkillMutationWriteOutput, +} from "./contracts.ts"; +import { EvolutionExecutionError } from "./errors.ts"; +import type { + EvolutionArtifact, + EvolutionArtifactStore, + EvolutionClock, + EvolutionLeaseStore, + SkillCandidateRegistry, +} from "./ports.ts"; +import { + computeEvolutionContentDigest, + computeEvolutionTextDigest, + createEvolutionArtifactMetadata, + serializeEvolutionValue, +} from "./provenance.ts"; + +const CANDIDATE_SCHEMA_VERSION = 1 as const; +const ACTIVATION_SCHEMA_VERSION = 1 as const; +const SAFE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/; + +interface StoredSkillWriterCandidate { + readonly schemaVersion: typeof CANDIDATE_SCHEMA_VERSION; + readonly writerVersion: string; + readonly writtenAt: string; + readonly writeIdempotencyKey: string; + readonly candidateId: string; + readonly candidateDigest: string; + readonly batchId: string; + readonly batchHash: string; + readonly expectedBaseVersion: string; + readonly resultingHarnessVersion: string; + readonly operationIds: readonly string[]; + readonly afterSkills: readonly HarnessSkillInput[]; + readonly diff: SkillMutationExactDiff; +} + +interface StoredSkillWriterActivation { + readonly schemaVersion: typeof ACTIVATION_SCHEMA_VERSION; + readonly batchId: string; + readonly batchHash: string; + readonly writeReceiptDigest: string; + readonly activationIdempotencyKey: string; + readonly receipt: HarnessActivationReceipt; +} + +interface AcceptedSkillMutationSource { + readonly group: SkillCandidateGroup; + readonly draft: SkillDraft; + readonly validation: SkillDraftValidationReport; + readonly evaluation: SkillEvaluationReport; +} + +export interface SkillWriterBuildInput { + readonly scope: EvolutionScope; + readonly groupId: string; + readonly createdAt: string; + readonly signal?: AbortSignal; +} + +export interface SkillWriterContext { + readonly idempotencyKey: string; + readonly lease: EvolutionLeaseToken; + readonly signal?: AbortSignal; +} + +export interface SkillWriterDependencies { + readonly version: string; + readonly workspace: HarnessWorkspaceIdentity; + readonly harnessState: HarnessStateStore; + readonly artifacts: EvolutionArtifactStore; + readonly registry: SkillCandidateRegistry; + readonly leases: EvolutionLeaseStore; + readonly clock: EvolutionClock; + readonly statePath: string; +} + +function isRecord(value: unknown): value is Readonly> { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function contentWithoutMetadata(value: T): Omit { + const { metadata: _metadata, ...content } = value; + return content; +} + +function stableId(kind: string, value: unknown): string { + return `${kind}-${computeEvolutionContentDigest(value).slice("sha256:".length, "sha256:".length + 32)}`; +} + +function sameValues(left: readonly string[], right: readonly string[]): boolean { + const normalizedLeft = [...new Set(left)].sort((a, b) => a.localeCompare(b)); + const normalizedRight = [...new Set(right)].sort((a, b) => a.localeCompare(b)); + return ( + normalizedLeft.length === normalizedRight.length && + normalizedLeft.every((value, index) => value === normalizedRight[index]) + ); +} + +function sameReference(left: EvolutionArtifactReference | null, right: EvolutionArtifactReference): boolean { + return left?.artifactId === right.artifactId && left.contentDigest === right.contentDigest; +} + +function sameWorkspace( + left: { readonly repositoryId: string; readonly directoryId?: string }, + right: { readonly repositoryId: string; readonly directoryId?: string }, +): boolean { + return left.repositoryId === right.repositoryId && left.directoryId === right.directoryId; +} + +function isSkillDraft(artifact: EvolutionArtifact): artifact is SkillDraft { + return "artifactKind" in artifact && artifact.artifactKind === "skill-draft"; +} + +function isSkillValidation(artifact: EvolutionArtifact): artifact is SkillDraftValidationReport { + return "artifactKind" in artifact && artifact.artifactKind === "skill-draft-validation"; +} + +function isSkillEvaluation(artifact: EvolutionArtifact): artifact is SkillEvaluationReport { + return "artifactKind" in artifact && artifact.artifactKind === "skill-evaluation"; +} + +function artifactReference( + artifact: SkillDraft | SkillDraftValidationReport | SkillEvaluationReport, +): EvolutionArtifactReference { + return { + artifactId: artifact.metadata.artifactId, + contentDigest: computeEvolutionContentDigest(artifact), + }; +} + +function assertArtifactIdentity( + artifact: SkillDraft | SkillDraftValidationReport | SkillEvaluationReport, + reference: EvolutionArtifactReference, + description: string, +): void { + if ( + artifact.metadata.artifactId !== reference.artifactId || + artifact.metadata.contentDigest !== computeEvolutionContentDigest(contentWithoutMetadata(artifact)) || + computeEvolutionContentDigest(artifact) !== reference.contentDigest + ) { + throw new Error(`${description} immutable identity is invalid`); + } +} + +function assertWriterContext(context: SkillWriterContext): void { + if (context.idempotencyKey.trim() === "") throw new Error("Skill writer idempotency key must be non-empty"); + if (context.lease.kind !== "writer" || context.lease.key !== "global") { + throw new Error("Skill mutations require the global writer lease"); + } +} + +function assertSafeId(value: string, description: string): void { + if (!SAFE_ID_PATTERN.test(value)) throw new Error(`Invalid ${description} "${value}"`); +} + +function skillInput(skill: HarnessSkill): HarnessSkillInput { + return { + path: skill.path, + content: skill.content, + workspace: { + repositoryId: skill.workspace.repositoryId, + ...(skill.workspace.directoryId === undefined ? {} : { directoryId: skill.workspace.directoryId }), + }, + }; +} + +function sortSkillInputs(skills: readonly HarnessSkillInput[]): HarnessSkillInput[] { + return [...skills].sort((left, right) => left.path.localeCompare(right.path)); +} + +function outputForCandidate(candidate: StoredSkillWriterCandidate): SkillMutationWriteOutput { + return { + candidateId: candidate.candidateId, + candidateDigest: candidate.candidateDigest, + batchHash: candidate.batchHash, + expectedBaseVersion: candidate.expectedBaseVersion, + resultingHarnessVersion: candidate.resultingHarnessVersion, + operationIds: candidate.operationIds, + diff: candidate.diff, + }; +} + +function parseWriteOutput(result: MutationWriteResult): SkillMutationWriteOutput { + const output = result.output; + if ( + result.status !== "written" || + !isRecord(output) || + typeof output.candidateId !== "string" || + typeof output.candidateDigest !== "string" || + typeof output.batchHash !== "string" || + typeof output.expectedBaseVersion !== "string" || + typeof output.resultingHarnessVersion !== "string" || + !Array.isArray(output.operationIds) || + !isRecord(output.diff) || + !Array.isArray(output.diff.removed) || + !Array.isArray(output.diff.added) + ) { + throw new Error("Skill write receipt is invalid"); + } + return output as SkillMutationWriteOutput; +} + +function receiptContent(receipt: MutationWriteReceipt | HarnessActivationReceipt): Omit { + return contentWithoutMetadata(receipt); +} + +export class SkillWriter { + readonly #dependencies: SkillWriterDependencies; + readonly #producer: EvolutionArtifactProducer; + readonly #statePath: string; + readonly #candidatePath: string; + readonly #activationPath: string; + + constructor(dependencies: SkillWriterDependencies) { + if (dependencies.version.trim() === "") throw new Error("Skill writer version must be non-empty"); + if (dependencies.workspace.repositoryId.trim() === "") { + throw new Error("Skill writer workspace repository ID must be non-empty"); + } + this.#dependencies = dependencies; + this.#producer = { + name: "diffex-evolution-skill-writer", + version: dependencies.version, + promptVersion: null, + model: null, + }; + this.#statePath = resolve(dependencies.statePath); + this.#candidatePath = join(this.#statePath, "candidates"); + this.#activationPath = join(this.#statePath, "activations"); + } + + async #ensureDirectories(): Promise { + await mkdir(this.#candidatePath, { recursive: true, mode: 0o700 }); + await mkdir(this.#activationPath, { recursive: true, mode: 0o700 }); + } + + #candidateFile(batchId: string): string { + assertSafeId(batchId, "skill mutation batch ID"); + return join(this.#candidatePath, `${batchId}.json`); + } + + #activationFile(batchId: string): string { + assertSafeId(batchId, "skill mutation batch ID"); + return join(this.#activationPath, `${batchId}.json`); + } + + async #readOptional(path: string): Promise { + try { + return await readFile(path, "utf8"); + } catch (error) { + if (error instanceof Error && "code" in error && error.code === "ENOENT") return null; + throw error; + } + } + + async #writeImmutable(path: string, value: unknown): Promise { + await this.#ensureDirectories(); + const serialized = `${serializeEvolutionValue(value)}\n`; + const existing = await this.#readOptional(path); + if (existing !== null) { + if (existing !== serialized) throw new Error(`Immutable skill writer state at "${path}" changed`); + return; + } + const temporaryDirectory = await mkdtemp(join(dirname(path), `.${basename(path)}.tmp-`)); + try { + const temporaryFile = join(temporaryDirectory, basename(path)); + await writeFile(temporaryFile, serialized, { encoding: "utf8", flag: "wx", mode: 0o600 }); + try { + await link(temporaryFile, path); + } catch (error) { + if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error; + const raced = await this.#readOptional(path); + if (raced !== serialized) throw new Error(`Immutable skill writer state race at "${path}" changed`); + } + } finally { + await rm(temporaryDirectory, { recursive: true, force: true }); + } + } + + async #loadCandidate(batchId: string): Promise { + const contents = await this.#readOptional(this.#candidateFile(batchId)); + if (contents === null) return null; + const parsed: unknown = JSON.parse(contents); + if ( + !isRecord(parsed) || + parsed.schemaVersion !== CANDIDATE_SCHEMA_VERSION || + parsed.batchId !== batchId || + typeof parsed.writerVersion !== "string" || + typeof parsed.writtenAt !== "string" || + typeof parsed.writeIdempotencyKey !== "string" || + typeof parsed.candidateId !== "string" || + typeof parsed.candidateDigest !== "string" || + typeof parsed.batchHash !== "string" || + typeof parsed.expectedBaseVersion !== "string" || + typeof parsed.resultingHarnessVersion !== "string" || + !Array.isArray(parsed.operationIds) || + !Array.isArray(parsed.afterSkills) || + !isRecord(parsed.diff) + ) { + throw new Error(`Skill writer candidate for "${batchId}" is invalid`); + } + const candidate = parsed as unknown as StoredSkillWriterCandidate; + const { candidateDigest: _candidateDigest, ...content } = candidate; + if (candidate.candidateDigest !== computeEvolutionContentDigest(content)) { + throw new Error(`Skill writer candidate for "${batchId}" has an invalid digest`); + } + return candidate; + } + + async #loadActivation(batchId: string): Promise { + const contents = await this.#readOptional(this.#activationFile(batchId)); + if (contents === null) return null; + const parsed: unknown = JSON.parse(contents); + if ( + !isRecord(parsed) || + parsed.schemaVersion !== ACTIVATION_SCHEMA_VERSION || + parsed.batchId !== batchId || + typeof parsed.batchHash !== "string" || + typeof parsed.writeReceiptDigest !== "string" || + typeof parsed.activationIdempotencyKey !== "string" || + !isRecord(parsed.receipt) + ) { + throw new Error(`Skill writer activation for "${batchId}" is invalid`); + } + const activation = parsed as unknown as StoredSkillWriterActivation; + if ( + activation.receipt.batchId !== batchId || + activation.receipt.idempotencyKey !== activation.activationIdempotencyKey || + activation.receipt.result.batchId !== batchId || + activation.receipt.result.status !== "activated" || + activation.receipt.metadata.contentDigest !== computeEvolutionContentDigest(receiptContent(activation.receipt)) + ) { + throw new Error(`Skill writer activation for "${batchId}" has an invalid receipt`); + } + return activation; + } + + async #acceptedSource(scope: EvolutionScope, groupId: string): Promise { + const group = (await this.#dependencies.registry.list(scope)).find((candidate) => candidate.groupId === groupId); + if ( + group === undefined || + group.state !== "accepted" || + group.draft === null || + group.validation === null || + group.evaluation === null + ) { + throw new Error(`Candidate group "${groupId}" is not accepted for skill activation`); + } + const [draftArtifact, validationArtifact, evaluationArtifact] = await Promise.all([ + this.#dependencies.artifacts.get(group.draft), + this.#dependencies.artifacts.get(group.validation), + this.#dependencies.artifacts.get(group.evaluation), + ]); + if ( + !isSkillDraft(draftArtifact) || + !isSkillValidation(validationArtifact) || + !isSkillEvaluation(evaluationArtifact) + ) { + throw new Error(`Candidate group "${groupId}" has invalid writer prerequisites`); + } + assertArtifactIdentity(draftArtifact, group.draft, "Skill draft"); + assertArtifactIdentity(validationArtifact, group.validation, "Skill validation"); + assertArtifactIdentity(evaluationArtifact, group.evaluation, "Skill evaluation"); + if ( + group.profile.workspace.evolutionScope.repositoryId !== scope.repositoryId || + group.profile.workspace.evolutionScope.scopeId !== scope.scopeId || + draftArtifact.metadata.scope.repositoryId !== scope.repositoryId || + draftArtifact.metadata.scope.scopeId !== scope.scopeId || + validationArtifact.metadata.scope.repositoryId !== scope.repositoryId || + validationArtifact.metadata.scope.scopeId !== scope.scopeId || + evaluationArtifact.metadata.scope.repositoryId !== scope.repositoryId || + evaluationArtifact.metadata.scope.scopeId !== scope.scopeId || + draftArtifact.groupId !== group.groupId || + validationArtifact.groupId !== group.groupId || + evaluationArtifact.groupId !== group.groupId || + !sameReference(validationArtifact.draft, group.draft) || + !sameReference(evaluationArtifact.draft, group.draft) || + !sameReference(evaluationArtifact.validation, group.validation) || + validationArtifact.disposition !== "accepted" || + evaluationArtifact.disposition !== "accepted" || + !evaluationArtifact.gates.suite.passed || + !evaluationArtifact.gates.retrieval.passed || + !evaluationArtifact.gates.behavior.passed || + !sameValues( + draftArtifact.proposalRevisionIds, + group.proposalRevisions.map((revision) => revision.revisionId), + ) || + !sameValues(evaluationArtifact.proposalRevisionIds, draftArtifact.proposalRevisionIds) + ) { + throw new Error(`Candidate group "${groupId}" accepted artifacts are stale or incomplete`); + } + return { group, draft: draftArtifact, validation: validationArtifact, evaluation: evaluationArtifact }; + } + + #mutationFor(source: AcceptedSkillMutationSource, base: HarnessVersionBundle): SkillMutation { + const { group, draft, validation, evaluation } = source; + if ( + !sameWorkspace(group.profile.workspace, this.#dependencies.workspace) || + !sameWorkspace(draft.workspace, this.#dependencies.workspace) || + !sameValues(group.profile.existingSkillIds, draft.existingSkillIds) || + new Set(draft.existingSkillIds).size !== draft.existingSkillIds.length || + !sameValues( + draft.sourceSkillVersions.map((skill) => skill.skillId), + draft.existingSkillIds, + ) || + draft.workspace.evolutionScope.repositoryId !== group.profile.workspace.evolutionScope.repositoryId || + draft.workspace.evolutionScope.scopeId !== group.profile.workspace.evolutionScope.scopeId || + validation.validatedHarnessVersion !== base.manifest.version || + evaluation.harnessVersion !== base.manifest.version + ) { + throw new EvolutionExecutionError( + "Accepted skill candidate does not match the active workspace and harness base", + { + reasonCode: "harness-base-conflict", + retryable: false, + }, + ); + } + const expectedPath = `skills/${draft.proposedName}/SKILL.md`; + validateHarnessSkillContent(expectedPath, draft.content); + if (!sameValues(validation.canonicalInventory, [expectedPath])) { + throw new Error(`Candidate group "${group.groupId}" has a noncanonical validated inventory`); + } + if ( + evaluation.candidateSkill.skillId !== draft.draftId || + evaluation.candidateSkill.name !== draft.proposedName || + evaluation.candidateSkill.version !== draft.metadata.contentDigest || + evaluation.candidateSkill.content !== draft.content || + evaluation.candidateSkill.contentDigest !== computeEvolutionContentDigest(draft.content) + ) { + throw new Error(`Candidate group "${group.groupId}" evaluation does not match its draft`); + } + + const skillsById = new Map( + base.state.skills.map((skill) => [computeHarnessSkillId(skill.workspace, skill.path), skill] as const), + ); + const existingSkills = draft.existingSkillIds.map((skillId) => { + const skill = skillsById.get(skillId); + if (skill === undefined || !sameWorkspace(skill.workspace, this.#dependencies.workspace)) { + throw new Error(`Existing skill "${skillId}" is not an evolved skill in the frozen harness base`); + } + const frozenVersions = draft.sourceSkillVersions.filter((version) => version.skillId === skillId); + if ( + frozenVersions.length === 0 || + frozenVersions.some( + (version) => version.version !== skill.contentHash || version.contentDigest !== skill.contentHash, + ) + ) { + throw new Error(`Existing skill "${skillId}" no longer matches its frozen digest`); + } + return { skillId, path: skill.path, contentDigest: skill.contentHash }; + }); + if ( + (draft.operation === "CREATE" && existingSkills.length !== 0) || + ((draft.operation === "UPDATE" || draft.operation === "RETIRE") && existingSkills.length !== 1) || + (draft.operation === "MERGE" && existingSkills.length < 2) + ) { + throw new Error(`Candidate group "${group.groupId}" has invalid ${draft.operation} mutation inputs`); + } + const draftReference = artifactReference(draft); + const validationReference = artifactReference(validation); + const evaluationReference = artifactReference(evaluation); + return { + mutationId: stableId("skill-mutation", { + groupId: group.groupId, + proposalRevisionIds: draft.proposalRevisionIds, + draft: draftReference, + validation: validationReference, + evaluation: evaluationReference, + }), + groupId: group.groupId, + operation: draft.operation, + proposedName: draft.proposedName, + workspace: { + repositoryId: this.#dependencies.workspace.repositoryId, + ...(this.#dependencies.workspace.directoryId === undefined + ? {} + : { directoryId: this.#dependencies.workspace.directoryId }), + }, + proposalRevisionIds: draft.proposalRevisionIds, + draft: draftReference, + validation: validationReference, + evaluation: evaluationReference, + existingSkills, + skill: + draft.operation === "RETIRE" + ? null + : { + path: expectedPath, + content: draft.content, + contentDigest: computeEvolutionTextDigest(draft.content), + }, + }; + } + + async buildMutationBatch(input: SkillWriterBuildInput): Promise { + input.signal?.throwIfAborted(); + if (!Number.isFinite(Date.parse(input.createdAt))) throw new Error("Skill mutation batch time must be valid"); + const base = await this.#dependencies.harnessState.loadCurrent(); + const source = await this.#acceptedSource(input.scope, input.groupId); + const mutation = this.#mutationFor(source, base); + const content = { + batchId: stableId("skill-mutation-batch", { + groupId: input.groupId, + expectedBaseVersion: base.manifest.version, + mutation, + }), + targetKind: "skill" as const, + expectedBaseVersion: base.manifest.version, + workspace: mutation.workspace, + mutations: [mutation], + }; + return { + metadata: createEvolutionArtifactMetadata({ + artifactId: content.batchId, + producer: this.#producer, + sourceIds: [ + input.groupId, + mutation.draft.artifactId, + mutation.validation.artifactId, + mutation.evaluation.artifactId, + ], + scope: input.scope, + createdAt: input.createdAt, + content, + }), + ...content, + }; + } + + async #assertBatch( + batch: SkillMutationBatch, + ): Promise<{ readonly base: HarnessVersionBundle; readonly mutation: SkillMutation }> { + if ( + batch.targetKind !== "skill" || + batch.mutations.length !== 1 || + batch.metadata.artifactId !== batch.batchId || + batch.metadata.contentDigest !== computeEvolutionContentDigest(contentWithoutMetadata(batch)) || + !sameWorkspace(batch.workspace, this.#dependencies.workspace) + ) { + throw new Error(`Skill mutation batch "${batch.batchId}" is invalid`); + } + assertSafeId(batch.batchId, "skill mutation batch ID"); + const mutation = batch.mutations[0] as SkillMutation; + assertSafeId(mutation.mutationId, "skill mutation ID"); + const base = await this.#dependencies.harnessState.loadVersion(batch.expectedBaseVersion); + const source = await this.#acceptedSource(batch.metadata.scope, mutation.groupId); + const expected = this.#mutationFor(source, base); + if (serializeEvolutionValue(mutation) !== serializeEvolutionValue(expected)) { + throw new Error(`Skill mutation batch "${batch.batchId}" does not match its accepted candidate`); + } + return { base, mutation }; + } + + #applyMutation(base: HarnessVersionBundle, mutation: SkillMutation): HarnessSkillInput[] { + const removedPaths = new Set(mutation.existingSkills.map((skill) => skill.path)); + const after = base.state.skills.filter((skill) => !removedPaths.has(skill.path)).map(skillInput); + if (mutation.skill !== null) { + if (after.some((skill) => skill.path === mutation.skill?.path)) { + throw new Error(`Skill mutation target "${mutation.skill.path}" already exists`); + } + after.push({ + path: mutation.skill.path, + content: mutation.skill.content, + workspace: mutation.workspace, + }); + } + return sortSkillInputs(after); + } + + #writeReceipt(batch: SkillMutationBatch, candidate: StoredSkillWriterCandidate): MutationWriteReceipt { + const result: MutationWriteResult = { + batchId: batch.batchId, + status: "written", + output: outputForCandidate(candidate), + message: null, + }; + const content = { batchId: batch.batchId, idempotencyKey: candidate.writeIdempotencyKey, result }; + return { + metadata: createEvolutionArtifactMetadata({ + artifactId: stableId("skill-write-receipt", { + batchId: batch.batchId, + idempotencyKey: candidate.writeIdempotencyKey, + }), + producer: this.#producer, + sourceIds: [batch.batchId, ...candidate.operationIds], + scope: batch.metadata.scope, + createdAt: candidate.writtenAt, + content, + }), + ...content, + }; + } + + async write(batch: SkillMutationBatch, context: SkillWriterContext): Promise { + context.signal?.throwIfAborted(); + assertWriterContext(context); + await this.#dependencies.leases.assertHeld(context.lease); + const batchHash = computeEvolutionContentDigest(batch); + const prior = await this.#loadCandidate(batch.batchId); + if (prior !== null) { + if (prior.batchHash !== batchHash || prior.writeIdempotencyKey !== context.idempotencyKey) { + throw new Error(`Skill mutation batch "${batch.batchId}" was already staged with different content`); + } + return this.#writeReceipt(batch, prior); + } + + const { base, mutation } = await this.#assertBatch(batch); + const afterSkills = this.#applyMutation(base, mutation); + const evolved = await this.#dependencies.harnessState.saveVersion( + { memory: base.state.memory, skills: afterSkills }, + batch.expectedBaseVersion, + base.manifest.metadata, + ); + const staged = await this.#dependencies.harnessState.loadVersion(evolved.manifest.version); + if ( + staged.manifest.base !== batch.expectedBaseVersion || + staged.state.memory !== base.state.memory || + serializeEvolutionValue(sortSkillInputs(staged.state.skills.map(skillInput))) !== + serializeEvolutionValue(afterSkills) + ) { + throw new Error(`Skill mutation batch "${batch.batchId}" did not stage the complete harness bundle`); + } + const diff: SkillMutationExactDiff = { + removed: mutation.existingSkills.map((skill) => ({ + skillId: skill.skillId, + path: skill.path, + contentDigest: skill.contentDigest, + })), + added: + mutation.skill === null ? [] : [{ path: mutation.skill.path, contentDigest: mutation.skill.contentDigest }], + }; + const candidateContent = { + schemaVersion: CANDIDATE_SCHEMA_VERSION, + writerVersion: this.#dependencies.version, + writtenAt: this.#dependencies.clock.now(), + writeIdempotencyKey: context.idempotencyKey, + candidateId: stableId("skill-writer-candidate", { batchId: batch.batchId }), + batchId: batch.batchId, + batchHash, + expectedBaseVersion: batch.expectedBaseVersion, + resultingHarnessVersion: staged.manifest.version, + operationIds: [mutation.mutationId], + afterSkills, + diff, + }; + const candidate: StoredSkillWriterCandidate = { + ...candidateContent, + candidateDigest: computeEvolutionContentDigest(candidateContent), + }; + await this.#writeImmutable(this.#candidateFile(batch.batchId), candidate); + return this.#writeReceipt(batch, candidate); + } + + #activationReceipt( + batch: SkillMutationBatch, + idempotencyKey: string, + result: HarnessActivationResult, + createdAt: string, + ): HarnessActivationReceipt { + const content = { batchId: batch.batchId, idempotencyKey, result }; + return { + metadata: createEvolutionArtifactMetadata({ + artifactId: stableId("skill-activation-receipt", { batchId: batch.batchId, idempotencyKey }), + producer: this.#producer, + sourceIds: [batch.batchId, ...batch.mutations.map((mutation) => mutation.mutationId)], + scope: batch.metadata.scope, + createdAt, + content, + }), + ...content, + }; + } + + async activate( + batch: SkillMutationBatch, + writeReceipt: MutationWriteReceipt, + context: SkillWriterContext, + ): Promise { + context.signal?.throwIfAborted(); + assertWriterContext(context); + await this.#dependencies.leases.assertHeld(context.lease); + const prior = await this.#loadActivation(batch.batchId); + const writeReceiptDigest = computeEvolutionContentDigest(writeReceipt); + if (prior !== null) { + if ( + prior.batchHash !== computeEvolutionContentDigest(batch) || + prior.writeReceiptDigest !== writeReceiptDigest || + prior.activationIdempotencyKey !== context.idempotencyKey + ) { + throw new Error(`Skill mutation batch "${batch.batchId}" was already activated with different content`); + } + return prior.receipt; + } + await this.#assertBatch(batch); + const output = parseWriteOutput(writeReceipt.result); + const candidate = await this.#loadCandidate(batch.batchId); + if ( + writeReceipt.batchId !== batch.batchId || + writeReceipt.idempotencyKey !== candidate?.writeIdempotencyKey || + writeReceipt.metadata.contentDigest !== computeEvolutionContentDigest(receiptContent(writeReceipt)) || + candidate === null || + candidate.batchHash !== computeEvolutionContentDigest(batch) || + candidate.candidateId !== output.candidateId || + candidate.candidateDigest !== output.candidateDigest || + serializeEvolutionValue(output) !== serializeEvolutionValue(outputForCandidate(candidate)) + ) { + throw new Error(`Skill mutation batch "${batch.batchId}" write receipt does not match its staged bundle`); + } + const base = await this.#dependencies.harnessState.loadVersion(candidate.expectedBaseVersion); + const staged = await this.#dependencies.harnessState.loadVersion(candidate.resultingHarnessVersion); + if ( + staged.manifest.base !== candidate.expectedBaseVersion || + staged.state.memory !== base.state.memory || + serializeEvolutionValue(sortSkillInputs(staged.state.skills.map(skillInput))) !== + serializeEvolutionValue(candidate.afterSkills) + ) { + throw new Error(`Skill mutation batch "${batch.batchId}" staged harness bundle is invalid`); + } + await this.#dependencies.leases.assertHeld(context.lease); + const selection = await this.#dependencies.harnessState.selectVersionIfCurrent( + candidate.resultingHarnessVersion, + candidate.expectedBaseVersion, + ); + if (selection.status === "conflict" && selection.actualCurrent !== candidate.resultingHarnessVersion) { + throw new EvolutionExecutionError("Harness revision changed before skill activation", { + reasonCode: "harness-base-conflict", + retryable: false, + diagnostics: { + expectedHarnessVersion: selection.expectedCurrent, + actualHarnessVersion: selection.actualCurrent, + }, + }); + } + const result: HarnessActivationResult = { + batchId: batch.batchId, + status: "activated", + resultingHarnessVersion: candidate.resultingHarnessVersion, + message: null, + }; + const receipt = this.#activationReceipt(batch, context.idempotencyKey, result, this.#dependencies.clock.now()); + await this.#writeImmutable(this.#activationFile(batch.batchId), { + schemaVersion: ACTIVATION_SCHEMA_VERSION, + batchId: batch.batchId, + batchHash: candidate.batchHash, + writeReceiptDigest, + activationIdempotencyKey: context.idempotencyKey, + receipt, + } satisfies StoredSkillWriterActivation); + return receipt; + } +} diff --git a/packages/harness-state/src/index.ts b/packages/harness-state/src/index.ts index 87acb96dc..9bd78a3ca 100644 --- a/packages/harness-state/src/index.ts +++ b/packages/harness-state/src/index.ts @@ -50,10 +50,7 @@ export interface HarnessWorkspaceIdentity { readonly directoryId?: string; } -export function computeHarnessSkillId( - workspace: HarnessWorkspaceIdentity, - path: string, -): string { +export function computeHarnessSkillId(workspace: HarnessWorkspaceIdentity, path: string): string { validateArtifactPath(path); return `skill:${createHash("sha256") .update(JSON.stringify(["evolved", workspace.repositoryId, workspace.directoryId ?? null, path])) From 8aedc5430b44328ac8d7e279c1f4d7fb2d730aba Mon Sep 17 00:00:00 2001 From: Jay Yu Date: Fri, 28 Aug 2026 14:01:37 -0400 Subject: [PATCH 3/3] feat(agent): verify evolved skill activation --- .../evolution/test/filesystem-store.test.ts | 2 + packages/evolution/test/skill-writer.test.ts | 492 ++++++++++++++++++ .../harness-state/test/harness-state.test.ts | 6 +- 3 files changed, 497 insertions(+), 3 deletions(-) create mode 100644 packages/evolution/test/skill-writer.test.ts diff --git a/packages/evolution/test/filesystem-store.test.ts b/packages/evolution/test/filesystem-store.test.ts index 1c0983887..8f2476c95 100644 --- a/packages/evolution/test/filesystem-store.test.ts +++ b/packages/evolution/test/filesystem-store.test.ts @@ -259,6 +259,7 @@ describe("filesystem evolution persistence", () => { const { store } = await storeFixture(); const artifactReference = await store.put(instructionSnapshot()); const firstLease = await scopeLease(store, "owner-1"); + await expect(store.assertHeld(firstLease)).resolves.toBeUndefined(); await store.create(job(artifactReference), firstLease); const takeoverTime = new Date(Date.parse(firstLease.expiresAt) + 1).toISOString(); @@ -271,6 +272,7 @@ describe("filesystem evolution persistence", () => { ); if (secondLease === null) throw new Error("Expected stale lease takeover"); expect(secondLease.fence).toBe(firstLease.fence + 1); + await expect(store.assertHeld(firstLease)).rejects.toThrow("lease was lost"); await expect(store.commit(scope, 0, 3, "job-1", new Date().toISOString(), firstLease)).rejects.toThrow( "lease was lost", ); diff --git a/packages/evolution/test/skill-writer.test.ts b/packages/evolution/test/skill-writer.test.ts new file mode 100644 index 000000000..d15f3a654 --- /dev/null +++ b/packages/evolution/test/skill-writer.test.ts @@ -0,0 +1,492 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { + computeHarnessSkillId, + HarnessStateStore, + type HarnessWorkspaceIdentity, +} from "../../harness-state/src/index.ts"; +import { + computeEvolutionContentDigest, + createEvolutionArtifactMetadata, + type EvolutionArtifact, + type EvolutionArtifactProducer, + type EvolutionArtifactReference, + type EvolutionArtifactStore, + type EvolutionScope, + FileEvolutionPersistence, + type SkillCandidateGroup, + type SkillCandidateOperation, + type SkillCandidateRegistry, + type SkillDraft, + type SkillDraftValidationReport, + type SkillEvaluationReport, + SkillWriter, +} from "../src/index.ts"; + +const now = "2026-08-29T12:00:00.000Z"; +const scope: EvolutionScope = { repositoryId: "diffex", scopeId: "repository" }; +const workspace: HarnessWorkspaceIdentity = { repositoryId: "diffex", directoryId: "directory-1" }; +const producer: EvolutionArtifactProducer = { + name: "fixture", + version: "1", + promptVersion: null, + model: null, +}; +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true }))); +}); + +function skillContent(name: string, marker = name): string { + return `---\nname: ${name}\ndescription: Use ${name} for ${marker} tasks.\n---\n\n# ${name}\n\nApply ${marker}.\n`; +} + +function metadata(artifactId: string, sourceIds: readonly string[], content: unknown) { + return createEvolutionArtifactMetadata({ artifactId, producer, sourceIds, scope, createdAt: now, content }); +} + +function reference(artifact: EvolutionArtifact): EvolutionArtifactReference { + return { artifactId: artifact.metadata.artifactId, contentDigest: computeEvolutionContentDigest(artifact) }; +} + +class MemoryArtifactStore implements EvolutionArtifactStore { + readonly #artifacts = new Map(); + + constructor(artifacts: readonly EvolutionArtifact[]) { + for (const artifact of artifacts) this.#artifacts.set(reference(artifact).contentDigest, artifact); + } + + async put(artifact: EvolutionArtifact): Promise { + const artifactReference = reference(artifact); + this.#artifacts.set(artifactReference.contentDigest, artifact); + return artifactReference; + } + + async get(artifactReference: EvolutionArtifactReference): Promise { + const artifact = this.#artifacts.get(artifactReference.contentDigest); + if (artifact === undefined) throw new Error("Unknown fixture artifact"); + return artifact; + } +} + +function registry(groups: readonly SkillCandidateGroup[]): SkillCandidateRegistry { + const unsupported = () => Promise.reject(new Error("Unsupported fixture registry mutation")); + return { + list: async () => groups, + consolidate: unsupported, + recordDraft: unsupported, + recordValidation: unsupported, + startEvaluation: unsupported, + recordEvaluation: unsupported, + }; +} + +interface WriterFixtureOptions { + readonly operation: SkillCandidateOperation; + readonly existingNames?: readonly string[]; + readonly existingSkillIdsOverride?: readonly string[]; + readonly retainedNames?: readonly string[]; + readonly proposedName: string; +} + +async function writerFixture(options: WriterFixtureOptions) { + const root = await mkdtemp(join(tmpdir(), "diffex-skill-writer-")); + temporaryDirectories.push(root); + const harnessState = new HarnessStateStore(join(root, "agent")); + const empty = await harnessState.initialize(); + const existingNames = options.existingNames ?? []; + const retainedNames = options.retainedNames ?? []; + const initialSkills = [...existingNames, ...retainedNames].map((name) => ({ + path: `skills/${name}/SKILL.md`, + content: skillContent(name, `${name}-original`), + workspace, + })); + const advanced = await harnessState.advance( + { memory: "Preserve exact harness memory.\n", skills: initialSkills }, + empty.manifest.version, + ); + if (advanced.status !== "advanced") throw new Error("Expected fixture harness advance"); + const base = advanced.bundle; + const existingSkills = base.state.skills.filter((skill) => existingNames.includes(skill.name)); + const existingSkillIds = + options.existingSkillIdsOverride ?? + existingSkills.map((skill) => computeHarnessSkillId(skill.workspace, skill.path)); + const draftContent = skillContent(options.proposedName, `${options.proposedName}-evolved`); + const draftBody = { + artifactKind: "skill-draft" as const, + draftId: "draft-1", + groupId: "group-1", + synthesisIdempotencyKey: "synthesis-1", + proposalRevisionIds: ["revision-1"], + sourceCandidateIds: ["candidate-1"], + sourceEvidenceIds: ["evidence-1"], + sourceTraceIds: ["trace-1"], + sourceSkillVersions: existingSkillIds.map((skillId, index) => ({ + skillId, + version: existingSkills[index]?.contentHash ?? `sha256:${"9".repeat(64)}`, + contentDigest: existingSkills[index]?.contentHash ?? `sha256:${"9".repeat(64)}`, + })), + repositoryInstructionDigests: [], + operation: options.operation, + proposedName: options.proposedName, + existingSkillIds, + workspace: { + repositoryId: workspace.repositoryId, + directoryId: workspace.directoryId, + rootPath: "/repo", + repositoryRevision: "revision-1", + evolutionScope: scope, + }, + content: draftContent, + }; + const draft: SkillDraft = { + metadata: metadata(draftBody.draftId, draftBody.sourceCandidateIds, draftBody), + ...draftBody, + }; + const draftReference = reference(draft); + const validationBody = { + artifactKind: "skill-draft-validation" as const, + validationId: "validation-1", + groupId: draft.groupId, + draft: draftReference, + validatorVersion: "validator-1", + assurance: "structure-provenance-scope-and-security-only" as const, + disposition: "accepted" as const, + issues: [], + validatedHarnessVersion: base.manifest.version, + validatedSkillVersions: draft.sourceSkillVersions.map( + (skill) => `${skill.skillId}\u0000${skill.version}\u0000${skill.contentDigest}`, + ), + canonicalInventory: [`skills/${options.proposedName}/SKILL.md`], + memoryRelationshipDigest: computeEvolutionContentDigest([]), + }; + const validation: SkillDraftValidationReport = { + metadata: metadata(validationBody.validationId, [draft.draftId], validationBody), + ...validationBody, + }; + const validationReference = reference(validation); + const evaluationBody = { + artifactKind: "skill-evaluation" as const, + schemaVersion: 1 as const, + evaluationId: "evaluation-1", + groupId: draft.groupId, + evaluationIdempotencyKey: "evaluation-1", + proposalRevisionIds: draft.proposalRevisionIds, + draft: draftReference, + validation: validationReference, + evaluatorVersion: "evaluator-1", + assurance: "retrieval-and-deterministic-behavior-gates" as const, + disposition: "accepted" as const, + harnessVersion: base.manifest.version, + candidateSkill: { + skillId: draft.draftId, + name: draft.proposedName, + description: `Use ${draft.proposedName} for ${draft.proposedName}-evolved tasks.`, + source: "candidate" as const, + version: draft.metadata.contentDigest, + contentDigest: computeEvolutionContentDigest(draft.content), + content: draft.content, + }, + neighboringSkills: [], + configuration: { + model: { provider: "faux", model: "fixture", configurationDigest: `sha256:${"1".repeat(64)}` }, + runtime: { name: "node", version: "24", configurationDigest: `sha256:${"2".repeat(64)}` }, + retriever: { name: "fixture", version: "1", configurationDigest: `sha256:${"3".repeat(64)}` }, + executor: { name: "fixture", version: "1", configurationDigest: `sha256:${"4".repeat(64)}` }, + }, + cases: [], + retrievalResults: [], + behaviorResults: [], + metrics: { + exactRetrievalAccuracy: 1, + sourceReplayReproducibility: 1, + heldOutIntendedBaselineScore: 0, + heldOutIntendedFullPoolScore: 1, + heldOutIntendedImprovement: 1, + negativeRegressionCount: 0, + misapplicationCount: 0, + }, + gates: { + suite: { passed: true, reasonCodes: [] }, + retrieval: { passed: true, reasonCodes: [] }, + behavior: { passed: true, reasonCodes: [] }, + }, + }; + const evaluation: SkillEvaluationReport = { + metadata: metadata(evaluationBody.evaluationId, [draft.draftId, validation.validationId], evaluationBody), + ...evaluationBody, + }; + const group: SkillCandidateGroup = { + groupId: draft.groupId, + profile: { + workspace: { repositoryId: workspace.repositoryId, directoryId: workspace.directoryId, evolutionScope: scope }, + existingSkillIds, + skillability: "ordered-workflow", + triggerTerms: ["fixture"], + proceduralIntentTerms: ["fixture"], + applicabilityTerms: ["repository"], + }, + proposalRevisions: [ + { + revisionId: "revision-1", + candidateId: "candidate-1", + candidate: { artifactId: "candidate-1", contentDigest: `sha256:${"5".repeat(64)}` }, + sourceEvolutionJobId: "job-1", + createdAt: now, + evidence: { + supportingEvidenceIds: ["evidence-1"], + contradictingEvidenceIds: [], + supportingTraceIds: ["trace-1"], + contradictingTraceIds: [], + supportingTaskLineageIds: ["task-1", "task-2"], + contradictingTaskLineageIds: [], + }, + }, + ], + state: "accepted", + dispositionHistory: [], + evidence: { + supportingEvidenceIds: ["evidence-1"], + contradictingEvidenceIds: [], + supportingTraceIds: ["trace-1"], + contradictingTraceIds: [], + supportingTaskLineageIds: ["task-1", "task-2"], + contradictingTaskLineageIds: [], + }, + priorityScore: null, + draft: draftReference, + validation: validationReference, + evaluation: reference(evaluation), + synthesisIdempotencyKey: draft.synthesisIdempotencyKey, + validationIdempotencyKey: "validation-1", + evaluationIdempotencyKey: evaluation.evaluationIdempotencyKey, + }; + const groups = [group]; + const persistence = new FileEvolutionPersistence(join(root, "evolution")); + const lease = await persistence.acquire("writer", "global", "writer-1", new Date().toISOString(), 60_000); + if (lease === null) throw new Error("Expected writer lease"); + const writer = new SkillWriter({ + version: "writer-1", + workspace, + harnessState, + artifacts: new MemoryArtifactStore([draft, validation, evaluation]), + registry: registry(groups), + leases: persistence, + clock: { now: () => now }, + statePath: join(root, "writer-state"), + }); + return { base, draft, group, groups, harnessState, lease, persistence, root, writer }; +} + +describe("skill writer", () => { + it.each([ + { + operation: "CREATE" as const, + existingNames: [], + retainedNames: ["retained"], + proposedName: "created-skill", + expectedNames: ["created-skill", "retained"], + }, + { + operation: "UPDATE" as const, + existingNames: ["old-skill"], + retainedNames: ["retained"], + proposedName: "updated-skill", + expectedNames: ["retained", "updated-skill"], + }, + { + operation: "MERGE" as const, + existingNames: ["first-skill", "second-skill"], + retainedNames: ["retained"], + proposedName: "merged-skill", + expectedNames: ["merged-skill", "retained"], + }, + { + operation: "RETIRE" as const, + existingNames: ["retired-skill"], + retainedNames: ["retained"], + proposedName: "retired-skill", + expectedNames: ["retained"], + }, + ])("stages and atomically activates an accepted $operation mutation", async (testCase) => { + const data = await writerFixture(testCase); + const batch = await data.writer.buildMutationBatch({ scope, groupId: data.group.groupId, createdAt: now }); + const writeReceipt = await data.writer.write(batch, { idempotencyKey: "write-1", lease: data.lease }); + + expect((await data.harnessState.loadCurrent()).manifest.version).toBe(data.base.manifest.version); + expect(writeReceipt.result).toMatchObject({ + status: "written", + output: { expectedBaseVersion: data.base.manifest.version }, + }); + + const activation = await data.writer.activate(batch, writeReceipt, { + idempotencyKey: "activate-1", + lease: data.lease, + }); + const current = await data.harnessState.loadCurrent(); + expect(activation.result.status).toBe("activated"); + expect(current.manifest.version).toBe(activation.result.resultingHarnessVersion); + expect(current.manifest.base).toBe(data.base.manifest.version); + expect(current.state.memory).toBe(data.base.state.memory); + expect(current.state.skills.map((skill) => skill.name).sort()).toEqual([...testCase.expectedNames].sort()); + }); + + it("makes write and activation retries idempotent and preserves exact rollback lineage", async () => { + const data = await writerFixture({ + operation: "CREATE", + retainedNames: ["retained"], + proposedName: "created-skill", + }); + const batch = await data.writer.buildMutationBatch({ scope, groupId: data.group.groupId, createdAt: now }); + const firstWrite = await data.writer.write(batch, { idempotencyKey: "write-1", lease: data.lease }); + const secondWrite = await data.writer.write(batch, { idempotencyKey: "write-1", lease: data.lease }); + expect(secondWrite).toEqual(firstWrite); + + const firstActivation = await data.writer.activate(batch, firstWrite, { + idempotencyKey: "activate-1", + lease: data.lease, + }); + const secondActivation = await data.writer.activate(batch, secondWrite, { + idempotencyKey: "activate-1", + lease: data.lease, + }); + expect(secondActivation).toEqual(firstActivation); + const activated = await data.harnessState.loadCurrent(); + const rollback = await data.harnessState.selectVersionIfCurrent( + data.base.manifest.version, + activated.manifest.version, + ); + expect(rollback).toEqual({ status: "selected", bundle: data.base }); + expect(await data.harnessState.loadCurrent()).toEqual(data.base); + }); + + it("rejects non-harness targets, stale digests, and target name collisions", async () => { + const installed = await writerFixture({ + operation: "UPDATE", + existingSkillIdsOverride: ["installed:workspace:external-skill"], + proposedName: "updated-skill", + }); + await expect( + installed.writer.buildMutationBatch({ scope, groupId: installed.group.groupId, createdAt: now }), + ).rejects.toThrow("is not an evolved skill in the frozen harness base"); + + const staleDigest = await writerFixture({ + operation: "UPDATE", + existingNames: ["old-skill"], + proposedName: "updated-skill", + }); + const validBatch = await staleDigest.writer.buildMutationBatch({ + scope, + groupId: staleDigest.group.groupId, + createdAt: now, + }); + const validMutation = validBatch.mutations[0]; + if (validMutation === undefined) throw new Error("Expected skill mutation"); + const changedMutation = { + ...validMutation, + existingSkills: validMutation.existingSkills.map((skill) => ({ + ...skill, + contentDigest: `sha256:${"f".repeat(64)}`, + })), + }; + const changedContent = { + batchId: validBatch.batchId, + targetKind: validBatch.targetKind, + expectedBaseVersion: validBatch.expectedBaseVersion, + workspace: validBatch.workspace, + mutations: [changedMutation], + }; + const changedBatch = { + metadata: metadata(validBatch.batchId, validBatch.metadata.sourceIds, changedContent), + ...changedContent, + }; + await expect( + staleDigest.writer.write(changedBatch, { idempotencyKey: "write-1", lease: staleDigest.lease }), + ).rejects.toThrow("does not match its accepted candidate"); + expect(await staleDigest.harnessState.loadCurrent()).toEqual(staleDigest.base); + + const collision = await writerFixture({ + operation: "UPDATE", + existingNames: ["old-skill"], + retainedNames: ["retained"], + proposedName: "retained", + }); + const collisionBatch = await collision.writer.buildMutationBatch({ + scope, + groupId: collision.group.groupId, + createdAt: now, + }); + await expect( + collision.writer.write(collisionBatch, { idempotencyKey: "write-1", lease: collision.lease }), + ).rejects.toThrow('target "skills/retained/SKILL.md" already exists'); + expect(await collision.harnessState.loadCurrent()).toEqual(collision.base); + }); + + it("rejects stale writer leases before selecting the staged harness", async () => { + const data = await writerFixture({ operation: "CREATE", proposedName: "created-skill" }); + const batch = await data.writer.buildMutationBatch({ scope, groupId: data.group.groupId, createdAt: now }); + const receipt = await data.writer.write(batch, { idempotencyKey: "write-1", lease: data.lease }); + await data.persistence.release(data.lease); + + await expect( + data.writer.activate(batch, receipt, { idempotencyKey: "activate-1", lease: data.lease }), + ).rejects.toThrow("lease was lost"); + expect(await data.harnessState.loadCurrent()).toEqual(data.base); + }); + + it("rechecks accepted candidate state immediately before activation", async () => { + const data = await writerFixture({ operation: "CREATE", proposedName: "created-skill" }); + const batch = await data.writer.buildMutationBatch({ scope, groupId: data.group.groupId, createdAt: now }); + const receipt = await data.writer.write(batch, { idempotencyKey: "write-1", lease: data.lease }); + data.groups[0] = { ...data.group, state: "evidence-qualified", draft: null, validation: null, evaluation: null }; + + await expect( + data.writer.activate(batch, receipt, { idempotencyKey: "activate-1", lease: data.lease }), + ).rejects.toThrow("is not accepted for skill activation"); + expect(await data.harnessState.loadCurrent()).toEqual(data.base); + }); + + it("rejects stale harness bases and tampered write receipts", async () => { + const stale = await writerFixture({ operation: "CREATE", proposedName: "created-skill" }); + const staleBatch = await stale.writer.buildMutationBatch({ scope, groupId: stale.group.groupId, createdAt: now }); + const staleReceipt = await stale.writer.write(staleBatch, { + idempotencyKey: "write-1", + lease: stale.lease, + }); + const competing = await stale.harnessState.advance( + { memory: "Competing harness memory.\n" }, + stale.base.manifest.version, + ); + if (competing.status !== "advanced") throw new Error("Expected competing harness advance"); + await expect( + stale.writer.activate(staleBatch, staleReceipt, { + idempotencyKey: "activate-1", + lease: stale.lease, + }), + ).rejects.toMatchObject({ reasonCode: "harness-base-conflict", retryable: false }); + expect(await stale.harnessState.loadCurrent()).toEqual(competing.bundle); + + const tampered = await writerFixture({ operation: "CREATE", proposedName: "created-skill" }); + const batch = await tampered.writer.buildMutationBatch({ + scope, + groupId: tampered.group.groupId, + createdAt: now, + }); + const receipt = await tampered.writer.write(batch, { idempotencyKey: "write-1", lease: tampered.lease }); + if (receipt.result.output === null) throw new Error("Expected skill write output"); + const changed = { + ...receipt, + result: { + ...receipt.result, + output: { ...receipt.result.output, resultingHarnessVersion: `sha256:${"f".repeat(64)}` }, + }, + }; + await expect( + tampered.writer.activate(batch, changed, { idempotencyKey: "activate-1", lease: tampered.lease }), + ).rejects.toThrow("write receipt does not match its staged bundle"); + expect(await tampered.harnessState.loadCurrent()).toEqual(tampered.base); + }); +}); diff --git a/packages/harness-state/test/harness-state.test.ts b/packages/harness-state/test/harness-state.test.ts index c90349c50..8cc269cdd 100644 --- a/packages/harness-state/test/harness-state.test.ts +++ b/packages/harness-state/test/harness-state.test.ts @@ -216,9 +216,9 @@ describe("canonical harness versions", () => { expect(skillId).toMatch(/^skill:[0-9a-f]{64}$/); expect(computeHarnessSkillId(WORKSPACE, "skills/focused-review/SKILL.md")).toBe(skillId); - expect( - computeHarnessSkillId({ ...WORKSPACE, directoryId: "other" }, "skills/focused-review/SKILL.md"), - ).not.toBe(skillId); + expect(computeHarnessSkillId({ ...WORKSPACE, directoryId: "other" }, "skills/focused-review/SKILL.md")).not.toBe( + skillId, + ); expect(() => computeHarnessSkillId(WORKSPACE, "../SKILL.md")).toThrow(/artifact path/i); });