@on-par/factory-cli 0.9.11-next.9 → 0.9.11

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.
Files changed (175) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/base-health-ci.d.ts +16 -2
  3. package/dist/cli/base-health-ci.d.ts.map +1 -1
  4. package/dist/cli/base-health-ci.js +30 -5
  5. package/dist/cli/base-health-ci.js.map +1 -1
  6. package/dist/cli/base-hold.d.ts +2 -0
  7. package/dist/cli/base-hold.d.ts.map +1 -1
  8. package/dist/cli/base-hold.js +24 -3
  9. package/dist/cli/base-hold.js.map +1 -1
  10. package/dist/cli/cost-json.d.ts +5 -31
  11. package/dist/cli/cost-json.d.ts.map +1 -1
  12. package/dist/cli/cost-json.js +13 -2
  13. package/dist/cli/cost-json.js.map +1 -1
  14. package/dist/cli/daemon.d.ts +2 -15
  15. package/dist/cli/daemon.d.ts.map +1 -1
  16. package/dist/cli/daemon.js +5 -11
  17. package/dist/cli/daemon.js.map +1 -1
  18. package/dist/cli/doctor.d.ts.map +1 -1
  19. package/dist/cli/doctor.js +3 -1
  20. package/dist/cli/doctor.js.map +1 -1
  21. package/dist/cli/harness-check.d.ts.map +1 -1
  22. package/dist/cli/harness-check.js +8 -3
  23. package/dist/cli/harness-check.js.map +1 -1
  24. package/dist/cli/help-groups.d.ts.map +1 -1
  25. package/dist/cli/help-groups.js +16 -2
  26. package/dist/cli/help-groups.js.map +1 -1
  27. package/dist/cli/index.d.ts +29 -32
  28. package/dist/cli/index.d.ts.map +1 -1
  29. package/dist/cli/index.js +88 -174
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/cli/json-contract.d.ts +309 -0
  32. package/dist/cli/json-contract.d.ts.map +1 -0
  33. package/dist/cli/json-contract.js +211 -0
  34. package/dist/cli/json-contract.js.map +1 -0
  35. package/dist/cli/land-pr.d.ts +4 -4
  36. package/dist/cli/land-pr.d.ts.map +1 -1
  37. package/dist/cli/land-pr.js +18 -17
  38. package/dist/cli/land-pr.js.map +1 -1
  39. package/dist/cli/land-risk.d.ts +29 -0
  40. package/dist/cli/land-risk.d.ts.map +1 -0
  41. package/dist/cli/land-risk.js +92 -0
  42. package/dist/cli/land-risk.js.map +1 -0
  43. package/dist/cli/land-runtime.d.ts +2 -1
  44. package/dist/cli/land-runtime.d.ts.map +1 -1
  45. package/dist/cli/land-runtime.js +3 -0
  46. package/dist/cli/land-runtime.js.map +1 -1
  47. package/dist/cli/land.d.ts +2 -0
  48. package/dist/cli/land.d.ts.map +1 -1
  49. package/dist/cli/land.js +46 -4
  50. package/dist/cli/land.js.map +1 -1
  51. package/dist/cli/lane-environment-pause-error.d.ts +3 -0
  52. package/dist/cli/lane-environment-pause-error.d.ts.map +1 -1
  53. package/dist/cli/lane-environment-pause-error.js +4 -0
  54. package/dist/cli/lane-environment-pause-error.js.map +1 -1
  55. package/dist/cli/logs.d.ts +4 -0
  56. package/dist/cli/logs.d.ts.map +1 -1
  57. package/dist/cli/logs.js +6 -2
  58. package/dist/cli/logs.js.map +1 -1
  59. package/dist/cli/model-override-messages.d.ts +4 -0
  60. package/dist/cli/model-override-messages.d.ts.map +1 -0
  61. package/dist/cli/model-override-messages.js +15 -0
  62. package/dist/cli/model-override-messages.js.map +1 -0
  63. package/dist/cli/octokit.d.ts +3 -0
  64. package/dist/cli/octokit.d.ts.map +1 -1
  65. package/dist/cli/octokit.js +1 -1
  66. package/dist/cli/octokit.js.map +1 -1
  67. package/dist/cli/program-observe.d.ts.map +1 -1
  68. package/dist/cli/program-observe.js +11 -0
  69. package/dist/cli/program-observe.js.map +1 -1
  70. package/dist/cli/program-run-work.d.ts.map +1 -1
  71. package/dist/cli/program-run-work.js +12 -0
  72. package/dist/cli/program-run-work.js.map +1 -1
  73. package/dist/cli/queue-add-json.d.ts +3 -25
  74. package/dist/cli/queue-add-json.d.ts.map +1 -1
  75. package/dist/cli/queue-add-json.js.map +1 -1
  76. package/dist/cli/queue-list.d.ts +6 -24
  77. package/dist/cli/queue-list.d.ts.map +1 -1
  78. package/dist/cli/queue-list.js +6 -1
  79. package/dist/cli/queue-list.js.map +1 -1
  80. package/dist/cli/queue-preflight.d.ts +52 -0
  81. package/dist/cli/queue-preflight.d.ts.map +1 -0
  82. package/dist/cli/queue-preflight.js +358 -0
  83. package/dist/cli/queue-preflight.js.map +1 -0
  84. package/dist/cli/risk-policy.d.ts +8 -0
  85. package/dist/cli/risk-policy.d.ts.map +1 -0
  86. package/dist/cli/risk-policy.js +39 -0
  87. package/dist/cli/risk-policy.js.map +1 -0
  88. package/dist/cli/risk.d.ts +42 -0
  89. package/dist/cli/risk.d.ts.map +1 -0
  90. package/dist/cli/risk.js +103 -0
  91. package/dist/cli/risk.js.map +1 -0
  92. package/dist/cli/run-branch.d.ts +26 -0
  93. package/dist/cli/run-branch.d.ts.map +1 -0
  94. package/dist/cli/run-branch.js +42 -0
  95. package/dist/cli/run-branch.js.map +1 -0
  96. package/dist/cli/run-lane.d.ts +6 -1
  97. package/dist/cli/run-lane.d.ts.map +1 -1
  98. package/dist/cli/run-lane.js +52 -5
  99. package/dist/cli/run-lane.js.map +1 -1
  100. package/dist/cli/run-restack.d.ts +14 -0
  101. package/dist/cli/run-restack.d.ts.map +1 -0
  102. package/dist/cli/run-restack.js +21 -0
  103. package/dist/cli/run-restack.js.map +1 -0
  104. package/dist/cli/run.d.ts.map +1 -1
  105. package/dist/cli/run.js +19 -2
  106. package/dist/cli/run.js.map +1 -1
  107. package/dist/cli/runs-diffstat.d.ts +6 -25
  108. package/dist/cli/runs-diffstat.d.ts.map +1 -1
  109. package/dist/cli/runs-diffstat.js.map +1 -1
  110. package/dist/cli/runs.d.ts +4 -19
  111. package/dist/cli/runs.d.ts.map +1 -1
  112. package/dist/cli/runs.js +1 -1
  113. package/dist/cli/runs.js.map +1 -1
  114. package/dist/cli/self-update-stop.d.ts +7 -0
  115. package/dist/cli/self-update-stop.d.ts.map +1 -0
  116. package/dist/cli/self-update-stop.js +27 -0
  117. package/dist/cli/self-update-stop.js.map +1 -0
  118. package/dist/cli/ship-reports.d.ts +34 -0
  119. package/dist/cli/ship-reports.d.ts.map +1 -0
  120. package/dist/cli/ship-reports.js +54 -0
  121. package/dist/cli/ship-reports.js.map +1 -0
  122. package/dist/cli/ship-run-state.d.ts +5 -0
  123. package/dist/cli/ship-run-state.d.ts.map +1 -0
  124. package/dist/cli/ship-run-state.js +17 -0
  125. package/dist/cli/ship-run-state.js.map +1 -0
  126. package/dist/cli/ship-steps.d.ts +4 -47
  127. package/dist/cli/ship-steps.d.ts.map +1 -1
  128. package/dist/cli/ship-steps.js +22 -82
  129. package/dist/cli/ship-steps.js.map +1 -1
  130. package/dist/cli/ship.d.ts +2 -0
  131. package/dist/cli/ship.d.ts.map +1 -1
  132. package/dist/cli/ship.js +64 -67
  133. package/dist/cli/ship.js.map +1 -1
  134. package/dist/cli/slice-index.d.ts +16 -0
  135. package/dist/cli/slice-index.d.ts.map +1 -0
  136. package/dist/cli/slice-index.js +27 -0
  137. package/dist/cli/slice-index.js.map +1 -0
  138. package/dist/cli/status-json.d.ts +7 -28
  139. package/dist/cli/status-json.d.ts.map +1 -1
  140. package/dist/cli/status-json.js +3 -1
  141. package/dist/cli/status-json.js.map +1 -1
  142. package/dist/cli/status.d.ts.map +1 -1
  143. package/dist/cli/status.js +4 -1
  144. package/dist/cli/status.js.map +1 -1
  145. package/dist/cli/steer.d.ts +50 -0
  146. package/dist/cli/steer.d.ts.map +1 -0
  147. package/dist/cli/steer.js +145 -0
  148. package/dist/cli/steer.js.map +1 -0
  149. package/dist/cli/steerability-report.d.ts +9 -0
  150. package/dist/cli/steerability-report.d.ts.map +1 -0
  151. package/dist/cli/steerability-report.js +28 -0
  152. package/dist/cli/steerability-report.js.map +1 -0
  153. package/dist/cli/stop-resume-json.d.ts +2 -10
  154. package/dist/cli/stop-resume-json.d.ts.map +1 -1
  155. package/dist/cli/stop-resume-json.js +1 -0
  156. package/dist/cli/stop-resume-json.js.map +1 -1
  157. package/dist/cli/usage-json.d.ts +4 -13
  158. package/dist/cli/usage-json.d.ts.map +1 -1
  159. package/dist/cli/usage-json.js.map +1 -1
  160. package/dist/cli/worktree-reap.d.ts.map +1 -1
  161. package/dist/cli/worktree-reap.js +7 -1
  162. package/dist/cli/worktree-reap.js.map +1 -1
  163. package/package.json +9 -5
  164. package/schemas/cost.schema.json +114 -0
  165. package/schemas/daemon-status.schema.json +44 -0
  166. package/schemas/logs-line.schema.json +133 -0
  167. package/schemas/queue-add.schema.json +73 -0
  168. package/schemas/queue-list.schema.json +77 -0
  169. package/schemas/resume.schema.json +36 -0
  170. package/schemas/runs-diffstat.schema.json +81 -0
  171. package/schemas/runs.schema.json +75 -0
  172. package/schemas/status.schema.json +200 -0
  173. package/schemas/stop.schema.json +36 -0
  174. package/schemas/unpark.schema.json +57 -0
  175. package/schemas/usage.schema.json +28 -0
