@on-par/factory-cli 0.9.5-next.11 → 0.9.5-next.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -8,8 +8,8 @@ import { arch, hostname as osHostname, release, platform, userInfo } from 'node:
8
8
  import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
9
9
  import { promisify, styleText } from 'node:util';
10
10
  import { BRANCH_PREFIX_MATCH_OPTION_DESCRIPTION, BRANCH_PREFIX_OPTION_DESCRIPTION, INVALID_BRANCH_PREFIX_MESSAGE, resolveBranchPrefixOption, } from './branch-prefix.js';
11
- import { acquirePortLease, aggregateCosts, appendKpiHistoryLine, applyRepoConfig, buildPhase, checkPhase, clearProxyState, closedWorkSkipReason, computeHealthKpis, ConstitutionLoader, createDefaultWorkSourceRegistry, createFileApprovalGate, createLogger, createLaneProxy, defaultFindPortListeners, describeEffectiveConfig, detectPostMergeDefects, diagnoseModels, probeDeclaredHarnesses, drainSteering, estimateTrailingSpend, fetchDefectSources, fetchHumanEventSources, fetchSubscriptionUsage, formatKpiLines, formatUsageReport, getConstitutionsDir, getFactoryPaths, GITHUB_ISSUE_SOURCE, hasUnresolvedPark, InvalidArtifactsDirError, InvalidWorkRequestInputError, InvalidWorkspaceError, inspectPortLeases, isCommandAvailable, isProxyRunning, kpiBranchPrefixes, KNOWN_HARNESS_IDS, kpisToHistoryRecord, laneBaseUrl, laneEnv, LaneFileGuard, laneHostLabel, listQueuedSteering, loadFactoryConfig, loadFactoryConfigForRepo, loadModelsConfig, loadRepoConfig, loadRoutesConfig, LOCAL_BRIEF_SOURCE, localOnlyWorkspace, joinClassifierOutcomes, mergeClassifierOutcomes, parseClassifierOutcomes, summarizeClassifierOutcomes, formatClassifierReport, mergedPrRefs, ModelRegistry, ModelRouter, parkReasonFor, parseKpiHistory, parseQueue, partitionLocalQueueByActivity, phaseSnapshotFile, planPhase, provisionLaneContainer, ProviderBreaker, readEvents, readPortLeases, readQueue, readUsage, reapOrphanProcesses, reapStalePortLeases, reconstructKpiEvents, recordLeasePgid, releasePortLease, renderKpiReport, renderKpiTrend, resolveAutoFailover, resolveCodexDisabled, resolveDefectWindowDays, resolveBuildPublish, resolveSizeGateMode, parseSizeGateMode, parseMaxSlices, resolveSizeGateMaxSlices, SIZE_GATE_MODES, resolveDesignRegressionBlock, resolveReadinessUpdateIssueBody, resolveEffectiveBuildRoute, resolveEffectiveModelPins, resolveEfficiencyPolicy, resolveLaneBreakerThreshold, resolveEnvironmentPorts, resolveEnvironmentProxy, resolveArtifactsDir, resolveIngestConfig, resolveLocalOnlyPolicy, resolveMergePolicy, resolvePlanApproval, resolveProcessGroupGraceMs, resolveSandboxPolicy, resolveSkipCI, resolveTimeouts, resolveUsageCap, resolveWatchdogPolicy, ReworkHistory, rewriteQueueForDecomposition, runAutoIngest, runIssue, shipPhase, summarizeEvent, formatCostTotal, touchLastEvent, touchRunActivity, validateQueue, watchUsage, worktreeWorkspace, writeBenchmarkArtifacts, writeLocalRunReport, writePhaseSnapshot, writeProxyState, } from '@on-par/factory-core';
12
- import { acquirePidFile, createCheckResumeStore, createPlanReuseStore, checkBaseHealth, currentSlice, egressHostMessage, egressSummaryMessage, findSlicePlanComment, recordSliceMerged, upsertSlicePlanComment, withSliceState, DEFAULT_GARDEN_MAX_CLUSTERS, DEFAULT_GARDEN_SINCE, capGardenClusters, clusterGarden, dedupGardenClusters, filterGardenOnlyNew, filterGardenWindow, markGardenTrackingUnknown, parseGardenDuration, readHarvestEvents, renderGardenJson, renderGardenReport, formatWorktreeLocation, laneWorktreePath, resolveWorktreeRoot, migrateRepoConfigToYaml, REPO_CONFIG_YAML_HEADER, branchFor, claudeConfigDirOverride, claudeKeychainService, cleanupWorktree, createDaemonLogSink, factoryBranchIssue, factoryBranchPrefixes, LEGACY_BRANCH_PREFIX, createDockerEngine, createFactorydServer, createRunRuntime, createShipExecutor, createGithubQueue, createLocalSmallDryRun, createOctokitFilingClient, createOctokitGreenPrClient, createOctokitQueueClient, daemonRuntimePaths, DEFAULT_FACTORYD_PORT, defaultRegistryPath, ensureDir, FACTORY_RUNTIME_CONFIG_KEYS, findUnmergedGreenPrs, formatGcReport, gitFetch, listMicroVms, listOrphanContainers, logCost, logEvent, microVmName, planQueueMigration, readCosts, readGithubQueueSnapshot, reapLaneWorktree, reapOrphanContainers, reapOrphanMicroVm, releaseRuntimeFiles, releaseStaleClaims, resolveBranchPrefix, resolveEffectiveConfig, worktreeSandboxFor, resolveExperimental, resolveFilingPolicy, resolveLocalOnly, resolvePrClassifierPolicy, resolveRunUsageHarness, resolveReviewFloorRules, RunLockHeldError, runOvernightQueue, findOpenPR, parseRemoteSlug, setupWorktree, shellEscape, listBuiltInCheckers, shouldProxyEgress, slugify, startEgressProxy, sweepWorktrees, watchChecks, withAdmissionGuard, withFileLock, withGitLock, withRunLock, wrapCommandInSandbox, writePortFile, buildTriagePrompt, normalizeTriageQueue, parseTriageCandidates, codexChatgptSessionState, hasClaudeLoginEvidence, claudeStoredSessionState, localAuthFailureSinceLogin, recordLocalAuthFailure, hasOpencodeLoginEvidence, } from '@on-par/factory-core/internal';
11
+ import { acquirePortLease, aggregateCosts, appendKpiHistoryLine, applyRepoConfig, buildPhase, checkPhase, clearProxyState, computeHealthKpis, ConstitutionLoader, createDefaultWorkSourceRegistry, createLogger, createLaneProxy, defaultFindPortListeners, describeEffectiveConfig, detectPostMergeDefects, diagnoseModels, probeDeclaredHarnesses, estimateTrailingSpend, fetchDefectSources, fetchHumanEventSources, fetchSubscriptionUsage, formatKpiLines, formatUsageReport, getConstitutionsDir, getFactoryPaths, GITHUB_ISSUE_SOURCE, hasUnresolvedPark, InvalidArtifactsDirError, InvalidWorkRequestInputError, InvalidWorkspaceError, inspectPortLeases, isCommandAvailable, isProxyRunning, kpiBranchPrefixes, KNOWN_HARNESS_IDS, kpisToHistoryRecord, laneBaseUrl, laneEnv, laneHostLabel, loadFactoryConfig, loadFactoryConfigForRepo, loadModelsConfig, loadRepoConfig, loadRoutesConfig, LOCAL_BRIEF_SOURCE, joinClassifierOutcomes, mergeClassifierOutcomes, parseClassifierOutcomes, summarizeClassifierOutcomes, formatClassifierReport, mergedPrRefs, ModelRegistry, ModelRouter, parkReasonFor, parseKpiHistory, parseQueue, partitionLocalQueueByActivity, planPhase, provisionLaneContainer, ProviderBreaker, readEvents, readPortLeases, readQueue, readUsage, reapOrphanProcesses, reapStalePortLeases, reconstructKpiEvents, recordLeasePgid, releasePortLease, renderKpiReport, renderKpiTrend, resolveCodexDisabled, resolveDefectWindowDays, resolveSizeGateMode, parseSizeGateMode, parseMaxSlices, SIZE_GATE_MODES, resolveEffectiveBuildRoute, resolveEffectiveModelPins, resolveLaneBreakerThreshold, resolveEnvironmentProxy, resolveArtifactsDir, resolveIngestConfig, resolveLocalOnlyPolicy, resolveMergePolicy, resolveProcessGroupGraceMs, resolveSandboxPolicy, resolveSkipCI, resolveTimeouts, resolveUsageCap, resolveWatchdogPolicy, runAutoIngest, shipPhase, formatCostTotal, validateQueue, watchUsage, writeLocalRunReport, writeProxyState, } from '@on-par/factory-core';
12
+ import { acquirePidFile, createCheckResumeStore, findSlicePlanComment, recordSliceMerged, upsertSlicePlanComment, withSliceState, DEFAULT_GARDEN_MAX_CLUSTERS, DEFAULT_GARDEN_SINCE, capGardenClusters, clusterGarden, dedupGardenClusters, filterGardenOnlyNew, filterGardenWindow, markGardenTrackingUnknown, parseGardenDuration, readHarvestEvents, renderGardenJson, renderGardenReport, formatWorktreeLocation, laneWorktreePath, resolveWorktreeRoot, migrateRepoConfigToYaml, REPO_CONFIG_YAML_HEADER, claudeConfigDirOverride, claudeKeychainService, cleanupWorktree, createDaemonLogSink, factoryBranchIssue, factoryBranchPrefixes, LEGACY_BRANCH_PREFIX, createDockerEngine, createFactorydServer, createRunRuntime, createShipExecutor, createGithubQueue, createLocalSmallDryRun, createOctokitFilingClient, createOctokitGreenPrClient, createOctokitQueueClient, daemonRuntimePaths, DEFAULT_FACTORYD_PORT, defaultRegistryPath, ensureDir, FACTORY_RUNTIME_CONFIG_KEYS, findUnmergedGreenPrs, formatGcReport, gitFetch, listMicroVms, listOrphanContainers, logCost, logEvent, microVmName, planQueueMigration, readCosts, readGithubQueueSnapshot, reapLaneWorktree, reapOrphanContainers, reapOrphanMicroVm, releaseRuntimeFiles, releaseStaleClaims, resolveBranchPrefix, resolveEffectiveConfig, worktreeSandboxFor, resolveExperimental, resolveLocalOnly, resolvePrClassifierPolicy, resolveRunUsageHarness, resolveReviewFloorRules, RunLockHeldError, runOvernightQueue, parseRemoteSlug, setupWorktree, shellEscape, listBuiltInCheckers, slugify, sweepWorktrees, watchChecks, withAdmissionGuard, withFileLock, withGitLock, withRunLock, wrapCommandInSandbox, writePortFile, buildTriagePrompt, normalizeTriageQueue, parseTriageCandidates, codexChatgptSessionState, hasClaudeLoginEvidence, claudeStoredSessionState, localAuthFailureSinceLogin, recordLocalAuthFailure, hasOpencodeLoginEvidence, } from '@on-par/factory-core/internal';
13
13
  import { runTui } from '@on-par/factory-tui';
