@miller-tech/uap 2.1.7 → 2.11.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 (142) hide show
  1. package/config/capacity-policy.json +32 -0
  2. package/config/classify-thresholds.json +9 -0
  3. package/config/supervise-policy.json +13 -0
  4. package/dist/.tsbuildinfo +1 -1
  5. package/dist/bin/cli.js +87 -2
  6. package/dist/bin/cli.js.map +1 -1
  7. package/dist/capacity/policy.d.ts +67 -0
  8. package/dist/capacity/policy.d.ts.map +1 -0
  9. package/dist/capacity/policy.js +133 -0
  10. package/dist/capacity/policy.js.map +1 -0
  11. package/dist/capacity/probe.d.ts +23 -0
  12. package/dist/capacity/probe.d.ts.map +1 -0
  13. package/dist/capacity/probe.js +93 -0
  14. package/dist/capacity/probe.js.map +1 -0
  15. package/dist/classify/baseline.d.ts +24 -0
  16. package/dist/classify/baseline.d.ts.map +1 -0
  17. package/dist/classify/baseline.js +140 -0
  18. package/dist/classify/baseline.js.map +1 -0
  19. package/dist/classify/index.d.ts +30 -0
  20. package/dist/classify/index.d.ts.map +1 -0
  21. package/dist/classify/index.js +84 -0
  22. package/dist/classify/index.js.map +1 -0
  23. package/dist/classify/shadow.d.ts +34 -0
  24. package/dist/classify/shadow.d.ts.map +1 -0
  25. package/dist/classify/shadow.js +33 -0
  26. package/dist/classify/shadow.js.map +1 -0
  27. package/dist/classify/types.d.ts +64 -0
  28. package/dist/classify/types.d.ts.map +1 -0
  29. package/dist/classify/types.js +37 -0
  30. package/dist/classify/types.js.map +1 -0
  31. package/dist/cli/classify.d.ts +24 -0
  32. package/dist/cli/classify.d.ts.map +1 -0
  33. package/dist/cli/classify.js +94 -0
  34. package/dist/cli/classify.js.map +1 -0
  35. package/dist/cli/deliver.d.ts.map +1 -1
  36. package/dist/cli/deliver.js +26 -0
  37. package/dist/cli/deliver.js.map +1 -1
  38. package/dist/cli/doctor.d.ts +9 -0
  39. package/dist/cli/doctor.d.ts.map +1 -0
  40. package/dist/cli/doctor.js +49 -0
  41. package/dist/cli/doctor.js.map +1 -0
  42. package/dist/cli/review.d.ts +12 -0
  43. package/dist/cli/review.d.ts.map +1 -0
  44. package/dist/cli/review.js +118 -0
  45. package/dist/cli/review.js.map +1 -0
  46. package/dist/cli/supervise.d.ts +8 -0
  47. package/dist/cli/supervise.d.ts.map +1 -0
  48. package/dist/cli/supervise.js +76 -0
  49. package/dist/cli/supervise.js.map +1 -0
  50. package/dist/delivery/convergence-loop.d.ts +8 -0
  51. package/dist/delivery/convergence-loop.d.ts.map +1 -1
  52. package/dist/delivery/convergence-loop.js +1 -0
  53. package/dist/delivery/convergence-loop.js.map +1 -1
  54. package/dist/delivery/delivery-result.d.ts.map +1 -1
  55. package/dist/delivery/delivery-result.js +5 -0
  56. package/dist/delivery/delivery-result.js.map +1 -1
  57. package/dist/delivery/gate-evidence.d.ts +143 -0
  58. package/dist/delivery/gate-evidence.d.ts.map +1 -0
  59. package/dist/delivery/gate-evidence.js +239 -0
  60. package/dist/delivery/gate-evidence.js.map +1 -0
  61. package/dist/delivery/index.d.ts +1 -0
  62. package/dist/delivery/index.d.ts.map +1 -1
  63. package/dist/delivery/index.js +1 -0
  64. package/dist/delivery/index.js.map +1 -1
  65. package/dist/evals/routing-corpus.d.ts +35 -0
  66. package/dist/evals/routing-corpus.d.ts.map +1 -0
  67. package/dist/evals/routing-corpus.js +142 -0
  68. package/dist/evals/routing-corpus.js.map +1 -0
  69. package/dist/evals/routing-eval.d.ts +104 -0
  70. package/dist/evals/routing-eval.d.ts.map +1 -0
  71. package/dist/evals/routing-eval.js +231 -0
  72. package/dist/evals/routing-eval.js.map +1 -0
  73. package/dist/evals/tfidf.d.ts +35 -0
  74. package/dist/evals/tfidf.d.ts.map +1 -0
  75. package/dist/evals/tfidf.js +132 -0
  76. package/dist/evals/tfidf.js.map +1 -0
  77. package/dist/review/prepass.d.ts +88 -0
  78. package/dist/review/prepass.d.ts.map +1 -0
  79. package/dist/review/prepass.js +428 -0
  80. package/dist/review/prepass.js.map +1 -0
  81. package/dist/review/visual-captures.d.ts +60 -0
  82. package/dist/review/visual-captures.d.ts.map +1 -0
  83. package/dist/review/visual-captures.js +222 -0
  84. package/dist/review/visual-captures.js.map +1 -0
  85. package/dist/supervise/assess.d.ts +73 -0
  86. package/dist/supervise/assess.d.ts.map +1 -0
  87. package/dist/supervise/assess.js +215 -0
  88. package/dist/supervise/assess.js.map +1 -0
  89. package/dist/supervise/config.d.ts +13 -0
  90. package/dist/supervise/config.d.ts.map +1 -0
  91. package/dist/supervise/config.js +81 -0
  92. package/dist/supervise/config.js.map +1 -0
  93. package/dist/supervise/loop.d.ts +78 -0
  94. package/dist/supervise/loop.d.ts.map +1 -0
  95. package/dist/supervise/loop.js +311 -0
  96. package/dist/supervise/loop.js.map +1 -0
  97. package/dist/supervise/observe.d.ts +31 -0
  98. package/dist/supervise/observe.d.ts.map +1 -0
  99. package/dist/supervise/observe.js +172 -0
  100. package/dist/supervise/observe.js.map +1 -0
  101. package/dist/supervise/policy.d.ts +16 -0
  102. package/dist/supervise/policy.d.ts.map +1 -0
  103. package/dist/supervise/policy.js +147 -0
  104. package/dist/supervise/policy.js.map +1 -0
  105. package/dist/supervise/types.d.ts +109 -0
  106. package/dist/supervise/types.d.ts.map +1 -0
  107. package/dist/supervise/types.js +11 -0
  108. package/dist/supervise/types.js.map +1 -0
  109. package/docs/INDEX.md +9 -3
  110. package/docs/architecture/adr/0001-automode-risk-floor.md +75 -0
  111. package/docs/architecture/adr/0002-evidence-bound-ship.md +81 -0
  112. package/docs/guides/CAPACITY_POLICY.md +98 -0
  113. package/docs/performance/automode-risk-calibration.md +72 -0
  114. package/docs/performance/reliability-ladder.md +78 -0
  115. package/docs/plans/system1-uplift-2026-09-18.md +191 -0
  116. package/docs/reference/automode-risk.md +134 -0
  117. package/docs/reference/evidence-bound-ship.md +145 -0
  118. package/docs/reference/review-prepass.md +77 -0
  119. package/docs/reference/routing-evals.md +71 -0
  120. package/docs/reference/semantic-supervisor.md +140 -0
  121. package/docs/reference/system1-classifier.md +103 -0
  122. package/docs/reference/visual-captures.md +113 -0
  123. package/package.json +5 -3
  124. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  125. package/src/policies/enforcers/__pycache__/expert_review_required.cpython-312.pyc +0 -0
  126. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
  127. package/src/policies/enforcers/enforcement_self_protect.py +25 -2
  128. package/src/policies/enforcers/evidence_bound_ship.py +257 -0
  129. package/src/policies/enforcers/expert_review_required.py +120 -0
  130. package/src/policies/schemas/policies/evidence-bound-ship.md +106 -0
  131. package/src/policies/schemas/policies/visual-verification.md +8 -0
  132. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  133. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  134. package/tools/agents/scripts/anthropic_proxy.py +782 -19
  135. package/tools/agents/scripts/project_telemetry.py +52 -0
  136. package/tools/agents/scripts/tool_risk.py +789 -0
  137. package/tools/agents/scripts/tool_risk_calibrate.py +255 -0
  138. package/tools/agents/tests/fixtures/tool_risk_cases.json +124 -0
  139. package/tools/agents/tests/test_enforcer_escape_hatches.py +13 -0
  140. package/tools/agents/tests/test_gate_evidence.py +56 -0
  141. package/tools/agents/tests/test_tool_risk.py +521 -0
  142. package/tools/agents/tests/test_verbatim_compact.py +629 -0
