@miller-tech/uap 1.161.24 → 1.162.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.
@@ -217,6 +217,37 @@ export declare function hasExplicitAidFlags(options: DeliverOptions): boolean;
217
217
  * filling only fields the user left unset. Exported for tests.
218
218
  */
219
219
  export declare function applyAutoPlan(options: DeliverOptions, plan: AutoPlan): void;
220
+ /**
221
+ * Default wedge timeout (seconds). Override with UAP_DELIVER_WEDGE_TIMEOUT.
222
+ * Deliberately generous (30 min): the heartbeat only advances between
223
+ * convergence iterations, and one turn on a slow local model (generation + the
224
+ * full gate ladder) can legitimately take many minutes. The timeout must sit
225
+ * comfortably above the worst-case single-turn latency so a healthy-but-slow
226
+ * run is never mistaken for a wedged one and reclaimed out from under itself.
227
+ */
228
+ export declare const DEFAULT_WEDGE_TIMEOUT_S = 1800;
229
+ /** Resolve the wedge timeout in seconds (env override, else the default). */
230
+ export declare function wedgeTimeoutS(): number;
231
+ /**
232
+ * Stamp the current epoch-seconds into `.uap/deliver.heartbeat`, ATOMICALLY
233
+ * (write a temp file then rename over the target). A plain truncate-in-place
234
+ * write leaves the file momentarily empty, and a concurrent reader would parse
235
+ * `""` as 0 (epoch 1970) and wrongly conclude the holder is wedged. Best-effort.
236
+ */
237
+ export declare function updateDeliverHeartbeat(projectRoot: string): void;
238
+ /**
239
+ * Read the heartbeat epoch-seconds, or null if absent/unreadable/non-positive.
240
+ * Rejecting non-positive values guards against a torn read (empty file parses
241
+ * to 0) being treated as a real 1970 timestamp.
242
+ */
243
+ export declare function readDeliverHeartbeat(projectRoot: string): number | null;
244
+ /**
245
+ * True when the lock holder is alive but WEDGED — its heartbeat is older than
246
+ * the wedge timeout. A MISSING heartbeat is not wedged (the holder may be
247
+ * starting up), so this returns false in that case and the caller keeps
248
+ * deferring to PID-liveness.
249
+ */
250
+ export declare function isDeliverHolderWedged(projectRoot: string, nowS?: number): boolean;
220
251
  /**
221
252
  * Project-level deliver concurrency lock. Prevents a fan-out where an impatient
222
253
  * caller (e.g. a model that launched `uap deliver` for some work, didn't wait
@@ -1 +1 @@
1
- {"version":3,"file":"deliver.d.ts","sourceRoot":"","sources":["../../src/cli/deliver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"deliver.d.ts","sourceRoot":"","sources":["../../src/cli/deliver.ts"],"names":[],"mappings":"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"}
@@ -7,8 +7,8 @@
7
7
  */
8
8
  import chalk from 'chalk';
9
9
  import { spawnSync } from 'child_process';
10
- import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeSync, closeSync } from 'fs';
11
- import { join, resolve } from 'path';
10
+ import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeSync, closeSync, renameSync } from 'fs';
11
+ import { join, resolve, relative } from 'path';
12
12
  import { ConvergenceLoop, composeIterationHooks } from '../delivery/convergence-loop.js';
13
13
  import { createModelJudge } from '../delivery/judge.js';
14
14
  import { createModelCritic } from '../delivery/critic.js';