package/dist/cli/index.js CHANGED
@@ -10,15 +10,19 @@ import { LaneEnvironmentPauseError } from './lane-environment-pause-error.js';
10
10
  import { INVALID_BRANCH_PREFIX_MESSAGE, resolveBranchPrefixOption } from './branch-prefix.js';
11
11
  import { sweepLaneClaims, warnQueueDiagnostics } from './queue-startup.js';
12
12
  import { proposeTriageQueue } from './triage-open-prs.js';
13
+ import { makeQueuePreflightOps, preflightQueuedIssue } from './queue-preflight.js';
13
14
  import { acquirePortLease, aggregateCosts, appendKpiHistoryLine, applyRepoConfig, buildPhase, checkPhase, clearProxyState, computeHealthKpis, ConstitutionLoader, createDefaultWorkSourceRegistry, createLogger, createLaneProxy, defaultFindPortListeners, describeEffectiveConfig, detectPostMergeDefects, diagnoseModels, probeDeclaredHarnesses, estimateTrailingSpend, fetchDefectSources, fetchSubscriptionUsage, formatUsageReport, getConstitutionsDir, getFactoryPaths, GITHUB_ISSUE_SOURCE, hasUnresolvedPark, InvalidArtifactsDirError, InvalidWorkRequestInputError, InvalidWorkspaceError, isCommandAvailable, isProxyRunning, kpiBranchPrefixes, kpisToHistoryRecord, laneBaseUrl, laneEnv, laneHostLabel, loadFactoryConfigForRepo, loadModelsConfig, loadRepoConfig, loadRoutesConfig, LOCAL_BRIEF_SOURCE, joinClassifierOutcomes, mergeClassifierOutcomes, parseClassifierOutcomes, summarizeClassifierOutcomes, formatClassifierReport, mergedPrRefs, ModelRegistry, ModelRouter, parkReasonFor, parseKpiHistory, parseQueue, planPhase, readEvents, readPortLeases, readQueue, readUsage, reapOrphanProcesses, reconstructKpiEvents, recordLeasePgid, releasePortLease, renderKpiReport, renderKpiTrend, resolveDefectWindowDays, parseSizeGateMode, parseMaxSlices, SIZE_GATE_MODES, resolveEffectiveModelPins, resolveEnvironmentProxy, resolveArtifactsDir, resolveLocalOnlyPolicy, resolveMergePolicy, resolveSkipCI, resolveTimeouts, resolveUsageCap, resolveWatchdogPolicy, runAutoIngest, shipPhase, formatCostTotal, validateQueue, watchUsage, writeLocalRunReport, writeProxyState, } from '@on-par/factory-core';