@@ -0,0 +1,77 @@
1
+ # Review Pre-Pass
2
+
3
+ Deterministic ruleset scanner that runs **before** the LLM reviewers in the
4
+ parallel review protocol — item 0.2 of the
5
+ [System-1 uplift plan](../plans/system1-uplift-2026-09-18.md), after the
6
+ open-code-review precedent (deterministic rule pipelines first, LLM agent
7
+ second).
8
+
9
+ ## Running
10
+
11
+ ```bash
12
+ uap review prepass # scan changed files vs upstream base
13
+ uap review prepass --files src/a.ts,src/b.py
14
+ uap review prepass --write # merge findings into the review artifact
15
+ uap review prepass --json
16
+ ```
17
+
18
+ Exit code is 1 when any HIGH finding fires (advisory — blocking stays with
19
+ the `expert-review-required` enforcer), so CI and scripts can gate on it.
20
+
21
+ ## What it catches
22
+
23
+ High-precision, line-anchored rules — the bar is "almost always worth a
24
+ reviewer's glance," because a scanner that cries wolf teaches reviewers to
25
+ ignore it:
26
+
27
+ | Category | Rules |
28
+ | --- | --- |
29
+ | secrets | AWS access key ids, private key blocks, secret-named string literals, GitHub/Slack/LLM-provider/npm token prefixes, JWTs |
30
+ | injection | `eval(`, `new Function(`, interpolated or concatenated `exec`, `shell=True`, `innerHTML =` / `dangerouslySetInnerHTML` / `document.write` / `insertAdjacentHTML`, CORS wildcards (header and middleware forms) |
31
+ | sql | template interpolation or string concatenation building SQL |
32
+ | error-handling | empty `catch` (single- and multi-line forms), Python `except: pass` (single- and two-line forms) |
33
+ | concurrency | Python `threading.Thread` with no `Lock` in the file |
34
+
35
+ Secret rules scan every file type (prose leaks keys too); code rules only
36
+ fire on source extensions. Ambiguous heuristics deliberately stay with the
37
+ LLM reviewers — this pass owns pattern-matching, not judgment.
38
+
39
+ **Secrets findings are redacted**: the snippet in the artifact and on stdout
40
+ replaces the matched secret with `***redacted***`, because the artifact's
41
+ purpose is inclusion in LLM reviewer prompts — the scanner must not multiply
42
+ copies of what it detects. The file:line anchor is enough to inspect the
43
+ secret in place.
44
+
45
+ ## Review protocol integration
46
+
47
+ The `parallel-expert-review` skill runs `uap review prepass --write` as step
48
+ 0. Findings are written to `.uap/reviews/<branch-slug>.pre-pass.json` — a
49
+ **sibling** of the review artifact, never `<branch-slug>.json` itself, because
50
+ the expert-review enforcer treats that file's existence as "a review
51
+ happened". Writes are atomic (tmp + rename), and the slug encoding matches the
52
+ enforcer's `slug_for` exactly. At consolidation the reviewers' verdict merges
53
+ the pre-pass findings into the real artifact; every reviewer receives the
54
+ pre-filtered findings in its prompt scope and must say so explicitly when
55
+ dismissing one.
56
+
57
+ ## Decisions
58
+
59
+ - **`src/review` vs reusing `src/quality/scanner.ts`:** the quality scanner is
60
+ a pure, I/O-free *metrics* scanner (complexity/CRAP/LOC) consumed by the
61
+ quality gate; the pre-pass is a security-*pattern* ruleset with git I/O
62
+ consumed by the review protocol. Different consumers and failure modes.
63
+ - **Sibling artifact, not a `pre_pass` block in the enforcer's file:** the
64
+ enforcer's checks are all conditional on keys the pre-pass never writes, so
65
+ co-locating would let an advisory pass satisfy the blocking gate. (Found in
66
+ parallel review of the introducing PR.)
67
+ - **Advisory exit code, blocking left to the enforcer:** HIGH findings exit 1
68
+ for CI/scripts, but the pre-pass itself never blocks a ship action.
69
+ - **Slug encoding duplicated across TS/Python:** `branchSlug()` mirrors
70
+ `slug_for()` byte-for-byte and is pinned by a cross-checking test.
71
+
72
+ ## Proven on day one
73
+
74
+ The first dogfood scan of this repo's own CLI found a real command-injection
75
+ path (`uap worktree create <slug>` interpolating the argument into a shell
76
+ string) — fixed in the same change that introduced the scanner. Remaining
77
+ interpolated-`execSync` call sites are ticketed for a follow-up sweep.
@@ -0,0 +1,71 @@
1
+ # Routing Evals
2
+
3
+ CI-enforced measurement of how well UAP's routable surfaces stay separable as
4
+ the registry grows: the pattern library (`.factory/patterns/index.json`), the
5
+ droid registry (`.factory/droids/`), and skills (`.factory/skills/`).
6
+
7
+ This is item 0.4 of the
8
+ [System-1 uplift plan](../plans/system1-uplift-2026-09-18.md) (lands with the
9
+ plan PR; merge order: plan first), after the
10
+ agent-dev-team precedent: rank-1 routing evals with planted traps, enforced in
11
+ CI, so a new or edited description that collides with an existing entry fails
12
+ the build instead of silently degrading routing.
13
+
14
+ This is a **separability proxy**, not a replica of production routing — the
15
+ production pattern router is keyword-substring multi-match, and droids/skills
16
+ are routed by the LLM reading descriptions. The report includes the production
17
+ router's fixture inclusion rate as an informational baseline alongside the
18
+ TF-IDF rank-1 numbers.
19
+
20
+ ## Running
21
+
22
+ ```bash
23
+ npm run eval:routing # console report, exit 1 below threshold
24
+ npm run eval:routing -- --json # machine-readable report
25
+ npm run eval:routing -- --threshold 0.95
26
+ ```
27
+
28
+ The same eval also runs as a vitest gate (`test/routing-eval.test.ts`, the
29
+ "shipped corpus eval" block) and in the `routing-evals` GitHub workflow.
30
+
31
+ ## How it works
32
+
33
+ 1. **Corpus loading** (`src/evals/routing-corpus.ts`): patterns, droids, and
34
+ skills become uniform entries keyed `<kind>:<id>`. Entries without a
35
+ frontmatter description fall back to their first body paragraph and are
36
+ reported as *weak entries* — an unroutable-surface finding in itself.
37
+ 2. **Ranking** (`src/evals/tfidf.ts`): pure-TypeScript TF-IDF with naive
38
+ suffix stemming and stopword removal. No embedding model, no network —
39
+ airgap-pure and deterministic.
40
+ 3. **Evaluation** (`src/evals/routing-eval.ts`): fixtures in
41
+ `evals/routing/cases.json` declare a kind, a natural-language query, and
42
+ the expected entry id. Ranking is **kind-scoped** (patterns only against
43
+ patterns, etc.), matching how the surfaces are actually consumed.
44
+
45
+ ## Gate semantics
46
+
47
+ - **Rank-1 accuracy ≥ 90%** over positive cases (configurable via
48
+ `--threshold`).
49
+ - **Negatives are hard-gated**: a case with `expect: null` fails the whole run
50
+ when its top match shares **2+ distinct content tokens** with the query (a
51
+ route claim resting on a single shared word is not a route), or scores above
52
+ a 0.5 cosine backstop. This is ranker-agnostic by design — calibration showed
53
+ raw cosine cannot separate near-domain negatives (single high-idf token, up
54
+ to ~0.31) from weak positives (down to ~0.23). False-routing garbage input
55
+ is not a percentage-point problem.
56
+ - **Fixture integrity**: a case referencing an entry that no longer exists is
57
+ an error, not a failure — stale fixtures can never silently shrink the eval.
58
+
59
+ Local runs read the working tree, so untracked `.factory/` leftovers enter
60
+ the corpus; CI checkouts are clean, so the gate is deterministic there.
61
+ Local failures that CI doesn't reproduce usually mean untracked files.
62
+
63
+ ## Maintaining fixtures
64
+
65
+ - Add one positive case per new pattern/droid/skill when you add the entry.
66
+ - Add a trap (`"trap": true`) whenever two entries are confusable — the
67
+ diff-scoped vs proactive reviewer pairs are the canonical example. A trap
68
+ must still have one defensibly correct answer.
69
+ - If a legitimate case fails, prefer improving the entry's description (that
70
+ is the eval working as intended) over rewording the query to dodge the
71
+ collision.
@@ -0,0 +1,140 @@
1
+ # Semantic Supervisor (`uap supervise`)
2
+
3
+ A watchdog loop for `uap deliver` missions. It observes a run, assesses nine
4
+ dimensions, and applies a deterministic, reviewed policy. Models may only
5
+ **assess** — the decision is never made by a model.
6
+
7
+ ```
8
+ uap supervise [runId|latest] [--once] [--json] [--interval-ms N] [-d projectDir]
9
+ ```
10
+
11
+ - **Watch mode** (default): reassesses at most every `debounceMs` (5s,
12
+ state-change triggered) and at least every `intervalMs` (30s, heartbeat).
13
+ STOP and FINISH end the watch; ESCALATE **keeps watching** — a paged human
14
+ may take a while — until the mission ends or 20 consecutive suppressed
15
+ ESCALATE repeats go unanswered, at which point the watcher writes a final
16
+ `escalation unanswered` event and detaches. Ctrl-C detaches the watcher at
17
+ any time; the mission is unaffected. Watch mode exits 2 when the final
18
+ decision is STOP or ESCALATE, 0 otherwise.
19
+ - **`--once`**: one observe → assess → decide → persist cycle, prints the
20
+ decision, exits. Exit code 0, or 2 when the action is STOP/ESCALATE (or the
21
+ supervisor refused to run).
22
+ - **`--json`**: machine-readable decision; errors are emitted as JSON too.
23
+
24
+ ## What it observes
25
+
26
+ Bounded snapshots only: the sanitized run state from
27
+ `.uap/deliver-runs/<runId>/state.json` (via the same loader `uap deliver
28
+ --resume` uses), the last 8KB of the newest matching `.uap/deliver-logs/*.log`
29
+ (fd seek, never a whole-file read), `git status --porcelain` line count, and a
30
+ capped `git diff --stat` (both via arg-array `execFileSync` with
31
+ `core.fsmonitor`/`core.untrackedCache` disabled, 5s timeout, fail-soft).
32
+ Nothing unbounded is ever read.
33
+
34
+ **Path discipline.** Log reads lstat first and silently skip anything that is
35
+ not a plain regular file (symlinks, FIFOs). Every directory the supervisor
36
+ writes through — `.uap/supervise/<runId>` and, for the STOP file,
37
+ `.uap/deliver-runs/<runId>` — is lstat'd and realpath-checked before use; a
38
+ symlinked or non-directory path throws `SupervisorError` instead of writing
39
+ outside the project.
40
+
41
+ ## Dimensions
42
+
43
+ Nine per cycle: `progress-stalled`, `off-track`, `human-needed`,
44
+ `stuck-loop`, `completion-signaled`, `verification-pending` (flags), and
45
+ `error-density`, `iteration-pressure`, `risk` (1–5 scores). Heuristics always
46
+ answer. The human-needed matcher is interrogative-only: credential prompts
47
+ and approve/confirm questions fire; declaratives like "Permission denied" or
48
+ "approved the change" do not. Healthy summary lines like "0 failed" never
49
+ count as error evidence.
50
+
51
+ An optional classifier (SYS1 contract, injected structurally — see Decisions)
52
+ is asked only the built-in questions (`escalation-risk` noul, `action-class`
53
+ choice). `escalation-risk=true` at probability ≥ `classifierTau` and
54
+ confidence ≥ `classifierConfidenceMin` raises `off-track`; `action-class`
55
+ `escalate` raises `stuck-loop`, `stop` raises `off-track`. The classifier may
56
+ **raise** a flag, never clear one, and a throwing or absent classifier leaves
57
+ the heuristics fully in charge. That is the fail-closed posture: model
58
+ failure degrades to reviewed heuristics, never to looser supervision.
59
+
60
+ ## Actions
61
+
62
+ Priority is fixed and safety-first: **terminal status → FINISH (delivered) /
63
+ CONTINUE · iteration bounds → STOP · human-needed → ESCALATE ·
64
+ off-track/stuck → RETRY (else ESCALATE when retries are spent) ·
65
+ verification-pending → VERIFY · else CONTINUE.**
66
+
67
+ | Action | Effect |
68
+ | --- | --- |
69
+ | `CONTINUE` | none — mission is within bounds |
70
+ | `STOP` | writes the run's cooperative **STOP file** (`requestStop`). The deliver loop checkpoints and exits at its next turn boundary. **Never a signal, never a kill.** Outranks even human-need: an over-budget mission that is also waiting on a human is stopped, with the human-need recorded as `alsoEscalate` evidence. |
71
+ | `RETRY` | consumes one retry from the budget (recorded; persists across processes) |
72
+ | `VERIFY` | recorded prompt to verify; advisory. Never twice consecutively, and bounded by `maxVerify` — an unanswered verification falls into the stuck path (RETRY/ESCALATE) |
73
+ | `FINISH` | records completion. Requires the authoritative `status: delivered`; a log tail claiming "mission complete" on a still-running mission never detaches supervision |
74
+ | `ESCALATE` | event + console warning only; once per streak |
75
+
76
+ The supervisor cannot kill, signal, or restart a mission. The only mutation it
77
+ ever performs on a run is the cooperative STOP file. A run that is no longer
78
+ running (`delivered`/`failed`/`interrupted`) is never STOPped or ESCALATEd,
79
+ whatever its log tail contains.
80
+
81
+ ## Oscillation guards
82
+
83
+ - `VERIFY` can never fire twice consecutively — an intervening non-VERIFY
84
+ action must re-arm it (policy-level, unit-tested).
85
+ - `VERIFY` is budgeted: after `maxVerify` unanswered requests the mission is
86
+ treated as stuck.
87
+ - `ESCALATE` and `FINISH` warn/act once per streak; repeats are recorded with
88
+ `suppressed: true` instead of re-firing. After 20 consecutive suppressed
89
+ ESCALATEs the watcher writes `escalation unanswered` and detaches.
90
+
91
+ ## State and ledger
92
+
93
+ Per supervised run under `.uap/supervise/<runId>/`:
94
+
95
+ - `state.json` — assessments count, retry/verify budgets spent, last action
96
+ (atomic tmp+rename writes, mode 0600). This is what makes the retry budget
97
+ survive across separate `--once` invocations.
98
+ - `events.jsonl` — one v-stamped record per cycle: action, reason, evidence,
99
+ and every dimension's value **with its source** (`heuristic` |
100
+ `classifier`). File mode 0600.
101
+
102
+ ## Policy config
103
+
104
+ Thresholds ship reviewed in `config/supervise-policy.json`:
105
+
106
+ ```json
107
+ { "version": 1, "stallMinutes": 10, "maxMinutes": 120, "maxTurns": 50,
108
+ "maxRetries": 2, "maxFailures": 3, "debounceMs": 5000, "intervalMs": 30000,
109
+ "classifierConfidenceMin": 0.7, "classifierTau": 0.6, "maxVerify": 3 }
110
+ ```
111
+
112
+ Loading is loud: a missing, corrupt, mis-versioned, or out-of-range file
113
+ throws `SupervisorError` and the supervisor refuses to run — it never invents
114
+ defaults. `UAP_SUPERVISE_POLICY` points at an alternate policy file (tests,
115
+ staged rollouts).
116
+
117
+ ## Decisions
118
+
119
+ - **Budget authority is `max(policy, deliver budget)`.** The STOP threshold
120
+ for elapsed time is `max(maxMinutes, runBudgetMinutes(projectRoot))` — an
121
+ operator who raised a mission's budget (`UAP_DELIVER_MAX_MINUTES` or
122
+ `.uap.json delivery.maxRunMinutes`) must not be overruled by the
123
+ supervisor's static 120. A *lower* operator budget needs no supervisor
124
+ support: the deliver loop stops itself first.
125
+ - **Classifier is a structural contract, not an import.** The SYS1 classifier
126
+ lands in PR #807; importing unmerged code would break the build the moment
127
+ either branch moves. Until #807 merges, `ClassifierLike` mirrors its shape,
128
+ only the built-in question names are ever sent (the #807 baseline throws on
129
+ custom names), and `recordShadow` wiring is deferred — the per-dimension
130
+ `source` field in `events.jsonl` is the seam it will write through. When
131
+ #807 lands: replace the interface with the real import, reconcile the
132
+ `noul`/`choice` kind vocabulary, and wire shadow recording.
133
+ - **STOP outranks ESCALATE.** A mission past its iteration budget stops even
134
+ when it is also blocked on a human — stopping is the safer default and the
135
+ human-need survives as `alsoEscalate` evidence in the ledger.
136
+ - **FINISH requires authoritative state.** Only `status: delivered` ends
137
+ supervision; success-shaped log lines are evidence, never proof.
138
+ - **Escalations are bounded.** Twenty unanswered ESCALATE repeats detach the
139
+ watcher with a final ledger event — supervision that nobody reads must not
140
+ run forever.
@@ -0,0 +1,103 @@
1
+ # System-1 Classifier
2
+
3
+ Uplift §6: the local, airgap-pure backend every Wave 1 consumer (semantic
4
+ supervisor 1.1, compaction 1.2, AutoMode 1.3) talks to through **one
5
+ interface**, so the model can evolve without touching policy code.
6
+
7
+ ## Interface
8
+
9
+ ```ts
10
+ assess(state, questions) -> { [name]: { value, probability, confidence, backend } }
11
+ ```
12
+
13
+ Question shapes mirror the noul/score/choice families so Jev-compatible
14
+ fixtures work unchanged:
15
+
16
+ | Question | Kind | Asks |
17
+ | --- | --- | --- |
18
+ | `escalation-risk` | noul | Does this state need escalation to a stronger technique or human review? |
19
+ | `task-risk` | score (1–5) | How risky is the operation (1 trivial, 5 irreversible/destructive)? |
20
+ | `action-class` | choice | `proceed` / `verify-first` / `escalate` / `stop` |
21
+
22
+ v1 ships **one dimension end-to-end** (escalation risk) per the plan's scope
23
+ discipline; the other two questions ride the same plumbing.
24
+
25
+ ## CLI
26
+
27
+ ```bash
28
+ uap classify "<state text>" # all built-in questions
29
+ uap classify "<state>" -q escalation-risk
30
+ uap classify "<state>" --shadow # also append to the shadow log
31
+ uap classify --bench 500 # latency self-check
32
+ uap classify "<state>" --json
33
+ ```
34
+
35
+ Advisory: exit 0 on success, 1 on usage errors or a failed latency bench.
36
+
37
+ ## Backend
38
+
39
+ `baseline-tfidf-v1`: deterministic token scoring against incident/destructive
40
+ vocabulary seeded from UAP's own telemetry language (the 2026-09-18 OOM
41
+ timeline, gate outcomes), squashed through a logistic curve. CPU-only, no
42
+ dependencies, no network, measured **p95 ≈ 0.02ms** against the 50ms budget.
43
+ The trained-head slot (embedding + logistic/isotonic on nomic embeddings)
44
+ inserts into `defaultBackend()` when a head ships — consumers don't change.
45
+
46
+ ## Thresholds are policy, not model output
47
+
48
+ `config/classify-thresholds.json` (project) or
49
+ `~/.config/uap/classify-thresholds.json` (operator):
50
+
51
+ ```json
52
+ { "version": 1, "questions": { "escalation-risk": { "tau": 0.6, "confidenceFloor": 0.3 } } }
53
+ ```
54
+
55
+ - `tau` decides noul questions (`probability >= tau`) — noul only;
56
+ score/choice return their value directly, gated by `confidenceFloor`.
57
+ - Below `confidenceFloor` the assessment carries `defer: true` — **the
58
+ heuristic decides**. That is the shadow-mode contract.
59
+
60
+ Probability semantics per kind (consumers must rely on 0..1 ordering only):
61
+ noul = probability the answer is true; score = 1 − normalized rounding
62
+ distance; choice = winning score's share. The calibrated head replaces these
63
+ approximations with true probabilities when it ships.
64
+
65
+ ## Shadow mode and promotion
66
+
67
+ `--shadow` appends to `.uap/classify-shadow.jsonl` (git-ignored, local-only,
68
+ mode 0600). The record is `{v, ts, stateHash, wordCount, questions, result,
69
+ heuristicDecision?}` — a SHA-256 of the state text, never the text itself,
70
+ so the log carries no raw-text surface (the hash is equality-revealing for
71
+ low-entropy boilerplate states; acceptable for a local join key).
72
+ `heuristicDecision` is the other half of the divergence loop: Wave 1
73
+ consumers record what their heuristic actually decided, so the promotion
74
+ gate can measure agreement from the log alone. Calibration joins outcome
75
+ labels by hash.
76
+
77
+ Promotion path (per the plan's risk section): classifier assesses, heuristics
78
+ decide, divergence accumulates in the shadow log; a trained head is promoted
79
+ only after measured agreement, and the calibration report lands in-repo with
80
+ the thresholds that came from it.
81
+
82
+ ## Decisions
83
+
84
+ - **Baseline before head.** An untrained embedding+head is unproven; the
85
+ deterministic baseline is the floor a head must beat on held-out shadow
86
+ data before it serves policy.
87
+ - **Empty states get zero confidence**, not a confident guess — a blind
88
+ classifier must say so (same doctrine as `uap doctor`'s UNKNOWN).
89
+ - **One GPU/CPU probe budget.** Assessments are synchronous and batched per
90
+ call; Wave 1 consumers batch questions per state.
91
+ - **The baseline is question-scoped, not question-blind.** It answers only
92
+ the built-in questions and refuses others loudly — a custom noul question
93
+ must not silently inherit the escalation vocabulary. The trained head is
94
+ the generalizer.
95
+ - **Kinship:** `src/models/complexity.ts` runs the same
96
+ heuristic-now-model-behind-the-same-interface-later play for task
97
+ complexity. The two should converge deliberately when the trained head
98
+ lands, not drift apart.
99
+
100
+ Scope note: this is the **foundation slice** of uplift §6. The acceptance
101
+ items that need shadow data and consumers — calibration report committed,
102
+ supervisor/compaction/AutoMode running against the backend — land with the
103
+ Wave 1 PRs that wire those consumers.
@@ -0,0 +1,113 @@
1
+ # Visual Captures (Review Binding)
2
+
3
+ Uplift 0.3 of the [System-1 uplift plan](../plans/system1-uplift-2026-09-18.md):
4
+ any diff touching UI files requires before/after captures bound to the
5
+ review, and completion gate 7 (self-review) refuses DONE without them.
6
+
7
+ `uap design check` remains the design-token enforcer; `uap verify` /
8
+ `visual_verification.py` remain the runtime render gate. This layer closes
9
+ the gap between them: deterministic proof that a human-or-vision reviewer
10
+ *looked at rendered before/after evidence* for the exact diff being shipped.
11
+
12
+ ## Surface definition
13
+
14
+ A file is UI when its extension is `.css .scss .sass .less .tsx .jsx .vue
15
+ .svelte .html .astro`, or it lives under `web/`, `src/dashboard/`, or
16
+ `public/`. The same definition is mirrored in
17
+ `src/review/visual-captures.ts` (`UI_EXT`/`UI_DIR_PREFIXES`),
18
+ `src/policies/enforcers/expert_review_required.py`, and
19
+ `visual_verification.py` — keep all three in sync.
20
+
21
+ ## CLI
22
+
23
+ ```bash
24
+ uap review captures add --before <img> --after <img> [--tool tuistory] [--note "..."]
25
+ uap review captures check [--json] # also: bare `uap review captures`
26
+ ```
27
+
28
+ - `add` registers one before/after pair for the current branch. Capture files
29
+ must exist inside the project (absolute paths under the project are
30
+ relativized; paths escaping the project are refused). Pairs accumulate.
31
+ - `check` validates the current diff and exits 1 when UI files lack a
32
+ complete, fresh pair — on the `--json` path too, so CI can gate on it.
33
+
34
+ Captures are produced by the agent with a capture tool: `agent-browser` for
35
+ web surfaces, `tuistory`/`pty-capture` for terminal ones. The tool only
36
+ proves pairing and freshness; judgment about what the captures show stays
37
+ with the (vision-capable) reviewer.
38
+
39
+ ## Artifact
40
+
41
+ `.uap/reviews/<branch-slug>.captures.json` — a **sibling** of the review
42
+ artifact, never `<branch-slug>.json` itself, because the expert-review
43
+ enforcer treats that file's existence as "a review happened" (the same
44
+ decision as the [review pre-pass](review-prepass.md)). Writes are atomic
45
+ (tmp + rename), and review consolidation embeds the capture paths into the
46
+ real artifact.
47
+
48
+ ```json
49
+ {
50
+ "branch": "feature/x",
51
+ "head": "…",
52
+ "ui_files": ["src/app.tsx"],
53
+ "captures": [
54
+ { "tool": "tuistory", "before": ".uap/visual/before.png",
55
+ "after": ".uap/visual/after.png", "at": "2026-09-18T…" }
56
+ ],
57
+ "at": "2026-09-18T…"
58
+ }
59
+ ```
60
+
61
+ ## Ship-time enforcement
62
+
63
+ `expert_review_required.py` runs the captures check **before** its low-risk
64
+ early-allow: UI-only diffs are exactly what the gate exists for, and they
65
+ would otherwise wave through unobserved. The check blocks the ship when:
66
+
67
+ 1. the diff vs the upstream base touches UI files and no captures artifact
68
+ exists;
69
+ 2. the artifact has no complete before/after pair;
70
+ 3. a referenced capture file is missing on disk;
71
+ 4. any UI file's mtime postdates the captures (stale — re-capture), with the
72
+ same 1-second grace the commit-time visual enforcer uses.
73
+
74
+ Fail-open posture matches the rest of the enforcer: when no upstream base
75
+ resolves, or a PR is shipped by reference (`gh pr merge <N>`) so the local
76
+ diff is not the PR's contents, the check is skipped. In the PR-ship case the
77
+ captures reach the trail through the review artifact's embedded capture paths
78
+ (the consolidation contract), not through a mechanical check.
79
+
80
+ The gate fires on every ship verb, so on a branch that already contains UI
81
+ commits, each later commit needs captures fresher than the newest UI mtime —
82
+ per-commit freshness, deliberately heavier than per-review.
83
+
84
+ **Bypass surface (all three routes, deliberately):**
85
+
86
+ 1. `UAP_VISUAL_GATE_OFF=1` — environment-only, set by whoever launches the
87
+ session; never parsed from the command line (an inline form would be
88
+ self-grantable), and inline/persisted assignments are refused by
89
+ `enforcement_self_protect.py`.
90
+ 2. `UAP_NO_REVIEW=1` — the operator's global ship-gate override, same shape.
91
+ 3. An expert-review waiver file (`policies/waivers/*expert-review*.md` or
92
+ `.uap/reviews/WAIVER`) — a deliberate, committable act; waiving review
93
+ waives captures too (the waiver check precedes the captures check).
94
+
95
+ Artifact fields `branch`, `head`, and `ui_files` are informational context
96
+ for reviewers, not verified claims: the gate checks pairing, existence, and
97
+ freshness only — an agent can self-author an artifact, which is accepted
98
+ scope ("deterministic pairing, not judgment"; mtime freshness is gameable
99
+ with `touch` and is an honesty scaffold, not a tamper-proof log).
100
+
101
+ ## Decisions
102
+
103
+ - **Why a separate layer when `uap verify` already renders?** The render gate
104
+ proves the UI *runs* (not blank/static/erroring) on entry pages it can
105
+ find headlessly. Captures prove a reviewer *looked at the change* —
106
+ including surfaces the render gate cannot reach (TUI flows, states behind
107
+ interaction) — and bind that evidence to the review trail.
108
+ - **Why the enforcer and not just the skill?** The skill is prose; the
109
+ enforcer makes "gate 7 refuses DONE on UI diffs without captures" a
110
+ deterministic fact at ship time, including UI-only diffs that skip the
111
+ parallel review.
112
+ - **Freshness by mtime, like `visual_verification.py`:** cheap, deterministic,
113
+ and matches the existing commit-time backstop's semantics (1s grace).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "2.1.7",
3
+ "version": "2.11.0",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -21,9 +21,10 @@
21
21
  "start": "node dist/bin/cli.js",
22
22
  "test": "vitest",
23
23
  "test:ci": "vitest run",
24
- "test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_conditional_destructive_verbs tools.agents.tests.test_live_deliver_kill tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_schema_diff_inline tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer tools.agents.tests.test_gate_evidence tools.agents.tests.test_repeat_call_guard tools.agents.tests.test_repeat_guard tools.agents.tests.test_gate_integrity tools.agents.tests.test_gate_failclosed_indirection tools.agents.tests.test_gate_failclosed_schema_diff tools.agents.tests.test_expert_review_pr_scope tools.agents.tests.test_path_containment tools.agents.tests.test_path_normalizer_hardened tools.agents.tests.test_sandbox_tool_strip tools.agents.tests.test_proxy_env_loader tools.agents.tests.test_stream_required_tool tools.agents.tests.test_tool_call_wrapper_profiles tools.agents.tests.test_tool_convert_cache tools.agents.tests.test_doubling_break tools.agents.tests.test_error_loop_ignores_correctives tools.agents.tests.test_anthropic_proxy_tool_arg_types tools.agents.tests.test_anthropic_sse_schema tools.agents.tests.test_attractor_detection tools.agents.tests.test_client_disconnect tools.agents.tests.test_confidence_escalation tools.agents.tests.test_coordination_ban tools.agents.tests.test_coordination_early_ban tools.agents.tests.test_cycle_break_exploration tools.agents.tests.test_deferral_break tools.agents.tests.test_deliver_autoroute tools.agents.tests.test_delivery_enforcement_all_langs tools.agents.tests.test_delivery_enforcement_exemptions tools.agents.tests.test_delivery_enforcement_filepath tools.agents.tests.test_delivery_enforcement_write_complexity tools.agents.tests.test_fastpath_config tools.agents.tests.test_delivery_enforcement_web_and_bash tools.agents.tests.test_disconnect_watcher tools.agents.tests.test_empty_maxtokens_recovery tools.agents.tests.test_empty_tool_loop_break tools.agents.tests.test_enforcer_escape_hatches tools.agents.tests.test_error_loop_break tools.agents.tests.test_finalize_suppression tools.agents.tests.test_malformed_unclosed_think tools.agents.tests.test_mandate_beats_recon tools.agents.tests.test_mandate_deliver tools.agents.tests.test_overflow_truncate_count_tokens tools.agents.tests.test_passthrough_oauth tools.agents.tests.test_project_telemetry tools.agents.tests.test_proxy_auth_headers tools.agents.tests.test_prune_preserve_force_write tools.agents.tests.test_recon_deliver_gate tools.agents.tests.test_session_admission tools.agents.tests.test_stream_heartbeat tools.agents.tests.test_stuck_break_reattach tools.agents.tests.test_stuck_break_hard tools.agents.tests.test_turn_count_breaker_periodic tools.agents.tests.test_upstream_chokepoint tools.agents.tests.test_vision_passthrough tools.agents.tests.test_worktree_required tools.agents.tests.test_enforcer_suite_coverage tools.agents.tests.test_validate_plan_gate tools.agents.tests.test_validate_plan_inside_project tools.agents.tests.test_anthropic_proxy_streaming tools.agents.tests.test_non_stream_resp_log tools.agents.tests.test_delivery_enforcement_worktree tools.agents.tests.test_output_token_ceilings tools.agents.tests.test_tool_narrowing_core tools.agents.tests.test_models_context_window tools.agents.tests.test_thinking_template_kwargs tools.agents.tests.test_cycle_break_wait_poll tools.agents.tests.test_upstream_model_aliases tools.agents.tests.test_stream_thinking_deltas",
24
+ "test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_conditional_destructive_verbs tools.agents.tests.test_live_deliver_kill tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_schema_diff_inline tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer tools.agents.tests.test_gate_evidence tools.agents.tests.test_repeat_call_guard tools.agents.tests.test_repeat_guard tools.agents.tests.test_gate_integrity tools.agents.tests.test_gate_failclosed_indirection tools.agents.tests.test_gate_failclosed_schema_diff tools.agents.tests.test_expert_review_pr_scope tools.agents.tests.test_path_containment tools.agents.tests.test_path_normalizer_hardened tools.agents.tests.test_sandbox_tool_strip tools.agents.tests.test_proxy_env_loader tools.agents.tests.test_stream_required_tool tools.agents.tests.test_tool_call_wrapper_profiles tools.agents.tests.test_tool_convert_cache tools.agents.tests.test_doubling_break tools.agents.tests.test_error_loop_ignores_correctives tools.agents.tests.test_anthropic_proxy_tool_arg_types tools.agents.tests.test_anthropic_sse_schema tools.agents.tests.test_attractor_detection tools.agents.tests.test_client_disconnect tools.agents.tests.test_confidence_escalation tools.agents.tests.test_coordination_ban tools.agents.tests.test_coordination_early_ban tools.agents.tests.test_cycle_break_exploration tools.agents.tests.test_deferral_break tools.agents.tests.test_deliver_autoroute tools.agents.tests.test_delivery_enforcement_all_langs tools.agents.tests.test_delivery_enforcement_exemptions tools.agents.tests.test_delivery_enforcement_filepath tools.agents.tests.test_delivery_enforcement_write_complexity tools.agents.tests.test_fastpath_config tools.agents.tests.test_delivery_enforcement_web_and_bash tools.agents.tests.test_disconnect_watcher tools.agents.tests.test_empty_maxtokens_recovery tools.agents.tests.test_empty_tool_loop_break tools.agents.tests.test_enforcer_escape_hatches tools.agents.tests.test_error_loop_break tools.agents.tests.test_finalize_suppression tools.agents.tests.test_malformed_unclosed_think tools.agents.tests.test_mandate_beats_recon tools.agents.tests.test_mandate_deliver tools.agents.tests.test_overflow_truncate_count_tokens tools.agents.tests.test_passthrough_oauth tools.agents.tests.test_project_telemetry tools.agents.tests.test_proxy_auth_headers tools.agents.tests.test_prune_preserve_force_write tools.agents.tests.test_recon_deliver_gate tools.agents.tests.test_session_admission tools.agents.tests.test_stream_heartbeat tools.agents.tests.test_stuck_break_reattach tools.agents.tests.test_stuck_break_hard tools.agents.tests.test_turn_count_breaker_periodic tools.agents.tests.test_upstream_chokepoint tools.agents.tests.test_vision_passthrough tools.agents.tests.test_worktree_required tools.agents.tests.test_enforcer_suite_coverage tools.agents.tests.test_validate_plan_gate tools.agents.tests.test_validate_plan_inside_project tools.agents.tests.test_anthropic_proxy_streaming tools.agents.tests.test_non_stream_resp_log tools.agents.tests.test_delivery_enforcement_worktree tools.agents.tests.test_output_token_ceilings tools.agents.tests.test_tool_narrowing_core tools.agents.tests.test_models_context_window tools.agents.tests.test_thinking_template_kwargs tools.agents.tests.test_cycle_break_wait_poll tools.agents.tests.test_upstream_model_aliases tools.agents.tests.test_stream_thinking_deltas tools.agents.tests.test_verbatim_compact tools.agents.tests.test_tool_risk",
25
25
  "test:coverage": "vitest --coverage",
26
26
  "bench": "vitest --config vitest.bench.config.ts",
27
+ "eval:routing": "tsx scripts/eval-routing.ts",
27
28
  "lint": "eslint src --ext .ts",
28
29
  "lint:fix": "eslint src --ext .ts --fix",
29
30
  "format": "prettier --write src/**/*.ts",
@@ -37,7 +38,8 @@
37
38
  "version:major": "bash scripts/version-bump.sh major",
38
39
  "update-uap": "bash scripts/maintenance/update-uap-compliance.sh",
39
40
  "verify-uap": "bash scripts/maintenance/verify-compliance.sh",
40
- "check-claude": "bash scripts/maintenance/verify-compliance.sh"
41
+ "check-claude": "bash scripts/maintenance/verify-compliance.sh",
42
+ "bench:ladder": "tsx scripts/reliability-ladder.ts"
41
43
  },
