Skip to content

Commit 978f2d4

Browse files
committed
feat: enhance readiness reporting with review gate status and isolation modes
1 parent f297350 commit 978f2d4

16 files changed

Lines changed: 317 additions & 16 deletions

src/core/doctor.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,13 @@ export interface DoctorReadinessSnapshot {
5353
workspaceRoot: string;
5454
workspaceProbePath: string;
5555
blocked: boolean;
56+
llmMode?: "codex_chatgpt_only" | "openai_api" | "ollama";
57+
pdfAnalysisMode?: "codex_text_image_hybrid" | "responses_api_pdf" | "ollama_vision";
5658
approvalMode: "manual" | "minimal";
5759
executionApprovalMode: "manual" | "risk_ack" | "full_auto";
5860
dependencyMode: "local" | "docker" | "remote_gpu" | "plan_only";
5961
sessionMode: "fresh" | "existing";
62+
candidateIsolation?: "attempt_snapshot_restore" | "attempt_worktree";
6063
networkPolicy?: ExperimentNetworkPolicy;
6164
networkPurpose?: ExperimentNetworkPurpose;
6265
networkDeclarationPresent: boolean;
@@ -310,10 +313,13 @@ export async function runDoctorReport(
310313
workspaceRoot,
311314
workspaceProbePath: workspaceWriteProbe.probePath,
312315
blocked: failedChecks.length > 0 || harness?.status === "fail",
316+
llmMode: opts?.llmMode,
317+
pdfAnalysisMode: opts?.pdfAnalysisMode,
313318
approvalMode,
314319
executionApprovalMode,
315320
dependencyMode,
316321
sessionMode,
322+
candidateIsolation: opts?.candidateIsolation,
317323
networkPolicy,
318324
networkPurpose,
319325
networkDeclarationPresent,
@@ -416,6 +422,17 @@ async function directoryExists(dirPath: string): Promise<boolean> {
416422

417423
export function buildDoctorHighlightLines(report: DoctorReport): string[] {
418424
const lines: string[] = [];
425+
const profileMark = report.readiness.blocked
426+
? "[ATTN]"
427+
: report.readiness.warningChecks.length > 0
428+
? "[WARN]"
429+
: "[OK]";
430+
const isolationLabel = report.readiness.candidateIsolation || "not-configured";
431+
lines.push(
432+
`${profileMark} profile: llm=${report.readiness.llmMode || "unknown"}, `
433+
+ `pdf=${report.readiness.pdfAnalysisMode || "unknown"}, `
434+
+ `dependency=${report.readiness.dependencyMode}, isolation=${isolationLabel}.`
435+
);
419436
const networkCheck = report.checks.find((check) => check.name === "experiment-web-restriction");
420437
const networkStatus = networkCheck ? getDoctorCheckStatus(networkCheck) : undefined;
421438
if (networkStatus === "warning") {
@@ -431,6 +448,25 @@ export function buildDoctorHighlightLines(report: DoctorReport): string[] {
431448
);
432449
}
433450
}
451+
const providerFailures = report.readiness.failedChecks.filter((check) =>
452+
check === "openai-api-key"
453+
|| check === "codex-cli"
454+
|| check === "codex-login"
455+
|| check.startsWith("ollama-")
456+
|| check === "python"
457+
|| check === "pip"
458+
|| check === "latex"
459+
|| check === "pdftotext"
460+
|| check === "pdfinfo"
461+
|| check === "pdftoppm"
462+
);
463+
if (providerFailures.length > 0) {
464+
lines.push(`[ATTN] provider/runtime blockers: ${providerFailures.join(", ")}.`);
465+
}
466+
const degradedChecks = report.readiness.warningChecks.filter((check) => check !== "experiment-web-restriction");
467+
if (degradedChecks.length > 0) {
468+
lines.push(`[WARN] degraded checks: ${degradedChecks.join(", ")}.`);
469+
}
434470
const pageBudgetCheck = report.checks.find((check) => check.name === "paper-page-budget");
435471
if (pageBudgetCheck) {
436472
lines.push(

src/core/nodes/analyzeResults.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,14 +383,17 @@ export function createAnalyzeResultsNode(deps: NodeExecutionDeps): GraphNodeHand
383383
runId: run.id,
384384
title: run.title,
385385
stage: "analysis",
386-
summary: [buildAnalyzeResultsCompletionSummary(summary)],
386+
summary: [
387+
buildAnalyzeResultsCompletionSummary(summary),
388+
`Next governed gate: ${transitionRecommendation.targetNode || "review"} via ${transitionRecommendation.action}.`
389+
],
387390
decision: `Transition recommendation: ${transitionRecommendation.action}${transitionRecommendation.targetNode ? ` -> ${transitionRecommendation.targetNode}` : ""}. ${transitionRecommendation.reason}`,
388391
blockers: (summary.failure_taxonomy || []).slice(0, 3).map((item) => item.summary),
389392
openQuestions: (summary.synthesis?.discussion_points || []).slice(0, 3),
390393
nextActions:
391394
(summary.synthesis?.follow_up_actions || []).slice(0, 3).length > 0
392395
? (summary.synthesis?.follow_up_actions || []).slice(0, 3)
393-
: [transitionRecommendation.reason],
396+
: [transitionRecommendation.reason, "Enter review and inspect the review packet before treating the run as paper-ready."],
394397
references: [
395398
{ label: "Analysis report", path: "result_analysis.json" },
396399
{ label: "Transition recommendation", path: "transition_recommendation.json" },

src/core/nodes/review.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export function createReviewNode(deps: NodeExecutionDeps): GraphNodeHandler {
164164
});
165165

166166
const findingsPath = await writeRunArtifact(run, "review/findings.jsonl", renderJsonl(panel.findings));
167-
await writeRunArtifact(run, "review/scorecard.json", `${JSON.stringify(panel.scorecard, null, 2)}\n`);
167+
const scorecardPath = await writeRunArtifact(run, "review/scorecard.json", `${JSON.stringify(panel.scorecard, null, 2)}\n`);
168168
await writeRunArtifact(
169169
run,
170170
"review/consistency_report.json",
@@ -196,18 +196,25 @@ export function createReviewNode(deps: NodeExecutionDeps): GraphNodeHandler {
196196
stage: "review",
197197
summary: [
198198
packet.objective_summary,
199-
`Review readiness: ${packet.readiness.status}.`
199+
`Review readiness: ${packet.readiness.status}.`,
200+
`Panel scorecard: ${panel.scorecard.overall_score_1_to_5}/5 overall across ${panel.reviewers.length} reviewer(s).`,
201+
`Paper quality: ${llmEvalResult.evaluation.overall_score_1_to_10}/10 (${llmEvalResult.evaluation.paper_worthiness}).`
200202
],
201203
decision: `${panel.decision.outcome}${panel.decision.recommended_transition ? ` -> ${panel.decision.recommended_transition}` : ""}. ${panel.decision.summary}`,
202204
blockers: [
203205
...preDraftCritique.blocking_issues.slice(0, 3).map((issue) => issue.summary),
204206
...readinessRisks.risks.filter((risk) => risk.severity === "blocked").slice(0, 2).map((risk) => risk.message)
205207
],
206-
openQuestions: panel.decision.required_actions.slice(0, 3),
208+
openQuestions: [
209+
...panel.decision.required_actions.slice(0, 2),
210+
...llmEvalResult.evaluation.weaknesses.slice(0, 2)
211+
].slice(0, 3),
207212
nextActions: packet.suggested_actions.slice(0, 3),
208213
references: [
209214
{ label: "Review packet", path: "review/review_packet.json" },
215+
{ label: "Review scorecard", path: "review/scorecard.json" },
210216
{ label: "Paper critique", path: "review/paper_critique.json" },
217+
{ label: "Review decision", path: "review/decision.json" },
211218
{ label: "Minimum gate", path: "review/minimum_gate.json" },
212219
{ label: "Readiness risks", path: "review/readiness_risks.json" }
213220
]
@@ -229,6 +236,10 @@ export function createReviewNode(deps: NodeExecutionDeps): GraphNodeHandler {
229236
sourcePath: reviewPacketPath,
230237
targetRelativePath: "review_packet.json"
231238
},
239+
{
240+
sourcePath: scorecardPath,
241+
targetRelativePath: "scorecard.json"
242+
},
232243
{
233244
sourcePath: checklistPath,
234245
targetRelativePath: "checklist.md"

src/core/nodes/writePaper.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1010,14 +1010,16 @@ export function createWritePaperNode(deps: NodeExecutionDeps): GraphNodeHandler
10101010
stage: "paper",
10111011
summary: [
10121012
`Paper readiness: ${paperReadiness.readiness_state}.`,
1013-
paperReadiness.reason
1013+
paperReadiness.reason,
1014+
`Venue: ${postDraftCritique.target_venue_style}. Manuscript decision: ${postDraftCritique.overall_decision}.`
10141015
],
10151016
decision: `paper_ready=${paperReadiness.paper_ready}; evidence_gate=${paperReadiness.evidence_gate_status}; scientific_validation=${paperReadiness.scientific_validation_status}.`,
10161017
blockers: readinessRisks.risks.filter((risk) => risk.severity === "blocked").slice(0, 4).map((risk) => risk.message),
10171018
openQuestions: readinessRisks.risks.filter((risk) => risk.severity === "warning").slice(0, 3).map((risk) => risk.message),
10181019
nextActions: readinessRisks.risks.slice(0, 3).map((risk) => risk.recommended_action),
10191020
references: [
10201021
{ label: "Paper readiness", path: "paper/paper_readiness.json" },
1022+
{ label: "Paper critique", path: "paper/paper_critique.json" },
10211023
{ label: "Readiness risks", path: "paper/readiness_risks.json" },
10221024
{ label: "Claim evidence table", path: "paper/claim_evidence_table.json" },
10231025
{ label: "Evidence gate decision", path: "paper/evidence_gate_decision.json" },
@@ -1103,6 +1105,11 @@ export function createWritePaperNode(deps: NodeExecutionDeps): GraphNodeHandler
11031105
targetRelativePath: "paper_readiness.json",
11041106
optional: true
11051107
},
1108+
{
1109+
sourcePath: path.join(runPaperDir, "paper_critique.json"),
1110+
targetRelativePath: "paper_critique.json",
1111+
optional: true
1112+
},
11061113
{
11071114
sourcePath: path.join(runPaperDir, "readiness_risks.json"),
11081115
targetRelativePath: "readiness_risks.json",

src/core/runs/jobsProjection.ts

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ interface ReviewCritiqueProjection {
2121
overall_decision?: string;
2222
}
2323

24+
interface ReviewPacketProjection {
25+
readiness?: {
26+
status?: "ready" | "warning" | "blocking";
27+
};
28+
decision?: {
29+
outcome?: string;
30+
recommended_transition?: string;
31+
};
32+
}
33+
34+
interface ReviewScorecardProjection {
35+
overall_score_1_to_5?: number;
36+
}
37+
2438
interface PaperReadinessProjection {
2539
paper_ready?: boolean;
2640
readiness_state?: string;
@@ -77,7 +91,9 @@ export async function buildRunJobsSnapshot(input: {
7791

7892
return {
7993
generated_at: new Date().toISOString(),
80-
runs: projected.map(stripInternalProjection),
94+
runs: projected
95+
.sort((left, right) => Date.parse(right.last_event_at) - Date.parse(left.last_event_at))
96+
.map(stripInternalProjection),
8197
top_failures: summarizeFailures(projected)
8298
};
8399
}
@@ -93,13 +109,21 @@ export async function buildAnalyzeResultsOperatorSummary(input: {
93109
const transitionRecommendation = await readJsonArtifact<Record<string, unknown>>(
94110
path.join(runDir, "transition_recommendation.json")
95111
);
112+
const reviewPacket = await readJsonArtifact<ReviewPacketProjection>(
113+
path.join(runDir, "review", "review_packet.json")
114+
);
115+
const reviewScorecard = await readJsonArtifact<ReviewScorecardProjection>(
116+
path.join(runDir, "review", "scorecard.json")
117+
);
96118

97119
const artifactRefs = [
98120
maybeArtifactRef(projected.analysis_ready, "Analysis report", "result_analysis.json"),
99121
maybeArtifactRef(projected.analysis_ready, "Transition recommendation", "transition_recommendation.json"),
100-
maybeArtifactRef(projected.review_ready, "Review packet", "review/review_packet.json"),
122+
maybeArtifactRef(Boolean(reviewPacket), "Review packet", "review/review_packet.json"),
123+
maybeArtifactRef(Boolean(reviewScorecard), "Review scorecard", "review/scorecard.json"),
101124
maybeArtifactRef(projected.review_ready, "Paper critique", "review/paper_critique.json"),
102125
maybeArtifactRef(projected.review_ready, "Review minimum gate", "review/minimum_gate.json"),
126+
maybeArtifactRef(projected.review_ready, "Review readiness risks", "review/readiness_risks.json"),
103127
maybeArtifactRef(projected.paper_ready || Boolean(projected.paper_readiness_state), "Paper readiness", "paper/paper_readiness.json")
104128
].filter((item): item is { label: string; path: string } => Boolean(item));
105129

@@ -121,6 +145,26 @@ export async function buildAnalyzeResultsOperatorSummary(input: {
121145
lines.push(`Transition: ${transitionRecommendation.action}${target}.`);
122146
}
123147

148+
if (!projected.review_ready) {
149+
lines.push("Review gate: not started yet or still missing one of the required review artifacts.");
150+
} else if (projected.review_gate_status || projected.review_decision_outcome) {
151+
const transitionHint = projected.review_recommended_transition
152+
? ` -> ${projected.review_recommended_transition}`
153+
: "";
154+
const decisionHint = projected.review_decision_outcome
155+
? `${projected.review_decision_outcome}${transitionHint}`
156+
: projected.review_gate_status;
157+
lines.push(`Review gate: ${decisionHint}.`);
158+
}
159+
160+
if (typeof projected.review_score_overall === "number") {
161+
lines.push(`Review scorecard: ${projected.review_score_overall}/5 overall.`);
162+
}
163+
164+
if (projected.paper_readiness_state) {
165+
lines.push(`Paper readiness state: ${projected.paper_readiness_state}.`);
166+
}
167+
124168
if (projected.blocker_summary) {
125169
lines.push(`Blocker: ${projected.blocker_summary}`);
126170
}
@@ -148,9 +192,16 @@ export function buildJobsTemplateLines(input: {
148192
}): string[] {
149193
const activeCount = input.snapshot.runs.filter((run) => run.lifecycle_status !== "completed").length;
150194
const blockedCount = input.snapshot.runs.filter((run) => run.recommended_next_action === "inspect_blocker").length;
195+
const reviewPendingCount = input.snapshot.runs.filter(
196+
(run) => run.recommended_next_action === "resume_review" || run.current_node === "review"
197+
).length;
198+
const paperBlockedCount = input.snapshot.runs.filter(
199+
(run) => Boolean(run.paper_readiness_state) && !run.paper_ready
200+
).length;
151201
return [
152202
`${input.window === "3d" ? "3-day" : "7-day"} operator check-in template`,
153203
`Runs in view: ${input.snapshot.runs.length}. Active: ${activeCount}. Blocked for inspection: ${blockedCount}.`,
204+
`Review-adjacent runs: ${reviewPendingCount}. Paper-blocked runs: ${paperBlockedCount}.`,
154205
"1. Confirm the current_node and recommended_next_action for the top active runs.",
155206
"2. Inspect one blocker artifact before retrying any failed or paused run.",
156207
"3. Verify whether review is the next governed gate before treating a run as paper-ready.",
@@ -200,6 +251,12 @@ async function buildRunJobProjectionInternal(input: {
200251
const reviewCritique = await readJsonArtifact<ReviewCritiqueProjection>(
201252
path.join(runDir, "review", "paper_critique.json")
202253
);
254+
const reviewPacket = await readJsonArtifact<ReviewPacketProjection>(
255+
path.join(runDir, "review", "review_packet.json")
256+
);
257+
const reviewScorecard = await readJsonArtifact<ReviewScorecardProjection>(
258+
path.join(runDir, "review", "scorecard.json")
259+
);
203260
const lifecycleStatus = deriveLifecycleStatus(input.run);
204261
const lastEventAt = await readLastEventTimestamp(runDir, input.run.updatedAt);
205262
const dominantFailure = deriveDominantFailure({
@@ -229,8 +286,16 @@ async function buildRunJobProjectionInternal(input: {
229286
analysis_ready: analysisReady,
230287
review_ready: reviewReady,
231288
paper_ready: paperReady,
289+
review_gate_status: normalizeReviewGateStatus(reviewPacket?.readiness?.status),
290+
review_decision_outcome: asNonEmptyString(reviewPacket?.decision?.outcome),
291+
review_recommended_transition: asNonEmptyString(reviewPacket?.decision?.recommended_transition),
292+
review_score_overall:
293+
typeof reviewScorecard?.overall_score_1_to_5 === "number"
294+
? Number(reviewScorecard.overall_score_1_to_5.toFixed(1))
295+
: undefined,
232296
paper_readiness_state:
233297
asNonEmptyString(paperReadiness?.readiness_state) || asNonEmptyString(reviewCritique?.paper_readiness_state),
298+
paper_readiness_reason: asNonEmptyString(paperReadiness?.reason),
234299
blocker_summary: dominantFailure?.summary,
235300
dominantFailure
236301
};
@@ -248,7 +313,12 @@ function stripInternalProjection(input: RunJobProjectionInternal): RunJobProject
248313
analysis_ready: input.analysis_ready,
249314
review_ready: input.review_ready,
250315
paper_ready: input.paper_ready,
316+
review_gate_status: input.review_gate_status,
317+
review_decision_outcome: input.review_decision_outcome,
318+
review_recommended_transition: input.review_recommended_transition,
319+
review_score_overall: input.review_score_overall,
251320
paper_readiness_state: input.paper_readiness_state,
321+
paper_readiness_reason: input.paper_readiness_reason,
252322
blocker_summary: input.blocker_summary
253323
};
254324
}
@@ -480,6 +550,15 @@ function asNonEmptyString(value: unknown): string | undefined {
480550
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
481551
}
482552

553+
function normalizeReviewGateStatus(
554+
value: "ready" | "warning" | "blocking" | undefined
555+
): RunJobProjection["review_gate_status"] {
556+
if (value === "ready" || value === "warning" || value === "blocking") {
557+
return value;
558+
}
559+
return undefined;
560+
}
561+
483562
function yesNo(value: boolean): string {
484563
return value ? "yes" : "no";
485564
}
@@ -524,6 +603,21 @@ export function formatRunJobProjectionLines(input: {
524603
` readiness: analysis=${yesNo(input.projection.analysis_ready)} review=${yesNo(input.projection.review_ready)} paper=${yesNo(input.projection.paper_ready)} | next=${input.projection.recommended_next_action}`,
525604
` last event: ${input.projection.last_event_at}`
526605
];
606+
if (input.projection.review_gate_status || input.projection.review_decision_outcome || typeof input.projection.review_score_overall === "number") {
607+
const gateLabel = input.projection.review_decision_outcome
608+
? `${input.projection.review_decision_outcome}${input.projection.review_recommended_transition ? ` -> ${input.projection.review_recommended_transition}` : ""}`
609+
: input.projection.review_gate_status || "missing";
610+
const scoreLabel = typeof input.projection.review_score_overall === "number"
611+
? ` | score=${input.projection.review_score_overall}/5`
612+
: "";
613+
lines.push(` review gate: ${gateLabel}${scoreLabel}`);
614+
}
615+
if (input.projection.paper_readiness_state) {
616+
const paperDetail = input.projection.paper_readiness_reason
617+
? ` | ${compactOneLine(input.projection.paper_readiness_reason, 120)}`
618+
: "";
619+
lines.push(` paper state: ${input.projection.paper_readiness_state}${paperDetail}`);
620+
}
527621
if (input.projection.blocker_summary) {
528622
lines.push(` blocker: ${compactOneLine(input.projection.blocker_summary, 180)}`);
529623
}

src/interaction/InteractionSession.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,9 +1224,11 @@ export class InteractionSession {
12241224
maxHarnessFindings: 30
12251225
});
12261226
this.pushLog(
1227-
`[${report.readiness.blocked ? "ATTN" : "OK"}] readiness: approval=${report.readiness.approvalMode}, `
1227+
`[${report.readiness.blocked ? "ATTN" : "OK"}] readiness: llm=${report.readiness.llmMode || "unknown"}, `
1228+
+ `pdf=${report.readiness.pdfAnalysisMode || "unknown"}, approval=${report.readiness.approvalMode}, `
12281229
+ `execution=${report.readiness.executionApprovalMode}, dependency=${report.readiness.dependencyMode}, `
1229-
+ `session=${report.readiness.sessionMode}, network=${formatDoctorNetworkSummary(report.readiness)}`
1230+
+ `session=${report.readiness.sessionMode}, isolation=${report.readiness.candidateIsolation || "not-configured"}, `
1231+
+ `network=${formatDoctorNetworkSummary(report.readiness)}`
12301232
);
12311233
for (const line of buildDoctorHighlightLines(report)) {
12321234
this.pushLog(line);

src/tui/TerminalApp.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2623,9 +2623,11 @@ export class TerminalApp {
26232623
maxHarnessFindings: 30
26242624
});
26252625
this.pushLog(
2626-
`[${report.readiness.blocked ? "ATTN" : "OK"}] readiness: approval=${report.readiness.approvalMode}, `
2626+
`[${report.readiness.blocked ? "ATTN" : "OK"}] readiness: llm=${report.readiness.llmMode || "unknown"}, `
2627+
+ `pdf=${report.readiness.pdfAnalysisMode || "unknown"}, approval=${report.readiness.approvalMode}, `
26272628
+ `execution=${report.readiness.executionApprovalMode}, dependency=${report.readiness.dependencyMode}, `
2628-
+ `session=${report.readiness.sessionMode}, network=${formatDoctorNetworkSummary(report.readiness)}`
2629+
+ `session=${report.readiness.sessionMode}, isolation=${report.readiness.candidateIsolation || "not-configured"}, `
2630+
+ `network=${formatDoctorNetworkSummary(report.readiness)}`
26292631
);
26302632
for (const line of buildDoctorHighlightLines(report)) {
26312633
this.pushLog(line);

0 commit comments

Comments
 (0)