@on-par/factory-cli 0.9.8-next.9 → 0.9.9-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/cli/agent-job.d.ts +29 -0
  2. package/dist/cli/agent-job.d.ts.map +1 -0
  3. package/dist/cli/agent-job.js +63 -0
  4. package/dist/cli/agent-job.js.map +1 -0
  5. package/dist/cli/doctor-harness.d.ts +11 -0
  6. package/dist/cli/doctor-harness.d.ts.map +1 -1
  7. package/dist/cli/doctor-harness.js +43 -1
  8. package/dist/cli/doctor-harness.js.map +1 -1
  9. package/dist/cli/doctor-opencode-probe.d.ts +22 -0
  10. package/dist/cli/doctor-opencode-probe.d.ts.map +1 -0
  11. package/dist/cli/doctor-opencode-probe.js +110 -0
  12. package/dist/cli/doctor-opencode-probe.js.map +1 -0
  13. package/dist/cli/doctor.d.ts +30 -8
  14. package/dist/cli/doctor.d.ts.map +1 -1
  15. package/dist/cli/doctor.js +114 -44
  16. package/dist/cli/doctor.js.map +1 -1
  17. package/dist/cli/first-run.d.ts +3 -1
  18. package/dist/cli/first-run.d.ts.map +1 -1
  19. package/dist/cli/first-run.js +5 -2
  20. package/dist/cli/first-run.js.map +1 -1
  21. package/dist/cli/github-stub.js +1 -1
  22. package/dist/cli/github-stub.js.map +1 -1
  23. package/dist/cli/harness-check.d.ts +27 -0
  24. package/dist/cli/harness-check.d.ts.map +1 -0
  25. package/dist/cli/harness-check.js +58 -0
  26. package/dist/cli/harness-check.js.map +1 -0
  27. package/dist/cli/help-groups.d.ts.map +1 -1
  28. package/dist/cli/help-groups.js +2 -1
  29. package/dist/cli/help-groups.js.map +1 -1
  30. package/dist/cli/host-slot.d.ts +5 -0
  31. package/dist/cli/host-slot.d.ts.map +1 -0
  32. package/dist/cli/host-slot.js +9 -0
  33. package/dist/cli/host-slot.js.map +1 -0
  34. package/dist/cli/index.d.ts +21 -18
  35. package/dist/cli/index.d.ts.map +1 -1
  36. package/dist/cli/index.js +66 -61
  37. package/dist/cli/index.js.map +1 -1
  38. package/dist/cli/program-advanced.d.ts.map +1 -1
  39. package/dist/cli/program-advanced.js +19 -0
  40. package/dist/cli/program-advanced.js.map +1 -1
  41. package/dist/cli/program-run-work.js +2 -2
  42. package/dist/cli/program-run-work.js.map +1 -1
  43. package/dist/cli/program-setup.d.ts.map +1 -1
  44. package/dist/cli/program-setup.js +10 -1
  45. package/dist/cli/program-setup.js.map +1 -1
  46. package/dist/cli/queue-add-json.d.ts +3 -1
  47. package/dist/cli/queue-add-json.d.ts.map +1 -1
  48. package/dist/cli/queue-add-json.js +1 -0
  49. package/dist/cli/queue-add-json.js.map +1 -1
  50. package/dist/cli/queue-output.d.ts +9 -0
  51. package/dist/cli/queue-output.d.ts.map +1 -0
  52. package/dist/cli/queue-output.js +38 -0
  53. package/dist/cli/queue-output.js.map +1 -0
  54. package/dist/cli/queue-startup.d.ts +8 -0
  55. package/dist/cli/queue-startup.d.ts.map +1 -0
  56. package/dist/cli/queue-startup.js +27 -0
  57. package/dist/cli/queue-startup.js.map +1 -0
  58. package/dist/cli/run-lane.d.ts +11 -1
  59. package/dist/cli/run-lane.d.ts.map +1 -1
  60. package/dist/cli/run-lane.js +27 -2
  61. package/dist/cli/run-lane.js.map +1 -1
  62. package/dist/cli/run.d.ts +1 -0
  63. package/dist/cli/run.d.ts.map +1 -1
  64. package/dist/cli/run.js +17 -2
  65. package/dist/cli/run.js.map +1 -1
  66. package/dist/cli/self-update-apply.d.ts.map +1 -1
  67. package/dist/cli/self-update-apply.js +24 -2
  68. package/dist/cli/self-update-apply.js.map +1 -1
  69. package/dist/cli/self-update.d.ts +4 -0
  70. package/dist/cli/self-update.d.ts.map +1 -1
  71. package/dist/cli/self-update.js +1 -1
  72. package/dist/cli/self-update.js.map +1 -1
  73. package/dist/cli/ship.d.ts +4 -3
  74. package/dist/cli/ship.d.ts.map +1 -1
  75. package/dist/cli/ship.js +21 -10
  76. package/dist/cli/ship.js.map +1 -1
  77. package/dist/cli/status-host-slots.d.ts +4 -0
  78. package/dist/cli/status-host-slots.d.ts.map +1 -0
  79. package/dist/cli/status-host-slots.js +17 -0
  80. package/dist/cli/status-host-slots.js.map +1 -0
  81. package/dist/cli/status.d.ts +5 -0
  82. package/dist/cli/status.d.ts.map +1 -1
  83. package/dist/cli/status.js +16 -5
  84. package/dist/cli/status.js.map +1 -1
  85. package/dist/cli/stop-sentinel.d.ts +4 -0
  86. package/dist/cli/stop-sentinel.d.ts.map +1 -1
  87. package/dist/cli/stop-sentinel.js +11 -0
  88. package/dist/cli/stop-sentinel.js.map +1 -1
  89. package/dist/cli/supervise.d.ts +5 -0
  90. package/dist/cli/supervise.d.ts.map +1 -1
  91. package/dist/cli/supervise.js +1 -0
  92. package/dist/cli/supervise.js.map +1 -1
  93. package/dist/cli/triage-open-prs.d.ts +10 -0
  94. package/dist/cli/triage-open-prs.d.ts.map +1 -0
  95. package/dist/cli/triage-open-prs.js +38 -0
  96. package/dist/cli/triage-open-prs.js.map +1 -0
  97. package/dist/cli/update-state.d.ts +2 -0
  98. package/dist/cli/update-state.d.ts.map +1 -1
  99. package/dist/cli/update-state.js +14 -0
  100. package/dist/cli/update-state.js.map +1 -1
  101. package/dist/cli/worktree-reap.d.ts +31 -0
  102. package/dist/cli/worktree-reap.d.ts.map +1 -0
  103. package/dist/cli/worktree-reap.js +72 -0
  104. package/dist/cli/worktree-reap.js.map +1 -0
  105. package/package.json +3 -3
