@on-par/factory-cli 0.9.11-next.5 → 0.9.11-next.51

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 (114) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/base-health-ci.d.ts +18 -0
  3. package/dist/cli/base-health-ci.d.ts.map +1 -0
  4. package/dist/cli/base-health-ci.js +34 -0
  5. package/dist/cli/base-health-ci.js.map +1 -0
  6. package/dist/cli/base-hold.d.ts +16 -11
  7. package/dist/cli/base-hold.d.ts.map +1 -1
  8. package/dist/cli/base-hold.js +64 -20
  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.js +1 -1
  19. package/dist/cli/doctor.js.map +1 -1
  20. package/dist/cli/index.d.ts +10 -48
  21. package/dist/cli/index.d.ts.map +1 -1
  22. package/dist/cli/index.js +43 -191
  23. package/dist/cli/index.js.map +1 -1
  24. package/dist/cli/json-contract.d.ts +306 -0
  25. package/dist/cli/json-contract.d.ts.map +1 -0
  26. package/dist/cli/json-contract.js +208 -0
  27. package/dist/cli/json-contract.js.map +1 -0
  28. package/dist/cli/land-pr.d.ts.map +1 -1
  29. package/dist/cli/land-pr.js +5 -0
  30. package/dist/cli/land-pr.js.map +1 -1
  31. package/dist/cli/lane-environment-pause-error.d.ts +22 -0
  32. package/dist/cli/lane-environment-pause-error.d.ts.map +1 -0
  33. package/dist/cli/lane-environment-pause-error.js +28 -0
  34. package/dist/cli/lane-environment-pause-error.js.map +1 -0
  35. package/dist/cli/logs.d.ts +14 -0
  36. package/dist/cli/logs.d.ts.map +1 -1
  37. package/dist/cli/logs.js +46 -4
  38. package/dist/cli/logs.js.map +1 -1
  39. package/dist/cli/program-observe.d.ts.map +1 -1
  40. package/dist/cli/program-observe.js +5 -2
  41. package/dist/cli/program-observe.js.map +1 -1
  42. package/dist/cli/queue-add-json.d.ts +3 -25
  43. package/dist/cli/queue-add-json.d.ts.map +1 -1
  44. package/dist/cli/queue-add-json.js.map +1 -1
  45. package/dist/cli/queue-list.d.ts +6 -24
  46. package/dist/cli/queue-list.d.ts.map +1 -1
  47. package/dist/cli/queue-list.js +6 -1
  48. package/dist/cli/queue-list.js.map +1 -1
  49. package/dist/cli/queue-preflight.d.ts +45 -0
  50. package/dist/cli/queue-preflight.d.ts.map +1 -0
  51. package/dist/cli/queue-preflight.js +277 -0
  52. package/dist/cli/queue-preflight.js.map +1 -0
  53. package/dist/cli/run-branch.d.ts +24 -0
  54. package/dist/cli/run-branch.d.ts.map +1 -0
  55. package/dist/cli/run-branch.js +38 -0
  56. package/dist/cli/run-branch.js.map +1 -0
  57. package/dist/cli/run-lane.d.ts.map +1 -1
  58. package/dist/cli/run-lane.js +6 -2
  59. package/dist/cli/run-lane.js.map +1 -1
  60. package/dist/cli/run.d.ts.map +1 -1
  61. package/dist/cli/run.js +17 -3
  62. package/dist/cli/run.js.map +1 -1
  63. package/dist/cli/runs-diffstat.d.ts +6 -25
  64. package/dist/cli/runs-diffstat.d.ts.map +1 -1
  65. package/dist/cli/runs-diffstat.js.map +1 -1
  66. package/dist/cli/runs.d.ts +4 -19
  67. package/dist/cli/runs.d.ts.map +1 -1
  68. package/dist/cli/runs.js +1 -1
  69. package/dist/cli/runs.js.map +1 -1
  70. package/dist/cli/self-update-stop.d.ts +7 -0
  71. package/dist/cli/self-update-stop.d.ts.map +1 -0
  72. package/dist/cli/self-update-stop.js +27 -0
  73. package/dist/cli/self-update-stop.js.map +1 -0
  74. package/dist/cli/ship-run-state.d.ts +5 -0
  75. package/dist/cli/ship-run-state.d.ts.map +1 -0
  76. package/dist/cli/ship-run-state.js +12 -0
  77. package/dist/cli/ship-run-state.js.map +1 -0
  78. package/dist/cli/ship-steps.d.ts +1 -1
  79. package/dist/cli/ship-steps.d.ts.map +1 -1
  80. package/dist/cli/ship-steps.js +15 -12
  81. package/dist/cli/ship-steps.js.map +1 -1
  82. package/dist/cli/ship.d.ts.map +1 -1
  83. package/dist/cli/ship.js +27 -24
  84. package/dist/cli/ship.js.map +1 -1
  85. package/dist/cli/status-json.d.ts +7 -28
  86. package/dist/cli/status-json.d.ts.map +1 -1
  87. package/dist/cli/status-json.js +3 -1
  88. package/dist/cli/status-json.js.map +1 -1
  89. package/dist/cli/status.d.ts.map +1 -1
  90. package/dist/cli/status.js +4 -1
  91. package/dist/cli/status.js.map +1 -1
  92. package/dist/cli/stop-resume-json.d.ts +2 -10
  93. package/dist/cli/stop-resume-json.d.ts.map +1 -1
  94. package/dist/cli/stop-resume-json.js +1 -0
  95. package/dist/cli/stop-resume-json.js.map +1 -1
  96. package/dist/cli/usage-json.d.ts +4 -13
  97. package/dist/cli/usage-json.d.ts.map +1 -1
  98. package/dist/cli/usage-json.js.map +1 -1
  99. package/dist/cli/worktree-reap.d.ts.map +1 -1
  100. package/dist/cli/worktree-reap.js +7 -1
  101. package/dist/cli/worktree-reap.js.map +1 -1
  102. package/package.json +8 -5
  103. package/schemas/cost.schema.json +114 -0
  104. package/schemas/daemon-status.schema.json +44 -0
  105. package/schemas/logs-line.schema.json +118 -0
  106. package/schemas/queue-add.schema.json +73 -0
  107. package/schemas/queue-list.schema.json +77 -0
  108. package/schemas/resume.schema.json +36 -0
  109. package/schemas/runs-diffstat.schema.json +81 -0
  110. package/schemas/runs.schema.json +75 -0
  111. package/schemas/status.schema.json +200 -0
  112. package/schemas/stop.schema.json +36 -0
  113. package/schemas/unpark.schema.json +57 -0
  114. package/schemas/usage.schema.json +28 -0
