Skip to content

Commit 75e709a

Browse files
committed
feat: resolve exploration configuration from workspace overrides and update related components
1 parent 9da7c07 commit 75e709a

15 files changed

Lines changed: 164 additions & 48 deletions

ISSUES.md

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ Usage rules:
3030
## Active live validation issues
3131

3232
### LV-084 — `/explore` and `/api/exploration/status` ignore persisted exploration artifacts and always report the global disabled contract
33-
- Status: OPEN
33+
- Status: FIXED
3434
- Validation target: real `test/.live` TUI `/explore` output and real web `/api/exploration/status` / bootstrap state for a run that already has `experiment_tree/tree.json`, `manager_state.json`, `baseline_lock.json`, and `figure_audit/figure_audit_summary.json`
35-
- Environment/session context: repo head on 2026-04-02, live fixture workspace `/home/hanyong/AutoLabOS/test/.live/autolabos-live-explore-uhei2J`, run `run-explore-live`, launched with real `node /home/hanyong/AutoLabOS/dist/cli/main.js` and `node /home/hanyong/AutoLabOS/dist/cli/main.js web --host 127.0.0.1 --port 4318`
35+
- Environment/session context: repo head on 2026-04-02. Original failing repro used `/home/hanyong/AutoLabOS/test/.live/autolabos-live-explore-uhei2J`. Post-fix revalidation used `/home/hanyong/AutoLabOS/test/.live/autolabos-live-explore-enabled-dNua2B`, run `run-explore-live`, launched with real `node /home/hanyong/AutoLabOS/dist/cli/main.js` and `node /home/hanyong/AutoLabOS/dist/cli/main.js web --host 127.0.0.1 --port 4318`.
3636
- Reproduction steps:
3737
1. Create a real `test/.live` workspace containing a paused review run plus persisted exploration artifacts under `.autolabos/runs/run-explore-live/experiment_tree/` and `figure_audit/`.
3838
2. Launch a fresh TUI rooted at that workspace and run `/explore`.
@@ -44,20 +44,33 @@ Usage rules:
4444
- `GET /api/exploration/status?run_id=run-explore-live` returned `{\"enabled\":false,...,\"baseline_lock_status\":\"not_applicable\"}`.
4545
- `GET /api/bootstrap` still anchored to the correct active run and showed the run graph paused at `review`, so the disabled exploration result was not caused by selecting the wrong run.
4646
- Fresh vs existing session comparison:
47-
- Fresh session: the first real TUI process showed the disabled exploration contract.
48-
- Existing/reopened session: reopening the same persisted workspace in a second TUI process produced the same disabled exploration contract.
49-
- Divergence: none observed; the behavior appears stable across fresh and reopened sessions.
47+
- Fresh session before fix: the first real TUI process showed the disabled exploration contract.
48+
- Existing session before fix: reopening the same persisted workspace in a second TUI process produced the same disabled exploration contract.
49+
- Fresh session after fix: `/explore` showed `Enabled: true`, `Current Stage: main_agenda`, `Nodes: 2 explored / 1 promoted / 1 blocked`, `Baseline Lock: locked`, and `Fig Audit Warns: 1 (1 severe flag)`.
50+
- Existing session after fix: reopening the same persisted workspace in a second TUI process produced the same enabled exploration snapshot.
51+
- Divergence: none observed after the fix; fresh and reopened sessions now agree.
5052
- Root cause hypothesis:
5153
- Type: `in_memory_projection_bug`
52-
- Hypothesis: `src/core/exploration/status.ts` short-circuits on `loadExplorationConfig().enabled === false`, and `loadExplorationConfig()` currently reads only the repo-default YAML (`src/config/exploration.default.yaml`) instead of any run/workspace/runtime seam. That prevents the live status surfaces from reading persisted exploration artifacts even when they exist.
53-
- Code/test changes: none yet; this entry records the first real live-validation reproduction after the exploration status surfaces landed.
54+
- Hypothesis: `src/core/exploration/status.ts` short-circuited on the repo-default `loadExplorationConfig().enabled === false` path, so the live TUI/web surfaces ignored workspace config and runtime config even when persisted `experiment_tree/` artifacts existed.
55+
- Code/test changes:
56+
- Added `resolveExplorationConfig(...)` in `src/core/exploration/explorationConfig.ts` so exploration enablement resolves from workspace `.autolabos/config.yaml` and in-memory runtime config instead of the repo default only.
57+
- Updated `src/core/exploration/status.ts`, `src/core/nodes/designExperiments.ts`, `src/core/nodes/analyzeResults.ts`, `src/core/nodes/figureAudit.ts`, `src/tui/TerminalApp.ts`, `src/interaction/InteractionSession.ts`, `src/web/server.ts`, `src/runtime/createRuntime.ts`, and `src/types.ts` to use the same seam.
58+
- Updated regressions in `tests/explorationConfig.test.ts`, `tests/explorationStatus.test.ts`, `tests/figureAuditNode.test.ts`, and `tests/newSlashCommands.test.ts`.
5459
- Regression status:
55-
- Automated regression coverage exists for the enabled path via mocked config in `tests/explorationStatus.test.ts`, `tests/newSlashCommands.test.ts`, and `web/src/App.test.tsx`.
56-
- Real live revalidation result: still reproduces.
60+
- Automated regression coverage:
61+
- `npm run build`
62+
- `npm test`
63+
- `npm run validate:harness`
64+
- `npm run test:web`
65+
- Real live revalidation result:
66+
- Fresh TUI `/doctor` still surfaces the expected fixture-scope missing artifacts.
67+
- Fresh TUI `/explore` now reports the enabled exploration snapshot from persisted artifacts.
68+
- Reopened TUI `/explore` reports the same enabled snapshot.
69+
- `GET /api/exploration/status?run_id=run-explore-live` now returns `{\"enabled\":true,\"current_stage\":\"main_agenda\",...}`.
70+
- `GET /api/bootstrap` remains anchored to the same active run.
5771
- Follow-up risks:
58-
- Operators can be misled into thinking exploration never ran, even when `experiment_tree/` and `figure_audit/` artifacts are present.
59-
- The current tests only verify the enabled path through explicit config mocking, so this runtime configuration seam can drift unnoticed.
60-
- Direct browser rendering of the web card was not rechecked because the Playwright navigation approval was rejected during this validation loop; the live API behavior was verified instead.
72+
- Direct browser rendering of the web card was not rechecked in this loop; the real web API contract was verified instead.
73+
- The live fixture still lacks several full-run artifacts, so `/doctor` continues to show expected fixture-scope missing-artifact findings unrelated to the exploration projection fix.
6174

