@miller-tech/uap 1.161.23 → 1.162.0

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';
@@ -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,6 +1054,11 @@ 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,
@@ -1286,7 +1371,12 @@ async function runDeliver(instruction, options) {
1286
1371
  errorTail),
1287
1372
  })
1288
1373
  : undefined;
1289
- return composeIterationHooks((record) => printProgress(record), (record) => {
1374
+ return composeIterationHooks((record) => printProgress(record), (_record) => {
1375
+ // P0 wedge watchdog: each iteration is real progress — refresh the
1376
+ // heartbeat so the watchdog + lock wedge-reclaim only fire on a true stall.
1377
+ updateDeliverHeartbeat(projectRoot);
1378
+ return undefined;
1379
+ }, (record) => {
1290
1380
  // Feed the acceptance breaker's zero-diff guard.
1291
1381
  specs.recordWrites(evidenceRoot, record.filesApplied.length);
1292
1382
  return undefined;
@@ -1822,7 +1912,31 @@ async function runDeliver(instruction, options) {
1822
1912
  ledgerInit: (items) => initLedger(projectRoot, instruction, items.map((i) => ({ ...i, kind: 'epic' }))),
1823
1913
  ledgerMark: (id, status, noteText) => markItem(projectRoot, id, status, noteText),
1824
1914
  lockedContracts: () => [...contractLock],
1825
- lockContracts: (files) => lockContractFiles(contractLock, projectRoot, files),
1915
+ lockContracts: (files) => {
1916
+ const locked = lockContractFiles(contractLock, projectRoot, files);
1917
+ for (const p of locked)
1918
+ if (!contractPaths.includes(p))
1919
+ contractPaths.push(p);
1920
+ return locked;
1921
+ },
1922
+ // P1: concatenate the locked contract files' CONTENTS (original-cased
1923
+ // paths) for verbatim injection into later epics (build against the EXACT
1924
+ // shared surface). Reads are contained to the project root.
1925
+ readContractFiles: () => {
1926
+ if (contractPaths.length === 0)
1927
+ return null;
1928
+ const parts = [];
1929
+ for (const rel of contractPaths) {
1930
+ const abs = resolve(projectRoot, rel);
1931
+ if (relative(projectRoot, abs).startsWith('..'))
1932
+ continue; // never read outside the project
1933
+ try {
1934
+ parts.push(`// ===== ${rel} =====\n${readFileSync(abs, 'utf8')}`);
1935
+ }
1936
+ catch { /* a locked path that vanished — skip it */ }
1937
+ }
1938
+ return parts.length > 0 ? parts.join('\n\n') : null;
1939
+ },
1826
1940
  maxAttemptsPerEpic: Number(process.env.UAP_DELIVER_EPIC_ATTEMPTS ?? 3), // (#4b) 2→3
1827
1941
  // (#4c) Recursive split depth: a huge epic that still can't land after
1828
1942
  // a split is split again, one level shallower — bounded.
@@ -1894,7 +2008,7 @@ async function runDeliver(instruction, options) {
1894
2008
  redetectRungs: true,
1895
2009
  redetectFilter: loopConfig.redetectFilter,
1896
2010
  protectTests: options.protectTests,
1897
- onIteration: (record) => printProgress(record),
2011
+ onIteration: (record) => { updateDeliverHeartbeat(projectRoot); printProgress(record); },
1898
2012
  }, executor, seams);
1899
2013
  const lazyResult = await lazyLoop.deliver(instruction);
1900
2014
  if (lazyResult.success) {