42
44
  "keywords": [
43
45
  "claude",
@@ -67,10 +67,21 @@ BYPASS_PATTERNS = (
67
67
  # agent writes its own command strings, so an inline form waives its own
68
68
  # review. Refuse it here so the attempt is visible rather than silent.
69
69
  re.compile(r"UAP_NO_REVIEW\s*=\s*['\"]?1", re.I),
70
+ # evidence-bound-ship's hatch is environment-only for the same reason: the
71
+ # agent writes its own command strings, so an inline form would let it
72
+ # ship a candidate with no gate evidence at all.
73
+ re.compile(r"UAP_EVIDENCE_GATE_OFF\s*=\s*['\"]?1", re.I),
74
+ # The staleness WINDOW is a gate parameter: any assignment, not just =1.
75
+ # An agent persisting UAP_EVIDENCE_MAX_AGE_HOURS=8760 into ~/.bashrc would
76
+ # quietly retire the staleness check for every later session, so the
77
+ # assignment itself is refused wherever it appears.
78
+ re.compile(r"UAP_EVIDENCE_MAX_AGE_HOURS\s*=", re.I),
70
79
  # quality-metrics gate hatch, same operator-only shape: the enforcer reads
71
80
  # the launch env, so the only agent-reachable move is PERSISTING the flag
72
81
  # (rc files, proxy env) — which is exactly what this pattern refuses.
73
82
  re.compile(r"UAP_QUALITY_GATE_OFF\s*=\s*['\"]?1", re.I),
83
+ # visual-captures gate hatch (uplift 0.3), same operator-only shape.
84
+ re.compile(r"UAP_VISUAL_GATE_OFF\s*=\s*['\"]?1", re.I),
74
85
  re.compile(r"UAP_USER_VALIDATION\s*=\s*['\"]?0", re.I),
75
86
  # Single-flight is a DATA-SAFETY control, not a policy preference: deliver
76
87
  # runs each candidate in a git worktree, and two runs against one repo are
@@ -572,6 +583,17 @@ def _kills_live_deliver_run(command: str, root) -> str:
572
583
  # is what makes this narrow list sufficient — anything else the agent writes
573
584
  # is distrusted on READ, so it need not be write-blocked here.
574
585
  _TRUST_ANCHORS = (".uap/operator-overrides.json", ".uap/policy-liveness.json")
586
+ # Gate-evidence artifacts (.uap/evidence/<sha>.json for evidence-bound-ship,
587
+ # plus the read/memory evidence logs) get the SAME interpreter-write
588
+ # protection as the trust anchors: they are proof a ship/plan gate accepts, so
589
+ # python3 -c 'open(".uap/evidence/<sha>.json","w").write("{}') # forgery
590
+ # node -e 'require("fs").writeFileSync(".uap/evidence/x.json",…)'
591
+ # must be refused even though the Write-tool path into that directory is
592
+ # already covered by PROTECTED_TARGETS — the interpreter path carried no
593
+ # destructive verb and walked straight past that scan. The dir is matched with
594
+ # a trailing separator/quote so a sibling like `.uap/evidence-notes` is NOT
595
+ # caught.
596
+ _EVIDENCE_DIR_RE = re.compile(r"\.uap/evidence(?:[/\"'\\)\s]|$)")
575
597
  _INTERPRETER_RE = re.compile(r"(?:^|[\s;|&('\"`])(?:python\d*(?:\.\d+)?|perl|ruby|node)(?:\s|$)")
576
598
  # A write primitive in interpreter code: open(…, "w"/"a"/"x"/"+"), or a
577
599
  # mutating method call. Reads (open without a mode, json.load, print) match
@@ -583,9 +605,10 @@ _INTERPRETER_WRITE_RE = re.compile(
583
605
 
584
606
 
585
607
  def _interpreter_write_to_anchor(command: str) -> bool:
586
- """True when an interpreter command names a trust anchor with write intent."""
608
+ """True when an interpreter command names a trust anchor OR the gate
609
+ evidence directory with write intent."""
587
610
  lowered = command.lower()
588
- if not any(anchor in lowered for anchor in _TRUST_ANCHORS):
611
+ if not any(anchor in lowered for anchor in _TRUST_ANCHORS) and not _EVIDENCE_DIR_RE.search(lowered):
589
612
  return False
590
613
  return bool(_INTERPRETER_RE.search(command) and _INTERPRETER_WRITE_RE.search(command))
591
614