@miller-tech/uap 1.186.2 → 1.187.3

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 (55) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/bin/cli.js +4 -0
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/cli/deliver.d.ts +100 -0
  5. package/dist/cli/deliver.d.ts.map +1 -1
  6. package/dist/cli/deliver.js +507 -20
  7. package/dist/cli/deliver.js.map +1 -1
  8. package/dist/coordination/reactor.d.ts.map +1 -1
  9. package/dist/coordination/reactor.js +28 -5
  10. package/dist/coordination/reactor.js.map +1 -1
  11. package/dist/delivery/agentic-executor.d.ts +3 -3
  12. package/dist/delivery/agentic-executor.d.ts.map +1 -1
  13. package/dist/delivery/agentic-executor.js +74 -4
  14. package/dist/delivery/agentic-executor.js.map +1 -1
  15. package/dist/delivery/convergence-loop.d.ts +6 -0
  16. package/dist/delivery/convergence-loop.d.ts.map +1 -1
  17. package/dist/delivery/convergence-loop.js +31 -0
  18. package/dist/delivery/convergence-loop.js.map +1 -1
  19. package/dist/delivery/edit-match.d.ts.map +1 -1
  20. package/dist/delivery/edit-match.js +19 -1
  21. package/dist/delivery/edit-match.js.map +1 -1
  22. package/dist/delivery/epic-controller.d.ts +6 -0
  23. package/dist/delivery/epic-controller.d.ts.map +1 -1
  24. package/dist/delivery/epic-controller.js +10 -0
  25. package/dist/delivery/epic-controller.js.map +1 -1
  26. package/dist/delivery/epic-mission.d.ts.map +1 -1
  27. package/dist/delivery/epic-mission.js +3 -0
  28. package/dist/delivery/epic-mission.js.map +1 -1
  29. package/dist/delivery/verifier-ladder.d.ts +99 -6
  30. package/dist/delivery/verifier-ladder.d.ts.map +1 -1
  31. package/dist/delivery/verifier-ladder.js +208 -25
  32. package/dist/delivery/verifier-ladder.js.map +1 -1
  33. package/dist/mcp-router/tools/deliver.d.ts.map +1 -1
  34. package/dist/mcp-router/tools/deliver.js +17 -0
  35. package/dist/mcp-router/tools/deliver.js.map +1 -1
  36. package/dist/models/long-fetch.d.ts +19 -0
  37. package/dist/models/long-fetch.d.ts.map +1 -1
  38. package/dist/models/long-fetch.js +47 -0
  39. package/dist/models/long-fetch.js.map +1 -1
  40. package/dist/telemetry/tool-failure.d.ts +9 -0
  41. package/dist/telemetry/tool-failure.d.ts.map +1 -1
  42. package/dist/telemetry/tool-failure.js +4 -0
  43. package/dist/telemetry/tool-failure.js.map +1 -1
  44. package/docs/reference/CLI.md +13 -2
  45. package/docs/reference/CONFIGURATION.md +2 -0
  46. package/package.json +2 -2
  47. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  48. package/src/policies/enforcers/enforcement_infra_protect.py +68 -5
  49. package/src/policies/enforcers/enforcement_self_protect.py +36 -0
  50. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  51. package/templates/hooks/pre-tool-use-bash.sh +10 -2
  52. package/templates/hooks/uap-policy-gate.sh +4 -1
  53. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  54. package/tools/agents/scripts/anthropic_proxy.py +101 -9
  55. package/tools/agents/tests/test_repeat_call_guard.py +228 -0
@@ -7,7 +7,9 @@
7
7
  */
8
8
  import chalk from 'chalk';
9
9
  import { spawnSync } from 'child_process';
10
- import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeSync, closeSync, readdirSync, statSync } from 'fs';
10
+ import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeSync, writeFileSync, renameSync, realpathSync, closeSync, readdirSync, statSync } from 'fs';
11
+ import { homedir } from 'os';
12
+ import { createHash } from 'crypto';
11
13
  import { join, resolve, relative } from 'path';
12
14
  import { ConvergenceLoop, composeIterationHooks } from '../delivery/convergence-loop.js';
13
15
  import { createModelJudge } from '../delivery/judge.js';
@@ -215,7 +217,7 @@ import { createRunCoordinator } from '../delivery/run-coordinator.js';
215
217
  import { readCuratedIdeas } from './ideate.js';
216
218
  import { createEscalationController, defaultEscalationLadder } from '../delivery/escalation.js';
217
219
  import { FilePracticeStore, defaultPracticePath, extractKeywords, retrievePracticesSemantic, } from '../delivery/practice.js';
218
- import { detectRungs, mergeRedetectedRungs, runLadder, runTieredLadder, tierOf, TIER_ORDER, demoteBaselineFailures } from '../delivery/verifier-ladder.js';
220
+ import { detectRungs, mergeRedetectedRungs, runLadder, runTieredLadder, tierOf, TIER_ORDER, demoteBaselineFailures, baselineRegressions } from '../delivery/verifier-ladder.js';
219
221
  import { runDeployDevLadder } from '../delivery/deploy-dev-gate.js';
