@miller-tech/uap 1.40.0 → 1.41.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/README.md +109 -642
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli/deliver-defaults.d.ts +23 -0
- package/dist/cli/deliver-defaults.d.ts.map +1 -0
- package/dist/cli/deliver-defaults.js +121 -0
- package/dist/cli/deliver-defaults.js.map +1 -0
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +29 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +19 -0
- package/dist/cli/setup.js.map +1 -1
- package/dist/policies/policy-tools.d.ts +7 -0
- package/dist/policies/policy-tools.d.ts.map +1 -1
- package/dist/policies/policy-tools.js +24 -2
- package/dist/policies/policy-tools.js.map +1 -1
- package/docs/INDEX.md +48 -286
- package/docs/architecture/OVERVIEW.md +328 -0
- package/docs/architecture/PROTOCOL.md +204 -0
- package/docs/benchmarks/README.md +17 -192
- package/docs/getting-started/CONFIGURATION.md +237 -0
- package/docs/getting-started/INSTALLATION.md +125 -0
- package/docs/getting-started/QUICKSTART.md +115 -0
- package/docs/guides/COORDINATION.md +162 -0
- package/docs/guides/DELIVER.md +115 -0
- package/docs/guides/DEPLOY_BATCHING.md +212 -0
- package/docs/guides/DROIDS_AND_SKILLS.md +202 -0
- package/docs/guides/LOCAL_MODELS.md +148 -0
- package/docs/guides/MCP_ROUTER.md +195 -0
- package/docs/guides/MEMORY.md +235 -0
- package/docs/guides/MULTI_MODEL.md +223 -0
- package/docs/guides/POLICIES.md +190 -0
- package/docs/guides/WORKTREE_WORKFLOW.md +185 -0
- package/docs/integrations/MCP_ROUTER.md +147 -0
- package/docs/integrations/RTK.md +102 -0
- package/docs/reference/API.md +485 -0
- package/docs/reference/CLI.md +719 -0
- package/docs/reference/CONFIGURATION.md +90 -193
- package/docs/reference/DATABASE_SCHEMA.md +110 -344
- package/docs/reference/FEATURES.md +176 -472
- package/docs/reference/PATTERNS.md +102 -0
- package/docs/reference/PLATFORMS.md +83 -0
- package/package.json +3 -1
- package/src/policies/enforcers/7ebbc721-7540-4e9f-879a-770e0213a09b_architecture_review.py +101 -0
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/_common.py +100 -0
- package/src/policies/enforcers/artifact_hygiene.py +52 -0
- package/src/policies/enforcers/cluster_routing.py +63 -0
- package/src/policies/enforcers/codebase_read_before_plan.py +52 -0
- package/src/policies/enforcers/coord_overlap.py +81 -0
- package/src/policies/enforcers/delivery_enforcement.py +97 -0
- package/src/policies/enforcers/doc_live_over_report.py +50 -0
- package/src/policies/enforcers/expert_review_required.py +135 -0
- package/src/policies/enforcers/iac_parity.py +53 -0
- package/src/policies/enforcers/mcp_router_first.py +37 -0
- package/src/policies/enforcers/memory_before_plan.py +61 -0
- package/src/policies/enforcers/parallel_reads.py +50 -0
- package/src/policies/enforcers/rtk_wrap.py +44 -0
- package/src/policies/enforcers/schema_diff_gate.py +80 -0
- package/src/policies/enforcers/session_memory_write.py +52 -0
- package/src/policies/enforcers/task_required.py +131 -0
- package/src/policies/enforcers/test_gate.py +58 -0
- package/src/policies/enforcers/validate_plan_before_build.py +75 -0
- package/src/policies/enforcers/worktree_required.py +57 -0
- package/src/policies/schemas/policies/architecture-review.md +51 -0
- package/src/policies/schemas/policies/artifact-hygiene.md +29 -0
- package/src/policies/schemas/policies/cluster-routing.md +31 -0
- package/src/policies/schemas/policies/codebase-read-before-plan.md +30 -0
- package/src/policies/schemas/policies/coord-overlap.md +24 -0
- package/src/policies/schemas/policies/delivery-enforcement.md +45 -0
- package/src/policies/schemas/policies/doc-live-over-report.md +32 -0
- package/src/policies/schemas/policies/expert-review-required.md +60 -0
- package/src/policies/schemas/policies/iac-parity.md +31 -0
- package/src/policies/schemas/policies/mandatory-testing-deployment.md +147 -0
- package/src/policies/schemas/policies/mcp-router-first.md +24 -0
- package/src/policies/schemas/policies/memory-before-plan.md +24 -0
- package/src/policies/schemas/policies/merge-deploy-monitor-verify.md +145 -0
- package/src/policies/schemas/policies/parallel-reads.md +24 -0
- package/src/policies/schemas/policies/rtk-wrap.md +26 -0
- package/src/policies/schemas/policies/schema-diff-gate.md +30 -0
- package/src/policies/schemas/policies/session-memory-write.md +24 -0
- package/src/policies/schemas/policies/task-required.md +49 -0
- package/src/policies/schemas/policies/test-gate.md +24 -0
- package/src/policies/schemas/policies/validate-plan-before-build.md +28 -0
- package/src/policies/schemas/policies/worktree-required.md +28 -0
- package/templates/hooks/uap-policy-gate.sh +5 -0
- package/docs/AGENTS.md +0 -423
- package/docs/DOCUMENTATION_AUDIT_REPORT.md +0 -131
- package/docs/GETTING_STARTED.md +0 -288
- package/docs/PROJECT_ANALYSIS_REPORT.md +0 -510
- package/docs/architecture/COMPLETE_ARCHITECTURE.md +0 -748
- package/docs/architecture/EXPERT_STACK.md +0 -137
- package/docs/architecture/MULTI_MODEL.md +0 -224
- package/docs/architecture/PLATFORM_GATING.md +0 -68
- package/docs/architecture/SYSTEM_ANALYSIS.md +0 -334
- package/docs/architecture/UAP_COMPLIANCE.md +0 -217
- package/docs/architecture/UAP_PROTOCOL.md +0 -339
- package/docs/architecture/UAP_STRICT_DROIDS.md +0 -172
- package/docs/archive/BALLS_MODE_SELF_ANALYSIS.md +0 -260
- package/docs/archive/BENCHMARK_GAPS_AND_PLAN.md +0 -146
- package/docs/archive/FAILING_TASKS_SOLUTION_PLAN.md +0 -668
- package/docs/archive/JINJA2-SYSTEM-MESSAGE-FIX.md +0 -209
- package/docs/archive/MODEL_ROUTING_IMPLEMENTATION_SUMMARY.md +0 -281
- package/docs/archive/MODEL_ROUTING_OPTIMIZATION_PLAN.md +0 -320
- package/docs/archive/NPM-PUBLISH-V0.9.1.md +0 -240
- package/docs/archive/OPTIMIZATION_OPTIONS.md +0 -334
- package/docs/archive/PARALLELISM_GAPS_AND_OPTIONS.md +0 -422
- package/docs/archive/POLICY_GATE_IMPLEMENTATION.md +0 -245
- package/docs/archive/SETUP_IMPROVEMENTS.md +0 -213
- package/docs/archive/UAP_GENERIC_OPTIMIZATION_PLAN.md +0 -270
- package/docs/archive/UAP_OPTIMIZATION_PLAN.md +0 -701
- package/docs/archive/UAP_V103_PATTERN_DESIGN.md +0 -315
- package/docs/archive/UAP_V104_COMPLIANCE_DESIGN.md +0 -223
- package/docs/archive/changelog/2026-03-10_uap-100-compliance.md +0 -77
- package/docs/archive/changelog/2026-03-10_uap-full-system-verification.md +0 -109
- package/docs/archive/opencode-integration-guide.md +0 -740
- package/docs/archive/opencode-integration-quickref.md +0 -180
- package/docs/benchmarks/OVERNIGHT_RUNNER.md +0 -341
- package/docs/benchmarks/SPECULATIVE_DECODING_JOURNEY_2026-03.md +0 -221
- package/docs/benchmarks/VALIDATION_PLAN.md +0 -568
- package/docs/blog/SPECULATIVE_DECODING_PRODUCTION_PLAYBOOK.md +0 -139
- package/docs/blog/local-coding-agents.md +0 -266
- package/docs/blog/x-thread.md +0 -254
- package/docs/deployment/DEPLOYMENT.md +0 -895
- package/docs/deployment/DEPLOYMENT_STRATEGIES.md +0 -518
- package/docs/deployment/DEPLOY_BATCHER_ANALYSIS.md +0 -224
- package/docs/deployment/DEPLOY_BATCHING.md +0 -273
- package/docs/deployment/DEPLOY_BUCKETING_ANALYSIS.md +0 -420
- package/docs/deployment/QWEN35_LLAMA_CPP.md +0 -426
- package/docs/deployment/UAP_LLAMA_ANTHROPIC_PROXY_BOOTSTRAP.md +0 -279
- package/docs/getting-started/INTEGRATION.md +0 -628
- package/docs/getting-started/OVERVIEW.md +0 -324
- package/docs/getting-started/SETUP.md +0 -377
- package/docs/integrations/MCP_ROUTER_SETUP.md +0 -445
- package/docs/integrations/RTK_INTEGRATION.md +0 -468
- package/docs/operations/TROUBLESHOOTING.md +0 -660
- package/docs/pr/PR_SPECULATIVE_DOCS_TEMPLATE.md +0 -146
- package/docs/pr/UPSTREAM_PRS.md +0 -424
- package/docs/reference/API_REFERENCE.md +0 -903
- package/docs/reference/EXPERT_DROIDS.md +0 -219
- package/docs/reference/HARNESS-MATRIX.md +0 -318
- package/docs/reference/PATTERN_LIBRARY.md +0 -636
- package/docs/reference/UAP_CLI_REFERENCE.md +0 -620
- package/docs/research/BEHAVIORAL_PATTERNS.md +0 -228
- package/docs/research/DOMAIN_STRATEGIES.md +0 -316
- package/docs/research/MEMORY_SYSTEMS_COMPARISON.md +0 -812
- package/docs/research/PATTERN_ANALYSIS_2026-01-18.md +0 -436
- package/docs/research/PERFORMANCE_ANALYSIS_2026-01-18.md +0 -209
- package/docs/research/PERFORMANCE_TEST_PLAN.md +0 -383
- package/docs/research/TERMINAL_BENCH_LEARNINGS.md +0 -217
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Pattern Library Reference
|
|
2
|
+
|
|
3
|
+
> Universal Agent Protocol (UAP) v1.40.0
|
|
4
|
+
|
|
5
|
+
UAP ships an execution-pattern library: a set of reusable problem-solving
|
|
6
|
+
strategies that are matched to the current task and surfaced to the agent.
|
|
7
|
+
Patterns are defined as markdown files under `.factory/patterns/`, catalogued
|
|
8
|
+
in `.factory/patterns/index.json`, and retrieved on demand via a Qdrant-backed
|
|
9
|
+
RAG flow (`uap patterns query`).
|
|
10
|
+
|
|
11
|
+
## The 23 Patterns
|
|
12
|
+
|
|
13
|
+
The canonical roster lives in `.factory/patterns/index.json`. Each pattern has
|
|
14
|
+
a numeric (or string) id, a markdown body file, a title, an abbreviation, a
|
|
15
|
+
category, and a keyword set used for retrieval.
|
|
16
|
+
|
|
17
|
+
| ID | Title | Abbreviation | Category | What it does |
|
|
18
|
+
|----|-------|--------------|----------|--------------|
|
|
19
|
+
| P12 | Output Existence Verification | OE-Verify | Verification | Confirm the artifact a task was supposed to produce actually exists before claiming done. **Always enforced.** |
|
|
20
|
+
| P13 | Iterative Refinement Loop | Iter-Ref | Testing | Run tests, fix, and re-run in a loop until they pass. |
|
|
21
|
+
| P14 | Output Format Validation | Format-Check | Verification | Validate that produced output matches the required format (JSON/YAML/CSV). |
|
|
22
|
+
| P16 | Task-First Execution | Task-First | Execution | When the task is clear, execute directly instead of over-planning. |
|
|
23
|
+
| P17 | Constraint Extraction | Extract-Constraints | Planning | Pull hard constraints ("exactly", "only", "no more than") out of the prompt before acting. |
|
|
24
|
+
| P19 | Impossible Task Refusal | Refuse-Impossible | Safety | Detect and refuse tasks that are fundamentally impossible. |
|
|
25
|
+
| P20 | Adversarial Thinking | Adversarial | Security | Reason about how something could be bypassed/exploited/evaded. |
|
|
26
|
+
| P21 | Chess Engine Integration | Chess-Engine | Domain-Specific | Delegate chess reasoning (FEN, best move, checkmate) to an engine. |
|
|
27
|
+
| P22 | Git Recovery Forensics | Git-Recovery | Recovery | Recover lost/corrupted git state via reflog and forensic inspection. |
|
|
28
|
+
| P23 | Compression Impossibility Detection | Compress-Check | Verification | Detect already-compressed data so re-compression isn't attempted. |
|
|
29
|
+
| P24 | Polyglot Code Construction | Polyglot | Code-Golf | Construct source that compiles/runs as multiple languages. |
|
|
30
|
+
| P25 | Service Configuration Pipeline | Service-Config | DevOps | Configure, validate, and reload a service/daemon as a pipeline. |
|
|
31
|
+
| P26 | Near-Miss Iteration | Near-Miss | Testing | When a test result is a small gap away, tweak and re-check. |
|
|
32
|
+
| P28 | Service Smoke Test | Smoke-Test | Verification | After deploy/start, run a health check / smoke test to verify. |
|
|
33
|
+
| P30 | Performance Threshold Tuning | Perf-Threshold | Optimization | Measure performance and tune against a percentage/threshold target. |
|
|
34
|
+
| P31 | Round-Trip Verification | Round-Trip | Verification | Encode then decode (or compress/decompress) and verify the round trip. |
|
|
35
|
+
| P32 | CLI Execution Verification | CLI-Verify | Verification | Verify a CLI/binary actually runs as expected. |
|
|
36
|
+
| P33 | Numerical Stability Testing | Num-Stable | Testing | Test floating-point/numerical work for precision and stability. |
|
|
37
|
+
| P34 | Image-to-Structured Pipeline | Image-Structured | Domain-Specific | Convert images (OCR/diagram/board) into structured data. |
|
|
38
|
+
| P35 | Decoder-First Analysis | Decoder-First | Analysis | Build/understand the decoder/parser first when reverse-engineering a format. **Always enforced.** |
|
|
39
|
+
| P36 | Competition Domain Research | Competition-Research | Research | Research the competitive domain (win rate, leaderboard, tournament) before optimizing. |
|
|
40
|
+
| P37 | Ambiguity Detection & Resolution | Ambiguity-Detect | Planning | Detect vague/unspecified requirements and clarify before acting. |
|
|
41
|
+
| IaC | Infrastructure as Code Parity | IaC-Parity | Infrastructure | Keep infrastructure changes reflected in IaC (terraform/kubernetes) for reproducibility. |
|
|
42
|
+
|
|
43
|
+
### Always-on patterns
|
|
44
|
+
|
|
45
|
+
Two patterns are unconditionally included regardless of the matched task, set
|
|
46
|
+
in `src/coordination/pattern-router.ts`:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
const alwaysIncludeIds = ['P12', 'P35']; // Output Existence, Decoder-First
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- **P12 (Output Existence Verification)** — guards against "claiming done"
|
|
53
|
+
without producing the artifact.
|
|
54
|
+
- **P35 (Decoder-First Analysis)** — anchors format/reverse-engineering work.
|
|
55
|
+
|
|
56
|
+
## How pattern RAG works
|
|
57
|
+
|
|
58
|
+
Pattern retrieval is semantic, not keyword-based. The flow:
|
|
59
|
+
|
|
60
|
+
1. **Indexing** (`uap patterns index`) runs a generated Python indexer
|
|
61
|
+
(`agents/scripts/index_patterns_to_qdrant.py`). It scans multiple sources —
|
|
62
|
+
`CLAUDE.md` (with `@include` resolution), additional source files
|
|
63
|
+
(`AGENTS.md`, etc.), `*/SKILL.md` skill files, and `.factory/patterns/*.md`
|
|
64
|
+
(using `index.json` to preserve the canonical `PNN: Title` identity and
|
|
65
|
+
keyword set). Documents are de-duplicated by content hash, embedded, and
|
|
66
|
+
upserted into the Qdrant collection.
|
|
67
|
+
2. **Querying** (`uap patterns query "<task>"`) runs the generated query script
|
|
68
|
+
(`agents/scripts/query_patterns.py`), embeds the query with the same model,
|
|
69
|
+
and runs a cosine `query_points` search against the collection, returning the
|
|
70
|
+
top-K hits above the score threshold.
|
|
71
|
+
3. **Fallback** — if the Python query script is absent, the CLI falls back to a
|
|
72
|
+
direct Qdrant `scroll` + keyword match (less accurate) and prints a notice.
|
|
73
|
+
|
|
74
|
+
### RAG defaults
|
|
75
|
+
|
|
76
|
+
From `src/cli/patterns.ts` (`getPatternRagConfig`), overridable via
|
|
77
|
+
`.uap.json` under `memory.patternRag`:
|
|
78
|
+
|
|
79
|
+
| Setting | Default |
|
|
80
|
+
|---------|---------|
|
|
81
|
+
| Collection | `agent_patterns` |
|
|
82
|
+
| Embedding model | `all-MiniLM-L6-v2` |
|
|
83
|
+
| Vector size | `384` (Cosine distance) |
|
|
84
|
+
| Score threshold | `0.35` |
|
|
85
|
+
| Top-K | `2` |
|
|
86
|
+
| Index script | `./agents/scripts/index_patterns_to_qdrant.py` |
|
|
87
|
+
| Query script | `./agents/scripts/query_patterns.py` |
|
|
88
|
+
| Source file | `CLAUDE.md` |
|
|
89
|
+
| Max body chars | `400` (display); indexer truncates bodies at 2000 |
|
|
90
|
+
|
|
91
|
+
The query script requires a Python with `sentence-transformers` and
|
|
92
|
+
`qdrant-client`. The CLI auto-discovers `agents/.venv/bin/python`,
|
|
93
|
+
`.venv/bin/python`, then `python3`/`python`, and can bootstrap a venv.
|
|
94
|
+
|
|
95
|
+
## `uap patterns` CLI
|
|
96
|
+
|
|
97
|
+
| Command | Description |
|
|
98
|
+
|---------|-------------|
|
|
99
|
+
| `uap patterns status` | Show RAG config, Qdrant collection point count, Python/script availability. |
|
|
100
|
+
| `uap patterns index` | (Re)index pattern/doc sources into the Qdrant collection. `--verbose` for full output. |
|
|
101
|
+
| `uap patterns query "<task>"` | Semantic search for task-relevant patterns. Flags: `--top <n>`, `--min-score <f>`, `--format text\|json`. |
|
|
102
|
+
| `uap patterns generate` | Generate the Python index/query scripts. `--force` to overwrite. |
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Platform & Harness Reference
|
|
2
|
+
|
|
3
|
+
> Universal Agent Protocol (UAP) v1.40.0
|
|
4
|
+
|
|
5
|
+
UAP integrates with 9 agent harnesses. The canonical list is `ALL_TARGETS` in
|
|
6
|
+
`src/cli/hooks.ts`. Every platform receives the UAP enforcement layer: lifecycle
|
|
7
|
+
hooks, the DB-driven policy gate, and (where supported) the hierarchical MCP
|
|
8
|
+
router.
|
|
9
|
+
|
|
10
|
+
## Supported platforms
|
|
11
|
+
|
|
12
|
+
| Target flag | Platform | Config location | Integration style |
|
|
13
|
+
|-------------|----------|-----------------|-------------------|
|
|
14
|
+
| `claude` | Claude Code | `.claude/settings.local.json` + `.claude/hooks/` | Native hook events |
|
|
15
|
+
| `factory` | Factory.AI Droid | `.factory/settings.local.json` + `.factory/hooks/` | Native hook events (`$FACTORY_PROJECT_DIR`) |
|
|
16
|
+
| `cursor` | Cursor | `.cursor/hooks.json` + `.cursor/hooks/` | Native hook events |
|
|
17
|
+
| `vscode` | VSCode | `.claude/settings.local.json` + `.claude/hooks/` | Claude Code hook format (via third-party skills) |
|
|
18
|
+
| `opencode` | OpenCode | `.opencode/plugin/uap-session-hooks.ts` + `.opencode/hooks/` | TypeScript plugin |
|
|
19
|
+
| `codex` | Codex CLI | `AGENTS.md`, `.codex/config.toml`, `.codex/hooks/`, `.agents/skills/` | AGENTS.md + skills + MCP server |
|
|
20
|
+
| `forgecode` | ForgeCode | `.forge/forgecode.plugin.sh` + `.forge/hooks/` | ZSH plugin |
|
|
21
|
+
| `omp` | Oh-My-Pi | `.uap/omp/settings.json` + `.uap/omp/hooks/{pre,post}/` | omp pre/post hook dirs |
|
|
22
|
+
| `hermes` | Hermes Agent (NousResearch) | `~/.hermes/config.yaml` + `~/.hermes/agent-hooks/` | **Global** YAML config + MCP |
|
|
23
|
+
|
|
24
|
+
> **Hermes is global.** Its config lives under `~/.hermes` (or `$HERMES_HOME`),
|
|
25
|
+
> so it is excluded from the default project install loop (`PROJECT_TARGETS`).
|
|
26
|
+
> Installing it requires an explicit `-t hermes`. `status`/`doctor` still report it.
|
|
27
|
+
|
|
28
|
+
## Support matrix
|
|
29
|
+
|
|
30
|
+
| Platform | Lifecycle hooks | Worktree/Bash guards | Policy gate | MCP router |
|
|
31
|
+
|----------|-----------------|----------------------|-------------|------------|
|
|
32
|
+
| Claude Code | Native | Yes | Hard (PreToolUse) | Yes |
|
|
33
|
+
| Factory.AI | Native | Yes | Hard (PreToolUse) | Yes |
|
|
34
|
+
| Cursor | Native | Yes | Hard (preToolUse) | Yes |
|
|
35
|
+
| VSCode | Native (Claude format) | Yes | Hard (PreToolUse) | Yes |
|
|
36
|
+
| OpenCode | Plugin events | Yes | Hard (`tool.execute.before` → exit 2 blocks) | Yes |
|
|
37
|
+
| Codex CLI | Advisory (no native pre-tool event) | Advisory | Hard for MCP-routed tools; advisory for native edit/bash | Yes (`[mcp_servers.uap]`) |
|
|
38
|
+
| ForgeCode | ZSH plugin | Yes (scripts) | Via gate script | — |
|
|
39
|
+
| Oh-My-Pi | pre/post dirs | Yes | Yes (`uap-policy-gate.sh`) | — |
|
|
40
|
+
| Hermes | `pre_tool_call` / `on_session_start` | `pre_tool_call` matcher | Hard via `uap-policy-gate-hermes.sh` (emits block JSON) | Yes (`mcp_servers.uap`) |
|
|
41
|
+
|
|
42
|
+
> **Gating notes.** Codex CLI has no native pre-tool-use hook event, so policy
|
|
43
|
+
> gating is **hard** only for tools routed through the UAP MCP server
|
|
44
|
+
> (`execute_tool` runs the PolicyGate) and **advisory** for Codex-native
|
|
45
|
+
> edit/bash. `uap hooks doctor` reports Codex as MCP-gated. Hermes hooks are
|
|
46
|
+
> fail-open by design, but the UAP gate always emits a decision JSON so real
|
|
47
|
+
> blocks are enforced; Hermes prompts once to approve each hook command
|
|
48
|
+
> (`~/.hermes/shell-hooks-allowlist.json`) unless `hooks_auto_accept: true`.
|
|
49
|
+
|
|
50
|
+
## Lifecycle hooks installed
|
|
51
|
+
|
|
52
|
+
The shared hook scripts (copied from `templates/hooks/`) are wired into each
|
|
53
|
+
platform's lifecycle events:
|
|
54
|
+
|
|
55
|
+
| Script | Event | Purpose |
|
|
56
|
+
|--------|-------|---------|
|
|
57
|
+
| `session-start.sh` | SessionStart | Injects recent memory context. |
|
|
58
|
+
| `pre-tool-use-edit-write.sh` | PreToolUse (Edit/Write/MultiEdit) | Worktree file guard — **blocks** edits outside worktree dirs. |
|
|
59
|
+
| `pre-tool-use-bash.sh` | PreToolUse (Bash) | Dangerous-command guard — **blocks** force push, `terraform apply`, etc. |
|
|
60
|
+
| `uap-policy-gate.sh` | PreToolUse | DB-driven policy gate (`policies.db` + `.policy-tools/*.py`). |
|
|
61
|
+
| `post-tool-use-edit-write.sh` | PostToolUse (Edit/Write) | Build gate + backup reminder after edits. |
|
|
62
|
+
| `pre-compact.sh` | PreCompact | Flushes a compaction marker to memory. |
|
|
63
|
+
| `post-compact.sh` | PostCompact | Re-injects policy awareness after compaction. |
|
|
64
|
+
| `stop.sh` | Stop | Completion-gate checklist + session cleanup. |
|
|
65
|
+
| `session-end.sh` | SessionEnd | Agent deregistration + backup retention. |
|
|
66
|
+
| `loop-protection.sh` | — | Loop/spawn protection. |
|
|
67
|
+
|
|
68
|
+
## Install & verify
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uap hooks install # install for all PROJECT_TARGETS (excludes hermes)
|
|
72
|
+
uap hooks install -t claude # install for a single platform
|
|
73
|
+
uap hooks install -t hermes # required to install the global Hermes config
|
|
74
|
+
uap hooks status # per-platform script + settings status (all targets)
|
|
75
|
+
uap hooks doctor # health check across all targets
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The MCP router is configured separately and across all harnesses with:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uap mcp-setup # configure the hierarchical MCP router everywhere
|
|
82
|
+
uap mcp-router start # run the stdio MCP router server
|
|
83
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miller-tech/uap",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.41.0",
|
|
4
4
|
"description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -107,6 +107,8 @@
|
|
|
107
107
|
"docs",
|
|
108
108
|
"config",
|
|
109
109
|
"templates",
|
|
110
|
+
"src/policies/schemas/policies",
|
|
111
|
+
"src/policies/enforcers",
|
|
110
112
|
"tools/agents",
|
|
111
113
|
"scripts/setup",
|
|
112
114
|
"scripts/version-bump.sh",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""architecture-review-required enforcer.
|
|
3
|
+
|
|
4
|
+
Triggered when merge / PR-ready operations are attempted and the diff
|
|
5
|
+
touches qualifying paths. Requires either:
|
|
6
|
+
- An ADR file under docs/architecture/adr/ added or modified in the same diff, OR
|
|
7
|
+
- An active waiver under policies/waivers/ matching this policy slug
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
16
|
+
from _common import emit, parse_cli, repo_root, run # noqa: E402
|
|
17
|
+
|
|
18
|
+
PR_OPS_RE = re.compile(
|
|
19
|
+
r"\b(pr[-_ ]?ready|pr-create|gh pr create|signoff|ready[-_ ]for[-_ ]review|merge)\b",
|
|
20
|
+
re.I,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Paths whose modification triggers the architecture-review requirement.
|
|
24
|
+
TRIGGER_PATTERNS = [
|
|
25
|
+
re.compile(r"^src/types/"),
|
|
26
|
+
re.compile(r".*/schemas/"),
|
|
27
|
+
re.compile(r"^src/index\.ts$"),
|
|
28
|
+
re.compile(r"^docs/architecture/(?!adr/)"), # docs/architecture/ but NOT the adr/ we want
|
|
29
|
+
re.compile(r"^src/coordination/(capability|pattern)-router\.ts$"),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
ADR_PATH_RE = re.compile(r"^docs/architecture/adr/.+\.md$")
|
|
33
|
+
WAIVER_RE = re.compile(r"^policies/waivers/.*architecture-review.*\.md$", re.I)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main() -> None:
|
|
37
|
+
op, args = parse_cli()
|
|
38
|
+
cmd = (args.get("command") or "").lower()
|
|
39
|
+
|
|
40
|
+
if not (PR_OPS_RE.search(op) or PR_OPS_RE.search(cmd)):
|
|
41
|
+
emit(True, "not a PR-ready gate point")
|
|
42
|
+
|
|
43
|
+
root = repo_root()
|
|
44
|
+
rc, out, _ = run(
|
|
45
|
+
["git", "diff", "--name-only", "origin/master...HEAD"], cwd=root, timeout=10
|
|
46
|
+
)
|
|
47
|
+
if rc != 0:
|
|
48
|
+
# Try origin/main as fallback
|
|
49
|
+
rc, out, _ = run(
|
|
50
|
+
["git", "diff", "--name-only", "origin/main...HEAD"], cwd=root, timeout=10
|
|
51
|
+
)
|
|
52
|
+
if rc != 0:
|
|
53
|
+
emit(True, "cannot compute diff vs upstream; allowing")
|
|
54
|
+
|
|
55
|
+
changed = [line for line in out.splitlines() if line.strip()]
|
|
56
|
+
|
|
57
|
+
# Detect qualifying changes
|
|
58
|
+
triggered = []
|
|
59
|
+
for f in changed:
|
|
60
|
+
for pat in TRIGGER_PATTERNS:
|
|
61
|
+
if pat.match(f):
|
|
62
|
+
triggered.append(f)
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
if not triggered:
|
|
66
|
+
emit(True, "no architecture-qualifying paths touched in diff")
|
|
67
|
+
|
|
68
|
+
# Look for an ADR in the diff
|
|
69
|
+
adr_present = any(ADR_PATH_RE.match(f) for f in changed)
|
|
70
|
+
|
|
71
|
+
# Look for an active waiver
|
|
72
|
+
waiver_present = False
|
|
73
|
+
waivers_dir = root / "policies" / "waivers"
|
|
74
|
+
if waivers_dir.exists():
|
|
75
|
+
for waiver in waivers_dir.glob("*architecture-review*.md"):
|
|
76
|
+
if waiver.is_file():
|
|
77
|
+
waiver_present = True
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
if adr_present or waiver_present:
|
|
81
|
+
emit(
|
|
82
|
+
True,
|
|
83
|
+
f"architecture-review satisfied "
|
|
84
|
+
f"({'adr-in-diff' if adr_present else 'active-waiver'})",
|
|
85
|
+
adr=adr_present,
|
|
86
|
+
waiver=waiver_present,
|
|
87
|
+
triggers=sorted(set(triggered)),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
emit(
|
|
91
|
+
False,
|
|
92
|
+
"architecture-review-required: diff touches "
|
|
93
|
+
f"{', '.join(sorted(set(triggered))[:3])} but no ADR under "
|
|
94
|
+
"docs/architecture/adr/ and no active waiver. "
|
|
95
|
+
"Add an ADR or request a waiver from compliance-officer.",
|
|
96
|
+
triggers=sorted(set(triggered)),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
main()
|
|
Binary file
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Shared helpers for UAP policy enforcers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_cli() -> tuple[str, dict[str, Any]]:
|
|
13
|
+
p = argparse.ArgumentParser()
|
|
14
|
+
p.add_argument("--operation", required=True)
|
|
15
|
+
p.add_argument("--args", default="{}")
|
|
16
|
+
ns = p.parse_args()
|
|
17
|
+
try:
|
|
18
|
+
args = json.loads(ns.args)
|
|
19
|
+
except json.JSONDecodeError:
|
|
20
|
+
args = {}
|
|
21
|
+
return ns.operation, args
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def emit(allowed: bool, reason: str, **extra: Any) -> None:
|
|
25
|
+
payload: dict[str, Any] = {"allowed": allowed, "reason": reason}
|
|
26
|
+
payload.update(extra)
|
|
27
|
+
json.dump(payload, sys.stdout)
|
|
28
|
+
sys.exit(0 if allowed else 2)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def repo_root() -> Path:
|
|
32
|
+
env = os.environ.get("UAP_REPO_ROOT")
|
|
33
|
+
if env:
|
|
34
|
+
return Path(env)
|
|
35
|
+
cwd = Path.cwd()
|
|
36
|
+
for p in [cwd, *cwd.parents]:
|
|
37
|
+
if (p / ".git").exists():
|
|
38
|
+
return p
|
|
39
|
+
return cwd
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def worktree_root() -> Path:
|
|
43
|
+
"""Root of the current WORKING TREE for git operations.
|
|
44
|
+
|
|
45
|
+
Distinct from repo_root() (the main checkout, where runtime data like
|
|
46
|
+
policies.db lives). git-diff based enforcers must run against the working
|
|
47
|
+
tree — which is the worktree when an operation runs from inside one. The
|
|
48
|
+
policy gate exports UAP_WORKTREE_ROOT; fall back to `git rev-parse` from cwd,
|
|
49
|
+
then to repo_root().
|
|
50
|
+
"""
|
|
51
|
+
env = os.environ.get("UAP_WORKTREE_ROOT")
|
|
52
|
+
if env:
|
|
53
|
+
return Path(env)
|
|
54
|
+
try:
|
|
55
|
+
r = subprocess.run(
|
|
56
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
57
|
+
capture_output=True, text=True, timeout=3, env=_clean_env(),
|
|
58
|
+
)
|
|
59
|
+
if r.returncode == 0 and r.stdout.strip():
|
|
60
|
+
return Path(r.stdout.strip())
|
|
61
|
+
except Exception: # noqa: BLE001
|
|
62
|
+
pass
|
|
63
|
+
return repo_root()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# git exports repo-context vars (GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, ...)
|
|
67
|
+
# into hook environments. An enforcer spawned during a hook would then run its
|
|
68
|
+
# own git calls against the HOOK'S repo instead of cwd — silently no-op'ing
|
|
69
|
+
# every git-diff based check. Strip them so cwd decides the repo.
|
|
70
|
+
_GIT_CONTEXT_VARS = (
|
|
71
|
+
"GIT_DIR",
|
|
72
|
+
"GIT_WORK_TREE",
|
|
73
|
+
"GIT_INDEX_FILE",
|
|
74
|
+
"GIT_COMMON_DIR",
|
|
75
|
+
"GIT_OBJECT_DIRECTORY",
|
|
76
|
+
"GIT_PREFIX",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _clean_env() -> dict[str, str]:
|
|
81
|
+
return {k: v for k, v in os.environ.items() if k not in _GIT_CONTEXT_VARS}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def run(cmd: list[str], cwd: Path | None = None, timeout: int = 5) -> tuple[int, str, str]:
|
|
85
|
+
try:
|
|
86
|
+
r = subprocess.run(
|
|
87
|
+
cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout,
|
|
88
|
+
env=_clean_env(),
|
|
89
|
+
)
|
|
90
|
+
return r.returncode, r.stdout, r.stderr
|
|
91
|
+
except Exception as e: # noqa: BLE001
|
|
92
|
+
return 1, "", str(e)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def arg_str(args: dict[str, Any]) -> str:
|
|
96
|
+
"""Flatten args to a single lowercase string for substring checks."""
|
|
97
|
+
try:
|
|
98
|
+
return json.dumps(args, default=str).lower()
|
|
99
|
+
except Exception: # noqa: BLE001
|
|
100
|
+
return str(args).lower()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""artifact-hygiene enforcer: block binary artifacts outside curated dirs."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
9
|
+
from _common import emit, parse_cli # noqa: E402
|
|
10
|
+
|
|
11
|
+
BINARY_RE = re.compile(r"\.(png|jpe?g|gif|pdf|zip|tar\.gz|tgz|db|sqlite\d?)$", re.I)
|
|
12
|
+
ALLOWED_PREFIXES = (
|
|
13
|
+
"docs/",
|
|
14
|
+
"tests/",
|
|
15
|
+
"apps/",
|
|
16
|
+
"agents/data/memory/",
|
|
17
|
+
".playwright-mcp/",
|
|
18
|
+
"observability/",
|
|
19
|
+
)
|
|
20
|
+
ALLOWED_SUBSTRINGS = ("/__screenshots__/", "/public/", "/static/", "/assets/")
|
|
21
|
+
WRITE_OPS = {"Write", "write", "create-file"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
op, args = parse_cli()
|
|
26
|
+
if op not in WRITE_OPS:
|
|
27
|
+
emit(True, "not a write op")
|
|
28
|
+
|
|
29
|
+
path = (args.get("file_path") or args.get("path") or "").replace("\\", "/")
|
|
30
|
+
if not BINARY_RE.search(path):
|
|
31
|
+
emit(True, "not a binary artifact")
|
|
32
|
+
|
|
33
|
+
rel = path
|
|
34
|
+
for marker in ("/pay2u/", "/miller-tech/"):
|
|
35
|
+
if marker in rel:
|
|
36
|
+
rel = rel.split(marker, 1)[1]
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
if any(rel.startswith(p) for p in ALLOWED_PREFIXES):
|
|
40
|
+
emit(True, f"allowed prefix: {rel}")
|
|
41
|
+
if any(s in rel for s in ALLOWED_SUBSTRINGS):
|
|
42
|
+
emit(True, f"allowed subdir: {rel}")
|
|
43
|
+
|
|
44
|
+
emit(
|
|
45
|
+
False,
|
|
46
|
+
f"artifact-hygiene: binary '{rel}' must live under docs/, tests/, or apps/**/public|static|assets. "
|
|
47
|
+
"Do not litter repo root.",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
main()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""cluster-routing enforcer: kubectl/helm context must match component domain."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
9
|
+
from _common import arg_str, emit, parse_cli, run # noqa: E402
|
|
10
|
+
|
|
11
|
+
DOMAINS = {
|
|
12
|
+
"openobserve": re.compile(
|
|
13
|
+
r"grafana|prometheus|openobserve|fluent-?bit|servicemonitor|alertmanager|loki|tempo|jaeger",
|
|
14
|
+
re.I,
|
|
15
|
+
),
|
|
16
|
+
"zitadel": re.compile(r"zitadel|oidc|keycloak|iam-crd", re.I),
|
|
17
|
+
}
|
|
18
|
+
CONTEXTS = {
|
|
19
|
+
"openobserve": "do-syd1-pay2u-openobserve",
|
|
20
|
+
"zitadel": "do-syd1-zitadel",
|
|
21
|
+
"main": "do-syd1-pay2u",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def pick_domain(blob: str) -> str:
|
|
26
|
+
for d, rx in DOMAINS.items():
|
|
27
|
+
if rx.search(blob):
|
|
28
|
+
return d
|
|
29
|
+
return "main"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main() -> None:
|
|
33
|
+
op, args = parse_cli()
|
|
34
|
+
blob = f"{op} {arg_str(args)}"
|
|
35
|
+
|
|
36
|
+
if not re.search(r"\b(kubectl|helm)\b", blob):
|
|
37
|
+
emit(True, "not a kubectl/helm call")
|
|
38
|
+
|
|
39
|
+
if not re.search(
|
|
40
|
+
r"\b(apply|patch|create|edit|delete|install|upgrade|uninstall|rollout)\b", blob
|
|
41
|
+
):
|
|
42
|
+
emit(True, "read-only kubectl/helm call")
|
|
43
|
+
|
|
44
|
+
rc, out, _ = run(["kubectl", "config", "current-context"])
|
|
45
|
+
ctx = out.strip() if rc == 0 else ""
|
|
46
|
+
|
|
47
|
+
wanted_domain = pick_domain(blob)
|
|
48
|
+
wanted_ctx = CONTEXTS[wanted_domain]
|
|
49
|
+
|
|
50
|
+
if ctx != wanted_ctx:
|
|
51
|
+
emit(
|
|
52
|
+
False,
|
|
53
|
+
f"cluster-routing: context '{ctx}' does not match domain "
|
|
54
|
+
f"'{wanted_domain}'. Run: kubectl config use-context {wanted_ctx}",
|
|
55
|
+
wanted_context=wanted_ctx,
|
|
56
|
+
current_context=ctx,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
emit(True, f"context '{ctx}' matches domain '{wanted_domain}'")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""codebase-read-before-plan enforcer: plans require prior reads of target paths."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
11
|
+
from _common import arg_str, emit, parse_cli # noqa: E402
|
|
12
|
+
|
|
13
|
+
PLAN_OPS = {"ExitPlanMode", "Plan", "TodoWrite"}
|
|
14
|
+
PLAN_WORD_RE = re.compile(r"(?<![-\w/])(plan the|design the|architect the|propose a plan|spec the)", re.I)
|
|
15
|
+
READ_LOG = Path(os.environ.get("UAP_STATE_DIR", ".uap")) / "read_log.state"
|
|
16
|
+
RECENT_SEC = 1800
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def recent_reads() -> set[str]:
|
|
20
|
+
if not READ_LOG.exists():
|
|
21
|
+
return set()
|
|
22
|
+
out: set[str] = set()
|
|
23
|
+
now = time.time()
|
|
24
|
+
for line in READ_LOG.read_text().splitlines():
|
|
25
|
+
try:
|
|
26
|
+
ts, path = line.split("\t", 1)
|
|
27
|
+
if now - float(ts) < RECENT_SEC:
|
|
28
|
+
out.add(path)
|
|
29
|
+
except ValueError:
|
|
30
|
+
continue
|
|
31
|
+
return out
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main() -> None:
|
|
35
|
+
op, args = parse_cli()
|
|
36
|
+
blob = f"{op} {arg_str(args)}"
|
|
37
|
+
if op not in PLAN_OPS and not PLAN_WORD_RE.search(blob):
|
|
38
|
+
emit(True, "not a plan op")
|
|
39
|
+
|
|
40
|
+
reads = recent_reads()
|
|
41
|
+
if reads:
|
|
42
|
+
emit(True, f"{len(reads)} recent codebase reads on record")
|
|
43
|
+
|
|
44
|
+
emit(
|
|
45
|
+
False,
|
|
46
|
+
"codebase-read-before-plan: no Read/Grep/Glob within the last 30 min. "
|
|
47
|
+
"Read the existing codebase in the target scope before emitting a plan.",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
main()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""coord-overlap enforcer: check for in-flight agent path reservations."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import sqlite3
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
9
|
+
from _common import emit, parse_cli, repo_root # noqa: E402
|
|
10
|
+
|
|
11
|
+
AGENT_OPS = {"Agent", "spawn-agent", "subagent", "delegate"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def overlapping_reservations(root: Path, paths: list[str]) -> list[str]:
|
|
15
|
+
db = root / "agents" / "data" / "coordination" / "coordination.db"
|
|
16
|
+
if not db.exists() or not paths:
|
|
17
|
+
return []
|
|
18
|
+
try:
|
|
19
|
+
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True, timeout=1.0)
|
|
20
|
+
# Best-effort: look for any reservations table with path-like column
|
|
21
|
+
cur = con.execute(
|
|
22
|
+
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
23
|
+
)
|
|
24
|
+
tables = [r[0] for r in cur.fetchall()]
|
|
25
|
+
hits: list[str] = []
|
|
26
|
+
for t in tables:
|
|
27
|
+
try:
|
|
28
|
+
cols = [r[1] for r in con.execute(f"PRAGMA table_info({t})")]
|
|
29
|
+
path_col = next(
|
|
30
|
+
(c for c in cols if c.lower() in ("path", "paths", "file", "scope")),
|
|
31
|
+
None,
|
|
32
|
+
)
|
|
33
|
+
status_col = next(
|
|
34
|
+
(c for c in cols if c.lower() in ("status", "state", "active")), None
|
|
35
|
+
)
|
|
36
|
+
if not path_col:
|
|
37
|
+
continue
|
|
38
|
+
where = f" WHERE {status_col} IN ('active','in_progress',1)" if status_col else ""
|
|
39
|
+
rows = con.execute(f"SELECT {path_col} FROM {t}{where}").fetchall()
|
|
40
|
+
for (v,) in rows:
|
|
41
|
+
if not v:
|
|
42
|
+
continue
|
|
43
|
+
for p in paths:
|
|
44
|
+
if p and p in str(v):
|
|
45
|
+
hits.append(f"{t}:{v}")
|
|
46
|
+
except sqlite3.Error:
|
|
47
|
+
continue
|
|
48
|
+
con.close()
|
|
49
|
+
return hits
|
|
50
|
+
except sqlite3.Error:
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main() -> None:
|
|
55
|
+
op, args = parse_cli()
|
|
56
|
+
if op not in AGENT_OPS:
|
|
57
|
+
emit(True, "not an agent-spawn op")
|
|
58
|
+
|
|
59
|
+
paths_raw = (
|
|
60
|
+
args.get("paths")
|
|
61
|
+
or args.get("scope")
|
|
62
|
+
or args.get("prompt", "")
|
|
63
|
+
)
|
|
64
|
+
if isinstance(paths_raw, list):
|
|
65
|
+
paths = [str(p) for p in paths_raw]
|
|
66
|
+
else:
|
|
67
|
+
paths = [p for p in str(paths_raw).split() if "/" in p]
|
|
68
|
+
|
|
69
|
+
hits = overlapping_reservations(repo_root(), paths)
|
|
70
|
+
if hits:
|
|
71
|
+
emit(
|
|
72
|
+
False,
|
|
73
|
+
f"coord-overlap: active reservations on: {', '.join(hits[:5])}. "
|
|
74
|
+
"Run `uap coordination check` before spawning.",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
emit(True, "no overlapping reservations")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
main()
|