14
14
  import { Command } from 'commander';
15
15
  import { admissionStateReaderFor, runQueueReconcile } from './admission.js';
@@ -17,6 +17,8 @@ import { cmdClassifierBacktest, ClassifierBacktestInputError, } from './classifi
17
17
  import { findOpenPRForIssue, landIssue, waitForMerge } from './land.js';
18
18
  import { AwaitingReviewError, CiFailedError, CiUnverifiedError, getPullRequestLandState, LandConflictError, LandFailureError, MergeGatedError, } from './land-pr.js';
19
19
  import { setLandRuntime } from './land-runtime.js';
20
+ import { shipIssue } from './ship.js';
21
+ import { setShipRuntime } from './ship-runtime.js';
20
22
  import { runQueueClear } from './queue-clear.js';
21
23
  import { applyStateRootOption } from './state-root.js';
22
24
  import { buildCostJson } from './cost-json.js';
@@ -43,8 +45,8 @@ import { ensureFactoryExcluded, writeSampleQueue } from './init-files.js';
43
45
  import { cmdLogs } from './logs.js';
44
46
  import { applyHelpGroups } from './help-groups.js';
45
47
  import { mergeScopeNotice } from './merge-scope.js';
46
- import { GITHUB_STUB_CALLS_FILE, recordGitPushes, resolveGitHubMode, selectOctokit } from './github-stub.js';
47
- import { createFactoryOctokit, formatDeprecation } from './octokit.js';
48
+ import { selectOctokit } from './github-stub.js';
49
+ import { createFactoryOctokit } from './octokit.js';
48
50
  import { DeepCheckError, createDeepModelRunner, runIssueCheck } from './ready-check.js';
