From 479c00d74ce6ef9785a14311f2ca41864664afb7 Mon Sep 17 00:00:00 2001 From: issacboy123 Date: Sat, 15 Aug 2026 14:56:54 -0700 Subject: [PATCH 1/6] feat: add persistent harness state bundles --- package-lock.json | 18 + package.json | 4 +- packages/harness-state/README.md | 37 +++ packages/harness-state/package.json | 34 ++ packages/harness-state/src/default/EVOLVE.md | 5 + .../harness-state/src/default/manifest.json | 1 + packages/harness-state/src/index.ts | 133 ++++++++ .../harness-state/test/harness-state.test.ts | 307 ++++++++++++++++++ packages/harness-state/tsconfig.build.json | 9 + packages/harness-state/tsconfig.test.json | 10 + packages/harness-state/vitest.config.ts | 9 + tsconfig.json | 1 + 12 files changed, 566 insertions(+), 2 deletions(-) create mode 100644 packages/harness-state/README.md create mode 100644 packages/harness-state/package.json create mode 100644 packages/harness-state/src/default/EVOLVE.md create mode 100644 packages/harness-state/src/default/manifest.json create mode 100644 packages/harness-state/src/index.ts create mode 100644 packages/harness-state/test/harness-state.test.ts create mode 100644 packages/harness-state/tsconfig.build.json create mode 100644 packages/harness-state/tsconfig.test.json create mode 100644 packages/harness-state/vitest.config.ts diff --git a/package-lock.json b/package-lock.json index aae3ec002..e5e7f408b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -719,6 +719,10 @@ "node": ">=20" } }, + "node_modules/@diffexai/diffex-harness-state": { + "resolved": "packages/harness-state", + "link": true + }, "node_modules/@earendil-works/diffex-agent-core": { "resolved": "packages/agent", "link": true @@ -5710,6 +5714,20 @@ "dev": true, "license": "MIT" }, + "packages/harness-state": { + "name": "@diffexai/diffex-harness-state", + "version": "0.2.0", + "dependencies": { + "typebox": "1.3.7" + }, + "devDependencies": { + "shx": "0.4.0", + "vitest": "4.1.9" + }, + "engines": { + "node": ">=22.19.0" + } + }, "packages/protocol": { "name": "@earendil-works/diffex-protocol", "version": "0.2.0", diff --git a/package.json b/package.json index 1b3a4c8a3..e4cbe1721 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ ], "scripts": { "clean": "npm run clean --workspaces", - "build": "cd packages/tui && npm run build && cd ../telemetry && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../session-backends/sqlite-node && npm run build && cd ../../protocol && npm run build && cd ../client && npm run build && cd ../server && npm run build && cd ../coding-agent && npm run build", - "build:offline": "cd packages/tui && npm run build && cd ../telemetry && npm run build && cd ../ai && npm run build:offline && cd ../agent && npm run build && cd ../session-backends/sqlite-node && npm run build && cd ../../protocol && npm run build && cd ../client && npm run build && cd ../server && npm run build && cd ../coding-agent && npm run build", + "build": "cd packages/harness-state && npm run build && cd ../tui && npm run build && cd ../telemetry && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../session-backends/sqlite-node && npm run build && cd ../../protocol && npm run build && cd ../client && npm run build && cd ../server && npm run build && cd ../coding-agent && npm run build", + "build:offline": "cd packages/harness-state && npm run build && cd ../tui && npm run build && cd ../telemetry && npm run build && cd ../ai && npm run build:offline && cd ../agent && npm run build && cd ../session-backends/sqlite-node && npm run build && cd ../../protocol && npm run build && cd ../client && npm run build && cd ../server && npm run build && cd ../coding-agent && npm run build", "check": "biome check --write --error-on-warnings . && npm run check:pinned-deps && npm run check:ts-imports && npm run check:shrinkwrap && npm run check:install-lock:coding-agent && tsgo --noEmit && npm run check:browser-smoke", "check:browser-smoke": "node scripts/check-browser-smoke.mjs", "check:pinned-deps": "node scripts/check-pinned-deps.mjs", diff --git a/packages/harness-state/README.md b/packages/harness-state/README.md new file mode 100644 index 000000000..6ed483748 --- /dev/null +++ b/packages/harness-state/README.md @@ -0,0 +1,37 @@ +# `@diffexai/diffex-harness-state` + +Private workspace package for the version 1 Diffex harness-state schema and directory-bundle persistence. + +## State and bundle format + +A version 1 logical state contains only the evolvable static system prompt: + +```ts +{ + schemaVersion: 1; + systemPrompt: string; +} +``` + +The prompt must not start with U+FEFF and must contain a non-whitespace character. A portable bundle is a directory with exactly these direct regular files and exact casing: + +```text +state-directory/ +|-- manifest.json # exact UTF-8 bytes for {"schemaVersion":1} plus one LF +`-- EVOLVE.md # systemPrompt UTF-8 bytes, preserved verbatim +``` + +Both files must be fatal, BOM-free UTF-8. The manifest must contain only `schemaVersion: 1`. + +## API + +- `HarnessStateSchema` and `HarnessStateManifestSchema` are strict TypeBox schemas. Their inferred `HarnessState` and `HarnessStateManifest` types expose readonly fields. +- `loadHarnessState(bundlePath)` validates a bundle and returns its flat logical state without normalizing the prompt. +- `saveHarnessState(state, destinationPath)` validates the state, stages a canonical two-file bundle beside the destination, and renames it into place. The parent must already be a directory, and any existing destination entry is rejected. +- `loadDefaultHarnessState()` loads the packaged clean-default bundle through `loadHarnessState()`. + +Invalid state, layout, encoding, manifest, or prompt data rejects with an ordinary descriptive `Error`. JSON syntax failures retain the original syntax error as their cause. Other filesystem errors propagate unchanged. + +## Version 1 limitations + +Version 1 stores only the static system prompt. It has no migration or compatibility behavior and does not represent tools, models, context, sessions, lineage, provenance, or evolution metadata. Persistence does not provide cross-process coordination, retries, or durability guarantees. diff --git a/packages/harness-state/package.json b/packages/harness-state/package.json new file mode 100644 index 000000000..24a8af8f1 --- /dev/null +++ b/packages/harness-state/package.json @@ -0,0 +1,34 @@ +{ + "name": "@diffexai/diffex-harness-state", + "version": "0.2.0", + "description": "Persistent harness state schema and bundle storage for Diffex", + "private": true, + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "clean": "shx rm -rf dist", + "build": "tsgo -p tsconfig.build.json && shx rm -rf dist/default && shx cp -r src/default dist/default", + "test": "vitest --run" + }, + "engines": { + "node": ">=22.19.0" + }, + "dependencies": { + "typebox": "1.3.7" + }, + "devDependencies": { + "shx": "0.4.0", + "vitest": "4.1.9" + } +} diff --git a/packages/harness-state/src/default/EVOLVE.md b/packages/harness-state/src/default/EVOLVE.md new file mode 100644 index 000000000..c3eab8757 --- /dev/null +++ b/packages/harness-state/src/default/EVOLVE.md @@ -0,0 +1,5 @@ +You are an expert coding assistant operating inside Diffex, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files. + +Guidelines: +- Be concise in your responses +- Show file paths clearly when working with files diff --git a/packages/harness-state/src/default/manifest.json b/packages/harness-state/src/default/manifest.json new file mode 100644 index 000000000..156e9b317 --- /dev/null +++ b/packages/harness-state/src/default/manifest.json @@ -0,0 +1 @@ +{"schemaVersion":1} diff --git a/packages/harness-state/src/index.ts b/packages/harness-state/src/index.ts new file mode 100644 index 000000000..513c59fe2 --- /dev/null +++ b/packages/harness-state/src/index.ts @@ -0,0 +1,133 @@ +import { lstat, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises"; +import { basename, dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { TextDecoder } from "node:util"; +import Type, { type Static } from "typebox"; +import { Check } from "typebox/value"; + +export const HARNESS_STATE_SCHEMA_VERSION = 1 as const; + +const SystemPromptSchema = Type.Readonly(Type.String({ pattern: "^(?!\\uFEFF)(?=[\\s\\S]*\\S)[\\s\\S]*$" })); + +export const HarnessStateSchema = Type.Object( + { + schemaVersion: Type.Readonly(Type.Literal(HARNESS_STATE_SCHEMA_VERSION)), + systemPrompt: SystemPromptSchema, + }, + { additionalProperties: false }, +); +export type HarnessState = Static; + +export const HarnessStateManifestSchema = Type.Object( + { schemaVersion: Type.Readonly(Type.Literal(HARNESS_STATE_SCHEMA_VERSION)) }, + { additionalProperties: false }, +); +export type HarnessStateManifest = Static; + +const MANIFEST_NAME = "manifest.json"; +const PROMPT_NAME = "EVOLVE.md"; +const BUNDLE_ENTRY_NAMES = [MANIFEST_NAME, PROMPT_NAME] as const; +const CANONICAL_MANIFEST = '{"schemaVersion":1}\n'; +const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); + +function decodeBundleFile(bytes: Uint8Array, filePath: string): string { + let value: string; + try { + value = UTF8_DECODER.decode(bytes); + } catch (cause) { + throw new Error(`Invalid UTF-8 in harness state file "${filePath}"`, { cause }); + } + if (value.startsWith("\uFEFF")) throw new Error(`Harness state file "${filePath}" must not start with a UTF-8 BOM`); + return value; +} + +function parseManifest(value: string, manifestPath: string): HarnessStateManifest { + let parsed: unknown; + try { + parsed = JSON.parse(value); + } catch (cause) { + throw new Error(`Invalid JSON in harness state manifest "${manifestPath}"`, { cause }); + } + if (!Check(HarnessStateManifestSchema, parsed)) { + throw new Error(`Invalid harness state manifest at "${manifestPath}"`); + } + return parsed; +} + +async function inspectBundleDirectory(bundlePath: string): Promise { + const bundleStats = await lstat(bundlePath); + if (!bundleStats.isDirectory() || bundleStats.isSymbolicLink()) { + throw new Error(`Harness state bundle "${bundlePath}" must be a direct directory`); + } + + const entries = await readdir(bundlePath, { withFileTypes: true }); + const expectedNames = new Set(BUNDLE_ENTRY_NAMES); + if (entries.length !== BUNDLE_ENTRY_NAMES.length || entries.some((entry) => !expectedNames.has(entry.name))) { + throw new Error(`Harness state bundle "${bundlePath}" must contain exactly manifest.json and EVOLVE.md`); + } + for (const name of BUNDLE_ENTRY_NAMES) { + const entry = entries.find((candidate) => candidate.name === name); + if (!entry?.isFile() || entry.isSymbolicLink()) { + throw new Error(`Harness state bundle entry "${join(bundlePath, name)}" must be a direct regular file`); + } + } +} + +export async function loadHarnessState(bundlePath: string): Promise { + await inspectBundleDirectory(bundlePath); + const manifestPath = join(bundlePath, MANIFEST_NAME); + const promptPath = join(bundlePath, PROMPT_NAME); + const manifestText = decodeBundleFile(await readFile(manifestPath), manifestPath); + const manifest = parseManifest(manifestText, manifestPath); + const systemPrompt = decodeBundleFile(await readFile(promptPath), promptPath); + const state = { schemaVersion: manifest.schemaVersion, systemPrompt }; + if (!Check(HarnessStateSchema, state)) throw new Error(`Invalid harness state prompt at "${promptPath}"`); + return state; +} + +function isMissingEntryError(error: unknown): boolean { + return error instanceof Error && "code" in error && error.code === "ENOENT"; +} + +async function assertDestinationAbsent(destinationPath: string): Promise { + try { + await lstat(destinationPath); + } catch (error) { + if (isMissingEntryError(error)) return; + throw error; + } + throw new Error(`Harness state destination "${destinationPath}" already exists`); +} + +export async function saveHarnessState(state: unknown, destinationPath: string): Promise { + if (!Check(HarnessStateSchema, state)) throw new Error("Invalid harness state to save"); + const systemPrompt = state.systemPrompt; + const absoluteDestinationPath = resolve(destinationPath); + const parentPath = dirname(absoluteDestinationPath); + const parentStats = await stat(parentPath); + if (!parentStats.isDirectory()) { + throw new Error(`Harness state destination parent "${parentPath}" must be a directory`); + } + await assertDestinationAbsent(absoluteDestinationPath); + + let temporaryPath: string | undefined; + try { + temporaryPath = await mkdtemp(join(parentPath, `.${basename(absoluteDestinationPath)}.harness-state.tmp-`)); + await writeFile(join(temporaryPath, MANIFEST_NAME), CANONICAL_MANIFEST, "utf8"); + await writeFile(join(temporaryPath, PROMPT_NAME), systemPrompt, "utf8"); + await assertDestinationAbsent(absoluteDestinationPath); + await rename(temporaryPath, absoluteDestinationPath); + temporaryPath = undefined; + } catch (error) { + if (temporaryPath !== undefined) { + try { + await rm(temporaryPath, { recursive: true, force: true }); + } catch {} + } + throw error; + } +} + +export async function loadDefaultHarnessState(): Promise { + return loadHarnessState(fileURLToPath(new URL("./default", import.meta.url))); +} diff --git a/packages/harness-state/test/harness-state.test.ts b/packages/harness-state/test/harness-state.test.ts new file mode 100644 index 000000000..f4cd84860 --- /dev/null +++ b/packages/harness-state/test/harness-state.test.ts @@ -0,0 +1,307 @@ +import { lstat, mkdir, mkdtemp, readdir, readFile, rm, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { basename, join } from "node:path"; +import { Check } from "typebox/value"; +import { afterEach, describe, expect, test } from "vitest"; +import { + HARNESS_STATE_SCHEMA_VERSION, + type HarnessState, + type HarnessStateManifest, + HarnessStateManifestSchema, + HarnessStateSchema, + loadDefaultHarnessState, + loadHarnessState, + saveHarnessState, +} from "../src/index.ts"; + +const DEFAULT_PROMPT = + "You are an expert coding assistant operating inside Diffex, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nGuidelines:\n- Be concise in your responses\n- Show file paths clearly when working with files\n"; +const MANIFEST = '{"schemaVersion":1}\n'; +const temporaryRoots: string[] = []; + +afterEach(async () => { + await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); +}); + +async function createTemporaryRoot(): Promise { + const root = await mkdtemp(join(tmpdir(), "diffex-harness-state-test-")); + temporaryRoots.push(root); + return root; +} + +async function createBundle( + root: string, + name = "state", + manifest: string | Uint8Array = MANIFEST, + prompt: string | Uint8Array = "Act carefully.\n", +): Promise { + const bundlePath = join(root, name); + await mkdir(bundlePath); + await writeFile(join(bundlePath, "manifest.json"), manifest); + await writeFile(join(bundlePath, "EVOLVE.md"), prompt); + return bundlePath; +} + +async function pathExists(path: string): Promise { + try { + await lstat(path); + return true; + } catch (error) { + if (error instanceof Error && "code" in error && error.code === "ENOENT") return false; + throw error; + } +} + +function assertReadonlyTypes(state: HarnessState, manifest: HarnessStateManifest): void { + // @ts-expect-error HarnessState fields are readonly. + state.systemPrompt = "changed"; + // @ts-expect-error HarnessStateManifest fields are readonly. + manifest.schemaVersion = 1; +} +void assertReadonlyTypes; + +describe("schemas", () => { + test("exports strict version 1 schemas with readonly inferred fields", () => { + expect(HARNESS_STATE_SCHEMA_VERSION).toBe(1); + expect(Check(HarnessStateSchema, { schemaVersion: 1, systemPrompt: "Work carefully." })).toBe(true); + expect(Check(HarnessStateManifestSchema, { schemaVersion: 1 })).toBe(true); + expect(Check(HarnessStateSchema, { schemaVersion: 1, systemPrompt: "Work.", extra: true })).toBe(false); + expect(Check(HarnessStateManifestSchema, { schemaVersion: 1, extra: true })).toBe(false); + }); + + test.each([ + ["wrong state version", { schemaVersion: 2, systemPrompt: "Work." }], + ["string state version", { schemaVersion: "1", systemPrompt: "Work." }], + ["blank prompt", { schemaVersion: 1, systemPrompt: "" }], + ["whitespace-only prompt", { schemaVersion: 1, systemPrompt: " \t\r\n" }], + ["leading U+FEFF prompt", { schemaVersion: 1, systemPrompt: "\uFEFFWork." }], + ] as const)("rejects %s", (_label, state) => { + expect(Check(HarnessStateSchema, state)).toBe(false); + }); + + test.each([ + ["wrong manifest version", { schemaVersion: 2 }], + ["string manifest version", { schemaVersion: "1" }], + ["missing manifest version", {}], + ] as const)("rejects %s", (_label, manifest) => { + expect(Check(HarnessStateManifestSchema, manifest)).toBe(false); + }); +}); + +describe("default bundle", () => { + test("loads the exact clean default through normal validation", async () => { + const state = await loadDefaultHarnessState(); + expect(state).toEqual({ schemaVersion: 1, systemPrompt: DEFAULT_PROMPT }); + expect(Object.isFrozen(state)).toBe(false); + }); +}); + +describe("saving and round trips", () => { + test.each([ + ["Unicode", "Inspect caf\u00e9, \u6c34, and \u{10151}."], + ["LF", "First line\nSecond line\n"], + ["CRLF", "First line\r\nSecond line\r\n"], + ["multiline", "One\n\nThree\nFour"], + ["trailing whitespace", "Keep this exactly. \n\t "], + ] as const)("preserves %s prompt content verbatim", async (_label, systemPrompt) => { + const root = await createTemporaryRoot(); + const destination = join(root, "saved-state"); + await saveHarnessState({ schemaVersion: 1, systemPrompt }, destination); + + expect(await readFile(join(destination, "manifest.json"), "utf8")).toBe(MANIFEST); + expect(await readFile(join(destination, "EVOLVE.md"), "utf8")).toBe(systemPrompt); + expect(await loadHarnessState(destination)).toEqual({ schemaVersion: 1, systemPrompt }); + }); + + test("validates state before filesystem access", async () => { + const root = await createTemporaryRoot(); + const destination = join(root, "missing-parent", "state"); + await expect(saveHarnessState({ schemaVersion: 1, systemPrompt: " " }, destination)).rejects.toThrow( + /invalid harness state/i, + ); + expect(await pathExists(destination)).toBe(false); + }); + + test("requires an existing directory parent", async () => { + const root = await createTemporaryRoot(); + const parentFile = join(root, "parent-file"); + await writeFile(parentFile, "not a directory"); + await expect( + saveHarnessState({ schemaVersion: 1, systemPrompt: "Valid." }, join(parentFile, "state")), + ).rejects.toThrow(); + }); + + test.each(["file", "directory"] as const)("refuses to overwrite an existing %s destination", async (kind) => { + const root = await createTemporaryRoot(); + const destination = join(root, "state"); + if (kind === "file") await writeFile(destination, "original"); + else await mkdir(destination); + + await expect(saveHarnessState({ schemaVersion: 1, systemPrompt: "Replacement." }, destination)).rejects.toThrow( + /already exists/i, + ); + if (kind === "file") expect(await readFile(destination, "utf8")).toBe("original"); + else expect(await readdir(destination)).toEqual([]); + }); + + test("refuses a dangling linked destination", async () => { + const root = await createTemporaryRoot(); + const destination = join(root, "state"); + await symlink(join(root, "missing-target"), destination, process.platform === "win32" ? "junction" : "dir"); + await expect(saveHarnessState({ schemaVersion: 1, systemPrompt: "Replacement." }, destination)).rejects.toThrow( + /already exists/i, + ); + }); + + test("cleans its staged directory after a late failure without exposing a partial bundle", async () => { + const root = await createTemporaryRoot(); + const destination = join(root, "state"); + const temporaryPrefix = `.${basename(destination)}.harness-state.tmp-`; + const saving = saveHarnessState( + { schemaVersion: 1, systemPrompt: "x".repeat(16 * 1024 * 1024) }, + destination, + ).then( + () => ({ ok: true as const }), + (error: unknown) => ({ ok: false as const, error }), + ); + + let stagedPath: string | undefined; + for (let attempt = 0; attempt < 1_000; attempt++) { + const stagedName = (await readdir(root)).find((entry) => entry.startsWith(temporaryPrefix)); + if (stagedName !== undefined) { + stagedPath = join(root, stagedName); + break; + } + await new Promise((resolveDelay) => setTimeout(resolveDelay, 1)); + } + expect(stagedPath).toBeDefined(); + + await mkdir(destination); + await writeFile(join(destination, "unrelated"), "preserved"); + const result = await saving; + expect(result.ok).toBe(false); + if (result.ok) throw new Error("Expected the concurrent destination to make saving fail"); + expect(result.error).toBeInstanceOf(Error); + expect(await readdir(destination)).toEqual(["unrelated"]); + expect((await readdir(root)).filter((entry) => entry.startsWith(temporaryPrefix))).toEqual([]); + }); +}); + +describe("bundle layout", () => { + test("rejects a missing root", async () => { + const root = await createTemporaryRoot(); + await expect(loadHarnessState(join(root, "missing"))).rejects.toThrow(); + }); + + test("rejects a non-directory root", async () => { + const root = await createTemporaryRoot(); + const bundlePath = join(root, "state"); + await writeFile(bundlePath, "not a directory"); + await expect(loadHarnessState(bundlePath)).rejects.toThrow(/direct directory/i); + }); + + test("rejects a linked root", async () => { + const root = await createTemporaryRoot(); + const target = await createBundle(root, "target"); + const bundlePath = join(root, "linked-state"); + await symlink(target, bundlePath, process.platform === "win32" ? "junction" : "dir"); + await expect(loadHarnessState(bundlePath)).rejects.toThrow(/direct directory/i); + }); + + test.each(["manifest.json", "EVOLVE.md"] as const)("rejects a missing %s entry", async (missingName) => { + const root = await createTemporaryRoot(); + const bundlePath = await createBundle(root); + await rm(join(bundlePath, missingName)); + await expect(loadHarnessState(bundlePath)).rejects.toThrow(/exactly manifest\.json and EVOLVE\.md/i); + }); + + test("rejects additional entries", async () => { + const root = await createTemporaryRoot(); + const bundlePath = await createBundle(root); + await writeFile(join(bundlePath, "extra.txt"), "extra"); + await expect(loadHarnessState(bundlePath)).rejects.toThrow(/exactly manifest\.json and EVOLVE\.md/i); + }); + + test("rejects mis-cased entries", async () => { + const root = await createTemporaryRoot(); + const bundlePath = join(root, "state"); + await mkdir(bundlePath); + await writeFile(join(bundlePath, "MANIFEST.json"), MANIFEST); + await writeFile(join(bundlePath, "EVOLVE.md"), "Valid."); + await expect(loadHarnessState(bundlePath)).rejects.toThrow(/exactly manifest\.json and EVOLVE\.md/i); + }); + + test.each(["manifest.json", "EVOLVE.md"] as const)("rejects a linked %s entry", async (linkedName) => { + const root = await createTemporaryRoot(); + const bundlePath = await createBundle(root); + await rm(join(bundlePath, linkedName)); + const linkTarget = join(root, `${linkedName}-target`); + await mkdir(linkTarget); + await symlink(linkTarget, join(bundlePath, linkedName), process.platform === "win32" ? "junction" : "dir"); + await expect(loadHarnessState(bundlePath)).rejects.toThrow(/direct regular file/i); + }); + + test.each(["manifest.json", "EVOLVE.md"] as const)("rejects a non-regular %s entry", async (entryName) => { + const root = await createTemporaryRoot(); + const bundlePath = await createBundle(root); + await rm(join(bundlePath, entryName)); + await mkdir(join(bundlePath, entryName)); + await expect(loadHarnessState(bundlePath)).rejects.toThrow(/direct regular file/i); + }); +}); + +describe("bundle data validation", () => { + test.each(["manifest.json", "EVOLVE.md"] as const)("rejects malformed UTF-8 in %s", async (entryName) => { + const root = await createTemporaryRoot(); + const bundlePath = await createBundle(root); + await writeFile(join(bundlePath, entryName), new Uint8Array([0xc3, 0x28])); + await expect(loadHarnessState(bundlePath)).rejects.toThrow(new RegExp(entryName.replace(".", "\\."), "i")); + }); + + test.each(["manifest.json", "EVOLVE.md"] as const)("rejects a UTF-8 BOM in %s", async (entryName) => { + const root = await createTemporaryRoot(); + const bundlePath = await createBundle(root); + const validContent = entryName === "manifest.json" ? MANIFEST : "Valid prompt."; + await writeFile( + join(bundlePath, entryName), + Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(validContent)]), + ); + await expect(loadHarnessState(bundlePath)).rejects.toThrow(/BOM/i); + }); + + test("wraps malformed manifest JSON with its path and syntax error cause", async () => { + const root = await createTemporaryRoot(); + const bundlePath = await createBundle(root, "state", "{not-json}\n"); + const manifestPath = join(bundlePath, "manifest.json"); + let thrown: unknown; + try { + await loadHarnessState(bundlePath); + } catch (error) { + thrown = error; + } + expect(thrown).toBeInstanceOf(Error); + expect((thrown as Error).message).toContain(manifestPath); + expect((thrown as Error).cause).toBeInstanceOf(SyntaxError); + }); + + test.each([ + ["unsupported version", '{"schemaVersion":2}\n'], + ["unknown field", '{"schemaVersion":1,"extra":true}\n'], + ["missing version", "{}\n"], + ["non-object", "[]\n"], + ] as const)("rejects a manifest with %s", async (_label, manifest) => { + const root = await createTemporaryRoot(); + const bundlePath = await createBundle(root, "state", manifest); + await expect(loadHarnessState(bundlePath)).rejects.toThrow(/manifest/i); + }); + + test.each([ + ["blank", ""], + ["whitespace-only", " \t\r\n"], + ["leading U+FEFF", "\uFEFFPrompt."], + ] as const)("rejects a %s prompt", async (_label, prompt) => { + const root = await createTemporaryRoot(); + const bundlePath = await createBundle(root, "state", MANIFEST, prompt); + await expect(loadHarnessState(bundlePath)).rejects.toThrow(/EVOLVE\.md/i); + }); +}); diff --git a/packages/harness-state/tsconfig.build.json b/packages/harness-state/tsconfig.build.json new file mode 100644 index 000000000..695dd9ad2 --- /dev/null +++ b/packages/harness-state/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.d.ts", "src/**/*.d.ts"] +} diff --git a/packages/harness-state/tsconfig.test.json b/packages/harness-state/tsconfig.test.json new file mode 100644 index 000000000..e1276ec0b --- /dev/null +++ b/packages/harness-state/tsconfig.test.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": ["node", "vitest"] + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/packages/harness-state/vitest.config.ts b/packages/harness-state/vitest.config.ts new file mode 100644 index 000000000..157f467a0 --- /dev/null +++ b/packages/harness-state/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + reporters: process.env.GITHUB_ACTIONS ? ["dot", "github-actions"] : ["dot"], + }, +}); diff --git a/tsconfig.json b/tsconfig.json index 13b2a0956..e0593e724 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "moduleResolution": "NodeNext", "paths": { "*": ["./*"], + "@diffexai/diffex-harness-state": ["./packages/harness-state/src/index.ts"], "@earendil-works/diffex-telemetry": ["./packages/telemetry/src/index.ts"], "@earendil-works/diffex-telemetry/testing": ["./packages/telemetry/src/testing/index.ts"], "@earendil-works/diffex-ai": ["./packages/ai/src/index.ts"], From 83b572fb63ed8f5dbe5efeca1374f1b56fd2505e Mon Sep 17 00:00:00 2001 From: issacboy123 Date: Sat, 15 Aug 2026 15:07:00 -0700 Subject: [PATCH 2/6] fix: tighten harness state package contract --- packages/harness-state/README.md | 6 +++--- packages/harness-state/src/index.ts | 6 +----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/harness-state/README.md b/packages/harness-state/README.md index 6ed483748..17f23de6e 100644 --- a/packages/harness-state/README.md +++ b/packages/harness-state/README.md @@ -13,11 +13,11 @@ A version 1 logical state contains only the evolvable static system prompt: } ``` -The prompt must not start with U+FEFF and must contain a non-whitespace character. A portable bundle is a directory with exactly these direct regular files and exact casing: +The prompt must not start with U+FEFF and must contain a non-whitespace character. A portable bundle root is a direct directory, not a symbolic link, with exactly these direct regular files and exact casing: ```text state-directory/ -|-- manifest.json # exact UTF-8 bytes for {"schemaVersion":1} plus one LF +|-- manifest.json # canonical saved form is {"schemaVersion":1} plus one LF `-- EVOLVE.md # systemPrompt UTF-8 bytes, preserved verbatim ``` @@ -27,7 +27,7 @@ Both files must be fatal, BOM-free UTF-8. The manifest must contain only `schema - `HarnessStateSchema` and `HarnessStateManifestSchema` are strict TypeBox schemas. Their inferred `HarnessState` and `HarnessStateManifest` types expose readonly fields. - `loadHarnessState(bundlePath)` validates a bundle and returns its flat logical state without normalizing the prompt. -- `saveHarnessState(state, destinationPath)` validates the state, stages a canonical two-file bundle beside the destination, and renames it into place. The parent must already be a directory, and any existing destination entry is rejected. +- `saveHarnessState(state, destinationPath)` validates the state, stages a canonical two-file bundle beside the destination, and renames it into place. The parent must already be a directory, any existing destination entry is rejected, and a failed save attempts to remove its owned staging directory. - `loadDefaultHarnessState()` loads the packaged clean-default bundle through `loadHarnessState()`. Invalid state, layout, encoding, manifest, or prompt data rejects with an ordinary descriptive `Error`. JSON syntax failures retain the original syntax error as their cause. Other filesystem errors propagate unchanged. diff --git a/packages/harness-state/src/index.ts b/packages/harness-state/src/index.ts index 513c59fe2..b62d78f2e 100644 --- a/packages/harness-state/src/index.ts +++ b/packages/harness-state/src/index.ts @@ -85,15 +85,11 @@ export async function loadHarnessState(bundlePath: string): Promise { try { await lstat(destinationPath); } catch (error) { - if (isMissingEntryError(error)) return; + if (error instanceof Error && "code" in error && error.code === "ENOENT") return; throw error; } throw new Error(`Harness state destination "${destinationPath}" already exists`); From e3e661ab30b3bd7e1a5dacd38fe1773cdf8ca8a0 Mon Sep 17 00:00:00 2001 From: issacboy123 Date: Sat, 15 Aug 2026 16:48:06 -0700 Subject: [PATCH 3/6] feat: identify harness state bundles by content hash --- packages/harness-state/README.md | 17 ++-- .../harness-state/src/default/manifest.json | 2 +- packages/harness-state/src/index.ts | 35 +++---- .../harness-state/test/harness-state.test.ts | 94 ++++++++++--------- 4 files changed, 78 insertions(+), 70 deletions(-) diff --git a/packages/harness-state/README.md b/packages/harness-state/README.md index 17f23de6e..077057e39 100644 --- a/packages/harness-state/README.md +++ b/packages/harness-state/README.md @@ -1,14 +1,13 @@ # `@diffexai/diffex-harness-state` -Private workspace package for the version 1 Diffex harness-state schema and directory-bundle persistence. +Private workspace package for the Diffex harness-state schema and directory-bundle persistence. ## State and bundle format -A version 1 logical state contains only the evolvable static system prompt: +A logical state contains only the evolvable static system prompt: ```ts { - schemaVersion: 1; systemPrompt: string; } ``` @@ -17,21 +16,21 @@ The prompt must not start with U+FEFF and must contain a non-whitespace characte ```text state-directory/ -|-- manifest.json # canonical saved form is {"schemaVersion":1} plus one LF +|-- manifest.json # SHA-256 metadata for the exact EVOLVE.md bytes `-- EVOLVE.md # systemPrompt UTF-8 bytes, preserved verbatim ``` -Both files must be fatal, BOM-free UTF-8. The manifest must contain only `schemaVersion: 1`. +Both files must be fatal, BOM-free UTF-8. The manifest must contain only `contentHash`, formatted as `sha256:` followed by 64 lowercase hexadecimal characters. Loading verifies that it matches the exact `EVOLVE.md` bytes, including line endings and trailing whitespace. ## API - `HarnessStateSchema` and `HarnessStateManifestSchema` are strict TypeBox schemas. Their inferred `HarnessState` and `HarnessStateManifest` types expose readonly fields. -- `loadHarnessState(bundlePath)` validates a bundle and returns its flat logical state without normalizing the prompt. -- `saveHarnessState(state, destinationPath)` validates the state, stages a canonical two-file bundle beside the destination, and renames it into place. The parent must already be a directory, any existing destination entry is rejected, and a failed save attempts to remove its owned staging directory. +- `loadHarnessState(bundlePath)` validates a bundle, verifies its content hash, and returns its flat logical state without normalizing the prompt. +- `saveHarnessState(state, destinationPath)` validates the state, derives its content hash, stages a canonical two-file bundle beside the destination, and renames it into place. The parent must already be a directory, any existing destination entry is rejected, and a failed save attempts to remove its owned staging directory. - `loadDefaultHarnessState()` loads the packaged clean-default bundle through `loadHarnessState()`. Invalid state, layout, encoding, manifest, or prompt data rejects with an ordinary descriptive `Error`. JSON syntax failures retain the original syntax error as their cause. Other filesystem errors propagate unchanged. -## Version 1 limitations +## Limitations -Version 1 stores only the static system prompt. It has no migration or compatibility behavior and does not represent tools, models, context, sessions, lineage, provenance, or evolution metadata. Persistence does not provide cross-process coordination, retries, or durability guarantees. +The state stores only the static system prompt. It has no migration or compatibility behavior and does not represent tools, models, context, sessions, lineage, provenance, or evolution metadata beyond the derived content hash. Persistence does not provide cross-process coordination, retries, or durability guarantees. diff --git a/packages/harness-state/src/default/manifest.json b/packages/harness-state/src/default/manifest.json index 156e9b317..4e6f9f90e 100644 --- a/packages/harness-state/src/default/manifest.json +++ b/packages/harness-state/src/default/manifest.json @@ -1 +1 @@ -{"schemaVersion":1} +{"contentHash":"sha256:86f539ee34b13502d336a4973223597516ac3079794ee2505565b00008ecc82f"} diff --git a/packages/harness-state/src/index.ts b/packages/harness-state/src/index.ts index b62d78f2e..5c7befb05 100644 --- a/packages/harness-state/src/index.ts +++ b/packages/harness-state/src/index.ts @@ -1,25 +1,18 @@ +import { createHash } from "node:crypto"; import { lstat, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises"; import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { TextDecoder } from "node:util"; +import { TextDecoder, TextEncoder } from "node:util"; import Type, { type Static } from "typebox"; import { Check } from "typebox/value"; -export const HARNESS_STATE_SCHEMA_VERSION = 1 as const; - const SystemPromptSchema = Type.Readonly(Type.String({ pattern: "^(?!\\uFEFF)(?=[\\s\\S]*\\S)[\\s\\S]*$" })); -export const HarnessStateSchema = Type.Object( - { - schemaVersion: Type.Readonly(Type.Literal(HARNESS_STATE_SCHEMA_VERSION)), - systemPrompt: SystemPromptSchema, - }, - { additionalProperties: false }, -); +export const HarnessStateSchema = Type.Object({ systemPrompt: SystemPromptSchema }, { additionalProperties: false }); export type HarnessState = Static; export const HarnessStateManifestSchema = Type.Object( - { schemaVersion: Type.Readonly(Type.Literal(HARNESS_STATE_SCHEMA_VERSION)) }, + { contentHash: Type.Readonly(Type.String({ pattern: "^sha256:[0-9a-f]{64}$" })) }, { additionalProperties: false }, ); export type HarnessStateManifest = Static; @@ -27,8 +20,12 @@ export type HarnessStateManifest = Static; const MANIFEST_NAME = "manifest.json"; const PROMPT_NAME = "EVOLVE.md"; const BUNDLE_ENTRY_NAMES = [MANIFEST_NAME, PROMPT_NAME] as const; -const CANONICAL_MANIFEST = '{"schemaVersion":1}\n'; const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); +const UTF8_ENCODER = new TextEncoder(); + +function computeContentHash(bytes: Uint8Array): string { + return `sha256:${createHash("sha256").update(bytes).digest("hex")}`; +} function decodeBundleFile(bytes: Uint8Array, filePath: string): string { let value: string; @@ -79,9 +76,13 @@ export async function loadHarnessState(bundlePath: string): Promise { export async function saveHarnessState(state: unknown, destinationPath: string): Promise { if (!Check(HarnessStateSchema, state)) throw new Error("Invalid harness state to save"); const systemPrompt = state.systemPrompt; + const promptBytes = UTF8_ENCODER.encode(systemPrompt); + const manifest = `${JSON.stringify({ contentHash: computeContentHash(promptBytes) })}\n`; const absoluteDestinationPath = resolve(destinationPath); const parentPath = dirname(absoluteDestinationPath); const parentStats = await stat(parentPath); @@ -109,8 +112,8 @@ export async function saveHarnessState(state: unknown, destinationPath: string): let temporaryPath: string | undefined; try { temporaryPath = await mkdtemp(join(parentPath, `.${basename(absoluteDestinationPath)}.harness-state.tmp-`)); - await writeFile(join(temporaryPath, MANIFEST_NAME), CANONICAL_MANIFEST, "utf8"); - await writeFile(join(temporaryPath, PROMPT_NAME), systemPrompt, "utf8"); + await writeFile(join(temporaryPath, MANIFEST_NAME), manifest, "utf8"); + await writeFile(join(temporaryPath, PROMPT_NAME), promptBytes); await assertDestinationAbsent(absoluteDestinationPath); await rename(temporaryPath, absoluteDestinationPath); temporaryPath = undefined; diff --git a/packages/harness-state/test/harness-state.test.ts b/packages/harness-state/test/harness-state.test.ts index f4cd84860..b67828c34 100644 --- a/packages/harness-state/test/harness-state.test.ts +++ b/packages/harness-state/test/harness-state.test.ts @@ -1,10 +1,10 @@ +import { createHash } from "node:crypto"; import { lstat, mkdir, mkdtemp, readdir, readFile, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { basename, join } from "node:path"; import { Check } from "typebox/value"; import { afterEach, describe, expect, test } from "vitest"; import { - HARNESS_STATE_SCHEMA_VERSION, type HarnessState, type HarnessStateManifest, HarnessStateManifestSchema, @@ -16,9 +16,13 @@ import { const DEFAULT_PROMPT = "You are an expert coding assistant operating inside Diffex, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nGuidelines:\n- Be concise in your responses\n- Show file paths clearly when working with files\n"; -const MANIFEST = '{"schemaVersion":1}\n'; const temporaryRoots: string[] = []; +function createManifest(prompt: string): string { + const digest = createHash("sha256").update(prompt, "utf8").digest("hex"); + return `{"contentHash":"sha256:${digest}"}\n`; +} + afterEach(async () => { await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); }); @@ -32,7 +36,7 @@ async function createTemporaryRoot(): Promise { async function createBundle( root: string, name = "state", - manifest: string | Uint8Array = MANIFEST, + manifest: string | Uint8Array = createManifest("Act carefully.\n"), prompt: string | Uint8Array = "Act carefully.\n", ): Promise { const bundlePath = join(root, name); @@ -56,33 +60,40 @@ function assertReadonlyTypes(state: HarnessState, manifest: HarnessStateManifest // @ts-expect-error HarnessState fields are readonly. state.systemPrompt = "changed"; // @ts-expect-error HarnessStateManifest fields are readonly. - manifest.schemaVersion = 1; + manifest.contentHash = "sha256:changed"; } void assertReadonlyTypes; describe("schemas", () => { - test("exports strict version 1 schemas with readonly inferred fields", () => { - expect(HARNESS_STATE_SCHEMA_VERSION).toBe(1); - expect(Check(HarnessStateSchema, { schemaVersion: 1, systemPrompt: "Work carefully." })).toBe(true); - expect(Check(HarnessStateManifestSchema, { schemaVersion: 1 })).toBe(true); - expect(Check(HarnessStateSchema, { schemaVersion: 1, systemPrompt: "Work.", extra: true })).toBe(false); - expect(Check(HarnessStateManifestSchema, { schemaVersion: 1, extra: true })).toBe(false); + test("exports strict schemas with readonly inferred fields", () => { + expect(Check(HarnessStateSchema, { systemPrompt: "Work carefully." })).toBe(true); + expect( + Check(HarnessStateManifestSchema, { + contentHash: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + }), + ).toBe(true); + expect(Check(HarnessStateSchema, { systemPrompt: "Work.", extra: true })).toBe(false); + expect( + Check(HarnessStateManifestSchema, { + contentHash: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + extra: true, + }), + ).toBe(false); }); test.each([ - ["wrong state version", { schemaVersion: 2, systemPrompt: "Work." }], - ["string state version", { schemaVersion: "1", systemPrompt: "Work." }], - ["blank prompt", { schemaVersion: 1, systemPrompt: "" }], - ["whitespace-only prompt", { schemaVersion: 1, systemPrompt: " \t\r\n" }], - ["leading U+FEFF prompt", { schemaVersion: 1, systemPrompt: "\uFEFFWork." }], + ["blank prompt", { systemPrompt: "" }], + ["whitespace-only prompt", { systemPrompt: " \t\r\n" }], + ["leading U+FEFF prompt", { systemPrompt: "\uFEFFWork." }], ] as const)("rejects %s", (_label, state) => { expect(Check(HarnessStateSchema, state)).toBe(false); }); test.each([ - ["wrong manifest version", { schemaVersion: 2 }], - ["string manifest version", { schemaVersion: "1" }], - ["missing manifest version", {}], + ["wrong algorithm", { contentHash: "sha1:0123456789abcdef0123456789abcdef01234567" }], + ["short digest", { contentHash: "sha256:0123456789abcdef" }], + ["uppercase digest", { contentHash: "sha256:0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" }], + ["missing content hash", {}], ] as const)("rejects %s", (_label, manifest) => { expect(Check(HarnessStateManifestSchema, manifest)).toBe(false); }); @@ -91,7 +102,7 @@ describe("schemas", () => { describe("default bundle", () => { test("loads the exact clean default through normal validation", async () => { const state = await loadDefaultHarnessState(); - expect(state).toEqual({ schemaVersion: 1, systemPrompt: DEFAULT_PROMPT }); + expect(state).toEqual({ systemPrompt: DEFAULT_PROMPT }); expect(Object.isFrozen(state)).toBe(false); }); }); @@ -106,19 +117,17 @@ describe("saving and round trips", () => { ] as const)("preserves %s prompt content verbatim", async (_label, systemPrompt) => { const root = await createTemporaryRoot(); const destination = join(root, "saved-state"); - await saveHarnessState({ schemaVersion: 1, systemPrompt }, destination); + await saveHarnessState({ systemPrompt }, destination); - expect(await readFile(join(destination, "manifest.json"), "utf8")).toBe(MANIFEST); + expect(await readFile(join(destination, "manifest.json"), "utf8")).toBe(createManifest(systemPrompt)); expect(await readFile(join(destination, "EVOLVE.md"), "utf8")).toBe(systemPrompt); - expect(await loadHarnessState(destination)).toEqual({ schemaVersion: 1, systemPrompt }); + expect(await loadHarnessState(destination)).toEqual({ systemPrompt }); }); test("validates state before filesystem access", async () => { const root = await createTemporaryRoot(); const destination = join(root, "missing-parent", "state"); - await expect(saveHarnessState({ schemaVersion: 1, systemPrompt: " " }, destination)).rejects.toThrow( - /invalid harness state/i, - ); + await expect(saveHarnessState({ systemPrompt: " " }, destination)).rejects.toThrow(/invalid harness state/i); expect(await pathExists(destination)).toBe(false); }); @@ -126,9 +135,7 @@ describe("saving and round trips", () => { const root = await createTemporaryRoot(); const parentFile = join(root, "parent-file"); await writeFile(parentFile, "not a directory"); - await expect( - saveHarnessState({ schemaVersion: 1, systemPrompt: "Valid." }, join(parentFile, "state")), - ).rejects.toThrow(); + await expect(saveHarnessState({ systemPrompt: "Valid." }, join(parentFile, "state"))).rejects.toThrow(); }); test.each(["file", "directory"] as const)("refuses to overwrite an existing %s destination", async (kind) => { @@ -137,9 +144,7 @@ describe("saving and round trips", () => { if (kind === "file") await writeFile(destination, "original"); else await mkdir(destination); - await expect(saveHarnessState({ schemaVersion: 1, systemPrompt: "Replacement." }, destination)).rejects.toThrow( - /already exists/i, - ); + await expect(saveHarnessState({ systemPrompt: "Replacement." }, destination)).rejects.toThrow(/already exists/i); if (kind === "file") expect(await readFile(destination, "utf8")).toBe("original"); else expect(await readdir(destination)).toEqual([]); }); @@ -148,19 +153,14 @@ describe("saving and round trips", () => { const root = await createTemporaryRoot(); const destination = join(root, "state"); await symlink(join(root, "missing-target"), destination, process.platform === "win32" ? "junction" : "dir"); - await expect(saveHarnessState({ schemaVersion: 1, systemPrompt: "Replacement." }, destination)).rejects.toThrow( - /already exists/i, - ); + await expect(saveHarnessState({ systemPrompt: "Replacement." }, destination)).rejects.toThrow(/already exists/i); }); test("cleans its staged directory after a late failure without exposing a partial bundle", async () => { const root = await createTemporaryRoot(); const destination = join(root, "state"); const temporaryPrefix = `.${basename(destination)}.harness-state.tmp-`; - const saving = saveHarnessState( - { schemaVersion: 1, systemPrompt: "x".repeat(16 * 1024 * 1024) }, - destination, - ).then( + const saving = saveHarnessState({ systemPrompt: "x".repeat(16 * 1024 * 1024) }, destination).then( () => ({ ok: true as const }), (error: unknown) => ({ ok: false as const, error }), ); @@ -226,7 +226,7 @@ describe("bundle layout", () => { const root = await createTemporaryRoot(); const bundlePath = join(root, "state"); await mkdir(bundlePath); - await writeFile(join(bundlePath, "MANIFEST.json"), MANIFEST); + await writeFile(join(bundlePath, "MANIFEST.json"), createManifest("Valid.")); await writeFile(join(bundlePath, "EVOLVE.md"), "Valid."); await expect(loadHarnessState(bundlePath)).rejects.toThrow(/exactly manifest\.json and EVOLVE\.md/i); }); @@ -261,7 +261,7 @@ describe("bundle data validation", () => { test.each(["manifest.json", "EVOLVE.md"] as const)("rejects a UTF-8 BOM in %s", async (entryName) => { const root = await createTemporaryRoot(); const bundlePath = await createBundle(root); - const validContent = entryName === "manifest.json" ? MANIFEST : "Valid prompt."; + const validContent = entryName === "manifest.json" ? createManifest("Act carefully.\n") : "Valid prompt."; await writeFile( join(bundlePath, entryName), Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(validContent)]), @@ -285,9 +285,9 @@ describe("bundle data validation", () => { }); test.each([ - ["unsupported version", '{"schemaVersion":2}\n'], - ["unknown field", '{"schemaVersion":1,"extra":true}\n'], - ["missing version", "{}\n"], + ["wrong algorithm", '{"contentHash":"sha1:0123456789abcdef0123456789abcdef01234567"}\n'], + ["unknown field", `${createManifest("Act carefully.\n").trimEnd().slice(0, -1)},"extra":true}\n`], + ["missing content hash", "{}\n"], ["non-object", "[]\n"], ] as const)("rejects a manifest with %s", async (_label, manifest) => { const root = await createTemporaryRoot(); @@ -301,7 +301,13 @@ describe("bundle data validation", () => { ["leading U+FEFF", "\uFEFFPrompt."], ] as const)("rejects a %s prompt", async (_label, prompt) => { const root = await createTemporaryRoot(); - const bundlePath = await createBundle(root, "state", MANIFEST, prompt); + const bundlePath = await createBundle(root, "state", createManifest(String(prompt)), prompt); await expect(loadHarnessState(bundlePath)).rejects.toThrow(/EVOLVE\.md/i); }); + + test("rejects prompt content that does not match the manifest hash", async () => { + const root = await createTemporaryRoot(); + const bundlePath = await createBundle(root, "state", createManifest("Original."), "Changed."); + await expect(loadHarnessState(bundlePath)).rejects.toThrow(/content hash mismatch/i); + }); }); From e60ef021964e8fcd08e6e1d9587851a889d72cc9 Mon Sep 17 00:00:00 2001 From: issacboy123 Date: Sun, 16 Aug 2026 04:44:32 -0700 Subject: [PATCH 4/6] feat(coding-agent): remove default harness state bundle --- packages/harness-state/README.md | 3 ++- packages/harness-state/package.json | 2 +- packages/harness-state/src/default/EVOLVE.md | 5 ----- packages/harness-state/src/default/manifest.json | 1 - packages/harness-state/src/index.ts | 5 ----- packages/harness-state/test/harness-state.test.ts | 11 ----------- 6 files changed, 3 insertions(+), 24 deletions(-) delete mode 100644 packages/harness-state/src/default/EVOLVE.md delete mode 100644 packages/harness-state/src/default/manifest.json diff --git a/packages/harness-state/README.md b/packages/harness-state/README.md index 077057e39..b8bae496d 100644 --- a/packages/harness-state/README.md +++ b/packages/harness-state/README.md @@ -27,7 +27,8 @@ Both files must be fatal, BOM-free UTF-8. The manifest must contain only `conten - `HarnessStateSchema` and `HarnessStateManifestSchema` are strict TypeBox schemas. Their inferred `HarnessState` and `HarnessStateManifest` types expose readonly fields. - `loadHarnessState(bundlePath)` validates a bundle, verifies its content hash, and returns its flat logical state without normalizing the prompt. - `saveHarnessState(state, destinationPath)` validates the state, derives its content hash, stages a canonical two-file bundle beside the destination, and renames it into place. The parent must already be a directory, any existing destination entry is rejected, and a failed save attempts to remove its owned staging directory. -- `loadDefaultHarnessState()` loads the packaged clean-default bundle through `loadHarnessState()`. + +The package does not include or select a default harness. Callers must provide an explicit bundle path or state. Invalid state, layout, encoding, manifest, or prompt data rejects with an ordinary descriptive `Error`. JSON syntax failures retain the original syntax error as their cause. Other filesystem errors propagate unchanged. diff --git a/packages/harness-state/package.json b/packages/harness-state/package.json index 24a8af8f1..8fd03034e 100644 --- a/packages/harness-state/package.json +++ b/packages/harness-state/package.json @@ -18,7 +18,7 @@ ], "scripts": { "clean": "shx rm -rf dist", - "build": "tsgo -p tsconfig.build.json && shx rm -rf dist/default && shx cp -r src/default dist/default", + "build": "tsgo -p tsconfig.build.json && shx rm -rf dist/default", "test": "vitest --run" }, "engines": { diff --git a/packages/harness-state/src/default/EVOLVE.md b/packages/harness-state/src/default/EVOLVE.md deleted file mode 100644 index c3eab8757..000000000 --- a/packages/harness-state/src/default/EVOLVE.md +++ /dev/null @@ -1,5 +0,0 @@ -You are an expert coding assistant operating inside Diffex, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files. - -Guidelines: -- Be concise in your responses -- Show file paths clearly when working with files diff --git a/packages/harness-state/src/default/manifest.json b/packages/harness-state/src/default/manifest.json deleted file mode 100644 index 4e6f9f90e..000000000 --- a/packages/harness-state/src/default/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"contentHash":"sha256:86f539ee34b13502d336a4973223597516ac3079794ee2505565b00008ecc82f"} diff --git a/packages/harness-state/src/index.ts b/packages/harness-state/src/index.ts index 5c7befb05..fe9ec408b 100644 --- a/packages/harness-state/src/index.ts +++ b/packages/harness-state/src/index.ts @@ -1,7 +1,6 @@ import { createHash } from "node:crypto"; import { lstat, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises"; import { basename, dirname, join, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; import { TextDecoder, TextEncoder } from "node:util"; import Type, { type Static } from "typebox"; import { Check } from "typebox/value"; @@ -126,7 +125,3 @@ export async function saveHarnessState(state: unknown, destinationPath: string): throw error; } } - -export async function loadDefaultHarnessState(): Promise { - return loadHarnessState(fileURLToPath(new URL("./default", import.meta.url))); -} diff --git a/packages/harness-state/test/harness-state.test.ts b/packages/harness-state/test/harness-state.test.ts index b67828c34..375d8e17c 100644 --- a/packages/harness-state/test/harness-state.test.ts +++ b/packages/harness-state/test/harness-state.test.ts @@ -9,13 +9,10 @@ import { type HarnessStateManifest, HarnessStateManifestSchema, HarnessStateSchema, - loadDefaultHarnessState, loadHarnessState, saveHarnessState, } from "../src/index.ts"; -const DEFAULT_PROMPT = - "You are an expert coding assistant operating inside Diffex, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nGuidelines:\n- Be concise in your responses\n- Show file paths clearly when working with files\n"; const temporaryRoots: string[] = []; function createManifest(prompt: string): string { @@ -99,14 +96,6 @@ describe("schemas", () => { }); }); -describe("default bundle", () => { - test("loads the exact clean default through normal validation", async () => { - const state = await loadDefaultHarnessState(); - expect(state).toEqual({ systemPrompt: DEFAULT_PROMPT }); - expect(Object.isFrozen(state)).toBe(false); - }); -}); - describe("saving and round trips", () => { test.each([ ["Unicode", "Inspect caf\u00e9, \u6c34, and \u{10151}."], From d015509a7c3f851fe0d06f36c7f0824a0b418b63 Mon Sep 17 00:00:00 2001 From: issacboy123 Date: Sun, 16 Aug 2026 05:08:52 -0700 Subject: [PATCH 5/6] feat(coding-agent): track harness state lineage --- packages/harness-state/README.md | 12 +-- packages/harness-state/src/index.ts | 36 +++++-- .../harness-state/test/harness-state.test.ts | 101 ++++++++++++++---- 3 files changed, 116 insertions(+), 33 deletions(-) diff --git a/packages/harness-state/README.md b/packages/harness-state/README.md index b8bae496d..5808b7de1 100644 --- a/packages/harness-state/README.md +++ b/packages/harness-state/README.md @@ -16,17 +16,17 @@ The prompt must not start with U+FEFF and must contain a non-whitespace characte ```text state-directory/ -|-- manifest.json # SHA-256 metadata for the exact EVOLVE.md bytes +|-- manifest.json # SHA-256 identity and base-version metadata `-- EVOLVE.md # systemPrompt UTF-8 bytes, preserved verbatim ``` -Both files must be fatal, BOM-free UTF-8. The manifest must contain only `contentHash`, formatted as `sha256:` followed by 64 lowercase hexadecimal characters. Loading verifies that it matches the exact `EVOLVE.md` bytes, including line endings and trailing whitespace. +Both files must be fatal, BOM-free UTF-8. The manifest must contain only `contentHash` and `base`, each formatted as `sha256:` followed by 64 lowercase hexadecimal characters. Loading verifies that `contentHash` matches the exact `EVOLVE.md` bytes, including line endings and trailing whitespace. `base` identifies the predecessor's `contentHash`; a root harness conventionally uses its own `contentHash` as `base`. ## API -- `HarnessStateSchema` and `HarnessStateManifestSchema` are strict TypeBox schemas. Their inferred `HarnessState` and `HarnessStateManifest` types expose readonly fields. -- `loadHarnessState(bundlePath)` validates a bundle, verifies its content hash, and returns its flat logical state without normalizing the prompt. -- `saveHarnessState(state, destinationPath)` validates the state, derives its content hash, stages a canonical two-file bundle beside the destination, and renames it into place. The parent must already be a directory, any existing destination entry is rejected, and a failed save attempts to remove its owned staging directory. +- `HarnessStateSchema`, `HarnessStateManifestSchema`, and `HarnessStateBundleSchema` are strict TypeBox schemas. Their inferred types expose readonly fields. +- `loadHarnessState(bundlePath)` validates a bundle, verifies its content hash, and returns `{ state, manifest }` without normalizing the prompt. +- `saveHarnessState(state, destinationPath, options?)` derives the content hash, stages a canonical two-file bundle beside the destination, and returns `{ state, manifest }`. Passing `options.base` identifies the predecessor; omitting it creates a self-based root. The parent must already be a directory, any existing destination entry is rejected, and a failed save attempts to remove its owned staging directory. The package does not include or select a default harness. Callers must provide an explicit bundle path or state. @@ -34,4 +34,4 @@ Invalid state, layout, encoding, manifest, or prompt data rejects with an ordina ## Limitations -The state stores only the static system prompt. It has no migration or compatibility behavior and does not represent tools, models, context, sessions, lineage, provenance, or evolution metadata beyond the derived content hash. Persistence does not provide cross-process coordination, retries, or durability guarantees. +The state stores only the static system prompt. The manifest records one base hash but does not resolve predecessors, verify their existence, detect cycles, or otherwise manage lineage. The package has no migration or compatibility behavior and does not represent tools, models, context, sessions, or provenance. Persistence does not provide cross-process coordination, retries, or durability guarantees. diff --git a/packages/harness-state/src/index.ts b/packages/harness-state/src/index.ts index fe9ec408b..69ed7150a 100644 --- a/packages/harness-state/src/index.ts +++ b/packages/harness-state/src/index.ts @@ -6,16 +6,32 @@ import Type, { type Static } from "typebox"; import { Check } from "typebox/value"; const SystemPromptSchema = Type.Readonly(Type.String({ pattern: "^(?!\\uFEFF)(?=[\\s\\S]*\\S)[\\s\\S]*$" })); +const ContentHashSchema = Type.Readonly(Type.String({ pattern: "^sha256:[0-9a-f]{64}$" })); export const HarnessStateSchema = Type.Object({ systemPrompt: SystemPromptSchema }, { additionalProperties: false }); export type HarnessState = Static; +const HarnessStateSaveOptionsSchema = Type.Object( + { base: Type.Optional(ContentHashSchema) }, + { additionalProperties: false }, +); +export type HarnessStateSaveOptions = Static; + export const HarnessStateManifestSchema = Type.Object( - { contentHash: Type.Readonly(Type.String({ pattern: "^sha256:[0-9a-f]{64}$" })) }, + { contentHash: ContentHashSchema, base: ContentHashSchema }, { additionalProperties: false }, ); export type HarnessStateManifest = Static; +export const HarnessStateBundleSchema = Type.Object( + { + state: Type.Readonly(HarnessStateSchema), + manifest: Type.Readonly(HarnessStateManifestSchema), + }, + { additionalProperties: false }, +); +export type HarnessStateBundle = Static; + const MANIFEST_NAME = "manifest.json"; const PROMPT_NAME = "EVOLVE.md"; const BUNDLE_ENTRY_NAMES = [MANIFEST_NAME, PROMPT_NAME] as const; @@ -69,7 +85,7 @@ async function inspectBundleDirectory(bundlePath: string): Promise { } } -export async function loadHarnessState(bundlePath: string): Promise { +export async function loadHarnessState(bundlePath: string): Promise { await inspectBundleDirectory(bundlePath); const manifestPath = join(bundlePath, MANIFEST_NAME); const promptPath = join(bundlePath, PROMPT_NAME); @@ -82,7 +98,7 @@ export async function loadHarnessState(bundlePath: string): Promise { @@ -95,11 +111,18 @@ async function assertDestinationAbsent(destinationPath: string): Promise { throw new Error(`Harness state destination "${destinationPath}" already exists`); } -export async function saveHarnessState(state: unknown, destinationPath: string): Promise { +export async function saveHarnessState( + state: unknown, + destinationPath: string, + options: HarnessStateSaveOptions = {}, +): Promise { if (!Check(HarnessStateSchema, state)) throw new Error("Invalid harness state to save"); + if (!Check(HarnessStateSaveOptionsSchema, options)) throw new Error("Invalid harness state save options"); const systemPrompt = state.systemPrompt; const promptBytes = UTF8_ENCODER.encode(systemPrompt); - const manifest = `${JSON.stringify({ contentHash: computeContentHash(promptBytes) })}\n`; + const contentHash = computeContentHash(promptBytes); + const manifest = { contentHash, base: options.base ?? contentHash }; + const manifestText = `${JSON.stringify(manifest)}\n`; const absoluteDestinationPath = resolve(destinationPath); const parentPath = dirname(absoluteDestinationPath); const parentStats = await stat(parentPath); @@ -111,7 +134,7 @@ export async function saveHarnessState(state: unknown, destinationPath: string): let temporaryPath: string | undefined; try { temporaryPath = await mkdtemp(join(parentPath, `.${basename(absoluteDestinationPath)}.harness-state.tmp-`)); - await writeFile(join(temporaryPath, MANIFEST_NAME), manifest, "utf8"); + await writeFile(join(temporaryPath, MANIFEST_NAME), manifestText, "utf8"); await writeFile(join(temporaryPath, PROMPT_NAME), promptBytes); await assertDestinationAbsent(absoluteDestinationPath); await rename(temporaryPath, absoluteDestinationPath); @@ -124,4 +147,5 @@ export async function saveHarnessState(state: unknown, destinationPath: string): } throw error; } + return { state, manifest }; } diff --git a/packages/harness-state/test/harness-state.test.ts b/packages/harness-state/test/harness-state.test.ts index 375d8e17c..9bea235c5 100644 --- a/packages/harness-state/test/harness-state.test.ts +++ b/packages/harness-state/test/harness-state.test.ts @@ -6,18 +6,25 @@ import { Check } from "typebox/value"; import { afterEach, describe, expect, test } from "vitest"; import { type HarnessState, + type HarnessStateBundle, + HarnessStateBundleSchema, type HarnessStateManifest, HarnessStateManifestSchema, + type HarnessStateSaveOptions, HarnessStateSchema, loadHarnessState, saveHarnessState, } from "../src/index.ts"; +const EXTERNAL_BASE = `sha256:${"0".repeat(64)}`; const temporaryRoots: string[] = []; -function createManifest(prompt: string): string { - const digest = createHash("sha256").update(prompt, "utf8").digest("hex"); - return `{"contentHash":"sha256:${digest}"}\n`; +function computeContentHash(prompt: string): string { + return `sha256:${createHash("sha256").update(prompt, "utf8").digest("hex")}`; +} + +function createManifest(prompt: string, base = computeContentHash(prompt)): string { + return `${JSON.stringify({ contentHash: computeContentHash(prompt), base })}\n`; } afterEach(async () => { @@ -53,29 +60,37 @@ async function pathExists(path: string): Promise { } } -function assertReadonlyTypes(state: HarnessState, manifest: HarnessStateManifest): void { +function assertReadonlyTypes( + state: HarnessState, + manifest: HarnessStateManifest, + options: HarnessStateSaveOptions, + bundle: HarnessStateBundle, +): void { // @ts-expect-error HarnessState fields are readonly. state.systemPrompt = "changed"; // @ts-expect-error HarnessStateManifest fields are readonly. manifest.contentHash = "sha256:changed"; + // @ts-expect-error HarnessStateManifest fields are readonly. + manifest.base = "sha256:changed"; + // @ts-expect-error HarnessStateSaveOptions fields are readonly. + options.base = "sha256:changed"; + // @ts-expect-error HarnessStateBundle fields are readonly. + bundle.state = state; + // @ts-expect-error HarnessStateBundle fields are readonly. + bundle.manifest = manifest; } void assertReadonlyTypes; describe("schemas", () => { test("exports strict schemas with readonly inferred fields", () => { - expect(Check(HarnessStateSchema, { systemPrompt: "Work carefully." })).toBe(true); - expect( - Check(HarnessStateManifestSchema, { - contentHash: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", - }), - ).toBe(true); + const state = { systemPrompt: "Work carefully." }; + const manifest = { contentHash: computeContentHash(state.systemPrompt), base: EXTERNAL_BASE }; + expect(Check(HarnessStateSchema, state)).toBe(true); + expect(Check(HarnessStateManifestSchema, manifest)).toBe(true); + expect(Check(HarnessStateBundleSchema, { state, manifest })).toBe(true); expect(Check(HarnessStateSchema, { systemPrompt: "Work.", extra: true })).toBe(false); - expect( - Check(HarnessStateManifestSchema, { - contentHash: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", - extra: true, - }), - ).toBe(false); + expect(Check(HarnessStateManifestSchema, { ...manifest, extra: true })).toBe(false); + expect(Check(HarnessStateBundleSchema, { state, manifest, extra: true })).toBe(false); }); test.each([ @@ -92,6 +107,15 @@ describe("schemas", () => { ["uppercase digest", { contentHash: "sha256:0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" }], ["missing content hash", {}], ] as const)("rejects %s", (_label, manifest) => { + expect(Check(HarnessStateManifestSchema, { base: EXTERNAL_BASE, ...manifest })).toBe(false); + }); + + test.each([ + ["missing base", { contentHash: computeContentHash("Work.") }], + ["wrong base algorithm", { contentHash: computeContentHash("Work."), base: "sha1:0123456789abcdef" }], + ["short base", { contentHash: computeContentHash("Work."), base: "sha256:0123456789abcdef" }], + ["uppercase base", { contentHash: computeContentHash("Work."), base: `sha256:${"A".repeat(64)}` }], + ] as const)("rejects a manifest with %s", (_label, manifest) => { expect(Check(HarnessStateManifestSchema, manifest)).toBe(false); }); }); @@ -106,11 +130,29 @@ describe("saving and round trips", () => { ] as const)("preserves %s prompt content verbatim", async (_label, systemPrompt) => { const root = await createTemporaryRoot(); const destination = join(root, "saved-state"); - await saveHarnessState({ systemPrompt }, destination); + const state = { systemPrompt }; + const manifest = { contentHash: computeContentHash(systemPrompt), base: EXTERNAL_BASE }; + const saved = await saveHarnessState(state, destination, { base: EXTERNAL_BASE }); - expect(await readFile(join(destination, "manifest.json"), "utf8")).toBe(createManifest(systemPrompt)); + expect(await readFile(join(destination, "manifest.json"), "utf8")).toBe( + createManifest(systemPrompt, EXTERNAL_BASE), + ); expect(await readFile(join(destination, "EVOLVE.md"), "utf8")).toBe(systemPrompt); - expect(await loadHarnessState(destination)).toEqual({ systemPrompt }); + expect(saved).toEqual({ state, manifest }); + expect(await loadHarnessState(destination)).toEqual(saved); + }); + + test("uses the computed content hash as the base for a root state", async () => { + const root = await createTemporaryRoot(); + const destination = join(root, "root-state"); + const state = { systemPrompt: "Root harness.\n" }; + const contentHash = computeContentHash(state.systemPrompt); + + const saved = await saveHarnessState(state, destination); + + expect(saved).toEqual({ state, manifest: { contentHash, base: contentHash } }); + expect(await readFile(join(destination, "manifest.json"), "utf8")).toBe(createManifest(state.systemPrompt)); + expect(await loadHarnessState(destination)).toEqual(saved); }); test("validates state before filesystem access", async () => { @@ -120,6 +162,15 @@ describe("saving and round trips", () => { expect(await pathExists(destination)).toBe(false); }); + test("validates an explicit base before filesystem access", async () => { + const root = await createTemporaryRoot(); + const destination = join(root, "missing-parent", "state"); + await expect(saveHarnessState({ systemPrompt: "Valid." }, destination, { base: "sha256:short" })).rejects.toThrow( + /invalid harness state save options/i, + ); + expect(await pathExists(destination)).toBe(false); + }); + test("requires an existing directory parent", async () => { const root = await createTemporaryRoot(); const parentFile = join(root, "parent-file"); @@ -274,9 +325,17 @@ describe("bundle data validation", () => { }); test.each([ - ["wrong algorithm", '{"contentHash":"sha1:0123456789abcdef0123456789abcdef01234567"}\n'], + [ + "wrong algorithm", + `{"contentHash":"sha1:0123456789abcdef0123456789abcdef01234567","base":"${EXTERNAL_BASE}"}\n`, + ], ["unknown field", `${createManifest("Act carefully.\n").trimEnd().slice(0, -1)},"extra":true}\n`], - ["missing content hash", "{}\n"], + ["missing content hash", `{"base":"${EXTERNAL_BASE}"}\n`], + ["missing base", `{"contentHash":"${computeContentHash("Act carefully.\n")}"}\n`], + [ + "wrong base algorithm", + `{"contentHash":"${computeContentHash("Act carefully.\n")}","base":"sha1:0123456789abcdef"}\n`, + ], ["non-object", "[]\n"], ] as const)("rejects a manifest with %s", async (_label, manifest) => { const root = await createTemporaryRoot(); From 9044109d23683605e6ade58feb2fe4e4fd9efc3e Mon Sep 17 00:00:00 2001 From: Jay Yu Date: Sun, 16 Aug 2026 09:32:04 -0700 Subject: [PATCH 6/6] feat(harness-state): model evolve notes as memory --- packages/harness-state/README.md | 14 ++-- packages/harness-state/src/index.ts | 29 ++++---- .../harness-state/test/harness-state.test.ts | 73 +++++++++---------- 3 files changed, 57 insertions(+), 59 deletions(-) diff --git a/packages/harness-state/README.md b/packages/harness-state/README.md index 5808b7de1..7690bb4e6 100644 --- a/packages/harness-state/README.md +++ b/packages/harness-state/README.md @@ -4,20 +4,20 @@ Private workspace package for the Diffex harness-state schema and directory-bund ## State and bundle format -A logical state contains only the evolvable static system prompt: +A logical state contains only Diffex's evolving memory: ```ts { - systemPrompt: string; + memory: string; } ``` -The prompt must not start with U+FEFF and must contain a non-whitespace character. A portable bundle root is a direct directory, not a symbolic link, with exactly these direct regular files and exact casing: +The memory consists of notes collected throughout the evolution process to improve Diffex's behavior. It must not start with U+FEFF and must contain a non-whitespace character. A portable bundle root is a direct directory, not a symbolic link, with exactly these direct regular files and exact casing: ```text state-directory/ |-- manifest.json # SHA-256 identity and base-version metadata -`-- EVOLVE.md # systemPrompt UTF-8 bytes, preserved verbatim +`-- EVOLVE.md # memory notes as UTF-8 bytes, preserved verbatim ``` Both files must be fatal, BOM-free UTF-8. The manifest must contain only `contentHash` and `base`, each formatted as `sha256:` followed by 64 lowercase hexadecimal characters. Loading verifies that `contentHash` matches the exact `EVOLVE.md` bytes, including line endings and trailing whitespace. `base` identifies the predecessor's `contentHash`; a root harness conventionally uses its own `contentHash` as `base`. @@ -25,13 +25,13 @@ Both files must be fatal, BOM-free UTF-8. The manifest must contain only `conten ## API - `HarnessStateSchema`, `HarnessStateManifestSchema`, and `HarnessStateBundleSchema` are strict TypeBox schemas. Their inferred types expose readonly fields. -- `loadHarnessState(bundlePath)` validates a bundle, verifies its content hash, and returns `{ state, manifest }` without normalizing the prompt. +- `loadHarnessState(bundlePath)` validates a bundle, verifies its content hash, and returns `{ state, manifest }` without normalizing the memory. - `saveHarnessState(state, destinationPath, options?)` derives the content hash, stages a canonical two-file bundle beside the destination, and returns `{ state, manifest }`. Passing `options.base` identifies the predecessor; omitting it creates a self-based root. The parent must already be a directory, any existing destination entry is rejected, and a failed save attempts to remove its owned staging directory. The package does not include or select a default harness. Callers must provide an explicit bundle path or state. -Invalid state, layout, encoding, manifest, or prompt data rejects with an ordinary descriptive `Error`. JSON syntax failures retain the original syntax error as their cause. Other filesystem errors propagate unchanged. +Invalid state, layout, encoding, manifest, or memory data rejects with an ordinary descriptive `Error`. JSON syntax failures retain the original syntax error as their cause. Other filesystem errors propagate unchanged. ## Limitations -The state stores only the static system prompt. The manifest records one base hash but does not resolve predecessors, verify their existence, detect cycles, or otherwise manage lineage. The package has no migration or compatibility behavior and does not represent tools, models, context, sessions, or provenance. Persistence does not provide cross-process coordination, retries, or durability guarantees. +The state stores only the evolving memory notes. The manifest records one base hash but does not resolve predecessors, verify their existence, detect cycles, or otherwise manage lineage. The package has no migration or compatibility behavior and does not represent tools, models, context, sessions, or provenance. Persistence does not provide cross-process coordination, retries, or durability guarantees. diff --git a/packages/harness-state/src/index.ts b/packages/harness-state/src/index.ts index 69ed7150a..c7ec00108 100644 --- a/packages/harness-state/src/index.ts +++ b/packages/harness-state/src/index.ts @@ -5,10 +5,10 @@ import { TextDecoder, TextEncoder } from "node:util"; import Type, { type Static } from "typebox"; import { Check } from "typebox/value"; -const SystemPromptSchema = Type.Readonly(Type.String({ pattern: "^(?!\\uFEFF)(?=[\\s\\S]*\\S)[\\s\\S]*$" })); +const EvolutionMemorySchema = Type.Readonly(Type.String({ pattern: "^(?!\\uFEFF)(?=[\\s\\S]*\\S)[\\s\\S]*$" })); const ContentHashSchema = Type.Readonly(Type.String({ pattern: "^sha256:[0-9a-f]{64}$" })); -export const HarnessStateSchema = Type.Object({ systemPrompt: SystemPromptSchema }, { additionalProperties: false }); +export const HarnessStateSchema = Type.Object({ memory: EvolutionMemorySchema }, { additionalProperties: false }); export type HarnessState = Static; const HarnessStateSaveOptionsSchema = Type.Object( @@ -33,8 +33,8 @@ export const HarnessStateBundleSchema = Type.Object( export type HarnessStateBundle = Static; const MANIFEST_NAME = "manifest.json"; -const PROMPT_NAME = "EVOLVE.md"; -const BUNDLE_ENTRY_NAMES = [MANIFEST_NAME, PROMPT_NAME] as const; +const EVOLUTION_MEMORY_NAME = "EVOLVE.md"; +const BUNDLE_ENTRY_NAMES = [MANIFEST_NAME, EVOLUTION_MEMORY_NAME] as const; const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); const UTF8_ENCODER = new TextEncoder(); @@ -88,15 +88,15 @@ async function inspectBundleDirectory(bundlePath: string): Promise { export async function loadHarnessState(bundlePath: string): Promise { await inspectBundleDirectory(bundlePath); const manifestPath = join(bundlePath, MANIFEST_NAME); - const promptPath = join(bundlePath, PROMPT_NAME); + const memoryPath = join(bundlePath, EVOLUTION_MEMORY_NAME); const manifestText = decodeBundleFile(await readFile(manifestPath), manifestPath); const manifest = parseManifest(manifestText, manifestPath); - const promptBytes = await readFile(promptPath); - const systemPrompt = decodeBundleFile(promptBytes, promptPath); - const state = { systemPrompt }; - if (!Check(HarnessStateSchema, state)) throw new Error(`Invalid harness state prompt at "${promptPath}"`); - if (manifest.contentHash !== computeContentHash(promptBytes)) { - throw new Error(`Harness state content hash mismatch at "${promptPath}"`); + const memoryBytes = await readFile(memoryPath); + const memory = decodeBundleFile(memoryBytes, memoryPath); + const state = { memory }; + if (!Check(HarnessStateSchema, state)) throw new Error(`Invalid harness state memory at "${memoryPath}"`); + if (manifest.contentHash !== computeContentHash(memoryBytes)) { + throw new Error(`Harness state content hash mismatch at "${memoryPath}"`); } return { state, manifest }; } @@ -118,9 +118,8 @@ export async function saveHarnessState( ): Promise { if (!Check(HarnessStateSchema, state)) throw new Error("Invalid harness state to save"); if (!Check(HarnessStateSaveOptionsSchema, options)) throw new Error("Invalid harness state save options"); - const systemPrompt = state.systemPrompt; - const promptBytes = UTF8_ENCODER.encode(systemPrompt); - const contentHash = computeContentHash(promptBytes); + const memoryBytes = UTF8_ENCODER.encode(state.memory); + const contentHash = computeContentHash(memoryBytes); const manifest = { contentHash, base: options.base ?? contentHash }; const manifestText = `${JSON.stringify(manifest)}\n`; const absoluteDestinationPath = resolve(destinationPath); @@ -135,7 +134,7 @@ export async function saveHarnessState( try { temporaryPath = await mkdtemp(join(parentPath, `.${basename(absoluteDestinationPath)}.harness-state.tmp-`)); await writeFile(join(temporaryPath, MANIFEST_NAME), manifestText, "utf8"); - await writeFile(join(temporaryPath, PROMPT_NAME), promptBytes); + await writeFile(join(temporaryPath, EVOLUTION_MEMORY_NAME), memoryBytes); await assertDestinationAbsent(absoluteDestinationPath); await rename(temporaryPath, absoluteDestinationPath); temporaryPath = undefined; diff --git a/packages/harness-state/test/harness-state.test.ts b/packages/harness-state/test/harness-state.test.ts index 9bea235c5..124b6ad7d 100644 --- a/packages/harness-state/test/harness-state.test.ts +++ b/packages/harness-state/test/harness-state.test.ts @@ -19,12 +19,12 @@ import { const EXTERNAL_BASE = `sha256:${"0".repeat(64)}`; const temporaryRoots: string[] = []; -function computeContentHash(prompt: string): string { - return `sha256:${createHash("sha256").update(prompt, "utf8").digest("hex")}`; +function computeContentHash(memory: string): string { + return `sha256:${createHash("sha256").update(memory, "utf8").digest("hex")}`; } -function createManifest(prompt: string, base = computeContentHash(prompt)): string { - return `${JSON.stringify({ contentHash: computeContentHash(prompt), base })}\n`; +function createManifest(memory: string, base = computeContentHash(memory)): string { + return `${JSON.stringify({ contentHash: computeContentHash(memory), base })}\n`; } afterEach(async () => { @@ -40,13 +40,13 @@ async function createTemporaryRoot(): Promise { async function createBundle( root: string, name = "state", - manifest: string | Uint8Array = createManifest("Act carefully.\n"), - prompt: string | Uint8Array = "Act carefully.\n", + manifest: string | Uint8Array = createManifest("Prefer focused changes.\n"), + memory: string | Uint8Array = "Prefer focused changes.\n", ): Promise { const bundlePath = join(root, name); await mkdir(bundlePath); await writeFile(join(bundlePath, "manifest.json"), manifest); - await writeFile(join(bundlePath, "EVOLVE.md"), prompt); + await writeFile(join(bundlePath, "EVOLVE.md"), memory); return bundlePath; } @@ -67,7 +67,7 @@ function assertReadonlyTypes( bundle: HarnessStateBundle, ): void { // @ts-expect-error HarnessState fields are readonly. - state.systemPrompt = "changed"; + state.memory = "changed"; // @ts-expect-error HarnessStateManifest fields are readonly. manifest.contentHash = "sha256:changed"; // @ts-expect-error HarnessStateManifest fields are readonly. @@ -83,20 +83,20 @@ void assertReadonlyTypes; describe("schemas", () => { test("exports strict schemas with readonly inferred fields", () => { - const state = { systemPrompt: "Work carefully." }; - const manifest = { contentHash: computeContentHash(state.systemPrompt), base: EXTERNAL_BASE }; + const state = { memory: "Focused changes produced better results." }; + const manifest = { contentHash: computeContentHash(state.memory), base: EXTERNAL_BASE }; expect(Check(HarnessStateSchema, state)).toBe(true); expect(Check(HarnessStateManifestSchema, manifest)).toBe(true); expect(Check(HarnessStateBundleSchema, { state, manifest })).toBe(true); - expect(Check(HarnessStateSchema, { systemPrompt: "Work.", extra: true })).toBe(false); + expect(Check(HarnessStateSchema, { memory: "Prefer focused changes.", extra: true })).toBe(false); expect(Check(HarnessStateManifestSchema, { ...manifest, extra: true })).toBe(false); expect(Check(HarnessStateBundleSchema, { state, manifest, extra: true })).toBe(false); }); test.each([ - ["blank prompt", { systemPrompt: "" }], - ["whitespace-only prompt", { systemPrompt: " \t\r\n" }], - ["leading U+FEFF prompt", { systemPrompt: "\uFEFFWork." }], + ["blank memory", { memory: "" }], + ["whitespace-only memory", { memory: " \t\r\n" }], + ["leading U+FEFF memory", { memory: "\uFEFFObservation." }], ] as const)("rejects %s", (_label, state) => { expect(Check(HarnessStateSchema, state)).toBe(false); }); @@ -127,17 +127,15 @@ describe("saving and round trips", () => { ["CRLF", "First line\r\nSecond line\r\n"], ["multiline", "One\n\nThree\nFour"], ["trailing whitespace", "Keep this exactly. \n\t "], - ] as const)("preserves %s prompt content verbatim", async (_label, systemPrompt) => { + ] as const)("preserves %s memory content verbatim", async (_label, memory) => { const root = await createTemporaryRoot(); const destination = join(root, "saved-state"); - const state = { systemPrompt }; - const manifest = { contentHash: computeContentHash(systemPrompt), base: EXTERNAL_BASE }; + const state = { memory }; + const manifest = { contentHash: computeContentHash(memory), base: EXTERNAL_BASE }; const saved = await saveHarnessState(state, destination, { base: EXTERNAL_BASE }); - expect(await readFile(join(destination, "manifest.json"), "utf8")).toBe( - createManifest(systemPrompt, EXTERNAL_BASE), - ); - expect(await readFile(join(destination, "EVOLVE.md"), "utf8")).toBe(systemPrompt); + expect(await readFile(join(destination, "manifest.json"), "utf8")).toBe(createManifest(memory, EXTERNAL_BASE)); + expect(await readFile(join(destination, "EVOLVE.md"), "utf8")).toBe(memory); expect(saved).toEqual({ state, manifest }); expect(await loadHarnessState(destination)).toEqual(saved); }); @@ -145,29 +143,29 @@ describe("saving and round trips", () => { test("uses the computed content hash as the base for a root state", async () => { const root = await createTemporaryRoot(); const destination = join(root, "root-state"); - const state = { systemPrompt: "Root harness.\n" }; - const contentHash = computeContentHash(state.systemPrompt); + const state = { memory: "Initial evolution notes.\n" }; + const contentHash = computeContentHash(state.memory); const saved = await saveHarnessState(state, destination); expect(saved).toEqual({ state, manifest: { contentHash, base: contentHash } }); - expect(await readFile(join(destination, "manifest.json"), "utf8")).toBe(createManifest(state.systemPrompt)); + expect(await readFile(join(destination, "manifest.json"), "utf8")).toBe(createManifest(state.memory)); expect(await loadHarnessState(destination)).toEqual(saved); }); test("validates state before filesystem access", async () => { const root = await createTemporaryRoot(); const destination = join(root, "missing-parent", "state"); - await expect(saveHarnessState({ systemPrompt: " " }, destination)).rejects.toThrow(/invalid harness state/i); + await expect(saveHarnessState({ memory: " " }, destination)).rejects.toThrow(/invalid harness state/i); expect(await pathExists(destination)).toBe(false); }); test("validates an explicit base before filesystem access", async () => { const root = await createTemporaryRoot(); const destination = join(root, "missing-parent", "state"); - await expect(saveHarnessState({ systemPrompt: "Valid." }, destination, { base: "sha256:short" })).rejects.toThrow( - /invalid harness state save options/i, - ); + await expect( + saveHarnessState({ memory: "Valid observation." }, destination, { base: "sha256:short" }), + ).rejects.toThrow(/invalid harness state save options/i); expect(await pathExists(destination)).toBe(false); }); @@ -175,7 +173,7 @@ describe("saving and round trips", () => { const root = await createTemporaryRoot(); const parentFile = join(root, "parent-file"); await writeFile(parentFile, "not a directory"); - await expect(saveHarnessState({ systemPrompt: "Valid." }, join(parentFile, "state"))).rejects.toThrow(); + await expect(saveHarnessState({ memory: "Valid observation." }, join(parentFile, "state"))).rejects.toThrow(); }); test.each(["file", "directory"] as const)("refuses to overwrite an existing %s destination", async (kind) => { @@ -184,7 +182,7 @@ describe("saving and round trips", () => { if (kind === "file") await writeFile(destination, "original"); else await mkdir(destination); - await expect(saveHarnessState({ systemPrompt: "Replacement." }, destination)).rejects.toThrow(/already exists/i); + await expect(saveHarnessState({ memory: "Replacement notes." }, destination)).rejects.toThrow(/already exists/i); if (kind === "file") expect(await readFile(destination, "utf8")).toBe("original"); else expect(await readdir(destination)).toEqual([]); }); @@ -193,14 +191,14 @@ describe("saving and round trips", () => { const root = await createTemporaryRoot(); const destination = join(root, "state"); await symlink(join(root, "missing-target"), destination, process.platform === "win32" ? "junction" : "dir"); - await expect(saveHarnessState({ systemPrompt: "Replacement." }, destination)).rejects.toThrow(/already exists/i); + await expect(saveHarnessState({ memory: "Replacement notes." }, destination)).rejects.toThrow(/already exists/i); }); test("cleans its staged directory after a late failure without exposing a partial bundle", async () => { const root = await createTemporaryRoot(); const destination = join(root, "state"); const temporaryPrefix = `.${basename(destination)}.harness-state.tmp-`; - const saving = saveHarnessState({ systemPrompt: "x".repeat(16 * 1024 * 1024) }, destination).then( + const saving = saveHarnessState({ memory: "x".repeat(16 * 1024 * 1024) }, destination).then( () => ({ ok: true as const }), (error: unknown) => ({ ok: false as const, error }), ); @@ -301,7 +299,8 @@ describe("bundle data validation", () => { test.each(["manifest.json", "EVOLVE.md"] as const)("rejects a UTF-8 BOM in %s", async (entryName) => { const root = await createTemporaryRoot(); const bundlePath = await createBundle(root); - const validContent = entryName === "manifest.json" ? createManifest("Act carefully.\n") : "Valid prompt."; + const validContent = + entryName === "manifest.json" ? createManifest("Prefer focused changes.\n") : "Valid memory."; await writeFile( join(bundlePath, entryName), Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(validContent)]), @@ -346,14 +345,14 @@ describe("bundle data validation", () => { test.each([ ["blank", ""], ["whitespace-only", " \t\r\n"], - ["leading U+FEFF", "\uFEFFPrompt."], - ] as const)("rejects a %s prompt", async (_label, prompt) => { + ["leading U+FEFF", "\uFEFFObservation."], + ] as const)("rejects a %s memory", async (_label, memory) => { const root = await createTemporaryRoot(); - const bundlePath = await createBundle(root, "state", createManifest(String(prompt)), prompt); + const bundlePath = await createBundle(root, "state", createManifest(String(memory)), memory); await expect(loadHarnessState(bundlePath)).rejects.toThrow(/EVOLVE\.md/i); }); - test("rejects prompt content that does not match the manifest hash", async () => { + test("rejects memory content that does not match the manifest hash", async () => { const root = await createTemporaryRoot(); const bundlePath = await createBundle(root, "state", createManifest("Original."), "Changed."); await expect(loadHarnessState(bundlePath)).rejects.toThrow(/content hash mismatch/i);