@miller-tech/uap 1.149.5 → 1.150.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.
Files changed (31) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/bin/cli.js +2 -0
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/cli/deliver.d.ts +13 -0
  5. package/dist/cli/deliver.d.ts.map +1 -1
  6. package/dist/cli/deliver.js +289 -54
  7. package/dist/cli/deliver.js.map +1 -1
  8. package/dist/delivery/agentic-executor.d.ts +24 -0
  9. package/dist/delivery/agentic-executor.d.ts.map +1 -1
  10. package/dist/delivery/agentic-executor.js +69 -1
  11. package/dist/delivery/agentic-executor.js.map +1 -1
  12. package/dist/delivery/convergence-loop.d.ts +30 -0
  13. package/dist/delivery/convergence-loop.d.ts.map +1 -1
  14. package/dist/delivery/convergence-loop.js +72 -2
  15. package/dist/delivery/convergence-loop.js.map +1 -1
  16. package/dist/delivery/pending-intents.d.ts +45 -0
  17. package/dist/delivery/pending-intents.d.ts.map +1 -0
  18. package/dist/delivery/pending-intents.js +86 -0
  19. package/dist/delivery/pending-intents.js.map +1 -0
  20. package/dist/delivery/task-workspace.d.ts +70 -0
  21. package/dist/delivery/task-workspace.d.ts.map +1 -0
  22. package/dist/delivery/task-workspace.js +305 -0
  23. package/dist/delivery/task-workspace.js.map +1 -0
  24. package/docs/reference/CONFIGURATION.md +9 -0
  25. package/docs/specs/p0-anti-vacuous.md +497 -0
  26. package/package.json +1 -1
  27. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  28. package/src/policies/enforcers/delivery_enforcement.py +14 -0
  29. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  30. package/templates/hooks/deliver_autoroute.py +28 -5
  31. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
@@ -19,6 +19,7 @@ import { planAutoOptimization } from '../delivery/auto-optimizer.js';
19
19
  import { RoutingPresets, resolvePresetModel } from '../models/types.js';
20
20
  import { measureQueryComplexity } from '../utils/query-complexity.js';
21
21
  import { authorAcceptanceGate } from '../delivery/self-gate.js';
22
+ import { applyPendingIntents } from '../delivery/pending-intents.js';
22
23
  import { runAcceptanceGate, formatAcceptanceReport, createAcceptanceChurnBreaker } from '../delivery/acceptance-judge.js';
23
24
  import { runExecutionGate } from '../delivery/execution-gate.js';
24
25
  import { runVisualGate, visualRuntimeNote } from '../delivery/visual-gate.js';
@@ -41,7 +42,9 @@ import { runVisualGate, visualRuntimeNote } from '../delivery/visual-gate.js';
41
42
  */
42
43
  export function decideGateStrategy(opts) {
43
44
  const acceptancePrimary = opts.hasAcceptance && opts.noRealGates && !opts.forceSelfGate;
44
- const needsSelfGate = opts.selfGateAllowed && !acceptancePrimary && (opts.noRealGates || opts.forceSelfGate);
45
+ const needsSelfGate = opts.selfGateAllowed &&
46
+ !acceptancePrimary &&
47
+ (opts.noRealGates || opts.forceSelfGate || opts.baselineAllGreen === true);
45
48
  const noGatesError = opts.noRealGates && !needsSelfGate && !acceptancePrimary;
46
49
  return { acceptancePrimary, needsSelfGate, noGatesError };
47
50
  }
@@ -118,6 +121,7 @@ import { extractContract } from '../delivery/contract-extractor.js';
118
121
  import { completeDeliveryTask, openDeliveryTask, recordDeliveryOutcome, recordOrchestratorTaskOutcome, reopenDeliveryTask } from '../delivery/task-sync.js';
119
122
  import { autoMineHaloTraces, summarizeWeaknesses, weaknessGuidance, loadPersistedWeaknesses } from '../delivery/auto-mine.js';
120
123
  import { createGitWorktreeProvider } from '../delivery/candidate-workspace.js';
124
+ import { createTaskWorkspaceManager, resolveParallelTasks } from '../delivery/task-workspace.js';
121
125
  import { createHaloDeliveryTracer } from '../delivery/halo-trace.js';
122
126
  import { haloTracePath, isHaloTracingEnabled } from '../observability/halo-exporter.js';
123
127
  import { createRunCoordinator } from '../delivery/run-coordinator.js';