package/dist/cli/index.js CHANGED
@@ -6,18 +6,21 @@ import { createRequire } from 'node:module';
6
6
  import { arch, hostname as osHostname, release, platform, userInfo } from 'node:os';
7
7
  import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
8
8
  import { promisify, styleText } from 'node:util';
9
+ import { LaneEnvironmentPauseError } from './lane-environment-pause-error.js';
9
10
  import { INVALID_BRANCH_PREFIX_MESSAGE, resolveBranchPrefixOption } from './branch-prefix.js';
10
11
  import { sweepLaneClaims, warnQueueDiagnostics } from './queue-startup.js';
11
12
  import { proposeTriageQueue } from './triage-open-prs.js';
13
+ import { makeQueuePreflightOps, preflightQueuedIssue } from './queue-preflight.js';
12
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';
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, 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';
14
16
  import { runTui } from '@on-par/factory-tui';
17
+ import { clearSelfUpdateStop } from './self-update-stop.js';
15
18
  import { admissionStateReaderFor, runQueueReconcile } from './admission.js';
16
19
  import { cmdClassifierBacktest, ClassifierBacktestInputError, } from './classifier-backtest.js';
17
20
  import { runHarnessCheckCommand } from './harness-check.js';
18
- import { findOpenPRForIssue, landIssue } from './land.js';
21
+ import { landIssue } from './land.js';
19
22
  import { STOP_SET_MESSAGE } from './stop-sentinel.js';
