Run prettier on backlog folder

This commit is contained in:
Claudio Ortolina
2026-05-04 21:22:27 +01:00
parent 7bf89d9a1d
commit dc3b31378d
177 changed files with 2594 additions and 936 deletions
@@ -2,9 +2,10 @@
id: doc-1
title: Alternatives for reducing collection chat token usage
type: other
created_date: '2026-05-02 16:12'
updated_date: '2026-05-04 06:55'
created_date: "2026-05-02 16:12"
updated_date: "2026-05-04 06:55"
---
# ML-156 Research: Alternatives for reducing collection chat token usage
Research document for [ML-156 - Explore alternatives to reduce token usage when providing collection context to LLM for collection chat](backlog://task/ML-156).
@@ -14,11 +15,12 @@ Research document for [ML-156 - Explore alternatives to reduce token usage when
## Current state
**Token flow per new collection chat:**
1. `Collection.collection_summary/0` runs on mount via `start_async`
2. Loads ALL records: `from(r in Record, where: not is_nil(r.purchased_at), order_by: [order_alphabetically()], select: ^essential_fields())`
3. Groups by `musicbrainz_id`, formats each group as `"Artist - Title (year, formats) [genre1, genre2]"`
4. Builds stats header: `"# Stats: N releases, M artists\nGenres: ...\nFormats: ...\nEras: ..."`
5. Returns `{stats + "\n\n" + catalog, group_count}`
5. Returns `{stats + "\n\n" + catalog, group_count}`
6. Stored in `@collection_summary` assign on the LiveView
7. Passed to Chat component as `chat_context={@collection_summary}`
8. When user sends first message, `do_send_message` calls `chat_module.stream_response(messages, chat_context, callback)`
@@ -30,6 +32,7 @@ Research document for [ML-156 - Explore alternatives to reduce token usage when
**For 500 releases:** ~9,000 input tokens for catalog alone + ~100 tokens for stats + ~500 tokens for prompt template = ~9,600 tokens
**Key files:**
- `lib/music_library/collection.ex:203-235``collection_summary/0` (loads and formats all records)
- `lib/music_library/chats/collection_chat.ex:18-31``build_instructions/2` (embeds summary in prompt)
- `lib/music_library/chats/prompt.ex``Prompt.build/2` (wraps in identity + approach)
@@ -41,6 +44,7 @@ Research document for [ML-156 - Explore alternatives to reduce token usage when
### Streaming architecture constraints
The Chat component dispatches streaming to a `Task.Supervisor` child:
```elixir
Task.Supervisor.start_child(MusicLibrary.TaskSupervisor, fn ->
case chat_module.stream_response(stream_messages, chat_context, fn chunk ->
@@ -65,17 +69,20 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
**Token savings:** ~9,000 → ~100 input tokens (~99% reduction)
**Pros:**
- Simplest possible change; < 10 lines of code
- No architectural changes needed
- No streaming infrastructure changes
- The LLM can still answer statistical questions ("what's my most common genre?", "how many jazz records do I have?")
**Cons:**
- LLM loses ability to answer specific questions ("do I have Kid A?", "which Radiohead albums do I own?", "show me my 90s electronic albums")
- User experience degrades for record-specific queries
- The LLM will hallucinate or say "I don't have access to your specific collection" frequently
**Impact on code:**
1. `CollectionChat.build_instructions/2` — remove `#{collection_summary}` interpolation, keep only stats
2. `Collection.collection_summary/0` — could be simplified to return only stats (or keep as-is, the function is also tested independently)
@@ -88,7 +95,9 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
**Token savings:** ~9,000 → ~100 input tokens base + tool call overhead + tool results (~200-500 tokens when actually searching)
**How it works:**
1. Add a tool definition to the Responses API request:
```json
{
"type": "function",
@@ -97,7 +106,7 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"}
"query": { "type": "string", "description": "Search query" }
},
"required": ["query"]
}
@@ -114,6 +123,7 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
3. The Chat component's streaming architecture needs to handle this multi-turn flow.
**Pros:**
- Maximal token efficiency — only pay for what's actually needed
- LLM can answer arbitrary specific questions with real data
- Scales to any collection size
@@ -121,6 +131,7 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
- Leverages the existing `tools` infrastructure in the Responses API request
**Cons:**
- **Significantly more complex** — requires:
- Changes to `OpenAI.API.chat_stream/6` to support function calls in streaming mode
- Changes to `decode_responses_event/2` to handle function call SSE events
@@ -132,6 +143,7 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
- More error states to handle (function execution failures, parse errors)
**Implementation scope:**
1. **Tool definition module** — New module ~30 lines defining the OpenAI function tool schema
2. **Function executor** — New module or function in `CollectionChat` that executes the tool call (~20 lines)
3. **Streaming changes in `OpenAI.API`** — New `chat_stream_with_tools/7` or modify `chat_stream/6` to accept tools and handle function call events (~80 lines)
@@ -150,12 +162,14 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
**Token savings:** ~9,000 → ~400 tokens (summary text + stats)
**Pros:**
- Good middle ground — compact but informative
- The LLM has a narrative understanding of the collection
- No streaming architecture changes needed
- Single one-time cost to generate (amortized across many chats)
**Cons:**
- Still doesn't give the LLM ability to answer specific queries ("do I have Kid A?")
- Summary can go stale if not regenerated on collection changes
- Requires an initial LLM call to generate the summary (token cost + latency)
@@ -164,6 +178,7 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
- Adds a new background job / async concern
**Implementation scope:**
1. Store the cached summary (new DB field on a canonical collection `Chat` record, or a new schema)
2. A function/worker to generate the LLM summary (calls OpenAI, stores result)
3. Invalidation triggers (PubSub on record add/edit/delete, regenerate async)
@@ -179,11 +194,13 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
**Token savings:** ~9,000 → ~100 tokens base + tool results (0-500 tokens per use)
**Pros:**
- Best of both worlds: statistical awareness always available, specific lookup on demand
- Token-efficient — base cost is minimal
- LLM knows to reach for the tool when appropriate
**Cons:**
- Same implementation complexity as Alternative B for the tool infrastructure
- Slightly more prompt engineering to ensure the model uses the tool appropriately
@@ -198,6 +215,7 @@ Upload the collection catalog as a file to OpenAI, create a vector store, and us
**Token savings:** ~9,000 → ~100 tokens base + retrieval overhead (~200-500 when model searches)
**How it works:**
1. Format collection catalog as text (same as current `collection_summary/0` output)
2. Upload to OpenAI: `POST /v1/files` with `purpose: "assistants"``file_id`
3. Create vector store: `POST /v1/vector_stores``vector_store_id`
@@ -208,6 +226,7 @@ Upload the collection catalog as a file to OpenAI, create a vector store, and us
**Critical difference from Alt B:** No SSE event handling changes, no orchestration loop, no custom tool execution. `file_search` works exactly like `web_search_preview` (already in use) — OpenAI handles retrieval automatically.
**Pros:**
- ~150 lines vs ~250 for Alt B
- No changes to `decode_responses_event/2` or Chat streaming loop
- Semantic search (finds "upbeat 80s rock" not just keywords)
@@ -215,6 +234,7 @@ Upload the collection catalog as a file to OpenAI, create a vector store, and us
- Reusable for artist bios, notes, etc.
**Cons:**
- File can go stale if not updated on collection change
- 2-3 new API endpoints needed (Files API, Vector Stores API)
- Vector store indexing is async (may need brief poll)
@@ -226,13 +246,13 @@ Upload the collection catalog as a file to OpenAI, create a vector store, and us
## Comparison
| Criterion | A (stats-only) | B (custom func) | C (cached) | E (file_search) |
|---|---|---|---|---|
| Implementation complexity | ★☆☆☆☆ | ★★★★☆ | ★★★☆☆ | ★★☆☆☆ |
| Response quality | ★★☆☆☆ | ★★★★★ | ★★★☆☆ | ★★★★★ |
| Token efficiency | ★★★★★ | ★★★★★ | ★★★★☆ | ★★★★★ |
| Infrastructure risk | ★★★★★ | ★★★☆☆ | ★★★★☆ | ★★★★☆ |
| Reusability | ★☆☆☆☆ | ★★★★☆ | ★★☆☆☆ | ★★★☆☆ |
| Criterion | A (stats-only) | B (custom func) | C (cached) | E (file_search) |
| ------------------------- | -------------- | --------------- | ---------- | --------------- |
| Implementation complexity | ★☆☆☆☆ | ★★★★☆ | ★★★☆☆ | ★★☆☆☆ |
| Response quality | ★★☆☆☆ | ★★★★★ | ★★★☆☆ | ★★★★★ |
| Token efficiency | ★★★★★ | ★★★★★ | ★★★★☆ | ★★★★★ |
| Infrastructure risk | ★★★★★ | ★★★☆☆ | ★★★★☆ | ★★★★☆ |
| Reusability | ★☆☆☆☆ | ★★★★☆ | ★★☆☆☆ | ★★★☆☆ |
---
@@ -2,9 +2,10 @@
id: doc-2
title: Prevent pi from accessing sensitive files
type: other
created_date: '2026-05-03 13:32'
updated_date: '2026-05-04 06:55'
created_date: "2026-05-03 13:32"
updated_date: "2026-05-04 06:55"
---
# Implementation Analysis: Prevent pi from accessing sensitive files
## Summary
@@ -13,14 +14,14 @@ The task requires a **declarative** way to intercept pi tool calls that would ac
## Sensitive File Categories (for this project)
| Category | Patterns | Examples |
|----------|----------|----------|
| Environment secrets | `.env`, `.env.*`, `.envrc` | `.env`, `.env.production` |
| Key files | `*.pem`, `*.key`, `*.key.pub` | SSH keys |
| Secret/credential files | `*secret*`, `*credential*`, `*credentials*` | `secrets.yml`, `credentials.json` |
| Config dirs | `.ssh/`, `.aws/`, `.gnupg/` | SSH config, AWS credentials |
| Production config | Any path known to contain secrets | `rel/`, production env vars |
| Pi session files | `~/.pi/agent/sessions/` | Session JSONL files (contain tool outputs) |
| Category | Patterns | Examples |
| ----------------------- | ------------------------------------------- | ------------------------------------------ |
| Environment secrets | `.env`, `.env.*`, `.envrc` | `.env`, `.env.production` |
| Key files | `*.pem`, `*.key`, `*.key.pub` | SSH keys |
| Secret/credential files | `*secret*`, `*credential*`, `*credentials*` | `secrets.yml`, `credentials.json` |
| Config dirs | `.ssh/`, `.aws/`, `.gnupg/` | SSH config, AWS credentials |
| Production config | Any path known to contain secrets | `rel/`, production env vars |
| Pi session files | `~/.pi/agent/sessions/` | Session JSONL files (contain tool outputs) |
## Available Pi Extension Mechanisms
@@ -37,6 +38,7 @@ pi.on("tool_call", async (event, ctx) => {
```
**Key capabilities:**
- `event.toolName` — identifies the tool (`read`, `bash`, `grep`, `find`, `ls`, `write`, `edit`)
- `event.input` — tool parameters (mutable, can be patched)
- `isToolCallEventType("read", event)` — type-narrows for specific tools
@@ -46,15 +48,15 @@ pi.on("tool_call", async (event, ctx) => {
### Tools that can access sensitive files
| Tool | Danger | Input fields to check |
|------|--------|----------------------|
| `read` | **High** — reads file content into LLM context | `input.path` |
| `bash` | **High** — can `cat`, `grep`, `curl` any file | `input.command` (regex match) |
| `grep` | **High** — searches file contents | `input.path` |
| `find` | **Low** — lists filenames only | `input.path` (directory listing) |
| `ls` | **Low** — lists directory contents | `input.path` |
| `write` | **Medium** — could overwrite sensitive files | `input.path` |
| `edit` | **Medium** — could modify sensitive files | `input.path` |
| Tool | Danger | Input fields to check |
| ------- | ---------------------------------------------- | -------------------------------- |
| `read` | **High** — reads file content into LLM context | `input.path` |
| `bash` | **High** — can `cat`, `grep`, `curl` any file | `input.command` (regex match) |
| `grep` | **High** — searches file contents | `input.path` |
| `find` | **Low** — lists filenames only | `input.path` (directory listing) |
| `ls` | **Low** — lists directory contents | `input.path` |
| `write` | **Medium** — could overwrite sensitive files | `input.path` |
| `edit` | **Medium** — could modify sensitive files | `input.path` |
### `tool_result` event (secondary approach)
@@ -85,6 +87,7 @@ pi.registerTool({
**Description:** Single extension listening on `tool_call`, checking paths/commands against a configuration file of protected patterns. Blocks matching calls.
**Implementation:**
1. Create `.pi/extensions/sensitive-file-guard.ts` (or `index.ts` in a directory)
2. Create `.pi/sensitive-paths.json` — declarative config listing protected patterns
3. In `tool_call` handler:
@@ -94,6 +97,7 @@ pi.registerTool({
- Block if match, notify in UI
**Sample config file (`.pi/sensitive-paths.json`):**
```json
{
"blocked_paths": [
@@ -109,16 +113,12 @@ pi.registerTool({
".gnupg/",
"config/*.secret.exs"
],
"blocked_commands": [
"cat .env",
"cat ~/.ssh",
"printenv",
"cat ~/.aws"
]
"blocked_commands": ["cat .env", "cat ~/.ssh", "printenv", "cat ~/.aws"]
}
```
**Pros:**
- Minimal code (~60 lines of TypeScript)
- Declarative configuration file (easy to audit and update)
- Works with ALL tools (read, bash, grep, find, ls, write, edit)
@@ -128,6 +128,7 @@ pi.registerTool({
- Fails safe (blocks before any file access)
**Cons:**
- Can't sanitize partial output (but blocking prevents all access)
- Regex-based command scanning for bash has edge cases
- `find`/`ls` only blocked at directory level, not per-file
@@ -141,17 +142,20 @@ pi.registerTool({
**Description:** Override the `read` and `bash` tools with custom implementations that include path checking. Delegates to original implementation for allowed paths.
**Implementation:**
1. Create `.pi/extensions/sensitive-file-guard/` directory
2. Override `read` tool: check paths, delegate allowed reads to original implementation
3. Override `bash` tool: scan commands, delegate allowed commands to original implementation
4. Optionally override `grep`, `find`, `ls`, `write`, `edit`
**Pros:**
- Full control (can log, can sanitize output, can modify behavior)
- No event overhead per tool call
- Can add audit logging
**Cons:**
- Must reimplement or re-wrap built-in tool logic (~200+ lines)
- Must maintain compatibility with pi updates
- More complex testing
@@ -170,10 +174,12 @@ pi.registerTool({
Same as Route A, plus a `tool_result` handler that scans output for sensitive patterns and redacts them.
**Pros:**
- Most comprehensive protection
- Can allow partial access with sanitization
**Cons:**
- Most complex
- `tool_result` sanitization is error-prone (false positives/negatives)
- Redacted content still consumes context tokens
@@ -195,14 +201,14 @@ Route A (pure `tool_call` event interception with declarative config) is the **s
## Architectural Impact
| Touchpoint | Impact |
|------------|--------|
| `.pi/extensions/` | New extension file added |
| `.pi/sensitive-paths.json` | New config file (declarative rules) |
| `tool_call` event | New subscriber, no breaking changes |
| Built-in tools | Unchanged (event interception is non-invasive) |
| `ctx.ui` | Used for notification only (no blocking dependency) |
| Other extensions | Compatible — tool_call handlers chain in load order |
| Touchpoint | Impact |
| -------------------------- | --------------------------------------------------- |
| `.pi/extensions/` | New extension file added |
| `.pi/sensitive-paths.json` | New config file (declarative rules) |
| `tool_call` event | New subscriber, no breaking changes |
| Built-in tools | Unchanged (event interception is non-invasive) |
| `ctx.ui` | Used for notification only (no blocking dependency) |
| Other extensions | Compatible — tool_call handlers chain in load order |
## Open Questions
@@ -2,14 +2,16 @@
id: doc-3
title: Force production logs to single-line format
type: other
created_date: '2026-05-03 13:53'
updated_date: '2026-05-04 06:55'
created_date: "2026-05-03 13:53"
updated_date: "2026-05-04 06:55"
---
# Research: Force production logs to single-line format
## Current state
**Config** (`config/config.exs`):
```elixir
config :logger, :default_formatter,
format: "$time $metadata[$level] $message\n",
@@ -17,18 +19,19 @@ config :logger, :default_formatter,
```
**Override** (`config/prod.exs`):
```elixir
config :logger, level: :info
```
**Log sources in prod**:
| Source | Produces | Problem |
|---|---|---|
| `Phoenix.Logger` (telemetry) | `GET /health` + `Sent 200 in Xms` | Two separate lines for one request — can't reverse |
| `Phoenix.Logger` (telemetry) | `CONNECTED TO Phoenix.LiveView.Socket...` | One event with embedded `\n` — 4+ lines |
| Custom `Logger.info/error/...` | Varies | Some may be multi-line |
| OTP / stack traces | Crash reports | Multi-line |
| Source | Produces | Problem |
| ------------------------------ | ----------------------------------------- | -------------------------------------------------- |
| `Phoenix.Logger` (telemetry) | `GET /health` + `Sent 200 in Xms` | Two separate lines for one request — can't reverse |
| `Phoenix.Logger` (telemetry) | `CONNECTED TO Phoenix.LiveView.Socket...` | One event with embedded `\n` — 4+ lines |
| Custom `Logger.info/error/...` | Varies | Some may be multi-line |
| OTP / stack traces | Crash reports | Multi-line |
`Phoenix.Logger` is auto-attached by Phoenix.Endpoint and handles telemetry events including `[:phoenix, :endpoint, :start]`, `[:phoenix, :endpoint, :stop]`, and `[:phoenix, :socket_connected]`.
@@ -46,14 +49,14 @@ v2 (`~> 2.0.0-rc.5`) uses `Logster.attach_phoenix_logger()` in `application.ex`
### Architecture impact
| Touchpoint | Change |
|---|---|
| `config/prod.exs` | Add `config :phoenix, :logger, false` + Logster config + formatter config |
| `lib/music_library/application.ex` | Add conditional `Logster.attach_phoenix_logger()` |
| `lib/music_library_web/telemetry_log_handler.ex` | **New module** — handles socket_connected telemetry |
| `lib/music_library/logger/single_line_formatter.ex` | **New module** — Logger.Formatter format function |
| `mix.exs` | Add `{:logster, "~> 2.0.0-rc.5"}` |
| Docs | Update `docs/production-infrastructure.md` with logging config |
| Touchpoint | Change |
| --------------------------------------------------- | ------------------------------------------------------------------------- |
| `config/prod.exs` | Add `config :phoenix, :logger, false` + Logster config + formatter config |
| `lib/music_library/application.ex` | Add conditional `Logster.attach_phoenix_logger()` |
| `lib/music_library_web/telemetry_log_handler.ex` | **New module** — handles socket_connected telemetry |
| `lib/music_library/logger/single_line_formatter.ex` | **New module** — Logger.Formatter format function |
| `mix.exs` | Add `{:logster, "~> 2.0.0-rc.5"}` |
| Docs | Update `docs/production-infrastructure.md` with logging config |
### Performance
@@ -2,9 +2,10 @@
id: doc-4
title: GitHub Actions → Tangled Spindles
type: other
created_date: '2026-05-03 16:57'
updated_date: '2026-05-04 06:54'
created_date: "2026-05-03 16:57"
updated_date: "2026-05-04 06:54"
---
# CI Pipeline Porting Analysis: GitHub Actions → Tangled Spindles
## Current State (GitHub Actions)
@@ -22,6 +23,7 @@ Key GHA features used: `jdx/mise-action`, `actions/cache`, concurrency control,
Tangled provides **Spindles** — a CI runner service. Workflows are YAML files in `.tangled/workflows/`.
**Available features:**
- Triggers: `push`, `pull_request`, `manual` — with branch/tag glob filtering
- Engine: `nixery` (uses Nixpkgs packages via Nixery at `nixery.tangled.sh`)
- Dependencies: declared from nixpkgs (or custom registries)
@@ -34,6 +36,7 @@ Tangled provides **Spindles** — a CI runner service. Workflows are YAML files
- Timeout: configurable via `SPINDLE_PIPELINES_WORKFLOW_TIMEOUT` (default 5m)
**Missing features (vs GitHub Actions):**
- ❌ No job dependencies (`needs`) — all steps in a workflow are strictly sequential
- ❌ No parallel jobs — single linear pipeline per workflow file
- ❌ No inter-run caching primitive — no equivalent to `actions/cache`
@@ -51,20 +54,20 @@ Tangled also provides **Webhooks** (push events, HMAC-signed, 3 retries with bac
Nixery builds OCI images with each nixpkgs package in a **separate Docker layer**. Docker reuses pre-existing layers, so any nixpkgs dependency declared in the `dependencies` block — `mise`, `nodejs`, `shellcheck`, `hurl`, `sqlite` — is cached at the Docker level between runs.
This is confirmed by the Tangled team: *"caching for commonly used packages is free thanks to Docker (pre-existing layers get reused)."* — [Introducing Spindle blog post](https://blog.tangled.org/ci/)
This is confirmed by the Tangled team: _"caching for commonly used packages is free thanks to Docker (pre-existing layers get reused)."_ — [Introducing Spindle blog post](https://blog.tangled.org/ci/)
### What is NOT cached (fetched/compiled fresh every run)
| Artifact | Reason |
|----------|--------|
| Elixir 1.20.0-rc.4, Erlang 28.5 | Not in nixpkgs; `mise install` downloads fresh each run |
| `mix deps` + `_build` | No directory-level cache primitive in Spindle |
| `npm` packages + asset builds | Same — no cache primitive |
| Fluxon private Hex repo packages | Fetched each run |
| Artifact | Reason |
| -------------------------------- | ------------------------------------------------------- |
| Elixir 1.20.0-rc.4, Erlang 28.5 | Not in nixpkgs; `mise install` downloads fresh each run |
| `mix deps` + `_build` | No directory-level cache primitive in Spindle |
| `npm` packages + asset builds | Same — no cache primitive |
| Fluxon private Hex repo packages | Fetched each run |
### Within a run
Per the blog post: *"the `/tangled/workspace` and `/nix` volumes persisted across steps."* So within a single workflow run, compiled artifacts are available to subsequent steps. But between runs, everything resets except the Nixery Docker layers.
Per the blog post: _"the `/tangled/workspace` and `/nix` volumes persisted across steps."_ So within a single workflow run, compiled artifacts are available to subsequent steps. But between runs, everything resets except the Nixery Docker layers.
### Potential caching strategy: Custom Nix derivation for Elixir 1.20.0-rc.4
@@ -74,20 +77,21 @@ Similarly, Erlang 28.5 could be pinned via a custom derivation if not in nixpkgs
## Dependency Availability in Nixpkgs
| Tool | In nixpkgs stable? | Version | Notes |
|------|-------------------|---------|-------|
| `elixir` | ✅ | 1.18.4 / 1.19.5 | **Not 1.20.0-rc.4** — must use mise or custom Nix derivation |
| `erlang` | ✅ | via beam packages | OTP versions tied to package sets |
| `nodejs` | ✅ | Yes | |
| `mise` | ✅ | 2025.11.7 | Can install exact Elixir/Erlang/Node from mise.toml |
| `shellcheck` | ✅ | Yes | |
| `hurl` | ✅ (likely) | | |
| `sqlite` | ✅ | Yes | |
| `credo` | ❌ | Elixir mix task | Must install via mix after Elixir is available |
| `sobelow` | ❌ | Elixir mix task | Same |
| `mix_audit` | ❌ | Elixir mix task | Same |
| Tool | In nixpkgs stable? | Version | Notes |
| ------------ | ------------------ | ----------------- | ------------------------------------------------------------ |
| `elixir` | ✅ | 1.18.4 / 1.19.5 | **Not 1.20.0-rc.4** — must use mise or custom Nix derivation |
| `erlang` | ✅ | via beam packages | OTP versions tied to package sets |
| `nodejs` | ✅ | Yes | |
| `mise` | ✅ | 2025.11.7 | Can install exact Elixir/Erlang/Node from mise.toml |
| `shellcheck` | ✅ | Yes | |
| `hurl` | ✅ (likely) | | |
| `sqlite` | ✅ | Yes | |
| `credo` | ❌ | Elixir mix task | Must install via mix after Elixir is available |
| `sobelow` | ❌ | Elixir mix task | Same |
| `mix_audit` | ❌ | Elixir mix task | Same |
The workflow pattern with mise:
1. Add `mise` as a nixpkgs dependency (cached via Docker layers ✅)
2. `mise install` to fetch Elixir 1.20.0-rc.4, Erlang 28.5, Node 25.9.0 (NOT cached ❌)
3. Run existing mise tasks
@@ -95,30 +99,35 @@ The workflow pattern with mise:
## Implementation Routes
### Route A: Pure Spindle — Single Linear Workflow
One `.tangled/workflows/ci.yml` that runs lint → test → deploy sequentially.
**Pros:** Simplest — one file, fully self-contained on Tangled.
**Cons:** Slowest (no parallelism between lint+test), deploy runs on every PR push to main, no separation of concerns.
### Route B: Multiple Spindle Workflows — Split CI/CD
Separate files: `lint.yml`, `test.yml`, `deploy.yml`. Deploy is manual-trigger only.
**Pros:** Lint and test can trigger on PRs, deploy is intentional. Better separation of concerns.
**Cons:** No enforcement that test passes before manual deploy. Lint and test still run sequentially within each workflow.
### Route C: Spindle for CI + Webhook for Deploy
Spindle handles lint+test on PRs. A Tangled webhook on push to main triggers Coolify deployment via an external endpoint.
**Pros:** Deploy runs independently. Can add gating logic in webhook receiver.
**Cons:** Requires a webhook receiver (external service). More infrastructure.
### Route D: Spindle for All — Lean Pipeline (Accept Gaps)
Full pipeline on Spindle but accept the missing features. Configure longer timeout, accept sequential runs, use manual deploy trigger.
**Pros:** Fully native Tangled experience.
**Cons:** All gaps accepted as-is. May frustrate PR workflow.
### Route E: Mirror to GitHub + Keep Actions
Mirror the Tangled repo to GitHub (or use Tangled as a mirror of GitHub) and keep existing Actions.
**Pros:** Zero CI changes. All existing features preserved.
@@ -2,9 +2,10 @@
id: doc-5
title: Log Line Copy in Prod-Logs Extension
type: other
created_date: '2026-05-03 21:06'
updated_date: '2026-05-04 06:55'
created_date: "2026-05-03 21:06"
updated_date: "2026-05-04 06:55"
---
# Implementation Analysis: Log Line Copy in Prod-Logs Extension
## Problem Summary
@@ -28,13 +29,15 @@ The `/prod-logs` extension (`.pi/extensions/prod-logs/index.ts`) displays log li
## Route A: Cursor + Toggle Selection + setEditorText
### Concept
Add a visual cursor line. Enter to copy single line. Space to toggle line selection (multi-select mode). `y` to copy all selected lines and exit. After exit, call `ctx.ui.setEditorText()` with the copied text.
### Changes to LogViewer
1. Add `cursorIndex: number` (absolute line index into `this.lines`)
2. Add `selectedIndices: Set<number>` (absolute indices)
3. New key bindings:
- `Enter` → copy cursor line, `done(line)`
- `Enter` → copy cursor line, `done(line)`
- `Space` → toggle selection of cursor line
- `y` → copy selected lines (or cursor line if none selected), `done(text)`
4. Render changes:
@@ -44,6 +47,7 @@ Add a visual cursor line. Enter to copy single line. Space to toggle line select
5. Help text update: add "y copy · Space select · Enter copy line"
### Return value handling
```typescript
const copiedText = await ctx.ui.custom<string | null>(...);
if (copiedText !== null) {
@@ -52,12 +56,14 @@ if (copiedText !== null) {
```
### Pros
- No external dependencies
- `setEditorText` is already available in pi's ExtensionAPI
- Aligns with the "paste" use case (text in editor = ready to send or copy)
- Simple, contained change to one file
### Cons
- Modifies editor content (replaces whatever the user had typed)
- Not a true "system clipboard" copy — the user can't paste outside pi without an extra step
- Toggle-based selection for multi-line is somewhat unusual UX (vs. range selection)
@@ -67,9 +73,11 @@ if (copiedText !== null) {
## Route B: Visual Mode (Vim-style) + setEditorText
### Concept
Vim-inspired visual mode: `v` enters linewise visual selection, `j`/`k` extend the range, `y` copies and exits. Enter still copies single line. Selection is always a contiguous range.
### Changes to LogViewer
1. Add `cursorIndex: number` (absolute)
2. Add `visualMode: boolean` + `visualAnchor: number` (start of visual selection)
3. New key bindings:
@@ -84,11 +92,13 @@ Vim-inspired visual mode: `v` enters linewise visual selection, `j`/`k` extend t
5. Help text: show visual mode help when active
### Pros
- Familiar UX for vim users
- Contiguous range selection is intuitive
- Same `setEditorText` approach — no external deps
### Cons
- Slightly more complex state machine (normal mode vs. visual mode)
- Doesn't allow non-contiguous selection (but the user said "contiguous")
- Still modifies editor content
@@ -98,19 +108,23 @@ Vim-inspired visual mode: `v` enters linewise visual selection, `j`/`k` extend t
## Route C: System Clipboard + Either Selection Model
### Concept
Either selection model (A or B), but instead of `setEditorText`, write to the system clipboard. Requires a package like `clipboardy`.
### Additional Changes
- Add `clipboardy` dependency (or use `node:child_process` with `pbcopy`/`xclip`)
- On copy: `clipboardy.writeSync(text)` or similar
- No need to change return type — can stay `void` since we don't pass text back
### Pros
- True "copy" behavior — paste anywhere
- Doesn't touch editor content
- Works across applications
### Cons
- External dependency (`clipboardy` — native module, may have install issues)
- Platform-specific behavior (macOS `pbcopy`, Linux `xclip`/`wl-copy`, Windows)
- pi extensions run in the pi process; clipboard access may have security implications
@@ -121,6 +135,7 @@ Either selection model (A or B), but instead of `setEditorText`, write to the sy
## Recommendation: Route B (Visual Mode + setEditorText)
**Rationale:**
1. **No external dependencies**`setEditorText` is already available
2. **Familiar UX** — vim-style visual mode is intuitive for developers
3. **Contiguous selection** — matches the user's "contiguous lines" description
@@ -136,6 +151,7 @@ Either selection model (A or B), but instead of `setEditorText`, write to the sy
## Outstanding Question for User
How should the copied text be placed in the editor?
- **Append** to current editor content (preserves what's there)
- **Replace** current editor content (clean slate)
@@ -2,9 +2,10 @@
id: doc-6
title: Programmatic Access to Production Logs
type: other
created_date: '2026-05-04 06:43'
updated_date: '2026-05-04 06:55'
created_date: "2026-05-04 06:43"
updated_date: "2026-05-04 06:55"
---
# Implementation Route Analysis: Programmatic Access to Production Logs
## Decision: Route B — Pi Tool via `pi.registerTool()` ✓ SELECTED
@@ -28,19 +29,23 @@ The LLM needs a tool it can call to retrieve production logs directly into its c
## Route A: Tidewave MCP Tool (Elixir-side)
### Description
Add a new MCP tool `get_production_logs` to Tidewave that calls the Coolify API from Elixir using `Req`.
### Pros
- Architecturally consistent with existing MCP tools
- Req is battle-tested in this codebase
- Accessible by any MCP client
### Cons
- Must forward Coolify env vars to Phoenix app runtime
- Rewrites the API call in Elixir (no code reuse from existing `fetchLogs`)
- New Elixir module to maintain
### Verdict
**Rejected** in favor of Route B. The pi tool approach is simpler, reuses existing code and credentials directly, and keeps the change contained to a single file.
---
@@ -48,9 +53,11 @@ Add a new MCP tool `get_production_logs` to Tidewave that calls the Coolify API
## Route B: Pi Tool via `pi.registerTool()` ✓ SELECTED
### Description
Add a `fetch_production_logs` tool registration to the existing `prod-logs` extension. The tool is called by the LLM directly (just like `read`, `bash`, etc.), fetches logs via the existing `fetchLogs()` function, and returns text content to the LLM.
### Pros
- **Direct code reuse**: Same `fetchLogs()` function, same `resolveVar()` helper, same env vars
- **No infrastructure changes**: No new Elixir modules, no credential forwarding, no config changes
- **First-class LLM tool**: The LLM calls it natively during its agent loop, the result flows through the same context pipeline as any other tool result
@@ -58,10 +65,12 @@ Add a `fetch_production_logs` tool registration to the existing `prod-logs` exte
- **No external dependencies**: `typebox` is already available as a pi built-in import
### Cons
- pi-only (not accessible from other MCP clients — irrelevant for this use case)
- Tool definition consumes a small amount of LLM context window space (same as any tool)
### Architecture Impact
- **Modified file**: `.pi/extensions/prod-logs/index.ts` (add tool registration, ~50 lines)
- **No changes**: No Elixir modules, no config, no schemas, no routes, no UI
@@ -2,9 +2,10 @@
id: doc-7
title: Pi access to production errors
type: other
created_date: '2026-05-04 08:07'
updated_date: '2026-05-04 08:13'
created_date: "2026-05-04 08:07"
updated_date: "2026-05-04 08:13"
---
# Implementation Routes: pi access to production errors
## Problem
@@ -12,6 +13,7 @@ updated_date: '2026-05-04 08:13'
The project captures production errors via the `error_tracker` Elixir dependency (stored in `MusicLibrary.TelemetryRepo` SQLite database). Errors can currently only be viewed through the built-in web dashboard at `/dev/errors` (behind login auth, only with `:monitoring_routes` enabled). There is no programmatic access — no API, no tooling, no pi extension.
The goal is to enable pi (the coding agent) to fetch and browse production errors. This requires three layers:
1. A programmatic API to expose error data (behind auth)
2. Pi tools the LLM can call to fetch errors
3. A pi extension for interactive browsing
@@ -22,6 +24,7 @@ The goal is to enable pi (the coding agent) to fetch and browse production error
**How it works:**
Add a new API controller (`MusicLibraryWeb.ErrorsController`) with two endpoints under `/api/v1/errors`:
- `GET /api/v1/errors` — list errors with optional filtering (status, muted, search) and pagination
- `GET /api/v1/errors/:id` — single error detail with its occurrences and stacktraces
@@ -30,6 +33,7 @@ The controller queries the `error_tracker_errors` and `error_tracker_occurrences
Pi tools (`fetch_production_errors` and `fetch_production_error`) make HTTP requests to this API using `fetch()` or `pi.exec("curl", ...)`. The pi extension builds on these tools for an interactive TUI.
**Pros:**
- Follows existing patterns exactly (see `CollectionController`, `require_api_token`)
- Clean separation of concerns: API layer, tool layer, extension layer
- Pi tools work remotely — no need for SSH or filesystem access to the production server
@@ -40,6 +44,7 @@ Pi tools (`fetch_production_errors` and `fetch_production_error`) make HTTP requ
- The TelemetryRepo already exists and has the tables — no new database work
**Cons:**
- Requires a server code change and deployment
- Adds two new routes
@@ -62,11 +67,13 @@ Pi tools execute SQL queries directly against the TelemetryRepo SQLite database
No server-side API changes needed. The pi tools would use `pi.exec("sqlite3", ...)` or read the database file directly.
**Pros:**
- Zero server code changes
- Immediate access to ALL data — no API shape limitations
- Can run complex ad-hoc queries without API changes
**Cons:**
- **Production access requires SSH or filesystem access** — violates the project's existing API-based pattern for pi access (cf. `fetch_production_logs` which uses Coolify API, not SSH)
- No auth layer — tools have full read access to the entire database
- Tightly couples pi tools to the error_tracker schema — any migration could break tools
@@ -86,10 +93,12 @@ The direct-access approach introduces deployment friction (SSH key management, f
Use the existing Tidewave MCP tools (`tidewave_execute_sql_query`) to query the error_tracker tables directly. This is available in dev but would need the Tidewave MCP server to be accessible in production (e.g., via SSH tunnel or a production-side MCP server).
**Pros:**
- No new code at all — uses what's already there
- `tidewave_execute_sql_query` already understands the repo structure
**Cons:**
- **Tidewave MCP server does not run in production** — it's a development-only tool
- Even if it did, running a dev tool against production is architecturally wrong
- No browsing UX, no filtering, no pagination — basic SQL results only
@@ -106,9 +115,11 @@ Tidewave is a development tool. Exposing it to production would be an architectu
Add an endpoint to the existing Coolify-like API pattern used by `fetch_production_logs`. This would require Coolify to expose error_tracker data, which it doesn't natively support. Could potentially parse production logs for error patterns, but that's unstructured and duplicative.
**Pros:**
- Consistent with the existing `fetch_production_logs` pattern
**Cons:**
- Coolify doesn't expose error_tracker data
- ErrorTracker already has structured data — parsing it from raw logs is backwards and lossy
- Would require Coolify API changes or custom Coolify plugin