Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
## vexp - Context-Aware AI Coding <!-- vexp v2.0.21 -->

### MANDATORY: use vexp pipeline - do NOT grep or glob the codebase
For every task - bug fixes, features, refactors, debugging:
**call `run_pipeline` FIRST**. It executes context search + impact analysis +
memory recall in a single call, returning compressed results.

Do NOT use grep, glob, Bash, or cat to search/explore the codebase.
vexp returns pre-indexed, graph-ranked context that is more relevant and
uses fewer tokens than manual searching. Prefer `get_skeleton` over Read to
inspect files (detail: minimal/standard/detailed, 70-90% token savings).
Only use Read when you need exact raw content to edit a specific line.

### Primary Tool
- `run_pipeline` - **USE THIS FOR EVERYTHING**. Single call that runs
capsule + impact + memory server-side. Returns compressed results.
Auto-detects intent (debug/modify/refactor/explore) from your task.
Includes full file content for pivots.
Examples:
- `run_pipeline({ "task": "fix JWT validation bug" })` - auto-detect
- `run_pipeline({ "task": "refactor db layer", "preset": "refactor" })` - explicit
- `run_pipeline({ "task": "add auth", "observation": "using JWT" })` - save insight in same call

### Other MCP tools (use only when run_pipeline is insufficient)
- `get_skeleton` - **preferred over Read** for inspecting files (minimal/standard/detailed detail levels, 70-90% token savings)
- `index_status` - indexing status and health check
- `expand_vexp_ref` - expand V-REF hash placeholders in v2 compact output

### Workflow
1. `run_pipeline("your task")` - ALWAYS FIRST. Returns pivots + impact + memories in 1 call
2. Need more detail on a file? Use `get_skeleton({ files: [...], detail: "detailed" })` - avoid Read unless editing
3. Make targeted changes based on the context returned
4. `run_pipeline` again ONLY if you need more context during implementation
5. Do NOT chain multiple vexp calls - one `run_pipeline` replaces capsule + impact + memory + observation

### Subagent / Explore / Plan mode
- Subagents CAN and MUST call `run_pipeline` - always include the task description
- The PreToolUse hook blocks Grep/Glob when vexp daemon is running
- Do NOT spawn Agent(Explore) to freely search - call `run_pipeline` first,
then pass the returned context into the agent prompt if needed
- Always: `run_pipeline` -> get context -> spawn agent with context

### Smart Features (automatic - no action needed)
- **Intent Detection**: auto-detects from your task keywords. "fix bug" -> Debug, "refactor" -> blast-radius, "add" -> Modify
- **Hybrid Search**: keyword + semantic + graph centrality ranking
- **Session Memory**: auto-captures observations; memories auto-surfaced in results
- **LSP Bridge**: VS Code captures type-resolved call edges
- **Change Coupling**: co-changed files included as related context

### Advanced Parameters
- `preset: "debug"` - forces debug mode (capsule+tests+impact+memory)
- `preset: "refactor"` - deep impact analysis (depth 5)
- `max_tokens: 12000` - increase total budget for complex tasks
- `include_tests: true` - include test files in results
- `include_file_content: false` - omit full file content (lighter response)

### Multi-Repo Workspaces
`run_pipeline` auto-queries all indexed repos. Use `repos: ["alias"]` to scope.
Use `index_status` to discover available repo aliases.
<!-- /vexp -->
14 changes: 14 additions & 0 deletions .claude/hooks/vexp-guard.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash
# vexp-guard: block Grep/Glob when vexp daemon is running AND index is healthy.
# Fast path: if socket file or healthy marker doesn't exist, allow immediately.
# PID check: verify daemon process is alive (handles stale files after kill -9).
VEXP_DIR="${CLAUDE_PROJECT_DIR:-.}/.vexp"
SOCK="$VEXP_DIR/daemon.sock"
HEALTHY="$VEXP_DIR/healthy"
PID_FILE="$VEXP_DIR/daemon.pid"
if [ -S "$SOCK" ] && [ -f "$HEALTHY" ] && [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null; then
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"vexp daemon is running. Use run_pipeline instead of Grep/Glob."}}'
else
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"vexp index not ready, allowing direct search fallback."}}'
fi
exit 0
18 changes: 17 additions & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,23 @@
"Bash(docker run *)",
"Bash(python -c *)",
"Bash(pip install *)",
"Bash(ruff *)"
"Bash(ruff *)",
"Bash(git add *)",
"Bash(git commit -m ' *)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Grep|Glob|Regex",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-guard.sh",
"timeout": 3000
}
]
}
]
}
}
28 changes: 28 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## vexp context tools <!-- vexp v2.0.21 -->