package/dist/cli/index.js CHANGED
@@ -7,12 +7,16 @@ import { arch, hostname as osHostname, release, platform, userInfo } from 'node:
7
7
  import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
8
8
  import { promisify, styleText } from 'node:util';
9
9
  import { INVALID_BRANCH_PREFIX_MESSAGE, resolveBranchPrefixOption } from './branch-prefix.js';
10
- import { acquirePortLease, aggregateCosts, appendKpiHistoryLine, applyRepoConfig, buildPhase, checkPhase, clearProxyState, computeHealthKpis, ConstitutionLoader, createDefaultWorkSourceRegistry, createLogger, createLaneProxy, defaultFindPortListeners, describeEffectiveConfig, detectPostMergeDefects, diagnoseModels, probeDeclaredHarnesses, estimateTrailingSpend, fetchDefectSources, fetchHumanEventSources, 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, resolveSandboxPolicy, resolveSkipCI, resolveTimeouts, resolveUsageCap, resolveWatchdogPolicy, runAutoIngest, shipPhase, formatCostTotal, validateQueue, watchUsage, writeLocalRunReport, writeProxyState, } from '@on-par/factory-core';
11
- 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, FACTORY_RUNTIME_CONFIG_KEYS, findUnmergedGreenPrs, formatGcReport, gitFetch, listMicroVms, listOrphanContainers, logCost, logEvent, planQueueMigration, pollBlockedParks, readCosts, readGithubQueueSnapshot, reapLaneWorktree, reapOrphanContainers, reapOrphanMicroVm, releaseRuntimeFiles, releaseStaleClaims, resolveBranchPrefix, resolveEffectiveConfig, worktreeSandboxFor, resolveExperimental, resolveLocalOnly, resolvePrClassifierPolicy, resolveReviewFloorRules, RunLockHeldError, runOvernightQueue, parseRemoteSlug, setupWorktree, shellEscape, listBuiltInCheckers, sweepWorktrees, watchChecks, withFileLock, withGitLock, withRunLock, writePortFile, buildTriagePrompt, normalizeTriageQueue, parseTriageCandidates, recordLocalAuthFailure, } from '@on-par/factory-core/internal';
10
+ import { sweepLaneClaims, warnQueueDiagnostics } from './queue-startup.js';
11
+ import { proposeTriageQueue } from './triage-open-prs.js';
12
+ import { acquirePortLease, aggregateCosts, appendKpiHistoryLine, applyRepoConfig, buildPhase, checkPhase, clearProxyState, computeHealthKpis, ConstitutionLoader, createDefaultWorkSourceRegistry, createLogger, createLaneProxy, defaultFindPortListeners, describeEffectiveConfig, detectPostMergeDefects, diagnoseModels, probeDeclaredHarnesses, estimateTrailingSpend, fetchDefectSources, fetchHumanEventSources, 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';
13
+ 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, 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, } from '@on-par/factory-core/internal';
12
14
  import { runTui } from '@on-par/factory-tui';
