@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin/cli.js +2 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/deliver.d.ts +13 -0
- package/dist/cli/deliver.d.ts.map +1 -1
- package/dist/cli/deliver.js +289 -54
- package/dist/cli/deliver.js.map +1 -1
- package/dist/delivery/agentic-executor.d.ts +24 -0
- package/dist/delivery/agentic-executor.d.ts.map +1 -1
- package/dist/delivery/agentic-executor.js +69 -1
- package/dist/delivery/agentic-executor.js.map +1 -1
- package/dist/delivery/convergence-loop.d.ts +30 -0
- package/dist/delivery/convergence-loop.d.ts.map +1 -1
- package/dist/delivery/convergence-loop.js +72 -2
- package/dist/delivery/convergence-loop.js.map +1 -1
- package/dist/delivery/pending-intents.d.ts +45 -0
- package/dist/delivery/pending-intents.d.ts.map +1 -0
- package/dist/delivery/pending-intents.js +86 -0
- package/dist/delivery/pending-intents.js.map +1 -0
- package/dist/delivery/task-workspace.d.ts +70 -0
- package/dist/delivery/task-workspace.d.ts.map +1 -0
- package/dist/delivery/task-workspace.js +305 -0
- package/dist/delivery/task-workspace.js.map +1 -0
- package/docs/reference/CONFIGURATION.md +9 -0
- package/docs/specs/p0-anti-vacuous.md +497 -0
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/delivery_enforcement.py +14 -0
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/templates/hooks/deliver_autoroute.py +28 -5
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
package/dist/cli/deliver.js
CHANGED
|
@@ -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 &&
|
|
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
|
-
|
|
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
|
-
|
|
1205
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
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
|
-
|
|
1560
|
-
|
|
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
|
|
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) =>
|
|
1703
|
+
.filter((f) => SOURCE_FILE_RE.test(f))
|
|
1582
1704
|
.map((f) => {
|
|
1583
1705
|
try {
|
|
1584
|
-
return { path: f, content: readFileSync(join(
|
|
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
|
|
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
|
|
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 =
|
|
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}`;
|