@on-par/factory-cli 0.9.5-next.1 → 0.9.5-next.3
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/doctor.d.ts +8 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +41 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +20 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +118 -39
- package/dist/cli/index.js.map +1 -1
- package/package.json +4 -4
package/dist/cli/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:pat
|
|
|
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
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, 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, getIssueTitle, 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, isAutoMergeBlocked, 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, watchChecksWithInfraRerun, withAdmissionGuard, withFileLock, withGitLock, withRunLock, wrapCommandInSandbox, writePortFile, buildTriagePrompt, normalizeTriageQueue, parseTriageCandidates, codexChatgptSessionState, hasClaudeLoginEvidence, hasOpencodeLoginEvidence, } from '@on-par/factory-core/internal';
|
|
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, getIssueTitle, 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, isAutoMergeBlocked, 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, watchChecksWithInfraRerun, 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';
|
|
@@ -902,6 +902,7 @@ async function cmdCheck(issueRaw, opts) {
|
|
|
902
902
|
const repoConfig = configLoaders.loadRepoConfig(repoRoot);
|
|
903
903
|
const effective = resolveEffectiveConfig(repoConfig);
|
|
904
904
|
const router = new coreOps.ModelRouter(applyRepoConfig(configLoaders.loadModelsConfig(), repoConfig), configLoaders.loadRoutesConfig(), false, undefined, effective.allowExperimental, effective.localOnly);
|
|
905
|
+
router.setLocalAuthSink?.(recordLocalAuthFailure);
|
|
905
906
|
runModel = createDeepModelRunner(router, repoRoot);
|
|
906
907
|
}
|
|
907
908
|
let report;
|
|
@@ -1005,6 +1006,7 @@ async function cmdClassifierBacktestEntry(opts) {
|
|
|
1005
1006
|
const repoConfig = configLoaders.loadRepoConfig(repoRoot);
|
|
1006
1007
|
const effective = resolveEffectiveConfig(repoConfig);
|
|
1007
1008
|
const router = new coreOps.ModelRouter(applyRepoConfig(configLoaders.loadModelsConfig(), repoConfig), configLoaders.loadRoutesConfig(), false, undefined, effective.allowExperimental, effective.localOnly);
|
|
1009
|
+
router.setLocalAuthSink?.(recordLocalAuthFailure);
|
|
1008
1010
|
try {
|
|
1009
1011
|
await cmdClassifierBacktest({ ...opts, branchPrefix }, {
|
|
1010
1012
|
repoRoot,
|
|
@@ -1247,6 +1249,7 @@ export async function cmdStatus(opts = {}) {
|
|
|
1247
1249
|
const routesConfig = configLoaders.loadRoutesConfig();
|
|
1248
1250
|
const effective = resolveEffectiveConfig(repoConfig);
|
|
1249
1251
|
const router = new coreOps.ModelRouter(modelsConfig, routesConfig, false, undefined, effective.allowExperimental, effective.localOnly);
|
|
1252
|
+
router.setLocalAuthSink?.(recordLocalAuthFailure);
|
|
1250
1253
|
const product = readActiveProduct(paths.product) ?? '(none)';
|
|
1251
1254
|
console.log(styleText('bold', `== ${ghRepo} ==`));
|
|
1252
1255
|
console.log(`Product: ${product}`);
|
|
@@ -1425,6 +1428,7 @@ async function cmdTui(opts = {}, runTuiImpl = runTui) {
|
|
|
1425
1428
|
const routesConfig = configLoaders.loadRoutesConfig();
|
|
1426
1429
|
const effective = resolveEffectiveConfig(repoConfig);
|
|
1427
1430
|
const router = new coreOps.ModelRouter(modelsConfig, routesConfig, false, undefined, effective.allowExperimental, effective.localOnly);
|
|
1431
|
+
router.setLocalAuthSink?.(recordLocalAuthFailure);
|
|
1428
1432
|
const effectiveConfigLines = describeEffectiveConfig({
|
|
1429
1433
|
router,
|
|
1430
1434
|
repo: repoConfig,
|
|
@@ -1618,6 +1622,7 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1618
1622
|
: { ...resolveEffectiveConfig(repoConfig), branchPrefix: opts.branchPrefix },
|
|
1619
1623
|
};
|
|
1620
1624
|
const router = new core.ModelRouter(policy.models, policy.routes, false, undefined, policy.effective.allowExperimental, policy.effective.localOnly);
|
|
1625
|
+
router.setLocalAuthSink?.(recordLocalAuthFailure);
|
|
1621
1626
|
router.skipMissingHarnessClis(core.isCommandAvailable);
|
|
1622
1627
|
let issueSpend = 0;
|
|
1623
1628
|
let laneSandboxRuntime = 'none';
|
|
@@ -2595,7 +2600,10 @@ async function landIssue(issueNum, repoRoot, ghRepo, paths, octokit, skipCI, bra
|
|
|
2595
2600
|
// Legacy-prefix (LEGACY_BRANCH_PREFIX) branches are guessed too, after the resolved prefix (#1708).
|
|
2596
2601
|
const resolved = branchPrefix ?? resolveEffectiveConfig(configLoaders.loadRepoConfig(repoRoot)).branchPrefix;
|
|
2597
2602
|
const title = await getIssueTitle(ghRepo, issueNum, octokit);
|
|
2598
|
-
|
|
2603
|
+
// ADR-0161: a stack layer lands by its exact branch, never by title guess or the Closes PR (the top layer).
|
|
2604
|
+
const guesses = opts.branch
|
|
2605
|
+
? [opts.branch]
|
|
2606
|
+
: factoryBranchPrefixes(resolved).map((p) => branchFor(issueNum, title, p));
|
|
2599
2607
|
const guessedBranch = guesses[0];
|
|
2600
2608
|
let branch = guessedBranch;
|
|
2601
2609
|
let prNumber;
|
|
@@ -2611,7 +2619,7 @@ async function landIssue(issueNum, repoRoot, ghRepo, paths, octokit, skipCI, bra
|
|
|
2611
2619
|
return undefined;
|
|
2612
2620
|
};
|
|
2613
2621
|
[, prNumber] = await Promise.all([gitOps.gitFetch(repoRoot), findByGuess()]);
|
|
2614
|
-
if (!prNumber) {
|
|
2622
|
+
if (!prNumber && !opts.branch) {
|
|
2615
2623
|
const fallback = await findOpenPRForIssue(octokit, owner, repoName, issueNum);
|
|
2616
2624
|
if (fallback) {
|
|
2617
2625
|
branch = fallback.branch;
|
|
@@ -2723,6 +2731,7 @@ async function cmdTriage(opts) {
|
|
|
2723
2731
|
const routesConfig = configLoaders.loadRoutesConfig();
|
|
2724
2732
|
const effective = resolveEffectiveConfig(repoConfig);
|
|
2725
2733
|
const router = new coreOps.ModelRouter(modelsConfig, routesConfig, false, undefined, effective.allowExperimental, effective.localOnly);
|
|
2734
|
+
router.setLocalAuthSink?.(recordLocalAuthFailure);
|
|
2726
2735
|
router.skipMissingHarnessClis(coreOps.isCommandAvailable);
|
|
2727
2736
|
router.setCostSink((entry) => logCost(paths.costs, { ...entry, issue: '-' }));
|
|
2728
2737
|
let plannerError;
|
|
@@ -3272,7 +3281,10 @@ async function cmdRun(opts = {}) {
|
|
|
3272
3281
|
repo,
|
|
3273
3282
|
preflight: withAdmissionGuard({
|
|
3274
3283
|
reader: admissionStateReaderFor(paths.state),
|
|
3275
|
-
preflight: (issue) => preflightQueuedIssue(issue, createQueuePreflightOps(octokit, owner, repo
|
|
3284
|
+
preflight: (issue) => preflightQueuedIssue(issue, createQueuePreflightOps(octokit, owner, repo, {
|
|
3285
|
+
autoMergeStack: resolveSizeGateMode(factoryConfig) === 'stack' &&
|
|
3286
|
+
resolveMergePolicy(factoryConfig, process.env, { auto: opts.autoMerge }).auto,
|
|
3287
|
+
})),
|
|
3276
3288
|
onConflict: (issue, verdict) => {
|
|
3277
3289
|
const line = `#${issue} refused — ${verdict.reason} | repair: ${verdict.repair}`;
|
|
3278
3290
|
console.error(styleText('yellow', `factory: ${line}`));
|
|
@@ -3388,6 +3400,7 @@ async function cmdRun(opts = {}) {
|
|
|
3388
3400
|
ship: (issue, o, c) => shipIssue(issue, { ...o, branchPrefix, prClassifier: opts.prClassifier }, c),
|
|
3389
3401
|
sweepFinished: (issue) => sweepBetweenLaneIssues(issue, planned.lane, repoRoot, ghRepo, paths).then(() => { }),
|
|
3390
3402
|
reapWorktree: (issue) => reapParkedLaneWorktree(issue, repoRoot, ghRepo, paths, opts.branchPrefix === undefined ? undefined : branchPrefix),
|
|
3403
|
+
recordStackMerge: (issue, prNumber) => recordSliceMerged({ octokit: getOctokit(), repo: ghRepo, issue, prNumber }).then(() => { }),
|
|
3391
3404
|
waitMerge: (issue, branch, root, gh, p, d = {}) => waitForMerge(issue, branch, root, gh, p, { ...d, mergeOverrides: { auto: opts.autoMerge } }),
|
|
3392
3405
|
}));
|
|
3393
3406
|
}
|
|
@@ -3522,8 +3535,13 @@ export function laneQueueDeps(queue, lane) {
|
|
|
3522
3535
|
countRemaining: async () => (await queue.list(lane)).length,
|
|
3523
3536
|
};
|
|
3524
3537
|
}
|
|
3525
|
-
export function createQueuePreflightOps(octokit, owner, repo) {
|
|
3538
|
+
export function createQueuePreflightOps(octokit, owner, repo, opts = {}) {
|
|
3526
3539
|
return {
|
|
3540
|
+
autoMergeStack: opts.autoMergeStack ?? false,
|
|
3541
|
+
getPullRefs: async (prNumber) => {
|
|
3542
|
+
const { data } = await octokit.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
|
3543
|
+
return { headRef: data.head.ref, baseRef: data.base.ref, headSha: data.head.sha };
|
|
3544
|
+
},
|
|
3527
3545
|
expectedHeadRepoFullName: `${owner}/${repo}`,
|
|
3528
3546
|
findOpenPR: (issue) => findOpenPRForIssue(octokit, owner, repo, issue),
|
|
3529
3547
|
getLandState: (prNumber) => getPullRequestLandState(octokit, owner, repo, prNumber),
|
|
@@ -3540,8 +3558,10 @@ export function createQueuePreflightOps(octokit, owner, repo) {
|
|
|
3540
3558
|
},
|
|
3541
3559
|
};
|
|
3542
3560
|
}
|
|
3543
|
-
/** ADR-0147:
|
|
3544
|
-
*
|
|
3561
|
+
/** ADR-0147/ADR-0161: walk every pr-open slice PR bottom-up. Merged layers are recorded, a closed layer parks
|
|
3562
|
+
* (naming the layers above it), and the lowest open layer defers, or is adopted when stack auto-merge is on and
|
|
3563
|
+
* its base is main with green CI on the rebased head. Returns undefined to fall through to the Closes-PR
|
|
3564
|
+
* preflight only when no slice PR is still open. */
|
|
3545
3565
|
async function preflightSlice(issue, deps) {
|
|
3546
3566
|
let lookup;
|
|
3547
3567
|
try {
|
|
@@ -3559,36 +3579,80 @@ async function preflightSlice(issue, deps) {
|
|
|
3559
3579
|
if (lookup.status === 'not-found')
|
|
3560
3580
|
return undefined;
|
|
3561
3581
|
const { plan } = lookup;
|
|
3562
|
-
const
|
|
3563
|
-
|
|
3582
|
+
const n = plan.slices.length;
|
|
3583
|
+
const open = plan.slices.filter((s) => s.state === 'pr-open');
|
|
3584
|
+
if (open.length === 0)
|
|
3564
3585
|
return undefined;
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3586
|
+
let next = plan;
|
|
3587
|
+
let lastMerged;
|
|
3588
|
+
const save = async () => {
|
|
3589
|
+
if (lastMerged === undefined)
|
|
3590
|
+
return undefined;
|
|
3591
|
+
try {
|
|
3592
|
+
await deps.saveSlicePlan(issue, next);
|
|
3593
|
+
}
|
|
3594
|
+
catch (err) {
|
|
3595
|
+
return {
|
|
3596
|
+
kind: 'park',
|
|
3597
|
+
reason: `${lastMerged.label} PR #${lastMerged.pr} merged but updating the slice plan failed: ${errorDetail(err)}`,
|
|
3598
|
+
};
|
|
3599
|
+
}
|
|
3600
|
+
return undefined;
|
|
3601
|
+
};
|
|
3602
|
+
for (const [at, slice] of open.entries()) {
|
|
3603
|
+
const label = `slice ${slice.index}/${n}`;
|
|
3604
|
+
if (slice.prNumber === undefined) {
|
|
3605
|
+
return { kind: 'park', reason: `${label} of #${issue} is pr-open but the slice plan records no PR number` };
|
|
3606
|
+
}
|
|
3607
|
+
const pr = slice.prNumber;
|
|
3608
|
+
let state;
|
|
3609
|
+
try {
|
|
3610
|
+
state = await deps.getPullState(pr);
|
|
3611
|
+
}
|
|
3612
|
+
catch (err) {
|
|
3613
|
+
return { kind: 'park', reason: `${label} PR #${pr} state lookup failed: ${errorDetail(err)}` };
|
|
3614
|
+
}
|
|
3615
|
+
if (state === 'merged') {
|
|
3616
|
+
next = withSliceState(next, slice.index, 'merged', pr);
|
|
3617
|
+
lastMerged = { label, pr };
|
|
3618
|
+
continue;
|
|
3619
|
+
}
|
|
3620
|
+
if (state === 'closed') {
|
|
3621
|
+
const above = open.slice(at + 1);
|
|
3622
|
+
const suffix = above.length > 0
|
|
3623
|
+
? `; layers above it: ${above.map((a) => `slice ${a.index}/${n} (#${a.prNumber})`).join(', ')}`
|
|
3624
|
+
: '';
|
|
3625
|
+
return { kind: 'park', reason: `${label} PR #${pr} was closed without merging${suffix}` };
|
|
3626
|
+
}
|
|
3627
|
+
const saved = await save();
|
|
3628
|
+
if (saved)
|
|
3629
|
+
return saved;
|
|
3630
|
+
if (!deps.autoMergeStack)
|
|
3631
|
+
return { kind: 'defer' };
|
|
3632
|
+
let refs;
|
|
3633
|
+
try {
|
|
3634
|
+
refs = await deps.getPullRefs(pr);
|
|
3635
|
+
}
|
|
3636
|
+
catch (err) {
|
|
3637
|
+
return { kind: 'park', reason: `${label} PR #${pr} ref lookup failed: ${errorDetail(err)}` };
|
|
3638
|
+
}
|
|
3639
|
+
// GitHub has not retargeted the layer to main yet: the lower layer is still unmerged.
|
|
3640
|
+
if (refs.baseRef !== 'main')
|
|
3641
|
+
return { kind: 'defer' };
|
|
3642
|
+
let outcome;
|
|
3643
|
+
try {
|
|
3644
|
+
outcome = await deps.watch(refs.headSha);
|
|
3645
|
+
}
|
|
3646
|
+
catch (err) {
|
|
3647
|
+
return { kind: 'park', reason: `CI watch for ${label} PR #${pr} failed: ${errorDetail(err)}` };
|
|
3648
|
+
}
|
|
3649
|
+
if (outcome === 'failure')
|
|
3650
|
+
return { kind: 'park', reason: `CI for ${label} PR #${pr} ended failure` };
|
|
3651
|
+
if (outcome !== 'success')
|
|
3652
|
+
return { kind: 'defer' };
|
|
3653
|
+
return { kind: 'adopt', branch: refs.headRef, stackLayer: { index: slice.index, count: n, prNumber: pr } };
|
|
3590
3654
|
}
|
|
3591
|
-
return undefined;
|
|
3655
|
+
return (await save()) ?? undefined;
|
|
3592
3656
|
}
|
|
3593
3657
|
/** Resolve queued work from GitHub evidence without consuming model or worktree resources. */
|
|
3594
3658
|
export async function preflightQueuedIssue(issue, deps) {
|
|
@@ -3690,7 +3754,7 @@ export function renderLanePausedComment(payload) {
|
|
|
3690
3754
|
].join('\n\n');
|
|
3691
3755
|
}
|
|
3692
3756
|
export async function runLane(lane, issues, repoRoot, ghRepo, paths, deps = {}) {
|
|
3693
|
-
const { ship = shipIssue, waitMerge = waitForMerge, pathExists = existsSync, emitEvent = logEvent, claimNext = async () => null, releaseIssue = async () => { }, heartbeat, reapWorktree = async () => { }, sweepFinished = async () => { }, countRemaining, baseHoldGate, laneBreakerThreshold = 0, commentIssue = async () => { }, } = deps;
|
|
3757
|
+
const { ship = shipIssue, waitMerge = waitForMerge, recordStackMerge = async () => { }, pathExists = existsSync, emitEvent = logEvent, claimNext = async () => null, releaseIssue = async () => { }, heartbeat, reapWorktree = async () => { }, sweepFinished = async () => { }, countRemaining, baseHoldGate, laneBreakerThreshold = 0, commentIssue = async () => { }, } = deps;
|
|
3694
3758
|
let streak = null;
|
|
3695
3759
|
let merged = 0;
|
|
3696
3760
|
let awaitingReview = 0;
|
|
@@ -3752,6 +3816,7 @@ export async function runLane(lane, issues, repoRoot, ghRepo, paths, deps = {})
|
|
|
3752
3816
|
streak = null;
|
|
3753
3817
|
continue;
|
|
3754
3818
|
}
|
|
3819
|
+
const layer = decision.kind === 'adopt' ? decision.stackLayer : undefined;
|
|
3755
3820
|
try {
|
|
3756
3821
|
const sliceOutcome = {};
|
|
3757
3822
|
const branch = decision.kind === 'adopt'
|
|
@@ -3774,6 +3839,15 @@ export async function runLane(lane, issues, repoRoot, ghRepo, paths, deps = {})
|
|
|
3774
3839
|
await settle(issue, 'queued');
|
|
3775
3840
|
continue;
|
|
3776
3841
|
}
|
|
3842
|
+
if (layer) {
|
|
3843
|
+
// ADR-0161: land exactly this layer's branch, record it, and re-queue for the next layer.
|
|
3844
|
+
await withHeartbeat(issue, heartbeat, () => waitMerge(issue, branch, repoRoot, ghRepo, paths, { landBranch: branch }));
|
|
3845
|
+
await recordStackMerge(issue, layer.prNumber).catch((err) => emitEvent(paths.events, 'warn', issue, `slice ${layer.index}/${layer.count} merged but recording it failed: ${errorDetail(err)}`, { lane }));
|
|
3846
|
+
merged++;
|
|
3847
|
+
streak = null;
|
|
3848
|
+
await settle(issue, layer.index < layer.count ? 'queued' : 'done');
|
|
3849
|
+
continue;
|
|
3850
|
+
}
|
|
3777
3851
|
await withHeartbeat(issue, heartbeat, () => waitMerge(issue, branch, repoRoot, ghRepo, paths));
|
|
3778
3852
|
merged++;
|
|
3779
3853
|
streak = null;
|
|
@@ -3785,7 +3859,7 @@ export async function runLane(lane, issues, repoRoot, ghRepo, paths, deps = {})
|
|
|
3785
3859
|
// worktree — this is a clean outcome, not a park; move to the next issue.
|
|
3786
3860
|
awaitingReview++;
|
|
3787
3861
|
streak = null;
|
|
3788
|
-
await settle(issue, 'done');
|
|
3862
|
+
await settle(issue, layer && layer.index < layer.count ? 'queued' : 'done');
|
|
3789
3863
|
continue;
|
|
3790
3864
|
}
|
|
3791
3865
|
if (err instanceof IssueSkippedError) {
|
|
@@ -4295,7 +4369,7 @@ async function defaultListIssueLabels(octokit, owner, repo, issue) {
|
|
|
4295
4369
|
return data.map((label) => label.name);
|
|
4296
4370
|
}
|
|
4297
4371
|
export async function waitForMerge(issue, branch, repoRoot, ghRepo, paths, deps = {}) {
|
|
4298
|
-
const { createOctokit = getOctokit, pathExists = existsSync, checkMerged = isPrMerged, loadConfig = configLoaders.loadFactoryConfigForRepo, land = landIssue, listIssueLabels = defaultListIssueLabels, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), emitEvent = logEvent, mergeEnabled, writeLine = (line) => console.log(line), now = () => Date.now(), mergeOverrides, } = deps;
|
|
4372
|
+
const { createOctokit = getOctokit, pathExists = existsSync, checkMerged = isPrMerged, loadConfig = configLoaders.loadFactoryConfigForRepo, land = landIssue, listIssueLabels = defaultListIssueLabels, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), emitEvent = logEvent, mergeEnabled, writeLine = (line) => console.log(line), now = () => Date.now(), mergeOverrides, landBranch, } = deps;
|
|
4299
4373
|
const factoryConfig = loadConfig(paths.config);
|
|
4300
4374
|
const isMergeEnabled = mergeEnabled ?? (() => resolveMergePolicy(factoryConfig, process.env, mergeOverrides).auto);
|
|
4301
4375
|
const skipCI = configLoaders.resolveSkipCI(factoryConfig);
|
|
@@ -4353,7 +4427,10 @@ export async function waitForMerge(issue, branch, repoRoot, ghRepo, paths, deps
|
|
|
4353
4427
|
await sleep(120_000);
|
|
4354
4428
|
continue;
|
|
4355
4429
|
}
|
|
4356
|
-
|
|
4430
|
+
if (landBranch)
|
|
4431
|
+
await land(issue, repoRoot, ghRepo, paths, octokit, skipCI, undefined, { branch: landBranch });
|
|
4432
|
+
else
|
|
4433
|
+
await land(issue, repoRoot, ghRepo, paths, octokit, skipCI);
|
|
4357
4434
|
return;
|
|
4358
4435
|
}
|
|
4359
4436
|
writeLine(`[factory] #${issue} awaiting human merge (poll 120s)`);
|
|
@@ -4521,6 +4598,8 @@ async function cmdDoctor(opts = {}) {
|
|
|
4521
4598
|
},
|
|
4522
4599
|
fileExists: existsSync,
|
|
4523
4600
|
}),
|
|
4601
|
+
claudeSessionState: () => claudeStoredSessionState(),
|
|
4602
|
+
localAuthFailure: (agent) => localAuthFailureSinceLogin(agent),
|
|
4524
4603
|
opencodeLoggedIn: () => hasOpencodeLoginEvidence(),
|
|
4525
4604
|
commandHarnesses: () => repoRoot === null
|
|
4526
4605
|
? []
|