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
11 changes: 11 additions & 0 deletions docs/BUILD_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

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 — R2 change evidence accepted

- Added a bounded read-only Git revision reader that verifies full base/target commit IDs, parses NUL-delimited name status plus zero-context hunks, detects renames, caps source and revision size, and keeps every emitted path workspace-relative.
- Added TypeScript compiler-API extraction for functions, arrow/function variables, classes, methods, and explicit module boundaries. Removed and added logical lines are hashed rather than stored; syntax failures and unsupported languages remain explicit results.
- Added deterministic evidence assembly behind the exact `ExtractionResult`, `SemanticUnit`, and `CandidateSeam` contracts. Fixture-manifest check links are authoritative, matching Flight Recorder before/after hashes and evidence refs are verified, unknown readiness factors stay `null`, and missing evidence becomes a gap rather than a fabricated score.
- Corrected the checked replay transcript's file hashes, command ID, and test ID to the actual R0 fixture identities so R1→R2 integration can be verified rather than merely shaped correctly.
- Added six R2 tests covering the real cache-key revision pair, golden unit/seam IDs, file rename, multi-file changes, added/deleted functions, a class boundary, Git hunk parsing, unsupported syntax/language, missing links, unsafe paths, and revision/run identity drift.
- Local Windows evidence: `npm run check`, all 43 extension tests, production build, and VSIX packaging passed. Protected PR #12 run `30666648522` then passed `extension`, `extension-windows`, `contract`, `web`, and `jules-rnd-policy`.

Evidence: `extension/src/change/`, `extension/test/change-evidence.test.ts`, the checked agent transcript, `docs/v0.3/CONTRACTS.md`, local command output on 2026-07-31, and protected GitHub Actions run `30666648522` on PR #12.

## 2026-07-31 — R1 replay and Flight Recorder accepted

- Implemented the exact schema-v1 `AgentTask`, `AgentWorkspace`, `AgentRun`, `AgentDriver`, `RunEvent`, `RunEnvelope`, `RunRecorder`, and `TaskStore` boundaries behind a checked, credential-free replay driver.
Expand Down
8 changes: 5 additions & 3 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 — R1 replay and Flight Recorder complete
## Current branch milestone — R2 change evidence complete

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