**MANDATORY: use `run_pipeline` - do NOT grep, glob, or read files manually.**
vexp returns pre-indexed, graph-ranked context in a single call.

### Workflow
1. `run_pipeline` with your task description - ALWAYS FIRST (replaces all other tools)
2. Make targeted changes based on the context returned
3. `run_pipeline` again only if you need more context

### Available MCP tools
- `run_pipeline` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
Auto-detects intent. Includes file content. Example: `run_pipeline({ "task": "fix auth bug" })`
- `get_skeleton` - compact file structure
- `index_status` - indexing status
- `expand_vexp_ref` - expand V-REF placeholders in v2 output

### Agentic search
- Do NOT use built-in file search, grep, or codebase indexing - always call `run_pipeline` first
- If you spawn sub-agents or background tasks, pass them the context from `run_pipeline`
rather than letting them search the codebase independently

### Smart Features
Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.

### Multi-Repo
`run_pipeline` auto-queries all indexed repos. Use `repos: ["alias"]` to scope. Run `index_status` to see aliases.
<!-- /vexp -->
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,7 @@ htmlcov/
.pytest_cache/

# YAML backups if generated
/config/backups/
/config/backups/

# vexp
.vexp/
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.4.1] — 2026-05-29

### Fixed
- DNS-only deployments no longer flood the log with a "Service Tracker
Dashboard not enabled" line on every container, every refresh cycle.
When `STD_URL` or `STD_API_TOKEN` is missing, the notifier now logs a
single line at startup that STD integration is disabled, skips the
periodic refresh loop entirely (it only ever served STD), and does
not attempt STD dispatch on subsequent events.

### Added
- New env var `HOST_NAME_OVERRIDE`. Sets the host name used as the DNS
CNAME target (`<host>.<dockerdomain>`), overriding the auto-detected
hostname. Useful in WSL or other environments where the host's
hostname differs from the name your DNS entries should point at
(e.g. Docker Desktop reports the LinuxKit/WSL VM name rather than the
real host). Unset preserves the existing auto-detection behavior.

## [0.4.0] — 2026-05-14

### Added
Expand Down Expand Up @@ -168,7 +186,8 @@ Released.

Initial public release.

[Unreleased]: https://github.com/crzykidd/docker-api-notifier/compare/v0.4.0...HEAD
[Unreleased]: https://github.com/crzykidd/docker-api-notifier/compare/v0.4.1...HEAD
[0.4.1]: https://github.com/crzykidd/docker-api-notifier/releases/tag/v0.4.1
[0.4.0]: https://github.com/crzykidd/docker-api-notifier/releases/tag/v0.4.0
[0.3.0]: https://github.com/crzykidd/docker-api-notifier/releases/tag/v0.3.0
[0.2.3]: https://github.com/crzykidd/docker-api-notifier/releases/tag/v0.2.3
Expand Down
116 changes: 89 additions & 27 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
# docker-api-notifier — Claude Code Instructions

## Always

- After any change that affects architecture, dependencies, supported
notifier targets, or the wire contract with downstream consumers,
update `docs/PRD.md` and `README.md` accordingly.
- After completing a phase, update `README.md` with what has been built.
- Never leave PRD or README out of sync with the codebase.
## Always — Definition of Done

No change is complete until its documentation is updated **in the same
commit as the code**. This is not optional and not deferrable:

1. **CHANGELOG.md — every change, no exceptions.** Add an entry under
`[Unreleased]` describing what changed for the operator. A code
change with no CHANGELOG entry is an incomplete change.
2. **PRD (`docs/PRD.md`) — confirm on every change.** Before
finishing, explicitly check whether the change touches anything the
PRD documents (architecture, the wire contract with STD, supported
notifier targets, env vars, labels, the interpreter YAML format).
If yes, update the PRD and bump its revision-history table. If no,
confirm that in your summary ("PRD reviewed, no change needed")
rather than silently skipping it.
3. **README.md — when operator-facing behavior changes.** Env vars,
labels, deployment, interpreters: keep the README tables current.
4. Never leave CHANGELOG, PRD, or README out of sync with the code.

> **Known debt:** the PRDs and CLAUDE.md files in both this repo and
> STD have drifted from shipped reality before. A full audit of both
> repos' `docs/` against actual shipped state is a pending task (see
> the matching note in STD's `CLAUDE.md`). Until that audit happens,
> trust the code and CHANGELOG over the PRD where they disagree, and
> flag any contradiction you notice rather than propagating it.

