Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/BUILD_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

This is a concise chronological record of material implementation work and runtime evidence. It is not a substitute for Git history; it captures intent, verification, and blockers that a commit alone may not explain.

## 2026-07-31 — R4.5 fixture-only Control Pulse accepted

- Implemented exact bounded Change Claim, participant/internal probe, Side Coach capsule/proposal, attempt, and result contracts. Claim, probe, attempt, command result, and probe result identities use their normative domain-separated hashes.
- Added one immutable Explain-to-Break catalog input for the reviewed tenant-isolation fixture. The participant commits a prediction before observation; execution can resolve only the catalog-owned `mutated` state and declared tenant-isolation check through `TrustedFixtureRunner`.
- Kept controller bindings out of participant serialization and model context. The Side Coach receives only capped participant-visible excerpts with evidence IDs, local paths, and known credential shapes removed; its output cannot become code, paths, arguments, environment, or tests.
- Six R4.5 tests pass locally on Windows. They cover golden hashes, exact projection, evidence/privacy bounds, model proposal isolation, precommit ordering, result invariance to prose, catalog-only execution, replay/cross-project/late rejection, and timeout/cancellation/runner-error invalidation. The complete extension suite passes 63/63; TypeScript, production build, and VSIX packaging pass. Protected PR #15 run `30671499103` passed the required Linux, Windows, contract, web, and policy checks, so R4.5 acceptance is complete.

Evidence: `extension/src/pulse/`, `extension/test/control-pulse.test.ts`, local command/package output on 2026-07-31, and protected GitHub Actions run `30671499103` on PR #15.

## 2026-07-31 — R4 Experience Compiler and Phase-A Judge accepted

- Added deterministic seam selection over fully supported R2 evidence and compiled the real R1/R2 fixture path into separate internal and participant recovery manifests. The participant projection is exact-schema and omits source revisions, run identity, setup, judge internals, hidden repair, production paths, and controller handles.
Expand Down
13 changes: 8 additions & 5 deletions docs/PROJECT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Last updated: 2026-07-31

## Current branch milestone — R4 Experience Compiler and Judge accepted
## Current branch milestone — R4.5 fixture-only Control Pulse accepted

Branch `codex/shadow-cockpit-rnd` resets the product R&D thesis around **Dual-Control Development**.

