@miller-tech/uap 1.165.0 → 1.168.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.
Files changed (41) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/bin/cli.js +47 -2
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/cli/coord.d.ts +1 -1
  5. package/dist/cli/coord.d.ts.map +1 -1
  6. package/dist/cli/coord.js +59 -0
  7. package/dist/cli/coord.js.map +1 -1
  8. package/dist/cli/merge-queue.d.ts +53 -0
  9. package/dist/cli/merge-queue.d.ts.map +1 -0
  10. package/dist/cli/merge-queue.js +239 -0
  11. package/dist/cli/merge-queue.js.map +1 -0
  12. package/dist/cli/worktree.d.ts +69 -1
  13. package/dist/cli/worktree.d.ts.map +1 -1
  14. package/dist/cli/worktree.js +560 -26
  15. package/dist/cli/worktree.js.map +1 -1
  16. package/dist/config/policy-recommendations.d.ts.map +1 -1
  17. package/dist/config/policy-recommendations.js +4 -0
  18. package/dist/config/policy-recommendations.js.map +1 -1
  19. package/dist/coordination/index.d.ts +1 -0
  20. package/dist/coordination/index.d.ts.map +1 -1
  21. package/dist/coordination/index.js +1 -0
  22. package/dist/coordination/index.js.map +1 -1
  23. package/dist/coordination/ownership.d.ts +51 -0
  24. package/dist/coordination/ownership.d.ts.map +1 -0
  25. package/dist/coordination/ownership.js +175 -0
  26. package/dist/coordination/ownership.js.map +1 -0
  27. package/docs/INDEX.md +2 -1
  28. package/docs/guides/PARALLEL_AGENTS.md +209 -0
  29. package/docs/guides/POLICIES.md +1 -0
  30. package/docs/guides/WORKTREE_WORKFLOW.md +5 -2
  31. package/docs/reference/CLI.md +24 -1
  32. package/package.json +1 -1
  33. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  34. package/src/policies/enforcers/branch_freshness.py +134 -0
  35. package/src/policies/enforcers/coord_overlap.py +126 -47
  36. package/src/policies/schemas/policies/branch-freshness.md +81 -0
  37. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  38. package/templates/hooks/coordinate-file.sh +153 -3
  39. package/templates/hooks/session-end.sh +15 -0
  40. package/templates/hooks/session-start.sh +24 -2
  41. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
