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
8 changes: 8 additions & 0 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@
"./replay": {
"types": "./src/facades/replay.ts",
"default": "./src/facades/replay.ts"
},
"./divergence": {
"types": "./src/facades/divergence.ts",
"default": "./src/facades/divergence.ts"
},
"./progress": {
"types": "./src/facades/progress.ts",
"default": "./src/facades/progress.ts"
}
}
}
1 change: 1 addition & 0 deletions packages/contracts/src/facades/divergence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '../replay-divergence.ts';
1 change: 1 addition & 0 deletions packages/contracts/src/facades/progress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '../request-progress.ts';
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
REPLAY_DIVERGENCE_SUGGESTION_LIMIT,
truncateUtf8Field,
type ReplayDivergence,
} from '../divergence.ts';
} from './replay-divergence.ts';

function buildDivergence(overrides: Partial<ReplayDivergence> = {}): ReplayDivergence {
return {
Expand Down Expand Up @@ -239,7 +239,7 @@ test('sanitizeReplayDivergenceField redacts sensitive content even when no trunc
// --- Text report carries the repair data (bounded refs + unavailable hint) ---

test('formatReplayDivergenceReport lists a bounded ref/role/label subset for an available screen', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand Down Expand Up @@ -272,7 +272,7 @@ test('formatReplayDivergenceReport lists a bounded ref/role/label subset for an
});

test('formatReplayDivergenceReport carries the unavailable-screen hint', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand All @@ -295,7 +295,7 @@ test('formatReplayDivergenceReport carries the unavailable-screen hint', async (
});

test('scrubReplayVarValues replaces every occurrence with a named marker, longest value first', async () => {
const { scrubReplayVarValues } = await import('../divergence.ts');
const { scrubReplayVarValues } = await import('./replay-divergence.ts');
const entries = [
{ name: 'LONG', value: 'abc-def' },
{ name: 'SHORT', value: 'abc' },
Expand Down Expand Up @@ -414,7 +414,7 @@ test('boundReplayDivergence keeps targetBinding on the minimal overflow fallback
});

test('formatReplayDivergenceReport renders matchCount, mismatches, and candidates for a target-binding divergence', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand Down Expand Up @@ -442,7 +442,7 @@ test('formatReplayDivergenceReport renders matchCount, mismatches, and candidate
});

test('formatReplayDivergenceReport lists candidates for an identity-unverifiable target-binding divergence', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand Down Expand Up @@ -477,7 +477,7 @@ test('formatReplayDivergenceReport lists candidates for an identity-unverifiable
// refused — it surfaces `resume.reason` instead. ---

test('formatReplayDivergenceReport embeds the concrete resume command for an allowed record-and-heal divergence', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand All @@ -502,7 +502,7 @@ test('formatReplayDivergenceReport embeds the concrete resume command for an all
});

test('formatReplayDivergenceReport never renders a --from command when resume is NOT allowed, surfacing the reason instead', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand Down Expand Up @@ -532,7 +532,7 @@ test('formatReplayDivergenceReport never renders a --from command when resume is
});

test('formatReplayDivergenceReport falls back to a generic non-resumable sentence when resume carries no reason', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand Down Expand Up @@ -561,7 +561,7 @@ test('formatReplayDivergenceReport falls back to a generic non-resumable sentenc
// NEVER re-derives resumability, so text and structured wire never disagree. ---

test('formatReplayDivergenceReport embeds BOTH concrete resume commands for a caution divergence whose wire carries alternateFrom', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand Down Expand Up @@ -594,7 +594,7 @@ test('formatReplayDivergenceReport embeds BOTH concrete resume commands for a ca
});

test('formatReplayDivergenceReport embeds BOTH concrete resume commands for a manual divergence whose wire carries alternateFrom', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand Down Expand Up @@ -622,7 +622,7 @@ test('formatReplayDivergenceReport embeds BOTH concrete resume commands for a ma
});

test('formatReplayDivergenceReport renders ONLY the state-fix command for a caution divergence WITHOUT alternateFrom (the diverged step is not skip-safe)', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
// resume.allowed is true (resuming AT N is fine), but the diverged step N
// is a runScript/control-flow action, so `--from N + 1` would be refused —
// the daemon omits alternateFrom, and the text must NOT offer `--from N + 1`.
Expand Down Expand Up @@ -654,7 +654,7 @@ test('formatReplayDivergenceReport renders ONLY the state-fix command for a caut
});

test('formatReplayDivergenceReport renders neither caution command when resume is NOT allowed', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand Down Expand Up @@ -695,7 +695,7 @@ const REPAIR_DIAGNOSTICS_CLAUSE_PATTERN =
/Read-only inspection while armed \(snapshot -i, get attrs, find, is\) is excluded from the healed script by default — no --no-record needed\. If the step you are repairing is itself a read, add --record to that command so it lands in the heal\./;

test('formatReplayDivergenceReport appends the diagnostics default-exclusion clause for record-and-heal when repairSessionHeld is true', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand All @@ -718,7 +718,7 @@ test('formatReplayDivergenceReport appends the diagnostics default-exclusion cla
});

test('formatReplayDivergenceReport OMITS the diagnostics clause for record-and-heal when repairSessionHeld is absent (plain, non-repair divergence)', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand All @@ -741,7 +741,7 @@ test('formatReplayDivergenceReport OMITS the diagnostics clause for record-and-h
});

test('formatReplayDivergenceReport appends the diagnostics clause for state-repair when armed, distinct from the existing app-state --no-record clause', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand All @@ -765,7 +765,7 @@ test('formatReplayDivergenceReport appends the diagnostics clause for state-repa
});

test('formatReplayDivergenceReport OMITS the diagnostics clause for state-repair when repairSessionHeld is absent', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand All @@ -786,7 +786,7 @@ test('formatReplayDivergenceReport OMITS the diagnostics clause for state-repair
});

test('formatReplayDivergenceReport appends the diagnostics clause for caution when armed, alongside the dual-path resume commands', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand Down Expand Up @@ -816,7 +816,7 @@ test('formatReplayDivergenceReport appends the diagnostics clause for caution wh
});

test('formatReplayDivergenceReport OMITS the diagnostics clause for caution when repairSessionHeld is absent', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand All @@ -836,7 +836,7 @@ test('formatReplayDivergenceReport OMITS the diagnostics clause for caution when
});

test('formatReplayDivergenceReport appends the diagnostics clause for manual when armed, even when resume is NOT allowed', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
// `repairSessionHeld` reports the daemon KEPT THE SESSION LIVE, independent
// of `resume.allowed` (plan-resumability) — the diagnostics clause must
// still render here: the agent may still inspect the (held) session while
Expand Down Expand Up @@ -868,7 +868,7 @@ test('formatReplayDivergenceReport appends the diagnostics clause for manual whe
});

test('formatReplayDivergenceReport OMITS the diagnostics clause for manual when repairSessionHeld is absent', async () => {
const { formatReplayDivergenceReport } = await import('../divergence.ts');
const { formatReplayDivergenceReport } = await import('./replay-divergence.ts');
const report = formatReplayDivergenceReport({
divergence: {
version: 1,
Expand Down
File renamed without changes.
58 changes: 58 additions & 0 deletions packages/contracts/src/request-progress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Progress events the daemon streams to a connected client while a request runs.
*
* These are wire values: `src/daemon/request-progress-protocol.ts` serializes them onto the
* response stream and the CLI reconstructs them before handing them to the replay-test
* reporter registry. They therefore belong to neither side — the daemon's request-global
* progress plumbing carries them, and the replay-test scheduler produces and interprets
* them, so the vocabulary sits below both.
*/

export type ReplayTestSuiteProgressEvent = {
type: 'replay-test-suite';
status: 'start';
total: number;
runnable: number;
skipped: number;
artifactsDir: string;
shardMode?: 'all' | 'split';
shardCount?: number;
};

export type ReplayTestProgressEvent = {
type: 'replay-test';
file: string;
title?: string;
status: 'start' | 'progress' | 'pass' | 'fail' | 'skip';
index: number;
total: number;
stepIndex?: number;
stepTotal?: number;
stepCommand?: string;
stepValue?: string;
attempt?: number;
maxAttempts?: number;
durationMs?: number;
retrying?: boolean;
message?: string;
hint?: string;
session?: string;
artifactsDir?: string;
shardIndex?: number;
shardCount?: number;
deviceId?: string;
deviceName?: string;
};

export type CommandProgressEvent = {
type: 'command';
status: 'progress';
message: string;
};

export type RequestProgressEvent =
| ReplayTestSuiteProgressEvent
| ReplayTestProgressEvent
| CommandProgressEvent;

export type RequestProgressSink = (event: RequestProgressEvent) => void;
27 changes: 13 additions & 14 deletions scripts/layering/daemon-modularity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,21 +128,20 @@ test('replay-test may still import its own files inside the wider replay engine
assert.deepEqual(checkDaemonModularityRatchets([...baselineEdges(), ...edges], []), []);
});

test('recorded replay-test migration imports are exempt until the import is deleted', () => {
const recorded = LOGICAL_MODULE_POLICIES.find(
({ name }) => name === 'replay-test',
)?.recordedMigrationImports;
assert.deepEqual(recorded, [
'src/replay/test/reporters/default.ts -> src/replay/divergence.ts',
'src/replay/test/reporters/progress.ts -> src/request/progress.ts',
'src/replay/test/reporters/registry.ts -> src/request/progress.ts',
'src/replay/test/reporting.ts -> src/request/progress.ts',
]);
// #1478 P3 cleared every recorded replay-test migration import: the ADR 0012 divergence
// vocabulary became a neutral contracts leaf, and the reporter tree now reads the progress
// wire vocabulary from contracts instead of request-global plumbing. The rule enforces
// unconditionally for replay-test from here on.
test('replay-test carries no recorded migration imports', () => {
assert.equal(
LOGICAL_MODULE_POLICIES.find(({ name }) => name === 'replay-test')?.recordedMigrationImports,
undefined,
);
assert.deepEqual(
LOGICAL_MODULE_POLICIES.flatMap((module) => module.recordedMigrationImports ?? []),
[],
);
assert.deepEqual(checkDaemonModularityRatchets(baselineEdges(), []), []);

const withoutOne = checkDaemonModularityRatchets(baselineEdges().slice(0, -1), []);
assert.equal(withoutOne.length, 1);
assert.match(withoutOne[0]!.message, /delete it from replay-test's recordedMigrationImports/);
});

test('internal trees reject deep imports globally, including from daemon', () => {
Expand Down
6 changes: 0 additions & 6 deletions scripts/layering/daemon-modularity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,6 @@ export const LOGICAL_MODULE_POLICIES: readonly LogicalModulePolicy[] = [
'src/maestro/',
'src/ad-replay/',
],
recordedMigrationImports: [
'src/replay/test/reporters/default.ts -> src/replay/divergence.ts',
'src/replay/test/reporters/progress.ts -> src/request/progress.ts',
'src/replay/test/reporters/registry.ts -> src/request/progress.ts',
'src/replay/test/reporting.ts -> src/request/progress.ts',
],
},
];

Expand Down
2 changes: 2 additions & 0 deletions scripts/layering/package-boundaries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ const CONTRACT_EXPORTS = [
'@agent-device/contracts/client',
'@agent-device/contracts/command',
'@agent-device/contracts/device',
'@agent-device/contracts/divergence',
'@agent-device/contracts/interaction',
'@agent-device/contracts/observability',
'@agent-device/contracts/platform',
'@agent-device/contracts/progress',
'@agent-device/contracts/recording',
'@agent-device/contracts/remote',
'@agent-device/contracts/replay',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ vi.mock('../../../core/dispatch.ts', async (importOriginal) => {
import { dispatchCommand } from '../../../core/dispatch.ts';
import { makeIosSession } from '../../../__tests__/test-utils/session-factories.ts';
import type { SnapshotState } from '@agent-device/kernel/snapshot';
import type { ReplayDivergence } from '../../../replay/divergence.ts';
import type { ReplayDivergence } from '@agent-device/contracts/divergence';
import { bindInternalObservationAuthority } from '../../internal-observation.ts';
import { expireRefFrame } from '../../ref-frame.ts';
import { markSessionPartialRefsIssued, setSessionSnapshot } from '../../session-snapshot.ts';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { SessionStore } from '../../session-store.ts';
import type { DaemonResponse } from '../../types.ts';
import { dispatchCommand } from '../../../core/dispatch.ts';
import { makeIosSession } from '../../../__tests__/test-utils/session-factories.ts';
import { formatReplayDivergenceReport } from '../../../replay/divergence.ts';
import { formatReplayDivergenceReport } from '@agent-device/contracts/divergence';
import {
baseReplayRequest as baseReq,
writeReplayFile,
Expand Down
5 changes: 3 additions & 2 deletions src/daemon/handlers/__tests__/session-test-artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
materializeReplayTestAttemptArtifacts,
prepareReplayTestAttemptArtifacts,
} from '../session-test-artifacts.ts';
import { toReplayTestAttemptOutcome } from '../session-test-outcome.ts';
import type { DaemonResponse } from '../../types.ts';

test('materializeReplayTestAttemptArtifacts writes replay and result manifests for passing attempts', () => {
Expand All @@ -27,7 +28,7 @@ test('materializeReplayTestAttemptArtifacts writes replay and result manifests f
},
};
materializeReplayTestAttemptArtifacts({
response,
outcome: toReplayTestAttemptOutcome(response),
filePath: replayPath,
sessionName: 'default:test:suite:1',
attempts: 1,
Expand Down Expand Up @@ -83,7 +84,7 @@ test('materializeReplayTestAttemptArtifacts writes failure manifest and copies l
},
};
materializeReplayTestAttemptArtifacts({
response,
outcome: toReplayTestAttemptOutcome(response),
filePath: replayPath,
sessionName: 'default:test:suite:2',
attempts: 2,
Expand Down
Loading
Loading