@on-par/factory-cli 0.9.6-next.1 → 0.9.6-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -7,20 +7,20 @@ import { createRequire } from 'node:module';
7
7
  import { arch, hostname as osHostname, release, platform, userInfo } from 'node:os';
8
8
  import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
9
9
  import { promisify, styleText } from 'node:util';
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, computeHealthKpis, ConstitutionLoader, createDefaultWorkSourceRegistry, createLogger, createLaneProxy, defaultFindPortListeners, describeEffectiveConfig, detectPostMergeDefects, diagnoseModels, probeDeclaredHarnesses, 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, laneHostLabel, loadFactoryConfig, loadFactoryConfigForRepo, loadModelsConfig, loadRepoConfig, loadRoutesConfig, LOCAL_BRIEF_SOURCE, joinClassifierOutcomes, mergeClassifierOutcomes, parseClassifierOutcomes, summarizeClassifierOutcomes, formatClassifierReport, mergedPrRefs, ModelRegistry, ModelRouter, parkReasonFor, parseKpiHistory, parseQueue, partitionLocalQueueByActivity, planPhase, provisionLaneContainer, ProviderBreaker, readEvents, readPortLeases, readQueue, readUsage, reapOrphanProcesses, reapStalePortLeases, reconstructKpiEvents, recordLeasePgid, releasePortLease, renderKpiReport, renderKpiTrend, resolveCodexDisabled, resolveDefectWindowDays, resolveSizeGateMode, parseSizeGateMode, parseMaxSlices, SIZE_GATE_MODES, resolveEffectiveBuildRoute, resolveEffectiveModelPins, resolveLaneBreakerThreshold, resolveEnvironmentProxy, resolveArtifactsDir, resolveIngestConfig, resolveLocalOnlyPolicy, resolveMergePolicy, resolveProcessGroupGraceMs, resolveSandboxPolicy, resolveSkipCI, resolveTimeouts, resolveUsageCap, resolveWatchdogPolicy, runAutoIngest, shipPhase, formatCostTotal, validateQueue, watchUsage, writeLocalRunReport, writeProxyState, } from '@on-par/factory-core';
12
- import { acquirePidFile, createCheckResumeStore, findSlicePlanComment, recordSliceMerged, upsertSlicePlanComment, withSliceState, DEFAULT_GARDEN_MAX_CLUSTERS, DEFAULT_GARDEN_SINCE, capGardenClusters, clusterGarden, dedupGardenClusters, filterGardenOnlyNew, filterGardenWindow, markGardenTrackingUnknown, parseGardenDuration, readHarvestEvents, renderGardenJson, renderGardenReport, formatWorktreeLocation, laneWorktreePath, resolveWorktreeRoot, migrateRepoConfigToYaml, REPO_CONFIG_YAML_HEADER, claudeConfigDirOverride, claudeKeychainService, cleanupWorktree, createDaemonLogSink, factoryBranchIssue, factoryBranchPrefixes, 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, listMicroVms, listOrphanContainers, logCost, logEvent, microVmName, planQueueMigration, readCosts, readGithubQueueSnapshot, reapLaneWorktree, reapOrphanContainers, reapOrphanMicroVm, releaseRuntimeFiles, releaseStaleClaims, resolveBranchPrefix, resolveEffectiveConfig, worktreeSandboxFor, resolveExperimental, resolveLocalOnly, resolvePrClassifierPolicy, resolveRunUsageHarness, resolveReviewFloorRules, RunLockHeldError, runOvernightQueue, parseRemoteSlug, setupWorktree, shellEscape, listBuiltInCheckers, slugify, sweepWorktrees, watchChecks, withAdmissionGuard, withFileLock, withGitLock, withRunLock, wrapCommandInSandbox, writePortFile, buildTriagePrompt, normalizeTriageQueue, parseTriageCandidates, codexChatgptSessionState, hasClaudeLoginEvidence, claudeStoredSessionState, localAuthFailureSinceLogin, recordLocalAuthFailure, hasOpencodeLoginEvidence, } from '@on-par/factory-core/internal';
10
+ import { INVALID_BRANCH_PREFIX_MESSAGE, resolveBranchPrefixOption } from './branch-prefix.js';
11
+ import { acquirePortLease, aggregateCosts, appendKpiHistoryLine, applyRepoConfig, buildPhase, checkPhase, clearProxyState, computeHealthKpis, ConstitutionLoader, createDefaultWorkSourceRegistry, createLogger, createLaneProxy, defaultFindPortListeners, describeEffectiveConfig, detectPostMergeDefects, diagnoseModels, probeDeclaredHarnesses, 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, laneHostLabel, loadFactoryConfig, loadFactoryConfigForRepo, loadModelsConfig, loadRepoConfig, loadRoutesConfig, LOCAL_BRIEF_SOURCE, joinClassifierOutcomes, mergeClassifierOutcomes, parseClassifierOutcomes, summarizeClassifierOutcomes, formatClassifierReport, mergedPrRefs, ModelRegistry, ModelRouter, parkReasonFor, parseKpiHistory, parseQueue, partitionLocalQueueByActivity, planPhase, provisionLaneContainer, ProviderBreaker, readEvents, readPortLeases, readQueue, readUsage, reapOrphanProcesses, reapStalePortLeases, reconstructKpiEvents, recordLeasePgid, releasePortLease, renderKpiReport, renderKpiTrend, resolveCodexDisabled, resolveDefectWindowDays, resolveSizeGateMode, parseSizeGateMode, parseMaxSlices, SIZE_GATE_MODES, resolveEffectiveBuildRoute, resolveEffectiveModelPins, resolveLaneBreakerThreshold, resolveEnvironmentProxy, resolveArtifactsDir, resolveLocalOnlyPolicy, resolveMergePolicy, resolveProcessGroupGraceMs, resolveSandboxPolicy, resolveSkipCI, resolveTimeouts, resolveUsageCap, resolveWatchdogPolicy, runAutoIngest, shipPhase, formatCostTotal, validateQueue, watchUsage, writeLocalRunReport, writeProxyState, } from '@on-par/factory-core';
12
+ import { acquirePidFile, createCheckResumeStore, findSlicePlanComment, recordSliceMerged, upsertSlicePlanComment, withSliceState, DEFAULT_GARDEN_MAX_CLUSTERS, DEFAULT_GARDEN_SINCE, capGardenClusters, clusterGarden, dedupGardenClusters, filterGardenOnlyNew, filterGardenWindow, markGardenTrackingUnknown, parseGardenDuration, readHarvestEvents, renderGardenJson, renderGardenReport, formatWorktreeLocation, laneWorktreePath, resolveWorktreeRoot, migrateRepoConfigToYaml, REPO_CONFIG_YAML_HEADER, claudeConfigDirOverride, claudeKeychainService, cleanupWorktree, createDaemonLogSink, factoryBranchIssue, factoryBranchPrefixes, createDockerEngine, createFactorydServer, createRunRuntime, createShipExecutor, createGithubQueue, createLocalSmallDryRun, createOctokitFilingClient, createOctokitGreenPrClient, createOctokitQueueClient, daemonRuntimePaths, defaultRegistryPath, ensureDir, FACTORY_RUNTIME_CONFIG_KEYS, findUnmergedGreenPrs, formatGcReport, gitFetch, listMicroVms, listOrphanContainers, logCost, logEvent, microVmName, planQueueMigration, readCosts, readGithubQueueSnapshot, reapLaneWorktree, reapOrphanContainers, reapOrphanMicroVm, releaseRuntimeFiles, releaseStaleClaims, resolveBranchPrefix, resolveEffectiveConfig, worktreeSandboxFor, resolveExperimental, resolveLocalOnly, resolvePrClassifierPolicy, resolveRunUsageHarness, resolveReviewFloorRules, RunLockHeldError, runOvernightQueue, parseRemoteSlug, setupWorktree, shellEscape, listBuiltInCheckers, slugify, sweepWorktrees, watchChecks, withAdmissionGuard, withFileLock, withGitLock, withRunLock, wrapCommandInSandbox, writePortFile, buildTriagePrompt, normalizeTriageQueue, parseTriageCandidates, codexChatgptSessionState, hasClaudeLoginEvidence, claudeStoredSessionState, localAuthFailureSinceLogin, recordLocalAuthFailure, hasOpencodeLoginEvidence, } from '@on-par/factory-core/internal';
13
13
  import { runTui } from '@on-par/factory-tui';