@@ -0,0 +1,81 @@
1
+ # Policy: Branch Freshness
2
+
3
+ **ID**: `policy-branch-freshness`
4
+ **Name**: Work Against the Latest Integration Branch
5
+ **Category**: workflow
6
+ **Level**: REQUIRED
7
+ **Enforcement Stage**: pre-exec
8
+ **Version**: 1.0
9
+
10
+ ## Purpose
11
+
12
+ Multiple agents working the same codebase in parallel lose each other's work in two
13
+ distinct ways. Only one of them is a "merge conflict".
14
+
15
+ 1. **Concurrent collision** — two agents hold the same file at the same moment.
16
+ Git surfaces this loudly at merge time. Handled by `coordinate-file.sh`'s
17
+ live-agent lock.
18
+ 2. **Sequential drift** — agent A lands a change; agent B's branch was cut before
19
+ that and never re-synced, so B edits a stale copy. B's merge silently reverts
20
+ A's work, or forces a conflict resolution that a model resolves by picking its
21
+ own version. Git does NOT warn about this. This is the expensive one.
22
+
23
+ This policy addresses (2): work must be based on, and kept close to, the current
24
+ integration branch.
25
+
26
+ ## Rules
27
+
28
+ ```rules
29
+ - title: "Fresh Base"
30
+ keywords: ["worktree", "branch", "create", "start work", "new feature"]
31
+ antiPatterns: ["branch from stale local", "no fetch before branching", "based on local master", "detached stale base"]
32
+
33
+ - title: "Stale File Guard"
34
+ keywords: ["edit", "write", "modify", "change file"]
35
+ antiPatterns: ["edit file changed upstream", "ignore upstream change", "overwrite landed work", "revert merged change"]
36
+
37
+ - title: "Drift Ceiling"
38
+ keywords: ["edit", "write", "worktree", "long-running branch"]
39
+ antiPatterns: ["hundreds of commits behind", "never synced", "stale worktree", "abandoned branch still edited"]
40
+ ```
41
+
42
+ ## Enforcement Behavior
43
+
44
+ ### When Triggered
45
+
46
+ - **Worktree creation** — `uap worktree create` fetches and bases the new branch on
47
+ `origin/<default>` rather than whatever the local checkout has at HEAD.
48
+ - **Every worktree file edit** — two independent checks:
49
+ - *file-precise* (`coordinate-file.sh`): blocks when the specific file being
50
+ edited has changed on the integration branch since this branch's merge-base.
51
+ - *branch-coarse* (`branch_freshness.py`): warns at 50 commits behind, blocks at 200.
52
+
53
+ ### Required Actions
54
+
55
+ 1. Cut branches from the fetched remote tip, never from a stale local ref.
56
+ 2. Before editing a file that moved upstream, run `uap worktree sync` and re-apply
57
+ the change on top of the landed version.
58
+ 3. Keep a branch within the drift ceiling for its whole life; sync routinely rather
59
+ than once at `finish`, when resolution is most expensive.
60
+ 4. Land PRs through `uap merge queue` so each merge re-syncs the PRs it impacts.
61
+
62
+ ### Overrides
63
+
64
+ | Variable | Effect |
65
+ |---|---|
66
+ | `UAP_COORD_DRIFT=warn` | file-precise check warns instead of blocking |
67
+ | `UAP_COORD_DRIFT=off` | disable the file-precise check |
68
+ | `UAP_NO_FRESHNESS=1` | disable the branch-coarse check |
69
+ | `UAP_FRESHNESS_WARN` / `UAP_FRESHNESS_BLOCK` | tune the drift thresholds |
70
+ | `uap worktree create --no-fetch` | branch offline, accepting a possibly stale base |
71
+
72
+ All checks fail **open**: no remote, no git, or an unreadable coordination DB
73
+ allows the edit. Coordination being unavailable must never stop work.
74
+
75
+ ## Rationale
76
+
77
+ Measured on this repository before these gates existed: 151 worktrees, the worst
78
+ 1241 commits behind `origin/master`, 23 holding unmerged commits and 15 with
79
+ uncommitted files. A worktree created during that audit was born 1 commit behind
80
+ because creation read local `HEAD` and never fetched. None of that drift was
81
+ visible to any gate, and none of it produced a warning at edit time.
@@ -11,7 +11,9 @@
11
11
  # different worktrees collides (that IS the future merge conflict).
12
12
  #
13
13
  # Exit 0 = allow (no conflict, or only a stale/self-healed announcement → warn).
14
- # Exit 2 = BLOCK: another LIVE agent (heartbeat < THRESHOLD) holds this file.
14
+ # Exit 2 = BLOCK: another LIVE agent (heartbeat < THRESHOLD) holds this file, OR
15
+ # the file has MOVED on the integration branch since this branch's
16
+ # merge-base (editing it now would silently revert landed work).
15
17
  #
16
18
  # Always fails OPEN: any missing dependency or DB error allows the edit. This
17
19
  # hook must never break editing because coordination is unavailable.
@@ -27,10 +29,158 @@ ABS="${6:-}"
27
29
  # Seconds since another agent's last heartbeat for it to count as "live".
28
30
  THRESHOLD="${UAP_COORD_LIVE_SECONDS:-120}"
29
31
 