14
- import { acquirePidFile, createCheckResumeStore, findSlicePlanComment, upsertSlicePlanComment, withSliceState, DEFAULT_GARDEN_MAX_CLUSTERS, DEFAULT_GARDEN_SINCE, capGardenClusters, clusterGarden, dedupGardenClusters, filterGardenOnlyNew, filterGardenWindow, markGardenTrackingUnknown, parseGardenDuration, readHarvestEvents, renderGardenJson, renderGardenReport, laneWorktreePath, migrateRepoConfigToYaml, REPO_CONFIG_YAML_HEADER, claudeConfigDirOverride, claudeKeychainService, cleanupWorktree, createDaemonLogSink, factoryBranchIssue, factoryBranchPrefixes, createFactorydServer, createRunRuntime, createShipExecutor, createGithubQueue, createLocalSmallDryRun, createOctokitFilingClient, createOctokitGreenPrClient, createOctokitQueueClient, daemonRuntimePaths, defaultRegistryPath, ensureDir, fetchHumanEventSourcesCached, FACTORY_RUNTIME_CONFIG_KEYS, findUnmergedGreenPrs, formatGcReport, gitFetch, listMicroVms, listOrphanContainers, logCost, logEvent, planQueueMigration, pollBlockedParks, readCosts, readGithubQueueSnapshot, reapCleanShipWorktree, reapLaneWorktree, reapOrphanContainers, reapOrphanMicroVm, releaseRuntimeFiles, releaseStaleClaims, resolveBranchPrefix, resolveEffectiveConfig, resolveExperimental, resolveLocalOnly, resolvePrClassifierPolicy, resolveReviewFloorRules, RunLockHeldError, runOvernightQueue, parseRemoteSlug, setupWorktree, shellEscape, listBuiltInCheckers, sweepWorktrees, watchChecks, withFileLock, withGitLock, withRunLock, writePortFile, buildTriagePrompt, parseTriageCandidates, recordLocalAuthFailure, laneSlug, } from '@on-par/factory-core/internal';
15
+ import { acquirePidFile, createCheckResumeStore, DEFAULT_GARDEN_MAX_CLUSTERS, DEFAULT_GARDEN_SINCE, capGardenClusters, clusterGarden, dedupGardenClusters, filterGardenOnlyNew, filterGardenWindow, markGardenTrackingUnknown, parseGardenDuration, readHarvestEvents, renderGardenJson, renderGardenReport, laneWorktreePath, migrateRepoConfigToYaml, REPO_CONFIG_YAML_HEADER, claudeConfigDirOverride, claudeKeychainService, cleanupWorktree, createDaemonLogSink, factoryBranchIssue, factoryBranchPrefixes, createFactorydServer, createRunRuntime, createShipExecutor, createGithubQueue, createLocalSmallDryRun, createOctokitFilingClient, createOctokitGreenPrClient, createOctokitQueueClient, daemonRuntimePaths, defaultRegistryPath, ensureDir, fetchHumanEventSourcesCached, FACTORY_RUNTIME_CONFIG_KEYS, findUnmergedGreenPrs, formatGcReport, gitFetch, listMicroVms, listOrphanContainers, logCost, logEvent, planQueueMigration, pollBlockedParks, readCosts, readGithubQueueSnapshot, reapCleanShipWorktree, reapLaneWorktree, reapOrphanContainers, reapOrphanMicroVm, releaseRuntimeFiles, releaseStaleClaims, resolveBranchPrefix, resolveEffectiveConfig, resolveExperimental, resolveLocalOnly, resolvePrClassifierPolicy, resolveReviewFloorRules, RunLockHeldError, runOvernightQueue, parseRemoteSlug, setupWorktree, shellEscape, listBuiltInCheckers, sweepWorktrees, watchChecks, withFileLock, withGitLock, withRunLock, writePortFile, buildTriagePrompt, parseTriageCandidates, recordLocalAuthFailure, laneSlug, formatStopMarker, readStopMarker, } from '@on-par/factory-core/internal';
16
+ import { formatSteerabilityReport } from '@on-par/factory-core/internal';
15
17
  import { runTui } from '@on-par/factory-tui';