14
- import { Command } from 'commander';
15
14
  import { admissionStateReaderFor, runQueueReconcile } from './admission.js';
16
15
  import { cmdClassifierBacktest, ClassifierBacktestInputError, } from './classifier-backtest.js';
17
16
  import { findOpenPRForIssue, landIssue, waitForMerge } from './land.js';
18
17
  import { AwaitingReviewError, CiFailedError, CiUnverifiedError, getPullRequestLandState, LandConflictError, LandFailureError, MergeGatedError, } from './land-pr.js';
19
18
  import { setLandRuntime } from './land-runtime.js';
20
19
  import { shipIssue } from './ship.js';
20
+ import { cmdSupervise } from './supervise.js';
21
21
  import { setShipRuntime } from './ship-runtime.js';
22
22
  import { runQueueClear } from './queue-clear.js';
23
- import { applyStateRootOption } from './state-root.js';
23
+ import { buildProgram } from './program.js';
24
24
  import { buildCostJson } from './cost-json.js';
25
25
  import { buildQueueAddJson } from './queue-add-json.js';
26
26
  import { buildQueueListJson } from './queue-list.js';
@@ -43,7 +43,6 @@ import { resolveFactoryCheckoutCommit, runFilingPreview } from './filing.js';
43
43
  import { buildFeedbackDeps, resolveFeedbackRepo, runFeedback } from './feedback.js';
44
44
  import { ensureFactoryExcluded, writeSampleQueue } from './init-files.js';
45
45
  import { cmdLogs } from './logs.js';
46
- import { applyHelpGroups } from './help-groups.js';
47
46
  import { mergeScopeNotice } from './merge-scope.js';
48
47
  import { selectOctokit } from './github-stub.js';
49
48
  import { createFactoryOctokit } from './octokit.js';
@@ -2911,60 +2910,6 @@ export function createIngestHook(repoRoot, paths, ingestCfg, deps = {}) {
2911
2910
  }
2912
2911
  };
2913
2912
  }