6275
---
6376

src/core/exploration/explorationConfig.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { readFileSync } from "node:fs";
1+
import { existsSync, readFileSync } from "node:fs";
22
import path from "node:path";
33
import { fileURLToPath } from "node:url";
44

55
import YAML from "yaml";
6+
import type { AppConfig } from "../../types.js";
7+
import { resolveAppPaths } from "../../config.js";
68

79
export interface ExplorationStageBudgetConfig {
810
max_nodes: number;
@@ -73,3 +75,45 @@ export function loadExplorationConfig(configPath?: string): ExplorationConfig {
7375
const raw = readFileSync(resolvedPath, "utf8");
7476
return YAML.parse(raw) as ExplorationConfig;
7577
}
78+
79+
function loadWorkspaceConfigOverride(workspaceRoot: string): Partial<AppConfig> | null {
80+
const configPath = resolveAppPaths(workspaceRoot).configFile;
81+
if (!existsSync(configPath)) {
82+
return null;
83+
}
84+
try {
85+
return YAML.parse(readFileSync(configPath, "utf8")) as Partial<AppConfig>;
86+
} catch (error) {
87+
const message = error instanceof Error ? error.message : String(error);
88+
throw new Error(`Failed to parse workspace config at ${configPath}: ${message}`);
89+
}
90+
}
91+
92+
export function resolveExplorationConfig(options?: {
93+
workspaceRoot?: string;
94+
appConfig?: Partial<AppConfig> | null;
95+
}): ExplorationConfig {
96+
const base = loadExplorationConfig();
97+
const workspaceOverride =
98+
options?.appConfig || !options?.workspaceRoot
99+
? null
100+
: loadWorkspaceConfigOverride(options.workspaceRoot);
101+
const source = options?.appConfig || workspaceOverride;
102+
const enabled =
103+
source?.runtime?.exploration_enabled
104+
?? source?.exploration?.enabled
105+
?? base.enabled;
106+
const figureAuditorEnabled =
107+
source?.exploration?.figure_auditor?.enabled
108+
?? base.figure_auditor.enabled;
109+
110+
return {
111+
...base,
112+
enabled,
113+
figure_auditor: {
114+
...base.figure_auditor,
115+
...(source?.exploration?.figure_auditor || {}),
116+
enabled: figureAuditorEnabled
117+
}
118+
};
119+
}

src/core/exploration/status.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import path from "node:path";
22
import { promises as fs } from "node:fs";
33

44
import { loadBaselineLock } from "./baselineLock.js";
5-
import { loadExplorationConfig } from "./explorationConfig.js";
5+
import { resolveExplorationConfig } from "./explorationConfig.js";
66
import { loadResearchTree, type ResearchTree } from "./researchTree.js";
77
import type { ExplorationStage, FigureAuditSummary, ResearchTreeNode } from "./types.js";
8+
import type { AppConfig } from "../../types.js";
89

910
export interface ExplorationStatusSnapshot {
1011
enabled: boolean;
@@ -139,8 +140,12 @@ function evidenceCompletenessForNode(node: ResearchTreeNode): number {
139140
export async function buildExplorationStatusSnapshot(options: {
140141
workspaceRoot: string;
141142
runId?: string | null;
143+
appConfig?: Partial<AppConfig> | null;
142144
}): Promise<ExplorationStatusSnapshot> {
143-
const config = loadExplorationConfig();
145+
const config = resolveExplorationConfig({
146+
workspaceRoot: options.workspaceRoot,
147+
appConfig: options.appConfig
148+
});
144149
if (!config.enabled) {
145150
return disabledExplorationStatusSnapshot();
146151
}

src/core/nodes/analyzeResults.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ import {
7777
lintFigures,
7878
type FigureAuditInput
7979
} from "../analysis/figureAuditor.js";
80-
import { loadExplorationConfig } from "../exploration/explorationConfig.js";
80+
import { resolveExplorationConfig } from "../exploration/explorationConfig.js";
8181
import { loadResearchTree } from "../exploration/researchTree.js";
8282
import { buildWriteupInputManifest } from "../exploration/evidenceSerializer.js";
8383

@@ -401,7 +401,10 @@ export function createAnalyzeResultsNode(deps: NodeExecutionDeps): GraphNodeHand
401401
`${JSON.stringify(riskSignals, null, 2)}\n`
402402
);
403403
const resultAnalysisPath = await writeRunArtifact(run, "result_analysis.json", JSON.stringify(summary, null, 2));
404-
const explorationFigureConfig = loadExplorationConfig().figure_auditor;
404+
const explorationFigureConfig = resolveExplorationConfig({
405+
workspaceRoot: process.cwd(),
406+
appConfig: deps.config
407+
}).figure_auditor;
405408
if (explorationFigureConfig.enabled) {
406409
const figureAuditInput = await buildFigureAuditInput({
407410
runDir: path.join(process.cwd(), ".autolabos", "runs", run.id),
@@ -608,14 +611,10 @@ export function createAnalyzeResultsNode(deps: NodeExecutionDeps): GraphNodeHand
608611
"run_completeness_checklist.json",
609612
`${JSON.stringify(completenessChecklist, null, 2)}\n`
610613
);
611-
const runtimeExplorationEnabled = (deps.config as any)?.runtime?.exploration_enabled;
612-
const explorationConfig =
613-
typeof runtimeExplorationEnabled === "boolean"
614-
? {
615-
...loadExplorationConfig(),
616-
enabled: runtimeExplorationEnabled
617-
}
618-
: loadExplorationConfig();
614+
const explorationConfig = resolveExplorationConfig({
615+
workspaceRoot: process.cwd(),
616+
appConfig: deps.config
617+
});
619618
if (explorationConfig.enabled) {
620619
const runDir = path.join(process.cwd(), ".autolabos", "runs", run.id);
621620
const tree = loadResearchTree(runDir);

src/core/nodes/designExperiments.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { parseMarkdownRunBriefSections } from "../runs/runBriefParser.js";
3232
import type { MarkdownRunBriefSections } from "../runs/runBriefParser.js";
3333
import { BriefCompletenessArtifact, buildBriefCompletenessArtifact } from "../runs/researchBriefFiles.js";
3434
import { buildWorkspaceRunRoot } from "../runs/runPaths.js";
35-
import { loadExplorationConfig } from "../exploration/explorationConfig.js";
35+
import { resolveExplorationConfig } from "../exploration/explorationConfig.js";
3636
import { ExplorationManager } from "../exploration/explorationManager.js";
3737

3838
interface FilteredHypothesis {
@@ -121,7 +121,10 @@ export function createDesignExperimentsNode(deps: NodeExecutionDeps): GraphNodeH
121121
});
122122
};
123123

124-
const explorationConfig = loadExplorationConfig();
124+
const explorationConfig = resolveExplorationConfig({
125+
workspaceRoot: process.cwd(),
126+
appConfig: deps.config
127+
});
125128
if (explorationConfig.enabled) {
126129
const explorationManager = new ExplorationManager(
127130
run.id,

src/core/nodes/figureAudit.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
lintFigures,
1010
type FigureAuditInput
1111
} from "../analysis/figureAuditor.js";
12-
import { loadExplorationConfig } from "../exploration/explorationConfig.js";
12+
import { resolveExplorationConfig } from "../exploration/explorationConfig.js";
1313
import type { FigureAuditIssue, FigureAuditSummary } from "../exploration/types.js";
1414
import { buildRunCompletenessChecklist } from "../runs/runCompletenessChecklist.js";
1515
import { buildRunOperatorStatus } from "../runs/runStatus.js";
@@ -23,7 +23,10 @@ export function createFigureAuditNode(deps: NodeExecutionDeps): GraphNodeHandler
2323
id: "figure_audit",
2424
async execute({ run }) {
2525
const runDir = path.join(process.cwd(), ".autolabos", "runs", run.id);
26-
const config = loadExplorationConfig();
26+
const config = resolveExplorationConfig({
27+
workspaceRoot: process.cwd(),
28+
appConfig: deps.config
29+
});
2730
const input = await buildFigureAuditInput(runDir);
2831

2932
let gate1Gate2Issues: FigureAuditIssue[] = [];

src/interaction/InteractionSession.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1339,7 +1339,8 @@ export class InteractionSession {
13391339
const targetRunId = this.activeRunId || runs[0]?.id || null;
13401340
const snapshot = await buildExplorationStatusSnapshot({
13411341
workspaceRoot: this.workspaceRoot,
1342-
runId: targetRunId
1342+
runId: targetRunId,
1343+
appConfig: this.config
13431344
});
13441345
for (const line of formatExplorationStatusLines(snapshot)) {
13451346
this.pushLog(line);

src/runtime/createRuntime.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { DEFAULT_OLLAMA_BASE_URL } from "../integrations/ollama/modelCatalog.js"
3333
import { recoverCollectEnrichmentJobs } from "../core/nodes/collectPapers.js";
3434
import { detectExecutionProfile } from "./executionProfile.js";
3535
import { resolveNodeOptionsForPackage } from "../core/stateGraph/defaults.js";
36+
import { loadExplorationConfig } from "../core/exploration/explorationConfig.js";
3637

3738
export interface AutoLabOSRuntime {
3839
paths: AppPaths;
@@ -84,6 +85,7 @@ export async function bootstrapAutoLabOSRuntime(opts?: {
8485
runtime: {
8586
...(config.runtime || {}),
8687
execution_profile: executionProfile,
88+
exploration_enabled: config.exploration?.enabled ?? loadExplorationConfig().enabled,
8789
node_option_package: opts?.nodeOptionPackageName,
8890
resolved_node_options: resolveNodeOptionsForPackage(opts?.nodeOptionPackageName)
8991
}

src/tui/TerminalApp.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2272,7 +2272,8 @@ export class TerminalApp {
22722272
const targetRunId = this.activeRunId || runs[0]?.id || null;
22732273
const snapshot = await buildExplorationStatusSnapshot({
22742274
workspaceRoot: process.cwd(),
2275-
runId: targetRunId
2275+
runId: targetRunId,
2276+
appConfig: this.config
22762277
});
22772278
this.clearTransientLogs();
22782279
for (const line of formatExplorationStatusLines(snapshot)) {

src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,11 +312,21 @@ export interface AppConfig {
312312
runs_dir: string;
313313
logs_dir: string;
314314
};
315+
exploration?: {
316+
enabled?: boolean;
317+
figure_auditor?: {
318+
enabled?: boolean;
319+
block_on_severe_mismatch?: boolean;
320+
require_caption_alignment?: boolean;
321+
require_reference_alignment?: boolean;
322+
};
323+
};
315324
/** Runtime-only environment detection. This is attached in memory and stripped before persisting config.yaml. */
316325
runtime?: {
317326
execution_profile?: ExecutionProfile;
318327
node_option_package?: NodeOptionPackageName;
319328
resolved_node_options?: NodeOptions;
329+
exploration_enabled?: boolean;
320330
};
321331
}
322332

0 commit comments

Comments
 (0)