Expand All @@ -17,7 +17,9 @@ Branch `codex/shadow-cockpit-rnd` resets the product R&D thesis around **Dual-Co
- The full local Windows extension suite passes 27/27 with `npm run check`, build, and VSIX packaging. Protected PR #10 run `30663623200` independently reproduced the exact fixture/runtime behavior on Linux and Windows; all five required checks passed, so R0 acceptance is complete.
- R1 implements the schema-v1 `AgentDriver` boundary, checked replay driver, canonical task-intent storage, and append-only local Flight Recorder behind injected storage and evidence-ownership interfaces. Ten R1 tests cover deterministic replay, canonical round trips, sequence/execution/identity violations, cross-project evidence, path and size bounds, failed/cancelled honesty, persistence, range reads, and secret/local-handle omission.
- The full local Windows extension suite passes 37/37 with `npm run check`; the production bundle and VSIX package also pass. Protected PR #11 run `30665384997` independently passed `extension`, `extension-windows`, `contract`, `web`, and `jules-rnd-policy`, so R1 acceptance is complete.
- The semantic extractor, Experience Compiler, Takeover Twin lifecycle, Evidence Judge, Control Pulse runtime, readiness ledger, and v0.3 cockpit do not exist yet. `TrustedFixtureRunner` exists for the closed R0 fixture substrate.
- R2 implements bounded Git revision diffs, zero-context changed-line extraction, TypeScript compiler-API symbol resolution, explicit fixture check linkage, Flight Recorder hash/attribution validation, and exact `ExtractionResult` / `SemanticUnit` / `CandidateSeam` outputs. It fails to `partial` or `unsupported` for missing links, syntax failures, unsupported languages, or incomplete coverage instead of inventing an invariant.
- Six R2 tests cover the real cache-key fixture, cross-run determinism and golden IDs, file rename, multi-file changes, added/deleted functions, class boundaries, Git hunk parsing, unsupported syntax/language, missing checks, unsafe paths, and revision/run drift. The full local Windows extension suite passes 43/43 with TypeScript, build, and VSIX packaging. Protected PR #12 run `30666648522` passed `extension`, `extension-windows`, `contract`, `web`, and `jules-rnd-policy`, so R2 acceptance is complete.
- The Experience Compiler, Takeover Twin lifecycle, Evidence Judge, Control Pulse runtime, readiness ledger, and v0.3 cockpit do not exist yet. `TrustedFixtureRunner` exists for the closed R0 fixture substrate.
- 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 @@ -135,7 +137,7 @@ No external input blocks the repository-owned fixture R0–R4.5 mechanism in `do

## Next ordered actions

1. Implement R2 and R3 behind the accepted contracts: deterministic change-evidence extraction and the safe Takeover Twin lifecycle.
1. Implement R3 behind the accepted contract: safe Takeover Twin snapshot, lifecycle, catalog, and command boundaries.
2. Integrate R4: one compiled recovery episode and deterministic Evidence Judge.
3. Pass R4.5: one bounded, catalog-only Explain-to-Break Pulse with replay/error fail-closed tests.
4. Run the 30-patch recovery-plus-probe technical corpus audit before expanding the product surface.
Expand Down
15 changes: 14 additions & 1 deletion docs/v0.3/CONTRACTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Canonical object digests and raw byte hashes use one explicitly separated scheme
- `canonicalHash(domain, value)` is lowercase hex SHA-256 over `UTF8("pureflow/v0.3/" + domain + "\n") || UTF8(JCS(value))`;
- fields named `sha256` contain lowercase hex raw `SHA-256(bytes)` for the exact stored bytes; Git OIDs retain Git's native object algorithm and are never placed in a `Sha256` field;
- a file manifest is sorted by the UTF-8 byte order of normalized `path`, rejects duplicate or case-colliding paths, and hashes `[{ path, mode, sha256 }]` with domain `tree`;
- `FixtureManifest` uses domain `fixture-manifest`, command registry snapshots use `command-registry`, task intents use `task-intent`, participant diffs use `candidate-diff`, command results use `command-result`, judge results use `judge-result`, Control Pulse claims use `control-claim`, internal probes use `control-probe`, probe attempts use `control-probe-attempt`, probe results use `control-probe-result`, capability records use `capability-evidence`, and readiness records use `verified-readiness`; a value omits its own digest field before hashing.
- `FixtureManifest` uses domain `fixture-manifest`, command registry snapshots use `command-registry`, task intents use `task-intent`, semantic units use `semantic-unit`, candidate seams use `candidate-seam`, participant diffs use `candidate-diff`, command results use `command-result`, judge results use `judge-result`, Control Pulse claims use `control-claim`, internal probes use `control-probe`, probe attempts use `control-probe-attempt`, probe results use `control-probe-result`, capability records use `capability-evidence`, and readiness records use `verified-readiness`; a value omits its own digest field before hashing.

`candidate-diff` never hashes Git's formatted patch output. It hashes this normalized value, with `changes` sorted by UTF-8 bytes of `path` and duplicate or case-colliding paths rejected:

Expand Down Expand Up @@ -507,6 +507,19 @@ R2 owns this boundary. It may mark a unit unattributed or a factor unknown; it m

Known factor values are normalized to `[0, 1]`; `null` means unavailable, not zero. `estimatedAttentionMinutes` is an integer from 1 to 30 in R&D. Selection records the factor values and deterministic tie-breaker (`CandidateSeam.id` ascending) used for the choice.

The first R2 implementation is deliberately narrow and deterministic:

- Git changes are read by full verified base/target OID with `--name-status -z`, rename detection, and zero-context patches; paths are normalized workspace-relative values, source files are capped at 1 MiB, and a revision pair is capped at 256 changed files.
- Only `.ts` and `.tsx` implementation files are parsed with the TypeScript compiler API. Declaration files and syntactically invalid sources are unsupported; unrecognized changed code returns an explicit reason rather than an inferred symbol.
- Fixture check linkage is explicit from the committed fixture manifest. R2 never guesses a test from a filename, import, or LLM narrative.
- `changedLineSha256` is the sorted unique list of raw SHA-256 hashes of the exact UTF-8 logical line bytes, excluding the line terminator, for removed base lines and added target lines that intersect the unit. An unchanged file rename may therefore have an empty list.
- `SemanticUnit.id` is `unit_` plus `canonicalHash("semantic-unit", { schemaVersion, path, symbol, kind, changedLineSha256 })`.
- `CandidateSeam.id` is `seam_` plus `canonicalHash("candidate-seam", { schemaVersion, projectId, sourceRunId, baseRevision, targetRevision, unitId, linkedChecks })`; `linkedChecks` is sorted and unique.
- R2 verifies a matching Flight Recorder file event against the exact before/after file bytes. Attribution exists only when that event and `task.started.intentHash` exist; otherwise the unit is explicitly unattributed.
- Until R5 exists, `blastRadius`, `novelty`, and `capabilityAgeMs` are `null`. `evidenceGap` is the fraction of missing observable slots across one file-change reference plus every linked test result. The fixture slice uses a fixed five-minute attention estimate rather than pretending to have a calibrated model.

Cross-platform golden tests pin the first fixture's semantic-unit and candidate-seam IDs. Any change to these rules requires a schema-version decision rather than silently changing existing identities.

## 7. Sanitized snapshot boundary

```ts
Expand Down
2 changes: 1 addition & 1 deletion docs/v0.3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ PureFlow v0.3 asks whether an AI IDE can keep autonomous coding fast while behav
## Current truth