2914
- async function cmdSupervise(opts) {
2915
- const branchPrefix = branchPrefixOrExit(opts.branchPrefix);
2916
- const repoRoot = await getRepoRoot();
2917
- const paths = getFactoryPaths(repoRoot);
2918
- const ingestCfg = resolveIngestConfig(configLoaders.loadFactoryConfigForRepo(paths.config));
2919
- const countPending = async () => {
2920
- if (opts.localQueue === true) {
2921
- const content = existsSync(paths.queue) ? readFileSync(paths.queue, 'utf-8') : '';
2922
- const parsed = parseQueue(content);
2923
- warnQueueDiagnostics(parsed.diagnostics);
2924
- return parsed.entries.length;
2925
- }
2926
- const [owner, repo] = (await getGitHubRepo()).split('/');
2927
- const gq = createGithubQueue({ client: createOctokitQueueClient(getOctokit()), owner, repo });
2928
- const lanes = await gq.lanes();
2929
- const counts = await Promise.all(lanes.map((lane) => gq.list(lane)));
2930
- return counts.reduce((total, issues) => total + issues.length, 0);
2931
- };
2932
- if ((await countPending()) === 0 && !ingestCfg.enabled) {
2933
- throw new CliExitError('queue empty — run factory init + triage first', 2);
2934
- }
2935
- let knobs;
2936
- try {
2937
- knobs = resolveUsageKnobs(process.env, configLoaders.loadRepoConfig(repoRoot), usageFlagOverrides(opts, 'resumeAt'));
2938
- }
2939
- catch (err) {
2940
- throw new CliExitError(`factory: ${err.message}`, 2);
2941
- }
2942
- await withRepoRunLock(paths, 'factory supervise', () => superviseLoop({
2943
- cap: knobs.cap,
2944
- resumeAt: knobs.resumeAt,
2945
- pollMs: knobs.pollMs,
2946
- watch: knobs.watch,
2947
- watchSource: knobs.watchSource,
2948
- resumeAtSource: knobs.resumeAtSource,
2949
- pollSource: knobs.pollSource,
2950
- estimator: knobs.estimator,
2951
- stopFile: paths.stop,
2952
- eventsFile: paths.events,
2953
- now: opts.now,
2954
- runQueue: createSuperviseRunQueue(paths, ingestCfg, {
2955
- cmdRunFn: () => cmdRun({
2956
- localQueue: opts.localQueue,
2957
- autoMerge: opts.autoMerge,
2958
- usageWatch: opts.usageWatch,
2959
- usageThreshold: opts.usageThreshold,
2960
- usagePoll: opts.usagePoll,
2961
- branchPrefix: opts.branchPrefix,
2962
- }),
2963
- pendingCount: countPending,
2964
- }),
2965
- ingest: ingestCfg.enabled ? createIngestHook(repoRoot, paths, ingestCfg, { branchPrefix }) : undefined,
2966
- }));
2967
- }
2968
2913
  export function laneQueueDeps(queue, lane) {
2969
2914
  return {
2970
2915
  claimNext: () => queue.claimNext(lane),
@@ -3932,6 +3877,83 @@ async function resumeFromCheck(issueNum) {
3932
3877
  }
3933
3878
  });
3934
3879
  }
3880
+ /** index.ts bindings the commander registrations in program*.ts call (#2793); type-only imports there avoid a runtime cycle. */
3881
+ /** `factory supervise` (#2797): the handler lives in ./supervise.ts; this binds its runtime. */
3882
+ function runSupervise(opts) {
3883
+ return cmdSupervise(opts, {
3884
+ configLoaders,
3885
+ getRepoRoot: () => getRepoRoot(),
3886
+ getGitHubRepo: () => getGitHubRepo(),
3887
+ getOctokit: () => getOctokit(),
3888
+ branchPrefixOrExit,
3889
+ warnQueueDiagnostics,
3890
+ resolveUsageKnobs,
3891
+ usageFlagOverrides,
3892
+ withRepoRunLock,
3893
+ superviseLoop,
3894
+ createSuperviseRunQueue,
3895
+ createIngestHook,
3896
+ cmdRun: (o) => cmdRun(o),
3897
+ CliExitError,
3898
+ });
3899
+ }
3900
+ export const PROGRAM_HANDLERS = {
3901
+ cmdInit,
3902
+ cmdMigrate,
3903
+ cmdConstitution,
3904
+ cmdModels,
3905
+ cmdDoctor,
3906
+ cmdCost,
3907
+ cmdUsage,
3908
+ cmdStatus,
3909
+ cmdRuns,
3910
+ cmdKpis,
3911
+ cmdClassifierReport,
3912
+ cmdClassifierBacktestEntry,
3913
+ cmdGarden,
3914
+ cmdTui,
3915
+ cmdLogs,
3916
+ cmdFeedback,
3917
+ cmdFilingPreview,
3918
+ cmdTriage,
3919
+ cmdTriageAccept,
3920
+ cmdUnpark,
3921
+ cmdQueueMigrate,
3922
+ cmdQueueAdd,
3923
+ cmdQueueReconcile,
3924
+ cmdQueueList,
3925
+ cmdQueueClear,
3926
+ cmdHostedSmoke,
3927
+ cmdHostedRunner,
3928
+ cmdHostedQueue,
3929
+ cmdCheck,
3930
+ cmdShip,
3931
+ cmdRunIssue,
3932
+ cmdRunBrief,
3933
+ cmdLocalSmallDryRun,
3934
+ cmdLocalSmallOvernight,
3935
+ cmdLand,
3936
+ cmdResumeApproved,
3937
+ cmdRun,
3938
+ cmdProxy,
3939
+ cmdFactoryd,
3940
+ cmdDaemonStart,
3941
+ cmdDaemonStop,
3942
+ cmdDaemonStatus,
3943
+ cmdDaemonLogs,
3944
+ cmdSelfUpdateCheck,
3945
+ cmdWorktreeGc,
3946
+ cmdReset,
3947
+ cmdSupervise: runSupervise,
3948
+ cmdStop,
3949
+ cmdResume,
3950
+ parseIssueArg,
3951
+ getRepoRoot,
3952
+ getCliVersion,
3953
+ CliExitError,
3954
+ PREREQUISITES_TEXT,
3955
+ HELP_FOOTER_TEXT,
3956
+ };
3935
3957
  // ---------- main ----------