18
+ import { clearSelfUpdateStop } from './self-update-stop.js';
16
19
  import { admissionStateReaderFor, runQueueReconcile } from './admission.js';
17
20
  import { cmdClassifierBacktest, ClassifierBacktestInputError, } from './classifier-backtest.js';
21
+ import { buildSteerabilityReport } from './steerability-report.js';
18
22
  import { runHarnessCheckCommand } from './harness-check.js';
19
- import { findOpenPRForIssue, landIssue } from './land.js';
23
+ import { landIssue } from './land.js';
20
24
  import { STOP_SET_MESSAGE } from './stop-sentinel.js';
21
- import { AwaitingReviewError, CiFailedError, CiUnverifiedError, getPullRequestLandState, LandConflictError, LandFailureError, MergeGatedError, } from './land-pr.js';
25
+ import { AwaitingReviewError, CiFailedError, CiUnverifiedError, LandConflictError, LandFailureError, MergeGatedError, } from './land-pr.js';
22
26
  import { setLandRuntime } from './land-runtime.js';
23
27
  import { cmdRun } from './run.js';
24
28
  import { shipIssue } from './ship.js';
@@ -31,6 +35,7 @@ import { buildCostJson } from './cost-json.js';
31
35
  import { buildQueueAddJson } from './queue-add-json.js';
32
36
  import { renderQueueAddLines, renderUnparkLines } from './queue-output.js';
33
37
  import { buildQueueListJson } from './queue-list.js';
38
+ import { cmdSteer } from './steer.js';
34
39
  import { appendUnparkEvent, runUnpark, UnparkError } from './unpark.js';
35
40
  import { buildRunsJson, parseRunsLimit } from './runs.js';
36
41
  import { buildRunDiffstatJson, NoRunError, parseDiffstatIssue } from './runs-diffstat.js';
@@ -46,6 +51,7 @@ import { cmdHostedSmoke } from './hosted.js';
46
51
  import { cmdHostedQueue } from './hosted-queue.js';
47
52
  import { cmdHostedRunner } from './hosted-runner.js';
48
53
  import { resolveFactoryCheckoutCommit, runFilingPreview } from './filing.js';
54
+ import { cmdRisk } from './risk.js';
49
55
  import { buildFeedbackDeps, resolveFeedbackRepo, runFeedback } from './feedback.js';
50
56
  import { ensureFactoryExcluded, writeSampleQueue } from './init-files.js';
51
57
  import { cmdLogs } from './logs.js';
