fix(cli): Restore-Packet M2c-CLI R2 — --source-jsonl provenance from parsed session_meta

Closes guard BLOCK on M2c-CLI at e5de3ab. Per dispatch
qitem-20260502014948-af2d4713 + BLOCK artifact at
substrate/rigs/openrig-velocity/state/orch/restore-packet-M2c-cli-guard-checkpoint-block-2026-05-02.md.

Bug (R1 / e5de3ab):
- `--source-jsonl` adapter wrote the JSONL FILE PATH into
  restore-summary.json.source_session_id (not the parsed session id).
- packet-writer.ts:174-175 silently derived source_rig from sourceSessionId
  via @ split, masking the bug as `source_rig: "unknown"` for any value
  without @ shape.
- Schema-valid but semantically WRONG; exactly the source-provenance
  drift the restore-packet standard exists to prevent. Writer-internal
  tests bypassed the parse-to-write provenance path entirely.

Fix:
1. CLI adapter (commands/restore-packet.ts): after parseCodexJsonl /
   parseClaudeTranscript, derive canonical (source_session_id, source_rig)
   honestly via new deriveProvenance() helper.
   - For --source-jsonl: prefer parsed structured.sessionMeta?.sessionId
     over the file path; fail with explicit operator guidance if absent
     and no override flag supplied.
   - source_rig: <seat>@<rig> regex split; fail if shape doesn't match
     and no override flag supplied. No silent "unknown" fallback.
2. New flags --source-session-id-override and --source-rig-override give
   operators an explicit honest path when source JSONL has no session_meta
   (e.g., raw test fixtures, mid-session captures) or session id is bare.
3. packet-writer.ts: WritePacketOptions adds required `sourceRig: string`.
   Writer no longer parses sourceSessionId — that derivation moved to the
   adapter where it belongs.

Tests (TDD red-then-green; 5 new R2 cases at the CLI surface using
createProgram().parseAsync with synthetic JSONL fixtures, exercising the
parse-to-writePacket adapter path that 75/75 was bypassing):
- guard reproducer: --source-jsonl with session_meta sets
  source_session_id from parsed id and source_rig from <seat>@<rig> split.
- --source-jsonl with NO session_meta and NO overrides fails with
  explicit operator guidance referencing both override flags.
- --source-jsonl with NO session_meta + both overrides succeeds with
  overridden provenance.
- --source-jsonl with bare-id session_meta fails without
  --source-rig-override (no silent unknown).
- --source-jsonl with --source-session-id-override overrides parsed
  session_meta id (override-wins precedence).

Updates to existing tests:
- M2c-CLI atomic-emission, Velocity-shape round-trip, M2b R2 nested-content
  fixtures pass `sourceRig:` explicitly to writePacket() (now required).
- Mock-daemon --source-session round-trip uses --source-rig-override
  for bare "src-session" fixture and asserts source_rig is honored.

Re-verification (all green; honest baseline match per Quality Lesson v8):
- npx vitest run test/restore-packet.test.ts → 80/80 pass (75 baseline + 5 R2).
- npx tsc --noEmit → exit 0.
- npm run build → exit 0; dist/schemas/restore-summary.schema.json present.
- Compiled-output guard reproducer (BLOCK artifact verbatim) →
  source_session_id="velocity-driver@openrig-velocity",
  source_rig="openrig-velocity", validation_valid=true.
- Full CLI suite → 720/724; 4 failures match named flake baseline
  (chatroom wait timing, config-store env pollution, two preflight
  host-port). No regression vs 715/719 + same 4 flakes pre-R2.

M2c-Daemon surfaces (packages/daemon/src/routes/transcripts.ts +
packages/daemon/test/transcript-routes.test.ts) NOT touched per orch
sub-chunking. Velocity prior-art .mjs UNCHANGED. M3 read/validate stubs
preserved. No auth tokens or credentials in fixtures (Quality Lesson v9).

