diff --git a/assistant-decision-handoff-guard/README.md b/assistant-decision-handoff-guard/README.md
new file mode 100644
index 00000000..3c51693a
--- /dev/null
+++ b/assistant-decision-handoff-guard/README.md
@@ -0,0 +1,49 @@
+# Assistant Decision Handoff Guard
+
+This module adds a focused assistant decision handoff guard for the
+AI-Powered Research Assistant Suite bounty. It checks whether outputs from
+peer-review, reproducibility, and research-gap assistants are safe to turn into
+researcher-facing recommendations or tasks.
+
+## Scope
+
+The guard evaluates synthetic assistant handoff packets for:
+
+- source snapshot freshness before recommendations are released
+- artifact revision drift between the assistant context and current project
+- cross-assistant conflicts from peer-review, reproducibility, and gap engines
+- evidence anchors required by handoff type
+- high-impact action authority and human approval state
+- private reviewer note or identity leakage before user-facing release
+- confidence and remediation coverage for actionability
+- deterministic release, revise, or hold decisions with audit digests
+
+This is not another peer-review generator, reproducibility score, research-gap
+finder, evidence binder, prompt-safety filter, structured abstract checker, or
+external-validity assistant. It is the handoff boundary that decides whether
+existing assistant outputs can safely become user-visible actions.
+
+## Issue #16 Requirement Mapping
+
+- Auto peer review reports: blocks private-note leakage, stale manuscript
+ context, unsupported reviewer-facing actions, and missing evidence anchors.
+- Reproducibility checker: prevents badge or release handoffs when run logs,
+ environment locks, result digests, or human approvals are incomplete.
+- Research gap finder: checks corpus freshness, novelty evidence, limitation
+ signals, and confidence before opportunities become researcher tasks.
+- Suite orchestration: reconciles conflicts between assistant outputs before a
+ unified action is shown to the researcher.
+
+## Validation
+
+```bash
+node assistant-decision-handoff-guard/test.js
+node assistant-decision-handoff-guard/demo.js
+node assistant-decision-handoff-guard/render-video.js
+node --check assistant-decision-handoff-guard/index.js
+node --check assistant-decision-handoff-guard/sample-data.js
+node --check assistant-decision-handoff-guard/test.js
+node --check assistant-decision-handoff-guard/demo.js
+node --check assistant-decision-handoff-guard/render-video.js
+git diff --check
+```
diff --git a/assistant-decision-handoff-guard/demo.js b/assistant-decision-handoff-guard/demo.js
new file mode 100644
index 00000000..38c633e7
--- /dev/null
+++ b/assistant-decision-handoff-guard/demo.js
@@ -0,0 +1,27 @@
+const fs = require("fs");
+const path = require("path");
+const packets = require("./sample-data");
+const {
+ evaluateHandoffBatch,
+ renderMarkdownReport,
+ renderSvgReport,
+} = require("./index");
+
+const reportDir = path.join(__dirname, "reports");
+fs.mkdirSync(reportDir, { recursive: true });
+
+const report = evaluateHandoffBatch(packets);
+const jsonPath = path.join(reportDir, "assistant-handoff-review.json");
+const markdownPath = path.join(reportDir, "assistant-handoff-review.md");
+const svgPath = path.join(reportDir, "assistant-handoff-review.svg");
+
+fs.writeFileSync(jsonPath, `${JSON.stringify(report, null, 2)}\n`);
+fs.writeFileSync(markdownPath, renderMarkdownReport(report));
+fs.writeFileSync(svgPath, renderSvgReport(report));
+
+console.log(`wrote ${path.relative(process.cwd(), jsonPath)}`);
+console.log(`wrote ${path.relative(process.cwd(), markdownPath)}`);
+console.log(`wrote ${path.relative(process.cwd(), svgPath)}`);
+console.log(
+ `summary: ${report.summary.release} release, ${report.summary.revise} revise, ${report.summary.hold} hold`
+);
diff --git a/assistant-decision-handoff-guard/index.js b/assistant-decision-handoff-guard/index.js
new file mode 100644
index 00000000..026a8c36
--- /dev/null
+++ b/assistant-decision-handoff-guard/index.js
@@ -0,0 +1,287 @@
+const crypto = require("crypto");
+
+const GENERATED_AT = "2026-05-31T00:00:00Z";
+
+function requiredEvidenceFor(packet) {
+ const byType = {
+ peer_review_comment: ["manuscript-section", "evidence-snippet"],
+ reproducibility_release: ["run-log", "environment-lock", "result-digest"],
+ research_gap_opportunity: ["corpus-snapshot", "limitation-signal", "novelty-check"],
+ };
+
+ return byType[packet.outputType] || ["source-artifact", "evidence-snippet"];
+}
+
+function maxAgeDaysFor(packet) {
+ const byType = {
+ peer_review_comment: 14,
+ reproducibility_release: 7,
+ research_gap_opportunity: 30,
+ };
+
+ return byType[packet.outputType] || 14;
+}
+
+function daysOld(packet, now = GENERATED_AT) {
+ const captured = Date.parse(packet.snapshotCapturedAt);
+ const current = Date.parse(now);
+ if (!Number.isFinite(captured) || !Number.isFinite(current)) {
+ return Number.POSITIVE_INFINITY;
+ }
+ return Math.max(0, Math.floor((current - captured) / (24 * 60 * 60 * 1000)));
+}
+
+function missingEvidence(packet) {
+ const present = new Set(packet.evidenceAnchors || []);
+ return requiredEvidenceFor(packet).filter((item) => !present.has(item));
+}
+
+function conflictSignals(packet) {
+ return (packet.upstreamSignals || []).filter((signal) =>
+ ["block", "contradiction"].includes(signal.status)
+ );
+}
+
+function needsHumanApproval(packet) {
+ return packet.autoExecute || packet.impact === "high" || packet.outputType === "reproducibility_release";
+}
+
+function evaluateHandoff(packet, now = GENERATED_AT) {
+ const blockers = [];
+ const warnings = [];
+ const actions = [];
+ const missing = missingEvidence(packet);
+ const conflicts = conflictSignals(packet);
+ const age = daysOld(packet, now);
+ const maxAge = maxAgeDaysFor(packet);
+
+ if (packet.snapshotRevision !== packet.targetRevision) {
+ blockers.push(
+ `artifact revision drift: assistant used ${packet.snapshotRevision}, current target is ${packet.targetRevision}`
+ );
+ actions.push("refresh assistant context against the current artifact revision");
+ }
+
+ if (age > maxAge) {
+ blockers.push(`snapshot is ${age} days old, above ${maxAge} day limit`);
+ actions.push("refresh source snapshot before handoff");
+ }
+
+ if (missing.length > 0) {
+ blockers.push(`missing evidence anchors: ${missing.join(", ")}`);
+ actions.push("attach required evidence anchors before release");
+ }
+
+ for (const signal of conflicts) {
+ blockers.push(`${signal.assistant} reported ${signal.status}: ${signal.detail}`);
+ actions.push("resolve cross-assistant conflict before user-facing handoff");
+ }
+
+ if (needsHumanApproval(packet) && packet.humanApproval !== "approved") {
+ blockers.push(`human approval is ${packet.humanApproval}`);
+ actions.push("obtain responsible human approval for high-impact handoff");
+ }
+
+ if (packet.containsPrivateNotes) {
+ blockers.push("private reviewer notes are present");
+ actions.push("redact private reviewer notes before author-visible release");
+ }
+
+ if (packet.reviewerIdentityRisk) {
+ blockers.push("reviewer identity leakage risk detected");
+ actions.push("remove identity-bearing details from the handoff");
+ }
+
+ if (packet.confidence < 0.55) {
+ blockers.push(`assistant confidence ${packet.confidence} is below release floor`);
+ actions.push("rerun or demote the assistant output");
+ } else if (packet.confidence < 0.7) {
+ warnings.push(`assistant confidence ${packet.confidence} needs strengthening`);
+ actions.push("keep the output in draft until more support is attached");
+ }
+
+ if ((packet.remediationSteps || []).length === 0) {
+ warnings.push("no remediation steps provided");
+ actions.push("add concrete next steps before handoff");
+ }
+
+ if (blockers.length === 0 && warnings.length === 0) {
+ actions.push("release handoff to researcher-facing queue");
+ } else if (blockers.length === 0) {
+ actions.push("revise handoff before promotion");
+ }
+
+ const decision = blockers.length > 0 ? "hold" : warnings.length > 0 ? "revise" : "release";
+ const result = {
+ handoffId: packet.id,
+ assistant: packet.assistant,
+ outputType: packet.outputType,
+ title: packet.title,
+ targetArtifact: packet.targetArtifact,
+ targetRevision: packet.targetRevision,
+ snapshotRevision: packet.snapshotRevision,
+ snapshotAgeDays: age,
+ impact: packet.impact,
+ confidence: packet.confidence,
+ decision,
+ blockers,
+ warnings,
+ actions: [...new Set(actions)],
+ };
+
+ return {
+ ...result,
+ auditDigest: digest(result),
+ };
+}
+
+function evaluateHandoffBatch(packets, now = GENERATED_AT) {
+ const decisions = packets.map((packet) => evaluateHandoff(packet, now));
+ const summary = decisions.reduce(
+ (acc, item) => {
+ acc.total += 1;
+ acc[item.decision] += 1;
+ acc.blockers += item.blockers.length;
+ acc.warnings += item.warnings.length;
+ if (item.snapshotAgeDays > acc.maxSnapshotAgeDays) {
+ acc.maxSnapshotAgeDays = item.snapshotAgeDays;
+ }
+ return acc;
+ },
+ {
+ total: 0,
+ release: 0,
+ revise: 0,
+ hold: 0,
+ blockers: 0,
+ warnings: 0,
+ maxSnapshotAgeDays: 0,
+ }
+ );
+
+ const report = {
+ generatedAt: new Date(now).toISOString(),
+ summary,
+ decisions,
+ };
+
+ return {
+ ...report,
+ auditDigest: digest(report),
+ };
+}
+
+function renderMarkdownReport(report) {
+ const lines = [
+ "# Assistant Decision Handoff Guard Report",
+ "",
+ `Generated: ${report.generatedAt}`,
+ `Audit digest: ${report.auditDigest}`,
+ "",
+ "## Summary",
+ "",
+ `- Handoff packets: ${report.summary.total}`,
+ `- Release: ${report.summary.release}`,
+ `- Revise: ${report.summary.revise}`,
+ `- Hold: ${report.summary.hold}`,
+ `- Blockers: ${report.summary.blockers}`,
+ `- Warnings: ${report.summary.warnings}`,
+ `- Max snapshot age days: ${report.summary.maxSnapshotAgeDays}`,
+ "",
+ "## Decisions",
+ "",
+ ];
+
+ for (const item of report.decisions) {
+ lines.push(`### ${item.handoffId}`);
+ lines.push("");
+ lines.push(`- Assistant: ${item.assistant}`);
+ lines.push(`- Output type: ${item.outputType}`);
+ lines.push(`- Decision: ${item.decision}`);
+ lines.push(`- Target: ${item.targetArtifact} @ ${item.targetRevision}`);
+ lines.push(`- Snapshot revision: ${item.snapshotRevision}`);
+ lines.push(`- Snapshot age days: ${item.snapshotAgeDays}`);
+ lines.push(`- Confidence: ${item.confidence}`);
+ lines.push(`- Blockers: ${item.blockers.join("; ") || "none"}`);
+ lines.push(`- Warnings: ${item.warnings.join("; ") || "none"}`);
+ lines.push(`- Actions: ${item.actions.join("; ")}`);
+ lines.push(`- Digest: ${item.auditDigest}`);
+ lines.push("");
+ }
+
+ while (lines[lines.length - 1] === "") {
+ lines.pop();
+ }
+
+ return `${lines.join("\n")}\n`;
+}
+
+function escapeXml(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+}
+
+function renderSvgReport(report) {
+ const cards = [
+ ["Packets", report.summary.total, "#264653"],
+ ["Release", report.summary.release, "#2a9d8f"],
+ ["Revise", report.summary.revise, "#e9c46a"],
+ ["Hold", report.summary.hold, "#e76f51"],
+ ["Blockers", report.summary.blockers, "#8d3b72"],
+ ["Max age", `${report.summary.maxSnapshotAgeDays}d`, "#457b9d"],
+ ];
+
+ const cardSvg = cards
+ .map(([label, value, color], index) => {
+ const x = 70 + (index % 3) * 382;
+ const y = 184 + Math.floor(index / 3) * 148;
+ return [
+ ``,
+ ``,
+ `${escapeXml(label)}`,
+ `${escapeXml(value)}`,
+ ].join("\n");
+ })
+ .join("\n");
+
+ const queueRows = report.decisions
+ .filter((item) => item.decision !== "release")
+ .slice(0, 4)
+ .map((item, index) => {
+ const y = 528 + index * 31;
+ return `${escapeXml(item.handoffId)} - ${escapeXml(item.decision)} - ${escapeXml(item.actions[0])}`;
+ })
+ .join("\n");
+
+ return ``;
+}
+
+function digest(value) {
+ return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
+}
+
+module.exports = {
+ GENERATED_AT,
+ conflictSignals,
+ daysOld,
+ digest,
+ evaluateHandoff,
+ evaluateHandoffBatch,
+ missingEvidence,
+ needsHumanApproval,
+ renderMarkdownReport,
+ renderSvgReport,
+ requiredEvidenceFor,
+};
diff --git a/assistant-decision-handoff-guard/render-video.js b/assistant-decision-handoff-guard/render-video.js
new file mode 100644
index 00000000..c08eb03b
--- /dev/null
+++ b/assistant-decision-handoff-guard/render-video.js
@@ -0,0 +1,50 @@
+const fs = require("fs");
+const path = require("path");
+const { execFileSync } = require("child_process");
+
+const reportDir = path.join(__dirname, "reports");
+const jsonPath = path.join(reportDir, "assistant-handoff-review.json");
+const svgPath = path.join(reportDir, "assistant-handoff-review.svg");
+const framePath = path.join(reportDir, "assistant-handoff-review.png");
+const outputPath = path.join(reportDir, "demo.mp4");
+
+if (!fs.existsSync(jsonPath) || !fs.existsSync(svgPath)) {
+ require("./demo");
+}
+
+const report = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
+
+execFileSync(
+ "rsvg-convert",
+ ["--width", "1280", "--height", "720", "--output", framePath, svgPath],
+ { stdio: "inherit" }
+);
+
+execFileSync(
+ "ffmpeg",
+ [
+ "-y",
+ "-loop",
+ "1",
+ "-framerate",
+ "25",
+ "-i",
+ framePath,
+ "-t",
+ "4",
+ "-pix_fmt",
+ "yuv420p",
+ "-vf",
+ "scale=1280:720",
+ "-movflags",
+ "+faststart",
+ outputPath,
+ ],
+ { stdio: "inherit" }
+);
+
+fs.unlinkSync(framePath);
+
+console.log(
+ `wrote ${path.relative(process.cwd(), outputPath)} for ${report.summary.total} handoff packets`
+);
diff --git a/assistant-decision-handoff-guard/reports/assistant-handoff-review.json b/assistant-decision-handoff-guard/reports/assistant-handoff-review.json
new file mode 100644
index 00000000..6db87738
--- /dev/null
+++ b/assistant-decision-handoff-guard/reports/assistant-handoff-review.json
@@ -0,0 +1,131 @@
+{
+ "generatedAt": "2026-05-31T00:00:00.000Z",
+ "summary": {
+ "total": 5,
+ "release": 1,
+ "revise": 1,
+ "hold": 3,
+ "blockers": 9,
+ "warnings": 1,
+ "maxSnapshotAgeDays": 50
+ },
+ "decisions": [
+ {
+ "handoffId": "handoff:peer-review-methods-clarity",
+ "assistant": "auto-peer-review",
+ "outputType": "peer_review_comment",
+ "title": "Methods section clarity request",
+ "targetArtifact": "manuscript.md",
+ "targetRevision": "ms-v12",
+ "snapshotRevision": "ms-v12",
+ "snapshotAgeDays": 1,
+ "impact": "medium",
+ "confidence": 0.82,
+ "decision": "release",
+ "blockers": [],
+ "warnings": [],
+ "actions": [
+ "release handoff to researcher-facing queue"
+ ],
+ "auditDigest": "421868aaa57b79f52841dbf0504c34e679406471ab77d6dd856799548199bb36"
+ },
+ {
+ "handoffId": "handoff:reproducibility-badge-release",
+ "assistant": "reproducibility-checker",
+ "outputType": "reproducibility_release",
+ "title": "Publish reproducibility badge",
+ "targetArtifact": "analysis/notebook.ipynb",
+ "targetRevision": "analysis-v8",
+ "snapshotRevision": "analysis-v8",
+ "snapshotAgeDays": 0,
+ "impact": "high",
+ "confidence": 0.91,
+ "decision": "hold",
+ "blockers": [
+ "missing evidence anchors: environment-lock",
+ "notebook-runner reported block: cell 18 remains nondeterministic",
+ "human approval is pending"
+ ],
+ "warnings": [],
+ "actions": [
+ "attach required evidence anchors before release",
+ "resolve cross-assistant conflict before user-facing handoff",
+ "obtain responsible human approval for high-impact handoff"
+ ],
+ "auditDigest": "574cb0e6776fee74eff6f72e05cfc152078a2b5ed932214200125423b8cc75ef"
+ },
+ {
+ "handoffId": "handoff:gap-opportunity-corpus-drift",
+ "assistant": "research-gap-finder",
+ "outputType": "research_gap_opportunity",
+ "title": "Suggest CRISPR organoid replication gap",
+ "targetArtifact": "gap-feed.json",
+ "targetRevision": "corpus-2026-05",
+ "snapshotRevision": "corpus-2026-04",
+ "snapshotAgeDays": 50,
+ "impact": "medium",
+ "confidence": 0.76,
+ "decision": "hold",
+ "blockers": [
+ "artifact revision drift: assistant used corpus-2026-04, current target is corpus-2026-05",
+ "snapshot is 50 days old, above 30 day limit",
+ "missing evidence anchors: novelty-check"
+ ],
+ "warnings": [],
+ "actions": [
+ "refresh assistant context against the current artifact revision",
+ "refresh source snapshot before handoff",
+ "attach required evidence anchors before release"
+ ],
+ "auditDigest": "ef7b59e2be719c3f573cf059171dfbcf8d4496c521f75ef3f7363d2e844166ac"
+ },
+ {
+ "handoffId": "handoff:gap-opportunity-low-confidence",
+ "assistant": "research-gap-finder",
+ "outputType": "research_gap_opportunity",
+ "title": "Suggest weakly supported imaging opportunity",
+ "targetArtifact": "gap-feed.json",
+ "targetRevision": "corpus-2026-05",
+ "snapshotRevision": "corpus-2026-05",
+ "snapshotAgeDays": 6,
+ "impact": "low",
+ "confidence": 0.64,
+ "decision": "revise",
+ "blockers": [],
+ "warnings": [
+ "assistant confidence 0.64 needs strengthening"
+ ],
+ "actions": [
+ "keep the output in draft until more support is attached",
+ "revise handoff before promotion"
+ ],
+ "auditDigest": "e8b49c282d105781c1287a607ecb7bda0a84245d219fe67567ef562637e91850"
+ },
+ {
+ "handoffId": "handoff:peer-review-private-note",
+ "assistant": "auto-peer-review",
+ "outputType": "peer_review_comment",
+ "title": "Reviewer concern copied from private note",
+ "targetArtifact": "manuscript.md",
+ "targetRevision": "ms-v12",
+ "snapshotRevision": "ms-v12",
+ "snapshotAgeDays": 0,
+ "impact": "high",
+ "confidence": 0.88,
+ "decision": "hold",
+ "blockers": [
+ "review-redaction reported block: private reviewer rationale was copied",
+ "private reviewer notes are present",
+ "reviewer identity leakage risk detected"
+ ],
+ "warnings": [],
+ "actions": [
+ "resolve cross-assistant conflict before user-facing handoff",
+ "redact private reviewer notes before author-visible release",
+ "remove identity-bearing details from the handoff"
+ ],
+ "auditDigest": "c5ad64bf782f11ceae2da3bb73bea57bb753df660acdea82b341d4b2ae2d15d9"
+ }
+ ],
+ "auditDigest": "57569e4993e08c385bc9dcf05b6bf58625f7449b8d7cda2d519acfa3a7e4366a"
+}
diff --git a/assistant-decision-handoff-guard/reports/assistant-handoff-review.md b/assistant-decision-handoff-guard/reports/assistant-handoff-review.md
new file mode 100644
index 00000000..62ae454a
--- /dev/null
+++ b/assistant-decision-handoff-guard/reports/assistant-handoff-review.md
@@ -0,0 +1,86 @@
+# Assistant Decision Handoff Guard Report
+
+Generated: 2026-05-31T00:00:00.000Z
+Audit digest: 57569e4993e08c385bc9dcf05b6bf58625f7449b8d7cda2d519acfa3a7e4366a
+
+## Summary
+
+- Handoff packets: 5
+- Release: 1
+- Revise: 1
+- Hold: 3
+- Blockers: 9
+- Warnings: 1
+- Max snapshot age days: 50
+
+## Decisions
+
+### handoff:peer-review-methods-clarity
+
+- Assistant: auto-peer-review
+- Output type: peer_review_comment
+- Decision: release
+- Target: manuscript.md @ ms-v12
+- Snapshot revision: ms-v12
+- Snapshot age days: 1
+- Confidence: 0.82
+- Blockers: none
+- Warnings: none
+- Actions: release handoff to researcher-facing queue
+- Digest: 421868aaa57b79f52841dbf0504c34e679406471ab77d6dd856799548199bb36
+
+### handoff:reproducibility-badge-release
+
+- Assistant: reproducibility-checker
+- Output type: reproducibility_release
+- Decision: hold
+- Target: analysis/notebook.ipynb @ analysis-v8
+- Snapshot revision: analysis-v8
+- Snapshot age days: 0
+- Confidence: 0.91
+- Blockers: missing evidence anchors: environment-lock; notebook-runner reported block: cell 18 remains nondeterministic; human approval is pending
+- Warnings: none
+- Actions: attach required evidence anchors before release; resolve cross-assistant conflict before user-facing handoff; obtain responsible human approval for high-impact handoff
+- Digest: 574cb0e6776fee74eff6f72e05cfc152078a2b5ed932214200125423b8cc75ef
+
+### handoff:gap-opportunity-corpus-drift
+
+- Assistant: research-gap-finder
+- Output type: research_gap_opportunity
+- Decision: hold
+- Target: gap-feed.json @ corpus-2026-05
+- Snapshot revision: corpus-2026-04
+- Snapshot age days: 50
+- Confidence: 0.76
+- Blockers: artifact revision drift: assistant used corpus-2026-04, current target is corpus-2026-05; snapshot is 50 days old, above 30 day limit; missing evidence anchors: novelty-check
+- Warnings: none
+- Actions: refresh assistant context against the current artifact revision; refresh source snapshot before handoff; attach required evidence anchors before release
+- Digest: ef7b59e2be719c3f573cf059171dfbcf8d4496c521f75ef3f7363d2e844166ac
+
+### handoff:gap-opportunity-low-confidence
+
+- Assistant: research-gap-finder
+- Output type: research_gap_opportunity
+- Decision: revise
+- Target: gap-feed.json @ corpus-2026-05
+- Snapshot revision: corpus-2026-05
+- Snapshot age days: 6
+- Confidence: 0.64
+- Blockers: none
+- Warnings: assistant confidence 0.64 needs strengthening
+- Actions: keep the output in draft until more support is attached; revise handoff before promotion
+- Digest: e8b49c282d105781c1287a607ecb7bda0a84245d219fe67567ef562637e91850
+
+### handoff:peer-review-private-note
+
+- Assistant: auto-peer-review
+- Output type: peer_review_comment
+- Decision: hold
+- Target: manuscript.md @ ms-v12
+- Snapshot revision: ms-v12
+- Snapshot age days: 0
+- Confidence: 0.88
+- Blockers: review-redaction reported block: private reviewer rationale was copied; private reviewer notes are present; reviewer identity leakage risk detected
+- Warnings: none
+- Actions: resolve cross-assistant conflict before user-facing handoff; redact private reviewer notes before author-visible release; remove identity-bearing details from the handoff
+- Digest: c5ad64bf782f11ceae2da3bb73bea57bb753df660acdea82b341d4b2ae2d15d9
diff --git a/assistant-decision-handoff-guard/reports/assistant-handoff-review.svg b/assistant-decision-handoff-guard/reports/assistant-handoff-review.svg
new file mode 100644
index 00000000..7ce7acfb
--- /dev/null
+++ b/assistant-decision-handoff-guard/reports/assistant-handoff-review.svg
@@ -0,0 +1,36 @@
+
\ No newline at end of file
diff --git a/assistant-decision-handoff-guard/reports/demo.mp4 b/assistant-decision-handoff-guard/reports/demo.mp4
new file mode 100644
index 00000000..36289723
Binary files /dev/null and b/assistant-decision-handoff-guard/reports/demo.mp4 differ
diff --git a/assistant-decision-handoff-guard/sample-data.js b/assistant-decision-handoff-guard/sample-data.js
new file mode 100644
index 00000000..a6b85013
--- /dev/null
+++ b/assistant-decision-handoff-guard/sample-data.js
@@ -0,0 +1,114 @@
+const handoffPackets = [
+ {
+ id: "handoff:peer-review-methods-clarity",
+ assistant: "auto-peer-review",
+ outputType: "peer_review_comment",
+ title: "Methods section clarity request",
+ targetArtifact: "manuscript.md",
+ targetRevision: "ms-v12",
+ snapshotRevision: "ms-v12",
+ snapshotCapturedAt: "2026-05-29T09:00:00Z",
+ proposedAction: "show author a methods clarification task",
+ impact: "medium",
+ confidence: 0.82,
+ evidenceAnchors: ["manuscript-section", "evidence-snippet"],
+ upstreamSignals: [
+ { assistant: "claim-evidence-checker", status: "pass", detail: "claim maps to methods paragraph 4" },
+ ],
+ remediationSteps: ["ask author to define reagent lot policy", "link methods paragraph 4"],
+ autoExecute: false,
+ humanApproval: "not_required",
+ containsPrivateNotes: false,
+ reviewerIdentityRisk: false,
+ },
+ {
+ id: "handoff:reproducibility-badge-release",
+ assistant: "reproducibility-checker",
+ outputType: "reproducibility_release",
+ title: "Publish reproducibility badge",
+ targetArtifact: "analysis/notebook.ipynb",
+ targetRevision: "analysis-v8",
+ snapshotRevision: "analysis-v8",
+ snapshotCapturedAt: "2026-05-30T11:20:00Z",
+ proposedAction: "publish reproducibility badge automatically",
+ impact: "high",
+ confidence: 0.91,
+ evidenceAnchors: ["run-log", "result-digest"],
+ upstreamSignals: [
+ { assistant: "notebook-runner", status: "block", detail: "cell 18 remains nondeterministic" },
+ ],
+ remediationSteps: ["rerun notebook with locked random seed", "attach environment lockfile"],
+ autoExecute: true,
+ humanApproval: "pending",
+ containsPrivateNotes: false,
+ reviewerIdentityRisk: false,
+ },
+ {
+ id: "handoff:gap-opportunity-corpus-drift",
+ assistant: "research-gap-finder",
+ outputType: "research_gap_opportunity",
+ title: "Suggest CRISPR organoid replication gap",
+ targetArtifact: "gap-feed.json",
+ targetRevision: "corpus-2026-05",
+ snapshotRevision: "corpus-2026-04",
+ snapshotCapturedAt: "2026-04-10T12:00:00Z",
+ proposedAction: "create a thesis opportunity card",
+ impact: "medium",
+ confidence: 0.76,
+ evidenceAnchors: ["corpus-snapshot", "limitation-signal"],
+ upstreamSignals: [
+ { assistant: "literature-refresh", status: "warn", detail: "new review article landed after snapshot" },
+ ],
+ remediationSteps: ["refresh corpus snapshot", "rerun novelty check"],
+ autoExecute: false,
+ humanApproval: "not_required",
+ containsPrivateNotes: false,
+ reviewerIdentityRisk: false,
+ },
+ {
+ id: "handoff:gap-opportunity-low-confidence",
+ assistant: "research-gap-finder",
+ outputType: "research_gap_opportunity",
+ title: "Suggest weakly supported imaging opportunity",
+ targetArtifact: "gap-feed.json",
+ targetRevision: "corpus-2026-05",
+ snapshotRevision: "corpus-2026-05",
+ snapshotCapturedAt: "2026-05-24T08:00:00Z",
+ proposedAction: "show as a draft opportunity card",
+ impact: "low",
+ confidence: 0.64,
+ evidenceAnchors: ["corpus-snapshot", "limitation-signal", "novelty-check"],
+ upstreamSignals: [
+ { assistant: "citation-freshness", status: "pass", detail: "no retraction or replacement found" },
+ ],
+ remediationSteps: ["add two more supporting papers before promoting"],
+ autoExecute: false,
+ humanApproval: "not_required",
+ containsPrivateNotes: false,
+ reviewerIdentityRisk: false,
+ },
+ {
+ id: "handoff:peer-review-private-note",
+ assistant: "auto-peer-review",
+ outputType: "peer_review_comment",
+ title: "Reviewer concern copied from private note",
+ targetArtifact: "manuscript.md",
+ targetRevision: "ms-v12",
+ snapshotRevision: "ms-v12",
+ snapshotCapturedAt: "2026-05-30T16:00:00Z",
+ proposedAction: "show reviewer concern to author",
+ impact: "high",
+ confidence: 0.88,
+ evidenceAnchors: ["manuscript-section", "evidence-snippet"],
+ upstreamSignals: [
+ { assistant: "review-redaction", status: "block", detail: "private reviewer rationale was copied" },
+ ],
+ remediationSteps: ["redact private note", "rewrite concern using manuscript evidence only"],
+ autoExecute: false,
+ humanApproval: "approved",
+ containsPrivateNotes: true,
+ reviewerIdentityRisk: true,
+ },
+];
+
+module.exports = handoffPackets;
diff --git a/assistant-decision-handoff-guard/test.js b/assistant-decision-handoff-guard/test.js
new file mode 100644
index 00000000..3c31636b
--- /dev/null
+++ b/assistant-decision-handoff-guard/test.js
@@ -0,0 +1,90 @@
+const assert = require("assert");
+const packets = require("./sample-data");
+const {
+ GENERATED_AT,
+ conflictSignals,
+ daysOld,
+ evaluateHandoff,
+ evaluateHandoffBatch,
+ missingEvidence,
+ needsHumanApproval,
+ requiredEvidenceFor,
+} = require("./index");
+
+const report = evaluateHandoffBatch(packets);
+
+function decision(id) {
+ return report.decisions.find((item) => item.handoffId === id);
+}
+
+assert.strictEqual(report.generatedAt, "2026-05-31T00:00:00.000Z");
+assert.strictEqual(report.summary.total, 5);
+assert.strictEqual(report.summary.release, 1);
+assert.strictEqual(report.summary.revise, 1);
+assert.strictEqual(report.summary.hold, 3);
+assert.strictEqual(report.summary.blockers, 9);
+assert.strictEqual(report.summary.warnings, 1);
+assert.strictEqual(report.summary.maxSnapshotAgeDays, 50);
+
+const cleanPeerReview = decision("handoff:peer-review-methods-clarity");
+assert.strictEqual(cleanPeerReview.decision, "release");
+assert.deepStrictEqual(cleanPeerReview.blockers, []);
+assert(cleanPeerReview.actions.includes("release handoff to researcher-facing queue"));
+
+const reproducibility = decision("handoff:reproducibility-badge-release");
+assert.strictEqual(reproducibility.decision, "hold");
+assert(
+ reproducibility.blockers.some((item) => item.includes("environment-lock")),
+ "reproducibility handoff must require environment lock evidence"
+);
+assert(
+ reproducibility.blockers.some((item) => item.includes("notebook-runner")),
+ "reproducibility handoff should stop on upstream runner blocks"
+);
+assert(
+ reproducibility.blockers.some((item) => item.includes("human approval")),
+ "auto-publishing a badge should require human approval"
+);
+
+const staleGap = decision("handoff:gap-opportunity-corpus-drift");
+assert.strictEqual(staleGap.decision, "hold");
+assert(
+ staleGap.blockers.some((item) => item.includes("artifact revision drift")),
+ "gap opportunity should detect corpus revision drift"
+);
+assert(
+ staleGap.blockers.some((item) => item.includes("snapshot is 50 days old")),
+ "gap opportunity should detect stale corpus context"
+);
+assert(
+ staleGap.blockers.some((item) => item.includes("novelty-check")),
+ "gap opportunity should require novelty evidence"
+);
+
+const draftGap = decision("handoff:gap-opportunity-low-confidence");
+assert.strictEqual(draftGap.decision, "revise");
+assert(
+ draftGap.warnings.some((item) => item.includes("needs strengthening")),
+ "low-confidence opportunity should stay in draft revision"
+);
+
+const privateNote = decision("handoff:peer-review-private-note");
+assert.strictEqual(privateNote.decision, "hold");
+assert(
+ privateNote.blockers.some((item) => item.includes("private reviewer notes")),
+ "private reviewer notes should block author-facing release"
+);
+assert(
+ privateNote.blockers.some((item) => item.includes("identity leakage")),
+ "reviewer identity leakage should block author-facing release"
+);
+
+assert.deepStrictEqual(requiredEvidenceFor(packets[0]), ["manuscript-section", "evidence-snippet"]);
+assert.strictEqual(daysOld(packets[0], GENERATED_AT), 1);
+assert.strictEqual(missingEvidence(packets[1]).includes("environment-lock"), true);
+assert.strictEqual(needsHumanApproval(packets[1]), true);
+assert.strictEqual(conflictSignals(packets[1]).length, 1);
+assert.match(report.auditDigest, /^[a-f0-9]{64}$/);
+assert.match(evaluateHandoff(packets[0]).auditDigest, /^[a-f0-9]{64}$/);
+
+console.log("assistant decision handoff guard tests passed");