@@ -65,7 +65,7 @@ export function resolveEvaluatorPreset(opts) {
65
65
  export { resolveAcceptanceVerdict } from '../delivery/mission-acceptance.js';
66
66
  import { createAgenticExecutor, noopApplier, selectExecutorMode, lockContractFiles } from '../delivery/agentic-executor.js';
67
67
  import { createRepairEscalation } from '../delivery/repair-escalation.js';
68
- import { clearStop, isStopRequested, loadRunState, newRunId, saveRunState, MAX_PERSISTED_PHASES } from '../delivery/run-state.js';
68
+ import { clearStop, isStopRequested, requestStop, loadRunState, newRunId, saveRunState, MAX_PERSISTED_PHASES } from '../delivery/run-state.js';
69
69
  import { planDeliveryPhases, shouldDecompose } from '../delivery/decompose.js';
70
70
  import { initLedger, markItem } from '../delivery/completion-ledger.js';
71
71
  import { loadUapConfigRaw } from '../utils/config-loader.js';
@@ -263,6 +263,73 @@ export function applyAutoPlan(options, plan) {
263
263
  if (plan.deployDev && options.deployDev === undefined)
264
264
  options.deployDev = true;
265
265
  }
266
+ // ─── Deliver wedge detection (P0 reliability) ────────────────────────────────
267
+ // A deliver can wedge (alive PID, but stuck making no progress — e.g. a
268
+ // plan-check re-plan loop or a hung upstream). The lock alone can't tell a
269
+ // healthy long run from a wedged one, so the running deliver stamps a heartbeat
270
+ // on every iteration and the lock path reclaims a holder whose heartbeat has
271
+ // gone stale. Language-agnostic contract: `.uap/deliver.heartbeat` = one integer
272
+ // (unix epoch seconds), rewritten each turn; also read by deliver_autoroute.py.
273
+ /**
274
+ * Default wedge timeout (seconds). Override with UAP_DELIVER_WEDGE_TIMEOUT.
275
+ * Deliberately generous (30 min): the heartbeat only advances between
276
+ * convergence iterations, and one turn on a slow local model (generation + the
277
+ * full gate ladder) can legitimately take many minutes. The timeout must sit
278
+ * comfortably above the worst-case single-turn latency so a healthy-but-slow
279
+ * run is never mistaken for a wedged one and reclaimed out from under itself.
280
+ */
281
+ export const DEFAULT_WEDGE_TIMEOUT_S = 1800;
282
+ /** Resolve the wedge timeout in seconds (env override, else the default). */
283
+ export function wedgeTimeoutS() {
284
+ const raw = process.env.UAP_DELIVER_WEDGE_TIMEOUT;
285
+ const n = raw !== undefined ? Number(raw) : NaN;
286
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_WEDGE_TIMEOUT_S;
287
+ }
288
+ /**
289
+ * Stamp the current epoch-seconds into `.uap/deliver.heartbeat`, ATOMICALLY
290
+ * (write a temp file then rename over the target). A plain truncate-in-place
291
+ * write leaves the file momentarily empty, and a concurrent reader would parse
292
+ * `""` as 0 (epoch 1970) and wrongly conclude the holder is wedged. Best-effort.
293
+ */
294
+ export function updateDeliverHeartbeat(projectRoot) {
295
+ try {
296
+ const dir = join(projectRoot, '.uap');
297
+ mkdirSync(dir, { recursive: true });
298
+ const hbPath = join(dir, 'deliver.heartbeat');
299
+ const tmp = `${hbPath}.${process.pid}.tmp`;
300
+ const fd = openSync(tmp, 'w');
301
+ writeSync(fd, String(Math.floor(Date.now() / 1000)));
302
+ closeSync(fd);
303
+ renameSync(tmp, hbPath);
304
+ }
305
+ catch { /* non-fatal — lock path falls back to PID-liveness only */ }
306
+ }
307
+ /**
308
+ * Read the heartbeat epoch-seconds, or null if absent/unreadable/non-positive.
309
+ * Rejecting non-positive values guards against a torn read (empty file parses
310
+ * to 0) being treated as a real 1970 timestamp.
311
+ */
312
+ export function readDeliverHeartbeat(projectRoot) {
313
+ try {
314
+ const v = Number(readFileSync(join(projectRoot, '.uap', 'deliver.heartbeat'), 'utf8').trim());
315
+ return Number.isFinite(v) && v > 0 ? v : null;
316
+ }
317
+ catch {
318
+ return null;
319
+ }
320
+ }
321
+ /**
322
+ * True when the lock holder is alive but WEDGED — its heartbeat is older than
323
+ * the wedge timeout. A MISSING heartbeat is not wedged (the holder may be
324
+ * starting up), so this returns false in that case and the caller keeps
325
+ * deferring to PID-liveness.
326
+ */
327
+ export function isDeliverHolderWedged(projectRoot, nowS = Math.floor(Date.now() / 1000)) {
328
+ const hb = readDeliverHeartbeat(projectRoot);
329
+ if (hb === null)
330
+ return false;
331
+ return nowS - hb > wedgeTimeoutS();
332
+ }
266
333
  /**
267
334
  * Project-level deliver concurrency lock. Prevents a fan-out where an impatient
268
335
  * caller (e.g. a model that launched `uap deliver` for some work, didn't wait
@@ -291,8 +358,9 @@ export function acquireDeliverLock(projectRoot) {
291
358
  // Reclaim a stale lock (holder no longer alive).
292
359
  if (existsSync(lockPath)) {
293
360
  const held = Number((readFileSync(lockPath, 'utf8').split('|')[0] || '').trim());
294
- if (held && held !== process.pid && pidAlive(held))
295
- return null; // live holder
361
+ // Defer only to a live holder that is NOT wedged; reclaim a stuck one.
362
+ if (held && held !== process.pid && pidAlive(held) && !isDeliverHolderWedged(projectRoot))
363
+ return null;
296
364
  try {
297
365
  rmSync(lockPath);
298
366
  }
@@ -308,6 +376,10 @@ export function acquireDeliverLock(projectRoot) {
308
376
  }
309
377
  writeSync(fd, `${process.pid}|${new Date().toISOString()}`);
310
378
  closeSync(fd);
379
+ // Stamp a fresh heartbeat immediately so this brand-new holder is never
380
+ // classified as wedged in the window before its first iteration (and so a
381
+ // reclaim overwrites the previous holder's stale heartbeat).
382
+ updateDeliverHeartbeat(projectRoot);
311
383
  }
312
384
  catch {
313
385
  // Lock machinery unavailable — fail OPEN (don't block a legitimate run).
@@ -413,7 +485,7 @@ async function runDeliver(instruction, options) {
413
485
  if (releaseLock === null) {
414
486
  let holder = '';
415
487
  try {
416
- holder = readFileSync(join(projectRoot, '.uap', 'deliver.lock'), 'utf8').split('|')[0];
488
+ holder = stripControl(readFileSync(join(projectRoot, '.uap', 'deliver.lock'), 'utf8').split('|')[0]);
417
489
  }
418
490
  catch { /* gone already */ }
