@miller-tech/uap 1.149.5 → 1.150.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +77 -2
- 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/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
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
# SPEC: P0 deliver hardening — anti-vacuous floor + no-op acceptance rail + autoroute default off
|
|
2
|
+
|
|
3
|
+
Exact, pre-reviewed operations. Apply VERBATIM. See
|
|
4
|
+
docs/plans/deliver-hardening-plan-2026-07-13.md (A2 + C1 + C2 + D2).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## File 1: `src/delivery/convergence-loop.ts`
|
|
9
|
+
|
|
10
|
+
### Op 1.1 — REPLACE
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import type { GateRung, LadderResult, LadderOptions } from './verifier-ladder.js';
|
|
14
|
+
import { detectRungs, runLadder, tierOf } from './verifier-ladder.js';
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
WITH
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { execSync } from 'child_process';
|
|
21
|
+
|
|
22
|
+
import type { GateRung, LadderResult, LadderOptions } from './verifier-ladder.js';
|
|
23
|
+
import { detectRungs, runLadder, tierOf } from './verifier-ladder.js';
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Op 1.2 — REPLACE
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
* applied" optimization permanently scores such turns 0%.
|
|
30
|
+
*/
|
|
31
|
+
alwaysVerify?: boolean;
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
WITH
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
* applied" optimization permanently scores such turns 0%.
|
|
38
|
+
*/
|
|
39
|
+
alwaysVerify?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Withhold acceptance until the run has actually changed the tree
|
|
42
|
+
* (default true). An LLM acceptance judge must never be the only thing
|
|
43
|
+
* standing between a zero-diff run and "delivered" (2026-07-13 incident:
|
|
44
|
+
* a mission on a gates-green repo "delivered" after writing nothing).
|
|
45
|
+
* Change detection: files the applier wrote, else a git tree fingerprint
|
|
46
|
+
* (covers the direct-mutation agentic executor). Fail-closed when neither
|
|
47
|
+
* signal shows a change; skipped automatically on resume (prior-session
|
|
48
|
+
* turns may have written) and via --allow-noop for genuinely no-op
|
|
49
|
+
* missions.
|
|
50
|
+
*/
|
|
51
|
+
requireDiffForAcceptance?: boolean;
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Op 1.3 — REPLACE
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
private readonly acceptanceGate?: AcceptanceGate;
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
WITH
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
private readonly acceptanceGate?: AcceptanceGate;
|
|
66
|
+
/** Every file the applier has written this run (union across turns). */
|
|
67
|
+
private appliedFilesTotal = new Set<string>();
|
|
68
|
+
/** Git tree fingerprint at run start; null when unavailable (non-git). */
|
|
69
|
+
private runStartTreeFingerprint: string | null = null;
|
|
70
|
+
|
|
71
|
+
constructor(
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Op 1.4 — REPLACE
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
this.acceptanceGate = seams.acceptanceGate;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
WITH
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
this.acceptanceGate = seams.acceptanceGate;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Cheap, stable fingerprint of the working tree: porcelain status (covers
|
|
89
|
+
* untracked adds/removes) + diff stat (covers content edits). Returns null
|
|
90
|
+
* outside a git repo or on any git failure.
|
|
91
|
+
*/
|
|
92
|
+
private fingerprintTree(): string | null {
|
|
93
|
+
try {
|
|
94
|
+
const opts = {
|
|
95
|
+
cwd: this.config.projectRoot,
|
|
96
|
+
timeout: 10_000,
|
|
97
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
98
|
+
} as const;
|
|
99
|
+
const status = execSync('git status --porcelain=v1 --untracked-files=all', opts).toString();
|
|
100
|
+
const diff = execSync('git diff HEAD --stat', opts).toString();
|
|
101
|
+
return `${status}\n---\n${diff}`;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Has this run changed the project tree yet? Applier-written files are
|
|
109
|
+
* authoritative; the git fingerprint covers direct-mutation executors
|
|
110
|
+
* (agentic write_file bypasses the applier). FAIL-CLOSED: when neither
|
|
111
|
+
* signal shows a change — including when git is unavailable — the answer
|
|
112
|
+
* is false. See requireDiffForAcceptance.
|
|
113
|
+
*/
|
|
114
|
+
private hasAppliedChanges(): boolean {
|
|
115
|
+
if (this.appliedFilesTotal.size > 0) return true;
|
|
116
|
+
if (this.runStartTreeFingerprint !== null) {
|
|
117
|
+
const now = this.fingerprintTree();
|
|
118
|
+
if (now !== null) return now !== this.runStartTreeFingerprint;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Op 1.5 — REPLACE
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
private async judgeAcceptance(ladder: LadderResult): Promise<{ ladder: LadderResult; acceptanceMet?: number }> {
|
|
128
|
+
if (!this.acceptanceGate || !ladder.passed) return { ladder };
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
WITH
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
private async judgeAcceptance(
|
|
135
|
+
ladder: LadderResult,
|
|
136
|
+
opts: { atBaseline?: boolean } = {}
|
|
137
|
+
): Promise<{ ladder: LadderResult; acceptanceMet?: number }> {
|
|
138
|
+
if (!this.acceptanceGate || !ladder.passed) return { ladder };
|
|
139
|
+
// Anti-no-op rail (P0, 2026-07-13): acceptance is deterministically
|
|
140
|
+
// withheld until the run has changed the tree — this also stops the
|
|
141
|
+
// baseline check from short-circuiting a coding mission as
|
|
142
|
+
// alreadyDelivered on a gates-green repo. At baseline nothing can have
|
|
143
|
+
// changed by definition. Skipped on resume (prior-session turns already
|
|
144
|
+
// wrote; their changes are invisible to this process's fingerprint).
|
|
145
|
+
const unchanged = opts.atBaseline ? true : !this.hasAppliedChanges();
|
|
146
|
+
if ((this.config.requireDiffForAcceptance ?? true) && !this.config.resumeFrom && unchanged) {
|
|
147
|
+
return {
|
|
148
|
+
ladder: {
|
|
149
|
+
...ladder,
|
|
150
|
+
passed: false,
|
|
151
|
+
feedback:
|
|
152
|
+
`${ladder.feedback}\n\nAcceptance withheld: this run has not changed any files yet — a no-op cannot be "delivered". Apply the mission's changes (or re-run with --allow-noop if no change is genuinely required).`.trim(),
|
|
153
|
+
},
|
|
154
|
+
acceptanceMet: 0,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Op 1.6 — REPLACE
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const baseline = (await this.judgeAcceptance(rawBaseline)).ladder;
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
WITH
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const baseline = (await this.judgeAcceptance(rawBaseline, { atBaseline: true })).ladder;
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Op 1.7 — REPLACE
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// Gate-integrity snapshot, taken AFTER the baseline ladder run so files
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
WITH
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
// t0 tree fingerprint for the anti-no-op acceptance rail — taken AFTER
|
|
181
|
+
// the baseline ladder run so files the gates themselves create on first
|
|
182
|
+
// run (e.g. snapshots, lockfiles) don't read as mission changes.
|
|
183
|
+
this.runStartTreeFingerprint = this.fingerprintTree();
|
|
184
|
+
|
|
185
|
+
// Gate-integrity snapshot, taken AFTER the baseline ladder run so files
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Op 1.8 — REPLACE
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const outcome = explorerSettings
|
|
192
|
+
? await this.runExplorerTurn(instruction, prompt, rungs, explorerSettings, executor, ladderRunner, applyOptions)
|
|
193
|
+
: await this.runSingleTurn(prompt, rungs, executor, ladderRunner, applyOptions);
|
|
194
|
+
|
|
195
|
+
// Acceptance: judge ONCE on this turn's committed verdict (single-turn
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
WITH
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
const outcome = explorerSettings
|
|
202
|
+
? await this.runExplorerTurn(instruction, prompt, rungs, explorerSettings, executor, ladderRunner, applyOptions)
|
|
203
|
+
: await this.runSingleTurn(prompt, rungs, executor, ladderRunner, applyOptions);
|
|
204
|
+
|
|
205
|
+
// Anti-no-op rail bookkeeping: fold this turn's applier writes into the
|
|
206
|
+
// run-wide union BEFORE the acceptance judge consults it.
|
|
207
|
+
for (const f of outcome.filesApplied) this.appliedFilesTotal.add(f);
|
|
208
|
+
|
|
209
|
+
// Acceptance: judge ONCE on this turn's committed verdict (single-turn
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## File 2: `src/cli/deliver.ts`
|
|
215
|
+
|
|
216
|
+
### Op 2.1 — REPLACE
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
export function decideGateStrategy(opts: {
|
|
220
|
+
hasAcceptance: boolean;
|
|
221
|
+
noRealGates: boolean;
|
|
222
|
+
forceSelfGate: boolean;
|
|
223
|
+
selfGateAllowed: boolean;
|
|
224
|
+
}): { acceptancePrimary: boolean; needsSelfGate: boolean; noGatesError: boolean } {
|
|
225
|
+
const acceptancePrimary = opts.hasAcceptance && opts.noRealGates && !opts.forceSelfGate;
|
|
226
|
+
const needsSelfGate =
|
|
227
|
+
opts.selfGateAllowed && !acceptancePrimary && (opts.noRealGates || opts.forceSelfGate);
|
|
228
|
+
const noGatesError = opts.noRealGates && !needsSelfGate && !acceptancePrimary;
|
|
229
|
+
return { acceptancePrimary, needsSelfGate, noGatesError };
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
WITH
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
export function decideGateStrategy(opts: {
|
|
237
|
+
hasAcceptance: boolean;
|
|
238
|
+
noRealGates: boolean;
|
|
239
|
+
forceSelfGate: boolean;
|
|
240
|
+
selfGateAllowed: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* Anti-vacuous floor (P0, 2026-07-13): every REQUIRED project gate passed a
|
|
243
|
+
* pre-run baseline probe. Gates that cannot fail are not a convergence
|
|
244
|
+
* target — "delivered" must mean "something that was red is now green" —
|
|
245
|
+
* so a mission self-gate is engaged exactly as if no gates were detected.
|
|
246
|
+
*/
|
|
247
|
+
baselineAllGreen?: boolean;
|
|
248
|
+
}): { acceptancePrimary: boolean; needsSelfGate: boolean; noGatesError: boolean } {
|
|
249
|
+
const acceptancePrimary = opts.hasAcceptance && opts.noRealGates && !opts.forceSelfGate;
|
|
250
|
+
const needsSelfGate =
|
|
251
|
+
opts.selfGateAllowed &&
|
|
252
|
+
!acceptancePrimary &&
|
|
253
|
+
(opts.noRealGates || opts.forceSelfGate || opts.baselineAllGreen === true);
|
|
254
|
+
const noGatesError = opts.noRealGates && !needsSelfGate && !acceptancePrimary;
|
|
255
|
+
return { acceptancePrimary, needsSelfGate, noGatesError };
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Op 2.2 — REPLACE
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
epics?: boolean;
|
|
263
|
+
dryRun?: boolean;
|
|
264
|
+
json?: boolean;
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
WITH
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
epics?: boolean;
|
|
272
|
+
/** `--allow-noop`: permit success without any tree change (disables the
|
|
273
|
+
* anti-no-op acceptance rail for missions that genuinely require none). */
|
|
274
|
+
allowNoop?: boolean;
|
|
275
|
+
dryRun?: boolean;
|
|
276
|
+
json?: boolean;
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Op 2.3 — REPLACE
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
let autoPlan: AutoPlan | undefined;
|
|
284
|
+
if (options.auto !== false && process.env.UAP_DELIVER_AUTO !== '0' && !hasExplicitAidFlags(options)) {
|
|
285
|
+
autoPlan = planAutoOptimization(instruction);
|
|
286
|
+
applyAutoPlan(options, autoPlan);
|
|
287
|
+
if (!options.dryRun) {
|
|
288
|
+
console.log(chalk.cyan(`⚙ auto-optimize: ${autoPlan.summary}`));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
WITH
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
let autoPlan: AutoPlan | undefined;
|
|
297
|
+
if (options.auto !== false && process.env.UAP_DELIVER_AUTO !== '0' && !hasExplicitAidFlags(options)) {
|
|
298
|
+
autoPlan = planAutoOptimization(instruction);
|
|
299
|
+
applyAutoPlan(options, autoPlan);
|
|
300
|
+
if (!options.dryRun) {
|
|
301
|
+
console.log(chalk.cyan(`⚙ auto-optimize: ${autoPlan.summary}`));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Verification RAILS are independent of the optimization AIDS (P0,
|
|
305
|
+
// 2026-07-13): --no-auto / explicit aid flags stand down exploration,
|
|
306
|
+
// critic and ideation, but must not silently drop the acceptance judge —
|
|
307
|
+
// without it, a gates-green no-op run reads as delivered. When the
|
|
308
|
+
// auto-planner did not run, acceptance defaults ON; opt out explicitly
|
|
309
|
+
// with UAP_DELIVER_ACCEPTANCE=0.
|
|
310
|
+
if (!autoPlan && options.acceptance === undefined && process.env.UAP_DELIVER_ACCEPTANCE !== '0') {
|
|
311
|
+
options.acceptance = true;
|
|
312
|
+
if (!options.dryRun) {
|
|
313
|
+
console.log(chalk.cyan('⚖ acceptance judge on (verification rail; UAP_DELIVER_ACCEPTANCE=0 to disable)'));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Op 2.4 — REPLACE
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
const { acceptancePrimary, needsSelfGate, noGatesError } = decideGateStrategy({
|
|
322
|
+
hasAcceptance: Boolean(options.acceptance),
|
|
323
|
+
noRealGates,
|
|
324
|
+
forceSelfGate: options.forceSelfGate === true,
|
|
325
|
+
selfGateAllowed,
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
WITH
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
// Anti-vacuous floor (P0, 2026-07-13 incident): probe the REQUIRED rungs
|
|
333
|
+
// once before choosing the convergence target. If everything is already
|
|
334
|
+
// green, gate-satisfaction cannot measure this mission — a run on a
|
|
335
|
+
// gates-green repo would false-green as a no-op (observed live: a 6-file
|
|
336
|
+
// C++ mission "delivered" after writing nothing, because the only detected
|
|
337
|
+
// gates were unrelated npm web gates). UAP_DELIVER_VACUOUS_FLOOR=0 opts out.
|
|
338
|
+
let baselineAllGreen = false;
|
|
339
|
+
if (
|
|
340
|
+
!noRealGates &&
|
|
341
|
+
options.forceSelfGate !== true &&
|
|
342
|
+
selfGateAllowed &&
|
|
343
|
+
process.env.UAP_DELIVER_VACUOUS_FLOOR !== '0' &&
|
|
344
|
+
!options.dryRun
|
|
345
|
+
) {
|
|
346
|
+
try {
|
|
347
|
+
const requiredRungs = rungs.filter((r) => r.required);
|
|
348
|
+
baselineAllGreen = requiredRungs.length > 0 && runLadder(requiredRungs, projectRoot).passed;
|
|
349
|
+
} catch {
|
|
350
|
+
baselineAllGreen = false; // probe is best-effort; never blocks a run
|
|
351
|
+
}
|
|
352
|
+
if (baselineAllGreen) {
|
|
353
|
+
console.log(
|
|
354
|
+
chalk.cyan(
|
|
355
|
+
'⚖ anti-vacuous floor: all required project gates are ALREADY green — authoring a mission self-gate so success requires real, verified change'
|
|
356
|
+
)
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const { acceptancePrimary, needsSelfGate, noGatesError } = decideGateStrategy({
|
|
362
|
+
hasAcceptance: Boolean(options.acceptance),
|
|
363
|
+
noRealGates,
|
|
364
|
+
forceSelfGate: options.forceSelfGate === true,
|
|
365
|
+
selfGateAllowed,
|
|
366
|
+
baselineAllGreen,
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Op 2.5 — REPLACE
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
if (sg.vacuous) {
|
|
374
|
+
console.log(
|
|
375
|
+
chalk.yellow(
|
|
376
|
+
' ⚠ acceptance gate may be weak (could not force an initially-failing check); running multi-turn anyway.'
|
|
377
|
+
)
|
|
378
|
+
);
|
|
379
|
+
} else {
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
WITH
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
if (sg.vacuous) {
|
|
386
|
+
// P0 hard-fail: a REQUIRED self-gate that passes on the unsolved repo
|
|
387
|
+
// re-opens the false-green door — "delivered" would be meaningless.
|
|
388
|
+
if (process.env.UAP_DELIVER_ALLOW_WEAK_SELF_GATE !== '1') {
|
|
389
|
+
fail(
|
|
390
|
+
'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.'
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
console.log(
|
|
394
|
+
chalk.yellow(
|
|
395
|
+
' ⚠ acceptance gate may be weak (could not force an initially-failing check); UAP_DELIVER_ALLOW_WEAK_SELF_GATE=1 — running multi-turn anyway.'
|
|
396
|
+
)
|
|
397
|
+
);
|
|
398
|
+
} else {
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Op 2.6 — REPLACE
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
// The agentic executor mutates the repo directly (no-op applier), so
|
|
405
|
+
// gates must run every turn regardless of applier file count.
|
|
406
|
+
alwaysVerify: agentic ? true : undefined,
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
WITH
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
// The agentic executor mutates the repo directly (no-op applier), so
|
|
413
|
+
// gates must run every turn regardless of applier file count.
|
|
414
|
+
alwaysVerify: agentic ? true : undefined,
|
|
415
|
+
// Anti-no-op acceptance rail (P0): success requires an actual tree
|
|
416
|
+
// change unless the caller explicitly allows a no-op mission.
|
|
417
|
+
requireDiffForAcceptance: options.allowNoop === true ? false : undefined,
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## File 3: `src/bin/cli.ts`
|
|
423
|
+
|
|
424
|
+
### Op 3.1 — REPLACE
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
.option('--force-self-gate', 'Author a task-specific acceptance gate even when project gates exist')
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
WITH
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
.option('--force-self-gate', 'Author a task-specific acceptance gate even when project gates exist')
|
|
434
|
+
.option('--allow-noop', 'Permit delivery without any tree change (disables the anti-no-op acceptance rail for missions that genuinely require none)')
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## File 4: `templates/hooks/deliver_autoroute.py`
|
|
440
|
+
|
|
441
|
+
### Op 4.1 — REPLACE
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
def _autoroute_enabled() -> bool:
|
|
445
|
+
# Default ON: a blocked source edit auto-routes into `uap deliver` in the
|
|
446
|
+
# background instead of dead-ending the agent. Opt out with
|
|
447
|
+
# UAP_DELIVER_AUTOROUTE=0/off/false/no.
|
|
448
|
+
v = os.environ.get("UAP_DELIVER_AUTOROUTE", "on").lower()
|
|
449
|
+
return v not in {"0", "off", "false", "no"}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
WITH
|
|
453
|
+
|
|
454
|
+
```python
|
|
455
|
+
def _autoroute_enabled() -> bool:
|
|
456
|
+
# Default OFF (P0, 2026-07-13): the auto-spawned deliver run carries only a
|
|
457
|
+
# vacuous "implement the intended change to <file>" hint — the blocked
|
|
458
|
+
# edit's actual content is not plumbed through yet (plan D1) — so a blind
|
|
459
|
+
# background model run is spawned per blocked file. Blind fan-out mangles
|
|
460
|
+
# shared worktrees; the recorded intent + block message let the agent run
|
|
461
|
+
# `uap deliver` itself with the real spec. Opt IN with
|
|
462
|
+
# UAP_DELIVER_AUTOROUTE=1/on/true/yes.
|
|
463
|
+
v = os.environ.get("UAP_DELIVER_AUTOROUTE", "off").lower()
|
|
464
|
+
return v in {"1", "on", "true", "yes"}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Op 4.2 — REPLACE
|
|
468
|
+
|
|
469
|
+
```python
|
|
470
|
+
intent = {"ts": int(time.time()), "tool": tool, "file_path": file_path, "hint": hint}
|
|
471
|
+
spawn = bool(autoroute_on and hint and file_path and file_path not in seen_files)
|
|
472
|
+
message = reason
|
|
473
|
+
if spawn:
|
|
474
|
+
message = reason + " [auto-routed to `uap deliver` — running in the background]"
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
WITH
|
|
478
|
+
|
|
479
|
+
```python
|
|
480
|
+
intent = {"ts": int(time.time()), "tool": tool, "file_path": file_path, "hint": hint}
|
|
481
|
+
spawn = bool(autoroute_on and hint and file_path and file_path not in seen_files)
|
|
482
|
+
message = reason
|
|
483
|
+
if spawn:
|
|
484
|
+
message = reason + " [auto-routed to `uap deliver` — running in the background]"
|
|
485
|
+
elif file_path:
|
|
486
|
+
message = reason + (
|
|
487
|
+
" [intent recorded to .uap/pending-deliver.jsonl — apply it yourself by running"
|
|
488
|
+
" `uap deliver` with the exact intended change as the instruction]"
|
|
489
|
+
)
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
END OF SPEC. After applying, `npm run build` and the vitest suites
|
|
495
|
+
`test/cli/acceptance-verdict.test.ts`, `test/delivery/convergence-loop.test.ts`,
|
|
496
|
+
and `test/hooks/deliver-autoroute-default.test.ts` MUST pass. If an anchor does
|
|
497
|
+
not match verbatim, STOP and report the mismatch instead of improvising.
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -384,11 +384,25 @@ def main() -> None:
|
|
|
384
384
|
# auto-route the blocked change INTO `uap deliver` instead of leaving the
|
|
385
385
|
# agent to flail. `route`/`deliverHint` are advisory extra fields; harness
|
|
386
386
|
# adapters that understand them route, others just show `reason`.
|
|
387
|
+
# P1 (plan D1): carry the blocked edit's ACTUAL content so the harness
|
|
388
|
+
# can record a replayable intent — a vacuous "implement the intended
|
|
389
|
+
# change" hint spawns a blind model run that knows nothing (observed
|
|
390
|
+
# live 2026-07-13). Apply with `uap deliver --pending <file>`.
|
|
391
|
+
intent_payload = {
|
|
392
|
+
k: v
|
|
393
|
+
for k, v in (
|
|
394
|
+
("old_string", args.get("old_string")),
|
|
395
|
+
("new_string", args.get("new_string")),
|
|
396
|
+
("content", args.get("content")),
|
|
397
|
+
)
|
|
398
|
+
if isinstance(v, str)
|
|
399
|
+
}
|
|
387
400
|
emit(
|
|
388
401
|
False,
|
|
389
402
|
msg,
|
|
390
403
|
route="deliver",
|
|
391
404
|
deliverHint=f'implement the intended change to {rel_posix}',
|
|
405
|
+
editIntent=intent_payload or None,
|
|
392
406
|
)
|
|
393
407
|
return
|
|
394
408
|
|
|
Binary file
|
|
@@ -30,11 +30,15 @@ UAP_DIR = ".uap"
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def _autoroute_enabled() -> bool:
|
|
33
|
-
# Default
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
# Default OFF (P0, 2026-07-13): the auto-spawned deliver run carries only a
|
|
34
|
+
# vacuous "implement the intended change to <file>" hint — the blocked
|
|
35
|
+
# edit's actual content is not plumbed through yet (plan D1) — so a blind
|
|
36
|
+
# background model run is spawned per blocked file. Blind fan-out mangles
|
|
37
|
+
# shared worktrees; the recorded intent + block message let the agent run
|
|
38
|
+
# `uap deliver` itself with the real spec. Opt IN with
|
|
39
|
+
# UAP_DELIVER_AUTOROUTE=1/on/true/yes.
|
|
40
|
+
v = os.environ.get("UAP_DELIVER_AUTOROUTE", "off").lower()
|
|
41
|
+
return v in {"1", "on", "true", "yes"}
|
|
38
42
|
|
|
39
43
|
|
|
40
44
|
def decide(out: dict, tool: str, args: dict, autoroute_on: bool, seen_files: set) -> dict:
|
|
@@ -69,12 +73,31 @@ def decide(out: dict, tool: str, args: dict, autoroute_on: bool, seen_files: set
|
|
|
69
73
|
# were blocked and then silently dropped. The hint is what deliver actually
|
|
70
74
|
# runs, so it is the correct spawn key.
|
|
71
75
|
dedup_key = file_path or hint
|
|
76
|
+
# P1 (plan D1): persist the blocked edit's actual old/new content (from the
|
|
77
|
+
# enforcer's editIntent, falling back to the raw tool args) so the intent
|
|
78
|
+
# is REPLAYABLE via `uap deliver --pending <file>` instead of a blind hint.
|
|
79
|
+
edit_intent = out.get("editIntent") or {
|
|
80
|
+
k: v
|
|
81
|
+
for k, v in (
|
|
82
|
+
("old_string", args.get("old_string")),
|
|
83
|
+
("new_string", args.get("new_string")),
|
|
84
|
+
("content", args.get("content")),
|
|
85
|
+
)
|
|
86
|
+
if isinstance(v, str)
|
|
87
|
+
}
|
|
88
|
+
if edit_intent:
|
|
89
|
+
intent["edit"] = edit_intent
|
|
72
90
|
spawn = bool(autoroute_on and hint and dedup_key and dedup_key not in seen_files)
|
|
73
91
|
message = reason
|
|
74
92
|
if spawn:
|
|
75
93
|
message = reason + " [auto-routed to `uap deliver` — running in the background]"
|
|
76
94
|
elif autoroute_on and dedup_key and dedup_key in seen_files:
|
|
77
95
|
message = reason + " [already auto-routed to `uap deliver` for this change — see .uap/autoroute.log / pending-deliver.jsonl]"
|
|
96
|
+
elif file_path:
|
|
97
|
+
message = reason + (
|
|
98
|
+
" [intent recorded to .uap/pending-deliver.jsonl — apply it yourself by running"
|
|
99
|
+
" `uap deliver` with the exact intended change as the instruction]"
|
|
100
|
+
)
|
|
78
101
|
return {"message": message, "route": route, "spawn": spawn,
|
|
79
102
|
"file_path": file_path, "hint": hint, "dedup_key": dedup_key, "intent": intent}
|
|
80
103
|
|