13
15
  import { admissionStateReaderFor, runQueueReconcile } from './admission.js';
14
16
  import { cmdClassifierBacktest, ClassifierBacktestInputError, } from './classifier-backtest.js';
17
+ import { runHarnessCheckCommand } from './harness-check.js';
15
18
  import { findOpenPRForIssue, landIssue } from './land.js';
19
+ import { STOP_SET_MESSAGE } from './stop-sentinel.js';
16
20
  import { AwaitingReviewError, CiFailedError, CiUnverifiedError, getPullRequestLandState, LandConflictError, LandFailureError, MergeGatedError, } from './land-pr.js';
17
21
  import { setLandRuntime } from './land-runtime.js';
18
22
  import { cmdRun } from './run.js';
@@ -24,6 +28,7 @@ import { runQueueClear } from './queue-clear.js';
24
28
  import { buildProgram } from './program.js';
25
29
  import { buildCostJson } from './cost-json.js';
26
30
  import { buildQueueAddJson } from './queue-add-json.js';
31
+ import { renderQueueAddLines, renderUnparkLines } from './queue-output.js';
27
32
  import { buildQueueListJson } from './queue-list.js';
28
33
  import { appendUnparkEvent, runUnpark, UnparkError } from './unpark.js';
29
34
  import { buildRunsJson, parseRunsLimit } from './runs.js';
@@ -50,6 +55,7 @@ import { DeepCheckError, createDeepModelRunner, runIssueCheck } from './ready-ch
50
55
  import { childRunSucceeded, formatChildRunSummary, runChildrenInOrder } from './run-children.js';
51
56
  import { readRunFlagOverrides } from './run-flags.js';
52
57
  import { runStalenessGuard } from './staleness.js';
