/** * Orchestrator-side worker re-verification gate (T1589 / T1586). * * Closes lie #4 from HONEST-HANDOFF-2026-04-28.md: predecessor orchestrators * trusted subagent self-reports without re-running gates. This module * re-validates a worker's claim BEFORE the orchestrator accepts completion. * * Project-agnostic: uses canonical `tool:test` resolution per ADR-061 so * pnpm/npm/cargo/pytest/go all work identically. The test re-run is scoped * (T12962): `tool:test-affected` for the worker's change first, the full * `tool:test` only when affected planning refuses, and both go through the * ADR-061 cache, so a result the worker already recorded for the same tree * and resource limits is reused instead of re-run. Git operations go through * the standard `git` CLI. The audit log lives at the project's * `.cleo/audit/worker-mismatch.jsonl` (matches `force-bypass.jsonl` / * `contract-violations.jsonl` conventions). * * Wire-in: `packages/core/src/sentient/tick.ts` calls {@link reVerifyWorkerReport} * after `spawnResult.exitCode === 0` and before `writeSuccessReceipt`. A * rejection downgrades the success path to the failure-receipt path so the * task is not silently marked complete on false-success worker output. * * @task T1589 * @epic T1586 * @adr ADR-051 (evidence-based gate ritual) * @adr ADR-061 (project-agnostic tool resolution) */ import type { AffectedTestRun } from '../tasks/affected-packages.js'; import type { ToolRunResult } from '../tasks/tool-cache.js'; import { type ResolvedToolCommand, type ResolveToolResult } from '../tasks/tool-resolver.js'; /** * Self-reported outcome from a subagent worker. The orchestrator MUST NOT * trust any field here without re-verification through {@link reVerifyWorkerReport}. * * @task T1589 */ export interface WorkerReport { /** Task ID the worker claims to have completed. */ taskId: string; /** Worker's claimed success / failure outcome. */ selfReportSuccess: boolean; /** * Evidence atoms the worker captured (CLI `--evidence` syntax, e.g. * `tool:test`, `commit:;files:a.ts,b.ts`). Used for cross-checking; * re-verify always re-runs the tests (affected scope first, T12962) * regardless of what the worker claimed. */ evidenceAtoms: string[]; /** * Files the worker claims it touched, relative to project root. Compared * against `git status --porcelain` since the last commit on the working * branch. */ touchedFiles: string[]; /** * Absolute path of the worker's git worktree — the tree its tests must run * in. `undefined` asks {@link reVerifyWorkerReport} to locate it with * {@link resolveWorkerWorktree}; `null` declares it unknown, which runs the * full suite in the project root. * * @task T12962 */ worktreePath?: string | null; } /** * One mismatch between the worker's self-report and the re-verified ground * truth. Lives inside {@link WorkerMismatchAuditEntry.mismatches}. * * @task T1589 */ export interface WorkerMismatch { /** Which dimension failed: tests, files, or evidence. */ kind: 'tests' | 'files' | 'evidence'; /** What the worker claimed. */ claimed: string; /** What re-verification observed. */ actual: string; /** Short human-readable reason. */ reason: string; } /** * Append-only audit row written to `.cleo/audit/worker-mismatch.jsonl` when * {@link reVerifyWorkerReport} rejects. Each line is standalone JSON (same * convention as `force-bypass.jsonl`). * * @task T1589 */ export interface WorkerMismatchAuditEntry { /** ISO-8601 timestamp the mismatch was detected. */ timestamp: string; /** Task the worker claimed to have completed. */ taskId: string; /** Worker's claimed success boolean (echoed for audit). */ claimedSuccess: boolean; /** Files the worker claimed it touched. */ claimedFiles: string[]; /** Files git reports as modified (porcelain --short). */ actualFiles: string[]; /** Per-dimension mismatch records. */ mismatches: WorkerMismatch[]; } /** * Result of {@link reVerifyWorkerReport}. The orchestrator MUST treat * `accepted: false` as a failure and route the task into the retry/backoff * path (e.g. `writeFailureReceipt` in the sentient tick loop). * * @task T1589 */ export interface ReVerifyResult { /** True only when every re-verified dimension matches the worker's claim. */ accepted: boolean; /** * The verdict is not in yet: the only gap is a test run that could not * resolve its scope (test slot busy). Retry later; this is not a failed * attempt, and no mismatch audit row is written. * * @task T12962 */ pending?: true; /** Human-readable mismatch summaries (one per failed dimension). */ mismatches: string[]; /** Audit row written on a real rejection; `null` on acceptance or `pending`. */ auditEntry: WorkerMismatchAuditEntry | null; } /** Project-relative audit log path. Mirrors `.cleo/audit/force-bypass.jsonl`. */ export declare const WORKER_MISMATCH_AUDIT_FILE = ".cleo/audit/worker-mismatch.jsonl"; /** * Options for {@link reVerifyWorkerReport}. All fields except `projectRoot` * are injection seams used by unit tests to avoid spawning real subprocesses. * * @task T1589 */ export interface ReVerifyOptions { /** Absolute path to the project root (where `.cleo/` and `.git/` live). */ projectRoot: string; /** * Override the default test runner. Receives the project root and the * worker's worktree (`null` when unknown). Returns `{ ok: true }` when the * tests pass, `{ ok: false, reason }` otherwise. Tests inject a stub here; * production calls {@link defaultRunProjectTests}. */ runProjectTests?: (projectRoot: string, workerTree: string | null) => Promise; /** * Override how the worker's worktree is located when the report does not * carry one. Production calls {@link resolveWorkerWorktree}. * * @task T12962 */ resolveWorkerTree?: (projectRoot: string, taskId: string) => string | null; /** * Override the default `git status --porcelain` reader. Tests inject a * stub. Production calls {@link defaultListChangedFiles}. */ listChangedFiles?: (projectRoot: string) => Promise; } /** Outcome of running the project's canonical test command. */ export interface TestRunResult { ok: boolean; reason?: string; /** * Which run produced the verdict: `affected` (`tool:test-affected`) or * `full` (`tool:test`). Absent from injected stubs. * * @task T12962 */ scope?: 'affected' | 'full'; /** * The verdict is not in yet: the affected scope could not be resolved * because the test slot was busy. Retry later; never a full-suite fallback. * * @task T12962 */ pending?: true; } /** * Injection seams for {@link defaultRunProjectTests}. Production resolves each * from the evidence layer; tests inject stubs. * * @task T12962 */ export interface ProjectTestDeps { /** Plan the affected-scope run for the tree at `executionRoot`. */ planAffected?: (storeRoot: string, executionRoot: string) => Promise; /** Resolve the full `tool:test` command, reading scripts in `executionRoot`. */ resolveTest?: (storeRoot: string, executionRoot: string) => ResolveToolResult; /** Run a resolved command through the ADR-061 cache in `executionRoot`. */ runCached?: (command: ResolvedToolCommand, storeRoot: string, executionRoot: string) => Promise>; } /** * Locate the worker's git worktree for `taskId`. `null` means the worker's * tree is unknown. * * 1. The canonical `/worktrees///` checkout * (ADR-055). * 2. Otherwise a worktree registered for the task with `cleo worktree adopt` * (the `.cleo/worktrees.json` sentinel index, e.g. a Claude Code * `.claude/worktrees//` tree). Only an unambiguous match counts: two * adopted trees for one task is unknown, never a guess. * * Either way the directory must be a checkout's toplevel. * * @param projectRoot - Project root the worker was spawned from. * @param taskId - The worker's task. * @returns Absolute worktree path, or `null`. * * @task T12962 */ export declare function resolveWorkerWorktree(projectRoot: string, taskId: string): string | null; /** * Default test runner for worker re-verification (T1589, scoped by T12962). * * The tests run in the WORKER's tree, never in whatever directory the daemon * happens to run from: an affected diff taken in the main checkout would * measure unrelated edits there and could accept an untested change. * * - Worker tree known: plan the affected scope there (the packages its * branch diff touches plus their dependents) with the scope-aware * `tool:test` planner (T12959, no merge check). Run the full `tool:test` in * that tree only when the planner decides on the full suite (no * `testing.affectedCommand`, `testing.preferAffected: false`, no default * branch, a root-config change, an untested dependent, an empty or * unresolvable scope). A failing affected run is the verdict; it never * escalates. A busy test slot is not a refusal: the result is a retryable * rejection, never a full run. * - Worker tree unknown (`null`): the full `tool:test` in `projectRoot`. * Affected tests never run on a tree that is not the worker's. * * Every run goes through the ADR-061 cache, so a result the worker already * recorded for the same tree and resource limits is reused, not re-run. This * used to run a full `tool:test` for every worker exit, whatever the worker * claimed. * * @param projectRoot - CLEO store root. * @param workerTree - The worker's worktree, or `null` when unknown. * @param deps - Injection seams; production defaults when omitted. * @returns The verdict and which scope produced it. * * @task T1589 * @task T12959 * @task T12962 * @adr ADR-061 */ export declare function defaultRunProjectTests(projectRoot: string, workerTree?: string | null, deps?: ProjectTestDeps): Promise; /** * Default git-status reader. Returns the list of paths reported by * `git status --porcelain` since the last commit. Used to fact-check the * worker's `touchedFiles` claim. * * @task T1589 */ export declare function defaultListChangedFiles(projectRoot: string): Promise; /** * Re-verify a subagent worker's self-report against ground truth. * * Performs three independent checks and rejects on any hard-evidence * mismatch: * * 1. **Test status** — runs the tests in the worker's worktree (see * {@link defaultRunProjectTests}: affected first, the full `tool:test` on a * planning refusal or when the worker's tree is unknown), and compares the exit code against the worker's `selfReportSuccess` claim. * Worker says success but tests fail → reject. * 2. **Touched files** — compares `touchedFiles` against `git status * --porcelain`. Sets must match exactly (order-independent). Counts * matter: worker says 3 but git shows 5 → reject. * 3. **Evidence atoms** — sanity-check that the worker actually captured * evidence (non-empty list when claiming success). Does NOT re-validate * every atom (that's `revalidateEvidence`'s job at `cleo complete`). * * On rejection, writes one append-only line to `.cleo/audit/worker-mismatch.jsonl` * with the full claimed-vs-actual diff. The audit write is best-effort; an * error there does not change the rejection verdict. * * @param report - The worker's self-report (untrusted input). * @param options - Project root + injectable test/git seams. * @returns Acceptance verdict + machine-readable mismatch detail. * * @task T1589 * @epic T1586 * * @example * ```ts * const result = await reVerifyWorkerReport( * { taskId: 'T123', selfReportSuccess: true, * evidenceAtoms: ['tool:test'], touchedFiles: ['src/a.ts'] }, * { projectRoot: '/path/to/project' }, * ); * if (!result.accepted) throw new Error(result.mismatches.join('; ')); * ``` */ export declare function reVerifyWorkerReport(report: WorkerReport, options: ReVerifyOptions): Promise; /** * Append one {@link WorkerMismatchAuditEntry} to `.cleo/audit/worker-mismatch.jsonl`. * * Errors are swallowed: an audit-write failure must never change the * rejection verdict (matches `appendOwnerOverrideAudit` / * `appendContractViolation`). * * @internal */ export declare function appendWorkerMismatchAudit(projectRoot: string, entry: WorkerMismatchAuditEntry): void; //# sourceMappingURL=worker-verify.d.ts.map