- The released v0.1 VSCodium IDE exists and remains the runtime baseline.
- The complete v0.3 Dual-Control product runtime is not implemented. R0 provides canonical hashing, fail-closed fixture contracts, the deterministic fixture, standalone hash-pinned runtime, external oracle, mutation, and exact repair. R1 adds the checked replay `AgentDriver` and append-only Flight Recorder. Protected PR #11 run `30665384997` passed the required Linux and Windows checks.
- The complete v0.3 Dual-Control product runtime is not implemented. R0 provides canonical hashing, fail-closed fixture contracts, the deterministic fixture, standalone hash-pinned runtime, external oracle, mutation, and exact repair. R1 adds the checked replay `AgentDriver` and append-only Flight Recorder. R2 adds deterministic Git/TypeScript change-evidence extraction and explicit fixture check linkage; protected PR #12 run `30666648522` passed the required Linux and Windows checks.
- No retention, takeover, productivity, or usability target has been measured.
- The first valid build is one test-backed vertical slice, not a full Cursor clone.
- R0–R4 may execute only finite, repository-owned fixture states. Arbitrary participant or corpus code remains blocked until ADR-003 selects and runtime-verifies a real sandbox backend.
Expand Down
184 changes: 184 additions & 0 deletions extension/src/change/diff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { execFile } from "node:child_process";
import { resolve } from "node:path";
import { promisify } from "node:util";
import { assertRelPath } from "../rnd/canonical";
import { assertGitOid } from "../agent/types";

const runFile = promisify(execFile);
const maxSourceBytes = 1024 * 1024;

export type RevisionChangeStatus = "added" | "modified" | "deleted" | "renamed";

export interface RevisionFileChange {
status: RevisionChangeStatus;
beforePath?: string;
afterPath?: string;
beforeText?: string;
afterText?: string;
beforeChangedLines: number[];
afterChangedLines: number[];
}

export interface RevisionDiff {
baseRevision: string;
targetRevision: string;
files: RevisionFileChange[];
}

export class GitRevisionDiffReader {
async read(repository: string, baseRevision: string, targetRevision: string): Promise<RevisionDiff> {
assertGitOid(baseRevision, "baseRevision");
assertGitOid(targetRevision, "targetRevision");
const root = resolve(repository);
const [base, target] = await Promise.all([
git(root, ["rev-parse", "--verify", `${baseRevision}^{commit}`]),
git(root, ["rev-parse", "--verify", `${targetRevision}^{commit}`]),
]);
if (base !== baseRevision || target !== targetRevision) throw new Error("Git revision identity changed during extraction");

const status = await git(root, [
"diff",
"--name-status",
"-z",
"--find-renames=90%",
"--no-ext-diff",
baseRevision,
targetRevision,
"--",
], false);
const entries = parseNameStatus(status);
if (entries.length > 256) throw new Error("Revision diff exceeds 256 changed files");
const files: RevisionFileChange[] = [];

for (const entry of entries) {
const paths = [entry.beforePath, entry.afterPath].filter((path): path is string => path !== undefined);
const patch = await git(root, [
"diff",
"--unified=0",
"--no-color",
"--no-ext-diff",
"--no-textconv",
baseRevision,
targetRevision,
"--",
...paths,
], false);
const lines = parseZeroContextPatch(patch);
const [beforeText, afterText] = await Promise.all([
entry.beforePath === undefined ? undefined : readRevisionFile(root, baseRevision, entry.beforePath),
entry.afterPath === undefined ? undefined : readRevisionFile(root, targetRevision, entry.afterPath),
]);
files.push({ ...entry, beforeText, afterText, beforeChangedLines: lines.before, afterChangedLines: lines.after });
}

files.sort((left, right) => Buffer.compare(
Buffer.from(left.afterPath ?? left.beforePath!, "utf8"),
Buffer.from(right.afterPath ?? right.beforePath!, "utf8"),
));
return { baseRevision, targetRevision, files };
}
}