Quality Lesson v12 (NEW; filed at this dispatch): writer-internal round
trips are not enough for CLI source adapters — slices that claim adapter
functionality MUST include at least one compiled-output CLI-path
provenance check that exercises the actual command surface
(createProgram().parseAsync against synthetic JSONL with realistic
session_meta). Filed inline at this commit; will be formalized at M2
marshal-acceptance close.

Posture: full QA/testing window (matches M2 declared posture).
Dispatch: qitem-20260502014948-af2d4713.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-01 19:01:08 -07:00
1 parent e5de3abd12
commit a8401f6181
3 files changed
+350 -5

No files matched your search

+90 -4
View File
@@ -30,6 +30,19 @@ interface WriteOptions {
sourceSession?: string;
sourceJsonl?: string;
sourceRuntime?: string;
/**
* R2: explicit operator override for source_session_id. Wins over parsed
* session_meta and over the --source-session arg. Required when JSONL
* has no session_meta record.
*/
sourceSessionIdOverride?: string;
/**
* R2: explicit operator override for source_rig. Wins over <seat>@<rig>
* derivation. Required when the canonical source_session_id is bare
* (no @) and the operator has not supplied --source-session-id-override
* with @ shape.
*/
sourceRigOverride?: string;
target: string;
targetRig?: string;
targetRuntime?: string;
@@ -44,6 +57,53 @@ interface WriteOptions {
generatorVersion?: string;
}
/**
* R2: derive canonical (source_session_id, source_rig) per honest-fallback
* policy. Override flags > parsed session_meta (jsonl) > supplied --source-session.
*
* Returns either the derived pair (with no exception) OR throws an Error
* containing the explicit operator-facing guidance message. The action
* caller catches and routes to reportFailure().
*/
function deriveProvenance(
opts: WriteOptions,
parsedSessionId: string | null,
hasJsonl: boolean,
): { sourceSessionId: string; sourceRig: string } {
// Step 1: source_session_id — override > parsed > --source-session arg.
let sourceSessionId: string;
if (typeof opts.sourceSessionIdOverride === "string" && opts.sourceSessionIdOverride.length > 0) {
sourceSessionId = opts.sourceSessionIdOverride;
} else if (hasJsonl) {
if (!parsedSessionId) {
throw new Error(
"no session_meta record in source JSONL; supply --source-session-id-override <id> AND --source-rig-override <rig> to set provenance explicitly. Silent fallback to file-path-as-session-id is not allowed (M2c-CLI R2 honest-provenance policy).",
);
}
sourceSessionId = parsedSessionId;
} else {
// hasSession path: operator-named session is authoritative.
sourceSessionId = opts.sourceSession!;
}
// Step 2: source_rig — override > <seat>@<rig> split.
let sourceRig: string;
if (typeof opts.sourceRigOverride === "string" && opts.sourceRigOverride.length > 0) {
sourceRig = opts.sourceRigOverride;
} else {
const m = /^[^@]+@([^@]+)$/.exec(sourceSessionId);
const captured = m?.[1];
if (!captured) {
throw new Error(
`could not derive source_rig from session id '${sourceSessionId}' (does not match <seat>@<rig> shape); supply --source-rig-override <rig> explicitly. Silent fallback to source_rig:"unknown" is not allowed (M2c-CLI R2 honest-provenance policy).`,
);
}
sourceRig = captured;
}
return { sourceSessionId, sourceRig };
}
function reportFailure(message: string): void {
console.error(`rig restore-packet write: ${message}`);
process.exitCode = 2;
@@ -92,6 +152,7 @@ function buildWritePacketOptions(
sourceRuntime: SourceRuntime,
sourceCwdFallback: string,
sourceSessionId: string,
sourceRig: string,
): WritePacketOptions {
const trustRankingDefault = "rig_whoami,bounded_latest_transcript";
const trustRankingRaw = opts.sourceTrustRanking ?? trustRankingDefault;
@@ -115,6 +176,7 @@ function buildWritePacketOptions(
authorityBoundaries: opts.authorityBoundaries ?? "Restored seat authority per source session role.",
sourceTrustRanking: trustRanking,
sourceSessionId,
sourceRig,
sourceCwd: structured.sessionMeta?.cwd ?? sourceCwdFallback,
generatorVersion: opts.generatorVersion ?? "[email protected]",
includeFullTranscript: structured.messages.length > 0,
@@ -187,6 +249,14 @@ export function restorePacketCommand(depsOverride?: RestorePacketDeps): Command
"--generator-version <version>",
'Generator version identifier (default: "[email protected]")',
)
.option(
"--source-session-id-override <id>",
"Operator override for source_session_id (R2; required when JSONL has no session_meta record)",
)
.option(
"--source-rig-override <rig>",
"Operator override for source_rig (R2; required when canonical session id is bare with no <seat>@<rig> shape)",
)
.action(async (opts: WriteOptions) => {
const hasSession = typeof opts.sourceSession === "string" && opts.sourceSession.length > 0;
const hasJsonl = typeof opts.sourceJsonl === "string" && opts.sourceJsonl.length > 0;
@@ -206,18 +276,15 @@ export function restorePacketCommand(depsOverride?: RestorePacketDeps): Command
try {
let content: string;
let sourceCwdFallback: string;
let sourceSessionId: string;
if (hasJsonl) {
const jsonlPath = resolvePath(opts.sourceJsonl!);
content = readFileSync(jsonlPath, "utf-8");
sourceCwdFallback = "/";
sourceSessionId = jsonlPath;
} else {
const session = opts.sourceSession!;
const fetched = await fetchSourceTranscriptViaDaemon(session, depsOverride);
content = fetched.content;
sourceCwdFallback = fetched.sourceCwd ?? "/";
sourceSessionId = session;
}
let runtime: SourceRuntime | null = null;
@@ -242,7 +309,26 @@ export function restorePacketCommand(depsOverride?: RestorePacketDeps): Command
? parseCodexJsonl(content)
: parseClaudeTranscript(content);
const writeOpts = buildWritePacketOptions(opts, structured, runtime, sourceCwdFallback, sourceSessionId);
// R2: derive (source_session_id, source_rig) honestly. Override
// flags > parsed session_meta (jsonl) > supplied --source-session
// arg. Throws an Error with explicit operator guidance when the
// honest-provenance policy can't be satisfied.
let provenance: { sourceSessionId: string; sourceRig: string };
try {
provenance = deriveProvenance(opts, structured.sessionMeta?.sessionId ?? null, hasJsonl);
} catch (err) {
reportFailure((err as Error).message);
return;
}
const writeOpts = buildWritePacketOptions(
opts,
structured,
runtime,
sourceCwdFallback,
provenance.sourceSessionId,
provenance.sourceRig,
);
const result = await writePacket(writeOpts);
// Per Quality Lesson v9: log only metadata, NOT transcript content.
@@ -57,6 +57,13 @@ export interface WritePacketOptions {
sourceTrustRanking: string[];
/** Source session id. */
sourceSessionId: string;
/**
* Source rig name. Per M2c-CLI R2: passed explicitly by the CLI adapter
* (computed from parsed session_meta `<seat>@<rig>` split or operator
* --source-rig-override). The writer does NOT derive this from
* sourceSessionId — that hidden derivation was the M2c-CLI R1 bug.
*/
sourceRig: string;
/** Source cwd (absolute path). */
sourceCwd: string;
/** Generator version identifier (e.g., "[email protected]"). */
@@ -172,7 +179,7 @@ function buildSummary(opts: WritePacketOptions): Record<string, unknown> {
const messageCount = Math.min(opts.structured.messages.length, TRANSCRIPT_BOUND);
const summary: Record<string, unknown> = {
source_session_id: opts.sourceSessionId,
source_rig: opts.sourceSessionId.includes("@") ? opts.sourceSessionId.split("@")[1] ?? "unknown" : "unknown",
source_rig: opts.sourceRig,
source_runtime: opts.sourceRuntime,
source_cwd: opts.sourceCwd,
target_rig: opts.targetRig,
+252
View File
@@ -861,6 +861,7 @@ describe("M2c-CLI packet-writer atomic emission", () => {
authorityBoundaries: "May edit packages/cli/ and packages/daemon/ within M2c boundary; no M3+ surfaces.",
sourceTrustRanking: ["rig_whoami", "bounded_latest_transcript"],
sourceSessionId: "velocity-driver@openrig-velocity",
sourceRig: "openrig-velocity",
sourceCwd: "/Users/wrandom/code/projects/openrig-hub",
generatorVersion: "[email protected]",
includeFullTranscript: false,
@@ -1046,6 +1047,7 @@ describe("M2c-CLI Velocity-shape round-trip (synthetic 4-role fixtures)", () =>
authorityBoundaries: `${role} authority for Velocity-replay context.`,
sourceTrustRanking: ["rig_whoami", "bounded_latest_transcript"],
sourceSessionId: `velocity-${role}@openrig-velocity-code`,
sourceRig: "openrig-velocity-code",
sourceCwd: "/Users/wrandom/code/projects/openrig-hub",
generatorVersion: "[email protected]",
includeFullTranscript: true,
@@ -1127,6 +1129,11 @@ describe("M2c-CLI redaction + omitted-record round-trip end-to-end", () => {
"--target", targetDir,
"--target-rig", "openrig-velocity-claude",
"--target-runtime", "claude-code",
// src-session is bare (no @); operator must supply --source-rig-override
// per R2 honest-fallback policy.
"--source-rig-override", "openrig-test",
"--current-work-summary", "mock-daemon round-trip with bare session id + rig override.",
"--authority-boundaries", "test-only.",
]);
// Daemon was queried at the new full-read route; no mutation methods called.
@@ -1139,6 +1146,8 @@ describe("M2c-CLI redaction + omitted-record round-trip end-to-end", () => {
const validation = validateRestoreSummary(summary);
expect(validation.valid, JSON.stringify(validation.errors)).toBe(true);
expect(summary.source_session_id).toBe("src-session");
// R2: operator-supplied --source-rig-override is honored.
expect(summary.source_rig).toBe("openrig-test");
});
it("M2b R2 nested Claude content fixture: round-trips with non-zero omittedCounts in summary", async () => {
@@ -1185,6 +1194,7 @@ describe("M2c-CLI redaction + omitted-record round-trip end-to-end", () => {
authorityBoundaries: "Velocity authority.",
sourceTrustRanking: ["rig_whoami"],
sourceSessionId: "claude-session-id",
sourceRig: "openrig-velocity-claude",
sourceCwd: "/Users/wrandom/code/projects/openrig-hub",
generatorVersion: "[email protected]",
includeFullTranscript: false,
@@ -1198,3 +1208,245 @@ describe("M2c-CLI redaction + omitted-record round-trip end-to-end", () => {
p.path === "/Users/wrandom/code/projects/openrig-hub")).toBe(true);
});
});
// ─────────────────────────────────────────────────────────────────────
// M2c-CLI R2 — `--source-jsonl` provenance fix.
//
// Guard BLOCKED M2c-CLI at openrig e5de3ab because the `--source-jsonl`
// adapter wrote the JSONL FILE PATH into `restore-summary.json.source_session_id`
// and forced `source_rig: "unknown"`, EVEN WHEN the parsed Codex JSONL had
// `session_meta.payload.id = "<seat>@<rig>"` available.
//
// These tests exercise the CLI command surface end-to-end (Quality Lesson
// v12 candidate) — they parseAsync through createProgram() against synthetic
// JSONL fixtures with realistic session_meta records, so the bug between
// parse and writePacket call IS exercised. Writer-internal tests bypass
// this path by passing manually-constructed sourceSessionId.
// ─────────────────────────────────────────────────────────────────────
describe("M2c-CLI R2 --source-jsonl provenance from parsed session_meta", () => {
let tmpRoot: string;
beforeEach(async () => {
const fs = await import("node:fs");
const os = await import("node:os");
const path = await import("node:path");
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "restore-packet-r2-"));
});
afterEach(async () => {
const fs = await import("node:fs");
fs.rmSync(tmpRoot, { recursive: true, force: true });
});
// The exact reproducer fixture from the guard's BLOCK artifact.
function jsonlWithSessionMeta(): string {
return [
JSON.stringify({
type: "session_meta",
payload: {
cwd: "/Users/wrandom/code/projects/openrig-hub",
id: "velocity-driver@openrig-velocity",
},
}),
JSON.stringify({
type: "response_item",
payload: {
type: "message",
role: "user",
content: "hello direct source",
},
}),
].join("\n");
}
function jsonlWithoutSessionMeta(): string {
return [
JSON.stringify({
type: "response_item",
payload: {
type: "message",
role: "user",
content: "no session_meta in this JSONL",
},
}),
].join("\n");
}
function jsonlWithBareSessionMetaId(): string {
// session_meta exists but the id is a bare token, not <seat>@<rig> shape.
return [
JSON.stringify({
type: "session_meta",
payload: { cwd: "/x", id: "bare-id-no-at" },
}),
JSON.stringify({
type: "response_item",
payload: { type: "message", role: "user", content: "hi" },
}),
].join("\n");
}
it("guard reproducer: --source-jsonl with session_meta sets source_session_id from parsed id and source_rig from <seat>@<rig> split", async () => {
const { createProgram } = await import("../src/index.js");
const fs = await import("node:fs");
const path = await import("node:path");
const src = path.join(tmpRoot, "source.jsonl");
fs.writeFileSync(src, jsonlWithSessionMeta(), "utf8");
const target = path.join(tmpRoot, "packet");
const program = createProgram();
program.exitOverride();
await program.parseAsync([
"node", "rig", "restore-packet", "write",
"--source-jsonl", src,
"--target", target,
"--target-rig", "openrig-velocity-claude",
"--target-runtime", "claude-code",
"--current-work-summary", "R2 guard reproducer: provenance from parsed session_meta.",
"--authority-boundaries", "R2 provenance check only.",
]);
expect(fs.existsSync(target)).toBe(true);
const summary = JSON.parse(fs.readFileSync(path.join(target, "restore-summary.json"), "utf-8"));
const validation = validateRestoreSummary(summary);
expect(validation.valid, JSON.stringify(validation.errors)).toBe(true);
// Bug reproducer's expected values per BLOCK artifact:
expect(summary.source_session_id).toBe("velocity-driver@openrig-velocity");
expect(summary.source_rig).toBe("openrig-velocity");
expect(summary.source_cwd).toBe("/Users/wrandom/code/projects/openrig-hub");
// session_session_id MUST NOT be the file path.
expect(summary.source_session_id).not.toContain("/source.jsonl");
expect(summary.source_session_id).not.toContain(tmpRoot);
// transcript.md emitted (messageCount > 0 → includeFullTranscript true).
expect(fs.existsSync(path.join(target, "transcript.md"))).toBe(true);
});
it("--source-jsonl with NO session_meta and NO override flags fails with explicit guidance", async () => {
const { createProgram } = await import("../src/index.js");
const fs = await import("node:fs");
const path = await import("node:path");
const src = path.join(tmpRoot, "no-session.jsonl");
fs.writeFileSync(src, jsonlWithoutSessionMeta(), "utf8");
const target = path.join(tmpRoot, "packet-no-session");
const stderr: string[] = [];
const origConsoleError = console.error;
const origExitCode = process.exitCode;
console.error = (...args: unknown[]) => stderr.push(args.join(" "));
process.exitCode = undefined;
try {
const program = createProgram();
program.exitOverride();
await program.parseAsync([
"node", "rig", "restore-packet", "write",
"--source-jsonl", src,
"--target", target,
"--target-rig", "openrig-velocity-claude",
"--target-runtime", "claude-code",
"--current-work-summary", "no-session-meta error path.",
"--authority-boundaries", "R2 provenance check only.",
]);
} finally {
console.error = origConsoleError;
}
expect(process.exitCode).not.toBe(0);
process.exitCode = origExitCode;
const errStr = stderr.join("\n");
expect(errStr).toMatch(/session.meta|--source-session-id-override|--source-rig-override|provenance/i);
// No partial packet written.
expect(fs.existsSync(target)).toBe(false);
});
it("--source-jsonl with NO session_meta + both overrides succeeds with overridden provenance", async () => {
const { createProgram } = await import("../src/index.js");
const fs = await import("node:fs");
const path = await import("node:path");
const src = path.join(tmpRoot, "no-session-with-overrides.jsonl");
fs.writeFileSync(src, jsonlWithoutSessionMeta(), "utf8");
const target = path.join(tmpRoot, "packet-overrides");
const program = createProgram();
program.exitOverride();
await program.parseAsync([
"node", "rig", "restore-packet", "write",
"--source-jsonl", src,
"--target", target,
"--target-rig", "openrig-velocity-claude",
"--target-runtime", "claude-code",
"--source-session-id-override", "manual-driver@manual-rig",
"--source-rig-override", "manual-rig",
"--current-work-summary", "override path; honest provenance.",
"--authority-boundaries", "R2 override path.",
]);
const summary = JSON.parse(fs.readFileSync(path.join(target, "restore-summary.json"), "utf-8"));
expect(summary.source_session_id).toBe("manual-driver@manual-rig");
expect(summary.source_rig).toBe("manual-rig");
const validation = validateRestoreSummary(summary);
expect(validation.valid, JSON.stringify(validation.errors)).toBe(true);
});
it("--source-jsonl with bare-id session_meta fails without --source-rig-override (no silent unknown)", async () => {
const { createProgram } = await import("../src/index.js");
const fs = await import("node:fs");
const path = await import("node:path");
const src = path.join(tmpRoot, "bare-id.jsonl");
fs.writeFileSync(src, jsonlWithBareSessionMetaId(), "utf8");
const target = path.join(tmpRoot, "packet-bare");
const stderr: string[] = [];
const origConsoleError = console.error;
const origExitCode = process.exitCode;
console.error = (...args: unknown[]) => stderr.push(args.join(" "));
process.exitCode = undefined;
try {
const program = createProgram();
program.exitOverride();
await program.parseAsync([
"node", "rig", "restore-packet", "write",
"--source-jsonl", src,
"--target", target,
"--target-rig", "openrig-velocity-claude",
"--target-runtime", "claude-code",
"--current-work-summary", "bare-id no-rig-override path.",
"--authority-boundaries", "R2 honest fallback.",
]);
} finally {
console.error = origConsoleError;
}
expect(process.exitCode).not.toBe(0);
process.exitCode = origExitCode;
const errStr = stderr.join("\n");
expect(errStr).toMatch(/source.rig|--source-rig-override|<seat>@<rig>|derive/i);
expect(fs.existsSync(target)).toBe(false);
});
it("--source-jsonl with --source-session-id-override overrides parsed session_meta id", async () => {
const { createProgram } = await import("../src/index.js");
const fs = await import("node:fs");
const path = await import("node:path");
// JSONL has session_meta.id = velocity-driver@openrig-velocity
const src = path.join(tmpRoot, "override-wins.jsonl");
fs.writeFileSync(src, jsonlWithSessionMeta(), "utf8");
const target = path.join(tmpRoot, "packet-override-wins");
const program = createProgram();
program.exitOverride();
await program.parseAsync([
"node", "rig", "restore-packet", "write",
"--source-jsonl", src,
"--target", target,
"--target-rig", "openrig-velocity-claude",
"--target-runtime", "claude-code",
"--source-session-id-override", "operator-renamed@operator-rig",
"--source-rig-override", "operator-rig",
"--current-work-summary", "override-wins path.",
"--authority-boundaries", "operator-supplied provenance.",
]);
const summary = JSON.parse(fs.readFileSync(path.join(target, "restore-summary.json"), "utf-8"));
expect(summary.source_session_id).toBe("operator-renamed@operator-rig");
expect(summary.source_rig).toBe("operator-rig");
});
});