Expand All @@ -24,7 +24,10 @@ Branch `codex/shadow-cockpit-rnd` resets the product R&D thesis around **Dual-Co
- R4 now compiles one fully attributed, bounded, test-backed seam from real R1 replay plus R2 change evidence into separate internal and participant recovery manifests. The participant projection excludes source revisions, run identity, judge internals, setup, hidden repair, production paths, and controller handles.
- The Phase-A Evidence Judge reopens the immutable snapshot and command registry, verifies the standalone Git boundary, computes the participant diff, rejects protected or out-of-scope changes, and evaluates only the exact catalog-owned known repair in a clean evaluation twin. No-op and unrelated candidates fail without execution; reveal and abandonment produce no executable readiness evidence.
- Seven R4 tests cover deterministic compilation, strict projection/schema rejection, unsupported and fixture-drift rejection, protected-path integrity, no-op/unrelated non-execution, three identical clean replays, reveal/abandon honesty, and project-scoped persistent command evidence with tamper detection. The full local Windows extension suite passes 57/57; `npm run check`, production build, and VSIX packaging pass. Protected PR #14 run `30670322126` passed `extension`, `extension-windows`, `contract`, `web`, and `jules-rnd-policy`, so R4 acceptance is complete.
- The Control Pulse runtime, readiness ledger, and v0.3 cockpit do not exist yet. R4 remains a closed reviewed-fixture mechanism and does not execute arbitrary participant or workspace code.
- R4.5 now implements bounded `ChangeClaim`, participant/internal probe, Side Coach capsule/proposal, attempt, and result contracts with exact schemas and domain-separated hashes. The deterministic no-model path commits a developer prediction before observation and resolves only one extension-owned fixture state/check pair.
- Participant serialization contains no fixture binding, command, state, revision, oracle, controller handle, or production path. Capsule construction reopens only participant-visible project evidence, caps and scrubs excerpts, and treats every model proposal as non-executable text plus an optional existing input ID.
- Six R4.5 tests cover cross-platform golden claim/probe/attempt hashes, strict projection and catalog isolation, unstable/cross-project/hidden/malformed evidence rejection, capsule path/secret/ID redaction, precommitted prediction, prose-invariant observation, replay/late/identity failures, and timeout/cancellation/runner-error invalidation. The full local Windows extension suite passes 63/63; `npm run check`, production build, and VSIX packaging pass. Protected PR #15 run `30671499103` passed `extension`, `extension-windows`, `contract`, `web`, and `jules-rnd-policy`, so R4.5 acceptance is complete.
- The readiness ledger and v0.3 cockpit do not exist yet. R0–R4.5 remain a closed reviewed-fixture mechanism and do not execute arbitrary participant or workspace code.
- No skill-retention or speed metric has been measured. Values in the PRD are predeclared R&D targets.
- A new implementation audit found five R0 ambiguities: candidate-diff identity, pre-store fixture blobs, runtime identity, check IDs, and Git object format. The normative contract closes them with structured diffs, catalog-owned blobs, standalone Node `v22.17.0`, declared test IDs, and SHA-1 Git initialization; R0a/R0b now implement and verify that complete substrate.
- A guarded Jules dispatcher and PR policy are defined as a finite R0→R4 queue. They create at most one session after a successful preflight, stop after merged R4, remain inert unless dispatch is explicitly enabled, and keep plan approval on by default. Merges remain manual because the current project tests are not an independent immutable verifier. Full scheduled continuation still requires the dispatcher workflow to be reviewed into the default branch.
Expand Down Expand Up @@ -142,9 +145,9 @@ No external input blocks the repository-owned fixture R0–R4.5 mechanism in `do

## Next ordered actions

1. Pass R4.5: one bounded, catalog-only Explain-to-Break Pulse with replay/error fail-closed tests.
2. Run the 30-patch recovery-plus-probe technical corpus audit before expanding the product surface.
3. Add the local readiness ledger and minimal cockpit only after the vertical slice is reliable.
1. Run the 30-patch recovery-plus-probe technical corpus audit before expanding the product surface.
2. Select and verify the Windows-capable Phase-B sandbox required by R7; fail closed if none meets ADR-003.
3. Add the local readiness ledger and minimal cockpit only after the R7 gate passes.
4. Run the preregistered delayed-transfer pilot before making any skill-retention claim.

## Recent milestone commits
Expand Down
110 changes: 110 additions & 0 deletions extension/src/pulse/capsule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { assertBoundedText, assertObjectShape, assertRecord } from "../agent/types";
import { assertExactKeys, assertSha256, canonicalJson } from "../rnd/canonical";
import type {
ParticipantControlProbe,
SideCoachCapsule,
SideCoachProposal,
ValidatedClaim,
} from "./types";
import { assertParticipantControlProbe, assertProbeSurface } from "./validate";

const windowsPath = /(?:[A-Za-z]:\\|\\\\)[^\s"'<>]+/g;
const unixPath = /(?:^|[\s("'])(\/(?:home|Users|opt|var|tmp|workspace|repo)\/[^\s"')<>]+)/g;
const secret = /\b(?:ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,}|AKIA[0-9A-Z]{16})\b/g;

export async function buildSideCoachCapsule(
participant: ParticipantControlProbe,
validated: ValidatedClaim,
developerAnswer: string,
): Promise<SideCoachCapsule> {
assertParticipantControlProbe(participant);
assertBoundedText(developerAnswer, 4096, "developer answer");
if (canonicalJson(participant.claim) !== canonicalJson(validated.claim)) {
throw new Error("Side Coach claim projection changed");
}
if (participant.internalProbeHash.length !== 64) throw new Error("Side Coach probe binding is invalid");

const ids = new Set(validated.evidence.map(({ ref }) => ref.id));
const evidence = validated.evidence.slice(0, 4).map(({ ref, content }) => ({
kind: ref.kind,
sha256: ref.sha256,
mediaType: ref.mediaType,
excerpt: truncateUtf8(scrub(content, ids), 2048),
}));
const claim = {
intent: validated.claim.intent,
changedBehavior: validated.claim.changedBehavior,
boundary: structuredClone(validated.claim.boundary),
invariant: validated.claim.invariant,
...(validated.claim.unresolvedAssumption === undefined
? {}
: { unresolvedAssumption: validated.claim.unresolvedAssumption }),
};
const capsule: SideCoachCapsule = {
schemaVersion: 1,
claimHash: validated.claimHash,
claim,
evidence,
probe: structuredClone(participant.participant),
developerAnswer,
};
assertSideCoachCapsule(capsule);
if (Buffer.byteLength(canonicalJson(capsule)) > 16_384) throw new Error("Side Coach capsule exceeds 16384 bytes");
return capsule;
}

export function validateSideCoachProposal(value: unknown, capsule: SideCoachCapsule): SideCoachProposal {
assertSideCoachCapsule(capsule);
assertRecord(value, "Side Coach proposal");
assertObjectShape(value, ["schemaVersion", "hypothesis"], ["probeInputId", "clarification"], "Side Coach proposal");
if (value.schemaVersion !== 1) throw new Error("Unsupported Side Coach proposal");
assertBoundedText(value.hypothesis, 2048, "Side Coach hypothesis");
if (value.probeInputId !== undefined) {
assertBoundedText(value.probeInputId, 128, "Side Coach probe input ID");
if (!capsule.probe.inputs.some(({ id }) => id === value.probeInputId)) {
throw new Error("Side Coach proposal selected an unknown probe input");
}
}
if (value.clarification !== undefined) assertBoundedText(value.clarification, 2048, "Side Coach clarification");
if (Buffer.byteLength(canonicalJson(value)) > 4096) throw new Error("Side Coach proposal exceeds 4096 bytes");
return structuredClone(value) as unknown as SideCoachProposal;
}

function assertSideCoachCapsule(value: unknown): asserts value is SideCoachCapsule {
assertRecord(value, "Side Coach capsule");
assertExactKeys(value, ["schemaVersion", "claimHash", "claim", "evidence", "probe", "developerAnswer"], "Side Coach capsule");
if (value.schemaVersion !== 1) throw new Error("Unsupported Side Coach capsule");
assertSha256(String(value.claimHash), "claimHash");
assertRecord(value.claim, "Side Coach claim");
assertObjectShape(value.claim, ["intent", "changedBehavior", "boundary", "invariant"], ["unresolvedAssumption"], "Side Coach claim");
assertBoundedText(value.claim.intent, 1024, "claim intent");
assertBoundedText(value.claim.changedBehavior, 1024, "changed behavior");
assertBoundedText(value.claim.invariant, 1024, "claim invariant");
if (value.claim.unresolvedAssumption !== undefined) assertBoundedText(value.claim.unresolvedAssumption, 1024, "unresolved assumption");
assertRecord(value.claim.boundary, "claim boundary");
assertExactKeys(value.claim.boundary, ["path", "symbol"], "claim boundary");
if (!Array.isArray(value.evidence) || value.evidence.length > 4) throw new Error("Side Coach evidence is invalid");
for (const item of value.evidence) {
assertRecord(item, "Side Coach evidence item");
assertExactKeys(item, ["kind", "sha256", "mediaType", "excerpt"], "Side Coach evidence item");
assertSha256(String(item.sha256), "evidence hash");
assertBoundedText(item.mediaType, 128, "evidence media type");
assertBoundedText(item.excerpt, 2048, "evidence excerpt");
}
assertProbeSurface(value.probe);
assertBoundedText(value.developerAnswer, 4096, "developer answer");
}

function scrub(value: string, ids: Set<string>): string {
let text = value.replace(windowsPath, "[local-path]");
text = text.replace(unixPath, (match, path: string) => match.replace(path, "[local-path]"));
text = text.replace(secret, "[secret]");
for (const id of ids) text = text.replaceAll(id, "[evidence]");
return text;
}

function truncateUtf8(value: string, maxBytes: number): string {
let result = value;
while (Buffer.byteLength(result) > maxBytes) result = result.slice(0, -1);
return result;
}
166 changes: 166 additions & 0 deletions extension/src/pulse/catalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { assertToken } from "../agent/types";
import { assertExactKeys, assertSha256, canonicalHash, canonicalJson, compareUtf8 } from "../rnd/canonical";
import { EXPECTED_TENANT_CACHE_KEY } from "../twin/fixture-factory";
import type {
CatalogControlProbe,
FixtureControlProbe,
ParticipantControlProbe,
ParticipantProbeSurface,
ValidatedClaim,
} from "./types";
import {
assertCatalogControlProbe,
assertFixtureControlProbe,
assertParticipantControlProbe,
assertProbeSurface,
} from "./validate";

interface CatalogEntry {
probe: CatalogControlProbe;
label: string;
}

const entries: CatalogEntry[] = [{
probe: {
schemaVersion: 1,
id: "tenant-isolation-boundary",
fixtureId: "tenant-cache-key",
fixtureManifestHash: EXPECTED_TENANT_CACHE_KEY.manifestHash,
state: "mutated",
checkId: "cache-key.tenant-isolation",
prompt: "Before the check runs: will tenant isolation hold for two tenants sharing the same id?",
},
label: "Run the tenant-isolation boundary check",
}];

export class FixtureControlProbeCatalog {
list(fixtureId: string, fixtureManifestHash: string): CatalogControlProbe[] {
const values = entries
.filter(({ probe }) => probe.fixtureId === fixtureId && probe.fixtureManifestHash === fixtureManifestHash)
.map(({ probe }) => structuredClone(probe))
.sort((left, right) => compareUtf8(left.id, right.id));
values.forEach(assertCatalogControlProbe);
return values;
}

open(id: string, fixtureId: string, fixtureManifestHash: string): CatalogControlProbe | undefined {
const entry = entries.find(({ probe }) =>
probe.id === id && probe.fixtureId === fixtureId && probe.fixtureManifestHash === fixtureManifestHash,
);
if (!entry) return undefined;
assertCatalogControlProbe(entry.probe);
return structuredClone(entry.probe);
}

surface(fixtureId: string, fixtureManifestHash: string): ParticipantProbeSurface {
const selected = entries
.filter(({ probe }) => probe.fixtureId === fixtureId && probe.fixtureManifestHash === fixtureManifestHash)
.sort((left, right) => compareUtf8(left.probe.id, right.probe.id));
if (selected.length === 0) throw new Error("Fixture has no allowlisted control probes");
const prompts = new Set(selected.map(({ probe }) => probe.prompt));
if (prompts.size !== 1) throw new Error("Catalog probe prompts do not form one committed surface");
const surface = {
prompt: selected[0]!.probe.prompt,
inputs: selected.map(({ probe, label }) => ({ id: probe.id, label })),
};
assertProbeSurface(surface);
return surface;
}
}

export interface PublishedFixtureProbe {
internal: FixtureControlProbe;
internalProbeHash: string;
participant: ParticipantControlProbe;
}

export class FixtureControlProbeStore {
private readonly values = new Map<string, PublishedFixtureProbe>();

publish(value: PublishedFixtureProbe): PublishedFixtureProbe {
assertPublished(value);
const key = `${value.internal.projectId}/${value.internal.id}/${value.internalProbeHash}`;
const current = this.values.get(key);
if (current && canonicalJson(current) !== canonicalJson(value)) {
throw new Error("Published control probe is immutable");
}
this.values.set(key, structuredClone(value));
return structuredClone(value);
}

open(projectId: string, id: string, hash: string): PublishedFixtureProbe | undefined {
const value = this.values.get(`${projectId}/${id}/${hash}`);
return value ? structuredClone(value) : undefined;
}
}

export function publishFixtureControlProbe(input: {
id: string;
projectId: string;
claim: ValidatedClaim;
sourceTreeHash: string;
catalog: FixtureControlProbeCatalog;
store: FixtureControlProbeStore;
}): PublishedFixtureProbe {
assertToken(input.id, "probeId");
assertToken(input.projectId, "projectId");
assertSha256(input.sourceTreeHash, "sourceTreeHash");
const fixtureId = "tenant-cache-key";
const fixtureManifestHash = EXPECTED_TENANT_CACHE_KEY.manifestHash;
const internal: FixtureControlProbe = {
schemaVersion: 1,
mode: "fixture",
id: input.id,
projectId: input.projectId,
claimHash: input.claim.claimHash,
sourceTreeHash: input.sourceTreeHash,
fixtureId,
fixtureManifestHash,
participant: input.catalog.surface(fixtureId, fixtureManifestHash),
};
assertFixtureControlProbe(internal);
const internalProbeHash = canonicalHash("control-probe", internal);
const participant: ParticipantControlProbe = {
schemaVersion: 1,
id: internal.id,
internalProbeHash,
claim: structuredClone(input.claim.claim),
participant: structuredClone(internal.participant),
};
assertParticipantControlProbe(participant);
return input.store.publish({ internal, internalProbeHash, participant });
}

export function serializeParticipantControlProbe(value: ParticipantControlProbe): string {
assertParticipantControlProbe(value);
return canonicalJson(value);
}

export function assertParticipantProjection(
published: PublishedFixtureProbe,
participant: ParticipantControlProbe,
): void {
assertPublished(published);
assertParticipantControlProbe(participant);
if (canonicalJson(participant) !== canonicalJson(published.participant)) {
throw new Error("Participant control probe projection changed");
}
}

function assertPublished(value: PublishedFixtureProbe): void {
assertExactKeys(value, ["internal", "internalProbeHash", "participant"], "published fixture probe");
assertFixtureControlProbe(value.internal);
assertSha256(value.internalProbeHash, "internalProbeHash");
assertParticipantControlProbe(value.participant);
if (canonicalHash("control-probe", value.internal) !== value.internalProbeHash) {
throw new Error("Internal control probe hash changed");
}
if (
value.participant.id !== value.internal.id ||
value.participant.internalProbeHash !== value.internalProbeHash ||
canonicalHash("control-claim", value.participant.claim) !== value.internal.claimHash ||
canonicalJson(value.participant.participant) !== canonicalJson(value.internal.participant)
) {
throw new Error("Participant control probe projection changed");
}
}
Loading
Loading