export function parseZeroContextPatch(patch: string): { before: number[]; after: number[] } {
const before = new Set<number>();
const after = new Set<number>();
let oldLine = 0;
let newLine = 0;
let inHunk = false;

for (const line of patch.split(/\r?\n/)) {
const header = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
if (header) {
oldLine = Number(header[1]);
newLine = Number(header[3]);
inHunk = true;
continue;
}
if (!inHunk) continue;
if (line.startsWith("diff --git ") || line.startsWith("@@ ")) {
inHunk = false;
continue;
}
if (line.startsWith("\\ No newline at end of file")) continue;
if (line.startsWith("-")) {
before.add(oldLine++);
continue;
}
if (line.startsWith("+")) {
after.add(newLine++);
continue;
}
if (line.startsWith(" ")) {
oldLine += 1;
newLine += 1;
}
}

return { before: [...before].sort((a, b) => a - b), after: [...after].sort((a, b) => a - b) };
}

function parseNameStatus(value: string): Array<Pick<RevisionFileChange, "status" | "beforePath" | "afterPath">> {
if (value === "") return [];
const tokens = value.split("\0");
if (tokens.at(-1) === "") tokens.pop();
const files: Array<Pick<RevisionFileChange, "status" | "beforePath" | "afterPath">> = [];

for (let index = 0; index < tokens.length;) {
const code = tokens[index++]!;
if (/^R\d{1,3}$/.test(code)) {
const beforePath = tokens[index++];
const afterPath = tokens[index++];
if (beforePath === undefined || afterPath === undefined) throw new Error("Malformed Git rename status");
assertBoundedPath(beforePath);
assertBoundedPath(afterPath);
files.push({ status: "renamed", beforePath, afterPath });
continue;
}

const path = tokens[index++];
if (path === undefined) throw new Error("Malformed Git name status");
assertBoundedPath(path);
if (code === "A") files.push({ status: "added", afterPath: path });
else if (code === "D") files.push({ status: "deleted", beforePath: path });
else if (code === "M") files.push({ status: "modified", beforePath: path, afterPath: path });
else throw new Error(`Unsupported Git change status: ${code}`);
}
return files;
}

async function readRevisionFile(root: string, revision: string, path: string): Promise<string> {
const value = await git(root, ["show", `${revision}:${path}`], false);
if (Buffer.byteLength(value, "utf8") > maxSourceBytes) throw new Error(`Changed source exceeds ${maxSourceBytes} bytes`);
return value;
}

async function git(root: string, args: string[], trim = true): Promise<string> {
const { stdout } = await runFile("git", args, {
cwd: root,
encoding: "utf8",
windowsHide: true,
maxBuffer: 4 * 1024 * 1024,
env: gitEnvironment(),
});
return trim ? stdout.trim() : stdout;
}

function gitEnvironment(): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {
GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null",
GIT_CONFIG_NOSYSTEM: "1",
GIT_TERMINAL_PROMPT: "0",
GIT_OPTIONAL_LOCKS: "0",
LANG: "C",
LC_ALL: "C",
};
for (const name of ["PATH", "Path", "SystemRoot", "WINDIR", "COMSPEC", "PATHEXT", "TMP", "TEMP"]) {
if (process.env[name] !== undefined) env[name] = process.env[name];
}
return env;
}

function assertBoundedPath(path: string): void {
assertRelPath(path);
if (Buffer.byteLength(path, "utf8") > 1024) throw new Error("Changed path exceeds 1024 UTF-8 bytes");
}
Loading
Loading