58
+ import { gcWorktreeSandbox, reapParkedLane, reapPrlessShip } from './worktree-reap.js';
53
59
  const DEFAULT_CONFIG_LOADERS = {
54
60
  loadModelsConfig,
55
61
  loadRoutesConfig,
@@ -96,6 +102,7 @@ const DEFAULT_INTERNAL_OPS = {
96
102
  createLocalSmallDryRun,
97
103
  readCosts,
98
104
  sweepWorktrees,
105
+ reapCleanShipWorktree,
99
106
  releaseStaleClaims,
100
107
  pollBlockedParks,
101
108
  findUnmergedGreenPrs,
@@ -114,6 +121,7 @@ function resolveInternalOps(overrides, base = internalOps) {
114
121
  createLocalSmallDryRun: overrides.createLocalSmallDryRun ?? base.createLocalSmallDryRun,
115
122
  readCosts: overrides.readCosts ?? base.readCosts,
116
123
  sweepWorktrees: overrides.sweepWorktrees ?? base.sweepWorktrees,
124
+ reapCleanShipWorktree: overrides.reapCleanShipWorktree ?? base.reapCleanShipWorktree,
117
125
  releaseStaleClaims: overrides.releaseStaleClaims ?? base.releaseStaleClaims,
118
126
  pollBlockedParks: overrides.pollBlockedParks ?? base.pollBlockedParks,
119
127
  findUnmergedGreenPrs: overrides.findUnmergedGreenPrs ?? base.findUnmergedGreenPrs,
@@ -414,7 +422,7 @@ async function cmdInit(opts = {}) {
414
422
  // Doctor-style validation (policy=auto, N models reachable) — informational, never fails init.
415
423
  const modelsConfig = applyRepoConfig(configLoaders.loadModelsConfig(), configLoaders.loadRepoConfig(repoRoot));
416
424
  const registry = new coreOps.ModelRegistry(modelsConfig);
417
- const diagnoses = coreOps.diagnoseModels(registry, modelDoctorProbes({ requireOllamaOnPath: true }), resolveExperimental(), resolveLocalOnly());
425
+ const diagnoses = coreOps.diagnoseModels(registry, modelDoctorProbes(), resolveExperimental(), resolveLocalOnly());
418
426
  console.log(formatInitReachability(diagnoses));
419
427
  if (!hasReachableWorker(diagnoses)) {
420
428
  console.log(styleText('yellow', 'No worker model reachable yet — see `factory doctor` and `factory models --doctor`.'));
@@ -815,8 +823,8 @@ export function formatDoctorReport(diagnoses, repoDefined = new Set()) {
815
823
  export function hasReachableWorker(diagnoses) {
816
824
  return diagnoses.some((d) => d.reachable && (d.tiers.includes('worker') || d.tiers.includes('worker_fallback')));
817
825
  }
818
- function ollamaModelSet(requireOnPath = false) {
819
- if (requireOnPath && !coreOps.isCommandAvailable('ollama'))
826
+ function ollamaModelSet() {
827
+ if (!coreOps.isCommandAvailable('ollama'))
820
828
  return undefined;
821
829
  try {
822
830
  const out = shellOps.execSync('ollama list', { timeout: 10_000 });
@@ -831,15 +839,21 @@ function ollamaModelSet(requireOnPath = false) {
831
839
  }
832
840
  }
833
841
  /** Probes shared by every doctor-style reachability check (init, models --doctor, overnight
834
- * preflight), so their counts agree. init passes `requireOllamaOnPath` so a missing binary never
835
- * reaches the shell (#2507). */
836
- export function modelDoctorProbes(opts = {}) {
837
- const ollamaModels = ollamaModelSet(opts.requireOllamaOnPath === true);
842
+ * preflight), so their counts agree. The probes never invoke `ollama` unless it is on PATH, so a
843
+ * missing binary never reaches the shell (#2507, #2724). */
844
+ export function modelDoctorProbes() {
845
+ const ollamaModels = ollamaModelSet();
838
846
  return {
839
847
  commandAvailable: coreOps.isCommandAvailable,
840
848
  ollamaModelPresent: ollamaModels ? (model) => ollamaModels.has(model) : undefined,
841
849
  };
842
850
  }
851
+ async function cmdHarnessCheck(id, opts) {
852
+ const r = await runHarnessCheckCommand(id, opts, { repoRoot: await getRepoRoot(), ...configLoaders });
853
+ if (r.exitCode !== 0)
854
+ throw new CliExitError(r.output, r.exitCode);
855
+ console.log(r.output);
856
+ }
843
857
  async function cmdModels(opts = {}) {
844
858
  const repoRoot = await getRepoRoot();
845
859
  const repoConfig = configLoaders.loadRepoConfig(repoRoot);
@@ -1190,16 +1204,17 @@ export function resolveUsageKnobs(env = process.env, repoConfig = null, override
1190
1204
  };
1191
1205
  }
1192
1206
  /** Human-readable name for whatever decided the usage-watch gate, used in the `factory run`
1193
- * and `factory supervise` logs so an explicit operator override is always attributable (#1404). */
1194
- export function usageWatchSourceLabel(source, watch) {
1207
+ * and `factory supervise` logs so an explicit operator override is always attributable (#1404). `repoConfigLabel` names the
1208
+ * repo config file that was loaded (#2570). */
1209
+ export function usageWatchSourceLabel(source, watch, repoConfigLabel = 'the repo config') {
1195
1210
  switch (source) {
1196
1211
  case 'flag':
1197
1212
  return watch ? 'flag: --usage-watch' : 'flag: --no-usage-watch';
1198
1213
  case 'repo':
1199
- return '.factory/config.json: budget.watchdog.watch';
1214
+ return `${repoConfigLabel}: budget.watchdog.watch`;
1200
1215
  case 'env':
1201
1216
  return 'env: FACTORY_USAGE_WATCH';
1202
- default:
1217
+ case 'default':
1203
1218
  return 'default';
1204
1219
  }
1205
1220
  }
@@ -1240,11 +1255,6 @@ export async function cmdUsage(deps = {}, opts = {}) {
1240
1255
  console.log(heuristicLine);
1241
1256
  }
1242
1257
  }
1243
- function warnQueueDiagnostics(diagnostics) {
1244
- for (const d of diagnostics) {
1245
- console.error(styleText('yellow', `factory: queue ${d.message} — skipped`));
1246
- }
1247
- }
1248
1258
  /** GitHub is polled far less often than local files: a queue changes on the order of minutes. */
1249
1259
  const TUI_GITHUB_QUEUE_POLL_MS = 30_000;
1250
1260
  /** The Queue tab's backlog source for `factory tui` (#1362). Mirrors `factory run`: GitHub Issues
@@ -1304,7 +1314,7 @@ async function cmdTui(opts = {}, runTuiImpl = runTui) {
1304
1314
  const effectiveConfigLines = describeEffectiveConfig({
1305
1315
  router,
1306
1316
  repo: repoConfig,
1307
- repoConfigPath: '.factory/config.json',
1317
+ repoConfigPath: repoConfigFileFor(repoRoot, paths.config).relPath,
1308
1318
  mergePolicy: resolveMergePolicy(configLoaders.loadFactoryConfigForRepo(paths.config), process.env, {
1309
1319
  auto: readRunFlagOverrides(paths.runFlags).autoMerge,
1310
1320
  }),
@@ -1451,6 +1461,7 @@ export function resolveEnvironmentAcquirer(opts) {
1451
1461
  };
1452
1462
  };
1453
1463
  }
1464
+ const sweepStaleLaneClaims = (paths, ghRepo) => sweepLaneClaims(paths, ghRepo, () => (hasGitHubToken() ? getOctokit() : undefined));
1454
1465
  /** The autoGcOnRun-gated worktree sweep every pipeline entry point runs before shipping
1455
1466
  * (`factory run`, and since #1756 the one-shot `factory ship` / `factory run-issue`). Never
1456
1467
  * throws — a GC failure is a warn, never a reason to abort the command. */
@@ -1479,14 +1490,15 @@ async function runAutoWorktreeGc(repoRoot, paths, ghRepo, eventScope) {
1479
1490
  * lane holds the land lock so a sweep can never race a merge. Logs worktree-gc only when it
1480
1491
  * removed something. Never throws. */
1481
1492
  export async function sweepBetweenLaneIssues(issue, lane, repoRoot, ghRepo, paths, deps = {}) {
1482
- const { loadConfig = configLoaders.loadFactoryConfigForRepo, sweep = internalOps.sweepWorktrees, emitEvent = logEvent, octokit = getOctokit, } = deps;
1493
+ const { loadConfig = configLoaders.loadFactoryConfigForRepo, sweep = internalOps.sweepWorktrees, emitEvent = logEvent, octokit = getOctokit, protectedIssues = () => new Set(), } = deps;
1483
1494
  try {
1484
1495
  const cfg = loadConfig(paths.config);
1485
1496
  if (!cfg.worktree.autoGcOnRun)
1486
1497
  return 'disabled';
1487
1498
  const gcLog = (type, msg) => emitEvent(paths.events, type, issue, msg, { lane });
1488
1499
  const sandbox = gcWorktreeSandbox(cfg.sandbox, repoRoot);
1489
- const report = await withGitLock(repoRoot, () => withFileLock(paths.mergeLock, () => withFileLock(paths.gitLock, () => sweep({ repoRoot, ttlDays: cfg.worktree.gcTtlDays, repo: ghRepo, branchPrefix: resolveBranchPrefix() }, { log: gcLog, octokit: octokit(), sandbox })), { timeoutMs: 0 }));
1500
+ const gcOpts = { repoRoot, ttlDays: cfg.worktree.gcTtlDays, repo: ghRepo, branchPrefix: resolveBranchPrefix() };
1501
+ const report = await withGitLock(repoRoot, () => withFileLock(paths.mergeLock, () => withFileLock(paths.gitLock, () => sweep({ ...gcOpts, protectedIssues: protectedIssues() }, { log: gcLog, octokit: octokit(), sandbox })), { timeoutMs: 0 }));
1490
1502
  if (report.removed.length > 0) {
1491
1503
  emitEvent(paths.events, 'worktree-gc', issue, `removed ${report.removed.length} stale worktree(s), kept ${report.kept}`, { lane });
1492
1504
  console.log(internalOps.formatGcReport(report));
@@ -1538,6 +1550,9 @@ async function cmdShip(issueNum, opts) {
1538
1550
  catch (err) {
1539
1551
  if (err instanceof IssueSkippedError)
1540
1552
  return;
1553
+ if (ghRepo && !(err instanceof InvalidWorkRequestInputError) && !(err instanceof LaneEnvironmentPauseError)) {
1554
+ await reapPrlessShipWorktree(issueNum, repoRoot, ghRepo, paths, branchPrefix);
1555
+ }
1541
1556
  // #2492: a pull request number is a usage error, not a failed ship.
1542
1557
  if (err instanceof InvalidWorkRequestInputError)
1543
1558
  throw new CliExitError(`factory: ${err.message}`, 2);
@@ -1853,30 +1868,23 @@ export async function withRepoRunLock(paths, command, fn, deps = {}) {
1853
1868
  throw err;
1854
1869
  }
1855
1870
  }
1856
- /** The repo's current docker-sandbox descriptor for worktree-gc sweeps, or undefined for
1857
- * every other runtime/config state (including a fixture config with no `sandbox` key). */
1858
- function gcWorktreeSandbox(sandboxCfg, repoRoot) {
1859
- if (!sandboxCfg)
1860
- return undefined;
1861
- const policy = resolveSandboxPolicy(sandboxCfg, { worktree: repoRoot, repoRoot });
1862
- return worktreeSandboxFor(policy?.runtime);
1863
- }
1864
- /** Eagerly reaps a parked lane's own worktree inside `factory run` (#1007). All the safety
1865
- * gates live in reapLaneWorktree; this wrapper resolves the lane's conventional worktree
1866
- * path and holds the same git+file lock pairing as every other worktree mutation site. A
1867
- * reap failure is logged as a warn and never surfaces — parking must always succeed. */
1868
1871
  export async function reapParkedLaneWorktree(issue, repoRoot, ghRepo, paths, prefixOverride) {
1869
- try {
1870
- const factoryConfig = configLoaders.loadFactoryConfigForRepo(paths.config);
1871
- const branchPrefix = prefixOverride ?? resolveEffectiveConfig(configLoaders.loadRepoConfig(repoRoot)).branchPrefix;
1872
- const worktreePath = worktreePathFor(repoRoot, ghRepo, issue, branchPrefix);
1873
- const log = (type, msg) => logEvent(paths.events, type, issue, msg);
1874
- const sandbox = gcWorktreeSandbox(factoryConfig.sandbox, repoRoot);
1875
- await withGitLock(repoRoot, () => withFileLock(paths.gitLock, () => reapLaneWorktree(repoRoot, worktreePath, { issue, branchPrefix, log, sandbox })));
1876
- }
1877
- catch (err) {
1878
- logEvent(paths.events, 'warn', issue, `parked-lane worktree reap failed: ${err?.message ?? String(err)}`);
1879
- }
1872
+ await reapParkedLane({ configLoaders, worktreePathFor }, reapLaneWorktree, [
1873
+ issue,
1874
+ repoRoot,
1875
+ ghRepo,
1876
+ paths,
1877
+ prefixOverride,
1878
+ ]);
1879
+ }
1880
+ export async function reapPrlessShipWorktree(issue, repoRoot, ghRepo, paths, prefixOverride) {
1881
+ await reapPrlessShip({ configLoaders, worktreePathFor }, internalOps.reapCleanShipWorktree, [
1882
+ issue,
1883
+ repoRoot,
1884
+ ghRepo,
1885
+ paths,
1886
+ prefixOverride,
1887
+ ]);
1880
1888
  }
1881
1889
  export async function cmdWorktreeGc(opts) {
1882
1890
  const repoRoot = await getRepoRoot();
@@ -2016,7 +2024,7 @@ async function cmdTriage(opts) {
2016
2024
  onLog: (msg) => logEvent(paths.events, 'triage', '-', msg),
2017
2025
  });
2018
2026
  model = result.model;
2019
- const queue = normalizeTriageQueue(result.output);
2027
+ const queue = await proposeTriageQueue(result.output, ghRepo, paths.events, shellOps.exec);
2020
2028
  if (queue) {
2021
2029
  const header = triageQueueHeader(model, product);
2022
2030
  writeFileSync(paths.queueProposed, header + queue);
@@ -2176,7 +2184,8 @@ export async function cmdUnpark(issueArg, opts = {}) {
2176
2184
  console.log(JSON.stringify(result));
2177
2185
  return;
2178
2186
  }
2179
- console.log(styleText('green', `#${issue} unparked → lane ${result.lane}, position ${result.order}`));
2187
+ for (const line of renderUnparkLines(result))
2188
+ console.log(line);
2180
2189
  }
2181
2190
  export async function cmdQueueAdd(lane, issueArgs, opts = {}) {
2182
2191
  // Validate + dedupe BEFORE any GitHub call. parseIssueArg throws CliExitError(2) on bad input.
@@ -2215,17 +2224,8 @@ export async function cmdQueueAdd(lane, issueArgs, opts = {}) {
2215
2224
  }
2216
2225
  return;
2217
2226
  }
2218
- for (const r of results) {
2219
- if (r.outcome === 'queued') {
2220
- console.log(styleText('green', `#${r.issue} queued → lane ${lane}, position ${r.position}`));
2221
- }
2222
- else if (r.outcome === 'already-queued') {
2223
- console.log(`#${r.issue} already queued — skipped`);
2224
- }
2225
- else {
2226
- console.error(styleText('red', `#${r.issue} failed — ${r.detail}`));
2227
- }
2228
- }
2227
+ for (const line of renderQueueAddLines(lane, results))
2228
+ (line.stream === 'err' ? console.error : console.log)(line.text);
2229
2229
  const failed = results.filter((r) => r.outcome === 'failed');
2230
2230
  if (failed.length > 0) {
2231
2231
  throw new CliExitError(`factory: ${failed.length} issue(s) failed to queue`, 1);
@@ -2235,6 +2235,7 @@ export async function cmdQueueReconcile(opts = {}) {
2235
2235
  const repoRoot = await getRepoRoot();
2236
2236
  const paths = getFactoryPaths(repoRoot);
2237
2237
  const [owner, repo] = (await getGitHubRepo()).split('/');
2238
+ await sweepStaleLaneClaims(paths, `${owner}/${repo}`);
2238
2239
  const client = createOctokitQueueClient(getOctokit());
2239
2240
  const { report, conflicts } = await runQueueReconcile({
2240
2241
  readSnapshot: () => readGithubQueueSnapshot({ client, owner, repo }),
@@ -2849,7 +2850,7 @@ export async function cmdResumeApproved(opts = {}) {
2849
2850
  }
2850
2851
  const USAGE_UNAVAILABLE_LINE = '[factory] supervise: usage signal unavailable — proceeding without resume gate (set FACTORY_USAGE_ESTIMATOR=1 to gate on the heuristic)';
2851
2852
  export async function superviseLoop(deps) {
2852
- const { cap, resumeAt, pollMs, watch = true, watchSource = 'default', 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;
2853
+ 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;
2853
2854
  let cycle = 0;
2854
2855
  if (resumeAtSource === 'flag') {
2855
2856
  const detail = 'usage watchdog threshold set by flag: --usage-threshold';
@@ -2866,11 +2867,11 @@ export async function superviseLoop(deps) {
2866
2867
  let pct = 0;
2867
2868
  let source = 'unavailable';
2868
2869
  if (!watch) {
2869
- writeLine(`[factory] supervise: usage watchdog disabled (${usageWatchSourceLabel(watchSource, watch)}) — skipping resume gate`);
2870
+ writeLine(`[factory] supervise: usage watchdog disabled (${usageWatchSourceLabel(watchSource, watch, repoConfigLabel)}) — skipping resume gate`);
2870
2871
  }
2871
2872
  else {
2872
2873
  if (watchSource === 'flag') {
2873
- writeLine(`[factory] supervise: usage watchdog enabled (${usageWatchSourceLabel(watchSource, watch)}) — enforcing resume gate`);
2874
+ writeLine(`[factory] supervise: usage watchdog enabled (${usageWatchSourceLabel(watchSource, watch, repoConfigLabel)}) — enforcing resume gate`);
2874
2875
  }
2875
2876
  let reading = await readUsageFn();
2876
2877
  if (reading === null) {
@@ -3009,7 +3010,7 @@ export async function cmdStop(opts = {}) {
3009
3010
  console.log(JSON.stringify(buildStopResumeJson({ action: 'stop', repo: json.repo, before, after })));
3010
3011
  return;
3011
3012
  }
3012
- console.log('STOP set — lanes halt between issues');
3013
+ console.log(STOP_SET_MESSAGE);
3013
3014
  }
3014
3015
  export async function cmdResume(issue, opts = {}) {
3015
3016
  if (issue === undefined && opts.from !== undefined) {
@@ -3095,6 +3096,7 @@ function runSupervise(opts) {
3095
3096
  createIngestHook,
3096
3097
  cmdRun: (o) => runRun(o),
3097
3098
  CliExitError,
3099
+ repoConfigFileFor,
3098
3100
  });
3099
3101
  }
3100
3102
  /** `factory status` (#2798): the handler lives in ./status.ts; this binds its runtime. */
@@ -3111,6 +3113,7 @@ function runStatus(opts = {}) {
3111
3113
  warnQueueDiagnostics,
3112
3114
  errorDetail,
3113
3115
  CliExitError,
3116
+ repoConfigFileFor,
3114
3117
  });
3115
3118
  }
3116
3119
  /** `factory run` (#2800): the handler lives in ./run.ts; this binds its runtime. */
@@ -3126,6 +3129,7 @@ function runRun(opts = {}) {
3126
3129
  branchPrefixOrExit,
3127
3130
  withRepoRunLock,
3128
3131
  runAutoWorktreeGc,
3132
+ sweepStaleLaneClaims,
3129
3133
  planRunLanes,
3130
3134
  preflightQueuedIssue,
3131
3135
  createQueuePreflightOps,
@@ -3165,6 +3169,7 @@ export const PROGRAM_HANDLERS = {
3165
3169
  cmdMigrate,
3166
3170
  cmdConstitution,
3167
3171
  cmdModels,
3172
+ cmdHarnessCheck,
3168
3173
  cmdDoctor: runDoctor,
3169
3174
  cmdCost,
3170
3175
  cmdUsage,