@@ -875,7 +881,7 @@ async function cmdModels(opts = {}) {
875
881
  const byModel = new Map(diagnoses.map((d) => [d.model, d]));
876
882
  for (const m of registry.list()) {
877
883
  const tiers = registry.getTiers(m).join('/');
878
- const est = registry.estimateCost(m, 1_000_000, 1_000_000);
884
+ const est = registry.estimateCost(m, { inputTokens: 1_000_000, outputTokens: 1_000_000 }).cost;
879
885
  const cost = est === null ? 'unpriced' : `$${est.toFixed(2)}/M`;
880
886
  const d = byModel.get(m);
881
887
  const avail = d ? modelStatusIcon(d) : styleText('red', '❌');
@@ -888,6 +894,7 @@ async function cmdModels(opts = {}) {
888
894
  console.log(` ${tier}: ${models.join(' ')}`);
889
895
  }
890
896
  }
897
+ const CACHED_RATE_MARKER = ' (cached at full input rate)';
891
898
  async function cmdCost(opts = {}) {
892
899
  const repoRoot = await getRepoRoot();
893
900
  const paths = getFactoryPaths(repoRoot);
@@ -911,7 +918,7 @@ async function cmdCost(opts = {}) {
911
918
  }
912
919
  console.log(styleText('bold', `== Costs for issue ${opts.issue} ==`));
913
920
  for (const c of filtered) {
914
- console.log(` ${c.task} ${c.model} ${c.cost === null || c.cost === undefined ? 'unknown' : `$${c.cost.toFixed(4)}`}${c.failoverReason ? ` [failover: ${c.failoverReason}]` : ''}`);
921
+ console.log(` ${c.task} ${c.model} ${c.cost === null || c.cost === undefined ? 'unknown' : `$${c.cost.toFixed(4)}`}${c.failoverReason ? ` [failover: ${c.failoverReason}]` : ''}${c.cachedRateMissing === true ? CACHED_RATE_MARKER : ''}`);
915
922
  }
916
923
  const t = aggregateCosts(filtered).total;
917
924
  console.log(' ---');
@@ -924,7 +931,14 @@ async function cmdCost(opts = {}) {
924
931
  }
925
932
  const byModel = new Map();
926
933
  for (const c of costs) {
927
- const e = byModel.get(c.model) ?? { tasks: 0, priced: 0, pricedCount: 0, unpriced: 0, failovers: 0 };
934
+ const e = byModel.get(c.model) ?? {
935
+ tasks: 0,
936
+ priced: 0,
937
+ pricedCount: 0,
938
+ unpriced: 0,
939
+ failovers: 0,
940
+ cachedRateMissing: 0,
941
+ };
928
942
  e.tasks++;
929
943
  if (c.unpriced === true || c.cost === null || c.cost === undefined)
930
944
  e.unpriced++;
@@ -934,13 +948,16 @@ async function cmdCost(opts = {}) {
934
948
  }
935
949
  if (c.failoverReason)
936
950
  e.failovers++;
951
+ if (c.cachedRateMissing === true)
952
+ e.cachedRateMissing++;
937
953
  byModel.set(c.model, e);
938
954
  }
939
955
  console.log(styleText('bold', '== Cost Summary =='));
940
- for (const [model, { tasks, priced, pricedCount, unpriced, failovers }] of byModel) {
956
+ for (const [model, { tasks, priced, pricedCount, unpriced, failovers, cachedRateMissing }] of byModel) {
941
957
  const failoverSuffix = failovers > 0 ? ` (${failovers} failover${failovers === 1 ? '' : 's'})` : '';
942
958
  const modelCost = formatCostTotal(pricedCount === 0 && unpriced > 0 ? null : priced, unpriced);
943
- console.log(` ${model}: ${tasks} tasks, ${modelCost}${failoverSuffix}`);
959
+ const cachedSuffix = cachedRateMissing > 0 ? CACHED_RATE_MARKER : '';
960
+ console.log(` ${model}: ${tasks} tasks, ${modelCost}${failoverSuffix}${cachedSuffix}`);
944
961
  }
945
962
  const grand = aggregateCosts(costs).total;
946
963
  console.log(' ---');
@@ -956,6 +973,7 @@ async function cmdCheck(issueRaw, opts) {
956
973
  const repoConfig = configLoaders.loadRepoConfig(repoRoot);
957
974
  const effective = resolveEffectiveConfig(repoConfig);
958
975
  const router = new coreOps.ModelRouter(applyRepoConfig(configLoaders.loadModelsConfig(), repoConfig), configLoaders.loadRoutesConfig(), false, undefined, effective.allowExperimental, effective.localOnly);
976
+ router.setCodexFullAccessTasks?.(configLoaders.loadFactoryConfigForRepo(getFactoryPaths(repoRoot).config).sandbox.codexFullAccessTasks);
959
977
  router.setLocalAuthSink?.(recordLocalAuthFailure);
960
978
  runModel = createDeepModelRunner(router, repoRoot);
961
979
  }
@@ -1045,16 +1063,35 @@ function persistClassifierOutcomes(paths, events, sources, opts) {
1045
1063
  ensureDir(paths.state);
1046
1064
  writeFileSync(paths.classifierOutcomes, updated);
1047
1065
  }
1066
+ /** `factory risk <pr>` (#3385): the handler lives in ./risk.ts; this binds its runtime. */
1067
+ function runRisk(pr, opts = {}) {
1068
+ return cmdRisk(pr, opts, { getRepoRoot, getGitHubRepo, getOctokit, CliExitError, log: console.log });
1069
+ }
1048
1070
  async function cmdClassifierReport(opts = {}) {
1049
1071
  const repoRoot = await getRepoRoot();
1050
1072
  const paths = getFactoryPaths(repoRoot);
1051
1073
  const report = summarizeClassifierOutcomes(parseClassifierOutcomes(readTextFileOrEmpty(paths.classifierOutcomes)));
1074
+ const steerability = await buildSteerabilityReport(paths);
1052
1075
  if (opts.json) {
1053
- console.log(JSON.stringify(report));
1076
+ console.log(JSON.stringify({ ...report, steerability }));
1054
1077
  return;
1055
1078
  }
1056
1079
  for (const line of formatClassifierReport(report))
1057
1080
  console.log(line);
1081
+ console.log('');
1082
+ for (const line of formatSteerabilityReport(steerability))
1083
+ console.log(line);
1084
+ }
1085
+ async function cmdReport(opts = {}) {
1086
+ if (!opts.steerability)
1087
+ throw new CliExitError('factory: report requires --steerability', 2);
1088
+ const report = await buildSteerabilityReport(getFactoryPaths(await getRepoRoot()));
1089
+ if (opts.json) {
1090
+ console.log(JSON.stringify(report));
1091
+ return;
1092
+ }
1093
+ for (const line of formatSteerabilityReport(report))
1094
+ console.log(line);
1058
1095
  }
1059
1096
  async function cmdClassifierBacktestEntry(opts) {
1060
1097
  const branchPrefix = branchPrefixOrExit(opts.branchPrefix);
@@ -1064,6 +1101,7 @@ async function cmdClassifierBacktestEntry(opts) {
1064
1101
  const repoConfig = configLoaders.loadRepoConfig(repoRoot);
1065
1102
  const effective = resolveEffectiveConfig(repoConfig);
1066
1103
  const router = new coreOps.ModelRouter(applyRepoConfig(configLoaders.loadModelsConfig(), repoConfig), configLoaders.loadRoutesConfig(), false, undefined, effective.allowExperimental, effective.localOnly);
1104
+ router.setCodexFullAccessTasks?.(configLoaders.loadFactoryConfigForRepo(paths.config).sandbox.codexFullAccessTasks);
1067
1105
  router.setLocalAuthSink?.(recordLocalAuthFailure);
1068
1106
  try {
1069
1107
  await cmdClassifierBacktest({ ...opts, branchPrefix }, {
@@ -1315,6 +1353,7 @@ async function cmdTui(opts = {}, runTuiImpl = runTui) {
1315
1353
  const routesConfig = configLoaders.loadRoutesConfig();
1316
1354
  const effective = resolveEffectiveConfig(repoConfig);
1317
1355
  const router = new coreOps.ModelRouter(modelsConfig, routesConfig, false, undefined, effective.allowExperimental, effective.localOnly);
1356
+ router.setCodexFullAccessTasks?.(configLoaders.loadFactoryConfigForRepo(paths.config).sandbox.codexFullAccessTasks);
1318
1357
  router.setLocalAuthSink?.(recordLocalAuthFailure);
1319
1358
  const effectiveConfigLines = describeEffectiveConfig({
1320
1359
  router,
@@ -1537,7 +1576,11 @@ async function cmdShip(issueNum, opts) {
1537
1576
  catch (err) {
1538
1577
  if (err instanceof IssueSkippedError)
1539
1578
  return;
1540
- if (ghRepo && !(err instanceof InvalidWorkRequestInputError) && !(err instanceof LaneEnvironmentPauseError)) {
1579
+ // A --no-pr run keeps its worktree either way: inspecting it is the point.
1580
+ if (ghRepo &&
1581
+ opts.pr !== false &&
1582
+ !(err instanceof InvalidWorkRequestInputError) &&
1583
+ !(err instanceof LaneEnvironmentPauseError)) {
1541
1584
  await reapPrlessShipWorktree(issueNum, repoRoot, ghRepo, paths, branchPrefix);
1542
1585
  }
1543
1586
  // #2492: a pull request number is a usage error, not a failed ship.
@@ -1560,6 +1603,9 @@ async function cmdRunIssue(issueNum, opts) {
1560
1603
  const branchPrefix = branchPrefixOrExit(opts.branchPrefix);
1561
1604
  const sizeGate = sizeGateOrExit(rawSizeGate);
1562
1605
  const maxSlices = maxSlicesOrExit(rawMaxSlices);
1606
+ if (opts.pr === false && ((sizeGate !== undefined && sizeGate !== 'off') || runChildren)) {
1607
+ throw new CliExitError('factory: --no-pr runs with the size gate off — drop --size-gate/--run-children or drop --no-pr', 2);
1608
+ }
1563
1609
  await withRepoRunLock(paths, 'factory run-issue', async () => {
1564
1610
  const ghRepo = await getGitHubRepo();
1565
1611
  // Pre-flight: resolve the issue through the canonical work-request seam
@@ -1995,6 +2041,7 @@ async function cmdTriage(opts) {
1995
2041
  const routesConfig = configLoaders.loadRoutesConfig();
1996
2042
  const effective = resolveEffectiveConfig(repoConfig);
1997
2043
  const router = new coreOps.ModelRouter(modelsConfig, routesConfig, false, undefined, effective.allowExperimental, effective.localOnly);
2044
+ router.setCodexFullAccessTasks?.(configLoaders.loadFactoryConfigForRepo(paths.config).sandbox.codexFullAccessTasks);
1998
2045
  router.setLocalAuthSink?.(recordLocalAuthFailure);
1999
2046
  router.skipMissingHarnessClis(coreOps.isCommandAvailable);
2000
2047
  router.setCostSink((entry) => logCost(paths.costs, { ...entry, issue: '-' }));
@@ -2415,8 +2462,7 @@ async function cmdFactoryd(opts) {
2415
2462
  const previousUpdate = readUpdateState(updateFile);
2416
2463
  for (const stop of recoverStaleDrain(previousUpdate, (file) => {
2417
2464
  try {
2418
- unlinkSync(file);
2419
- return true;
2465
+ return clearSelfUpdateStop(file);
2420
2466
  }
2421
2467
  catch {
2422
2468
  return false;
@@ -2542,167 +2588,12 @@ export function laneQueueDeps(queue, lane) {
2542
2588
  countRemaining: async () => (await queue.list(lane)).length,
2543
2589
  };
2544
2590
  }
2591
+ export { preflightQueuedIssue };
2545
2592
  export function createQueuePreflightOps(octokit, owner, repo, opts = {}) {
2546
- return {
2547
- autoMergeStack: opts.autoMergeStack ?? false,
2548
- getPullRefs: async (prNumber) => {
2549
- const { data } = await octokit.rest.pulls.get({ owner, repo, pull_number: prNumber });
2550
- return { headRef: data.head.ref, baseRef: data.base.ref, headSha: data.head.sha };
2551
- },
2552
- expectedHeadRepoFullName: `${owner}/${repo}`,
2553
- findOpenPR: (issue) => findOpenPRForIssue(octokit, owner, repo, issue),
2554
- getLandState: (prNumber) => getPullRequestLandState(octokit, owner, repo, prNumber),
2593
+ return makeQueuePreflightOps(octokit, owner, repo, {
2555
2594
  watch: (ref) => internalOps.watchChecks({ octokit, owner, repo, ref }),
2556
- findSlicePlan: (issue) => findSlicePlanComment({ octokit, repo: `${owner}/${repo}`, issue }),
2557
- getPullState: async (prNumber) => {
2558
- const { data } = await octokit.rest.pulls.get({ owner, repo, pull_number: prNumber });
2559
- if (data.merged || data.merged_at)
2560
- return 'merged';
2561
- return data.state === 'open' ? 'open' : 'closed';
2562
- },
2563
- saveSlicePlan: async (issue, plan) => {
2564
- await upsertSlicePlanComment({ octokit, repo: `${owner}/${repo}`, issue, plan });
2565
- },
2566
- };
2567
- }
2568
- /** ADR-0147/ADR-0161: walk every pr-open slice PR bottom-up. Merged layers are recorded, a closed layer parks
2569
- * (naming the layers above it), and the lowest open layer defers, or is adopted when stack auto-merge is on and
2570
- * its base is main with green CI on the rebased head. Returns undefined to fall through to the Closes-PR
2571
- * preflight only when no slice PR is still open. */
2572
- async function preflightSlice(issue, deps) {
2573
- let lookup;
2574
- try {
2575
- lookup = await deps.findSlicePlan(issue);
2576
- }
2577
- catch (err) {
2578
- return { kind: 'park', reason: `slice plan lookup failed: ${errorDetail(err)}` };
2579
- }
2580
- if (lookup.status === 'invalid') {
2581
- return {
2582
- kind: 'park',
2583
- reason: `slice plan comment ${lookup.commentId} on #${issue} is unreadable (${lookup.error})`,
2584
- };
2585
- }
2586
- if (lookup.status === 'not-found')
2587
- return undefined;
2588
- const { plan } = lookup;
2589
- const n = plan.slices.length;
2590
- const open = plan.slices.filter((s) => s.state === 'pr-open');
2591
- if (open.length === 0)
2592
- return undefined;
2593
- let next = plan;
2594
- let lastMerged;
2595
- const save = async () => {
2596
- if (lastMerged === undefined)
2597
- return undefined;
2598
- try {
2599
- await deps.saveSlicePlan(issue, next);
2600
- }
2601
- catch (err) {
2602
- return {
2603
- kind: 'park',
2604
- reason: `${lastMerged.label} PR #${lastMerged.pr} merged but updating the slice plan failed: ${errorDetail(err)}`,
2605
- };
2606
- }
2607
- return undefined;
2608
- };
2609
- for (const [at, slice] of open.entries()) {
2610
- const label = `slice ${slice.index}/${n}`;
2611
- if (slice.prNumber === undefined) {
2612
- return { kind: 'park', reason: `${label} of #${issue} is pr-open but the slice plan records no PR number` };
2613
- }
2614
- const pr = slice.prNumber;
2615
- let state;
2616
- try {
2617
- state = await deps.getPullState(pr);
2618
- }
2619
- catch (err) {
2620
- return { kind: 'park', reason: `${label} PR #${pr} state lookup failed: ${errorDetail(err)}` };
2621
- }
2622
- if (state === 'merged') {
2623
- next = withSliceState(next, slice.index, 'merged', pr);
2624
- lastMerged = { label, pr };
2625
- continue;
2626
- }
2627
- if (state === 'closed') {
2628
- const above = open.slice(at + 1);
2629
- const suffix = above.length > 0
2630
- ? `; layers above it: ${above.map((a) => `slice ${a.index}/${n} (#${a.prNumber})`).join(', ')}`
2631
- : '';
2632
- return { kind: 'park', reason: `${label} PR #${pr} was closed without merging${suffix}` };
2633
- }
2634
- const saved = await save();
2635
- if (saved)
2636
- return saved;
2637
- if (!deps.autoMergeStack)
2638
- return { kind: 'defer' };
2639
- let refs;
2640
- try {
2641
- refs = await deps.getPullRefs(pr);
2642
- }
2643
- catch (err) {
2644
- return { kind: 'park', reason: `${label} PR #${pr} ref lookup failed: ${errorDetail(err)}` };
2645
- }
2646
- // GitHub has not retargeted the layer to main yet: the lower layer is still unmerged.
2647
- if (refs.baseRef !== 'main')
2648
- return { kind: 'defer' };
2649
- let outcome;
2650
- try {
2651
- outcome = await deps.watch(refs.headSha);
2652
- }
2653
- catch (err) {
2654
- return { kind: 'park', reason: `CI watch for ${label} PR #${pr} failed: ${errorDetail(err)}` };
2655
- }
2656
- if (outcome === 'failure')
2657
- return { kind: 'park', reason: `CI for ${label} PR #${pr} ended failure` };
2658
- if (outcome !== 'success')
2659
- return { kind: 'defer' };
2660
- return { kind: 'adopt', branch: refs.headRef, stackLayer: { index: slice.index, count: n, prNumber: pr } };
2661
- }
2662
- return (await save()) ?? undefined;
2663
- }
2664
- /** Resolve queued work from GitHub evidence without consuming model or worktree resources. */
2665
- export async function preflightQueuedIssue(issue, deps) {
2666
- if (issue.labels.includes('factory:in-progress'))
2667
- return { kind: 'defer' };
2668
- const slice = await preflightSlice(issue.number, deps);
2669
- if (slice)
2670
- return slice;
2671
- let pr;
2672
- try {
2673
- pr = await deps.findOpenPR(issue.number);
2674
- }
2675
- catch (err) {
2676
- return { kind: 'park', reason: `PR lookup failed: ${errorDetail(err)}` };
2677
- }
2678
- if (!pr)
2679
- return { kind: 'build' };
2680
- if (pr.headRepoFullName !== deps.expectedHeadRepoFullName) {
2681
- return { kind: 'park', reason: `PR #${pr.number} head is not in ${deps.expectedHeadRepoFullName}` };
2682
- }
2683
- let state;
2684
- try {
2685
- state = await deps.getLandState(pr.number);
2686
- }
2687
- catch (err) {
2688
- return { kind: 'park', reason: `PR #${pr.number} merge-state lookup failed: ${errorDetail(err)}` };
2689
- }
2690
- if (state.isDraft !== false) {
2691
- return { kind: 'park', reason: `PR #${pr.number} is draft or its draft state is unavailable` };
2692
- }
2693
- if (!state.mergeStateStatus || state.mergeStateStatus === 'UNKNOWN') {
2694
- return { kind: 'park', reason: `PR #${pr.number} merge state is unavailable` };
2695
- }
2696
- let outcome;
2697
- try {
2698
- outcome = await deps.watch(pr.headSha);
2699
- }
2700
- catch (err) {
2701
- return { kind: 'park', reason: `CI watch for PR #${pr.number} failed: ${errorDetail(err)}` };
2702
- }
2703
- if (outcome !== 'success')
2704
- return { kind: 'park', reason: `CI for PR #${pr.number} ended ${outcome}` };
2705
- return { kind: 'adopt', branch: pr.branch };
2595
+ ...opts,
2596
+ });
2706
2597
  }
2707
2598
  export async function planRunLanes(input) {
2708
2599
  if (input.localQueue) {
@@ -2831,7 +2722,7 @@ export async function cmdResumeApproved(opts = {}) {
2831
2722
  }
2832
2723
  const USAGE_UNAVAILABLE_LINE = '[factory] supervise: usage signal unavailable — proceeding without resume gate (set FACTORY_USAGE_ESTIMATOR=1 to gate on the heuristic)';
2833
2724
  export async function superviseLoop(deps) {
2834
- const { cap, resumeAt, pollMs, watch = true, watchSource = 'default', repoConfigLabel, resumeAtSource = 'default', pollSource = 'default', estimator = false, stopFile, eventsFile, now, runQueue, readUsageFn = () => readUsage({ cap, estimator }), pathExists = existsSync, clearStop = (path) => rmSync(path, { force: true }), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), emitEvent = logEvent, writeLine = (line) => console.log(line), ingest, } = deps;
2725
+ const { cap, resumeAt, pollMs, watch = true, watchSource = 'default', repoConfigLabel, resumeAtSource = 'default', pollSource = 'default', estimator = false, stopFile, eventsFile, now, runQueue, readUsageFn = () => readUsage({ cap, estimator }), pathExists = existsSync, clearStop = (path) => rmSync(path, { force: true }), readStopMarker: readMarker = readStopMarker, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), emitEvent = logEvent, writeLine = (line) => console.log(line), ingest, } = deps;
2835
2726
  let cycle = 0;
2836
2727
  if (resumeAtSource === 'flag') {
2837
2728
  const detail = 'usage watchdog threshold set by flag: --usage-threshold';
@@ -2879,7 +2770,15 @@ export async function superviseLoop(deps) {
2879
2770
  }
2880
2771
  }
2881
2772
  emitEvent(eventsFile, 'resumed', 'usage', `supervise cycle ${cycle}: trailing usage (${source}) at ${Math.round(pct * 100)}% of cap — starting run`);
2882
- clearStop(stopFile);
2773
+ const marker = readMarker(stopFile);
2774
+ if (marker?.source === 'usage-watchdog') {
2775
+ clearStop(stopFile);
2776
+ }
2777
+ else if (marker) {
2778
+ writeLine(`[factory] supervise: STOP set by ${marker.source}${marker.at ? ` at ${marker.at}` : ''}; waiting for factory resume`);
2779
+ while (pathExists(stopFile))
2780
+ await sleep(pollMs);
2781
+ }
2883
2782
  if (ingest) {
2884
2783
  const appended = await ingest();
2885
2784
  if (appended > 0)
@@ -2958,7 +2857,7 @@ function logEventQuietly(eventsFile, type, msg) {
2958
2857
  /** Creates the STOP file in one exclusive open (no check-then-write race). Returns false when it already existed. */
2959
2858
  function createStopFile(stopFile) {
2960
2859
  try {
2961
- writeFileSync(stopFile, '', { flag: 'wx' });
2860
+ writeFileSync(stopFile, formatStopMarker('manual'), { flag: 'wx' });
2962
2861
  return true;
2963
2862
  }
2964
2863
  catch (err) {
@@ -2993,6 +2892,18 @@ export async function cmdStop(opts = {}) {
2993
2892
  }
2994
2893
  console.log(STOP_SET_MESSAGE);
2995
2894
  }
2895
+ /** `factory steer <ref>` (#3401): the handler lives in ./steer.ts; this binds its runtime. */
2896
+ function runSteerCommand(ref, opts = {}) {
2897
+ return cmdSteer(ref, opts, {
2898
+ getRepoRoot,
2899
+ gitUserName: async () => (await shellOps.exec('git config user.name')).stdout,
2900
+ env: process.env,
2901
+ now: () => new Date().toISOString(),
2902
+ CliExitError,
2903
+ errorDetail,
2904
+ log: console.log,
2905
+ });
2906
+ }
2996
2907
  export async function cmdResume(issue, opts = {}) {
2997
2908
  if (issue === undefined && opts.from !== undefined) {
2998
2909
  throw new CliExitError('factory: resume --from needs an issue number: factory resume <issue> --from check', 2);
@@ -3158,6 +3069,8 @@ export const PROGRAM_HANDLERS = {
3158
3069
  cmdRuns,
3159
3070
  cmdKpis,
3160
3071
  cmdClassifierReport,
3072
+ cmdReport,
3073
+ cmdRisk: runRisk,
3161
3074
  cmdClassifierBacktestEntry,
3162
3075
  cmdGarden,
3163
3076
  cmdTui,
@@ -3196,6 +3109,7 @@ export const PROGRAM_HANDLERS = {
3196
3109
  cmdSupervise: runSupervise,
3197
3110
  cmdStop,
3198
3111
  cmdResume,
3112
+ cmdSteer: runSteerCommand,
3199
3113
  parseIssueArg,
3200
3114
  getRepoRoot,
3201
3115
  getCliVersion,