@on-par/factory-cli 0.9.4 → 0.9.5-next.2
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/README.md +3 -3
- package/dist/cli/doctor.d.ts +42 -3
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +221 -42
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/github-stub.d.ts +38 -0
- package/dist/cli/github-stub.d.ts.map +1 -0
- package/dist/cli/github-stub.js +206 -0
- package/dist/cli/github-stub.js.map +1 -0
- package/dist/cli/index.d.ts +35 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +336 -87
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/stop-sentinel.js +3 -3
- package/dist/cli/stop-sentinel.js.map +1 -1
- package/dist/cli/sweep-heartbeat.js +5 -5
- package/dist/cli/sweep-heartbeat.js.map +1 -1
- package/dist/entry.d.ts +4 -1
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +14 -3
- package/dist/entry.js.map +1 -1
- package/dist/node-version.d.ts +5 -1
- package/dist/node-version.d.ts.map +1 -1
- package/dist/node-version.js +11 -3
- package/dist/node-version.js.map +1 -1
- package/package.json +4 -4
package/dist/cli/index.js
CHANGED
|
@@ -5,11 +5,11 @@ import { randomUUID } from 'node:crypto';
|
|
|
5
5
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
7
|
import { arch, hostname as osHostname, release, platform, userInfo } from 'node:os';
|
|
8
|
-
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
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, 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, 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, createPlanReuseStore, checkBaseHealth, currentSlice, 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, 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';
|
|
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, 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';
|
|
@@ -29,7 +29,7 @@ import { buildUsageJson } from './usage-json.js';
|
|
|
29
29
|
import { parseResetIssues, runReset } from './reset.js';
|
|
30
30
|
import { cmdDaemonLogs, cmdDaemonStart, cmdDaemonStatus, cmdDaemonStop, createAutoUpdateDeps, DaemonCtlError, detectInstall, loadDaemonFileConfig, recoverStaleDrain, resolveFactorydConfig, runAutoUpdateLoop, } from './daemon.js';
|
|
31
31
|
import { checkForUpdate, cmdSelfUpdateCheck } from './self-update.js';
|
|
32
|
-
import { analyzeEventLog, claudeKeychainCheck, doctorFailed, eventLogCheck, formatClaimReconcileReport, formatContainerReconcileReport, formatDoctorChecks, formatMicroVmReconcileReport, formatReconcileReport, formatWorktreeReconcileReport, declaredHarnessChecks, hasSandboxRuntime, leaseChecks, keychainPreflightError, orphanContainerChecks, modelsRegistryChecks, orphanMicroVmChecks, runDoctorChecks, sandboxAuthSkipDetail, reconcileAgentAuthChecks, sandboxClaudeAuthChecks, unmergedGreenPrChecks, } from './doctor.js';
|
|
32
|
+
import { analyzeEventLog, claudeKeychainCheck, doctorFailed, eventLogCheck, formatClaimReconcileReport, formatContainerReconcileReport, formatDoctorChecks, formatMicroVmReconcileReport, formatReconcileReport, formatWorktreeReconcileReport, declaredHarnessChecks, hasSandboxRuntime, leaseChecks, keychainPreflightError, orphanContainerChecks, configLintChecks, modelsRegistryChecks, orphanMicroVmChecks, runDoctorChecks, sandboxAuthSkipDetail, reconcileAgentAuthChecks, repoCommandHarnesses, sandboxClaudeAuthChecks, unmergedGreenPrChecks, } from './doctor.js';
|
|
33
33
|
import { DOCS_URL, formatOverview, availableAgentClis, missingAgentCliMessage, missingTokenMessage, notInitializedMessage, } from './first-run.js';
|
|
34
34
|
import { cmdHostedSmoke } from './hosted.js';
|
|
35
35
|
import { cmdHostedQueue } from './hosted-queue.js';
|
|
@@ -40,6 +40,7 @@ import { ensureFactoryExcluded, writeSampleQueue } from './init-files.js';
|
|
|
40
40
|
import { cmdLogs } from './logs.js';
|
|
41
41
|
import { applyHelpGroups } from './help-groups.js';
|
|
42
42
|
import { mergeScopeNotice } from './merge-scope.js';
|
|
43
|
+
import { GITHUB_STUB_CALLS_FILE, recordGitPushes, resolveGitHubMode, selectOctokit } from './github-stub.js';
|
|
43
44
|
import { createFactoryOctokit, formatDeprecation } from './octokit.js';
|
|
44
45
|
import { DeepCheckError, createDeepModelRunner, runIssueCheck } from './ready-check.js';
|
|
45
46
|
import { childRunSucceeded, formatChildRunSummary, runChildrenInOrder } from './run-children.js';
|
|
@@ -61,6 +62,11 @@ const DEFAULT_CONFIG_LOADERS = {
|
|
|
61
62
|
/** Loaders for the current main() invocation; DEFAULT_CONFIG_LOADERS outside main(). */
|
|
62
63
|
let configLoaders = DEFAULT_CONFIG_LOADERS;
|
|
63
64
|
/** Per-field `??` so an explicit `undefined` override falls back instead of replacing the default. */
|
|
65
|
+
/** Display path for the repo config file in preflight messages: repo-relative, or absolute when the state root lives outside the repo (#2586). */
|
|
66
|
+
function repoConfigFileFor(repoRoot, absPath) {
|
|
67
|
+
const rel = relative(repoRoot, absPath);
|
|
68
|
+
return { absPath, relPath: rel === '' || rel.startsWith('..') || isAbsolute(rel) ? absPath : rel };
|
|
69
|
+
}
|
|
64
70
|
function resolveConfigLoaders(overrides, base = configLoaders) {
|
|
65
71
|
return {
|
|
66
72
|
loadModelsConfig: overrides.loadModelsConfig ?? base.loadModelsConfig,
|
|
@@ -223,13 +229,39 @@ async function getRepoRoot(shell = shellOps) {
|
|
|
223
229
|
throw new CliExitError('factory: not inside a git repository', 2);
|
|
224
230
|
}
|
|
225
231
|
}
|
|
232
|
+
const GH_UNAUTHENTICATED = /gh auth login|not logged in/i;
|
|
233
|
+
const GH_TOKEN_REJECTED = /bad credentials|HTTP 401|401 Unauthorized/i;
|
|
234
|
+
const GH_NO_GITHUB_REMOTE = /no git remotes found|point to a known GitHub host/i;
|
|
235
|
+
/**
|
|
236
|
+
* CliExitError message for a failed `gh repo view`; names a rejected token and its `repo` scope when gh reports a 401 (#2593),
|
|
237
|
+
* and gh auth when gh's stderr says it is logged out (#2592). The 401 check runs first because gh's 401 output also says "gh auth login".
|
|
238
|
+
* gh's known-host remote error is checked before the auth pattern because it also names `gh auth login` (#2594).
|
|
239
|
+
*/
|
|
240
|
+
export function ghRepoViewFailureMessage(stderr) {
|
|
241
|
+
const detail = stderr
|
|
242
|
+
?.trim()
|
|
243
|
+
.split('\n')
|
|
244
|
+
.find((l) => l.trim())
|
|
245
|
+
?.trim();
|
|
246
|
+
if (stderr && GH_TOKEN_REJECTED.test(stderr)) {
|
|
247
|
+
return `factory: GitHub rejected the token (GH_TOKEN/GITHUB_TOKEN); use a token with the \`repo\` scope${detail ? ` (gh repo view failed: ${detail})` : ''}`;
|
|
248
|
+
}
|
|
249
|
+
if (stderr && GH_NO_GITHUB_REMOTE.test(stderr)) {
|
|
250
|
+
return 'factory: no GitHub remote detected (gh repo view failed)';
|
|
251
|
+
}
|
|
252
|
+
if (stderr && GH_UNAUTHENTICATED.test(stderr)) {
|
|
253
|
+
return `factory: gh is not authenticated, run \`gh auth login\`${detail ? ` (gh repo view failed: ${detail})` : ''}`;
|
|
254
|
+
}
|
|
255
|
+
return 'factory: no GitHub remote detected (gh repo view failed)';
|
|
256
|
+
}
|
|
226
257
|
async function getGitHubRepo(shell = shellOps) {
|
|
227
258
|
try {
|
|
228
259
|
const { stdout } = await shell.exec('gh repo view --json nameWithOwner --jq .nameWithOwner');
|
|
229
260
|
return stdout.trim();
|
|
230
261
|
}
|
|
231
|
-
catch {
|
|
232
|
-
|
|
262
|
+
catch (err) {
|
|
263
|
+
const stderr = err?.stderr;
|
|
264
|
+
throw new CliExitError(ghRepoViewFailureMessage(typeof stderr === 'string' ? stderr : undefined), 2);
|
|
233
265
|
}
|
|
234
266
|
}
|
|
235
267
|
/** Env token first, then one `gh auth token` subprocess (≤5 s). `undefined` when neither yields one. */
|
|
@@ -247,7 +279,9 @@ function resolveGitHubToken(shell = shellOps) {
|
|
|
247
279
|
/** Octokit factory for the current main() or cmdLand invocation (CliDeps.octokit); unset outside them. */
|
|
248
280
|
let octokitFactory;
|
|
249
281
|
function getOctokit(shell = shellOps, options) {
|
|
250
|
-
return octokitFactory
|
|
282
|
+
return octokitFactory
|
|
283
|
+
? octokitFactory()
|
|
284
|
+
: selectOctokit(process.env, () => createFactoryOctokit(resolveGitHubToken(shell), options));
|
|
251
285
|
}
|
|
252
286
|
export function errorDetail(err) {
|
|
253
287
|
const e = err;
|
|
@@ -868,6 +902,7 @@ async function cmdCheck(issueRaw, opts) {
|
|
|
868
902
|
const repoConfig = configLoaders.loadRepoConfig(repoRoot);
|
|
869
903
|
const effective = resolveEffectiveConfig(repoConfig);
|
|
870
904
|
const router = new coreOps.ModelRouter(applyRepoConfig(configLoaders.loadModelsConfig(), repoConfig), configLoaders.loadRoutesConfig(), false, undefined, effective.allowExperimental, effective.localOnly);
|
|
905
|
+
router.setLocalAuthSink?.(recordLocalAuthFailure);
|
|
871
906
|
runModel = createDeepModelRunner(router, repoRoot);
|
|
872
907
|
}
|
|
873
908
|
let report;
|
|
@@ -971,6 +1006,7 @@ async function cmdClassifierBacktestEntry(opts) {
|
|
|
971
1006
|
const repoConfig = configLoaders.loadRepoConfig(repoRoot);
|
|
972
1007
|
const effective = resolveEffectiveConfig(repoConfig);
|
|
973
1008
|
const router = new coreOps.ModelRouter(applyRepoConfig(configLoaders.loadModelsConfig(), repoConfig), configLoaders.loadRoutesConfig(), false, undefined, effective.allowExperimental, effective.localOnly);
|
|
1009
|
+
router.setLocalAuthSink?.(recordLocalAuthFailure);
|
|
974
1010
|
try {
|
|
975
1011
|
await cmdClassifierBacktest({ ...opts, branchPrefix }, {
|
|
976
1012
|
repoRoot,
|
|
@@ -1213,6 +1249,7 @@ export async function cmdStatus(opts = {}) {
|
|
|
1213
1249
|
const routesConfig = configLoaders.loadRoutesConfig();
|
|
1214
1250
|
const effective = resolveEffectiveConfig(repoConfig);
|
|
1215
1251
|
const router = new coreOps.ModelRouter(modelsConfig, routesConfig, false, undefined, effective.allowExperimental, effective.localOnly);
|
|
1252
|
+
router.setLocalAuthSink?.(recordLocalAuthFailure);
|
|
1216
1253
|
const product = readActiveProduct(paths.product) ?? '(none)';
|
|
1217
1254
|
console.log(styleText('bold', `== ${ghRepo} ==`));
|
|
1218
1255
|
console.log(`Product: ${product}`);
|
|
@@ -1391,6 +1428,7 @@ async function cmdTui(opts = {}, runTuiImpl = runTui) {
|
|
|
1391
1428
|
const routesConfig = configLoaders.loadRoutesConfig();
|
|
1392
1429
|
const effective = resolveEffectiveConfig(repoConfig);
|
|
1393
1430
|
const router = new coreOps.ModelRouter(modelsConfig, routesConfig, false, undefined, effective.allowExperimental, effective.localOnly);
|
|
1431
|
+
router.setLocalAuthSink?.(recordLocalAuthFailure);
|
|
1394
1432
|
const effectiveConfigLines = describeEffectiveConfig({
|
|
1395
1433
|
router,
|
|
1396
1434
|
repo: repoConfig,
|
|
@@ -1448,8 +1486,12 @@ export class LaneEnvironmentPauseError extends Error {
|
|
|
1448
1486
|
baseSha;
|
|
1449
1487
|
failingChecks;
|
|
1450
1488
|
failureSignature;
|
|
1489
|
+
/** BUILD output was kept for `factory resume --from check` (#2564); runLane must not reap it. */
|
|
1490
|
+
buildPreserved;
|
|
1451
1491
|
constructor(message, failure) {
|
|
1452
1492
|
super(message);
|
|
1493
|
+
if (failure.buildPreserved !== undefined)
|
|
1494
|
+
this.buildPreserved = failure.buildPreserved;
|
|
1453
1495
|
this.baseSha = failure.baseSha;
|
|
1454
1496
|
this.failingChecks = failure.failingChecks;
|
|
1455
1497
|
if (failure.failureSignature !== undefined)
|
|
@@ -1543,6 +1585,15 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1543
1585
|
const shell = resolveShellOps(deps);
|
|
1544
1586
|
const core = resolveCoreOps(deps);
|
|
1545
1587
|
const phases = resolvePhaseOps(deps);
|
|
1588
|
+
// FACTORY_GITHUB=stub (#2652): record each `git push` ship runs in the stub's calls.jsonl.
|
|
1589
|
+
let shipPhaseForMode = phases.shipPhase;
|
|
1590
|
+
if (resolveGitHubMode(process.env) === 'stub') {
|
|
1591
|
+
const base = phases.shipPhase;
|
|
1592
|
+
const stubCalls = join(resolve(process.env.FACTORY_GITHUB_STUB_DIR ?? ''), GITHUB_STUB_CALLS_FILE);
|
|
1593
|
+
const defaultShipRunner = promisify(execCb);
|
|
1594
|
+
const wrapped = (o) => base({ ...o, run: recordGitPushes(o.run ?? defaultShipRunner, stubCalls, o.branch) });
|
|
1595
|
+
shipPhaseForMode = wrapped;
|
|
1596
|
+
}
|
|
1546
1597
|
const repoRoot = ctx?.repoRoot ?? (await getRepoRoot(shell));
|
|
1547
1598
|
const ghRepo = ctx?.ghRepo ?? (await getGitHubRepo(shell));
|
|
1548
1599
|
const paths = ctx?.paths ?? getFactoryPaths(repoRoot);
|
|
@@ -1571,6 +1622,7 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1571
1622
|
: { ...resolveEffectiveConfig(repoConfig), branchPrefix: opts.branchPrefix },
|
|
1572
1623
|
};
|
|
1573
1624
|
const router = new core.ModelRouter(policy.models, policy.routes, false, undefined, policy.effective.allowExperimental, policy.effective.localOnly);
|
|
1625
|
+
router.setLocalAuthSink?.(recordLocalAuthFailure);
|
|
1574
1626
|
router.skipMissingHarnessClis(core.isCommandAvailable);
|
|
1575
1627
|
let issueSpend = 0;
|
|
1576
1628
|
let laneSandboxRuntime = 'none';
|
|
@@ -1600,7 +1652,11 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1600
1652
|
const maxSlices = opts.maxSlices ?? resolveSizeGateMaxSlices(factoryConfig);
|
|
1601
1653
|
// ADR-0147: slice k >= 2 builds on its own branch, so read the slice plan before the worktree exists.
|
|
1602
1654
|
let sliceIndex;
|
|
1603
|
-
|
|
1655
|
+
// ADR-0161: stack runs as slice until stacked PRs are built.
|
|
1656
|
+
if ((sizeGateMode === 'slice' || sizeGateMode === 'stack') &&
|
|
1657
|
+
!ctx?.localOnly &&
|
|
1658
|
+
!ctx?.resumeFrom &&
|
|
1659
|
+
work.kind === GITHUB_ISSUE_SOURCE) {
|
|
1604
1660
|
try {
|
|
1605
1661
|
const lookup = await findSlicePlanComment({ octokit, repo: ghRepo, issue: issueNum });
|
|
1606
1662
|
if (lookup.status === 'found')
|
|
@@ -1610,10 +1666,12 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1610
1666
|
throw new LaneParkError(`slice plan lookup failed for #${issueNum}: ${errorDetail(err)} — parked before creating a worktree`, 'escalate');
|
|
1611
1667
|
}
|
|
1612
1668
|
}
|
|
1613
|
-
const branch = branchFor(issueNum, issueTitle, policy.effective.branchPrefix, sliceIndex);
|
|
1614
|
-
const worktree = ctx?.
|
|
1615
|
-
? ctx.
|
|
1616
|
-
:
|
|
1669
|
+
const branch = ctx?.resumeFrom?.branch ?? branchFor(issueNum, issueTitle, policy.effective.branchPrefix, sliceIndex);
|
|
1670
|
+
const worktree = ctx?.resumeFrom
|
|
1671
|
+
? ctx.resumeFrom.worktree
|
|
1672
|
+
: ctx?.localOnly
|
|
1673
|
+
? ctx.localOnly.workspace
|
|
1674
|
+
: worktreePathFor(repoRoot, ghRepo, issueNum, policy.effective.branchPrefix, loaders);
|
|
1617
1675
|
const specPath = resolve(paths.plans, `issue-${issueNum}.md`);
|
|
1618
1676
|
const runStartedAt = new Date().toISOString();
|
|
1619
1677
|
const lane = ctx?.lane;
|
|
@@ -1628,6 +1686,7 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1628
1686
|
'escalate',
|
|
1629
1687
|
'held',
|
|
1630
1688
|
'conflict',
|
|
1689
|
+
'config',
|
|
1631
1690
|
'ci-failed',
|
|
1632
1691
|
'ci-infra',
|
|
1633
1692
|
'timeout',
|
|
@@ -1710,7 +1769,11 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1710
1769
|
// stays a CLI concern (not called here, matching today — a worktree persists past
|
|
1711
1770
|
// a successful run for GC/sweep commands to reclaim later).
|
|
1712
1771
|
let workspace;
|
|
1713
|
-
if (
|
|
1772
|
+
if (ctx?.resumeFrom) {
|
|
1773
|
+
workspace = { path: worktree, dispose: async () => { } };
|
|
1774
|
+
log('worktree', `reusing kept worktree at ${worktree} (resume --from check)`);
|
|
1775
|
+
}
|
|
1776
|
+
else if (!ctx?.localOnly) {
|
|
1714
1777
|
workspace = await worktreeWorkspace({
|
|
1715
1778
|
repoRoot,
|
|
1716
1779
|
branch,
|
|
@@ -1791,9 +1854,21 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1791
1854
|
sandboxPolicy: activeSandboxPolicy,
|
|
1792
1855
|
processGroupGraceMs,
|
|
1793
1856
|
preferredRoute: repoConfig?.route,
|
|
1857
|
+
...(ctx?.resumeFrom
|
|
1858
|
+
? {
|
|
1859
|
+
resumeFrom: {
|
|
1860
|
+
phase: 'check',
|
|
1861
|
+
diffBase: ctx.resumeFrom.diffBase,
|
|
1862
|
+
route: ctx.resumeFrom.route,
|
|
1863
|
+
model: ctx.resumeFrom.model,
|
|
1864
|
+
buildMinutes: ctx.resumeFrom.buildMinutes,
|
|
1865
|
+
},
|
|
1866
|
+
}
|
|
1867
|
+
: {}),
|
|
1794
1868
|
eventsFile: paths.events,
|
|
1795
1869
|
logsDir: paths.logs,
|
|
1796
1870
|
baselineCachePath: paths.baselineCache,
|
|
1871
|
+
repoConfigFile: repoConfigFileFor(repoRoot, paths.config),
|
|
1797
1872
|
flakyLedgerPath: paths.flakyLedger,
|
|
1798
1873
|
};
|
|
1799
1874
|
const ports = {
|
|
@@ -1806,10 +1881,11 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1806
1881
|
getIssueSpend: () => issueSpend,
|
|
1807
1882
|
breaker,
|
|
1808
1883
|
planReuse: ctx?.localOnly ? undefined : createPlanReuseStore(specPath),
|
|
1884
|
+
checkResume: ctx?.localOnly ? undefined : createCheckResumeStore(specPath),
|
|
1809
1885
|
planPhase: phases.planPhase,
|
|
1810
1886
|
buildPhase: phases.buildPhase,
|
|
1811
1887
|
checkPhase: phases.checkPhase,
|
|
1812
|
-
shipPhase:
|
|
1888
|
+
shipPhase: shipPhaseForMode,
|
|
1813
1889
|
checkBaseHealth: deps.checkBaseHealth ?? checkBaseHealth,
|
|
1814
1890
|
resolveConstitution: () =>
|
|
1815
1891
|
// Resolved once here — runIssue calls this exactly once and reuses the value for
|
|
@@ -1884,7 +1960,8 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1884
1960
|
const egressProxy = proxied
|
|
1885
1961
|
? await startEgressProxy({
|
|
1886
1962
|
allowHosts: proxied.allowHosts,
|
|
1887
|
-
|
|
1963
|
+
blockTelemetry: proxied.blockTelemetry,
|
|
1964
|
+
onHost: (host, allowed) => log('egress_host', egressHostMessage(host, allowed)),
|
|
1888
1965
|
})
|
|
1889
1966
|
: undefined;
|
|
1890
1967
|
let outcome;
|
|
@@ -1918,6 +1995,7 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1918
1995
|
baseSha: outcome.baseSha,
|
|
1919
1996
|
failingChecks: outcome.failingChecks,
|
|
1920
1997
|
failureSignature: outcome.failureSignature,
|
|
1998
|
+
buildPreserved: outcome.buildPreserved,
|
|
1921
1999
|
});
|
|
1922
2000
|
}
|
|
1923
2001
|
const reason = outcome.state === 'escalated' ? 'escalate' : outcome.reason;
|
|
@@ -2522,7 +2600,10 @@ async function landIssue(issueNum, repoRoot, ghRepo, paths, octokit, skipCI, bra
|
|
|
2522
2600
|
// Legacy-prefix (LEGACY_BRANCH_PREFIX) branches are guessed too, after the resolved prefix (#1708).
|
|
2523
2601
|
const resolved = branchPrefix ?? resolveEffectiveConfig(configLoaders.loadRepoConfig(repoRoot)).branchPrefix;
|
|
2524
2602
|
const title = await getIssueTitle(ghRepo, issueNum, octokit);
|
|
2525
|
-
|
|
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));
|
|
2526
2607
|
const guessedBranch = guesses[0];
|
|
2527
2608
|
let branch = guessedBranch;
|
|
2528
2609
|
let prNumber;
|
|
@@ -2538,7 +2619,7 @@ async function landIssue(issueNum, repoRoot, ghRepo, paths, octokit, skipCI, bra
|
|
|
2538
2619
|
return undefined;
|
|
2539
2620
|
};
|
|
2540
2621
|
[, prNumber] = await Promise.all([gitOps.gitFetch(repoRoot), findByGuess()]);
|
|
2541
|
-
if (!prNumber) {
|
|
2622
|
+
if (!prNumber && !opts.branch) {
|
|
2542
2623
|
const fallback = await findOpenPRForIssue(octokit, owner, repoName, issueNum);
|
|
2543
2624
|
if (fallback) {
|
|
2544
2625
|
branch = fallback.branch;
|
|
@@ -2650,6 +2731,7 @@ async function cmdTriage(opts) {
|
|
|
2650
2731
|
const routesConfig = configLoaders.loadRoutesConfig();
|
|
2651
2732
|
const effective = resolveEffectiveConfig(repoConfig);
|
|
2652
2733
|
const router = new coreOps.ModelRouter(modelsConfig, routesConfig, false, undefined, effective.allowExperimental, effective.localOnly);
|
|
2734
|
+
router.setLocalAuthSink?.(recordLocalAuthFailure);
|
|
2653
2735
|
router.skipMissingHarnessClis(coreOps.isCommandAvailable);
|
|
2654
2736
|
router.setCostSink((entry) => logCost(paths.costs, { ...entry, issue: '-' }));
|
|
2655
2737
|
let plannerError;
|
|
@@ -2667,7 +2749,7 @@ async function cmdTriage(opts) {
|
|
|
2667
2749
|
model = result.model;
|
|
2668
2750
|
const queue = normalizeTriageQueue(result.output);
|
|
2669
2751
|
if (queue) {
|
|
2670
|
-
const header =
|
|
2752
|
+
const header = triageQueueHeader(model, product);
|
|
2671
2753
|
writeFileSync(paths.queueProposed, header + queue);
|
|
2672
2754
|
}
|
|
2673
2755
|
}
|
|
@@ -2686,6 +2768,9 @@ async function cmdTriage(opts) {
|
|
|
2686
2768
|
throw triageNoProposalError(plannerError);
|
|
2687
2769
|
}
|
|
2688
2770
|
}
|
|
2771
|
+
export function triageQueueHeader(model, product) {
|
|
2772
|
+
return `# Triage queue (model ${model}${product ? `, constitution ${product}` : ''})\n`;
|
|
2773
|
+
}
|
|
2689
2774
|
export function triageProposalMessage(proposed, proposedPath, queuePath) {
|
|
2690
2775
|
if (!proposed.trim())
|
|
2691
2776
|
return null;
|
|
@@ -2695,13 +2780,13 @@ export async function cmdTriageAccept(opts) {
|
|
|
2695
2780
|
const repoRoot = await getRepoRoot();
|
|
2696
2781
|
const paths = getFactoryPaths(repoRoot);
|
|
2697
2782
|
if (!existsSync(paths.queueProposed)) {
|
|
2698
|
-
console.log(`nothing to accept
|
|
2783
|
+
console.log(`nothing to accept: ${paths.queueProposed} not found`);
|
|
2699
2784
|
return; // zero exit
|
|
2700
2785
|
}
|
|
2701
2786
|
const content = readFileSync(paths.queueProposed, 'utf-8');
|
|
2702
2787
|
const result = validateQueue(content);
|
|
2703
2788
|
if (!opts.force && !result.ok) {
|
|
2704
|
-
throw new CliExitError(`factory: proposed queue is invalid
|
|
2789
|
+
throw new CliExitError(`factory: proposed queue is invalid: ${paths.queueProposed} left unchanged\n` +
|
|
2705
2790
|
result.errors.map((e) => ` - ${e}`).join('\n'), 1);
|
|
2706
2791
|
}
|
|
2707
2792
|
renameSync(paths.queueProposed, paths.queue); // same dir → atomic
|
|
@@ -2714,7 +2799,7 @@ export async function cmdTriageAccept(opts) {
|
|
|
2714
2799
|
}
|
|
2715
2800
|
const suffix = opts.force ? ' (--force, validation skipped)' : '';
|
|
2716
2801
|
logEvent(paths.events, 'triage_accepted', '-', `accepted ${result.issues.length} issue(s) [${result.issues.join(', ')}] by ${acceptedBy}${suffix}`);
|
|
2717
|
-
console.log(styleText('green', `queue accepted
|
|
2802
|
+
console.log(styleText('green', `queue accepted: ${result.issues.length} issue(s) promoted to ${paths.queue}`));
|
|
2718
2803
|
}
|
|
2719
2804
|
export async function cmdQueueMigrate(opts = {}) {
|
|
2720
2805
|
const repoRoot = await getRepoRoot();
|
|
@@ -3196,7 +3281,10 @@ async function cmdRun(opts = {}) {
|
|
|
3196
3281
|
repo,
|
|
3197
3282
|
preflight: withAdmissionGuard({
|
|
3198
3283
|
reader: admissionStateReaderFor(paths.state),
|
|
3199
|
-
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
|
+
})),
|
|
3200
3288
|
onConflict: (issue, verdict) => {
|
|
3201
3289
|
const line = `#${issue} refused — ${verdict.reason} | repair: ${verdict.repair}`;
|
|
3202
3290
|
console.error(styleText('yellow', `factory: ${line}`));
|
|
@@ -3231,6 +3319,27 @@ async function cmdRun(opts = {}) {
|
|
|
3231
3319
|
console.log(`[factory] run: ${detail}`);
|
|
3232
3320
|
logEvent(paths.events, 'watchdog', 'all', detail);
|
|
3233
3321
|
}
|
|
3322
|
+
let watchdogHarness;
|
|
3323
|
+
if (knobs.watch) {
|
|
3324
|
+
try {
|
|
3325
|
+
const repoConfig = configLoaders.loadRepoConfig(repoRoot);
|
|
3326
|
+
const registry = new coreOps.ModelRegistry(applyRepoConfig(configLoaders.loadModelsConfig(), repoConfig));
|
|
3327
|
+
const pins = configLoaders.resolveEffectiveModelPins(registry, repoConfig, process.env);
|
|
3328
|
+
const build = resolveEffectiveBuildRoute(registry, repoConfig, pins);
|
|
3329
|
+
watchdogHarness = resolveRunUsageHarness({
|
|
3330
|
+
buildRoute: build.route,
|
|
3331
|
+
buildRouteSource: build.source,
|
|
3332
|
+
planHarnesses: [pins.plan, pins.planFallback]
|
|
3333
|
+
.filter((m) => !!m)
|
|
3334
|
+
.map((m) => registry.getHarnessId(m)),
|
|
3335
|
+
codexDisabled: resolveCodexDisabled(repoConfig),
|
|
3336
|
+
localOnly: resolveLocalOnly(),
|
|
3337
|
+
});
|
|
3338
|
+
}
|
|
3339
|
+
catch {
|
|
3340
|
+
watchdogHarness = undefined; // unresolvable config keeps today's Claude gating
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3234
3343
|
const controller = new AbortController();
|
|
3235
3344
|
const watchdog = knobs.watch
|
|
3236
3345
|
? coreOps
|
|
@@ -3242,6 +3351,7 @@ async function cmdRun(opts = {}) {
|
|
|
3242
3351
|
eventsFile: paths.events,
|
|
3243
3352
|
signal: controller.signal,
|
|
3244
3353
|
estimator: knobs.estimator,
|
|
3354
|
+
...(watchdogHarness ? { harness: watchdogHarness } : {}),
|
|
3245
3355
|
})
|
|
3246
3356
|
.catch((err) => {
|
|
3247
3357
|
// a watchdog crash must never take down the run
|
|
@@ -3290,6 +3400,7 @@ async function cmdRun(opts = {}) {
|
|
|
3290
3400
|
ship: (issue, o, c) => shipIssue(issue, { ...o, branchPrefix, prClassifier: opts.prClassifier }, c),
|
|
3291
3401
|
sweepFinished: (issue) => sweepBetweenLaneIssues(issue, planned.lane, repoRoot, ghRepo, paths).then(() => { }),
|
|
3292
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(() => { }),
|
|
3293
3404
|
waitMerge: (issue, branch, root, gh, p, d = {}) => waitForMerge(issue, branch, root, gh, p, { ...d, mergeOverrides: { auto: opts.autoMerge } }),
|
|
3294
3405
|
}));
|
|
3295
3406
|
}
|
|
@@ -3424,8 +3535,13 @@ export function laneQueueDeps(queue, lane) {
|
|
|
3424
3535
|
countRemaining: async () => (await queue.list(lane)).length,
|
|
3425
3536
|
};
|
|
3426
3537
|
}
|
|
3427
|
-
export function createQueuePreflightOps(octokit, owner, repo) {
|
|
3538
|
+
export function createQueuePreflightOps(octokit, owner, repo, opts = {}) {
|
|
3428
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
|
+
},
|
|
3429
3545
|
expectedHeadRepoFullName: `${owner}/${repo}`,
|
|
3430
3546
|
findOpenPR: (issue) => findOpenPRForIssue(octokit, owner, repo, issue),
|
|
3431
3547
|
getLandState: (prNumber) => getPullRequestLandState(octokit, owner, repo, prNumber),
|
|
@@ -3442,8 +3558,10 @@ export function createQueuePreflightOps(octokit, owner, repo) {
|
|
|
3442
3558
|
},
|
|
3443
3559
|
};
|
|
3444
3560
|
}
|
|
3445
|
-
/** ADR-0147:
|
|
3446
|
-
*
|
|
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. */
|
|
3447
3565
|
async function preflightSlice(issue, deps) {
|
|
3448
3566
|
let lookup;
|
|
3449
3567
|
try {
|
|
@@ -3461,36 +3579,80 @@ async function preflightSlice(issue, deps) {
|
|
|
3461
3579
|
if (lookup.status === 'not-found')
|
|
3462
3580
|
return undefined;
|
|
3463
3581
|
const { plan } = lookup;
|
|
3464
|
-
const
|
|
3465
|
-
|
|
3582
|
+
const n = plan.slices.length;
|
|
3583
|
+
const open = plan.slices.filter((s) => s.state === 'pr-open');
|
|
3584
|
+
if (open.length === 0)
|
|
3466
3585
|
return undefined;
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
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 } };
|
|
3492
3654
|
}
|
|
3493
|
-
return undefined;
|
|
3655
|
+
return (await save()) ?? undefined;
|
|
3494
3656
|
}
|
|
3495
3657
|
/** Resolve queued work from GitHub evidence without consuming model or worktree resources. */
|
|
3496
3658
|
export async function preflightQueuedIssue(issue, deps) {
|
|
@@ -3592,7 +3754,7 @@ export function renderLanePausedComment(payload) {
|
|
|
3592
3754
|
].join('\n\n');
|
|
3593
3755
|
}
|
|
3594
3756
|
export async function runLane(lane, issues, repoRoot, ghRepo, paths, deps = {}) {
|
|
3595
|
-
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;
|
|
3596
3758
|
let streak = null;
|
|
3597
3759
|
let merged = 0;
|
|
3598
3760
|
let awaitingReview = 0;
|
|
@@ -3654,6 +3816,7 @@ export async function runLane(lane, issues, repoRoot, ghRepo, paths, deps = {})
|
|
|
3654
3816
|
streak = null;
|
|
3655
3817
|
continue;
|
|
3656
3818
|
}
|
|
3819
|
+
const layer = decision.kind === 'adopt' ? decision.stackLayer : undefined;
|
|
3657
3820
|
try {
|
|
3658
3821
|
const sliceOutcome = {};
|
|
3659
3822
|
const branch = decision.kind === 'adopt'
|
|
@@ -3676,6 +3839,15 @@ export async function runLane(lane, issues, repoRoot, ghRepo, paths, deps = {})
|
|
|
3676
3839
|
await settle(issue, 'queued');
|
|
3677
3840
|
continue;
|
|
3678
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
|
+
}
|
|
3679
3851
|
await withHeartbeat(issue, heartbeat, () => waitMerge(issue, branch, repoRoot, ghRepo, paths));
|
|
3680
3852
|
merged++;
|
|
3681
3853
|
streak = null;
|
|
@@ -3687,7 +3859,7 @@ export async function runLane(lane, issues, repoRoot, ghRepo, paths, deps = {})
|
|
|
3687
3859
|
// worktree — this is a clean outcome, not a park; move to the next issue.
|
|
3688
3860
|
awaitingReview++;
|
|
3689
3861
|
streak = null;
|
|
3690
|
-
await settle(issue, 'done');
|
|
3862
|
+
await settle(issue, layer && layer.index < layer.count ? 'queued' : 'done');
|
|
3691
3863
|
continue;
|
|
3692
3864
|
}
|
|
3693
3865
|
if (err instanceof IssueSkippedError) {
|
|
@@ -3712,7 +3884,9 @@ export async function runLane(lane, issues, repoRoot, ghRepo, paths, deps = {})
|
|
|
3712
3884
|
// The base is broken, not the issue (#1928): release to queued (never parked),
|
|
3713
3885
|
// pause the lane whatever laneBreakerThreshold is, and stop claiming.
|
|
3714
3886
|
streak = null;
|
|
3715
|
-
|
|
3887
|
+
// A preserved build is kept for `factory resume --from check` (#2564).
|
|
3888
|
+
if (!err.buildPreserved)
|
|
3889
|
+
await reapWorktree(issue).catch(() => { });
|
|
3716
3890
|
await settle(issue, 'queued');
|
|
3717
3891
|
// Persist the hold so a restarted supervisor remembers the broken base (#2348);
|
|
3718
3892
|
// best-effort, never allowed to change lane flow.
|
|
@@ -4195,7 +4369,7 @@ async function defaultListIssueLabels(octokit, owner, repo, issue) {
|
|
|
4195
4369
|
return data.map((label) => label.name);
|
|
4196
4370
|
}
|
|
4197
4371
|
export async function waitForMerge(issue, branch, repoRoot, ghRepo, paths, deps = {}) {
|
|
4198
|
-
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;
|
|
4199
4373
|
const factoryConfig = loadConfig(paths.config);
|
|
4200
4374
|
const isMergeEnabled = mergeEnabled ?? (() => resolveMergePolicy(factoryConfig, process.env, mergeOverrides).auto);
|
|
4201
4375
|
const skipCI = configLoaders.resolveSkipCI(factoryConfig);
|
|
@@ -4253,7 +4427,10 @@ export async function waitForMerge(issue, branch, repoRoot, ghRepo, paths, deps
|
|
|
4253
4427
|
await sleep(120_000);
|
|
4254
4428
|
continue;
|
|
4255
4429
|
}
|
|
4256
|
-
|
|
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);
|
|
4257
4434
|
return;
|
|
4258
4435
|
}
|
|
4259
4436
|
writeLine(`[factory] #${issue} awaiting human merge (poll 120s)`);
|
|
@@ -4383,6 +4560,18 @@ function probeClaudeKeychain() {
|
|
|
4383
4560
|
}
|
|
4384
4561
|
}
|
|
4385
4562
|
async function cmdDoctor(opts = {}) {
|
|
4563
|
+
let repoRoot;
|
|
4564
|
+
try {
|
|
4565
|
+
repoRoot = shellOps
|
|
4566
|
+
.execSync('git rev-parse --show-toplevel', {
|
|
4567
|
+
timeout: 10_000,
|
|
4568
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
4569
|
+
})
|
|
4570
|
+
.trim();
|
|
4571
|
+
}
|
|
4572
|
+
catch {
|
|
4573
|
+
repoRoot = null;
|
|
4574
|
+
}
|
|
4386
4575
|
const checks = runDoctorChecks({
|
|
4387
4576
|
commandAvailable: coreOps.isCommandAvailable,
|
|
4388
4577
|
envPresent: (key) => !!process.env[key],
|
|
@@ -4409,20 +4598,13 @@ async function cmdDoctor(opts = {}) {
|
|
|
4409
4598
|
},
|
|
4410
4599
|
fileExists: existsSync,
|
|
4411
4600
|
}),
|
|
4601
|
+
claudeSessionState: () => claudeStoredSessionState(),
|
|
4602
|
+
localAuthFailure: (agent) => localAuthFailureSinceLogin(agent),
|
|
4412
4603
|
opencodeLoggedIn: () => hasOpencodeLoginEvidence(),
|
|
4604
|
+
commandHarnesses: () => repoRoot === null
|
|
4605
|
+
? []
|
|
4606
|
+
: repoCommandHarnesses(configLoaders.loadModelsConfig().harnesses, configLoaders.loadRepoConfig(repoRoot)?.models?.registry),
|
|
4413
4607
|
});
|
|
4414
|
-
let repoRoot;
|
|
4415
|
-
try {
|
|
4416
|
-
repoRoot = shellOps
|
|
4417
|
-
.execSync('git rev-parse --show-toplevel', {
|
|
4418
|
-
timeout: 10_000,
|
|
4419
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
4420
|
-
})
|
|
4421
|
-
.trim();
|
|
4422
|
-
}
|
|
4423
|
-
catch {
|
|
4424
|
-
repoRoot = null;
|
|
4425
|
-
}
|
|
4426
4608
|
// Only with --probe-inference: these send a real prompt (uses quota). Plain doctor makes no inference call.
|
|
4427
4609
|
if (opts.probeInference) {
|
|
4428
4610
|
const CLAUDE_AUTH_PROBE = 'claude -p "reply with exactly: ok"';
|
|
@@ -4435,13 +4617,13 @@ async function cmdDoctor(opts = {}) {
|
|
|
4435
4617
|
}
|
|
4436
4618
|
};
|
|
4437
4619
|
let host = 'skipped';
|
|
4438
|
-
let hostDetail = 'skipped
|
|
4620
|
+
let hostDetail = 'skipped: claude CLI not on PATH';
|
|
4439
4621
|
if (coreOps.isCommandAvailable('claude')) {
|
|
4440
4622
|
host = probeExec(CLAUDE_AUTH_PROBE) === null ? 'failed' : 'ok';
|
|
4441
4623
|
hostDetail = host === 'ok' ? 'claude -p succeeded on the host' : 'claude -p failed on the host';
|
|
4442
4624
|
}
|
|
4443
4625
|
let sandboxed = 'skipped';
|
|
4444
|
-
let sandboxDetail = 'skipped
|
|
4626
|
+
let sandboxDetail = 'skipped: host claude auth was not verified';
|
|
4445
4627
|
const runtimeAvailable = hasSandboxRuntime(process.platform, (cmd) => isCommandAvailableViaShell(shellOps, cmd));
|
|
4446
4628
|
const skip = sandboxAuthSkipDetail({ runtimeAvailable, hostOk: host === 'ok', inRepo: repoRoot !== null });
|
|
4447
4629
|
if (skip !== null) {
|
|
@@ -4454,13 +4636,13 @@ async function cmdDoctor(opts = {}) {
|
|
|
4454
4636
|
isAvailable: (cmd) => isCommandAvailableViaShell(shellOps, cmd),
|
|
4455
4637
|
});
|
|
4456
4638
|
if (!policy) {
|
|
4457
|
-
sandboxDetail = 'skipped
|
|
4639
|
+
sandboxDetail = 'skipped: sandbox disabled by config or FACTORY_SANDBOX';
|
|
4458
4640
|
}
|
|
4459
4641
|
else if (policy.runtime === 'none') {
|
|
4460
|
-
sandboxDetail = 'skipped
|
|
4642
|
+
sandboxDetail = 'skipped: no sandbox runtime (sandbox-exec/firejail) on this host';
|
|
4461
4643
|
}
|
|
4462
4644
|
else if (policy.runtime === 'docker-sandbox') {
|
|
4463
|
-
sandboxDetail = 'skipped
|
|
4645
|
+
sandboxDetail = 'skipped: docker-sandbox containment is the microVM, not command wrapping (#653)';
|
|
4464
4646
|
}
|
|
4465
4647
|
else {
|
|
4466
4648
|
sandboxed = probeExec(wrapCommandInSandbox(CLAUDE_AUTH_PROBE, policy)) === null ? 'failed' : 'ok';
|
|
@@ -4476,7 +4658,13 @@ async function cmdDoctor(opts = {}) {
|
|
|
4476
4658
|
checks.splice(0, checks.length, ...reconcileAgentAuthChecks(checks));
|
|
4477
4659
|
if (repoRoot !== null) {
|
|
4478
4660
|
const paths = getFactoryPaths(repoRoot);
|
|
4479
|
-
|
|
4661
|
+
let factoryConfig;
|
|
4662
|
+
try {
|
|
4663
|
+
factoryConfig = configLoaders.loadFactoryConfigForRepo(paths.config);
|
|
4664
|
+
}
|
|
4665
|
+
catch {
|
|
4666
|
+
factoryConfig = loadFactoryConfig(); // schema errors are reported by the config row (#2611)
|
|
4667
|
+
}
|
|
4480
4668
|
const health = await inspectPortLeases({ registryFile: paths.ports });
|
|
4481
4669
|
checks.push(...leaseChecks(health.map(toLeaseRow)));
|
|
4482
4670
|
const dockerAvailable = coreOps.isCommandAvailable('docker');
|
|
@@ -4493,6 +4681,15 @@ async function cmdDoctor(opts = {}) {
|
|
|
4493
4681
|
checks.push(stopSentinelCheck(readStopFileStatus(paths)));
|
|
4494
4682
|
const eventsContent = existsSync(paths.events) ? readFileSync(paths.events, 'utf-8') : null;
|
|
4495
4683
|
checks.push(eventLogCheck(eventsContent === null ? null : analyzeEventLog(eventsContent)));
|
|
4684
|
+
let constitution = null;
|
|
4685
|
+
let loadError;
|
|
4686
|
+
try {
|
|
4687
|
+
constitution = new ConstitutionLoader().loadRepoConstitution(repoRoot, factoryConfig.paths?.constitution);
|
|
4688
|
+
}
|
|
4689
|
+
catch (err) {
|
|
4690
|
+
loadError = `cannot parse constitution: ${err instanceof Error ? err.message : String(err)}`;
|
|
4691
|
+
}
|
|
4692
|
+
checks.push(...configLintChecks({ constitution, configFile: repoConfigFileFor(repoRoot, paths.config) }, loadError));
|
|
4496
4693
|
try {
|
|
4497
4694
|
const declared = Object.keys(configLoaders.loadModelsConfig().harnesses ?? {});
|
|
4498
4695
|
checks.push(...modelsRegistryChecks(paths.config, configLoaders.loadRepoConfig(repoRoot)?.models?.registry, [
|
|
@@ -4534,7 +4731,7 @@ async function cmdDoctor(opts = {}) {
|
|
|
4534
4731
|
const graceMs = resolveProcessGroupGraceMs(factoryConfig);
|
|
4535
4732
|
const orphanEvents = await coreOps.reapOrphanProcesses({ reaped, graceMs });
|
|
4536
4733
|
for (const e of orphanEvents) {
|
|
4537
|
-
console.log(`reconcile: ${e.action === 'killed' ? 'killed' : 'found'} pid ${e.pid} (pgid ${e.pgid}, ${e.command}) squatting port ${e.port} of dead lane ${e.worktreeId}${e.action === 'reported' ? '
|
|
4734
|
+
console.log(`reconcile: ${e.action === 'killed' ? 'killed' : 'found'} pid ${e.pid} (pgid ${e.pgid}, ${e.command}) squatting port ${e.port} of dead lane ${e.worktreeId}${e.action === 'reported' ? '; not factory-started, left running' : ''}`);
|
|
4538
4735
|
}
|
|
4539
4736
|
}
|
|
4540
4737
|
try {
|
|
@@ -4549,7 +4746,7 @@ async function cmdDoctor(opts = {}) {
|
|
|
4549
4746
|
console.log(formatWorktreeReconcileReport(report.removed));
|
|
4550
4747
|
}
|
|
4551
4748
|
catch (err) {
|
|
4552
|
-
console.log(`reconcile: worktree sweep failed
|
|
4749
|
+
console.log(`reconcile: worktree sweep failed: ${err?.message ?? String(err)}`);
|
|
4553
4750
|
}
|
|
4554
4751
|
// Stale claim reap (#999): factory:in-progress claims naming a dead local pid go back to
|
|
4555
4752
|
// factory:queued so the next run can claim them with no manual label surgery.
|
|
@@ -4564,11 +4761,11 @@ async function cmdDoctor(opts = {}) {
|
|
|
4564
4761
|
console.log(formatClaimReconcileReport(released));
|
|
4565
4762
|
}
|
|
4566
4763
|
else {
|
|
4567
|
-
console.log('reconcile: skipping stale claims
|
|
4764
|
+
console.log('reconcile: skipping stale claims: no GitHub repo or token');
|
|
4568
4765
|
}
|
|
4569
4766
|
}
|
|
4570
4767
|
catch (err) {
|
|
4571
|
-
console.log(`reconcile: stale claim release failed
|
|
4768
|
+
console.log(`reconcile: stale claim release failed: ${err?.message ?? String(err)}`);
|
|
4572
4769
|
}
|
|
4573
4770
|
}
|
|
4574
4771
|
}
|
|
@@ -4618,7 +4815,17 @@ export async function cmdStop(opts = {}) {
|
|
|
4618
4815
|
}
|
|
4619
4816
|
console.log('STOP set — lanes halt between issues');
|
|
4620
4817
|
}
|
|
4621
|
-
export async function cmdResume(opts = {}) {
|
|
4818
|
+
export async function cmdResume(issue, opts = {}) {
|
|
4819
|
+
if (issue === undefined && opts.from !== undefined) {
|
|
4820
|
+
throw new CliExitError('factory: resume --from needs an issue number: factory resume <issue> --from check', 2);
|
|
4821
|
+
}
|
|
4822
|
+
if (issue !== undefined) {
|
|
4823
|
+
if (opts.from !== 'check') {
|
|
4824
|
+
throw new CliExitError('factory: resume <issue> only supports --from check (BUILD resume is #2320)', 2);
|
|
4825
|
+
}
|
|
4826
|
+
await resumeFromCheck(parseIssueArg(issue));
|
|
4827
|
+
return;
|
|
4828
|
+
}
|
|
4622
4829
|
const repoRoot = await getRepoRoot();
|
|
4623
4830
|
const paths = getFactoryPaths(repoRoot);
|
|
4624
4831
|
const json = opts.json ? { repo: await getGitHubRepo() } : undefined;
|
|
@@ -4633,6 +4840,47 @@ export async function cmdResume(opts = {}) {
|
|
|
4633
4840
|
}
|
|
4634
4841
|
console.log('STOP cleared');
|
|
4635
4842
|
}
|
|
4843
|
+
/** `factory resume <issue> --from check` (#2564): re-run CHECK and later phases on the kept worktree.
|
|
4844
|
+
* `--json` only applies to the STOP form and is ignored here. */
|
|
4845
|
+
async function resumeFromCheck(issueNum) {
|
|
4846
|
+
if (availableAgentClis(coreOps.isCommandAvailable).length === 0) {
|
|
4847
|
+
throw new CliExitError(`factory: ${missingAgentCliMessage()}`, 2);
|
|
4848
|
+
}
|
|
4849
|
+
const repoRoot = await getRepoRoot();
|
|
4850
|
+
const paths = getFactoryPaths(repoRoot);
|
|
4851
|
+
if (!existsSync(paths.root)) {
|
|
4852
|
+
throw new CliExitError(`factory: ${notInitializedMessage()}`, 2);
|
|
4853
|
+
}
|
|
4854
|
+
const record = await createCheckResumeStore(resolve(paths.plans, `issue-${issueNum}.md`)).load();
|
|
4855
|
+
if (!record || !existsSync(record.worktree)) {
|
|
4856
|
+
throw new CliExitError(`factory: no preserved build for #${issueNum} — nothing to resume; run factory run-issue ${issueNum}`, 2);
|
|
4857
|
+
}
|
|
4858
|
+
await withRepoRunLock(paths, 'factory resume', async () => {
|
|
4859
|
+
const ghRepo = await getGitHubRepo();
|
|
4860
|
+
const workSources = createDefaultWorkSourceRegistry({ octokit: getOctokit() });
|
|
4861
|
+
let work;
|
|
4862
|
+
try {
|
|
4863
|
+
work = await workSources.resolve(GITHUB_ISSUE_SOURCE, {
|
|
4864
|
+
repo: ghRepo,
|
|
4865
|
+
issue: issueNum,
|
|
4866
|
+
});
|
|
4867
|
+
}
|
|
4868
|
+
catch (err) {
|
|
4869
|
+
if (err instanceof InvalidWorkRequestInputError) {
|
|
4870
|
+
throw new CliExitError(`factory: ${err.message}`, 2);
|
|
4871
|
+
}
|
|
4872
|
+
throw new CliExitError(`factory: could not resolve issue #${issueNum} in ${ghRepo} (${errorDetail(err)}) — nothing was resumed`, 2);
|
|
4873
|
+
}
|
|
4874
|
+
try {
|
|
4875
|
+
await shipIssue(issueNum, {}, { repoRoot, ghRepo, workRequest: work, resumeFrom: record });
|
|
4876
|
+
}
|
|
4877
|
+
catch (err) {
|
|
4878
|
+
if (err instanceof IssueSkippedError)
|
|
4879
|
+
return;
|
|
4880
|
+
throw new CliExitError(`Resume failed for issue #${issueNum}: ${err.message}`, 1);
|
|
4881
|
+
}
|
|
4882
|
+
});
|
|
4883
|
+
}
|
|
4636
4884
|
// ---------- main ----------
|
|
4637
4885
|
export async function main(argv = process.argv, deps = {}) {
|
|
4638
4886
|
if (argv.slice(2).length === 0) {
|
|
@@ -4793,13 +5041,13 @@ export async function main(argv = process.argv, deps = {}) {
|
|
|
4793
5041
|
.action((runId, opts) => cmdFilingPreview(runId, opts));
|
|
4794
5042
|
const triage = program
|
|
4795
5043
|
.command('triage')
|
|
4796
|
-
.description('Have a model propose a queue from open issues into .factory/queue.proposed')
|
|
5044
|
+
.description('Have a model propose a queue from open issues into .factory/state/queue.proposed')
|
|
4797
5045
|
.option('--product <name>', 'Product constitution to scope triage')
|
|
4798
5046
|
.option('--model <id>', 'Model to triage with (any registered model, Claude or Codex)')
|
|
4799
5047
|
.action(cmdTriage);
|
|
4800
5048
|
triage
|
|
4801
5049
|
.command('accept')
|
|
4802
|
-
.description('Validate and atomically promote .factory/queue.proposed to .factory/queue')
|
|
5050
|
+
.description('Validate and atomically promote .factory/state/queue.proposed to .factory/state/queue')
|
|
4803
5051
|
.option('--force', 'Skip validation and promote as-is')
|
|
4804
5052
|
.action(async (opts) => {
|
|
4805
5053
|
await cmdTriageAccept(opts);
|
|
@@ -4919,7 +5167,7 @@ export async function main(argv = process.argv, deps = {}) {
|
|
|
4919
5167
|
.option('--no-sandbox', 'Disable the containment sandbox for agent runs (dangerous)')
|
|
4920
5168
|
.option('--branch-prefix <prefix>', BRANCH_PREFIX_OPTION_DESCRIPTION)
|
|
4921
5169
|
.option('--run-children', 'File mode only: when the size gate files child issues, run them (slice mode never files issues)')
|
|
4922
|
-
.option('--size-gate <mode>', 'Size gate mode for this run: file (file child issues / park), slice (ship as sequential slice PRs under the same issue; never files issues)
|
|
5170
|
+
.option('--size-gate <mode>', 'Size gate mode for this run: file (file child issues / park), slice (ship as sequential slice PRs under the same issue; never files issues), off (skip both gates) or stack (stacked PR set, ADR-0161; runs as slice until stacks are built); beats sizeGate.mode in the config')
|
|
4923
5171
|
.option('--max-slices <count>', 'Slice mode: most slices a not-yet-started slice plan may have (1-20); an over-cap plan is recorded and parks; beats sizeGate.maxSlices (default 10)')
|
|
4924
5172
|
.action(async (issueNum, opts) => {
|
|
4925
5173
|
await cmdRunIssue(parseIssueArg(issueNum), opts);
|
|
@@ -5069,10 +5317,11 @@ export async function main(argv = process.argv, deps = {}) {
|
|
|
5069
5317
|
.option('--json', 'Print one JSON object (schemaVersion 1): action, repo, stopFlag before/after')
|
|
5070
5318
|
.action((opts) => cmdStop(opts));
|
|
5071
5319
|
program
|
|
5072
|
-
.command('resume')
|
|
5073
|
-
.description('Clear a stop so lanes pick up new issues again')
|
|
5320
|
+
.command('resume [issue]')
|
|
5321
|
+
.description('Clear a stop so lanes pick up new issues again; with <issue> --from check, re-run CHECK on a build kept after an environment failure')
|
|
5074
5322
|
.option('--json', 'Print one JSON object (schemaVersion 1): action, repo, stopFlag before/after')
|
|
5075
|
-
.
|
|
5323
|
+
.option('--from <phase>', 'Resume a kept build at this phase (only: check)')
|
|
5324
|
+
.action((issue, opts) => cmdResume(issue, opts));
|
|
5076
5325
|
applyHelpGroups(program);
|
|
5077
5326
|
const previousLoaders = configLoaders;
|
|
5078
5327
|
const previousGitOps = gitOps;
|