@ai-dossier/cli 0.92.1 → 0.93.0-next.714.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.
@@ -49,6 +49,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
49
49
  exports.UPGRADE_RESTART_EXIT_CODE = void 0;
50
50
  exports.createBatchCapabilityRunner = createBatchCapabilityRunner;
51
51
  exports.reexecUpdatedCli = reexecUpdatedCli;
52
+ exports.buildEngineDeps = buildEngineDeps;
52
53
  exports.registerSchedCommand = registerSchedCommand;
53
54
  const node_child_process_1 = require("node:child_process");
54
55
  const node_fs_1 = __importDefault(require("node:fs"));
@@ -73,9 +74,11 @@ const runstate_1 = require("../runstate");
73
74
  const runstate_stats_1 = require("../runstate-stats");
74
75
  const sched_alert_1 = require("../sched-alert");
75
76
  const sched_run_stats_1 = require("../sched-run-stats");
77
+ const sched_service_1 = require("../sched-service");
78
+ const sched_tick_runner_1 = require("../sched-tick-runner");
76
79
  const table_1 = require("../table");
77
80
  const batch_prep_1 = require("../usage/batch-prep");
78
- const sched_service_1 = require("./sched-service");
81
+ const sched_service_2 = require("./sched-service");
79
82
  /**
80
83
  * Batch-worktree `ai-dossier cap run <id>` runner for the per-member
81
84
  * incremental gate (#523 AC2). `cap run`'s `task-failed` outcome — a
@@ -93,13 +96,17 @@ const sched_service_1 = require("./sched-service");
93
96
  */