30
- # Fail open on any missing prerequisite.
32
+ # ---------------------------------------------------------------------------
33
+ # Merge-base drift check (sequential-overwrite protection).
34
+ #
35
+ # The live-lock below only covers CONCURRENT edits — two agents holding the same
36
+ # file at the same moment. The far more common loss is SEQUENTIAL: agent A landed
37
+ # a change to foo.ts an hour ago; agent B's worktree was cut before that and never
38
+ # re-synced, so B edits a stale copy and its merge quietly reverts A's work.
39
+ #
40
+ # So: if THIS exact file changed on the integration branch since our merge-base,
41
+ # block until the agent syncs. Scoped to the single file being edited, so a stale
42
+ # worktree touching untouched files keeps working — no blanket freeze.
43
+ #
44
+ # UAP_COORD_DRIFT=block|warn|off (default block)
45
+ # UAP_COORD_FETCH_SECONDS=<n> throttle for the background fetch (default 600)
46
+ # ---------------------------------------------------------------------------
47
+ DRIFT_MODE="${UAP_COORD_DRIFT:-block}"
48
+ FETCH_THROTTLE="${UAP_COORD_FETCH_SECONDS:-600}"
49
+
50
+ # Validate every numeric knob before it reaches SQL or arithmetic. THRESHOLD is
51
+ # interpolated into the liveness SQL below; a non-numeric value there produced a
52
+ # silent SQL error, which fails the query open and disables the lock entirely.
53
+ case "$THRESHOLD" in ''|*[!0-9]*) THRESHOLD=120 ;; esac
54
+ case "$FETCH_THROTTLE" in ''|*[!0-9]*) FETCH_THROTTLE=600 ;; esac
55
+
56
+ # Bound the git calls in check_drift. Only the fetch was bounded before, so a
57
+ # slow merge-base/diff on a huge repo or a stalled network FS could hang the
58
+ # agent's Edit tool with no diagnostic. macOS has no `timeout` by default.
59
+ if command -v timeout >/dev/null 2>&1; then
60
+ DRIFT_TIMEOUT="timeout -k 1 10"
61
+ elif command -v gtimeout >/dev/null 2>&1; then
62
+ DRIFT_TIMEOUT="gtimeout -k 1 10"
63
+ else
64
+ DRIFT_TIMEOUT=""
65
+ fi
66
+
67
+ [ -n "$ME" ] && [ -n "$REL" ] || exit 0
68
+
69
+ # Resolve the integration ref (origin/HEAD → origin/master → origin/main).
70
+ integration_ref() {
71
+ local d="$1" r
72
+ r=$(git -C "$d" symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null) && {
73
+ printf '%s' "${r#refs/remotes/}"; return 0; }
74
+ for c in master main; do
75
+ git -C "$d" rev-parse --verify --quiet "refs/remotes/origin/$c" >/dev/null 2>&1 && {
76
+ printf 'origin/%s' "$c"; return 0; }
77
+ done
78
+ return 1
79
+ }
80
+
81
+ check_drift() {
82
+ [ "$DRIFT_MODE" = "off" ] && return 0
83
+ command -v git >/dev/null 2>&1 || return 0
84
+ [ -n "$ABS" ] || return 0
85
+
86
+ local dir; dir="${ABS%/*}"
87
+ [ -d "$dir" ] || return 0
88
+
89
+ # Anchor EVERY git call to the working tree that actually contains the file.
90
+ # Running them from the file's own directory was a latent no-op: git resolves a
91
+ # pathspec against the process prefix, so `-C <wt>/src/cli ... -- src/cli/x.ts`
92
+ # looked for <wt>/src/cli/src/cli/x.ts and matched nothing. Drift then "passed"
93
+ # for every file below the repo root — i.e. all real source — silently and
94
+ # fail-open. It only appeared to work for files sitting at the root, which is
95
+ # exactly what the first round of tests used.
96
+ local top; top=$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null) || return 0
97
+ [ -n "$top" ] || return 0
98
+
99
+ # Never fight an in-progress merge/rebase: the conflict markers the agent has to
100
+ # edit are, by definition, in files that moved upstream. Blocking there deadlocks
101
+ # the very resolution `uap worktree sync` just asked for.
102
+ local gitdir; gitdir=$(git -C "$top" rev-parse --absolute-git-dir 2>/dev/null) || return 0
103
+ for marker in MERGE_HEAD REBASE_HEAD CHERRY_PICK_HEAD REVERT_HEAD; do
104
+ [ -e "$gitdir/$marker" ] && return 0
105
+ done
106
+ [ -d "$gitdir/rebase-merge" ] || [ -d "$gitdir/rebase-apply" ] && return 0
107
+
108
+ # Path relative to THIS working tree, derived here rather than trusting the
109
+ # caller's REL — which is relative to the agent's cwd and is wrong whenever the
110
+ # agent stands in the main checkout while editing into a worktree.
111
+ local rel="${ABS#"$top"/}"
112
+ [ "$rel" != "$ABS" ] || return 0
113
+
114
+ local ref; ref=$(integration_ref "$top") || return 0
115
+
116
+ # Throttled fetch: an edit-time network call on EVERY keystroke-scale edit would
117
+ # be intolerable, so refresh at most once per FETCH_THROTTLE seconds. Between
118
+ # refreshes we compare against the last known remote tip — still catches the
119
+ # overwhelming majority of drift, and `uap worktree sync` refreshes on demand.
120
+ local common; common=$(git -C "$top" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) \
121
+ || common=$(git -C "$top" rev-parse --git-common-dir 2>/dev/null) || return 0
122
+ case "$common" in /*) ;; *) common="$top/$common" ;; esac
123
+ # If the stamp dir is not real, DISABLE the fetch rather than the throttle —
124
+ # otherwise a bad path silently means "fetch on every single edit".
125
+ if [ -d "$common" ]; then
126
+ local stamp="$common/.uap-drift-fetch"
127
+ local now; now=$(date +%s)
128
+ local last=0
129
+ [ -f "$stamp" ] && read -r last < "$stamp" 2>/dev/null
130
+ case "$last" in ''|*[!0-9]*) last=0 ;; esac
131
+ # A future-dated stamp (clock skew, or a stray write) would otherwise wedge
132
+ # fetching off indefinitely.
133
+ [ "$last" -gt "$now" ] 2>/dev/null && last=0
134
+ # mkdir is atomic on POSIX and NFS: with many agents sharing one common dir,
135
+ # a read-compare-write throttle lets every agent in the window fetch at once.
136
+ if [ $((now - last)) -ge "$FETCH_THROTTLE" ] && mkdir "$stamp.lock" 2>/dev/null; then
137
+ printf '%s' "$now" > "$stamp.tmp.$$" 2>/dev/null &&
138
+ mv -f "$stamp.tmp.$$" "$stamp" 2>/dev/null || true
139
+ # Detached: nothing in THIS edit depends on the fetch landing — we compare
140
+ # against the last known tip either way. Redirections are load-bearing; a
141
+ # background job holding the hook's stdout makes the harness wait on it.
142
+ (
143
+ GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND='ssh -oBatchMode=yes -oConnectTimeout=5' \
144
+ $DRIFT_TIMEOUT git -C "$top" fetch -q origin "${ref#origin/}" >/dev/null 2>&1
145
+ rmdir "$stamp.lock" 2>/dev/null
146
+ ) </dev/null >/dev/null 2>&1 &
147
+ fi
148
+ # Reap a lock orphaned by a killed agent (older than 10 min).
149
+ if [ -d "$stamp.lock" ]; then
150
+ local lockage; lockage=$(find "$stamp.lock" -maxdepth 0 -mmin +10 2>/dev/null || true)
151
+ [ -n "$lockage" ] && rmdir "$stamp.lock" 2>/dev/null
152
+ fi
153
+ fi
154
+
155
+ local mb; mb=$($DRIFT_TIMEOUT git -C "$top" merge-base HEAD "$ref" 2>/dev/null) || return 0
156
+ [ -n "$mb" ] || return 0
157
+
158
+ # Did this specific path change on the integration branch since we branched?
159
+ # diff-tree (plumbing) skips the work-tree setup and index read that `git diff`
160
+ # performs, and ":(top)" pins the pathspec to the repo root regardless of cwd.
161
+ local moved; moved=$($DRIFT_TIMEOUT git -C "$top" diff-tree -r --name-only --no-renames \
162
+ "$mb" "$ref" -- ":(top)$rel" 2>/dev/null)
163
+ [ -n "$moved" ] || return 0
164
+
165
+ local n; n=$($DRIFT_TIMEOUT git -C "$top" rev-list --count "$mb..$ref" 2>/dev/null || echo '?')
166
+
167
+ if [ "$DRIFT_MODE" = "warn" ]; then
168
+ echo "COORDINATION WARNING: ${rel} changed on ${ref} since your branch point (${n} commits behind). Run 'uap worktree sync' before editing or your merge may revert landed work." >&2
169
+ return 0
170
+ fi
171
+
172
+ echo "{\"decision\":\"block\",\"reason\":\"STALE FILE: ${rel} has changed on ${ref} since your branch point (you are ${n} commits behind). Editing this copy risks silently reverting work that already landed. Run 'uap worktree sync' first, then re-apply your change on top. Override: UAP_COORD_DRIFT=warn.\"}" >&2
173
+ return 2
174
+ }
175
+
176
+ # 0) Sequential-drift check FIRST. It depends only on git, never on the
177
+ # coordination DB — a missing/unwritable DB must not silently disable
178
+ # overwrite protection (it did: the DB prerequisite used to exit 0 above it).
179
+ check_drift || exit 2
180
+
181
+ # The announcement half needs sqlite3 and a real DB file; fail open without them.
31
182
  command -v sqlite3 >/dev/null 2>&1 || exit 0
32
183
  [ -n "$DB" ] && [ -f "$DB" ] || exit 0
33
- [ -n "$ME" ] && [ -n "$REL" ] || exit 0
34
184
 
35
185
  # SQL-escape single quotes.
36
186
  q() { printf '%s' "${1:-}" | sed "s/'/''/g"; }
@@ -50,6 +50,21 @@ if [ -f "$COORD_DB" ]; then
50
50
  OR (strftime('%s','now') - strftime('%s', last_heartbeat)) >= $STALE_SECS
51
51
  );
52
52
  " 2>/dev/null || true
53
+
54
+ # Release THIS agent's own holds immediately. The stale sweep above only frees
55
+ # an agent once its heartbeat has been dead for STALE_SECS — so a session that
56
+ # ended cleanly seconds after an edit kept its files locked against every peer
57
+ # for the full window, and peers saw a "live agent" that had already exited.
58
+ # We know our own id, so there is no reason to wait for it to look dead.
59
+ if [ -n "${UAP_AGENT_ID:-}" ]; then
60
+ ME_Q=$(printf '%s' "$UAP_AGENT_ID" | sed "s/'/''/g")
61
+ sqlite3 "$COORD_DB" "
62
+ UPDATE work_announcements SET completed_at = '$TIMESTAMP'
63
+ WHERE completed_at IS NULL AND agent_id = '$ME_Q';
64
+ UPDATE agent_registry SET status = 'completed'
65
+ WHERE id = '$ME_Q';
66
+ " 2>/dev/null || true
67
+ fi
53
68
  fi
54
69
 
55
70
  # Clean up backup files older than 7 days (retention policy)
@@ -146,9 +146,27 @@ fi
146
146
  GIT_BRANCH=$(git -C "$PROJECT_DIR" branch --show-current 2>/dev/null || echo "?")
147
147
  GIT_DIRTY=$(git -C "$PROJECT_DIR" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
148
148
 
149
+ # .worktrees/ lives in the MAIN checkout, not in a linked worktree — so when the
150
+ # session starts INSIDE a worktree (the normal case under the worktree policy),
151
+ # PROJECT_DIR has no .worktrees/ and the count silently read 0. COORD_ROOT is
152
+ # already resolved from git-common-dir, i.e. the main checkout; reuse it.
149
153
  WORKTREE_COUNT=0
150
- if [ -d "${PROJECT_DIR}/.worktrees" ]; then
151
- WORKTREE_COUNT=$(find "${PROJECT_DIR}/.worktrees" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')
154
+ if [ -d "${COORD_ROOT}/.worktrees" ]; then
155
+ WORKTREE_COUNT=$(find "${COORD_ROOT}/.worktrees" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')
156
+ fi
157
+
158
+ # Worktree drift advisory. A count of worktrees says nothing about whether any of
159
+ # them still hold work — silent accumulation is exactly how parallel-agent work
160
+ # gets lost. Measured here before this line existed: 151 worktrees, worst 1241
161
+ # commits behind origin/master, 23 holding unmerged commits. Purely advisory and
162
+ # time-boxed, so a slow git call can never delay session start.
163
+ WORKTREE_DRIFT=""
164
+ if [ "$WORKTREE_COUNT" -gt 0 ] && command -v uap >/dev/null 2>&1; then
165
+ if command -v timeout >/dev/null 2>&1; then
166
+ WORKTREE_DRIFT=$(cd "$COORD_ROOT" 2>/dev/null && timeout 15 uap worktree hygiene --brief 2>/dev/null || true)
167
+ else
168
+ WORKTREE_DRIFT=$(cd "$COORD_ROOT" 2>/dev/null && uap worktree hygiene --brief 2>/dev/null || true)
169
+ fi
152
170
  fi
153
171
 
154
172
  PATTERN_COUNT=0
@@ -200,6 +218,10 @@ output+="- Worktree gate: run uap worktree ensure --strict (or uap worktree crea
200
218
  output+="- Backup gate: copy files to .uap-backups/$(date +%Y-%m-%d)/ before modification."$'\n'
201
219
  output+="- Coordination: agent=${AGENT_ID}; announce work, check overlaps, complete announcement when done."$'\n'
202
220
  output+="- Validation: run relevant build/tests for changed code before finalizing."$'\n'
221
+ if [ -n "$WORKTREE_DRIFT" ]; then
222
+ output+="- ${WORKTREE_DRIFT}"$'\n'
223
+ output+="- Freshness: branch from the fetched remote tip; \`uap worktree sync\` before editing a file that moved upstream."$'\n'
224
+ fi
203
225
  output+=""$'\n'
204
226
  output+="</system-reminder>"$'\n\n'
205
227