@@ -401,6 +405,34 @@ export async function deliverCommand(instruction, options) {
401
405
  }
402
406
  async function runDeliver(instruction, options) {
403
407
  const projectRoot = resolve(options.projectRoot ?? process.cwd());
408
+ // P1 (plan D1): --pending replays gate-recorded edit intents exactly, then
409
+ // verifies with the required gates — the sanctioned path for a blocked
410
+ // direct edit, with zero model improvisation. Runs BEFORE preflight: it
411
+ // never creates a worktree, so the git-repo requirement does not apply.
412
+ if (options.pending !== undefined && options.pending !== false) {
413
+ const pendingFile = typeof options.pending === 'string' ? options.pending : undefined;
414
+ const res = applyPendingIntents(projectRoot, pendingFile);
415
+ for (const a of res.applied)
416
+ console.log(chalk.green(` ✓ applied ${a.kind}: ${a.file}`));
417
+ for (const s of res.skipped)
418
+ console.log(chalk.yellow(` ⚠ skipped ${s.file}: ${s.reason}`));
419
+ if (res.applied.length === 0) {
420
+ fail(`No pending intents applied${pendingFile ? ` for ${pendingFile}` : ''} (${res.skipped.length} skipped).`);
421
+ }
422
+ const rungs = detectRungs(projectRoot).filter((r) => r.required && tierOf(r) === 'fast');
423
+ if (rungs.length > 0) {
424
+ console.log(chalk.cyan(`⚖ verifying ${res.applied.length} applied intent(s) against ${rungs.length} required fast gate(s)…`));
425
+ const ladder = runLadder(rungs, projectRoot);
426
+ if (!ladder.passed) {
427
+ fail(`Applied intents but the required gates FAIL:\n${ladder.feedback}`);
428
+ }
429
+ console.log(chalk.green(' ✓ gates pass'));
430
+ }
431
+ else {
432
+ console.log(chalk.yellow(' (no required fast gates detected — applied without verification)'));
433
+ }
434
+ return;
435
+ }
404
436
  // Preflight: refuse to start in a project that structurally cannot deliver
405
437
  // (chiefly: not a git repo — the candidate workspace is worktree-based, so
406
438
  // deliver/epics/orchestration are all dead and the mission would burn its
@@ -471,6 +503,18 @@ async function runDeliver(instruction, options) {
471
503
  console.log(chalk.cyan(`⚙ auto-optimize: ${autoPlan.summary}`));
472
504
  }
473
505
  }
506
+ // Verification RAILS are independent of the optimization AIDS (P0,
507
+ // 2026-07-13): --no-auto / explicit aid flags stand down exploration,
508
+ // critic and ideation, but must not silently drop the acceptance judge —
509
+ // without it, a gates-green no-op run reads as delivered. When the
510
+ // auto-planner did not run, acceptance defaults ON; opt out explicitly
511
+ // with UAP_DELIVER_ACCEPTANCE=0.
512
+ if (!autoPlan && options.acceptance === undefined && process.env.UAP_DELIVER_ACCEPTANCE !== '0') {
513
+ options.acceptance = true;
514
+ if (!options.dryRun) {
515
+ console.log(chalk.cyan('⚖ acceptance judge on (verification rail; UAP_DELIVER_ACCEPTANCE=0 to disable)'));
516
+ }
517
+ }
474
518
  // `--optimize` turns on every convergence aid at once. Deploy queueing is
475
519
  // deliberately excluded — committing applied files is a side effect the
476
520
  // user must opt into explicitly.
@@ -657,11 +701,35 @@ async function runDeliver(instruction, options) {
657
701
  console.log(chalk.cyan('⚖ thin gate set (≤1 required rung) — acceptance judge enabled to thicken the target'));
658
702
  }
659
703
  }