220
222
  import { commitPushAndWatch } from '../delivery/ci-watcher.js';
221
223
  import { snapshotTree, restoreTree, disposeSnapshot } from '../delivery/snapshot.js';
@@ -428,6 +430,371 @@ export function isDeliverLockAbandoned(projectRoot, nowS = Math.floor(Date.now()
428
430
  // A clock skew that puts the lock in the future must not read as ancient.
429
431
  return nowS - writtenS > wedgeTimeoutS();
430
432
  }
433
+ /**
434
+ * True when two deliver project roots share a subtree — identical, or one an
435
+ * ancestor of the other. Such runs edit the SAME files, so they must not run
436
+ * concurrently even though their `.uap/deliver.lock` paths differ.
437
+ */
438
+ export function deliverRootsOverlap(a, b) {
439
+ // realpath so one tree reached through a symlink and through its target does
440
+ // not read as two unrelated projects — a silent miss of the exact case this
441
+ // guards. Falls back to resolve() for a path that does not exist yet.
442
+ const norm = (p) => {
443
+ let abs;
444
+ try {
445
+ abs = realpathSync.native(resolve(p));
446
+ }
447
+ catch {
448
+ abs = resolve(p);
449
+ }
450
+ return abs.replace(/\/+$/, '');
451
+ };
452
+ const x = norm(a);
453
+ const y = norm(b);
454
+ if (x === y)
455
+ return true;
456
+ // Segment-boundary check: `/srv/app` must not "contain" `/srv/app-two`.
457
+ return x.startsWith(`${y}/`) || y.startsWith(`${x}/`);
458
+ }
459
+ /**
460
+ * A project root a registry entry is allowed to claim.
461
+ *
462
+ * `/` (and any root stripped to `''` by the trailing-slash trim) would overlap
463
+ * EVERY project on the machine, so one such entry disables deliver everywhere.
464
+ * Requiring at least two path segments keeps a corrupt or hostile entry from
465
+ * becoming a machine-wide off-switch.
466
+ */
467
+ function isPlausibleRunRoot(root) {
468
+ if (!root.startsWith('/'))
469
+ return false;
470
+ return root.replace(/\/+$/, '').split('/').filter(Boolean).length >= 2;
471
+ }
472
+ /**
473
+ * A pid a registry entry is allowed to claim.
474
+ *
475
+ * Bare `Number()` accepts `-1`, and `process.kill(-1, 0)` is a "does any
476
+ * process exist" query that always succeeds — so a negative pid would read as
477
+ * eternally alive and its entry could never be swept.
478
+ */
479
+ function isPlausibleRunPid(raw) {
480
+ return /^[1-9]\d{0,9}$/.test(raw.trim());
481
+ }
482
+ /** Only names this module writes are read or unlinked. */
483
+ const ACTIVE_RUN_FILE = /^[0-9a-f]{16}\.run$/;
484
+ /**
485
+ * Registry of live deliver runs, keyed across ALL projects on this machine.
486
+ * `UAP_ACTIVE_RUNS_DIR` redirects it so tests never write to a real home.
487
+ */
488
+ function activeRunsDir() {
489
+ const override = process.env.UAP_ACTIVE_RUNS_DIR;
490
+ return override && override.trim() ? resolve(override.trim()) : join(homedir(), '.uap', 'active-runs');
491
+ }
492
+ function activeRunEntry(projectRoot) {
493
+ // Path → filename, reversibly enough to read back and stable per root.
494
+ const key = createHash('sha1').update(resolve(projectRoot)).digest('hex').slice(0, 16);
495
+ return join(activeRunsDir(), `${key}.run`);
496
+ }
497
+ /** Publish this run's entry atomically, so no scanner can read a torn record. */
498
+ function publishActiveRun(projectRoot, iso) {
499
+ const dir = activeRunsDir();
500
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
501
+ // Absolute paths carry usernames and client/project names; keep them off
502
+ // other local uids, matching proxy-lifecycle's 0700/0600 convention.
503
+ const entry = activeRunEntry(projectRoot);
504
+ const tmp = `${entry}.${process.pid}.tmp`;
505
+ writeFileSync(tmp, `${process.pid}|${iso}|${resolve(projectRoot)}`, { encoding: 'utf8', mode: 0o600 });
506
+ renameSync(tmp, entry);
507
+ }
508
+ /**
509
+ * Deliver runs on OVERLAPPING project roots, anywhere on this machine.
510
+ *
511
+ * Returns the first live overlapping run that owns the subtree ahead of us
512
+ * (started earlier — PID breaks an exact tie), or null when we are clear to
513
+ * proceed. Stale entries are swept as they are read.
514
+ *
515
+ * `pidAlive` alone is NOT sufficient evidence that a run is live, and this
516
+ * function must not repeat the mistake the per-project lock already paid for
517
+ * (see isDeliverLockAbandoned): pids get recycled, and a run killed with
518
+ * SIGKILL never runs its exit handler, so its entry outlives it. An entry whose
519
+ * pid has been reissued to an unrelated process would otherwise block every
520
+ * overlapping deliver on the machine forever, with no operator-visible cause.
521
+ * So a claimed holder must ALSO still look live at its own root — it must hold
522
+ * that root's lock and not be abandoned there.
523
+ */
524
+ export function findOverlappingDeliverRun(projectRoot, selfPid = process.pid, selfStartedMs = Date.now()) {
525
+ const pidAlive = (pid) => {
526
+ try {
527
+ process.kill(pid, 0);
528
+ return true;
529
+ }
530
+ catch {
531
+ return false;
532
+ }
533
+ };
534
+ const dir = activeRunsDir();
535
+ let entries;
536
+ try {
537
+ entries = readdirSync(dir);
538
+ }
539
+ catch {
540
+ return null; // no registry yet — nothing to collide with
541
+ }
542
+ for (const name of entries) {
543
+ // Never read or unlink a name we did not write. Without this, pointing
544
+ // UAP_ACTIVE_RUNS_DIR at any directory turns the sweep below into an
545
+ // arbitrary-file delete.
546
+ if (!ACTIVE_RUN_FILE.test(name))
547
+ continue;
548
+ const file = join(dir, name);
549
+ const sweep = () => { try {
550
+ rmSync(file);
551
+ }
552
+ catch { /* racing sweep */ } };
553
+ try {
554
+ const [pidRaw = '', iso = '', rootRaw = ''] = readFileSync(file, 'utf8').split('|');
555
+ const root = rootRaw.trim();
556
+ if (!isPlausibleRunPid(pidRaw) || !isPlausibleRunRoot(root)) {
557
+ sweep();
558
+ continue;
559
+ }
560
+ const pid = Number(pidRaw.trim());
561
+ if (pid === selfPid)
562
+ continue;
563
+ if (!pidAlive(pid)) {
564
+ sweep();
565
+ continue;
566
+ }
567
+ if (!deliverRootsOverlap(projectRoot, root))
568
+ continue;
569
+ // The pid is alive, but is it still THIS run? Cross-check the holder's
570
+ // own root: a recycled pid does not hold that lock, and a wedged or
571
+ // abandoned holder is reclaimable there rather than blocking us here.
572
+ if (isDeliverLockAbandoned(root) || isDeliverHolderWedged(root)) {
573
+ sweep();
574
+ continue;
575
+ }
576
+ // Both runs see each other; exactly one must yield. The earlier start
577
+ // keeps the subtree, so the decision is the same from either side.
578
+ const theirs = Date.parse(iso.trim());
579
+ const olderThanUs = Number.isFinite(theirs)
580
+ ? theirs < selfStartedMs || (theirs === selfStartedMs && pid < selfPid)
581
+ : true;
582
+ if (olderThanUs)
583
+ return { pid, root };
584
+ }
585
+ catch { /* unreadable entry — ignore */ }
586
+ }
587
+ return null;
588
+ }
589
+ /**
590
+ * Cheap PREFILTER for "this directory might be a project root".
591
+ *
592
+ * Deliberately not the authority — a manifest can exist and still yield no
593
+ * gates (a package.json with no scripts is the common case), so a hit here is
594
+ * only a candidate. `dirHasGates` below is what actually decides, by asking the
595
+ * same detectors the real ladder uses. This list exists so we ask that question
596
+ * a handful of times instead of once per directory in the tree.
597
+ */
598
+ const BUILD_MANIFESTS = new Set([
599
+ 'package.json', 'Cargo.toml', 'go.mod', 'pyproject.toml', 'setup.py', 'requirements.txt',
600
+ 'pom.xml', 'build.gradle', 'build.gradle.kts', 'build.sbt',
601
+ 'Makefile', 'makefile', 'GNUmakefile', 'CMakeLists.txt',
602
+ 'Gemfile', 'composer.json', 'mix.exs', 'pubspec.yaml', 'Package.swift', 'build.zig',
603
+ ]);
604
+ /**
605
+ * Directories never worth descending into when looking for a project root.
606
+ *
607
+ * Two classes: build output / dependency trees (which are full of manifests
608
+ * belonging to somebody else — node_modules alone would otherwise nominate a
609
+ * dependency as the project), and directories that conventionally hold sample
610
+ * or fixture projects, which are never what the mission meant.
611
+ */
612
+ const NON_PROJECT_DIRS = new Set([
613
+ 'node_modules', 'target', 'dist', 'build', 'out', 'bin', 'obj', 'vendor', 'coverage',
614
+ 'venv', '__pycache__', 'Pods', 'bower_components', 'third_party',
615
+ 'testdata', 'fixtures', 'examples', 'templates',
616
+ ]);
617
+ /** Upper bound on directories read, so the cost never scales with the mistake. */
618
+ const GATED_SCAN_MAX_VISITS = 400;
619
+ /**
620
+ * Does this directory actually produce gate rungs? Asks the real detectors, so
621
+ * the advice cannot name a root that is just as gateless as the current one.
622
+ * Pure filesystem inspection — no commands run.
623
+ */
624
+ function dirHasGates(dir) {
625
+ // `detectRungs` already folds in the cargo, polyglot and non-npm detectors,
626
+ // so calling them again here could never change the answer — it only probed
627
+ // the filesystem three extra times per candidate.
628
+ try {
629
+ return detectRungs(dir).length > 0;
630
+ }
631
+ catch { /* detection is best-effort; treat a thrower as gateless */ }
632
+ return false;
633
+ }
634
+ /**
635
+ * The operator-facing advisory, or null when there is nothing useful to say.
636
+ *
637
+ * Split out from the call site so the wording is testable without standing up a
638
+ * whole delivery run — the message IS the feature, since nothing about the run
639
+ * changes.
640
+ */
641
+ /** Env escape hatch for the gateless-root refusal. */
642
+ export const GATELESS_ENV = 'UAP_ALLOW_GATELESS_ROOT';
643
+ /**
644
+ * True when the caller has explicitly accepted a root that cannot verify its
645
+ * own work. The env form exists for scripted launches that cannot easily add
646
+ * a flag.
647
+ */
648
+ export function allowGatelessRoot(flag) {
649
+ return flag === true || process.env[GATELESS_ENV] === '1';
650
+ }
651
+ /**
652
+ * Refuse a project root that cannot verify its own work. Returns false when the
653
+ * caller should stop.
654
+ *
655
+ * Escalated 2026-08-09 from a warning. The warning worked — it printed on line
656
+ * 3 of the log naming the exact right root — and the run was launched at the
657
+ * gateless root anyway, because a warning in a log is not a decision point for
658
+ * a scripted or agent-driven launch. That run then spent 34 minutes taking a
659
+ * Rust crate from 1 failing test to 3 while reporting "100% of gates" every
660
+ * turn, because with no cargo rung nothing could tell it otherwise.
661
+ *
662
+ * Refusing is safe precisely BECAUSE the trigger is narrow: this root has no
663
+ * gates AND a subdirectory demonstrably does. A genuinely gateless tree — a
664
+ * docs repo, an empty dir — finds no candidate and is untouched.
665
+ *
666
+ * The root is judged by its OWN unfiltered gate set, not by the post-`--gates`
667
+ * / post-tier `rungs` array: `--tiers fast` against a root whose only rungs are
668
+ * integration-tier empties that array, and refusing there would reject a root
669
+ * that is objectively well gated.
670
+ */
671
+ export function refuseGatelessRoot(projectRoot, options) {
672
+ // A dry-run plans and writes nothing, and is the one diagnostic an operator
673
+ // reaches for AFTER being refused ("show me what gates you see here").
674
+ // Refusing it would remove the tool that explains the refusal. Matches how
675
+ // preflight and the lock already exempt it.
676
+ if (options.dryRun)
677
+ return true;
678
+ // Resume continues a mission whose root was adjudicated at launch. Refusing
679
+ // here would strand a run that was legitimately started with consent, because
680
+ // the consent is not persisted in run state.
681
+ if (options.resume)
682
+ return true;
683
+ if (dirHasGates(projectRoot))
684
+ return true;
685
+ const gated = findGatedSubprojects(projectRoot);
686
+ const advice = formatGatelessRootAdvice(projectRoot, gated);
687
+ if (!advice)
688
+ return true;
689
+ if (allowGatelessRoot(options.allowGatelessRoot)) {
690
+ console.log(chalk.yellow(advice));
691
+ console.log(chalk.yellow(' (continuing anyway: gateless root explicitly allowed)'));
692
+ return true;
693
+ }
694
+ console.error(chalk.red(advice));
695
+ console.error(chalk.red('⛔ refusing to start: this root cannot verify the work. Re-run with the --project-root above. ' +
696
+ `(Operator override, human prose only — deliberately not in the JSON: --allow-gateless-root, or ${GATELESS_ENV}=1.)`));
697
+ if (options.json) {
698
+ console.log(JSON.stringify({
699
+ success: false,
700
+ gatelessRoot: true,
701
+ projectRoot,
702
+ suggestedProjectRoot: gated[0],
703
+ gatedSubprojects: gated,
704
+ reason: 'No objective gates were detected at this project root, but a subdirectory has them. ' +
705
+ 'Rooted here the run cannot compile or test its own work.',
706
+ // Deliberately does NOT name the bypass flag. This whole refusal
707
+ // exists because an agent-driven launch does not honour advice, and
708
+ // `nextStep` is the field such a caller reads first — advertising the
709
+ // off-switch there would hand it the way around the gate. The human
710
+ // prose on stderr names it; a model reading JSON gets the fix only.
711
+ nextStep: `Re-run with --project-root ${gated[0]}.`,
712
+ }, null, 2));
713
+ }
714
+ process.exitCode = 2;
715
+ return false;
716
+ }
717
+ export function formatGatelessRootAdvice(projectRoot, gated) {
718
+ if (!gated.length)
719
+ return null;
720
+ const rel = (p) => relative(projectRoot, p) || p;
721
+ return (`⚠ gateless root: no objective gates detected at ${projectRoot}, but ` +
722
+ `${gated.length === 1 ? 'a subdirectory has them' : `${gated.length} subdirectories have them`}: ` +
723
+ `${gated.map(rel).join(', ')}. Rooted here this mission has NO compile/test gate — the LLM judge is ` +
724
+ `the only convergence target, and rollback is sized against this whole tree. If the work is in ` +
725
+ `${rel(gated[0])}, re-run with \`--project-root ${gated[0]}\` to converge on its real gates.`);
726
+ }
727
+ /**
728
+ * Subdirectories that DO carry a build manifest, when the root itself carries none.
729
+ *
730
+ * A deliver rooted above the actual project gets no objective gates at all — the
731
+ * LLM judge becomes the only convergence target, so the run cannot tell whether
732
+ * the code it is fixing even compiles. Live on 2026-08-08: a mission to repair a
733
+ * Rust crate ran rooted at the crate's grandparent, which has no manifest. It got
734
+ * `bootstrap` (trivially passes) plus a skipped user-validation gate, reported
735
+ * "100% of gates" for five consecutive turns, and left the crate not compiling.
736
+ * The same misconfiguration also disables rollback, because the size guard walks
737
+ * the whole (here 22 GB) tree instead of the 117 MB crate.
738
+ *
739
+ * Breadth-first and hard-bounded: this runs on the startup path of every gateless
740
+ * deliver, including ones rooted at a huge tree — the cost of the advice must not
741
+ * scale with the mistake.
742
+ */
743
+ export function findGatedSubprojects(projectRoot, maxDepth = 3, maxHits = 5, maxVisits = GATED_SCAN_MAX_VISITS) {
744
+ const hits = [];
745
+ let frontier = [{ dir: resolve(projectRoot), depth: 0 }];
746
+ let visited = 0;
747
+ while (frontier.length && hits.length < maxHits && visited < maxVisits) {
748
+ const next = [];
749
+ for (const { dir, depth } of frontier) {
750
+ if (hits.length >= maxHits || visited >= maxVisits)
751
+ break;
752
+ visited += 1;
753
+ let entries;
754
+ try {
755
+ entries = readdirSync(dir, { withFileTypes: true });
756
+ }
757
+ catch {
758
+ continue;
759
+ }
760
+ // Do not report the root itself — the caller already knows it is gateless.
761
+ // A candidate is only reported once the real detectors confirm it, so the
762
+ // "would have gates" claim in the advice is measured, not assumed.
763
+ if (depth > 0 && entries.some((e) => !e.isDirectory() && BUILD_MANIFESTS.has(e.name))) {
764
+ if (dirHasGates(dir)) {
765
+ hits.push(dir);
766
+ // A workspace member inside a crate belongs to that crate; descending
767
+ // would offer a root strictly worse than the one just reported.
768
+ continue;
769
+ }
770
+ }
771
+ if (depth >= maxDepth)
772
+ continue;
773
+ for (const e of entries) {
774
+ if (next.length >= maxVisits)
775
+ break;
776
+ if (!e.isDirectory() || e.name.startsWith('.') || NON_PROJECT_DIRS.has(e.name))
777
+ continue;
778
+ next.push({ dir: join(dir, e.name), depth: depth + 1 });
779
+ }
780
+ }
781
+ frontier = next;
782
+ }
783
+ return hits;
784
+ }
785
+ /**
786
+ * Why the LAST refusal happened, when it was an overlapping foreign root.
787
+ *
788
+ * `acquireDeliverLock` returns a bare null, but "another run owns a DIFFERENT,
789
+ * overlapping root" and "another run owns THIS root" need opposite advice: the
790
+ * caller must send the operator to the holder's root, because following our own
791
+ * finds nothing in flight and loops straight back into relaunching.
792
+ */
793
+ let lastOverlapBlock = null;
794
+ /** The overlapping holder that caused the last refusal, if that was the cause. */
795
+ export function lastOverlapBlocker() {
796
+ return lastOverlapBlock;
797
+ }
431
798
  /**
432
799
  * Project-level deliver concurrency lock. Prevents a fan-out where an impatient
433
800
  * caller (e.g. a model that launched `uap deliver` for some work, didn't wait
@@ -436,6 +803,15 @@ export function isDeliverLockAbandoned(projectRoot, nowS = Math.floor(Date.now()
436
803
  * burning tokens/GPU in parallel. Returns a release() thunk, or null when
437
804
  * another live deliver already holds the lock (the caller then exits cleanly).
438
805
  * Stale locks (dead PID) are reclaimed. Off-switch: UAP_DELIVER_NO_LOCK=1.
806
+ *
807
+ * The lock is keyed on `<projectRoot>/.uap/`, so it only ever saw runs that
808
+ * named the SAME root. Two runs on NESTED roots therefore each took their own
809
+ * lock and edited one shared source tree: live on 2026-08-08, roots
810
+ * `cognition-engine` and `cognition-engine/src/rust-pg-ext` ran for ~2h,
811
+ * overwriting each other's edits to src/cooccurrence.rs until it stopped
812
+ * compiling — each one's gate failing on damage the other had just done. The
813
+ * cross-project registry below closes that, because overlap is a property of
814
+ * the SUBTREE, not of the lock path.
439
815
  */
440
816
  export function acquireDeliverLock(projectRoot) {
441
817
  if (process.env.UAP_DELIVER_NO_LOCK === '1')
@@ -479,8 +855,38 @@ export function acquireDeliverLock(projectRoot) {
479
855
  catch {
480
856
  return null;
481
857
  }
482
- writeSync(fd, `${process.pid}|${new Date().toISOString()}`);
858
+ const startedIso = new Date().toISOString();
859
+ writeSync(fd, `${process.pid}|${startedIso}`);
483
860
  closeSync(fd);
861
+ // Publish to the cross-project registry BEFORE checking it, so a run that
862
+ // starts while we are scanning still sees us and one of the two yields.
863
+ let published = false;
864
+ try {
865
+ publishActiveRun(projectRoot, startedIso);
866
+ published = true;
867
+ }
868
+ catch { /* registry unavailable — fall back to the per-project lock alone */ }
869
+ // Compare against the stamp we PUBLISHED, not a fresh Date.now(). The other
870
+ // run can only see the published one, and a tiebreak both sides must agree
871
+ // on has to be computed from values both sides can read — otherwise a
872
+ // millisecond crossing between publish and scan makes both runs yield.
873
+ const overlapping = published
874
+ ? findOverlappingDeliverRun(projectRoot, process.pid, Date.parse(startedIso))
875
+ : null;
876
+ if (overlapping) {
877
+ // Someone older owns this subtree. Give back everything we just took.
878
+ try {
879
+ rmSync(activeRunEntry(projectRoot));
880
+ }
881
+ catch { /* best-effort */ }
882
+ try {
883
+ rmSync(lockPath);
884
+ }
885
+ catch { /* best-effort */ }
886
+ lastOverlapBlock = overlapping;
887
+ return null;
888
+ }
889
+ lastOverlapBlock = null;
484
890
  // Stamp a fresh heartbeat immediately so this brand-new holder is never
485
891
  // classified as wedged in the window before its first iteration (and so a
486
892
  // reclaim overwrites the previous holder's stale heartbeat).
@@ -503,6 +909,15 @@ export function acquireDeliverLock(projectRoot) {
503
909
  }
504
910
  }
505
911
  catch { /* best-effort */ }
912
+ try {
913
+ const entry = activeRunEntry(projectRoot);
914
+ if (existsSync(entry)) {
915
+ const held = Number((readFileSync(entry, 'utf8').split('|')[0] || '').trim());
916
+ if (held === process.pid)
917
+ rmSync(entry);
918
+ }
919
+ }
920
+ catch { /* best-effort */ }
506
921
  };
507
922
  process.once('exit', release);
508
923
  return release;
@@ -588,6 +1003,18 @@ export async function deliverCommand(instruction, options) {
588
1003
  return;
589
1004
  }
590
1005
  const projectRootForDetach = resolve(options.projectRoot ?? process.cwd());
1006
+ // Gateless-root refusal, deliberately BEFORE the detach decision — and so
1007
+ // before preflight, the lock and the cross-project registry.
1008
+ //
1009
+ // It is a pure, bounded filesystem inspection of this invocation's own
1010
+ // arguments, so it depends on nothing those steps establish. Running it later
1011
+ // meant a refusal still spawned a detached child, printed "mission detached",
1012
+ // and created .uap/ state inside the directory it was about to declare the
1013
+ // wrong root — and, worse, when the CORRECT run was already live in the
1014
+ // subdirectory the overlap check fired first, so the operator got the generic
1015
+ // "another run owns an overlapping root" instead of the specific diagnosis.
1016
+ if (!refuseGatelessRoot(projectRootForDetach, options))
1017
+ return;
591
1018
  const decision = shouldDetach({
592
1019
  alreadyDetached: isDetachedChild(),
593
1020
  noDetach: process.env[NO_DETACH_ENV] === '1',
@@ -686,13 +1113,33 @@ async function runDeliver(instruction, options) {
686
1113
  // acts on. A lock file containing newlines and prose would otherwise become
687
1114
  // tool-result guidance. It is always a pid or it is not forwarded.
688
1115
  const holderRaw = holder.trim();
689
- const holderPid = /^\d{1,10}$/.test(holderRaw) ? holderRaw : '';
690
- console.log(chalk.yellow(`↩ deliver already running for this project${holderPid ? ` (pid ${holderPid})` : ''} ` +
691
- `skipping this duplicate launch. Follow it with \`uap deliver --await-run\` (waits and reports; ` +
692
- `from a tool call it returns within about a minute "still running" is an answer, not a failure, ` +
693
- `so just call it again. It starts nothing.) ` +
694
- `starts nothing). Do NOT use --resume on a live run: resume CONTINUES a mission and would start a ` +
695
- `second copy of this one. (override: UAP_DELIVER_NO_LOCK=1)`));
1116
+ let holderPid = /^\d{1,10}$/.test(holderRaw) ? holderRaw : '';
1117
+ // Was this refused by a run on a DIFFERENT, overlapping root? Then our own
1118
+ // lock was never left behind for us to read, and more importantly —
1119
+ // `--await-run` on OUR root finds nothing in flight and answers "safe to
1120
+ // launch", which sends the caller straight back here. Name the holder's
1121
+ // root so the follow lands where the mission actually is.
1122
+ const overlap = lastOverlapBlocker();
1123
+ // The root comes from a file any local process can write and ends up in
1124
+ // model-facing instruction text; forward it only if it still looks like a
1125
+ // path, control characters stripped.
1126
+ const overlapRoot = overlap ? stripControl(overlap.root).trim() : '';
1127
+ const foreign = overlap && overlapRoot.startsWith('/') && !overlapRoot.includes('\n')
1128
+ ? { pid: String(overlap.pid), root: overlapRoot }
1129
+ : null;
1130
+ if (foreign)
1131
+ holderPid = foreign.pid;
1132
+ console.log(chalk.yellow(foreign
1133
+ ? `↩ another deliver run (pid ${foreign.pid}) already owns an OVERLAPPING project root: ` +
1134
+ `${foreign.root} — it edits the same files, so this launch was skipped. Follow THAT run: ` +
1135
+ `\`uap deliver --project-root ${foreign.root} --await-run\` (following this root would report ` +
1136
+ `nothing in flight). Do NOT use --resume on a live run. (override: UAP_DELIVER_NO_LOCK=1)`
1137
+ : `↩ deliver already running for this project${holderPid ? ` (pid ${holderPid})` : ''} — ` +
1138
+ `skipping this duplicate launch. Follow it with \`uap deliver --await-run\` (waits and reports; ` +
1139
+ `from a tool call it returns within about a minute — "still running" is an answer, not a failure, ` +
1140
+ `so just call it again. It starts nothing.) ` +
1141
+ `Do NOT use --resume on a live run: resume CONTINUES a mission and would start a ` +
1142
+ `second copy of this one. (override: UAP_DELIVER_NO_LOCK=1)`));
696
1143
  // --json is a CONTRACT: every exit must emit a parseable result. This path
697
1144
  // used to print the yellow line above and return, leaving a caller that ran
698
1145
  // `deliver --json` with a stdout containing no JSON at all.
@@ -712,13 +1159,23 @@ async function runDeliver(instruction, options) {
712
1159
  alreadyRunning: true,
713
1160
  ...(holderPid ? { holderPid } : {}),
714
1161
  projectRoot,
715
- reason: 'A deliver run is already in progress for this project; this duplicate launch was skipped.',
716
- nextStep: 'The mission you asked for is ALREADY RUNNING — nothing is wrong and nothing is needed from you. ' +
717
- 'Follow it instead: call deliver again with follow:true (or `uap deliver --await-run` from a ' +
718
- 'shell). It returns within about a minute; if it says STILL RUNNING that is not a failure — ' +
719
- 'call it again to keep waiting. Do NOT start another ' +
720
- 'run, do NOT pass resume (resume CONTINUES a run rather than following it, and would start a ' +
721
- 'second copy of this live one), and do NOT change any gate or enforcement setting.',
1162
+ ...(foreign ? { overlappingRoot: foreign.root } : {}),
1163
+ reason: foreign
1164
+ ? `An older deliver run (pid ${foreign.pid}) holds an OVERLAPPING project root ` +
1165
+ `(${foreign.root}); it edits the same files, so this launch was skipped.`
1166
+ : 'A deliver run is already in progress for this project; this duplicate launch was skipped.',
1167
+ nextStep: foreign
1168
+ ? 'The files you asked for are ALREADY being worked on by a run rooted at ' +
1169
+ `${foreign.root}. Follow THAT run: \`uap deliver --project-root ${foreign.root} --await-run\`. ` +
1170
+ 'Do NOT follow this root — it has nothing in flight and will tell you it is safe to launch, ' +
1171
+ 'which puts you back here. Do NOT start another run, do NOT pass resume, and do NOT change ' +
1172
+ 'any gate or enforcement setting.'
1173
+ : 'The mission you asked for is ALREADY RUNNING — nothing is wrong and nothing is needed from you. ' +
1174
+ 'Follow it instead: call deliver again with follow:true (or `uap deliver --await-run` from a ' +
1175
+ 'shell). It returns within about a minute; if it says STILL RUNNING that is not a failure — ' +
1176
+ 'call it again to keep waiting. Do NOT start another ' +
1177
+ 'run, do NOT pass resume (resume CONTINUES a run rather than following it, and would start a ' +
1178
+ 'second copy of this live one), and do NOT change any gate or enforcement setting.',
722
1179
  }, null, 2));
723
1180
  }
724
1181
  return;
@@ -1022,6 +1479,16 @@ async function runDeliver(instruction, options) {
1022
1479
  });
