@miller-tech/uap 1.161.19 → 1.161.20
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/delivery/pending-intents.d.ts +14 -0
- package/dist/delivery/pending-intents.d.ts.map +1 -1
- package/dist/delivery/pending-intents.js +74 -2
- package/dist/delivery/pending-intents.js.map +1 -1
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/templates/hooks/deliver_autoroute.py +14 -6
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
|
@@ -8,6 +8,16 @@
|
|
|
8
8
|
* no model involved — then the caller runs the project gates as usual. A
|
|
9
9
|
* mismatched anchor fails loudly (the tree moved since the intent was
|
|
10
10
|
* recorded); nothing is ever fuzzily applied.
|
|
11
|
+
*
|
|
12
|
+
* Replay is REPLAY-ONCE (2026-07-18): an intent that applies (or is detected
|
|
13
|
+
* as already applied) is CONSUMED — removed from the pending log and archived
|
|
14
|
+
* to `.uap/pending-deliver.applied.jsonl`. Before this, every replay run
|
|
15
|
+
* re-scanned the full log and re-applied any intent whose anchor still
|
|
16
|
+
* matched; for insertion-style edits (old_string surviving as a prefix of
|
|
17
|
+
* new_string) that duplicated the inserted hunk on EVERY run — observed
|
|
18
|
+
* 2026-07-18 as a 4x-duplicated block from one intent replayed by
|
|
19
|
+
* hook-detached + manual runs. Stale-anchor skips stay in the log so they
|
|
20
|
+
* remain loudly visible.
|
|
11
21
|
*/
|
|
12
22
|
export interface PendingIntent {
|
|
13
23
|
ts: number;
|
|
@@ -40,6 +50,10 @@ export declare function readPendingIntents(projectRoot: string): PendingIntent[]
|
|
|
40
50
|
* the current content; content-intents overwrite the file whole (that is what
|
|
41
51
|
* the blocked Write would have done). Multiple intents for one file apply in
|
|
42
52
|
* recorded order, so a sequence of blocked Edits replays faithfully.
|
|
53
|
+
*
|
|
54
|
+
* Applied (and detected-already-applied) intents are consumed from the log —
|
|
55
|
+
* replay is idempotent across runs. Stale-anchor and pre-D1 skips are NOT
|
|
56
|
+
* consumed: they stay visible until an operator resolves or clears them.
|
|
43
57
|
*/
|
|
44
58
|
export declare function applyPendingIntents(projectRoot: string, file?: string): PendingApplyResult;
|
|
45
59
|
//# sourceMappingURL=pending-intents.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pending-intents.d.ts","sourceRoot":"","sources":["../../src/delivery/pending-intents.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"pending-intents.d.ts","sourceRoot":"","sources":["../../src/delivery/pending-intents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACvE;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAA;KAAE,CAAC,CAAC;IACxE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9D;AAKD,8EAA8E;AAC9E,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CAevE;AAuCD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAgE1F"}
|
|
@@ -8,10 +8,21 @@
|
|
|
8
8
|
* no model involved — then the caller runs the project gates as usual. A
|
|
9
9
|
* mismatched anchor fails loudly (the tree moved since the intent was
|
|
10
10
|
* recorded); nothing is ever fuzzily applied.
|
|
11
|
+
*
|
|
12
|
+
* Replay is REPLAY-ONCE (2026-07-18): an intent that applies (or is detected
|
|
13
|
+
* as already applied) is CONSUMED — removed from the pending log and archived
|
|
14
|
+
* to `.uap/pending-deliver.applied.jsonl`. Before this, every replay run
|
|
15
|
+
* re-scanned the full log and re-applied any intent whose anchor still
|
|
16
|
+
* matched; for insertion-style edits (old_string surviving as a prefix of
|
|
17
|
+
* new_string) that duplicated the inserted hunk on EVERY run — observed
|
|
18
|
+
* 2026-07-18 as a 4x-duplicated block from one intent replayed by
|
|
19
|
+
* hook-detached + manual runs. Stale-anchor skips stay in the log so they
|
|
20
|
+
* remain loudly visible.
|
|
11
21
|
*/
|
|
12
|
-
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
22
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync } from 'fs';
|
|
13
23
|
import { join, resolve, relative, isAbsolute } from 'path';
|
|
14
24
|
const PENDING_LOG = '.uap/pending-deliver.jsonl';
|
|
25
|
+
const APPLIED_LOG = '.uap/pending-deliver.applied.jsonl';
|
|
15
26
|
/** Read all recorded intents, oldest first. Unparseable lines are ignored. */
|
|
16
27
|
export function readPendingIntents(projectRoot) {
|
|
17
28
|
const log = join(projectRoot, PENDING_LOG);
|
|
@@ -33,17 +44,58 @@ export function readPendingIntents(projectRoot) {
|
|
|
33
44
|
}
|
|
34
45
|
return intents;
|
|
35
46
|
}
|
|
47
|
+
/** Stable identity for consume-filtering (ts + file + exact edit content). */
|
|
48
|
+
function intentKey(i) {
|
|
49
|
+
return JSON.stringify({ ts: i.ts, file_path: i.file_path, edit: i.edit ?? null });
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Remove consumed intents from the pending log (re-reading it first, so lines
|
|
53
|
+
* appended by a concurrent gate hook during this run survive) and archive
|
|
54
|
+
* them to the applied log for audit.
|
|
55
|
+
*/
|
|
56
|
+
function consumeIntents(root, consumed) {
|
|
57
|
+
if (consumed.length === 0)
|
|
58
|
+
return;
|
|
59
|
+
const keys = new Set(consumed.map(intentKey));
|
|
60
|
+
const log = join(root, PENDING_LOG);
|
|
61
|
+
const remaining = [];
|
|
62
|
+
if (existsSync(log)) {
|
|
63
|
+
for (const line of readFileSync(log, 'utf-8').split('\n')) {
|
|
64
|
+
const t = line.trim();
|
|
65
|
+
if (!t)
|
|
66
|
+
continue;
|
|
67
|
+
let keep = true;
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(t);
|
|
70
|
+
if (parsed && typeof parsed.file_path === 'string' && keys.has(intentKey(parsed)))
|
|
71
|
+
keep = false;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* keep garbage lines — readPendingIntents ignores them anyway */
|
|
75
|
+
}
|
|
76
|
+
if (keep)
|
|
77
|
+
remaining.push(t);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
writeFileSync(log, remaining.length ? remaining.join('\n') + '\n' : '', 'utf-8');
|
|
81
|
+
appendFileSync(join(root, APPLIED_LOG), consumed.map((i) => JSON.stringify({ ...i, applied_at: Date.now() })).join('\n') + '\n', 'utf-8');
|
|
82
|
+
}
|
|
36
83
|
/**
|
|
37
84
|
* Deterministically apply the recorded intents for `file` (or every file when
|
|
38
85
|
* omitted). Replace-intents require the old_string to match EXACTLY ONCE in
|
|
39
86
|
* the current content; content-intents overwrite the file whole (that is what
|
|
40
87
|
* the blocked Write would have done). Multiple intents for one file apply in
|
|
41
88
|
* recorded order, so a sequence of blocked Edits replays faithfully.
|
|
89
|
+
*
|
|
90
|
+
* Applied (and detected-already-applied) intents are consumed from the log —
|
|
91
|
+
* replay is idempotent across runs. Stale-anchor and pre-D1 skips are NOT
|
|
92
|
+
* consumed: they stay visible until an operator resolves or clears them.
|
|
42
93
|
*/
|
|
43
94
|
export function applyPendingIntents(projectRoot, file) {
|
|
44
95
|
const root = resolve(projectRoot);
|
|
45
96
|
const wanted = file ? resolve(root, file) : null;
|
|
46
97
|
const result = { applied: [], skipped: [] };
|
|
98
|
+
const consumed = [];
|
|
47
99
|
for (const intent of readPendingIntents(root)) {
|
|
48
100
|
const abs = isAbsolute(intent.file_path) ? intent.file_path : resolve(root, intent.file_path);
|
|
49
101
|
if (wanted && resolve(abs) !== wanted)
|
|
@@ -59,8 +111,14 @@ export function applyPendingIntents(projectRoot, file) {
|
|
|
59
111
|
continue;
|
|
60
112
|
}
|
|
61
113
|
if (typeof edit.content === 'string') {
|
|
114
|
+
if (existsSync(abs) && readFileSync(abs, 'utf-8') === edit.content) {
|
|
115
|
+
result.skipped.push({ file: rel, ts: intent.ts, reason: 'already applied (content identical)' });
|
|
116
|
+
consumed.push(intent);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
62
119
|
writeFileSync(abs, edit.content, 'utf-8');
|
|
63
120
|
result.applied.push({ file: rel, ts: intent.ts, kind: 'write' });
|
|
121
|
+
consumed.push(intent);
|
|
64
122
|
continue;
|
|
65
123
|
}
|
|
66
124
|
if (!existsSync(abs)) {
|
|
@@ -69,6 +127,18 @@ export function applyPendingIntents(projectRoot, file) {
|
|
|
69
127
|
}
|
|
70
128
|
const current = readFileSync(abs, 'utf-8');
|
|
71
129
|
const oldStr = String(edit.old_string);
|
|
130
|
+
const newStr = String(edit.new_string ?? '');
|
|
131
|
+
// Idempotency guard for insertion-style edits (old_string survives inside
|
|
132
|
+
// new_string): after application the anchor STILL matches, so a naive
|
|
133
|
+
// re-run would insert the hunk again. If the new content is already on
|
|
134
|
+
// disk, the intent has been applied — consume it. Edits whose application
|
|
135
|
+
// removes the anchor never reach this branch falsely (their old is not
|
|
136
|
+
// contained in new).
|
|
137
|
+
if (newStr && newStr.includes(oldStr) && current.includes(newStr)) {
|
|
138
|
+
result.skipped.push({ file: rel, ts: intent.ts, reason: 'already applied (new content present)' });
|
|
139
|
+
consumed.push(intent);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
72
142
|
const count = current.split(oldStr).length - 1;
|
|
73
143
|
if (count !== 1) {
|
|
74
144
|
result.skipped.push({
|
|
@@ -78,9 +148,11 @@ export function applyPendingIntents(projectRoot, file) {
|
|
|
78
148
|
});
|
|
79
149
|
continue;
|
|
80
150
|
}
|
|
81
|
-
writeFileSync(abs, current.replace(oldStr,
|
|
151
|
+
writeFileSync(abs, current.replace(oldStr, newStr), 'utf-8');
|
|
82
152
|
result.applied.push({ file: rel, ts: intent.ts, kind: 'replace' });
|
|
153
|
+
consumed.push(intent);
|
|
83
154
|
}
|
|
155
|
+
consumeIntents(root, consumed);
|
|
84
156
|
return result;
|
|
85
157
|
}
|
|
86
158
|
//# sourceMappingURL=pending-intents.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pending-intents.js","sourceRoot":"","sources":["../../src/delivery/pending-intents.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"pending-intents.js","sourceRoot":"","sources":["../../src/delivery/pending-intents.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -23,6 +23,7 @@ intent is logged AND routed. The hook still blocks (exit 2); this only enriches
|
|
|
23
23
|
the block message and kicks off the sanctioned path in the background.
|
|
24
24
|
"""
|
|
25
25
|
import argparse
|
|
26
|
+
import hashlib
|
|
26
27
|
import json
|
|
27
28
|
import os
|
|
28
29
|
import re
|
|
@@ -109,12 +110,6 @@ def decide(out: dict, tool: str, args: dict, autoroute_on: bool, seen_files: set
|
|
|
109
110
|
"file_path": file_path, "hint": hint, "dedup_key": "", "intent": None}
|
|
110
111
|
|
|
111
112
|
intent = {"ts": int(time.time()), "tool": tool, "file_path": file_path, "hint": hint}
|
|
112
|
-
# Dedup on the file when we have one, else on the hint itself. Requiring a
|
|
113
|
-
# file_path made an entire class unspawnable: a BASH-routed source-write
|
|
114
|
-
# (`cat > app.js <<EOF`) carries a `command`, not a path — so those intents
|
|
115
|
-
# were blocked and then silently dropped. The hint is what deliver actually
|
|
116
|
-
# runs, so it is the correct spawn key.
|
|
117
|
-
dedup_key = file_path or hint
|
|
118
113
|
# P1 (plan D1): persist the blocked edit's actual old/new content (from the
|
|
119
114
|
# enforcer's editIntent, falling back to the raw tool args) so the intent
|
|
120
115
|
# is REPLAYABLE via `uap deliver --pending <file>` instead of a blind hint.
|
|
@@ -133,6 +128,19 @@ def decide(out: dict, tool: str, args: dict, autoroute_on: bool, seen_files: set
|
|
|
133
128
|
edit_intent = {"content": bash_content}
|
|
134
129
|
if edit_intent:
|
|
135
130
|
intent["edit"] = edit_intent
|
|
131
|
+
# Dedup PER CHANGE, not per file: base is the file when we have one, else
|
|
132
|
+
# the hint (a BASH-routed source-write carries a `command`, not a path, and
|
|
133
|
+
# requiring a path made that class unspawnable). When the edit's content is
|
|
134
|
+
# recorded, suffix a hash of it — a bare file-path key marked the FILE seen
|
|
135
|
+
# forever, so every LATER (different) blocked edit to the same file was
|
|
136
|
+
# silently swallowed: never recorded as replayable, never applied (observed
|
|
137
|
+
# 2026-07-18). Identical retries of the same edit still dedup exactly.
|
|
138
|
+
dedup_key = file_path or hint
|
|
139
|
+
if edit_intent:
|
|
140
|
+
change_hash = hashlib.sha1(
|
|
141
|
+
json.dumps(edit_intent, sort_keys=True).encode("utf-8", "replace")
|
|
142
|
+
).hexdigest()[:12]
|
|
143
|
+
dedup_key = f"{dedup_key}#{change_hash}"
|
|
136
144
|
# A REPLAYABLE intent (plan D1) carries the blocked edit's exact content, so
|
|
137
145
|
# it can be applied to disk DETERMINISTICALLY via `uap deliver --pending`
|
|
138
146
|
# (writeFileSync of the captured content — no model, no blind fan-out). This
|