49
51
  import { childRunSucceeded, formatChildRunSummary, runChildrenInOrder } from './run-children.js';
50
52
  import { createBaseHoldGate, formatBaseHoldCleared, formatBaseHoldStatus, readBaseHold, writeBaseHold, } from './base-hold.js';
@@ -306,6 +308,31 @@ setLandRuntime(() => ({
306
308
  worktreePathFor,
307
309
  LaneParkError,
308
310
  }));
311
+ // The ship flow (ship.ts, ship-steps.ts) reads these live, so per-call deps injection reaches it (#2788).
312
+ setShipRuntime(() => ({
313
+ resolveConfigLoaders: (overrides) => resolveConfigLoaders(overrides),
314
+ resolveGitOps: (overrides) => resolveGitOps(overrides),
315
+ resolveShellOps: (overrides) => resolveShellOps(overrides),
316
+ resolveCoreOps: (overrides) => resolveCoreOps(overrides),
317
+ resolvePhaseOps: (overrides) => resolvePhaseOps(overrides),
318
+ getRepoRoot,
319
+ getGitHubRepo,
320
+ getOctokit,
321
+ worktreePathFor,
322
+ repoConfigFileFor,
323
+ isCommandAvailableViaShell,
324
+ readTextFileOrEmpty,
325
+ readActiveProduct,
326
+ errorDetail,
327
+ resolveEnvironmentAcquirer,
328
+ resolveLaneBaseUrl,
329
+ defaultWriteLocalRunReport: () => coreOps.writeLocalRunReport,
330
+ LaneParkError,
331
+ LaneEnvironmentPauseError,
332
+ IssueSkippedError,
333
+ IssueDecomposedError,
334
+ }));
335
+ export { shipIssue };
309
336
  export function hasGitHubToken(env = process.env, tryToken) {
310
337
  if (env.GITHUB_TOKEN || env.GH_TOKEN)
311
338
  return true;
@@ -1593,480 +1620,6 @@ export function resolveEnvironmentAcquirer(opts) {
1593
1620
  };
1594
1621
  };
1595
1622
  }