## Commit style

Expand Down Expand Up @@ -42,20 +60,19 @@

## Build Status

Current shipped release: **v0.2.3** (latest tag on `main`)
Current shipped release: **v0.4.1** (latest tag on `main`).

Next release target: **v0.3.0** — cleanup release. Cannot ship until
**STD v0.5.0** is released (v0.3.0 emits canonical keys against
`/api/v1/register`, which STD v0.5.0 introduces).
Nothing currently in flight (`[Unreleased]` in `CHANGELOG.md` is
empty). v0.4.1 is a DNS/logging fix release (DNS host-name override,
STD-unconfigured log-flood fix) and does not change the STD wire
contract — the paired STD release for the capture/interpreter features
remains STD v0.6.0+.

- Phase 1 — Documentation baseline: IN PROGRESS
- Phase 2 — Logging consolidation: NOT STARTED
- Phase 3 — Shared retry helper: NOT STARTED
- Phase 4 — Stack-name fallback fix: NOT STARTED
- Phase 5 — `watched_actions` / `NOTIFIER_TRIGGERS` cleanup: NOT STARTED
- Phase 6 — Drop `trigger_reason` param: NOT STARTED
- Phase 7 — Switch to `/api/v1/register` + canonical keys: NOT STARTED
*(blocked until STD v0.5.0 is released)*
> Do not maintain a per-phase checklist here — it rots (this section
> was stale by two minor releases before this note was added). The
> CHANGELOG `[Unreleased]` section is the live record of work in
> flight; git tags are the record of what shipped. On release, update
> only the "Current shipped release" line above.

## Git Workflow

Expand Down Expand Up @@ -97,14 +114,59 @@ Next release target: **v0.3.0** — cleanup release. Cannot ship until
## Cross-Repo Coordination