94
97
  function createBatchCapabilityRunner(opts) {
95
98
  const timeoutMs = opts?.timeoutMs ?? batch_suite_runner_1.BATCH_SUITE_TIMEOUT_MS;
99
+ const inFlightDir = opts?.inFlightDir;
96
100
  return (worktree, capabilityId) => {
97
- const run = (0, cap_envelope_1.spawnCapRun)(capabilityId, worktree, { timeoutMs });
101
+ const run = (0, cap_envelope_1.spawnCapRun)(capabilityId, worktree, {
102
+ timeoutMs,
103
+ ...(inFlightDir !== undefined ? { inFlightDir } : {}),
104
+ });
98
105
  const result = run.spawned;
99
106
  // Captured subprocess output — the evidence fallback on every path below.
100
107
  const captured = `${result.stdout ?? ''}${result.stderr ?? ''}`;
101
108
  if (result.error && !run.exitedWithVerdict) {
102
- // #681: this runner's OWN timeout (spawnSync's, `BATCH_SUITE_TIMEOUT_MS`)
109
+ // #681: this runner's OWN timeout (`BATCH_SUITE_TIMEOUT_MS` — spawnSync's, or the tracked launcher's process-group kill with a synthetic ETIMEDOUT, #1233)
103
110
  // must be distinguishable from a genuine machinery failure — before, it
104
111
  // collapsed into the evidence-free `{outcome: 'automation-broken'}`
105
112
  // below, byte-for-byte the #679 shape. Emit the same
@@ -1233,11 +1240,24 @@ function registerStatusSubcommand(cmd) {
1233
1240
  // — it reads whatever `sched start` last cached rather than risking
1234
1241
  // a multi-second hang on an unreachable npm registry.
1235
1242
  const staleness = await (0, engine_version_1.checkEngineStaleness)({ noFetch: true });
1243
+ // #1233: gates the engine launched and has not consumed — after an
1244
+ // engine death this is where "gate pid N is still running" shows.
1245
+ const gatesInFlight = (0, sched_1.listInFlight)((0, sched_1.inFlightDir)(store.dir));
1236
1246
  if (opts.json) {
1237
- console.log(JSON.stringify({ ...report, engine_staleness: staleness }, null, 2));
1247
+ console.log(JSON.stringify({
1248
+ ...report,
1249
+ engine_staleness: staleness,
1250
+ gates_in_flight: gatesInFlight.map((g) => ({
1251
+ ...g.record,
1252
+ alive: g.alive,
1253
+ finished: g.finished,
1254
+ })),
1255
+ }, null, 2));
1238
1256
  }
1239
1257
  else {
1240
1258
  console.log(renderReport(report, staleness));
1259
+ for (const g of gatesInFlight)
1260
+ console.log(`Gate in flight: ${(0, sched_1.describeInFlight)(g)}`);
1241
1261
  }
1242
1262
  }
1243
1263
  catch (err) {
@@ -1282,8 +1302,9 @@ function resumeBatchGate(opts) {
1282
1302
  exec: (0, sched_1.createExecFn)(sched_1.TEARDOWN_TIMEOUT_MS, {
1283
1303
  onError: (file, args, err) => process.stderr.write(`⚠ sched resume --batch: '${file} ${args.join(' ')}' failed: ${err.message}\n`),
1284
1304
  }),
1285
- runSuite: (0, batch_suite_runner_1.createBatchSuiteRunner)(config),
1286
- runCapability: createBatchCapabilityRunner(),
1305
+ // #1233: adopt (never duplicate) a gate the engine already has in flight here.
1306
+ runSuite: (0, batch_suite_runner_1.createBatchSuiteRunner)(config, { inFlightDir: (0, sched_1.inFlightDir)(store.dir) }),
1307
+ runCapability: createBatchCapabilityRunner({ inFlightDir: (0, sched_1.inFlightDir)(store.dir) }),
1287
1308
  };
1288
1309
  try {
1289
1310
  const result = (0, sched_1.resumeBlockedGate)(deps, config, (0, sched_1.resolveDispatch)(config), opts.batch, new Date());
@@ -2048,6 +2069,90 @@ function reexecUpdatedCli(execveOverride) {
2048
2069
  return false;
2049
2070
  }
2050
2071
  }
2072
+ /**
2073
+ * #1233: the off-thread tick runner for the continuous loop, or null for the
2074
+ * inline tick (`DOSSIER_SCHED_INLINE_TICK=1`, or no compiled worker entry —
2075
+ * running from sources). The inline fallback is announced: it is the mode in
2076
+ * which a long gate freezes the heartbeat and defers signals.
2077
+ */
2078
+ function resolveTickRunner(data) {
2079
+ if (process.env[sched_tick_runner_1.INLINE_TICK_ENV] === '1') {
2080
+ console.error(`⚠ sched: ${sched_tick_runner_1.INLINE_TICK_ENV}=1 — ticks run on the main thread; a long gate freezes the lease heartbeat and defers SIGTERM/SIGINT`);
2081
+ return null;
2082
+ }
2083
+ const file = (0, sched_tick_runner_1.tickWorkerFile)();
2084
+ if (file === null) {
2085
+ console.error('⚠ sched: tick worker entry not found next to the CLI build — ticks run on the main thread');
2086
+ return null;
2087
+ }
2088
+ return (0, sched_tick_runner_1.createWorkerTickRunner)(file, data);
2089
+ }
2090
+ /**
2091
+ * The engine's dependencies for `sched start` — one place, so the inline tick
2092
+ * and the tick worker (#1233, `sched-tick-worker.ts`) build identical engines.
2093
+ */
2094
+ function buildEngineDeps(opts) {
2095
+ const { store, journal, anchorRepo, getTickConfig } = opts;
2096
+ const repoDir = opts.repoDir ?? process.cwd();
2097
+ const gatesInFlightDir = (0, sched_1.inFlightDir)(store.dir);
2098
+ return {
2099
+ store,
2100
+ journal,
2101
+ groundTruth: (0, sched_1.createExecGroundTruth)(undefined, {
2102
+ repoDir,
2103
+ ...(anchorRepo !== undefined ? { repo: anchorRepo } : {}),
2104
+ }),
2105
+ ...(anchorRepo !== undefined ? { anchorRepo } : {}),
2106
+ spawnDeps: (0, sched_1.createSpawnDeps)(repoDir),
2107
+ now: () => new Date(),
2108
+ repoDir,
2109
+ teardownExec: (0, sched_1.createExecFn)(sched_1.TEARDOWN_TIMEOUT_MS, {
2110
+ onError: (file, args, err) => process.stderr.write(`⚠ sched teardown: '${file} ${args.join(' ')}' failed: ${err.message}\n`),
2111
+ }),
2112
+ // #945: takeover rescue (git commit-tree + push of a dead agent's WIP), run
2113
+ // OUTSIDE the state lock. Bounded like every other engine exec; failures
2114
+ // are per-call diagnostics — a failed rescue never blocks the respawn.
2115
+ rescueExec: (0, sched_1.createExecFn)(RESCUE_TIMEOUT_MS, {
2116
+ // A push that wants credentials must fail, never hang on a prompt.
2117
+ env: { GIT_TERMINAL_PROMPT: '0' },
2118
+ onError: (file, args, err) => process.stderr.write(`⚠ sched rescue: '${file} ${args.slice(0, 2).join(' ')}…' failed: ${err.message}\n`),
2119
+ }),
2120
+ // #504: the ladder fences a superseded run before respawning its takeover.
2121
+ // Its own exec rather than the ground-truth one: a fence is a WRITE, and
2122
+ // borrowing `groundTruthExec` would file the only diagnostic for a failed write
2123
+ // under `sched ground truth`, where nobody debugging a fence would look.
2124
+ fencer: (0, sched_1.createExecRunFencer)((0, sched_1.createExecFn)(sched_1.FENCE_TIMEOUT_MS, {
2125
+ onError: (file, args, err) => process.stderr.write(`⚠ sched fence: '${file} ${args.join(' ')}' failed: ${err.message}\n`),
2126
+ }), { repoDir }),
2127
+ // #840: a requeued parked batch member's first dispatch seeds its
2128
+ // resume trail (a `setup done` milestone on its member branch) so the
2129
+ // full-cycle gate resumes ON that branch. A write, so its own exec +
2130
+ // diagnostic prefix, on the fence's budget (two short CLI calls).
2131
+ resumeSeeder: (0, sched_1.createExecResumeSeeder)((0, sched_1.createExecFn)(sched_1.FENCE_TIMEOUT_MS, {
2132
+ onError: (file, args, err) => process.stderr.write(`⚠ sched resume-seed: '${file} ${args.join(' ')}' failed: ${err.message}\n`),
2133
+ }), { repoDir }),
2134
+ // #523: batch git/milestone-CLI operations (worktree claim, commit-range
2135
+ // recording, milestone posting, PR watch) and the aggregate suite runner
2136
+ // that gates `executing → reviewing`. Reuses the same timeout as teardown
2137
+ // (worktree/git ops). The cold-path warm-up install/build (#561) does
2138
+ // NOT reuse this — see `batchWarmExec` below — a real `npm ci`+build
2139
+ // routinely exceeds this budget.
2140
+ batchExec: (0, sched_1.createExecFn)(sched_1.TEARDOWN_TIMEOUT_MS, {
2141
+ onError: (file, args, err) => process.stderr.write(`⚠ sched batch: '${file} ${args.join(' ')}' failed: ${err.message}\n`),
2142
+ }),
2143
+ // #561: batch-setup's cold-path warm-up (install + build) on its own
2144
+ // budget, matching `@ai-dossier/worktree-pool`'s own per-issue warm-up
2145
+ // budget for the identical work (`WARM_COMMAND_TIMEOUT_MS`) rather than
2146
+ // the git-op-tuned `batchExec` above.
2147
+ batchWarmExec: (0, sched_1.createExecFn)(worktree_pool_1.WARM_COMMAND_TIMEOUT_MS, {
2148
+ onError: (file, args, err) => process.stderr.write(`⚠ sched batch warm-up: '${file} ${args.join(' ')}' failed: ${err.message}\n`),
2149
+ }),
2150
+ // One config per tick: the suite runner reads the snapshot the tick started with.
2151
+ // #1233: engine gates are tracked in the in-flight registry (adopted, never duplicated).
2152
+ runBatchSuite: (0, batch_suite_runner_1.createBatchSuiteRunner)(getTickConfig, { inFlightDir: gatesInFlightDir }),
2153
+ runBatchCapability: createBatchCapabilityRunner({ inFlightDir: gatesInFlightDir }),
2154
+ };
2155
+ }
2051
2156
  function registerStartSubcommand(cmd) {
2052
2157
  cmd
2053
2158
  .command('start')
@@ -2057,6 +2162,7 @@ function registerStartSubcommand(cmd) {
2057
2162
  .option('--once', 'Run a single reconcile+refill tick and exit (cron-style)')
2058
2163
  .option('--auto-upgrade', 'After a completed tick, install and re-exec @ai-dossier/cli@latest when this engine is stale')
2059
2164
  .option('--alert-issue <n>', 'Comment on this issue when the engine restarts after a crash (#945); default $DOSSIER_SCHED_ALERT_ISSUE. The alert is always journaled and printed to stderr')
2165
+ .option('--unsupervised', 'Acknowledge running without a supervisor (no `sched service install`) and silence the warning (#1233)')
2060
2166
  .option('--project <slug>', 'Project slug (default: owner-repo of the current directory)')
2061
2167
  .option('--json', 'Output tick results as JSON')
2062
2168
  .action(async (opts) => {
@@ -2119,7 +2225,7 @@ function registerStartSubcommand(cmd) {
2119
2225
  if (acquisition.reclaimed) {
2120
2226
  (0, sched_1.reportCrashRestart)(journalAtStart, acquisition.reclaimed, (0, sched_alert_1.createAlertNotifier)(project, () => (0, sched_1.resolveProjectRepo)(project, sched_1.defaultExec) ?? undefined, alertIssue, { stateDir: store.dir }), new Date(), previousStop !== null && previousStop.pid === acquisition.reclaimed.pid
2121
2227
  ? previousStop
2122
- : null);
2228
+ : null, { stateDir: store.dir });
2123
2229
  }
2124
2230
  try {
2125
2231
  let config;
@@ -2228,62 +2334,12 @@ function registerStartSubcommand(cmd) {
2228
2334
  if (anchorRepo === undefined && !(opts.once && opts.json)) {
2229
2335
  console.log(`▶ sched anchor close: off — the current directory is not ${project}'s GitHub repository`);
2230
2336
  }
2231
- const deps = {
2337
+ const deps = buildEngineDeps({
2232
2338
  store,
2233
2339
  journal: engineJournal,
2234
- groundTruth: (0, sched_1.createExecGroundTruth)(undefined, {
2235
- repoDir: process.cwd(),
2236
- ...(anchorRepo !== undefined ? { repo: anchorRepo } : {}),
2237
- }),
2238
- ...(anchorRepo !== undefined ? { anchorRepo } : {}),
2239
- spawnDeps: (0, sched_1.createSpawnDeps)(process.cwd()),
2240
- now: () => new Date(),
2241
- repoDir: process.cwd(),
2242
- teardownExec: (0, sched_1.createExecFn)(sched_1.TEARDOWN_TIMEOUT_MS, {
2243
- onError: (file, args, err) => process.stderr.write(`⚠ sched teardown: '${file} ${args.join(' ')}' failed: ${err.message}\n`),
2244
- }),
2245
- // #945: takeover rescue (git commit-tree + push of a dead agent's WIP), run
2246
- // OUTSIDE the state lock. Bounded like every other engine exec; failures
2247
- // are per-call diagnostics — a failed rescue never blocks the respawn.
2248
- rescueExec: (0, sched_1.createExecFn)(RESCUE_TIMEOUT_MS, {
2249
- // A push that wants credentials must fail, never hang on a prompt.
2250
- env: { GIT_TERMINAL_PROMPT: '0' },
2251
- onError: (file, args, err) => process.stderr.write(`⚠ sched rescue: '${file} ${args.slice(0, 2).join(' ')}…' failed: ${err.message}\n`),
2252
- }),
2253
- // #504: the ladder fences a superseded run before respawning its takeover.
2254
- // Its own exec rather than the ground-truth one: a fence is a WRITE, and
2255
- // borrowing `groundTruthExec` would file the only diagnostic for a failed write
2256
- // under `sched ground truth`, where nobody debugging a fence would look.
2257
- fencer: (0, sched_1.createExecRunFencer)((0, sched_1.createExecFn)(sched_1.FENCE_TIMEOUT_MS, {
2258
- onError: (file, args, err) => process.stderr.write(`⚠ sched fence: '${file} ${args.join(' ')}' failed: ${err.message}\n`),
2259
- }), { repoDir: process.cwd() }),
2260
- // #840: a requeued parked batch member's first dispatch seeds its
2261
- // resume trail (a `setup done` milestone on its member branch) so the
2262
- // full-cycle gate resumes ON that branch. A write, so its own exec +
2263
- // diagnostic prefix, on the fence's budget (two short CLI calls).
2264
- resumeSeeder: (0, sched_1.createExecResumeSeeder)((0, sched_1.createExecFn)(sched_1.FENCE_TIMEOUT_MS, {
2265
- onError: (file, args, err) => process.stderr.write(`⚠ sched resume-seed: '${file} ${args.join(' ')}' failed: ${err.message}\n`),
2266
- }), { repoDir: process.cwd() }),
2267
- // #523: batch git/milestone-CLI operations (worktree claim, commit-range
2268
- // recording, milestone posting, PR watch) and the aggregate suite runner
2269
- // that gates `executing → reviewing`. Reuses the same timeout as teardown
2270
- // (worktree/git ops). The cold-path warm-up install/build (#561) does
2271
- // NOT reuse this — see `batchWarmExec` below — a real `npm ci`+build
2272
- // routinely exceeds this budget.
2273
- batchExec: (0, sched_1.createExecFn)(sched_1.TEARDOWN_TIMEOUT_MS, {
2274
- onError: (file, args, err) => process.stderr.write(`⚠ sched batch: '${file} ${args.join(' ')}' failed: ${err.message}\n`),
2275
- }),
2276
- // #561: batch-setup's cold-path warm-up (install + build) on its own
2277
- // budget, matching `@ai-dossier/worktree-pool`'s own per-issue warm-up
2278
- // budget for the identical work (`WARM_COMMAND_TIMEOUT_MS`) rather than
2279
- // the git-op-tuned `batchExec` above.
2280
- batchWarmExec: (0, sched_1.createExecFn)(worktree_pool_1.WARM_COMMAND_TIMEOUT_MS, {
2281
- onError: (file, args, err) => process.stderr.write(`⚠ sched batch warm-up: '${file} ${args.join(' ')}' failed: ${err.message}\n`),
2282
- }),
2283
- // One config per tick: the suite runner reads the snapshot the tick started with.
2284
- runBatchSuite: (0, batch_suite_runner_1.createBatchSuiteRunner)(() => tickConfig),
2285
- runBatchCapability: createBatchCapabilityRunner(),
2286
- };
2340
+ anchorRepo,
2341
+ getTickConfig: () => tickConfig,
2342
+ });
2287
2343
  const describe = (result) => {
2288
2344
  const parts = [];
2289
2345
  if (result.spawned.length > 0)
@@ -2368,8 +2424,23 @@ function registerStartSubcommand(cmd) {
2368
2424
  exitLogger.logNormalExit('once tick complete');
2369
2425
  return;
2370
2426
  }
2427
+ // #1233: an unsupervised engine dies for good the first time anything
2428
+ // kills it (the 2026-09-29 engine sat dead for 15 days). Say so loudly.
2429
+ if (!opts.unsupervised) {
2430
+ const warning = (0, sched_service_1.unsupervisedWarning)(project, () => (0, sched_service_1.serviceStatus)(project, (0, sched_service_2.serviceIo)()));
2431
+ if (warning !== null)
2432
+ console.error(warning);
2433
+ }
2371
2434
  const interval = (engineConfig.reconcile_interval_ms ?? sched_1.DEFAULT_RECONCILE_INTERVAL_MS) / 1000;
2372
2435
  console.log(`▶ Scheduler engine running for ${project} (tick every ${interval}s, Ctrl-C to stop)`);
2436
+ // #1233: run each tick in a worker thread so this event loop stays free
2437
+ // during a long gate — the lease keeps its heartbeat and SIGTERM/SIGINT
2438
+ // are handled (and journaled) instead of piling up behind a spawnSync.
2439
+ const tickRunner = resolveTickRunner({
2440
+ stateDir: store.dir,
2441
+ repoDir: process.cwd(),
2442
+ ...(anchorRepo !== undefined ? { anchorRepo } : {}),
2443
+ });
2373
2444
  let upgradeHandoff = false;
2374
2445
  await (0, sched_1.runLoop)(deps, () => {
2375
2446
  tickConfig = configReloader.current();
@@ -2393,7 +2464,8 @@ function registerStartSubcommand(cmd) {
2393
2464
  upgradeHandoff = true;
2394
2465
  stopping = true;
2395
2466
  }
2396
- });
2467
+ }, tickRunner !== null ? { runTick: (cfg) => tickRunner.runTick(cfg) } : {});
2468
+ await tickRunner?.close();
2397
2469
  if (upgradeHandoff) {
2398
2470
  exitLogger.logNormalExit('auto-upgrade handoff');
2399
2471
  releaseLease();
@@ -2427,6 +2499,6 @@ function registerSchedCommand(program) {
2427
2499
  registerReprioritizeSubcommand(schedCmd);
2428
2500
  registerStartSubcommand(schedCmd);
2429
2501
  registerStatsSubcommand(schedCmd);
2430
- (0, sched_service_1.registerSchedServiceCommands)(schedCmd);
2502
+ (0, sched_service_2.registerSchedServiceCommands)(schedCmd);
2431
2503
  }
2432
2504
  //# sourceMappingURL=sched.js.map