704
+ // Anti-vacuous floor (P0, 2026-07-13 incident): probe the REQUIRED rungs
705
+ // once before choosing the convergence target. If everything is already
706
+ // green, gate-satisfaction cannot measure this mission — a run on a
707
+ // gates-green repo would false-green as a no-op (observed live: a 6-file
708
+ // C++ mission "delivered" after writing nothing, because the only detected
709
+ // gates were unrelated npm web gates). UAP_DELIVER_VACUOUS_FLOOR=0 opts out.
710
+ let baselineAllGreen = false;
711
+ if (!noRealGates &&
712
+ options.forceSelfGate !== true &&
713
+ selfGateAllowed &&
714
+ process.env.UAP_DELIVER_VACUOUS_FLOOR !== '0' &&
715
+ !options.dryRun) {
716
+ try {
717
+ const requiredRungs = rungs.filter((r) => r.required);
718
+ baselineAllGreen = requiredRungs.length > 0 && runLadder(requiredRungs, projectRoot).passed;
719
+ }
720
+ catch {
721
+ baselineAllGreen = false; // probe is best-effort; never blocks a run
722
+ }
723
+ if (baselineAllGreen) {
724
+ console.log(chalk.cyan('⚖ anti-vacuous floor: all required project gates are ALREADY green — authoring a mission self-gate so success requires real, verified change'));
725
+ }
726
+ }
660
727
  const { acceptancePrimary, needsSelfGate, noGatesError } = decideGateStrategy({
661
728
  hasAcceptance: Boolean(options.acceptance),
662
729
  noRealGates,
663
730
  forceSelfGate: options.forceSelfGate === true,
664
731
  selfGateAllowed,
732
+ baselineAllGreen,
665
733
  });
