@on-par/factory-cli 0.9.5-next.12 → 0.9.5-next.14
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.d.ts +6 -40
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +31 -478
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/ship-runtime.d.ts +41 -0
- package/dist/cli/ship-runtime.d.ts.map +1 -0
- package/dist/cli/ship-runtime.js +11 -0
- package/dist/cli/ship-runtime.js.map +1 -0
- package/dist/cli/ship-steps.d.ts +140 -0
- package/dist/cli/ship-steps.d.ts.map +1 -0
- package/dist/cli/ship-steps.js +273 -0
- package/dist/cli/ship-steps.js.map +1 -0
- package/dist/cli/ship.d.ts +41 -0
- package/dist/cli/ship.d.ts.map +1 -0
- package/dist/cli/ship.js +293 -0
- package/dist/cli/ship.js.map +1 -0
- package/package.json +3 -3
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,
|
|
12
|
-
import { acquirePidFile, createCheckResumeStore,
|
|
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 {
|
|
47
|
-
import { createFactoryOctokit
|
|
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. */
|