20
- import { AwaitingReviewError, CiFailedError, CiUnverifiedError, getPullRequestLandState, LandConflictError, LandFailureError, MergeGatedError, } from './land-pr.js';
23
+ import { AwaitingReviewError, CiFailedError, CiUnverifiedError, LandConflictError, LandFailureError, MergeGatedError, } from './land-pr.js';
21
24
  import { setLandRuntime } from './land-runtime.js';
22
25
  import { cmdRun } from './run.js';
23
26
  import { shipIssue } from './ship.js';
@@ -338,7 +341,7 @@ setShipRuntime(() => ({
338
341
  IssueSkippedError,
339
342
  IssueDecomposedError,
340
343
  }));
341
- export { shipIssue };
344
+ export { LaneEnvironmentPauseError, shipIssue };
342
345
  export function hasGitHubToken(env = process.env, tryToken) {
343
346
  if (env.GITHUB_TOKEN || env.GH_TOKEN)
344
347
  return true;
@@ -874,7 +877,7 @@ async function cmdModels(opts = {}) {
874
877
  const byModel = new Map(diagnoses.map((d) => [d.model, d]));
875
878
  for (const m of registry.list()) {
876
879
  const tiers = registry.getTiers(m).join('/');
877
- const est = registry.estimateCost(m, 1_000_000, 1_000_000);
880
+ const est = registry.estimateCost(m, { inputTokens: 1_000_000, outputTokens: 1_000_000 }).cost;
878
881
  const cost = est === null ? 'unpriced' : `$${est.toFixed(2)}/M`;
879
882
  const d = byModel.get(m);
880
883
  const avail = d ? modelStatusIcon(d) : styleText('red', '❌');
@@ -887,6 +890,7 @@ async function cmdModels(opts = {}) {
887
890
  console.log(` ${tier}: ${models.join(' ')}`);
888
891
  }
889
892
  }
893
+ const CACHED_RATE_MARKER = ' (cached at full input rate)';
890
894
  async function cmdCost(opts = {}) {
891
895
  const repoRoot = await getRepoRoot();
892
896
  const paths = getFactoryPaths(repoRoot);
@@ -910,7 +914,7 @@ async function cmdCost(opts = {}) {
910
914
  }
911
915
  console.log(styleText('bold', `== Costs for issue ${opts.issue} ==`));
912
916
  for (const c of filtered) {
913
- console.log(` ${c.task} ${c.model} ${c.cost === null || c.cost === undefined ? 'unknown' : `$${c.cost.toFixed(4)}`}${c.failoverReason ? ` [failover: ${c.failoverReason}]` : ''}`);
917
+ 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 : ''}`);
914
918
  }
915
919
  const t = aggregateCosts(filtered).total;
916
920
  console.log(' ---');
@@ -923,7 +927,14 @@ async function cmdCost(opts = {}) {
923
927
  }
924
928
  const byModel = new Map();
925
929
  for (const c of costs) {
926
- const e = byModel.get(c.model) ?? { tasks: 0, priced: 0, pricedCount: 0, unpriced: 0, failovers: 0 };
930
+ const e = byModel.get(c.model) ?? {
931
+ tasks: 0,
932
+ priced: 0,
933
+ pricedCount: 0,
934
+ unpriced: 0,
935
+ failovers: 0,
936
+ cachedRateMissing: 0,
937
+ };
927
938
  e.tasks++;
928
939
  if (c.unpriced === true || c.cost === null || c.cost === undefined)
929
940
  e.unpriced++;
@@ -933,13 +944,16 @@ async function cmdCost(opts = {}) {
933
944
  }
934
945
  if (c.failoverReason)
935
946
  e.failovers++;
947
+ if (c.cachedRateMissing === true)
948
+ e.cachedRateMissing++;
936
949
  byModel.set(c.model, e);
937
950
  }
938
951
  console.log(styleText('bold', '== Cost Summary =='));
939
- for (const [model, { tasks, priced, pricedCount, unpriced, failovers }] of byModel) {
952
+ for (const [model, { tasks, priced, pricedCount, unpriced, failovers, cachedRateMissing }] of byModel) {
940
953
  const failoverSuffix = failovers > 0 ? ` (${failovers} failover${failovers === 1 ? '' : 's'})` : '';
941
954
  const modelCost = formatCostTotal(pricedCount === 0 && unpriced > 0 ? null : priced, unpriced);
942
- console.log(` ${model}: ${tasks} tasks, ${modelCost}${failoverSuffix}`);
955
+ const cachedSuffix = cachedRateMissing > 0 ? CACHED_RATE_MARKER : '';
956
+ console.log(` ${model}: ${tasks} tasks, ${modelCost}${failoverSuffix}${cachedSuffix}`);
943
957
  }
944
958
  const grand = aggregateCosts(costs).total;
945
959
  console.log(' ---');
@@ -955,6 +969,7 @@ async function cmdCheck(issueRaw, opts) {
955
969
  const repoConfig = configLoaders.loadRepoConfig(repoRoot);
956
970
  const effective = resolveEffectiveConfig(repoConfig);
957
971
  const router = new coreOps.ModelRouter(applyRepoConfig(configLoaders.loadModelsConfig(), repoConfig), configLoaders.loadRoutesConfig(), false, undefined, effective.allowExperimental, effective.localOnly);
972
+ router.setCodexFullAccessTasks?.(configLoaders.loadFactoryConfigForRepo(getFactoryPaths(repoRoot).config).sandbox.codexFullAccessTasks);
958
973
  router.setLocalAuthSink?.(recordLocalAuthFailure);
959
974
  runModel = createDeepModelRunner(router, repoRoot);
960
975
  }
@@ -1063,6 +1078,7 @@ async function cmdClassifierBacktestEntry(opts) {
1063
1078
  const repoConfig = configLoaders.loadRepoConfig(repoRoot);
1064
1079
  const effective = resolveEffectiveConfig(repoConfig);
1065
1080
  const router = new coreOps.ModelRouter(applyRepoConfig(configLoaders.loadModelsConfig(), repoConfig), configLoaders.loadRoutesConfig(), false, undefined, effective.allowExperimental, effective.localOnly);
1081
+ router.setCodexFullAccessTasks?.(configLoaders.loadFactoryConfigForRepo(paths.config).sandbox.codexFullAccessTasks);
1066
1082
  router.setLocalAuthSink?.(recordLocalAuthFailure);
1067
1083
  try {
1068
1084
  await cmdClassifierBacktest({ ...opts, branchPrefix }, {
@@ -1314,6 +1330,7 @@ async function cmdTui(opts = {}, runTuiImpl = runTui) {
1314
1330
  const routesConfig = configLoaders.loadRoutesConfig();
1315
1331
  const effective = resolveEffectiveConfig(repoConfig);
1316
1332
  const router = new coreOps.ModelRouter(modelsConfig, routesConfig, false, undefined, effective.allowExperimental, effective.localOnly);
1333
+ router.setCodexFullAccessTasks?.(configLoaders.loadFactoryConfigForRepo(paths.config).sandbox.codexFullAccessTasks);
1317
1334
  router.setLocalAuthSink?.(recordLocalAuthFailure);
1318
1335
  const effectiveConfigLines = describeEffectiveConfig({
1319
1336
  router,
@@ -1366,24 +1383,6 @@ export class LaneParkError extends Error {
1366
1383
  return this.outcome.failingChecks;
1367
1384
  }
1368
1385
  }
1369
- /** CHECK found every failing checker also failing on the base SHA (#1928): the issue is
1370
- * released to queued (not parked) and the lane pauses. */
1371
- export class LaneEnvironmentPauseError extends Error {
1372
- baseSha;
1373
- failingChecks;
1374
- failureSignature;
1375
- /** BUILD output was kept for `factory resume --from check` (#2564); runLane must not reap it. */
1376
- buildPreserved;
1377
- constructor(message, failure) {
1378
- super(message);
1379
- if (failure.buildPreserved !== undefined)
1380
- this.buildPreserved = failure.buildPreserved;
1381
- this.baseSha = failure.baseSha;
1382
- this.failingChecks = failure.failingChecks;
1383
- if (failure.failureSignature !== undefined)
1384
- this.failureSignature = failure.failureSignature;
1385
- }
1386
- }
1387
1386
  /** Resolves the stable lane URL a build/check agent should use, by probing whether
1388
1387
  * a factory proxy (started by `factory run`/`supervise` or `factory proxy`) is
1389
1388
  * currently alive for the configured domain. Never throws — an absent/dead proxy
@@ -2012,6 +2011,7 @@ async function cmdTriage(opts) {
2012
2011
  const routesConfig = configLoaders.loadRoutesConfig();
2013
2012
  const effective = resolveEffectiveConfig(repoConfig);
2014
2013
  const router = new coreOps.ModelRouter(modelsConfig, routesConfig, false, undefined, effective.allowExperimental, effective.localOnly);
2014
+ router.setCodexFullAccessTasks?.(configLoaders.loadFactoryConfigForRepo(paths.config).sandbox.codexFullAccessTasks);
2015
2015
  router.setLocalAuthSink?.(recordLocalAuthFailure);
2016
2016
  router.skipMissingHarnessClis(coreOps.isCommandAvailable);
2017
2017
  router.setCostSink((entry) => logCost(paths.costs, { ...entry, issue: '-' }));
@@ -2432,8 +2432,7 @@ async function cmdFactoryd(opts) {
2432
2432
  const previousUpdate = readUpdateState(updateFile);
2433
2433
  for (const stop of recoverStaleDrain(previousUpdate, (file) => {
2434
2434
  try {
2435
- unlinkSync(file);
2436
- return true;
2435
+ return clearSelfUpdateStop(file);
2437
2436
  }
2438
2437
  catch {
2439
2438
  return false;
@@ -2559,167 +2558,12 @@ export function laneQueueDeps(queue, lane) {
2559
2558
  countRemaining: async () => (await queue.list(lane)).length,
2560
2559
  };
2561
2560
  }
2561
+ export { preflightQueuedIssue };
2562
2562
  export function createQueuePreflightOps(octokit, owner, repo, opts = {}) {
2563
- return {
2564
- autoMergeStack: opts.autoMergeStack ?? false,
2565
- getPullRefs: async (prNumber) => {
2566
- const { data } = await octokit.rest.pulls.get({ owner, repo, pull_number: prNumber });
2567
- return { headRef: data.head.ref, baseRef: data.base.ref, headSha: data.head.sha };
2568
- },
2569
- expectedHeadRepoFullName: `${owner}/${repo}`,
2570
- findOpenPR: (issue) => findOpenPRForIssue(octokit, owner, repo, issue),
2571
- getLandState: (prNumber) => getPullRequestLandState(octokit, owner, repo, prNumber),
2563
+ return makeQueuePreflightOps(octokit, owner, repo, {
2572
2564
  watch: (ref) => internalOps.watchChecks({ octokit, owner, repo, ref }),
2573
- findSlicePlan: (issue) => findSlicePlanComment({ octokit, repo: `${owner}/${repo}`, issue }),
2574
- getPullState: async (prNumber) => {
2575
- const { data } = await octokit.rest.pulls.get({ owner, repo, pull_number: prNumber });
2576
- if (data.merged || data.merged_at)
2577
- return 'merged';
2578
- return data.state === 'open' ? 'open' : 'closed';
2579
- },
2580
- saveSlicePlan: async (issue, plan) => {
2581
- await upsertSlicePlanComment({ octokit, repo: `${owner}/${repo}`, issue, plan });
2582
- },
2583
- };
2584
- }
2585
- /** ADR-0147/ADR-0161: walk every pr-open slice PR bottom-up. Merged layers are recorded, a closed layer parks
2586
- * (naming the layers above it), and the lowest open layer defers, or is adopted when stack auto-merge is on and
2587
- * its base is main with green CI on the rebased head. Returns undefined to fall through to the Closes-PR
2588
- * preflight only when no slice PR is still open. */
2589
- async function preflightSlice(issue, deps) {
2590
- let lookup;
2591
- try {
2592
- lookup = await deps.findSlicePlan(issue);
2593
- }
2594
- catch (err) {
2595
- return { kind: 'park', reason: `slice plan lookup failed: ${errorDetail(err)}` };
2596
- }
2597
- if (lookup.status === 'invalid') {
2598
- return {
2599
- kind: 'park',
2600
- reason: `slice plan comment ${lookup.commentId} on #${issue} is unreadable (${lookup.error})`,
2601
- };
2602
- }
2603
- if (lookup.status === 'not-found')
2604
- return undefined;
2605
- const { plan } = lookup;
2606
- const n = plan.slices.length;
2607
- const open = plan.slices.filter((s) => s.state === 'pr-open');
2608
- if (open.length === 0)
2609
- return undefined;
2610
- let next = plan;
2611
- let lastMerged;
2612
- const save = async () => {
2613
- if (lastMerged === undefined)
2614
- return undefined;
2615
- try {
2616
- await deps.saveSlicePlan(issue, next);
2617
- }
2618
- catch (err) {
2619
- return {
2620
- kind: 'park',
2621
- reason: `${lastMerged.label} PR #${lastMerged.pr} merged but updating the slice plan failed: ${errorDetail(err)}`,
2622
- };
2623
- }
2624
- return undefined;
2625
- };
2626
- for (const [at, slice] of open.entries()) {
2627
- const label = `slice ${slice.index}/${n}`;
2628
- if (slice.prNumber === undefined) {
2629
- return { kind: 'park', reason: `${label} of #${issue} is pr-open but the slice plan records no PR number` };
2630
- }
2631
- const pr = slice.prNumber;
2632
- let state;
2633
- try {
2634
- state = await deps.getPullState(pr);
2635
- }
2636
- catch (err) {
2637
- return { kind: 'park', reason: `${label} PR #${pr} state lookup failed: ${errorDetail(err)}` };
2638
- }
2639
- if (state === 'merged') {
2640
- next = withSliceState(next, slice.index, 'merged', pr);
2641
- lastMerged = { label, pr };
2642
- continue;
2643
- }
2644
- if (state === 'closed') {
2645
- const above = open.slice(at + 1);
2646
- const suffix = above.length > 0
2647
- ? `; layers above it: ${above.map((a) => `slice ${a.index}/${n} (#${a.prNumber})`).join(', ')}`
2648
- : '';
2649
- return { kind: 'park', reason: `${label} PR #${pr} was closed without merging${suffix}` };
2650
- }
2651
- const saved = await save();
2652
- if (saved)
2653
- return saved;
2654
- if (!deps.autoMergeStack)
2655
- return { kind: 'defer' };
2656
- let refs;
2657
- try {
2658
- refs = await deps.getPullRefs(pr);
2659
- }
2660
- catch (err) {
2661
- return { kind: 'park', reason: `${label} PR #${pr} ref lookup failed: ${errorDetail(err)}` };
2662
- }
2663
- // GitHub has not retargeted the layer to main yet: the lower layer is still unmerged.
2664
- if (refs.baseRef !== 'main')
2665
- return { kind: 'defer' };
2666
- let outcome;
2667
- try {
2668
- outcome = await deps.watch(refs.headSha);
2669
- }
2670
- catch (err) {
2671
- return { kind: 'park', reason: `CI watch for ${label} PR #${pr} failed: ${errorDetail(err)}` };
2672
- }
2673
- if (outcome === 'failure')
2674
- return { kind: 'park', reason: `CI for ${label} PR #${pr} ended failure` };
2675
- if (outcome !== 'success')
2676
- return { kind: 'defer' };
2677
- return { kind: 'adopt', branch: refs.headRef, stackLayer: { index: slice.index, count: n, prNumber: pr } };
2678
- }
2679
- return (await save()) ?? undefined;
2680
- }
2681
- /** Resolve queued work from GitHub evidence without consuming model or worktree resources. */
2682
- export async function preflightQueuedIssue(issue, deps) {
2683
- if (issue.labels.includes('factory:in-progress'))
2684
- return { kind: 'defer' };
2685
- const slice = await preflightSlice(issue.number, deps);
2686
- if (slice)
2687
- return slice;
2688
- let pr;
2689
- try {
2690
- pr = await deps.findOpenPR(issue.number);
2691
- }
2692
- catch (err) {
2693
- return { kind: 'park', reason: `PR lookup failed: ${errorDetail(err)}` };
2694
- }
2695
- if (!pr)
2696
- return { kind: 'build' };
2697
- if (pr.headRepoFullName !== deps.expectedHeadRepoFullName) {
2698
- return { kind: 'park', reason: `PR #${pr.number} head is not in ${deps.expectedHeadRepoFullName}` };
2699
- }
2700
- let state;
2701
- try {
2702
- state = await deps.getLandState(pr.number);
2703
- }
2704
- catch (err) {
2705
- return { kind: 'park', reason: `PR #${pr.number} merge-state lookup failed: ${errorDetail(err)}` };
2706
- }
2707
- if (state.isDraft !== false) {
2708
- return { kind: 'park', reason: `PR #${pr.number} is draft or its draft state is unavailable` };
2709
- }
2710
- if (!state.mergeStateStatus || state.mergeStateStatus === 'UNKNOWN') {
2711
- return { kind: 'park', reason: `PR #${pr.number} merge state is unavailable` };
2712
- }
2713
- let outcome;
2714
- try {
2715
- outcome = await deps.watch(pr.headSha);
2716
- }
2717
- catch (err) {
2718
- return { kind: 'park', reason: `CI watch for PR #${pr.number} failed: ${errorDetail(err)}` };
2719
- }
2720
- if (outcome !== 'success')
2721
- return { kind: 'park', reason: `CI for PR #${pr.number} ended ${outcome}` };
2722
- return { kind: 'adopt', branch: pr.branch };
2565
+ ...opts,
2566
+ });
2723
2567
  }
