@on-par/factory-cli 0.9.3 → 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 +106 -3
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +494 -57
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/first-run.d.ts +6 -1
- package/dist/cli/first-run.d.ts.map +1 -1
- package/dist/cli/first-run.js +9 -2
- package/dist/cli/first-run.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 +25 -6
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +351 -131
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/stop-sentinel.d.ts.map +1 -1
- package/dist/cli/stop-sentinel.js +5 -4
- 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, 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, } 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,8 +29,8 @@ 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, orphanMicroVmChecks, runDoctorChecks, sandboxAuthSkipDetail, sandboxClaudeAuthChecks, unmergedGreenPrChecks, } from './doctor.js';
|
|
33
|
-
import { DOCS_URL, formatOverview,
|
|
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
|
+
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';
|
|
36
36
|
import { cmdHostedRunner } from './hosted-runner.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,
|
|
@@ -179,7 +185,9 @@ function resolvePhaseOps(overrides, base = phaseOps) {
|
|
|
179
185
|
shipPhase: overrides.shipPhase ?? base.shipPhase,
|
|
180
186
|
};
|
|
181
187
|
}
|
|
182
|
-
export const PREREQUISITES_TEXT = `
|
|
188
|
+
export const PREREQUISITES_TEXT = `First step: run \`factory doctor\` to check your setup and see what to fix.
|
|
189
|
+
|
|
190
|
+
Prerequisites:
|
|
183
191
|
- Claude Code CLI installed and authenticated (Claude subscription): https://claude.com/claude-code
|
|
184
192
|
- GitHub CLI authenticated: gh auth login
|
|
185
193
|
- GITHUB_TOKEN or GH_TOKEN set (falls back to \`gh auth token\`)
|
|
@@ -221,13 +229,39 @@ async function getRepoRoot(shell = shellOps) {
|
|
|
221
229
|
throw new CliExitError('factory: not inside a git repository', 2);
|
|
222
230
|
}
|
|
223
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
|
+
}
|
|
224
257
|
async function getGitHubRepo(shell = shellOps) {
|
|
225
258
|
try {
|
|
226
259
|
const { stdout } = await shell.exec('gh repo view --json nameWithOwner --jq .nameWithOwner');
|
|
227
260
|
return stdout.trim();
|
|
228
261
|
}
|
|
229
|
-
catch {
|
|
230
|
-
|
|
262
|
+
catch (err) {
|
|
263
|
+
const stderr = err?.stderr;
|
|
264
|
+
throw new CliExitError(ghRepoViewFailureMessage(typeof stderr === 'string' ? stderr : undefined), 2);
|
|
231
265
|
}
|
|
232
266
|
}
|
|
233
267
|
/** Env token first, then one `gh auth token` subprocess (≤5 s). `undefined` when neither yields one. */
|
|
@@ -245,7 +279,9 @@ function resolveGitHubToken(shell = shellOps) {
|
|
|
245
279
|
/** Octokit factory for the current main() or cmdLand invocation (CliDeps.octokit); unset outside them. */
|
|
246
280
|
let octokitFactory;
|
|
247
281
|
function getOctokit(shell = shellOps, options) {
|
|
248
|
-
return octokitFactory
|
|
282
|
+
return octokitFactory
|
|
283
|
+
? octokitFactory()
|
|
284
|
+
: selectOctokit(process.env, () => createFactoryOctokit(resolveGitHubToken(shell), options));
|
|
249
285
|
}
|
|
250
286
|
export function errorDetail(err) {
|
|
251
287
|
const e = err;
|
|
@@ -339,13 +375,13 @@ async function cmdInit(opts = {}) {
|
|
|
339
375
|
// Doctor-style validation (policy=auto, N models reachable) — informational, never fails init.
|
|
340
376
|
const modelsConfig = applyRepoConfig(configLoaders.loadModelsConfig(), configLoaders.loadRepoConfig(repoRoot));
|
|
341
377
|
const registry = new coreOps.ModelRegistry(modelsConfig);
|
|
342
|
-
const diagnoses = coreOps.diagnoseModels(registry, modelDoctorProbes(), resolveExperimental(), resolveLocalOnly());
|
|
378
|
+
const diagnoses = coreOps.diagnoseModels(registry, modelDoctorProbes({ requireOllamaOnPath: true }), resolveExperimental(), resolveLocalOnly());
|
|
343
379
|
console.log(formatInitReachability(diagnoses));
|
|
344
380
|
if (!hasReachableWorker(diagnoses)) {
|
|
345
381
|
console.log(styleText('yellow', 'No worker model reachable yet — see `factory doctor` and `factory models --doctor`.'));
|
|
346
382
|
}
|
|
347
383
|
console.log(styleText('green', `Initialized ${paths.root}`));
|
|
348
|
-
console.log(
|
|
384
|
+
console.log('Next: factory doctor, then factory triage (optional: factory constitution --product <name> to swap the default constitution)');
|
|
349
385
|
}
|
|
350
386
|
/** Rewrite a checkout's legacy `.factory/` inputs and runtime files to the v2
|
|
351
387
|
* layout. Every write is idempotent so it is safe to run this more than once. */
|
|
@@ -723,12 +759,16 @@ export async function cmdConstitution(opts, deps = {}) {
|
|
|
723
759
|
export function modelStatusIcon(d) {
|
|
724
760
|
return d.reachable ? styleText('green', '✅') : d.unverified ? styleText('yellow', '⚠️') : styleText('red', '❌');
|
|
725
761
|
}
|
|
726
|
-
|
|
762
|
+
/** Single owner of the `[repo-defined]` marker for models declared in `.factory/config.json` models.registry, shared by `factory models` and `models --doctor` (#2443). */
|
|
763
|
+
export function repoDefinedTag(model, repoDefined) {
|
|
764
|
+
return repoDefined.has(model) ? styleText('cyan', ' [repo-defined]') : '';
|
|
765
|
+
}
|
|
766
|
+
export function formatDoctorReport(diagnoses, repoDefined = new Set()) {
|
|
727
767
|
const lines = [styleText('bold', '\n== Model Doctor ==')];
|
|
728
768
|
for (const d of diagnoses) {
|
|
729
769
|
const icon = modelStatusIcon(d);
|
|
730
770
|
const tiers = d.tiers.join('/');
|
|
731
|
-
lines.push(` ${icon} ${d.model} provider=${d.provider} tier=${tiers} — ${d.reason}`);
|
|
771
|
+
lines.push(` ${icon} ${d.model} provider=${d.provider} tier=${tiers} — ${d.reason}${repoDefinedTag(d.model, repoDefined)}`);
|
|
732
772
|
}
|
|
733
773
|
lines.push(` ${formatReachabilityCount(diagnoses)}`);
|
|
734
774
|
return lines.join('\n');
|
|
@@ -736,7 +776,9 @@ export function formatDoctorReport(diagnoses) {
|
|
|
736
776
|
export function hasReachableWorker(diagnoses) {
|
|
737
777
|
return diagnoses.some((d) => d.reachable && (d.tiers.includes('worker') || d.tiers.includes('worker_fallback')));
|
|
738
778
|
}
|
|
739
|
-
function ollamaModelSet() {
|
|
779
|
+
function ollamaModelSet(requireOnPath = false) {
|
|
780
|
+
if (requireOnPath && !coreOps.isCommandAvailable('ollama'))
|
|
781
|
+
return undefined;
|
|
740
782
|
try {
|
|
741
783
|
const out = shellOps.execSync('ollama list', { timeout: 10_000 });
|
|
742
784
|
return new Set(out
|
|
@@ -750,9 +792,10 @@ function ollamaModelSet() {
|
|
|
750
792
|
}
|
|
751
793
|
}
|
|
752
794
|
/** Probes shared by every doctor-style reachability check (init, models --doctor, overnight
|
|
753
|
-
* preflight), so their counts agree.
|
|
754
|
-
|
|
755
|
-
|
|
795
|
+
* preflight), so their counts agree. init passes `requireOllamaOnPath` so a missing binary never
|
|
796
|
+
* reaches the shell (#2507). */
|
|
797
|
+
export function modelDoctorProbes(opts = {}) {
|
|
798
|
+
const ollamaModels = ollamaModelSet(opts.requireOllamaOnPath === true);
|
|
756
799
|
return {
|
|
757
800
|
commandAvailable: coreOps.isCommandAvailable,
|
|
758
801
|
ollamaModelPresent: ollamaModels ? (model) => ollamaModels.has(model) : undefined,
|
|
@@ -760,13 +803,15 @@ export function modelDoctorProbes() {
|
|
|
760
803
|
}
|
|
761
804
|
async function cmdModels(opts = {}) {
|
|
762
805
|
const repoRoot = await getRepoRoot();
|
|
763
|
-
const
|
|
806
|
+
const repoConfig = configLoaders.loadRepoConfig(repoRoot);
|
|
807
|
+
const modelsConfig = applyRepoConfig(configLoaders.loadModelsConfig(), repoConfig);
|
|
808
|
+
const repoDefined = new Set(Object.keys(repoConfig?.models?.registry ?? {}));
|
|
764
809
|
const registry = new coreOps.ModelRegistry(modelsConfig);
|
|
765
810
|
const allowExperimental = resolveExperimental();
|
|
766
811
|
const localOnly = resolveLocalOnly();
|
|
767
812
|
const diagnoses = coreOps.diagnoseModels(registry, modelDoctorProbes(), allowExperimental, localOnly);
|
|
768
813
|
if (opts.doctor) {
|
|
769
|
-
console.log(formatDoctorReport(diagnoses));
|
|
814
|
+
console.log(formatDoctorReport(diagnoses, repoDefined));
|
|
770
815
|
if (!hasReachableWorker(diagnoses)) {
|
|
771
816
|
throw new CliExitError('factory: no worker model is reachable — fix the reasons above before running a queue', 1);
|
|
772
817
|
}
|
|
@@ -781,7 +826,7 @@ async function cmdModels(opts = {}) {
|
|
|
781
826
|
const d = byModel.get(m);
|
|
782
827
|
const avail = d ? modelStatusIcon(d) : styleText('red', '❌');
|
|
783
828
|
const tag = registry.isExperimental(m) ? styleText('yellow', ' [experimental]') : '';
|
|
784
|
-
console.log(` ${avail} ${m} tier=${tiers} ${cost}${tag}`);
|
|
829
|
+
console.log(` ${avail} ${m} tier=${tiers} ${cost}${tag}${repoDefinedTag(m, repoDefined)}`);
|
|
785
830
|
}
|
|
786
831
|
console.log(styleText('bold', '\n== Tiers =='));
|
|
787
832
|
for (const tier of ['boss', 'worker', 'checker', 'triage']) {
|
|
@@ -1437,8 +1482,12 @@ export class LaneEnvironmentPauseError extends Error {
|
|
|
1437
1482
|
baseSha;
|
|
1438
1483
|
failingChecks;
|
|
1439
1484
|
failureSignature;
|
|
1485
|
+
/** BUILD output was kept for `factory resume --from check` (#2564); runLane must not reap it. */
|
|
1486
|
+
buildPreserved;
|
|
1440
1487
|
constructor(message, failure) {
|
|
1441
1488
|
super(message);
|
|
1489
|
+
if (failure.buildPreserved !== undefined)
|
|
1490
|
+
this.buildPreserved = failure.buildPreserved;
|
|
1442
1491
|
this.baseSha = failure.baseSha;
|
|
1443
1492
|
this.failingChecks = failure.failingChecks;
|
|
1444
1493
|
if (failure.failureSignature !== undefined)
|
|
@@ -1532,6 +1581,15 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1532
1581
|
const shell = resolveShellOps(deps);
|
|
1533
1582
|
const core = resolveCoreOps(deps);
|
|
1534
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
|
+
}
|
|
1535
1593
|
const repoRoot = ctx?.repoRoot ?? (await getRepoRoot(shell));
|
|
1536
1594
|
const ghRepo = ctx?.ghRepo ?? (await getGitHubRepo(shell));
|
|
1537
1595
|
const paths = ctx?.paths ?? getFactoryPaths(repoRoot);
|
|
@@ -1560,6 +1618,7 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1560
1618
|
: { ...resolveEffectiveConfig(repoConfig), branchPrefix: opts.branchPrefix },
|
|
1561
1619
|
};
|
|
1562
1620
|
const router = new core.ModelRouter(policy.models, policy.routes, false, undefined, policy.effective.allowExperimental, policy.effective.localOnly);
|
|
1621
|
+
router.skipMissingHarnessClis(core.isCommandAvailable);
|
|
1563
1622
|
let issueSpend = 0;
|
|
1564
1623
|
let laneSandboxRuntime = 'none';
|
|
1565
1624
|
const laneWorkspaceBackend = factoryConfig.workspace?.backend === 'disposable-docker' ? 'disposable-docker' : 'worktree';
|
|
@@ -1588,7 +1647,11 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1588
1647
|
const maxSlices = opts.maxSlices ?? resolveSizeGateMaxSlices(factoryConfig);
|
|
1589
1648
|
// ADR-0147: slice k >= 2 builds on its own branch, so read the slice plan before the worktree exists.
|
|
1590
1649
|
let sliceIndex;
|
|
1591
|
-
|
|
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) {
|
|
1592
1655
|
try {
|
|
1593
1656
|
const lookup = await findSlicePlanComment({ octokit, repo: ghRepo, issue: issueNum });
|
|
1594
1657
|
if (lookup.status === 'found')
|
|
@@ -1598,10 +1661,12 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1598
1661
|
throw new LaneParkError(`slice plan lookup failed for #${issueNum}: ${errorDetail(err)} — parked before creating a worktree`, 'escalate');
|
|
1599
1662
|
}
|
|
1600
1663
|
}
|
|
1601
|
-
const branch = branchFor(issueNum, issueTitle, policy.effective.branchPrefix, sliceIndex);
|
|
1602
|
-
const worktree = ctx?.
|
|
1603
|
-
? ctx.
|
|
1604
|
-
:
|
|
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);
|
|
1605
1670
|
const specPath = resolve(paths.plans, `issue-${issueNum}.md`);
|
|
1606
1671
|
const runStartedAt = new Date().toISOString();
|
|
1607
1672
|
const lane = ctx?.lane;
|
|
@@ -1616,6 +1681,7 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1616
1681
|
'escalate',
|
|
1617
1682
|
'held',
|
|
1618
1683
|
'conflict',
|
|
1684
|
+
'config',
|
|
1619
1685
|
'ci-failed',
|
|
1620
1686
|
'ci-infra',
|
|
1621
1687
|
'timeout',
|
|
@@ -1698,7 +1764,11 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1698
1764
|
// stays a CLI concern (not called here, matching today — a worktree persists past
|
|
1699
1765
|
// a successful run for GC/sweep commands to reclaim later).
|
|
1700
1766
|
let workspace;
|
|
1701
|
-
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) {
|
|
1702
1772
|
workspace = await worktreeWorkspace({
|
|
1703
1773
|
repoRoot,
|
|
1704
1774
|
branch,
|
|
@@ -1779,9 +1849,21 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1779
1849
|
sandboxPolicy: activeSandboxPolicy,
|
|
1780
1850
|
processGroupGraceMs,
|
|
1781
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
|
+
: {}),
|
|
1782
1863
|
eventsFile: paths.events,
|
|
1783
1864
|
logsDir: paths.logs,
|
|
1784
1865
|
baselineCachePath: paths.baselineCache,
|
|
1866
|
+
repoConfigFile: repoConfigFileFor(repoRoot, paths.config),
|
|
1785
1867
|
flakyLedgerPath: paths.flakyLedger,
|
|
1786
1868
|
};
|
|
1787
1869
|
const ports = {
|
|
@@ -1794,10 +1876,11 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1794
1876
|
getIssueSpend: () => issueSpend,
|
|
1795
1877
|
breaker,
|
|
1796
1878
|
planReuse: ctx?.localOnly ? undefined : createPlanReuseStore(specPath),
|
|
1879
|
+
checkResume: ctx?.localOnly ? undefined : createCheckResumeStore(specPath),
|
|
1797
1880
|
planPhase: phases.planPhase,
|
|
1798
1881
|
buildPhase: phases.buildPhase,
|
|
1799
1882
|
checkPhase: phases.checkPhase,
|
|
1800
|
-
shipPhase:
|
|
1883
|
+
shipPhase: shipPhaseForMode,
|
|
1801
1884
|
checkBaseHealth: deps.checkBaseHealth ?? checkBaseHealth,
|
|
1802
1885
|
resolveConstitution: () =>
|
|
1803
1886
|
// Resolved once here — runIssue calls this exactly once and reuses the value for
|
|
@@ -1872,7 +1955,8 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1872
1955
|
const egressProxy = proxied
|
|
1873
1956
|
? await startEgressProxy({
|
|
1874
1957
|
allowHosts: proxied.allowHosts,
|
|
1875
|
-
|
|
1958
|
+
blockTelemetry: proxied.blockTelemetry,
|
|
1959
|
+
onHost: (host, allowed) => log('egress_host', egressHostMessage(host, allowed)),
|
|
1876
1960
|
})
|
|
1877
1961
|
: undefined;
|
|
1878
1962
|
let outcome;
|
|
@@ -1906,6 +1990,7 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1906
1990
|
baseSha: outcome.baseSha,
|
|
1907
1991
|
failingChecks: outcome.failingChecks,
|
|
1908
1992
|
failureSignature: outcome.failureSignature,
|
|
1993
|
+
buildPreserved: outcome.buildPreserved,
|
|
1909
1994
|
});
|
|
1910
1995
|
}
|
|
1911
1996
|
const reason = outcome.state === 'escalated' ? 'escalate' : outcome.reason;
|
|
@@ -2014,8 +2099,8 @@ export async function sweepBetweenLaneIssues(issue, lane, repoRoot, ghRepo, path
|
|
|
2014
2099
|
}
|
|
2015
2100
|
}
|
|
2016
2101
|
async function cmdShip(issueNum, opts) {
|
|
2017
|
-
if (
|
|
2018
|
-
throw new CliExitError(`factory: ${
|
|
2102
|
+
if (availableAgentClis(coreOps.isCommandAvailable).length === 0) {
|
|
2103
|
+
throw new CliExitError(`factory: ${missingAgentCliMessage()}`, 2);
|
|
2019
2104
|
}
|
|
2020
2105
|
const repoRoot = await getRepoRoot();
|
|
2021
2106
|
const paths = getFactoryPaths(repoRoot);
|
|
@@ -2050,13 +2135,16 @@ async function cmdShip(issueNum, opts) {
|
|
|
2050
2135
|
catch (err) {
|
|
2051
2136
|
if (err instanceof IssueSkippedError)
|
|
2052
2137
|
return;
|
|
2138
|
+
// #2492: a pull request number is a usage error, not a failed ship.
|
|
2139
|
+
if (err instanceof InvalidWorkRequestInputError)
|
|
2140
|
+
throw new CliExitError(`factory: ${err.message}`, 2);
|
|
2053
2141
|
throw new CliExitError(`Ship failed for issue #${issueNum}: ${err.message}`, 1);
|
|
2054
2142
|
}
|
|
2055
2143
|
});
|
|
2056
2144
|
}
|
|
2057
2145
|
async function cmdRunIssue(issueNum, opts) {
|
|
2058
|
-
if (
|
|
2059
|
-
throw new CliExitError(`factory: ${
|
|
2146
|
+
if (availableAgentClis(coreOps.isCommandAvailable).length === 0) {
|
|
2147
|
+
throw new CliExitError(`factory: ${missingAgentCliMessage()}`, 2);
|
|
2060
2148
|
}
|
|
2061
2149
|
const repoRoot = await getRepoRoot();
|
|
2062
2150
|
const paths = getFactoryPaths(repoRoot);
|
|
@@ -2173,8 +2261,8 @@ async function runDecomposedChildren(parent, children, opts, env) {
|
|
|
2173
2261
|
}
|
|
2174
2262
|
}
|
|
2175
2263
|
async function cmdRunBrief(briefPath, opts) {
|
|
2176
|
-
if (
|
|
2177
|
-
throw new CliExitError(`factory: ${
|
|
2264
|
+
if (availableAgentClis(coreOps.isCommandAvailable).length === 0) {
|
|
2265
|
+
throw new CliExitError(`factory: ${missingAgentCliMessage()}`, 2);
|
|
2178
2266
|
}
|
|
2179
2267
|
const repoRoot = await getRepoRoot();
|
|
2180
2268
|
const paths = getFactoryPaths(repoRoot);
|
|
@@ -2290,8 +2378,8 @@ async function cmdLocalSmallOvernight(opts) {
|
|
|
2290
2378
|
const ghRepo = await getGitHubRepo();
|
|
2291
2379
|
process.env.FACTORY_LOCAL_ONLY = '1';
|
|
2292
2380
|
const preflight = async () => {
|
|
2293
|
-
if (
|
|
2294
|
-
return { ok: false, reason:
|
|
2381
|
+
if (availableAgentClis(coreOps.isCommandAvailable).length === 0)
|
|
2382
|
+
return { ok: false, reason: missingAgentCliMessage() };
|
|
2295
2383
|
const registry = new coreOps.ModelRegistry(applyRepoConfig(configLoaders.loadModelsConfig(), configLoaders.loadRepoConfig(repoRoot)));
|
|
2296
2384
|
const diagnoses = coreOps.diagnoseModels(registry, modelDoctorProbes(), resolveExperimental(), true);
|
|
2297
2385
|
if (!hasReachableWorker(diagnoses)) {
|
|
@@ -2635,28 +2723,33 @@ async function cmdTriage(opts) {
|
|
|
2635
2723
|
const routesConfig = configLoaders.loadRoutesConfig();
|
|
2636
2724
|
const effective = resolveEffectiveConfig(repoConfig);
|
|
2637
2725
|
const router = new coreOps.ModelRouter(modelsConfig, routesConfig, false, undefined, effective.allowExperimental, effective.localOnly);
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
const resolved = router.resolveAll('triage');
|
|
2641
|
-
const candidates = resolved.length > 0 ? resolved : ['claude-sonnet-5'];
|
|
2642
|
-
const model = candidates.find((m) => registry.getHarnessId(m) === 'claude-cli' && registry.getClaudeFlag(m));
|
|
2643
|
-
const flag = model ? registry.getClaudeFlag(model) : undefined;
|
|
2644
|
-
const constitutionNote = product ? `Active constitution: ${product}.` : '';
|
|
2645
|
-
const prompt = `Triage the open GitHub issues of ${ghRepo} for an autonomous software factory.
|
|
2646
|
-
${constitutionNote}
|
|
2647
|
-
Run: gh issue list --repo ${ghRepo} --state open --limit 100 --json number,title,labels,body
|
|
2648
|
-
Read every body. Exclude epics/PRDs/meta, external-account/credential/outreach issues,
|
|
2649
|
-
and anything too vague. Group by lane (same-file issues together). Order by dependency then value.
|
|
2650
|
-
Write ONLY the queue to ${paths.queueProposed} in format '<lane> <issue#>', with '#' comments
|
|
2651
|
-
explaining exclusions.`;
|
|
2726
|
+
router.skipMissingHarnessClis(coreOps.isCommandAvailable);
|
|
2727
|
+
router.setCostSink((entry) => logCost(paths.costs, { ...entry, issue: '-' }));
|
|
2652
2728
|
let plannerError;
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2729
|
+
let model;
|
|
2730
|
+
try {
|
|
2731
|
+
const { stdout: listed } = await shellOps.exec(`gh issue list --repo ${shellEscape(ghRepo)} --state open --limit 100 --json number,title,labels,body`);
|
|
2732
|
+
const prompt = buildTriagePrompt({ repo: ghRepo, product, candidates: parseTriageCandidates(listed || '[]') });
|
|
2733
|
+
logEvent(paths.events, 'triage', '-', `Triaging ${ghRepo} via the triage route`);
|
|
2734
|
+
const result = await router.run('triage', prompt, {
|
|
2735
|
+
worktree: repoRoot,
|
|
2736
|
+
timeoutSeconds: 600,
|
|
2737
|
+
...(opts.model ? { modelOverride: opts.model } : {}),
|
|
2738
|
+
onLog: (msg) => logEvent(paths.events, 'triage', '-', msg),
|
|
2739
|
+
});
|
|
2740
|
+
model = result.model;
|
|
2741
|
+
const queue = normalizeTriageQueue(result.output);
|
|
2742
|
+
if (queue) {
|
|
2743
|
+
const header = triageQueueHeader(model, product);
|
|
2744
|
+
writeFileSync(paths.queueProposed, header + queue);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
catch (err) {
|
|
2657
2748
|
plannerError = err;
|
|
2658
2749
|
logEvent(paths.events, 'warn', '-', `triage planner failed: ${errorDetail(err)}`);
|
|
2659
|
-
}
|
|
2750
|
+
}
|
|
2751
|
+
if (model)
|
|
2752
|
+
logEvent(paths.events, 'triage', '-', `Triaged ${ghRepo} with ${model}`);
|
|
2660
2753
|
const proposed = existsSync(paths.queueProposed) ? readFileSync(paths.queueProposed, 'utf-8') : '';
|
|
2661
2754
|
const message = triageProposalMessage(proposed, paths.queueProposed, paths.queue);
|
|
2662
2755
|
if (message) {
|
|
@@ -2666,6 +2759,9 @@ explaining exclusions.`;
|
|
|
2666
2759
|
throw triageNoProposalError(plannerError);
|
|
2667
2760
|
}
|
|
2668
2761
|
}
|
|
2762
|
+
export function triageQueueHeader(model, product) {
|
|
2763
|
+
return `# Triage queue (model ${model}${product ? `, constitution ${product}` : ''})\n`;
|
|
2764
|
+
}
|
|
2669
2765
|
export function triageProposalMessage(proposed, proposedPath, queuePath) {
|
|
2670
2766
|
if (!proposed.trim())
|
|
2671
2767
|
return null;
|
|
@@ -2675,13 +2771,13 @@ export async function cmdTriageAccept(opts) {
|
|
|
2675
2771
|
const repoRoot = await getRepoRoot();
|
|
2676
2772
|
const paths = getFactoryPaths(repoRoot);
|
|
2677
2773
|
if (!existsSync(paths.queueProposed)) {
|
|
2678
|
-
console.log(`nothing to accept
|
|
2774
|
+
console.log(`nothing to accept: ${paths.queueProposed} not found`);
|
|
2679
2775
|
return; // zero exit
|
|
2680
2776
|
}
|
|
2681
2777
|
const content = readFileSync(paths.queueProposed, 'utf-8');
|
|
2682
2778
|
const result = validateQueue(content);
|
|
2683
2779
|
if (!opts.force && !result.ok) {
|
|
2684
|
-
throw new CliExitError(`factory: proposed queue is invalid
|
|
2780
|
+
throw new CliExitError(`factory: proposed queue is invalid: ${paths.queueProposed} left unchanged\n` +
|
|
2685
2781
|
result.errors.map((e) => ` - ${e}`).join('\n'), 1);
|
|
2686
2782
|
}
|
|
2687
2783
|
renameSync(paths.queueProposed, paths.queue); // same dir → atomic
|
|
@@ -2694,7 +2790,7 @@ export async function cmdTriageAccept(opts) {
|
|
|
2694
2790
|
}
|
|
2695
2791
|
const suffix = opts.force ? ' (--force, validation skipped)' : '';
|
|
2696
2792
|
logEvent(paths.events, 'triage_accepted', '-', `accepted ${result.issues.length} issue(s) [${result.issues.join(', ')}] by ${acceptedBy}${suffix}`);
|
|
2697
|
-
console.log(styleText('green', `queue accepted
|
|
2793
|
+
console.log(styleText('green', `queue accepted: ${result.issues.length} issue(s) promoted to ${paths.queue}`));
|
|
2698
2794
|
}
|
|
2699
2795
|
export async function cmdQueueMigrate(opts = {}) {
|
|
2700
2796
|
const repoRoot = await getRepoRoot();
|
|
@@ -3211,6 +3307,27 @@ async function cmdRun(opts = {}) {
|
|
|
3211
3307
|
console.log(`[factory] run: ${detail}`);
|
|
3212
3308
|
logEvent(paths.events, 'watchdog', 'all', detail);
|
|
3213
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
|
+
}
|
|
3214
3331
|
const controller = new AbortController();
|
|
3215
3332
|
const watchdog = knobs.watch
|
|
3216
3333
|
? coreOps
|
|
@@ -3222,6 +3339,7 @@ async function cmdRun(opts = {}) {
|
|
|
3222
3339
|
eventsFile: paths.events,
|
|
3223
3340
|
signal: controller.signal,
|
|
3224
3341
|
estimator: knobs.estimator,
|
|
3342
|
+
...(watchdogHarness ? { harness: watchdogHarness } : {}),
|
|
3225
3343
|
})
|
|
3226
3344
|
.catch((err) => {
|
|
3227
3345
|
// a watchdog crash must never take down the run
|
|
@@ -3692,7 +3810,9 @@ export async function runLane(lane, issues, repoRoot, ghRepo, paths, deps = {})
|
|
|
3692
3810
|
// The base is broken, not the issue (#1928): release to queued (never parked),
|
|
3693
3811
|
// pause the lane whatever laneBreakerThreshold is, and stop claiming.
|
|
3694
3812
|
streak = null;
|
|
3695
|
-
|
|
3813
|
+
// A preserved build is kept for `factory resume --from check` (#2564).
|
|
3814
|
+
if (!err.buildPreserved)
|
|
3815
|
+
await reapWorktree(issue).catch(() => { });
|
|
3696
3816
|
await settle(issue, 'queued');
|
|
3697
3817
|
// Persist the hold so a restarted supervisor remembers the broken base (#2348);
|
|
3698
3818
|
// best-effort, never allowed to change lane flow.
|
|
@@ -4363,20 +4483,6 @@ function probeClaudeKeychain() {
|
|
|
4363
4483
|
}
|
|
4364
4484
|
}
|
|
4365
4485
|
async function cmdDoctor(opts = {}) {
|
|
4366
|
-
const checks = runDoctorChecks({
|
|
4367
|
-
commandAvailable: coreOps.isCommandAvailable,
|
|
4368
|
-
envPresent: (key) => !!process.env[key],
|
|
4369
|
-
tryExec: (cmd) => {
|
|
4370
|
-
try {
|
|
4371
|
-
return shellOps.execSync(cmd, { timeout: 10_000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
4372
|
-
}
|
|
4373
|
-
catch {
|
|
4374
|
-
return null;
|
|
4375
|
-
}
|
|
4376
|
-
},
|
|
4377
|
-
pathExists: existsSync,
|
|
4378
|
-
distFreshness: distFreshnessProbe(import.meta.url),
|
|
4379
|
-
});
|
|
4380
4486
|
let repoRoot;
|
|
4381
4487
|
try {
|
|
4382
4488
|
repoRoot = shellOps
|
|
@@ -4389,56 +4495,97 @@ async function cmdDoctor(opts = {}) {
|
|
|
4389
4495
|
catch {
|
|
4390
4496
|
repoRoot = null;
|
|
4391
4497
|
}
|
|
4392
|
-
const
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4498
|
+
const checks = runDoctorChecks({
|
|
4499
|
+
commandAvailable: coreOps.isCommandAvailable,
|
|
4500
|
+
envPresent: (key) => !!process.env[key],
|
|
4501
|
+
tryExec: (cmd, opts) => {
|
|
4502
|
+
try {
|
|
4503
|
+
return shellOps
|
|
4504
|
+
.execSync(cmd, {
|
|
4505
|
+
timeout: opts?.timeoutMs ?? 10_000,
|
|
4506
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
4507
|
+
...(opts?.env ? { env: { ...process.env, ...opts.env } } : {}),
|
|
4508
|
+
})
|
|
4509
|
+
.trim();
|
|
4510
|
+
}
|
|
4511
|
+
catch {
|
|
4512
|
+
return null;
|
|
4513
|
+
}
|
|
4514
|
+
},
|
|
4515
|
+
pathExists: existsSync,
|
|
4516
|
+
distFreshness: distFreshnessProbe(import.meta.url),
|
|
4517
|
+
codexSessionState: () => codexChatgptSessionState(),
|
|
4518
|
+
claudeLoggedIn: () => hasClaudeLoginEvidence({
|
|
4519
|
+
exec: (file, args) => {
|
|
4520
|
+
shellOps.execSync([file, ...args].map(shellEscape).join(' '), { timeout: 10_000, stdio: 'ignore' });
|
|
4521
|
+
},
|
|
4522
|
+
fileExists: existsSync,
|
|
4523
|
+
}),
|
|
4524
|
+
opencodeLoggedIn: () => hasOpencodeLoginEvidence(),
|
|
4525
|
+
commandHarnesses: () => repoRoot === null
|
|
4526
|
+
? []
|
|
4527
|
+
: repoCommandHarnesses(configLoaders.loadModelsConfig().harnesses, configLoaders.loadRepoConfig(repoRoot)?.models?.registry),
|
|
4528
|
+
});
|
|
4529
|
+
// Only with --probe-inference: these send a real prompt (uses quota). Plain doctor makes no inference call.
|
|
4530
|
+
if (opts.probeInference) {
|
|
4531
|
+
const CLAUDE_AUTH_PROBE = 'claude -p "reply with exactly: ok"';
|
|
4532
|
+
const probeExec = (cmd) => {
|
|
4533
|
+
try {
|
|
4534
|
+
return shellOps.execSync(cmd, { timeout: 120_000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
4535
|
+
}
|
|
4536
|
+
catch {
|
|
4537
|
+
return null;
|
|
4538
|
+
}
|
|
4539
|
+
};
|
|
4540
|
+
let host = 'skipped';
|
|
4541
|
+
let hostDetail = 'skipped: claude CLI not on PATH';
|
|
4542
|
+
if (coreOps.isCommandAvailable('claude')) {
|
|
4543
|
+
host = probeExec(CLAUDE_AUTH_PROBE) === null ? 'failed' : 'ok';
|
|
4544
|
+
hostDetail = host === 'ok' ? 'claude -p succeeded on the host' : 'claude -p failed on the host';
|
|
4545
|
+
}
|
|
4546
|
+
let sandboxed = 'skipped';
|
|
4547
|
+
let sandboxDetail = 'skipped: host claude auth was not verified';
|
|
4548
|
+
const runtimeAvailable = hasSandboxRuntime(process.platform, (cmd) => isCommandAvailableViaShell(shellOps, cmd));
|
|
4549
|
+
const skip = sandboxAuthSkipDetail({ runtimeAvailable, hostOk: host === 'ok', inRepo: repoRoot !== null });
|
|
4550
|
+
if (skip !== null) {
|
|
4551
|
+
sandboxDetail = skip;
|
|
4552
|
+
}
|
|
4553
|
+
else if (repoRoot !== null) {
|
|
4554
|
+
const policy = resolveSandboxPolicy(configLoaders.loadFactoryConfigForRepo(getFactoryPaths(repoRoot).config).sandbox, {
|
|
4555
|
+
worktree: repoRoot,
|
|
4556
|
+
repoRoot,
|
|
4557
|
+
isAvailable: (cmd) => isCommandAvailableViaShell(shellOps, cmd),
|
|
4558
|
+
});
|
|
4559
|
+
if (!policy) {
|
|
4560
|
+
sandboxDetail = 'skipped: sandbox disabled by config or FACTORY_SANDBOX';
|
|
4561
|
+
}
|
|
4562
|
+
else if (policy.runtime === 'none') {
|
|
4563
|
+
sandboxDetail = 'skipped: no sandbox runtime (sandbox-exec/firejail) on this host';
|
|
4564
|
+
}
|
|
4565
|
+
else if (policy.runtime === 'docker-sandbox') {
|
|
4566
|
+
sandboxDetail = 'skipped: docker-sandbox containment is the microVM, not command wrapping (#653)';
|
|
4567
|
+
}
|
|
4568
|
+
else {
|
|
4569
|
+
sandboxed = probeExec(wrapCommandInSandbox(CLAUDE_AUTH_PROBE, policy)) === null ? 'failed' : 'ok';
|
|
4570
|
+
sandboxDetail =
|
|
4571
|
+
sandboxed === 'ok'
|
|
4572
|
+
? `claude -p succeeded under ${policy.runtime}`
|
|
4573
|
+
: `claude -p failed under ${policy.runtime} (local_auth)`;
|
|
4574
|
+
}
|
|
4435
4575
|
}
|
|
4576
|
+
checks.push(...sandboxClaudeAuthChecks({ host, sandboxed, hostDetail, sandboxDetail }));
|
|
4436
4577
|
}
|
|
4437
4578
|
checks.push(claudeKeychainCheck(probeClaudeKeychain()));
|
|
4438
|
-
checks.
|
|
4579
|
+
checks.splice(0, checks.length, ...reconcileAgentAuthChecks(checks));
|
|
4439
4580
|
if (repoRoot !== null) {
|
|
4440
4581
|
const paths = getFactoryPaths(repoRoot);
|
|
4441
|
-
|
|
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
|
+
}
|
|
4442
4589
|
const health = await inspectPortLeases({ registryFile: paths.ports });
|
|
4443
4590
|
checks.push(...leaseChecks(health.map(toLeaseRow)));
|
|
4444
4591
|
const dockerAvailable = coreOps.isCommandAvailable('docker');
|
|
@@ -4455,6 +4602,25 @@ async function cmdDoctor(opts = {}) {
|
|
|
4455
4602
|
checks.push(stopSentinelCheck(readStopFileStatus(paths)));
|
|
4456
4603
|
const eventsContent = existsSync(paths.events) ? readFileSync(paths.events, 'utf-8') : null;
|
|
4457
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));
|
|
4614
|
+
try {
|
|
4615
|
+
const declared = Object.keys(configLoaders.loadModelsConfig().harnesses ?? {});
|
|
4616
|
+
checks.push(...modelsRegistryChecks(paths.config, configLoaders.loadRepoConfig(repoRoot)?.models?.registry, [
|
|
4617
|
+
...KNOWN_HARNESS_IDS,
|
|
4618
|
+
...declared,
|
|
4619
|
+
]));
|
|
4620
|
+
}
|
|
4621
|
+
catch {
|
|
4622
|
+
// malformed config is reported by the load paths / `factory models --doctor`; doctor keeps going
|
|
4623
|
+
}
|
|
4458
4624
|
let harnessProbes = [];
|
|
4459
4625
|
try {
|
|
4460
4626
|
const registry = new coreOps.ModelRegistry(applyRepoConfig(configLoaders.loadModelsConfig(), configLoaders.loadRepoConfig(repoRoot)));
|
|
@@ -4486,7 +4652,7 @@ async function cmdDoctor(opts = {}) {
|
|
|
4486
4652
|
const graceMs = resolveProcessGroupGraceMs(factoryConfig);
|
|
4487
4653
|
const orphanEvents = await coreOps.reapOrphanProcesses({ reaped, graceMs });
|
|
4488
4654
|
for (const e of orphanEvents) {
|
|
4489
|
-
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' : ''}`);
|
|
4490
4656
|
}
|
|
4491
4657
|
}
|
|
4492
4658
|
try {
|
|
@@ -4501,7 +4667,7 @@ async function cmdDoctor(opts = {}) {
|
|
|
4501
4667
|
console.log(formatWorktreeReconcileReport(report.removed));
|
|
4502
4668
|
}
|
|
4503
4669
|
catch (err) {
|
|
4504
|
-
console.log(`reconcile: worktree sweep failed
|
|
4670
|
+
console.log(`reconcile: worktree sweep failed: ${err?.message ?? String(err)}`);
|
|
4505
4671
|
}
|
|
4506
4672
|
// Stale claim reap (#999): factory:in-progress claims naming a dead local pid go back to
|
|
4507
4673
|
// factory:queued so the next run can claim them with no manual label surgery.
|
|
@@ -4516,15 +4682,15 @@ async function cmdDoctor(opts = {}) {
|
|
|
4516
4682
|
console.log(formatClaimReconcileReport(released));
|
|
4517
4683
|
}
|
|
4518
4684
|
else {
|
|
4519
|
-
console.log('reconcile: skipping stale claims
|
|
4685
|
+
console.log('reconcile: skipping stale claims: no GitHub repo or token');
|
|
4520
4686
|
}
|
|
4521
4687
|
}
|
|
4522
4688
|
catch (err) {
|
|
4523
|
-
console.log(`reconcile: stale claim release failed
|
|
4689
|
+
console.log(`reconcile: stale claim release failed: ${err?.message ?? String(err)}`);
|
|
4524
4690
|
}
|
|
4525
4691
|
}
|
|
4526
4692
|
}
|
|
4527
|
-
console.log(formatDoctorChecks(checks));
|
|
4693
|
+
console.log(formatDoctorChecks(checks, { inRepo: repoRoot !== null }));
|
|
4528
4694
|
if (doctorFailed(checks))
|
|
4529
4695
|
process.exit(1);
|
|
4530
4696
|
}
|
|
@@ -4570,7 +4736,17 @@ export async function cmdStop(opts = {}) {
|
|
|
4570
4736
|
}
|
|
4571
4737
|
console.log('STOP set — lanes halt between issues');
|
|
4572
4738
|
}
|
|
4573
|
-
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
|
+
}
|
|
4574
4750
|
const repoRoot = await getRepoRoot();
|
|
4575
4751
|
const paths = getFactoryPaths(repoRoot);
|
|
4576
4752
|
const json = opts.json ? { repo: await getGitHubRepo() } : undefined;
|
|
@@ -4585,6 +4761,47 @@ export async function cmdResume(opts = {}) {
|
|
|
4585
4761
|
}
|
|
4586
4762
|
console.log('STOP cleared');
|
|
4587
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
|
+
}
|
|
4588
4805
|
// ---------- main ----------
|
|
4589
4806
|
export async function main(argv = process.argv, deps = {}) {
|
|
4590
4807
|
if (argv.slice(2).length === 0) {
|
|
@@ -4648,8 +4865,9 @@ export async function main(argv = process.argv, deps = {}) {
|
|
|
4648
4865
|
.action(cmdModels);
|
|
4649
4866
|
program
|
|
4650
4867
|
.command('doctor')
|
|
4651
|
-
.description('Check your environment (git,
|
|
4868
|
+
.description('Check your environment (git, agent install and sign-in, gh auth, GitHub token, npm, sandbox, .factory/)')
|
|
4652
4869
|
.option('--reconcile', 'Reap stale port leases, dead-run worktrees, stale issue claims, orphan sf-job-*/factory.managed containers, and orphan factory-* sbx VMs')
|
|
4870
|
+
.option('--probe-inference', 'Also send one real prompt with claude -p on the host and in the sandbox to verify Claude auth (uses quota)')
|
|
4653
4871
|
.action((opts) => cmdDoctor(opts));
|
|
4654
4872
|
program
|
|
4655
4873
|
.command('cost')
|
|
@@ -4744,12 +4962,13 @@ export async function main(argv = process.argv, deps = {}) {
|
|
|
4744
4962
|
.action((runId, opts) => cmdFilingPreview(runId, opts));
|
|
4745
4963
|
const triage = program
|
|
4746
4964
|
.command('triage')
|
|
4747
|
-
.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')
|
|
4748
4966
|
.option('--product <name>', 'Product constitution to scope triage')
|
|
4967
|
+
.option('--model <id>', 'Model to triage with (any registered model, Claude or Codex)')
|
|
4749
4968
|
.action(cmdTriage);
|
|
4750
4969
|
triage
|
|
4751
4970
|
.command('accept')
|
|
4752
|
-
.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')
|
|
4753
4972
|
.option('--force', 'Skip validation and promote as-is')
|
|
4754
4973
|
.action(async (opts) => {
|
|
4755
4974
|
await cmdTriageAccept(opts);
|
|
@@ -4869,7 +5088,7 @@ export async function main(argv = process.argv, deps = {}) {
|
|
|
4869
5088
|
.option('--no-sandbox', 'Disable the containment sandbox for agent runs (dangerous)')
|
|
4870
5089
|
.option('--branch-prefix <prefix>', BRANCH_PREFIX_OPTION_DESCRIPTION)
|
|
4871
5090
|
.option('--run-children', 'File mode only: when the size gate files child issues, run them (slice mode never files issues)')
|
|
4872
|
-
.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')
|
|
4873
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)')
|
|
4874
5093
|
.action(async (issueNum, opts) => {
|
|
4875
5094
|
await cmdRunIssue(parseIssueArg(issueNum), opts);
|
|
@@ -5019,10 +5238,11 @@ export async function main(argv = process.argv, deps = {}) {
|
|
|
5019
5238
|
.option('--json', 'Print one JSON object (schemaVersion 1): action, repo, stopFlag before/after')
|
|
5020
5239
|
.action((opts) => cmdStop(opts));
|
|
5021
5240
|
program
|
|
5022
|
-
.command('resume')
|
|
5023
|
-
.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')
|
|
5024
5243
|
.option('--json', 'Print one JSON object (schemaVersion 1): action, repo, stopFlag before/after')
|
|
5025
|
-
.
|
|
5244
|
+
.option('--from <phase>', 'Resume a kept build at this phase (only: check)')
|
|
5245
|
+
.action((issue, opts) => cmdResume(issue, opts));
|
|
5026
5246
|
applyHelpGroups(program);
|
|
5027
5247
|
const previousLoaders = configLoaders;
|
|
5028
5248
|
const previousGitOps = gitOps;
|