1023
1480
  console.log(chalk.cyan('⚖ acceptance: LLM judge is the convergence target (no objective project gates; self-gate skipped)'));
1024
1481
  }
1482
+ // A gateless root is sometimes the truth (a docs repo, a fresh dir) and
1483
+ // sometimes a misrouted --project-root pointing one level above the actual
1484
+ // project. Those two are indistinguishable in the logs, and the second is
1485
+ // expensive: the run cannot compile or test the thing it was asked to fix.
1486
+ //
1487
+ // Deliberately OUTSIDE the acceptancePrimary branch. Both gateless strategies
1488
+ // reach here — acceptance-as-primary and self-gate — and the misrouted root
1489
+ // hurts them identically. Scoping the advice to one of them would have left
1490
+ // the DEFAULT path (no --acceptance) silent, which is the more common way to
1491
+ // hit this.
1025
1492
  const cfgRawEarlyForUv = cfgRawEarlyForUvFactory(projectRoot);
1026
1493
  // Baseline-delta gating: a required rung that is ALREADY red cannot be a
1027
1494
  // regression this mission causes, but it makes acceptance unreachable and
@@ -1052,6 +1519,16 @@ async function runDeliver(instruction, options) {
1052
1519
  else {
1053
1520
  console.log(chalk.dim(` baseline-delta: baseline green (preflight ${(bd.preflightMs / 1000).toFixed(0)}s)`));
1054
1521
  }
1522
+ // KNOWN LIMIT on --resume: this re-preflights a tree the mission has
1523
+ // already changed, so a regression introduced before the interruption is
1524
+ // adopted as the new "pre-existing" state and forgiven. Skipping the
1525
+ // preflight instead is WORSE: rungs are not persisted in run state, so a
1526
+ // resumed run re-detects them as REQUIRED, the pre-existing red suite
1527
+ // fail-fasts, and the run cannot converge at all — the wedge demotion
1528
+ // exists to remove. The real fix is to persist the demotion in run state.
1529
+ if (options.resume && bd.demoted.length > 0) {
1530
+ console.log(chalk.dim(' (resumed run: baseline re-measured against the current tree)'));
1531
+ }
1055
1532
  }