2724
2568
  export async function planRunLanes(input) {
2725
2569
  if (input.localQueue) {
@@ -2848,7 +2692,7 @@ export async function cmdResumeApproved(opts = {}) {
2848
2692
  }
2849
2693
  const USAGE_UNAVAILABLE_LINE = '[factory] supervise: usage signal unavailable — proceeding without resume gate (set FACTORY_USAGE_ESTIMATOR=1 to gate on the heuristic)';
2850
2694
  export async function superviseLoop(deps) {
2851
- 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;
2695
+ 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;
2852
2696
  let cycle = 0;
2853
2697
  if (resumeAtSource === 'flag') {
2854
2698
  const detail = 'usage watchdog threshold set by flag: --usage-threshold';
@@ -2896,7 +2740,15 @@ export async function superviseLoop(deps) {
2896
2740
  }
2897
2741
  }
2898
2742
  emitEvent(eventsFile, 'resumed', 'usage', `supervise cycle ${cycle}: trailing usage (${source}) at ${Math.round(pct * 100)}% of cap — starting run`);
2899
- clearStop(stopFile);
2743
+ const marker = readMarker(stopFile);
2744
+ if (marker?.source === 'usage-watchdog') {
2745
+ clearStop(stopFile);
2746
+ }
2747
+ else if (marker) {
2748
+ writeLine(`[factory] supervise: STOP set by ${marker.source}${marker.at ? ` at ${marker.at}` : ''}; waiting for factory resume`);
2749
+ while (pathExists(stopFile))
2750
+ await sleep(pollMs);
2751
+ }
2900
2752
  if (ingest) {
2901
2753
  const appended = await ingest();
2902
2754
  if (appended > 0)
@@ -2975,7 +2827,7 @@ function logEventQuietly(eventsFile, type, msg) {
2975
2827
  /** Creates the STOP file in one exclusive open (no check-then-write race). Returns false when it already existed. */
2976
2828
  function createStopFile(stopFile) {
2977
2829
  try {
2978
- writeFileSync(stopFile, '', { flag: 'wx' });
2830
+ writeFileSync(stopFile, formatStopMarker('manual'), { flag: 'wx' });
2979
2831
  return true;
2980
2832
  }
2981
2833
  catch (err) {