@on-par/factory-cli 0.9.4 → 0.9.5-next.1
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 +34 -3
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +180 -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 +15 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +219 -49
- 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 +3 -3
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, 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';
|
|
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;
|
|
@@ -1448,8 +1482,12 @@ export class LaneEnvironmentPauseError extends Error {
|
|
|
1448
1482
|
baseSha;
|
|
1449
1483
|
failingChecks;
|
|
1450
1484
|
failureSignature;
|
|
1485
|
+
/** BUILD output was kept for `factory resume --from check` (#2564); runLane must not reap it. */
|
|
1486
|
+
buildPreserved;
|
|
1451
1487
|
constructor(message, failure) {
|
|
1452
1488
|
super(message);
|
|
1489
|
+
if (failure.buildPreserved !== undefined)
|
|
1490
|
+
this.buildPreserved = failure.buildPreserved;
|
|
1453
1491
|
this.baseSha = failure.baseSha;
|
|
1454
1492
|
this.failingChecks = failure.failingChecks;
|
|
1455
1493
|
if (failure.failureSignature !== undefined)
|
|
@@ -1543,6 +1581,15 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1543
1581
|
const shell = resolveShellOps(deps);
|
|
1544
1582
|
const core = resolveCoreOps(deps);
|
|
1545
1583
|
const phases = resolvePhaseOps(deps);
|
|
1584
|
+
// FACTORY_GITHUB=stub (#2652): record each `git push` ship runs in the stub's calls.jsonl.
|
|
1585
|
+
let shipPhaseForMode = phases.shipPhase;
|
|
1586
|
+
if (resolveGitHubMode(process.env) === 'stub') {
|
|
1587
|
+
const base = phases.shipPhase;
|
|
1588
|
+
const stubCalls = join(resolve(process.env.FACTORY_GITHUB_STUB_DIR ?? ''), GITHUB_STUB_CALLS_FILE);
|
|
1589
|
+
const defaultShipRunner = promisify(execCb);
|
|
1590
|
+
const wrapped = (o) => base({ ...o, run: recordGitPushes(o.run ?? defaultShipRunner, stubCalls, o.branch) });
|
|
1591
|
+
shipPhaseForMode = wrapped;
|
|
1592
|
+
}
|
|
1546
1593
|
const repoRoot = ctx?.repoRoot ?? (await getRepoRoot(shell));
|
|
1547
1594
|
const ghRepo = ctx?.ghRepo ?? (await getGitHubRepo(shell));
|
|
1548
1595
|
const paths = ctx?.paths ?? getFactoryPaths(repoRoot);
|
|
@@ -1600,7 +1647,11 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1600
1647
|
const maxSlices = opts.maxSlices ?? resolveSizeGateMaxSlices(factoryConfig);
|
|
1601
1648
|
// ADR-0147: slice k >= 2 builds on its own branch, so read the slice plan before the worktree exists.
|
|
1602
1649
|
let sliceIndex;
|
|
1603
|
-
|
|
1650
|
+
// ADR-0161: stack runs as slice until stacked PRs are built.
|
|
1651
|
+
if ((sizeGateMode === 'slice' || sizeGateMode === 'stack') &&
|
|
1652
|
+
!ctx?.localOnly &&
|
|
1653
|
+
!ctx?.resumeFrom &&
|
|
1654
|
+
work.kind === GITHUB_ISSUE_SOURCE) {
|
|
1604
1655
|
try {
|
|
1605
1656
|
const lookup = await findSlicePlanComment({ octokit, repo: ghRepo, issue: issueNum });
|
|
1606
1657
|
if (lookup.status === 'found')
|
|
@@ -1610,10 +1661,12 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1610
1661
|
throw new LaneParkError(`slice plan lookup failed for #${issueNum}: ${errorDetail(err)} — parked before creating a worktree`, 'escalate');
|
|
1611
1662
|
}
|
|
1612
1663
|
}
|
|
1613
|
-
const branch = branchFor(issueNum, issueTitle, policy.effective.branchPrefix, sliceIndex);
|
|
1614
|
-
const worktree = ctx?.
|
|
1615
|
-
? ctx.
|
|
1616
|
-
:
|
|
1664
|
+
const branch = ctx?.resumeFrom?.branch ?? branchFor(issueNum, issueTitle, policy.effective.branchPrefix, sliceIndex);
|
|
1665
|
+
const worktree = ctx?.resumeFrom
|
|
1666
|
+
? ctx.resumeFrom.worktree
|
|
1667
|
+
: ctx?.localOnly
|
|
1668
|
+
? ctx.localOnly.workspace
|
|
1669
|
+
: worktreePathFor(repoRoot, ghRepo, issueNum, policy.effective.branchPrefix, loaders);
|
|
1617
1670
|
const specPath = resolve(paths.plans, `issue-${issueNum}.md`);
|
|
1618
1671
|
const runStartedAt = new Date().toISOString();
|
|
1619
1672
|
const lane = ctx?.lane;
|
|
@@ -1628,6 +1681,7 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1628
1681
|
'escalate',
|
|
1629
1682
|
'held',
|
|
1630
1683
|
'conflict',
|
|
1684
|
+
'config',
|
|
1631
1685
|
'ci-failed',
|
|
1632
1686
|
'ci-infra',
|
|
1633
1687
|
'timeout',
|
|
@@ -1710,7 +1764,11 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1710
1764
|
// stays a CLI concern (not called here, matching today — a worktree persists past
|
|
1711
1765
|
// a successful run for GC/sweep commands to reclaim later).
|
|
1712
1766
|
let workspace;
|
|
1713
|
-
if (
|
|
1767
|
+
if (ctx?.resumeFrom) {
|
|
1768
|
+
workspace = { path: worktree, dispose: async () => { } };
|
|
1769
|
+
log('worktree', `reusing kept worktree at ${worktree} (resume --from check)`);
|
|
1770
|
+
}
|
|
1771
|
+
else if (!ctx?.localOnly) {
|
|
1714
1772
|
workspace = await worktreeWorkspace({
|
|
1715
1773
|
repoRoot,
|
|
1716
1774
|
branch,
|
|
@@ -1791,9 +1849,21 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1791
1849
|
sandboxPolicy: activeSandboxPolicy,
|
|
1792
1850
|
processGroupGraceMs,
|
|
1793
1851
|
preferredRoute: repoConfig?.route,
|
|
1852
|
+
...(ctx?.resumeFrom
|
|
1853
|
+
? {
|
|
1854
|
+
resumeFrom: {
|
|
1855
|
+
phase: 'check',
|
|
1856
|
+
diffBase: ctx.resumeFrom.diffBase,
|
|
1857
|
+
route: ctx.resumeFrom.route,
|
|
1858
|
+
model: ctx.resumeFrom.model,
|
|
1859
|
+
buildMinutes: ctx.resumeFrom.buildMinutes,
|
|
1860
|
+
},
|
|
1861
|
+
}
|
|
1862
|
+
: {}),
|
|
1794
1863
|
eventsFile: paths.events,
|
|
1795
1864
|
logsDir: paths.logs,
|
|
1796
1865
|
baselineCachePath: paths.baselineCache,
|
|
1866
|
+
repoConfigFile: repoConfigFileFor(repoRoot, paths.config),
|
|
1797
1867
|
flakyLedgerPath: paths.flakyLedger,
|
|
1798
1868
|
};
|
|
1799
1869
|
const ports = {
|
|
@@ -1806,10 +1876,11 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1806
1876
|
getIssueSpend: () => issueSpend,
|
|
1807
1877
|
breaker,
|
|
1808
1878
|
planReuse: ctx?.localOnly ? undefined : createPlanReuseStore(specPath),
|
|
1879
|
+
checkResume: ctx?.localOnly ? undefined : createCheckResumeStore(specPath),
|
|
1809
1880
|
planPhase: phases.planPhase,
|
|
1810
1881
|
buildPhase: phases.buildPhase,
|
|
1811
1882
|
checkPhase: phases.checkPhase,
|
|
1812
|
-
shipPhase:
|
|
1883
|
+
shipPhase: shipPhaseForMode,
|
|
1813
1884
|
checkBaseHealth: deps.checkBaseHealth ?? checkBaseHealth,
|
|
1814
1885
|
resolveConstitution: () =>
|
|
1815
1886
|
// Resolved once here — runIssue calls this exactly once and reuses the value for
|
|
@@ -1884,7 +1955,8 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1884
1955
|
const egressProxy = proxied
|
|
1885
1956
|
? await startEgressProxy({
|
|
1886
1957
|
allowHosts: proxied.allowHosts,
|
|
1887
|
-
|
|
1958
|
+
blockTelemetry: proxied.blockTelemetry,
|
|
1959
|
+
onHost: (host, allowed) => log('egress_host', egressHostMessage(host, allowed)),
|
|
1888
1960
|
})
|
|
1889
1961
|
: undefined;
|
|
1890
1962
|
let outcome;
|
|
@@ -1918,6 +1990,7 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1918
1990
|
baseSha: outcome.baseSha,
|
|
1919
1991
|
failingChecks: outcome.failingChecks,
|
|
1920
1992
|
failureSignature: outcome.failureSignature,
|
|
1993
|
+
buildPreserved: outcome.buildPreserved,
|
|
1921
1994
|
});
|
|
1922
1995
|
}
|
|
1923
1996
|
const reason = outcome.state === 'escalated' ? 'escalate' : outcome.reason;
|
|
@@ -2667,7 +2740,7 @@ async function cmdTriage(opts) {
|
|
|
2667
2740
|
model = result.model;
|
|
2668
2741
|
const queue = normalizeTriageQueue(result.output);
|
|
2669
2742
|
if (queue) {
|
|
2670
|
-
const header =
|
|
2743
|
+
const header = triageQueueHeader(model, product);
|
|
2671
2744
|
writeFileSync(paths.queueProposed, header + queue);
|
|
2672
2745
|
}
|
|
2673
2746
|
}
|
|
@@ -2686,6 +2759,9 @@ async function cmdTriage(opts) {
|
|
|
2686
2759
|
throw triageNoProposalError(plannerError);
|
|
2687
2760
|
}
|
|
2688
2761
|
}
|
|
2762
|
+
export function triageQueueHeader(model, product) {
|
|
2763
|
+
return `# Triage queue (model ${model}${product ? `, constitution ${product}` : ''})\n`;
|
|
2764
|
+
}
|
|
2689
2765
|
export function triageProposalMessage(proposed, proposedPath, queuePath) {
|
|
2690
2766
|
if (!proposed.trim())
|
|
2691
2767
|
return null;
|
|
@@ -2695,13 +2771,13 @@ export async function cmdTriageAccept(opts) {
|
|
|
2695
2771
|
const repoRoot = await getRepoRoot();
|
|
2696
2772
|
const paths = getFactoryPaths(repoRoot);
|
|
2697
2773
|
if (!existsSync(paths.queueProposed)) {
|
|
2698
|
-
console.log(`nothing to accept
|
|
2774
|
+
console.log(`nothing to accept: ${paths.queueProposed} not found`);
|
|
2699
2775
|
return; // zero exit
|
|
2700
2776
|
}
|
|
2701
2777
|
const content = readFileSync(paths.queueProposed, 'utf-8');
|
|
2702
2778
|
const result = validateQueue(content);
|
|
2703
2779
|
if (!opts.force && !result.ok) {
|
|
2704
|
-
throw new CliExitError(`factory: proposed queue is invalid
|
|
2780
|
+
throw new CliExitError(`factory: proposed queue is invalid: ${paths.queueProposed} left unchanged\n` +
|
|
2705
2781
|
result.errors.map((e) => ` - ${e}`).join('\n'), 1);
|
|
2706
2782
|
}
|
|
2707
2783
|
renameSync(paths.queueProposed, paths.queue); // same dir → atomic
|
|
@@ -2714,7 +2790,7 @@ export async function cmdTriageAccept(opts) {
|
|
|
2714
2790
|
}
|
|
2715
2791
|
const suffix = opts.force ? ' (--force, validation skipped)' : '';
|
|
2716
2792
|
logEvent(paths.events, 'triage_accepted', '-', `accepted ${result.issues.length} issue(s) [${result.issues.join(', ')}] by ${acceptedBy}${suffix}`);
|
|
2717
|
-
console.log(styleText('green', `queue accepted
|
|
2793
|
+
console.log(styleText('green', `queue accepted: ${result.issues.length} issue(s) promoted to ${paths.queue}`));
|
|
2718
2794
|
}
|
|
2719
2795
|
export async function cmdQueueMigrate(opts = {}) {
|
|
2720
2796
|
const repoRoot = await getRepoRoot();
|
|
@@ -3231,6 +3307,27 @@ async function cmdRun(opts = {}) {
|
|
|
3231
3307
|
console.log(`[factory] run: ${detail}`);
|
|
3232
3308
|
logEvent(paths.events, 'watchdog', 'all', detail);
|
|
3233
3309
|
}
|
|
3310
|
+
let watchdogHarness;
|
|
3311
|
+
if (knobs.watch) {
|
|
3312
|
+
try {
|
|
3313
|
+
const repoConfig = configLoaders.loadRepoConfig(repoRoot);
|
|
3314
|
+
const registry = new coreOps.ModelRegistry(applyRepoConfig(configLoaders.loadModelsConfig(), repoConfig));
|
|
3315
|
+
const pins = configLoaders.resolveEffectiveModelPins(registry, repoConfig, process.env);
|
|
3316
|
+
const build = resolveEffectiveBuildRoute(registry, repoConfig, pins);
|
|
3317
|
+
watchdogHarness = resolveRunUsageHarness({
|
|
3318
|
+
buildRoute: build.route,
|
|
3319
|
+
buildRouteSource: build.source,
|
|
3320
|
+
planHarnesses: [pins.plan, pins.planFallback]
|
|
3321
|
+
.filter((m) => !!m)
|
|
3322
|
+
.map((m) => registry.getHarnessId(m)),
|
|
3323
|
+
codexDisabled: resolveCodexDisabled(repoConfig),
|
|
3324
|
+
localOnly: resolveLocalOnly(),
|
|
3325
|
+
});
|
|
3326
|
+
}
|
|
3327
|
+
catch {
|
|
3328
|
+
watchdogHarness = undefined; // unresolvable config keeps today's Claude gating
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3234
3331
|
const controller = new AbortController();
|
|
3235
3332
|
const watchdog = knobs.watch
|
|
3236
3333
|
? coreOps
|
|
@@ -3242,6 +3339,7 @@ async function cmdRun(opts = {}) {
|
|
|
3242
3339
|
eventsFile: paths.events,
|
|
3243
3340
|
signal: controller.signal,
|
|
3244
3341
|
estimator: knobs.estimator,
|
|
3342
|
+
...(watchdogHarness ? { harness: watchdogHarness } : {}),
|
|
3245
3343
|
})
|
|
3246
3344
|
.catch((err) => {
|
|
3247
3345
|
// a watchdog crash must never take down the run
|
|
@@ -3712,7 +3810,9 @@ export async function runLane(lane, issues, repoRoot, ghRepo, paths, deps = {})
|
|
|
3712
3810
|
// The base is broken, not the issue (#1928): release to queued (never parked),
|
|
3713
3811
|
// pause the lane whatever laneBreakerThreshold is, and stop claiming.
|
|
3714
3812
|
streak = null;
|
|
3715
|
-
|
|
3813
|
+
// A preserved build is kept for `factory resume --from check` (#2564).
|
|
3814
|
+
if (!err.buildPreserved)
|
|
3815
|
+
await reapWorktree(issue).catch(() => { });
|
|
3716
3816
|
await settle(issue, 'queued');
|
|
3717
3817
|
// Persist the hold so a restarted supervisor remembers the broken base (#2348);
|
|
3718
3818
|
// best-effort, never allowed to change lane flow.
|
|
@@ -4383,6 +4483,18 @@ function probeClaudeKeychain() {
|
|
|
4383
4483
|
}
|
|
4384
4484
|
}
|
|
4385
4485
|
async function cmdDoctor(opts = {}) {
|
|
4486
|
+
let repoRoot;
|
|
4487
|
+
try {
|
|
4488
|
+
repoRoot = shellOps
|
|
4489
|
+
.execSync('git rev-parse --show-toplevel', {
|
|
4490
|
+
timeout: 10_000,
|
|
4491
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
4492
|
+
})
|
|
4493
|
+
.trim();
|
|
4494
|
+
}
|
|
4495
|
+
catch {
|
|
4496
|
+
repoRoot = null;
|
|
4497
|
+
}
|
|
4386
4498
|
const checks = runDoctorChecks({
|
|
4387
4499
|
commandAvailable: coreOps.isCommandAvailable,
|
|
4388
4500
|
envPresent: (key) => !!process.env[key],
|
|
@@ -4410,19 +4522,10 @@ async function cmdDoctor(opts = {}) {
|
|
|
4410
4522
|
fileExists: existsSync,
|
|
4411
4523
|
}),
|
|
4412
4524
|
opencodeLoggedIn: () => hasOpencodeLoginEvidence(),
|
|
4525
|
+
commandHarnesses: () => repoRoot === null
|
|
4526
|
+
? []
|
|
4527
|
+
: repoCommandHarnesses(configLoaders.loadModelsConfig().harnesses, configLoaders.loadRepoConfig(repoRoot)?.models?.registry),
|
|
4413
4528
|
});
|
|
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
4529
|
// Only with --probe-inference: these send a real prompt (uses quota). Plain doctor makes no inference call.
|
|
4427
4530
|
if (opts.probeInference) {
|
|
4428
4531
|
const CLAUDE_AUTH_PROBE = 'claude -p "reply with exactly: ok"';
|
|
@@ -4435,13 +4538,13 @@ async function cmdDoctor(opts = {}) {
|
|
|
4435
4538
|
}
|
|
4436
4539
|
};
|
|
4437
4540
|
let host = 'skipped';
|
|
4438
|
-
let hostDetail = 'skipped
|
|
4541
|
+
let hostDetail = 'skipped: claude CLI not on PATH';
|
|
4439
4542
|
if (coreOps.isCommandAvailable('claude')) {
|
|
4440
4543
|
host = probeExec(CLAUDE_AUTH_PROBE) === null ? 'failed' : 'ok';
|
|
4441
4544
|
hostDetail = host === 'ok' ? 'claude -p succeeded on the host' : 'claude -p failed on the host';
|
|
4442
4545
|
}
|
|
4443
4546
|
let sandboxed = 'skipped';
|
|
4444
|
-
let sandboxDetail = 'skipped
|
|
4547
|
+
let sandboxDetail = 'skipped: host claude auth was not verified';
|
|
4445
4548
|
const runtimeAvailable = hasSandboxRuntime(process.platform, (cmd) => isCommandAvailableViaShell(shellOps, cmd));
|
|
4446
4549
|
const skip = sandboxAuthSkipDetail({ runtimeAvailable, hostOk: host === 'ok', inRepo: repoRoot !== null });
|
|
4447
4550
|
if (skip !== null) {
|
|
@@ -4454,13 +4557,13 @@ async function cmdDoctor(opts = {}) {
|
|
|
4454
4557
|
isAvailable: (cmd) => isCommandAvailableViaShell(shellOps, cmd),
|
|
4455
4558
|
});
|
|
4456
4559
|
if (!policy) {
|
|
4457
|
-
sandboxDetail = 'skipped
|
|
4560
|
+
sandboxDetail = 'skipped: sandbox disabled by config or FACTORY_SANDBOX';
|
|
4458
4561
|
}
|
|
4459
4562
|
else if (policy.runtime === 'none') {
|
|
4460
|
-
sandboxDetail = 'skipped
|
|
4563
|
+
sandboxDetail = 'skipped: no sandbox runtime (sandbox-exec/firejail) on this host';
|
|
4461
4564
|
}
|
|
4462
4565
|
else if (policy.runtime === 'docker-sandbox') {
|
|
4463
|
-
sandboxDetail = 'skipped
|
|
4566
|
+
sandboxDetail = 'skipped: docker-sandbox containment is the microVM, not command wrapping (#653)';
|
|
4464
4567
|
}
|
|
4465
4568
|
else {
|
|
4466
4569
|
sandboxed = probeExec(wrapCommandInSandbox(CLAUDE_AUTH_PROBE, policy)) === null ? 'failed' : 'ok';
|
|
@@ -4476,7 +4579,13 @@ async function cmdDoctor(opts = {}) {
|
|
|
4476
4579
|
checks.splice(0, checks.length, ...reconcileAgentAuthChecks(checks));
|
|
4477
4580
|
if (repoRoot !== null) {
|
|
4478
4581
|
const paths = getFactoryPaths(repoRoot);
|
|
4479
|
-
|
|
4582
|
+
let factoryConfig;
|
|
4583
|
+
try {
|
|
4584
|
+
factoryConfig = configLoaders.loadFactoryConfigForRepo(paths.config);
|
|
4585
|
+
}
|
|
4586
|
+
catch {
|
|
4587
|
+
factoryConfig = loadFactoryConfig(); // schema errors are reported by the config row (#2611)
|
|
4588
|
+
}
|
|
4480
4589
|
const health = await inspectPortLeases({ registryFile: paths.ports });
|
|
4481
4590
|
checks.push(...leaseChecks(health.map(toLeaseRow)));
|
|
4482
4591
|
const dockerAvailable = coreOps.isCommandAvailable('docker');
|
|
@@ -4493,6 +4602,15 @@ async function cmdDoctor(opts = {}) {
|
|
|
4493
4602
|
checks.push(stopSentinelCheck(readStopFileStatus(paths)));
|
|
4494
4603
|
const eventsContent = existsSync(paths.events) ? readFileSync(paths.events, 'utf-8') : null;
|
|
4495
4604
|
checks.push(eventLogCheck(eventsContent === null ? null : analyzeEventLog(eventsContent)));
|
|
4605
|
+
let constitution = null;
|
|
4606
|
+
let loadError;
|
|
4607
|
+
try {
|
|
4608
|
+
constitution = new ConstitutionLoader().loadRepoConstitution(repoRoot, factoryConfig.paths?.constitution);
|
|
4609
|
+
}
|
|
4610
|
+
catch (err) {
|
|
4611
|
+
loadError = `cannot parse constitution: ${err instanceof Error ? err.message : String(err)}`;
|
|
4612
|
+
}
|
|
4613
|
+
checks.push(...configLintChecks({ constitution, configFile: repoConfigFileFor(repoRoot, paths.config) }, loadError));
|
|
4496
4614
|
try {
|
|
4497
4615
|
const declared = Object.keys(configLoaders.loadModelsConfig().harnesses ?? {});
|
|
4498
4616
|
checks.push(...modelsRegistryChecks(paths.config, configLoaders.loadRepoConfig(repoRoot)?.models?.registry, [
|
|
@@ -4534,7 +4652,7 @@ async function cmdDoctor(opts = {}) {
|
|
|
4534
4652
|
const graceMs = resolveProcessGroupGraceMs(factoryConfig);
|
|
4535
4653
|
const orphanEvents = await coreOps.reapOrphanProcesses({ reaped, graceMs });
|
|
4536
4654
|
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' ? '
|
|
4655
|
+
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
4656
|
}
|
|
4539
4657
|
}
|
|
4540
4658
|
try {
|
|
@@ -4549,7 +4667,7 @@ async function cmdDoctor(opts = {}) {
|
|
|
4549
4667
|
console.log(formatWorktreeReconcileReport(report.removed));
|
|
4550
4668
|
}
|
|
4551
4669
|
catch (err) {
|
|
4552
|
-
console.log(`reconcile: worktree sweep failed
|
|
4670
|
+
console.log(`reconcile: worktree sweep failed: ${err?.message ?? String(err)}`);
|
|
4553
4671
|
}
|
|
4554
4672
|
// Stale claim reap (#999): factory:in-progress claims naming a dead local pid go back to
|
|
4555
4673
|
// factory:queued so the next run can claim them with no manual label surgery.
|
|
@@ -4564,11 +4682,11 @@ async function cmdDoctor(opts = {}) {
|
|
|
4564
4682
|
console.log(formatClaimReconcileReport(released));
|
|
4565
4683
|
}
|
|
4566
4684
|
else {
|
|
4567
|
-
console.log('reconcile: skipping stale claims
|
|
4685
|
+
console.log('reconcile: skipping stale claims: no GitHub repo or token');
|
|
4568
4686
|
}
|
|
4569
4687
|
}
|
|
4570
4688
|
catch (err) {
|
|
4571
|
-
console.log(`reconcile: stale claim release failed
|
|
4689
|
+
console.log(`reconcile: stale claim release failed: ${err?.message ?? String(err)}`);
|
|
4572
4690
|
}
|
|
4573
4691
|
}
|
|
4574
4692
|
}
|
|
@@ -4618,7 +4736,17 @@ export async function cmdStop(opts = {}) {
|
|
|
4618
4736
|
}
|
|
4619
4737
|
console.log('STOP set — lanes halt between issues');
|
|
4620
4738
|
}
|
|
4621
|
-
export async function cmdResume(opts = {}) {
|
|
4739
|
+
export async function cmdResume(issue, opts = {}) {
|
|
4740
|
+
if (issue === undefined && opts.from !== undefined) {
|
|
4741
|
+
throw new CliExitError('factory: resume --from needs an issue number: factory resume <issue> --from check', 2);
|
|
4742
|
+
}
|
|
4743
|
+
if (issue !== undefined) {
|
|
4744
|
+
if (opts.from !== 'check') {
|
|
4745
|
+
throw new CliExitError('factory: resume <issue> only supports --from check (BUILD resume is #2320)', 2);
|
|
4746
|
+
}
|
|
4747
|
+
await resumeFromCheck(parseIssueArg(issue));
|
|
4748
|
+
return;
|
|
4749
|
+
}
|
|
4622
4750
|
const repoRoot = await getRepoRoot();
|
|
4623
4751
|
const paths = getFactoryPaths(repoRoot);
|
|
4624
4752
|
const json = opts.json ? { repo: await getGitHubRepo() } : undefined;
|
|
@@ -4633,6 +4761,47 @@ export async function cmdResume(opts = {}) {
|
|
|
4633
4761
|
}
|
|
4634
4762
|
console.log('STOP cleared');
|
|
4635
4763
|
}
|
|
4764
|
+
/** `factory resume <issue> --from check` (#2564): re-run CHECK and later phases on the kept worktree.
|
|
4765
|
+
* `--json` only applies to the STOP form and is ignored here. */
|
|
4766
|
+
async function resumeFromCheck(issueNum) {
|
|
4767
|
+
if (availableAgentClis(coreOps.isCommandAvailable).length === 0) {
|
|
4768
|
+
throw new CliExitError(`factory: ${missingAgentCliMessage()}`, 2);
|
|
4769
|
+
}
|
|
4770
|
+
const repoRoot = await getRepoRoot();
|
|
4771
|
+
const paths = getFactoryPaths(repoRoot);
|
|
4772
|
+
if (!existsSync(paths.root)) {
|
|
4773
|
+
throw new CliExitError(`factory: ${notInitializedMessage()}`, 2);
|
|
4774
|
+
}
|
|
4775
|
+
const record = await createCheckResumeStore(resolve(paths.plans, `issue-${issueNum}.md`)).load();
|
|
4776
|
+
if (!record || !existsSync(record.worktree)) {
|
|
4777
|
+
throw new CliExitError(`factory: no preserved build for #${issueNum} — nothing to resume; run factory run-issue ${issueNum}`, 2);
|
|
4778
|
+
}
|
|
4779
|
+
await withRepoRunLock(paths, 'factory resume', async () => {
|
|
4780
|
+
const ghRepo = await getGitHubRepo();
|
|
4781
|
+
const workSources = createDefaultWorkSourceRegistry({ octokit: getOctokit() });
|
|
4782
|
+
let work;
|
|
4783
|
+
try {
|
|
4784
|
+
work = await workSources.resolve(GITHUB_ISSUE_SOURCE, {
|
|
4785
|
+
repo: ghRepo,
|
|
4786
|
+
issue: issueNum,
|
|
4787
|
+
});
|
|
4788
|
+
}
|
|
4789
|
+
catch (err) {
|
|
4790
|
+
if (err instanceof InvalidWorkRequestInputError) {
|
|
4791
|
+
throw new CliExitError(`factory: ${err.message}`, 2);
|
|
4792
|
+
}
|
|
4793
|
+
throw new CliExitError(`factory: could not resolve issue #${issueNum} in ${ghRepo} (${errorDetail(err)}) — nothing was resumed`, 2);
|
|
4794
|
+
}
|
|
4795
|
+
try {
|
|
4796
|
+
await shipIssue(issueNum, {}, { repoRoot, ghRepo, workRequest: work, resumeFrom: record });
|
|
4797
|
+
}
|
|
4798
|
+
catch (err) {
|
|
4799
|
+
if (err instanceof IssueSkippedError)
|
|
4800
|
+
return;
|
|
4801
|
+
throw new CliExitError(`Resume failed for issue #${issueNum}: ${err.message}`, 1);
|
|
4802
|
+
}
|
|
4803
|
+
});
|
|
4804
|
+
}
|
|
4636
4805
|
// ---------- main ----------
|
|
4637
4806
|
export async function main(argv = process.argv, deps = {}) {
|
|
4638
4807
|
if (argv.slice(2).length === 0) {
|
|
@@ -4793,13 +4962,13 @@ export async function main(argv = process.argv, deps = {}) {
|
|
|
4793
4962
|
.action((runId, opts) => cmdFilingPreview(runId, opts));
|
|
4794
4963
|
const triage = program
|
|
4795
4964
|
.command('triage')
|
|
4796
|
-
.description('Have a model propose a queue from open issues into .factory/queue.proposed')
|
|
4965
|
+
.description('Have a model propose a queue from open issues into .factory/state/queue.proposed')
|
|
4797
4966
|
.option('--product <name>', 'Product constitution to scope triage')
|
|
4798
4967
|
.option('--model <id>', 'Model to triage with (any registered model, Claude or Codex)')
|
|
4799
4968
|
.action(cmdTriage);
|
|
4800
4969
|
triage
|
|
4801
4970
|
.command('accept')
|
|
4802
|
-
.description('Validate and atomically promote .factory/queue.proposed to .factory/queue')
|
|
4971
|
+
.description('Validate and atomically promote .factory/state/queue.proposed to .factory/state/queue')
|
|
4803
4972
|
.option('--force', 'Skip validation and promote as-is')
|
|
4804
4973
|
.action(async (opts) => {
|
|
4805
4974
|
await cmdTriageAccept(opts);
|
|
@@ -4919,7 +5088,7 @@ export async function main(argv = process.argv, deps = {}) {
|
|
|
4919
5088
|
.option('--no-sandbox', 'Disable the containment sandbox for agent runs (dangerous)')
|
|
4920
5089
|
.option('--branch-prefix <prefix>', BRANCH_PREFIX_OPTION_DESCRIPTION)
|
|
4921
5090
|
.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)
|
|
5091
|
+
.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
5092
|
.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
5093
|
.action(async (issueNum, opts) => {
|
|
4925
5094
|
await cmdRunIssue(parseIssueArg(issueNum), opts);
|
|
@@ -5069,10 +5238,11 @@ export async function main(argv = process.argv, deps = {}) {
|
|
|
5069
5238
|
.option('--json', 'Print one JSON object (schemaVersion 1): action, repo, stopFlag before/after')
|
|
5070
5239
|
.action((opts) => cmdStop(opts));
|
|
5071
5240
|
program
|
|
5072
|
-
.command('resume')
|
|
5073
|
-
.description('Clear a stop so lanes pick up new issues again')
|
|
5241
|
+
.command('resume [issue]')
|
|
5242
|
+
.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
5243
|
.option('--json', 'Print one JSON object (schemaVersion 1): action, repo, stopFlag before/after')
|
|
5075
|
-
.
|
|
5244
|
+
.option('--from <phase>', 'Resume a kept build at this phase (only: check)')
|
|
5245
|
+
.action((issue, opts) => cmdResume(issue, opts));
|
|
5076
5246
|
applyHelpGroups(program);
|
|
5077
5247
|
const previousLoaders = configLoaders;
|
|
5078
5248
|
const previousGitOps = gitOps;
|