@on-par/factory-cli 0.9.3 → 0.9.4
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 +1 -1
- package/dist/cli/doctor.d.ts +75 -3
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +332 -33
- 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/index.d.ts +10 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +141 -91
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/stop-sentinel.d.ts.map +1 -1
- package/dist/cli/stop-sentinel.js +4 -3
- package/dist/cli/stop-sentinel.js.map +1 -1
- package/package.json +3 -3
package/dist/cli/index.js
CHANGED
|
@@ -8,8 +8,8 @@ import { arch, hostname as osHostname, release, platform, userInfo } from 'node:
|
|
|
8
8
|
import { basename, dirname, 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, 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';
|
|
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, modelsRegistryChecks, orphanMicroVmChecks, runDoctorChecks, sandboxAuthSkipDetail, reconcileAgentAuthChecks, 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';
|
|
@@ -179,7 +179,9 @@ function resolvePhaseOps(overrides, base = phaseOps) {
|
|
|
179
179
|
shipPhase: overrides.shipPhase ?? base.shipPhase,
|
|
180
180
|
};
|
|
181
181
|
}
|
|
182
|
-
export const PREREQUISITES_TEXT = `
|
|
182
|
+
export const PREREQUISITES_TEXT = `First step: run \`factory doctor\` to check your setup and see what to fix.
|
|
183
|
+
|
|
184
|
+
Prerequisites:
|
|
183
185
|
- Claude Code CLI installed and authenticated (Claude subscription): https://claude.com/claude-code
|
|
184
186
|
- GitHub CLI authenticated: gh auth login
|
|
185
187
|
- GITHUB_TOKEN or GH_TOKEN set (falls back to \`gh auth token\`)
|
|
@@ -339,13 +341,13 @@ async function cmdInit(opts = {}) {
|
|
|
339
341
|
// Doctor-style validation (policy=auto, N models reachable) — informational, never fails init.
|
|
340
342
|
const modelsConfig = applyRepoConfig(configLoaders.loadModelsConfig(), configLoaders.loadRepoConfig(repoRoot));
|
|
341
343
|
const registry = new coreOps.ModelRegistry(modelsConfig);
|
|
342
|
-
const diagnoses = coreOps.diagnoseModels(registry, modelDoctorProbes(), resolveExperimental(), resolveLocalOnly());
|
|
344
|
+
const diagnoses = coreOps.diagnoseModels(registry, modelDoctorProbes({ requireOllamaOnPath: true }), resolveExperimental(), resolveLocalOnly());
|
|
343
345
|
console.log(formatInitReachability(diagnoses));
|
|
344
346
|
if (!hasReachableWorker(diagnoses)) {
|
|
345
347
|
console.log(styleText('yellow', 'No worker model reachable yet — see `factory doctor` and `factory models --doctor`.'));
|
|
346
348
|
}
|
|
347
349
|
console.log(styleText('green', `Initialized ${paths.root}`));
|
|
348
|
-
console.log(
|
|
350
|
+
console.log('Next: factory doctor, then factory triage (optional: factory constitution --product <name> to swap the default constitution)');
|
|
349
351
|
}
|
|
350
352
|
/** Rewrite a checkout's legacy `.factory/` inputs and runtime files to the v2
|
|
351
353
|
* layout. Every write is idempotent so it is safe to run this more than once. */
|
|
@@ -723,12 +725,16 @@ export async function cmdConstitution(opts, deps = {}) {
|
|
|
723
725
|
export function modelStatusIcon(d) {
|
|
724
726
|
return d.reachable ? styleText('green', '✅') : d.unverified ? styleText('yellow', '⚠️') : styleText('red', '❌');
|
|
725
727
|
}
|
|
726
|
-
|
|
728
|
+
/** Single owner of the `[repo-defined]` marker for models declared in `.factory/config.json` models.registry, shared by `factory models` and `models --doctor` (#2443). */
|
|
729
|
+
export function repoDefinedTag(model, repoDefined) {
|
|
730
|
+
return repoDefined.has(model) ? styleText('cyan', ' [repo-defined]') : '';
|
|
731
|
+
}
|
|
732
|
+
export function formatDoctorReport(diagnoses, repoDefined = new Set()) {
|
|
727
733
|
const lines = [styleText('bold', '\n== Model Doctor ==')];
|
|
728
734
|
for (const d of diagnoses) {
|
|
729
735
|
const icon = modelStatusIcon(d);
|
|
730
736
|
const tiers = d.tiers.join('/');
|
|
731
|
-
lines.push(` ${icon} ${d.model} provider=${d.provider} tier=${tiers} — ${d.reason}`);
|
|
737
|
+
lines.push(` ${icon} ${d.model} provider=${d.provider} tier=${tiers} — ${d.reason}${repoDefinedTag(d.model, repoDefined)}`);
|
|
732
738
|
}
|
|
733
739
|
lines.push(` ${formatReachabilityCount(diagnoses)}`);
|
|
734
740
|
return lines.join('\n');
|
|
@@ -736,7 +742,9 @@ export function formatDoctorReport(diagnoses) {
|
|
|
736
742
|
export function hasReachableWorker(diagnoses) {
|
|
737
743
|
return diagnoses.some((d) => d.reachable && (d.tiers.includes('worker') || d.tiers.includes('worker_fallback')));
|
|
738
744
|
}
|
|
739
|
-
function ollamaModelSet() {
|
|
745
|
+
function ollamaModelSet(requireOnPath = false) {
|
|
746
|
+
if (requireOnPath && !coreOps.isCommandAvailable('ollama'))
|
|
747
|
+
return undefined;
|
|
740
748
|
try {
|
|
741
749
|
const out = shellOps.execSync('ollama list', { timeout: 10_000 });
|
|
742
750
|
return new Set(out
|
|
@@ -750,9 +758,10 @@ function ollamaModelSet() {
|
|
|
750
758
|
}
|
|
751
759
|
}
|
|
752
760
|
/** Probes shared by every doctor-style reachability check (init, models --doctor, overnight
|
|
753
|
-
* preflight), so their counts agree.
|
|
754
|
-
|
|
755
|
-
|
|
761
|
+
* preflight), so their counts agree. init passes `requireOllamaOnPath` so a missing binary never
|
|
762
|
+
* reaches the shell (#2507). */
|
|
763
|
+
export function modelDoctorProbes(opts = {}) {
|
|
764
|
+
const ollamaModels = ollamaModelSet(opts.requireOllamaOnPath === true);
|
|
756
765
|
return {
|
|
757
766
|
commandAvailable: coreOps.isCommandAvailable,
|
|
758
767
|
ollamaModelPresent: ollamaModels ? (model) => ollamaModels.has(model) : undefined,
|
|
@@ -760,13 +769,15 @@ export function modelDoctorProbes() {
|
|
|
760
769
|
}
|
|
761
770
|
async function cmdModels(opts = {}) {
|
|
762
771
|
const repoRoot = await getRepoRoot();
|
|
763
|
-
const
|
|
772
|
+
const repoConfig = configLoaders.loadRepoConfig(repoRoot);
|
|
773
|
+
const modelsConfig = applyRepoConfig(configLoaders.loadModelsConfig(), repoConfig);
|
|
774
|
+
const repoDefined = new Set(Object.keys(repoConfig?.models?.registry ?? {}));
|
|
764
775
|
const registry = new coreOps.ModelRegistry(modelsConfig);
|
|
765
776
|
const allowExperimental = resolveExperimental();
|
|
766
777
|
const localOnly = resolveLocalOnly();
|
|
767
778
|
const diagnoses = coreOps.diagnoseModels(registry, modelDoctorProbes(), allowExperimental, localOnly);
|
|
768
779
|
if (opts.doctor) {
|
|
769
|
-
console.log(formatDoctorReport(diagnoses));
|
|
780
|
+
console.log(formatDoctorReport(diagnoses, repoDefined));
|
|
770
781
|
if (!hasReachableWorker(diagnoses)) {
|
|
771
782
|
throw new CliExitError('factory: no worker model is reachable — fix the reasons above before running a queue', 1);
|
|
772
783
|
}
|
|
@@ -781,7 +792,7 @@ async function cmdModels(opts = {}) {
|
|
|
781
792
|
const d = byModel.get(m);
|
|
782
793
|
const avail = d ? modelStatusIcon(d) : styleText('red', '❌');
|
|
783
794
|
const tag = registry.isExperimental(m) ? styleText('yellow', ' [experimental]') : '';
|
|
784
|
-
console.log(` ${avail} ${m} tier=${tiers} ${cost}${tag}`);
|
|
795
|
+
console.log(` ${avail} ${m} tier=${tiers} ${cost}${tag}${repoDefinedTag(m, repoDefined)}`);
|
|
785
796
|
}
|
|
786
797
|
console.log(styleText('bold', '\n== Tiers =='));
|
|
787
798
|
for (const tier of ['boss', 'worker', 'checker', 'triage']) {
|
|
@@ -1560,6 +1571,7 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1560
1571
|
: { ...resolveEffectiveConfig(repoConfig), branchPrefix: opts.branchPrefix },
|
|
1561
1572
|
};
|
|
1562
1573
|
const router = new core.ModelRouter(policy.models, policy.routes, false, undefined, policy.effective.allowExperimental, policy.effective.localOnly);
|
|
1574
|
+
router.skipMissingHarnessClis(core.isCommandAvailable);
|
|
1563
1575
|
let issueSpend = 0;
|
|
1564
1576
|
let laneSandboxRuntime = 'none';
|
|
1565
1577
|
const laneWorkspaceBackend = factoryConfig.workspace?.backend === 'disposable-docker' ? 'disposable-docker' : 'worktree';
|
|
@@ -2014,8 +2026,8 @@ export async function sweepBetweenLaneIssues(issue, lane, repoRoot, ghRepo, path
|
|
|
2014
2026
|
}
|
|
2015
2027
|
}
|
|
2016
2028
|
async function cmdShip(issueNum, opts) {
|
|
2017
|
-
if (
|
|
2018
|
-
throw new CliExitError(`factory: ${
|
|
2029
|
+
if (availableAgentClis(coreOps.isCommandAvailable).length === 0) {
|
|
2030
|
+
throw new CliExitError(`factory: ${missingAgentCliMessage()}`, 2);
|
|
2019
2031
|
}
|
|
2020
2032
|
const repoRoot = await getRepoRoot();
|
|
2021
2033
|
const paths = getFactoryPaths(repoRoot);
|
|
@@ -2050,13 +2062,16 @@ async function cmdShip(issueNum, opts) {
|
|
|
2050
2062
|
catch (err) {
|
|
2051
2063
|
if (err instanceof IssueSkippedError)
|
|
2052
2064
|
return;
|
|
2065
|
+
// #2492: a pull request number is a usage error, not a failed ship.
|
|
2066
|
+
if (err instanceof InvalidWorkRequestInputError)
|
|
2067
|
+
throw new CliExitError(`factory: ${err.message}`, 2);
|
|
2053
2068
|
throw new CliExitError(`Ship failed for issue #${issueNum}: ${err.message}`, 1);
|
|
2054
2069
|
}
|
|
2055
2070
|
});
|
|
2056
2071
|
}
|
|
2057
2072
|
async function cmdRunIssue(issueNum, opts) {
|
|
2058
|
-
if (
|
|
2059
|
-
throw new CliExitError(`factory: ${
|
|
2073
|
+
if (availableAgentClis(coreOps.isCommandAvailable).length === 0) {
|
|
2074
|
+
throw new CliExitError(`factory: ${missingAgentCliMessage()}`, 2);
|
|
2060
2075
|
}
|
|
2061
2076
|
const repoRoot = await getRepoRoot();
|
|
2062
2077
|
const paths = getFactoryPaths(repoRoot);
|
|
@@ -2173,8 +2188,8 @@ async function runDecomposedChildren(parent, children, opts, env) {
|
|
|
2173
2188
|
}
|
|
2174
2189
|
}
|
|
2175
2190
|
async function cmdRunBrief(briefPath, opts) {
|
|
2176
|
-
if (
|
|
2177
|
-
throw new CliExitError(`factory: ${
|
|
2191
|
+
if (availableAgentClis(coreOps.isCommandAvailable).length === 0) {
|
|
2192
|
+
throw new CliExitError(`factory: ${missingAgentCliMessage()}`, 2);
|
|
2178
2193
|
}
|
|
2179
2194
|
const repoRoot = await getRepoRoot();
|
|
2180
2195
|
const paths = getFactoryPaths(repoRoot);
|
|
@@ -2290,8 +2305,8 @@ async function cmdLocalSmallOvernight(opts) {
|
|
|
2290
2305
|
const ghRepo = await getGitHubRepo();
|
|
2291
2306
|
process.env.FACTORY_LOCAL_ONLY = '1';
|
|
2292
2307
|
const preflight = async () => {
|
|
2293
|
-
if (
|
|
2294
|
-
return { ok: false, reason:
|
|
2308
|
+
if (availableAgentClis(coreOps.isCommandAvailable).length === 0)
|
|
2309
|
+
return { ok: false, reason: missingAgentCliMessage() };
|
|
2295
2310
|
const registry = new coreOps.ModelRegistry(applyRepoConfig(configLoaders.loadModelsConfig(), configLoaders.loadRepoConfig(repoRoot)));
|
|
2296
2311
|
const diagnoses = coreOps.diagnoseModels(registry, modelDoctorProbes(), resolveExperimental(), true);
|
|
2297
2312
|
if (!hasReachableWorker(diagnoses)) {
|
|
@@ -2635,28 +2650,33 @@ async function cmdTriage(opts) {
|
|
|
2635
2650
|
const routesConfig = configLoaders.loadRoutesConfig();
|
|
2636
2651
|
const effective = resolveEffectiveConfig(repoConfig);
|
|
2637
2652
|
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.`;
|
|
2653
|
+
router.skipMissingHarnessClis(coreOps.isCommandAvailable);
|
|
2654
|
+
router.setCostSink((entry) => logCost(paths.costs, { ...entry, issue: '-' }));
|
|
2652
2655
|
let plannerError;
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2656
|
+
let model;
|
|
2657
|
+
try {
|
|
2658
|
+
const { stdout: listed } = await shellOps.exec(`gh issue list --repo ${shellEscape(ghRepo)} --state open --limit 100 --json number,title,labels,body`);
|
|
2659
|
+
const prompt = buildTriagePrompt({ repo: ghRepo, product, candidates: parseTriageCandidates(listed || '[]') });
|
|
2660
|
+
logEvent(paths.events, 'triage', '-', `Triaging ${ghRepo} via the triage route`);
|
|
2661
|
+
const result = await router.run('triage', prompt, {
|
|
2662
|
+
worktree: repoRoot,
|
|
2663
|
+
timeoutSeconds: 600,
|
|
2664
|
+
...(opts.model ? { modelOverride: opts.model } : {}),
|
|
2665
|
+
onLog: (msg) => logEvent(paths.events, 'triage', '-', msg),
|
|
2666
|
+
});
|
|
2667
|
+
model = result.model;
|
|
2668
|
+
const queue = normalizeTriageQueue(result.output);
|
|
2669
|
+
if (queue) {
|
|
2670
|
+
const header = `# Triage queue (model ${model}${product ? `, constitution ${product}` : ''})\n`;
|
|
2671
|
+
writeFileSync(paths.queueProposed, header + queue);
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
catch (err) {
|
|
2657
2675
|
plannerError = err;
|
|
2658
2676
|
logEvent(paths.events, 'warn', '-', `triage planner failed: ${errorDetail(err)}`);
|
|
2659
|
-
}
|
|
2677
|
+
}
|
|
2678
|
+
if (model)
|
|
2679
|
+
logEvent(paths.events, 'triage', '-', `Triaged ${ghRepo} with ${model}`);
|
|
2660
2680
|
const proposed = existsSync(paths.queueProposed) ? readFileSync(paths.queueProposed, 'utf-8') : '';
|
|
2661
2681
|
const message = triageProposalMessage(proposed, paths.queueProposed, paths.queue);
|
|
2662
2682
|
if (message) {
|
|
@@ -4366,9 +4386,15 @@ async function cmdDoctor(opts = {}) {
|
|
|
4366
4386
|
const checks = runDoctorChecks({
|
|
4367
4387
|
commandAvailable: coreOps.isCommandAvailable,
|
|
4368
4388
|
envPresent: (key) => !!process.env[key],
|
|
4369
|
-
tryExec: (cmd) => {
|
|
4389
|
+
tryExec: (cmd, opts) => {
|
|
4370
4390
|
try {
|
|
4371
|
-
return shellOps
|
|
4391
|
+
return shellOps
|
|
4392
|
+
.execSync(cmd, {
|
|
4393
|
+
timeout: opts?.timeoutMs ?? 10_000,
|
|
4394
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
4395
|
+
...(opts?.env ? { env: { ...process.env, ...opts.env } } : {}),
|
|
4396
|
+
})
|
|
4397
|
+
.trim();
|
|
4372
4398
|
}
|
|
4373
4399
|
catch {
|
|
4374
4400
|
return null;
|
|
@@ -4376,6 +4402,14 @@ async function cmdDoctor(opts = {}) {
|
|
|
4376
4402
|
},
|
|
4377
4403
|
pathExists: existsSync,
|
|
4378
4404
|
distFreshness: distFreshnessProbe(import.meta.url),
|
|
4405
|
+
codexSessionState: () => codexChatgptSessionState(),
|
|
4406
|
+
claudeLoggedIn: () => hasClaudeLoginEvidence({
|
|
4407
|
+
exec: (file, args) => {
|
|
4408
|
+
shellOps.execSync([file, ...args].map(shellEscape).join(' '), { timeout: 10_000, stdio: 'ignore' });
|
|
4409
|
+
},
|
|
4410
|
+
fileExists: existsSync,
|
|
4411
|
+
}),
|
|
4412
|
+
opencodeLoggedIn: () => hasOpencodeLoginEvidence(),
|
|
4379
4413
|
});
|
|
4380
4414
|
let repoRoot;
|
|
4381
4415
|
try {
|
|
@@ -4389,53 +4423,57 @@ async function cmdDoctor(opts = {}) {
|
|
|
4389
4423
|
catch {
|
|
4390
4424
|
repoRoot = null;
|
|
4391
4425
|
}
|
|
4392
|
-
|
|
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
|
-
repoRoot,
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
sandboxed === 'ok'
|
|
4433
|
-
|
|
4434
|
-
|
|
4426
|
+
// Only with --probe-inference: these send a real prompt (uses quota). Plain doctor makes no inference call.
|
|
4427
|
+
if (opts.probeInference) {
|
|
4428
|
+
const CLAUDE_AUTH_PROBE = 'claude -p "reply with exactly: ok"';
|
|
4429
|
+
const probeExec = (cmd) => {
|
|
4430
|
+
try {
|
|
4431
|
+
return shellOps.execSync(cmd, { timeout: 120_000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
4432
|
+
}
|
|
4433
|
+
catch {
|
|
4434
|
+
return null;
|
|
4435
|
+
}
|
|
4436
|
+
};
|
|
4437
|
+
let host = 'skipped';
|
|
4438
|
+
let hostDetail = 'skipped — claude CLI not on PATH';
|
|
4439
|
+
if (coreOps.isCommandAvailable('claude')) {
|
|
4440
|
+
host = probeExec(CLAUDE_AUTH_PROBE) === null ? 'failed' : 'ok';
|
|
4441
|
+
hostDetail = host === 'ok' ? 'claude -p succeeded on the host' : 'claude -p failed on the host';
|
|
4442
|
+
}
|
|
4443
|
+
let sandboxed = 'skipped';
|
|
4444
|
+
let sandboxDetail = 'skipped — host claude auth was not verified';
|
|
4445
|
+
const runtimeAvailable = hasSandboxRuntime(process.platform, (cmd) => isCommandAvailableViaShell(shellOps, cmd));
|
|
4446
|
+
const skip = sandboxAuthSkipDetail({ runtimeAvailable, hostOk: host === 'ok', inRepo: repoRoot !== null });
|
|
4447
|
+
if (skip !== null) {
|
|
4448
|
+
sandboxDetail = skip;
|
|
4449
|
+
}
|
|
4450
|
+
else if (repoRoot !== null) {
|
|
4451
|
+
const policy = resolveSandboxPolicy(configLoaders.loadFactoryConfigForRepo(getFactoryPaths(repoRoot).config).sandbox, {
|
|
4452
|
+
worktree: repoRoot,
|
|
4453
|
+
repoRoot,
|
|
4454
|
+
isAvailable: (cmd) => isCommandAvailableViaShell(shellOps, cmd),
|
|
4455
|
+
});
|
|
4456
|
+
if (!policy) {
|
|
4457
|
+
sandboxDetail = 'skipped — sandbox disabled by config or FACTORY_SANDBOX';
|
|
4458
|
+
}
|
|
4459
|
+
else if (policy.runtime === 'none') {
|
|
4460
|
+
sandboxDetail = 'skipped — no sandbox runtime (sandbox-exec/firejail) on this host';
|
|
4461
|
+
}
|
|
4462
|
+
else if (policy.runtime === 'docker-sandbox') {
|
|
4463
|
+
sandboxDetail = 'skipped — docker-sandbox containment is the microVM, not command wrapping (#653)';
|
|
4464
|
+
}
|
|
4465
|
+
else {
|
|
4466
|
+
sandboxed = probeExec(wrapCommandInSandbox(CLAUDE_AUTH_PROBE, policy)) === null ? 'failed' : 'ok';
|
|
4467
|
+
sandboxDetail =
|
|
4468
|
+
sandboxed === 'ok'
|
|
4469
|
+
? `claude -p succeeded under ${policy.runtime}`
|
|
4470
|
+
: `claude -p failed under ${policy.runtime} (local_auth)`;
|
|
4471
|
+
}
|
|
4435
4472
|
}
|
|
4473
|
+
checks.push(...sandboxClaudeAuthChecks({ host, sandboxed, hostDetail, sandboxDetail }));
|
|
4436
4474
|
}
|
|
4437
4475
|
checks.push(claudeKeychainCheck(probeClaudeKeychain()));
|
|
4438
|
-
checks.
|
|
4476
|
+
checks.splice(0, checks.length, ...reconcileAgentAuthChecks(checks));
|
|
4439
4477
|
if (repoRoot !== null) {
|
|
4440
4478
|
const paths = getFactoryPaths(repoRoot);
|
|
4441
4479
|
const factoryConfig = configLoaders.loadFactoryConfigForRepo(paths.config);
|
|
@@ -4455,6 +4493,16 @@ async function cmdDoctor(opts = {}) {
|
|
|
4455
4493
|
checks.push(stopSentinelCheck(readStopFileStatus(paths)));
|
|
4456
4494
|
const eventsContent = existsSync(paths.events) ? readFileSync(paths.events, 'utf-8') : null;
|
|
4457
4495
|
checks.push(eventLogCheck(eventsContent === null ? null : analyzeEventLog(eventsContent)));
|
|
4496
|
+
try {
|
|
4497
|
+
const declared = Object.keys(configLoaders.loadModelsConfig().harnesses ?? {});
|
|
4498
|
+
checks.push(...modelsRegistryChecks(paths.config, configLoaders.loadRepoConfig(repoRoot)?.models?.registry, [
|
|
4499
|
+
...KNOWN_HARNESS_IDS,
|
|
4500
|
+
...declared,
|
|
4501
|
+
]));
|
|
4502
|
+
}
|
|
4503
|
+
catch {
|
|
4504
|
+
// malformed config is reported by the load paths / `factory models --doctor`; doctor keeps going
|
|
4505
|
+
}
|
|
4458
4506
|
let harnessProbes = [];
|
|
4459
4507
|
try {
|
|
4460
4508
|
const registry = new coreOps.ModelRegistry(applyRepoConfig(configLoaders.loadModelsConfig(), configLoaders.loadRepoConfig(repoRoot)));
|
|
@@ -4524,7 +4572,7 @@ async function cmdDoctor(opts = {}) {
|
|
|
4524
4572
|
}
|
|
4525
4573
|
}
|
|
4526
4574
|
}
|
|
4527
|
-
console.log(formatDoctorChecks(checks));
|
|
4575
|
+
console.log(formatDoctorChecks(checks, { inRepo: repoRoot !== null }));
|
|
4528
4576
|
if (doctorFailed(checks))
|
|
4529
4577
|
process.exit(1);
|
|
4530
4578
|
}
|
|
@@ -4648,8 +4696,9 @@ export async function main(argv = process.argv, deps = {}) {
|
|
|
4648
4696
|
.action(cmdModels);
|
|
4649
4697
|
program
|
|
4650
4698
|
.command('doctor')
|
|
4651
|
-
.description('Check your environment (git,
|
|
4699
|
+
.description('Check your environment (git, agent install and sign-in, gh auth, GitHub token, npm, sandbox, .factory/)')
|
|
4652
4700
|
.option('--reconcile', 'Reap stale port leases, dead-run worktrees, stale issue claims, orphan sf-job-*/factory.managed containers, and orphan factory-* sbx VMs')
|
|
4701
|
+
.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
4702
|
.action((opts) => cmdDoctor(opts));
|
|
4654
4703
|
program
|
|
4655
4704
|
.command('cost')
|
|
@@ -4746,6 +4795,7 @@ export async function main(argv = process.argv, deps = {}) {
|
|
|
4746
4795
|
.command('triage')
|
|
4747
4796
|
.description('Have a model propose a queue from open issues into .factory/queue.proposed')
|
|
4748
4797
|
.option('--product <name>', 'Product constitution to scope triage')
|
|
4798
|
+
.option('--model <id>', 'Model to triage with (any registered model, Claude or Codex)')
|
|
4749
4799
|
.action(cmdTriage);
|
|
4750
4800
|
triage
|
|
4751
4801
|
.command('accept')
|