419
491
  console.log(chalk.yellow(`↩ deliver already running for this project${holder ? ` (pid ${holder.trim()})` : ''} — ` +
@@ -424,6 +496,14 @@ async function runDeliver(instruction, options) {
424
496
  }
425
497
  // The lock is released via acquireDeliverLock's process-exit handler (the
426
498
  // deliver CLI is a one-shot process), and explicitly on early returns below.
499
+ // ── Wedge handling (P0 reliability) ─────────────────────────────────────
500
+ // A stuck deliver (e.g. a plan-check re-plan loop) no longer blocks future
501
+ // runs: the heartbeat is refreshed on every iteration (see the loop hook
502
+ // below) and acquireDeliverLock PASSIVELY reclaims a holder whose heartbeat
503
+ // has gone stale past the wedge timeout. We deliberately do NOT self-abort via
504
+ // process.exit here — an abrupt teardown skips snapshot/worktree disposal
505
+ // (re-opening the known tmpfs-leak class), and a merely-slow turn must never
506
+ // kill a healthy run. Passive reclaim frees the lock without either hazard.
427
507
  // Durable runs: `--resume <id|latest>` restores an interrupted mission —
428
508
  // instruction, phase cursor, and the loop checkpoint — and continues it.
429
509
  let resumeState = null;
@@ -974,10 +1054,16 @@ async function runDeliver(instruction, options) {
974
1054
  // read-only for later epics. Mutable by reference — the epic controller
975
1055
  // grows it between epics; the executor reads it live on every tool call.
976
1056
  const contractLock = new Set();
1057
+ // ORIGINAL-cased locked contract paths (contractLock stores lowercased match
1058
+ // keys, which are NOT valid FS paths on a case-sensitive filesystem — reading
1059
+ // those back would silently drop PascalCase files like Config.ts). P1 reads
1060
+ // these for verbatim injection.
1061
+ const contractPaths = [];
977
1062
  const executor = agentic
978
1063
  ? createAgenticExecutor(model, {
979
1064
  projectRoot,
980
1065
  endpoint: agenticEndpoint,
1066
+ onToolProgress: () => updateDeliverHeartbeat(projectRoot), // #2a per-tool-call heartbeat
981
1067
  temperature,
982
1068
  contextTokenBudget: sessionBudget,
983
1069
  contractFiles: contractLock,
@@ -998,6 +1084,7 @@ async function runDeliver(instruction, options) {
998
1084
  ? createAgenticExecutor(repairModel, {
999
1085
  projectRoot,
1000
1086
  endpoint: agenticEndpoint,
1087
+ onToolProgress: () => updateDeliverHeartbeat(projectRoot), // #2a per-tool-call heartbeat
1001
1088
  temperature: 0.2,
1002
1089
  contextTokenBudget: sessionBudget,
1003
1090
  contractFiles: contractLock,
@@ -1286,7 +1373,12 @@ async function runDeliver(instruction, options) {
1286
1373
  errorTail),
1287
1374
  })
1288
1375
  : undefined;
1289
- return composeIterationHooks((record) => printProgress(record), (record) => {
1376
+ return composeIterationHooks((record) => printProgress(record), (_record) => {
1377
+ // P0 wedge watchdog: each iteration is real progress — refresh the
1378
+ // heartbeat so the watchdog + lock wedge-reclaim only fire on a true stall.
1379
+ updateDeliverHeartbeat(projectRoot);
1380
+ return undefined;
1381
+ }, (record) => {
1290
1382
  // Feed the acceptance breaker's zero-diff guard.
1291
1383
  specs.recordWrites(evidenceRoot, record.filesApplied.length);
1292
1384
  return undefined;
@@ -1363,6 +1455,32 @@ async function runDeliver(instruction, options) {
1363
1455
  };
1364
1456
  // ---- Durable run + optional mission decomposition (fable-style) ----
1365
1457
  const runId = resumeState?.runId ?? newRunId();
1458
+ // #2b: safe cooperative wedge watchdog. If this run stops making tool progress
1459
+ // (heartbeat goes stale past the wedge timeout — #2a stamps it per tool call),
1460
+ // request a COOPERATIVE stop rather than process.exit: the convergence loop's
1461
+ // shouldStop picks it up at the next turn boundary and unwinds through the
1462
+ // normal finally (snapshot/worktree/lock cleanup all run). This lets a
1463
+ // genuinely-wedged run free its lock and exit instead of lingering as an idle
1464
+ // zombie, WITHOUT the abrupt-teardown resource leak the earlier process.exit
1465
+ // watchdog risked. Off-switch rides the existing UAP_DELIVER_NO_LOCK=1.
1466
+ let wedgeWatchdog = null;
1467
+ if (!options.dryRun && process.env.UAP_DELIVER_NO_LOCK !== '1') {
1468
+ // Fresh baseline at arm time so the watchdog never fires on a run that just
1469
+ // started but hasn't stamped yet — critically on --resume (which does NOT
1470
+ // acquire the lock, so acquireDeliverLock's stamp never ran and the on-disk
1471
+ // heartbeat still holds the PRIOR interrupted run's timestamp).
1472
+ updateDeliverHeartbeat(projectRoot);
1473
+ const wedgeS = wedgeTimeoutS();
1474
+ wedgeWatchdog = setInterval(() => {
1475
+ if (isDeliverHolderWedged(projectRoot) && !isStopRequested(projectRoot, runId)) {
1476
+ console.error(chalk.yellow(`⏱ wedge watchdog: no tool progress for ${wedgeS}s — requesting a clean stop so the lock is released.`));
1477
+ requestStop(projectRoot, runId);
1478
+ }
1479
+ }, Math.min(30_000, Math.max(1000, wedgeS * 1000)));
1480
+ wedgeWatchdog.unref();
1481
+ process.once('exit', () => { if (wedgeWatchdog)
1482
+ clearInterval(wedgeWatchdog); });
1483
+ }
1366
1484
  let phases = resumeState?.phases;
1367
1485
  let phaseIndex = resumeState?.phaseIndex ?? 0;
1368
1486
  const phaseSummaries = [...(resumeState?.phaseSummaries ?? [])];
@@ -1645,6 +1763,12 @@ async function runDeliver(instruction, options) {
1645
1763
  ? createAgenticExecutor(model, {
1646
1764
  projectRoot: root,
1647
1765
  endpoint: agenticEndpoint,
1766
+ // #2a per-tool-call heartbeat. Stamp projectRoot (NOT the
1767
+ // per-task `root`): the deliver heartbeat is a project-global
1768
+ // wedge signal the watchdog + lock reclaim read at projectRoot;
1769
+ // stamping `root` here would leave projectRoot's advancing only
1770
+ // per-turn, re-opening the false-wedge window for isolated tasks.
1771
+ onToolProgress: () => updateDeliverHeartbeat(projectRoot),
1648
1772
  temperature,
1649
1773
  contextTokenBudget: sessionBudget,
1650
1774
  contractFiles: contractLock,
@@ -1822,7 +1946,31 @@ async function runDeliver(instruction, options) {
1822
1946
  ledgerInit: (items) => initLedger(projectRoot, instruction, items.map((i) => ({ ...i, kind: 'epic' }))),
1823
1947
  ledgerMark: (id, status, noteText) => markItem(projectRoot, id, status, noteText),
1824
1948
  lockedContracts: () => [...contractLock],
1825
- lockContracts: (files) => lockContractFiles(contractLock, projectRoot, files),
1949
+ lockContracts: (files) => {
1950
+ const locked = lockContractFiles(contractLock, projectRoot, files);
1951
+ for (const p of locked)
1952
+ if (!contractPaths.includes(p))
1953
+ contractPaths.push(p);
1954
+ return locked;
1955
+ },
1956
+ // P1: concatenate the locked contract files' CONTENTS (original-cased
1957
+ // paths) for verbatim injection into later epics (build against the EXACT
1958
+ // shared surface). Reads are contained to the project root.
1959
+ readContractFiles: () => {
1960
+ if (contractPaths.length === 0)
1961
+ return null;
1962
+ const parts = [];
1963
+ for (const rel of contractPaths) {
1964
+ const abs = resolve(projectRoot, rel);
1965
+ if (relative(projectRoot, abs).startsWith('..'))
1966
+ continue; // never read outside the project
1967
+ try {
1968
+ parts.push(`// ===== ${rel} =====\n${readFileSync(abs, 'utf8')}`);
1969
+ }
1970
+ catch { /* a locked path that vanished — skip it */ }
1971
+ }
1972
+ return parts.length > 0 ? parts.join('\n\n') : null;
1973
+ },
1826
1974
  maxAttemptsPerEpic: Number(process.env.UAP_DELIVER_EPIC_ATTEMPTS ?? 3), // (#4b) 2→3
1827
1975
  // (#4c) Recursive split depth: a huge epic that still can't land after
1828
1976
  // a split is split again, one level shallower — bounded.
@@ -1894,7 +2042,7 @@ async function runDeliver(instruction, options) {
1894
2042
  redetectRungs: true,
1895
2043
  redetectFilter: loopConfig.redetectFilter,
1896
2044
  protectTests: options.protectTests,
1897
- onIteration: (record) => printProgress(record),
2045
+ onIteration: (record) => { updateDeliverHeartbeat(projectRoot); printProgress(record); },
1898
2046
  }, executor, seams);
1899
2047
  const lazyResult = await lazyLoop.deliver(instruction);
1900
2048
  if (lazyResult.success) {
@@ -2060,6 +2208,13 @@ async function runDeliver(instruction, options) {
2060
2208
  // The loop is over: from here a normal exit is expected, so stop treating a
2061
2209
  // signal as a mystery death.
2062
2210
  disposeExitRecorder();
2211
+ // Stop the wedge watchdog on the normal completion path too (not only on
2212
+ // process exit) — avoids a stray requestStop on a finished runId and, for
2213
+ // repeated in-process callers, interval/listener accumulation.
2214
+ if (wedgeWatchdog) {
2215
+ clearInterval(wedgeWatchdog);
2216
+ wedgeWatchdog = null;
2217
+ }
2063
2218
  }
2064
2219
  // --keep-best: if deliver left the project worse than it started (by real
2065
2220
  // gate score), roll back to the snapshot so the run is never a regression.