This project is paired with
[service-tracker-dashboard](https://github.com/crzykidd/service-tracker-dashboard).
The contract is:

- **STD** owns the wire contract for the register endpoint.
- **Notifier** is a producer — it sends what STD documents.
- Wire-format changes start in STD. The notifier follows.
- For v0.3.0 specifically: STD v0.5.0 must ship first; this notifier
release switches to canonical keys + `/api/v1/register` after.
[service-tracker-dashboard](https://github.com/crzykidd/service-tracker-dashboard)
(STD). They are **two independent apps** — separate version lines,
separate Docker images, separate release cadences. The notifier is not
an STD component: it also drives Technitium DNS and can run with DNS
only, STD only, or both.

The two are coupled at exactly one seam: STD's `/api/v1/register` wire
contract.

- **STD owns the contract.** Its pydantic schemas define a valid
payload and use `extra="forbid"` — unknown keys get rejected with
HTTP 422, not ignored.
- **This notifier is the producer.** It sends what STD documents. The
wire format lives in `notifiers/service_tracker_dashboard.py`
(`_to_canonical`).

### The ordering rule

Because STD's validator rejects unknown keys, **the schema-accepting
side (STD) must ship before the schema-sending side (this notifier).**

- Adding a field this notifier will send → STD must accept it first.
Ship STD's schema change, *then* ship the notifier that emits the
field. Shipping the notifier first means STD 422s every payload from
upgraded hosts.
- A field STD wants to consume → STD can add the column/UI, but it
stays empty until a notifier version populates it. Feature isn't live
until both ship, notifier last.

**Consumer leads, producer follows. STD may lead; this notifier may
lag; this notifier must not lead the schema.**

### Safe-degradation guarantee (don't break it)

The notifier must keep older STD versions working:

- Capture fields (`networks` / `exposed_ports` / `published_ports`)
and `exposure_observations` require STD v0.6.0+. They are documented
as such in the README.
- `exposure_observations` semantics STD relies on: omit the field
entirely when no interpreters are loaded (STD reads absence as "no
update, preserve existing exposure rows"); send `[]` when
interpreters ran but nothing matched (STD reads that as "clear the
rows"). Do not collapse these two cases — `interpreter_loader`
returning `None` vs. `[]` is the distinction, and
`_run_interpreters` in `main.py` preserves it.

### Version pairing history (informational, not the rule)

- Notifier v0.3.0 switched to canonical keys + `/api/v1/register`;
requires STD v0.5.0+.
- Notifier v0.4.0 added capture fields + the interpreter mechanism
(`exposure_observations`); requires STD v0.6.0+.

## Notifier Module Conventions

Expand Down
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ every Docker host without it touching things you didn't ask it to touch.
2. Subscribes to the Docker event stream for ongoing changes (`start`,
`stop`, `die`, `pause`, `unpause`, `destroy`, `kill`, `update`).
3. Re-scans every running container on a periodic interval as a
self-healing measure (default every 60 seconds).
self-healing measure (default every 60 seconds). This loop only runs
when STD is configured — it exists to re-report containers to STD,
and does nothing for DNS-only deployments.

For each event, it reads the container's labels and dispatches to whichever
notifiers the container has opted in to via `dockernotifier.notifiers`.
Expand All @@ -66,6 +68,7 @@ notifier targets can be added without touching the core event loop.
| `TZ` | No | `UTC` | Timezone for log timestamps. |
| `STD_REFRESH_SECONDS` | No | `60` | Periodic re-scan interval in **seconds**. |
| `NOTIFIER_LOG_TO_STDOUT` | No | `1` | Set to `0` to silence console output. Logs still go to `/config/notifier.log`. Replaces the per-notifier `DNS_LOG_TO_STDOUT` and `STD_LOG_TO_STDOUT` vars, which are no longer recognized. |
| `HOST_NAME_OVERRIDE` | No | *host hostname* | Overrides the host name used as the DNS CNAME target (`<host>.<dockerdomain>`). If unset, the notifier reads the host's hostname from the `/etc/hostname` mount (falling back to the container's own hostname). Set this if running in WSL or other environments where the host has a different name than your DNS entries expect. |

### Technitium DNS

Expand Down Expand Up @@ -95,9 +98,12 @@ notifier targets can be added without touching the core event loop.
| `STD_REPORT_ALL_CONTAINERS` | No | `false` | When truthy (`true`, `1`, `yes` — case-insensitive), report **every running container on this host** to STD regardless of whether it has the `dockernotifier.notifiers=service-tracker-dashboard` opt-in label. Default off preserves per-container opt-in behavior. **Only affects STD** — the DNS notifier still requires explicit per-container opt-in via labels. Unrecognized values log a warning at startup and are treated as off. |
| `INTERPRETER_RELOAD_ON_EACH_EVENT` | No | `false` | Debug-only. When truthy, re-reads YAML interpreters from disk on every dispatch instead of once at startup. Use while iterating on a new YAML; do not leave on in production. |

If a notifier's required env vars are missing, that notifier silently
no-ops — the container won't fail to start. This is intentional so you
can run the same image with only DNS, only STD, or both.
If a notifier's required env vars are missing, that notifier no-ops —
the container won't fail to start. This is intentional so you can run
the same image with only DNS, only STD, or both. When STD's env vars
(`STD_URL` / `STD_API_TOKEN`) are absent the notifier logs a single
line at startup noting STD is disabled, skips the periodic refresh
loop, and does not attempt STD dispatch on any event.

---

Expand Down Expand Up @@ -237,6 +243,12 @@ services:
- STD_API_TOKEN=TOKENFROMSTDSERVER
- TZ=America/Los_Angeles
- STD_REFRESH_SECONDS=60
# Override the host name used as the DNS CNAME target (<host>.<dockerdomain>).
# If unset, the notifier uses the host's hostname (read from the
# /etc/hostname mount below). Set this if running in WSL or other
# environments where the host has a different name than your DNS
# entries expect.
# - HOST_NAME_OVERRIDE=wsl-host
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /etc/hostname:/etc/host_hostname:ro
Expand Down
13 changes: 13 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,22 @@ services:
- STD_API_TOKEN=${STD_API_TOKEN}
- TZ=America/Los_Angeles
- STD_REFRESH_SECONDS=120
# Override the host name used as the DNS CNAME target (<host>.<dockerdomain>).
# If unset, the notifier uses the host's hostname (read from the
# /etc/hostname mount below). Set this if running in WSL or other
# environments where the host has a different name than your DNS
# entries expect.
# - HOST_NAME_OVERRIDE=wsl-host
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /etc/hostname:/etc/host_hostname:ro
- /var/docker/docker-api-notifier:/config
- .:/app
restart: unless-stopped
labels:
# dockernotifier
- "dockernotifier.notifiers=dns,service-tracker-dashboard" #which services to run.
- "dockernotifier.dns.containerhostname=container_name" #put the host name you want to use for the cname entry
- "dockernotifier.dns.containerzone=containerzone" # This is the domain or zone name you want to add the container entry to
- "dockernotifier.dns.dockerdomain=home.arpa" #this is the domain your docker host is a member of

Loading
Loading