666
734
  if (noGatesError) {
667
735
  fail(`No verifiable gates detected in ${projectRoot} (need package.json scripts, or drop --no-self-gate to author one).`);
@@ -975,7 +1043,12 @@ async function runDeliver(instruction, options) {
975
1043
  }
976
1044
  rungs.push(sg.rung);
977
1045
  if (sg.vacuous) {
978
- console.log(chalk.yellow(' ⚠ acceptance gate may be weak (could not force an initially-failing check); running multi-turn anyway.'));
1046
+ // P0 hard-fail: a REQUIRED self-gate that passes on the unsolved repo
1047
+ // re-opens the false-green door — "delivered" would be meaningless.
1048
+ if (process.env.UAP_DELIVER_ALLOW_WEAK_SELF_GATE !== '1') {
1049
+ fail('The self-gate is REQUIRED for this run (anti-vacuous floor) but stayed vacuous after retries — it passes on the unsolved repo. Add concrete, checkable ACCEPTANCE CRITERIA to the instruction, or set UAP_DELIVER_ALLOW_WEAK_SELF_GATE=1 to accept the risk.');
1050
+ }
1051
+ console.log(chalk.yellow(' ⚠ acceptance gate may be weak (could not force an initially-failing check); UAP_DELIVER_ALLOW_WEAK_SELF_GATE=1 — running multi-turn anyway.'));
979
1052
  }
980
1053
  else {
981
1054
  console.log(chalk.green(' ✓ acceptance gate authored — fails on the unsolved repo, a real convergence target.'));
@@ -1195,14 +1268,45 @@ async function runDeliver(instruction, options) {
1195
1268
  // Phased missions re-point the judge at the current phase's goal — judging
1196
1269
  // phase 1 against the FULL mission would fail every phase by construction.
1197
1270
  let acceptanceSpec = instruction;
1271
+ // Parallel orchestrated tasks run in ISOLATED worktree roots; each loop's
1272
+ // judge must grade against ITS task's spec, not a shared mutable. The
1273
+ // acceptance gate resolves by the root it is invoked with; the phased and
1274
+ // epic paths (single loop at a time) keep using the shared variable.
1275
+ const acceptanceSpecByRoot = new Map();
1198
1276
  // Secondary-judge churn breaker: bounds consecutive judge rejections of
1199
1277
  // objectively-green turns per spec (env UAP_DELIVER_ACCEPTANCE_FLIP_LIMIT,
1200
1278
  // default 2), after which the objective gates win. Primary mode is exempt.
1201
1279
  // Change-evidence for the breaker's zero-diff guard: files applied since the
1202
1280
  // current acceptance spec (epic) began. Incremented by the iteration hook,
1203
1281
  // reset wherever acceptanceSpec is re-pointed.
1204
- const specChangeEvidence = { writes: 0 };
1205
- const acceptanceChurnBreaker = createAcceptanceChurnBreaker(Number(process.env.UAP_DELIVER_ACCEPTANCE_FLIP_LIMIT ?? 2), () => specChangeEvidence.writes > 0);
1282
+ // Per-ROOT under parallel dispatch: concurrent loops must not zero or
1283
+ // inflate each other's evidence (an inflated count could let the breaker
1284
+ // accept a task with zero writes of its own). projectRoot's entry is the
1285
+ // historic shared counter the phased/epic paths keep using.
1286
+ const writesByRoot = new Map();
1287
+ const evidenceFor = (root) => {
1288
+ let e = writesByRoot.get(root);
1289
+ if (!e) {
1290
+ e = { writes: 0 };
1291
+ writesByRoot.set(root, e);
1292
+ }
1293
+ return e;
1294
+ };
1295
+ const specChangeEvidence = evidenceFor(projectRoot);
1296
+ // Breaker state is per SPEC: one shared instance thrashes its current-spec
1297
+ // slot under parallel dispatch and can then never trip.
1298
+ const acceptanceFlipLimit = Number(process.env.UAP_DELIVER_ACCEPTANCE_FLIP_LIMIT ?? 2);
1299
+ const acceptanceBreakers = new Map();
1300
+ const breakerFor = (spec, root) => {
1301
+ let b = acceptanceBreakers.get(spec);
1302
+ if (!b) {
1303
+ if (acceptanceBreakers.size > 100)
1304
+ acceptanceBreakers.clear(); // runaway guard
1305
+ b = createAcceptanceChurnBreaker(acceptanceFlipLimit, () => evidenceFor(root).writes > 0);
1306
+ acceptanceBreakers.set(spec, b);
1307
+ }
1308
+ return b;
1309
+ };
1206
1310
  const acceptanceGate = options.acceptance && !skipJudgeForSimple
1207
1311
  ? async (root) => {
1208
1312
  // Primary mode: the only objective rung is the trivial bootstrap, and the
@@ -1234,13 +1338,14 @@ async function runDeliver(instruction, options) {
1234
1338
  // gates all passed — hand the judge that fact as evidence, so
1235
1339
  // requirements like "make the tests pass" are graded on the gate
1236
1340
  // result instead of speculated about from static code.
1341
+ const resolvedSpec = acceptanceSpecByRoot.get(root) ?? acceptanceSpec;
1237
1342
  const uvNote = buildUserPathsNote(root);
1238
1343
  const baseNote = acceptancePrimary
1239
1344
  ? visualNote
1240
1345
  : 'Objective project gates (build/test suite) ALL PASSED on this turn — treat test/build-related requirements as objectively verified.';
1241
1346
  const runtimeNote = [baseNote, uvNote?.note].filter(Boolean).join(' ');
1242
1347
  const r = await runAcceptanceGate({
1243
- spec: acceptanceSpec,
1348
+ spec: resolvedSpec,
1244
1349
  projectRoot: root,
1245
1350
  executor: verdictExecutor,
1246
1351
  ...(runtimeNote ? { runtimeNote } : {}),
@@ -1250,7 +1355,7 @@ async function runDeliver(instruction, options) {
1250
1355
  // objective gates ALL passed, so a bounded number of consecutive judge
1251
1356
  // rejections hands the verdict back to the gates instead of wedging.
1252
1357
  if (!acceptancePrimary) {
1253
- const checked = acceptanceChurnBreaker.check(acceptanceSpec, verdict);
1358
+ const checked = breakerFor(resolvedSpec, root).check(resolvedSpec, verdict);
1254
1359
  if (checked.overridden) {
1255
1360
  console.log(chalk.yellow(' ⚖ acceptance: judge rejected consecutive objectively-green turns — accepting on gates (raise UAP_DELIVER_ACCEPTANCE_FLIP_LIMIT to let the judge argue longer)'));
1256
1361
  }
@@ -1267,7 +1372,7 @@ async function runDeliver(instruction, options) {
1267
1372
  ...(acceptanceGate ? { acceptanceGate } : {}),
1268
1373
  };
1269
1374
  /** Compose the per-turn hooks with a FRESH escalation controller. */
1270
- const makeIterationHook = () => {
1375
+ const makeIterationHook = (evidenceRoot = projectRoot) => {
1271
1376
  const escalation = makeEscalation();
1272
1377
  const repair = repairAgent && process.env.UAP_DELIVER_REPAIR_ESCALATION !== '0'
1273
1378
  ? createRepairEscalation({
@@ -1284,7 +1389,7 @@ async function runDeliver(instruction, options) {
1284
1389
  return composeIterationHooks((record) => printProgress(record), (record) => {
1285
1390
  // Feed the acceptance breaker's zero-diff guard.
1286
1391
  if (record.filesApplied.length > 0)
1287
- specChangeEvidence.writes += record.filesApplied.length;
1392
+ evidenceFor(evidenceRoot).writes += record.filesApplied.length;
1288
1393
  return undefined;
1289
1394
  }, (record) => haloTracer.onIteration(record), repair ? (record) => repair.onIteration(record) : undefined, coordinator ? (record) => coordinator.onIteration(record) : undefined, escalation ? (record) => escalation.onIteration(record) : undefined);
1290
1395
  };
@@ -1295,6 +1400,9 @@ async function runDeliver(instruction, options) {
1295
1400
  // The agentic executor mutates the repo directly (no-op applier), so
1296
1401
  // gates must run every turn regardless of applier file count.
1297
1402
  alwaysVerify: agentic ? true : undefined,
1403
+ // Anti-no-op acceptance rail (P0): success requires an actual tree
1404
+ // change unless the caller explicitly allows a no-op mission.
1405
+ requireDiffForAcceptance: options.allowNoop === true ? false : undefined,
1298
1406
  // From-scratch builds have no artifact at t0, so the runtime execution gate
1299
1407
  // (and build/test/lint) aren't detectable yet. Re-detect each turn so they
1300
1408
  // engage once the model writes files, rather than relying only on the t0
@@ -1517,47 +1625,61 @@ async function runDeliver(instruction, options) {
1517
1625
  const tasks = phases.map((ph) => ({
1518
1626
  id: ph.id, title: ph.title, goal: ph.goal, ...(ph.deps ? { deps: ph.deps } : {}),
1519
1627
  }));
1520
- const orchResult = await orchestrate({
1521
- mission: instruction,
1522
- tasks,
1523
- contextBudgetChars: Number(process.env.UAP_DELIVER_CONTEXT_BUDGET ?? 6000),
1524
- maxTasks: Number(process.env.UAP_DELIVER_MAX_TASKS ?? 40),
1525
- // P3 per-task memory retrieval: pull the few most relevant established
1526
- // decisions/patterns/gotchas for THIS task's goal (a small semantic
1527
- // query, not the full spec), so a fresh-context task reconstructs "what
1528
- // am I building and why" from memory. Fail-soft no design lines.
1529
- retrieveDesign: async (task) => {
1530
- try {
1531
- const { retrieveDynamicMemoryContext } = await import('../memory/dynamic-retrieval.js');
1532
- const mem = await retrieveDynamicMemoryContext(task.goal, projectRoot, { maxTokens: 400 });
1533
- const lines = [];
1534
- for (const m of mem.relevantMemories.slice(0, 4))
1535
- lines.push(m.content.slice(0, 160));
1536
- for (const g of mem.gotchas.slice(0, 2))
1537
- lines.push(`gotcha: ${g.slice(0, 140)}`);
1538
- for (const pat of mem.patterns.slice(0, 2))
1539
- lines.push(`pattern: ${pat.slice(0, 140)}`);
1540
- return lines;
1541
- }
1542
- catch {
1543
- return [];
1544
- }
1545
- },
1546
- // P3/P4 durable publish: a completed task's verified contract lands in
1547
- // short-term memory so later tasks (and future fresh sessions) retrieve
1548
- // the interface from memory instead of re-reading source.
1549
- publish: async (outcome, task) => {
1550
- if (!outcome.success)
1551
- return;
1552
- await recordOrchestratorTaskOutcome(task.id, task.title, outcome.contract ?? outcome.summary, projectRoot);
1553
- },
1554
- runTask: async (ctx, task) => {
1555
- console.log(chalk.bold(`\u25b6 task ${task.id}: ${task.title}`) +
1556
- chalk.dim(` (ctx ${ctx.prompt.length} chars, deps: ${ctx.includedDeps.join(',') || 'none'})`));
1557
- const taskRecord = await openDeliveryTask(`${task.title} — ${task.goal.slice(0, 120)}`, projectRoot, missionTask?.id);
1628
+ // -- Worktree-isolated parallel dispatch --
1629
+ // `.uap.json` deliver.parallelTasks (config-only BY DESIGN - no env; see
1630
+ // the concurrency notes in task-orchestrator.ts) dispatches independent
1631
+ // READY tasks concurrently, EACH in its own detached git worktree seeded
1632
+ // with the main tree's current uncommitted state and judged against its
1633
+ // own spec. Merge-backs are SERIALIZED under a lock; a conflicting merge
1634
+ // fails the task, and the orchestrator's minimal repair retries it in a
1635
+ // fresh workspace seeded with the updated baseline - conflicts resolve
1636
+ // through the ATG repair path instead of corrupting the tree.
1637
+ const parallelTasks = resolveParallelTasks(cfgRaw.deliver?.parallelTasks);
1638
+ const wsManager = parallelTasks > 1 ? createTaskWorkspaceManager(projectRoot) : null;
1639
+ if (parallelTasks > 1 && !wsManager) {
1640
+ console.log(chalk.yellow(' ⇉ parallel tasks: worktree isolation unavailable (not a git repo?) - running sequentially'));
1641
+ }
1642
+ else if (wsManager) {
1643
+ console.log(chalk.cyan(` ⇉ parallel tasks: up to ${parallelTasks} independent tasks in isolated worktrees`));
1644
+ }
1645
+ // Merge-boundary defense-in-depth: refuse a delta touching protected
1646
+ // tests/gate configs at the boundary, instead of trusting the in-worktree
1647
+ // layers transitively (mode-only changes and capture-skipped files would
1648
+ // otherwise ride the patch back).
1649
+ const mergeProtected = new Set(options.protectTests !== false
1650
+ ? [...snapshotProtection(projectRoot).protectedFiles, ...listGateConfigFiles(projectRoot)].map((f) => f.toLowerCase())
1651
+ : []);
1652
+ let mergeChain = Promise.resolve();
1653
+ const withMergeLock = (fn) => {
1654
+ const next = mergeChain.then(fn, fn);
1655
+ mergeChain = next.then(() => undefined, () => undefined);
1656
+ return next;
1657
+ };
1658
+ // Regexes built from strings so policy hooks never mistake a literal for a path.
1659
+ const SOURCE_FILE_RE = new RegExp('[.](js|mjs|cjs|ts|tsx|jsx|py)$');
1660
+ const NEW_TASKS_RE = new RegExp('NEW_TASKS:\\s*(\\[[\\s\\S]*?\\])');
1661
+ /** Run one task's convergence loop against `root` with executor `exec` -
1662
+ * the historic in-tree body, parameterized so isolated workspaces reuse it
1663
+ * verbatim. Turns/history flow into the mission aggregate as before. */
1664
+ const runTaskAt = async (ctx, task, root, exec) => {
1665
+ const taskRecord = await openDeliveryTask(`${task.title} — ${task.goal.slice(0, 120)}`, projectRoot, missionTask?.id);
1666
+ acceptanceSpecByRoot.set(root, ctx.prompt);
1667
+ if (root === projectRoot)
1558
1668
  acceptanceSpec = ctx.prompt;
1559
- specChangeEvidence.writes = 0;
1560
- const loop = new ConvergenceLoop({ ...loopConfig, baselineCheck: false, resumeFrom: undefined, onIteration: makeIterationHook() }, executor, seams);
1669
+ evidenceFor(root).writes = 0;
1670
+ try {
1671
+ const loop = new ConvergenceLoop({
1672
+ ...loopConfig,
1673
+ projectRoot: root,
1674
+ baselineCheck: false,
1675
+ resumeFrom: undefined,
1676
+ // Checkpoints describe THE mission tree; isolated roots must not
1677
+ // clobber the durable run state.
1678
+ // The inherited explorer's workspaceProvider is bound to the MAIN
1679
+ // projectRoot; inside an isolated task it must not run at all.
1680
+ ...(root !== projectRoot ? { onCheckpoint: undefined, explorer: undefined } : {}),
1681
+ onIteration: makeIterationHook(root),
1682
+ }, exec, seams);
1561
1683
  const r = await loop.deliver(ctx.prompt);
1562
1684
  all.turns += r.turns;
1563
1685
  all.history.push(...r.history);
@@ -1570,7 +1692,7 @@ async function runDeliver(instruction, options) {
1570
1692
  all.finalOutput = r.finalOutput;
1571
1693
  completeDeliveryTask(taskRecord, r);
1572
1694
  const files = [...new Set(r.history.flatMap((h) => h.filesApplied ?? []))];
1573
- // P4 extract the VERIFIED public contract of what this task built so
1695
+ // P4 - extract the VERIFIED public contract of what this task built so
1574
1696
  // dependents load the interface (a few hundred chars), not the source.
1575
1697
  let contract;
1576
1698
  if (r.success && files.length > 0) {
@@ -1578,10 +1700,10 @@ async function runDeliver(instruction, options) {
1578
1700
  const { readFileSync } = await import('fs');
1579
1701
  const { join } = await import('path');
1580
1702
  const srcs = files
1581
- .filter((f) => /\.(js|mjs|cjs|ts|tsx|jsx|py)$/.test(f))
1703
+ .filter((f) => SOURCE_FILE_RE.test(f))
1582
1704
  .map((f) => {
1583
1705
  try {
1584
- return { path: f, content: readFileSync(join(projectRoot, f), 'utf-8') };
1706
+ return { path: f, content: readFileSync(join(root, f), 'utf-8') };
1585
1707
  }
1586
1708
  catch {
1587
1709
  return null;
@@ -1594,16 +1716,16 @@ async function runDeliver(instruction, options) {
1594
1716
  contract = undefined;
1595
1717
  }
1596
1718
  }
1597
- // P5 adaptive re-planning feed: a task may surface work the initial
1719
+ // P5 - adaptive re-planning feed: a task may surface work the initial
1598
1720
  // plan missed by emitting a `NEW_TASKS: [ {id,title,goal,deps} ]` JSON
1599
1721
  // array in its output. Parsed through the same validator as the planner
1600
1722
  // (well-formed only), then folded into the DAG by orchestrate() (which
1601
1723
  // dedupes, topo-resorts, and caps at maxTasks). Only structural fields
1602
- // are used never free-form model text as durable memory.
1724
+ // are used - never free-form model text as durable memory.
1603
1725
  let newTasks;
1604
1726
  if (r.success) {
1605
1727
  try {
1606
- const marker = /NEW_TASKS:\s*(\[[\s\S]*?\])/.exec(r.finalOutput || r.finalFeedback || '');
1728
+ const marker = NEW_TASKS_RE.exec(r.finalOutput || r.finalFeedback || '');
1607
1729
  if (marker) {
1608
1730
  const parsed = parsePhaseArray(marker[1]).filter((t) => t.id !== task.id);
1609
1731
  if (parsed.length > 0) {
@@ -1631,8 +1753,121 @@ async function runDeliver(instruction, options) {
1631
1753
  ...(contract ? { contract } : {}),
1632
1754
  ...(newTasks && newTasks.length > 0 ? { newTasks } : {}),
1633
1755
  };
1756
+ }
1757
+ finally {
1758
+ acceptanceSpecByRoot.delete(root);
1759
+ }
1760
+ };
1761
+ const orchResult = await orchestrate({
1762
+ mission: instruction,
1763
+ tasks,
1764
+ contextBudgetChars: Number(process.env.UAP_DELIVER_CONTEXT_BUDGET ?? 6000),
1765
+ maxTasks: Number(process.env.UAP_DELIVER_MAX_TASKS ?? 40),
1766
+ // Dependency-aware parallel dispatch — only with worktree isolation.
1767
+ concurrency: wsManager ? parallelTasks : 1,
1768
+ // P3 — per-task memory retrieval: pull the few most relevant established
1769
+ // decisions/patterns/gotchas for THIS task's goal (a small semantic
1770
+ // query, not the full spec), so a fresh-context task reconstructs "what
1771
+ // am I building and why" from memory. Fail-soft → no design lines.
1772
+ retrieveDesign: async (task) => {
1773
+ try {
1774
+ const { retrieveDynamicMemoryContext } = await import('../memory/dynamic-retrieval.js');
1775
+ const mem = await retrieveDynamicMemoryContext(task.goal, projectRoot, { maxTokens: 400 });
1776
+ const lines = [];
1777
+ for (const m of mem.relevantMemories.slice(0, 4))
1778
+ lines.push(m.content.slice(0, 160));
1779
+ for (const g of mem.gotchas.slice(0, 2))
1780
+ lines.push(`gotcha: ${g.slice(0, 140)}`);
1781
+ for (const pat of mem.patterns.slice(0, 2))
1782
+ lines.push(`pattern: ${pat.slice(0, 140)}`);
1783
+ return lines;
1784
+ }
1785
+ catch {
1786
+ return [];
1787
+ }
1788
+ },
1789
+ // P3/P4 — durable publish: a completed task's verified contract lands in
1790
+ // short-term memory so later tasks (and future fresh sessions) retrieve
1791
+ // the interface from memory instead of re-reading source.
1792
+ publish: async (outcome, task) => {
1793
+ if (!outcome.success)
1794
+ return;
1795
+ await recordOrchestratorTaskOutcome(task.id, task.title, outcome.contract ?? outcome.summary, projectRoot);
1796
+ },
1797
+ runTask: async (ctx, task) => {
1798
+ console.log(chalk.bold(`▶ task ${task.id}: ${task.title}`) +
1799
+ chalk.dim(` (ctx ${ctx.prompt.length} chars, deps: ${ctx.includedDeps.join(',') || 'none'})`));
1800
+ // Workspace acquisition mutates git state (worktree add) - serialize
1801
+ // it behind the same lock the merges use.
1802
+ const ws = wsManager ? await withMergeLock(() => wsManager.acquire(task.id)) : null;
1803
+ if (!ws) {
1804
+ if (wsManager) {
1805
+ console.log(chalk.yellow(` ⇉ task ${task.id}: workspace unavailable - running in-tree (serialized)`));
1806
+ // In-tree execution mutates the shared tree; hold the lock for the
1807
+ // WHOLE run so it never races an isolated task's merge-back.
1808
+ return withMergeLock(() => runTaskAt(ctx, task, projectRoot, executor));
1809
+ }
1810
+ return runTaskAt(ctx, task, projectRoot, executor);
1811
+ }
1812
+ try {
1813
+ // The agentic executor binds tools to a root at construction - give
1814
+ // the isolated task its own, with the same protections re-derived
1815
+ // for its tree. The blind executor is root-free (the per-task loop's
1816
+ // projectRoot directs the applier and gates).
1817
+ const taskExecutor = agentic
1818
+ ? createAgenticExecutor(model, {
1819
+ projectRoot: ws.root,
1820
+ endpoint: agenticEndpoint,
1821
+ temperature,
1822
+ contextTokenBudget: sessionBudget,
1823
+ contractFiles: contractLock,
1824
+ protectedFiles: options.protectTests !== false ? snapshotProtection(ws.root).protectedFiles : new Set(),
1825
+ protectGateConfigs: options.protectTests !== false,
1826
+ allowBash: options.allowBash === true || process.env.UAP_DELIVER_ALLOW_BASH === '1',
1827
+ onEvent: (e) => console.log(chalk.dim(` [${task.id} r${e.round} ${e.kind}${e.tool ? `:${e.tool}` : ''}] ${e.detail ?? ''}`)),
1828
+ })
1829
+ : executor;
1830
+ const outcome = await runTaskAt(ctx, task, ws.root, taskExecutor);
1831
+ if (!outcome.success)
1832
+ return outcome;
1833
+ const merged = await withMergeLock(() => ws.mergeBack((f) => mergeProtected.has(f.toLowerCase())));
1834
+ if (!merged.ok) {
1835
+ console.log(chalk.yellow(` ⇉ task ${task.id}: merge-back conflicted (${merged.reason ?? 'apply failed'}) - minimal repair retries on the updated tree`));
1836
+ return {
1837
+ taskId: task.id,
1838
+ success: false,
1839
+ turns: 0,
1840
+ summary: `${task.goal.slice(0, 120)} — FAILED: the tree changed underneath this task (parallel merge conflict) — rebuild it against the CURRENT state of the repository. Detail: ${(merged.reason ?? 'could not apply the task delta').slice(0, 200)}`,
1841
+ };
1842
+ }
1843
+ if (merged.files.length > 0) {
1844
+ console.log(chalk.dim(` ⇉ task ${task.id}: merged ${merged.files.length} file(s) into the main tree`));
1845
+ }
1846
+ return outcome;
1847
+ }
1848
+ finally {
1849
+ ws.cleanup();
1850
+ }
1634
1851
  },
1635
1852
  });
1853
+ // Post-merge verification: every gate passed in SOME tree, but 3-way
1854
+ // resolution can produce a combined main tree no gate ever saw. One
1855
+ // ladder run on the merged result closes that. Fail-soft on runner
1856
+ // errors; hard on gate failures.
1857
+ if (wsManager && orchResult.success) {
1858
+ try {
1859
+ console.log(chalk.cyan(' ⇉ post-merge verification: gate ladder on the merged main tree…'));
1860
+ const combined = await tieredRunner(rungs, projectRoot, {});
1861
+ if (!combined.passed) {
1862
+ orchResult.success = false;
1863
+ orchResult.failed.push('post-merge-verification');
1864
+ all.finalFeedback = `post-merge verification FAILED on the combined tree:\n${combined.feedback}`.slice(0, 4000);
1865
+ }
1866
+ }
1867
+ catch {
1868
+ // verification unavailable — keep the per-task verdicts
1869
+ }
1870
+ }
1636
1871
  all.success = orchResult.success;
1637
1872
  if (!orchResult.success) {
1638
1873
  all.finalFeedback = `orchestration incomplete — failed tasks: ${orchResult.failed.join(', ')}\n${all.finalFeedback}`;