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
1 change: 1 addition & 0 deletions src/campaign/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export {
type RunCampaignOptions,
runCampaign,
} from './run-campaign'
export { resolveRunDir, tangleTracesRoot } from './run-dir.js'
export { type CampaignBreakdown, campaignBreakdown, campaignMeanComposite } from './score-utils'
export {
type ApplySkillPatchResult,
Expand Down
12 changes: 11 additions & 1 deletion src/campaign/run-campaign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { join } from 'node:path'
import { BackendIntegrityError, type BackendIntegrityReport } from '../integrity/backend-integrity'
import { confidenceInterval } from '../statistics'
import { contentHash } from '../verdict-cache'
import { resolveRunDir } from './run-dir'
import { type CampaignStorage, fsCampaignStorage } from './storage'
import type {
CampaignAggregates,
Expand Down Expand Up @@ -71,8 +72,13 @@ export interface RunCampaignOptions<TScenario extends Scenario, TArtifact> {
* every loop/CI job above it) forever. `undefined`/`0` = unbounded (legacy).
*/
dispatchTimeoutMs?: number
/** Required: where artifacts + traces land. */
/** Required: where artifacts + traces land. A bare name (not an absolute path)
* resolves to the shared `~/.tangle/traces/<repo>/runs/<name>` root so run
* bundles never pollute a repo working tree. Pass an absolute path to override. */
runDir: string
/** Subject repo for the shared run-dir root (defaults to the CWD basename).
* Only consulted when `runDir` is a bare name. */
repo?: string
/** Tracing posture. Default is the substrate's `FileSystemTraceStore` rooted
* at `<runDir>/traces/`. `'off'` disables capture entirely — substrate
* refuses this when the caller wires `autoOnPromote !== 'none'`. */
Expand Down Expand Up @@ -130,6 +136,7 @@ export async function runCampaign<TScenario extends Scenario, TArtifact>(
if (typeof opts.runDir !== 'string' || opts.runDir.trim().length === 0) {
throw new Error('runCampaign: runDir is required and must be a non-empty string')
}
opts.runDir = resolveRunDir(opts.runDir, opts.repo)
storage.ensureDir(opts.runDir)

const manifestHash = computeManifestHash({
Expand Down Expand Up @@ -434,6 +441,8 @@ export interface PlanCampaignRunOptions<TScenario extends Scenario, TArtifact> {
reps?: number
resumable?: boolean
runDir: string
/** Subject repo for the shared run-dir root (see RunCampaignOptions.repo). */
repo?: string
storage?: CampaignStorage
}

Expand All @@ -448,6 +457,7 @@ export function planCampaignRun<TScenario extends Scenario, TArtifact>(
if (typeof opts.runDir !== 'string' || opts.runDir.trim().length === 0) {
throw new Error('planCampaignRun: runDir is required and must be a non-empty string')
}
opts.runDir = resolveRunDir(opts.runDir, opts.repo)

const manifestHash = computeManifestHash({
scenarios: opts.scenarios,
Expand Down
30 changes: 30 additions & 0 deletions src/campaign/run-dir.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { homedir } from 'node:os'
import { basename, isAbsolute, join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { resolveRunDir, tangleTracesRoot } from './run-dir'

describe('resolveRunDir', () => {
it('places a bare name under the shared home root, namespaced by repo', () => {
expect(resolveRunDir('tax-legal-research-r220', 'traces')).toBe(
join(homedir(), '.tangle', 'traces', 'traces', 'runs', 'tax-legal-research-r220'),
)
})

it('defaults the repo segment to the CWD basename', () => {
expect(resolveRunDir('r1')).toBe(
join(tangleTracesRoot(), basename(process.cwd()), 'runs', 'r1'),
)
})

it('honors an absolute path unchanged (explicit override)', () => {
const abs = join(homedir(), 'somewhere', 'else')
expect(isAbsolute(abs)).toBe(true)
expect(resolveRunDir(abs, 'traces')).toBe(abs)
})

it('never lands inside the current repo tree for a bare name', () => {
const resolved = resolveRunDir('r1', 'traces')
expect(resolved.startsWith(process.cwd())).toBe(false)
expect(resolved.startsWith(tangleTracesRoot())).toBe(true)
})
})
21 changes: 21 additions & 0 deletions src/campaign/run-dir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { homedir } from 'node:os'
import { basename, isAbsolute, join } from 'node:path'

/** The shared, out-of-repo root for campaign/benchmark run bundles. Keeping run
* outputs here means they never land in a repo working tree (no per-repo
* gitignore, no clutter, no accidental commits). Layout:
* ~/.tangle/traces/<repo>/runs/<runName>/
* where <repo> disambiguates runs across repos in one place. */
export function tangleTracesRoot(): string {
return join(homedir(), '.tangle', 'traces')
}

/** Resolve a campaign `runDir`. An absolute path is honored as-is (the caller
* chose an explicit location). A bare name is placed under the shared home root
* so bundles never pollute a repo working tree — the default the harness should
* compute so callers pass a *name*, not a path. */
export function resolveRunDir(runDir: string, repo?: string): string {
if (isAbsolute(runDir)) return runDir
const r = repo && repo.trim().length > 0 ? repo : basename(process.cwd())
return join(tangleTracesRoot(), r, 'runs', runDir)
}
Loading