1056
1533
  // User-path validation: the terminal 'final'-tier rung. The delivered
1057
1534
  // artifact must pass its critical user journeys through the REAL client
@@ -2688,11 +3165,21 @@ async function runDeliver(instruction, options) {
2688
3165
  // --keep-best: if deliver left the project worse than it started (by real
2689
3166
  // gate score), roll back to the snapshot so the run is never a regression.
2690
3167
  if (keepBest && regressSnapshot) {
2691
- const endScore = runLadder(fastRungs, projectRoot).score;
2692
- if (endScore < baselineGateScore) {
3168
+ // Score alone cannot see a name-level regression: a demoted rung is red
3169
+ // both before and after, so `score` is byte-identical while the suite got
3170
+ // worse. That matters here specifically, because this rollback is what the
3171
+ // demotion docstring names as the whole-tree backstop for exactly the
3172
+ // breakage rung-granularity cannot catch.
3173
+ const endLadder = runLadder(fastRungs, projectRoot);
3174
+ const endScore = endLadder.score;
3175
+ const endRegressions = baselineRegressions(fastRungs, endLadder.results);
3176
+ if (endScore < baselineGateScore || endRegressions.length > 0) {
2693
3177
  try {
2694
3178
  restoreTree(projectRoot, regressSnapshot);
2695
- console.log(chalk.yellow(` ↩ no-regress: reverted to best (end gate score ${endScore.toFixed(2)} < best ${baselineGateScore.toFixed(2)})`));
3179
+ console.log(chalk.yellow(endRegressions.length > 0
3180
+ ? ` ↩ no-regress: reverted to best — tests that passed at baseline are failing now: ` +
3181
+ `${endRegressions.flatMap((r) => r.tests).slice(0, 5).join(', ')}`
3182
+ : ` ↩ no-regress: reverted to best (end gate score ${endScore.toFixed(2)} < best ${baselineGateScore.toFixed(2)})`));
2696
3183
  disposeSnapshot(regressSnapshot);
2697
3184
  }
2698
3185
  catch (restoreErr) {