3936
3958
  export async function main(argv = process.argv, deps = {}) {
3937
3959
  if (argv.slice(2).length === 0) {
@@ -3949,431 +3971,7 @@ export async function main(argv = process.argv, deps = {}) {
3949
3971
  process.exitCode = staleExit;
3950
3972
  return;
3951
3973
  }
3952
- const program = new Command();
3953
- program
3954
- .name('factory')
3955
- .description('Multi-agent software factory with boss-worker-checker orchestration')
3956
- .version(getCliVersion())
3957
- .addHelpText('before', PREREQUISITES_TEXT)
3958
- .addHelpText('after', HELP_FOOTER_TEXT)
3959
- .option('--state-root <path>', 'Keep factory state and config outside the checkout: an absolute path, ~/..., or "auto" (~/.factory/<owner>/<name>); env FACTORY_STATE_ROOT');
3960
- program.hook('preAction', async () => {
3961
- const flag = program.opts().stateRoot;
3962
- if (flag === undefined && !process.env.FACTORY_STATE_ROOT?.trim())
3963
- return;
3964
- try {
3965
- applyStateRootOption(flag, await getRepoRoot());
3966
- }
3967
- catch (e) {
3968
- throw new CliExitError(`factory: ${e.message}`, 2);
3969
- }
3970
- });
3971
- program
3972
- .command('init')
3973
- .description('Set up .factory/ in this repo (config, constitution, queue file, .gitignore) and check model reachability')
3974
- .option('--force', 'Overwrite an existing .factory config, constitution, and .gitignore')
3975
- .action((opts) => cmdInit(opts));
3976
- program
3977
- .command('migrate')
3978
- .description('Upgrade .factory/ to the v2 layout (config, constitution.md, .gitignore, state/); --to-yaml converts config.json to config.yaml')
3979
- .option('--dry-run', 'Print planned changes without writing')
3980
- .option('--to-yaml', 'Convert .factory/config.json to .factory/config.yaml (removes the JSON only after a round-trip check)')
3981
- .action((opts) => cmdMigrate(opts));
3982
- program
3983
- .command('constitution')
3984
- .description('Manage product constitutions — .factory/constitution.md in this repo is the source of truth')
3985
- .option('--init [product]', 'Write .factory/constitution.md in this repo, scaffolded from the template (an optional name overrides the display name)')
3986
- .option('--list', 'List available constitutions with their checkers')
3987
- .option('--checkers', 'List every built-in checker name with a one-line description')
3988
- .option('--product <name>', 'Seed .factory/constitution.md from the bundled constitution for <name>; replaces an unmodified scaffold without --force')
3989
- .option('--force', 'With --init or --product, overwrite an existing .factory/constitution.md')
3990
- .action((opts) => cmdConstitution(opts));
3991
- program
3992
- .command('models')
3993
- .description('List models with their tiers, cost per 1M tokens, and availability')
3994
- .option('--doctor', 'Probe provider CLIs and env keys; report per-model reachability')
3995
- .action(cmdModels);
3996
- program
3997
- .command('doctor')
3998
- .description('Check your environment (git, agent install and sign-in, gh auth, GitHub token, npm, sandbox, .factory/)')
3999
- .option('--reconcile', 'Reap stale port leases, dead-run worktrees, stale issue claims, orphan sf-job-*/factory.managed containers, and orphan factory-* sbx VMs')
4000
- .option('--probe-inference', 'Also send one real prompt with claude -p on the host and in the sandbox to verify Claude auth (uses quota)')
4001
- .action((opts) => cmdDoctor(opts));
4002
- program
4003
- .command('cost')
4004
- .description('Show recorded model spend by model (--issue for one issue)')
4005
- .option('--issue <number>', 'Show per-entry detail for one issue')
4006
- .option('--json', 'Print one JSON object (schemaVersion 1): rows, perModel, total; unpriced cost is null')
4007
- .action((opts) => cmdCost(opts));
4008
- program
4009
- .command('usage')
4010
- .description('Report 5-hour subscription usage (falls back to a list-price estimate)')
4011
- .option('--json', 'Print one JSON object (schemaVersion 1) instead of text')
4012
- .action((opts) => cmdUsage({}, opts));
4013
- program
4014
- .command('status')
4015
- .description('Show active runs, the GitHub queue, provider health, and recent events')
4016
- .option('--kpis', 'Also show the full health KPIs and the effective config')
4017
- .option('--json', 'Print one JSON object (schemaVersion 1): repo, product, stop, breaker, active claims, update; --kpis is ignored')
4018
- .action((opts) => cmdStatus(opts));
4019
- program
4020
- .command('runs')
4021
- .description('Print per-issue run state from the local runs directory as one JSON object, newest first (read-only)')
4022
- .option('--json', 'Print one JSON object (schemaVersion 1) — required')
4023
- .option('--limit <n>', 'Show at most N runs (default 50)')
4024
- .option('--diffstat <issue>', "Print per-file added/deleted line counts for one issue's run branch vs origin/<default branch>")
4025
- .action(async (opts) => {
4026
- await cmdRuns(opts);
4027
- });
4028
- program
4029
- .command('kpis')
4030
- .description('Compute factory health KPIs and record a trend snapshot')
4031
- .option('--branch-prefix <prefix>', BRANCH_PREFIX_MATCH_OPTION_DESCRIPTION)
4032
- .action(async (opts) => {
4033
- await cmdKpis(opts);
4034
- });
4035
- const classifierCmd = program
4036
- .command('classifier')
4037
- .description('Inspect or backtest the shadow PR classifier track record');
4038
- classifierCmd
4039
- .command('report')
4040
- .description('Confusion table, rule-of-three slip bounds and model-vs-floor agreement from classifier-outcomes.jsonl')
4041
- .option('--json', 'Print one JSON object with the same numbers')
4042
- .action((opts) => cmdClassifierReport(opts));
4043
- classifierCmd
4044
- .command('backtest')
4045
- .description('Replay the floor and shadow classifier over merged factory PRs and print the outcome report')
4046
- .requiredOption('--since <date>', 'Only PRs merged on or after this date (YYYY-MM-DD)')
4047
- .option('--limit <n>', 'Classify at most N PRs (oldest merge first)')
4048
- .option('--labels <file>', 'CSV of hand labels `pr,class` (class: defect|clean|gated) that override heuristic outcomes')
4049
- .option('--max-cost <usd>', 'Stop before the next PR once estimated model spend reaches this many USD')
4050
- .option('--branch-prefix <prefix>', BRANCH_PREFIX_MATCH_OPTION_DESCRIPTION)
4051
- .option('--json', 'Print one JSON object instead of the table')
4052
- .action((opts) => cmdClassifierBacktestEntry(opts));
4053
- program
4054
- .command('garden')
4055
- .description('Read-only report of recurring failure patterns in local events, deduped against open issues by GitHub search')
4056
- .option('--report', 'Print recurring CHECK failure-signature clusters as markdown')
4057
- .option('--since <duration>', 'Only consider events from this window, e.g. 7d, 36h, 2w', DEFAULT_GARDEN_SINCE)
4058
- .option('--max-clusters <n>', 'Print at most this many clusters', String(DEFAULT_GARDEN_MAX_CLUSTERS))
4059
- .option('--json', 'Print the same cluster data as JSON')
4060
- .option('--out <file>', 'Write the report to this file instead of stdout')
4061
- .option('--only-new', 'Omit clusters an open issue already tracks; keep new and tracked: unknown clusters')
4062
- .action((opts) => cmdGarden(opts));
4063
- program
4064
- .command('tui')
4065
- .description('Live read-only view of the current run (q to quit)')
4066
- .option('--local-queue', 'Read .factory/queue instead of claiming issues from GitHub Issues')
4067
- .action((opts) => cmdTui(opts, deps.runTui ?? runTui));
4068
- program
4069
- .command('logs')
4070
- .description('Print pipeline events from .factory/events.ndjson (--follow to tail)')
4071
- .option('--follow', 'Keep watching for new events')
4072
- .option('--json', 'Emit one JSON object per line')
4073
- .option('--issue <number>', 'Only show events for this issue')
4074
- .action(async (opts) => {
4075
- const repoRoot = await getRepoRoot();
4076
- const paths = getFactoryPaths(repoRoot);
4077
- await cmdLogs(opts, { eventsFile: paths.events });
4078
- });
4079
- program
4080
- .command('feedback <pr-url>')
4081
- .description("File one factory issue (bug, factory:needs-triage) from a PR's human review findings")
4082
- .option('--note <text>', 'Use this as the issue title instead of the first finding')
4083
- .option('--repo <owner/name>', 'Repo to file the issue in (default: filing.repo, then the origin remote)')
4084
- .option('--include-local-paths', 'Include this host name and local design/log paths in the issue')
4085
- .option('--branch-prefix <prefix>', BRANCH_PREFIX_MATCH_OPTION_DESCRIPTION)
4086
- .action(cmdFeedback);
4087
- const filingCmd = program.command('filing').description('Inspect upstream factory reports (nothing is sent)');
4088
- filingCmd
4089
- .command('preview <run-id>')
4090
- .description('Print the exact redacted [factory-report] title and body that would be sent upstream; sends nothing')
4091
- .option('--branch-prefix <prefix>', BRANCH_PREFIX_MATCH_OPTION_DESCRIPTION)
4092
- .action((runId, opts) => cmdFilingPreview(runId, opts));
4093
- const triage = program
4094
- .command('triage')
4095
- .description('Have a model propose a queue from open issues into .factory/state/queue.proposed')
4096
- .option('--product <name>', 'Product constitution to scope triage')
4097
- .option('--model <id>', 'Model to triage with (any registered model, Claude or Codex)')
4098
- .action(cmdTriage);
4099
- triage
4100
- .command('accept')
4101
- .description('Validate and atomically promote .factory/state/queue.proposed to .factory/state/queue')
4102
- .option('--force', 'Skip validation and promote as-is')
4103
- .action(async (opts) => {
4104
- await cmdTriageAccept(opts);
4105
- });
4106
- program
4107
- .command('unpark <issue>')
4108
- .description('Unpark a factory:parked issue: remove factory:parked and re-queue it in its lane with a fresh order label (same path as `queue add`)')
4109
- .option('--lane <lane>', "Lane to re-queue into (default: the issue's existing factory:lane:* label)")
4110
- .option('--json', 'Print one JSON object (schemaVersion 1): ok, action, issue, lane, order, labelsBefore, labelsAfter')
4111
- .action(async (issue, opts) => {
4112
- await cmdUnpark(issue, opts);
4113
- });
4114
- const queue = program
4115
- .command('queue')
4116
- .description('Manage the GitHub-label work queue (list, add, clear, reconcile, migrate)');
4117
- queue
4118
- .command('migrate')
4119
- .description('Copy valid .factory/queue lane order into GitHub issue labels')
4120
- .option('--file <path>', 'Read the legacy queue from an explicit path instead of .factory/queue')
4121
- .option('--dry-run', 'Print the intended label updates without mutating GitHub')
4122
- .action((opts) => cmdQueueMigrate(opts));
4123
- queue
4124
- .command('add <lane> <issues...>')
4125
- .description('Queue explicit GitHub issues into a lane — applies factory:queued + factory:lane:<lane> (and an order label), creating any missing factory labels idempotently')
4126
- .option('--json', 'Print one JSON object (schemaVersion 1): ok, action, lane, issues[{number, outcome, order, labelsAdded}], labelsCreated')
4127
- .action(async (lane, issues, opts) => {
4128
- await cmdQueueAdd(lane, issues, opts);
4129
- });
4130
- queue
4131
- .command('reconcile')
4132
- .description('Report queued GitHub issues that conflict with Factory App admission state, with the repair-flow pointer for each')
4133
- .option('--lane <lane>', 'Only reconcile one lane')
4134
- .action(async (opts) => {
4135
- await cmdQueueReconcile(opts);
4136
- });
4137
- queue
4138
- .command('list')
4139
- .description('Print queued, in-progress and parked GitHub-queue issues per lane as one JSON object (read-only)')
4140
- .option('--json', 'Print one JSON object (schemaVersion 1) — required')
4141
- .option('--lane <lane>', 'Only list this lane (factory:lane:<lane>)')
4142
- .action(async (opts) => {
4143
- await cmdQueueList(opts);
4144
- });
4145
- queue
4146
- .command('clear')
4147
- .description('Empty the GitHub-backed queue: removes factory:queued and factory:order:* from every unclaimed queued issue (lane labels kept; claimed issues skipped). Preview with --dry-run; limit to one lane with --lane')
4148
- .option('--dry-run', 'Print the preview and exit 0 without changing any label')
4149
- .option('--yes', 'Confirm and remove the queue labels')
4150
- .option('--lane <lane>', 'Only clear queued issues in this lane (factory:lane:<lane>)')
4151
- .action(async (opts) => {
4152
- await cmdQueueClear(opts);
4153
- });
4154
- const hosted = program
4155
- .command('hosted')
4156
- .description('Experimental remote-runner execution (needs FACTORY_HOSTED_EXEC=1)');
4157
- hosted
4158
- .command('smoke')
4159
- .description('Local end-to-end hosted-exec smoke: create → lease → Docker run → result → cleanup')
4160
- .option('--repo <slug>', 'owner/repo to clone and run against', 'on-par/software-factory')
4161
- .option('--image <image>', 'Container image for the smoke run', 'node:20-alpine')
4162
- .action(async (opts) => {
4163
- await cmdHostedSmoke(opts);
4164
- });
4165
- hosted
4166
- .command('runner')
4167
- .description('Register capabilities with the local control plane and lease one compatible job, then exit')
4168
- .option('--url <url>', 'Control-plane base URL', 'http://127.0.0.1:8799')
4169
- .option('--runner-id <id>', 'Runner identity (default runner-<pid>)')
4170
- .option('--capabilities <csv>', 'Comma-separated capability list', 'git,node')
4171
- .option('--timeout <ms>', 'Bounded wait window for a compatible job, in ms', '30000')
4172
- .option('--poll-interval <ms>', 'Delay between poll attempts, in ms', '2000')
4173
- .option('--lease-ttl <ms>', 'Lease TTL, in ms', '300000')
4174
- .option('--heartbeat-interval <ms>', 'Expected heartbeat interval, in ms', '30000')
4175
- .action(async (opts) => {
4176
- await cmdHostedRunner(opts);
4177
- });
4178
- hosted
4179
- .command('queue')
4180
- .description('Queue one job to the local control plane, tail it to terminal, print the result')
4181
- .option('--url <url>', 'Control-plane base URL', 'http://127.0.0.1:8799')
4182
- .option('--repo <slug>', 'owner/repo the job runs against', 'on-par/software-factory')
4183
- .option('--task <text>', 'Opaque task payload')
4184
- .option('--capabilities <csv>', 'Comma-separated required capabilities', 'git,node')
4185
- .option('--authority <authority>', 'Required authority', 'repo:read')
4186
- .option('--timeout <ms>', 'Bounded wait for a terminal result, in ms', '120000')
4187
- .option('--poll-interval <ms>', 'Delay between summary polls, in ms', '1000')
4188
- .option('--job-id <id>', 'Explicit job id (default server-generated)')
4189
- .action(async (opts) => {
4190
- await cmdHostedQueue(opts);
4191
- });
4192
- program
4193
- .command('check <issue>')
4194
- .alias('ready')
4195
- .description('Check an issue before queuing: required fields and whether the size gate runs it as-is or would split it, plus an advisory INVEST report (read-only, optional --deep model review; exit 0 ready, 1 missing fields or ungradeable criteria, 2 invalid --deep output, 3 would split)')
4196
- .option('--json', 'Print one JSON object with fields, criteria findings, size verdict, INVEST findings, and reasons')
4197
- .option('--deep', 'Also call the triage-tier model once: preview the proposed split when the issue would split, otherwise suggest missing criteria and scope concerns (prints model id and cost; never files anything; exit 2 on invalid model output)')
4198
- .action(cmdCheck);
4199
- program
4200
- .command('ship <issue>')
4201
- .description('Plan → build → check one GitHub issue and open a ready-for-review PR; never merges (merge later with land)')
4202
- .option('--product <name>', 'Override active product constitution')
4203
- .option('--no-auto-rework', 'Disable automatic rework loop')
4204
- .option('--interactive', 'Pause before opening the PR and wait for approval from the TUI')
4205
- .option('--approve-plan', 'Pause after PLAN freezes the spec and wait for approval before BUILD')
4206
- .option('--no-sandbox', 'Disable the containment sandbox for agent runs (dangerous)')
4207
- .option('--branch-prefix <prefix>', BRANCH_PREFIX_OPTION_DESCRIPTION)
4208
- .action(async (issueNum, opts) => {
4209
- await cmdShip(parseIssueArg(issueNum), opts);
4210
- });
4211
- program
4212
- .command('run-issue <issue>')
4213
- .description('Like ship, but loads the issue first and exits 2 before any worktree or PR if it cannot be found')
4214
- .option('--product <name>', 'Override active product constitution')
4215
- .option('--no-auto-rework', 'Disable automatic rework loop')
4216
- .option('--interactive', 'Pause before opening the PR and wait for approval from the TUI')
4217
- .option('--approve-plan', 'Pause after PLAN freezes the spec and wait for approval before BUILD')
4218
- .option('--no-sandbox', 'Disable the containment sandbox for agent runs (dangerous)')
4219
- .option('--branch-prefix <prefix>', BRANCH_PREFIX_OPTION_DESCRIPTION)
4220
- .option('--run-children', 'File mode only: when the size gate files child issues, run them (slice mode never files issues)')
4221
- .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')
4222
- .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)')
4223
- .action(async (issueNum, opts) => {
4224
- await cmdRunIssue(parseIssueArg(issueNum), opts);
4225
- });
4226
- program
4227
- .command('run-brief <file>')
4228
- .description('Like run-issue, but the work comes from a local Markdown brief instead of a GitHub issue')
4229
- .option('--product <name>', 'Override active product constitution')
4230
- .option('--no-auto-rework', 'Disable automatic rework loop')
4231
- .option('--interactive', 'Pause before opening the PR and wait for approval from the TUI')
4232
- .option('--approve-plan', 'Pause after PLAN freezes the spec and wait for approval before BUILD')
4233
- .option('--no-sandbox', 'Disable the containment sandbox for agent runs (dangerous)')
4234
- .option('--workspace <dir>', 'Local-only: run PLAN/BUILD/CHECK in this caller-provided git workspace and disable PR creation, merging, queue polling, and GitHub mutations')
4235
- .option('--artifacts <dir>', 'Local-only: write a versioned benchmark artifact manifest (manifest.json, request.json, events.ndjson, diff.patch) to this directory; requires --workspace')
4236
- .option('--branch-prefix <prefix>', BRANCH_PREFIX_OPTION_DESCRIPTION)
4237
- .action(async (file, opts) => {
4238
- await cmdRunBrief(file, opts);
4239
- });
4240
- program
4241
- .command('local-small-dry-run <issue>')
4242
- .description('Create a bounded local-small step plan and first context pack without changing source files')
4243
- .option('--spec <path>', 'Frozen spec path; defaults to .factory/plans/issue-<n>.md')
4244
- .option('--output <path>', 'Artifact directory; defaults to .factory/local-small/issue-<n>')
4245
- .action(async (issueNum, opts) => {
4246
- await cmdLocalSmallDryRun(parseIssueArg(issueNum), opts);
4247
- });
4248
- program
4249
- .command('local-small-overnight')
4250
- .description('Process a curated local-small queue one issue at a time: review-only PRs, never merges, parks on ambiguity')
4251
- .option('--queue <path>', 'Curated queue file; defaults to .factory/local-small/overnight-queue')
4252
- .option('--state <path>', 'Resume-state file; defaults to .factory/local-small/overnight-state.json')
4253
- .action(async (opts) => {
4254
- await cmdLocalSmallOvernight(opts);
4255
- });
4256
- program
4257
- .command('land <issue>')
4258
- .description("Squash-merge the issue's open PR once CI is green, then remove its worktree (left open if CI fails or review is pending)")
4259
- .option('--branch-prefix <prefix>', BRANCH_PREFIX_MATCH_OPTION_DESCRIPTION)
4260
- .option('--allow-gated', 'Merge even when the issue or PR carries the no-auto-merge gate label (human override; writes an AUDIT line and a merge-gated-override event)')
4261
- .action(async (issueNum, opts) => {
4262
- await cmdLand(parseIssueArg(issueNum), opts);
4263
- });
4264
- program
4265
- .command('resume-approved')
4266
- .description(`Land open factory PRs (factory/*, plus legacy ${LEGACY_BRANCH_PREFIX}/*) whose review is now approved; skip the rest`)
4267
- .option('--branch-prefix <prefix>', BRANCH_PREFIX_MATCH_OPTION_DESCRIPTION)
4268
- .action(async (opts) => {
4269
- await cmdResumeApproved(opts);
4270
- });
4271
- program
4272
- .command('run')
4273
- .description('Claim queued issues (factory:queued labels) and ship them, lanes in parallel; merges only when auto-merge is on')
4274
- .option('--local-queue', 'Read .factory/queue instead of claiming issues from GitHub Issues')
4275
- .option('--auto-merge', 'Merge eligible PRs autonomously, overriding .factory/config.yaml and FACTORY_MERGE')
4276
- .option('--no-auto-merge', 'Never merge autonomously, overriding .factory/config.yaml and FACTORY_MERGE')
4277
- .option('--pr-classifier', 'Hold PRs whose review floor is B or C for a human (applies no-auto-merge), overriding .factory/config.json and FACTORY_PR_CLASSIFIER')
4278
- .option('--no-pr-classifier', 'Never run the PR classifier gate, overriding .factory/config.json and FACTORY_PR_CLASSIFIER')
4279
- .option('--usage-watch', 'Enforce the usage gate for this run, overriding .factory/config.yaml and FACTORY_USAGE_WATCH')
4280
- .option('--no-usage-watch', 'Skip the usage gate for this run, overriding .factory/config.yaml and FACTORY_USAGE_WATCH')
4281
- .option('--usage-threshold <fraction>', "Override .factory/config.yaml and FACTORY_STOP_AT for this run's usage threshold")
4282
- .option('--usage-poll <seconds>', "Override .factory/config.yaml and FACTORY_USAGE_POLL for this run's usage poll")
4283
- .option('--branch-prefix <prefix>', BRANCH_PREFIX_OPTION_DESCRIPTION)
4284
- .action((opts) => cmdRun(opts));
4285
- program
4286
- .command('proxy')
4287
- .description('Run the opt-in lane reverse proxy in the foreground (run and supervise start it themselves)')
4288
- .action(cmdProxy);
4289
- const daemonCmd = program
4290
- .command('daemon')
4291
- .description('Manage factoryd, the long-running background service for this user');
4292
- daemonCmd
4293
- .command('run')
4294
- .description('Run factoryd in the foreground: a localhost-only HTTP API over the repo registry')
4295
- .option('--port <n>', `Port to bind on 127.0.0.1 (default ${DEFAULT_FACTORYD_PORT})`)
4296
- .option('--registry <file>', 'Registry file to serve (default ~/.factory/registry.json)')
4297
- .option('--update-mode <mode>', 'Self-update channel: manual, stable (npm latest) or prerelease (npm next) (default: stable on npm installs, manual otherwise)')
4298
- .option('--auto-update', 'Deprecated: same as --update-mode stable')
4299
- .action((opts) => cmdFactoryd(opts));
4300
- const daemonCtl = (fn) => fn().catch((err) => {
4301
- if (err instanceof DaemonCtlError)
4302
- throw new CliExitError(err.message, err.code);
4303
- throw err;
4304
- });
4305
- daemonCmd
4306
- .command('start')
4307
- .description('Install and load the com.onpar.factoryd LaunchAgent (macOS only; KeepAlive, RunAtLoad)')
4308
- .action(() => daemonCtl(() => cmdDaemonStart(deps.daemon)));
4309
- daemonCmd
4310
- .command('stop')
4311
- .description('Unload the LaunchAgent (macOS only; plist stays installed)')
4312
- .action(() => daemonCtl(() => cmdDaemonStop(deps.daemon)));
4313
- daemonCmd
4314
- .command('status')
4315
- .description('Report factoryd pid, uptime, and attached repos')
4316
- .option('--json', 'Print one JSON object (schemaVersion 1) instead of text')
4317
- .action((opts) => daemonCtl(() => cmdDaemonStatus(deps.daemon, { json: opts.json })));
4318
- daemonCmd
4319
- .command('logs')
4320
- .description('Print or tail ~/.factory/daemon.log')
4321
- .option('-f, --follow', 'Keep tailing')
4322
- .option('-n, --lines <n>', 'Lines to print first (default 100)')
4323
- .action((opts) => daemonCtl(() => cmdDaemonLogs(opts, deps.daemon)));
4324
- program
4325
- .command('self-update')
4326
- .description('Check npm for a newer factory release (read-only; nothing is installed)')
4327
- .option('--check', 'Compare the running version with the npm dist-tag (required)')
4328
- .option('--json', 'Print one JSON object instead of text')
4329
- .option('--update-mode <mode>', 'Channel to check: stable (npm latest, default) or prerelease (npm next)')
4330
- .action(async (opts) => {
4331
- if (opts.check !== true)
4332
- throw new CliExitError('factory self-update: only --check is supported', 2);
4333
- process.exitCode = await daemonCtl(() => cmdSelfUpdateCheck(opts, deps.selfUpdate));
4334
- });
4335
- const worktreeCmd = program.command('worktree').description('Clean up stale factory worktrees (gc)');
4336
- worktreeCmd
4337
- .command('gc')
4338
- .description('Remove stale factory worktrees (merged/closed branches or older than TTL) and scrub credentials')
4339
- .option('--dry-run', 'Preview what would be removed without deleting anything')
4340
- .option('--ttl-days <n>', 'Override worktree.gcTtlDays from .factory/config.yaml (default 7)')
4341
- .option('--branch-prefix <prefix>', BRANCH_PREFIX_MATCH_OPTION_DESCRIPTION)
4342
- .action(cmdWorktreeGc);
4343
- program
4344
- .command('reset <issue...>')
4345
- .description("Remove an issue's local factory state (worktree, local branch, plan/phase files, rework history, logs, leases) so the next run starts fresh. A worktree or branch with uncommitted changes or unpushed commits is kept unless --force is passed. Do not run while that issue's lane is active. Preview with --dry-run")
4346
- .option('--dry-run', 'List every worktree, branch, file, history entry, log dir, claim and lease that would be removed, without changing anything')
4347
- .option('--force', 'Remove worktrees and branches even when they have uncommitted changes or unpushed commits')
4348
- .option('--branch-prefix <prefix>', BRANCH_PREFIX_MATCH_OPTION_DESCRIPTION)
4349
- .action(cmdReset);
4350
- program
4351
- .command('supervise')
4352
- .description('Unattended loop: wait for usage headroom, run the queue, repeat until it is empty')
4353
- .option('--now', 'Skip the initial headroom wait')
4354
- .option('--local-queue', 'Read .factory/queue instead of claiming issues from GitHub Issues')
4355
- .option('--auto-merge', 'Merge eligible PRs autonomously, overriding .factory/config.yaml and FACTORY_MERGE')
4356
- .option('--no-auto-merge', 'Never merge autonomously, overriding .factory/config.yaml and FACTORY_MERGE')
4357
- .option('--usage-watch', 'Enforce the usage gate for this run, overriding .factory/config.yaml and FACTORY_USAGE_WATCH')
4358
- .option('--no-usage-watch', 'Skip the usage gate for this run, overriding .factory/config.yaml and FACTORY_USAGE_WATCH')
4359
- .option('--usage-threshold <fraction>', "Override .factory/config.yaml and FACTORY_RESUME_AT for this run's usage threshold")
4360
- .option('--usage-poll <seconds>', "Override .factory/config.yaml and FACTORY_USAGE_POLL for this run's usage poll")
4361
- .option('--branch-prefix <prefix>', BRANCH_PREFIX_OPTION_DESCRIPTION)
4362
- .action(async (opts) => {
4363
- await cmdSupervise(opts);
4364
- });
4365
- program
4366
- .command('stop')
4367
- .description('Tell running lanes to halt after their current issue')
4368
- .option('--json', 'Print one JSON object (schemaVersion 1): action, repo, stopFlag before/after')
4369
- .action((opts) => cmdStop(opts));
4370
- program
4371
- .command('resume [issue]')
4372
- .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')
4373
- .option('--json', 'Print one JSON object (schemaVersion 1): action, repo, stopFlag before/after')
4374
- .option('--from <phase>', 'Resume a kept build at this phase (only: check)')
4375
- .action((issue, opts) => cmdResume(issue, opts));
4376
- applyHelpGroups(program);
3974
+ const program = buildProgram(PROGRAM_HANDLERS, deps);
4377
3975
  const previousLoaders = configLoaders;
4378
3976
  const previousGitOps = gitOps;
4379
3977
  const previousInternalOps = internalOps;