1596
- export async function shipIssue(issueNum, opts, ctx, deps = {}) {
1597
- const loaders = resolveConfigLoaders(deps);
1598
- const git = resolveGitOps(deps);
1599
- const shell = resolveShellOps(deps);
1600
- const core = resolveCoreOps(deps);
1601
- const phases = resolvePhaseOps(deps);
1602
- // FACTORY_GITHUB=stub (#2652): record each `git push` ship runs in the stub's calls.jsonl.
1603
- let shipPhaseForMode = phases.shipPhase;
1604
- if (resolveGitHubMode(process.env) === 'stub') {
1605
- const base = phases.shipPhase;
1606
- const stubCalls = join(resolve(process.env.FACTORY_GITHUB_STUB_DIR ?? ''), GITHUB_STUB_CALLS_FILE);
1607
- const defaultShipRunner = promisify(execCb);
1608
- const wrapped = (o) => base({ ...o, run: recordGitPushes(o.run ?? defaultShipRunner, stubCalls, o.branch) });
1609
- shipPhaseForMode = wrapped;
1610
- }
1611
- const repoRoot = ctx?.repoRoot ?? (await getRepoRoot(shell));
1612
- const ghRepo = ctx?.ghRepo ?? (await getGitHubRepo(shell));
1613
- const paths = ctx?.paths ?? getFactoryPaths(repoRoot);
1614
- // GitHub REST deprecations seen on this run's client become one warn event per route (#2218).
1615
- const octokit = (deps.octokit ??
1616
- (() => getOctokit(shell, {
1617
- log: (d) => logEvent(paths.events, 'github_api_deprecated', issueNum, formatDeprecation(d), { lane: ctx?.lane }),
1618
- })))();
1619
- const [ghOwner, ghName] = ghRepo.split('/');
1620
- const repoConfig = loaders.loadRepoConfig(repoRoot, paths.root);
1621
- const factoryConfig = loaders.loadFactoryConfigForRepo(paths.config);
1622
- const classifierPolicy = resolvePrClassifierPolicy(factoryConfig, process.env, { classifier: opts.prClassifier });
1623
- const timeouts = loaders.resolveTimeouts(factoryConfig);
1624
- const failoverSettings = resolveAutoFailover(factoryConfig);
1625
- const breaker = new ProviderBreaker(paths.breaker);
1626
- const reworkHistory = new ReworkHistory(paths.reworkHistory);
1627
- const laneFileGuard = new LaneFileGuard(paths.laneFiles);
1628
- const efficiency = resolveEfficiencyPolicy(repoConfig);
1629
- const policy = {
1630
- models: applyRepoConfig(loaders.loadModelsConfig(), repoConfig),
1631
- routes: loaders.loadRoutesConfig(),
1632
- sandbox: factoryConfig.sandbox,
1633
- budget: { perIssueCapUsd: efficiency.perIssueCapUsd },
1634
- effective: opts.branchPrefix === undefined
1635
- ? resolveEffectiveConfig(repoConfig)
1636
- : { ...resolveEffectiveConfig(repoConfig), branchPrefix: opts.branchPrefix },
1637
- };
1638
- const router = new core.ModelRouter(policy.models, policy.routes, false, undefined, policy.effective.allowExperimental, policy.effective.localOnly);
1639
- router.setLocalAuthSink?.(recordLocalAuthFailure);
1640
- router.skipMissingHarnessClis(core.isCommandAvailable);
1641
- let issueSpend = 0;
1642
- let laneSandboxRuntime = 'none';
1643
- const laneWorkspaceBackend = factoryConfig.workspace?.backend === 'disposable-docker' ? 'disposable-docker' : 'worktree';
1644
- router.setCostSink((entry) => {
1645
- issueSpend += entry.cost ?? 0;
1646
- logCost(paths.costs, {
1647
- ...entry,
1648
- issue: String(issueNum),
1649
- sandboxRuntime: laneSandboxRuntime,
1650
- reworkRoundCount: laneReworkRounds,
1651
- workspaceBackend: laneWorkspaceBackend,
1652
- });
1653
- });
1654
- const modelPins = loaders.resolveEffectiveModelPins(router.registryRef, repoConfig);
1655
- const codexOff = resolveCodexDisabled(repoConfig);
1656
- const constitutionLoader = new core.ConstitutionLoader();
1657
- const product = opts.product ?? readActiveProduct(paths.product);
1658
- const autoRework = opts.autoRework ?? true;
1659
- const work = ctx?.workRequest ??
1660
- (await createDefaultWorkSourceRegistry({ octokit }).resolve(GITHUB_ISSUE_SOURCE, {
1661
- repo: ghRepo,
1662
- issue: issueNum,
1663
- }));
1664
- const issueTitle = work.title;
1665
- const sizeGateMode = opts.sizeGate ?? resolveSizeGateMode(factoryConfig);
1666
- const maxSlices = opts.maxSlices ?? resolveSizeGateMaxSlices(factoryConfig);
1667
- // ADR-0147: slice k >= 2 builds on its own branch, so read the slice plan before the worktree exists.
1668
- let sliceIndex;
1669
- // ADR-0161: stack runs as slice until stacked PRs are built.
1670
- if ((sizeGateMode === 'slice' || sizeGateMode === 'stack') &&
1671
- !ctx?.localOnly &&
1672
- !ctx?.resumeFrom &&
1673
- work.kind === GITHUB_ISSUE_SOURCE) {
1674
- try {
1675
- const lookup = await findSlicePlanComment({ octokit, repo: ghRepo, issue: issueNum });
1676
- if (lookup.status === 'found')
1677
- sliceIndex = currentSlice(lookup.plan)?.index;
1678
- }
1679
- catch (err) {
1680
- throw new LaneParkError(`slice plan lookup failed for #${issueNum}: ${errorDetail(err)} — parked before creating a worktree`, 'escalate');
1681
- }
1682
- }
1683
- const branch = ctx?.resumeFrom?.branch ?? branchFor(issueNum, issueTitle, policy.effective.branchPrefix, sliceIndex);
1684
- const worktree = ctx?.resumeFrom
1685
- ? ctx.resumeFrom.worktree
1686
- : ctx?.localOnly
1687
- ? ctx.localOnly.workspace
1688
- : worktreePathFor(repoRoot, ghRepo, issueNum, policy.effective.branchPrefix, loaders);
1689
- const specPath = resolve(paths.plans, `issue-${issueNum}.md`);
1690
- const runStartedAt = new Date().toISOString();
1691
- const lane = ctx?.lane;
1692
- // Captures the message of the last terminal event runIssue emits (ready/fail/
1693
- // escalate/held/conflict/ci-failed/timeout) so the LaneParkError this adapter
1694
- // raises below carries the same diagnostic text shipIssue used to throw with —
1695
- // RunOutcome's parked variant carries only the ParkReason enum, not free text.
1696
- const TERMINAL_EVENT_KINDS = new Set([
1697
- 'ready',
1698
- 'environment-released',
1699
- 'fail',
1700
- 'escalate',
1701
- 'held',
1702
- 'conflict',
1703
- 'config',
1704
- 'ci-failed',
1705
- 'ci-infra',
1706
- 'timeout',
1707
- ]);
1708
- let terminalMessage;
1709
- let laneReworkRounds = 0;
1710
- const mkLog = (phase) => (type, msg, extra) => {
1711
- if (TERMINAL_EVENT_KINDS.has(type))
1712
- terminalMessage = msg;
1713
- if (type === 'rework')
1714
- laneReworkRounds++;
1715
- // checkPhase logs each skipped checker as a 'check' event prefixed "SKIPPED: " —
1716
- // echo it to the terminal the way shipIssue used to print it directly.
1717
- if (type === 'check' && msg.startsWith('SKIPPED: ')) {
1718
- console.error(styleText('yellow', ` SKIP: ${msg.slice('SKIPPED: '.length)}`));
1719
- }
1720
- // Likewise for each failing checker, so a parked run still names which checker
1721
- // failed and why — the parked RunOutcome carries only an aggregate count (#675).
1722
- if (type === 'check' && msg.startsWith('FAILED: ')) {
1723
- console.error(styleText('red', ` FAIL: ${msg.slice('FAILED: '.length)}`));
1724
- }
1725
- logEvent(paths.events, type, issueNum, msg, { ...extra, lane, phase });
1726
- // Best-effort lastEvent summary (#1327) for factory.run.snapshot — an
1727
- // observability side channel, so a write failure is swallowed, not logged (that
1728
- // would re-enter this same function for every event this chokepoint fires on).
1729
- touchLastEvent(phaseSnapshotFile(paths.runs, issueNum), summarizeEvent(type, msg)).catch(() => { });
1730
- };
1731
- const log = mkLog();
1732
- log('issue-title', issueTitle);
1733
- if (modelPins.plan) {
1734
- const source = modelPins.sources.plan === 'repo' ? '.factory/config.json' : 'FACTORY_PLAN_MODEL';
1735
- log('model-override', `plan model pinned to ${modelPins.plan} (${source})`);
1736
- }
1737
- if (modelPins.build) {
1738
- const source = modelPins.sources.build === 'repo' ? '.factory/config.json' : 'FACTORY_BUILD_MODEL';
1739
- log('model-override', `build model pinned to ${modelPins.build} (${source})`);
1740
- }
1741
- const skipReason = closedWorkSkipReason(work);
1742
- if (skipReason) {
1743
- log('skipped-already-closed', skipReason);
1744
- console.log(styleText('yellow', `skipped: ${skipReason}`));
1745
- throw new IssueSkippedError(skipReason, 'already-closed');
1746
- }
1747
- const sandboxPolicy = resolveSandboxPolicy(policy.sandbox, {
1748
- worktree,
1749
- repoRoot,
1750
- cliDisabled: opts.sandbox === false,
1751
- laneId: lane,
1752
- isAvailable: (cmd) => isCommandAvailableViaShell(shell, cmd),
1753
- });
1754
- laneSandboxRuntime = sandboxPolicy?.runtime ?? 'none';
1755
- const worktreeSandbox = worktreeSandboxFor(sandboxPolicy?.runtime, {
1756
- allowHosts: sandboxPolicy?.allowHosts ?? [],
1757
- });
1758
- let activeSandboxPolicy;
1759
- if (opts.sandbox === false) {
1760
- console.error(styleText('yellow', 'factory: sandbox disabled by --no-sandbox — agent runs are UNCONTAINED'));
1761
- log('sandbox-disabled', 'sandbox disabled by --no-sandbox');
1762
- }
1763
- else if (!sandboxPolicy) {
1764
- log('sandbox-disabled', 'sandbox disabled by config/FACTORY_SANDBOX');
1765
- }
1766
- else if (sandboxPolicy.runtime === 'none') {
1767
- log('sandbox-unavailable', 'no sandbox runtime found (sandbox-exec/firejail) — running uncontained');
1768
- }
1769
- else if (sandboxPolicy.runtime === 'docker-sandbox') {
1770
- // docker-sandbox's containment is the microVM created by worktreeWorkspace below
1771
- // (#653), not a command-prefix wrap — activeSandboxPolicy stays undefined because
1772
- // wrapCommandInSandbox remains a no-op for this runtime. createMicroVm logs the
1773
- // actual outcome ('sandbox' on success, 'sandbox-unavailable' on fallback), so no
1774
- // speculative log is emitted here.
1775
- }
1776
- else {
1777
- activeSandboxPolicy = sandboxPolicy;
1778
- if (sandboxPolicy.allowHosts.length > 0) {
1779
- log('sandbox-degraded', `host-level egress filtering unavailable in v1; intended allowlist: ${sandboxPolicy.allowHosts.join(', ')}`);
1780
- }
1781
- }
1782
- // Setup worktree FIRST — PLAN needs cwd=worktree to run claude. Worktree cleanup
1783
- // stays a CLI concern (not called here, matching today — a worktree persists past
1784
- // a successful run for GC/sweep commands to reclaim later).
1785
- let workspace;
1786
- if (ctx?.resumeFrom) {
1787
- workspace = { path: worktree, dispose: async () => { } };
1788
- log('worktree', `reusing kept worktree at ${worktree} (resume --from check)`);
1789
- }
1790
- else if (!ctx?.localOnly) {
1791
- workspace = await worktreeWorkspace({
1792
- repoRoot,
1793
- branch,
1794
- worktreePath: worktree,
1795
- log,
1796
- sandbox: worktreeSandbox,
1797
- findOpenPr: (br) => findOpenPR(octokit, ghOwner, ghName, br),
1798
- setup: (root, br, wt, sp, sandbox, setupLog, setupOpts) => withGitLock(root, () => withFileLock(paths.gitLock,
1799
- // setupWorktree fetches origin itself before creating the worktree (#1167),
1800
- // so no explicit gitFetch here — still under the git + file locks.
1801
- () => git.setupWorktree(root, br, wt, sp, sandbox, setupLog, setupOpts), { onSteal: (pid) => log('lock-stolen', `stole ${paths.gitLock} from dead holder pid ${pid ?? 'unknown'}`) })),
1802
- });
1803
- log('worktree', `Worktree ready at ${worktree}`);
1804
- }
1805
- else {
1806
- workspace = localOnlyWorkspace(ctx.localOnly);
1807
- log('workspace', `local-only: using caller-provided workspace ${worktree} (no factory worktree created)`);
1808
- }
1809
- const planApprovalEnabled = opts.approvePlan ?? resolvePlanApproval(factoryConfig);
1810
- const processGroupGraceMs = resolveProcessGroupGraceMs(factoryConfig);
1811
- const portsSettings = resolveEnvironmentPorts(factoryConfig);
1812
- if (!portsSettings.enabled) {
1813
- log('environment_lease', 'port leasing disabled (environment.ports.enabled=false or FACTORY_ENV_PORTS=0)');
1814
- }
1815
- // acquireEnvironment.recordPgid/release only persist/deallocate the lease — process-
1816
- // group tracking and killing is runIssue's own unconditional tracker (Invariant 3),
1817
- // so a lease-disabled/failed run still tears down every spawned agent process.
1818
- const acquireEnvironment = portsSettings.enabled
1819
- ? resolveEnvironmentAcquirer({
1820
- laneSandboxRuntime: sandboxPolicy?.runtime ?? 'none',
1821
- paths,
1822
- worktree,
1823
- branch,
1824
- range: portsSettings.range,
1825
- processGroupGraceMs,
1826
- log,
1827
- findPortListeners: core.defaultFindPortListeners,
1828
- reapOrphanProcesses: core.reapOrphanProcesses,
1829
- })
1830
- : undefined;
1831
- const proxySettings = resolveEnvironmentProxy(factoryConfig);
1832
- const resolveBaseUrl = (appPort) => resolveLaneBaseUrl(paths, proxySettings, worktree, appPort);
1833
- const request = {
1834
- issue: issueNum,
1835
- repo: ghRepo,
1836
- branch,
1837
- specPath,
1838
- work,
1839
- workSource: ctx?.workSource,
1840
- product,
1841
- startedAt: runStartedAt,
1842
- lane,
1843
- options: {
1844
- interactive: Boolean(opts.interactive),
1845
- autoRework,
1846
- approvePlan: planApprovalEnabled,
1847
- sandboxDisabled: opts.sandbox === false,
1848
- },
1849
- localOnly: Boolean(ctx?.localOnly),
1850
- blockUnresolvedRegressions: resolveDesignRegressionBlock(factoryConfig),
1851
- readinessUpdateIssueBody: resolveReadinessUpdateIssueBody(factoryConfig),
1852
- publishFromBuild: resolveBuildPublish(factoryConfig),
1853
- sizeGateMode,
1854
- maxSlices,
1855
- prClassifier: classifierPolicy.enabled
1856
- ? {
1857
- rules: resolveReviewFloorRules(repoConfig),
1858
- gateLabel: resolveFilingPolicy(factoryConfig).selfFixLabel,
1859
- modelPin: repoConfig?.models?.pins?.classifier,
1860
- }
1861
- : undefined,
1862
- timeouts,
1863
- modelPins,
1864
- codexDisabled: codexOff,
1865
- skipCI: loaders.resolveSkipCI(factoryConfig),
1866
- failover: failoverSettings,
1867
- efficiency: { maxReworkRounds: efficiency.maxReworkRounds, fastPath: efficiency.fastPath },
1868
- sandboxPolicy: activeSandboxPolicy,
1869
- processGroupGraceMs,
1870
- preferredRoute: repoConfig?.route,
1871
- ...(ctx?.resumeFrom
1872
- ? {
1873
- resumeFrom: {
1874
- phase: 'check',
1875
- diffBase: ctx.resumeFrom.diffBase,
1876
- route: ctx.resumeFrom.route,
1877
- model: ctx.resumeFrom.model,
1878
- buildMinutes: ctx.resumeFrom.buildMinutes,
1879
- },
1880
- }
1881
- : {}),
1882
- eventsFile: paths.events,
1883
- logsDir: paths.logs,
1884
- baselineCachePath: paths.baselineCache,
1885
- repoConfigFile: repoConfigFileFor(repoRoot, paths.config),
1886
- flakyLedgerPath: paths.flakyLedger,
1887
- };
1888
- const ports = {
1889
- router,
1890
- octokit,
1891
- workspace,
1892
- events: mkLog,
1893
- acquireEnvironment,
1894
- resolveBaseUrl,
1895
- getIssueSpend: () => issueSpend,
1896
- breaker,
1897
- planReuse: ctx?.localOnly ? undefined : createPlanReuseStore(specPath),
1898
- checkResume: ctx?.localOnly ? undefined : createCheckResumeStore(specPath),
1899
- planPhase: phases.planPhase,
1900
- buildPhase: phases.buildPhase,
1901
- checkPhase: phases.checkPhase,
1902
- shipPhase: shipPhaseForMode,
1903
- checkBaseHealth: deps.checkBaseHealth ?? checkBaseHealth,
1904
- resolveConstitution: () =>
1905
- // Resolved once here — runIssue calls this exactly once and reuses the value for
1906
- // every phase, so the build worker can never author the standards it is graded by.
1907
- // A local-only workspace is caller-provided, not a checkout of repoRoot, so it keeps its own constitution.
1908
- constitutionLoader.resolve(worktree, product, factoryConfig.paths?.constitution, ctx?.localOnly ? undefined : repoRoot),
1909
- createApprovalGate: () => createFileApprovalGate({ dir: paths.approvals, timeoutMs: timeouts.approval * 1000 }),
1910
- drainSteering: () => drainSteering(paths.steering, issueNum, worktree),
1911
- reworkHistory,
1912
- laneFileGuard,
1913
- recordPhase: (phase) => {
1914
- const now = new Date().toISOString();
1915
- return writePhaseSnapshot(phaseSnapshotFile(paths.runs, issueNum), {
1916
- issue: issueNum,
1917
- phase,
1918
- updatedAt: now,
1919
- lastActivityAt: now,
1920
- });
1921
- },
1922
- onActivity: () => touchRunActivity(phaseSnapshotFile(paths.runs, issueNum), new Date().toISOString()),
1923
- onDecomposed: (childIssues) => {
1924
- const childList = childIssues.map((n) => `#${n}`).join(', ');
1925
- const planLog = mkLog('plan');
1926
- try {
1927
- const before = readTextFileOrEmpty(paths.queue);
1928
- const rewrite = rewriteQueueForDecomposition(before, { issue: issueNum, childIssues });
1929
- if (rewrite.changed) {
1930
- writeFileSync(paths.queue, rewrite.content);
1931
- planLog('decompose_filed', `queue entry for #${issueNum} replaced with ${childList}`);
1932
- }
1933
- else {
1934
- planLog('decompose_filed', `#${issueNum} had no queue entry to replace — continuing the lane with ${childList}`);
1935
- }
1936
- }
1937
- catch (err) {
1938
- planLog('decompose_file_failed', `queue rewrite for #${issueNum} failed: ${errorDetail(err)}`);
1939
- }
1940
- throw new IssueDecomposedError(`issue #${issueNum} decomposed into ${childList}`, childIssues);
1941
- },
1942
- writeLocalRunReport: (info) => maybeWriteLocalRunReport({
1943
- issueNum,
1944
- paths,
1945
- startedAt: runStartedAt,
1946
- outcome: info.outcome,
1947
- branch: info.branch,
1948
- worktree,
1949
- specPath,
1950
- route: info.route,
1951
- reason: info.reason,
1952
- writeLocalRunReport: core.writeLocalRunReport,
1953
- }),
1954
- writeBenchmarkArtifacts: (info) => maybeWriteBenchmarkArtifacts({
1955
- issueNum,
1956
- paths,
1957
- ctx,
1958
- startedAt: runStartedAt,
1959
- outcome: info.outcome === 'parked' ? 'failed' : info.outcome,
1960
- branch: info.branch,
1961
- specPath,
1962
- route: info.route,
1963
- checkSummary: info.checkSummary,
1964
- reworkRounds: info.reworkRounds,
1965
- failure: info.failure,
1966
- reportPath: info.reportPath,
1967
- diffBase: info.diffBase,
1968
- log,
1969
- }),
1970
- };
1971
- // #2214: report-only per-lane egress proxy for sandbox-exec. Started here, after worktree setup,
1972
- // so the finally below is the single place that stops it on every exit path.
1973
- const proxied = shouldProxyEgress(request.sandboxPolicy) ? request.sandboxPolicy : undefined;
1974
- const egressProxy = proxied
1975
- ? await startEgressProxy({
1976
- allowHosts: proxied.allowHosts,
1977
- blockTelemetry: proxied.blockTelemetry,
1978
- onHost: (host, allowed) => log('egress_host', egressHostMessage(host, allowed)),
1979
- })
1980
- : undefined;
1981
- let outcome;
1982
- try {
1983
- outcome = await runIssue(proxied && egressProxy ? { ...request, sandboxPolicy: { ...proxied, proxyUrl: egressProxy.url } } : request, policy, ports);
1984
- }
1985
- finally {
1986
- if (proxied && egressProxy) {
1987
- log('egress_summary', egressSummaryMessage(egressProxy.hosts(), proxied.allowHosts));
1988
- await egressProxy.close();
1989
- }
1990
- }
1991
- ctx?.onOutcome?.(outcome);
1992
- if (opts.interactive && !ctx?.localOnly && (outcome.state === 'ready' || outcome.state === 'shipped')) {
1993
- const leftover = listQueuedSteering(paths.steering, issueNum);
1994
- if (leftover.length > 0) {
1995
- log('steering_unconsumed', `${leftover.length} steering message(s) not consumed (no worker phase remained)`);
1996
- }
1997
- }
1998
- if (outcome.state === 'ready' || outcome.state === 'shipped') {
1999
- if (ctx?.localOnly) {
2000
- console.log(styleText('green', `✅ Local-only run complete in ${worktree} (no PR — publishing disabled)`));
2001
- }
2002
- else {
2003
- console.log(styleText('green', `✅ Issue #${issueNum} → ${terminalMessage ?? 'ready'}`));
2004
- }
2005
- return outcome.branch;
2006
- }
2007
- if (outcome.state === 'released') {
2008
- throw new LaneEnvironmentPauseError(terminalMessage ?? 'environment failure on base', {
2009
- baseSha: outcome.baseSha,
2010
- failingChecks: outcome.failingChecks,
2011
- failureSignature: outcome.failureSignature,
2012
- buildPreserved: outcome.buildPreserved,
2013
- });
2014
- }
2015
- const reason = outcome.state === 'escalated' ? 'escalate' : outcome.reason;
2016
- const message = outcome.state === 'escalated' ? outcome.reason : (terminalMessage ?? `run parked: ${outcome.reason}`);
2017
- throw new LaneParkError(message, reason, outcome.state === 'parked'
2018
- ? { failureSignature: outcome.failureSignature, failingChecks: outcome.failingChecks }
2019
- : undefined);
2020
- }
2021
- async function maybeWriteLocalRunReport(opts) {
2022
- if (process.env.FACTORY_LOCAL_ONLY !== '1')
2023
- return undefined;
2024
- const report = await (opts.writeLocalRunReport ?? coreOps.writeLocalRunReport)({
2025
- issue: opts.issueNum,
2026
- eventsFile: opts.paths.events,
2027
- reportsDir: opts.paths.reports,
2028
- startedAt: opts.startedAt,
2029
- outcome: opts.outcome,
2030
- profile: 'local-only',
2031
- branch: opts.branch,
2032
- worktree: opts.worktree,
2033
- specPath: opts.specPath,
2034
- route: opts.route,
2035
- reason: opts.reason,
2036
- });
2037
- console.log(styleText('cyan', `local-only report: ${report.path}`));
2038
- return report.path;
2039
- }
2040
- async function maybeWriteBenchmarkArtifacts(opts) {
2041
- if (!opts.ctx?.localOnly || !opts.ctx.artifactsDir)
2042
- return;
2043
- try {
2044
- const { manifestPath } = await writeBenchmarkArtifacts({
2045
- issue: opts.issueNum,
2046
- artifactsDir: opts.ctx.artifactsDir,
2047
- eventsFile: opts.paths.events,
2048
- costsFile: opts.paths.costs,
2049
- startedAt: opts.startedAt,
2050
- outcome: opts.outcome,
2051
- workspace: opts.ctx.localOnly.workspace,
2052
- branch: opts.branch,
2053
- specPath: opts.specPath,
2054
- route: opts.route,
2055
- request: opts.ctx.workRequest,
2056
- checkSummary: opts.checkSummary,
2057
- reworkRounds: opts.reworkRounds,
2058
- failure: opts.failure,
2059
- reportPath: opts.reportPath,
2060
- diffBase: opts.diffBase,
2061
- });
2062
- opts.log('benchmark-artifacts', `manifest written to ${manifestPath}`);
2063
- console.log(styleText('cyan', `benchmark artifacts: ${manifestPath}`));
2064
- }
2065
- catch (err) {
2066
- // Never let artifact emission mask the run outcome.
2067
- opts.log('benchmark-artifacts-failed', `could not write benchmark artifacts: ${err.message}`);
2068
- }
2069
- }
2070
1623
  /** The autoGcOnRun-gated worktree sweep every pipeline entry point runs before shipping
2071
1624
  * (`factory run`, and since #1756 the one-shot `factory ship` / `factory run-issue`). Never
2072
1625
  * throws — a GC failure is a warn, never a reason to abort the command. */