chore: repo cleanup — drop process reports and stale planning docs
gates / consistency-and-conventions (push) Successful in 18s
gates / consistency-and-conventions (push) Successful in 18s
Remove 123 temp/process files so the repo tracks only deliverable code and docs: - AgentsReport/ (86) + reportAgents/ (28): per-round agent handoff reports — shared memory for the workflow, not project deliverables. AgentsReport/ is now gitignored so future reports stay local-only. - tmp_request.md: stale task ledger (R1-R7 all landed; file's own instruction was 'delete when all are implemented'). - reports/bug-report-smb-server-access-denied.md: old bug writeup; the fix landed in the stabilization round. - FINAL_SUMMARY.md, IMPLEMENTATION_PLAN.md, AUDIT.md, AUDIT_TABLE.md, Design-and-implement-a-self-describing-command-registry-for-POS.md: unreferenced phase artifacts, superseded by code. - Stray fragments: 'To' (empty), '.n' (stray redirect). - AGENT_TODO.md: cleanup entry moved to Done. Kept: DOC/ (19), tools-docs/ytsync.md (live-referenced), AGENT_TODO.md, AGENTS.md, MAINTENANCE.md, bin/lib/apps/tests/config/scripts/templates/ systemd/entertainment/features/completions/, x64_bin/ (installer). Nothing lost — git history retains every removed file. Verified: make gen idempotent, make check OK, make lint 0 FAIL/0 WARN, make test 17 files / 299 checks green, git diff --check clean.
This commit is contained in:
@@ -30,3 +30,6 @@ session
|
||||
# Scratch/plan notes — never committed
|
||||
TEMP_PLAN.md
|
||||
dream.md
|
||||
|
||||
# Agent process reports — local-only shared memory (not tracked)
|
||||
AgentsReport/
|
||||
|
||||
@@ -42,6 +42,8 @@ summary (newest last).
|
||||
|
||||
## Done
|
||||
|
||||
- **2026-09-06** — Repo cleanup: removed 114 temp/process files (AgentsReport/ 86 + reportAgents/ 28 agent reports), stale task/plan/audit docs (tmp_request.md, FINAL_SUMMARY.md, IMPLEMENTATION_PLAN.md, AUDIT.md, AUDIT_TABLE.md, registry-design doc), stray `To`/`.n`/`reports/` artifacts; gitignored AgentsReport/ so agent process reports stay local-only. DOC/, tools-docs/ytsync.md, AGENT_TODO.md, bin/lib/apps/tests/config/scripts etc. kept untouched.
|
||||
|
||||
- **2026-09-06** — `pos ai` OpenRouter 402 + unbounded session — Architect→Builder→Reviewer. User hit OpenRouter 402 on the `assist` alias: "You requested up to 131072 tokens, but can only afford 4511" — no provider sent `max_tokens`, so OpenRouter pre-bills the routed model's full worst-case output (131072 on `openrouter/auto`); user also asked to bound session history to last-5 requests. Architect decisions: `AI_MAX_TOKENS` (num, default **2048**, real cost cap) sent as `max_tokens` on OpenRouter and `generationConfig.maxOutputTokens` on Gemini (llamacpp skipped — local/free); `AI_SESSION_TURNS` (messages, 2 per exchange; default 40 kept back-compat; 10 = last 5 conversations) resolved lazily in `session_push` because config loads after the hardcoded line-25 default; both registered in the `bin/pos-ai` `# POS_CONFIG:` `@General` section → visible in `pos config ai`. Reviewer hardening (CHANGES_REQUIRED → fixed): unguarded env input could reach jq `tonumber` (0/-5/010/abc) — both providers + `session_push` now guard with `^[1-9][0-9]*$` fallback-to-default. Verified: fake-curl shim smoke (16 provider-body + 12 session-window checks incl. 010-regression proof), `make gen` idempotent, `make check` OK, `make lint` 0/0, `make test` 17 files/299 checks green; Reviewer ACCEPT (twice). Tester regression round not run this cycle (user's call); permanent coverage remains a follow-up.
|
||||
|
||||
- **2026-09-06** — `pos ai alias` create-flow silent abort + bogus step labels. User hit: pressing Enter on "System prompt (empty = use built-in)" silently returned to the menu (no alias created); step counters showed `[1/4] [2/4]` in a 5-step flow. Detective: `menu_ask_value` (lib/menu-lib.sh) contract returns rc 1 for empty+no-default, collapsing "empty" with "cancel"; the Step 4 call at `:410` passed `""` default so the advertised empty answer triggered `|| return 0` → silent abort; same latent trap at alias-name `:353` (re-prompt dead code). Pre-existing (introduced with the alias feature `f61766b0`/`9f289ba3`/`300b742a`), NOT a 2026-09-06 regression; 11 other `menu_ask_value` callers correct (6 external rely on empty=cancel, 4 pass defaults) → no global semantic change allowed. Architect: opt-in `--allow-empty` flag on `menu_ask_value` (backward-compatible; empty+no-default → rc 0 empty value, genuine cancel/EOF → rc 1, default wins) + step labels fixed to `/5`. Builder: implemented (lib/menu-lib.sh flag+docs, bin/pos-ai-alias `:353`/`:410` + step counters), 7-case smoke matrix PASS, gen idempotent, check OK, lint 0 FAIL/0 WARN. User chose to commit without the Tester regression round (report: `AgentsReport/tester/2026-09-06_alias-menu-tests.md` — pty feasibility proven, steps 2-6 pending).
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
# Audit of POS AI Tools Implementation
|
||||
|
||||
## Current Status Analysis
|
||||
|
||||
Based on my examination of the codebase, I can see that the `pos ai hf` and `pos ai server` tools are implemented but appear to be incomplete compared to the underlying applications they're supposed to wrap.
|
||||
|
||||
## Audit Findings
|
||||
|
||||
### 1. `pos ai hf` - Current State vs. Hugging Face CLI
|
||||
|
||||
**Current Implementation:**
|
||||
- Supports search, download, list, remove commands
|
||||
- Basic GGUF filtering capabilities
|
||||
- Branch/revision support
|
||||
- Authentication via HF_TOKEN
|
||||
- Basic file listing and downloading
|
||||
|
||||
**Missing Hugging Face CLI Features:**
|
||||
- **Repository Discovery:** The tool lacks advanced repository discovery features
|
||||
- **Model Information:** No `info` or `show` commands to get repository details
|
||||
- **Advanced Filtering:** Missing `--include` and `--exclude` patterns
|
||||
- **Revision Support:** Limited branch support, no tag/commit support
|
||||
- **Cache Management:** No cache inspection or management
|
||||
- **Authentication:** Only basic token support, no credential helper integration
|
||||
- **Multi-Shard Models:** Limited support for sharded GGUF models
|
||||
- **Progress Indicators:** Basic progress, no detailed download metrics
|
||||
- **Environment Variables:** Limited environment variable handling
|
||||
|
||||
### 2. `pos ai server` - Current State vs. llama.cpp
|
||||
|
||||
**Current Implementation:**
|
||||
- Supports start, stop, status, models, logs commands
|
||||
- Basic GPU detection and auto-config
|
||||
- Port and host configuration
|
||||
- Model selection
|
||||
- Basic health checking
|
||||
|
||||
**Missing llama.cpp Features:**
|
||||
- **GPU Configuration:** No support for detailed GPU offloading (`-ngl`, multi-GPU)
|
||||
- **Memory Management:** No support for KV cache configuration
|
||||
- **Performance Options:** Missing batch sizes, ubatch sizes, continuous batching
|
||||
- **Sampling Parameters:** No temperature, top-k, top-p, repetition penalty controls
|
||||
- **Advanced Features:** Missing JSON/schema, tool calling, reasoning options
|
||||
- **Server Configuration:** No support for all server options like metrics, health endpoints
|
||||
- **Version Detection:** No capability to detect and validate llama-server version
|
||||
- **Graceful Shutdown:** Limited shutdown handling
|
||||
|
||||
## Technical Limitations
|
||||
|
||||
### Hugging Face CLI Analysis
|
||||
Based on the Hugging Face documentation and typical CLI patterns, the actual `hf` command provides:
|
||||
- `hf list` - List repositories
|
||||
- `hf info <repo>` - Show repository information
|
||||
- `hf files <repo>` - List repository files
|
||||
- `hf download <repo>` - Download repository with various options
|
||||
- `--include`/`--exclude` - File pattern filtering
|
||||
- `--revision` - Specific revision support
|
||||
- `--cache` - Cache management
|
||||
- Authentication with tokens or credential helpers
|
||||
- Detailed progress reporting
|
||||
|
||||
### llama.cpp Analysis
|
||||
Based on llama.cpp documentation, the actual `llama-server` provides:
|
||||
- `--model <path>` - Model file specification
|
||||
- `--port <port>` - Port binding
|
||||
- `--host <host>` - Host binding
|
||||
- `--ctx-size <n>` - Context size
|
||||
- `--n-gpu-layers <n>` - GPU layer count
|
||||
- `--tensor-split` - Tensor split configuration
|
||||
- `--split-mode` - Split mode (none, layer, row)
|
||||
- `--flash-attn` - Flash attention support
|
||||
- `--threads <n>` - Thread count
|
||||
- `--mmap` - Memory mapping
|
||||
- `--mlock` - Memory locking
|
||||
- `--batch-size` - Batch size
|
||||
- `--ubatch-size` - UBatch size
|
||||
- `--log-disable` - Logging control
|
||||
- `--health` - Health endpoint
|
||||
- `--slots` - Concurrent request slots
|
||||
- `--metrics` - Metrics endpoint
|
||||
|
||||
## Required Improvements
|
||||
|
||||
### For `pos ai hf`:
|
||||
|
||||
1. **Add Info Command**: `pos ai hf info <repo>`
|
||||
2. **Add Files Command**: `pos ai hf files <repo>`
|
||||
3. **Add Cache Management**: `pos ai hf cache`
|
||||
4. **Enhance Download**: Support include/exclude, revision, and better progress
|
||||
5. **Model Information**: Show model details, size, and metadata
|
||||
6. **Repository Files**: List files with size and metadata
|
||||
7. **Authentication**: Better credential handling
|
||||
8. **Version Support**: Detect and support version-specific features
|
||||
|
||||
### For `pos ai server`:
|
||||
|
||||
1. **GPU Configuration**: Support detailed GPU offloading parameters
|
||||
2. **Memory Management**: Context size, KV cache, memory allocation
|
||||
3. **Performance Tuning**: Batch size, ubatch size, continuous batching
|
||||
4. **Sampling Controls**: Temperature, top-k, top-p, repetition penalty
|
||||
5. **Advanced Features**: JSON/schema, tool calling, reasoning
|
||||
6. **Server Options**: Health, metrics, concurrency control
|
||||
7. **Version Awareness**: Detect and validate supported options
|
||||
8. **Process Management**: Better monitoring and graceful shutdown
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
Given that we don't have the actual underlying CLI tools installed in this environment, I'll need to:
|
||||
1. Create a comprehensive audit document
|
||||
2. Design the proper interface based on documented capabilities
|
||||
3. Implement stubs and placeholders for actual functionality
|
||||
4. Ensure all the missing features are properly accounted for in the plan
|
||||
-166
@@ -1,166 +0,0 @@
|
||||
# Detailed Audit of POS AI Tools vs. Underlying Applications
|
||||
|
||||
## 1. `pos ai hf` - Audit Table
|
||||
|
||||
| Category | Current Implementation | Upstream Hugging Face CLI | Missing | Incorrect | Fix |
|
||||
|----------|----------------------|---------------------------|---------|-----------|-----|
|
||||
| **Repository Discovery** | Basic search | `hf list`, `hf info` | `info` command | - | Add `pos ai hf info <repo>` |
|
||||
| **File Listing** | `--list` flag | `hf files` | `files` command | - | Add `pos ai hf files <repo>` |
|
||||
| **Model Information** | Basic metadata | Detailed model info | No detailed info | - | Add model details display |
|
||||
| **Repository Files** | Limited listing | `hf files` with sizes | No file listing | - | Add file listing command |
|
||||
| **Downloading** | Basic download | `hf download` with include/exclude | No pattern filtering | - | Add `--include`/`--exclude` |
|
||||
| **Revisions** | `--branch` | `--revision` support | No tag/commit support | - | Add revision support |
|
||||
| **Cache Management** | No cache commands | `hf cache` | No cache commands | - | Add `pos ai hf cache` |
|
||||
| **Authentication** | `HF_TOKEN` only | Credential helpers, more tokens | Limited auth | - | Add enhanced auth |
|
||||
| **Progress Reporting** | Basic progress | Detailed metrics | Limited info | - | Add progress details |
|
||||
| **File Pattern Matching** | Basic file matching | Wildcards, patterns | No pattern support | - | Add pattern matching |
|
||||
| **Multi-Shard Support** | Limited | Full sharded support | No sharded model support | - | Add sharded model support |
|
||||
| **Error Handling** | Basic errors | Detailed error messages | Limited context | - | Improve error reporting |
|
||||
|
||||
## 2. `pos ai server` - Audit Table
|
||||
|
||||
| Category | Current Implementation | Upstream llama.cpp | Missing | Incorrect | Fix |
|
||||
|----------|----------------------|--------------------|---------|-----------|-----|
|
||||
| **Model Loading** | Basic model selection | `--model` with validation | - | - | Enhance model validation |
|
||||
| **GPU Offloading** | Auto-detect | `--n-gpu-layers`, multi-GPU | No detailed GPU control | - | Add GPU layer control |
|
||||
| **Memory Context** | Basic context | `--ctx-size`, KV cache | Limited memory options | - | Add memory controls |
|
||||
| **Performance Tuning** | Basic params | Batch sizes, threads, ubatch | No tuning | - | Add performance options |
|
||||
| **Sampling Controls** | Basic | Temperature, top-k, top-p | No sampling options | - | Add sampling parameters |
|
||||
| **Advanced Features** | No advanced | JSON/schema, tool calling | No advanced features | - | Add advanced options |
|
||||
| **Server Configuration** | Basic | Health, metrics, concurrency | Limited server options | - | Add server options |
|
||||
| **Version Detection** | No version | `--version` support | No version awareness | - | Add version detection |
|
||||
| **Process Management** | Basic | Graceful shutdown, monitoring | Limited process control | - | Add process monitoring |
|
||||
| **Logging** | Basic | Logging controls | No log options | - | Add logging controls |
|
||||
|
||||
## 3. Detailed Missing Features
|
||||
|
||||
### For `pos ai hf`:
|
||||
|
||||
**Missing Repository Information:**
|
||||
- No `info` command to show repository details
|
||||
- No way to see model architecture or parameters
|
||||
- No commit history or version details
|
||||
|
||||
**Missing File Operations:**
|
||||
- No `files` command to list repository contents
|
||||
- No file pattern matching or filtering
|
||||
- No file size information in listings
|
||||
|
||||
**Missing Advanced Downloading:**
|
||||
- No `--include`/`--exclude` patterns
|
||||
- No revision/tag support
|
||||
- No cache management commands
|
||||
|
||||
**Missing Authentication:**
|
||||
- No credential helper support
|
||||
- No token validation
|
||||
- No multi-auth method support
|
||||
|
||||
### For `pos ai server`:
|
||||
|
||||
**Missing GPU Configuration:**
|
||||
- No `--n-gpu-layers` support
|
||||
- No tensor splitting (`--tensor-split`)
|
||||
- No multi-GPU configuration
|
||||
- No GPU device selection
|
||||
|
||||
**Missing Memory Management:**
|
||||
- No `--ctx-size` control
|
||||
- No KV cache configuration
|
||||
- No memory mapping (`--mmap`) or locking (`--mlock`)
|
||||
|
||||
**Missing Performance Options:**
|
||||
- No batch size control (`--batch-size`)
|
||||
- No ubatch size (`--ubatch-size`)
|
||||
- No continuous batching options
|
||||
|
||||
**Missing Sampling Controls:**
|
||||
- No temperature control
|
||||
- No top-k, top-p options
|
||||
- No repetition penalty
|
||||
- No JSON/schema support
|
||||
|
||||
**Missing Server Features:**
|
||||
- No health endpoint configuration
|
||||
- No metrics endpoint support
|
||||
- No concurrency controls
|
||||
- No graceful shutdown handling
|
||||
|
||||
**Missing Version Awareness:**
|
||||
- No version detection capability
|
||||
- No feature compatibility checking
|
||||
- No version-specific option support
|
||||
|
||||
## 4. Implementation Priority
|
||||
|
||||
### Critical (Must Have):
|
||||
1. Add `info` and `files` commands for `pos ai hf`
|
||||
2. Add GPU control for `pos ai server`
|
||||
3. Add version detection
|
||||
4. Add proper error handling
|
||||
|
||||
### High Priority:
|
||||
1. Add `--include`/`--exclude` patterns
|
||||
2. Add revision support
|
||||
3. Add memory context control
|
||||
4. Add performance tuning options
|
||||
|
||||
### Medium Priority:
|
||||
1. Add cache management
|
||||
2. Add enhanced authentication
|
||||
3. Add advanced sampling
|
||||
4. Add advanced server configuration
|
||||
|
||||
### Low Priority:
|
||||
1. Add progress metrics
|
||||
2. Add logging controls
|
||||
3. Add process monitoring
|
||||
4. Add integration with POS ecosystem
|
||||
|
||||
## 5. Technical Requirements
|
||||
|
||||
### For `pos ai hf`:
|
||||
- Enhanced parsing for new flags
|
||||
- Integration with Hugging Face API
|
||||
- Cache management system
|
||||
- Better file pattern matching
|
||||
- Improved error reporting
|
||||
|
||||
### For `pos ai server`:
|
||||
- Enhanced parameter parsing
|
||||
- Version detection system
|
||||
- GPU configuration module
|
||||
- Memory management controls
|
||||
- Process management system
|
||||
- Configuration validation
|
||||
|
||||
## 6. Compatibility Considerations
|
||||
|
||||
### Backward Compatibility:
|
||||
- All existing commands must work unchanged
|
||||
- All existing options must work unchanged
|
||||
- Default behavior must be preserved
|
||||
- Configuration file compatibility maintained
|
||||
|
||||
### Breaking Changes:
|
||||
- None planned
|
||||
- All enhancements are additive
|
||||
- No existing functionality removed
|
||||
|
||||
## 7. Testing Requirements
|
||||
|
||||
### `pos ai hf` Testing:
|
||||
- Model download functionality
|
||||
- File pattern matching
|
||||
- Revision handling
|
||||
- Cache operations
|
||||
- Authentication testing
|
||||
|
||||
### `pos ai server` Testing:
|
||||
- GPU configuration validation
|
||||
- Memory parameter testing
|
||||
- Performance tuning options
|
||||
- Server startup/shutdown
|
||||
- Version compatibility testing
|
||||
|
||||
This audit identifies the comprehensive gap between the current POS tools and the capabilities of the underlying Hugging Face CLI and llama.cpp server. The implementation plan will address all these gaps systematically.
|
||||
@@ -1,527 +0,0 @@
|
||||
# Architecture: `pos ai alias` — AI Agent Alias Manager
|
||||
|
||||
**Date:** 2026-08-26
|
||||
**Author:** Architect (big-pickle)
|
||||
**Status:** DECISION_READY
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
| Decision | Choice |
|
||||
|----------|--------|
|
||||
| Storage | `~/.config/linux_post_install/ai-aliases.env` (pipe-delimited structured data) |
|
||||
| Shell aliases | `~/.config/linux_post_install/ai-aliases.sh` (generated, never hand-edited) |
|
||||
| Tool | `bin/pos-ai-alias` — new `ai` category tool |
|
||||
| No POS_CONFIG scope | Alias management is CRUD, not env-key editing; `pos config` is not involved |
|
||||
| Source of truth | `.env` file; `.sh` file is regenerated on every write |
|
||||
| .bashrc integration | One conditional `source` line, added by postinstall.sh |
|
||||
| INSTALL_CHANGES | `postinstall.sh` adds `.bashrc` source line; `bin/pos` adds `ai-alias` to INTERACTIVE_CMDS |
|
||||
|
||||
**Open items:**
|
||||
- None — all decisions are evidence-backed from project conventions.
|
||||
|
||||
---
|
||||
|
||||
## Decision 1: Data Format
|
||||
|
||||
**Problem:** Store named AI aliases (provider, session name, system prompt) durably, with safe special-character handling and trivial parsing.
|
||||
|
||||
### Options Considered
|
||||
|
||||
**Option A: Shell alias format (one-liner shell aliases)**
|
||||
```
|
||||
alias devbot='pos ai gemini ask --session devbot --system "You are a dev assistant"'
|
||||
```
|
||||
- Architecture: Store raw shell alias lines; `.bashrc` sources the file directly.
|
||||
- Advantages: No generation step; shell sources it natively.
|
||||
- Costs: Parsing aliases back into components (for edit/list) requires fragile quote-aware shell parsing; single quotes inside system prompts break the syntax.
|
||||
- Risks: Prompt containing `'` corrupts the file. Edit must read then reconstruct — fragile.
|
||||
|
||||
**Option B: Pipe-delimited structured data + generated .sh**
|
||||
```
|
||||
devbot|gemini|devbot|You are a dev assistant
|
||||
```
|
||||
- Architecture: `.env` file is the source of truth (pipe-delimited fields). A separate `.sh` file is regenerated from it on every write. `.bashrc` sources the `.sh` file.
|
||||
- Advantages: Parsing is trivial (`IFS='|'`); single-quote escaping is handled at generation time; data is safe to `grep`/`sort`/`awk`.
|
||||
- Costs: One extra file (`.sh`); a `_regen_aliases()` helper function.
|
||||
- Risks: Generation must escape correctly — but this is a single, testable function.
|
||||
|
||||
**Option C: Individual env files per alias**
|
||||
```
|
||||
~/.config/linux_post_install/ai-aliases/devbot.env
|
||||
```
|
||||
- Architecture: One file per alias; load all at shell startup.
|
||||
- Advantages: No parsing of multi-entry files.
|
||||
- Costs: Directory management; glob at shell startup; harder to list all; no atomic operations.
|
||||
|
||||
### Decision: Option B
|
||||
|
||||
Pipe-delimited structured data is the cleanest separation. The `.env` file is the source of truth. The `.sh` file is a generated artifact. This matches the project's own pattern of "generated code between GEN markers" — except here the generator lives inside the tool itself, not `make gen`.
|
||||
|
||||
**Rationale:** The project already has a pattern of generated files (e.g., `GEN:START`/`GEN:END` blocks, `completions/pos.bash`). The tool owns its own generation. Parsing shell aliases is fragile and error-prone; pipe-delimited data is trivially safe.
|
||||
|
||||
### Data Format Specification
|
||||
|
||||
```
|
||||
# ~/.config/linux_post_install/ai-aliases.env
|
||||
# Managed by: pos ai alias (do not hand-edit)
|
||||
# Format: alias_name|provider|session_name|system_prompt
|
||||
# Pipe characters in system_prompt are not supported.
|
||||
#
|
||||
# agent_name|provider|session_name|system_prompt
|
||||
devbot|gemini|devbot|You are a Linux dev assistant. Reply with commands only.
|
||||
code|openrouter|codereview|You are a code reviewer. Be concise.
|
||||
```
|
||||
|
||||
**Field constraints:**
|
||||
|
||||
| Field | Rules |
|
||||
|-------|-------|
|
||||
| `alias_name` | Shell-valid identifier: `[a-zA-Z][a-zA-Z0-9_-]*` |
|
||||
| `provider` | Must match an installed provider: `gemini`, `openrouter`, etc. |
|
||||
| `session_name` | Defaults to the alias name if empty; `[a-zA-Z0-9_-]+` |
|
||||
| `system_prompt` | Free text; no `\|` (pipe) characters; may be empty (uses built-in prompt) |
|
||||
|
||||
**Header:** Two comment lines at the top (file description + format) are auto-maintained by the tool.
|
||||
|
||||
**Empty session_name convention:** When session_name is empty (field is blank between pipes), the alias uses the alias name as the session name. This avoids redundant repetition for the common case.
|
||||
|
||||
---
|
||||
|
||||
## Decision 2: File Layout
|
||||
|
||||
### New files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `bin/pos-ai-alias` | New CLI tool (100755) |
|
||||
|
||||
### Modified files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `bin/pos` | Add `ai-alias` to `INTERACTIVE_CMDS` list |
|
||||
| `postinstall.sh` | Add `.bashrc` source line for `ai-aliases.sh` (conditional, no-clobber) |
|
||||
|
||||
### Runtime files (user config, NOT in repo)
|
||||
|
||||
| File | Purpose | Mutability |
|
||||
|------|---------|------------|
|
||||
| `~/.config/linux_post_install/ai-aliases.env` | Alias data (source of truth) | Created/modified by tool |
|
||||
| `~/.config/linux_post_install/ai-aliases.sh` | Generated shell aliases | Regenerated on every write |
|
||||
|
||||
### Files NOT modified
|
||||
|
||||
| File | Why not |
|
||||
|------|---------|
|
||||
| `.gitignore` | `~/.config/linux_post_install/` is a user directory, not in the repo. No new repo files to ignore. |
|
||||
| `config/` | No template file needed — the env file is user-created on first use. |
|
||||
| `lib/` | No new library. Tool sources `common.sh` + `menu-lib.sh` from existing libs. |
|
||||
|
||||
---
|
||||
|
||||
## Decision 3: Tool Interface
|
||||
|
||||
### POS Header
|
||||
|
||||
```bash
|
||||
# POS: ai alias — Create/edit/remove named AI agent aliases
|
||||
# POS_SUBCMDS: create edit remove list show
|
||||
```
|
||||
|
||||
No `POS_FLAGS:` — this is a subcommand-based tool, not a flag-based tool.
|
||||
|
||||
### CLI Interface
|
||||
|
||||
```
|
||||
pos ai alias [subcommand] [args]
|
||||
|
||||
Subcommands:
|
||||
(no args) Interactive menu (create/edit/remove/list)
|
||||
create Create a new alias (interactive prompts)
|
||||
create <name> Create with given name (interactive prompts for rest)
|
||||
edit Pick an alias to edit (interactive)
|
||||
edit <name> Edit a specific alias
|
||||
remove Pick an alias to remove (interactive, with confirmation)
|
||||
remove <name> Remove a specific alias (with confirmation)
|
||||
list List all aliases (non-interactive, machine-readable)
|
||||
show <name> Show one alias's details
|
||||
|
||||
Options:
|
||||
-h|--help Show this help.
|
||||
|
||||
Examples:
|
||||
pos ai alias # interactive menu
|
||||
pos ai alias list # show all aliases
|
||||
pos ai alias create # interactive create
|
||||
pos ai alias create mybot # create 'mybot' alias
|
||||
pos ai alias edit mybot # edit the 'mybot' alias
|
||||
pos ai alias remove mybot # remove 'mybot' (with confirm)
|
||||
pos ai alias show mybot # show alias details
|
||||
```
|
||||
|
||||
### Subcommand Details
|
||||
|
||||
**`pos ai alias` (no args):** Interactive menu using `menu_run` from `lib/menu-lib.sh`. Options:
|
||||
1. Create new alias
|
||||
2. Edit existing alias
|
||||
3. Remove alias
|
||||
4. List aliases
|
||||
|
||||
**`pos ai alias list`:** Non-interactive table output:
|
||||
```
|
||||
Aliases (3):
|
||||
mybot gemini mybot You are a helpful assistant
|
||||
code openrouter code You are a code reviewer
|
||||
dev gemini devbot Dev assistant
|
||||
```
|
||||
Format: `%-12s %-12s %-12s %s` (name, provider, session, prompt-truncated-to-60).
|
||||
|
||||
**`pos ai alias show <name>`:** Full details including the resolved shell command.
|
||||
|
||||
---
|
||||
|
||||
## Decision 4: POS_CONFIG
|
||||
|
||||
**Decision: NO POS_CONFIG scope.**
|
||||
|
||||
**Rationale:** The `pos config` / `POS_CONFIG` system is designed for simple `KEY=VALUE` env files (like `ai.env`, `telegram.env`, `entertainment.env`). Aliases are structured, multi-field records, not key-value pairs. The `cfg_ui()` pattern from `lib/config-ui.sh` doesn't apply here — it renders a numbered menu of KEY=VALUE pairs, not CRUD operations on named records.
|
||||
|
||||
The alias tool is self-contained with its own interactive menus. It does not participate in `pos config`.
|
||||
|
||||
---
|
||||
|
||||
## Decision 5: Shell Alias Generation
|
||||
|
||||
### The `_regen_aliases()` function
|
||||
|
||||
This function reads `ai-aliases.env` and writes `ai-aliases.sh`:
|
||||
|
||||
```bash
|
||||
_regen_aliases() {
|
||||
local env_file="$1" sh_file="$2" tmp
|
||||
tmp="$(mktemp)"
|
||||
printf '#!/usr/bin/env bash\n# Auto-generated by pos ai alias — do not hand-edit.\n# Source: %s\n\n' "$env_file" >"$tmp"
|
||||
|
||||
if [ -f "$env_file" ]; then
|
||||
while IFS='|' read -r name provider session prompt _rest; do
|
||||
# Skip comments and empty lines
|
||||
[[ "$name" =~ ^[[:space:]]*# ]] && continue
|
||||
[[ -z "$name" ]] && continue
|
||||
|
||||
# Validate alias name
|
||||
[[ "$name" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]] || continue
|
||||
|
||||
# Session defaults to alias name if empty
|
||||
[ -z "$session" ] && session="$name"
|
||||
|
||||
# Escape single quotes in the system prompt for shell-safe embedding
|
||||
local escaped_prompt="${prompt//\'/\'\\\'\'}"
|
||||
|
||||
printf "alias %s='pos ai %s ask --session %s" "$name" "$provider" "$session" >>"$tmp"
|
||||
if [ -n "$prompt" ]; then
|
||||
printf " --system '%s'" "$escaped_prompt" >>"$tmp"
|
||||
fi
|
||||
printf "'\n" >>"$tmp"
|
||||
done < <(grep -v '^[[:space:]]*#' "$env_file" | grep -v '^[[:space:]]*$' || true)
|
||||
fi
|
||||
|
||||
mv "$tmp" "$sh_file"
|
||||
chmod 644 "$sh_file"
|
||||
}
|
||||
```
|
||||
|
||||
**Single-quote escaping:** `${prompt//\'/\'\\\'\'}` — bash `parameter expansion` replaces every `'` with `'\''` (close-quote, escaped-quote, open-quote). This is the standard and safe pattern for embedding arbitrary strings in single-quoted shell contexts.
|
||||
|
||||
**Edge case — empty prompt:** When the system prompt is empty, the `--system` flag is omitted entirely, letting `pos ai` use its built-in default prompt.
|
||||
|
||||
### Syntax validation before commit
|
||||
|
||||
After generating the `.sh` file, run `bash -n` to verify syntax:
|
||||
```bash
|
||||
if ! bash -n "$sh_file" 2>/dev/null; then
|
||||
warn "Generated alias file has syntax errors — keeping previous version"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
```
|
||||
|
||||
### Shell integration in .bashrc
|
||||
|
||||
```bash
|
||||
# AI aliases (managed by pos ai alias)
|
||||
[ -f ~/.config/linux_post_install/ai-aliases.sh ] && source ~/.config/linux_post_install/ai-aliases.sh
|
||||
```
|
||||
|
||||
This line is added by `postinstall.sh` with the standard no-clobber grep check.
|
||||
|
||||
---
|
||||
|
||||
## Decision 6: Interactive Flow
|
||||
|
||||
### Main Menu (`pos ai alias` — no args)
|
||||
|
||||
Uses `menu_run` from `lib/menu-lib.sh`:
|
||||
|
||||
```
|
||||
════════════════════════════════════════════
|
||||
AI Agent Aliases
|
||||
════════════════════════════════════════════
|
||||
1) Create new alias
|
||||
2) Edit existing alias
|
||||
3) Remove alias
|
||||
4) List aliases
|
||||
0) Exit
|
||||
----------------------------------------
|
||||
Choose:
|
||||
```
|
||||
|
||||
### Create Flow
|
||||
|
||||
1. Prompt for alias name: `menu_ask_value "Alias name" ""` — validate format (`[a-zA-Z][a-zA-Z0-9_-]*`)
|
||||
2. Check for duplicate name → warn and re-prompt if taken
|
||||
3. Prompt for provider: show available providers (read from `$PROVIDER_DIR/*.sh`), default `gemini`
|
||||
4. Prompt for session name: default = alias name
|
||||
5. Prompt for system prompt: default = empty (uses built-in)
|
||||
6. Confirm: `confirm "Create alias '<name>'?" y`
|
||||
7. Write to `.env`, regenerate `.sh`, log success
|
||||
|
||||
### Edit Flow
|
||||
|
||||
1. List existing aliases (name + provider + first-40-chars of prompt)
|
||||
2. Pick one (if no arg given): `menu_pick "Pick alias" "${names[@]}"`
|
||||
3. Show current values
|
||||
4. For each field, prompt with current value as default (Enter = keep)
|
||||
5. Confirm changes
|
||||
6. Rewrite `.env` entry, regenerate `.sh`
|
||||
|
||||
### Remove Flow
|
||||
|
||||
1. Pick alias: `menu_pick` or named
|
||||
2. Show alias details
|
||||
3. `confirm "Remove alias '<name>'? This cannot be undone." n` (default = no)
|
||||
4. Remove from `.env`, regenerate `.sh`
|
||||
|
||||
### List Flow (non-interactive)
|
||||
|
||||
Prints formatted table to stdout. Provider column left-aligned, name left-aligned, prompt truncated to 60 chars with `...`.
|
||||
|
||||
### Show Flow (non-interactive)
|
||||
|
||||
Full details + resolved command:
|
||||
```
|
||||
Alias: mybot
|
||||
Provider: gemini
|
||||
Session: mybot
|
||||
Prompt: You are a helpful assistant
|
||||
Command: pos ai gemini ask --session mybot --system 'You are a helpful assistant'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decision 7: Edge Cases and Error Handling
|
||||
|
||||
| Edge Case | Handling |
|
||||
|-----------|----------|
|
||||
| **Duplicate alias name on create** | `warn "Alias '$name' already exists — use 'pos ai alias edit $name' instead"`; re-prompt |
|
||||
| **Empty alias name** | `err "Alias name cannot be empty"` |
|
||||
| **Invalid alias name** (contains spaces, starts with digit) | `err "Invalid alias name '$name' — use letters, digits, hyphens, underscores"` |
|
||||
| **Invalid provider** | `err "Unknown provider '$p' — available: $(ls ...)"` — uses same provider discovery as `pos-ai` |
|
||||
| **Empty system prompt** | Allowed — omit `--system` flag; `pos ai` uses its built-in default prompt |
|
||||
| **Very long system prompt** | Truncate display in `list` output (60 chars + `...`); full value preserved in `.env` and `.sh`. Warn if > 500 chars during creation. |
|
||||
| **Pipe character in system prompt** | Rejected on input: `err "System prompt must not contain '\|' characters"` |
|
||||
| **System prompt with single quotes** | Handled by `_regen_aliases()` escaping: `'` -> `'\''` in the generated alias line |
|
||||
| **Editing an alias "in use"** | No lock/detection needed. The user edits the `.env`; on next shell startup (or `source ~/.bashrc`), aliases update. No runtime state conflict. |
|
||||
| **File doesn't exist yet** | First create auto-creates both `.env` and `.sh` |
|
||||
| **Corrupted/invalid .env line** | Skipped by `_regen_aliases()` (name validation regex) |
|
||||
| **Concurrent edits** | Not a concern — personal single-user tool. Last write wins. |
|
||||
| **Missing pos-ai dependency** | Tool doesn't require `pos-ai` at runtime — it only writes config. No dep guard needed. |
|
||||
|
||||
---
|
||||
|
||||
## Decision 8: Security Considerations
|
||||
|
||||
### System prompt injection
|
||||
|
||||
System prompts are user-authored text that becomes a shell argument. Risks:
|
||||
- **Shell injection via alias execution:** The prompt is single-quoted in the alias, so shell metacharacters (`$`, backtick, `!`) are literal — safe.
|
||||
- **`pos ai` prompt injection:** This is a user's own prompt for their own AI. No trust boundary crossing.
|
||||
- **File permissions:** `ai-aliases.env` gets `chmod 600` (user-only read; consistent with other config files). `ai-aliases.sh` gets `chmod 644` (needed by bash `source`).
|
||||
|
||||
### Escaping correctness
|
||||
|
||||
The single-quote escaping `${prompt//\'/\'\\\'\'}` is the only place where correctness matters critically. If it fails, the generated alias has a syntax error and `source` will report it. Mitigation:
|
||||
- The tool runs `bash -n` on the generated `.sh` file before committing it.
|
||||
- If syntax check fails, warn and skip the regeneration (keep the old `.sh`).
|
||||
|
||||
### No secrets in the alias file
|
||||
|
||||
System prompts are not secrets — they're user-authored instructions. API keys stay in `ai.env` (already managed by `pos config ai`). No new secret surface.
|
||||
|
||||
---
|
||||
|
||||
## Decision 9: Integration with pos-ai
|
||||
|
||||
### How aliases invoke pos-ai
|
||||
|
||||
Each generated alias calls:
|
||||
```bash
|
||||
alias <name>='pos ai <provider> ask --session <session> --system "<prompt>"'
|
||||
```
|
||||
|
||||
This uses the existing `pos-ai` flags:
|
||||
- `--provider <name>` — supported since the beginning (line 633 of `bin/pos-ai`)
|
||||
- `--session <name>` — supported (line 639)
|
||||
- `--system <text>` — supported (line 642)
|
||||
|
||||
**No changes to `bin/pos-ai` are required.** The alias tool is a standalone configuration tool that writes shell aliases calling `pos ai`.
|
||||
|
||||
### Provider validation
|
||||
|
||||
The alias tool must validate the provider name against installed providers. It reuses the same discovery logic from `pos-ai`:
|
||||
```bash
|
||||
PROVIDER_DIR="$(dirname "$0")/../lib/ai-providers"
|
||||
# Fallback for installed layout
|
||||
[ -d "$PROVIDER_DIR" ] || PROVIDER_DIR="$(dirname "$0")/ai-providers"
|
||||
```
|
||||
|
||||
This is the same pattern used in `pos-ai` at line 17-18. The alias tool discovers providers independently (no dependency on `pos-ai` being sourced).
|
||||
|
||||
---
|
||||
|
||||
## Decision 10: Installation Changes
|
||||
|
||||
### postinstall.sh modification
|
||||
|
||||
Add a block after the existing `ai.env` installation (around line 50):
|
||||
|
||||
```bash
|
||||
# ── AI aliases shell integration ───────────────────────────────
|
||||
ALIAS_SRC_LINE='# AI aliases (managed by pos ai alias)
|
||||
[ -f ~/.config/linux_post_install/ai-aliases.sh ] && source ~/.config/linux_post_install/ai-aliases.sh'
|
||||
if ! grep -qsF "ai-aliases.sh" "$BASHRC" 2>/dev/null; then
|
||||
run printf '%s\n' "$ALIAS_SRC_LINE" >> "$BASHRC"
|
||||
log "Added AI aliases source to ~/.bashrc"
|
||||
fi
|
||||
```
|
||||
|
||||
### bin/pos modification
|
||||
|
||||
Add `ai-alias` to the `INTERACTIVE_CMDS` list (line 262). Current value:
|
||||
```
|
||||
INTERACTIVE_CMDS="docker-compose docker-vbox network-hotspot system-firewall media-mp4 media-sync system-backup system-uninstall share-usb-server share-smb-server share-smb-client share-nfs-client share-nfs-server communication-telegram-listener communication-matrix-listener ai ai-gemini ai-openrouter system-schedule entertainment-config config"
|
||||
```
|
||||
|
||||
Add `ai-alias` to this space-separated list.
|
||||
|
||||
### No other installation changes
|
||||
|
||||
- No new apt packages (no deps beyond bash)
|
||||
- No new lib files (tool sources `common.sh` + `menu-lib.sh` from existing libs)
|
||||
- No systemd services
|
||||
- No config template in `config/`
|
||||
|
||||
---
|
||||
|
||||
## Decision 11: Long Prompt Handling
|
||||
|
||||
System prompts can be arbitrarily long. Shell aliases have a practical limit (ARG_MAX, typically 2MB on Linux), so this is not a hard constraint. However:
|
||||
|
||||
- **Display:** `list` output truncates to 60 chars + `...`
|
||||
- **Storage:** Full prompt in `.env` and `.sh` — no truncation
|
||||
- **Interactive edit:** Shows full current value, allows full editing
|
||||
- **Warning:** During creation, if prompt exceeds 500 chars: `warn "System prompt is long (${#prompt} chars) — consider keeping it concise"`
|
||||
|
||||
**Decision:** No artificial length limit.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Core tool (single commit)
|
||||
|
||||
1. Create `bin/pos-ai-alias` with:
|
||||
- Shebang, strict mode, `common.sh` source, `menu-lib.sh` source
|
||||
- `# POS:` header + `# POS_SUBCMDS:`
|
||||
- `CONFIG_FILE` and `ALIASES_SH_FILE` path constants
|
||||
- `_load_aliases()` — reads `.env` into parallel arrays (names, providers, sessions, prompts)
|
||||
- `_find_alias()` — lookup by name, returns index
|
||||
- `_write_env_file()` — writes entire `.env` from arrays
|
||||
- `_regen_aliases()` — reads `.env`, writes `.sh` with proper escaping
|
||||
- `_list_aliases()` — non-interactive table output
|
||||
- `_show_alias()` — non-interactive single alias details
|
||||
- `_create_alias()` — interactive create with validation
|
||||
- `_edit_alias()` — interactive edit with field-level prompts
|
||||
- `_remove_alias()` — interactive remove with confirm
|
||||
- `_main_menu()` — interactive menu via `menu_run`
|
||||
- Subcommand dispatch (`case` pattern)
|
||||
- `usage()` function
|
||||
2. Add `ai-alias` to `INTERACTIVE_CMDS` in `bin/pos`
|
||||
3. Add `.bashrc` source line to `postinstall.sh`
|
||||
|
||||
### Phase 2: Verification
|
||||
|
||||
1. `chmod +x bin/pos-ai-alias`
|
||||
2. `bash -n bin/pos-ai-alias`
|
||||
3. `make gen` — regenerate tables (new tool appears in dispatch table, bin tree, file table)
|
||||
4. `make check` — self-consistency gate
|
||||
5. `make lint` — convention gate (0 FAIL, 0 WARN)
|
||||
6. Manual test: create, list, show, edit, remove aliases; verify `.sh` file is correct
|
||||
7. Source `.bashrc` and verify aliases work
|
||||
|
||||
### Phase 3: Documentation
|
||||
|
||||
1. `DOC/POS.md` — add `ai alias` section (hand-written)
|
||||
2. `DOC/HOWTO.md` — add index row
|
||||
3. `DOC/howto/ai.md` — add aliases section (if ai.md exists; otherwise add to existing ai howto)
|
||||
4. `DOC/AGENT_Context_Project.md` — regenerated by `make gen`; hand-add to Common Tasks table
|
||||
5. Update `AGENT_TODO.md` Done section (dated)
|
||||
|
||||
---
|
||||
|
||||
## Architectural Constraints
|
||||
|
||||
1. **Tool must source `lib/common.sh`** via the fallback chain (not self-contained)
|
||||
2. **Tool must source `lib/menu-lib.sh`** for interactive menus
|
||||
3. **Tool MUST be in `INTERACTIVE_CMDS`** in `bin/pos`
|
||||
4. **Generated `.sh` file must pass `bash -n`** before commit
|
||||
5. **Env-seam:** all file paths use `${CONFIG_DIR:-...}` pattern (already in `common.sh`)
|
||||
6. **`chmod 600`** for `.env`, `chmod 644` for `.sh`
|
||||
7. **No dependency on `pos-ai`** being installed — tool writes config, doesn't run `pos ai`
|
||||
8. **Pipe delimiter** — system prompts must not contain `|`; validated on input
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
- [ ] `bash -n bin/pos-ai-alias` passes
|
||||
- [ ] `shellcheck bin/pos-ai-alias` passes (or only known false positives)
|
||||
- [ ] `make gen` regenerates tables correctly
|
||||
- [ ] `make check` passes (bash -n, exec bits, doc sync, smoke)
|
||||
- [ ] `make lint` passes (0 FAIL, 0 WARN)
|
||||
- [ ] `pos ai alias --help` shows help
|
||||
- [ ] `pos ai alias` shows interactive menu
|
||||
- [ ] Create -> list -> show -> edit -> remove cycle works
|
||||
- [ ] Generated `.sh` file has correct alias syntax
|
||||
- [ ] `bash -n` on generated `.sh` passes
|
||||
- [ ] `.bashrc` source line works (aliases available after source)
|
||||
- [ ] Special chars in system prompt (single quotes, spaces, $) survive round-trip
|
||||
- [ ] Duplicate name is rejected
|
||||
- [ ] Invalid alias name is rejected
|
||||
- [ ] Invalid provider is rejected
|
||||
|
||||
---
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|------------|--------|------------|
|
||||
| Single-quote escaping fails for exotic prompts | Low | High (syntax error in .sh) | `bash -n` check before commit; warn + skip on failure |
|
||||
| User has many aliases -> list becomes long | Low | Low | `menu_pick` already supports filtering |
|
||||
| `.bashrc` source line conflicts with existing alias definitions | Very Low | Medium | Grep-check before adding; line is a conditional source, not an alias definition |
|
||||
| `make lint` rejects the new tool for a convention violation | Low | Low (blocking) | Follow template exactly; deps guards before help; proper header |
|
||||
|
||||
---
|
||||
|
||||
**End of architecture document.**
|
||||
@@ -1,223 +0,0 @@
|
||||
# Architecture: `pos ai alias` — robust activation mechanism
|
||||
|
||||
**Date:** 2026-08-26
|
||||
**Author:** Architect (ox-alpha)
|
||||
**Status:** DECISION_READY (supersedes activation decisions in `AgentsReport/architect/2026-08-26_ai-alias-architecture.md`)
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
| Decision | Choice |
|
||||
|----------|--------|
|
||||
| D1 — Activation artifact | **Option B**: executable wrapper scripts at `~/.local/bin/<name>`, generated from the ENV source of truth. No shell aliases. |
|
||||
| D2 — Migration | Internal `_alias_sync()` runs on every `pos ai alias` invocation; reconciles wrappers ↔ ENV both directions; legacy `ai-aliases.sh` auto-removed when marker-guarded, else manual guidance. `.sh` generation stops entirely (no deprecated shim). |
|
||||
| D3 — Edge cases | Name collisions refused (foreign file or other binary on PATH); empty set → sync deletes all owned wrappers; dead provider → existing `pos-ai:121` error is sufficient; list/show formats unchanged, show gains wrapper path. |
|
||||
| D4 — Scope fence | Only `bin/pos-ai-alias` + small marker-scan addition to `bin/pos-system-uninstall` + docs. NOT: `bin/pos-ai`, ENV format, config-ui, postinstall.sh, menu flows. |
|
||||
| Postinstall gap | The deferred `.bashrc` wiring is closed by **obsolescence**, not implementation — no wiring needed anymore. |
|
||||
|
||||
**Open items:** None blocking. One provisional nuance in D2 (legacy-file heuristic guard) flagged inline.
|
||||
|
||||
---
|
||||
|
||||
## Problem statement
|
||||
|
||||
- Aliases were frozen at source-time: editing provider gemini→openrouter left the stale alias live in running shells → Gemini 429 while `list` showed openrouter (live failure).
|
||||
- Activation required a `.bashrc` source line that postinstall never wired — the feature was broken-by-omission even before staleness.
|
||||
- Non-interactive contexts (cron, scripts, ssh non-login) could never use aliases at all.
|
||||
|
||||
## Evidence base
|
||||
|
||||
- `bin/pos-ai-alias:62-104` — current `_alias_regen()` writes `ai-aliases.sh`; success messages at :308 say "Reload shell: source ~/.bashrc".
|
||||
- `postinstall.sh:80` — PATH export already includes `$HOME/.local/bin` on pos-managed machines (Debian default `~/.profile` also adds it when the dir exists).
|
||||
- `bin/pos-system-uninstall:96,261` — established precedent for marker-managed user-local binaries (`$HOME/.local/bin/pos-ai-hook.sh`): discovery list + removal pass already exist as a pattern.
|
||||
- `bin/pos-ai:658-665, :118-121` — provider resolution flag > env > default; unknown provider yields `err "Unknown provider '$p' — available: …"` (good runtime failure quality, no pos-ai change needed).
|
||||
- Prior architect report D5/D10 chose `.sh` + `.bashrc` wiring; the wiring was never implemented. This report supersedes those two decisions; storage (ENV format) and CRUD UX decisions carry forward unchanged.
|
||||
|
||||
---
|
||||
|
||||
## Decision 1: Activation artifact
|
||||
|
||||
### Options evaluated
|
||||
|
||||
**Option A: keep generated bash aliases + wire `.bashrc` + louder hints + unalias guards**
|
||||
- Advantages: smallest diff; familiar alias UX.
|
||||
- Costs: staleness is *inherent* — the artifact is a snapshot copied into each shell at source time; the live failure (stale gemini alias) can only be mitigated, never eliminated. Requires postinstall `.bashrc` wiring (the deferred gap), reload-hint UX that demonstrably fails ("users miss it"), and per-shell unalias guard logic for a mechanism bash makes awkward to retract.
|
||||
- Risks: cron/scripts/ssh non-interactive shells get nothing. Two truths (env file vs sourced copy) persist forever.
|
||||
- Compatibility impact: none. Operational impact: permanent "did you re-source?" support burden.
|
||||
|
||||
**Option B: executable wrapper scripts at `~/.local/bin/<name>`**
|
||||
- Architecture: ENV stays the single source of truth; tool renders one standalone script per alias:
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Managed by pos ai alias — regenerated automatically; hand-edits are overwritten.
|
||||
# Alias: assist | provider: openrouter | session: assist
|
||||
set -euo pipefail
|
||||
exec pos ai openrouter ask --session assist --system <printf-%q-prompt> "$@"
|
||||
```
|
||||
(empty prompt → omit `--system`; prompt embedded via existing double-layer `printf %q` mechanics, reused from `_alias_quote_cmd`, so it lands as exactly ONE shell word; `"$@"` passes user args through).
|
||||
- Advantages: **staleness eliminated** — next invocation reads current file bytes; no shell integration of any kind (kills the postinstall gap instead of closing it); works identically in interactive shells, scripts, cron, ssh non-login; removal = delete one marker-identified file; no INTERACTIVE_CMDS/tee-pipe interaction changes; content edits bypass `hash` caching entirely (bash caches paths, not contents) and new names are found on first PATH scan.
|
||||
- Costs: PATH-presence dependency (`~/.local/bin` must be on PATH — already guaranteed by `postinstall.sh:80` and Debian default `~/.profile`, but needs a runtime check + guidance); collision policy needed (scripts are filesystem entries, aliases weren't); ~40 lines more logic than Option A.
|
||||
- Risks: name shadows a real binary → mitigated by refusal policy (D3); user hand-edits wrapper → healed by sync regeneration (D2), and the header says so.
|
||||
- Compatibility impact: activation semantics change (documented). Operational impact: self-healing artifacts; zero shell-config coupling. Migration impact: handled by D2.
|
||||
|
||||
**Option C: hybrid — wrappers primary + optional still-generated alias file**
|
||||
- Advantages: covers users attached to aliases.
|
||||
- Costs: keeps the stale-snapshot mechanism alive alongside the fix — two activation paths, two truth-drift surfaces, double the validation matrix. Directly contradicts the motivation ("more robust" = fewer failure modes, not one more).
|
||||
- Risks: the exact reported bug remains reachable through the optional path.
|
||||
|
||||
### Decision: Option B
|
||||
|
||||
Staleness was an architectural property of source-time snapshots, not an implementation bug — no amount of hints or guards fixes Option A. Option B removes the class of bug (artifact always equals source of truth at invocation time) and deletes the deferred `.bashrc` wiring requirement rather than implementing it. Option C preserves the bug class for zero new capability. Smallest robust design wins.
|
||||
|
||||
**Artifact specification (binding for Builder):**
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Location | `${HOME}/.local/bin/<name>` |
|
||||
| Permissions | `0755` |
|
||||
| Ownership marker | Line 2 contains literal `Managed by pos ai alias` (grep target for all ownership checks) |
|
||||
| Body | `set -euo pipefail` + single `exec pos ai <provider> ask --session <session>[ --system <%q prompt>] "$@"` |
|
||||
| Quoting | Reuse `_alias_quote_cmd` verbatim (double-layer `%q` mechanics preserved per brief) |
|
||||
| Atomic write | mktemp in same dir → `mv` → `chmod 755` (same pattern as current `_alias_regen`) |
|
||||
| Pre-commit validation | `bash -n` on rendered wrapper; on failure warn + keep previous file |
|
||||
| Secrets | None inside (prompt is content, not credential) |
|
||||
|
||||
[DECIDED]
|
||||
|
||||
## Decision 2: Migration & back-compat
|
||||
|
||||
### Regeneration trigger: `_alias_sync()` on every invocation
|
||||
|
||||
New internal function, called at the top of **every** subcommand dispatch entry (`create`, `edit`, `remove`, `list`, `show`, interactive menu) before the subcommand's own logic. It reconciles `~/.local/bin` against the ENV file in both directions:
|
||||
|
||||
1. For each ENV entry: render expected wrapper content; if target is missing **or differs byte-wise** → atomically install. This means:
|
||||
- first run after upgrade materializes wrappers for all existing aliases (one-time migration happens on any command, including a harmless `list`);
|
||||
- every create/edit/remove leaves artifacts consistent by construction;
|
||||
- hand-edited or half-deleted wrappers are silently healed (idempotent, cheap for realistic alias counts).
|
||||
2. Every executable in `~/.local/bin` bearing our marker whose name is **not** in ENV → deleted (covers remove, covers manual ENV edits, covers the empty-set case).
|
||||
3. If ≥1 wrapper exists/installed and `$HOME/.local/bin` is absent from `$PATH` → loud `warn` with copy-paste fix (`export PATH="$HOME/.local/bin:$PATH"` + persist to `~/.profile`). Wrappers are still written regardless.
|
||||
4. Legacy handling (below).
|
||||
|
||||
No public `sync` subcommand: every subcommand already syncs, so an explicit one adds surface without capability. `POS_SUBCMDS` header stays `create edit remove list show`.
|
||||
|
||||
### Existing `ai-aliases.sh`: stop writing entirely — no deprecated shim
|
||||
|
||||
A shim keeps two activation truths alive, and the sourced-alias-still-shadows-wrapper scenario is precisely the reported failure mode (in interactive bash, aliases take precedence over PATH lookups). The `.sh` artifact must die, not fade.
|
||||
|
||||
On detecting `SH_FILE`, sync emits a warning block explaining that activation moved to `~/.local/bin/<name>` scripts and that stale sourced aliases shadow them until cleaned. Then:
|
||||
|
||||
- **If line 1–3 of the file carry our generator marker** (`Auto-generated by pos ai alias`) → auto-remove the file and print remediation for *running* shells: an `unalias <names>` line with names extracted from the `.sh` contents themselves (the stale file inventories its own definitions — including names no longer in ENV), plus "or simply start a new shell". Auto-remove is safe because (a) the file is regenerable output, not user data, (b) postinstall never shipped the source line, so nothing references it at startup, and (c) the conditional-source idiom (`[ -f ] && source`) tolerates absence even if a user wired it manually.
|
||||
- **If the marker does not match** (foreign/hand-built file) → leave untouched; advise manual review. Never delete files we didn't generate — same policy as wrapper collisions.
|
||||
|
||||
*[PROVISIONAL nuance]* The auto-remove guard currently checks only the generator-marker header; a user who appended private aliases into our generated file would lose them on upgrade-migration. Accepted risk: the file header says "do not hand-edit", likelihood is low, and the alternative (parsing full-file provenance) buys complexity the requirement doesn't need. Revisit only if a real case appears.
|
||||
|
||||
### What carries over unchanged
|
||||
|
||||
- `ai-aliases.env` format, location, chmod 600, comment conventions — untouched. Previously created aliases migrate with zero data conversion.
|
||||
- `pos config` compatibility: no env-key semantics touched.
|
||||
- All menu flows, name regex, non-tty guard behavior.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
## Decision 3: Edge cases & subcommand semantics
|
||||
|
||||
### Name collisions with real binaries — refuse
|
||||
|
||||
Create-time check order (after existing ENV-duplicate redirect to `edit`):
|
||||
|
||||
1. `$HOME/.local/bin/<name>` exists **with** marker → not a collision; sync will overwrite (regeneration path).
|
||||
2. `$HOME/.local/bin/<name>` exists **without** marker → refuse: `err "File '~/.local/bin/<name>' already exists and was not created by pos ai alias — pick another name"`. Never silently overwrite foreign files.
|
||||
3. `command -v <name>` resolves to anything else on PATH (`ls`, `git`, `gcc`, …) → refuse with the conflicting path named.
|
||||
|
||||
No override flag. Shadowing an arbitrary binary is never a legitimate intent for an *alias* feature, a refusal error costs one rename, and a `--force` surface invites exactly the "surprise factor" this rework is meant to remove. Edit cannot collide (name is the record key); rename remains remove+create (Designer out-of-scope list already excludes renaming).
|
||||
|
||||
### Empty result set → wrappers fully retracted
|
||||
|
||||
With zero ENV entries, sync deletes every marker-bearing wrapper in `~/.local/bin`. `list` prints `Aliases (0):` as today. No empty husks left behind.
|
||||
|
||||
### Provider adapter deleted later → runtime failure is already good enough
|
||||
|
||||
Wrapper execs `pos ai <provider> …`; if the adapter vanished, `bin/pos-ai:121` errors: `Unknown provider '<p>' — available: gemini openrouter`. Actionable, names valid alternatives, zero changes to `pos-ai`. Sync does **not** prune wrappers whose provider directory entry disappeared (ENV is truth for existence; a temporarily missing adapter shouldn't silently eat user config). The provider picker at edit time only offers installed providers, so edit is the natural repair path.
|
||||
|
||||
### Subcommand semantics under Option B
|
||||
|
||||
| Subcommand | Change |
|
||||
|------------|--------|
|
||||
| `list` | Format unchanged; runs after sync so it always reflects disk truth |
|
||||
| `show <name>` | Adds one line: `Wrapper: ~/.local/bin/<name>` (or `(not installed)` if PATH check failed) |
|
||||
| `create` | Gains collision refusals above; success message replaces "Reload shell: source ~/.bashrc" with `Available immediately: ~/.local/bin/<name>` (+ PATH warning when applicable) |
|
||||
| `edit` | Unchanged flow; on save, sync refreshes the wrapper — change is live on next invocation (this kills the reported bug) |
|
||||
| `remove` | Unchanged confirm(default=n); success message notes the script was deleted from `~/.local/bin`; add hint that running shells may need `hash -r` only if the name still autocompletes stale (rare; bash normally re-scans when a hashed file vanishes) |
|
||||
| menu / `-h` | Help text updated: activation = executable scripts in `~/.local/bin`, no sourcing required |
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 4: Scope fence for Builder
|
||||
|
||||
### Approved outcome
|
||||
Alias activation via marker-managed wrapper scripts in `~/.local/bin`, synced against `ai-aliases.env` on every invocation, with legacy `.sh` auto-retirement.
|
||||
|
||||
### In-scope components/files
|
||||
| File | Allowed changes |
|
||||
|------|-----------------|
|
||||
| `bin/pos-ai-alias` | Replace `_alias_regen()` with `_wrapper_path()` + `_wrapper_render()` + `_alias_sync()`; keep `_alias_quote_cmd` mechanics verbatim; add `_alias_check_path()`; wire sync into all dispatch entries; collision checks in `_alias_create`; message deltas in create/edit/remove/show/usage; legacy `.sh` retirement block; SH_FILE constant retained solely for migration detection |
|
||||
| `bin/pos-system-uninstall` | Add marker-scan of `~/.local/bin` (grep for `Managed by pos ai alias`) to the discovery list (~line 96 area) and removal pass (~line 261 area), mirroring the existing `pos-ai-hook.sh` pattern — closes uninstall hygiene |
|
||||
| `DOC/POS.md` | `ai alias` section: activation semantics, subcommand table unchanged otherwise (hand-maintained file) |
|
||||
| `DOC/HOWTO.md` / relevant howto | Row/section wording update if it mentions sourcing/reload |
|
||||
| `AGENT_TODO.md` | Move task to Done (dated) in same commit |
|
||||
|
||||
### Must NOT change
|
||||
- `bin/pos-ai` — any file byte.
|
||||
- `ai-aliases.env` format, fields, header comments, chmod 600.
|
||||
- `lib/common.sh`, `lib/menu-lib.sh`, `lib/config-ui.sh`.
|
||||
- `postinstall.sh` — the deferred `.bashrc` wiring stays unimplemented by design (obsoleted, not added).
|
||||
- `# POS:` / `# POS_SUBCMDS:` headers (description and subcommand set unchanged → no gen churn beyond none).
|
||||
- Menu structure, step counts, name regex, non-tty guard behavior (Designer spec remains authoritative).
|
||||
- Other categories' tools; entertainment plugins; completions (no flag/subcmd changes).
|
||||
|
||||
### Architectural constraints
|
||||
1. Atomic writes only (mktemp+mv), never in-place truncation of live wrappers.
|
||||
2. Ownership established exclusively via the line-2 marker string; never delete/overwrite files failing the marker test.
|
||||
3. All output discipline per Designer spec: tables/results stdout, display/warnings stderr (`log`/`warn`/`err`).
|
||||
4. Sync must be idempotent and safe to run concurrently-lossy (single-user tool: last write wins, no locking).
|
||||
5. Wrapper body contains no secrets and no absolute paths except the `pos` lookup by name (PATH-resolved, consistent with old aliases).
|
||||
|
||||
### Required verification (adversarial where it matters)
|
||||
1. `bash -n bin/pos-ai-alias`; `make gen && make check && make lint` ending `0 FAIL, 0 WARN`.
|
||||
2. **Quoting round-trip through the NEW artifact**: prompts containing `'`, `"`, backtick, `$()`, `%`, `\`, unicode, leading/trailing spaces → create each; execute wrapper under a stubbed `pos` shim on a temp PATH capturing argv; assert `--system` arrives as exactly one intact word and passthrough args (`assist "hi there"`) append correctly.
|
||||
3. **Staleness kill-test**: create `assist`(gemini) → run wrapper via shim → edit provider→openrouter → run again → argv shows openrouter with **no shell reload** (the regression test for the live failure).
|
||||
4. Sync idempotency: two consecutive runs → byte-identical artifacts, mtimes stable second run.
|
||||
5. Orphan retraction: delete an ENV line manually → next `pos ai alias list` removes that wrapper; empty ENV → zero owned wrappers remain.
|
||||
6. Collision tests: foreign file at `~/.local/bin/<name>` → refused; marker file → refreshed; `command -v` conflict (e.g. `gcc`) → refused with path named.
|
||||
7. Legacy migration: plant prior-generator-format `ai-aliases.sh` with stale `alias assist=…gemini…` → any subcommand removes it, prints `unalias assist` remediation; plant foreign-content file → untouched, warned.
|
||||
8. PATH-absent: strip `$HOME/.local/bin` from PATH → loud warn, wrappers still written.
|
||||
9. Non-tty: `pos ai alias` (menu) still fails cleanly via `menu_guard`.
|
||||
10. Dead-provider runtime: wrapper pointing at removed adapter produces `pos-ai:121` available-providers error (assert message quality manually once).
|
||||
|
||||
### Explicitly out of scope
|
||||
Rename operation; multi-line prompt input; alias import/export; public `sync` subcommand; completion headers; `pos config` integration; systemd/cron integration examples beyond help text.
|
||||
|
||||
### Open risks
|
||||
- Users who sourced `ai-aliases.sh` into `.bashrc` manually keep a dead reference — harmless under the conditional-source idiom; warning text covers it.
|
||||
- `~/.local/bin` absent from PATH in exotic shells (non-login ssh without postinstall) — mitigated by persistent warning + fix line.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Builder-ready step order
|
||||
|
||||
1. Core rewrite in `bin/pos-ai-alias`: `_wrapper_path`, `_wrapper_render` (reuse `_alias_quote_cmd`; marker line 2; `set -euo pipefail`; `exec … "$@"`), `_alias_check_path`, `_alias_sync` (render-diff-install, orphan sweep, legacy block). Delete `_alias_regen` body (keep SH_FILE constant for migration).
|
||||
2. Wire `_alias_sync` into every dispatch entry before subcommand logic.
|
||||
3. Create-flow collision refusals (marker-aware, `command -v` check) + success-message swap ("Available immediately", drop reload hints everywhere including menu flows).
|
||||
4. Edit/remove/show/list/usage deltas per D3 table.
|
||||
5. `bin/pos-system-uninstall`: marker-scan additions in discovery + removal passes.
|
||||
6. Full verification suite (D4 list) — quoting round-trip and staleness kill-test are the acceptance gates.
|
||||
7. Docs (`DOC/POS.md`, HOWTO row) + `AGENT_TODO.md` Done entry; conventional commit (`feat:` or `fix:`).
|
||||
|
||||
Recommended next agent: **Builder** — scope is fully determined; no architectural choices remain. Suggest a Reviewer pass afterward focused on the marker-guard logic (the only place where a bug could delete/overwrite a foreign file).
|
||||
|
||||
Architect changes: this report only.
|
||||
@@ -1,808 +0,0 @@
|
||||
# Architect Report — Self-Describing Command Registry for POS
|
||||
|
||||
**Date:** 2026-08-26
|
||||
**Status:** DECISION_READY
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Decision:** Add a thin `lib/registry.sh` library that parses `# POS_*:` headers into a queryable API; two new optional headers (`# POS_DEPS:`, `# POS_EXAMPLES:`) extend existing conventions; no framework, no new abstraction layer.
|
||||
- **Key insight:** The existing header system is already 80% of a registry. The missing piece is a shared parsing library so every consumer stops reimplementing `sed` + `grep` header reading.
|
||||
- **Scope:** `lib/registry.sh` (new), `scripts/gen-docs.sh` (extend), `bin/pos-tree` (use registry), `templates/pos-tool.sh` (document new headers), docs. All existing tools keep working unchanged.
|
||||
- **Migration:** Zero-downtime — new headers are optional. Tools add `# POS_DEPS:` and `# POS_EXAMPLES:` incrementally. Registry degrades gracefully when headers are absent.
|
||||
- **Open items:** `pos help` and `pos menu` are future consumers (not in this phase). Dashboard is out of scope.
|
||||
|
||||
---
|
||||
|
||||
## Decision 1: Header Format — Extend, Don't Replace
|
||||
|
||||
### Problem
|
||||
|
||||
The spec asks for dependencies, curated examples, and a shared query API. Currently, each consumer (`pos tree`, `pos <category> --help`, `gen-docs.sh`, `pos config`) independently implements `sed`/`grep` header parsing with its own logic.
|
||||
|
||||
### Decision
|
||||
|
||||
Add two new optional `# POS_*:` header lines. Keep all existing headers unchanged.
|
||||
|
||||
### Exact Syntax
|
||||
|
||||
```bash
|
||||
# POS_DEPS: <binary1> [binary2 ...]
|
||||
# POS_EXAMPLES: <command> | <description>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- All new headers are **optional** — tools that don't declare them simply won't expose that metadata. Progressive metadata preserved.
|
||||
- `# POS_DEPS:` is space-separated binary names (what `command -v` checks, not apt package names).
|
||||
- `# POS_EXAMPLES:` can appear on multiple lines — each is `<command> | <description>` (pipe-delimited, max one `|`).
|
||||
- Headers must appear between the shebang/strict-mode block (lines 1–6) and the first non-comment line.
|
||||
- Gen-docs and lint only parse headers from the first ~10 lines of each file.
|
||||
|
||||
**Existing headers (unchanged):**
|
||||
|
||||
```bash
|
||||
# POS: <category> <command> — <description>
|
||||
# POS_FLAGS: --flag1 --flag2
|
||||
# POS_SUBCMDS: sub1 sub2 sub3
|
||||
# POS_CONFIG: <scope> | <env-file> | <KEY>=<flags>:<desc> | ...
|
||||
```
|
||||
|
||||
**New headers (optional):**
|
||||
|
||||
```bash
|
||||
# POS_DEPS: docker nmap jq
|
||||
# POS_EXAMPLES: pos network scan 192.168.1.0/24 | Scan a /24 CIDR
|
||||
# POS_EXAMPLES: pos network scan 10.0.0.0/28 --full | Full scan with OS detection
|
||||
```
|
||||
|
||||
### Example: Minimal Tool (No Change Needed)
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: ssh load-keys — Load all SSH keys into the agent
|
||||
# ... rest of script
|
||||
```
|
||||
|
||||
### Example: Rich Tool
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: network download — aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits)
|
||||
# POS_SUBCMDS: start stop status add torrent metalink list info files peers pause resume remove purge move limit set watch restart retry replace menu
|
||||
# POS_FLAGS: --dir --out --split --seed --force --upload --gid --tmux
|
||||
# POS_DEPS: aria2c jq curl
|
||||
# POS_EXAMPLES: pos network download add https://example.com/file.zip | Download a file
|
||||
# POS_EXAMPLES: pos network download status | Show download queue status
|
||||
# POS_EXAMPLES: pos network download --tmux start aria2 daemon with live view
|
||||
```
|
||||
|
||||
### Rationale
|
||||
|
||||
| Option | Architecture | Advantages | Costs | Risks | When to choose |
|
||||
|--------|-------------|------------|-------|-------|----------------|
|
||||
| **A: Extend existing headers** | Add `# POS_DEPS:` and `# POS_EXAMPLES:` alongside existing headers | Backward compatible, zero migration cost, progressive metadata, consistent with existing patterns | Two new header formats to parse | Low — optional headers degrade gracefully | **Chosen** — smallest sufficient design |
|
||||
| B: Unified YAML frontmatter | Replace all `# POS_*:` with a YAML block in each file | Richer structure, easier to extend | Breaks all existing consumers, requires migration of 40+ tools, adds YAML dependency | High — YAML parser availability in bash, migration burden | Only if the header system were fundamentally inadequate |
|
||||
| C: Separate registry file | `registry/<tool>.yaml` per tool | Clean separation, richer metadata | Duplicates what headers already provide, extra files to maintain, sync risk between header and registry | Medium — source of truth drift | Only if headers couldn't hold the metadata |
|
||||
|
||||
**Option A wins** because the existing header system already works, is already the source of truth for gen-docs output, and the new metadata (deps, examples) fits naturally into the comment-header format.
|
||||
|
||||
### Constraints for Builder
|
||||
|
||||
- `# POS_DEPS:` line: `sed -n '/^# POS_DEPS: /{s/^# POS_DEPS: //;p;q}' <file>` — space-separated tokens.
|
||||
- `# POS_EXAMPLES:` lines: `grep '^# POS_EXAMPLES:' <file | sed 's/^# POS_EXAMPLES:[[:space:]]*//'` — one per line, `|`-delimited command|description.
|
||||
- The `# POS:` header line **must remain the first metadata line** after shebang/strict-mode. New headers go after existing headers, before any code.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 2: Registry Library — `lib/registry.sh`
|
||||
|
||||
### Problem
|
||||
|
||||
Four consumers independently parse tool headers with their own `sed`/`grep` patterns:
|
||||
- `bin/pos` `_pos_category_help()` (lines 68–123): reads `# POS:` and `# POS_SUBCMDS:` per tool
|
||||
- `bin/pos-tree` (lines 47–68): reads `# POS:` and `# POS_SUBCMDS:` per tool
|
||||
- `scripts/gen-docs.sh` (lines 30–46): reads `# POS:`, `# POS_FLAGS:`, `# POS_SUBCMDS:`, `# POS_CONFIG:` per tool
|
||||
- `lib/config-ui.sh` (lines 50–57): reads `# POS_CONFIG:` per tool
|
||||
|
||||
Each reimplements the same header-reading pattern. Adding new headers means updating every consumer.
|
||||
|
||||
### Decision
|
||||
|
||||
Create `lib/registry.sh` — a thin library (target ~180 lines) that provides a shared API for querying tool metadata from `# POS_*:` headers.
|
||||
|
||||
### Data Structures
|
||||
|
||||
All data lives in bash associative arrays and indexed arrays, populated by a single `reg_scan` call.
|
||||
|
||||
```bash
|
||||
# Indexed array — all tool keys, sorted (LC_ALL=C)
|
||||
declare -a _reg_tools=()
|
||||
|
||||
# Associative arrays — keyed by tool key (e.g., "network-download", "config")
|
||||
declare -A _reg_cat=() # tool → category ("" for category-less)
|
||||
declare -A _reg_desc=() # tool → description (text after "— ")
|
||||
declare -A _reg_flags=() # tool → raw POS_FLAGS value
|
||||
declare -A _reg_subcmds=() # tool → raw POS_SUBCMDS value
|
||||
declare -A _reg_deps=() # tool → raw POS_DEPS value
|
||||
declare -A _reg_examples=() # tool → newline-joined POS_EXAMPLES lines
|
||||
|
||||
# Config is special: multiple headers per tool, multiple fields per header.
|
||||
# Stored as pipe-delimited lines keyed by scope (not tool).
|
||||
declare -a _reg_config_scopes=() # unique scope names, sorted
|
||||
declare -A _reg_config_keys=() # scope → newline-joined key|flags|desc lines
|
||||
```
|
||||
|
||||
**Why not store everything in one mega-array?** Bash associative arrays can't hold structured records. Separate arrays per field keep lookups O(1) and code readable.
|
||||
|
||||
### Tool Key Convention
|
||||
|
||||
Tool keys match the existing filename convention:
|
||||
- `bin/pos-network-download` → key `network-download`, category `network`
|
||||
- `bin/pos-config` → key `config`, category `""` (category-less)
|
||||
- `bin/pos-ai-alias` → key `ai-alias`, category `ai`
|
||||
|
||||
### API
|
||||
|
||||
```bash
|
||||
# ── Initialization ──────────────────────────────────────────────
|
||||
|
||||
reg_scan [dir]
|
||||
Scan all pos-* files in dir (default: auto-detect from BASH_SOURCE).
|
||||
Populates _reg_tools, _reg_cat, _reg_desc, _reg_flags, _reg_subcmds,
|
||||
_reg_deps, _reg_examples, _reg_config_scopes, _reg_config_keys.
|
||||
Must be called before any other reg_* function.
|
||||
Uses LC_ALL=C for deterministic sort.
|
||||
|
||||
# ── Discovery ───────────────────────────────────────────────────
|
||||
|
||||
reg_list
|
||||
Echo sorted list of all tool keys, one per line.
|
||||
|
||||
reg_categories
|
||||
Echo sorted unique category names (empty string for category-less tools).
|
||||
|
||||
reg_tools_in <category>
|
||||
Echo sorted tool keys belonging to <category>.
|
||||
Pass "" for category-less tools.
|
||||
|
||||
# ── Lookup ──────────────────────────────────────────────────────
|
||||
|
||||
reg_lookup <tool> <field>
|
||||
Echo a field's value for a tool. Fields:
|
||||
cat, desc, flags, subcmds, deps, examples
|
||||
Returns empty string if field not set or tool not found.
|
||||
Exit code: 0 if tool found, 1 if not.
|
||||
|
||||
reg_config_scopes
|
||||
Echo sorted list of unique config scope names.
|
||||
|
||||
reg_config_keys <scope>
|
||||
Echo key|flags|description lines for a scope (newline-delimited).
|
||||
|
||||
reg_config_envfile <scope>
|
||||
Echo the env-file basename for a scope.
|
||||
Exit code: 0 if found, 1 if not.
|
||||
|
||||
# ── Iteration ───────────────────────────────────────────────────
|
||||
|
||||
reg_each <callback>
|
||||
Call <callback> for each tool, passing:
|
||||
<callback> <category> <tool_key> <description>
|
||||
Category is empty for category-less tools.
|
||||
|
||||
# ── Convenience (for common patterns) ──────────────────────────
|
||||
|
||||
reg_tool_exists <tool>
|
||||
Exit 0 if tool is registered, 1 otherwise.
|
||||
|
||||
reg_tools_for_category <category>
|
||||
Alias for reg_tools_in. Kept for clarity.
|
||||
```
|
||||
|
||||
### Source Pattern
|
||||
|
||||
```bash
|
||||
# lib/registry.sh — no shebang (library, not executable)
|
||||
# Sourced opt-in by consumers that need tool metadata.
|
||||
|
||||
# Common.sh helpers (guarded fallback — mirrors lib/config-ui.sh pattern)
|
||||
declare -F log >/dev/null || log() { echo "[+] $*"; }
|
||||
declare -F warn >/dev/null || warn() { echo "[!] $*"; }
|
||||
declare -F err >/dev/null || err() { echo "ERROR: $*" >&2; exit 1; }
|
||||
|
||||
# ── Tool directory detection ────────────────────────────────────
|
||||
# Repo: lib/registry.sh → ../bin
|
||||
# Install: /usr/local/bin/registry.sh → /usr/local/bin (same dir)
|
||||
_reg_tools_dir() {
|
||||
local dir
|
||||
dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../bin" 2>/dev/null && pwd)"
|
||||
if [ -d "$dir" ] && ls "$dir"/pos-* &>/dev/null; then
|
||||
echo "$dir"
|
||||
else
|
||||
dirname "${BASH_SOURCE[0]}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Data stores ─────────────────────────────────────────────────
|
||||
declare -a _reg_tools=()
|
||||
declare -A _reg_cat=()
|
||||
declare -A _reg_desc=()
|
||||
declare -A _reg_flags=()
|
||||
declare -A _reg_subcmds=()
|
||||
declare -A _reg_deps=()
|
||||
declare -A _reg_examples=()
|
||||
declare -a _reg_config_scopes=()
|
||||
declare -A _reg_config_keys=()
|
||||
|
||||
# ── reg_scan ────────────────────────────────────────────────────
|
||||
reg_scan() {
|
||||
local dir="${1:-$(_reg_tools_dir)}" f
|
||||
local old LC_ALL_PREV="$LC_ALL"
|
||||
export LC_ALL=C
|
||||
|
||||
_reg_tools=()
|
||||
# Clear all associative arrays
|
||||
for key in "${!_reg_cat[@]:-}"; do
|
||||
unset "_reg_cat[$key]" "_reg_desc[$key]" "_reg_flags[$key]"
|
||||
unset "_reg_subcmds[$key]" "_reg_deps[$key]" "_reg_examples[$key]"
|
||||
done
|
||||
_reg_config_scopes=()
|
||||
for scope in "${!_reg_config_keys[@]:-}"; do
|
||||
unset "_reg_config_keys[$scope]"
|
||||
done
|
||||
|
||||
local -A scope_seen=()
|
||||
|
||||
for f in "$dir"/pos-*; do
|
||||
[ -x "$f" ] || continue
|
||||
local name="${f##*/pos-}"
|
||||
local key cat sub
|
||||
if [[ "$name" == *-* ]]; then
|
||||
cat="${name%%-*}"
|
||||
sub="${name#*-}"
|
||||
else
|
||||
cat=""
|
||||
sub="$name"
|
||||
fi
|
||||
key="$sub"
|
||||
|
||||
_reg_tools+=("$key")
|
||||
_reg_cat["$key"]="$cat"
|
||||
|
||||
# POS: — description (text after first "— ")
|
||||
local pos_line
|
||||
pos_line="$(sed -n '/^# POS: /{s/^# POS: //;p;q}' "$f" 2>/dev/null)"
|
||||
_reg_desc["$key"]="${pos_line#*— }"
|
||||
|
||||
# POS_FLAGS:
|
||||
_reg_flags["$key"]="$(sed -n '/^# POS_FLAGS: /{s/^# POS_FLAGS: //;p;q}' "$f" 2>/dev/null)"
|
||||
|
||||
# POS_SUBCMDS:
|
||||
_reg_subcmds["$key"]="$(sed -n '/^# POS_SUBCMDS: /{s/^# POS_SUBCMDS: //;p;q}' "$f" 2>/dev/null)"
|
||||
|
||||
# POS_DEPS:
|
||||
_reg_deps["$key"]="$(sed -n '/^# POS_DEPS: /{s/^# POS_DEPS: //;p;q}' "$f" 2>/dev/null)"
|
||||
|
||||
# POS_EXAMPLES: (may appear multiple times — join with newlines)
|
||||
local examples=""
|
||||
examples="$(sed -n '/^# POS_EXAMPLES: /{s/^# POS_EXAMPLES: //;p}' "$f" 2>/dev/null)"
|
||||
_reg_examples["$key"]="$examples"
|
||||
|
||||
# POS_CONFIG: (may appear multiple lines per file)
|
||||
local line
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] || continue
|
||||
line="${line#*POS_CONFIG:}"
|
||||
local scope="${line%%|*}"
|
||||
scope="${scope// }"
|
||||
[ -n "$scope" ] || continue
|
||||
_reg_config_keys["$scope"]+="${_reg_config_keys[$scope]:+$'\n'}$line"
|
||||
if [ -z "${scope_seen[$scope]:-}" ]; then
|
||||
scope_seen["$scope"]=1
|
||||
_reg_config_scopes+=("$scope")
|
||||
fi
|
||||
done < <(grep '^# POS_CONFIG:' "$f" 2>/dev/null || true)
|
||||
done
|
||||
|
||||
# Sort tools
|
||||
mapfile -t _reg_tools < <(printf '%s\n' "${_reg_tools[@]}" | sort)
|
||||
# Sort config scopes
|
||||
mapfile -t _reg_config_scopes < <(printf '%s\n' "${_reg_config_scopes[@]}" | sort -u)
|
||||
|
||||
export LC_ALL="$LC_ALL_PREV"
|
||||
}
|
||||
|
||||
# ── Discovery ───────────────────────────────────────────────────
|
||||
reg_list() { printf '%s\n' "${_reg_tools[@]}"; }
|
||||
|
||||
reg_categories() {
|
||||
local -A cats=()
|
||||
local t
|
||||
for t in "${_reg_tools[@]}"; do
|
||||
cats["${_reg_cat[$t]}"]=1
|
||||
done
|
||||
printf '%s\n' "${!cats[@]}" | sort
|
||||
}
|
||||
|
||||
reg_tools_in() {
|
||||
local cat="$1" t
|
||||
for t in "${_reg_tools[@]}"; do
|
||||
[ "${_reg_cat[$t]}" = "$cat" ] && echo "$t"
|
||||
done
|
||||
}
|
||||
|
||||
# ── Lookup ──────────────────────────────────────────────────────
|
||||
reg_lookup() {
|
||||
local tool="$1" field="$2"
|
||||
case "$field" in
|
||||
cat) echo "${_reg_cat[$tool]:-}" ;;
|
||||
desc) echo "${_reg_desc[$tool]:-}" ;;
|
||||
flags) echo "${_reg_flags[$tool]:-}" ;;
|
||||
subcmds) echo "${_reg_subcmds[$tool]:-}" ;;
|
||||
deps) echo "${_reg_deps[$tool]:-}" ;;
|
||||
examples) echo "${_reg_examples[$tool]:-}" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
reg_config_scopes() { printf '%s\n' "${_reg_config_scopes[@]}"; }
|
||||
|
||||
reg_config_keys() {
|
||||
local scope="$1"
|
||||
echo "${_reg_config_keys[$scope]:-}"
|
||||
}
|
||||
|
||||
reg_config_envfile() {
|
||||
local scope="$1" line
|
||||
line="$(echo "${_reg_config_keys[$scope]:-}" | head -1)"
|
||||
[ -n "$line" ] || return 1
|
||||
line="${line#*|}" # drop scope
|
||||
local env="${line%%|*}"
|
||||
echo "${env// }"
|
||||
}
|
||||
|
||||
# ── Iteration ───────────────────────────────────────────────────
|
||||
reg_each() {
|
||||
local cb="$1" t
|
||||
for t in "${_reg_tools[@]}"; do
|
||||
"$cb" "${_reg_cat[$t]}" "$t" "${_reg_desc[$t]}"
|
||||
done
|
||||
}
|
||||
|
||||
# ── Convenience ─────────────────────────────────────────────────
|
||||
reg_tool_exists() {
|
||||
[ -n "${_reg_desc[$1]+x}" ]
|
||||
}
|
||||
```
|
||||
|
||||
### Rationale
|
||||
|
||||
| Option | Architecture | Advantages | Costs | Risks | When to choose |
|
||||
|--------|-------------|------------|-------|-------|----------------|
|
||||
| **A: Regenerate-on-source library** | `reg_scan` parses all files into bash arrays on first call | O(1) lookups after scan, no external deps, works from /usr/local/bin, bash-native | ~180 lines of code, scan cost at startup (~5ms for 40 tools) | Low — scan is fast enough for interactive use | **Chosen** — matches project's bash-only, no-framework philosophy |
|
||||
| B: Cached JSON file | `make gen` produces `registry.json`, consumers parse with `jq` | Fast lookups, rich queries | Requires `jq` at runtime (currently a dep, but adds coupling), extra build step, staleness risk | Medium — JSON dependency for all consumers | Only if performance of header parsing became a bottleneck (it won't for 40 tools) |
|
||||
| C: Per-tool .meta files | Each tool has a sidecar `pos-<tool>.meta` | Clean separation, rich format | Duplicates header data, extra files to maintain, sync drift risk | Medium — two sources of truth | Only if headers were fundamentally limited |
|
||||
|
||||
**Option A wins** because:
|
||||
1. Headers are already the source of truth — no sync risk.
|
||||
2. The scan takes ~5ms for 40 tools — no performance concern.
|
||||
3. Works from `/usr/local/bin/` (all libs live there after install).
|
||||
4. Matches the project's "no unnecessary framework" philosophy.
|
||||
5. Follows the pattern established by `lib/config-ui.sh`.
|
||||
|
||||
### Source Chain
|
||||
|
||||
```bash
|
||||
# In any consumer:
|
||||
source "$(dirname "$0")/../lib/registry.sh" 2>/dev/null || source "$(dirname "$0")/registry.sh"
|
||||
reg_scan
|
||||
# ... use reg_list, reg_lookup, etc.
|
||||
```
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 3: Consumer Integration
|
||||
|
||||
### Problem
|
||||
|
||||
Four consumers need to use the registry. Each has different requirements:
|
||||
- `bin/pos-tree` needs tree-building from tool data
|
||||
- `bin/pos` `_pos_category_help()` needs category → tool listing
|
||||
- `scripts/gen-docs.sh` needs all metadata for code generation
|
||||
- `lib/config-ui.sh` needs config scopes and keys
|
||||
|
||||
### Decision
|
||||
|
||||
Migrate consumers to `lib/registry.sh` in this order (highest value first):
|
||||
|
||||
#### 3.1: `bin/pos-tree` (Priority: HIGH)
|
||||
|
||||
**Current state** (lines 47–68): Scans `pos-*` files independently, reads `# POS:` and `# POS_SUBCMDS:` via `sed`.
|
||||
|
||||
**After migration:**
|
||||
```bash
|
||||
source "$(dirname "$0")/../lib/registry.sh" 2>/dev/null || source "$(dirname "$0")/registry.sh"
|
||||
reg_scan
|
||||
|
||||
# Build tree from registry instead of scanning files
|
||||
for tool in $(reg_list); do
|
||||
cat="$(reg_lookup "$tool" cat)"
|
||||
desc="$(reg_lookup "$tool" desc)"
|
||||
deps="$(reg_lookup "$tool" deps)"
|
||||
# ... add to tree, annotate with deps if present
|
||||
done
|
||||
```
|
||||
|
||||
**Enhancement:** When `# POS_DEPS:` is present, show it in the tree view:
|
||||
```
|
||||
├── docker ps # Enhanced container overview (health, IPs, ports, uptime)
|
||||
│ [deps: docker]
|
||||
```
|
||||
|
||||
This is the highest-value consumer change — it proves the registry works at runtime and shows the immediate benefit of new metadata.
|
||||
|
||||
**Implementation note:** Keep the existing `add()` / `render()` tree-building logic. Replace only the data-collection loop (lines 47–68) with registry calls. The tree structure is already correct from filenames + `POS_SUBCMDS`.
|
||||
|
||||
#### 3.2: `scripts/gen-docs.sh` (Priority: HIGH)
|
||||
|
||||
**Current state** (lines 30–46): Collects tools into a pipe-delimited array via direct `sed` calls.
|
||||
|
||||
**After migration:**
|
||||
```bash
|
||||
# In the tools collection loop, use registry for new fields
|
||||
# Keep the existing collection pattern for backward compatibility
|
||||
# (gen-docs.sh has its own sorting and rendering logic)
|
||||
|
||||
# Add deps and examples to the tools array format
|
||||
tools+=("$cat|$sub|$desc|$flags|$subcmds|$deps|$examples")
|
||||
```
|
||||
|
||||
**New gen blocks:**
|
||||
- Add `deps` and `examples` columns to `gen_tree()`, `gen_dispatch()`, `gen_filetable()`.
|
||||
- These become visible in `DOC/AGENT_Context_Project.md` once tools add the new headers.
|
||||
|
||||
**Important:** The existing `gen_*` functions use their own `tools` array (not the registry) because they need specific formatting. The registry provides the raw data; gen-docs formats it. This avoids tight coupling between the generator and the library.
|
||||
|
||||
**Alternative considered:** Have gen-docs source the registry directly. Rejected because gen-docs needs the data in a specific format (pipe-delimited array) and the registry's data structure is an implementation detail. Keeping the data flow explicit (`registry → gen-docs tools array → gen_* functions`) is cleaner.
|
||||
|
||||
#### 3.3: `bin/pos` `_pos_category_help()` (Priority: MEDIUM)
|
||||
|
||||
**Current state** (lines 68–123): Scans `pos-<cat>-*` files, reads `# POS:` and `# POS_SUBCMDS:` per file.
|
||||
|
||||
**After migration:**
|
||||
```bash
|
||||
_pos_category_help() {
|
||||
local cat="$1"
|
||||
source "$(dirname "$0")/../lib/registry.sh" 2>/dev/null || source "$(dirname "$0")/registry.sh"
|
||||
reg_scan
|
||||
|
||||
echo "pos $cat — $cat tools"
|
||||
echo
|
||||
echo "USAGE"
|
||||
echo " pos $cat <command> [args]"
|
||||
echo
|
||||
echo "COMMANDS"
|
||||
for tool in $(reg_tools_in "$cat"); do
|
||||
local desc deps
|
||||
desc="$(reg_lookup "$tool" desc)"
|
||||
deps="$(reg_lookup "$tool" deps)"
|
||||
printf ' %-28s%s' "$tool" "$desc"
|
||||
[ -n "$deps" ] && printf ' [deps: %s]' "$deps"
|
||||
echo
|
||||
# ... subcommands from reg_lookup "$tool" subcmds
|
||||
done
|
||||
echo
|
||||
echo "Run 'pos $cat <command> --help' for details on a command."
|
||||
exit 0
|
||||
}
|
||||
```
|
||||
|
||||
**Trade-off:** This adds a `reg_scan` call every time `pos <category>` runs. For 40 tools, the scan takes ~5ms — negligible for interactive use. If profiling shows this matters, `pos` could cache the scan in a temp file (but this is premature optimization).
|
||||
|
||||
**Alternative considered:** Keep `_pos_category_help()` using direct `sed` (no registry dependency). Rejected because the whole point is to centralize header parsing. The 5ms scan cost is acceptable.
|
||||
|
||||
#### 3.4: `lib/config-ui.sh` (Priority: LOW — separate decision)
|
||||
|
||||
**Current state** (lines 50–203): Has its own header parsing for `# POS_CONFIG:` and `# POS_KEYS:`. The config library is already a mature, working abstraction.
|
||||
|
||||
**Decision: Do NOT integrate config-ui.sh with the registry in this phase.**
|
||||
|
||||
**Rationale:**
|
||||
1. config-ui.sh already works correctly and is well-tested.
|
||||
2. Its parsing is specialized (multi-line key fields, `*plugins` expansion, `*providers` expansion).
|
||||
3. Integrating it with the registry would require the registry to handle all config-ui's edge cases, bloating the library.
|
||||
4. config-ui.sh's `cfg_headers()` / `cfg_scopes()` / `cfg_scope_keys()` API is already the "registry" for config consumers.
|
||||
|
||||
**Future:** When config-ui.sh needs maintenance, it could source the registry for its initial scan. But this is not needed now.
|
||||
|
||||
### Rationale
|
||||
|
||||
| Option | Architecture | Advantages | Costs | Risks | When to choose |
|
||||
|--------|-------------|------------|-------|-------|----------------|
|
||||
| **A: Incremental migration** | Migrate tree + gen-docs first, then pos, defer config-ui | Lowest risk, proves value early, no breaking changes | Some consumers still use direct sed during transition | Low — transition period is harmless | **Chosen** — smallest risky step |
|
||||
| B: Big-bang migration | Rewrite all consumers at once | Consistent from day one | High risk, many things can break, hard to review | High — one bad refactor breaks everything | Only if the codebase were much smaller |
|
||||
| C: No migration — just add the library | Create registry.sh but don't change any consumers | Library exists for future use | No immediate value, consumers still duplicate logic | Low — but pointless | Only if the task were just "create a library" |
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 4: Migration Strategy — Zero-Downtime Incremental
|
||||
|
||||
### Problem
|
||||
|
||||
40+ tools exist. All must keep working. The new headers are optional. No tool should require changes to function.
|
||||
|
||||
### Decision
|
||||
|
||||
**Phase 1 (this implementation):**
|
||||
1. Create `lib/registry.sh` with full API.
|
||||
2. Update `scripts/gen-docs.sh` to parse new headers (degrades gracefully when absent).
|
||||
3. Migrate `bin/pos-tree` to use registry (proves runtime value).
|
||||
4. Update `templates/pos-tool.sh` to document new headers.
|
||||
5. Update `DOC/DEV.md` and `DOC/AGENT_Context_Project.md` with new header format.
|
||||
|
||||
**Phase 2 (future, out of scope):**
|
||||
1. Migrate `bin/pos` `_pos_category_help()` to use registry.
|
||||
2. Enhance `pos help` to show registry metadata (deps, examples).
|
||||
3. Add `# POS_DEPS:` and `# POS_EXAMPLES:` to tools incrementally (start with 3–5 representative tools per category).
|
||||
|
||||
**Phase 3 (future, out of scope):**
|
||||
1. `pos menu` — interactive menu from registry (new tool).
|
||||
2. `pos dashboard` — status dashboard from registry (new tool).
|
||||
|
||||
### Incremental Adoption for Tool Authors
|
||||
|
||||
1. Add `# POS_DEPS: docker jq` to your tool's header block. Done — deps appear in registry.
|
||||
2. Add `# POS_EXAMPLES: pos <tool> <args> | Description` lines. Done — examples appear in registry.
|
||||
3. No other changes required. The tool keeps working exactly as before.
|
||||
4. When you run `make gen`, the new metadata appears in generated docs.
|
||||
|
||||
### Tool Template Update
|
||||
|
||||
`templates/pos-tool.sh` gets new header documentation:
|
||||
|
||||
```bash
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
# TEMPLATE — new `pos` CLI tool
|
||||
#
|
||||
# 1. Copy: cp templates/pos-tool.sh bin/pos-<category>-<command>
|
||||
# 2. Header: add a `# POS:` line right after the shebang/strict-mode
|
||||
# lines (single source of truth for generated docs):
|
||||
# # POS: <category> <command> — one-line description
|
||||
# # POS_FLAGS: --flag1 --flag2 (flag-style tools only)
|
||||
# # POS_SUBCMDS: sub1 sub2 (multi-command tools only)
|
||||
# # POS_DEPS: binary1 binary2 (runtime deps, optional)
|
||||
# # POS_EXAMPLES: pos <tool> <args> | Description (optional)
|
||||
# 3. ...
|
||||
```
|
||||
|
||||
### Lint Gate Update
|
||||
|
||||
`scripts/lint-conventions.sh` gets a new WARN check:
|
||||
- If `# POS_DEPS:` is present, validate that each token looks like a binary name (no spaces, no special chars). This is a soft check — WARN on malformed deps, not FAIL.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 5: Scope Boundaries
|
||||
|
||||
### Approved Scope
|
||||
|
||||
```text
|
||||
In scope:
|
||||
lib/registry.sh — NEW FILE, ~180 lines
|
||||
scripts/gen-docs.sh — Extend tools array, add deps/examples to gen_* functions
|
||||
bin/pos-tree — Migrate to use registry (replace file-scanning loop)
|
||||
templates/pos-tool.sh — Document new headers in template comments
|
||||
DOC/DEV.md — Document new header format and registry usage
|
||||
DOC/AGENT_Context_Project.md — Update line count table for lib/registry.sh, gen blocks updated by make gen
|
||||
scripts/lint-conventions.sh — Optional: WARN for malformed POS_DEPS
|
||||
|
||||
Not in scope:
|
||||
bin/pos _pos_category_help() — Phase 2 (future)
|
||||
pos help enhancements — Phase 2 (future)
|
||||
pos menu (new tool) — Phase 3 (future)
|
||||
pos dashboard (new tool) — Phase 3 (future)
|
||||
Adding POS_DEPS/POS_EXAMPLES to existing tools — Individual tool authors, incremental
|
||||
lib/config-ui.sh integration — Deferred (already works, specialized parsing)
|
||||
completions/pos.bash changes — No new completion data needed (deps/examples aren't completable)
|
||||
install.sh changes — registry.sh is installed with existing lib/* loop
|
||||
```
|
||||
|
||||
### What Builder Must NOT Do
|
||||
|
||||
1. Do NOT add `# POS_DEPS:` or `# POS_EXAMPLES:` to any existing tool in this PR (that's incremental migration, separate commits).
|
||||
2. Do NOT change the `# POS:` header format or the em-dash convention.
|
||||
3. Do NOT change `bin/pos` dispatch logic or `INTERACTIVE_CMDS`.
|
||||
4. Do NOT add any new files beyond `lib/registry.sh`.
|
||||
5. Do NOT modify the completion script (`completions/pos.bash`).
|
||||
6. Do NOT refactor `lib/config-ui.sh` to use the registry.
|
||||
7. Do NOT add a shebang to `lib/registry.sh` (it's a library, not executable).
|
||||
8. Do NOT break `make gen && make check && make lint`.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 6: Risk Analysis
|
||||
|
||||
### Risk 1: `make gen` output changes break `git diff --exit-code` in CI
|
||||
|
||||
**Impact:** HIGH — blocks PRs.
|
||||
|
||||
**Cause:** Adding deps/examples parsing to gen-docs.sh changes the `tools` array format. The gen_* functions that consume this array will produce different output if any tool has the new headers. But since no tools have them yet, the output should be identical.
|
||||
|
||||
**Mitigation:**
|
||||
- The gen_* functions must produce **identical output** when no tools have `# POS_DEPS:` or `# POS_EXAMPLES:` headers.
|
||||
- Test: run `make gen && git diff --exit-code` before committing. Zero diff = safe.
|
||||
- The tools array format change (adding `$deps|$examples` fields) is internal to gen-docs.sh — the gen_* functions that render output must not use the new fields when they're empty.
|
||||
|
||||
### Risk 2: `reg_scan` performance degrades with many tools
|
||||
|
||||
**Impact:** LOW — current tool count is ~40.
|
||||
|
||||
**Cause:** Each tool file is read 4–6 times by `sed` during scan. For 40 tools, this is ~200 process spawns.
|
||||
|
||||
**Mitigation:**
|
||||
- At current scale, scan completes in ~5ms. Even 100 tools would be ~15ms.
|
||||
- If it ever becomes an issue, `reg_scan` could read each file once and parse all headers in a single `awk` pass. This is a future optimization, not needed now.
|
||||
|
||||
### Risk 3: Registry library conflicts with existing sourcing patterns
|
||||
|
||||
**Impact:** MEDIUM — could break tools that source both common.sh and registry.sh.
|
||||
|
||||
**Cause:** `registry.sh` declares guarded fallbacks for `log`, `warn`, `err` (same pattern as `config-ui.sh`). If both are sourced, the second source is a no-op because the functions already exist.
|
||||
|
||||
**Mitigation:**
|
||||
- Use the same guarded-declaration pattern as `config-ui.sh`: `declare -F log >/dev/null || log() { ... }`.
|
||||
- `registry.sh` does NOT define `run`, `spawn`, `confirm`, or any other common.sh functions.
|
||||
- `registry.sh` does NOT call `err` during normal operation — only if `reg_scan` is called with an invalid directory (which won't happen in practice).
|
||||
|
||||
### Risk 4: `bin/pos-tree` migration breaks tree output
|
||||
|
||||
**Impact:** HIGH — visible to users.
|
||||
|
||||
**Cause:** The tree-building logic in `pos-tree` is tightly coupled to the current data collection. Replacing the collection loop might subtly change tree structure.
|
||||
|
||||
**Mitigation:**
|
||||
- Keep the existing `add()`, `render()` functions unchanged.
|
||||
- Replace ONLY the data-collection loop (lines 47–68) with registry calls.
|
||||
- Test: run `bin/pos-tree` before and after, diff the output. Must be identical (for existing headers).
|
||||
- The only visible change should be when a tool has `# POS_DEPS:` — deps appear in the tree.
|
||||
|
||||
### Risk 5: `pos <category>` performance regression
|
||||
|
||||
**Impact:** LOW — adds ~5ms per invocation.
|
||||
|
||||
**Cause:** `_pos_category_help()` would source and call `reg_scan` on every invocation.
|
||||
|
||||
**Mitigation:**
|
||||
- Phase 2 only (not in this implementation).
|
||||
- If needed, cache scan results in a temp file: `reg_scan` writes to `/tmp/.pos-registry-<hash>` and consumers check for freshness. This is premature — implement only if profiling shows a problem.
|
||||
|
||||
### Risk 6: New headers malformed, breaking parsing
|
||||
|
||||
**Impact:** LOW — malformed headers produce empty values, not crashes.
|
||||
|
||||
**Cause:** A tool author writes `# POS_DEPS` (missing colon) or `# POS_EXAMPLES foo bar` (missing pipe).
|
||||
|
||||
**Mitigation:**
|
||||
- `reg_scan` uses strict pattern matching: `sed -n '/^# POS_DEPS: /{...}'`. Missing colon = no match = empty value. Graceful degradation.
|
||||
- Add a lint WARN (not FAIL) for `# POS_DEPS:` lines without space-separated tokens.
|
||||
- Add lint WARN for `# POS_EXAMPLES:` lines without `|` delimiter.
|
||||
- Document the expected format clearly in DEV.md.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Verification Plan
|
||||
|
||||
### Gate 1: Syntax
|
||||
|
||||
```bash
|
||||
bash -n lib/registry.sh # Must pass (no syntax errors)
|
||||
bash -n scripts/gen-docs.sh # Must pass (after modifications)
|
||||
bash -n bin/pos-tree # Must pass (after modifications)
|
||||
```
|
||||
|
||||
### Gate 2: Gen Drift
|
||||
|
||||
```bash
|
||||
make gen && git diff --exit-code # Zero diff (no tools have new headers yet)
|
||||
```
|
||||
|
||||
### Gate 3: Self-Consistency
|
||||
|
||||
```bash
|
||||
make check # Must pass (syntax + exec bits + doc/code sync + dispatch smoke)
|
||||
```
|
||||
|
||||
### Gate 4: Convention Lint
|
||||
|
||||
```bash
|
||||
make lint # Must pass (0 FAIL, 0 WARN)
|
||||
```
|
||||
|
||||
### Gate 5: Functional
|
||||
|
||||
```bash
|
||||
# Registry works standalone
|
||||
bash -c 'source lib/registry.sh; reg_scan; reg_list; reg_lookup docker ps desc'
|
||||
|
||||
# pos tree produces identical output
|
||||
bin/pos-tree > /tmp/tree-before.txt
|
||||
# ... apply changes ...
|
||||
bin/pos-tree > /tmp/tree-after.txt
|
||||
diff /tmp/tree-before.txt /tmp/tree-after.txt # Must be empty
|
||||
|
||||
# pos category help works
|
||||
bin/pos docker --help # Must show docker tools
|
||||
bin/pos network --help # Must show network tools
|
||||
```
|
||||
|
||||
### Gate 6: Regression
|
||||
|
||||
```bash
|
||||
# All existing commands still dispatch
|
||||
bin/pos --help
|
||||
bin/pos docker --help
|
||||
bin/pos network --help
|
||||
bin/pos help network scan
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Guidance for Builder
|
||||
|
||||
### Step-by-step
|
||||
|
||||
1. **Create `lib/registry.sh`** (~180 lines). Start from the API spec in Decision 2. Use `lib/config-ui.sh` as a structural reference for the guarded fallbacks and source pattern.
|
||||
|
||||
2. **Update `scripts/gen-docs.sh`**. In the tools collection loop (lines 30–46):
|
||||
- Add `deps` and `examples` fields to the `tools` array format: `"$cat|$sub|$desc|$flags|$subcmds|$deps|$examples"`
|
||||
- Parse new headers with the same `sed` pattern as existing ones.
|
||||
- In `gen_tree()`, `gen_dispatch()`, `gen_filetable()`: add deps/examples columns ONLY when non-empty. Empty fields = identical output to current.
|
||||
|
||||
3. **Migrate `bin/pos-tree`**. Replace lines 47–68 (the file-scanning loop) with registry calls. Keep `add()`, `render()`, and the rest unchanged. Test that output is identical for existing tools.
|
||||
|
||||
4. **Update `templates/pos-tool.sh`**. Add `# POS_DEPS:` and `# POS_EXAMPLES:` to the header documentation block. Add them after the existing `# POS_FLAGS:` example.
|
||||
|
||||
5. **Update `DOC/DEV.md`**. In "Adding a New CLI Tool → Make it discoverable":
|
||||
- Document the new `# POS_DEPS:` and `# POS_EXAMPLES:` headers.
|
||||
- Explain when to use each (deps: list runtime binaries; examples: show 1–3 representative usages).
|
||||
|
||||
6. **Update `lib/registry.sh` line count** in `DOC/AGENT_Context_Project.md` filetable (the hand-maintained rows above the GEN marker).
|
||||
|
||||
7. **Run gates:** `make gen && make check && make lint`. Verify 0 FAIL, 0 WARN.
|
||||
|
||||
### Critical Constraints
|
||||
|
||||
- `lib/registry.sh` must NOT have a shebang (library, not executable).
|
||||
- `lib/registry.sh` must be added to the `lib_names` list in `install.sh` Phase 2 (line 143).
|
||||
- `reg_scan` must set `LC_ALL=C` for deterministic sort.
|
||||
- gen-docs.sh changes must produce zero diff when no tools have new headers.
|
||||
- `pos-tree` output must be byte-identical before/after migration (for existing headers).
|
||||
|
||||
---
|
||||
|
||||
## Open Questions (for Orchestrator)
|
||||
|
||||
1. Should `pos <category> --help` (in `bin/pos`) be migrated in this phase or deferred to Phase 2? **Recommendation: defer to Phase 2** — lower risk, and the category help is already working.
|
||||
|
||||
2. Should the lint gate enforce that `# POS_DEPS:` tokens are valid binary names? **Recommendation: WARN only, not FAIL** — some deps might be shell builtins or paths, not just binary names.
|
||||
|
||||
3. Should `reg_scan` support a `--cached` mode? **Recommendation: no, not yet** — premature optimization for 40 tools.
|
||||
|
||||
---
|
||||
|
||||
*Report written by Architect agent. Next recommended agent: **Builder** (to implement the approved scope).*
|
||||
@@ -1,572 +0,0 @@
|
||||
# Architecture Report — `pos ai hf` (Hugging Face Model Downloader)
|
||||
|
||||
**Date:** 2026-09-04
|
||||
**Status:** DECISION_READY
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| File location | `bin/pos-ai-hf` | Subcommand of `ai` category — models are AI infrastructure |
|
||||
| Subcommands | `download`, `search`, `list`, `remove` | Core + discovery + local management |
|
||||
| Config scope | `ai` (existing) | No new scope needed — `HF_TOKEN` and `HF_DOWNLOAD_DIR` fit the existing `ai.env` |
|
||||
| Default download dir | `~/.local/share/linux_post_install/ai/models/<repo-id>` | Follows XDG data conventions, matches `SESSION_DIR` parent |
|
||||
| Dependencies | `curl`, `jq` | Already in `preinstall.sh` PACKAGES — no changes |
|
||||
| Progress | curl `--progress-bar` | Native, no extra deps, works for multi-GB files |
|
||||
| Resume | curl `-C -` | Automatic resume on interrupted downloads |
|
||||
| New files created | `bin/pos-ai-hf` (1 file) | Minimal scope — everything else is doc updates |
|
||||
| Open items | HOWTO.md + howto/ai.md updates (Writer task) | Not blocking implementation |
|
||||
|
||||
---
|
||||
|
||||
## Decision 1: File Location — `bin/pos-ai-hf`
|
||||
|
||||
**Problem:** Where does a Hugging Face model downloader live in the `pos` hierarchy?
|
||||
|
||||
**Evidence:**
|
||||
- Existing AI tools: `bin/pos-ai` (692 lines), `bin/pos-ai-alias` (760 lines), `bin/pos-ai-gemini`/`bin/pos-ai-openrouter` (7-line forwarders) — all under the `ai` category
|
||||
- The `ai` category covers AI providers, sessions, models, and assistants
|
||||
- Downloading models is AI infrastructure — it feeds ollama, llama.cpp, and similar local inference tools
|
||||
- The user's explicit context: "download AI models for local inference"
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Architecture | Pros | Cons |
|
||||
|--------|-------------|------|------|
|
||||
| A: `bin/pos-ai-hf` | Subcommand of `ai` category | Consistent with existing AI tool hierarchy; `pos ai hf download` is natural; `pos ai --help` shows it alongside other AI tools | Slightly longer invocation path |
|
||||
| B: `bin/pos-ai-download` | Named after the action, not the provider | Action-first naming | Conflates "AI download" with "HF download"; would need renaming when adding other model sources (e.g., CivitAI, Ollama registry) |
|
||||
| C: `bin/pos-hf` | Own category | Shortest invocation | Breaks `ai` category coherence; HF is not a general tool category |
|
||||
|
||||
**Decision:** Option A — `bin/pos-ai-hf`
|
||||
|
||||
**Reasoning:** HF is a provider/source within the AI domain. The `ai` category already contains provider-specific tools (`pos-ai-gemini`, `pos-ai-openrouter`). Adding `pos-ai-hf` for model downloading fits this pattern perfectly. The tool name communicates both the domain (`ai`) and the source (`hf`).
|
||||
|
||||
**Convention compliance:**
|
||||
- `# POS: ai hf — Download AI models from Hugging Face (search, download, manage)`
|
||||
- No new category in `pos --help`
|
||||
- Auto-discovered by `pos` dispatcher
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 2: Subcommands
|
||||
|
||||
**Problem:** What operations should `pos ai hf` support?
|
||||
|
||||
**Evidence:**
|
||||
- `pos media grab` pattern (file:83-141): thin classifier + delegator — minimal surface area
|
||||
- `pos network download` (18 subcommands): comprehensive but for a complex download manager with queuing, torrents, retry logic
|
||||
- `pos ai` pattern (file:1-10): flag-based with subcommands (`ask`, `chat`, `sessions`, `models`, `providers`)
|
||||
- User goal: "Download AI models for local inference" — primary operation is download; search and local management are secondary
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Subcommands | Pros | Cons |
|
||||
|--------|-------------|------|------|
|
||||
| A: download + search + list + remove | 4 subcommands | Full lifecycle; covers discovery, download, local management, cleanup | More surface area to maintain |
|
||||
| B: download + list + remove | 3 subcommands | Core + local management; search can be done via `curl` manually | User must leave `pos` for discovery |
|
||||
| C: download only | 1 subcommand | Minimal; simplest to implement and maintain | No local management; user must track paths manually |
|
||||
|
||||
**Decision:** Option A — `download`, `search`, `list`, `remove`
|
||||
|
||||
**Reasoning:**
|
||||
- `download` is the primary operation (user goal)
|
||||
- `search` is low-cost to implement (one API call, jq formatting) and high-value for discovery
|
||||
- `list` shows what's already downloaded — essential for a model management workflow
|
||||
- `remove` lets users clean up without manually tracking paths
|
||||
- Total surface area is manageable — each subcommand is a single function, not a complex state machine
|
||||
|
||||
**Subcommand contracts:**
|
||||
|
||||
```
|
||||
pos ai hf download <repo-id> [filename] # Download a file or entire repo
|
||||
pos ai hf download <repo-id> --files # List files, then download selected
|
||||
pos ai hf download <repo-id> --branch <rev> # Download from a specific branch/commit
|
||||
pos ai hf download <repo-id> --gguf # Download only .gguf files (inference-ready)
|
||||
pos ai hf search <query> # Search HF models
|
||||
pos ai hf list # List downloaded models
|
||||
pos ai hf remove <repo-id> # Remove a downloaded model
|
||||
```
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 3: Config Scope — Extend `ai.env`
|
||||
|
||||
**Problem:** Where do `HF_TOKEN` and `HF_DOWNLOAD_DIR` live?
|
||||
|
||||
**Evidence:**
|
||||
- `# POS_CONFIG:` header format: `scope | file | KEY=:description | ...`
|
||||
- Existing `ai` scope: `bin/pos-ai` line 6 — `# POS_CONFIG: ai | ai.env | AI_PROVIDER=…`
|
||||
- HF download is an AI tool — its config logically belongs with other AI config
|
||||
- Creating a new `hf` scope would add another `pos config` entry and `.env` file for just 2 keys
|
||||
- The `pos config ai` command already exists and users would expect AI-related config there
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Architecture | Pros | Cons |
|
||||
|--------|-------------|------|------|
|
||||
| A: Extend `ai.env` (existing scope) | `HF_TOKEN` and `HF_DOWNLOAD_DIR` added to `# POS_CONFIG: ai` in `bin/ai-hf` | One config location for all AI tools; user runs `pos config ai` to see everything | Mixes provider keys (GEMINI_API_KEY) with download config |
|
||||
| B: New `hf.env` (new scope) | `# POS_CONFIG: hf | hf.env | HF_TOKEN=…` | Clean separation; `pos config hf` is self-contained | Another `.env` file; users must know which scope has the token |
|
||||
| C: `system.env` (shared scope) | `HF_TOKEN` and `HF_DOWNLOAD_DIR` in system.env via `load_system_env()` | Centralizes shared config | Wrong semantic — HF is AI-specific, not system-wide |
|
||||
|
||||
**Decision:** Option A — Extend existing `ai` scope
|
||||
|
||||
**Reasoning:**
|
||||
- The `ai` scope already holds `AI_PROVIDER` and `AI_GEMINI_API_KEY` — adding HF keys keeps all AI config in one place
|
||||
- Users run `pos config ai` once to configure everything they need for AI tools
|
||||
- No new scope registration, no new `.env` file, no new completion entry
|
||||
- The `# POS_CONFIG:` header in `bin/pos-ai-hf` adds its keys to the same `ai` scope
|
||||
|
||||
**Header addition (in `bin/pos-ai-hf`):**
|
||||
```bash
|
||||
# POS_CONFIG: ai | ai.env | HF_TOKEN=:Hugging Face API token (https://huggingface.co/settings/tokens) (secret) | HF_DOWNLOAD_DIR=:Model download directory (default ~/.local/share/linux_post_install/ai/models)
|
||||
```
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 4: Download Directory Layout
|
||||
|
||||
**Problem:** Where do downloaded files land, and what directory structure?
|
||||
|
||||
**Evidence:**
|
||||
- `SESSION_DIR="$HOME/.local/share/linux_post_install/ai"` (bin/pos-ai line 12) — data convention for AI tools
|
||||
- `DOWNLOAD_DIR="${DOWNLOAD_DIR:-$HOME/Downloads}"` (bin/pos-network-download line 22) — general download convention
|
||||
- Ollama expects models in `~/.ollama/models/` — not our concern (user moves files)
|
||||
- llama.cpp uses `--model <path>` — just needs the path printed
|
||||
- HF repos use `<namespace>/<model-name>` format (e.g., `meta-llama/Llama-3.1-8B-Instruct`)
|
||||
|
||||
**Decision:** Flat layout under XDG data directory
|
||||
|
||||
```
|
||||
~/.local/share/linux_post_install/ai/models/
|
||||
├── meta-llama-Llama-3.1-8B-Instruct/
|
||||
│ ├── config.json
|
||||
│ ├── model.safetensors
|
||||
│ ├── tokenizer.json
|
||||
│ └── .hf-meta # our metadata: repo-id, branch, download date, files
|
||||
├── Qwen-Qwen2.5-7B-Instruct/
|
||||
│ ├── model-00001-of-00003.safetensors
|
||||
│ └── ...
|
||||
└── TheBloke-Mistral-7B-v0.1-GGUF/
|
||||
├── mistral-7b-v0.1.Q4_K_M.gguf
|
||||
└── .hf-meta
|
||||
```
|
||||
|
||||
**Key decisions:**
|
||||
- **Folder name:** `<namespace>-<model-name>` (dash-joined, slashes replaced). Clean, filesystem-safe, human-readable.
|
||||
- **Default base:** `~/.local/share/linux_post_install/ai/models/` (overridable via `HF_DOWNLOAD_DIR`)
|
||||
- **`.hf-meta` file:** JSON metadata (repo-id, branch, download timestamp, file list). Enables `list` and `remove` without API calls.
|
||||
- **No nesting by namespace:** Flat is simpler — users can see all models at a glance.
|
||||
|
||||
**Out of scope:** Integrating with ollama's model directory or llama.cpp's model directory. Users move files themselves or use `--output` flag.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 5: Download Logic
|
||||
|
||||
**Problem:** How to download files from HF repos reliably.
|
||||
|
||||
**Evidence:**
|
||||
- HF REST API: `GET /api/models/{ns}/{repo}` returns file list (`siblings[].rfilename`)
|
||||
- `GET /api/models/{ns}/{repo}/tree/{rev}/{path}` returns sizes + LFS info
|
||||
- Large files (7GB+): curl `-L` transparently handles LFS, Xet, CDN redirects
|
||||
- Rate limits: 500/5min anonymous, 1000/5min with token
|
||||
- No single "download all" endpoint — must loop through file list
|
||||
- `curl -C -` handles resume for interrupted downloads
|
||||
- `--progress-bar` gives native progress for large files
|
||||
|
||||
**Download flow:**
|
||||
|
||||
```
|
||||
1. Validate repo-id (must contain /)
|
||||
2. Call GET /api/models/{ns}/{repo} → extract siblings
|
||||
3. Filter files (by filename arg, --gguf flag, or download all)
|
||||
4. For each file:
|
||||
a. Create target directory (mkdir -p)
|
||||
b. Construct download URL: https://huggingface.co/{ns}/{repo}/resolve/{rev}/{filename}
|
||||
c. curl -L -C - --progress-bar -H "Authorization: Bearer $HF_TOKEN" → target
|
||||
d. Verify file exists and is non-empty
|
||||
5. Write .hf-meta (repo-id, branch, files, timestamp)
|
||||
6. Print summary: path, total size, file count
|
||||
```
|
||||
|
||||
**Key implementation details:**
|
||||
|
||||
| Concern | Solution |
|
||||
|---------|----------|
|
||||
| Auth | Always pass `Authorization: Bearer $HF_TOKEN` header — even public repos get better rate limits |
|
||||
| Large files | `curl -L` handles LFS/Xet transparently; `--progress-bar` shows native progress |
|
||||
| Resume | `curl -C -` resumes interrupted downloads automatically |
|
||||
| Rate limiting | Sleep 1s between files; on 429, wait `Retry-After` header value or 60s default |
|
||||
| Disk space | Pre-flight check: `df` available space vs estimated total (from `/tree/` endpoint) |
|
||||
| Partial download | If curl fails mid-file, the partial file remains (resume on next run) |
|
||||
| Gated repos | API returns 403 without token; with valid token, same flow works |
|
||||
|
||||
**File listing (for `--files` flag):**
|
||||
```
|
||||
GET /api/models/{ns}/{repo}/tree/main/ | jq to extract filenames + sizes
|
||||
```
|
||||
|
||||
**Progress for multi-file downloads:**
|
||||
- Single file: curl `--progress-bar` is sufficient
|
||||
- Multi-file: Print `[N/M]` counter before each file's download, curl `--progress-bar` for each
|
||||
|
||||
**Avoided complexity:**
|
||||
- No aria2 dependency (pos-network-download pattern) — curl is sufficient for sequential downloads
|
||||
- No parallel downloads — complexity not justified for single-user homelab use
|
||||
- No streaming/progress tracking library — curl's native progress bar is enough
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 6: Output Contract
|
||||
|
||||
**Problem:** What does the tool print?
|
||||
|
||||
**Evidence:**
|
||||
- `pos media grab` prints: emoji + title + path + size (file:202-226)
|
||||
- `pos network download add` prints: GID for tracking
|
||||
- User goal: "Print the path so the user knows where files landed"
|
||||
- Tool output goes to stdout (captured by `tee` for logging)
|
||||
|
||||
**Decision:** Structured, parseable output with human-friendly summary
|
||||
|
||||
```
|
||||
# Single file download:
|
||||
📥 Downloaded: meta-llama/Llama-3.1-8B-Instruct/model.safetensors (4.7 GB)
|
||||
📁 ~/.local/share/linux_post_install/ai/models/meta-llama-Llama-3.1-8B-Instruct/model.safetensors
|
||||
|
||||
# Multi-file download:
|
||||
📥 Downloaded: meta-llama/Llama-3.1-8B-Instruct (7 files, 4.7 GB)
|
||||
📁 ~/.local/share/linux_post_install/ai/models/meta-llama-Llama-3.1-8B-Instruct/
|
||||
|
||||
# Search results:
|
||||
Found 5 models for "llama 7b":
|
||||
meta-llama/Llama-2-7b-chat-hf 12.3k downloads 13.5 GB
|
||||
NousResearch/Llama-2-7b-hf 8.2k downloads 13.5 GB
|
||||
...
|
||||
|
||||
# List:
|
||||
Downloaded models (3):
|
||||
meta-llama-Llama-3.1-8B-Instruct 4.7 GB 2026-09-04
|
||||
Qwen-Qwen2.5-7B-Instruct 4.2 GB 2026-09-03
|
||||
TheBloke-Mistral-7B-v0.1-GGUF 4.1 GB 2026-09-02
|
||||
|
||||
# Remove:
|
||||
Removed: meta-llama-Llama-3.1-8B-Instruct (freed 4.7 GB)
|
||||
```
|
||||
|
||||
**stdout contract:**
|
||||
- Summary lines go to stdout (logged by `tee`)
|
||||
- Progress bars go to stderr (not logged)
|
||||
- Errors go to stderr via `err()` (exits 1)
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 7: Error Handling
|
||||
|
||||
**Problem:** How to handle failure modes gracefully.
|
||||
|
||||
**Evidence:**
|
||||
- `pos network download` has comprehensive error handling for RPC failures, dead sources, outages
|
||||
- `pos media grab` has URL validation and delegation failure summary
|
||||
- `err()` from common.sh exits 1 with red message
|
||||
- Network tools need to handle transient failures
|
||||
|
||||
**Error matrix:**
|
||||
|
||||
| Error | Detection | Response |
|
||||
|-------|-----------|----------|
|
||||
| Missing deps | `command -v` guard before `--help` | `err "curl not found (install curl)"` — exits before help |
|
||||
| Invalid repo format | No `/` in repo-id | `err "Invalid repo format: use namespace/model-name"` |
|
||||
| 404 (repo not found) | HTTP status from API | `err "Model not found: {repo-id}"` |
|
||||
| 401/403 (auth) | HTTP status | `err "Authentication failed — check HF_TOKEN (pos config ai)"` |
|
||||
| 429 (rate limit) | HTTP status | Sleep `Retry-After` or 60s, retry once, then fail |
|
||||
| Network timeout | curl exit code 28 | `err "Connection timed out — check network"` |
|
||||
| Disk space | `df` pre-flight | `warn "Low disk space: need {N} GB, only {M} GB available"` then continue (user's call) |
|
||||
| Partial download | curl exit code != 0 | `warn "Download interrupted for {file} (resume with same command)"` — partial file stays |
|
||||
| jq parse error | jq exit code | `err "Failed to parse API response — check network or HF status"` |
|
||||
| Token not set | Empty after config load | `warn "No HF_TOKEN set — using anonymous access (lower rate limits)"` — continue for public repos |
|
||||
|
||||
**Design principle:** Never fail silently. Always tell the user what happened and how to fix it. For transient errors, offer resume guidance.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 8: Dependencies and Lint Compliance
|
||||
|
||||
**Problem:** What deps are needed, and how to satisfy the lint gate?
|
||||
|
||||
**Evidence:**
|
||||
- `curl` and `jq` are in `preinstall.sh` PACKAGES (line 29: `git curl wget aria2 vim nano tmux tree jq`)
|
||||
- Deps guards must sit **before** `-h|--help` case (DEV.md line 110, lint rule)
|
||||
- `set -euo pipefail` required (lint rule)
|
||||
- `# POS:` header required (lint rule)
|
||||
- No stdin reading → not in `INTERACTIVE_CMDS` (lint rule)
|
||||
|
||||
**Lint compliance checklist:**
|
||||
|
||||
| Rule | Requirement | Implementation |
|
||||
|------|-------------|----------------|
|
||||
| Shebang | `#!/usr/bin/env bash` | Line 1 |
|
||||
| Strict mode | `set -euo pipefail` | Line 2 |
|
||||
| `# POS:` header | After shebang/strict-mode | Lines 3-7 |
|
||||
| Deps guards before `--help` | `command -v` guards before case | After source, before case |
|
||||
| `-h|--help` via case | `case "${1:-}" in -h\|--help) usage ;;` | Standard pattern |
|
||||
| Exec bits | 100755 | `chmod +x` on creation |
|
||||
| No stdin | Not in `INTERACTIVE_CMDS` | True — non-interactive tool |
|
||||
| Source chain | `source "$(dirname "$0")/../lib/common.sh" 2>/dev/null \|\| source "$(dirname "$0")/common.sh"` | Standard pattern |
|
||||
|
||||
**No new packages needed.** `curl` and `jq` are already installed by `preinstall.sh`.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 9: Testing Strategy
|
||||
|
||||
**Problem:** How to verify the tool works without a live HF token or network.
|
||||
|
||||
**Evidence:**
|
||||
- DEV.md (line 196-214): Stub PATH approach — fake binaries, temp HOME, assert on output
|
||||
- `pos network download` test pattern: fake curl/systemctl stubs with JSON fixtures
|
||||
- `pos system backup` test pattern: per-test lsblk JSON fixtures in temp dirs
|
||||
- Env-overridable paths: `HF_DOWNLOAD_DIR` is the seam
|
||||
|
||||
**Test architecture:**
|
||||
|
||||
```
|
||||
/tmp/opencode/hf-test/
|
||||
├── run-tests.sh # Test runner with check() helper
|
||||
├── stubs/
|
||||
│ ├── curl # Fake curl: returns fixtures based on URL pattern
|
||||
│ └── jq # Pass-through (real jq with fixture data)
|
||||
└── fixtures/
|
||||
├── model-meta.json # GET /api/models/{ns}/{repo} response
|
||||
├── model-tree.json # GET /api/models/{ns}/{repo}/tree/ response
|
||||
└── search.json # GET /api/models?search=... response
|
||||
```
|
||||
|
||||
**Test cases (target: ~40-50 cases):**
|
||||
|
||||
| Category | Cases |
|
||||
|----------|-------|
|
||||
| Argument parsing | Missing repo-id, invalid format (no /), unknown subcommand, unknown flag |
|
||||
| download | Single file download, whole repo download, --gguf filter, --branch, resume (partial file exists), 404 error, 401 error, 429 rate limit |
|
||||
| search | Successful search, empty results, network error |
|
||||
| list | Empty list, populated list, corrupted .hf-meta |
|
||||
| remove | Successful remove, nonexistent model, remove frees space |
|
||||
| Config | Token from env, token from file, download dir override |
|
||||
| Output | Summary format matches contract, paths are correct |
|
||||
| Edge cases | Empty repo, very long filename, special characters in repo-id |
|
||||
|
||||
**Stub `curl` behavior:**
|
||||
- Intercepts calls to `huggingface.co`
|
||||
- Routes `/api/models/` to fixture files
|
||||
- Routes `/resolve/` to a fake download (creates a small file)
|
||||
- Simulates error codes (401, 403, 404, 429)
|
||||
- Tracks call count for assertion
|
||||
|
||||
**No changes to the repo's test infrastructure** — tests live in `/tmp/opencode/` per convention.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Function Signatures
|
||||
|
||||
### Config
|
||||
|
||||
```bash
|
||||
load_hf_config()
|
||||
# Reads HF_TOKEN and HF_DOWNLOAD_DIR from:
|
||||
# 1. Already-exported env vars (highest precedence)
|
||||
# 2. ~/.config/linux_post_install/ai.env (HF_TOKEN, HF_DOWNLOAD_DIR)
|
||||
# 3. Defaults: HF_DOWNLOAD_DIR=~/.local/share/linux_post_install/ai/models
|
||||
```
|
||||
|
||||
### API Helpers
|
||||
|
||||
```bash
|
||||
hf_api() # hf_api <endpoint> → JSON response (GET only)
|
||||
# Calls: curl -fsS -H "Authorization: Bearer $HF_TOKEN" "https://huggingface.co/api$endpoint"
|
||||
# Handles: 401/403 auth errors, 429 rate limit (sleep + retry once), network errors
|
||||
|
||||
hf_repo_files() # hf_repo_files <repo-id> [branch] → JSON array of {rfilename, size}
|
||||
# Calls: GET /api/models/{ns}/{repo}/tree/{branch}/ for sizes, falls back to /api/models/{ns}/{repo} for file list
|
||||
|
||||
hf_search() # hf_search <query> [limit] → JSON array of {id, downloads, likes}
|
||||
# Calls: GET /api/models?search={query}&sort=downloads&direction=-1&limit={N}
|
||||
```
|
||||
|
||||
### Download
|
||||
|
||||
```bash
|
||||
hf_download_file() # hf_download_file <url> <target> → 0/1
|
||||
# curl -L -C - --progress-bar -H "Authorization: Bearer $HF_TOKEN" -o "$target" "$url"
|
||||
# Returns: 0 on success, 1 on curl failure
|
||||
|
||||
hf_download_repo() # hf_download_repo <repo-id> [branch] [filename|--gguf]
|
||||
# Orchestrates: API call → file list → loop → download → write .hf-meta → summary
|
||||
```
|
||||
|
||||
### Subcommands
|
||||
|
||||
```bash
|
||||
cmd_download() # cmd_download <repo-id> [args...]
|
||||
# Dispatches: single file / whole repo / --files interactive / --gguf filter
|
||||
|
||||
cmd_search() # cmd_search <query>
|
||||
# Calls hf_search, formats table
|
||||
|
||||
cmd_list() # cmd_list
|
||||
# Scans $HF_DOWNLOAD_DIR, reads .hf-meta, prints table
|
||||
|
||||
cmd_remove() # cmd_remove <repo-id>
|
||||
# Validates exists, rm -rf, prints freed space
|
||||
```
|
||||
|
||||
### Utilities
|
||||
|
||||
```bash
|
||||
hf_repo_dir() # hf_repo_dir <repo-id> → filesystem path (dash-joined)
|
||||
# "meta-llama/Llama-3.1-8B-Instruct" → "$HF_DOWNLOAD_DIR/meta-llama-Llama-3.1-8B-Instruct"
|
||||
|
||||
hf_human_size() # hf_human_size <bytes> → "4.7 GB" / "12.3 MB" / "1024 B"
|
||||
# Same pattern as pos-media-grab (file:213-221)
|
||||
|
||||
hf_resolve_branch() # hf_resolve_branch <repo-id> [branch] → resolved branch
|
||||
# Default "main"; calls API to get model metadata defaultBranch if not specified
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Config Keys
|
||||
|
||||
| Key | Scope | File | Default | Secret | Description |
|
||||
|-----|-------|------|---------|--------|-------------|
|
||||
| `HF_TOKEN` | `ai` | `ai.env` | (empty) | Yes | Hugging Face API token. Generate at huggingface.co/settings/tokens. Even for public repos, a token increases rate limits from 500/5min to 1000/5min. |
|
||||
| `HF_DOWNLOAD_DIR` | `ai` | `ai.env` | `~/.local/share/linux_post_install/ai/models` | No | Base directory for downloaded models. Each repo gets a subdirectory named `<namespace>-<model-name>`. |
|
||||
|
||||
**Precedence:** env var > `ai.env` file > default (standard `load_config` pattern).
|
||||
|
||||
---
|
||||
|
||||
## File List and Responsibilities
|
||||
|
||||
### New files
|
||||
|
||||
| File | Purpose | Lines (est.) |
|
||||
|------|---------|-------------|
|
||||
| `bin/pos-ai-hf` | Main tool: download, search, list, remove | ~350-400 |
|
||||
|
||||
### Modified files
|
||||
|
||||
| File | Change | Scope |
|
||||
|------|--------|-------|
|
||||
| `DOC/POS.md` | Add `ai hf` to `ai` category table + detail block | Hand-written |
|
||||
| `DOC/HOWTO.md` | Add row to AI category in index | Hand-written |
|
||||
| `DOC/howto/ai.md` | Add Hugging Face download section (recipes, config, troubleshooting) | Hand-written |
|
||||
| `DOC/AGENT_Context_Project.md` | GEN blocks auto-regenerated by `make gen` | Auto |
|
||||
|
||||
### NOT modified
|
||||
|
||||
| File | Reason |
|
||||
|------|--------|
|
||||
| `preinstall.sh` | `curl` and `jq` already in PACKAGES |
|
||||
| `lib/common.sh` | No shared helpers needed — tool is self-contained |
|
||||
| `bin/pos` | No `INTERACTIVE_CMDS` change (non-interactive tool); usage EXAMPLES updated by hand |
|
||||
| `install.sh` | No new lib files to install |
|
||||
|
||||
---
|
||||
|
||||
## Approved Scope
|
||||
|
||||
### In scope
|
||||
|
||||
1. **Create `bin/pos-ai-hf`** — single file, ~350-400 lines
|
||||
- Subcommands: `download`, `search`, `list`, `remove`
|
||||
- Config: `HF_TOKEN`, `HF_DOWNLOAD_DIR` via `# POS_CONFIG: ai`
|
||||
- Deps guards for `curl` and `jq`
|
||||
- Full `--help` text
|
||||
- Error handling for all failure modes listed in Decision 7
|
||||
- Resume support (`curl -C -`)
|
||||
- Rate limit handling (sleep + retry on 429)
|
||||
- `.hf-meta` metadata tracking per downloaded repo
|
||||
|
||||
2. **Documentation updates** (Writer task, not blocking)
|
||||
- `DOC/POS.md`: `ai hf` row + detail block
|
||||
- `DOC/howto/ai.md`: Hugging Face section
|
||||
- `DOC/HOWTO.md`: index row
|
||||
|
||||
3. **Run gates**
|
||||
- `make gen && make check && make lint` must pass (0 FAIL, 0 WARN)
|
||||
|
||||
### Explicitly out of scope
|
||||
|
||||
- **Ollama integration** — no `ollama import` or model registration; user moves files manually
|
||||
- **llama.cpp integration** — no quantization or conversion; just download
|
||||
- **Parallel downloads** — sequential is sufficient for homelab use
|
||||
- **Download queuing/history** — no aria2 dependency; simple curl-based downloads
|
||||
- **Model conversion** — pure download tool, not a model pipeline
|
||||
- **CivitAI/other sources** — HF only; other sources get their own tools if needed
|
||||
- **Interactive file picker** — `--files` lists files and downloads all (or filtered); no interactive selection menu
|
||||
- **New config scope** — extends existing `ai` scope, no new `pos config` entry
|
||||
- **`pos ai` changes** — `bin/pos-ai` is not modified; `pos-ai-hf` is independent
|
||||
|
||||
---
|
||||
|
||||
## Architectural Constraints for Builder
|
||||
|
||||
1. **Start from template:** `cp templates/pos-tool.sh bin/pos-ai-hf`
|
||||
2. **POS header must be on line ~4:** `# POS: ai hf — Download AI models from Hugging Face (search, download, manage)`
|
||||
3. **Deps guards before `-h|--help` case:** `command -v curl` and `command -v jq` before the case
|
||||
4. **Source chain:** Standard `source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"`
|
||||
5. **Config loader pattern:** Copy `load_config()` from `bin/pos-ai` (lines 130-160) — read `ai.env`, env-var precedence, strip CR
|
||||
6. **All file paths must be seam-guarded:** `HF_DOWNLOAD_DIR="${HF_DOWNLOAD_DIR:-$HOME/.local/share/linux_post_install/ai/models}"`
|
||||
7. **Output to stdout only:** Summary lines. Progress bars and curl output to stderr.
|
||||
8. **No INTERACTIVE_CMDS change:** Tool does not read stdin
|
||||
9. **`make gen && make check && make lint` must pass** before handoff to Writer
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
| Check | Command | Expected |
|
||||
|-------|---------|----------|
|
||||
| Syntax | `bash -n bin/pos-ai-hf` | No output, rc=0 |
|
||||
| Exec bit | `ls -la bin/pos-ai-hf` | `-rwxr-xr-x` |
|
||||
| POS header | `head -10 bin/pos-ai-hf` | Contains `# POS: ai hf —` |
|
||||
| Help | `bin/pos-ai-hf --help` | Prints usage, rc=0 |
|
||||
| Deps guard | Remove curl, run `bin/pos-ai-hf --help` | Error about curl, rc=1 |
|
||||
| Dispatch | `bin/pos help ai hf` | Shows pos-ai-hf help |
|
||||
| Category | `bin/pos ai --help` | Lists `hf` subcommand |
|
||||
| Gen | `make gen` | Regenerates tree/dispatch/completions |
|
||||
| Check | `make check` | 0 failures |
|
||||
| Lint | `make lint` | 0 FAIL, 0 WARN |
|
||||
| Stub tests | `/tmp/opencode/hf-test/run-tests.sh` | 40+ cases green |
|
||||
|
||||
---
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| HF API changes endpoint format | Low | Medium | Pin to v0 API (`/api/models/`); monitor HF changelog |
|
||||
| Token exposure in logs | Medium | High | Token passed via header, not URL; never printed in output; `HF_TOKEN` marked as secret in POS_CONFIG |
|
||||
| Very large repos (100+ files) | Low | Low | Sequential download with progress; user can Ctrl+C and resume |
|
||||
| LFS pointer files downloaded instead of content | Low | Medium | `curl -L` follows LFS redirect; test with known LFS repo |
|
||||
| Disk full during multi-file download | Medium | Medium | Pre-flight `df` check; partial files preserved for resume |
|
||||
@@ -1,533 +0,0 @@
|
||||
# Architecture Decision: `pos ai server` — llama.cpp Inference Server
|
||||
|
||||
## TL;DR
|
||||
|
||||
**Decisions:**
|
||||
1. New tool `bin/pos-ai-server` with subcommands: `start`, `stop`, `status`, `models`, `logs`
|
||||
2. Systemd user service generated at runtime (same pattern as `pos-network-download` and `pos-communication-telegram-listener`)
|
||||
3. Config extends existing `ai` scope in `ai.env` — no new config files
|
||||
4. Provider adapter `lib/ai-providers/llamacpp.sh` follows the 4-function contract
|
||||
5. Changes to `bin/pos-ai`: `resolve_key()` accepts `llamacpp` (no key needed), `resolve_model()` falls through to `LLAMACPP_MODEL`, `cmd_providers()` includes llamacpp
|
||||
6. No static `systemd/` unit file — the service is generated dynamically because model path, port, and GPU flags are user-configurable
|
||||
|
||||
**Open items:**
|
||||
- llama-server binary name varies by build (`llama-server`, `llama.cpp/server`, `server`) — detection logic needs a fallback chain
|
||||
- ROCm detection deferred (Debian/Ubuntu focus, CUDA-only auto-detect)
|
||||
|
||||
---
|
||||
|
||||
## Decision 1: Tool Structure
|
||||
|
||||
**Problem:** User needs to start/stop/manage a local llama.cpp inference server via `pos`.
|
||||
|
||||
**Decision:** Create `bin/pos-ai-server` as a standalone tool under the `ai` category, with subcommands.
|
||||
|
||||
**Evidence:**
|
||||
- Existing pattern: `bin/pos-network-download` is a standalone tool with `start`/`stop`/`status` subcommands for the aria2 daemon
|
||||
- Existing pattern: `bin/pos-communication-telegram-listener` manages its own systemd user service
|
||||
- Tool naming: `pos-ai-server` → `pos ai server` (category: `ai`, command: `server`)
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
| Subcommand | Description |
|
||||
|------------|-------------|
|
||||
| `start [model]` | Install & start the systemd user service (model from arg, config, or interactive pick) |
|
||||
| `stop` | Stop & remove the service |
|
||||
| `status` | Show running state, loaded model, port, health endpoint |
|
||||
| `models` | List available GGUF files from `HF_DOWNLOAD_DIR` |
|
||||
| `logs [lines]` | Show recent server logs via `journalctl --user` |
|
||||
|
||||
**Not interactive:** `pos ai server` does NOT read stdin (no prompts that block under `tee`). It does NOT need to be in `INTERACTIVE_CMDS`.
|
||||
|
||||
**Approved scope:**
|
||||
- `bin/pos-ai-server` — 1 file
|
||||
- POS header: `# POS: ai server — llama.cpp local inference server (start, stop, status, models, logs)`
|
||||
- POS_SUBCMDS: `start stop status models logs`
|
||||
- POS_DEPS: `curl jq` (curl for health check + API, jq for JSON parsing)
|
||||
- POS_FLAGS: `--port --host --model --ctx --gpu --threads`
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 2: Systemd User Service (Runtime-Generated)
|
||||
|
||||
**Problem:** The llama-server service needs model path, port, GPU layers, and other parameters that are user-configurable. A static unit file can't carry these.
|
||||
|
||||
**Decision:** Generate the systemd user service file at runtime (same pattern as `pos-network-download` lines 172-187 and `pos-communication-telegram-listener` lines 481-500).
|
||||
|
||||
**Evidence:**
|
||||
- `pos-network-download`: generates `pos-aria2.service` at `cmd_start()` with `$RPC_PORT`, `$RPC_SECRET`, `$DOWNLOAD_DIR` baked into `ExecStart`
|
||||
- `pos-communication-telegram-listener`: generates `pos-telegram-listener.service` with the runner path baked in
|
||||
- Both write to `$USER_SYSTEMD_DIR` (`~/.config/systemd/user/`), then `systemctl --user daemon-reload && enable --now`
|
||||
- Both use `cat > "$USER_SYSTEMD_DIR/$SERVICE" <<EOF` pattern
|
||||
|
||||
**Service name:** `pos-ai-server.service`
|
||||
|
||||
**Service content:**
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=pos llama.cpp inference server (linux-post-install)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/env llama-server -m <MODEL> --port <PORT> --host <HOST> --n-gpu-layers <GPU_LAYERS> --ctx-size <CTX_SIZE> --threads <THREADS>
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
TimeoutStopSec=10
|
||||
KillMode=control-group
|
||||
EnvironmentFile=-%h/.config/linux_post_install/ai.env
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
**Key design choices:**
|
||||
- `Type=simple` — llama-server runs in foreground by default (no daemonize flag needed)
|
||||
- `Restart=on-failure` — restart if it crashes, but not on clean exit (`stop` sends SIGTERM, which is clean)
|
||||
- `RestartSec=5` — give time for model unload/reload
|
||||
- `TimeoutStopSec=10` — llama-server handles SIGTERM gracefully (unloads model), 10s is generous
|
||||
- `KillMode=control-group` — ensures the whole process tree is cleaned up
|
||||
- `EnvironmentFile=-` (dash prefix) — missing file is not an error
|
||||
- ExecStart is a direct `llama-server` call (not a wrapper script) — systemd handles the lifecycle
|
||||
|
||||
**Where config values come from:** `cmd_start()` reads the config file, resolves all values, then bakes them into the generated unit. The `EnvironmentFile` line in the unit is a fallback but the actual arguments are baked in at generation time. This matches the aria2 pattern exactly.
|
||||
|
||||
**`start` subcommand flow:**
|
||||
1. Load config from `ai.env`
|
||||
2. Resolve model (argument → `LLAMACPP_MODEL` → interactive pick)
|
||||
3. Resolve port, host, ctx, gpu, threads (flag → config → default)
|
||||
4. Validate model file exists
|
||||
5. Auto-detect GPU if `LLAMACPP_GPU_LAYERS` is `-1`
|
||||
6. Check port availability
|
||||
7. Generate systemd unit file
|
||||
8. `systemctl --user daemon-reload`
|
||||
9. `systemctl --user enable --now pos-ai-server.service`
|
||||
10. Wait briefly, then check health endpoint
|
||||
|
||||
**Linger warning:** Same as existing tools — warn if `loginctl enable-linger` is needed.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 3: Config Keys (Extend `ai` Scope)
|
||||
|
||||
**Problem:** Server settings need to be persisted alongside existing AI config.
|
||||
|
||||
**Decision:** Extend the existing `ai` scope in `ai.env`. No new config file.
|
||||
|
||||
**Evidence:**
|
||||
- `ai.env` already holds `AI_PROVIDER`, `AI_GEMINI_API_KEY`, `HF_DOWNLOAD_DIR`, etc.
|
||||
- The `# POS_CONFIG:` header on `bin/pos-ai` already declares the `ai` scope
|
||||
- Adding `LLAMACPP_*` keys to the same file keeps all AI config in one place
|
||||
- `pos config ai` auto-discovers keys from `# POS_CONFIG:` headers
|
||||
|
||||
**Config keys to add:**
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `LLAMACPP_PORT` | `8088` | Server listen port |
|
||||
| `LLAMACPP_HOST` | `127.0.0.1` | Bind address |
|
||||
| `LLAMACPP_MODEL` | *(empty)* | Default model path (GGUF file) |
|
||||
| `LLAMACPP_CTX_SIZE` | `4096` | Context window size |
|
||||
| `LLAMACPP_GPU_LAYERS` | `-1` | GPU layers (`-1` = auto-detect, `0` = CPU only) |
|
||||
| `LLAMACPP_THREADS` | `$(nproc)` | CPU threads |
|
||||
|
||||
**POS_CONFIG header on `pos-ai`:** Extend the existing `# POS_CONFIG:` line to include the new keys. The existing header already uses `ai | ai.env | ...` format — we append `LLAMACPP_*` entries.
|
||||
|
||||
**New header addition (appended to existing `# POS_CONFIG:` line):**
|
||||
```
|
||||
| LLAMACPP_PORT=:Server port (default 8088) | LLAMACPP_HOST=:Bind address (default 127.0.0.1) | LLAMACPP_MODEL=:Default model path (GGUF) | LLAMACPP_CTX_SIZE:num:Context window size (default 4096) | LLAMACPP_GPU_LAYERS:num:GPU layers (-1=auto, 0=CPU only, default -1) | LLAMACPP_THREADS:num:CPU threads (default: nproc)
|
||||
```
|
||||
|
||||
**Config template update:** Add commented examples to `config/ai.env`.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 4: Provider Adapter
|
||||
|
||||
**Problem:** `pos ai ask` should work with the local llama.cpp server as a backend, just like gemini/openrouter.
|
||||
|
||||
**Decision:** Create `lib/ai-providers/llamacpp.sh` with the 4-function contract.
|
||||
|
||||
**Evidence:**
|
||||
- `lib/ai-providers/gemini.sh` and `lib/ai-providers/openrouter.sh` both implement: `provider_name()`, `provider_default_model()`, `provider_generate()`, `provider_models_list()`
|
||||
- `pos-ai` loads providers via `load_provider()` which sources `$PROVIDER_DIR/$p.sh`
|
||||
- The provider adapter pattern is established and stable
|
||||
|
||||
**Function signatures:**
|
||||
|
||||
```bash
|
||||
# provider_name → human-readable name
|
||||
provider_name() { printf 'Local llama.cpp'; }
|
||||
|
||||
# provider_default_model → what's loaded on the server
|
||||
provider_default_model() {
|
||||
local port="${LLAMACPP_PORT:-8088}"
|
||||
local model
|
||||
model="$(curl -sf "http://127.0.0.1:$port/v1/models" 2>/dev/null | jq -r '.data[0].id // empty')"
|
||||
[ -n "$model" ] && printf '%s' "$model" || printf '(no model loaded)'
|
||||
}
|
||||
|
||||
# provider_generate($1=model, $2=messages JSON, $3=optional system prompt)
|
||||
# → POST to /v1/chat/completions, stdout = response text
|
||||
provider_generate() {
|
||||
local model="$1" messages="$2" system="${3:-}" port="${LLAMACPP_PORT:-8088}"
|
||||
local body resp code body_out
|
||||
# Build messages array with optional system prompt
|
||||
if [ -n "$system" ]; then
|
||||
body="$(printf '%s' "$messages" | jq -c --arg s "$system" \
|
||||
'[{role:"system",content:$s}] + .messages')"
|
||||
else
|
||||
body="$(printf '%s' "$messages" | jq -c '.messages')"
|
||||
fi
|
||||
body="$(printf '%s' "$body" | jq -nc --arg m "$model" --argjson msgs "$body" \
|
||||
'{model:$m, messages:$msgs, stream:false}')"
|
||||
resp="$(curl -sS -m 120 -X POST "http://127.0.0.1:$port/v1/chat/completions" \
|
||||
-H "Content-Type: application/json" \
|
||||
--write-out $'\n%{http_code}' \
|
||||
--data "$body")" || { echo "request failed (curl exit $?)" >&2; return 1; }
|
||||
code="${resp##*$'\n'}"
|
||||
body_out="${resp%$'\n'*}"
|
||||
if [ "$code" != "200" ]; then
|
||||
echo "API error $code" >&2
|
||||
return 1
|
||||
fi
|
||||
printf '%s' "$body_out" | jq -r '.choices[0].message.content // ""'
|
||||
}
|
||||
|
||||
# provider_models_list($1=current model) → stdout = formatted list
|
||||
provider_models_list() {
|
||||
local model="$1" port="${LLAMACPP_PORT:-8088}" resp code body
|
||||
resp="$(curl -sf "http://127.0.0.1:$port/v1/models" \
|
||||
--write-out $'\n%{http_code}')" || { echo "server not running" >&2; return 1; }
|
||||
code="${resp##*$'\n'}"
|
||||
body="${resp%$'\n'*}"
|
||||
[ "$code" = "200" ] || { echo "API error $code" >&2; return 1; }
|
||||
echo "Local llama.cpp models:"
|
||||
printf '%s' "$body" | jq -r '.data[]? | .id' | while IFS= read -r m; do
|
||||
[ -n "$m" ] || continue
|
||||
if [ "$m" = "$model" ]; then
|
||||
printf ' %-48s <- loaded\n' "$m"
|
||||
else
|
||||
printf ' %-48s\n' "$m"
|
||||
fi
|
||||
done
|
||||
}
|
||||
```
|
||||
|
||||
**No API key:** `resolve_key()` in `bin/pos-ai` needs a `llamacpp)` case that succeeds without a key. The local server has no auth.
|
||||
|
||||
**Provider detection in `resolve_key()`:**
|
||||
```bash
|
||||
llamacpp) return 0 ;; # No API key needed for local server
|
||||
```
|
||||
|
||||
**Provider detection in `resolve_model()`:**
|
||||
```bash
|
||||
llamacpp)
|
||||
[ -n "${LLAMACPP_MODEL:-}" ] && printf '%s' "$(basename "$LLAMACPP_MODEL")" && return ;;
|
||||
```
|
||||
|
||||
**Provider detection in `cmd_providers()`:** The existing loop over `$PROVIDER_DIR/*.sh` auto-discovers `llamacpp.sh`. The `configured` check needs updating — llamacpp is "configured" when `llama-server` is available, not when an API key exists.
|
||||
|
||||
**POS_CONFIG header on adapter:** Add `# PROVIDER_CONFIG: LLAMACPP_MODEL=:Default model path (GGUF file)` to the adapter so `pos config ai` discovers it.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 5: GPU Auto-Detection
|
||||
|
||||
**Problem:** Auto-detect NVIDIA CUDA to set `--n-gpu-layers` appropriately.
|
||||
|
||||
**Decision:** Simple CUDA detection — check `nvidia-smi` and `/dev/nvidia*`. No ROCm for now (Debian/Ubuntu focus).
|
||||
|
||||
**Evidence:**
|
||||
- `nvidia-smi` is the standard NVIDIA management interface
|
||||
- `/dev/nvidia*` devices indicate driver presence
|
||||
- llama.cpp uses `--n-gpu-layers -1` for auto (offload all possible layers to GPU)
|
||||
- The user's request says "Debian/Ubuntu so mainly CUDA"
|
||||
|
||||
**Detection function (in `pos-ai-server`):**
|
||||
|
||||
```bash
|
||||
detect_gpu() {
|
||||
if command -v nvidia-smi &>/dev/null && nvidia-smi &>/dev/null 2>&1; then
|
||||
echo "cuda"
|
||||
else
|
||||
echo "cpu"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
**GPU layer resolution:**
|
||||
|
||||
```bash
|
||||
resolve_gpu_layers() {
|
||||
local configured="${LLAMACPP_GPU_LAYERS:-}"
|
||||
if [ -n "$configured" ] && [ "$configured" != "-1" ]; then
|
||||
echo "$configured"
|
||||
return
|
||||
fi
|
||||
# Auto-detect
|
||||
local gpu
|
||||
gpu="$(detect_gpu)"
|
||||
case "$gpu" in
|
||||
cuda) echo "-1" ;;
|
||||
*) echo "0" ;;
|
||||
esac
|
||||
}
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- `LLAMACPP_GPU_LAYERS=-1` (default) → auto-detect: CUDA → `-1`, no CUDA → `0`
|
||||
- `LLAMACPP_GPU_LAYERS=0` → CPU only (override)
|
||||
- `LLAMACPP_GPU_LAYERS=35` → explicit layer count (for fine-tuning)
|
||||
|
||||
**No ROCm:** Explicitly out of scope. The detection function can be extended later.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 6: Model Selection
|
||||
|
||||
**Problem:** User needs to pick from downloaded GGUF models.
|
||||
|
||||
**Decision:** `pos ai server models` scans `HF_DOWNLOAD_DIR` for `.gguf` files, reusing `pos ai hf list` patterns.
|
||||
|
||||
**Evidence:**
|
||||
- `pos-ai-hf` downloads to `$HF_DOWNLOAD_DIR` (default `~/.local/share/linux_post_install/ai/models/`)
|
||||
- `cmd_list()` in `pos-ai-hf` already scans for model directories with `.hf-meta` files
|
||||
- GGUF files are the inference-ready format; they're the only files that matter for serving
|
||||
|
||||
**`models` subcommand behavior:**
|
||||
|
||||
```bash
|
||||
cmd_models() {
|
||||
local dir="${HF_DOWNLOAD_DIR:-$HOME/.local/share/linux_post_install/ai/models}"
|
||||
[ -d "$dir" ] || { warn "No models directory — run 'pos ai hf download' first"; return 0; }
|
||||
|
||||
local found=0
|
||||
echo "Available GGUF models:"
|
||||
while IFS= read -r gguf; do
|
||||
[ -f "$gguf" ] || continue
|
||||
found=1
|
||||
local name size
|
||||
name="$(basename "$gguf")"
|
||||
local dir_name
|
||||
dir_name="$(basename "$(dirname "$gguf")")"
|
||||
size="$(stat -c%s "$gguf" 2>/dev/null || echo 0)"
|
||||
local human_size
|
||||
human_size="$(human_size "$size")"
|
||||
printf ' %-50s %s %s\n' "$dir_name/$name" "$human_size" ""
|
||||
done < <(find "$dir" -name '*.gguf' -type f 2>/dev/null | sort)
|
||||
|
||||
[ "$found" -eq 0 ] && warn "No .gguf files found — download with 'pos ai hf download <repo> --gguf'"
|
||||
}
|
||||
```
|
||||
|
||||
**Model resolution order for `start [model]`:**
|
||||
1. Explicit argument: `pos ai server start /path/to/model.gguf`
|
||||
2. Relative path argument: `pos ai server start model.gguf` → search `HF_DOWNLOAD_DIR`
|
||||
3. Config: `LLAMACPP_MODEL` from `ai.env`
|
||||
4. Interactive pick: prompt user to select from available models
|
||||
|
||||
**Interactive pick (only when on a TTY and no model specified):**
|
||||
```bash
|
||||
pick_model() {
|
||||
local models=() i
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
models+=("$f")
|
||||
done < <(find "$HF_DOWNLOAD_DIR" -name '*.gguf' -type f 2>/dev/null | sort)
|
||||
|
||||
[ ${#models[@]} -gt 0 ] || err "No GGUF models found — run 'pos ai hf download <repo> --gguf'"
|
||||
|
||||
echo "Available models:"
|
||||
for ((i = 0; i < ${#models[@]}; i++)); do
|
||||
local name size
|
||||
name="$(basename "${models[$i]}")"
|
||||
size="$(stat -c%s "${models[$i]}" 2>/dev/null || echo 0)"
|
||||
printf ' %2d) %-50s %s\n' "$((i + 1))" "$name" "$(human_size "$size")"
|
||||
done
|
||||
echo
|
||||
local choice
|
||||
printf 'Pick a model [1-%d]: ' "${#models[@]}"
|
||||
IFS= read -r choice </dev/tty || choice=""
|
||||
[[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "${#models[@]}" ] || err "Invalid selection"
|
||||
printf '%s' "${models[$((choice - 1))]}"
|
||||
}
|
||||
```
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 7: Health Check & Status
|
||||
|
||||
**Problem:** User needs to know if the server is running and healthy.
|
||||
|
||||
**Decision:** Use llama.cpp's `/health` endpoint + systemd state.
|
||||
|
||||
**`status` subcommand output:**
|
||||
|
||||
```
|
||||
service: running
|
||||
model: mistral-7b-v0.1.Q4_K_M.gguf
|
||||
port: 8088
|
||||
host: 127.0.0.1
|
||||
gpu: CUDA (-1 layers)
|
||||
context: 4096
|
||||
threads: 16
|
||||
autostart: enabled
|
||||
endpoint: http://127.0.0.1:8088
|
||||
health: ok (loaded)
|
||||
```
|
||||
|
||||
**Health check function:**
|
||||
|
||||
```bash
|
||||
check_health() {
|
||||
local port="${LLAMACPP_PORT:-8088}"
|
||||
local resp
|
||||
resp="$(curl -sf "http://127.0.0.1:$port/health" 2>/dev/null)" || { echo "not running"; return 1; }
|
||||
# llama.cpp /health returns {"status": "ok"} or {"status": "loading model", ...}
|
||||
local status
|
||||
status="$(printf '%s' "$resp" | jq -r '.status // "unknown"' 2>/dev/null)"
|
||||
echo "$status"
|
||||
}
|
||||
```
|
||||
|
||||
**`logs` subcommand:** Uses `journalctl --user -u pos-ai-server -n <lines> --no-pager`.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 8: Changes to `bin/pos-ai`
|
||||
|
||||
**Problem:** `pos ai --provider llamacpp` should work, routing through the local server.
|
||||
|
||||
**Decision:** Minimal changes to `bin/pos-ai` — 3 touch points.
|
||||
|
||||
**Evidence:**
|
||||
- `resolve_key()` has a `case "$p" in` that checks each provider — add `llamacpp)` case
|
||||
- `resolve_model()` has a `case "$p" in` for provider-specific fallbacks — add `llamacpp)` case
|
||||
- `cmd_providers()` checks API key configuration — add llamacpp case
|
||||
- `require_key()` has provider-specific error messages — add llamacpp case
|
||||
|
||||
**Changes:**
|
||||
|
||||
1. **`resolve_key()` (line 167):** Add `llamacpp) return 0 ;;` — no key needed
|
||||
2. **`resolve_model()` (line 193):** Add `llamacpp) [ -n "${LLAMACPP_MODEL:-}" ] && printf '%s' "$(basename "$LLAMACPP_MODEL")" && return ;;`
|
||||
3. **`cmd_providers()` (line 120):** Add `llamacpp) [ -n "${LLAMACPP_PORT:-}" ] && configured="configured" || configured="configured (default port)" ;;` — local server is always "configured"
|
||||
4. **`require_key()` (line 176):** Add `llamacpp) ;;` — no key needed, just return
|
||||
|
||||
These are all 1-2 line additions within existing `case` blocks.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 9: Error Handling
|
||||
|
||||
| Error | Detection | Response |
|
||||
|-------|-----------|----------|
|
||||
| `llama-server` not found | `command -v llama-server` fails | `err "llama-server not found — install llama.cpp (https://github.com/ggerganov/llama.cpp)"` |
|
||||
| Port in use | `ss -tlnp` or `curl` to port | `err "Port $PORT already in use — check with 'ss -tlnp'"` |
|
||||
| Model not found | `[ -f "$model" ]` | `err "Model not found: $model"` |
|
||||
| GPU not detected | `detect_gpu` returns `cpu` | `warn "No NVIDIA GPU detected — running in CPU mode"` (continues) |
|
||||
| Service start fails | `systemctl --user start` returns non-zero | `journalctl --user -u pos-ai-server -n 20 --no-pager` |
|
||||
| Server unhealthy | `/health` returns non-200 or times out | `warn "Server may not be ready yet — check with 'pos ai server status'"` |
|
||||
| Model too large | Not reliably detectable pre-load | Skip — llama.cpp will fail with OOM and the error is in journal logs |
|
||||
|
||||
**Binary detection fallback:** llama.cpp builds name the binary differently:
|
||||
|
||||
```bash
|
||||
find_llamacpp() {
|
||||
local candidates=("llama-server" "llama.cpp/server" "server" "llama-server-cuda")
|
||||
for bin in "${candidates[@]}"; do
|
||||
command -v "$bin" &>/dev/null && { echo "$bin"; return 0; }
|
||||
done
|
||||
return 1
|
||||
}
|
||||
```
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 10: File List & Responsibilities
|
||||
|
||||
| File | Action | Responsibility |
|
||||
|------|--------|----------------|
|
||||
| `bin/pos-ai-server` | **NEW** | Service manager: start/stop/status/models/logs, systemd unit generation, GPU detection, model selection |
|
||||
| `lib/ai-providers/llamacpp.sh` | **NEW** | Provider adapter: provider_name, provider_default_model, provider_generate, provider_models_list |
|
||||
| `bin/pos-ai` | **MODIFY** | Add `llamacpp` cases to resolve_key, resolve_model, cmd_providers, require_key |
|
||||
| `config/ai.env` | **MODIFY** | Add commented LLAMACPP_* key documentation |
|
||||
| `completions/pos.bash` | **AUTO** | `make gen` picks up new POS headers — no manual edit |
|
||||
|
||||
**NOT in scope:**
|
||||
- No static `systemd/pos-ai-server.service` file (generated at runtime)
|
||||
- No changes to `postinstall.sh` (service is user-managed, not installed by system)
|
||||
- No changes to `bin/pos` dispatcher (tool is auto-discovered)
|
||||
- No changes to `lib/common.sh`
|
||||
|
||||
---
|
||||
|
||||
## Decision 11: Implementation Constraints
|
||||
|
||||
1. **All `LLAMACPP_*` config reads must go through `load_config()`** — the existing config loader in `pos-ai` (line 131). The new tool also needs its own config loader (or sources `pos-ai`'s, which it can't cleanly). **Decision:** `pos-ai-server` uses its own `load_config()` copy (same pattern as `pos-ai-hf` line 26 — every tool that reads `ai.env` has its own loader).
|
||||
|
||||
2. **The systemd unit must NOT hardcode HOME.** The existing pattern (`pos-communication-telegram-listener` line 494-496) explains why: "Do NOT pin Environment=HOME here — the systemd user manager already sets the correct HOME."
|
||||
|
||||
3. **ExecStart must use full paths for llama-server** — systemd user services don't inherit the user's full `$PATH`. Resolve via `$(command -v llama-server)` at unit generation time.
|
||||
|
||||
4. **The service must use `--log-format` flag** if available (llama.cpp) to produce parseable logs. Not a hard requirement.
|
||||
|
||||
5. **`make gen` must run after creating `bin/pos-ai-server`** to regenerate the tree, dispatch table, completions, and doc tables.
|
||||
|
||||
6. **The tool must pass `make check && make lint`** — bash -n syntax, exec bit, POS header, --help, deps guards before help.
|
||||
|
||||
---
|
||||
|
||||
## Verification Plan
|
||||
|
||||
1. **Unit test (stub PATH):**
|
||||
- Fake `llama-server`, `nvidia-smi`, `curl`, `jq` in PATH
|
||||
- Assert `cmd_start` generates correct unit file content
|
||||
- Assert `cmd_models` finds `.gguf` files
|
||||
- Assert `detect_gpu` logic
|
||||
- Assert config resolution precedence (flag > env > default)
|
||||
|
||||
2. **Integration test (manual):**
|
||||
- `pos ai server start model.gguf` with a real llama-server binary
|
||||
- `pos ai server status` shows correct info
|
||||
- `pos ai server logs` shows journal output
|
||||
- `pos ai server stop` cleans up
|
||||
- `pos ai --provider llamacpp ask "hello"` routes through local server
|
||||
|
||||
3. **Gates:**
|
||||
- `make check` — green
|
||||
- `make lint` — 0 FAIL, 0 WARN
|
||||
|
||||
---
|
||||
|
||||
## Explicitly Out of Scope
|
||||
|
||||
- Model conversion/quantization
|
||||
- Multi-GPU support
|
||||
- Authentication on the API endpoint
|
||||
- Web UI
|
||||
- GPU driver installation
|
||||
- ROCm/AMD detection
|
||||
- Quantization awareness (context size vs model capability)
|
||||
- Model memory estimation / pre-flight checks
|
||||
- Automatic model download on `start` if none present
|
||||
@@ -1,544 +0,0 @@
|
||||
# Architect Report — `pos media grab` + Telegram URL Routing
|
||||
|
||||
**Date:** 2026-09-04
|
||||
**Status:** DECISION_READY
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Decision 1:** New tool `bin/pos-media-grab` — domain-based URL classifier that delegates to `pos media mp3`/`pos media mp4`, adds `--best` for non-interactive Telegram context.
|
||||
- **Decision 2:** Listener gains URL routing step between prefix map and AI bridge — detects bare URLs, forwards to `pos media grab`.
|
||||
- **Decision 3:** Config scope `grab` (`grab.env`) for `GRAB_DEFAULT` mode.
|
||||
- **Decision 4:** No INTERACTIVE_CMDS change — grab is fully non-interactive; yt-dlp progress suppressed in favor of clean summary.
|
||||
- **Decision 5:** 600s timeout for downloads in the listener context.
|
||||
- **Decision 6:** Stub-based test harness in `/tmp/opencode/media-grab-test/`.
|
||||
|
||||
---
|
||||
|
||||
## Decision 1: File Location — Tool, Not Feature
|
||||
|
||||
### Problem
|
||||
|
||||
Where does `pos media grab` live? `bin/pos-media-grab` (auto-discovered tool) or `features/media-grab.sh` (user-customizable, never-overwritten feature)?
|
||||
|
||||
### Evidence
|
||||
|
||||
- `bin/pos-media-grab` gets auto-discovery via `pos` dispatcher, `# POS:` headers, `make gen` doc tables, completion.
|
||||
- `features/` scripts are for user-customizable logic (currently: `autostart.sh`, `usb-automount.sh`). They are installed on demand via `./install.sh --feature` and never overwritten on install.
|
||||
- `pos media grab` is core media routing — it must be present by default, not opt-in.
|
||||
|
||||
### Decision
|
||||
|
||||
`bin/pos-media-grab` — standard tool.
|
||||
|
||||
### Rationale
|
||||
|
||||
Core routing behavior that the listener depends on. Not user-customizable. Fits the `pos-<category>-<command>` naming exactly.
|
||||
|
||||
### In Scope
|
||||
|
||||
- `bin/pos-media-grab` (new tool)
|
||||
- Listener change in `bin/pos-communication-telegram-listener` (add URL routing step)
|
||||
|
||||
### Explicitly Out of Scope
|
||||
|
||||
- Feature flag for grab (not needed — always installed)
|
||||
- Changes to `pos media mp3` or `pos media mp4` (consumed as-is)
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 2: URL Classification Logic
|
||||
|
||||
### Problem
|
||||
|
||||
How does `pos media grab` determine whether a URL should be downloaded as audio (mp3) or video (mp4)?
|
||||
|
||||
### Evidence
|
||||
|
||||
- `music.youtube.com` URLs are always audio-only (music streaming).
|
||||
- YouTube regular/shorts URLs are primarily video content.
|
||||
- SoundCloud, Bandcamp are audio-first platforms.
|
||||
- yt-dlp handles both audio and video for all supported sites.
|
||||
- The user's primary use case: YouTube music → ~/Music, YouTube video → ~/Videos.
|
||||
|
||||
### Classification Rules (Priority Order)
|
||||
|
||||
| Pattern | Classification | Reasoning |
|
||||
|---------|---------------|-----------|
|
||||
| `*music.youtube.com*` | `audio` | YouTube Music is audio-only streaming |
|
||||
| `*soundcloud.com*` | `audio` | Audio-first platform |
|
||||
| `*bandcamp.com*` | `audio` | Audio-first platform |
|
||||
| `*youtube.com*`, `*youtu.be*` | `video` | YouTube primary: video content |
|
||||
| `*youtube.com/shorts/*` | `video` | Short-form video |
|
||||
| `*vimeo.com*` | `video` | Video platform |
|
||||
| `*twitch.tv*` | `video` | Video streaming |
|
||||
| Everything else | `video` (default) | Safe default — yt-dlp handles format negotiation |
|
||||
|
||||
### Decision
|
||||
|
||||
Domain-based regex classification with a configurable default.
|
||||
|
||||
### Implementation
|
||||
|
||||
```bash
|
||||
classify_url() {
|
||||
local url="$1" mode="${GRAB_DEFAULT:-video}"
|
||||
case "$url" in
|
||||
*music.youtube.com*) echo "audio" ;;
|
||||
*soundcloud.com*) echo "audio" ;;
|
||||
*bandcamp.com*) echo "audio" ;;
|
||||
*youtube.com*|*youtu.be*) echo "video" ;;
|
||||
*vimeo.com*) echo "video" ;;
|
||||
*twitch.tv*) echo "video" ;;
|
||||
*) echo "$mode" ;;
|
||||
esac
|
||||
}
|
||||
```
|
||||
|
||||
### Why Not yt-dlp `--dump-json`?
|
||||
|
||||
Using `yt-dlp --dump-json` to detect content type (e.g., checking for audio-only formats) would:
|
||||
- Require a network round-trip per URL (slow — 2-5s on YouTube)
|
||||
- Fail on age-gated content without cookies
|
||||
- Add unnecessary complexity for a heuristic
|
||||
|
||||
Domain-based classification is instant, reliable for the primary use case, and covers 95%+ of real URLs. The `--audio`/`--video` flag overrides for edge cases.
|
||||
|
||||
### Overrides
|
||||
|
||||
- `--audio` forces mp3 regardless of classification
|
||||
- `--video` forces mp4 regardless of classification
|
||||
- `--best` is passed to mp4 by default (non-interactive mode — see Decision 4)
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 3: Config Scope
|
||||
|
||||
### Problem
|
||||
|
||||
Does `pos media grab` need its own config scope? What settings?
|
||||
|
||||
### Evidence
|
||||
|
||||
- `pos media mp3` has `OUT_DIR=$HOME/Music`
|
||||
- `pos media mp4` has `OUT_DIR=$HOME/Videos`
|
||||
- Both accept `--output` flag override
|
||||
- The user might want all grabs to go to a single directory
|
||||
- The user might want a different default mode (e.g., always audio for YouTube Music)
|
||||
|
||||
### Decision
|
||||
|
||||
Config scope `grab` with one key:
|
||||
|
||||
```bash
|
||||
# POS_CONFIG: grab | grab.env | GRAB_DEFAULT=:Default mode for unknown domains (video or audio, default video)
|
||||
```
|
||||
|
||||
### Rationale
|
||||
|
||||
Minimal config — the tool's primary job is routing, not download settings. The `GRAB_DEFAULT` key lets the user change the fallback for unrecognized domains without code changes. Output directories are inherited from mp3/mp4 and overridable via `--output`.
|
||||
|
||||
### Future Extensibility
|
||||
|
||||
If needed, future keys could include:
|
||||
- `GRAB_MUSIC_DIR` (override mp3 output dir)
|
||||
- `GRAB_VIDEO_DIR` (override mp4 output dir)
|
||||
- `GRAB_COOKIES` (shared cookies file for all grabs)
|
||||
|
||||
Not implemented now — premature without user demand.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 4: Non-Interactive Default for Telegram Context
|
||||
|
||||
### Problem
|
||||
|
||||
`pos media mp4` has an interactive format selector (prompts on TTY). When called from the Telegram listener via `run_and_reply` (which uses `bash -c "$cmdline"` with no TTY), the prompt would hang or be swallowed by `tee`.
|
||||
|
||||
### Evidence
|
||||
|
||||
- `pos media mp4` lines 92-116: interactive `read -rp "Format ID"` when no `--format`/`--best`/`--worst` is specified
|
||||
- `pos` dispatcher line 287: interactive tools are in `INTERACTIVE_CMDS` which skips `tee` logging
|
||||
- The listener's `run_and_reply` (line 349) runs commands via `timeout "$tmo" bash -c "$cmdline"` — no TTY
|
||||
- `spawn` helper (lib/common.sh line 76) runs commands in background — no TTY interaction
|
||||
|
||||
### Decision
|
||||
|
||||
`pos media grab` passes `--best` to `pos media mp4` by default. Users can override with `--worst` flag on grab.
|
||||
|
||||
### Implementation
|
||||
|
||||
```bash
|
||||
# When routing to mp4, always add --best unless user specified --worst
|
||||
MP4_ARGS=(--best)
|
||||
[ "$WORST" -eq 1 ] && MP4_ARGS=(--worst)
|
||||
```
|
||||
|
||||
### Why --best and Not --worst?
|
||||
|
||||
- Best quality is the expected default when sending a video link to a bot
|
||||
- "I want to download this video" implies "I want it to look good"
|
||||
- The `--worst` flag exists for bandwidth-constrained scenarios (explicit opt-in)
|
||||
|
||||
### Interaction with pos media mp4
|
||||
|
||||
`pos media mp4` already supports `--best` and `--worst` flags (line 4, 87-88). No changes needed to mp4.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 5: Listener Integration — URL Routing Step
|
||||
|
||||
### Problem
|
||||
|
||||
Where does URL detection go in the listener's routing chain, and how does it work?
|
||||
|
||||
### Evidence
|
||||
|
||||
Current `handle_message` routing (bin/pos-communication-telegram-listener lines 658-731):
|
||||
|
||||
```
|
||||
/help|/start → reply mapped commands list
|
||||
↓ (no match)
|
||||
text-prefix map → run mapped command with text as argument
|
||||
↓ (no match)
|
||||
AI bridge → forward to Gemini
|
||||
↓ (no match)
|
||||
command map → run /command
|
||||
↓ (no match)
|
||||
"Unknown command"
|
||||
```
|
||||
|
||||
The user sends bare URLs from their phone. These should be detected and routed to `pos media grab`.
|
||||
|
||||
### Decision
|
||||
|
||||
New routing step between prefix map and AI bridge:
|
||||
|
||||
```
|
||||
/help|/start → reply mapped commands list
|
||||
↓ (no match)
|
||||
text-prefix map → run mapped command
|
||||
↓ (no match)
|
||||
**URL detect → pos media grab** (NEW)
|
||||
↓ (no match)
|
||||
AI bridge → forward to Gemini
|
||||
↓ (no match)
|
||||
command map → run /command
|
||||
↓ (no match)
|
||||
"Unknown command"
|
||||
```
|
||||
|
||||
### Why After Prefix Map?
|
||||
|
||||
- A prefixed command like `ai https://...` should go to the AI bridge, not grab
|
||||
- A bare URL with no prefix should go to grab
|
||||
- The prefix map is explicit user configuration — it takes priority
|
||||
|
||||
### Why Before AI Bridge?
|
||||
|
||||
- A bare URL has no AI intent — it's a download request
|
||||
- The AI bridge would waste time (and API credits) analyzing a URL
|
||||
- Future non-AI intents (reminders, etc.) slot in as more case arms here
|
||||
|
||||
### Implementation
|
||||
|
||||
Add to `handle_message` after the prefix map block (after line 683, before line 685):
|
||||
|
||||
```bash
|
||||
# URL detect: bare HTTP(S) URLs → pos media grab
|
||||
local grab_url
|
||||
if grab_url="$(url_detect "$text")"; then
|
||||
log "grab: $grab_url"
|
||||
run_and_reply "pos media grab --best \"$grab_url\"" "$msg_id" 600
|
||||
return
|
||||
fi
|
||||
```
|
||||
|
||||
Add `url_detect` function before `handle_message`:
|
||||
|
||||
```bash
|
||||
# Detect a bare URL in message text. Extracts the first http(s) URL.
|
||||
# Returns 0 + prints the URL on success, 1 if no URL found.
|
||||
url_detect() {
|
||||
local text="$1"
|
||||
local url=""
|
||||
# Match http:// or https:// followed by non-whitespace
|
||||
if [[ "$text" =~ (https?://[^[:space:]]+) ]]; then
|
||||
url="${BASH_REMATCH[1]}"
|
||||
# Strip trailing punctuation that's likely not part of the URL
|
||||
url="${url%%[,.\)!?:;]}"
|
||||
url="${url%%\>*}"
|
||||
[ -n "$url" ] || return 1
|
||||
printf '%s' "$url"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
```
|
||||
|
||||
### Edge Cases
|
||||
|
||||
| Input | `url_detect` result | Routing |
|
||||
|-------|-------------------|---------|
|
||||
| `https://youtube.com/watch?v=xyz` | `https://youtube.com/watch?v=xyz` | grab → mp4 |
|
||||
| `check out https://youtu.be/xyz` | `https://youtu.be/xyz` | grab → mp4 |
|
||||
| `ai what is https://example.com` | `https://example.com` | grab (not AI!) |
|
||||
| `/status` | (no match) | command map |
|
||||
| `opencode check cpu` | (no match) | prefix map |
|
||||
| `hello world` | (no match) | AI bridge or unknown |
|
||||
|
||||
The `ai what is https://...` case is a minor trade-off — the user is more likely asking AI about the URL content than wanting to download it. However, this is a rare edge case, and the primary use case (bare URL from phone) is served correctly. If it becomes an issue, the URL detection could be refined to only trigger when the URL is the dominant content (e.g., text length < 2x URL length).
|
||||
|
||||
### Timeout
|
||||
|
||||
600 seconds (10 minutes). Reasonable for most videos. The listener's `run_and_reply` already handles timeout gracefully (returns "exit N" + output).
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 6: Tool Output Contract
|
||||
|
||||
### Problem
|
||||
|
||||
What does `pos media grab` print so the listener can reply to the user?
|
||||
|
||||
### Evidence
|
||||
|
||||
- `pos media mp3` line 85: `spawn "downloading audio → $OUT_DIR"` — prints via spawn (OK/FAIL + elapsed)
|
||||
- `pos media mp4` line 131: `spawn "downloading video → $OUT_DIR"` — same
|
||||
- The listener's `run_and_reply` (line 349-360): captures stdout+stderr, replies with output (truncated to 3800 chars)
|
||||
- Other tools: `pos system health` prints multi-line reports that the listener forwards verbatim
|
||||
|
||||
### Decision
|
||||
|
||||
`pos media grab` prints clean, user-friendly output to stdout:
|
||||
|
||||
```
|
||||
🎵 Downloaded: Artist - Title (3:42)
|
||||
📁 ~/Music/Artist - Title.mp3 (4.2 MB)
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
🎬 Downloaded: Video Title (10:15)
|
||||
📁 ~/Videos/Video Title.mp4 (125 MB)
|
||||
```
|
||||
|
||||
Error case:
|
||||
|
||||
```
|
||||
❌ Download failed: [yt-dlp error summary]
|
||||
```
|
||||
|
||||
### Implementation
|
||||
|
||||
After the mp3/mp4 delegation succeeds, the tool:
|
||||
|
||||
1. Runs `yt-dlp --print title --print duration_string --print filesize_approx "$URL"` to fetch metadata (fast, no download)
|
||||
2. Runs `stat --printf='%s' "$filepath"` to get actual file size
|
||||
3. Prints the formatted summary
|
||||
|
||||
For errors: capture stderr from the delegated command, print a clean error line.
|
||||
|
||||
### Why Not Just Forward spawn Output?
|
||||
|
||||
The `spawn` helper prints spinner text + OK/FAIL, which is good for terminal but not for Telegram. A structured summary (title, path, size) is more useful when you get a Telegram notification about a download.
|
||||
|
||||
### Why Metadata is a Separate Call?
|
||||
|
||||
The download itself (via mp3/mp4) doesn't expose title/size in a parseable format. The metadata call is fast (~1s) and gives us the info we need for the summary.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 7: Testing Strategy
|
||||
|
||||
### Problem
|
||||
|
||||
How to test without a real Telegram bot or yt-dlp network access?
|
||||
|
||||
### Evidence
|
||||
|
||||
- DEV.md "Testing tools that need root / systemd / missing deps" (lines 194-214)
|
||||
- Existing patterns: `FLAGS_DIR`, `SMB_CONF` env-seam approach; stub PATH fakes
|
||||
- The tool is a thin classifier + delegator — most logic is in URL matching
|
||||
|
||||
### Decision
|
||||
|
||||
Stub PATH approach with fake `pos media mp3`/`pos media mp4` scripts.
|
||||
|
||||
### Test Harness Layout
|
||||
|
||||
```
|
||||
/tmp/opencode/media-grab-test/
|
||||
├── run-tests.sh # Main test runner
|
||||
├── stubs/ # Fake binaries
|
||||
│ ├── pos # Fake pos dispatcher → delegates to stub mp3/mp4
|
||||
│ ├── pos-media-mp3 # Echoes args, creates a fake file
|
||||
│ ├── pos-media-mp4 # Echoes args, creates a fake file
|
||||
│ ├── yt-dlp # Echoes args, creates a fake file, prints metadata
|
||||
│ ├── stat # Returns fake file size
|
||||
│ └── ffmpeg # No-op
|
||||
└── fixtures/ # Test URLs (various domains)
|
||||
```
|
||||
|
||||
### Test Cases (20+)
|
||||
|
||||
| # | Test | Input | Expected |
|
||||
|---|------|-------|----------|
|
||||
| 1 | YouTube Music URL | `https://music.youtube.com/watch?v=xyz` | Routes to mp3 |
|
||||
| 2 | YouTube video URL | `https://youtube.com/watch?v=xyz` | Routes to mp4 --best |
|
||||
| 3 | YouTube short URL | `https://youtu.be/xyz` | Routes to mp4 --best |
|
||||
| 4 | YouTube shorts URL | `https://youtube.com/shorts/xyz` | Routes to mp4 --best |
|
||||
| 5 | SoundCloud URL | `https://soundcloud.com/artist/track` | Routes to mp3 |
|
||||
| 6 | Bandcamp URL | `https://bandcamp.com/album/track` | Routes to mp3 |
|
||||
| 7 | Vimeo URL | `https://vimeo.com/123456` | Routes to mp4 --best |
|
||||
| 8 | Unknown domain | `https://example.com/video.mp4` | Routes to mp4 --best (default) |
|
||||
| 9 | `--audio` override | `--audio https://youtube.com/watch?v=xyz` | Routes to mp3 |
|
||||
| 10 | `--video` override | `--video https://soundcloud.com/track` | Routes to mp4 |
|
||||
| 11 | `--worst` flag | `--worst https://youtube.com/watch?v=xyz` | Routes to mp4 --worst |
|
||||
| 12 | `--best` explicit | `--best https://youtube.com/watch?v=xyz` | Routes to mp4 --best |
|
||||
| 13 | `--dry-run` | `--dry-run https://youtube.com/watch?v=xyz` | Prints command, no download |
|
||||
| 14 | No URL | (empty) | usage |
|
||||
| 15 | `--help` | `--help` | Shows usage |
|
||||
| 16 | Invalid URL | `not-a-url` | Error: "not a valid URL" |
|
||||
| 17 | GRAB_DEFAULT=audio | `GRAB_DEFAULT=audio https://unknown.com/x` | Routes to mp3 |
|
||||
| 18 | `--output` override | `--output /tmp/test https://...` | Passes to mp3/mp4 |
|
||||
| 19 | HTTP URL | `http://youtube.com/watch?v=xyz` | Routes to mp4 (http not https) |
|
||||
| 20 | `--no-playlist` | `--no-playlist https://youtube.com/playlist?list=xyz` | Passes to mp3/mp4 |
|
||||
|
||||
### Stub Implementation
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Fake pos-media-mp3 — records args, creates a dummy file
|
||||
OUT_DIR="${OUT_DIR:-/tmp/test-output}"
|
||||
mkdir -p "$OUT_DIR"
|
||||
URL="${*: -1}" # last arg is URL
|
||||
echo "pos-media-mp3 called with: $*" > /tmp/test-output/mp3.log
|
||||
touch "$OUT_DIR/test.mp3"
|
||||
echo "Downloaded: Test Song (3:42)"
|
||||
echo "📁 $OUT_DIR/test.mp3 (4.2 MB)"
|
||||
```
|
||||
|
||||
### What NOT to Test
|
||||
|
||||
- yt-dlp actual behavior (covered by yt-dlp's own tests)
|
||||
- The listener integration (tested manually or with a Telegram bot test harness)
|
||||
- URL regex edge cases that are handled by bash `[[ =~ ]]` (well-tested in bash)
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 8: Dependency Handling
|
||||
|
||||
### Problem
|
||||
|
||||
What deps does `pos media grab` declare, and where?
|
||||
|
||||
### Evidence
|
||||
|
||||
- `pos media mp3` lines 17-18: guards `yt-dlp` and `ffmpeg`
|
||||
- `pos media mp4` lines 17-18: same guards
|
||||
- `pos media grab` delegates to mp3/mp4, which handle their own deps
|
||||
- Grab itself only needs bash builtins for URL classification
|
||||
|
||||
### Decision
|
||||
|
||||
No deps guard in `pos media grab`. Deps are the responsibility of mp3/mp4.
|
||||
|
||||
### Rationale
|
||||
|
||||
Grab is a pure classifier + delegator. It doesn't call yt-dlp directly. If mp3/mp4 are called and their deps are missing, they'll error with their own helpful messages. Adding redundant deps guards in grab would:
|
||||
- Duplicate error messages
|
||||
- Create maintenance overhead when mp3/mp4 deps change
|
||||
- Violate single-responsibility (grab classifies, mp3/mp4 download)
|
||||
|
||||
### Exception
|
||||
|
||||
If grab needs `stat` for file size reporting (Decision 6), that's a standard coreutil — no guard needed on Debian/Ubuntu.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Summary: Implementation Scope
|
||||
|
||||
### Files to Create
|
||||
|
||||
| File | Responsibility |
|
||||
|------|---------------|
|
||||
| `bin/pos-media-grab` | URL classifier, delegate to mp3/mp4, clean output |
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `bin/pos-communication-telegram-listener` | Add `url_detect` function + URL routing step in `handle_message` |
|
||||
|
||||
### Files to Update (Docs)
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `DOC/POS.md` | Add `pos media grab` row to media table + detail block |
|
||||
| `DOC/HOWTO.md` | Add media section reference if not already present |
|
||||
| `DOC/howto/media.md` | Add `grab` usage example |
|
||||
| `DOC/AGENT_Context_Project.md` | (auto via `make gen`) tree, dispatch, filetable |
|
||||
| `AGENT_TODO.md` | Move to Done |
|
||||
|
||||
### Post-Implementation Gates
|
||||
|
||||
```bash
|
||||
chmod +x bin/pos-media-grab
|
||||
bash -n bin/pos-media-grab
|
||||
bash -n bin/pos-communication-telegram-listener
|
||||
make gen && make check && make lint # 0 FAIL, 0 WARN
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Handoff
|
||||
|
||||
Status: DECISION_READY
|
||||
|
||||
Problem: Telegram bot needs intelligent URL routing to auto-download content as audio/video.
|
||||
|
||||
Decision: New `bin/pos-media-grab` tool (domain classifier + mp3/mp4 delegator) + listener URL routing step between prefix map and AI bridge.
|
||||
|
||||
Reasoning: Evidence-based — domain classification is fast/reliable for the primary use case, existing mp3/mp4 tools handle the heavy lifting, listener routing chain slots naturally for new intents.
|
||||
|
||||
Ownership: `bin/pos-media-grab` (new tool), listener integration in existing listener.
|
||||
|
||||
Interfaces: grab is called by the listener; grab calls mp3/mp4.
|
||||
|
||||
Approved scope: `bin/pos-media-grab` (new), listener modification (add ~30 lines), doc updates. No changes to mp3/mp4.
|
||||
|
||||
Explicitly out of scope: Changes to mp3/mp4 tools, feature flags, complex config.
|
||||
|
||||
Constraints: Must pass `make gen && make check && make lint` (0 FAIL, 0 WARN). Follow all pos conventions (shebang, strict mode, `# POS:` header, deps-before-help pattern, `common.sh` sourcing).
|
||||
|
||||
Verification: Stub-PATH test suite (20+ cases), `bash -n` on both modified files, `make gen && make check && make lint`.
|
||||
|
||||
Risks:
|
||||
- URL regex may not match all URL formats (mitigated: covers http/https, the dominant formats)
|
||||
- `--best` may not be what the user always wants for video (mitigated: `--worst` override)
|
||||
- Metadata fetch adds ~1s latency (mitigated: acceptable for download context)
|
||||
|
||||
Recommended next agent: Builder
|
||||
|
||||
Reason: Architecture is fully defined with specific file paths, function signatures, listener line changes, config keys, and test plan. Ready for implementation.
|
||||
|
||||
Architect changes: None — pure decision record.
|
||||
@@ -1,307 +0,0 @@
|
||||
# AI cost & session-window design — OpenRouter 402 + bounded session memory
|
||||
|
||||
**Date:** 2026-09-06
|
||||
**Author:** Architect (big-pickle)
|
||||
**HEAD:** 8ce5479 (clean tree)
|
||||
**Mode:** design-only (no file edits besides this report)
|
||||
|
||||
## TL;DR
|
||||
|
||||
Two tightly associated defects in `pos ai` / `bin/pos-ai`:
|
||||
|
||||
1. **402 root cause** — no provider ever sends `max_tokens`, so OpenRouter's credit
|
||||
pre-check charges the routed model's full worst-case output (131072 on
|
||||
`openrouter/auto`) and rejects balances under that. Fix: send an explicit capped
|
||||
`max_tokens`.
|
||||
2. **Unbounded-ish session** — `MAX_SESSION_TURNS=40` counts *messages* (20
|
||||
exchanges); user wants "last 5 req/response" = 10 messages.
|
||||
|
||||
**Decisions:** add `AI_MAX_TOKENS` (num, default **2048**) honored by openrouter
|
||||
(body `max_tokens`) and gemini (`generationConfig.maxOutputTokens`); **skip** llamacpp.
|
||||
Add `AI_SESSION_TURNS` (num, default **40** — backward compatible) honored lazily in
|
||||
`session_push`; user sets **10** for 5 pairs. Both vars declared in the `@General`
|
||||
section of the `# POS_CONFIG:` header, docs updated. No chat/alias wrapper changes.
|
||||
|
||||
**Open items:** none blocking. Tester should add provider-body + session-pruning
|
||||
coverage (see §Testing).
|
||||
|
||||
---
|
||||
|
||||
## Verified fact confirmation (A–D)
|
||||
|
||||
All user-reported facts confirmed against source at HEAD:
|
||||
|
||||
**A — 402 root cause: CONFIRMED.**
|
||||
- `lib/ai-providers/openrouter.sh:22-23` — body is only `{model,messages}`; no
|
||||
`max_tokens`. `:24-29` POSTs straight to OpenRouter with unchanged body.
|
||||
- `lib/ai-providers/gemini.sh:17-19` — body only `{contents,...}`; no
|
||||
`generationConfig`. (`:20-23` adds only `systemInstruction`.)
|
||||
- `lib/ai-providers/llamacpp.sh:32-33` — body `{model,messages,stream:false}`.
|
||||
- Conclusion: none carry a generation cap → OpenRouter 402 with the user's thin
|
||||
balance. Fix is to send an explicit `max_tokens`.
|
||||
|
||||
**B — session window: CONFIRMED.**
|
||||
- `bin/pos-ai:25` `MAX_SESSION_TURNS=40`.
|
||||
- `session_push()` `bin/pos-ai:285-290` → `'.messages |= .[-"$MAX_SESSION_TURNS":]'`
|
||||
prunes to last N **messages** (40 msgs = 20 exchanges). User wants 5 pairs = 10 messages.
|
||||
|
||||
**C — config surface: CONFIRMED.**
|
||||
- `# POS_CONFIG:` header `bin/pos-ai:6`, scope `ai | ai.env`.
|
||||
- `num:` type already used in the same header (`LLAMACPP_CTX_SIZE=num:…`,
|
||||
`LLAMACPP_GPU_LAYERS=num:…`).
|
||||
- Validation: `lib/config-ui.sh:419` `*,num,*) [[ "$val" =~ ^-?[0-9]+$ ]]` — integer-only
|
||||
on *entry*; empty input ="kept current value" (`:468-471`); `-` = clear (`:472-477`).
|
||||
So `num:` + empty/unset → falls back to code default. Clean.
|
||||
- Env precedence: `load_env_file` (`lib/config-ui.sh:336-357`) exports a file key only
|
||||
when the variable is not already set in the environment (`:351-353`) → env beats
|
||||
file beats default. Providers read config via env (`AI_API_KEY` pattern).
|
||||
- `PROVIDER_CONFIG` headers exist on `openrouter.sh:7-8`, `gemini.sh:7-8`,
|
||||
`llamacpp.sh:7` for *provider-specific* keys.
|
||||
|
||||
**D — provider resolution: CONFIRMED.**
|
||||
- `bin/pos-ai:679-683` — `--provider` flag > `AI_PROVIDER` env > `gemini`.
|
||||
- `load_config()` `bin/pos-ai:141-151` loads `ai.env` (+ legacy files); called by
|
||||
`resolve_key` (line 155) and at provider resolution (line 681).
|
||||
- Call site `bin/pos-ai:522` (ask) and `:557` (chat): `provider_generate "$model" "$messages" "$system"`.
|
||||
|
||||
**Gates/tests scan:**
|
||||
- No test pins `MAX_SESSION_TURNS`, the provider request bodies, or header text
|
||||
(`t-config-precedence.sh` targets `pos-ai-server`, a separate tool). No forced
|
||||
test update.
|
||||
- `DOC/POS.md` has a hand-maintained ai.env config table (`:88-97`) and mentions
|
||||
"capped at 40 turns" at `:64`. `DOC/HOWTO.md:45` and `DOC/AGENT_Context_Project.md:491`
|
||||
list ai.env vars (hand-maintained). All need doc rows/bumps for the new vars.
|
||||
|
||||
---
|
||||
|
||||
## Decision 1: `AI_MAX_TOKENS` — cap generation tokens
|
||||
|
||||
**Status: [DECIDED]**
|
||||
|
||||
### Options & trade-offs
|
||||
|
||||
**Option 1 (chosen) — single global `AI_MAX_TOKENS=num`, default 2048, honored by remote providers (openrouter + gemini); skip llamacpp.**
|
||||
- *Advantages:* smallest change that fixes the 402 (OpenRouter pre-check sees a
|
||||
capped cost) and is a real per-request cost ceiling; one var, one default; fits
|
||||
the existing `AI_*` env naming and the tool-level `@General` config section; no
|
||||
new per-provider surface.
|
||||
- *Costs:* remote providers share one ceiling (no per-provider cap without user
|
||||
intervention).
|
||||
- *Risks:* a too-low cap truncates long answers — mitigated by default 2048 being
|
||||
ample for terse `ask`/`chat` CLI answers; user can raise it.
|
||||
- *Reasoning for default 2048:* conservative (user's balance affords ~4511 tokens at
|
||||
routed price, so 2048 passes the pre-check with margin) while being a practical,
|
||||
real ceiling. 2048 tokens ≈ several thousand chars — plenty for the terse,
|
||||
commands-first assistant role this tool plays.
|
||||
|
||||
**Option 2 — per-provider caps via `PROVIDER_CONFIG` (e.g. `AI_MAX_TOKENS` on openrouter.sh, gemini.sh).**
|
||||
- *Advantages:* independent ceilings per provider.
|
||||
- *Costs:* two declarations, redundant section plumbing, and the *default* (which is
|
||||
the entire point) still has no shared home → awkward. `PROVIDER_CONFIG` is for
|
||||
provider-specific concerns; a cost ceiling + 402 pre-check over both remote
|
||||
providers is tool-level, not provider-specific.
|
||||
|
||||
**Option 3 — no gemini cap; only openrouter.**
|
||||
- *Advantages:* minimal (402 only affects OpenRouter).
|
||||
- *Costs:* leaves Gemini without any cost ceiling while introducing the same var —
|
||||
inconsistent, and Gemini's own pricing can surprise. Rejected.
|
||||
|
||||
**Option 4 — include llamacpp too (`max_tokens` in body; it accepts it).**
|
||||
- *Advantages:* provider parity on the OpenAI-compatible endpoint.
|
||||
- *Costs:* local & free — no credit pre-check, no cost. Adds surface with zero user
|
||||
benefit. Rejected on the "smallest sufficient design" principle.
|
||||
|
||||
### Implementation contract
|
||||
- Env var name: **`AI_MAX_TOKENS`**, type `num`, default **2048**.
|
||||
- OpenRouter body (`openrouter.sh:22-23`): add `max_tokens`.
|
||||
- Gemini body (`gemini.sh:17-19`): add `generationConfig.maxOutputTokens`.
|
||||
- llamacpp: **no change**.
|
||||
- Providers read `"${AI_MAX_TOKENS:-2048}"` from env; value is present because
|
||||
`load_config` runs before `provider_generate` (resolve path confirmed in D).
|
||||
- Declare in `@General` section of `# POS_CONFIG:` header (`bin/pos-ai:6`).
|
||||
|
||||
---
|
||||
|
||||
## Decision 2: `AI_SESSION_TURNS` — bounded session window
|
||||
|
||||
**Status: [DECIDED]**
|
||||
|
||||
### Options & trade-offs
|
||||
|
||||
**Option 1 (chosen) — new `AI_SESSION_TURNS=num`, default 40 (unchanged), resolved lazily in `session_push`.**
|
||||
- *Advantages:* fully backward compatible — no silent memory truncation for existing
|
||||
users. The user's "last 5 req/response" = setting `AI_SESSION_TURNS=10`. One var,
|
||||
one default.
|
||||
- *Costs:* existing users must opt in (they already have the 40 behavior, so no
|
||||
regression).
|
||||
- *Why keep default 40:* backward compatibility is a hard project value; silently
|
||||
shifting the default changes session context for *every* user, discards history
|
||||
they may rely on, and is a behavioral change not requested globally (only for this
|
||||
user). Keep 40.
|
||||
|
||||
**Option 2 — change the default to 10.**
|
||||
- *Advantages:* meets the stated want out-of-the-box.
|
||||
- *Costs:* silent behavior change for all users; discards memory; not requested
|
||||
globally. Rejected — keep the change opt-in via the new var.
|
||||
|
||||
### Semantics (must be documented)
|
||||
The existing prune is `.messages |= .[-N:]` where N counts **messages** — 2 messages
|
||||
per exchange. So `AI_SESSION_TURNS=10` ⇒ last **5** exchanges (5 user + 5 assistant).
|
||||
The config description must state: "message count (2 per exchange); 10 = last 5
|
||||
exchanges".
|
||||
|
||||
### Implementation constraint — lazy resolution (important)
|
||||
`MAX_SESSION_TURNS` is currently assigned at `bin/pos-ai:25`, which executes at
|
||||
top-level **before** `load_config` is first called (line 681). If we wrote
|
||||
`MAX_SESSION_TURNS="${AI_SESSION_TURNS:-40}"` at line 25, an `AI_SESSION_TURNS` set
|
||||
*only in ai.env* would not yet be loaded → always 40.
|
||||
|
||||
Therefore:
|
||||
- Keep **line 25** as-is (`MAX_SESSION_TURNS=40`), used for the help text (`:67`
|
||||
shows the default, accurate).
|
||||
- In `session_push()` (lines 285-290), resolve **lazily**:
|
||||
`local n="${AI_SESSION_TURNS:-$MAX_SESSION_TURNS}"` and use `$n` in the jq prune.
|
||||
Because `session_push` runs inside `cmd_ask`/`cmd_chat` — after `load_config`
|
||||
(via `resolve_key` at `:499`/`:538`) has exported `AI_SESSION_TURNS` into the
|
||||
process env — the ai.env value is honored. Shell-exported `AI_SESSION_TURNS`
|
||||
wins too (env-wins in `load_env_file`).
|
||||
- Help text `bin/pos-ai:67` stays accurate ("capped at $MAX_SESSION_TURNS turns")
|
||||
since the default remains 40. Optionally add a `Config:` help line documenting the
|
||||
var — recommended, and it confirms "turns = messages, 10 = 5 pairs".
|
||||
|
||||
---
|
||||
|
||||
## Decision 3: `# POS_CONFIG:` header change
|
||||
|
||||
**Status: [DECIDED]**
|
||||
|
||||
- Single `@General` section addition (not per-provider `PROVIDER_CONFIG`):
|
||||
- `AI_MAX_TOKENS=num:Max output tokens per request (default 2048; OpenRouter/Gemini cost cap)`
|
||||
- `AI_SESSION_TURNS=num:Session message cap — 2 per exchange (default 40 = 20 exchanges; 10 = last 5)`
|
||||
- Place both in the `@General` section alongside `AI_SYSTEM_PROMPT` (end of the
|
||||
long header line, `bin/pos-ai:6`).
|
||||
- **Why @General, not PROVIDER_CONFIG:** the default is shared across providers
|
||||
(2048, 40) and both caps are tool-level concerns. `PROVIDER_CONFIG` is reserved for
|
||||
provider-specific keys (API keys, models). A single global declaration is the
|
||||
cleanest and avoids duplicating the default in two provider files.
|
||||
- `num:` type confirmed suitable: `lib/config-ui.sh:419` enforces integer on entry;
|
||||
empty=keep current, `-`=clear (`:468-477`); unset → code default. No empty-parse
|
||||
concern.
|
||||
- Provider bodies consume the vars from env, so no `PROVIDER_CONFIG` additions are
|
||||
needed on `openrouter.sh`/`gemini.sh`. (They could be added later if per-provider
|
||||
caps are ever wanted — out of scope now.)
|
||||
|
||||
---
|
||||
|
||||
## Decision 4: Docs & gates
|
||||
|
||||
**Status: [DECIDED]**
|
||||
|
||||
- **`DOC/POS.md`** (hand-maintained):
|
||||
- ai.env table (`:88-97`): add rows for `AI_MAX_TOKENS` (no/`2048`/"Max output
|
||||
tokens per request (OpenRouter/Gemini cost cap)") and `AI_SESSION_TURNS`
|
||||
(no/`40`/"Session message cap — 2 per exchange; 10 = last 5 exchanges"). The
|
||||
`AI_SYSTEM_PROMPT` row (`:93`) is the placement anchor.
|
||||
- Line 64 text "capped at 40 turns" remains true (default unchanged) — no edit
|
||||
strictly needed, but a short parenthetical "(configurable via AI_SESSION_TURNS)"
|
||||
is recommended.
|
||||
- **`DOC/HOWTO.md:45`** — append `AI_MAX_TOKENS`, `AI_SESSION_TURNS` to the listed
|
||||
ai.env vars.
|
||||
- **`DOC/AGENT_Context_Project.md:491`** — append the two vars to the ai.env
|
||||
summary parenthetical (hand-maintained).
|
||||
- **`make gen`**: header text change does not add commands/subcommands/flags, so the
|
||||
generated tree/dispatch tables are unaffected; `completions/pos.bash` config-scope
|
||||
table regenerates to include the new keys. Run `make gen` (deterministic, `LC_ALL=C`
|
||||
per convention), then `make check`, then `make lint`.
|
||||
- **Line-count rows** above the filetable marker in `AGENT_Context_Project.md`: only
|
||||
bump a row if a `pos-*`/`lib/*` file's length changes (it does — lib/ai-providers
|
||||
grow; bin/pos-ai grows). Do not touch rows for files that don't change.
|
||||
- **tests**: existing suite does not pin the session default or provider bodies, so
|
||||
nothing is *forced*. Recommend new coverage (§Testing).
|
||||
|
||||
---
|
||||
|
||||
## Decision 5: Scope fence
|
||||
|
||||
**Status: [DECIDED]**
|
||||
|
||||
**Approved outcome:** OpenRouter 402 eliminated (explicit capped `max_tokens` on
|
||||
remote providers) and session history bounded via configurable `AI_SESSION_TURNS`.
|
||||
|
||||
**In-scope files:**
|
||||
- `bin/pos-ai` — `# POS_CONFIG:` header (line 6, @General additions); `session_push`
|
||||
lazy `AI_SESSION_TURNS` resolution (lines 285-290); optional `Config:` help lines
|
||||
for the two vars. Line 25 stays `MAX_SESSION_TURNS=40`.
|
||||
- `lib/ai-providers/openrouter.sh` — add `max_tokens` to body (lines 22-23).
|
||||
- `lib/ai-providers/gemini.sh` — add `generationConfig.maxOutputTokens` (lines 17-19).
|
||||
- `lib/ai-providers/llamacpp.sh` — **no change**.
|
||||
- Docs (hand-maintained): `DOC/POS.md`, `DOC/HOWTO.md`, `DOC/AGENT_Context_Project.md`.
|
||||
- tests (later, Tester).
|
||||
|
||||
**Allowed interface changes:** two new config keys in scope `ai`; provider request
|
||||
bodies gain a token cap. Provider call signature `provider_generate "$model" "$messages" "$system"`
|
||||
is unchanged.
|
||||
|
||||
**Explicitly out of scope:** `bin/pos-ai-server`, chat/alias wrappers, `pos-ai-alias`,
|
||||
HuggingFace downloader, other tools, per-provider caps, changing `MAX_SESSION_TURNS`
|
||||
default, any llmacpp body change.
|
||||
|
||||
**Architectural constraints:**
|
||||
- No new provider-side config plumbing; vars read from env (`AI_*` convention).
|
||||
- Backward compatible: session default 40 and empty/unset values fall back to
|
||||
documented defaults.
|
||||
- Deterministic `make gen`; `make check` + `make lint` green (definition of done).
|
||||
|
||||
**Open risks:**
|
||||
- `AI_MAX_TOKENS` too low truncates long `--full` answers — default 2048 mitigates;
|
||||
user can raise.
|
||||
- env-vs-file precedence: exported env var beats config file (per `load_env_file`) —
|
||||
expected and documented.
|
||||
|
||||
**Verification:**
|
||||
- `bash -n` clean; `make gen`/`make check`/`make lint` green.
|
||||
- A 402 reproduction no longer triggers when `AI_MAX_TOKENS` is set/at default.
|
||||
- `AI_SESSION_TURNS=10` prunes the session to last 5 exchanges.
|
||||
|
||||
---
|
||||
|
||||
## Testing budget (suggestion for Tester / Builder verification)
|
||||
|
||||
- **Provider body cap:** source each provider adapter in a sandbox with `curl`
|
||||
stubbed, assert the request JSON contains `max_tokens` (openrouter) / a truthy
|
||||
`generationConfig.maxOutputTokens` (gemini). Pattern: `t-config-precedence.sh` style
|
||||
curl-log capture with a fake `curl` in `PATH` (see existing tests).
|
||||
- **Session pruning:** drive `session_push` with a crafted messages JSON and
|
||||
`AI_SESSION_TURNS=10`, assert exactly the last 10 messages remain (5 pairs); and
|
||||
`AI_SESSION_TURNS` unset → 40 retained (default commits to backward compat).
|
||||
- **Help/default fidelity:** `pos ai --help` still shows 40; `pos config ai` lists the
|
||||
two new `num:` keys and rejects a non-integer (`cfg_validate`).
|
||||
- **Regression:** existing `t-config-precedence.sh`, `run-tests` full suite green.
|
||||
|
||||
---
|
||||
|
||||
## Handoff
|
||||
|
||||
**Status:** DECISION_READY
|
||||
|
||||
**Problem:** OpenRouter 402 (no `max_tokens` → full worst-case pre-check) and
|
||||
unbounded session memory; user wants 5 req/response.
|
||||
|
||||
**Decision:** add `AI_MAX_TOKENS=num` (default 2048) honored by openrouter + gemini
|
||||
(skip llamacpp); add `AI_SESSION_TURNS=num` (default 40, backward compatible) resolved
|
||||
lazily in `session_push`; both in `@General` config section; docs updated.
|
||||
|
||||
**Ownership:** `bin/pos-ai` + `lib/ai-providers/{openrouter,gemini}.sh` (+ docs).
|
||||
|
||||
**Interfaces:** two new `ai`-scope config keys; provider bodies gain a token cap.
|
||||
Call signature unchanged.
|
||||
|
||||
**Approved scope / constraints / verification / out-of-scope:** see Decision 5.
|
||||
|
||||
**Risks:** see Decision 5 (token-cap truncation; env-vs-file precedence — all mitigated).
|
||||
|
||||
**Recommended next agent:** **Builder**
|
||||
|
||||
**Reason:** the architecture and scope are fully specified with exact line-level
|
||||
changes (no architectural ambiguity left). Builder can implement without making
|
||||
architecture decisions. Tester follows for the recommended coverage.
|
||||
@@ -1,177 +0,0 @@
|
||||
# AI Server Start Breakage — Root Cause + Fix Design Decisions
|
||||
|
||||
**Date:** 2026-09-06
|
||||
**Architect:** Design pass over the Detective report `AgentsReport/detective/2026-09-06_ai-server-breakage.md` (root cause classified **FACT**).
|
||||
**Constraint:** No code changes in this pass; decisions + ratified fix scope (F1-F7) for the Builder; test-fixture requirements for the Tester. Preserve the existing Bash tool architecture and the D-A..D-F stabilization decisions.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
1. **DQ1 — Validation stays help-gated.** The SIGPIPE race (F2) is the only validation defect; help-based validation is binary-reality-based and correct once deterministic. Version is cosmetic. "unknown" means: validate against real `--help` regardless, message without the version. NO version→capability map. [DECIDED]
|
||||
2. **DQ2 — `resolve_model` gains file-inside-dir expansion.** `$HF_DOWNLOAD_DIR/<name>`/dir → exactly-one `*.gguf` resolves to it; multiple → list + err ("pick one"); zero → err as today. Also accept `<name>/<file>.gguf`. Precedence: absolute path > dir-with-exactly-one-gguf > `$HF_DOWNLOAD_DIR/<name>.gguf` flat file > `$HF_DOWNLOAD_DIR/<name>/<file>` > relative-as-is. Existing bare-file behavior unchanged (backward compatible). [DECIDED]
|
||||
3. **DQ3 — User-bus pre-flight by default, plus `--no-unit` direct-run escape hatch (option c).** Pre-flight aborts BEFORE writing the unit (no orphan); `--no-unit` runs the server directly under nohup+pidfile for headless/SSH boxes. Shared helper `ensure_user_bus` lives in `lib/common.sh` (all three tools source it). Matrix-listener + network-download get the same pre-flight. Remediation text includes `export XDG_RUNTIME_DIR=/run/user/$(id -u)` and `sudo loginctl enable-linger $(id -un)`. [DECIDED]
|
||||
4. **DQ4 — `find_llamacpp` drops bare `server` and `llama.cpp/server`.** Keep `llama-server`, `llama-server-cuda`. Do NOT add `llama-server-mtl` (Mac-only; out of target). Stale-unit: start F4 pre-flight WARNS about an existing unit, does not delete it (never gratuitously remove user state). [DECIDED]
|
||||
5. **DQ5 — Always pin `--port $PORT`.** Once F2 makes validation reliable, ExecStart always carries `--port $PORT` (help-gated, so a build without `--port` omits it). No adapter probing fallback change needed (deferred). [DECIDED]
|
||||
6. **DQ6 — Installer sanity IS in scope.** `apps/ai/llamacpp.sh` post-install runs `llama-server --version` (2>&1) + `--help` and errs if the binary doesn't execute; verifies the symlink target exists. [DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## DQ1: Validation posture when version is unknown
|
||||
|
||||
### Decision
|
||||
**Keep help-based validation as the single source of truth. Do NOT add a version→capability map.** "unknown" means: still validate against the real `--help` output; only the version string in log/error messages is dropped (message proceeds without version).
|
||||
|
||||
### Rationale
|
||||
Help-based validation reflects what the actual installed binary supports — it is always correct once the SIGPIPE race (F2) is fixed, because it peers at real capability output. Adding a version→capability table would introduce a second, version-coupled source of truth that drifts the moment llama.cpp adds/renames flags; its only benefit would be to decorate messages with a version string, which is cosmetic. The version is useful for *debugging* ("which build am I running") but must never gate validation. "unknown" therefore degrades only to "no version in messages", not to "no validation".
|
||||
|
||||
### How "unknown" behaves
|
||||
- `detect_llama_version` returns "unknown" only when `--version` is genuinely unreadable (no binary) — after F1, stderr is captured and the output IS readable for real builds.
|
||||
- With "unknown", `validate_*_flags` still fetch `--help` and judge flags normally; error texts simply omit the version token.
|
||||
- F1's broadened regex handles both semver and build-only strings, so "unknown" becomes rare (only truly missing binary/--version).
|
||||
|
||||
[DELIVERY: F1 capture-2>&1 + broadened regex; F2 race fix. No capability map.]
|
||||
|
||||
---
|
||||
|
||||
## DQ2: `resolve_model` directory expansion
|
||||
|
||||
### Decision
|
||||
Extend `resolve_model` (`bin/pos-ai-server:230-264`) to resolve a directory-typed explicit argument when it contains a usable model file, with exact precedence. Interactive `pick_model` stays recursive (already lists flat `.gguf` files one level up via `find -type f`; confirmed at `:210` — it lists each dir's files, so no F3 change needed there).
|
||||
|
||||
### Precedence (highest → lowest)
|
||||
1. **Absolute path** that is a file (`[ -f "$explicit" ]`) — unchanged; if the absolute path is a directory, treat as directory case below.
|
||||
2. **`$HF_DOWNLOAD_DIR/<name>` is a DIRECTORY** containing exactly one `*.gguf` → resolve to that file. If multiple `*.gguf` → print all and `err "pick one: <name>/<file>"`. If zero → fall through / err (never silently pick).
|
||||
3. **`$HF_DOWNLOAD_DIR/<name>.gguf` flat file** — unchanged (`[ -f "$candidate" ]`).
|
||||
4. **`$HF_DOWNLOAD_DIR/<name>/<file>.gguf`** (slug/file form) — resolve to `$HF_DOWNLOAD_DIR/<name>/<file>.gguf`.
|
||||
5. **Relative-as-is** — unchanged (`[ -f "$explicit" ]`).
|
||||
|
||||
### Backward compatibility
|
||||
Bare existing behavior (flat file path, absolute file path, relative path) is fully preserved — the new directory-expansion branches only fire where the current code would have errored with "Model not found". No silent picking: multiple matches always err with the disambiguating list.
|
||||
|
||||
### Exact rule (multiple-gguf dir)
|
||||
```
|
||||
if a dir contains >1 *.gguf: print each as "<dirname>/<file>" and err "model dir contains multiple — pick one"
|
||||
```
|
||||
This matches the fixture expectation (multi-gguf dir case errors with the file list).
|
||||
|
||||
[DELIVERY: F3 resolve_model expansion; no pick_model change.]
|
||||
|
||||
---
|
||||
|
||||
## DQ3: User-bus failure handling (scope across tools)
|
||||
|
||||
### Decision
|
||||
**Option (c): pre-flight err by default, `--no-unit` direct-run as escape hatch.**
|
||||
|
||||
- `ensure_user_bus` helper in **`lib/common.sh`** (the base lib already sourced by all three tools via the fallback chain; it is the natural shared home for a user-bus guard — the alterative, config-ui.sh, is a config/validation lib, not an execution helper, and would be a semantic misfit).
|
||||
- Wired into the unit-install paths of `bin/pos-ai-server:605`, `bin/pos-communication-matrix-listener:341`, and `bin/pos-network-download:191` — all three call it BEFORE `systemctl --user daemon-reload` and BEFORE the unit is written, so a failure leaves no orphaned unit.
|
||||
- On failure, `ensure_user_bus` `err`s with remediation text:
|
||||
- `export XDG_RUNTIME_DIR=/run/user/$(id -u)` (if the dir exists)
|
||||
- `sudo loginctl enable-linger $(id -un)`
|
||||
- plus the general "connect to the user's systemd bus" guidance.
|
||||
|
||||
### Why nohup/pidfile for `--no-unit` (direct-run)
|
||||
Headless/SSH boxes may lack a working user bus permanently; being unable to serve a model at all is worse than a supervisor-less process. `--no-unit` on `pos ai server start` execs `llama-server` directly under `nohup ... >$log 2>&1 &`, writes a pidfile under `$RUN_DIR`/`$HF_DOWNLOAD_DIR` sibling (e.g. `$RUN_DIR/pos-ai-server.pid`), and prints the log path + `kill $(cat pidfile)` hint. This is a thin escape hatch, NOT a second supervisor; supervision stays systemd when available.
|
||||
|
||||
### Why the helper lives in common.sh (not config-ui)
|
||||
- All three tools source `lib/common.sh` already (via the `$(dirname)/../lib/common.sh` fallback chain). Config-ui.sh is not guaranteed present in the standalone communication tools' spirit, and it is semantically a *config* lib. An *execution pre-flight* guard belongs with the other `run`/`spawn` execution helpers in common.sh.
|
||||
- Same one-line helper (declared with a `declare -F` guard like user-timers-lib.sh does) so all three tools share remediation text and behavior, and it can be unit-tested once.
|
||||
|
||||
### Minimal viable behavior for THIS pass
|
||||
- `ensure_user_bus` in common.sh.
|
||||
- Pre-flight wired into pos-ai-server, matrix-listener, network-download before unit write.
|
||||
- `--no-unit` direct-run flag on **pos-ai-server only** (the reported tool, and the only one whose unit manages llama-server). matrix-listener and network-download get the pre-flight but NO direct-run escape hatches this pass (their stateful daemons genuinely need systemd; a direct-run fallback would be a larger design change and is deferred).
|
||||
|
||||
[DELIVERY: F4 pre-flight + `--no-unit` on pos-ai-server.]
|
||||
|
||||
---
|
||||
|
||||
## DQ4: `find_llamacpp` candidates
|
||||
|
||||
### Decision
|
||||
New candidate list: **`llama-server`, `llama-server-cuda`** only. Drop bare `server` and `llama.cpp/server`. Do NOT add `llama-server-mtl`.
|
||||
|
||||
### Rationale
|
||||
- Bare `server` is an unrelated-generic-name hazard (fixture-proven: picks an unrelated binary, then ALL defaults rejected) — genuinely dangerous, drop it.
|
||||
- `llama.cpp/server` is a relative path-like token that `command -v` can only match as a literal filename `llama.cpp/server` — not a real on-PATH name for the archive's `/usr/local/bin/llama-server` layout; it provides no value, drop it.
|
||||
- `llama-server-cuda` is a genuine distinct binary name for some CUDA-series builds — keep it.
|
||||
- `llama-server-mtl` is the Apple Metal (macOS) binary name; this repo targets Debian/Ubuntu Linux only (`apps/ai/llamacpp.sh` errors on non-x64/arm64) — out of scope.
|
||||
- If `llama-server` and `llama-server-cuda` are both absent, the existing `find_llamacpp`-failure `err` already carries the installer hint (unchanged).
|
||||
|
||||
### Stale-unit handling on start (F4 interaction)
|
||||
`cmd_start` pre-flight: if `$USER_SYSTEMD_DIR/$SERVICE` already exists, **warn** that a unit is present (it may be stale/orphaned from a prior failed start) and that it will be overwritten; proceed to write. **Do NOT delete it.** `cmd_stop` (`:628-641`) is the only stated removal path and remains the exclusive way user state is removed. Rationale: deleting on start would gratuitously discard user state (a running, working unit) and would blur the unit's ownership. The pre-flight bus check (DQ3) already prevents NEW orphans, so an existing unit is either intentional or stale-legacy — warn, never auto-remove.
|
||||
|
||||
[DELIVERY: F5 candidate list + start stale-unit warning.]
|
||||
|
||||
---
|
||||
|
||||
## DQ5: Port pinning
|
||||
|
||||
### Decision
|
||||
**Always pin `--port $PORT` in ExecStart** (help-gated). Confirmed: the adapter probes and health check already use `$PORT` (default 8088; llama.cpp default is 8080 per `common/common.h:620`). After F2 makes default-flag validation reliable, `--port` is never spuriously omitted, so the server always binds the port the adapter and health check expect.
|
||||
|
||||
### Mechanics
|
||||
- `--port $PORT` is a DEFAULT flag already emitted at `bin/pos-ai-server:528` guarded by `DEFAULT_PORT_OK`. Once F2 removes the race, `DEFAULT_PORT_OK` is reliably 1 for real builds (which advertise `--port` in `--help`), so `--port $PORT` is pinned.
|
||||
- Strictly ancient llama-server builds without `--port` in `--help` omit it (help-gated); in that (degenerate) case the adapter's 8088 probe would not match the server's 8080 default. Since such builds are not the supported real release (b10822 ships `--port`, default 8080), this residual path is accepted and documented as the "ancient build" degradation.
|
||||
|
||||
### Explicitly deferred
|
||||
- **Adapter probing fallback** (probe both 8088 and 8080, or derive port from the unit): NOT in scope this pass. With F2+F5, the server and adapter agree on 8088 by construction. Adding a dual-port adapter probe would mask (not fix) a genuine port disagreement and complicate the adapter for a non-path that F5 removes. Deferred with reason.
|
||||
|
||||
[DELIVERY: F6 port pinning — achieved via F2+F5; no adapter fallback.]
|
||||
|
||||
---
|
||||
|
||||
## DQ6: Installer sanity (F7)
|
||||
|
||||
### Decision
|
||||
**In scope.** `apps/ai/llamacpp.sh:46-57` post-install adds a sanity check after the symlink loop:
|
||||
- Run `llama-server --version` capturing `2>&1` — `err` if the binary doesn't execute (non-zero exit).
|
||||
- Run `llama-server --help` similarly — `err` if unreadable.
|
||||
- Verify the symlink target exists: for each `/usr/local/bin/llama*` symlink created, `[ -e "$link" ]` (resolves target) — `err` if broken.
|
||||
|
||||
### Rationale
|
||||
This catches a genuinely broken install (missing shared lib → binary won't run; truncated/empty archive → symlink dangling) at install time with one clear `err`, instead of surfacing as a confusing "version unknown / flags rejected" on the first `pos ai server start`. It is cheap and self-contained in the installer; it is the front door to the whole tool chain and is the natural place to fail fast.
|
||||
|
||||
[DELIVERY: F7 installer post-install sanity.]
|
||||
|
||||
---
|
||||
|
||||
## Ratified Fix Scope (F1-F7)
|
||||
|
||||
| # | File:line | Decision | 1-line change |
|
||||
|---|-----------|----------|---------------|
|
||||
| **F1** | `bin/pos-ai-server:71` | **KEEP** | Capture `2>&1` and broaden regex to `[0-9]+\.[0-9]+\.[0-9]+|build [0-9]+|b[0-9]+`. |
|
||||
| **F2** | `bin/pos-ai-server:101,145` | **KEEP** | Replace `printf|grep -q` pipeline with `grep -E -- … >/dev/null <<<"$help_text"` (non-q, no pipe → no SIGPIPE); apply identically to `validate_requested_flags` and `validate_default_flags`. |
|
||||
| **F3** | `bin/pos-ai-server:230-264` | **KEEP** | `resolve_model` dir-expansion per DQ2 precedence (exactly-one-gguf → resolve; multiple → list+err; commit `$HF_DOWNLOAD_DIR/<name>/<file>` form). |
|
||||
| **F4** | `bin/pos-ai-server:605` + `:461-626`; `bin/pos-communication-matrix-listener:341`; `bin/pos-network-download:191`; **new** `lib/common.sh` | **KEEP (adjusted)** | Add `ensure_user_bus` to `lib/common.sh`; call before unit write in all three tools (abort, no orphan); plus `--no-unit` direct-run flag on `pos-ai-server`; start pre-flight warns on existing unit (DQ4). |
|
||||
| **F5** | `bin/pos-ai-server:56` | **KEEP** | `find_llamacpp` candidates → `("llama-server" "llama-server-cuda")`. |
|
||||
| **F6** | `bin/pos-ai-server:528` (+ no adapter change) | **KEEP** | Confirm `--port $PORT` always pinned (reliably emitted once F2 fixed); no adapter probing fallback. |
|
||||
| **F7** | `apps/ai/llamacpp.sh:46-57` | **KEEP (in scope)** | Post-install sanity: `llama-server --version` (2>&1) + `--help` execute, symlink targets exist, else `err`. |
|
||||
|
||||
## Explicitly deferred (NOT this pass)
|
||||
|
||||
1. **Adapter probing fallback** (probe 8088+8080 / derive port from unit) — F6 makes the adapter agree with the server by construction; a dual-port probe would mask (not fix) disagreement. Deferred with reason.
|
||||
2. **Direct-run escape hatch for matrix-listener and network-download** — stateful daemons genuinely need systemd; a direct-run supervisor is a larger design change. Deferred; only pos-ai-server gets `--no-unit` this pass.
|
||||
3. **Multi-user / TELEGRAM_GROUP_MODE and DQ-adjacent chat features** — unrelated to this breakage; tracked separately in the stabilization design.
|
||||
4. **D1 (pos-ai-hf single-file download failure recording)** and **LLAMACPP_HOST coherence (D2)** — separate defects already tracked in the stabilization open-items; not part of this fix map.
|
||||
5. **`--no-mmap`-style substring false positives beyond the current word-boundary regex** — D4 (already implemented in this file at `:101`); no change needed unless F2 rewrites it, which uses the identical word-boundary pattern.
|
||||
|
||||
---
|
||||
|
||||
## Test-Fixture Requirements (for Tester)
|
||||
|
||||
Per fix from the Detective's fixture spec, deterministically:
|
||||
|
||||
- **F1 (version):** fake `llama-server` printing `version: 0.4.0-dev (build 10822, commit …)` **to stderr**, help to stdout → assert `detect_llama_version` returns `0.4.0` (not "unknown"). Also a fixture printing only `version: b10822`/`build 10822` to stderr → assert broadened regex returns the build token.
|
||||
- **F2 (race + determinism):** fake `llama-server` whose `--help` emits a 59 KB body with `--threads`@line 7, `--ctx-size`@25, `--n-gpu-layers`@140, `--host`/`--port` near end (real llama.cpp layout). Run `validate_default_flags` **N≥20 times under `set -euo pipefail`** → fixed code reports all 5 supported on EVERY run (zero omissions). Regression tail: pre-fix code must fail at least once in the same loop (proves the race existed).
|
||||
- **F3 (model dir):** fixture `$HF_DOWNLOAD_DIR/Qwen-Qwen3-1.7B-GGUF/Qwen3-1.7B-Q8_0.gguf` (+ `.hf-meta`). Assert `DRY_RUN=1 pos ai server start Qwen-Qwen3-1.7B-GGUF` resolves to the file; multi-gguf dir case errors listing each as `dirname/file`; zero-gguf dir errors as today.
|
||||
- **F4 (bus):** env with `XDG_RUNTIME_DIR`/`DBUS_SESSION_BUS_ADDRESS` unset (or stub `systemctl` failing with the dbus message) → assert pre-flight `err`s with remediation text (both `export XDG_RUNTIME_DIR=…` and `sudo loginctl enable-linger …`), and **no unit file is written** (no orphan). Assert `--no-unit` path execs the direct command via a stub `llama-server` recording argv + writes pidfile.
|
||||
- **F5 (server fallback):** PATH containing ONLY an unrelated fake `server` (its own --help/--version), no llama-server → assert `find_llamacpp` does NOT return `server`.
|
||||
- **F6 (port):** with F2 fixed, `DRY_RUN=1` generate a unit → assert ExecStart always contains `--port 8088` (and `--host`, `--n-gpu-layers`, `--ctx-size`, `--threads`).
|
||||
- **F7 (installer):** run `apps/ai/llamacpp.sh` against a fixture tar containing a broken binary (missing shared lib) → assert post-install sanity `err`s.
|
||||
- **E2E regression (reported user scenario):** with stubs (fake `llama-server` b10822-shaped stderr + help, fake systemctl that succeeds only with `XDG_RUNTIME_DIR` set), run install→`pos ai server start Qwen-Qwen3-1.7B-GGUF`→assert the written unit is correct (ExecStart carries all 5 flags + `--port 8088`, no orphan unit, version resolvable).
|
||||
|
||||
---
|
||||
|
||||
*Deliverables complete. See `AgentsReport/architect/2026-09-06_stabilization-design.md` (appended post-merge section) for the consolidated record.*
|
||||
@@ -1,261 +0,0 @@
|
||||
# Alias Menu Abort — Fix Design (2026-09-06)
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Decision 1 (chosen):** Add an opt-in `--allow-empty` flag to `lib/menu-lib.sh` `menu_ask_value` — "empty answer with no default returns `rc 0` + empty value; only genuine cancel (reader `rc 1`) returns `rc 1`". Default behavior is unchanged, so the 6 external empty=cancel call sites keep their contract. **This is Option A.**
|
||||
- **Decision 2:** Switch exactly two `pos-ai-alias` create call sites to `--allow-empty`: Alias-name (line 353) and System-prompt (line 410).
|
||||
- **Decision 3:** Fix the create step-count cosmetic defect: lines 352 and 383 `/4` → `/5`.
|
||||
- **Decision 4:** Update the lib doc-comment / function-index to document `--allow-empty` (Builder-in-scope; it is a source edit, not a design artifact).
|
||||
- **Test scope:** `menu_ask_value --allow-empty` is testable coverage-free via the non-TTY stdin path (`echo "" | ...`); end-to-end create-flow verification is a TTY-level manual check (note for Tester).
|
||||
- **Scope fence:** NO change to the 6 external call sites, NO change to edit flow, NO global semantic change to `menu_ask_value` default contract, no refactor.
|
||||
|
||||
---
|
||||
|
||||
## Decision 1: Add opt-in `--allow-empty` to `menu_ask_value` `[DECIDED]`
|
||||
|
||||
**Problem solved:** The reader (`menu_read_value`) already distinguishes empty (`rc 0` + empty) from genuine cancel/EOF (`rc 1`). `menu_ask_value` collapses empty-with-no-default into `rc 1`. Two `pos-ai-alias` call sites advertise empty as valid but hit that collapse.
|
||||
|
||||
**Option evaluated (A chosen):** add `menu_ask_value [--allow-empty] <label> [default]`.
|
||||
|
||||
**Option B (local workaround) — rejected:**
|
||||
- A local re-implementation (e.g. call `menu_read_value` directly + replicate the default-resolution) **duplicates lib logic** (`menu_ask_value` lines 351-361) and risks divergence when the lib evolves.
|
||||
- A sentinel default + caller-side mapping is **fragile** (some impossible-to-type sentinel must be picked, mapped back, and could still theoretically be typed) and unreadable.
|
||||
- **Does not scale:** the next tool needing empty-valid would re-invent it; the flag exists once in the lib and every future caller reuses it.
|
||||
- Neither preserves the alias-name re-prompt UX as cleanly.
|
||||
|
||||
**Rationale for A:**
|
||||
1. **Backward compatible:** default contract (empty + no default → `rc 1`) is untouched for all existing callers — `--allow-empty` only widens behavior for callers that opt in. Satisfies the hard constraint (6 external empty=cancel sites unaffected).
|
||||
2. **Minimal surface:** one keyword param, two lines of parse, one line of behavior. Not a refactor.
|
||||
3. **Preserves the cancel/empty distinction:** genuine cancel still returns `rc 1` even with the flag; only empty-answer-with-no-default flips to `rc 0`.
|
||||
4. **Scales:** standard opt-in shape; `menu_ask_value` currently has no flags, so this defines the cleanest minimal first flag (leading keyword, parsed before the positional label — no ambiguity since no call site's label equals `--allow-empty`).
|
||||
5. **Matches intended UX:** the alias-name re-prompt (dead at line 355 today) becomes reachable; the system-prompt "use built-in" promise becomes true.
|
||||
|
||||
### Exact interface spec (lib/menu-lib.sh)
|
||||
|
||||
Signature:
|
||||
```bash
|
||||
menu_ask_value [--allow-empty] <label> [default]
|
||||
```
|
||||
|
||||
- `--allow-empty` is an optional leading keyword flag. When present it is consumed and shifted before parsing `<label>`/`[default]` positionals.
|
||||
- Behavior table:
|
||||
|
||||
| input | no flag | `--allow-empty` |
|
||||
|-------|---------|-----------------|
|
||||
| reader rc 1 (cancel/EOF/Ctrl-C) | rc 1 | rc 1 (unchanged — cancel stays cancel) |
|
||||
| empty value, default present | rc 0, echo default | rc 0, echo default (default still wins) |
|
||||
| empty value, no default | rc 1 (unchanged) | **rc 0, echo empty** |
|
||||
| non-empty value | rc 0, echo value | rc 0, echo value |
|
||||
|
||||
- `--allow-empty` only widens the empty+no-default cell; it never suppresses the default and never converts genuine cancel.
|
||||
- Doc-comment updates required in the same file:
|
||||
- Function index line 27: `menu_ask_value <label> [default]` → `menu_ask_value [--allow-empty] <label> [default]`.
|
||||
- Function doc block (lines 344-349): add a line documenting `--allow-empty` with the exact semantics above (empty+no-default → rc 0 empty; cancel still rc 1).
|
||||
|
||||
Reference implementation (approval shape — Builder reproduces this exactly):
|
||||
```bash
|
||||
menu_ask_value() {
|
||||
local allow_empty=0
|
||||
if [ "${1:-}" = "--allow-empty" ]; then
|
||||
allow_empty=1
|
||||
shift
|
||||
fi
|
||||
local label="$1" def="${2:-}" val pr="$1"
|
||||
[ -n "$def" ] && pr="$pr [$def]"
|
||||
if ! val="$(menu_read_value "$pr")"; then
|
||||
return 1 # EOF / cancel
|
||||
fi
|
||||
if [ -z "$val" ]; then
|
||||
[ -n "$def" ] && { echo "$def"; return 0; }
|
||||
[ "$allow_empty" -eq 1 ] || return 1
|
||||
echo ""
|
||||
return 0
|
||||
fi
|
||||
echo "$val"
|
||||
}
|
||||
```
|
||||
|
||||
**[DECIDED]**
|
||||
|
||||
---
|
||||
|
||||
## Decision 2: Switch exactly 2 call sites to `--allow-empty` `[DECIDED]`
|
||||
|
||||
Only the two class-a (empty advertised as valid) defunct sites get the flag. All other 12 call sites across the repo stay byte-identical.
|
||||
|
||||
### bin/pos-ai-alias line 353 (Alias name)
|
||||
From:
|
||||
```bash
|
||||
name="$(menu_ask_value "Alias name" "")" || return 0
|
||||
```
|
||||
To:
|
||||
```bash
|
||||
name="$(menu_ask_value --allow-empty "Alias name" "")" || return 0
|
||||
```
|
||||
Behavior after the change:
|
||||
- Genuine cancel (Ctrl-C/EOF, reader rc 1) → `menu_ask_value` rc 1 → `|| return 0` → **abort to menu** (correct; unchanged).
|
||||
- Empty Enter → `menu_ask_value` rc 0 + `""` → `name=""` → line 355 `[ -z "$name" ]` fires → `warn "Alias name cannot be empty"` → `continue` → re-prompt (the intended UX, now reachable). **The re-prompt loop is preserved by design.**
|
||||
- Non-empty / valid / collision cases: unchanged.
|
||||
|
||||
### bin/pos-ai-alias line 410 (System prompt)
|
||||
From:
|
||||
```bash
|
||||
prompt="$(menu_ask_value "System prompt (empty = use built-in)" "")" || return 0
|
||||
```
|
||||
To:
|
||||
```bash
|
||||
prompt="$(menu_ask_value --allow-empty "System prompt (empty = use built-in)" "")" || return 0
|
||||
```
|
||||
Behavior:
|
||||
- Genuine cancel → rc 1 → `|| return 0` → **abort to menu** (correct; unchanged — cancel still exits the create flow).
|
||||
- Empty Enter → rc 0 + `""` → `prompt=""` → passes the `|` check (empty has no `|`) → breaks → **proceeds with built-in prompt**. The UI text "(empty = use built-in)" now means what it says.
|
||||
- Non-empty prompt: unchanged (validation/truncate/continuation identical).
|
||||
|
||||
No change to lines 397, 433 (create) or the edit-flow sites (538, 557, 585) — those are correct today.
|
||||
|
||||
**[DECIDED]**
|
||||
|
||||
---
|
||||
|
||||
## Decision 3: Fix create step-counter 352/383 `/4` → `/5` `[DECIDED]`
|
||||
|
||||
Create flow has 5 steps (Alias Name `352`, Provider `383`, Session `396`, Prompt `409`, Trust `424`), and lines 396/409/424 already print `/5`. Only 352 (`step 1 4`) and 383 (`step 2 4`) are stale. Set both to `/5`.
|
||||
|
||||
No change to the edit flow — internally consistent at 4 (lines 520/537/556/581 all `/4`).
|
||||
|
||||
**[DECIDED]**
|
||||
|
||||
---
|
||||
|
||||
## Decision 4: Lib source edit (doc/comment) is Builder scope `[DECIDED]`
|
||||
|
||||
The doc-comment and function-index updates to `lib/menu-lib.sh` are ordinary source edits. They are part of the approved scope for Builder (decision 1 embedded them). They are NOT an Architect artifact — the architect report (this file) is the design record; the inline doc comment is implementation.
|
||||
|
||||
---
|
||||
|
||||
## Scope Fence
|
||||
|
||||
**Approved outcome:** `pos ai alias` create flow no longer silently aborts on an empty System Prompt; empty alias-name re-prompts with the warn; create step numbers are consistent (5/5).
|
||||
|
||||
**In-scope components/files:**
|
||||
- `lib/menu-lib.sh` — add `--allow-empty` flag + update doc-comment and function index.
|
||||
- `bin/pos-ai-alias` — lines 352, 353, 383, 410.
|
||||
|
||||
**Allowed interface changes:**
|
||||
- `menu_ask_value` gains optional leading `--allow-empty` (additive; no positional or rc semantics change for existing callers).
|
||||
- `bin/pos-ai-alias` call sites 353/410 pass `--allow-empty`; step counters 352/383 → `/5`.
|
||||
|
||||
**Allowed behavior changes:**
|
||||
- Empty System Prompt → proceeds (built-in), instead of aborting.
|
||||
- Empty Alias Name → warn + re-prompt (was dead/unreachable code).
|
||||
- Create step numbers 1 and 2 display `/5`.
|
||||
|
||||
**Required compatibility (untouched — must remain byte-identical):**
|
||||
- All 6 external `menu_ask_value` call sites: `pos-system-backup:231`, `pos-media-sync:67`, `pos-network-download:1001/1027`, `pos-docker-vbox:495/831`.
|
||||
- All edit-flow call sites (538/557/585) and create 397/433.
|
||||
- The default `menu_ask_value` contract (empty + no default → rc 1, no flag).
|
||||
- Genuine cancel at 353/410 still aborts the create flow (`|| return 0` retained).
|
||||
|
||||
**Explicitly out of scope:**
|
||||
- No global semantic change to `menu_ask_value`.
|
||||
- No change to `menu_read_value`.
|
||||
- No change to the edit flow.
|
||||
- No refactor of `_alias_create`; the re-prompt loop structure at 350-380 is retained.
|
||||
- No tests written by Architect/Builder (Tester scope, see below); no docs beyond the inline menu-lib.sh comment.
|
||||
|
||||
**Architectural constraints:**
|
||||
- Keep change surface minimal (two flag switches + two counters + one lib flag).
|
||||
- Preserve cancel/empty distinction end-to-end.
|
||||
- UI text "(empty = use built-in)" must keep its meaning (empty proceeds with empty).
|
||||
|
||||
---
|
||||
|
||||
## Test scope (for Tester)
|
||||
|
||||
`menu_ask_value --allow-empty` is covered **without a TTY** via the shared non-TTY stdin path (Detective already proved `echo "" | menu_ask_value ...` exercises the exact `menu_ask_value` logic). Recommended coverage matrix:
|
||||
|
||||
- `echo "" | menu_ask_value --allow-empty "l" ""` → rc 0, output empty.
|
||||
- `echo "" | menu_ask_value "l" ""` → rc 1 (regression: default contract unchanged).
|
||||
- `echo "" | menu_ask_value --allow-empty "l" "def"` → rc 0, output `def` (default wins even with flag).
|
||||
- `printf '' | menu_ask_value --allow-empty "l" ""` → rc 1 (genuine EOF still cancel).
|
||||
- `printf 'xyz\n' | menu_ask_value --allow-empty "l" ""` → rc 0, output `xyz`.
|
||||
- `echo "" | menu_ask_value "l" "def"` → rc 0, output `def` (regression).
|
||||
|
||||
Plus one **TTY-level manual check** (needs a real terminal): run `pos ai alias` create → leave System Prompt empty → expect it to proceed to the Trust step and create the alias with empty prompt (built-in); leave Alias Name empty → expect `warn "Alias name cannot be empty"` + re-prompt; press Ctrl-C on any step → expect clean abort to menu.
|
||||
|
||||
If a shared non-TTY repro harness exists under `tests/`, add the flag matrix there; otherwise the manual TTY note stands. This is Tester's domain — Architect fixes only scope, not the writing.
|
||||
|
||||
---
|
||||
|
||||
## Verification expected
|
||||
|
||||
1. `bash -n` on `lib/menu-lib.sh` and `bin/pos-ai-alias`.
|
||||
2. `make gen && make check && make lint` (definition of done: check green, lint `0 FAIL, 0 WARN`) — required because `bin/pos-ai-alias` is touched.
|
||||
3. Tester runs the `--allow-empty` matrix (above) and the TTY create-flow check.
|
||||
4. Confirm `git diff --exit-code` clean after `make gen` (determinism gate).
|
||||
|
||||
**Open risks / residual:**
|
||||
- None architectural. The only residual is the class-c edge (edit Session aborts if a stored session is empty), which create-flow line 404 prevents today — out of scope, documented by Detective.
|
||||
|
||||
---
|
||||
|
||||
## Handoff
|
||||
|
||||
```text
|
||||
Status: DECISION_READY
|
||||
|
||||
Problem:
|
||||
pos ai alias create silently aborts on empty "System prompt" (and empty alias-name
|
||||
re-prompt is unreachable) because menu_ask_value collapses empty+no-default into
|
||||
rc 1; plus cosmetic /4 vs /5 step numbers.
|
||||
|
||||
Decision:
|
||||
(A) Add opt-in `--allow-empty` to lib/menu-lib.sh menu_ask_value (empty+no-default
|
||||
→ rc 0 + empty; cancel still rc 1; default unchanged). (2) Switch pos-ai-alias 353
|
||||
and 410 to the flag. (3) Fix step counters 352/383 → /5.
|
||||
|
||||
Reasoning:
|
||||
Reader already distinguishes empty from cancel; the collapse happens only in
|
||||
menu_ask_value. Flag is opt-in (backward compatible, 6 external empty=cancel callers
|
||||
untouched), scales for future callers, and makes dead re-prompt code reachable.
|
||||
Option B (local re-implementation / sentinel) duplicates lib logic, risks divergence,
|
||||
and does not scale.
|
||||
|
||||
Ownership:
|
||||
Builder — edits lib/menu-lib.sh (flag + doc) and bin/pos-ai-alias (2 switches + 2 counters).
|
||||
|
||||
Interfaces:
|
||||
menu_ask_value gains optional leading `--allow-empty`. No positionals, default, or
|
||||
rc semantics change for existing callers. pos-ai-alias internal behavior: empty
|
||||
system prompt proceeds built-in; empty alias name re-prompts.
|
||||
|
||||
Approved scope:
|
||||
lib/menu-lib.sh (flag + inline doc; function-index line 27); bin/pos-ai-alias
|
||||
lines 352, 353, 383, 410.
|
||||
|
||||
Explicitly out of scope:
|
||||
6 external call sites, all edit-flow sites, 397/433, menu_read_value, global semantic
|
||||
change, refactor of _alias_create, alias-name cancel semantics (still aborts via || return 0).
|
||||
|
||||
Constraints:
|
||||
No global menu_ask_value semantic change. Cancel still aborts at 353/410. Empty
|
||||
systems-prompt proceeds; empty alias-name re-prompts with warn. Keep change surface minimal.
|
||||
|
||||
Verification:
|
||||
bash -n on both files; make gen && make check && make lint (0 FAIL, 0 WARN); Tester
|
||||
--allow-empty matrix + TTY create-flow check; git diff --exit-code clean after gen.
|
||||
|
||||
Risks:
|
||||
None architectural. Residual class-c (edit Session on stored-empty) is out of scope
|
||||
and today prevented by create (line 404).
|
||||
|
||||
Recommended next agent:
|
||||
Builder
|
||||
|
||||
Reason:
|
||||
Design and exact call-site/libeay changes are fully specified; Builder can implement
|
||||
without making architectural decisions. Tester follows for the matrix + TTY check.
|
||||
|
||||
Architect changes:
|
||||
AgentsReport/architect/2026-09-06_alias-menu-fix-design.md (this file) only.
|
||||
```
|
||||
@@ -1,95 +0,0 @@
|
||||
# D-A Fail-Mode Decision — Ratify Soft-Fail (2026-09-06)
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Decision 1:** D-A is formally **AMENDED** to the soft-fail model (option 2): unset `TELEGRAM_OWNER_ID` / `MATRIX_ROOM_ID` → daemon **starts**, logs a startup warning naming the missing var, and ignores every incoming command (no execution, no hint reply).
|
||||
- **Decision 2:** No doc/template/test changes required — they already describe soft-fail (`DOC/POS.md:372,402`, `DOC/howto/communication.md`, `config/telegram.env:10`, `config/matrix.env:10-11`, `tests/t-telegram-auth.sh:76`, `tests/t-matrix-auth.sh:78`).
|
||||
- **Residual caveat (explicit):** soft-fail misconfiguration is **SILENT to senders** — an admin who forgets `TELEGRAM_OWNER_ID` gets a daemon that looks alive (systemd healthy, long-polling) but ignores everyone; the only signal is the warning in the local log. Strict mode would have been LOUD at startup but would crash-loop a systemd `Restart=always` daemon for a non-fatal config gap.
|
||||
- Files changed: `AgentsReport/architect/2026-09-06_stabilization-design.md` (D-A section amendment, [DECIDED]).
|
||||
|
||||
---
|
||||
|
||||
## Decision 1: Ratify soft-fail as shipped (amend D-A)
|
||||
|
||||
**Problem:** D-A as written required `err`+exit (fail-stop) when `TELEGRAM_OWNER_ID`/`MATRIX_ROOM_ID` are unset. The security-track Builder implemented soft-fail: daemon starts, `warn`s, ignores commands. Reviewer finding F2 (BLOCKING) confirmed fail-closed security is preserved but the approved contract was substituted silently — violating my own decision criteria 1/5.
|
||||
|
||||
**Decision:** Formally amend D-A to the soft-fail model as shipped. The strict wording in `AgentsReport/architect/2026-09-06_stabilization-design.md` D-A (Telegram bullet, Matrix guard, acceptance criteria 1/5, Files-affected rows, risk note, TL;DR) is replaced and marked **"AMENDED at review (2026-09-06): soft-fail ratified as shipped"**.
|
||||
|
||||
**Rationale:**
|
||||
1. The entire shipped surface already encodes soft-fail: code (`bin/pos-communication-telegram-listener:758-762,789-791`, `bin/pos-communication-matrix-listener:510-512`), docs (`DOC/POS.md:372,402`), templates (`config/telegram.env:10`, `config/matrix.env:10-11`), and tests (`t-telegram-auth.sh:76`, `t-matrix-auth.sh:78`). Ratifying creates **zero** doc/template/test churn; choosing strict would require rewriting 5+ artifacts against an internally consistent implementation.
|
||||
2. The security property is **identical in both models**: fail-closed — an unauthorized/unauthored command never executes. The difference is purely operational (startup refusal vs degraded liveness), so there is no security case forcing strict.
|
||||
3. Strict fail-stop on a systemd-managed daemon with `Restart=always` produces a crash-loop for a non-fatal config gap; soft-fail keeps the daemon alive, diagnosable via its log, and recoverable via `pos config`.
|
||||
4. Both directions are cheaply reversible (a 2-line guard flip) if future evidence favors strict.
|
||||
|
||||
**Security implications of soft-fail (explicit):** misconfiguration is silent to senders — no reply, no error, no hint that commands exist; the only signal is the local-log warning. Weighing that against strict's startup crash-loop, soft-fail is the better operation for a personal toolkit where a missing optional auth key should not take down a supervised unit.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Decision 2: No docs/templates/tests changes
|
||||
|
||||
`DOC/POS.md` (lines 372, 402), `DOC/howto/communication.md`, `config/telegram.env` (line 10), `config/matrix.env` (lines 10-11), `tests/t-telegram-auth.sh` (line 76), and `tests/t-matrix-auth.sh` (line 78) all describe exactly the ratified behavior. They stay as-is.
|
||||
|
||||
[DECIDED]
|
||||
|
||||
---
|
||||
|
||||
## Handoff
|
||||
|
||||
```text
|
||||
Status: DECISION_READY
|
||||
|
||||
Problem:
|
||||
D-A unset-owner/room behavior conflicted between the written design (fail-stop) and the implemented, documented, template-encoded, and tested behavior (soft-fail).
|
||||
|
||||
Decision:
|
||||
Ratify soft-fail by amending D-A. Unset owner/room → daemon runs, warns (startup + per-message), ignores all commands (no execution, no hint reply). Security property unchanged: fail-closed.
|
||||
|
||||
Reasoning:
|
||||
Evidence (code + 4 doc/template artifacts + 2 test files) consistently describes soft-fail; no security property differs between models; strict would crash-loop systemd units. Reversible either way.
|
||||
|
||||
Ownership:
|
||||
Architect (amendment) — done. Implementations stay as shipped in bin/pos-communication-{telegram,matrix}-listener.
|
||||
|
||||
Interfaces:
|
||||
No CLI/config-facing contract change beyond the ratified D-A text. `TELEGRAM_OWNER_ID`/`MATRIX_ROOM_ID` remain optional config keys whose absence disables command execution, not daemon start.
|
||||
|
||||
Approved scope:
|
||||
- `AgentsReport/architect/2026-09-06_stabilization-design.md` D-A section (amended).
|
||||
- Reviewer finding F2 is resolved by amendment (downgrades); no code change.
|
||||
|
||||
Explicitly out of scope:
|
||||
- No change to listener code, docs, templates, or tests (they already match).
|
||||
- No change to F1/F3 (BLOCKING/REQUIRED D-B items) — those remain Builder work.
|
||||
|
||||
Constraints:
|
||||
- D-A acceptance criteria 1/5 now assert soft-fail wording (already updated in the amended doc).
|
||||
- Fail-closed remains a hard invariant: never execute a command from an unauthorized sender or chat.
|
||||
|
||||
Verification:
|
||||
- Re-read amended D-A section for consistency (no remaining "refuses to start"/`err` strict wording for the unset case).
|
||||
- Reviewer re-check of F2 against amended criteria.
|
||||
- Orchestrator runs `make gen && make check && make lint && make test` before merge (Step 10 gates).
|
||||
|
||||
Risks:
|
||||
- Silent misconfiguration (soft-fail caveat): mitigated by actionable warning text naming the missing var and the `listener running (chat X, owner unset)` startup log line.
|
||||
- Future preference for fail-stop is a 2-line code change + doc/template updates; D-A now records both models' trade-offs.
|
||||
|
||||
Recommended next agent:
|
||||
Builder
|
||||
|
||||
Reason:
|
||||
F1 (missing `--no-command-execution` in both bridge invocations) and F3 (NO_EXEC print semantics) remain BLOCKING/REQUIRED D-B fixes independent of this amendment; F2 no longer blocks. Builder should also add the D-A amendment note to AGENT_TODO.md if not already covered.
|
||||
|
||||
Architect changes:
|
||||
AgentsReport/architect/2026-09-06_stabilization-design.md — D-A section amended only.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files changed (this decision)
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `AgentsReport/architect/2026-09-06_stabilization-design.md` | D-A TL;DR item, Telegram bullet, Matrix guard, Files-affected rows, acceptance criteria 1/5, risk note — amended to soft-fail and marked "AMENDED at review (2026-09-06)" |
|
||||
@@ -1,782 +0,0 @@
|
||||
# Stabilization Pass — Architectural Design Decisions
|
||||
|
||||
**Date:** 2026-09-06
|
||||
**Architect:** Evidence-driven design pass over three Explorer audit reports
|
||||
**Constraint:** Preserve existing Bash architecture; no framework rewrites; smallest clean fix per issue; no commits; all gates (`make gen`, `make check`, `make lint`) must remain green at `0 FAIL, 0 WARN`.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
1. **D-A (Telegram/Matrix auth):** Introduce `TELEGRAM_OWNER_ID` as an AND-gate with `TELEGRAM_CHAT_ID`; Matrix requires `MATRIX_ROOM_ID` before any command runs. Both platforms converge on "sender AND chat authorized" contract; unset owner/room → daemon runs but ignores all commands (soft-fail, ratified at review 2026-09-06).
|
||||
2. **D-B (AI eval posture):** Flip tty confirmation default to `[y/N]` (deny); keep `--trust` interactive-only; add `--no-exec` flag to `pos-ai` and enforce it in chat bridges. Document the bridge invariant.
|
||||
3. **D-C (Test framework):** Create `tests/` with a minimal runner, `make test` target, stub-PATH + PTY patterns; first suite covers 14 surfaces.
|
||||
4. **D-D (Config loader):** Canonical `load_env_file` in `lib/config-ui.sh`; migrate 9 hand-rolled loaders; unify CRLF + XDG; collapse entertainment-lib read/write.
|
||||
5. **D-E (Install/uninstall manifest):** Extend `pos-system-uninstall` Tier 1 to cover 9 orphaned libs, ScaleTail dir, feature-flag store, and USER systemd units; de-hardcode plugin/binary lists.
|
||||
6. **D-F (pos-ai-server validation):** Validate ALL flags entering ExecStart (CLI + config + defaults); unsupported DEFAULT → warn + omit; unsupported REQUESTED → hard error.
|
||||
|
||||
---
|
||||
|
||||
## D-A: Chat Authorization Model (Telegram + Matrix)
|
||||
|
||||
### Decision
|
||||
|
||||
Adopt a unified "sender AND chat authorized" authorization contract for both Telegram and Matrix listeners. Concretely:
|
||||
|
||||
**Telegram:**
|
||||
- Add `TELEGRAM_OWNER_ID` to the `# POS_CONFIG:` registry in `bin/pos-communication-telegram-sender:6` and to `config/telegram.env` template.
|
||||
- Replace the OR-gate at `bin/pos-communication-telegram-listener:787` with:
|
||||
1. `chat == TELEGRAM_CHAT_ID` (chat must be the configured chat) — fail-continue (skip message silently).
|
||||
2. `from_id` must be one of the comma-separated `TELEGRAM_OWNER_ID` values — fail-continue (skip message silently).
|
||||
- If `TELEGRAM_OWNER_ID` is unset, the daemon **starts in fail-closed degraded mode**: `run_daemon` logs `warn "TELEGRAM_OWNER_ID unset — chat commands WILL BE IGNORED (fail-closed); set it with 'pos config telegram'"` at startup (same location as the token/chat-id guards, line ~761) and a per-message `warn "TELEGRAM_OWNER_ID unset — ignoring command (set it with 'pos config telegram')"`; every incoming command is skipped — **no execution, no hint reply**.
|
||||
|
||||
> **AMENDED at review (2026-09-06): soft-fail ratified as shipped.** The original wording required the daemon to `err`+exit on unset `TELEGRAM_OWNER_ID`. The security-track Builder implemented soft-fail (daemon runs, warns, ignores commands); `DOC/POS.md`, `DOC/howto/communication.md`, `config/telegram.env` and `tests/t-telegram-auth.sh` all consistently describe soft-fail. This is a deliberate deviation ratified at review time (Reviewer finding F2, downgraded). The security property is identical in both models: **fail-closed** — an unauthenticated/unauthorized command never executes. The difference is purely operational: strict = fail-stop (loud misconfiguration, systemd restart loop); soft-fail = degraded-liveness (silent-to-senders misconfiguration, one clean warning in the local log). Soft-fail was chosen because the entire shipped surface already encoded it and it avoids a systemd `Restart=always` crash-loop for a non-fatal config gap.
|
||||
- Reply on unauthorized messages: **none** (no leak that commands exist). The message is silently dropped.
|
||||
- Do **not** include a "trusted group" backward-compat mode in this pass. The OR-gate was never a documented feature; it was an implementation gap. Backward compatibility for group operation can be added later via an explicit `TELEGRAM_GROUP_MODE=true` opt-in — that is a separate, larger design decision (requires per-user allowlist, rate limiting, etc.) and explicitly **out of scope** for this stabilization.
|
||||
|
||||
**Matrix:**
|
||||
- Require `MATRIX_ROOM_ID` for command execution. If unset, the daemon **starts in fail-closed degraded mode**: guard in `run_daemon` at `bin/pos-communication-matrix-listener:510-512` logs `warn "MATRIX_ROOM_ID unset — refusing to run commands (fail-closed); set it with 'pos config matrix'"`; the daemon long-polls but runs no commands.
|
||||
|
||||
> **AMENDED at review (2026-09-06): soft-fail ratified as shipped.** The original wording required a hard `[ -n "${MATRIX_ROOM_ID:-}" ] || err "MATRIX_ROOM_ID is required — set it in pos config matrix"` guard. The security-track Builder implemented soft-fail; `DOC/POS.md`, `DOC/howto/communication.md`, `config/matrix.env` and `tests/t-matrix-auth.sh` all consistently describe soft-fail. Deliberate deviation ratified at review time (Reviewer finding F2, downgraded). Security property identical in both models: **fail-closed** — no command runs outside the configured room. Same operational trade-off as the Telegram case above (silent-to-senders vs. startup crash-loop).
|
||||
- Keep the sender=owner gate (`sender == owner` at line 550) unchanged.
|
||||
- This makes the Matrix behavior: sender authorized (== owner) AND chat authorized (== configured room) — identical to the Telegram contract.
|
||||
|
||||
**Consistency rule:** Both platforms enforce the same conceptual model: a message must come from an authorized sender in an authorized chat. No fallback to "any chat" or "any sender."
|
||||
|
||||
### Files affected
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `bin/pos-communication-telegram-sender:6` | Add `TELEGRAM_OWNER_ID` to `# POS_CONFIG:` registry |
|
||||
| `bin/pos-communication-telegram-listener:787-791` | Replace OR-gate with AND-gate (chat + sender) + soft-fail `TELEGRAM_OWNER_ID` unset warning in `run_daemon` and per-message skip |
|
||||
| `bin/pos-communication-matrix-listener:510-512` | Soft-fail `MATRIX_ROOM_ID` unset warning in `run_daemon` (commands refused) |
|
||||
| `config/telegram.env` | Add `TELEGRAM_OWNER_ID=` template line |
|
||||
| `DOC/HOWTO.md` (communication section) | Document new key, explain AND-gate |
|
||||
| `DOC/POS.md` (telegram listener section) | Document authorization model |
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. With `TELEGRAM_OWNER_ID` unset, `pos communication telegram listener --run` starts the daemon, logs a startup warning naming `TELEGRAM_OWNER_ID`, and ignores every incoming command (no execution, no hint reply).
|
||||
2. With `TELEGRAM_OWNER_ID=123` and `TELEGRAM_CHAT_ID=456`, a message from `from_id=123` in `chat=456` is dispatched.
|
||||
3. A message from `from_id=999` in `chat=456` is silently dropped (no reply, no log of "command not found").
|
||||
4. A message from `from_id=123` in `chat=789` (wrong chat) is silently dropped.
|
||||
5. With `MATRIX_ROOM_ID` unset, `pos communication matrix listener --run` starts the daemon, logs a warning naming `MATRIX_ROOM_ID`, and runs no commands (fail-closed).
|
||||
6. With `MATRIX_ROOM_ID` set, messages from the owner in the configured room are dispatched; messages from other senders or other rooms are dropped.
|
||||
7. `make gen && make check && make lint` all pass at 0 FAIL, 0 WARN after the change.
|
||||
8. `TELEGRAM_OWNER_ID` appears in `pos config telegram` output (masked if flagged as `digits` type, same as `TELEGRAM_CHAT_ID`).
|
||||
|
||||
### Risks / open questions
|
||||
|
||||
- Existing users who have not set `TELEGRAM_OWNER_ID` will find the daemon stays up but ignores every command after upgrade. **Mitigation:** the startup warning is actionable ("set it with 'pos config telegram'"). This is intentional — the previous behavior was a security vulnerability (V1), not a feature. Documented residual caveat (soft-fail): misconfiguration is **silent to senders** — no reply, no error, no hint that commands exist; the only signal is the startup/per-message warning in the local log. Strict mode would have been loud at startup but would crash-loop a systemd-managed daemon for a non-fatal config gap; soft-fail preserves liveness and diagnosability. (Amended 2026-09-06.)
|
||||
- The `TELEGRAM_OWNER_ID` supports comma-separated values (multi-user). The `digits` flag validation in `cfg_validate` already allows negative IDs (group/supergroup IDs); for `TELEGRAM_OWNER_ID` we should use a `digits` flag that also allows comma-separated positive-only values. **Decision:** use a plain `digits` flag (no comma support) for the initial pass; each `TELEGRAM_OWNER_ID` entry is a single numeric user ID. If multi-user is needed, a future pass adds it. This keeps the validation simple and the AND-gate implementation a direct string comparison.
|
||||
- The `TELEGRAM_CHAT_ID` comparison already allows negative values (group IDs). The `TELEGRAM_OWNER_ID` should always be a positive user ID. The `cfg_validate` `digits` flag allows leading `-`; for `TELEGRAM_OWNER_ID` use a new `positive-digits` flag or simply document that only positive values are valid for this key.
|
||||
|
||||
---
|
||||
|
||||
## D-B: Remote AI Command Execution Posture (eval)
|
||||
|
||||
### Decision
|
||||
|
||||
The `pos ai` command-execution path (`_prompt_run_command`) evaluates AI-generated shell code. The security posture change addresses three concerns: the default confirmation, the `--trust` flag, and the bridge invariant.
|
||||
|
||||
**1. Flip the tty confirmation default to DENY:**
|
||||
|
||||
Change `bin/pos-ai:391` from `[Y/n]` to `[y/N]`. The rationale: the AI model's output is untrusted external authority. When a user asks "run X", they mean the *task* — the specific command the model proposes is the model's interpretation, and a prompt-injection or model error can produce a harmful command. Defaulting to deny means the user must explicitly opt-in (`y` or Enter is now decline). The `[y/N]` pattern is the standard bash convention for non-destructive defaults. The user's request for help does not imply authorization to execute arbitrary code.
|
||||
|
||||
Current line 391:
|
||||
```bash
|
||||
printf 'Run this command? [Y/n] ' >&2
|
||||
```
|
||||
Change to:
|
||||
```bash
|
||||
printf 'Run this command? [y/N] ' >&2
|
||||
```
|
||||
|
||||
And invert the case logic at line 394: `y|Y)` executes; `*` (including Enter) declines and adds to history.
|
||||
|
||||
**2. `--trust` flag: keep as-is but document scope:**
|
||||
|
||||
The `--trust` flag (`TRUST_MODE=1`, `bin/pos-ai:644`) auto-executes without confirmation on a **tty only** (line 382 `[ -w /dev/tty ] || return 0` — non-tty never executes). This is correct: `--trust` is an explicit operator action on an interactive terminal. No change needed to the flag itself, but:
|
||||
- Add a `--trust` warning to `usage()` if not already present: note that this bypasses confirmation and should only be used in trusted local sessions.
|
||||
- Document that `--trust` has **no effect** when invoked from a chat bridge (non-tty → early return at line 382).
|
||||
|
||||
Do NOT rename `--trust` to `--trust-no-confirm`; the existing name is clear enough and renaming would break alias wrappers (`pos-ai-alias:433-436`).
|
||||
|
||||
**3. Chat bridge invariant: bridges NEVER execute code blocks.**
|
||||
|
||||
Both Telegram (`bin/pos-communication-telegram-listener:734`) and Matrix (`bin/pos-communication-matrix-listener:472`) invoke `pos ai gemini ask ...` which runs non-interactively. Inside `_prompt_run_command` (`bin/pos-ai:382`), `[ -w /dev/tty ] || return 0` means code blocks are never executed when the tool runs without a tty — they are printed but not run. This is the correct behavior.
|
||||
|
||||
To prevent future regressions if a refactor changes the tty check or adds an auto-confirm path:
|
||||
- Add `--no-command-execution` as a recognized flag in `pos-ai` (`bin/pos-ai:649` parse block, `# POS_FLAGS:` header).
|
||||
- When set, `_prompt_run_command` returns 0 immediately without printing or executing (same as current non-tty behavior, but explicit).
|
||||
- Both chat bridges pass `--no-command-execution` when invoking `pos ai`:
|
||||
- `bin/pos-communication-telegram-listener:734`: add `--no-command-execution` to the command.
|
||||
- `bin/pos-communication-matrix-listener:472`: same.
|
||||
- This makes the invariant **structurally enforced**: even if a future refactor removes the tty check, the bridge-parsed flag still prevents execution.
|
||||
|
||||
### Files affected
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `bin/pos-ai:391` | Change prompt from `[Y/n]` to `[y/N]` |
|
||||
| `bin/pos-ai:394-405` | Invert case logic: `y|Y` → execute, `*` → decline+history |
|
||||
| `bin/pos-ai:649-677` | Add `--no-command-execution` to flag parser |
|
||||
| `bin/pos-ai:380-382` | Check `$NO_EXEC` flag before the tty check |
|
||||
| `bin/pos-ai:5` (`# POS_FLAGS:`) | Add `--no-command-execution` |
|
||||
| `bin/pos-communication-telegram-listener:734` | Add `--no-command-execution` to AI bridge call |
|
||||
| `bin/pos-communication-matrix-listener:472` | Add `--no-command-execution` to AI bridge call |
|
||||
| `DOC/POS.md` (ai section) | Document new flag and confirmation change |
|
||||
| `DOC/HOWTO.md` (ai section) | Document the security posture |
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. On a tty, `pos ai` with a command block in the response shows `[y/N]` and declines on Enter.
|
||||
2. On a tty, typing `y` or `Y` at the prompt executes the command.
|
||||
3. `pos ai --trust` still auto-executes on a tty (no prompt).
|
||||
4. `pos ai --no-command-execution` skips execution entirely (no prompt, no output of the command block).
|
||||
5. `pos communication telegram listener` → AI bridge invocation includes `--no-command-execution` in the actual command string (verifiable by reading the source).
|
||||
6. Same for Matrix listener.
|
||||
7. `make gen && make check && make lint` all pass at 0 FAIL, 0 WARN.
|
||||
8. The `# POS_FLAGS:` header includes `--no-command-execution` and `completions/pos.bash` updates accordingly after `make gen`.
|
||||
|
||||
### Risks / open questions
|
||||
|
||||
- Changing the default from ALLOW to DENY is a **behavioral breaking change** for users who are accustomed to pressing Enter to run. This is intentional and justified by the security audit (V2): the model's output is untrusted. Users who want the old behavior can type `y`.
|
||||
- The `--no-command-execution` flag name is long. Alternatives: `--no-exec`, `--safe-mode`. **Decision:** `--no-command-execution` is preferred because it is self-documenting and unambiguous. The flag is consumed programmatically (by bridges), not by humans typing interactively.
|
||||
- Non-tty paths (`[ -w /dev/tty ] || return 0`) already prevent execution. `--no-command-execution` adds defense-in-depth for the tty path in case the tty check is ever removed.
|
||||
|
||||
---
|
||||
|
||||
## D-C: Test Framework Shape
|
||||
|
||||
### Decision
|
||||
|
||||
Create a committed `tests/` directory with a minimal runner and a `make test` target. The framework follows the repo's established stub-PATH + PTY patterns from `DOC/DEV.md:196-214`.
|
||||
|
||||
**Layout:**
|
||||
|
||||
```
|
||||
tests/
|
||||
run-tests.sh # runner (check helper, pass/fail counting, exit code)
|
||||
ai-server-flags.sh # unit: flag validation logic
|
||||
ai-hf-download.sh # unit: single-file failure path
|
||||
telegram-auth.sh # unit: auth gate logic (mocked)
|
||||
matrix-auth.sh # unit: auth gate logic (mocked)
|
||||
config-loader.sh # unit: load_env_file precedence
|
||||
systemd-unit.sh # unit: unit generation + systemd-analyze (skip if unavailable)
|
||||
uninstall-manifest.sh # unit: install/uninstall symmetry
|
||||
gen-docs-drift.sh # integration: make gen && git diff --check
|
||||
lint-gate.sh # integration: make lint exit code
|
||||
gpg-backup.sh # unit: passphrase not in argv (mocked gpg)
|
||||
config-precedence.sh # unit: env-wins-over-file precedence
|
||||
unsupported-flags.sh # unit: pos-ai-server unsupported flag handling
|
||||
ai-llama-detect.sh # unit: llama version detection (mocked binary)
|
||||
```
|
||||
|
||||
**Runner (`tests/run-tests.sh`):**
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Minimal test runner — sources test functions, counts pass/fail/skip.
|
||||
# Usage: tests/run-tests.sh [test-file ...]
|
||||
# If no args, runs all tests/*.sh files.
|
||||
PASS=0; FAIL=0; SKIP=0
|
||||
check() { local desc="$1" expected="$2" actual="$3"
|
||||
if [ "$expected" = "$actual" ]; then PASS=$((PASS+1)); printf ' PASS %s\n' "$desc"
|
||||
else FAIL=$((FAIL+1)); printf ' FAIL %s (expected=%s actual=%s)\n' "$desc" "$expected" "$actual"; fi }
|
||||
skip() { SKIP=$((SKIP+1)); printf ' SKIP %s (%s)\n' "$1" "$2"; }
|
||||
# ... file discovery, per-file sourcing, summary ...
|
||||
```
|
||||
|
||||
**`make test` target (Makefile addition):**
|
||||
|
||||
```makefile
|
||||
test:
|
||||
./tests/run-tests.sh
|
||||
```
|
||||
|
||||
**Interaction with existing gates:**
|
||||
- `make check` and `make lint` remain **unchanged** — they are static gates (syntax, exec bits, doc sync, convention).
|
||||
- `make test` is a **separate** target for behavioral regression tests.
|
||||
- CI (`.gitea/workflows/lint.yml`) does NOT need to run `make test` in this pass — that is a future CI enhancement. The tests exist for local validation and are committed as the regression baseline.
|
||||
|
||||
**Determinism and speed:**
|
||||
- All tests use stub PATH (temp dir with fake binaries) and env-overridable paths per `DEV.md:198`.
|
||||
- No network calls, no real systemd, no real llama-server binary.
|
||||
- Target: all tests complete in < 60 seconds on a typical dev box.
|
||||
- Tests that need `systemd-analyze` (for real validation) use a skip-if-unavailable pattern:
|
||||
```bash
|
||||
command -v systemd-analyze &>/dev/null || { skip "systemd-analyze validation" "systemd-analyze not available"; return 0; }
|
||||
```
|
||||
- Tests that need `jq` (for JSON assertions) use the same skip pattern.
|
||||
- Skip means "not applicable in this environment" — **never** "test passed." Tests never lie about pass/fail.
|
||||
|
||||
**What is unit-testable with stub PATH vs integration-only:**
|
||||
|
||||
| Test | Method | Skip condition |
|
||||
|------|--------|---------------|
|
||||
| AI server flag validation | Stub PATH with fake `llama-server` that echoes `--help` text | Never (fake binary is self-contained) |
|
||||
| AI HF single-file failure | Stub PATH with fake `curl` that returns rc=1 | Never |
|
||||
| Telegram auth gate | Direct function extraction (source the auth logic, call with test args) — or stub PATH with fake `jq`/`curl` | Never |
|
||||
| Matrix auth gate | Same as Telegram | Never |
|
||||
| Config loader | Direct sourcing of `load_env_file` | Never |
|
||||
| Systemd unit generation | Stub PATH with fake `systemd-escape`, `systemd-quote` output comparison | `systemd-analyze` not available → skip validation step |
|
||||
| Uninstall manifest | Direct comparison of install list vs uninstall list (grep both scripts) | Never |
|
||||
| Gen-docs drift | Run `make gen` then `git diff --check` | `git` repo not available → skip |
|
||||
| Lint gate | Run `make lint` and check exit code | Never |
|
||||
| GPG passphrase | Stub PATH with fake `gpg` that echoes argv to a log file; assert passphrase not in log | Never |
|
||||
| Config precedence | Export env var, write config file, call loader, assert env wins | Never |
|
||||
| Unsupported flags | Stub PATH with fake `llama-server` that echoes specific `--help` text | Never |
|
||||
| Llama version detect | Stub PATH with fake `llama-server` that echoes `--version` | Never |
|
||||
|
||||
### Files affected
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `tests/run-tests.sh` (new) | Test runner |
|
||||
| `tests/ai-server-flags.sh` (new) | Flag validation tests |
|
||||
| `tests/ai-hf-download.sh` (new) | HF single-file failure tests |
|
||||
| `tests/telegram-auth.sh` (new) | Telegram auth gate tests |
|
||||
| `tests/matrix-auth.sh` (new) | Matrix auth gate tests |
|
||||
| `tests/config-loader.sh` (new) | Config loader precedence tests |
|
||||
| `tests/systemd-unit.sh` (new) | Systemd unit generation tests |
|
||||
| `tests/uninstall-manifest.sh` (new) | Install/uninstall symmetry tests |
|
||||
| `tests/gen-docs-drift.sh` (new) | Gen-docs drift test |
|
||||
| `tests/lint-gate.sh` (new) | Lint gate test |
|
||||
| `tests/gpg-backup.sh` (new) | GPG passphrase tests |
|
||||
| `tests/config-precedence.sh` (new) | Config precedence tests |
|
||||
| `tests/unsupported-flags.sh` (new) | Unsupported flag handling tests |
|
||||
| `tests/ai-llama-detect.sh` (new) | Llama version detection tests |
|
||||
| `Makefile` | Add `test` target |
|
||||
| `scripts/check-sync.sh` | (Optional) Add `tests/*.sh` to the `bash -n` scan list |
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `make test` runs all 14 test files and reports PASS/FAIL/SKIP counts.
|
||||
2. `make test` exits 0 when all tests pass (or are skipped).
|
||||
3. `make test` exits non-zero when any test FAILs.
|
||||
4. No test makes network calls (verified by grepping test files for `curl`/`wget`/`http` without stub wrappers).
|
||||
5. All tests complete in < 60 seconds (measured on a representative dev box).
|
||||
6. `make check` and `make lint` are unaffected (0 FAIL, 0 WARN).
|
||||
7. `make gen` is unaffected.
|
||||
8. Tests that need `systemd-analyze` skip gracefully when unavailable (exit 0, report SKIP).
|
||||
9. The test runner does not leave temp files in the working tree (uses `/tmp` for all temp dirs).
|
||||
10. Each test file starts with `#!/usr/bin/env bash` + `set -euo pipefail` (passes `bash -n` and lint).
|
||||
|
||||
### Risks / open questions
|
||||
|
||||
- The test framework is **minimal by design**. It does not use bats, shunit2, or any external framework — consistent with the repo's zero-dependency philosophy. If the test surface grows beyond ~20 files, consider bats at that point.
|
||||
- Some tests (auth gate, config loader) require extracting logic from tools into testable functions or sourcing the tool and overriding variables. This is the established pattern from DEV.md's stub-PATH approach.
|
||||
- The gen-docs drift test depends on `make gen` being idempotent — which is a project invariant.
|
||||
- The lint gate test is inherently coupled to `scripts/lint-conventions.sh` behavior — if lint rules change, this test may need updating. Acceptable; it is a regression canary.
|
||||
|
||||
---
|
||||
|
||||
## D-D: Config Loader Centralization
|
||||
|
||||
### Decision
|
||||
|
||||
Establish `lib/config-ui.sh` as the canonical config loader. Add a single generic `load_env_file` function and migrate all 9 hand-rolled loaders to it.
|
||||
|
||||
**1. Add `load_env_file` to `lib/config-ui.sh`:**
|
||||
|
||||
```bash
|
||||
# Canonical env-file loader. Reads KEY=VALUE lines, strips comments and
|
||||
# CRLF, applies env-wins precedence (exported env vars are never overwritten).
|
||||
# Usage: load_env_file <file>
|
||||
load_env_file() {
|
||||
local f="$1" k v
|
||||
[ -f "$f" ] || return 0
|
||||
while IFS='=' read -r k v; do
|
||||
[ -n "$k" ] || continue
|
||||
case "$k" in \#*) continue ;; esac
|
||||
v="${v//$'\r'/}"
|
||||
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||
if [ -z "${!k:-}" ]; then
|
||||
export "$k"="$v"
|
||||
fi
|
||||
done < <(grep -E '^[A-Z_]+=' "$f" || true)
|
||||
}
|
||||
```
|
||||
|
||||
**Precedence contract (documented, matches dominant behavior):**
|
||||
```
|
||||
CLI flags > exported environment > config file values > defaults
|
||||
```
|
||||
This is the existing behavior in all 9 hand-rolled loaders. The `load_env_file` function implements "exported env wins over file" (the `if [ -z "${!k:-}" ]` check). CLI flags are applied later by the tool's own arg parser. Defaults are applied at variable declaration (`${VAR:-default}`). This contract is now explicit and single-sourced.
|
||||
|
||||
**2. Deprecate `load_system_env` in `lib/common.sh:146-159`:**
|
||||
|
||||
Make `load_system_env` delegate to `load_env_file`:
|
||||
```bash
|
||||
load_system_env() {
|
||||
load_env_file "$HOME/.config/linux_post_install/system.env"
|
||||
}
|
||||
```
|
||||
|
||||
This requires `common.sh` to source `config-ui.sh`. However, `common.sh` is the base library sourced by most tools, and `config-ui.sh` is not currently sourced by `common.sh`. **Decision:** do NOT make `common.sh` source `config-ui.sh`. Instead, keep `load_system_env` as-is (it is functionally identical to `load_env_file`) but note in `config-ui.sh` that `load_env_file` supersedes it. New tools use `load_env_file`; existing `load_system_env` callers continue working. This avoids a circular dependency risk and a mass-change to `common.sh` consumers.
|
||||
|
||||
The **real migration** targets are the 9 tools that hand-roll their own `load_config()`.
|
||||
|
||||
**3. CRLF policy (universal strip):**
|
||||
|
||||
`load_env_file` strips `\r` unconditionally (`v="${v//$'\r'/}"`). This resolves the 5-tool vs 5-tool CRLF divergence identified in the audit. All 5 tools that currently strip CRLF will continue to work; the 5 that don't will now strip it too (defensive improvement, no behavioral regression).
|
||||
|
||||
**4. XDG honoring:**
|
||||
|
||||
`load_env_file` takes an absolute file path — it does not resolve `CONFIG_DIR`. The caller passes the full path. For tools that currently hardcode `$HOME/.config/linux_post_install/...`, the migration replaces the hardcoded path with `${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}/<file>`. Since `common.sh` defines `CONFIG_DIR` at line 19, tools sourcing `common.sh` already have it; the self-contained communication tools already have their own `CONFIG_DIR` guarded copy.
|
||||
|
||||
**Migration list (9 tools):**
|
||||
|
||||
| Tool | Current function | Current file | Notes |
|
||||
|------|-----------------|-------------|-------|
|
||||
| `bin/pos-communication-telegram-sender` | `load_config:61-74` | `telegram.env` | Already sources `config-ui.sh` for `cfg_value` |
|
||||
| `bin/pos-communication-telegram-listener` | `load_config:86-98` | `telegram.env` | Standalone |
|
||||
| `bin/pos-communication-matrix-listener` | `load_config:65-77` | `matrix.env` | Standalone |
|
||||
| `bin/pos-communication-matrix-sender` | `load_config:44-57` | `matrix.env` | Standalone |
|
||||
| `bin/pos-communication-scrcpy` | `load_config:14-28` | `scrcpy.env` | Standalone |
|
||||
| `bin/pos-ai` | `load_config:130-160` | `ai.env` + legacy files | Has extra legacy-file loop |
|
||||
| `bin/pos-ai-server` | `load_config:21-35` | `ai.env` | Standalone |
|
||||
| `bin/pos-ai-hf` | `load_hf_config:30-44` | `ai.env` | Standalone |
|
||||
| `bin/pos-media-grab` | `load_grab_config:10-23` | `grab.env` | Standalone |
|
||||
|
||||
For each tool: source `config-ui.sh` (with the existing fallback chain pattern), replace the hand-rolled function body with `load_env_file "$CONFIG_FILE"`, preserve any tool-specific extras (like `pos-ai`'s legacy-file loop — keep that as a second `load_env_file` call on the legacy path).
|
||||
|
||||
**5. Collapse entertainment-lib read/write into config-ui:**
|
||||
|
||||
`entertainment-lib.sh`'s `config_value` and `write_config_key` (lines 28-54) are functionally identical to `cfg_value` and `cfg_write` in `config-ui.sh` (lines 311-344). **Decision:** add thin wrappers in `entertainment-lib.sh` that delegate to `cfg_value`/`cfg_write`:
|
||||
|
||||
```bash
|
||||
config_value() {
|
||||
local k="$1"
|
||||
cfg_value "$CONFIG_FILE" "$k"
|
||||
}
|
||||
write_config_key() {
|
||||
local key="$1" val="$2"
|
||||
cfg_write "$CONFIG_FILE" "$key" "$val"
|
||||
}
|
||||
```
|
||||
|
||||
This preserves the existing `config_value`/`write_config_key` API for the 5 entertainment tools that call them, while the implementation lives in one place. `entertainment-lib.sh` must source `config-ui.sh` (add to its source chain).
|
||||
|
||||
### Files affected
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `lib/config-ui.sh` | Add `load_env_file` function |
|
||||
| `bin/pos-communication-telegram-sender` | Replace `load_config` body with `load_env_file` |
|
||||
| `bin/pos-communication-telegram-listener` | Replace `load_config` body with `load_env_file` |
|
||||
| `bin/pos-communication-matrix-listener` | Replace `load_config` body with `load_env_file` |
|
||||
| `bin/pos-communication-matrix-sender` | Replace `load_config` body with `load_env_file` |
|
||||
| `bin/pos-communication-scrcpy` | Replace `load_config` body with `load_env_file` |
|
||||
| `bin/pos-ai` | Replace `load_config` body with `load_env_file` + legacy loop |
|
||||
| `bin/pos-ai-server` | Replace `load_config` body with `load_env_file` |
|
||||
| `bin/pos-ai-hf` | Replace `load_hf_config` body with `load_env_file` |
|
||||
| `bin/pos-media-grab` | Replace `load_grab_config` body with `load_env_file` |
|
||||
| `lib/entertainment-lib.sh` | Delegate `config_value`/`write_config_key` to `cfg_value`/`cfg_write` |
|
||||
| `DOC/DEV.md` | Update "No shared lib? Inline fallbacks" section to reference `config-ui.sh` as the canonical loader |
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `grep -rn 'while IFS.*read.*k.*v' bin/pos-communication-* bin/pos-ai* bin/pos-media-grab` returns **zero** hand-rolled loader matches (all replaced).
|
||||
2. Each migrated tool passes its existing behavioral expectations: env-wins precedence, CRLF stripping, missing-file graceful return (rc 0).
|
||||
3. `pos config telegram` and `pos config matrix` display and mask values correctly after migration.
|
||||
4. `entertainment-lib.sh` `config_value` and `write_config_key` still work (entertainment tools pass their tests).
|
||||
5. `make gen && make check && make lint` all pass at 0 FAIL, 0 WARN.
|
||||
6. No tool that previously sourced `common.sh` now additionally sources `config-ui.sh` unless it was already doing so. Tools that were standalone (communication tools) now source `config-ui.sh` via the fallback chain, which is the same pattern used by their existing inline copies.
|
||||
|
||||
### Risks / open questions
|
||||
|
||||
- **Self-contained tools sourcing config-ui.sh:** The 5 communication tools currently do NOT source any shared lib (they carry inline fallbacks). After migration, they source `config-ui.sh`. This is a net improvement (shared implementation) but changes the "no shared lib" classification of these tools. **Mitigation:** the inline fallback copies of `log`/`warn`/`err` stay in place; only the `load_config` body is replaced. The tools remain self-contained for basic logging; they just share the config loader.
|
||||
- **pos-ai's legacy file loop:** `pos-ai:130-160` reads `ai.env` plus legacy `gemini.env` and `openrouter.env` files. This is tool-specific logic; keep it as a second pass after `load_env_file "$CONFIG_FILE"`. Do not try to generalize the legacy loop into `load_env_file`.
|
||||
- **Regression risk:** Each migration touches a working tool's config loading. The test suite (D-C) covers config precedence, which mitigates this.
|
||||
|
||||
---
|
||||
|
||||
## D-E: Install/Uninstall Manifest
|
||||
|
||||
### Decision
|
||||
|
||||
Close the install-only gaps in `pos-system-uninstall` with the smallest targeted fixes. Do NOT introduce a full manifest abstraction (like `lib/install-manifest.sh`) — that would be a larger refactor inconsistent with the current "list-based" approach in both `install.sh` and `pos-system-uninstall`.
|
||||
|
||||
**1. Add 9 orphaned libs to Tier 1 scan and removal:**
|
||||
|
||||
Extend the lib list at `bin/pos-system-uninstall:62-64` (scan) and `:238-239` (remove) from:
|
||||
```bash
|
||||
for f in common.sh menu-lib.sh share-lib.sh; do
|
||||
```
|
||||
to:
|
||||
```bash
|
||||
for f in common.sh menu-lib.sh share-lib.sh flags.sh notify.sh entertainment-lib.sh scheduler-lib.sh config-ui.sh user-timers-lib.sh entertainment-plugin-lib.sh usb-lib.sh registry.sh; do
|
||||
```
|
||||
|
||||
Same change in both `scan_tier1` and `remove_tier1`.
|
||||
|
||||
**2. Add ScaleTail directory and feature-flag store to Tier 1 removal:**
|
||||
|
||||
After the lib removal block in `remove_tier1`:
|
||||
```bash
|
||||
# ScaleTail templates
|
||||
if [ -d /usr/local/share/linux_post_install/scale-tail ]; then
|
||||
rm -rf /usr/local/share/linux_post_install/scale-tail && count=$((count+1))
|
||||
fi
|
||||
|
||||
# Feature-flag store
|
||||
if [ -d /usr/local/share/linux_post_install/flags ]; then
|
||||
rm -rf /usr/local/share/linux_post_install/flags && count=$((count+1))
|
||||
fi
|
||||
|
||||
# Clean up parent dir if empty
|
||||
rmdir /usr/local/share/linux_post_install 2>/dev/null || true
|
||||
```
|
||||
|
||||
Add corresponding scan entries in `scan_tier1`:
|
||||
```bash
|
||||
[ -d /usr/local/share/linux_post_install/scale-tail ] && found+=("/usr/local/share/linux_post_install/scale-tail/")
|
||||
[ -d /usr/local/share/linux_post_install/flags ] && found+=("/usr/local/share/linux_post_install/flags/")
|
||||
```
|
||||
|
||||
**3. Add USER systemd unit discovery to Tier 1:**
|
||||
|
||||
After the system-unit removal block (`:287-311`), add:
|
||||
```bash
|
||||
# USER systemd units (pos-* and pos-entertainment-* and pos-schedule-*)
|
||||
local user_unit_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
if [ -d "$user_unit_dir" ]; then
|
||||
local ufile
|
||||
while IFS= read -r ufile; do
|
||||
local uname
|
||||
uname="$(basename "$ufile" .service)"
|
||||
uname="${uname%.timer}"
|
||||
systemctl --user disable --now "${basename "$ufile"}" 2>/dev/null || true
|
||||
rm -f "$ufile" && count=$((count+1))
|
||||
done < <(find "$user_unit_dir" -maxdepth 1 -name 'pos-*' -type f 2>/dev/null || true)
|
||||
systemctl --user daemon-reload 2>/dev/null || true
|
||||
fi
|
||||
```
|
||||
|
||||
Add corresponding scan in `scan_tier1`:
|
||||
```bash
|
||||
local user_unit_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
if [ -d "$user_unit_dir" ]; then
|
||||
while IFS= read -r ufile; do
|
||||
found+=("user-unit: $(basename "$ufile")")
|
||||
done < <(find "$user_unit_dir" -maxdepth 1 -name 'pos-*' -type f 2>/dev/null || true)
|
||||
fi
|
||||
```
|
||||
|
||||
**4. De-hardcode entertainment-plugin and prebuilt-binary lists:**
|
||||
|
||||
Replace the hardcoded loops for entertainment plugins (`:72-74` scan, `:248-250` remove) with directory-driven discovery (matching `install.sh`'s pattern):
|
||||
```bash
|
||||
# Scan: discover plugins by POS_PLUGIN marker
|
||||
while IFS= read -r f; do
|
||||
found+=("$f")
|
||||
done < <(for ep in /usr/local/bin/*.sh; do
|
||||
grep -q '^# POS_PLUGIN:' "$ep" 2>/dev/null && echo "$ep"
|
||||
done | sort)
|
||||
```
|
||||
```bash
|
||||
# Remove: all installed entertainment plugins (discovered by POS_PLUGIN marker)
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] && { rm -f "$f" && count=$((count+1)); }
|
||||
done < <(for ep in /usr/local/bin/*.sh; do
|
||||
grep -q '^# POS_PLUGIN:' "$ep" 2>/dev/null && echo "$ep"
|
||||
done | sort)
|
||||
```
|
||||
|
||||
**5. Fix the `.bash_completion` over-broad removal (H-002):**
|
||||
|
||||
Replace `sed -i '/pos/d'` at `:333` with marker-based or anchored patterns:
|
||||
```bash
|
||||
# Only remove lines the installer added (source pos.bash pattern)
|
||||
sed -i '/source.*pos\.bash/d' "$HOME/.bash_completion"
|
||||
```
|
||||
|
||||
This is conservative: only lines that `postinstall.sh` would have added are removed. If no such lines exist, nothing is touched.
|
||||
|
||||
**6. Fix `.bashrc` removal anchoring (H-003):**
|
||||
|
||||
Add word-boundary anchors to the sed patterns at `:320-322`:
|
||||
```bash
|
||||
sed -i '/source.*pos-ai-hook\.sh/d' "$HOME/.bashrc"
|
||||
sed -i '/linux_post_install.*PATH.*pos/d' "$HOME/.bashrc"
|
||||
sed -i '/source.*pos\.bash/d' "$HOME/.bashrc"
|
||||
```
|
||||
|
||||
**7. Document deliberately surviving artifacts:**
|
||||
|
||||
Add a comment block in `pos-system-uninstall`'s `usage()` and in the scan output:
|
||||
```bash
|
||||
# Intentionally NOT removed (user-managed):
|
||||
# - apt packages (system packages)
|
||||
# - /usr/local/bin/yt-dlp (manually installed)
|
||||
# - ~/.config/rclone/ (rclone manages its own config)
|
||||
# - ~/.ssh/authorized_keys additions (user SSH access)
|
||||
# - pos-owned config files (removed by --config tier)
|
||||
# - pos-owned data files (removed by --data tier)
|
||||
```
|
||||
|
||||
### Files affected
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `bin/pos-system-uninstall:62-64` | Add 9 libs to scan list |
|
||||
| `bin/pos-system-uninstall:238-239` | Add 9 libs to remove list |
|
||||
| `bin/pos-system-uninstall:248-250` | De-hardcode entertainment plugins |
|
||||
| `bin/pos-system-uninstall:260-262` | Keep prebuilt binaries (already correct, keep as-is for now) |
|
||||
| `bin/pos-system-uninstall` (after 311) | Add ScaleTail dir + flags dir removal |
|
||||
| `bin/pos-system-uninstall` (after 311) | Add USER systemd unit scan + removal |
|
||||
| `bin/pos-system-uninstall:72-74` | De-hardcode entertainment plugin scan |
|
||||
| `bin/pos-system-uninstall:333` | Fix `.bash_completion` over-broad removal |
|
||||
| `bin/pos-system-uninstall:320-322` | Fix `.bashrc` anchoring |
|
||||
| `bin/pos-system-uninstall` (usage) | Document deliberately surviving artifacts |
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `scan_tier1` shows all 12 libs (3 original + 9 new) when installed.
|
||||
2. `remove_tier1` removes all 12 libs from `/usr/local/bin/`.
|
||||
3. After `remove_tier1`, `/usr/local/share/linux_post_install/` is empty or removed.
|
||||
4. After `remove_tier1`, no `pos-*` user units remain in `~/.config/systemd/user/`.
|
||||
5. `.bashrc` removal does not remove lines unrelated to pos (verified by test: a `.bashrc` with `source pos.bash` in a comment is not affected).
|
||||
6. `.bash_completion` removal does not remove lines unrelated to pos (the `sed '/pos/d'` is replaced with anchored patterns).
|
||||
7. `make gen && make check && make lint` all pass at 0 FAIL, 0 WARN.
|
||||
8. The install manifest test (D-C `tests/uninstall-manifest.sh`) confirms: every artifact in the install scan has a corresponding removal path.
|
||||
|
||||
### Risks / open questions
|
||||
|
||||
- **USER systemd unit removal during `remove_tier1`:** This calls `systemctl --user` which may fail if no user session is available (e.g., running uninstall as a scheduled task). **Mitigation:** wrap in `2>/dev/null || true` (idempotent, like the system-unit path).
|
||||
- **Prebuilt binaries list is still hardcoded** (`wihotspot`, `wihotspot-gui`, `create_ap`). De-hardcoding this would require a marker system similar to POS_PLUGIN, which is overengineering for 3 binaries. Keep as-is for now; document as a known limitation.
|
||||
- **`/usr/local/share/linux_post_install` parent directory** is only removed if empty after removing `scale-tail` and `flags/`. This is safe because the parent is also the ScaleTail clone destination.
|
||||
|
||||
---
|
||||
|
||||
## D-F: pos-ai-server Validation Semantics
|
||||
|
||||
### Decision
|
||||
|
||||
Expand flag validation to cover **every flag** that will appear in `ExecStart`, not just CLI-explicit ones. Distinguish between "requested" (user or config intent) and "default" (tool-emitted) flags with different error behavior.
|
||||
|
||||
**1. Expand validation scope to all ExecStart flags:**
|
||||
|
||||
Currently, only `REQUESTED_FLAGS` (CLI-parsed at `:295-359`) is validated at `:407-409`. The always-emitted defaults (`--port`, `--host`, `--n-gpu-layers`, `--ctx-size`, `--threads`) and config-sourced optional flags (`--gpu-threads`, `--tensor-split`, `--batch-size`, etc.) are emitted into `exec_cmd` at `:445-490` without validation.
|
||||
|
||||
After this change, **all flags in `exec_cmd`** are validated. The validation split:
|
||||
|
||||
- **Requested flags** (CLI OR config-sourced): If unsupported, **hard error**. The user explicitly asked for something that doesn't work — fail loudly.
|
||||
- **Default flags** (emitted without user intent): If unsupported, **warn + omit**. The tool chose a default that this build doesn't support — silently degrade.
|
||||
|
||||
**2. How config-sourced flags get marked "requested":**
|
||||
|
||||
Add config-sourced flags to `REQUESTED_FLAGS` during config loading. After `load_config` in `cmd_start` (around `:395`), read the config file for keys that map to CLI flags and add them to `REQUESTED_FLAGS`:
|
||||
|
||||
```bash
|
||||
# After load_config, mark config-sourced flags as requested
|
||||
if [ -n "${LLAMACPP_PORT:-}" ] && [ "$PORT" != "8088" ]; then
|
||||
REQUESTED_FLAGS+=("--port")
|
||||
fi
|
||||
if [ -n "${LLAMACPP_HOST:-}" ] && [ "$HOST" != "127.0.0.1" ]; then
|
||||
REQUESTED_FLAGS+=("--host")
|
||||
fi
|
||||
# ... similar for other config keys that map to flags
|
||||
```
|
||||
|
||||
Wait — this is too fragile (needs manual comparison with defaults). **Better approach:** introduce a `CONFIG_REQUESTED_FLAGS` array that is populated during `load_config` when a config key that maps to a flag is actually set in the file:
|
||||
|
||||
```bash
|
||||
CONFIG_REQUESTED_FLAGS=()
|
||||
|
||||
load_config() {
|
||||
local f="$CONFIG_FILE"
|
||||
[ -f "$f" ] || return 0
|
||||
while IFS='=' read -r k v; do
|
||||
[ -n "$k" ] || continue
|
||||
case "$k" in \#*) continue ;; esac
|
||||
v="${v//$'\r'/}"
|
||||
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||
if [ -z "${!k:-}" ]; then
|
||||
export "$k"="$v"
|
||||
# Track config keys that map to flags
|
||||
case "$k" in
|
||||
LLAMACPP_PORT) CONFIG_REQUESTED_FLAGS+=("--port") ;;
|
||||
LLAMACPP_HOST) CONFIG_REQUESTED_FLAGS+=("--host") ;;
|
||||
LLAMACPP_CTX_SIZE) CONFIG_REQUESTED_FLAGS+=("--ctx-size") ;;
|
||||
LLAMACPP_GPU_LAYERS) CONFIG_REQUESTED_FLAGS+=("--n-gpu-layers") ;;
|
||||
LLAMACPP_THREADS) CONFIG_REQUESTED_FLAGS+=("--threads") ;;
|
||||
# ... other config-to-flag mappings
|
||||
esac
|
||||
fi
|
||||
done < <(grep -E '^[A-Z_]+=' "$f" || true)
|
||||
}
|
||||
```
|
||||
|
||||
Then in `cmd_start`:
|
||||
```bash
|
||||
# Merge CLI-requested and config-requested flags
|
||||
ALL_REQUESTED_FLAGS=("${REQUESTED_FLAGS[@]}" "${CONFIG_REQUESTED_FLAGS[@]}")
|
||||
# Deduplicate
|
||||
local deduped=()
|
||||
for flag in "${ALL_REQUESTED_FLAGS[@]}"; do
|
||||
case " ${deduped[*]:-} " in *" $flag "*) continue ;; esac
|
||||
deduped+=("$flag")
|
||||
done
|
||||
|
||||
# Validate requested flags (hard error on unsupported)
|
||||
if [ "${#deduped[@]}" -gt 0 ]; then
|
||||
validate_requested_flags "$llamacpp_bin" "$version" "${deduped[@]}"
|
||||
fi
|
||||
|
||||
# Validate default flags (warn + omit on unsupported)
|
||||
validate_default_flags "$llamacpp_bin" "$version"
|
||||
```
|
||||
|
||||
**3. New `validate_default_flags` function:**
|
||||
|
||||
```bash
|
||||
# validate_default_flags <binary> <version> — for every flag that will be
|
||||
# emitted by default (without user intent), check it exists in --help.
|
||||
# Unsupported defaults are silently omitted from exec_cmd.
|
||||
# Sets global flags: DEFAULT_PORT_OK, DEFAULT_HOST_OK, etc.
|
||||
DEFAULT_FLAGS_VALIDATED=0
|
||||
validate_default_flags() {
|
||||
local bin="$1" version="$2"
|
||||
local help_text
|
||||
help_text="$("$bin" --help 2>/dev/null)" || {
|
||||
warn "Cannot obtain llama-server --help output — skipping default flag validation"
|
||||
return 0
|
||||
}
|
||||
DEFAULT_PORT_OK=1; DEFAULT_HOST_OK=1; DEFAULT_CTX_OK=1
|
||||
DEFAULT_GPU_OK=1; DEFAULT_THREADS_OK=1
|
||||
local check_flag() {
|
||||
local flag="$1" varname="$2"
|
||||
if ! printf '%s' "$help_text" | grep -qF -- "$flag"; then
|
||||
warn "installed llama.cpp ${version} does not support default flag ${flag} — omitting"
|
||||
eval "$varname=0"
|
||||
fi
|
||||
}
|
||||
check_flag "--port" DEFAULT_PORT_OK
|
||||
check_flag "--host" DEFAULT_HOST_OK
|
||||
check_flag "--ctx-size" DEFAULT_CTX_OK
|
||||
check_flag "--n-gpu-layers" DEFAULT_GPU_OK
|
||||
check_flag "--threads" DEFAULT_THREADS_OK
|
||||
DEFAULT_FLAGS_VALIDATED=1
|
||||
}
|
||||
```
|
||||
|
||||
Then in `exec_cmd` construction (`:445-490`), wrap each default flag in a validation check:
|
||||
```bash
|
||||
exec_cmd="$(systemd_quote "$llamacpp_full") -m $(systemd_quote "$model")"
|
||||
[ "$DEFAULT_PORT_OK" -eq 1 ] && exec_cmd+=" --port $PORT"
|
||||
[ "$DEFAULT_HOST_OK" -eq 1 ] && exec_cmd+=" --host $HOST"
|
||||
[ "$DEFAULT_GPU_OK" -eq 1 ] && exec_cmd+=" --n-gpu-layers $gpu_layers"
|
||||
[ "$DEFAULT_CTX_OK" -eq 1 ] && exec_cmd+=" --ctx-size $CTX_SIZE"
|
||||
[ "$DEFAULT_THREADS_OK" -eq 1 ] && exec_cmd+=" --threads $THREADS"
|
||||
```
|
||||
|
||||
**4. `--help` unreadable → single warn + proceed (unchanged):**
|
||||
|
||||
The existing `help_text` fetch at `:72-75` already handles this: `warn "Cannot obtain llama-server --help output — skipping flag validation"; return 0`. This behavior is preserved for both `validate_requested_flags` and `validate_default_flags`.
|
||||
|
||||
**5. Make detected version inform error messaging (no version branching):**
|
||||
|
||||
The version is already interpolated into error strings (`:84`): `"installed llama.cpp ${version} does not expose ${flag}"`. This is correct and sufficient. No version-conditional logic or compat table is added. The version is informational for the user's debugging, not a branching variable.
|
||||
|
||||
**6. Fix the substring matching issue (D4, low priority):**
|
||||
|
||||
Replace `grep -qF -- "$flag"` at `:83` with a word-boundary match:
|
||||
```bash
|
||||
if ! printf '%s' "$help_text" | grep -qE -- "(^|[[:space:]])${flag}($|[[:space:]])"; then
|
||||
```
|
||||
|
||||
This prevents false-positive substring matches (e.g., `--mmap` matching `--no-mmap` if such a flag existed). Low priority but trivial to fix during this pass.
|
||||
|
||||
### Files affected
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `bin/pos-ai-server:21-35` | Extend `load_config` to populate `CONFIG_REQUESTED_FLAGS` |
|
||||
| `bin/pos-ai-server:66-87` | Fix `validate_requested_flags` substring matching (D4) |
|
||||
| `bin/pos-ai-server` (new function) | Add `validate_default_flags` |
|
||||
| `bin/pos-ai-server:407-409` | Extend validation to merge CLI + config requested flags |
|
||||
| `bin/pos-ai-server:445-490` | Wrap default flags in `DEFAULT_*_OK` guards |
|
||||
| `DOC/POS.md` (ai server section) | Document validation behavior: requested → hard error, default → warn+omit |
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. A config file with `LLAMACPP_PORT=9999` causes `--port` to be added to `REQUESTED_FLAGS` and validated against `--help`.
|
||||
2. If the binary's `--help` does not list `--port`, the server refuses to start with an error naming the flag and the detected version.
|
||||
3. If the binary's `--help` does not list `--threads` (a default), `--threads` is omitted from `ExecStart` with a warning.
|
||||
4. If `--help` cannot be read, all flags are accepted (warn + proceed, unchanged behavior).
|
||||
5. The `grep -qF` substring issue (D4) is resolved — `--mmap` no longer matches if only `--no-mmap` appears in `--help`.
|
||||
6. Generated `ExecStart` contains only flags that pass validation.
|
||||
7. `make gen && make check && make lint` all pass at 0 FAIL, 0 WARN.
|
||||
|
||||
### Risks / open questions
|
||||
|
||||
- The baseline-compatible flag set (`--port`, `--host`, `--n-gpu-layers`, `--ctx-size`, `--threads`) is assumed universal across modern llama.cpp builds. If a very old build lacks even `--port`, the server would start with `ExecStart` containing only the binary and model — functional but unusual. This is an acceptable degradation path.
|
||||
- The `CONFIG_REQUESTED_FLAGS` approach requires manually maintaining the config-key-to-flag mapping in `load_config`. If a new config key is added, the mapping must be updated. This is a small maintenance burden; document the mapping clearly.
|
||||
- The default-flags guard changes the ExecStart generation logic from unconditional to conditional. If `validate_default_flags` has a bug, the ExecStart could be missing expected flags. The test suite (D-C) covers this case.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Execution Order
|
||||
|
||||
The decisions have dependencies and independent tracks. Here is the recommended order:
|
||||
|
||||
### Phase 1: Infrastructure (no behavior changes, green gates)
|
||||
|
||||
1. **D-C (Test framework)** — Create `tests/` + `make test` + runner. Write skeleton test files with `skip` for features not yet implemented. Verify `make test` runs and exits 0 (all skipped). This establishes the regression safety net for all subsequent changes.
|
||||
2. **D-D part 1 (Config loader)** — Add `load_env_file` to `lib/config-ui.sh`. Do NOT migrate tools yet. Verify gates green.
|
||||
|
||||
### Phase 2: Security fixes (highest priority, isolated files)
|
||||
|
||||
3. **D-A (Telegram/Matrix auth)** — The Telegram fix is self-contained in `bin/pos-communication-telegram-listener` + `bin/pos-communication-telegram-sender` POS_CONFIG header + config template. The Matrix fix is a one-line guard in `bin/pos-communication-matrix-listener`. Both are isolated.
|
||||
4. **D-B (AI eval posture)** — Flip the confirmation default and add `--no-command-execution` flag. Touches `bin/pos-ai` + both listeners' AI bridge calls. Independent of D-A (different lines in the listeners).
|
||||
|
||||
### Phase 3: Validation and correctness
|
||||
|
||||
5. **D-F (pos-ai-server validation)** — Extends the validation in `bin/pos-ai-server`. Depends on D-D being complete (the config loader migration should happen first so `load_config` in `pos-ai-server` is already migrated — or do D-F's config-requested-flags addition within the hand-rolled loader before migration, then adjust during D-D migration). **Decision:** do D-F first (it modifies the hand-rolled loader in `pos-ai-server`), then D-D migration replaces it. This avoids double-touching.
|
||||
6. **D-E (Install/uninstall manifest)** — Self-contained in `bin/pos-system-uninstall`. Independent of all others except D-C (the uninstall test depends on the manifest being complete).
|
||||
|
||||
### Phase 4: Config loader migration
|
||||
|
||||
7. **D-D parts 2-5 (Migrate loaders, collapse entertainment-lib)** — Migrate the 9 tools' hand-rolled loaders to `load_env_file`. This touches many files but each change is mechanical. Do it last so all other changes (D-A, D-B, D-F) are already in place and their config keys are stable.
|
||||
|
||||
### Phase 5: Validation
|
||||
|
||||
8. Run full gate suite: `make gen && make check && make lint` — verify 0 FAIL, 0 WARN.
|
||||
9. Run `make test` — verify all tests pass (no more skips for implemented features).
|
||||
|
||||
### Builder-track split (parallel-safe groups)
|
||||
|
||||
**Group A (communication tools):** D-A Telegram + D-A Matrix → single Builder, one commit
|
||||
**Group B (AI tools):** D-B (pos-ai) + D-F (pos-ai-server) → single Builder, one commit
|
||||
**Group C (infrastructure):** D-C (tests) + D-D (config-ui.sh + loader) → single Builder, one commit
|
||||
**Group D (uninstaller):** D-E → single Builder, one commit
|
||||
**Group E (migration):** D-D tool migrations (after A+B+D complete) → single Builder, one commit
|
||||
|
||||
Groups A, B, C, and D are **parallel-safe** (no file overlap). Group E depends on B and D completing (to avoid merge conflicts in `pos-ai-server` and `pos-communication-*` tools).
|
||||
|
||||
---
|
||||
|
||||
## Open Items (require future decisions, not in this pass)
|
||||
|
||||
1. **Trusted-group mode for Telegram:** Explicit opt-in `TELEGRAM_GROUP_MODE=true` with per-user allowlist. Deferred — the current AND-gate is sufficient for the primary use case (1:1 private chat with the owner).
|
||||
2. **Lint performance optimization:** The Explorer identified ~17 hotspots in `scripts/lint-conventions.sh` (Task 1). This is a performance improvement, not a correctness fix — separate from the stabilization pass.
|
||||
3. **LLAMACPP_HOST coherence (D2 from AI audit):** The server honors `LLAMACPP_HOST` but the llamacpp provider adapter and probes hardcode `127.0.0.1`. This is a behavioral coherence issue, not a security issue. Deferred — it requires changes to `lib/ai-providers/llamacpp.sh` and the server's probe paths, which is a feature-level fix.
|
||||
4. **pos-ai-hf single-file download failure (D1 from AI audit):** The single-file download path does not record failures in `failed_files`, so `.hf-meta` is written for a partially-downloaded model. This is a HIGH defect but is a pure bug fix (not an architectural decision) — belongs in Builder scope directly, not in this design pass.
|
||||
5. **GPG passphrase in argv (V3 from Security audit):** Fixing this requires `gpg --batch --passphrase-fd` or a temp-file approach. Behavioral change to `bin/pos-system-backup` — defer to a separate fix commit.
|
||||
6. **H-002/H-003 bashrc/bash_completion over-broad removal:** Partially addressed in D-E (`.bash_completion` fix) but a more thorough marker-based approach would be ideal. The D-E fix is the minimum viable improvement.
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-06 (post-merge) — llama-server start breakage root cause + fix decisions
|
||||
|
||||
**References:** `AgentsReport/detective/2026-09-06_ai-server-breakage.md` (root cause, classified **FACT**); full decisions in `AgentsReport/architect/2026-09-06_ai-server-fix-design.md`.
|
||||
|
||||
**Summary:** A user's `pos ai server start` failing after `bash apps/install.sh llamacpp` traced to four independent defects in `bin/pos-ai-server` (plus one adapter-default interaction): (1) `detect_llama_version:71` discards stderr, and llama.cpp b10822 prints its version to stderr → always "unknown"; (2) `printf|grep -q` under `set -o pipefail` → SIGPIPE race (rc=141) → flags randomly rejected; (3) `resolve_model:229-259` rejects the HF-downloader's `<dir>/<file>.gguf` layout; (4) unit write precedes an unguarded `systemctl --user daemon-reload:605` that fails under SSH w/o `XDG_RUNTIME_DIR` → orphaned unit. Port pinning (llama.cpp default 8080 vs tool/adapter 8088) is downstream of the race.
|
||||
|
||||
**Decisions added (DQ1-DQ6, see the fix-design file for full rationale):** help-gated validation retained (no version→capability map; version is cosmetic); `resolve_model` expands a single-gguf dir (exact precedence, no silent multi-pick); user-bus pre-flight via new `ensure_user_bus` in `lib/common.sh` + `--no-unit` direct-run on `pos-ai-server`; `find_llamacpp` narrowed to `llama-server`/`llama-server-cuda`; `--port $PORT` always pinned; installer post-install sanity in scope. **Ratified fix scope F1-F7, all KEEP** (F4 adjusted to add `--no-unit` + shared helper; F2's SIGPIPE mechanism supersedes the earlier D-F's descriptive reliance on the broken pipeline). **Deferred:** adapter dual-port probing fallback, direct-run for matrix-listener/network-download, and the previously-tracked D1/D2 AI-server defects.
|
||||
|
||||
**Consistency with prior decisions (no D-A..D-F text altered above):** D-F's requested-vs-default validation semantics (requested → hard err, default → warn+omit) remains the contract; F2 fixes the mechanism that made D-F's help-gate non-deterministic, and F6 (port pinning) is a direct realization of D-F's always-emitted-defaults guarantee. The bus pre-flight (F4) extends what `scheduler-lib.sh:614` already does (a *query* guard) into a hard pre-write *enable* guard shared across the three tools that write user units.
|
||||
@@ -1,68 +0,0 @@
|
||||
# Builder Report — R7: `--last` for ANY command's output
|
||||
|
||||
## TL;DR
|
||||
- **Status:** IMPLEMENTED
|
||||
- **Files changed:** `bin/pos-ai-gemini` (565→586 ln), `bin/pos-ai-openrouter` (566→587 ln), `lib/pos-ai-hook.sh` (new, 32 ln), `DOC/howto/ai.md`, `DOC/POS.md`, `DOC/AGENT_Context_Project.md` (gen), `completions/pos.bash` (gen)
|
||||
- **Gates:** `bash -n` ✓, `make gen` ✓, `make check` OK, `make lint` 0 FAIL 0 WARN
|
||||
- **Verification:** all probes (a)–(i) pass
|
||||
- **No commits made**
|
||||
|
||||
## Step 1: Add `cmd_capture` + `capture` subcommand to `bin/pos-ai-gemini`
|
||||
[DONE]
|
||||
- Updated `# POS:` header and `# POS_SUBCMDS` to include `capture`
|
||||
- Added `LAST_CMD_OUTPUT_FILE` constant (line 15)
|
||||
- Added `cmd_capture()` function before `cmd_ask()` — runs cmd, tees to file + terminal, prints `[captured → ...]` on stderr, returns cmd's exit code
|
||||
- Added `capture` case to dispatch switch
|
||||
|
||||
## Step 2: Add `cmd_capture` + `capture` subcommand to `bin/pos-ai-openrouter`
|
||||
[DONE]
|
||||
- Identical changes as Step 1, with tool-appropriate naming
|
||||
|
||||
## Step 3: Update `cmd_ask` --last fallback chain in `bin/pos-ai-gemini`
|
||||
[DONE]
|
||||
- `--last` block now tries two sources in priority order: (1) `newest_pos_log()` for pos dispatcher logs, (2) `$LAST_CMD_OUTPUT_FILE` for captured output
|
||||
- Error message updated to mention `capture` subcommand
|
||||
|
||||
## Step 4: Update `cmd_ask` --last fallback chain in `bin/pos-ai-openrouter`
|
||||
[DONE]
|
||||
- Identical fallback chain changes
|
||||
|
||||
## Step 5: Update --last guard + usage() in both tools
|
||||
[DONE]
|
||||
- `--last` guard error now mentions `capture` subcommand
|
||||
- `usage()` updated: capture subcommand documented, `--last` description mentions both sources
|
||||
- Examples updated with capture + ask --last workflow
|
||||
|
||||
## Step 6: Create `lib/pos-ai-hook.sh`
|
||||
[DONE]
|
||||
- Sourceable file for `.bashrc` auto-capture
|
||||
- `bash -n` clean, creates file on source, only activates in interactive terminals
|
||||
- Sets `__POS_CAPTURE_ACTIVE=1`; truncated at 1 MB
|
||||
|
||||
## Step 7: Update `DOC/howto/ai.md`
|
||||
[DONE]
|
||||
- Added `capture` row to tool table
|
||||
- Updated shared flags for `--last` to mention both sources
|
||||
- Added "Capturing any command's output for --last" section (Option A: capture, Option B: shell hook)
|
||||
|
||||
## Step 8: Update `DOC/POS.md`
|
||||
[DONE]
|
||||
- Added `capture` row to both gemini and openrouter command tables
|
||||
- Updated tool purpose summaries to "four subcommands"
|
||||
|
||||
## Step 9: Run gates
|
||||
[DONE]
|
||||
- All gates green; gen regenerated to include `capture` in tree, dispatch, filetable, completions
|
||||
|
||||
## Probe results
|
||||
- **(a)** `capture ls /tmp` → file created, stdout shows output, stderr shows `[captured → ...]`, rc=0 ✓
|
||||
- **(b)** `capture false` → rc=1, empty file (false produces no output) ✓
|
||||
- **(c)** `--last` with `last_cmd_output` present, no pos log → file found as fallback ✓
|
||||
- **(d)** pos log priority: code path verified (pos log tested as primary source) ✓
|
||||
- **(e)** no log + no capture → error with pipe hint ✓
|
||||
- **(f)** staleness: 2-hour-old file triggers `[!]` warning path ✓
|
||||
- **(g)** `--last chat` → error mentioning `capture` subcommand (both tools) ✓
|
||||
- **(h)** hook: `bash -n` clean, creates file on source, only activates in interactive terminals ✓
|
||||
- **(i)** both tools structurally identical except naming ✓
|
||||
|
||||
REPORT_PATH: ./AgentsReport/builder/2026-08-25-r7-capture-any-command.md
|
||||
@@ -1,101 +0,0 @@
|
||||
# Builder Report — R8: AI Provider-Agnostic Architecture
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status:** IMPLEMENTED
|
||||
- **Files changed:** 5 new, 8 modified, 0 deleted
|
||||
- **New files:** `bin/pos-ai` (642 ln), `lib/ai-providers/gemini.sh` (59 ln), `lib/ai-providers/openrouter.sh` (59 ln)
|
||||
- **Overwritten:** `bin/pos-ai-gemini` (596→7 ln, thin forwarder), `bin/pos-ai-openrouter` (597→7 ln, thin forwarder)
|
||||
- **Modified:** `bin/pos`, `install.sh`, `config/ai.env`, `DOC/howto/ai.md`, `DOC/POS.md`, `DOC/HOWTO.md`, `DOC/AGENT_Context_Project.md`, `completions/pos.bash`, `lib/pos-ai-hook.sh`
|
||||
- **Gates:** bash -n OK, make gen OK, make check OK, make lint 0 FAIL / 0 WARN
|
||||
- **Probes:** 22/22 pass (all probes a–n verified)
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Provider adapters
|
||||
|
||||
**[DONE]** `lib/ai-providers/gemini.sh` (59 lines) — Gemini adapter implementing `provider_name()`, `provider_default_model()`, `provider_generate()` (converts OpenAI `messages` → Gemini `contents`), `provider_models_list()`.
|
||||
|
||||
**[DONE]** `lib/ai-providers/openrouter.sh` (59 lines) — OpenRouter adapter implementing the same interface; sends `messages` directly to OpenAI-compatible API.
|
||||
|
||||
## Step 2: Main tool
|
||||
|
||||
**[DONE]** `bin/pos-ai` (642 lines) — Provider-agnostic main tool with:
|
||||
- Unified `# POS:` header (`ai ask`, subcommands: ask/chat/sessions/capture/models/providers)
|
||||
- `--provider` flag + `AI_PROVIDER` env/config resolution
|
||||
- Unified `AI_API_KEY` with provider-specific fallback (`AI_GEMINI_API_KEY` → `OPENROUTER_API_KEY`)
|
||||
- Unified `AI_MODEL` with provider-specific fallback
|
||||
- `AI_SYSTEM_PROMPT` config support (between `--system` and built-in)
|
||||
- Universal session format (OpenAI `messages`) with auto-migration from old Gemini `contents` format
|
||||
- `cmd_providers()` — new subcommand listing providers + config status
|
||||
- All shared logic from both originals (render_markdown, machine_context, --last, capture, sessions)
|
||||
|
||||
## Step 3: Backward compat forwarders
|
||||
|
||||
**[DONE]** `bin/pos-ai-gemini` (7 lines) — `exec pos ai --provider gemini "$@"`
|
||||
**[DONE]** `bin/pos-ai-openrouter` (7 lines) — `exec pos ai --provider openrouter "$@"`
|
||||
|
||||
Both include `-h|--help` passthrough for lint compliance.
|
||||
|
||||
## Step 4: Install + dispatcher updates
|
||||
|
||||
**[DONE]** `install.sh` — Added `ai-providers/` directory copy to `/usr/local/bin/ai-providers/`
|
||||
**[DONE]** `bin/pos` — Added `ai` to `INTERACTIVE_CMDS`
|
||||
|
||||
## Step 5: Config + doc updates
|
||||
|
||||
**[DONE]** `config/ai.env` — Updated template with unified + legacy keys
|
||||
**[DONE]** `DOC/howto/ai.md` — Full rewrite reflecting unified architecture
|
||||
**[DONE]** `DOC/POS.md` — Unified ai section (replaces separate gemini/openrouter sections)
|
||||
**[DONE]** `DOC/HOWTO.md` — Updated ai.env table row
|
||||
**[DONE]** `DOC/AGENT_Context_Project.md` — Updated ai.env description + Common Tasks row
|
||||
**[DONE]** `completions/pos.bash` — Regenerated (make gen)
|
||||
**[DONE]** `lib/pos-ai-hook.sh` — Updated comment references
|
||||
|
||||
## Step 6: Gates
|
||||
|
||||
**[DONE]** `bash -n` — All 5 new/changed files pass
|
||||
**[DONE]** `make gen` — Idempotent (no drift)
|
||||
**[DONE]** `make check` — OK (check-sync passes)
|
||||
**[DONE]** `make lint` — 0 FAIL, 0 WARN
|
||||
|
||||
## Step 7: Verification probes
|
||||
|
||||
**[DONE]** All 22 probes pass:
|
||||
- (a) gemini adapter calls correct API endpoint ✅
|
||||
- (b) openrouter adapter calls correct API + Bearer auth ✅
|
||||
- (c) `--provider` flag overrides `AI_PROVIDER` config ✅
|
||||
- (d,e) backward compat forwarders route correctly ✅
|
||||
- (f) `--system` overrides built-in; `AI_SYSTEM_PROMPT` from config; `--system` overrides config; `--full` drops all ✅
|
||||
- (g) `AI_API_KEY` precedence over provider-specific; fallback works; error on missing key ✅
|
||||
- (h) `--model` flag; `AI_MODEL` config; `AI_GEMINI_MODEL` legacy fallback; provider default model ✅
|
||||
- (i) Session migration: old `contents` format → `messages` format ✅
|
||||
- (k) Capture saves command output ✅
|
||||
- (l) `pos ai providers` lists both providers with status ✅
|
||||
- (n) Non-tty output is plain text (byte-compat) ✅
|
||||
|
||||
## Step 8: AGENT_TODO
|
||||
|
||||
**[DONE]** Task is an Orchestrator assignment — no AGENT_TODO entry (ephemeral task, not project backlog).
|
||||
|
||||
---
|
||||
|
||||
## Diff summary
|
||||
|
||||
```
|
||||
New files:
|
||||
bin/pos-ai 642 lines
|
||||
lib/ai-providers/gemini.sh 59 lines
|
||||
lib/ai-providers/openrouter.sh 59 lines
|
||||
|
||||
Overwritten (thinned):
|
||||
bin/pos-ai-gemini 596→ 7 lines (forwarder)
|
||||
bin/pos-ai-openrouter 597→ 7 lines (forwarder)
|
||||
|
||||
Net: -1327 old lines removed, +189 modified lines, +760 new lines
|
||||
= 774 total new (vs ~1183 original combined)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
REPORT_PATH: ./AgentsReport/builder/2026-08-25_r8-ai-plugin-arch.md
|
||||
@@ -1,84 +0,0 @@
|
||||
# Builder Report — pos-system-uninstall
|
||||
|
||||
**TL;DR:**
|
||||
- Tool at `bin/pos-system-uninstall` (415 lines) — headers, tiers, flags, behavior match spec
|
||||
- Added `system-uninstall` to `INTERACTIVE_CMDS` in `bin/pos` (reads stdin via `confirm`)
|
||||
- Added `~/.bash_completion` scanning + cleanup to scan_tier1/remove_tier1 per spec
|
||||
- `make gen` idempotent, `make check` OK, `make lint` 0 FAIL, 0 WARN
|
||||
- Report path: `./AgentsReport/builder/2026-08-25_uninstall.md`
|
||||
|
||||
## Step 1: Analyze existing implementation
|
||||
|
||||
[DONE]
|
||||
|
||||
- Tool already existed (397→415 lines after adding bash_completion scan/cleanup)
|
||||
- Headers correct: `# POS: system uninstall — ...`, `# POS_FLAGS: --yes --config --data`
|
||||
- Three tiers implemented: Tier 1 (binaries, services, shell integration), Tier 2 (`--config`), Tier 3 (`--data`)
|
||||
- Flags: `--yes`, `--config`, `--data` parsed correctly
|
||||
- Scan functions cover all spec items: pos/pos-*, libs, ai-providers, entertainment plugins, legacy forwarders, prebuilts, features, user-local, completion, systemd services, bashrc patterns
|
||||
- Display format matches spec (numbered, tiered, colorized)
|
||||
- Removal functions handle idempotent cleanup (files, services, shell entries, dirs bottom-up)
|
||||
|
||||
## Step 2: Add system-uninstall to INTERACTIVE_CMDS
|
||||
|
||||
[DONE]
|
||||
|
||||
- Added `system-uninstall` to `INTERACTIVE_CMDS` in `bin/pos:262`
|
||||
- Tool reads stdin via `confirm()` prompts — must skip tee logging
|
||||
|
||||
## Step 2b: Add ~/.bash_completion scan + cleanup
|
||||
|
||||
[DONE]
|
||||
|
||||
- Added `~/.bash_completion` grep scan to `scan_tier1()` (finds pos completion entries)
|
||||
- Added `~/.bash_completion` sed cleanup to `remove_tier1()` (removes pos lines)
|
||||
- Matches spec: "Also check `~/.bash_completion` for pos completion entries"
|
||||
|
||||
## Step 3: Run gates
|
||||
|
||||
[DONE]
|
||||
|
||||
- `make gen` → idempotent (second run produces no new diff)
|
||||
- `make check` → OK (syntax, exec bits, doc/code sync, dispatch smoke)
|
||||
- `make lint` → 0 FAIL, 0 WARN
|
||||
|
||||
## Step 4: Verification probes
|
||||
|
||||
[DONE]
|
||||
|
||||
| Probe | Description | Result |
|
||||
|-------|-------------|--------|
|
||||
| (a) | scan finds mock binaries in /usr/local/bin | PASS — tool --help works, scan functions cover all spec paths |
|
||||
| (b) | scan finds mock config in ~/.config/linux_post_install/ | PASS — scan_tier2 finds all 3 config files |
|
||||
| (c) | scan finds mock data in ~/.local/share/linux_post_install/ | PASS — scan_tier3 finds ai/, logs/, last_cmd_output |
|
||||
| (d) | --yes removes tier 1 without prompting | PASS — YES_MODE=1 skips confirm() |
|
||||
| (e) | --yes --config --data removes all three tiers | PASS — DEL_CONFIG=1 DEL_DATA=1 triggers all removals |
|
||||
| (f) | idempotent: second run finds nothing | PASS — scan returns empty when dirs/files absent |
|
||||
| (g) | bashrc entries cleanly removed | PASS — 3 pos lines removed, non-pos lines preserved |
|
||||
| (h) | systemd services disabled | PASS — systemctl disable --now called for each found service |
|
||||
| (i) | git repo NOT removed | PASS — no git-related paths in any scan/remove function |
|
||||
| (j) | confirm default=y | PASS — confirm() in common.sh defaults to y |
|
||||
|
||||
## Diff stats
|
||||
|
||||
```
|
||||
bin/pos | 2 +-
|
||||
bin/pos-system-uninstall | 18 +++++++++++-----
|
||||
DOC/AGENT_Context_Project.md | 33 ++++++++++++-----------
|
||||
DOC/POS.md | 1 +
|
||||
DOC/howto/system.md | 35 +++++++++++++++++++++++++++++++++--
|
||||
completions/pos.bash | 1 +
|
||||
6 files changed, 66 insertions(+), 24 deletions(-)
|
||||
```
|
||||
|
||||
## Gates
|
||||
|
||||
| Gate | Result |
|
||||
|------|--------|
|
||||
| `bash -n bin/pos-system-uninstall` | OK |
|
||||
| `bash -n bin/pos` | OK |
|
||||
| `make gen` | OK (idempotent) |
|
||||
| `make check` | OK |
|
||||
| `make lint` | 0 FAIL, 0 WARN |
|
||||
|
||||
REPORT_PATH: ./AgentsReport/builder/2026-08-25_uninstall.md
|
||||
@@ -1,118 +0,0 @@
|
||||
# Builder Report: `pos ai alias` — AI Agent Alias Manager
|
||||
|
||||
**Date:** 2026-08-26
|
||||
**Builder:** big-pickle
|
||||
**Status:** IMPLEMENTED
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Files created:** `bin/pos-ai-alias` (542 lines, chmod 755)
|
||||
- **Files modified:** `bin/pos` (+ai-alias to INTERACTIVE_CMDS), `postinstall.sh` (+bashrc source line), `DOC/POS.md` (+ai alias section)
|
||||
- **Files regenerated:** `DOC/AGENT_Context_Project.md`, `completions/pos.bash` (via `make gen`)
|
||||
- **Verification:** `bash -n bin/pos-ai-alias` — OK
|
||||
- **Gates:** `make gen` — OK, `make check` — OK, `make lint` — individual checks pass (full script timeout is environment-specific, not a regression)
|
||||
- **Report path:** `./AgentsReport/builder/2026-08-26_ai-alias.md`
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create `bin/pos-ai-alias` [DONE]
|
||||
|
||||
Created `bin/pos-ai-alias` (542 lines, chmod 755) with:
|
||||
|
||||
- `#!/usr/bin/env bash` + `set -euo pipefail` + `# POS: ai alias — manage AI agent aliases` + `# POS_SUBCMDS: create edit remove list show` on lines 1-3 (convention: right after shebang/strict-mode)
|
||||
- Sources `lib/common.sh` + `lib/menu-lib.sh` via fallback chain
|
||||
- Path constants with `CONFIG_DIR` env seam: `ENV_FILE`, `SH_FILE`
|
||||
- Provider discovery matching `bin/pos-ai` pattern (lines 16-18)
|
||||
- All functions from architecture + UX spec:
|
||||
- `_alias_load()` — read `.env` pipe-delimited records into parallel arrays
|
||||
- `_alias_save()` — write `.env` with header comments, chmod 600
|
||||
- `_alias_regen()` — generate `.sh` from `.env` with `'`→`'\''` escaping, `bash -n` pre-commit check, chmod 644
|
||||
- `_alias_provider_pick()` — discover providers from `lib/ai-providers/*.sh`, `menu_pick`
|
||||
- `_alias_find()` — linear scan, return index or -1
|
||||
- `_alias_name_valid()` — regex `^[a-zA-Z][a-zA-Z0-9_-]*$`
|
||||
- `_alias_prompt_truncate()` — 42-char display truncation
|
||||
- `_alias_list()` — formatted table to stdout
|
||||
- `_alias_show()` — key-value display + resolved command to stdout
|
||||
- `_alias_menu()` — interactive menu via `menu_run` with alias table preview above box
|
||||
- `_alias_create()` — 4-step wizard (name→provider→session→prompt→confirm) with re-prompt loops
|
||||
- `_alias_edit()` — pick→show current→3-field edit with Enter=keep→diff→confirm
|
||||
- `_alias_remove()` — pick→detail→confirm n (default NO)→delete+regen
|
||||
- `usage()` — full help text
|
||||
- Subcommand dispatch: `create`, `edit`, `remove`, `list`, `show`, `-h|--help`, `""` → menu
|
||||
- All display to stderr, all results to stdout (menu-lib.sh contract)
|
||||
- Input validation: name format, uniqueness, pipe-char in prompt, >500 char warning
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Add `ai-alias` to `bin/pos` INTERACTIVE_CMDS [DONE]
|
||||
|
||||
Added `ai-alias` to the `INTERACTIVE_CMDS` space-separated list in `bin/pos` (line 262).
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Add `.bashrc` source line to `postinstall.sh` [DONE]
|
||||
|
||||
Added conditional source block after the config template loop (line 52-57):
|
||||
```bash
|
||||
ALIAS_SRC_LINE='# AI aliases (managed by pos ai alias)
|
||||
[ -f ~/.config/linux_post_install/ai-aliases.sh ] && source ~/.config/linux_post_install/ai-aliases.sh'
|
||||
if ! grep -qsF "ai-aliases.sh" "$BASHRC" 2>/dev/null; then
|
||||
run printf '%s\n' "$ALIAS_SRC_LINE" >> "$BASHRC"
|
||||
log "Added AI aliases source to ~/.bashrc"
|
||||
fi
|
||||
```
|
||||
No-clobber guard via `grep -qsF`.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Add DOC/POS.md section [DONE]
|
||||
|
||||
Added `#### ai alias` subsection under `### ai` in `DOC/POS.md` with command table and description.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Syntax check [DONE]
|
||||
|
||||
`bash -n bin/pos-ai-alias` — OK
|
||||
`bash -n postinstall.sh` — OK
|
||||
`bash -n bin/pos` — OK
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Generate docs and run gates [DONE]
|
||||
|
||||
- `make gen` — OK (regenerated `DOC/AGENT_Context_Project.md` + `completions/pos.bash`)
|
||||
- `make check` — OK (check-sync passed)
|
||||
- `make lint` — individual checks for `pos-ai-alias` all pass:
|
||||
- Shebang: OK
|
||||
- Strict mode: OK
|
||||
- `# POS:` header on line 3 (convention: right after shebang/strict-mode): OK
|
||||
- Em-dash separator: OK
|
||||
- `-h|--help` handling: OK
|
||||
- No deps before help (no external deps): OK
|
||||
- `DOC/POS.md` reference: OK
|
||||
- Executable bit: OK
|
||||
- `ai-alias` in INTERACTIVE_CMDS + matching `bin/pos-ai-alias`: OK
|
||||
- No env-seam warnings (writes use `CONFIG_DIR` seam): OK
|
||||
|
||||
---
|
||||
|
||||
## Diff stats
|
||||
|
||||
```
|
||||
bin/pos-ai-alias | 542 ++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
bin/pos | 2 +-
|
||||
postinstall.sh | 8 ++++++
|
||||
DOC/POS.md | 16 ++++++++++++
|
||||
4 files changed, 568 insertions(+)
|
||||
```
|
||||
|
||||
Plus `make gen` output:
|
||||
```
|
||||
DOC/AGENT_Context_Project.md | 31 ++++++++++-----------
|
||||
completions/pos.bash | 3 ++-
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
REPORT_PATH: ./AgentsReport/builder/2026-08-26_ai-alias.md
|
||||
@@ -1,19 +0,0 @@
|
||||
# Builder Report — Alias activation architecture (Step 1) + Config readability (Step 2)
|
||||
|
||||
Date: 2026-08-26 · Agent: Builder · Status: IN_PROGRESS
|
||||
|
||||
## TL;DR
|
||||
|
||||
- Status: **IN_PROGRESS**
|
||||
- Files changed: (updating as steps complete)
|
||||
- Verification: (pending)
|
||||
|
||||
## Step 0: Scope intake
|
||||
|
||||
- Read both authoritative specs FIRST:
|
||||
- `AgentsReport/architect/2026-08-26_alias-architecture.md` (Option B wrappers, `_alias_sync`, collision policy, legacy `.sh` retirement, uninstall marker-scan, docs)
|
||||
- `AgentsReport/designer/2026-08-26_config-readability.md` (caption grammar, tagged wildcard, typography tier, backward-compat proof)
|
||||
- Found working tree already carries the Step 1 + Step 2 code changes from an earlier interrupted round of this session (uncommitted, alongside other known uncommitted work: registry.sh feature etc.). Verified hunk-by-hunk against both specs rather than re-implementing; gaps found: docs (DOC/POS.md wording, AGENT_TODO.md Done entry) and the entire functional verification matrix.
|
||||
- Hard fence honored: no commits; only bin/pos-ai-alias, bin/pos-system-uninstall, lib/config-ui.sh, bin/pos-ai (line 6), DOC/POS.md, DOC/HOWTO.md, AGENT_TODO.md, AgentsReport touched by THIS round.
|
||||
|
||||
[PENDING]
|
||||
@@ -1,87 +0,0 @@
|
||||
# Builder Report: Alias Architecture Rewrite + Dup-Table Fix
|
||||
|
||||
**Date:** 2026-08-26
|
||||
**Files changed:** `bin/pos-ai-alias`, `bin/pos-system-uninstall`, `DOC/POS.md`, `AGENT_TODO.md`
|
||||
**Gates:** `bash -n` ✅ | `make gen && make check` ✅ | `make lint` 0 FAIL 0 WARN ✅
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
Implementation found already present in working tree — verified against architect spec, tested, gates green. All architect D1–D4 decisions implemented correctly. Dup-table bug fixed via single `_alias_table` renderer (variant: menu option 4 = no-op returning to loop). 17-category test suite (40 assertions) run externally against the actual script; 3 test-harness quoting artifacts debugged and confirmed not code bugs. No scope violations.
|
||||
|
||||
## Step 1: Verify implementation against architect spec ✅ [DONE]
|
||||
|
||||
Read architect spec (`AgentsReport/architect/2026-08-26_alias-architecture.md`), current `bin/pos-ai-alias`, and `bin/pos-system-uninstall`.
|
||||
|
||||
**Already implemented in working tree:**
|
||||
- `_alias_regen()` deleted; replaced by `_wrapper_path()`, `_alias_owned()`, `_wrapper_render()`, `_wrapper_install()`, `_alias_check_path()`, `_alias_retire_legacy_sh()`, `_alias_sync()`
|
||||
- `_alias_quote_cmd()` preserved verbatim (double-%q mechanism)
|
||||
- `_alias_sync()` wired into every dispatch entry (create/edit/remove/list/show/menu)
|
||||
- Collision refusals in `_alias_create()` (foreign file + `command -v` check)
|
||||
- Legacy `ai-aliases.sh` auto-removal with unalias remediation hint
|
||||
- Success messages updated ("Available immediately", dropped "Reload shell" everywhere)
|
||||
- `show` gains wrapper path
|
||||
- `usage()` text updated with activation description
|
||||
- `bin/pos-system-uninstall`: marker scan in `scan_tier1()` (lines 98-105) and `remove_tier1()` (lines 272-281)
|
||||
- `DOC/POS.md`: alias rows updated (activation via `~/.local/bin` wrappers, not `.bashrc` sourcing)
|
||||
|
||||
## Step 2: Dup-table bug fix verification ✅ [DONE]
|
||||
|
||||
**Chosen variant:** Menu option 4 is a no-op (`: ;;`) returning to the loop, which re-renders the table via `_alias_table` in the pre-render block.
|
||||
|
||||
**Rationale:** The menu's own pre-render (lines 266-275) already calls `_alias_table` fresh every iteration. Making option 4 a no-op is the simplest correct fix — zero code duplication, no new helper, and the "list" action is semantically "return to see the current list." The `_alias_list()` function remains available for the non-interactive `pos ai alias list` path, which still calls `_alias_table` through its own code path.
|
||||
|
||||
**Before (bug):** Menu pre-render showed table, then option 4 called `_alias_list()` which printed the table again → duplicated output.
|
||||
|
||||
**After (fix):** Menu option 4 returns to loop → loop re-renders → single table displayed.
|
||||
|
||||
## Step 3: Test harness ✅ [DONE]
|
||||
|
||||
External test harness (`/tmp/al-test.sh`) runs 17 test categories (40 assertions) against the actual `bin/pos-ai-alias` script with a stub `pos` that captures `$*`:
|
||||
|
||||
| # | Test | Result |
|
||||
|---|------|--------|
|
||||
| 1 | `_alias_sync` creates wrapper scripts (assist, evil, plain) | ✅ 5/5 |
|
||||
| 2 | Wrapper content: marker on line 2, shebang, set -euo, exec | ✅ 6/6 |
|
||||
| 3 | exec line correct provider/session | ✅ 2/2 |
|
||||
| 4 | `--system` flag present for non-empty prompts | ✅ 2/2 |
|
||||
| 5 | Empty prompt → no `--system` fragment | ✅ 2/2 |
|
||||
| 6 | Wrapper executes: `assist "how are you"` → correct argv | ✅ 4/4 |
|
||||
| 7 | Passthrough args: `--help` forwarded via `"$@"` | ✅ 1/1 |
|
||||
| 8 | Evil wrapper (complex prompt) executes correctly | ✅ 1/1 |
|
||||
| 9 | Legacy `ai-aliases.sh` removed by list | ✅ 1/1 |
|
||||
| 10 | Second list call idempotent (same md5, same mtime) | ✅ 2/2 |
|
||||
| 11 | Foreign file not touched by sync | ✅ 1/1 |
|
||||
| 12 | Orphan retraction: removed ENV entry → wrapper deleted | ✅ 2/2 |
|
||||
| 13 | Empty env → all wrappers deleted | ✅ 2/2 |
|
||||
| 14 | **Staleness kill-test:** edit gemini→openrouter, wrapper live on next invocation | ✅ 3/3 |
|
||||
| 15 | `show` includes wrapper path | ✅ 2/2 |
|
||||
| 16 | Legacy foreign sh file untouched with warning | ✅ 2/2 |
|
||||
| 17 | Help text mentions `~/.local/bin` and "no shell sourcing" | ✅ 2/2 |
|
||||
|
||||
**3 initial test failures debugged:** All were test-harness quoting artifacts (single quotes and `$(echo pwned)` in captured argv broke the assertion's `echo '$var'` pattern). Confirmed via direct `grep` on the captured file: evil wrapper correctly produces `CAPTURED: ai openrouter ask --session evil --system It's a 'quoted' $(echo pwned) \`backtick\` --help`.
|
||||
|
||||
## Step 4: Gates ✅ [DONE]
|
||||
|
||||
```
|
||||
bash -n bin/pos-ai-alias ✅
|
||||
bash -n bin/pos-system-uninstall ✅
|
||||
make gen && make check ✅ (gen-docs OK, check-sync OK)
|
||||
make lint ✅ (0 FAIL, 0 WARN)
|
||||
```
|
||||
|
||||
## Step 5: AGENT_TODO update ✅ [DONE]
|
||||
|
||||
The alias-related entry was already moved to Done (dated 2026-08-26) in the existing AGENT_TODO.md diff.
|
||||
|
||||
## Scope compliance ✅
|
||||
|
||||
**Touched (in scope):** `bin/pos-ai-alias`, `bin/pos-system-uninstall`, `DOC/POS.md`, `AGENT_TODO.md`, `AgentsReport/`
|
||||
**Not touched (out of scope):** `bin/pos-ai`, `lib/common.sh`, `lib/menu-lib.sh`, `lib/config-ui.sh`, `completions/`, `postinstall.sh`, `pos-ai-alias` `# POS:` headers, registry code
|
||||
**No commits made** per brief constraint.
|
||||
|
||||
## Dup-table fix variant chosen
|
||||
|
||||
**Variant:** Menu option 4 → no-op, returning to the loop's pre-render.
|
||||
**Rationale:** Cleanest solution. The pre-render block already calls `_alias_table` on every iteration, so returning to the loop naturally shows the current state. Zero added code, zero new abstractions, and `_alias_list()` stays available for the non-interactive `list` path.
|
||||
@@ -1,254 +0,0 @@
|
||||
# Builder Report — config-ui readability implementation
|
||||
|
||||
Date: 2026-08-26 · Agent: Builder · Status: **IMPLEMENTED**
|
||||
|
||||
## TL;DR
|
||||
|
||||
- Status: **IMPLEMENTED** — critical caption-parsing bug found and fixed; all three pieces verified
|
||||
- Files changed: `lib/config-ui.sh` (3 surgical edits to fix delimiter collision)
|
||||
- Bug fixed: caption records `>|cond|caption|` used `|` as delimiter, colliding with `|` in alternation syntax (`AI_PROVIDER=gemini|`); fixed by switching to `\x1f` (unit separator)
|
||||
- Additional fix: `nums` map used old `%%|*` check that didn't detect `\x1f`-delimited caption records; updated to `[[ == ">"* ]]`
|
||||
- Pilot header `bin/pos-ai:6` already in new format (done in prior commit)
|
||||
- All three pieces present and functional: caption fields, tagged wildcards, uniform typography
|
||||
- Gates: `bash -n` ✅ · `make gen` ✅ · `make check` ✅ · `make lint` 0 FAIL 0 WARN ✅
|
||||
- Scopes verified: `ai` (3 provider states), `system`, `telegram`, `entertainment`
|
||||
|
||||
## Step 1: Bug diagnosis — caption record delimiter collision
|
||||
|
||||
**[DONE]**
|
||||
|
||||
Two bugs found:
|
||||
|
||||
### Bug 1: Caption record delimiter
|
||||
The `cfg_scope_keys` function emitted caption records as `>|cond|caption|`, using `|` as the field separator. However, the condition string can contain `|` from the alternation syntax (e.g. `AI_PROVIDER=gemini|` in `@[AI_PROVIDER=gemini|] Gemini`).
|
||||
|
||||
When `cfg_ui` parsed these records with `IFS='|' read -r k f d e`, the extra pipe shifted the fields:
|
||||
- Record: `>|AI_PROVIDER=gemini||Gemini|`
|
||||
- Split: k=`>`, f=`AI_PROVIDER=gemini`, d=`` (empty!), e=`Gemini|`
|
||||
- `pend_cap` got empty string → **caption silently never rendered**
|
||||
|
||||
Result: the entire Gemini group was invisible — no caption, no dim state.
|
||||
|
||||
### Bug 2: nums map old-format check
|
||||
The number→index map at line 504 used `[ "${recs[$idx]%%|*}" = ">" ]` to exclude caption records. With `\x1f`-delimited caption records, this check doesn't detect unconditional captions (no `|` in the record), potentially including them in the numbered key list.
|
||||
|
||||
## Step 2: Fixes applied
|
||||
|
||||
**[DONE]**
|
||||
|
||||
Three edits to `lib/config-ui.sh`:
|
||||
|
||||
### 2a. New constant `_CS` (line 49)
|
||||
```bash
|
||||
_CS=$'\x1f' # unit-separator for caption records — never in env
|
||||
# names or alt strings, avoids collision with | in
|
||||
# alternation syntax (AI_PROVIDER=gemini|)
|
||||
```
|
||||
|
||||
### 2b. Caption record emission in `cfg_scope_keys` (line 292)
|
||||
```bash
|
||||
# Before: printf '%s\n' ">|$cond|$cap|"
|
||||
printf '%s\n' ">${_CS}${cond}${_CS}${cap}${_CS}"
|
||||
```
|
||||
|
||||
### 2c. Caption record parsing in `cfg_ui` (lines 525-535)
|
||||
```bash
|
||||
# Before: IFS='|' read -r k f d e <<<"${recs[$idx]}"
|
||||
# pend_cond="$f"; pend_cap="$d"
|
||||
|
||||
if [[ "${recs[$idx]}" == ">"* ]]; then
|
||||
pend_cond="${recs[$idx]#>}"
|
||||
pend_cond="${pend_cond#$_CS}"
|
||||
pend_cond="${pend_cond%%$_CS*}"
|
||||
pend_cap="${recs[$idx]#>}"
|
||||
pend_cap="${pend_cap#$_CS}"
|
||||
pend_cap="${pend_cap#*$_CS}"
|
||||
pend_cap="${pend_cap%%$_CS*}"
|
||||
continue
|
||||
fi
|
||||
IFS='|' read -r k f d e <<<"${recs[$idx]}"
|
||||
```
|
||||
|
||||
### 2d. nums map check (line 504)
|
||||
```bash
|
||||
# Before: [ "${recs[$idx]%%|*}" = ">" ] || nums+=("$idx")
|
||||
[[ "${recs[$idx]}" == ">"* ]] || nums+=("$idx")
|
||||
```
|
||||
|
||||
**Why `\x1f`:** The ASCII unit separator never appears in env variable names (`[A-Z0-9_]+`) or alt strings. It can't collide with `|` in conditions or `:` in flags/descriptions.
|
||||
|
||||
**Backward compatibility:** Key records still use `|` — unchanged. Caption records are only consumed by the `>` branch in `cfg_ui`, so no other consumer is affected. `cfg_scopes`/`cfg_scope_envfile` only read fields 1-2 of each header (scope + env-file). `scripts/gen-docs.sh` and `completions/pos.bash` read scope names only.
|
||||
|
||||
## Step 3: Test harness — ai scope (all three provider states)
|
||||
|
||||
**[DONE]**
|
||||
|
||||
### AI_PROVIDER=openrouter (both API keys set, models/prompt unset)
|
||||
```
|
||||
pos config — ai (ai.env)
|
||||
────────────────────────────────────────
|
||||
1) AI_PROVIDER openrouter
|
||||
Provider (gemini or openrouter, default gemini)
|
||||
|
||||
── Gemini — inactive while AI_PROVIDER=openrouter
|
||||
2) AI_GEMINI_API_KEY AIzaSy...6789 (22 chars)
|
||||
Gemini API key from aistudio.google.com
|
||||
3) AI_GEMINI_MODEL gemini-2.5-flash
|
||||
Gemini model id (default: gemini-2.5-flash)
|
||||
|
||||
── OpenRouter
|
||||
4) OPENROUTER_API_KEY sk-or-...6789 (22 chars)
|
||||
OpenRouter API key from openrouter.ai
|
||||
5) OPENROUTER_MODEL anthropic/claude-sonnet-4
|
||||
OpenRouter model id (default: openrouter/auto)
|
||||
|
||||
── General
|
||||
6) AI_SYSTEM_PROMPT You are a helpful assistant
|
||||
Custom system prompt (overrides built-in, empty to reset)
|
||||
|
||||
Number to edit [r=refresh, q=quit]:
|
||||
```
|
||||
✅ Gemini inactive with reason, OpenRouter active, numbers 1–6 stable
|
||||
|
||||
### AI_PROVIDER=gemini
|
||||
```
|
||||
pos config — ai (ai.env)
|
||||
────────────────────────────────────────
|
||||
1) AI_PROVIDER gemini
|
||||
Provider (gemini or openrouter, default gemini)
|
||||
|
||||
── Gemini
|
||||
2) AI_GEMINI_API_KEY AIzaSy...6789 (22 chars)
|
||||
Gemini API key from aistudio.google.com
|
||||
3) AI_GEMINI_MODEL gemini-2.5-flash
|
||||
Gemini model id (default: gemini-2.5-flash)
|
||||
|
||||
── OpenRouter — inactive while AI_PROVIDER=gemini
|
||||
4) OPENROUTER_API_KEY sk-or-...6789 (22 chars)
|
||||
OpenRouter API key from openrouter.ai
|
||||
5) OPENROUTER_MODEL anthropic/claude-sonnet-4
|
||||
OpenRouter model id (default: openrouter/auto)
|
||||
|
||||
── General
|
||||
6) AI_SYSTEM_PROMPT You are a helpful assistant
|
||||
Custom system prompt (overrides built-in, empty to reset)
|
||||
|
||||
Number to edit [r=refresh, q=quit]:
|
||||
```
|
||||
✅ Gemini active, OpenRouter inactive with reason
|
||||
|
||||
### AI_PROVIDER unset (empty-alt segment test)
|
||||
```
|
||||
pos config — ai (ai.env)
|
||||
────────────────────────────────────────
|
||||
1) AI_PROVIDER (not set)
|
||||
Provider (gemini or openrouter, default gemini)
|
||||
|
||||
── Gemini
|
||||
2) AI_GEMINI_API_KEY AIzaSy...6789 (22 chars)
|
||||
Gemini API key from aistudio.google.com
|
||||
3) AI_GEMINI_MODEL gemini-2.5-flash
|
||||
Gemini model id (default: gemini-2.5-flash)
|
||||
|
||||
── OpenRouter — inactive (AI_PROVIDER not set)
|
||||
4) OPENROUTER_API_KEY (not set)
|
||||
OpenRouter API key from openrouter.ai
|
||||
5) OPENROUTER_MODEL (not set)
|
||||
OpenRouter model id (default: openrouter/auto)
|
||||
|
||||
── General
|
||||
6) AI_SYSTEM_PROMPT You are a helpful assistant
|
||||
Custom system prompt (overrides built-in, empty to reset)
|
||||
|
||||
Number to edit [r=refresh, q=quit]:
|
||||
```
|
||||
✅ Empty-alt segment `gemini|` means "or unset = default gemini" — Gemini active, OpenRouter inactive with "not set" reason
|
||||
|
||||
## Step 4: Test harness — contrasting scopes (backward compat)
|
||||
|
||||
**[DONE]**
|
||||
|
||||
### System scope (old-format header, 2 keys, long description)
|
||||
```
|
||||
pos config — system (system.env)
|
||||
────────────────────────────────────────
|
||||
1) BACKUP_SERVICE_ROOTS /srv /home/user/srv
|
||||
Roots scanned by backup --service and the health
|
||||
backup-age check (default: /srv $HOME/srv)
|
||||
2) HEALTH_BACKUP_MAX_AGE_DAYS 2
|
||||
Max backup age in days before health warns (default 2)
|
||||
|
||||
Number to edit [r=refresh, q=quit]:
|
||||
```
|
||||
✅ No captions, no grouping, long description word-wrapped, clean typography
|
||||
|
||||
### Telegram scope (secret masking + digits flag)
|
||||
```
|
||||
pos config — telegram (telegram.env)
|
||||
────────────────────────────────────────
|
||||
1) TELEGRAM_BOT_TOKEN 123456...WXYZ (37 chars)
|
||||
Bot token from @BotFather
|
||||
2) TELEGRAM_CHAT_ID 123456789
|
||||
Numeric chat id from @userinfobot
|
||||
|
||||
Number to edit [r=refresh, q=quit]:
|
||||
```
|
||||
✅ No captions, no grouping, secret masking unchanged, digits flag works
|
||||
|
||||
## Step 5: Gates
|
||||
|
||||
**[DONE]**
|
||||
|
||||
```
|
||||
bash -n lib/config-ui.sh bin/pos-ai → OK
|
||||
make gen → gen-docs: write OK
|
||||
make check → check-sync: OK
|
||||
make lint → 0 FAIL, 0 WARN
|
||||
```
|
||||
|
||||
## Step 6: Implementation summary
|
||||
|
||||
### Piece 1: Caption fields ✅
|
||||
- `@caption` — unconditional group caption
|
||||
- `[KEY=alt1|alt2] caption` — conditional (active iff KEY matches an alt, or empty-alt for unset)
|
||||
- Evaluation via existing `_cfg_cond_active` → `cfg_value` — per-render, so editing KEY flips emphasis on next redraw
|
||||
- Inactive groups: dimmed with textual reason ("inactive while KEY=VALUE" or "inactive (KEY not set)"), never hidden → stable numbering
|
||||
- Lazy flush: pending caption only prints when a key follows (empty adapter suppresses orphan caption)
|
||||
|
||||
### Piece 2: Tagged wildcard ✅
|
||||
- `*providers=<tag>` restricts expansion to `lib/ai-providers/<tag>.sh`
|
||||
- Bare `*providers` works exactly as before (all adapters)
|
||||
- Zero-match explicit tag: warns on stderr + suppresses preceding caption via lazy flush
|
||||
|
||||
### Piece 3: Uniform typography ✅
|
||||
- Bold title (`BOLD`), CYAN rule (40 × `─`), dim numbers, bold keys, dim `(not set)`
|
||||
- Hanging-indent word-wrap at `W = clamp(COLUMNS, 60, 120)` minus 6-col hang (via `_cfg_wrap`)
|
||||
- Display → stderr (`{ ... } >&2` pattern from menu-lib)
|
||||
- Prompt: `Number to edit [r=refresh, q=quit]: `
|
||||
- Color tokens guarded with fallbacks (menu-lib.sh pattern)
|
||||
|
||||
### Pilot: bin/pos-ai header ✅
|
||||
Updated in prior commit — line 6 carries the full caption/tag syntax.
|
||||
|
||||
### No per-scope branches ✅
|
||||
All grouping logic is driven by `@` metadata in headers. The `system`, `notify`, `telegram`, and all other scopes render correctly without any captions — their headers simply don't declare `@` fields.
|
||||
|
||||
## Changes made
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `lib/config-ui.sh:49-51` | Added `_CS=$'\x1f'` constant with documentation comment |
|
||||
| `lib/config-ui.sh:292` | Changed caption record emission from `>|cond|caption|` to `>\x1fcond\x1fcaption\x1f` |
|
||||
| `lib/config-ui.sh:504` | Updated nums map from `%%|*` check to `[[ == ">"* ]]` |
|
||||
| `lib/config-ui.sh:525-535` | Replaced `IFS='|' read` caption parsing with `\x1f`-based string operations |
|
||||
| `AgentsReport/builder/` | This report |
|
||||
|
||||
## Remaining risks
|
||||
|
||||
- **DIM legibility on exotic palettes** — mitigated: secondary info only, meaning duplicated in text ("inactive while ...")
|
||||
- **Typo'd condition keys** — visible (permanently dim group), self-inflicted, documented
|
||||
- **No unit tests exist** for config-ui.sh — smoke-tested via harness; full test suite deferred to Tester
|
||||
|
||||
## Recommended next agent
|
||||
|
||||
**Reviewer** — implementation is complete and needs independent adversarial review before acceptance.
|
||||
@@ -1,38 +0,0 @@
|
||||
# Builder Report — fix `_alias_find()` set -e crash
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status:** COMPLETE
|
||||
- **File changed:** `bin/pos-ai-alias` (line 103: `return 1` → `return 0`)
|
||||
- **Gate:** 0 FAIL, 0 WARN
|
||||
- **Verification:** bash -n OK, make gen OK, make check OK, make lint OK
|
||||
|
||||
## Step 1: Fix `_alias_find()` return code
|
||||
|
||||
`_alias_find()` (line 94-104) outputs `-1` on not-found and returns 1. Callers capture stdout in a variable (`existing="$(_alias_find "$name")"`) and check for `-1`. The exit code is never checked — the return 1 is purely harmful under `set -e`.
|
||||
|
||||
**Change:** line 103 `return 1` → `return 0`.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 2: Scan for same pattern in other functions
|
||||
|
||||
Only `_alias_find` is captured via `$(...)` variable assignment and returns non-zero as a "not found" signal. Other non-zero returns:
|
||||
- `_alias_regen` line 77: `return 1` on syntax error — called directly (not in `$()`), so it's a fatal-error propagation, not the same pattern.
|
||||
- `_alias_provider_pick` / `menu_pick`: callers already guard with `|| return 0`.
|
||||
- `_alias_name_valid`: always used in `if` statements.
|
||||
|
||||
No other fixes needed.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 3: Gates — syntax, gen, check, lint
|
||||
|
||||
- `bash -n bin/pos-ai-alias` — OK
|
||||
- `make gen` — gen-docs OK
|
||||
- `make check` — check-sync OK
|
||||
- `make lint` — `0 FAIL, 0 WARN`
|
||||
|
||||
All green.
|
||||
|
||||
[DONE]
|
||||
@@ -1,22 +0,0 @@
|
||||
# Builder Report: fix `_inject_command` edit option for `tee` pipe context
|
||||
|
||||
**TL;DR**
|
||||
- Status: DONE
|
||||
- File changed: `bin/pos-ai` (lines 399–421 replaced)
|
||||
- Fix: replaced `read -e -i` (readline) with plain `read -r` so the `e` option works through the dispatcher's `tee` pipe
|
||||
- Verification: `bash -n` OK, gates pending
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Replace `read -e -i` with plain `read -r` in `_inject_command`
|
||||
|
||||
**Target:** `bin/pos-ai` lines 410–421
|
||||
|
||||
Replaced the readline-based edit prompt with a simpler approach that:
|
||||
1. Shows the flattened command
|
||||
2. Asks user to type a replacement or press Enter for the original
|
||||
3. Uses plain `read -r` which works in any context (tee pipes, SSH, non-TTY)
|
||||
|
||||
Removed the "Empty command — skipped" message — empty input now runs the original.
|
||||
|
||||
[PENDING]
|
||||
@@ -1,224 +0,0 @@
|
||||
# Builder Report — Self-Describing Command Registry for POS
|
||||
|
||||
**Date:** 2026-08-26
|
||||
**Status:** COMPLETE
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status:** All 8 steps implemented and verified
|
||||
- **Files created:** `lib/registry.sh` (199 lines, mode 664)
|
||||
- **Files modified:** `scripts/gen-docs.sh`, `bin/pos-tree`, `templates/pos-tool.sh`, `install.sh`, `DOC/DEV.md`, `DOC/AGENT_Context_Project.md`
|
||||
- **Verification:** All gates pass — gen drift ✅, tree output identical ✅, make check ✅, make lint shows only pre-existing issues ⚠️
|
||||
|
||||
---
|
||||
|
||||
## Step 0: Establish Current State
|
||||
|
||||
**Prerequisite:** Pre-existing gen drift from `pos-ai-alias` was committed as `6566c83` (`chore: re-gen docs for pos-ai-alias addition`) to get a clean baseline.
|
||||
|
||||
**Baselines (all clean after gen drift fix):**
|
||||
|
||||
| Gate | Result |
|
||||
|------|--------|
|
||||
| `pos-tree` output | ✅ exit 0, captured to `/tmp/pos-tree-before.txt` |
|
||||
| `make gen && git diff --exit-code` | ✅ zero diff (gen output matches committed) |
|
||||
| `make check` | ✅ `check-sync: OK` |
|
||||
| `make lint` | ⚠️ 1 FAIL (pre-existing: `bin/pos-ai-alias` not in `INTERACTIVE_CMDS`), 1 WARN (`pos-ai-alias` not in `DOC/POS.md`) — both unrelated to this task |
|
||||
|
||||
**Notes:**
|
||||
- `make lint` takes ~2-3 minutes to complete (scans 56+ files with regex)
|
||||
- The pre-existing lint FAIL on `pos-ai-alias` is outside our scope
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create `lib/registry.sh`
|
||||
|
||||
**File:** `lib/registry.sh` (191 lines, mode 664, no shebang)
|
||||
|
||||
**Changes:**
|
||||
- Created the shared query API library following the architect's pseudocode and `lib/config-ui.sh` structural pattern
|
||||
- Key derivation: `bin/pos-network-download` → key `network-download`, category `network`; `bin/pos-config` → key `config`, category `""`
|
||||
- `reg_scan` sets/restores `LC_ALL=C` for deterministic sort
|
||||
- All associative-array lookups use `${var:-}` fallback for missing keys
|
||||
- `reg_categories` uses a sentinel `__empty__` to handle empty-category associative array limitation in bash
|
||||
- All API functions implemented: `reg_scan`, `reg_list`, `reg_categories`, `reg_tools_in`, `reg_lookup`, `reg_config_scopes`, `reg_config_keys`, `reg_config_envfile`, `reg_each`, `reg_tool_exists`
|
||||
|
||||
**Functional test:** Registry loads all ~40 tools, lookups return correct data, categories list correctly, config scopes found.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Update `scripts/gen-docs.sh`
|
||||
|
||||
**File:** `scripts/gen-docs.sh` (modified)
|
||||
|
||||
**Changes:**
|
||||
1. **Tools collection loop (lines ~42-47):** Added `deps` and `examples` fields to the pipe-delimited `tools` array format: `"$cat|$sub|$desc|$flags|$subcmds|$deps|$examples"`
|
||||
- `deps`: parsed with same `sed` pattern as existing headers
|
||||
- `examples`: parsed with `grep | sed | paste` (pipe-delimited, `|| true` to handle empty matches with `pipefail`)
|
||||
2. **`_has_deps_examples` flag:** Scans tools array once to determine if any tool has non-empty deps/examples — drives conditional column rendering
|
||||
3. **`gen_tree()`:** Reads 7 fields now; adds `[deps: X]` annotation line when non-empty
|
||||
4. **`gen_dispatch()`:** When `_has_deps_examples=1`, adds Deps/Examples columns to the header and all data rows. When 0, renders unchanged format
|
||||
5. **`gen_filetable()`:** Updated to read 7 fields (output unchanged — no deps/examples columns for now)
|
||||
6. **`gen_posflags()`:** Updated to read 7 fields (output unchanged)
|
||||
7. **`gen_possubcmds()`:** Updated to read 7 fields (output unchanged)
|
||||
|
||||
**Drift check:** `git diff -- DOC/AGENT_Context_Project.md completions/pos.bash` → **zero diff** — gen output is byte-identical to current committed output.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Migrate `bin/pos-tree`
|
||||
|
||||
**File:** `bin/pos-tree` (modified)
|
||||
|
||||
**Changes:**
|
||||
1. Added `source lib/registry.sh` after common.sh sourcing (with fallback chain)
|
||||
2. Replaced file-scanning loop (`for f in "$self"/pos-*`) with `reg_scan "$self"` + `reg_list` iteration
|
||||
3. Tree data collection now uses `reg_lookup "$tool_key" cat|desc|subcmds|deps`
|
||||
4. `add()` and `render()` functions kept **unchanged** — only data-collection section changed
|
||||
5. When `deps` is non-empty, appended `[deps: X]` to the description string before passing to `add()`
|
||||
|
||||
**Output comparison:** `diff /tmp/pos-tree-before.txt /tmp/pos-tree-after.txt` → **empty (identical)**
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Update `templates/pos-tool.sh`
|
||||
|
||||
**File:** `templates/pos-tool.sh` (modified)
|
||||
|
||||
**Changes:**
|
||||
- Added `# POS_SUBCMDS:`, `# POS_DEPS:`, `# POS_EXAMPLES:` to the header documentation block (step 2 comment)
|
||||
- Placed after existing `# POS_FLAGS:` example
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Update `install.sh`
|
||||
|
||||
**File:** `install.sh` (modified)
|
||||
|
||||
**Changes:**
|
||||
- Added `registry.sh` to the `lib_names` array (line 143) alongside other library names
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Update `DOC/DEV.md`
|
||||
|
||||
**File:** `DOC/DEV.md` (modified)
|
||||
|
||||
**Changes:**
|
||||
- Added `# POS_DEPS:` and `# POS_EXAMPLES:` to the header code block in "Adding a New CLI Tool → Make it discoverable"
|
||||
- Added explanation paragraph for both new headers (POS_DEPS: runtime binary names; POS_EXAMPLES: curated usage examples)
|
||||
- Both documented as optional with graceful degradation
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Update `DOC/AGENT_Context_Project.md`
|
||||
|
||||
**File:** `DOC/AGENT_Context_Project.md` (modified)
|
||||
|
||||
**Changes:**
|
||||
- Added `lib/registry.sh | 199 | Shared query API for POS tool metadata headers (...)` row to the hand-maintained line-count table (above GEN:START filetable marker)
|
||||
- Gen blocks updated via `make gen` (docmap line numbers shifted by +1 section; filetable shows pos-tree at 118 lines)
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 8: Verification
|
||||
|
||||
### Syntax checks
|
||||
```
|
||||
bash -n lib/registry.sh ✅
|
||||
bash -n scripts/gen-docs.sh ✅
|
||||
bash -n bin/pos-tree ✅
|
||||
```
|
||||
|
||||
### Gen drift check
|
||||
```
|
||||
make gen && git diff --exit-code ✅ (no gen drift)
|
||||
```
|
||||
- Generated blocks (tree, dispatch, filetable, selfcontained, posflags, possubcmds, posconfigscopes) are **byte-identical** to committed output
|
||||
- `completions/pos.bash` has **zero diff**
|
||||
|
||||
### Tree output comparison
|
||||
```
|
||||
diff /tmp/pos-tree-before.txt /tmp/pos-tree-final.txt → empty ✅
|
||||
```
|
||||
|
||||
### Self-consistency gate
|
||||
```
|
||||
make check → check-sync: OK ✅
|
||||
```
|
||||
|
||||
### Convention lint gate
|
||||
```
|
||||
make lint → 1 FAIL, 1 WARN (both pre-existing, unrelated to this task)
|
||||
```
|
||||
- FAIL: `bin/pos-ai-alias` not in `INTERACTIVE_CMDS` — pre-existing
|
||||
- WARN: `bin/pos-ai-alias` not in `DOC/POS.md` — pre-existing
|
||||
- **Zero new issues from this implementation**
|
||||
|
||||
### Summary
|
||||
|
||||
| Gate | Result | Notes |
|
||||
|------|--------|-------|
|
||||
| Syntax | ✅ all pass | registry.sh, gen-docs.sh, pos-tree |
|
||||
| Gen drift | ✅ zero diff | Gen blocks byte-identical |
|
||||
| Tree output | ✅ identical | Before/after comparison empty |
|
||||
| make check | ✅ OK | Full self-consistency gate |
|
||||
| make lint | ⚠️ 1 FAIL, 1 WARN | Pre-existing only (pos-ai-alias) |
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Artifact Summary
|
||||
|
||||
### Files Created
|
||||
|
||||
| File | Mode | Lines | Purpose |
|
||||
|------|------|-------|---------|
|
||||
| `lib/registry.sh` | 664 | 199 | Shared query API for POS tool metadata headers |
|
||||
|
||||
### Files Modified
|
||||
|
||||
| File | Nature of Change |
|
||||
|------|-----------------|
|
||||
| `scripts/gen-docs.sh` | Added deps/examples to tools array format; conditional columns in gen_dispatch; deps annotation in gen_tree |
|
||||
| `bin/pos-tree` | Migrated data collection from direct sed to registry API; source registry.sh |
|
||||
| `templates/pos-tool.sh` | Documented POS_SUBCMDS, POS_DEPS, POS_EXAMPLES headers |
|
||||
| `install.sh` | Added `registry.sh` to lib_names array |
|
||||
| `DOC/DEV.md` | Documented POS_DEPS and POS_EXAMPLES in "Make it discoverable" |
|
||||
| `DOC/AGENT_Context_Project.md` | Added lib/registry.sh row to hand-maintained line-count table; gen blocks updated |
|
||||
|
||||
### Files NOT Modified (scope compliance)
|
||||
|
||||
- `bin/pos` — dispatcher logic untouched ✅
|
||||
- `completions/pos.bash` — no changes needed ✅
|
||||
- `lib/config-ui.sh` — not integrated with registry ✅
|
||||
- No `# POS_DEPS:` or `# POS_EXAMPLES:` added to existing tools ✅
|
||||
|
||||
### Prerequisite Commits
|
||||
|
||||
1. `6566c83` — `chore: re-gen docs for pos-ai-alias addition` (pre-existing gen drift)
|
||||
2. `5d7407e` — `chore: update docmap + filetable for registry.sh addition` (gen output for my changes)
|
||||
|
||||
### Pre-existing Issues (not in scope)
|
||||
|
||||
- `bin/pos-ai-alias`: FAIL — reads stdin but not in `INTERACTIVE_CMDS`
|
||||
- `bin/pos-ai-alias`: WARN — not referenced in `DOC/POS.md`
|
||||
@@ -1,234 +0,0 @@
|
||||
# Builder Report — POS Command Registry, Phase 2
|
||||
|
||||
**Date:** 2026-08-26
|
||||
**Status:** COMPLETE
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status:** All 3 steps implemented and verified
|
||||
- **Files changed (this phase):** `bin/pos` (`_pos_category_help()` migration), `bin/pos-network-download`, `bin/pos-media-sync`, `bin/pos-system-backup`, `bin/pos-docker-ps` (headers), `scripts/gen-docs.sh` (2 cell-rendering fixes), gen output in `DOC/AGENT_Context_Project.md`
|
||||
- **Gates:** `make check` ✅ OK · `make lint` ✅ **0 FAIL, 0 WARN** · fast dispatch path unchanged (26 ms)
|
||||
- **Key finding:** `pos help <command>` is a pure exec-redirect — no per-command dispatcher surface exists; per brief, nothing invented
|
||||
|
||||
---
|
||||
|
||||
## Baseline
|
||||
|
||||
- Phase 1 state on disk; `make check` → `check-sync: OK`
|
||||
- Maintainer's uncommitted fixes present (`ai-alias` added to `INTERACTIVE_CMDS` at bin/pos:262, DOC updates) — **untouched**
|
||||
- BEFORE category-help captured for all 10 categories → `/tmp/pos-phase2/h-*-before.txt`
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Migrate `_pos_category_help()`
|
||||
|
||||
**File:** `bin/pos`, `_pos_category_help()` (~lines 68–100 post-edit)
|
||||
|
||||
**Changes:**
|
||||
- Registry sourced + `reg_scan "$self"` called **lazily inside the function** — plain dispatch paths never pay scan cost (verified: fast path 26 ms before and after)
|
||||
- Tool list from `reg_tools_in "$cat"`, stripped of leading `$cat-` → identical short display names
|
||||
- All sed header reads replaced by `reg_lookup "<cat>-<short>" desc|subcmds|deps`
|
||||
- Nested-tool enrichment loop, `is_nested` skip, printf layout: **byte-for-byte preserved**
|
||||
|
||||
**Preservation proof (pre-headers):** diff of before/mid captures across all 10 categories → **zero differences**.
|
||||
|
||||
**New behavior (fires only when headers exist):** second indented line ` [deps: <deps>]` directly under a tool's description line.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Registry metadata in `pos help`
|
||||
|
||||
**Finding:** the `help` meta-command (bin/pos ~lines 233–245) joins args into `pos-<cat>-<cmd>` and `exec`s the tool's own `--help`. There is **no per-command rendering surface in the dispatcher** — examples/deps shown there come from each tool's `usage()` heredoc. Per the brief's instruction ("do NOT invent new UX"), this step is limited to that finding: no dispatcher-side extension made. The category-help surface (Step 1) is where registry metadata surfaces.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Representative headers on existing tools
|
||||
|
||||
Guards re-verified by reading source before annotating:
|
||||
|
||||
| Tool | Hard guards found | Headers added |
|
||||
|------|-------------------|---------------|
|
||||
| `bin/pos-network-download` (lines 10–12: `err` ×3) | aria2c, jq, curl | `# POS_DEPS:` + 3 real-subcommand EXAMPLES (add/status/watch) |
|
||||
| `bin/pos-media-sync` (lines 15–16: `err` ×2) | lsblk, jq | `# POS_DEPS:` + 2 EXAMPLES (`--mp3`, `--mp4 --dry-run` — straight from its usage()) |
|
||||
| `bin/pos-system-backup` (line 60 `err`; 72–73 `warn`; 179 conditional `err`) | tar | `# POS_DEPS: tar` only |
|
||||
| `bin/pos-docker-ps` (line 16: single `err`) | docker | `# POS_DEPS: docker` |
|
||||
|
||||
**Decision note (system-backup):** lsblk/jq are warn-only soft guards ("USB copy skipped", returns 0); gpg errors only when encryption is requested (`--no-encrypt` bypasses). Deps header = hard requirements only → `tar`.
|
||||
|
||||
4 tools total (cap 5 respected). Placement rule followed: metadata headers in comment block, `# POS:` first, new headers after existing POS_* lines, before any code.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Verification (per budget)
|
||||
|
||||
```
|
||||
bash -n bin/pos ✅
|
||||
category --help before/mid diff (10 categories) ✅ zero diff (Step 1 preservation)
|
||||
category --help before/after diff ✅ only [deps: …] lines added:
|
||||
network +1, docker +1, system +1, media +1; others 0
|
||||
make gen && git diff --stat ✅ GEN changes confined to expected blocks
|
||||
make check ✅ check-sync: OK
|
||||
make lint ✅ 0 FAIL, 0 WARN
|
||||
bin/pos-tree | grep deps ✅ 4 annotations visible
|
||||
bin/pos network --help | grep -i deps ✅ [deps: aria2c jq curl]
|
||||
fast path timing ✅ 26 ms (no reg_scan cost)
|
||||
completions/pos.bash ✅ untouched by gen
|
||||
```
|
||||
|
||||
**GEN block inspection (as instructed):**
|
||||
- Tree gains `[deps: …]` annotation lines under annotated tools ✓
|
||||
- Dispatch table flipped to 6-column format (`_has_deps_examples=1`) ✓
|
||||
- Two rendering defects found & fixed in `gen-docs.sh` during inspection:
|
||||
1. Examples cell contained raw inner pipes from the `cmd | desc` grammar → broke markdown column count. Fixed: inner ` | ` rendered as `→` within cells.
|
||||
2. Intended `//` example joiner actually produced single `/` (`paste -d'//'` cycles chars). Fixed: awk-based literal `//` join.
|
||||
- Resulting cell sample: `…| lsblk jq | pos media sync --mp3 → Sync only MP3 files to USB//pos media sync --mp4 --dry-run → Preview MP4 sync without copying |` — valid 6-column row, readable ✓
|
||||
- Docmap line ranges shifted accordingly (auto-generated) ✓
|
||||
|
||||
---
|
||||
|
||||
## Artifacts Summary
|
||||
|
||||
### Modified this phase
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `bin/pos` | `_pos_category_help()` → registry API + lazy source + deps line (Maintainer's INTERACTIVE_CMDS line preserved verbatim) |
|
||||
| `bin/pos-network-download` | DEPS + 3 EXAMPLES headers |
|
||||
| `bin/pos-media-sync` | DEPS + 2 EXAMPLES headers |
|
||||
| `bin/pos-system-backup` | DEPS header (hard dep only) |
|
||||
| `bin/pos-docker-ps` | DEPS header |
|
||||
| `scripts/gen-docs.sh` | Examples-cell pipe escaping (`→`) + correct `//` joiner |
|
||||
| `DOC/AGENT_Context_Project.md` | Gen output only (tree annotations, 6-col dispatch, docmap shift, filetable line counts) |
|
||||
|
||||
### Not touched (constraint compliance)
|
||||
`lib/config-ui.sh` ✅ · `completions/pos.bash` ✅ · `INTERACTIVE_CMDS` ✅ · Maintainer's fixes ✅ · `bin/pos-tree` rendering ✅ · no new files except report ✅
|
||||
|
||||
### Findings / deferred
|
||||
- `reg_scan` ≈ 1.3 s on this box (≈250 process forks / 41 tools) — acceptable on lazy help-only paths; single-pass awk scan remains the anticipated future optimization (Phase 1 Risk 2)
|
||||
- `pos help <command>` has no dispatcher-side surface to enrich (pure redirect)
|
||||
|
||||
---
|
||||
|
||||
## Review-fix round
|
||||
|
||||
Two findings from the Phase 2 adversarial review; both fixed minimally.
|
||||
|
||||
### Finding 1 (MEDIUM): stale hand-written table header above the dispatch GEN block — FIXED
|
||||
|
||||
**Exact lines removed** from `DOC/AGENT_Context_Project.md` (were lines 275–276):
|
||||
|
||||
```
|
||||
| Category | Command | Script | Description |
|
||||
|----------|---------|--------|-------------|
|
||||
```
|
||||
|
||||
The orphaned 4-column header + its underline row sat between `### Available Commands` and `<!-- GEN:START dispatch -->`, duplicating the GEN block's own 6-column header. Nothing else removed — the section now reads `### Available Commands` → blank line → `GEN:START dispatch` directly.
|
||||
|
||||
### Finding 2 (LOW): `//` example joiner collides with URL slashes — FIXED
|
||||
|
||||
**New separator:** `" · "` (space-middle-dot-space), replacing the literal `//` in `scripts/gen-docs.sh` line ~48. Awk literal-join approach unchanged.
|
||||
|
||||
Rationale for `·`: cannot appear in a URL path, reads as a list separator in a markdown cell, and stays clear of both the pipe grammar (` | ` inside a single example, rendered as `→` in cells) and the em-dash used by `# POS:` descriptions. Live case now renders:
|
||||
`pos network download add https://example.com/file.zip → Enqueue an HTTP download (auto-starts daemon) · pos network download status → Daemon health + global transfer stats · pos network download watch → Live progress view`
|
||||
— the URL's `//` survives intact and each example is cleanly delimited.
|
||||
|
||||
### Verification
|
||||
|
||||
```
|
||||
bash -n scripts/gen-docs.sh ✅
|
||||
make gen && make gen → md5 of generated files identical
|
||||
✅ double-gen idempotence: ZERO drift after second run
|
||||
grep -c 'Category.*Command' DOC/…Project.md ✅ = 1 (only the GEN block's own header)
|
||||
sed -n '/GEN:START dispatch/,+4p' ✅ GEN header is first row in block
|
||||
lines above GEN:START dispatch ✅ only "### Available Commands" + blank
|
||||
make check ✅ check-sync: OK
|
||||
make lint ✅ 0 FAIL, 0 WARN
|
||||
```
|
||||
|
||||
Note: the earlier bare `git diff --exit-code` after gen exits 1 because ALL Phase 1+2 work is still uncommitted vs HEAD — checksum comparison of run-1 vs run-2 outputs is the correct idempotence measure and passes.
|
||||
|
||||
### Constraint compliance
|
||||
|
||||
Touched ONLY `DOC/AGENT_Context_Project.md` (2 stale lines) and `scripts/gen-docs.sh` (separator string). No bin/*, lib/*, or completions/ changes. Everything else regenerated via `make gen`.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Alias generator quoting fix
|
||||
|
||||
**File:** `bin/pos-ai-alias` (`_alias_regen()`, new `_alias_quote_cmd()` helper, `_alias_show()` command line)
|
||||
|
||||
### Chosen mechanism + rationale
|
||||
|
||||
`printf '%q'` — the canonical bash mechanism — applied **per layer**:
|
||||
|
||||
1. `_alias_quote_cmd <provider> <session> <prompt>` builds the pos-ai command with `%q`-quoted prompt → the prompt is ONE shell word on expansion.
|
||||
2. `_alias_regen` then `%q`s the ENTIRE value before emitting `alias name=<value>` → the assignment survives any bytes.
|
||||
|
||||
**Justified deviation from the brief's literal snippet:** the prescribed single-`%q`-over-whole-string form expands to multi-word args after `--system`, but `bin/pos-ai`'s parser takes exactly one value (`SYSTEM_PROMPT="$2"; shift 2`, bin/pos-ai:640–642) — multi-word prompts would truncate to their first word with the remainder leaking into question positionals. The original code had the identical latent defect (single-quote fragments never survive alias-expansion re-parsing). Double-`%q` is the same single mechanism, applied once per quoting layer — no manual fragment concatenation, no mixing of approaches. Verified by exact round-trip (A4/A7).
|
||||
|
||||
### Diff summary
|
||||
|
||||
| Function | Change |
|
||||
|----------|--------|
|
||||
| `_alias_quote_cmd()` (new, ~10 lines) | Shared builder: `%q` prompt as one word; empty prompt → no `--system`; used by regen + show |
|
||||
| `_alias_regen()` | Naive `'…'<escaped>'…'` concatenation replaced by helper + whole-value `%q`; collects names; keeps `bash -n "$tmp"`; ADDS semantic per-name check in fresh shell: `bash --norc -c 'shopt -s expand_aliases; source "$1" && [[ $(type -t "$2") == alias ]]' _ "$tmp" "$n"`; failure → warn(alias name) + keep previous + rc 1 (mirrors existing path). Note: `expand_aliases` required — non-interactive shells define aliases but `type -t` only reports them when expansion is on (found during self-test) |
|
||||
| `_alias_show()` | Command line now rendered via `_alias_quote_cmd` (display-only; structure untouched) — what users copy pastes verbatim |
|
||||
|
||||
Unchanged per constraints: ENV_FILE format, `|` ban, name/session regexes, chmod/mv semantics, interactive flows.
|
||||
|
||||
### Harness (exact commands)
|
||||
|
||||
```bash
|
||||
export CONFIG_DIR=/tmp/pos-alias-test
|
||||
rm -rf "$CONFIG_DIR"; mkdir -p "$CONFIG_DIR"
|
||||
cat > "$CONFIG_DIR/ai-aliases.env" <<'EOF' # fixture as specified
|
||||
# AI aliases — managed by pos ai alias (do not hand-edit)
|
||||
assist|gemini|assist|You are assist, a concise and practical AI assistant.
|
||||
evil|gemini|evil|It's a 'quoted' $(rm -rf /) `backtick` \backslash "dq" ;semi|pipe-less
|
||||
plain|openrouter|plain|
|
||||
EOF
|
||||
# regen via function extraction (sanctioned approach):
|
||||
bash -c 'source lib/common.sh
|
||||
ENV_FILE="$CONFIG_DIR/ai-aliases.env"; SH_FILE="$CONFIG_DIR/ai-aliases.sh"
|
||||
eval "$(awk "/^_alias_quote_cmd\(\)/,/^}/" bin/pos-ai-alias)"
|
||||
eval "$(awk "/^_alias_regen\(\)/,/^}/" bin/pos-ai-alias)"
|
||||
_alias_regen'
|
||||
```
|
||||
|
||||
**Fixture nuance:** `evil`'s prompt contains `|`; the loader's `IFS='|' read … _rest` splits it off (pre-existing behavior — this is why prompts ban `|`). Round-trip asserts against the loader-stored string (`…;semi`), not the hand-edited tail.
|
||||
|
||||
### Assertion results
|
||||
|
||||
| # | Assert | Result |
|
||||
|---|--------|--------|
|
||||
| A1 | `bash --norc -c "source $SH"` → rc 0, **zero stderr** | ✅ |
|
||||
| A2 | No execution while sourcing (`$CONFIG_DIR/pwned` absent) | ✅ |
|
||||
| A3 | `type -t assist/evil/plain` == `alias` (fresh shell) | ✅ all three |
|
||||
| A4 | evil expansion round-trip via `eval "set -- ${BASH_ALIASES[evil]}"` (no execution): post-`--system` words join to **exact original prompt**, delivered as one arg | ✅ byte-exact |
|
||||
| A5 | `plain` emits no `--system` fragment | ✅ |
|
||||
| A6 | Regen idempotence (second run md5-identical) | ✅ |
|
||||
| A7 | `pos ai alias show evil` Command line, evaluated via `eval set -- $(cat …)`, yields exact prompt | ✅ byte-exact |
|
||||
|
||||
Also verified end-to-end: `bin/pos-ai-alias list`, `bin/pos ai alias show plain` (dispatcher path) behave normally.
|
||||
|
||||
### Gates
|
||||
|
||||
```
|
||||
bash -n bin/pos-ai-alias ✅ syntax OK (563 lines)
|
||||
make gen ✅ (filetable row for pos-ai-alias updated automatically)
|
||||
make check ✅ check-sync: OK
|
||||
make lint ✅ 0 FAIL, 0 WARN
|
||||
```
|
||||
|
||||
[DONE]
|
||||
@@ -1,29 +0,0 @@
|
||||
# Builder Report: remove `e` (edit) option from AI command prompt
|
||||
|
||||
## TL;DR
|
||||
- **Status:** COMPLETE
|
||||
- **Files changed:** `bin/pos-ai`, `DOC/AGENT_Context_Project.md` (gen: line-count update)
|
||||
- **Verification:** `bash -n` ✓, `make gen` ✓, `make check` ✓, `make lint` ✓ (0 FAIL, 0 WARN)
|
||||
- **Scope compliance:** in-scope only; no unauthorized changes
|
||||
|
||||
## Step 1: Remove `e|E)` case and fix prompt text
|
||||
[DONE]
|
||||
|
||||
- Changed prompt from `[Y/n/e(dit)]` to `[Y/n]` (line 379)
|
||||
- Removed the `e|E)` case branch that called `_inject_command` (was lines 388–390)
|
||||
- Kept `n|N)` and `*` (Y/Enter) cases intact
|
||||
|
||||
## Step 2: Delete `_inject_command()` function
|
||||
[DONE]
|
||||
|
||||
- Removed entire function (was lines 399–457, 55 lines + comment header)
|
||||
- Verified no other callers exist (only caller was the deleted `e|E` case)
|
||||
- Cleaned up trailing blank lines
|
||||
|
||||
## Step 3: Verification (`bash -n`, gates)
|
||||
[DONE]
|
||||
|
||||
- `bash -n bin/pos-ai` — syntax OK
|
||||
- `make gen` — regen OK (updated line-count table in `DOC/AGENT_Context_Project.md`: 735→680)
|
||||
- `make check` — OK
|
||||
- `make lint` — 0 FAIL, 0 WARN
|
||||
@@ -1,104 +0,0 @@
|
||||
# Builder Report — Paste-Safe Multi-Line Input in `pos ai alias` Insert Prompt
|
||||
|
||||
**Date:** 2026-08-27
|
||||
**Status:** COMPLETE — root cause established, fix implemented, verified, committed, pushed
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Root cause:** `menu_ask_value` used plain line-oriented `read -rp` in canonical mode. A multi-line Ctrl+V paste floods the tty queue; `read` consumes only the first line and the rest stay queued — later prompts eat them, and after the script exits the interactive shell executes whatever remains (user-verified: `$(whoami)`, `; ls`, `echo test`, `sudo apt update` ran). Single-line paste was unaffected.
|
||||
- **File changes:** `lib/menu-lib.sh` 169→362 (new `menu_read_value` raw-mode bracketed-paste reader + `menu_redraw`; `menu_ask_value` now delegates to it); `bin/pos-ai-alias` 712→760 (prompt encode/decode for the ENV file, newline-safe truncate, edit wizard keeps the full original prompt).
|
||||
- **Verification:** pty harnesses — multiline + single-line paste captured verbatim with **nothing executed**, clean exit; Ctrl-D and Ctrl-C cancel cleanly (terminal restored); full create→list→show→edit E2E; encode/decode round-trips byte-exact.
|
||||
- **Gates:** `bash -n` ×2 · `make gen` idempotent · `make check` OK · `make lint` 0 FAIL / 0 WARN.
|
||||
- **Commit:** `4306a53` — pushed to `main` (`300b742..4306a53`).
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## 1. Root Cause
|
||||
|
||||
`lib/menu-lib.sh` `menu_ask_value` ended in:
|
||||
|
||||
```bash
|
||||
IFS= read -rp "$pr: " val
|
||||
```
|
||||
|
||||
`bash -p read` is **line-oriented and canonical**: it reads until the first newline and returns. A bracketed multi-line paste delivers `line one\nline two\n…` into the tty input queue as a burst; the read takes line one, and every later line stays queued. Those leftovers are then consumed by the next prompt or — once the script exits and the interactive shell reads keyboard input again — **executed as shell commands**. That is the paste bug: user-pasted text (including `$(whoami)`, `; ls`, `sudo apt update` in the report which triggered real effects) was treated as live input.
|
||||
|
||||
### Why not `read -erp` (readline)?
|
||||
|
||||
Tested on a real pty with bracketed paste enabled. Readline consumes the paste **atomically** (nothing executes, clean exit) — but on accept it returns **only the first line**; readline buffers a single line. That violates the core requirement that multi-line prompts paste correctly. A custom raw-mode reader is required.
|
||||
|
||||
### Why not a plain `read -N 1` byte loop?
|
||||
|
||||
`bash read` self-interrupts: with `stty -isig` verified on (diag: `-isig -icanon -echo min 1 time 0`), `od -An -tu1 -N1` reads byte `3` (ETX) fine from the tty, and `read -N 1` reads `0x03` fine from a pipe — but `bash` + tty + `0x03` dies by SIGINT (raw waitpid status 2) in both direct and command-substitution contexts, even with no trap and ISIG off. The reader therefore uses `dd bs=4096 count=1 | od -An -tx1 | tr -d ' \n'`, which the pty tests show reads any byte (incl. `0x03`/`0x04`) as plain data.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## 2. Fix
|
||||
|
||||
### `lib/menu-lib.sh` — `menu_read_value()` (new)
|
||||
|
||||
- `stty -icanon -echo -isig min 1 time 0` raw mode (isig off so Ctrl-C/Z/\ arrive as bytes); `stty -g` snapshot restored on every exit path; `trap 'restore; trap - INT TERM; return 1' INT TERM`.
|
||||
- Enables bracketed paste (`\e[?2004h`) on entry, disables (`\e[?2004l`) on exit.
|
||||
- Byte input: chunked `dd|od|tr` reader (one fork per input burst — pastes cost O(chunks), not O(per-byte forks)); bytes delivered as 2-hex-digit strings to a nameref; state (chunk/offset) persists because the reader runs in-place, never inside a `$( )` subshell (an early implementation that returned bytes via command substitution looped forever — each subshell's offset increment was lost).
|
||||
- Behavior:
|
||||
- `\e[200~` … `\e[201~` → everything between inserted **literally**; newline/CR are data (echoed for display; CR renders matching CRLF pastes).
|
||||
- Enter outside a paste → submit. Backspace/DEL, Left/Right, Home/End, Delete, Ctrl-U. Up/Down ignored (no history).
|
||||
- Ctrl-D on empty → cancel; Ctrl-C/Ctrl-Z/Ctrl-\ → cancel. Cancel returns rc 1 → callers print `CANCELLED` and continue.
|
||||
- Fast append path (no redraw) when the cursor is at end-of-value — pastes render with 0 redraws.
|
||||
- Non-tty stdin or stty failure → falls back to plain `read` (fail-closed, no paste protection possible).
|
||||
- `menu_ask_value` now calls `menu_read_value` via command substitution (display → stderr, value → stdout, rc 1 = cancel/EOF).
|
||||
- Function index comment updated.
|
||||
|
||||
### `bin/pos-ai-alias` — prompt persistence + display
|
||||
|
||||
- `_alias_prompt_encode` — `\` → `\\`, newline → `\n` (escaped, so the ENV record stays single-line). Uses literal `[ "$c" = '\' ]` comparisons: bash `case` patterns do **not** match a single literal backslash (verified empirically).
|
||||
- `_alias_prompt_decode` — order-safe: `\n` first, then `\\`.
|
||||
- `_alias_load` now decodes the prompt field; `_alias_save` encodes it (previously the raw multi-line value was written straight into the `|`-delimited ENV record — corruption + line-splitting on round-trip).
|
||||
- `_alias_prompt_truncate` — now replaces newlines with `\n` for display and takes an optional max-length argument (create confirmation: 50; edit wizard: 80; edit summary: 40).
|
||||
- Edit wizard: the prompt default shown is the truncated render, but pressing Enter restores the **full** original prompt (`[ "$tmp" = "$default_display" ] && tmp="$default_prompt"`) — fixes a pre-existing bug where Edit+Enter silently truncated >80-char prompts; empty-original Enter keeps empty and continues instead of aborting the wizard.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## 3. Tests Performed
|
||||
|
||||
All harnesses under `/tmp` (not committed).
|
||||
|
||||
| Test | Result |
|
||||
|------|--------|
|
||||
| `bash -n lib/menu-lib.sh bin/pos-ai-alias` | ✅ |
|
||||
| Bracketed multiline paste through `menu_ask_value` (pty) — full capture | ✅ |
|
||||
| Nothing executed from the pasted content (incl. `$(whoami)`, `; ls`, `sudo apt update`) | ✅ |
|
||||
| Clean exit + terminal restored (`\e[?2004l`, sane termios) | ✅ |
|
||||
| Single-line paste | ✅ |
|
||||
| Ctrl-D on empty → CANCELLED → DONE, status 0 | ✅ |
|
||||
| Ctrl-C → CANCELLED → DONE, status 0 (terminal not left in raw mode) | ✅ |
|
||||
| Encode/decode round-trips: backslash, literal `\n` text, mixed, empty | ✅ |
|
||||
| Full E2E: create (multiline paste with `C:\temp\note` + shell fragments) → ENV single-line record (`\n`/`\\` escaped) → wrapper installed, executable, `bash -n` clean → wrapper passes whole prompt as ONE `--system` arg (`$'…'`) → `list` table newline-safe → `show` decodes full prompt → edit Enter-keeps-full, "No changes — nothing to save." | ✅ |
|
||||
| Gates: `make gen` idempotent · `make check` OK · `make lint` 0 FAIL / 0 WARN | ✅ |
|
||||
|
||||
Harness files: `pty_menulib_test.py`, `pty_cancel_test.py`, `pty_e2e_alias.py`, `roundtrip_test.sh`, plus early experiments `pty_paste_test.py`/`pty_paste_test2.py`.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## 4. Notes / Remaining Risks
|
||||
|
||||
- The reader is byte-oriented; multi-byte UTF-8 is assembled byte-by-byte (correct round-trip; cursor arithmetic counts bytes, so editing inside multi-byte text can be coarse — same limitation as the previous readline-free implementation, acceptable for this tool).
|
||||
- Only `menu_ask_value` was made paste-safe. Other menu-lib primitives (`menu_run`, `menu_pick`, `menu_guard`) use canonical reads where a multi-line paste is not a meaningful input shape; `pos-ai-alias` is the tool whose "Insert Prompt" step accepted multi-line text.
|
||||
- `Ctrl-C` inside the reader cancels the prompt and continues the script (CANCELLED). Previously any Ctrl-C killed the whole script; behavior is now more forgiving and the terminal is always restored first.
|
||||
- CRLF pastes render correctly (CR echoed) and are stored as raw `\r` only if the terminal emulator delivers them un-mapped; with `icrnl` on, they normalize to `\n` — either way the value stays literal text, never input.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
**Status: COMPLETE.** Original objective (paste injection + broken multiline paste in `pos ai alias` Insert Prompt) is fixed at the shared input layer, verified with pty-level regression harnesses, committed (`4306a53`), and pushed.
|
||||
@@ -1,98 +0,0 @@
|
||||
# Builder Report — Configurable AI-Bridge Trigger Word for the Telegram Listener
|
||||
|
||||
**Date:** 2026-08-27
|
||||
**Status:** COMPLETE — implemented, verified, committed (`e6fa0a4`), pushed
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Request:** make the Telegram listener's `ai ` → Gemini bridge prefix configurable ("like communication telegram-listener prefix").
|
||||
- **Change:** new `TELEGRAM_AI_PREFIX` config (default `ai`) + new `prefix` verb on the listener; live (per-message) reload like the command map; also editable via `pos config telegram`.
|
||||
- **Files:** `bin/pos-communication-telegram-listener` 566→623, `bin/pos-communication-telegram-sender` (config-scope field), `completions/pos.bash` (regen), `DOC/AGENT_Context_Project.md` (regen filetable row), `DOC/POS.md`, `DOC/howto/ai.md`, `AGENT_TODO.md`.
|
||||
- **Verification:** function-level routing harness (green), CLI verb tests, dispatch smoke via `pos`, `pos config telegram` render, gates `0 FAIL, 0 WARN`.
|
||||
- **Commit:** `e6fa0a4` — pushed (`4306a53..e6fa0a4`).
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## 1. Design
|
||||
|
||||
- **Config seam:** `TELEGRAM_AI_PREFIX` in `telegram.env` (shared by sender + listener). The listener already had a generic `load_config`; the key needs no code to load. Added to the `telegram` `# POS_CONFIG:` scope (in the sender's header, which owns that scope) so `pos config telegram` renders/edits it — registry-driven, no config-ui code.
|
||||
- **CLI verb:** `pos communication telegram listener prefix` → shows current word; `prefix <word>` → validates `[A-Za-z0-9][A-Za-z0-9_-]*` (one word, no spaces/pattern metachars, no leading `-` to avoid flag ambiguity), writes `TELEGRAM_AI_PREFIX=` to `telegram.env` (chmod 600, mktemp+mv, `grep -vE` old line). Registered via `# POS_SUBCMDS: prefix` → completions regenerate.
|
||||
- **Hot reload:** like the command map (re-read per message), a new `ai_bridge_prefix()` reads `telegram.env` per message. Precedence: file > env var from `load_config` (daemon start) > default `ai`. No daemon restart needed after `prefix <word>`.
|
||||
- **Matching:** literal, case-insensitive prefix followed by whitespace. `case` patterns cannot express "literal word + one space + case-insensitive", so the handler uses a scoped `shopt -s nocasematch` + quoted-literal `=~`: `^"$prefix"[[:space:]](.*)$` (quoted variable = literal; nocasematch covers `AI`/`Ai`/custom `BoT`…). Unset immediately to keep the rest of the handler case-sensitive.
|
||||
- **Preserved behavior:** bare `ai` (no trailing space) never matched the original `^[Aa][Ii][[:space:]]` regex → still falls through to "Unknown command". `ai /reset` (or `<prefix> /reset`) clears the session. Usage/`Unknown command`/log lines now interpolate the actual prefix.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## 2. Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `bin/pos-communication-telegram-listener` | `prefix_cmd()` (show/set/validate/write), `ai_bridge_prefix()` (hot-reload resolver), `handle_message` uses `$prefix` with scoped nocasematch + literal `=~`, `--status` shows `ai prefix:`, usage() documents `prefix`, `# POS_SUBCMDS: prefix`, updated header comments |
|
||||
| `bin/pos-communication-telegram-sender` | `# POS_CONFIG:` telegram scope + `TELEGRAM_AI_PREFIX=:AI-bridge trigger word in the telegram listener (default ai)::ai` |
|
||||
| `completions/pos.bash` | regen: `_pos_subcmds[communication-telegram-listener]="prefix"` |
|
||||
| `DOC/AGENT_Context_Project.md` | regen filetable: listener row 566→623 |
|
||||
| `DOC/POS.md` | listener table: two `prefix` rows; paragraph now says `<prefix>` (default `ai `) configurable via verb/config; corrected stale claim that AI errors reply "plus a `pos config ai` hint" — code replies `AI error: …` only |
|
||||
| `DOC/howto/ai.md` | Telegram section: trigger word configurable, `<prefix> /reset`, reply-context wording; troubleshooting points at `prefix` verb |
|
||||
| `AGENT_TODO.md` | dated Done entry |
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## 3. Tests Performed
|
||||
|
||||
### Routing harness — `/tmp/ai_prefix_routing_test.sh`
|
||||
Extracts the listener's real functions (`handle_message` + deps) via `sed`, routes messages through them with a PATH-stub `pos` (fixed: `timeout pos …` runs a child process, so a bash-function stub is invisible) and a stub `reply`.
|
||||
|
||||
| Case | Result |
|
||||
|------|--------|
|
||||
| `ai hello` → gemini ask (`--session telegram-123 --system … hello`), answer replied | ✅ |
|
||||
| `AI Hello` → routed, prompt kept (case-insensitive) | ✅ |
|
||||
| `ai` (no space) → **Unknown command** (preserved edge), no ask | ✅ |
|
||||
| `ai ` (trailing space) → Usage reply | ✅ |
|
||||
| `ai /reset` → `pos ai gemini sessions reset telegram-123`, "Memory cleared." | ✅ |
|
||||
| `hello` → Unknown command (not ai-routed) | ✅ |
|
||||
| `TELEGRAM_AI_PREFIX=bot`: `bot hi` → routed; `BOT Hi` → routed; `ai hi` → Unknown command | ✅ |
|
||||
| `bot /reset` → sessions reset | ✅ |
|
||||
| var removed → `ai back` routed again (hot-reload fallback) | ✅ |
|
||||
|
||||
### CLI verb — real script, temp CONFIG_DIR
|
||||
| Test | Result |
|
||||
|------|--------|
|
||||
| `prefix` (no arg) → `AI bridge prefix: ai` + usage hint | ✅ |
|
||||
| `prefix bot` → writes `TELEGRAM_AI_PREFIX=bot` to telegram.env, rc 0 | ✅ |
|
||||
| `prefix` after set → `AI bridge prefix: bot` | ✅ |
|
||||
| `prefix 'a b'` and `prefix 'a.b'` → rc 1, validation error | ✅ |
|
||||
| `prefix 1ok` → accepted (leading digit allowed) | ✅ |
|
||||
| `--status` → `ai prefix: <current>` | ✅ |
|
||||
| `prefix ai` → back to default | ✅ |
|
||||
|
||||
### Dispatch + config UI
|
||||
- `pos communication telegram listener prefix` and flat `pos communication telegram-listener prefix` both show the current prefix (repo `bin/pos` on PATH). ✅
|
||||
- `pos config telegram` (temp dir) renders `TELEGRAM_AI_PREFIX` with its description. ✅
|
||||
|
||||
### Gates
|
||||
`bash -n` ×2 · `make gen` idempotent · `make check` OK · `make lint` 0 FAIL / 0 WARN. ✅
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## 4. Notes / Remaining Risks
|
||||
|
||||
- **Matrix listener untouched** — it has its own hard-coded `ai …` bridge (session `matrix-<room>`); the same treatment is a possible follow-up if wanted.
|
||||
- The daemon still loads `TELEGRAM_BOT_TOKEN`/`TELEGRAM_CHAT_ID` once at start (unchanged); only the prefix hot-reloads.
|
||||
- A prefix change applies to ALL chats the listener serves (there is one owner chat by design).
|
||||
- Literal prefix matching means a prefix containing regex metacharacters would be matched literally (quoted), but validation restricts to word chars for predictability.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
**Status: COMPLETE.** The `ai ` trigger is now `TELEGRAM_AI_PREFIX` (default `ai`), configurable via `pos communication telegram listener prefix <word>` or `pos config telegram`, applied live.
|
||||
@@ -1,43 +0,0 @@
|
||||
# Telegram listener: generic text-prefix → app map
|
||||
|
||||
**TL;DR:** The listener now routes any non-command message `<word> <text>` to a user-configurable command with `<text>` appended as ONE quoted argument — `opencode=opencode` turns "opencode check cpu" into `opencode "check cpu"`. Routing order per message: text-prefix map → built-in Gemini `ai` bridge → `/command` map → "Unknown command". `prefix` verb reworked to manage the map; `TELEGRAM_AI_PREFIX` is now set only via `pos config telegram`.
|
||||
|
||||
## Steps
|
||||
|
||||
- [DONE] Design — generalized the previous scalar AI-prefix into a full prefix→command map per the user's clarification ("each prefix call a app with pass 'text'"); kept the `/command` map and the Gemini bridge untouched, with the prefix map checked first so a mapped word can shadow `ai`.
|
||||
- [DONE] Implement `bin/pos-communication-telegram-listener` (623→782):
|
||||
- `PREFIX_FILE="$CONFIG_DIR/telegram_prefixes.env"` (chmod 600, hot-reloaded per message like the command map).
|
||||
- `prefix_map_find(text)` — case-insensitive `^word[[:space:]](.*)$` (quoted-literal bash regex in scoped `nocasematch`); first file match wins; requires non-empty remainder; returns `command\x1fremainder`.
|
||||
- `prefix_map_set/del/show` — same mktemp+mv pattern as the map functions; words validated `[A-Za-z0-9][A-Za-z0-9_-]*`; commands `bash -n`-checked via `check_syntax` (fixed latent `set -e` abort: `errs="$(...)" || err ...`).
|
||||
- `run_and_reply(cmdline, msg_id, tmo, quiet)` — shared runner: `timeout <tmo> bash -c`, empty→`OK`, non-zero→`exit N`+output, `@quiet`→no reply. `/command` map uses 60s (unchanged), prefix map 120s.
|
||||
- `handle_message` — prefix bridge inserted after `/help|/start`, before the AI bridge; `/command` map block refactored onto `run_and_reply`.
|
||||
- `prefix` verb: bare = list + bridge word; `prefix <word> <command...>` = map; `prefix <word>` = show one; `prefix -r <word>` = remove. Dispatch `prefix) prefix_cmd "${@:2}"`.
|
||||
- `--status` lists prefix entries; usage() + header `# POS:` line updated.
|
||||
- [DONE] Docs — POS.md listener rows + new Text-prefix map / AI bridge paragraphs; howto/communication.md text-prefix bullet; howto/ai.md trigger-word wording (now `pos config telegram`) + prefix-map/shadowing note; AGENT_Context regen via `make gen`.
|
||||
- [DONE] AGENT_TODO.md Done entry appended (newest-last).
|
||||
- [PENDING] Commit + push (next step after this report).
|
||||
|
||||
## Verification
|
||||
|
||||
- Routing harness `/tmp/prefix_map_routing_test.sh` — extracts the real listener functions (incl. new prefix map helpers + `run_and_reply`) with PATH stubs for `pos` and `opencode`; **27/27 PASS**:
|
||||
- ai-bridge regression: `ai hello` → gemini ask, `ai /reset` → sessions reset, no-prefix → Unknown.
|
||||
- `opencode check cpu` → app runs, remainder is ONE arg (`argc=1 arg1=<check cpu>`), output replied.
|
||||
- Case-insensitive `OpEnCoDe`; bare `opencode` and `opencode ` (trailing space) fall through to Unknown.
|
||||
- Mapped `ai` shadows the Gemini bridge and passes the remainder.
|
||||
- No false match on partial prefixes (`o` vs `opencode`).
|
||||
- `exit N` reply, empty→`OK`, `@quiet` suppression, `$TELEGRAM_CHAT_ID` expansion in templates.
|
||||
- `/command` map regression through `run_and_reply` (`/health`, `/failx` exit 2, `@quiet`, `/help` list, unknown).
|
||||
- CLI verb suite (real tool, temp `CONFIG_DIR`): set / show / remove / remove-missing (rc 0) / invalid word (rc 1) / invalid cmd `if then` (rc 1, nice `ERROR:` instead of raw `set -e` abort) / update-existing / `@quiet` template accepted / `--status` listing.
|
||||
- Dispatch smoke via `bin/pos`: nested `pos communication telegram listener prefix …` and flat `pos communication telegram-listener prefix -r …` both work; bare `prefix` lists.
|
||||
- `pos config telegram` (temp dir) still renders `TELEGRAM_AI_PREFIX` with description.
|
||||
- Gates: `bash -n` clean; `make gen && make check` OK; `make lint` 0 FAIL / 0 WARN.
|
||||
|
||||
## Notes / behavior choices
|
||||
|
||||
- Bare `<word>` (no trailing space) does NOT trigger a prefix app — falls through to the Gemini bridge / command map / Unknown, preserving the pre-existing `ai` edge behavior.
|
||||
- Prefix apps run with the FULL remainder as one quoted argument (`printf '%q'`); a template wanting the text mid-line can place it anywhere since it is appended after the command.
|
||||
- Because matching requires `<word><whitespace>`, two distinct map words can never both match one message — "first match wins" only matters for hand-edited files with unusual words; kept as documented file-order semantics.
|
||||
- Text-prefix entries are NOT pushed to the bot command menu (`build_commands_json` reads only `MAP_FILE`) — they are text triggers, not `/`-commands.
|
||||
- The old `prefix <word>` = set `TELEGRAM_AI_PREFIX` behavior is intentionally replaced; the word is set via `pos config telegram` (field already exists in the `telegram` scope) and displayed by bare `prefix` + `--status`.
|
||||
|
||||
**Status: COMPLETE** (pre-commit). Next: commit + push.
|
||||
@@ -1,102 +0,0 @@
|
||||
# Builder Report — `pos ai hf` (Hugging Face Model Downloader)
|
||||
|
||||
**Date:** 2026-09-04
|
||||
**Status:** DONE
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
Implemented `bin/pos-ai-hf` per the architecture report: a bash-only (curl + jq) Hugging Face model downloader with `search`/`download`/`list`/`remove` subcommands, `ai`-scope config (`HF_TOKEN`, `HF_DOWNLOAD_DIR`), auth headers, HTTP 429 retry, `curl -C -` resume, `.hf-meta` bookkeeping, and emoji output. Test harness (46 cases) green; all gates pass.
|
||||
|
||||
| Item | Status |
|
||||
|------|--------|
|
||||
| `bin/pos-ai-hf` created | [DONE] |
|
||||
| Syntax check | [DONE] |
|
||||
| Test harness (46/46) | [DONE] |
|
||||
| `make gen && make check` | [DONE] |
|
||||
| `make lint` (0 FAIL, 0 WARN) | [DONE] |
|
||||
| Doc updates (POS.md, AGENT_TODO.md) | [DONE] |
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create `bin/pos-ai-hf` from template + implement full tool
|
||||
|
||||
Implemented the full tool: config loader (env-var precedence over `ai.env`), `hf_api`/`hf_repo_files`/`hf_search` helpers, `cmd_search`/`cmd_download`/`cmd_list`/`cmd_remove`, usage/help, error handling, `.hf-meta` writing. Deps guards (`curl`/`jq`) before `--help`. `# POS: ai hf —`, `# POS_FLAGS`, `# POS_DEPS`, `# POS_CONFIG`, `# POS_EXAMPLES` headers present. Chmod 100755.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 2: Syntax check (`bash -n bin/pos-ai-hf`)
|
||||
|
||||
`bash -n bin/pos-ai-hf` → Syntax OK.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 3: Test harness (`/tmp/opencode/hf-test/run-tests.sh`)
|
||||
|
||||
Stub-PATH harness: stub `curl` routes by URL pattern to fixtures; 46 numbered tests covering argument parsing (missing/unknown/invalid), download (single file via `-o`, whole repo, `--gguf`, `--branch`, `--output`), search, list, remove, config/token handling, and output format (emoji, size header/table formats).
|
||||
|
||||
Fixed two harness issues along the way: Test 22 size grep double-match (corrected regex), Test 40 token warning (empty config file + unset `HF_TOKEN` when the tool re-reads the config after `unset`).
|
||||
|
||||
**Result:** 46/46 green, exit 0.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 4: Gates — `make gen && make check && make lint`
|
||||
|
||||
- `make gen` → `gen-docs: write OK`
|
||||
- `make check` → `check-sync: OK`
|
||||
- `make lint` → **0 FAIL, 0 WARN**
|
||||
- Initially hit 1 WARN (`pos-ai-hf: file not referenced in DOC/POS.md`); added the `bin/pos-ai-hf` reference in the `### ai` `**File:**` line — lint green after.
|
||||
|
||||
Verified generated output: docmap/tree/filetable updated in AGENT_Context, `completions/pos.bash` gained `ai-hf` flags and `ai` subcmd list, dispatch (`pos ai --help`, `pos help ai hf`, `pos ai hf --help`) all resolve.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 5: Doc updates (POS.md, AGENT_TODO.md)
|
||||
|
||||
- `DOC/POS.md`: added `bin/pos-ai-hf` to the `### ai` `**File:**` line, and a dedicated `pos ai hf` command table + auth/rate-limit/resume detail block under the ai section.
|
||||
- `AGENT_TODO.md`: added a **Done** entry (newest-first, dated 2026-09-04) describing the tool, seam, config scope, verification.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Verification Summary
|
||||
|
||||
- `bash -n bin/pos-ai-hf` — OK
|
||||
- `/tmp/opencode/hf-test/run-tests.sh` — 46/46 passed, exit 0
|
||||
- `make gen` — write OK
|
||||
- `make check` — OK
|
||||
- `make lint` — 0 FAIL, 0 WARN
|
||||
- `git status --short` — only intended changes: `AGENT_TODO.md`, generated `DOC/AGENT_Context_Project.md`, `DOC/POS.md`, `completions/pos.bash`, new `bin/pos-ai-hf`, new report file.
|
||||
|
||||
## Remaining Risks / Follow-up
|
||||
|
||||
- No live-network smoke against the real `huggingface.co` was run (stub-based tests only); real end-to-end (auth 401/403, 429, 404, jq parse error paths) should be exercised manually on a machine with network access before first real use.
|
||||
- `.hf-meta` uses a plain JSON object — no schema version field; acceptable for this scope.
|
||||
|
||||
## Handoff
|
||||
|
||||
Status: IMPLEMENTED
|
||||
|
||||
Approved scope:
|
||||
`bin/pos-ai-hf` (from `templates/pos-tool.sh`), `DOC/POS.md`, `AGENT_TODO.md`, test harness, report. No changes to `bin/pos-ai`, `bin/pos`, `preinstall.sh`, `lib/common.sh`, or INTERACTIVE_CMDS.
|
||||
|
||||
Changes made:
|
||||
New tool `bin/pos-ai-hf`; POS.md ai row + hf detail block; AGENT_TODO Done entry; generated docs/completions refreshed via `make gen`.
|
||||
|
||||
Files changed:
|
||||
`bin/pos-ai-hf` (new), `DOC/POS.md`, `AGENT_TODO.md`, `DOC/AGENT_Context_Project.md` (generated), `completions/pos.bash` (generated), `AgentsReport/builder/2026-09-04_hf-downloader-implementation.md` (new).
|
||||
|
||||
Verification performed:
|
||||
`bash -n`, 46/46 stub tests, `make gen && make check && make lint` (0 FAIL, 0 WARN).
|
||||
|
||||
Scope compliance:
|
||||
In-scope changes only; no out-of-scope changes.
|
||||
|
||||
Recommended next agent:
|
||||
Reviewer
|
||||
|
||||
Reason:
|
||||
Implementation complete and gates green; needs independent adversarial review before acceptance.
|
||||
@@ -1,37 +0,0 @@
|
||||
# Builder Report: `pos ai server` — llama.cpp Inference Server
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status:** IMPLEMENTED
|
||||
- **Files created:** `bin/pos-ai-server`, `lib/ai-providers/llamacpp.sh`
|
||||
- **Files modified:** `bin/pos-ai` (4 case additions + POS_CONFIG header), `config/ai.env` (LLAMACPP_* docs), `DOC/POS.md` (ai server docs)
|
||||
- **Test harness:** `/tmp/opencode/llamacpp-test/run-tests.sh` — 87/87 passing
|
||||
- **Verification:** `make gen && make check && make lint` = 0 FAIL, 0 WARN
|
||||
|
||||
## Step 1: Create `bin/pos-ai-server`
|
||||
[DONE] — chmod 100755, syntax check passed, all conventions followed (set -euo pipefail, deps guards before --help, POS/POS_SUBCMDS/POS_FLAGS/POS_DEPS headers, common.sh source chain, err/log/warn/ok helpers, config seam guards)
|
||||
|
||||
## Step 2: Create `lib/ai-providers/llamacpp.sh`
|
||||
[DONE] — PROVIDER_CONFIG header present, 4-function contract (provider_name, provider_default_model, provider_generate, provider_models_list), syntax check passed
|
||||
|
||||
## Step 3: Modify `bin/pos-ai` — 4 case additions
|
||||
[DONE] — resolve_key: llamacpp returns 0 (no key needed), resolve_model: reads LLAMACPP_MODEL, cmd_providers: always "configured", require_key: llamacpp case (no key needed). POS_CONFIG header updated with LLAMACPP_* keys. Syntax check passed.
|
||||
|
||||
## Step 4: Modify `config/ai.env` — add LLAMACPP_* docs
|
||||
[DONE] — All 6 LLAMACPP_* config keys documented with defaults
|
||||
|
||||
## Step 5: Add POS.md documentation
|
||||
[DONE] — ai section updated with file references, server command table, flags, and config keys
|
||||
|
||||
## Step 6: Create test harness
|
||||
[DONE] — `/tmp/opencode/llamacpp-test/run-tests.sh` with 45 test groups (87 individual assertions), 0 failures. Tests cover: help/dispatch, error handling, start (config/arg/fallback), systemd unit generation (all fields), GPU detection (CPU/CUDA), model resolution (config/arg/relative/TTYPick), stop/status/models/logs, dry-run mode, flag overrides, POS headers, deps guards, pos-ai modifications.
|
||||
|
||||
## Step 7: Verification
|
||||
[DONE] — All gates pass:
|
||||
- `bash -n bin/pos-ai-server` — OK
|
||||
- `bash -n lib/ai-providers/llamacpp.sh` — OK
|
||||
- `bash -n bin/pos-ai` — OK
|
||||
- Test suite: 87/87 passed
|
||||
- `make gen` — OK
|
||||
- `make check` — OK
|
||||
- `make lint` — 0 FAIL, 0 WARN
|
||||
@@ -1,185 +0,0 @@
|
||||
# Builder Report — `pos media grab` Implementation
|
||||
|
||||
**Date:** 2026-09-04
|
||||
**Status:** IMPLEMENTED
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status:** IMPLEMENTED
|
||||
- **Files created:** `bin/pos-media-grab` (chmod 100755)
|
||||
- **Files modified:** `bin/pos-communication-telegram-listener`, `DOC/POS.md`, `AGENT_TODO.md`
|
||||
- **Test suite:** `/tmp/opencode/media-grab-test/run-tests.sh` — **28 test cases / 70 assertions, 0 failed**
|
||||
- **Gates:** `bash -n` on both files ✅; `make gen && make check` green ✅; `make lint` 0 FAIL / 0 WARN ✅
|
||||
- **Report path:** `./AgentsReport/builder/2026-09-04_media-grab-implementation.md`
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create `bin/pos-media-grab`
|
||||
|
||||
Created a new pos tool at `bin/pos-media-grab` (chmod 100755, `set -euo pipefail`).
|
||||
|
||||
**Headers (right after shebang):**
|
||||
- `# POS: media grab — Auto-download URL as audio or video (classify + route)`
|
||||
- `# POS_FLAGS: --audio --video --best --worst --output --no-playlist --cookies --dry-run`
|
||||
- `# POS_CONFIG: grab | grab.env | GRAB_DEFAULT=:Default mode for unknown domains (video or audio, default video)`
|
||||
|
||||
**Implemented functions/behavior:**
|
||||
- `classify_url()` — domain-based regex classification (full Decision 2 table)
|
||||
- `usage()` — full help with examples
|
||||
- Main arg parsing loop (`--audio`, `--video`, `--best`, `--worst`, `--output`, `--no-playlist`, `--cookies`, `--dry-run`, `-h|--help`)
|
||||
- Single positional URL; non-HTTP → `err "not a valid URL"`
|
||||
- Routes to `pos media mp3` (audio) or `pos media mp4` (video)
|
||||
- mp4 route adds `--best` by default, `--worst` if user passes `--worst`
|
||||
- All flag values forwarded to mp3/mp4 as-is
|
||||
- After download: `yt-dlp --print title --print duration_string` for metadata, `stat --printf='%s'` for file size, formatted summary
|
||||
- `--dry-run` prints the command, doesn't execute
|
||||
- Captures stderr from delegated command, prints `❌ Download failed: <summary>`
|
||||
- Config: `load_grab_config()` reads `GRAB_DEFAULT` from `~/.config/linux_post_install/grab.env`
|
||||
- No deps guards (delegates to mp3/mp4 which handle their own)
|
||||
- Sources `lib/common.sh` via the standard fallback chain
|
||||
|
||||
[`bin/pos-media-grab`](file:///home/unknown/projects/Linux_post_install/bin/pos-media-grab)
|
||||
[DONE]
|
||||
|
||||
## Step 2: Modify `bin/pos-communication-telegram-listener`
|
||||
|
||||
**Change 1 — `url_detect` function** (added before `handle_message`, after `strip_markdown`):
|
||||
- Extracts first http(s) URL, strips trailing punctuation (`.,\)!?:;` and `>*`)
|
||||
- Returns 0 + prints URL on success, 1 if no URL found
|
||||
|
||||
**Change 2 — URL routing step** in `handle_message` (after prefix map block, before AI bridge check):
|
||||
```bash
|
||||
# URL detect: bare HTTP(S) URLs → pos media grab
|
||||
local grab_url
|
||||
if grab_url="$(url_detect "$text")"; then
|
||||
log "grab: $grab_url"
|
||||
run_and_reply "pos media grab --best \"$grab_url\"" "$msg_id" 600
|
||||
return
|
||||
fi
|
||||
```
|
||||
|
||||
Routing chain is now: `/help|/start` → prefix map → **URL detect** → AI bridge → command map → Unknown.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 3: Create test harness + run tests
|
||||
|
||||
Created `/tmp/opencode/media-grab-test/` with:
|
||||
- `run-tests.sh` — main test runner (28 test cases / 70 assertions)
|
||||
- `stubs/pos` — fake dispatcher
|
||||
- `stubs/pos-media-mp3`, `stubs/pos-media-mp4` — fake download tools that log args + create dummy files (mp3 validates `--cookies` like the real tool)
|
||||
- `stubs/yt-dlp` — fake yt-dlp that prints metadata and creates dummy downloads
|
||||
- `stubs/stat` — fake stat returning fixed sizes
|
||||
|
||||
**Test results: 28/28 cases, 70/70 assertions, 0 failed.**
|
||||
|
||||
| # | Test | Result |
|
||||
|---|------|--------|
|
||||
| 1 | YouTube Music URL → mp3 | ✅ |
|
||||
| 2 | YouTube video URL → mp4 --best | ✅ |
|
||||
| 3 | YouTube youtu.be short → mp4 --best | ✅ |
|
||||
| 4 | YouTube shorts → mp4 --best | ✅ |
|
||||
| 5 | SoundCloud → mp3 | ✅ |
|
||||
| 6 | Bandcamp → mp3 | ✅ |
|
||||
| 7 | Vimeo → mp4 --best | ✅ |
|
||||
| 8 | Unknown domain → mp4 --best (default) | ✅ |
|
||||
| 9 | `--audio` override → mp3 | ✅ |
|
||||
| 10 | `--video` override → mp4 | ✅ |
|
||||
| 11 | `--worst` → mp4 --worst | ✅ |
|
||||
| 12 | `--best` explicit → mp4 --best | ✅ |
|
||||
| 13 | `--dry-run` prints command, no download | ✅ |
|
||||
| 14 | No URL → usage (exit 0) | ✅ |
|
||||
| 15 | `--help` → usage | ✅ |
|
||||
| 16 | Invalid URL → error | ✅ |
|
||||
| 17 | `GRAB_DEFAULT=audio` (config file) → mp3 | ✅ |
|
||||
| 18 | `--output` override → passed to mp3 | ✅ |
|
||||
| 19 | HTTP (not HTTPS) URL → mp4 | ✅ |
|
||||
| 20 | `--no-playlist` → passed to mp4 | ✅ |
|
||||
| 21 | `--cookies` missing file → error | ✅ |
|
||||
| 22 | Unknown option → error | ✅ |
|
||||
| 23 | FTP URL rejected → error | ✅ |
|
||||
| 24 | `--audio` + `--video` → error | ✅ |
|
||||
| 25 | `--best` + `--worst` → error | ✅ |
|
||||
| 26 | Output contains 🎬 + metadata | ✅ |
|
||||
| 27 | Output contains 📁 file info | ✅ |
|
||||
| 28 | Twitch URL → mp4 --best | ✅ |
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 4: Gates — syntax, gen, check, lint
|
||||
|
||||
- `bash -n bin/pos-media-grab` ✅
|
||||
- `bash -n bin/pos-communication-telegram-listener` ✅
|
||||
- `make gen && make check` — green ✅
|
||||
- `make lint` — **0 FAIL, 0 WARN** ✅
|
||||
|
||||
`make gen` regenerated `DOC/AGENT_Context_Project.md` (tree, dispatch, filetable: added `pos-media-grab`, bumped listener 782→805 and completions 308→309) and `completions/pos.bash` (added `media-grab` flags + `grab` config scope).
|
||||
|
||||
**Note on `DOC/POS.md`:** the lint gate requires every `bin/pos-*` tool to be referenced in `DOC/POS.md`. The Architect's report listed POS.md as a doc to update, so I added a `pos media grab` row to the media table (hand-maintained, lint-required).
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 5: Update `AGENT_TODO.md`
|
||||
|
||||
Added the completed `pos media grab` entry to the **Done** section (newest-first, dated 2026-09-04). No matching "Next" item existed (the only Next item — wire alerting into more tools — is unrelated to this task).
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Deviations from Architect design
|
||||
|
||||
| Area | Architect spec | Builder change | Justification |
|
||||
|------|---------------|----------------|---------------|
|
||||
| POS.md | Listed as "Files to Update (Docs)" | Added `pos media grab` row to media table | Required by `make lint` gate (lint WARNS on tools not referenced in POS.md) |
|
||||
| No-URL usage | brief says "No positional → usage (exit 1)" | usage() exits 0 (project convention) | All existing tools' `usage()` calls `exit 0` — the exit-1 spec conflicts with the established project pattern |
|
||||
|
||||
## Remaining uncertainty
|
||||
|
||||
- The `find`-based file-location logic (for summary) picks the most recent matching file in the output dir — in a rare scenario with many recent files it could select a stale match, but for the primary single-download use case this is reliable.
|
||||
- `--cookies` existence validation is delegated to mp3/mp4 (which validate it) — grab forwards as-is per spec. Test 21 verifies the failure path through the delegated tool.
|
||||
|
||||
## Recommended next agent
|
||||
|
||||
Reviewer
|
||||
|
||||
Reason: Implementation is complete and needs independent adversarial review before acceptance.
|
||||
|
||||
---
|
||||
|
||||
Status: IMPLEMENTED
|
||||
|
||||
**Approved scope:** `bin/pos-media-grab` (new tool), listener modification (add `url_detect` + URL routing step), doc updates (POS.md, AGENT_Context via `make gen`, AGENT_TODO.md). No changes to mp3/mp4.
|
||||
|
||||
**Changes made:**
|
||||
1. New `bin/pos-media-grab` — domain-based URL classifier that delegates to `pos media mp3`/`pos media mp4`, adds `--best` for non-interactive video, prints clean output. Config scope `grab`.
|
||||
2. `bin/pos-communication-telegram-listener` — added `url_detect()` + URL routing step (bare http(s) → `pos media grab --best`, 600s timeout).
|
||||
3. `DOC/POS.md` — added media table row (lint-required).
|
||||
4. `AGENT_TODO.md` — moved completed work to Done.
|
||||
|
||||
**Files changed:**
|
||||
- `bin/pos-media-grab` (new, chmod 100755)
|
||||
- `bin/pos-communication-telegram-listener` (modified)
|
||||
- `DOC/POS.md` (modified)
|
||||
- `DOC/AGENT_Context_Project.md` (regenerated via make gen)
|
||||
- `completions/pos.bash` (regenerated via make gen)
|
||||
- `AGENT_TODO.md` (modified)
|
||||
|
||||
**Verification performed:**
|
||||
- `bash -n` on both files — OK
|
||||
- Test suite `/tmp/opencode/media-grab-test/run-tests.sh` — 28 cases / 70 assertions / 0 failed
|
||||
- `make gen && make check` — green
|
||||
- `make lint` — 0 FAIL, 0 WARN
|
||||
|
||||
**Project validation:** All required gates pass.
|
||||
|
||||
**Scope compliance:** In-scope changes only. No changes to mp3/mp4, no feature flags, no scope expansion.
|
||||
|
||||
**Remaining risks:**
|
||||
- The `find`-based file-location for the summary picks the most recent matching file in the output dir; in rare multi-download scenarios it could select a stale match (reliable for the primary single-download use case).
|
||||
- `--cookies` file existence is validated by the delegated mp3/mp4 (not grab) — per spec, grab forwards as-is.
|
||||
- The installed `/usr/local/bin/pos-config` won't know the `grab` scope until `install.sh` refresh — this is a normal installed-vs-repo staleness, not a code bug.
|
||||
|
||||
**Changes made by Builder:** In-scope implementation only.
|
||||
@@ -1,120 +0,0 @@
|
||||
# Builder Report — `pos-ai-hf` GGUF/jq bug fix (2026-09-04)
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status:** IMPLEMENTED
|
||||
- **Scope:** fix `jq: endswith() requires string inputs` crash in `pos ai hf download … --gguf` per Detective spec (AgentsReport/detective/2026-09-04_pos-ai-hf-gguf-jq-bug.md, Step 4); Changes 1-3 implemented exactly; harness at `/tmp/opencode/hf-test2/`; no commit/push.
|
||||
- **Files changed (repo):** `bin/pos-ai-hf` (+17/-4); `DOC/AGENT_Context_Project.md` (2-line auto-gen filetable row, only the `pos-ai-hf` line count 495→506 — `make gen` output; no GEN block content changed).
|
||||
- **Verification:** `bash -n` OK; harness 9/9 PASS; `make gen/check/lint` green (`0 FAIL, 0 WARN`); live API: normalize→13 records (0 nulls, 0 bad sizes), --gguf→exactly 10, README/LICENSE/.gitattributes excluded; real tool path: single-file `LICENSE` download OK (7.2 KB, `.hf-meta` correct); mode-aware empty messages verified live (exit 1 unchanged).
|
||||
- **NOT committed.** Working-tree changes: `bin/pos-ai-hf`, `DOC/AGENT_Context_Project.md` (+1/-1 line-count row), untracked Detective report (pre-existing).
|
||||
|
||||
## Step 1: Read Detective report + confirm scope — [DONE]
|
||||
|
||||
Read full report (fix spec Step 4, edge cases Step 5, harness spec Step 6, verification Step 7). Confirmed fixtures exist: `/tmp/opencode/qwen-tree.json` (13 files, no rfilename), `/tmp/opencode/sd-tree.json` (8 files + 7 dirs).
|
||||
|
||||
## Step 2: Implement Change 1 — normalize tree response in `hf_repo_files()` — [DONE]
|
||||
|
||||
`bin/pos-ai-hf:201-207` — primary `/tree` path now pipes through the normalize jq instead of echoing raw:
|
||||
|
||||
```bash
|
||||
# Tree API returns {type,path,size,oid[,lfs]} per entry — normalize to the
|
||||
# {rfilename,size} shape the rest of the pipeline expects (same as fallback).
|
||||
# Skip "directory" entries: they have no resolvable file URL.
|
||||
printf '%s' "$result" | jq '[.[] | select(.type == "file") | {rfilename: .path, size: (.size // 0)}]'
|
||||
```
|
||||
|
||||
Fallback sibling path (line ~213) untouched — already emits `{rfilename, size}`.
|
||||
|
||||
## Step 3: Implement Change 2 — defense-in-depth guard in `--gguf` filter — [DONE]
|
||||
|
||||
`bin/pos-ai-hf:339`:
|
||||
|
||||
```bash
|
||||
filtered_files="$(printf '%s' "$files_json" | jq -c '[.[] | select((.rfilename | type) == "string" and (.rfilename | endswith(".gguf")))]')"
|
||||
```
|
||||
|
||||
## Step 4: Implement Change 3 — mode-aware empty results — [DONE]
|
||||
|
||||
`bin/pos-ai-hf:347-356` — replaced `[ "$file_count" -gt 0 ] || err "No files to download"` with `if [ "$file_count" -eq 0 ]` branch:
|
||||
- single-file mode: `err "File not found: $filename in $repo_id (branch: ${branch})"`
|
||||
- `--gguf` mode: `err "No .gguf files found in $repo_id${branch:+ (branch: $branch)} — try without --gguf"`
|
||||
- generic: `err "No files to download"` (unchanged text)
|
||||
|
||||
Exit semantics unchanged (same `err` path, exit 1).
|
||||
|
||||
## Step 5: Build harness `/tmp/opencode/hf-test2/` — [DONE]
|
||||
|
||||
- `fixtures/tree-files.json` = copy of `/tmp/opencode/qwen-tree.json` (13 files, rfilename ABSENT, 10 .gguf)
|
||||
- `fixtures/tree-with-dirs.json` = copy of `/tmp/opencode/sd-tree.json` (8 files + 7 `type:"directory"`)
|
||||
- `fixtures/tree-empty.json` = `[]`
|
||||
- `fixtures/tree-nogguf.json` = `[{"type":"file","path":"README.md","size":100}]`
|
||||
- `run-tests.sh`: 9 assertions per report Step 6 (t_tree_normalize, t_gguf_on_normalized, t_single_file, t_all_files_passthrough, t_empty, t_nogguf, t_defense_guard, t_dirs_excluded, t_code_sync). Deliberately does NOT `source` bin/pos-ai-hf (top-level dispatch executes; no-args → usage → exit 0). NORM/GGUF_FILTER/FN_FILTER duplicated verbatim; `grep -F` drift-guards catch divergence from the file.
|
||||
|
||||
## Step 6: Verification budget — [DONE]
|
||||
|
||||
1. `bash -n bin/pos-ai-hf` → OK
|
||||
2. `bash /tmp/opencode/hf-test2/run-tests.sh` → **9 passed, 0 failed** (output captured in Step 5 run)
|
||||
3. `make gen && make check && make lint` → gen OK, check-sync OK, lint **0 FAIL, 0 WARN**; regenerated: `DOC/AGENT_Context_Project.md` line-count row only (495→506); no GEN:START/END block changes
|
||||
4. LIVE validation (no download): fetched `https://huggingface.co/api/models/Qwen/Qwen2.5-3B-Instruct-GGUF/tree/main`; normalize jq → `{count:13, nulls:0, badsizes:0}`; --gguf filter → length 10; paths = 10 `qwen2.5-3b-instruct-*.gguf` (fp16×2 + q2_k..q8_0); README/LICENSE/.gitattributes excluded; pre-fix crash `endswith() requires string inputs` reproduced for contrast on raw tree
|
||||
5. Real tool-path proof (no multi-GB download):
|
||||
- `pos ai hf download Qwen/Qwen2.5-3B-Instruct-GGUF LICENSE --output /tmp/hf-small` → exit 0, `📥 Downloaded: Qwen/Qwen2.5-3B-Instruct-GGUF/LICENSE (7.2 KB)`, `.hf-meta` `"files": ["LICENSE"]`, real 7388-byte file present. NOTE: first attempt via `bin/pos` hit the stale **installed** `/usr/local/bin/pos-ai-hf` (PATH precedence) which silently no-matched → reproduced the pre-fix bug; installed copy is now byte-identical to repo and the same command succeeds. Environment detail, not a code issue.
|
||||
- `--gguf` no-gguf repo: `… distilbert/distilbert-base-uncased --gguf` → `ERROR: No .gguf files found in distilbert/distilbert-base-uncased (branch: main) — try without --gguf`, exit 1 (no crash, no download)
|
||||
- single-file not-found: `… totally-missing-file` → `ERROR: File not found: totally-missing-file in Qwen/Qwen2.5-3B-Instruct-GGUF (branch: main)`, exit 1
|
||||
|
||||
## Step 7: Final diff review + handoff — [DONE]
|
||||
|
||||
`git diff --stat`: `bin/pos-ai-hf | 17 ++++-----` (only the 3 sanctioned hunks), `DOC/AGENT_Context_Project.md | 2 +-` (gen line-count row). `git status`: no staged/committed changes; NOT committed or pushed.
|
||||
|
||||
## Handoff
|
||||
|
||||
Status: **IMPLEMENTED**
|
||||
|
||||
Approved scope: the 3 changes in Detective fix spec Step 4; harness at `/tmp/opencode/hf-test2/`; verification per Step 7. Only `bin/pos-ai-hf` changed in repo (+ `make gen` line-count row in DOC/AGENT_Context_Project.md).
|
||||
|
||||
Remaining risks / follow-ups (out of scope, flagged by Detective):
|
||||
- `hf_api` lacks `curl -L` → 307-redirect alias repos (e.g. runwayml) still fail (pre-existing).
|
||||
- Tree endpoint non-recursive → subdirectory files not listed (pre-existing semantics).
|
||||
- `/usr/local/bin` installed copy was stale at run time (auto-synced later); real deployers should reinstall.
|
||||
|
||||
Recommended next agent: **Tester** — the /tmp/opencode/hf-test2 harness is fixture-based and ready for adoption into the repo test suite if the project chooses (decision: Architect); otherwise Reviewer for acceptance of the 3-hunk fix.
|
||||
|
||||
Changes made by Builder: as listed above; nothing else touched.
|
||||
|
||||
## Harden+verification — error-object hardening (2026-09-04, Orchestrator follow-up) — [DONE]
|
||||
|
||||
Previous implementation APPROVED. Orchestrator/Reviewer found an additional crash class: `printf '%s' '{"error":"x"}' | jq '[.[] | select(.type == "file") | …]'` → `jq: error: Cannot index string with string "type"` rc 5 — `.[]` on an object iterates its VALUES; the string `"x"` then gets indexed with `.type`. Verified present on BOTH normalize paths pre-change (primary: `Cannot index string…`; fallback: `Cannot iterate over null (null)` on `.siblings`).
|
||||
|
||||
### Change 4 — object-safe normalize (primary), object-safe fallback (new)
|
||||
|
||||
`bin/pos-ai-hf:205` (primary, now object-guarded; jq `and` short-circuits so `.type` is never evaluated on non-objects):
|
||||
|
||||
```bash
|
||||
printf '%s' "$result" | jq '[.[] | select(type == "object" and .type == "file") | {rfilename: .path, size: (.size // 0)}]'
|
||||
```
|
||||
|
||||
`bin/pos-ai-hf:213` (fallback, previously unguarded — same crash class; now `[]?` suppresses null iteration + `select(type == "object")` skips junk elements + `(rfilename // "")` keeps the shape contract string-safe for nulls):
|
||||
|
||||
```bash
|
||||
printf '%s' "$fallback" | jq '[.siblings[]? | select(type == "object") | {rfilename: (.rfilename // ""), size: (.size // 0)}]'
|
||||
```
|
||||
|
||||
- On `{"error":"x"}`: primary → `[]` rc 0; fallback → `[]` rc 0 (both were rc 5 before).
|
||||
- On real fixtures: 13-file tree → 13 records, 0 nulls (identical to pre-hardening); dirs fixture → 8 (dirs dropped); real metadata qwen-meta.json → 13 records, string rfilename, numeric size.
|
||||
- Pathological `{"rfilename":42}` in siblings passes `// ""` unchanged (42 is truthy → kept) — non-null non-string rfilename still possible in the fallback shape; the `--gguf` filter's `type == "string"` guard prevents the crash class there, and single-file select simply won't match. Flagged as accepted residual risk (suggested-form semantics per Orchestrator).
|
||||
- Line count unchanged (506) → `make gen` produced no further DOC change beyond the already-tracked 495→506 line-count row.
|
||||
|
||||
### Harness update
|
||||
|
||||
`/tmp/opencode/hf-test2/run-tests.sh`:
|
||||
- `NORM` updated to hardened primary form; new `FB_NORM` duplicated verbatim.
|
||||
- New fixture `fixtures/meta-siblings.json` = copy of `/tmp/opencode/qwen-meta.json` (13 siblings, real metadata shape).
|
||||
- New assertions: `t_error_object_normalize` (`{"error":"x"}` → `[]` rc 0), `t_fallback_normalize` (real metadata → 13 records, string rfilename, numeric size), `t_error_object_fallback` (`{"error":"x"}` → `[]` rc 0).
|
||||
- `t_code_sync` drift-guards updated: greps `select(type == "object" and .type == "file")` (primary) and `select(type == "object")` (fallback) in addition to the GGUF guard + fn filter.
|
||||
|
||||
### Harden verification results
|
||||
|
||||
1. `bash -n bin/pos-ai-hf` → OK
|
||||
2. `bash /tmp/opencode/hf-test2/run-tests.sh` → **12 passed, 0 failed** (was 9; +3 new assertions)
|
||||
3. `make gen && make check && make lint` → gen OK, check-sync OK, **0 FAIL, 0 WARN**; `git diff --stat`: `bin/pos-ai-hf | 19 ++++---` (4 sanctioned hunks: normalize + fallback + gguf guard + message branch + comment), `DOC/AGENT_Context_Project.md | 2 +-` (line-count row from prior gen; unchanged by this pass)
|
||||
4. LIVE (real API, no download): Qwen tree → hardened normalize `{"count":13,"nulls":0}`; hardened `--gguf` filter → 10; README/LICENSE/.gitattributes excluded → `OK`
|
||||
5. Still NOT committed; only intended files modified (bin/pos-ai-hf, DOC line-count row) + untracked reports.
|
||||
@@ -1,78 +0,0 @@
|
||||
# Builder Report: ytsync channel-handle fix
|
||||
|
||||
Date: 2026-09-04
|
||||
Agent: Builder
|
||||
Status: IMPLEMENTED
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Scope:** Single-file bug fix in `bin/pos-media-ytsync` — three changes (new helper + probe canonicalization + entry filter)
|
||||
- **Files changed:** `bin/pos-media-ytsync` (+ `DOC/AGENT_Context_Project.md` line-count bump from `make gen`)
|
||||
- **Baseline:** 12 PASS / 5 FAIL on unfixed script
|
||||
- **Result:** 32 PASS / 0 FAIL; `make gen`/`check`/`lint` all green; live dry-run shows 151 real videos; NOT committed
|
||||
- **Status: IMPLEMENTED**
|
||||
|
||||
## Step 1: Implement `canonical_channel_url()` helper
|
||||
|
||||
Insert after `classify_url()` (after line 176), before `sanitize_component()`.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 2: Wire canonicalization into `run_probe()`
|
||||
|
||||
Add `url="$(canonical_channel_url "$url")"` after `local url="$1"` in `run_probe()`.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 3: Add entry filter in `collect_entries()`
|
||||
|
||||
(a) Filter `.entries[]` to watchable URLs only
|
||||
(b) Guard single-object fallback with `_type == "video"` check
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 4: Syntax check
|
||||
|
||||
`bash -n bin/pos-media-ytsync`
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 5: Test harness — all assertions pass
|
||||
|
||||
Baseline was 12 PASS / 5 FAIL. After fix: **32 PASS / 0 FAIL** (Detective's 17 logical checks; harness counts 32 check calls — all green).
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 6: Gates — make gen && make check && make lint
|
||||
|
||||
`make gen` OK (regenerated `DOC/AGENT_Context_Project.md` — bumped hand-maintained line-count row for `bin/pos-media-ytsync` 1191 → 1213). `make check` OK. `make lint` → `0 FAIL, 0 WARN`.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 7: Live dry-run — 3Blue1Brown sync
|
||||
|
||||
Seeded `/tmp/opencode/ytsync-live/` with a copy of the real registry (bare-handle URL). Ran:
|
||||
`YTSYNC_STATE_DIR=/tmp/opencode/ytsync-live YTSYNC_VIDEOS_DIR=/tmp/opencode/ytsync-live/vids bin/pos-media-ytsync sync --dry-run`
|
||||
|
||||
Output (exact match to expected):
|
||||
```
|
||||
Source : https://www.youtube.com/@3blue1brown ← stored URL unchanged (no migration)
|
||||
Resolved : 3Blue1Brown (channel · 151 videos)
|
||||
New : 151 would be downloaded (0 already present)
|
||||
But what is cross-entropy? | Compression is Intelligence Part 2.mp4
|
||||
Reinventing Entropy | Compression is Intelligence Part 1.mp4
|
||||
...
|
||||
… 146 more
|
||||
```
|
||||
Real video titles, not Videos/Live/Shorts tabs. Wrote nothing (archive empty, vids empty, registry unchanged).
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 8: One real download proof
|
||||
|
||||
Downloaded exactly one entry (`GlYgs6v2YfU` = "But what is cross-entropy?") standalone via
|
||||
`yt-dlp -o /tmp/opencode/ytsync-live/test.%(ext)s https://www.youtube.com/watch?v=GlYgs6v2YfU --no-playlist`.
|
||||
|
||||
yt-dlp resolved the video (not "[youtube] <id>: This video is unavailable"), pulled metadata, and began streaming (~154 MB / 31% of a 471 MiB file before the 180s tool timeout). Partial file cleaned up. This proves the dry-run probe's entry ids are valid and fetchable.
|
||||
|
||||
[DONE]
|
||||
@@ -1,91 +0,0 @@
|
||||
# Enhanced POS AI Tools Implementation Report
|
||||
|
||||
## TL;DR
|
||||
|
||||
Successfully implemented enhanced POS AI tools with the following key features:
|
||||
|
||||
1. **Enhanced `pos ai hf`**:
|
||||
- Added `info` and `files` commands
|
||||
- Enhanced download capabilities with include/exclude patterns
|
||||
- Added revision support (`--revision`)
|
||||
- Improved progress reporting
|
||||
- Added cache management framework
|
||||
|
||||
2. **Enhanced `pos ai server`**:
|
||||
- Added detailed GPU configuration options (`--gpu-layers`, `--gpu-threads`, `--tensor-split`)
|
||||
- Added memory context controls (`--mmap`, `--mlock`, `--kv-cache`)
|
||||
- Added performance tuning options (`--batch-size`, `--ubatch-size`)
|
||||
- Added sampling parameters (`--temperature`, `--top-k`, `--top-p`, `--repetition-penalty`)
|
||||
- Added server configuration options (`--metrics`, `--health`, `--slots`)
|
||||
- Implemented version awareness
|
||||
|
||||
All changes maintain backward compatibility and follow existing code conventions.
|
||||
|
||||
## Step 1: Enhanced `pos ai hf` Implementation
|
||||
|
||||
### Added New Commands
|
||||
- **Info Command**: `pos ai hf info <repo-id>` - Shows repository metadata including downloads, likes, tags, description, author, and creation dates
|
||||
- **Files Command**: `pos ai hf files <repo-id>` - Lists all repository files with sizes and metadata
|
||||
- **Cache Command**: Basic framework for cache management
|
||||
|
||||
### Enhanced Download Capabilities
|
||||
- **Include/Exclude Patterns**: Added `--include` and `--exclude` flags with glob support for filtering files during download
|
||||
- **Revision Support**: Added `--revision` flag for targeting specific commits, tags, or branches
|
||||
- **Improved Progress Reporting**: Enhanced download progress with better feedback and error handling
|
||||
- **Pattern Filtering**: Supports filtering by file patterns during download
|
||||
|
||||
## Step 2: Enhanced `pos ai server` Implementation
|
||||
|
||||
### GPU Configuration
|
||||
- **Detailed GPU Support**: Added `--gpu-layers`, `--gpu-threads`, and `--tensor-split` for advanced GPU offloading
|
||||
- **Device Selection**: Improved GPU detection and automatic configuration
|
||||
|
||||
### Memory and Context Management
|
||||
- **Memory Allocation**: Added `--mmap` and `--mlock` for memory mapping and locking
|
||||
- **KV Cache Configuration**: Added `--kv-cache` for custom KV cache sizing
|
||||
- **Context Size Control**: Enhanced `--ctx-size` control with better validation
|
||||
|
||||
### Performance Tuning
|
||||
- **Batch Size Configuration**: Added `--batch-size` and `--ubatch-size` for processing configuration
|
||||
- **Continuous Batching**: Support for batch processing options
|
||||
|
||||
### Sampling Controls
|
||||
- **Advanced Sampling**: Added `--temperature`, `--top-k`, `--top-p`, and `--repetition-penalty` for improved sampling behavior
|
||||
|
||||
### Server Configuration
|
||||
- **Endpoint Configuration**: Added `--metrics`, `--health`, and `--slots` for enhanced server configuration
|
||||
- **Version Awareness**: Added `detect_llama_version()` and `validate_server_features()` functions for version detection and feature validation
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Files Modified
|
||||
1. `bin/pos-ai-hf` - Enhanced with new commands and download capabilities
|
||||
2. `bin/pos-ai-server` - Enhanced with new GPU, memory, and performance options
|
||||
|
||||
### Backward Compatibility
|
||||
- All existing commands and flags continue to work exactly as before
|
||||
- New flags are optional and don't affect existing workflows
|
||||
- Default behavior unchanged
|
||||
- Configuration files remain compatible
|
||||
|
||||
### Code Quality
|
||||
- Follows existing project conventions and patterns
|
||||
- Consistent error handling and messaging
|
||||
- Proper usage documentation with examples
|
||||
- Modular code structure with clear separation of concerns
|
||||
- Comprehensive help text with examples
|
||||
|
||||
## Verification
|
||||
|
||||
The implementation has been tested to ensure:
|
||||
- All existing functionality remains intact
|
||||
- New commands properly parse arguments and display usage information
|
||||
- Help text displays correctly with updated examples
|
||||
- Error messages are descriptive and helpful
|
||||
- Scripts are executable with proper shebangs
|
||||
|
||||
All checks and tests pass:
|
||||
- `make check` - OK
|
||||
- `make lint` - 0 FAIL, 0 WARN
|
||||
|
||||
[COMPLETE]
|
||||
@@ -1,226 +0,0 @@
|
||||
# Builder Report — AI cost & session-window design
|
||||
|
||||
**Date:** 2026-09-06
|
||||
**HEAD:** 8ce5479 (clean tree; HEAD unchanged — implementation not committed)
|
||||
**Design source:** `./AgentsReport/architect/2026-09-06_ai-cost-window-design.md`
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status: IMPLEMENTED** (worktree changes uncommitted)
|
||||
- **Files changed:** `bin/pos-ai`, `lib/ai-providers/openrouter.sh`, `lib/ai-providers/gemini.sh`, `DOC/POS.md`, `DOC/HOWTO.md`, `DOC/AGENT_Context_Project.md` (6 files, +18/−11; `make gen` auto-updated the `bin/pos-ai` filetable row 705→709)
|
||||
- **Smoke:** provider-body cap (openrouter + gemini, default 2048 + custom 512) all pass; session_push lazy resolution + numeric guard all pass (incl. boundary 40, `10`, non-numeric/0/negative/empty fallback)
|
||||
- **Gates:** `make gen` idempotent, `make check` OK, `make lint` 0 FAIL 0 WARN, `make test` 17/17 files + 299/299 checks, `git diff --check` clean, `bash -n` all touched scripts OK
|
||||
- **Out-of-scope:** no changes to llamacpp.sh, pos-ai-server, aliases, other tools, tests, AGENT_TODO.md
|
||||
|
||||
---
|
||||
|
||||
## Step 1: POS_CONFIG header — add AI_MAX_TOKENS and AI_SESSION_TURNS
|
||||
|
||||
Append two entries to the `@General` section of `# POS_CONFIG:` header at `bin/pos-ai:6`.
|
||||
|
||||
- `AI_MAX_TOKENS=num:Max output tokens per request (default 2048; OpenRouter/Gemini cost cap)`
|
||||
- `AI_SESSION_TURNS=num:Session message cap — 2 per exchange (default 40 = 20 exchanges; 10 = last 5)`
|
||||
|
||||
Wording matches the design report §Decision 3 exactly. `num:` type reuses the existing `LLAMACPP_CTX_SIZE=num:…` pattern in the same header.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 2: session_push lazy resolution
|
||||
|
||||
Change `session_push()` (`bin/pos-ai:285-292`) to resolve `AI_SESSION_TURNS` lazily with numeric guard.
|
||||
|
||||
```bash
|
||||
local n="${AI_SESSION_TURNS:-$MAX_SESSION_TURNS}"
|
||||
[[ "$n" =~ ^[0-9]+$ ]] && (( n >= 1 )) || n="$MAX_SESSION_TURNS"
|
||||
```
|
||||
|
||||
- `^[0-9]+$` regex + `>= 1` → fallback on: empty, non-numeric, `0`, negative.
|
||||
- Line 25 `MAX_SESSION_TURNS=40` untouched (help text `:67` still accurate).
|
||||
- jq pruning now uses `--argjson n "$n" ... .[-$n:]` (no string interpolation into the filter).
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 3: Add Config help lines for the two new vars
|
||||
|
||||
Added to the `Config:` block in `pos ai --help` (`bin/pos-ai:95-96`):
|
||||
`AI_MAX_TOKENS` + `AI_SESSION_TURNS` lines, matching the design's recommended `Config:` help documentation.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 4: openrouter.sh — add max_tokens to body
|
||||
|
||||
`lib/ai-providers/openrouter.sh:22-23`: body now includes `max_tokens:($mt|tonumber)` via `--arg mt "${AI_MAX_TOKENS:-2048}"`. Empty/unset env → jq receives the literal `2048` (the `:-2048` default is a numeric literal, so there is no empty-string `tonumber` failure path).
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 5: gemini.sh — add generationConfig.maxOutputTokens
|
||||
|
||||
`lib/ai-providers/gemini.sh:17-19`: body now includes `generationConfig: {maxOutputTokens: ($mt|tonumber)}` via the same `--arg mt "${AI_MAX_TOKENS:-2048}"`. `systemInstruction` merging (`:20-23`) unchanged and composes with the new top-level field.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 6: Doc updates
|
||||
|
||||
- `DOC/POS.md:93-95` ai.env table: two new rows after `AI_SYSTEM_PROMPT` (`AI_MAX_TOKENS` no/`2048`/"Max output tokens per request (OpenRouter/Gemini cost cap)"; `AI_SESSION_TURNS` no/`40`/"Session message cap — 2 per exchange; 10 = last 5 exchanges").
|
||||
- `DOC/POS.md:64`: parenthetical "(configurable via `AI_SESSION_TURNS`)" added after "capped at 40 turns" — recommended edit from design §Decision 4.
|
||||
- `DOC/HOWTO.md:45`: `AI_MAX_TOKENS`, `AI_SESSION_TURNS` inserted after `AI_SYSTEM_PROMPT` in the `ai.env` var list.
|
||||
- `DOC/AGENT_Context_Project.md:491`: same two vars added to the ai.env summary parenthetical.
|
||||
- Line-count rows (hand-maintained, `:595-613`): no bump needed — none of those rows' files (install.sh, lib/*, features/*) changed length. `bin/pos-ai` row lives in the GENERATED filetable and was auto-updated 705 → 709 by `make gen`.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 7: make gen x2 + make check + make lint
|
||||
|
||||
- `make gen` ran 3× total (twice at implementation, once after the test suite): each rerun is a no-op → **idempotent**.
|
||||
- Generated delta is ONLY the `bin/pos-ai` line-count row (705 → 709) in the filetable block. `completions/pos.bash` unchanged — verified `_pos_config_scopes` (`completions/pos.bash:55`) lists only scope *names* (already includes `ai`); individual keys are read live by `pos config` via `lib/config-ui.sh` from the `# POS_CONFIG:` headers (confirmed via `lib/registry.sh` `reg_scan bin` + `reg_config_keys ai` showing both new keys). The design's expectation that completions would gain the keys is a no-op in practice — no key-level completion table exists by design.
|
||||
- `make check` (check-sync.sh): **OK**
|
||||
- `make lint` (lint-conventions.sh): **0 FAIL, 0 WARN**
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 8: Smoke tests (no network — fake curl shim in /tmp/opencode/ai-cost-smoke)
|
||||
|
||||
### Provider body cap (`test-provider-body.sh`, 12 checks, exit 0)
|
||||
|
||||
Fake `curl` on PATH captures the `--data` body to a file and returns a canned 200 response + the `\n%{http_code}` line the adapters expect.
|
||||
|
||||
| Case | Body assertion | Result |
|
||||
|---|---|---|
|
||||
| openrouter, AI_MAX_TOKENS unset | `"max_tokens":2048` | OK |
|
||||
| openrouter, unset — JSON shape | model kept, messages kept | OK |
|
||||
| openrouter, unset — canned response | `hello from fake openrouter` round-trips | OK |
|
||||
| openrouter, AI_MAX_TOKENS=2048 | `"max_tokens":2048` | OK |
|
||||
| openrouter, AI_MAX_TOKENS=512 | `"max_tokens":512` | OK |
|
||||
| gemini, AI_MAX_TOKENS unset | `generationConfig.maxOutputTokens == 2048` | OK |
|
||||
| gemini, unset — role conversion | assistant→model (unchanged behavior) | OK |
|
||||
| gemini, unset — no systemInstruction when empty | absent | OK |
|
||||
| gemini, unset — canned response | `hello from fake gemini` parses + round-trips | OK |
|
||||
| gemini, AI_MAX_TOKENS=512 | `generationConfig.maxOutputTokens == 512` | OK |
|
||||
|
||||
Canned bodies (shim): openrouter `{"choices":[{"message":{"role":"assistant","content":"hello from fake openrouter"}}]}`, gemini `{"candidates":[{"content":{"parts":[{"text":"hello from fake gemini"}]}}]}`; both + `200`.
|
||||
|
||||
### session_push lazy resolution (`test-session-push.sh`, 10 checks, exit 0)
|
||||
|
||||
`session_push` brace-extracted from `bin/pos-ai` (same `extract_fn` pattern as `tests/t-menu-allow-empty.sh`; function's braces are balanced so extraction is exact), with `MAX_SESSION_TURNS=40` declared as in `bin/pos-ai:25`.
|
||||
|
||||
| Input | AI_SESSION_TURNS | Output length | Result |
|
||||
|---|---|---|---|
|
||||
| 30 msgs + 1 push | unset | 31 (no truncation under default) | OK |
|
||||
| 30 msgs + 1 push | `10` | 10 (5 exchanges) | OK |
|
||||
| 30 msgs + 1 push | `abc` (non-numeric) | 31 (fallback 40 → no truncation) | OK |
|
||||
| 39 msgs + 1 push | unset | **40** (exact default boundary) | OK |
|
||||
| 39 msgs + 1 push | `10` | 10 | OK |
|
||||
| 39 msgs + 1 push | `0` (<1) | 40 (fallback) | OK |
|
||||
| 39 msgs + 1 push | `-5` (negative) | 40 (fallback) | OK |
|
||||
| 39 msgs + 1 push | `''` (empty) | 40 (fallback) | OK |
|
||||
|
||||
Note: the brief's "30-message array → 40 default" cannot be exact (30+1=31 < 40, so nothing is trimmed); the 39+1 boundary case proves the exact default of 40.
|
||||
|
||||
### Config registry discovery
|
||||
|
||||
`reg_scan bin` + `reg_config_keys ai` via `lib/registry.sh` returns both new keys in the `ai` scope (`AI_MAX_TOKENS=num:…`, `AI_SESSION_TURNS=num:…`) — `pos config ai` will present them.
|
||||
|
||||
### Full test suite
|
||||
|
||||
`make test`: **17 files pass / 0 fail / 0 skip; 299 checks pass / 0 fail / 0 skip** (matches the 17/299 budget).
|
||||
|
||||
### Other gates
|
||||
|
||||
- `bash -n bin/pos-ai lib/ai-providers/openrouter.sh lib/ai-providers/gemini.sh` — clean
|
||||
- `git diff --check` — clean
|
||||
- `make gen` idempotent; `make check` OK; `make lint` 0 FAIL 0 WARN
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Files changed (diff summary)
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `bin/pos-ai` | POS_CONFIG header +2 keys; help Config block +2 lines; session_push lazy + guard (+4 net) |
|
||||
| `lib/ai-providers/openrouter.sh` | body gains `max_tokens` |
|
||||
| `lib/ai-providers/gemini.sh` | body gains `generationConfig.maxOutputTokens` |
|
||||
| `DOC/POS.md` | table +2 rows; line 64 parenthetical |
|
||||
| `DOC/HOWTO.md` | ai.env var list +2 |
|
||||
| `DOC/AGENT_Context_Project.md` | ai.env parenthetical +2; generated filetable row auto-updated |
|
||||
|
||||
## Scope compliance
|
||||
|
||||
- In-scope changes only (design §Decision 5 file list). No llamacpp.sh, no pos-ai-server, no alias wrappers, no other tools, no tests, no AGENT_TODO.md edits.
|
||||
- Provider call signature unchanged (`provider_generate "$model" "$messages" "$system"`).
|
||||
|
||||
## Remaining risks / follow-up
|
||||
|
||||
- `AI_MAX_TOKENS` too low truncates long `--full` answers (default 2048 mitigates; user can raise) — design's known risk.
|
||||
- Env-vs-file precedence: exported env var beats config file (`load_env_file`) — expected and documented.
|
||||
- Tester is recommended to add permanent provider-body + session-pruning regression tests (design §Testing).
|
||||
|
||||
## Handoff
|
||||
|
||||
Status: IMPLEMENTED
|
||||
Recommended next agent: **Reviewer** (independent adversarial review of the diff before acceptance; Tester follows for permanent coverage per design §Testing).
|
||||
|
||||
---
|
||||
|
||||
## Reviewer finding F1 fix (2026-09-06, after reviewer CHANGES_REQUIRED)
|
||||
|
||||
**Source:** `AgentsReport/reviewer/2026-09-06_ai-cost-window-review.md` Finding 1 (REQUIRED) + Finding 2 (NOTE, folded).
|
||||
|
||||
**Problem:** `AI_MAX_TOKENS=0`/`-5`/`010` are accepted by `pos config ai` (`num:` regex `^-?[0-9]+$`, config-ui.sh:419) and passed unguarded into jq `($mt|tonumber)` → provider 400 (0/-5) or raw jq abort (010 = invalid JSON literal; abc = tonumber error). The sibling `AI_SESSION_TURNS` guard `^[0-9]+$` also let `010` through (octal 8 ≥ 1) then aborted jq via `--argjson n "010"`.
|
||||
|
||||
**Fix (exactly per reviewer demand, ~4 lines):**
|
||||
|
||||
1. `lib/ai-providers/openrouter.sh:22-23`:
|
||||
```bash
|
||||
local mt="${AI_MAX_TOKENS:-2048}"
|
||||
[[ "$mt" =~ ^[1-9][0-9]*$ ]] || mt=2048
|
||||
```
|
||||
then `--arg mt "$mt"` (previously `--arg mt "${AI_MAX_TOKENS:-2048}"`), keep `($mt|tonumber)`.
|
||||
2. `lib/ai-providers/gemini.sh:17-18`: identical guard + use.
|
||||
3. `bin/pos-ai:290-291` session guard tightened `^[0-9]+$` → `^[1-9][0-9]*$`; the `(( n >= 1 ))` check is now redundant (regex guarantees ≥ 1) and was dropped cleanly:
|
||||
```bash
|
||||
local n="${AI_SESSION_TURNS:-$MAX_SESSION_TURNS}"
|
||||
[[ "$n" =~ ^[1-9][0-9]*$ ]] || n="$MAX_SESSION_TURNS"
|
||||
```
|
||||
`--argjson n "$n"` is safe once n is strictly decimal (`[1-9][0-9]*` is a valid JSON numeric literal — no leading zeros).
|
||||
|
||||
**Re-verification (same harness, extended cases):**
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| bash -n all three files | OK |
|
||||
| Provider smoke (extended): AI_MAX_TOKENS unset / `0` / `-5` / `010` / `abc` → body `"max_tokens":2048`; `512` → `"max_tokens":512`; openrouter + gemini both | 16 checks OK |
|
||||
| session_push smoke (extended): AI_SESSION_TURNS `0` / `010` / `-5` / `abc` → output length == default (31 for 30-array / 40 for 39-array boundary); `10` → 10 | 12 checks OK |
|
||||
| `make gen` idempotent (POS_CONFIG unchanged; no new diff) | OK |
|
||||
| `make check` | OK |
|
||||
| `make lint` | 0 FAIL, 0 WARN |
|
||||
| `make test` | 17 files / 299 checks green |
|
||||
| `git diff --check` | clean |
|
||||
|
||||
**Smoke matrix detail (provider body, openrouter + gemini):**
|
||||
|
||||
| AI_MAX_TOKENS | openrouter body | gemini body |
|
||||
|---|---|---|
|
||||
| unset | `max_tokens:2048` | `maxOutputTokens:2048` |
|
||||
| (empty) | `max_tokens:2048` | `maxOutputTokens:2048` |
|
||||
| `0` | `max_tokens:2048` (fallback) | `maxOutputTokens:2048` (fallback) |
|
||||
| `-5` | `max_tokens:2048` (fallback) | `maxOutputTokens:2048` (fallback) |
|
||||
| `010` | `max_tokens:2048` (fallback, no jq abort) | `maxOutputTokens:2048` (fallback, no jq abort) |
|
||||
| `abc` | `max_tokens:2048` (fallback, no jq abort) | `maxOutputTokens:2048` (fallback, no jq abort) |
|
||||
| `512` | `max_tokens:512` | `maxOutputTokens:512` |
|
||||
|
||||
**session_push matrix (30-msg array + 1 push):**
|
||||
|
||||
| AI_SESSION_TURNS | output length | result |
|
||||
|---|---|---|
|
||||
| unset | 31 (all, default 40 no-op) | OK |
|
||||
| `10` | 10 | OK |
|
||||
| `0` | 31 (fallback 40) | OK |
|
||||
| `010` | 31 (fallback 40 — former jq abort hole closed) | OK |
|
||||
| `-5` | 31 (fallback 40) | OK |
|
||||
| `abc` | 31 (fallback 40) | OK |
|
||||
|
||||
**Scope compliance:** only the 3 already-approved files touched in this fix round; no design change, no jq logic shape change (still `($mt|tonumber)` / `generationConfig: {maxOutputTokens: …}`), no new files (report appended).
|
||||
|
||||
[Handoff priority for F1 fix: Reviewer re-verify → Orchestrator gates → Tester permanent coverage; same recommendation as original handoff.]
|
||||
@@ -1,103 +0,0 @@
|
||||
# Builder Report — llama-server breakage fixes (F1-F7)
|
||||
|
||||
**Date:** 2026-09-06
|
||||
**Builder:** Implementation of Architect DQ1-DQ6 ratified scope F1-F7.
|
||||
**Bindings:** `AgentsReport/architect/2026-09-06_ai-server-fix-design.md` (FACT), `AgentsReport/detective/2026-09-06_ai-server-breakage.md` (FACT root-cause evidence).
|
||||
|
||||
## TL;DR
|
||||
|
||||
Status: **IMPLEMENTED** (all F1-F7)
|
||||
|
||||
Files changed:
|
||||
- `bin/pos-ai-server` (F1, F2, F3, F4, F5, F6)
|
||||
- `lib/common.sh` (F4 — new `ensure_user_bus`)
|
||||
- `bin/pos-communication-matrix-listener` (F4 — inline + call)
|
||||
- `bin/pos-network-download` (F4 — call)
|
||||
- `apps/ai/llamacpp.sh` (F7 — post-install sanity)
|
||||
- `DOC/POS.md` (minimal — `--no-unit` documented)
|
||||
- generated docs via `make gen` (`DOC/AGENT_Context_Project.md`, `completions/pos.bash`)
|
||||
|
||||
Verification: `bash -n` all touched; probes 1-8 = **63/63 PASS, rc=141 count 0**; `make gen` idempotent x2; `make check` green; `make lint` `0 FAIL, 0 WARN`.
|
||||
|
||||
Probe workspace: `/tmp/opencode/ai-probes/` (stubs, fixtures, `run-probes.sh`, `PROBES-RESULT.txt`).
|
||||
|
||||
---
|
||||
|
||||
## Step 1: F1 — `detect_llama_version` stderr capture + broadened regex
|
||||
|
||||
`bin/pos-ai-server:82-90`. Real llama.cpp prints `version: 0.4.0-dev (build 10822, commit …)` to **STDERR**; the old code captured only stdout and regexed `[0-9]+\.[0-9]+\.[0-9]+` alone.
|
||||
|
||||
- Both streams captured: `"$bin" --version 2>&1`.
|
||||
- Regex accepts `X.Y.Z` OR `build [0-9]+` OR `b[0-9]+` (old builds).
|
||||
- `"${version#build }"` strips the phrase so `build 10822` displays as `10822`.
|
||||
- Guarded: missing binary / unreadable output → `unknown` (no errexit).
|
||||
|
||||
Evidence (P1): stderr semver → `0.4.0`; `build 10822` (stderr) → `10822`; `b10822` → `b10822`; missing binary → `unknown`. [DONE]
|
||||
|
||||
## Step 2: F2 — pipe-less flag validation (no rc=141 race)
|
||||
|
||||
`bin/pos-ai-server:97-124` (`validate_requested_flags`), `:135-170` (`validate_default_flags`). Old form `printf '%s' "$help_text" | grep -qE ...` died of SIGPIPE (rc=141) when the >64KB pipe-adjacent grep exited at the first match — valid flags sporadically judged unsupported. New form: non-quiet `grep -E -- "(^|[[:space:]])${flag}([[:space:]]|=|$)" <<<"$help_text" >/dev/null` (herestring needs no pipe; non-q grep consumes the whole input; same word-boundary regex and guard shape).
|
||||
|
||||
Evidence (P2): on the 58,362-byte/732-line fixture help, 25 runs of `validate_default_flags` → all 5 defaults kept every run, `rc=141` count **0**; requested-flag path: supported `--port` rc 0, unsupported flag rc 1 with version-aware err. [DONE]
|
||||
|
||||
## Step 3: F3 — `resolve_model` directory expansion (never silently pick)
|
||||
|
||||
`bin/pos-ai-server:213-264` + helper `resolve_gguf_in_dir` (`:196-210`). DQ2 precedence: absolute file → absolute dir with exactly one `.gguf` → `$HF_DOWNLOAD_DIR/<name>/<file>.gguf` (slug/file form) → `$HF_DOWNLOAD_DIR/<name>.gguf` (flat file) → relative-as-is. A dir with multiple `.gguf`s errors listing each as `<dirname>/<file>` + "pick one"; zero `.gguf`s falls through to the today's `Model not found:` err.
|
||||
|
||||
Evidence (P3): single-gguf dir resolves; slug/file form resolves; multi-gguf errors listing both files + "pick one"; empty dir → rc 1 with today's `Model not found: empty-dir` text. [DONE]
|
||||
|
||||
## Step 4: F4 — `ensure_user_bus` + `--no-unit` escape hatch
|
||||
|
||||
`lib/common.sh` (new `ensure_user_bus`, after `confirm()`): `systemctl --user show-environment` reachability probe; on failure `err` with BOTH remediation lines (`export XDG_RUNTIME_DIR=/run/user/$(id -u)` and `sudo loginctl enable-linger $(id -un)`) + "no unit was written". Called:
|
||||
|
||||
- `bin/pos-ai-server` `cmd_start` (before DRY_RUN return; only when `--no-unit` not given). `--no-unit` flag: added to `# POS_FLAGS:`, parse case, usage; direct run via `eval "nohup $exec_cmd >'$NO_UNIT_LOG' 2>&1 &"`, pidfile/log under `${XDG_RUNTIME_DIR:-/tmp}`; `cmd_stop`/`cmd_status` pidfile-aware (`kill -0`, `^[0-9]+$` guard).
|
||||
- `bin/pos-communication-matrix-listener` — **factually does NOT source `lib/common.sh`** (inline `err`/`log`/`warn` at lines 19-21); got an inline `declare -F ensure_user_bus`-guarded duplicate + call in `enable_service` (architect's "all three source common.sh" premise was wrong; sourcing it would redefine helpers with colors and change output).
|
||||
- `bin/pos-network-download` `cmd_start` else-branch (first statement before unit write).
|
||||
|
||||
Evidence (P4): with `XDG_RUNTIME_DIR`/`DBUS_SESSION_BUS_ADDRESS` unset and a failing `systemctl` stub: pos-ai-server DRY_RUN start rc 1 with both remediation lines and **no unit written**; matrix-listener `--enable` rc 1, no unit; network-download `start` rc 1, no unit. [DONE]
|
||||
|
||||
## Step 5: F5 — `find_llamacpp` candidate narrowing
|
||||
|
||||
`bin/pos-ai-server:56-61`. Candidates narrowed to `("llama-server" "llama-server-cuda")` (dropped bare `server` and `llama.cpp/server` which had previously matched unrelated binaries). Stale-unit overwrite warn added in start; user unit state never deleted.
|
||||
|
||||
Evidence (P5): PATH containing only an unrelated binary named `server` → `find_llamacpp` returns nothing, rc 1; stub `llama-server` → rc 0, returns `llama-server`. [DONE]
|
||||
|
||||
## Step 6: F6 — port pinning preserved (no change needed, verified)
|
||||
|
||||
Verified via P6: DRY_RUN start with the b10822-shaped stub help → ExecStart contains all 5 defaults (`--host 127.0.0.1`, `--port 8088`, `--n-gpu-layers`, `--ctx-size 4096`, `--threads`) and the version resolves to `0.4.0` (not `unknown`). [DONE]
|
||||
|
||||
## Step 7: F7 — llamacpp.sh post-install sanity
|
||||
|
||||
`apps/ai/llamacpp.sh`: new `llamacpp_sanity()` (seam `LLAMACPP_BIN_DIR="${LLAMACPP_BIN_DIR:-/usr/local/bin}"`), called after install `spawn`. Verifies: `[ -e "$bin" ]` (catches dangling symlink), `command -v llama-server`, `llama-server --version >/dev/null 2>&1`, `llama-server --help >/dev/null 2>&1`; on success logs `llama.cpp sanity OK`.
|
||||
|
||||
Evidence (P7): normal fixture → rc 0 + OK log; dangling-symlink fixture → rc 1 with "dangling symlink"; missing-shared-lib fixture → rc 1 with the binary's stderr text. [DONE]
|
||||
|
||||
## Step 8: E2E user scenario
|
||||
|
||||
Evidence (P8): positive (bus env set, `systemctl` stub succeeds): `pos ai-server start Qwen-Qwen3-1.7B-GGUF` rc 0, unit written with `--port 8088`, all 4 other defaults, model path expanded from dir, **no "unknown" version anywhere in start output**. SSH-shaped (no bus env, failing stub): rc 1 with both remediation lines, **no orphan unit**. [DONE]
|
||||
|
||||
## Gates
|
||||
|
||||
| Gate | Result |
|
||||
|---|---|
|
||||
| `bash -n` on 5 touched scripts | pass (pos-ai-server, common.sh, matrix-listener, network-download, llamacpp.sh) |
|
||||
| `make gen` x2 | rc 0 both; `git diff` identical → **idempotent** |
|
||||
| `make check` | `check-sync: OK` |
|
||||
| `make lint` | `0 FAIL, 0 WARN (convention lint)` |
|
||||
| Probes 1-8 | **63 PASS / 0 FAIL / rc=141 count 0** |
|
||||
|
||||
## Scope compliance
|
||||
|
||||
- Touched exactly the in-scope files (pos-ai-server, common.sh, matrix-listener, network-download, llamacpp.sh, DOC/POS.md, gen output). `tests/`, `scripts/lint-conventions.sh`, `bin/pos-system-uninstall`, `lib/config-ui.sh` untouched.
|
||||
- Unrelated pre-existing dirty file `AgentsReport/architect/2026-09-06_stabilization-design.md` NOT modified by Builder.
|
||||
- No commits made.
|
||||
|
||||
## Residual risks / known deviations
|
||||
|
||||
1. `--no-unit` direct run uses `eval "nohup $exec_cmd … &"` — tool-generated string; only binary (validated by `command -v`) and model (validated path) tokens are interpolated, both quoted; matches the namespace's bash DNA.
|
||||
2. F1 displays `build 10822` as `10822` via `"${version#build }"` — slight deviation from the architect's literal regex output wording, required to match probe 1's `10822` expectation; behavior documented at `bin/pos-ai-server:75-81`.
|
||||
3. `matrix-listener` carries an inline duplicate of `ensure_user_bus` (guarded by `declare -F` so the shared one wins if ever sourced) — the file deliberately does not source common.sh (would change helper output). If a future task refactors matrix-listener to source common.sh, the inline copy can be deleted.
|
||||
4. E2E positive run writes/overwrites a unit under `$USER_SYSTEMD_DIR` only inside the probe workspace — no real system units touched, no processes spawned (stubs only).
|
||||
5. Runtime flags validated by grep use `>`-redirected non-quiet grep instead of `grep -q`: a 58KB fixture costs ~1-2ms per flag — negligible for a unit-write path.
|
||||
|
||||
Recommended next agent: **Reviewer** (independent adversarial review of the F1-F7 diff before acceptance).
|
||||
@@ -1,49 +0,0 @@
|
||||
# Builder Report — Alias Menu Fix (2026-09-06)
|
||||
|
||||
## TL;DR
|
||||
- **Status:** IMPLEMENTED
|
||||
- **Files changed:** `lib/menu-lib.sh`, `bin/pos-ai-alias`, `AgentsReport/builder/2026-09-06_alias-menu-fix.md`
|
||||
- **Verification:** bash -n OK; make gen ×2 (deterministic, no drift); make check OK; make lint 0 FAIL 0 WARN; functional smoke 7/7 pass.
|
||||
- **Summary:** Added `--allow-empty` flag to `menu_ask_value`, switched 2 alias call sites (353, 410), fixed step counters 352/383 → /5.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Update function index line in lib/menu-lib.sh
|
||||
Updated line 27: `menu_ask_value <label> [default]` → `menu_ask_value [--allow-empty] <label> [default]`.
|
||||
[DONE]
|
||||
|
||||
## Step 2: Add --allow-empty documentation to menu_ask_value doc block
|
||||
Added doc line (after line 349) documenting the flag: empty+no-default → rc 0 + empty value; only cancel/EOF returns rc 1.
|
||||
[DONE]
|
||||
|
||||
## Step 3: Implement --allow-empty flag in menu_ask_value body
|
||||
Implemented per architect reference (`--allow-empty` parsed as first arg, shift; empty branch: default→echo+rc 0; no default → allow_empty rc 0 echo empty / else rc 1; cancel always rc 1). Prompt label display unchanged (default suffix only when non-empty).
|
||||
[DONE]
|
||||
|
||||
## Step 4: Fix step counters and add --allow-empty to pos-ai-alias call sites
|
||||
- line 352: `step 1 4` → `step 1 5`
|
||||
- line 353: added `--allow-empty` flag (kept `|| return 0`)
|
||||
- line 383: `step 2 4` → `step 2 5`
|
||||
- line 410: added `--allow-empty` flag (kept `|| return 0`)
|
||||
[DONE]
|
||||
|
||||
## Step 5: Verification (bash -n, make gen, make check, make lint, functional smoke)
|
||||
- `bash -n lib/menu-lib.sh` → OK; `bash -n bin/pos-ai-alias` → OK.
|
||||
- `make gen` run twice → both write OK, no gen drift (git diff after gen shows ONLY the 2 source files, no generated blocks touched) → byte-identical on rerun = deterministic.
|
||||
- `make check` → check-sync: OK.
|
||||
- `make lint` → `0 FAIL, 0 WARN`.
|
||||
- Functional smoke (non-TTY harness at /tmp/opencode/menu_smoke.sh, sources lib/menu-lib.sh):
|
||||
- empty-no-default no flag → rc 1 (contract unchanged) PASS
|
||||
- empty-no-default with flag → rc 0 + empty PASS
|
||||
- empty-with-default with flag → rc 0 + default (default wins) PASS
|
||||
- empty-with-default no flag → rc 0 + default PASS
|
||||
- non-empty with flag → rc 0 + value PASS
|
||||
- non-empty no flag → rc 0 + value PASS
|
||||
- EOF (no input) with flag → rc 1 (cancel stays cancel) PASS
|
||||
- TOTAL: 7 pass, 0 fail.
|
||||
- Alias-name re-prompt reachability (static): line 353 now returns rc 0 + "" on empty Enter → line 355 `[ -z "$name" ]` fires → `warn "Alias name cannot be empty"` → `continue` → re-prompt (step 1 5 shown again); genuine cancel → rc 1 → `|| return 0` → abort to menu. Verified reachable by design; TTY-level end-to-end create-flow run is a Tester/manual check per Architect's note.
|
||||
[DONE]
|
||||
|
||||
## Handoff
|
||||
Status: IMPLEMENTED (see final message)
|
||||
[DONE]
|
||||
@@ -1,119 +0,0 @@
|
||||
# Builder report — Reviewer F1/F3/F4 fixes — 2026-09-06
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status: IMPLEMENTED**
|
||||
- **Scope:** Reviewer block-list fixes F1 (BLOCKING), F3 (REQUIRED), F4 (SUGGESTED). No D-A/decision-pending behavior touched, no `tests/`, no `bin/pos-system-uninstall`, no `bin/pos-network-download`.
|
||||
- **F1:** `--no-command-execution` added to both bridge `pos ai gemini ask` invocations (telegram `bin/pos-communication-telegram-listener:731`, matrix `bin/pos-communication-matrix-listener:466`). Parse-order validated with probes.
|
||||
- **F3:** `_prompt_run_command` in `bin/pos-ai` now checks `NO_EXEC` BEFORE the "Command detected:" print and returns 0 with zero output (probe-verified). Kept the existing `return 0` rc.
|
||||
- **F4:** Restored `$f:$num:` format in the two lint WARN messages (byte-identical to HEAD's strings; functional probe shows correct line numbers).
|
||||
- **Docs:** minimal alignment (only literal contradictions of the fixed behavior): `DOC/POS.md:103`, `DOC/howto/ai.md:261-273`, plus the `--no-command-execution` usage text in `bin/pos-ai` — "still printed" → "neither printed nor run".
|
||||
- **Gates:** bash -n OK (4 files), `make gen` rc=0 + byte-identical output across runs, `make check` OK, `make lint` **0 FAIL, 0 WARN**, `git diff --check` OK. Not committed (per brief).
|
||||
|
||||
## Step 1: F1 — add `--no-command-execution` to both bridge `ask` invocations
|
||||
|
||||
[DONE]
|
||||
|
||||
**Change (the running subprocess now carries the flag):**
|
||||
- `bin/pos-communication-telegram-listener:731`:
|
||||
`timeout 120 pos ai gemini ask --no-command-execution --session "$session" --system "$AI_SYSTEM" "$prompt"`
|
||||
- `bin/pos-communication-matrix-listener:466`: identical change.
|
||||
|
||||
**Why that position is valid** (read `bin/pos-ai` arg parsing first):
|
||||
- Dispatcher (`bin/pos:271-298`) longest-prefix matches `pos ai gemini ask ...` to `bin/pos-ai-gemini` (exists, forwarder), which execs `pos ai --provider gemini ask --no-command-execution ...`.
|
||||
- `bin/pos-ai` parse loop (`bin/pos-ai:647-678`) handles flags at ANY position in the `while [ $# -gt 0 ]` case (lines 668-669 set `NO_EXEC=1; TRUST_MODE=0`; non-flag tokens become `cmd`/`args`). So the flag is accepted after the subcommand.
|
||||
- Last-wins semantics vs `--trust` unchanged; bridges never pass `--trust`.
|
||||
|
||||
**Probes:**
|
||||
- PROBE-C: `bin/pos-ai --provider gemini ask --no-command-execution --help` → rc=0, usage shows the flag.
|
||||
- PROBE-D (full chain): `PATH="$PWD/bin:$PATH" bin/pos ai gemini ask --no-command-execution --help` → rc=0, usage shows the flag (2 mentions).
|
||||
- PROBE-E negative control: `bin/pos-ai --provider gemini ask --definitely-not-a-flag --help` → rc=1 (parser genuinely rejects unknown options, so C/D are not vacuously passing).
|
||||
- grep: both bridge `ask` invocation command strings contain `--no-command-execution` (one occurrence each, in the executed `timeout ... pos ai gemini ask ...` line).
|
||||
|
||||
## Step 2: F3 — `_prompt_run_command` NO_EXEC returns before printing
|
||||
|
||||
[DONE]
|
||||
|
||||
**Change (`bin/pos-ai:369-384`):** the NO_EXEC check now sits immediately after the tty guard, BEFORE the print block:
|
||||
|
||||
```bash
|
||||
[ -w /dev/tty ] || return 0
|
||||
if [ "${NO_EXEC:-0}" -eq 1 ]; then
|
||||
return 0
|
||||
fi
|
||||
printf '\n%s\n' "Command detected:" >&2
|
||||
printf ' %s\n\n' "$cmd" >&2
|
||||
```
|
||||
|
||||
The old `printf 'command execution disabled (--no-command-execution)\n' >&2` + `printf '%s\n' "$cmd"` lines are removed. rc semantics preserved: current NO_EXEC path returned 0 → still returns 0.
|
||||
|
||||
**Probe (function extracted from production `bin/pos-ai`, run in tty-writable environment):**
|
||||
- PROBE-A (`NO_EXEC=1`): rc=0, **stdout_bytes=0, stderr_bytes=0** — `[ -n check ]` on captured output fails as required (no output).
|
||||
- PROBE-B (`NO_EXEC=0`, same environment): prints `Command detected:` + the command to stderr — proves the tty guard passed and the NO_EXEC branch is what silenced PROBE-A (differential, not vacuous).
|
||||
- grep: no `command execution disabled` string remains in `bin/pos-ai`.
|
||||
|
||||
## Step 3: Doc alignment for the fixed NO_EXEC behavior (literal contradictions only)
|
||||
|
||||
[DONE]
|
||||
|
||||
The brief permits doc edits only where a doc "literally contradicts the fixed behavior". After F3 the fixed behavior is: under `--no-command-execution` nothing is printed. These claims said "still printed" — fixed with minimal wording:
|
||||
|
||||
- `bin/pos-ai` usage() `--no-command-execution` block: "The proposed command **is still printed** but never executed" → "The proposed command **is neither printed nor executed**".
|
||||
- `DOC/POS.md:103`: "the block **is printed** but not run" → "the block **is neither printed nor run**"; "the detected command **is still printed** but never run" → "the detected command **is neither printed nor run**".
|
||||
- `DOC/howto/ai.md:261-263` and `:270-271`: same two claims → "neither printed nor run".
|
||||
|
||||
The F1-related claim (bridges rely on the flag as the structural guard) was already accurate after the F1 fix and was NOT rewritten. No other doc touched (`DOC/howto/communication.md`, `config/*.env` unchanged).
|
||||
|
||||
## Step 4: F4 — restore `:num` in lint WARN messages
|
||||
|
||||
[DONE]
|
||||
|
||||
**Change (`scripts/lint-conventions.sh`):** added a per-file line counter (`num=0` before each inner `while IFS= read -r line` loop; `num=$((num + 1))` as the FIRST statement of the loop body — before every `continue` gate, so skipped lines still get correct numbers). The two WARN messages now emit `"$f:$num: ..."`, byte-identical to HEAD's format:
|
||||
- `warn_ "$f:$num: secret-like literal assignment (manual review for hardcoded credentials)"` (line 247)
|
||||
- `warn_ "$f:$num: writes to a system path (verify a VAR=\"\${VAR:-path}\" test seam exists)"` (line 297)
|
||||
|
||||
No other lint behavior changed (only the two emit paths and the counters).
|
||||
|
||||
**Probe (real script run from a scratch tree with planted violations):**
|
||||
```
|
||||
WARN lib/violation.sh:3: secret-like literal assignment (manual review for hardcoded credentials)
|
||||
WARN lib/violation.sh:4: writes to a system path (verify a VAR="${VAR:-path}" test seam exists)
|
||||
0 FAIL, 2 WARN (convention lint)
|
||||
```
|
||||
`:3` / `:4` are the exact line numbers of the planted `SECRET_API_KEY="abc123"` and `echo "x" > /etc/example` lines.
|
||||
|
||||
## Step 5: Gates & probes
|
||||
|
||||
[DONE]
|
||||
|
||||
- `bash -n` on the 4 touched shell scripts: OK.
|
||||
- `make gen` ×2: rc=0 both runs; generated-output hash identical before/after (``711d6379...``) → **no gen drift** from my edits.
|
||||
- `make check` (`scripts/check-sync.sh`): OK.
|
||||
- `make lint` (`scripts/lint-conventions.sh`): **0 FAIL, 0 WARN** (rc=0).
|
||||
- `git diff --check`: OK.
|
||||
- Executable bits unchanged (all 4 scripts still `-rwxrwxr-x`).
|
||||
- Out-of-scope D-A behavior confirmed untouched: `bin/pos-communication-telegram-listener:761,790` and `bin/pos-communication-matrix-listener:511` still warn-based soft-fail (pending Architect decision); no edits in that region.
|
||||
- Not committed (per brief).
|
||||
|
||||
## Files changed
|
||||
|
||||
- `bin/pos-communication-telegram-listener` (F1, line 731)
|
||||
- `bin/pos-communication-matrix-listener` (F1, line 466)
|
||||
- `bin/pos-ai` (F3 `_prompt_run_command`; usage text for `--no-command-execution`)
|
||||
- `scripts/lint-conventions.sh` (F4, two WARN emit paths + counters)
|
||||
- `DOC/POS.md` (1 sentence, literal contradiction only)
|
||||
- `DOC/howto/ai.md` (2 sentences, literal contradiction only)
|
||||
- `AgentsReport/builder/2026-09-06_f1-f3-fixes.md` (this report)
|
||||
|
||||
## Remaining risks
|
||||
|
||||
- None in-scope. The two lint WARN messages now match HEAD's `:num` format; the rest of the lint rewrite (other message classes, FAIL rules) was not touched.
|
||||
- Note: `DOC/howto/ai.md:262` non-tty bullet claim ("the code block is neither printed nor run") is now accurate for current code AND for the bridges with the flag.
|
||||
|
||||
## Recommended next agent
|
||||
|
||||
**Orchestrator** — reviewer block-list F1/F3/F4 implemented and gate-verified; reviewer should re-run `make test` (untouched, per parallel track) and confirm F2 via the parallel D-A Architect decision before merge.
|
||||
|
||||
## Changes made by Builder
|
||||
|
||||
In-scope implementation only (F1, F3, F4 + literal-contradiction doc alignment). No out-of-scope modifications.
|
||||
@@ -1,56 +0,0 @@
|
||||
# Builder report — F1 regex fix in bin/pos-ai-server
|
||||
|
||||
Date: 2026-09-06 · Builder · Objective: fix `detect_llama_version` regex so `build 1.2.3` returns `1.2.3` not `1`. File scope: `bin/pos-ai-server` ONLY. No commit.
|
||||
|
||||
## TL;DR (status: IMPLEMENTED)
|
||||
|
||||
- **Root cause:** `grep -oE '[0-9]+\.[0-9]+\.[0-9]+|build [0-9]+|b[0-9]+'` picks the LEFTMOST match; for input `build 1.2.3`, `build [0-9]+` wins → `build 1` → after `${version#build }` → `1`.
|
||||
- **Fix:** `bin/pos-ai-server:86` → `build [0-9]+(\.[0-9]+)*` (full dotted tail consumed). Stripping and guarded behavior unchanged.
|
||||
- **Gates:** `bash -n` clean; `make gen` rc 0 with generated files byte-identical (sha256 before == after); `make check` OK; `make lint` 0 FAIL / 0 WARN.
|
||||
- **Tests:** `tests/t-ai-llama-detect.sh` (was failing on F1) now 9/9 PASS; `tests/t-ai-server-validate.sh` (Tester's F1 suite, real extracted function) 27/27 PASS.
|
||||
|
||||
## Step 1: Read & confirm scope
|
||||
- Read `AgentsReport/tester/2026-09-06_ai-server-tests.md` (F1 finding: lines 9, 63).
|
||||
- Read `bin/pos-ai-server` (lines 82–90).
|
||||
- Scope confirmed: touch `bin/pos-ai-server` only; do NOT touch tests/.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 2: Apply regex fix
|
||||
- `bin/pos-ai-server:86`: `build [0-9]+` → `build [0-9]+(\.[0-9]+)*`.
|
||||
- Stripping (`version="${version#build }"`) and guarded behavior (`command -v` guard + `|| true` + `unknown` fallback) unchanged — per brief.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 3: Verification matrix (shipped regex evaluated against stub binaries)
|
||||
- `version: 0.4.0-dev (build 10822, commit …)` (stderr) → `0.4.0` PASS
|
||||
- `build 10822` → `10822` PASS
|
||||
- `build 1.2.3` → `1.2.3` PASS (regression fixed)
|
||||
- `b10822` → `b10822` — see Decision Note below (brief's conditional `10822` not taken)
|
||||
- missing binary → `unknown` PASS
|
||||
- unreadable / non-matching output → `unknown` PASS
|
||||
- Additional: `semver only (1.5.0)` → `1.5.0` PASS (leftmost semver still wins)
|
||||
|
||||
### Decision Note (b-token)
|
||||
Brief lists `b10822` → `10822` "if your regex keeps a b-token", but also mandates "keep the same stripping logic" (`${version#build }` — strips only `build `, never `b`). The two are mutually exclusive. Resolution per the hard constraint: kept the b-token (matches existing comment: very old builds print no semver) and kept stripping identical → `b10822` yields `b10822`. This is exactly what the Tester's own fixture asserts (`tests/t-ai-server-validate.sh:118` → `b10822`), and `t-ai-server-validate.sh` passes 27/27 with the real extracted function.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 4: Gates
|
||||
- `bash -n bin/pos-ai-server` → clean
|
||||
- `make gen` → rc 0; sha256 of `DOC/AGENT_Context_Project.md` + `completions/pos.bash` identical before/after → NO output change (regex is code, not a GEN: block)
|
||||
- `make check` → OK
|
||||
- `make lint` → 0 FAIL, 0 WARN
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 5: Tester-requested regression verification
|
||||
- `./tests/run-tests.sh t-ai-llama-detect.sh` → PASS, 9/9 checks (was the failing test per Tester F1 finding; `llama.cpp build 1.2.3` fixture now yields `1.2.3`).
|
||||
- `./tests/run-tests.sh t-ai-server-validate.sh` → PASS, 27/27 checks (real extracted `detect_llama_version`, incl. `build 10822` → `10822` and `b10822` → `b10822` fixtures).
|
||||
|
||||
[PASS]
|
||||
|
||||
## Handoff
|
||||
- Status: IMPLEMENTED. Files changed: `bin/pos-ai-server` (single regex line, line 86). Git: NOT committed (per brief).
|
||||
- Note: the working tree already carried the pre-existing uncommitted F1–F7 Builder changes plus 4 untracked Tester test files (per Tester report Step 6); none of that is mine.
|
||||
- Recommended next: Reviewer (independent gate + `make test` re-run). Separate track: F4 bus-gap fixture updates in 3 pre-existing DRY_RUN tests are already routed to Orchestrator — not touched here.
|
||||
@@ -1,43 +0,0 @@
|
||||
# Builder Report — 2026-09-06 — llamacpp app installer + `ai` category + `pos ai server` hint wiring
|
||||
|
||||
## TL;DR
|
||||
- **Status:** IMPLEMENTED — all steps `[DONE]`, all gates green
|
||||
- **Scope:** new `apps/ai/llamacpp.sh` installer, `ai` category plumbing (CAT_NAMES, template comment, DOC/APPS.md), 3 error-hint lines in `bin/pos-ai-server`, 1 line in `DOC/POS.md`, 1 AGENT_TODO.md Done entry
|
||||
- **Real asset naming VERIFIED via live API probe** (network available): `releases/latest` is the `v0.4.0` milestone with **no binary assets** (only `nightly-tag.txt`); the binaries live on nightly `bNNNNN` prereleases as `llama-<tag>-bin-ubuntu-x64.tar.gz` / `llama-<tag>-bin-ubuntu-arm64.tar.gz` (`.tar.gz`, **not** `.zip`), with a top-level dir `llama-<tag>/`
|
||||
- **Key adaptations vs brief:** (1) fetch `/releases?per_page=10` + scan for the first release with a matching asset instead of `/releases/latest`; (2) extract with `tar xzf --strip-components=1` instead of `unzip` — the `.zip`/`unzip` assumption is obsolete (evidence: live API + archive listing); no `unzip` apt install (wrong mutation for tar.gz assets)
|
||||
- **Files changed:** `apps/ai/llamacpp.sh` (new), `apps/install.sh` (CAT_NAMES), `templates/app.sh` (comment), `DOC/APPS.md`, `bin/pos-ai-server` (3 text lines), `DOC/POS.md` (1 line), `AGENT_TODO.md` (1 entry), `AgentsReport/builder/2026-09-06_llamacpp-app.md` (new report). `make gen` changed nothing.
|
||||
|
||||
## Step 1: Probe real llama.cpp release assets
|
||||
- [x] Probe `api.github.com/repos/ggml-org/llama.cpp/releases/latest` + `/releases?per_page=10`, inspect archive layout
|
||||
- [DONE]
|
||||
|
||||
## Step 2: Write `apps/ai/llamacpp.sh`
|
||||
- [x] Installer with idempotent `install_llamacpp()` / `uninstall_llamacpp()` + uninstall case
|
||||
- [x] Verify `bash -n`, idempotent no-op paths
|
||||
- [DONE]
|
||||
|
||||
## Step 3: `ai` category plumbing
|
||||
- [x] `apps/install.sh` CAT_NAMES `[ai]="AI / ML"`
|
||||
- [x] `templates/app.sh` categories comment adds `ai`
|
||||
- [x] `DOC/APPS.md`: categories line, count 15→16, catalog row
|
||||
- [DONE]
|
||||
|
||||
## Step 4: Point `pos ai server` at the installer
|
||||
- [x] `bin/pos-ai-server` lines ~397, ~563, ~256 → scrcpy-style hint keeping the GitHub URL
|
||||
- [x] `DOC/POS.md` ~line 125 adds "(install via `bash apps/install.sh llamacpp`)"
|
||||
- [DONE]
|
||||
|
||||
## Step 5: AGENT_TODO.md Done entry
|
||||
- [x] Insert 2026-09-06 entry before the current first Done entry
|
||||
- [DONE]
|
||||
|
||||
## Step 6: Verification
|
||||
- [x] `bash -n apps/ai/llamacpp.sh apps/install.sh bin/pos-ai-server` → all OK
|
||||
- [x] `make gen && make gen` → idempotent, **no gen output changed** (apps installers aren't scanned by gen)
|
||||
- [x] `make check` → `check-sync: OK`, rc 0
|
||||
- [x] `make lint` → `0 FAIL, 0 WARN`, rc 0
|
||||
- [x] `bash apps/install.sh --uninstall llamacpp` → resolves app, "llama.cpp not installed", rc 0, no network
|
||||
- [x] `bash apps/ai/llamacpp.sh uninstall` → rc 0 (no llama-server on PATH)
|
||||
- [x] Stub-guard: `bash apps/ai/llamacpp.sh` with a fake `llama-server` on PATH → "llama.cpp already installed", rc 0 (no network/sudo)
|
||||
- [x] Dry-run install + uninstall with a real (stubbed) binary → parse + spawn expansion + symlink-loop body all verified
|
||||
- [DONE]
|
||||
@@ -1,143 +0,0 @@
|
||||
# Builder report — 2026-09-06: llamacpp provider forwarder + shorthand
|
||||
|
||||
## TL;DR
|
||||
|
||||
- Status: IMPLEMENTED
|
||||
- Objective: make `pos ai llamacpp ask ...` route to `pos ai --provider llamacpp ask ...`
|
||||
(fix "Unknown ai subcommand 'llamacpp'" for user alias `what|llamacpp|what`).
|
||||
- Changes: `bin/pos-ai` gains a `llamacpp` provider-shorthand case in its subcommand
|
||||
dispatch; new executable forwarder `bin/pos-ai-llamacpp` (byte-for-byte mirror of
|
||||
`bin/pos-ai-gemini`); `ai-llamacpp` registered in `bin/pos` INTERACTIVE_CMDS;
|
||||
`make gen` regenerated docs/completions; `DOC/POS.md` ai section hand-edited.
|
||||
- Verification: `bash -n` OK on all changed files; `make gen` idempotent;
|
||||
`make check` green; `make lint` ends **0 FAIL, 0 WARN**;
|
||||
smoke: `pos ai llamacpp --help` / `providers` / `ask` / bare `llamacpp` all parse
|
||||
as provider llamacpp (no "Unknown ai subcommand"; network error only when no
|
||||
local llama.cpp server is running, which is expected).
|
||||
- Note: `pos ai gemini`/`openrouter` shorthands are implemented by the dispatcher's
|
||||
longest-prefix match finding the thin forwarder scripts — there is no in-file
|
||||
shorthand parse in `bin/pos-ai` to mirror; the new dispatch case mirrors the
|
||||
forwarders' behaviour instead (per approved scope item 1).
|
||||
|
||||
## Step 1: Read code and confirm mechanism
|
||||
|
||||
- Read `bin/pos-ai` (parse loop lines 648-678, provider resolve 680-687, dispatch
|
||||
`case "${cmd:-}"` 693-702 with the "Unknown ai subcommand" error at line 701).
|
||||
- Read `bin/pos-ai-gemini` / `bin/pos-ai-openrouter` (thin forwarders, 7 lines).
|
||||
- Read `lib/ai-providers/llamacpp.sh` — implements `provider_name`,
|
||||
`provider_default_model`, `provider_generate` (ask/chat/sessions),
|
||||
`provider_models_list` (models).
|
||||
- Read `bin/pos` dispatcher + INTERACTIVE_CMDS; `scripts/lint-conventions.sh`;
|
||||
`scripts/gen-docs.sh`; `Makefile`.
|
||||
- Confirmed root cause: `pos ai llamacpp ask` falls through the `bin/pos` longest-
|
||||
prefix dispatch (no `pos-ai-llamacpp`) to `bin/pos-ai`, where `llamacpp` is parsed
|
||||
as the subcommand `cmd` → `*) err "Unknown ai subcommand 'llamacpp'"`.
|
||||
- No gemini/openrouter shorthand exists inside `bin/pos-ai`; the forwarders are the
|
||||
mechanism. The scope's step 1 is therefore implemented as a parallel dispatch case
|
||||
mirroring the forwarders' `exec pos ai --provider <name> "$@"` behaviour.
|
||||
[DONE]
|
||||
|
||||
## Step 2: Add `llamacpp` provider shorthand to `bin/pos-ai`
|
||||
|
||||
- Added to the final `case "${cmd:-}"` dispatch (after `sessions`):
|
||||
|
||||
```bash
|
||||
llamacpp)
|
||||
# Provider shorthand (backward compat, same as the gemini/openrouter
|
||||
# forwarders): pos ai llamacpp <subcmd> ... == pos ai --provider llamacpp <subcmd> ...
|
||||
exec "$0" --provider llamacpp "${args[@]}" ;;
|
||||
```
|
||||
|
||||
- No arg-parsing redesign; single parallel case, same style as sibling branches.
|
||||
- Edge behavior verified: `bin/pos-ai llamacpp` (no subcommand) → re-exec with
|
||||
`--provider llamacpp` → usage, exit 0.
|
||||
[DONE]
|
||||
|
||||
## Step 3: Create thin forwarder `bin/pos-ai-llamacpp` (100755)
|
||||
|
||||
- Byte-for-byte mirror of `bin/pos-ai-gemini` with provider name substituted
|
||||
(verified with `cmp` against a sed-substituted gemini file; em-dash intact).
|
||||
- `# POS: ai llamacpp — Forward to pos ai --provider llamacpp (backward compat)`
|
||||
- `# POS_SUBCMDS: ask chat models sessions capture` — mirrors the gemini forwarder:
|
||||
llamacpp adapter implements `provider_generate` (ask/chat/sessions/capture) and
|
||||
`provider_models_list` (models); no invented subcommands.
|
||||
- Executable: mode 100755 (`chmod 755`).
|
||||
[DONE]
|
||||
|
||||
## Step 4: Register `ai-llamacpp` in `bin/pos` INTERACTIVE_CMDS + EXAMPLES judgment
|
||||
|
||||
- Added `ai-llamacpp` to INTERACTIVE_CMDS (after `ai-openrouter`), matching the
|
||||
gemini/openrouter forwarders' registration — `pos ai llamacpp chat` reads stdin
|
||||
and must skip the logging `tee` pipe. Lint validates the entry against the new
|
||||
tool (`bin/pos-ai-llamacpp` exists → gate green).
|
||||
- `bin/pos` usage() EXAMPLES: NO change. Existing style showcases only the default
|
||||
provider (`pos ai gemini ...`); openrouter has no example line either. Adding a
|
||||
llamacpp line would be inconsistent — judgment call per brief ("keep minimal").
|
||||
[DONE]
|
||||
|
||||
## Step 5: `make gen` + hand-edit `DOC/POS.md`
|
||||
|
||||
- `make gen` regenerated `DOC/AGENT_Context_Project.md` (tree line 68, dispatch
|
||||
row 287, selfcontained line 374, filetable row 619, docmap shifts) and
|
||||
`completions/pos.bash` (`_pos_subcmds[ai-llamacpp]`, `llamacpp` appended under
|
||||
`_pos_subcmds[ai]`, filetable count). Pre-existing gen drift in the working tree
|
||||
(ai-hf/ai-server rows) preserved untouched.
|
||||
- `DOC/POS.md` hand-edits (3):
|
||||
1. File line: `bin/pos-ai-gemini / bin/pos-ai-openrouter` → `... / bin/pos-ai-llamacpp`
|
||||
(backward-compat forwarders).
|
||||
2. `--provider <name>` row: `(gemini\|openrouter)` → `(gemini\|openrouter\|llamacpp)`.
|
||||
3. Backward-compat sentence: `pos ai llamacpp` added alongside `gemini`/`openrouter`.
|
||||
- Provider adapters line already lists `lib/ai-providers/llamacpp.sh` — no new
|
||||
adapters-table row needed.
|
||||
[DONE]
|
||||
|
||||
## Step 6: Verify — bash -n, make check, make lint, smoke tests
|
||||
|
||||
- `bash -n bin/pos bin/pos-ai bin/pos-ai-llamacpp` → OK.
|
||||
- `make check` (check-sync.sh: bash -n + exec bits + gen-docs --check + dispatch
|
||||
smoke) → `check-sync: OK`.
|
||||
- `./scripts/gen-docs.sh --check` → idempotent, no drift.
|
||||
- `make lint` → `0 FAIL, 0 WARN (convention lint)`.
|
||||
- Smoke (PATH prefixed with repo `bin/`):
|
||||
- `pos ai llamacpp --help` → usage, exit 0.
|
||||
- `pos ai llamacpp providers` → lists providers, `llamacpp ... ← active`
|
||||
(no "Unknown ai subcommand").
|
||||
- `pos ai llamacpp ask "hi"` → `ERROR: curl: (7) Failed to connect to
|
||||
127.0.0.1 port 8088` (no local server — parse path verified, provider adapter
|
||||
loaded; NOT an unknown-subcommand error).
|
||||
- `bin/pos-ai llamacpp ask "hi"` (direct, item 1 dispatch) → same provider path.
|
||||
- `pos ai llamacpp` (bare) → usage, exit 0.
|
||||
- Regression: `pos ai gemini --help`, `pos ai --help` unchanged; `pos tree`
|
||||
shows the new node.
|
||||
[DONE]
|
||||
|
||||
## Remaining risks / follow-up (not in scope)
|
||||
|
||||
- `DOC/howto/ai.md` still documents only gemini/openrouter shorthands (line 29-30).
|
||||
Not in the brief's hand-edit list; left untouched — Writer/doc follow-up.
|
||||
- `bin/pos-ai` usage() text lines 42/59 still say `(gemini, openrouter)` /
|
||||
`(gemini|openrouter; default: gemini)` and `DOC/POS.md` AI_PROVIDER config row
|
||||
still says `(gemini\|openrouter)` — pre-existing staleness predating this change
|
||||
(llamacpp provider already existed). Out of approved scope; doc follow-up.
|
||||
- `pos ai llamacpp` with a real server was not exercised (no local llama.cpp
|
||||
server/config in this environment) — addressed by the parse-path verification.
|
||||
|
||||
## Handoff
|
||||
|
||||
Status: IMPLEMENTED
|
||||
Approved scope: provider shorthand in bin/pos-ai + bin/pos-ai-llamacpp forwarder +
|
||||
make gen + DOC/POS.md hand-edits + targeted verification. Nothing else touched.
|
||||
Files changed:
|
||||
- bin/pos-ai (dispatch case)
|
||||
- bin/pos-ai-llamacpp (new, 100755)
|
||||
- bin/pos (INTERACTIVE_CMDS)
|
||||
- DOC/POS.md (3 hand-edits)
|
||||
- DOC/AGENT_Context_Project.md, completions/pos.bash (make gen)
|
||||
- AgentsReport/builder/2026-09-06_llamacpp-forwarder.md (this report)
|
||||
Verification: bash -n OK; make gen idempotent; make check green; make lint
|
||||
0 FAIL, 0 WARN; smoke tests pass (parse path verified, no server available).
|
||||
Scope compliance: in-scope changes only; out-of-scope changes: none
|
||||
(README.md, AGENT_TODO.md, lib/ai-providers/*.sh, pos-ai-hf, pos-ai-server untouched).
|
||||
Recommended next agent: Reviewer
|
||||
Reason: implementation complete and independently verifiable; adversarial review
|
||||
of the dispatch case + forwarder + doc/tree sync before acceptance.
|
||||
@@ -1,52 +0,0 @@
|
||||
# Builder Report — 2026-09-06: NET_PROBE unbound-variable fix
|
||||
|
||||
## TL;DR
|
||||
- **Status:** IMPLEMENTED (one-line scope, verified)
|
||||
- **Root cause:** `NET_PROBE="${NET_PROBE:-timeout 3 bash -c 'exec 3<>/dev/tcp/$1/$2' _ 8.8.8.8 53}"` — inside the double-quoted assignment the OUTER shell expanded `$1`/`$2` at assignment time. Under `set -u` with no positional args this is an unbound-variable crash on every `pos network download` run (unless `NET_PROBE` is already exported).
|
||||
- **Fix:** escape the positional markers so only the inner `bash -c` sees them — `\$1`/`\$2` in the default string. Env-var override contract (`NET_PROBE` = full command string) unchanged.
|
||||
- **File changed (only):** `bin/pos-network-download` — line 31 (NET_PROBE default). Nothing else.
|
||||
- **Verification:** `bash -n` OK; bare `--help` and non-network `info` path reach the assignment with `NET_PROBE` UNSET → no unbound crash; `NET_PROBE='true'` override returns 0; static inspection confirms outer shell does not expand `$1/$2`; gates green (`make gen` idempotent, `make check` OK, `make lint` `0 FAIL, 0 WARN`).
|
||||
- **Not committed** (per brief).
|
||||
|
||||
## Step 1: Confirm bug & scope
|
||||
`bin/pos-network-download` line 31 holds the double-quoted default; `net_up()` (line 379) runs `bash -c "$NET_PROBE"`, which relies on the inner bash receiving the trailing positional args `_ 8.8.8.8 53` (`$0=_, $1=8.8.8.8, $2=53`).
|
||||
Reproduced: `set -u; NET_PROBE="${NET_PROBE:-...$1/$2...}"` → `/bin/bash: line 1: $1: unbound variable`.
|
||||
Confirmed the pre-existing HEAD default was `'</dev/tcp/8.8.8.8/53>'` (no `$1/$2`) — a prior security change to the probe introduced the regression.
|
||||
[DONE]
|
||||
|
||||
## Step 2: Apply fix (escaped literal default)
|
||||
Changed only line 31:
|
||||
`NET_PROBE="${NET_PROBE:-timeout 3 bash -c 'exec 3<>/dev/tcp/\$1/\$2' _ 8.8.8.8 53}"`
|
||||
The `\$` escapes keep the outer assignment from expanding `$1`/`$2`; the string stored is the literal `.../dev/tcp/$1/$2...`, so the inner `bash -c` receives the proper positional args. `net_up()` unchanged — it already passes the whole command string to the inner bash. Env override contract preserved and documented in usage() ("NET_PROBE override defaults (test seams)").
|
||||
[DONE]
|
||||
|
||||
## Probes (report output)
|
||||
1. **`set -u; bash -n bin/pos-network-download`** → OK (no error).
|
||||
2. **Bare help, NET_PROBE UNSET** (stubbed `aria2c`/`jq`/`curl`, isolated `HOME`/`CONFIG_DIR`): `--help` exits 0, no `unbound` in output → assignment reached without crash.
|
||||
**Non-network path, NET_PROBE UNSET:** `info` (no gid) → graceful `ERROR: info: gid required`, no unbound-variable crash (assignment ran at source time before dispatch).
|
||||
3. **`NET_PROBE='true'` env override** → `net_up()` returns 0 (override still honored).
|
||||
4. **Static default string inspection** → default stored as `timeout 3 bash -c 'exec 3<>/dev/tcp/$1/$2' _ 8.8.8.8 53` (literal `$1/$2`, NOT expanded by outer shell). Simulated probe with `echo inner sees $1 $2` → inner bash prints `8.8.8.8 53`, proving positional probe still correct.
|
||||
5. **`--dry-run`/usage unaffected** — neither references NET_PROBE; usage() unchanged by this fix.
|
||||
[DONE]
|
||||
|
||||
## Step 3: Repo gates
|
||||
- `bash -n bin/pos-network-download` → OK.
|
||||
- `make gen` → ran; re-ran: **idempotent** (byte-identical diff before/after), i.e. no new output. Net-probe is an env var, not a `# POS:` header, so it feeds nothing.
|
||||
- `make check` (`scripts/check-sync.sh`) → OK.
|
||||
- `make lint` (`scripts/lint-conventions.sh`) → `0 FAIL, 0 WARN`.
|
||||
[DONE]
|
||||
|
||||
## Scope compliance
|
||||
- Approved scope: `bin/pos-network-download` ONLY (NET_PROBE lines + net_up).
|
||||
- Change made: exactly the NET_PROBE default string (line 31). `net_up` confirmed correct, no change required.
|
||||
- Out-of-scope changes: none made by Builder. (Working tree contains pre-existing unstaged changes from prior agents — not authored here.)
|
||||
- Not committed (per brief).
|
||||
|
||||
## Remaining risks / follow-up
|
||||
- None for this fix. The env-var override string is operator-controlled; passing an invalid command there is the operator's responsibility (unchanged behavior).
|
||||
|
||||
## Recommended next agent
|
||||
Orchestrator — task is complete, targeted verification and all gates pass; no cross-track coordination needed.
|
||||
|
||||
## Changes made by Builder
|
||||
- `bin/pos-network-download`: escaped `$1`/`$2` to `\$1`/`\$2` in the `NET_PROBE` default so the inner `bash -c` (not the outer shell) performs the probe-host positional expansion.
|
||||
@@ -1,76 +0,0 @@
|
||||
# Builder Report — R1/R2 fixes (re-review findings)
|
||||
|
||||
Date: 2026-09-06
|
||||
Builder: implementation pass on top of the re-review (`AgentsReport/reviewer/2026-09-06_pos_ai_rereview.md`)
|
||||
Approved scope: fix R1 (`bin/pos-ai-server` ExecStart quoting) and R2 (`bin/pos-ai-hf` partial-failure honesty). Files modified: only `bin/pos-ai-server`, `bin/pos-ai-hf`. Generated docs were refreshed by the mandated `make gen` gate.
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status: IMPLEMENTED** — both REQUIRED findings fixed; all gates green; both targeted probes pass.
|
||||
- Files changed: `bin/pos-ai-server` (systemd_quote helper + quoted ExecStart tokens), `bin/pos-ai-hf` (honest failure count, `.hf-meta` gating, non-zero exit, honest progress labels).
|
||||
- Verification: `bash -n` clean; `make gen` idempotent (sha256-identical across runs); `make check` → `check-sync: OK` (rc 0); `make lint` → `0 FAIL, 0 WARN`.
|
||||
- R1 probe: dry-run ExecStart with a spaced model path → `systemd-analyze verify` rc 0; systemd word-split == exactly [binary, --model, <spaced path>, each flag, each value] (16 tokens, asserted programmatically).
|
||||
- R2 probe: stub-curl 2-file parallel download with 1 failure → summary `📥 Downloaded: org/probe (1 of 2 files, 1 failed: model-fail.bin)`, NO `.hf-meta` written, `list` shows nothing, exit code 1.
|
||||
- AGENT_TODO.md intentionally NOT updated: the brief constrains modifications to the two files only ("Do not touch other files").
|
||||
|
||||
## Step 1: R1 — systemd ExecStart quoting (`bin/pos-ai-server`)
|
||||
|
||||
Added `systemd_quote()` (bin/pos-ai-server:388-392): wraps a value in systemd double quotes and escapes embedded `"` as `\"` (verified: `${value//\"/\\\"}` produces `\"`). `cmd_start` now builds the base line as:
|
||||
|
||||
```bash
|
||||
exec_cmd="$(systemd_quote "$llamacpp_full") -m $(systemd_quote "$model") --port $PORT --host $HOST"
|
||||
```
|
||||
|
||||
(bin/pos-ai-server:445). Only the executable path and the model path — the tokens that may legally contain spaces per the brief — are quoted; numeric/flag tokens (`--port 8088`, `--n-gpu-layers 10`, …) stay unquoted, so no over-quoting. The unit heredoc still writes `ExecStart=$exec_cmd` (bin/pos-ai-server:508) and the dry-run prints the same `$exec_cmd` (bin/pos-ai-server:491), so dry-run output is byte-identical to the unit by construction.
|
||||
|
||||
Probe (run with fake `llama-server` from `/tmp/opencode/ai-probe/bin`, model at `/tmp/opencode/ai-probe/models/My Model/ggml-model-Q4_K_M.gguf`, flags `--port 9090 --gpu-layers 10 --ctx-size 2048 --temperature 0.7 --mmap`):
|
||||
|
||||
```
|
||||
[+] (dry-run) ExecStart: "/tmp/opencode/ai-probe/bin/llama-server" -m "/tmp/opencode/ai-probe/models/My Model/ggml-model-Q4_K_M.gguf" --port 9090 --host 127.0.0.1 --n-gpu-layers 10 --ctx-size 2048 --threads 4 --temperature 0.7 --mmap
|
||||
rc=0
|
||||
systemd-analyze verify → rc=0
|
||||
word-split == [binary, --model, <spaced path>, each flag, each value] → True (16 tokens)
|
||||
```
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 2: R2 — partial-failure honesty (`bin/pos-ai-hf`)
|
||||
|
||||
Changes inside `cmd_download` only; the sequential single-file path (else branch) is byte-for-byte unchanged:
|
||||
|
||||
- `failed_files` is now function-scoped (bin/pos-ai-hf:632), populated only by the parallel branch, so the sequential path is unaffected (empty array there).
|
||||
- Progress labels are honest: a reaped job that failed prints `[n/m] Failed: <file>` instead of `Completed:` (bin/pos-ai-hf:672, 686).
|
||||
- `.hf-meta` is written only when `failed_files` is empty; on partial failure a warn explains the skip (bin/pos-ai-hf:727-744). Consumers `list`/`cache` only discover models with `.hf-meta` (cmd_list/hf_cache_models), so an incomplete model is not advertised.
|
||||
- Summary is honest for the parallel path: `📥 Downloaded: <repo> (X of Y files, N failed: <f1> <f2>)` where X = Y − N (bin/pos-ai-hf:760-761). The all-success multi-file summary and the single-file summary are unchanged.
|
||||
- Exit: `return 1` when `failed_files` is non-empty (bin/pos-ai-hf:773-775) — only reachable from the parallel path.
|
||||
|
||||
Probe (stub curl in `/tmp/opencode/ai-probe/r2bin`, canned 2-file listing, target containing `fail` exits 1; `download org/probe --branch main` with both files in one parallel batch):
|
||||
|
||||
```
|
||||
[1/2] Completed: model-ok.bin
|
||||
[2/2] Failed: model-fail.bin
|
||||
[!] Failed to download model-fail.bin
|
||||
[!] Not writing .hf-meta — org/probe is incomplete (1 file(s) failed)
|
||||
📥 Downloaded: org/probe (1 of 2 files, 1 failed: model-fail.bin)
|
||||
📁 /tmp/opencode/ai-probe/dl/org-probe/
|
||||
rc=1
|
||||
```
|
||||
|
||||
Post-checks: dir contains only `model-ok.bin` (no `.hf-meta`); `pos ai hf list` → `No models downloaded yet`; single-file sequential success → meta written, rc 0 (unchanged); single-file sequential failure → warn-and-continue, rc 0 (unchanged, pre-existing behavior preserved per constraint).
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 3: Gates
|
||||
|
||||
- `bash -n bin/pos-ai-server bin/pos-ai-hf` → clean.
|
||||
- `make gen` ×2 → `gen-docs: write OK`; sha256 of `DOC/AGENT_Context_Project.md` + `completions/pos.bash` identical across runs → idempotent. Filetable row counts now match `wc -l` exactly (pos-ai-hf 1004, pos-ai-server 659).
|
||||
- `make check` → `check-sync: OK` (rc 0).
|
||||
- `make lint` → `0 FAIL, 0 WARN (convention lint)`.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Scope compliance / residual notes
|
||||
|
||||
- Only `bin/pos-ai-server` and `bin/pos-ai-hf` were edited by this pass; `DOC/AGENT_Context_Project.md`/`completions/pos.bash` were refreshed only by the mandated `make gen` gate (line counts/examples columns). No new flags/subcommands/config keys. AGENT_TODO.md not touched (brief forbids other files).
|
||||
- Residual risk (pre-existing, explicitly out of scope by the "keep sequential path unchanged" constraint): the single-file sequential failure path still warns-and-continues with rc 0 and writes `.hf-meta` including the failed file. Flagged for a future decision; not changed here.
|
||||
- Probes ran in this environment (real `systemd-analyze`, `bash`, `make` available); nothing was blocked.
|
||||
@@ -1,105 +0,0 @@
|
||||
# Builder Report — POS AI review fixes (F1–F6)
|
||||
|
||||
Date: 2026-09-06
|
||||
Builder: big-pickle
|
||||
|
||||
## TL;DR
|
||||
|
||||
- Status: IMPLEMENTED
|
||||
- Fixes F1–F6 from `AgentsReport/reviewer/2026-09-06_pos_ai_full_review.md` implemented in `bin/pos-ai-hf`, `bin/pos-ai-server`, `DOC/POS.md` (hf + server detail), generated docs via `make gen`.
|
||||
- Scope: exactly the approved fixes; no changes to `bin/pos-ai`, `bin/pos-ai-llamacpp`, `bin/pos`, README.md, AGENT_TODO.md, lib/ (AGENT_TODO.md not updated on purpose — out of scope, has pre-existing worktree edits).
|
||||
- Verification: `bash -n` clean; `make gen` idempotent (3rd run md5-identical); `make check` → check-sync: OK; `make lint` → `0 FAIL, 0 WARN`; targeted probe matrix below (all passed).
|
||||
- Deviation from reviewer wording: destroy-confirmation for `cache clear` reads `/dev/tty` (same pattern as `pos-ai-server pick_model`) instead of sourcing `lib/common.sh confirm()` — `pos-ai-hf` is NOT in `bin/pos` `INTERACTIVE_CMDS` and `bin/pos` is off-limits, so a plain stdin `read`/`confirm` would hang-or-trip the `uses_stdin` lint rule. Fail-closed default `n`, EOF/invalid denies (verified via `setsid`/pty probes).
|
||||
|
||||
## Step 1: F1 — include/exclude glob filtering (pos-ai-hf)
|
||||
[DONE]
|
||||
|
||||
- Removed the `--include/--exclude` + `--gguf` erroring pre-check (`bin/pos-ai-hf` cmd_download) — patterns now compose, not conflict.
|
||||
- Removed the old jq `match()` regex branch (regex semantics despite "supports glob" docs) from the `elif` chain; single composition point after gguf/filename filter, order: gguf/filename → include → exclude.
|
||||
- New `hf_apply_patterns()` (`bin/pos-ai-hf:422-447`): bash `case` glob semantics, always yields a JSON array (`[]` when no match) preserving `{"rfilename","size"}` shape.
|
||||
- Added error path `No files match include/exclude patterns in <repo> (branch: <branch>)` (rc 1) when patterns filter everything out.
|
||||
|
||||
Probes (fake curl serving canned repo):
|
||||
- `--include "model.gguf"` (single file, sequential): rc 0, only model.gguf fetched, `.hf-meta` files array is a JSON array of 1.
|
||||
- `--include "*.gguf"` (parallel): rc 0, model.gguf + Q8_0/model-q8.gguf fetched, meta files array length 2.
|
||||
- `--include "*.bin"` → `ERROR: No files match include/exclude patterns in org/model (branch: main)`, rc 1.
|
||||
- `--gguf --include "*.gguf"` composes: rc 0, same 2 gguf files.
|
||||
- exclude-only `--exclude "*.safetensors"` → rc 0, 3 files remain.
|
||||
|
||||
## Step 2: F2 — systemd unit single-line ExecStart (pos-ai-server)
|
||||
[DONE]
|
||||
|
||||
- `cmd_start` now builds ONE `exec_cmd` string with binary + model + all resolved flags (`bin/pos-ai-server:431-477`); unit written via heredoc with `ExecStart=$exec_cmd` on a single line (`bin/pos-ai-server:488-504`) — no more multi-line `echo >>` appends that systemd rejects.
|
||||
- Dry-run prints the same `$exec_cmd` it would write into the unit (previously the dry-run line missed all optional flags).
|
||||
- Real (non-dry) start: unit written, then `systemctl --user daemon-reload` (container has no systemd user session → fails after write, expected; unit itself verified).
|
||||
|
||||
Probes:
|
||||
- dry-run with all flags: single ExecStart line containing every flag, rc 0.
|
||||
- real run wrote the unit; `systemd-analyze verify <unit>` → **RC=0, no warnings** (executable path resolves, `EnvironmentFile=-%h/...` accepted).
|
||||
|
||||
## Step 3: F3 — --branch/--revision alias (pos-ai-hf)
|
||||
[DONE]
|
||||
|
||||
- Removed the separate `BRANCH` variable; `--branch` and `--revision` both set `REVISION` (`bin/pos-ai-hf:127-148`), last flag wins (usage documents the alias).
|
||||
- `hf_resolve_branch` unchanged: explicit revision or API default branch, falls back `main`.
|
||||
|
||||
Probes:
|
||||
- `--branch main`, `--revision v1.0`, and `--revision v2.0 --branch main` (later wins → main) all rc 0.
|
||||
|
||||
## Step 4: F4 — parallel download failure handling (pos-ai-hf)
|
||||
[DONE]
|
||||
|
||||
- Rewrote the parallel path (`bin/pos-ai-hf:638-692`): per-pid `wait` with `! wait` failure capture, parallel `job_pids`/`job_names` arrays, `failed_files` collection, per-job log files under `mktemp -d` temp dir (no interleaved output), individual job reaped as batch limit reached AND full drain at the end, per-file `Failed to download <file>` warns after the batch (same style as sequential path), `trap 'rm -rf "$temp_dir"' EXIT` + explicit `rm -rf` + `trap - EXIT` so temp dirs never survive.
|
||||
- Removed dead helpers `err_with_context`, `hf_download_file`, `run_parallel_download`.
|
||||
|
||||
Probes (poisoned fake curl failing only `Q8_0/model-q8.gguf`):
|
||||
- 2-file parallel download: 1 success + 1 failure — batch rc 0, `[!] Failed to download Q8_0/model-q8.gguf` reported at end, successful file on disk, **zero stray `/tmp/tmp.*` dirs** after exit.
|
||||
- Known limit (pre-existing, noted not in review scope): summary line counts *attempted* files (`2 files, 40 B`) even when one fails — same optimistic counting as the sequential path.
|
||||
|
||||
## Step 5: F5 — version/feature validation guard (pos-ai-server)
|
||||
[DONE]
|
||||
|
||||
- `detect_llama_version <binary>` guarded: missing binary or unreadable `--version` → `unknown`, never errexit (previously called `llama-server --version` directly → crash when binary absent).
|
||||
- Replaced no-op `validate_server_features` with `validate_requested_flags <binary> <version> <flag...>`: greps the binary's actual `--help` output for each **explicitly requested** flag token; first unsupported one errors `installed llama.cpp <version> does not expose <flag> — remove it or upgrade llama.cpp`; unreadable `--help` → warn once and proceed (no hard-fail). Alias-mapped requests dedupe (`--gpu`/`--gpu-layers`/`--n-gpu-layers` all validate `--n-gpu-layers`).
|
||||
- Parse loop records canonical request tokens in `REQUESTED_FLAGS` (defaults/config-derived values NOT validated — only what the user typed).
|
||||
- `cmd_status`: binary guard with actionable error before version probe; version printed via the *resolved* binary path.
|
||||
|
||||
Probes (fake llama-server v0.1.0 whose `--help` omits `--kv-cache` and `--slots`):
|
||||
- `start --model fake.gguf --slots 4` → `ERROR: installed llama.cpp 0.1.0 does not expose --slots — remove it or upgrade llama.cpp`, rc 1 (also proves validation runs before dry-run return).
|
||||
- `status` without llama-server on PATH → `ERROR: llama-server not found — install llama.cpp (...)`, rc 1, no crash.
|
||||
- `status` with shim → `service: stopped`, `version: 0.1.0`, rc 0.
|
||||
|
||||
## Step 6: F6 — hf cache real implementation (pos-ai-hf)
|
||||
[DONE]
|
||||
|
||||
- `cmd_cache {status|clear}` with default `status` (`bin/pos-ai-hf:887-894`); bad action → usage error. Top-level indent of `cmd_cache()` fixed (reviewer style nit).
|
||||
- `cmd_cache_status`: cache dir + model count + total on-disk size (excludes `.hf-meta` — same discovery as `list`/`remove`); empty dir → `Models: 0 (nothing downloaded yet)`, rc 0.
|
||||
- `cmd_cache_clear`: lists models to be removed, then confirm `Remove all downloaded models? [y/N]: ` read from `/dev/tty` (see TL;DR deviation), fail-closed — anything but `y`/`Y` (including EOF) → `Aborted — nothing removed`, rc 0; on `y` removes all model dirs and prints freed size.
|
||||
|
||||
Probes:
|
||||
- `cache status` empty cache: rc 0, `Models: 0 (nothing downloaded yet)`.
|
||||
- `cache status` 2 fake models: `Models: 2 / Size: 5 B`, rc 0.
|
||||
- `cache clear` under `setsid` (EOF, no tty): prompt shown, `Aborted — nothing removed`, rc 0, **models intact**; clean stderr (the `/dev/tty` open error is suppressed: `read -r yn 2>/dev/null </dev/tty` — redirection order matters).
|
||||
- `cache clear` via pty (`script -qec` feeding `y`): list shown, prompt, `Cache cleared (freed 9 B)`, rc 0, model dirs **gone**.
|
||||
|
||||
## Step 7: Docs (DOC/POS.md) + make gen/check/lint
|
||||
[DONE]
|
||||
|
||||
- `DOC/POS.md`: hf download row updated (`--revision` alias, include/exclude glob composition order, example), new `pos ai hf cache [status|clear]` row, server flags+validation detail extended.
|
||||
- `make gen` after touching bin files: tree/dispatch/filetable/completions regenerated (also repairs the pre-existing stale `_pos_flags` for ai-hf/ai-server and picks up the maintainer's untracked `pos-ai-llamacpp` forwarder).
|
||||
- Gates (final state): `bash -n bin/pos-ai-hf bin/pos-ai-server` → OK; `make gen` twice after edits → second run byte-identical (idempotent); `make check` → check-sync: OK; `make lint` → `0 FAIL, 0 WARN`.
|
||||
|
||||
## Files changed (this task)
|
||||
|
||||
- `bin/pos-ai-hf` — F1/F3/F4/F6 (+ dead code removal, style fix)
|
||||
- `bin/pos-ai-server` — F2/F5
|
||||
- `DOC/POS.md` — hf download row, cache row, server flags/validation
|
||||
- `DOC/AGENT_Context_Project.md`, `completions/pos.bash` — generated by `make gen` (auto-rows, flags, subcmd completion)
|
||||
|
||||
Pre-existing worktree drift NOT touched (confirmed untouched in final diff): `AGENT_TODO.md`, `DOC/howto/ai.md`, `bin/pos`, `bin/pos-ai`, untracked `bin/pos-ai-llamacpp`, `AUDIT.md` etc.
|
||||
|
||||
## Remaining risks / deferrals
|
||||
|
||||
- `systemctl --user daemon-reload`/`enable --now` cannot complete inside this container (no systemd user session) — real start verified only up to unit write + `systemd-analyze verify` RC 0; the systemctl calls themselves are otherwise standard.
|
||||
- Summary line after a partially failed parallel download counts attempted files, not successes (pre-existing, matches sequential path; not flagged in review).
|
||||
- `validate_requested_flags` warns-and-proceeds if `--help` cannot be read (deliberate: silent-basic-defaults behavior lost, actionable warning kept).
|
||||
@@ -1,96 +0,0 @@
|
||||
# Builder Report — AI Stabilization Fixes (2026-09-06)
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status: IMPLEMENTED** (all steps DONE; gates green; probes pass)
|
||||
- Scope: D-B (eval posture), D-F (pos-ai-server validation), D1 (hf single-file honesty), D2 (LLAMACPP_HOST coherence), M1/M2/M4 (POS metadata), docs (POS.md ai section + howto/ai.md eval section).
|
||||
- Files touched: `bin/pos-ai`, `bin/pos-ai-server`, `bin/pos-ai-hf`, `bin/pos-ai-gemini`, `bin/pos-ai-openrouter`, `bin/pos-ai-llamacpp`, `lib/ai-providers/llamacpp.sh`, `DOC/POS.md`, `DOC/howto/ai.md` + `make gen` output (`completions/pos.bash`, `DOC/AGENT_Context_Project.md`).
|
||||
- Verification: `bash -n` OK on all 7 scripts; `make gen` idempotent (×2); `make check` → `check-sync: OK`; `make lint` → `0 FAIL, 0 WARN (convention lint)`; 7 probe scenarios pass (4 server-validation, 3 hf download), plus 6 functional D-B prompt-posture tests under a pty.
|
||||
- Residual note: `scripts/lint-conventions.sh` is being actively refactored by its owning track (uncommitted, 213-line diff). During my gate sweeps it intermittently flapped (parse error at line 117 early on; later runs briefly reported a stale lint self-test fixture `bin/pos-zz-linttest-broken` that never existed on disk at `ls` time). Final sweep: `0 FAIL, 0 WARN`. None of this track's scripts ever appeared in lint FAIL/WARN lists.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: D-B — AI eval posture (`bin/pos-ai` + help + docs)
|
||||
|
||||
- Flipped `_prompt_run_command` confirm default to deny: prompt is `[y/N]`, only `[yY]` executes; anything else adds to history ("Command added to history — press ↑ to recall, edit, and run.").
|
||||
- Non-tty fail-safe kept (`[ -w /dev/tty ] || return 0`) and verified it sits BEFORE the trusted auto-exec branch, so `--trust` cannot auto-execute without a tty.
|
||||
- Added `--no-command-execution` flag (init `NO_EXEC=0`, parsed in arg loop, `# POS_FLAGS:` header, usage row, help text). When set, `_prompt_run_command` prints the block + "command execution disabled (--no-command-execution)" + the command, returns 0 — never executes.
|
||||
- `--trust`/`--no-command-execution` last-one-wins in the arg loop (`--trust` sets TRUST_MODE=1 NO_EXEC=0; `--no-command-execution` sets NO_EXEC=1 TRUST_MODE=0).
|
||||
- Help text for `--trust` updated: interactive-terminals-only semantics.
|
||||
- Docs: `DOC/howto/ai.md` (shared-flags mention + new "Command execution posture" section), `DOC/POS.md` (command execution posture paragraph after Messaging bridges).
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 2: D-F — validate ALL ExecStart flags (`bin/pos-ai-server`)
|
||||
|
||||
- Added `CONFIG_REQUESTED_FLAGS` tracking right after `load_config` (before CLI parse): config/env-set `LLAMACPP_PORT/LLAMACPP_HOST/LLAMACPP_CTX_SIZE/LLAMACPP_GPU_LAYERS/LLAMACPP_THREADS` map to `--port/--host/--ctx-size/--n-gpu-layers/--threads` via `requested_from_env_config` (dedupes).
|
||||
- `cmd_start` merges CLI `REQUESTED_FLAGS` + `CONFIG_REQUESTED_FLAGS`, dedupes, hard-validates the requested set (unsupported → hard `err` naming flag + detected version).
|
||||
- New `validate_default_flags` checks the 5 always-emitted defaults NOT in the requested set against `--help`; unsupported default → single `warn` + omit (sets `DEFAULT_PORT_OK/DEFAULT_HOST_OK/DEFAULT_GPU_OK/DEFAULT_CTX_OK/DEFAULT_THREADS_OK`); unreadable `--help` → `warn` + all OK=1.
|
||||
- `exec_cmd` only emits flags whose `DEFAULT_*_OK=1`; requested (hard-validated) flags always emitted.
|
||||
- D4: word-boundary match `(^|[[:space:]])flag([[:space:]]|=|$)` in both `validate_requested_flags` and `validate_default_flags`.
|
||||
- Docs: POS.md server Flags paragraph rewritten (requested-vs-default validation semantics).
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 3: D1 — `pos-ai-hf` single-file failure honesty
|
||||
|
||||
- Single-file path now records failures into `failed_files` (was `warn; continue` only); removed the dead `if [ "$file_count" -gt 1 ]` branch (multi-file no longer touches the sequential path).
|
||||
- `.hf-meta` suppressed when `failed_files` non-empty (pre-existing guard, now effective for single-file too): "Not writing .hf-meta — <repo> is incomplete (K file(s) failed)".
|
||||
- Summary rewritten: any failure yields honest "📥 Downloaded: <repo> (N of M files, K failed: ...)" + "📁 <dir>/"; single-file successful summary kept ("<repo>/<fname> (size)") only when no failures.
|
||||
- `return 1` now fires for single-file failures too (was parallel-only), so scripts can detect partial failures on both paths.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 4: D2 — `LLAMACPP_HOST` coherence
|
||||
|
||||
- `lib/ai-providers/llamacpp.sh`: `provider_default_model`, `provider_generate`, `provider_models_list` all use `host="${LLAMACPP_HOST:-127.0.0.1}"` + `port="${LLAMACPP_PORT:-8088}"` (was hardcoded 127.0.0.1).
|
||||
- `bin/pos-ai-server`: `check_health` and `cmd_status` `/v1/models` probe use `$HOST`/`$PORT` (same as the bind address).
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 5: M1/M2/M4 — POS metadata headers
|
||||
|
||||
- M1: `bin/pos-ai-hf` gained `# POS_SUBCMDS: search download list remove info files cache`.
|
||||
- M2: `bin/pos-ai` `# POS_SUBCMDS:` gained `llamacpp` (real dispatched subcommand — `pos ai llamacpp`).
|
||||
- M4: forwarders — openrouter `ask chat sessions capture models providers`; gemini `ask chat models sessions capture providers`; llamacpp `ask chat models sessions capture providers` (all passthrough to `pos ai` already worked; headers now advertise the full set).
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
### Gates
|
||||
|
||||
- `bash -n` on all 7 touched scripts: PASS (all OK).
|
||||
- `make gen` ×2: PASS, idempotent (`gen-docs: write OK` both runs).
|
||||
- `make check`: PASS — `check-sync: OK`, rc 0.
|
||||
- `make lint`: PASS — `0 FAIL, 0 WARN (convention lint)`, rc 0.
|
||||
- Note: an earlier run failed because working-tree `scripts/lint-conventions.sh` (modified by another track, off-limits) had a `[[ =~ .*<<-?… ]]` parse error at line 117. That track has since fixed it; gate green. Head-version lint also passed this track's scripts when the issue was live.
|
||||
|
||||
### Probes — pos-ai-server flag validation (stub llama-server, version 1.2.3; env seams `CONFIG_FILE`, `USER_SYSTEMD_DIR`, `DRY_RUN=1`)
|
||||
|
||||
1. `--split-mode` (unknown CLI flag) → rc 1, `ERROR: Unknown option '--split-mode' (see --help)`.
|
||||
2. `--tensor-split 1:2:3` with help lacking it → rc 1, `ERROR: installed llama.cpp 1.2.3 does not expose --tensor-split — remove it or upgrade llama.cpp`.
|
||||
3. Config `LLAMACPP_CTX_SIZE=2048` with help lacking `--ctx-size` → rc 1, `ERROR: installed llama.cpp 1.2.3 does not expose --ctx-size — remove it or upgrade llama.cpp`.
|
||||
4. Help lacking `--threads` (never requested) → rc 0, warning `installed llama.cpp 1.2.3 does not support default flag --threads — omitting it from the unit`; dry-run ExecStart contains NO `--threads` (only `--port 8088 --host 127.0.0.1 --n-gpu-layers 0 --ctx-size 4096`).
|
||||
5. No llama-server on PATH → rc 1, `ERROR: llama-server not found — install llama.cpp … see 'pos help ai server'`.
|
||||
|
||||
### Probes — pos-ai-hf download (stub curl; HF_DOWNLOAD_DIR temp dir)
|
||||
|
||||
6. Single-file download, `/resolve/` fails → rc 1; stderr shows `Failed to download model.gguf`, `Not writing .hf-meta — ns/test-model is incomplete (1 file(s) failed)`; summary `📥 Downloaded: ns/test-model (0 of 1 files, 1 failed: model.gguf)`; NO `.hf-meta` written.
|
||||
7. Single-file success control → rc 0, meta written (`{"repo_id": … ,"files":["model.gguf"]}`).
|
||||
8. Parallel 2-file, one fails → rc 1, honest summary `(1 of 2 files, 1 failed: b.gguf)`, NO `.hf-meta`.
|
||||
|
||||
### Probes — D-B prompt posture (real `_prompt_run_command` body extracted from `bin/pos-ai`, run under `script` pty)
|
||||
|
||||
- Non-tty (plain bash, no pty): nothing executes — `/dev/tty` read fails → auto-decline, rc 0.
|
||||
- `NO_EXEC=1` pty: prints "command execution disabled (--no-command-execution)", prints command, no execution.
|
||||
- `--trust` pty: auto-executes without prompt. Non-tty `--trust`: no execution.
|
||||
- `y`: executes. Enter / `n`: declines, adds to history, no execution.
|
||||
|
||||
### Remaining risks / notes
|
||||
|
||||
- `--split-mode` probe hits the arg-parser path (pre-existing behavior), not the D4 validation path; genuinely unsupported requested flags are covered by probes 2/3.
|
||||
- `--model`/`-m` and `--gpu-threads` handling predates this change and is unchanged; only flags listed in `REQUIRED_FLAGS`/defaults are validated.
|
||||
- `DOC/POS.md` and the gen artifacts also carry concurrent edits from the other active tracks (app installer message, listeners, etc.) — not part of this report's scope.
|
||||
@@ -1,152 +0,0 @@
|
||||
# Builder Report — 2026-09-06 — D-D Config-Loader Centralization
|
||||
|
||||
## TL;DR
|
||||
|
||||
Status: IMPLEMENTED
|
||||
|
||||
Objective: Canonical `load_env_file` in `lib/config-ui.sh`; migrate 9 hand-rolled loaders; collapse entertainment-lib read/write; decide load_system_env/download/docker-compose adoption; docs; probes.
|
||||
|
||||
Files changed (this task): `lib/config-ui.sh` (+loader, +supersession note), 9 migrated tools (`pos-ai`, `pos-ai-hf`, `pos-ai-server`, `pos-communication-{telegram-sender,matrix-listener,telegram-listener,matrix-sender,scrcpy}`, `pos-media-grab`), `pos-network-download` (`load_secret` + inline duplicate), `lib/entertainment-lib.sh` (collapsed helpers + config-ui.sh source), `DOC/DEV.md` (canonical-loader paragraph), `DOC/AGENT_Context_Project.md` (hand-maintained line-count row via `make gen`), this report.
|
||||
|
||||
Verification: `bash -n` all touched files OK; `make gen` idempotent (zero diff between runs); `make check` OK; `make lint` 0 FAIL, 0 WARN; behavioral probes: telegram-sender + pos-ai + pos-network-download env-wins/CRLF/comments OK; ai-server D-F tracking survives (hard error names `--ctx-size`); entertainment-lib wrapper smoke OK (read/quote-strip/CR-strip/write/delete/chmod-600); docker-compose confirmed NOT migrated (`source` semantics preserved).
|
||||
|
||||
Follow-ups (out of scope, flagged): `bin/pos-network-download` NET_PROBE line (pre-existing, from a parallel uncommitted change) crashes under `set -u` — `$1`/`$2` in a single-quoted default inside `${NET_PROBE:-...}` are expanded by the outer shell; every invocation without the env var set hits "unbound variable". Owner: the parallel track that introduced `exec 3<>/dev/tcp/$1/$2`. Also residual hand-rolled loops remain ONLY in `lib/common.sh` (`load_system_env` — kept per D-D) and `lib/entertainment-plugin-lib.sh` (plugin-only lib, must stay self-contained — by design).
|
||||
|
||||
---
|
||||
|
||||
## Step 0: Scope confirmation
|
||||
|
||||
Read the inputs per AGENTS.md before touching code: `AgentsReport/architect/2026-09-06_stabilization-design.md` (D-D section, esp. the loader contract at ~line 310-350 and the XDG-path replacement rule at line 326-328), `AgentsReport/explorer/2026-09-06_tooling-audit.md` (Task 3) + `AgentsReport/explorer/2026-09-06_ai-audit.md` (M3), plus the current working-tree state of every target file (repo carries uncommitted changes from parallel tracks D-A/D-B/D-F/security — always read before editing).
|
||||
|
||||
Rules honored: don't touch `scripts/lint-conventions.sh`, `bin/pos-system-uninstall`, `tests/`, `apps/`, `AGENT_TODO.md`, listeners' authorization logic; `DOC/AGENT_Context_Project.md` generated blocks only through `make gen`; gates = `bash -n` + `make gen`×2 idempotent + `make check` + `make lint` (0 FAIL, 0 WARN); self-contained tools get `source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh"`.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 1: `load_env_file` in `lib/config-ui.sh`
|
||||
|
||||
Added the canonical loader immediately before `cfg_value` (now `lib/config-ui.sh:331-356`). Contract, matching the D-D design and every migrated tool's historic behavior:
|
||||
|
||||
- `load_env_file <file> [scope]` — `[scope]` is informational/reserved.
|
||||
- A bare basename (no `/`) resolves under `${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}/`; full paths used as-is.
|
||||
- Reads `^[A-Z_]+=` lines; skips blank + `#` comments; strips CR from every value (CRLF-safe); trims one pair of surrounding quotes.
|
||||
- Exports a key ONLY when `${!k:-}` is empty → env always wins over file (precedence: CLI flags > env > file > defaults — CLI/defaults stay in the tools).
|
||||
- Appends loaded keys to global `LOADED_ENV_KEYS` (init guarded via `declare -p`, `set -u`-safe).
|
||||
- Never creates files, never chmods (chmod-600 stays with `cfg_write`); missing/unreadable file = quiet no-op, rc=0.
|
||||
- Doc comment notes it SUPERSEDES `lib/common.sh`'s `load_system_env()` (kept for its 3 existing callers per D-D §"Deprecate load_system_env"; `common.sh` must NOT source config-ui.sh — circular-dependency risk).
|
||||
|
||||
Smoke-tested in `/tmp/opencode/cfg-probe` before wiring tools: env-wins, CRLF strip, comments, XDG basename vs absolute path, missing-file rc=0, `LOADED_ENV_KEYS` content (only actually-loaded keys).
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Migrate the 9 hand-rolled loaders
|
||||
|
||||
Per-tool migration (each = add config-ui.sh source + replace the hand-rolled while-read body with `load_env_file`, preserving tool-specific extras):
|
||||
|
||||
| Tool | Before (loader) | After | Tool-specific preservation |
|
||||
|---|---|---|---|
|
||||
| `pos-communication-telegram-sender` | inline env-wins loop (no CR strip) | `load_config() { load_env_file "$CONFIG_FILE"; }` | — (self-contained: owns guarded CONFIG_DIR) |
|
||||
| `pos-communication-matrix-listener` | inline env-wins loop (no CR strip) | `load_config() { load_env_file "$CONFIG_FILE"; }` | — |
|
||||
| `pos-communication-telegram-listener` | inline env-wins loop (no CR strip) | `load_config() { load_env_file "$CONFIG_FILE"; }` | — |
|
||||
| `pos-communication-matrix-sender` | inline env-wins loop + CR strip | `load_config() { load_env_file "$CONFIG_FILE"; }` | — |
|
||||
| `pos-communication-scrcpy` | inline env-wins loop + CR strip; hardcoded `$HOME/.config/.../scrcpy.env` | `load_config() { load_env_file "$CONFIG_FILE"; }`; `CONFIG_FILE="$CONFIG_DIR/scrcpy.env"` | XDG path fix (design line 328) |
|
||||
| `pos-ai` | `load_config` loop + legacy two-file loop | `load_env_file "$CONFIG_FILE"` + legacy loop stays as second `load_env_file` per legacy path | legacy `LEGACY_GEMINI_CONFIG`/`LEGACY_OPENROUTER_CONFIG` loop structure kept; both legacy vars now `:-`-guarded with `$CONFIG_DIR` defaults; `CONFIG_FILE` gets env-override seam (`${CONFIG_FILE:-$CONFIG_DIR/ai.env}`) |
|
||||
| `pos-ai-server` | `load_config` loop + CR strip | `load_env_file "$CONFIG_FILE"` | D-F tracking untouched: `CONFIG_REQUESTED_FLAGS`/`requested_from_env_config` is VALUE-based (reads exported LLAMACPP_* after load) → unaffected by loader; `CONFIG_FILE="${CONFIG_FILE:-$CONFIG_DIR/ai.env}"` keeps the env-override seam (`tests/t-ai-server-flags.sh` passes `CONFIG_FILE=…`) and fixes XDG |
|
||||
| `pos-ai-hf` | `load_hf_config` loop + CR strip | `load_hf_config() { load_env_file "$CONFIG_FILE"; }` | `CONFIG_FILE` `:-`-guarded, `$CONFIG_DIR` default |
|
||||
| `pos-media-grab` | `load_grab_config` loop (no CR strip); hardcoded `$HOME/.config/.../grab.env` | `load_grab_config() { load_env_file "$CONFIG_DIR/grab.env"; }` | XDG path fix |
|
||||
|
||||
Acceptance criterion 1 (D-D): `grep -rn 'while IFS.*read.*k.*v' bin/pos-communication-* bin/pos-ai* bin/pos-media-grab` → **zero matches** (only `lib/config-ui.sh:346` — the canonical loader — and `lib/common.sh:149` `load_system_env` + `lib/entertainment-plugin-lib.sh:25` remain anywhere in bin/+).
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 3: `pos-network-download` `load_secret` + inline duplicate
|
||||
|
||||
- Added config-ui.sh source.
|
||||
- `load_secret()` now `load_env_file "$CONFIG_FILE"` + `RPC_SECRET="${RPC_SECRET:-}"` normalization (kept) — env-wins identical to the old guard; CR/quote-strips are no-ops for the machine-written unquoted hex file.
|
||||
- The duplicate inline `grep RPC_SECRET` in `cmd_start` replaced with a `load_secret` call (guarded on `[ -z "$RPC_SECRET" ]` like before).
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 4: `pos-docker-compose` — NOT migrated (deliberate)
|
||||
|
||||
`load_global_config` still `source "$CONFIG_ENV"` (`bin/pos-docker-compose:90`). Converting to `load_env_file` would change behavior: the compose.env file is shell-EXECUTED (variable expansion, file-beats-defaults layering, and `config set` writes unquoted `TS_AUTHKEY=…` values that the loader's quote/CR handling + env-wins would invert). Documented as the deliberate exception in `DOC/DEV.md` (new canonical-loader paragraph). Also noted but NOT changed (out of D-D loader scope): `pos-docker-compose:10` still hardcodes `$HOME/.config/...` for `CONFIG_ENV`.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 5: `lib/entertainment-lib.sh` collapse
|
||||
|
||||
- `config_value`/`write_config_key` are now thin wrappers over `cfg_value`/`cfg_write` (`lib/entertainment-lib.sh:33-43`) — identical call signatures, chmod-600 preserved, `"-"` delete supported.
|
||||
- entertainment-lib now sources `config-ui.sh` via a 4-way fallback chain mirroring the user-timers source block.
|
||||
- Documented minor delta: `cfg_write` emits a stderr-only warning on multi-line paste and truncates to the first line (old `write_config_key` truncated silently); write RESULT identical.
|
||||
- `config_value` now also strips a trailing CR (old entertainment version did not) — alignment with the other migrated tools.
|
||||
- Pre-existing contract documented in the lib header: must be sourced AFTER `lib/common.sh` (defines CONFIG_DIR); verified still true.
|
||||
- Verified callers: `bin/pos-entertainment-config` (1-arg `config_value`, 2-arg `write_config_key`, `"-"` delete at line 77) and `bin/pos-entertainment-status` all match the new signatures.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Docs
|
||||
|
||||
- `DOC/DEV.md` §"Config files (if needed)": added the canonical-loader bullet (precedence contract, CR/quote handling, LOADED_ENV_KEYS, XDG basename resolution, fallback source pattern, load_system_env supersession, docker-compose exception).
|
||||
- `DOC/AGENT_Context_Project.md`: hand-maintained `lib/entertainment-lib.sh` line-count row updated (311 → 300); `make gen` regenerated the filetable counts for the migrated tools (byte-order deterministic: second run produced zero diff) and `completions/pos.bash`.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Gates
|
||||
|
||||
- `bash -n` on all 12 touched code files: OK.
|
||||
- `make gen`: run 1 vs run 2 diff — zero bytes (idempotent).
|
||||
- `make check` (`scripts/check-sync.sh`): OK.
|
||||
- `make lint` (`scripts/lint-conventions.sh`): `0 FAIL, 0 WARN`.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 8: Behavioral probes (all PASS)
|
||||
|
||||
Stub harness in `/tmp/opencode/cfg-probe/` (XDG_CONFIG_HOME + stub curl/llama-server/systemctl/aria2c; throwaway per DEV.md):
|
||||
|
||||
1. **telegram-sender wiring** — telegram.env with `# comment`, blank line, `TELEGRAM_BOT_TOKEN=file-token\r\n`, `TELEGRAM_CHAT_ID=1289`; exported `TELEGRAM_CHAT_ID=env-chat`. Stub curl logged: `botfile-token` in URL (CR stripped, comment skipped) and `chat_id=env-chat` (env wins over file). Zero CR anywhere.
|
||||
2. **pos-ai wiring** — ai.env with `AI_PROVIDER=gemini` + `AI_GEMINI_API_KEY=file-key\r\n`; `pos ai ask hello` with stub curl → `stub-reply` printed; header `x-goog-api-key: file-key` (CR stripped from file). With `AI_GEMINI_API_KEY=env-key` exported → header `env-key` (env wins).
|
||||
3. **ai-server D-F survival** — ai.env `LLAMACPP_CTX_SIZE=8192\r\n`; stub `llama-server` whose `--help` lacks `--ctx-size`; `pos-ai-server start whatever.gguf` → `ERROR: installed llama.cpp 1.2.3 does not expose --ctx-size — remove it or upgrade llama.cpp`, exit 1. D-F "hard error on config/env-requested but unsupported flag" contract intact after the loader migration.
|
||||
4. **network-download load_secret** — download.env `RPC_SECRET=file-secret\r\n`; daemon faked active via stub systemctl; `status` → RPC payload `token:file-secret` (CR stripped, from file); with `RPC_SECRET=env-secret` exported → `token:env-secret` (env wins).
|
||||
5. **docker-compose** — spot-confirmed `source "$CONFIG_ENV"` unchanged (deliberate).
|
||||
6. **entertainment-lib wrappers** — direct smoke: `config_value` read (CR strip), quote-stripped read, `write_config_key` overwrite, `"-"` delete, chmod-600 preserved; plus `pos entertainment status` end-to-end (ENABLED `weather,1h` parsed, "every 1h" interval).
|
||||
7. **residual-loader rate** — only canonical `load_env_file` + kept `load_system_env` + plugin-only `entertainment-plugin-lib` remain.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 9: Discoveries (out of scope, flagged — not fixed)
|
||||
|
||||
- `bin/pos-network-download` NET_PROBE default `timeout 3 bash -c 'exec 3<>/dev/tcp/$1/$2' _ 8.8.8.8 53` (introduced by a PARALLEL uncommitted change; HEAD has `'</dev/tcp/8.8.8.8/53>'`): under `set -u`, the single quotes inside `${NET_PROBE:-…}` are literal, so the OUTER shell expands `$1`/`$2` (unset) → `line 31: $2: unbound variable` on every run without the env var set. Real blocker for the tool's runtime; owner = the parallel track (likely the retry/healer work). D-D probes bypassed it by exporting a non-empty NET_PROBE — do not fix here (unapproved component boundary, unclear ownership).
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Scope compliance
|
||||
|
||||
In-scope: loader addition + 9-tool migration + network-download load_secret + entertainment-lib collapse + config-ui.sh supersession note + DOC/DEV.md canonical-loader paragraph + hand-maintained line-count row + report. Out-of-scope changes: none (NET_PROBE bug intentionally NOT fixed; docker-compose NOT migrated; `load_system_env` NOT rewritten).
|
||||
|
||||
Remaining risks / follow-ups:
|
||||
1. NET_PROBE `set -u` crash (see Step 9) — needs the owning parallel track / Architect decision.
|
||||
2. `pos-docker-compose` `CONFIG_ENV` still hardcodes `$HOME/.config/…` (pre-existing; XDG seam gap for that tool; explorer-flagged, not in D-D's 9).
|
||||
3. `lib/entertainment-plugin-lib.sh:25` keeps its own read loop (plugin contract: plugins must not source config libs) — an intentional residual, could get a documented rationale later.
|
||||
4. `load_system_env` remains duplicated logic in `lib/common.sh:146-159` — supersession documented, legacy callers (`pos system health/backup`, `pos media sync`) untouched.
|
||||
|
||||
Recommended next agent: **Reviewer** (independent adversarial review of the loader migration + wrapper collapse) — or **Orchestrator** if the NET_PROBE ownership question should be routed to the owning track first. Not handed to Architect: no scope/design boundary dispute remains after the decisions above were recorded.
|
||||
@@ -1,70 +0,0 @@
|
||||
# Builder report — 2026-09-06 stabilization security track (D-A, V3, V5, V7)
|
||||
|
||||
## TL;DR
|
||||
|
||||
Status: **IMPLEMENTED** — all approved changes done, gates green, 23/23 probes PASS.
|
||||
Scope: architect decision D-A (telegram owner gate, matrix fail-closed) + explorer findings V3 (backup gpg argv leak), V5 (/dev/tcp host injection), V7 (documented notice); plus template-creation and two hygiene fixes listed in the brief.
|
||||
Verified: `bash -n` on all touched scripts, `make gen`, `make check` (check-sync: OK), `make lint` (0 FAIL, 0 WARN), runtime registry smoke (`pos config telegram` scope shows `TELEGRAM_OWNER_ID`), and a 23-assertion probe harness (fail-closed telegram/matrix behavior, gpg fd + cleanup, host-injection attempts) — all PASS.
|
||||
Files changed: 9 code/doc files in scope, 2 new config templates, plus `DOC/AGENT_Context_Project.md` + `completions/pos.bash` from the required `make gen`.
|
||||
|
||||
## Step 1: D-A telegram listener owner gate — [DONE]
|
||||
|
||||
- `bin/pos-communication-telegram-listener`: per-message authorization replaced by fail-closed AND-gate: message processed only when `chat == TELEGRAM_CHAT_ID` AND `from_id == TELEGRAM_OWNER_ID`; unauthorized → warn-only, skip (no reply) (`bin/pos-communication-telegram-listener:805`).
|
||||
- Owner unset → daemon runs but every command is ignored with warn "TELEGRAM_OWNER_ID unset — ignoring command" (`bin/pos-communication-telegram-listener:798`); startup logs "commands authorized" or warns fail-closed; run line now logs owner (`bin/pos-communication-telegram-listener:518-530` region).
|
||||
- `bin/pos-communication-telegram-sender` registry line gained `TELEGRAM_OWNER_ID=digits:Numeric Telegram user id (your account) allowed to run chat commands`; usage() config block updated.
|
||||
- Probes 1a–1d prove: owner-unset → warning + no send; right chat+user → executes; wrong user → ignore; wrong chat → ignore. 8/8 PASS.
|
||||
|
||||
## Step 2: D-A matrix listener fail-closed — [DONE]
|
||||
|
||||
- `bin/pos-communication-matrix-listener`: with `MATRIX_ROOM_ID` unset the daemon runs but shows `room none — fail-closed` and watches NO room; the room filter is now fail-closed (`[ -z "$room_only" ] || [ "$room" != "$room_only" ]`), startup warn at `bin/pos-communication-matrix-listener:519`.
|
||||
- Probe 2 (owner message, room unset) proves: warning logged, run line shows fail-closed, nothing answered. 3/3 PASS.
|
||||
|
||||
## Step 3: Config templates + sender registry — [DONE]
|
||||
|
||||
- `config/telegram.env` and `config/matrix.env` created as commented reference templates (config/ai.env style) since no templates existed.
|
||||
- Verified `postinstall.sh:22-50` copies only explicitly named `entertainment.env/system.env/notify.env/ai.env` (no glob) — templates are NOT auto-installed; runtime provisioning remains `pos config telegram` / `pos config matrix`. Residual gap: postinstall.sh does not copy the new templates (out of scope).
|
||||
|
||||
## Step 4: V3 backup gpg passphrase fd — [DONE]
|
||||
|
||||
- `bin/pos-system-backup`: both gpg calls use `--passphrase-fd 3` + `3<<<"$PASS"` (`bin/pos-system-backup:197`, `:207`); failed encrypt removes the plaintext archive + err "encryption failed — plaintext archive removed, nothing left behind"; failed verify removes the corrupt `.gpg` + err; `unset PASS` retained.
|
||||
- Probes: gpg argv has `--passphrase-fd 3`, no `--passphrase <value>`, secret absent from argv/logs; success path leaves only `.gpg` (chmod 600); fail path leaves neither plaintext nor partial artifact, with honest error. 8/8 PASS.
|
||||
|
||||
## Step 5: V5 /dev/tcp host injection — [DONE]
|
||||
|
||||
- Positional-arg form everywhere a remote host reaches `bash -c 'exec 3<>/dev/tcp/…'`:
|
||||
- `bin/pos-network-checkport:135` (check_tcp), `:168`/`:170` (banner probes) — `_ "$ip" "$port"`.
|
||||
- `bin/pos-share-smb-client:94` — `_ "$host" "$SMB_PORT"`.
|
||||
- `lib/share-lib.sh:61` (share_port_probe, shared core) — `_ "${1}" "${2}"`.
|
||||
- `bin/pos-network-download:28` — default `NET_PROBE` now `timeout 3 bash -c 'exec 3<>/dev/tcp/$1/$2' _ 8.8.8.8 53`.
|
||||
- POS.md NET_PROBE doc updated at line 188 (also fixed pre-existing missing `>` in the doc example).
|
||||
- Probes: hostile host `8.8.8.8;touch …` passed as ONE literal arg, probe source uses `$1/$2`, no marker file created, hostile connect fails harmlessly; same for `share_port_probe` and smb-client `probe_server`. 8/8 PASS.
|
||||
|
||||
## Step 6: V7 notice + doc/hygiene updates — [DONE]
|
||||
|
||||
- `DOC/howto/communication.md`: one-time Telegram setup notes `TELEGRAM_OWNER_ID` (set via `pos config telegram`) and the inherent token-in-argv caveat of Bot API URLs (revoke if leaked); owner-only bullet for chat commands; matrix self-messaging bullet.
|
||||
- POS.md: telegram-listener row (owner id), matrix-listener row (fail-closed), backup row (passphrase on internal fd — never argv), telegram/matrix paragraphs (~372/402).
|
||||
- Hygiene: `apps/ai/llamacpp.sh` chmod 755 (still untracked); `DOC/APPS.md` line 3 app count 16 → 18.
|
||||
|
||||
## Step 7: Gates + probes — [DONE]
|
||||
|
||||
- `bash -n` on all 10 touched scripts: OK.
|
||||
- `make gen` OK (required by POS_CONFIG change; also folded in parallel-track drift), then regeneration re-verified.
|
||||
- `make check`: OK. `make lint`: 0 FAIL, 0 WARN.
|
||||
- Probe harness `/tmp/opencode/probe-stab.sh` (23 assertions): **PASS=23 FAIL=0**. Harness uses stub `curl`/`gpg`/`sudo` (secret-free argv assertions), extracted real function bodies verbatim for checkport/smb-client/share-lib, and verifies no marker file is created by hostile hosts.
|
||||
|
||||
## Residual risks / follow-up
|
||||
|
||||
- `config/telegram.env` / `config/matrix.env` are reference templates only — not wired into `postinstall.sh` (out of scope; installer currently copies only 4 explicit env files).
|
||||
- Existing deployments without `TELEGRAM_OWNER_ID` / `MATRIX_ROOM_ID` now ignore all chat commands (INTENDED fail-closed; startup warn tells the operator to run `pos config telegram` / `pos config matrix`).
|
||||
- Matrix-sender unchanged (its registry already listed MATRIX_ROOM_ID).
|
||||
- Parallel tracks still dirty in git (ai track, app templates, lint-conventions, AGENT_TODO, docs) — not touched here.
|
||||
|
||||
## Files changed (this track only)
|
||||
|
||||
- `bin/pos-communication-telegram-listener`, `bin/pos-communication-telegram-sender`, `bin/pos-communication-matrix-listener` (D-A)
|
||||
- `bin/pos-system-backup` (V3)
|
||||
- `bin/pos-network-checkport`, `bin/pos-share-smb-client`, `lib/share-lib.sh`, `bin/pos-network-download` (V5)
|
||||
- `config/telegram.env`, `config/matrix.env` (new templates)
|
||||
- `DOC/POS.md`, `DOC/howto/communication.md`, `DOC/APPS.md` (docs)
|
||||
- `DOC/AGENT_Context_Project.md`, `completions/pos.bash` (make gen output)
|
||||
- `apps/ai/llamacpp.sh` (mode 755 only)
|
||||
@@ -1,100 +0,0 @@
|
||||
# Builder Report — 2026-09-06 — Stabilization: Tooling track (lint perf, D-E uninstall, H-002/H-003)
|
||||
|
||||
**Scope (per brief):** `scripts/lint-conventions.sh`, `bin/pos-system-uninstall`, `DOC/POS.md` (system uninstall row only if factual); NOT touched: other files listed in the brief. `lib/install-manifest.sh` is **NOT** created — see D-E.
|
||||
|
||||
**TL;DR**
|
||||
- Status: **DONE** — all 3 steps implemented, probed, gated.
|
||||
- Files in scope changed by me: `scripts/lint-conventions.sh` (rewrite), `bin/pos-system-uninstall` (D-E/H-002/H-003), `DOC/POS.md` (system-uninstall row only), plus gen output in `DOC/AGENT_Context_Project.md` + `completions/pos.bash` (reflecting the whole shared tree, incl. parallel tracks).
|
||||
- D-E explicitly says **do NOT** create `lib/install-manifest.sh` — the brief's manifest is conditional ("if D-E specifies it"); it does not, so I implement D-E's actual decision (extend `bin/pos-system-uninstall` directly, no manifest file).
|
||||
- Baseline `make lint` = **145.9 s** (measured, `time`); final = **4.38 s**, `0 FAIL, 0 WARN`, output byte-identical to original (differential test). `make check` OK; `make gen` idempotent.
|
||||
- H-002/H-003 probes pass: exact-literal + anchored `awk` removal removes only pos-owned lines; comments, user PATH/MY_POS_REPO lines, `repos`/`dispose`/`compose` completion, and `unrelated.service` all survive.
|
||||
- Remaining risk: real-machine removal path validated via fixtures only (no live uninstall run) — see Step 3.
|
||||
|
||||
## Step plan
|
||||
1. Lint performance rewrite (Task 1).
|
||||
2. D-E uninstall extensions + H-002/H-003 + surviving-artifacts docs (Tasks 2, 3).
|
||||
3. Probes + full gates.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Lint performance rewrite
|
||||
|
||||
Refactored `scripts/lint-conventions.sh` hot paths to bash-native parsing:
|
||||
- Merged per-line `uses_stdin` + top-level-`local` scans (was `printf | sed | tail` × 3 forks per line) into ONE single-pass per-file read with heredoc-delimiter state via `[[ =~ ]]`.
|
||||
- Folded `# POS:` header / em-dash / posline / first `-h|--help` line into the same loop.
|
||||
- Shebang via `read -r first < "$f"`; POS.md coverage via one preloaded `cat` + `[[ ]]`; secret-literal + system-path scans → bash `[[ =~ ]]` with cheap string gates; deleted dead `last_line()`.
|
||||
- System-path outer filter (`(\btee\b|>>?)[^#]*?(path)`) reimplemented faithfully (`_syspath_outer` — no fork, global `_SI`), preserving the no-`#`-between-operator-and-path semantics that bash `=~` cannot replicate.
|
||||
|
||||
**Verification:**
|
||||
- `bash -n` OK.
|
||||
- Differential test vs `git show HEAD:scripts/lint-conventions.sh`: **outputs byte-identical** (both exit 0, `0 FAIL, 0 WARN`).
|
||||
- Negative test (broken tool): all expected FAIL/WARNs fire; skipped per-file checks on missing POS header match original `continue`.
|
||||
- Timings (`time bash scripts/lint-conventions.sh`): **before 145.9 s → after 4.79 s (~30x)**. `time make lint` before: >120 s (timed out at 145.9 s for the bare script); after: see gates below.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 2: D-E install/uninstall + H-002/H-003
|
||||
|
||||
### D-E reconciliation — NO `lib/install-manifest.sh`
|
||||
D-E (architect decision `stabilization-design.md:399-401`) explicitly says: *"Do NOT introduce a full manifest abstraction (like `lib/install-manifest.sh`) — that would be a larger refactor inconsistent with the current 'list-based' approach"*. The brief's manifest requirement was conditional ("If D-E specifies `lib/install-manifest.sh`"); it does not. Per the scope rule (necessary-to-complete vs better-design), I implemented D-E's actual decision: **one** source of truth inside `bin/pos-system-uninstall` (singleton `POS_LIBS` array + `PATH_LINE`/`COMPLETION_LINE`/`HOOK_PATTERN` constants + `installed_plugins()` helper) used by both scan and remove — the removal list is no longer two hardcoded places. Nothing else in the codebase was introduced.
|
||||
|
||||
### Changes to `bin/pos-system-uninstall`
|
||||
- **Libs (D-E 1):** `POS_LIBS` = all 12 libs from install.sh phase 2 (`common.sh flags.sh notify.sh entertainment-lib.sh scheduler-lib.sh config-ui.sh user-timers-lib.sh entertainment-plugin-lib.sh usb-lib.sh share-lib.sh menu-lib.sh registry.sh`); scan + remove loops both read the single array. Verified **byte-identical** to install.sh:143.
|
||||
- **ScaleTail + flags store (D-E 2):** scan + remove for `/usr/local/share/linux_post_install/scale-tail/` and `/flags/`, then `rmdir` the parent if empty.
|
||||
- **User systemd units (D-E 3):** scan + remove `$XDG_CONFIG_HOME/$HOME/.config/systemd/user/pos-*` (`disable --now` + `rm`, `|| true` wrapped; `daemon-reload` at end). `unrelated.service` probe survives.
|
||||
- **De-hardcoded entertainment plugins (D-E 4):** `installed_plugins()` discovers installed plugins by `# POS_PLUGIN:` marker (matching install.sh's directory-driven install), used by scan + remove.
|
||||
- **H-002 (D-E 5):** `.bash_completion` `sed -i '/pos/d'` replaced with anchored `awk '/^[[:space:]]*source[[:space:]].*pos\.bash/ { next } { print }'` + mktemp + `chmod --reference` + count.
|
||||
- **H-003 (D-E 6):** `.bashrc` sed removals replaced with exact-literal `awk` removal of postinstall.sh's `PATH_LINE` + `COMPLETION_LINE` and anchored `HOOK_PATTERN='^[[:space:]]*source[[:space:]].*pos-ai-hook\.sh'` (comment-safe per D-E acceptance criterion 5). Note: the old `/linux_post_install.*PATH/d` never removed the real PATH_LINE anyway (that literal has no `linux_post_install` substring); the new code removes the installer's actual line.
|
||||
- **Surviving artifacts (D-E 7):** usage() documents deliberately NOT removed: apt packages, `/usr/local/bin/yt-dlp`, `~/.config/rclone/`, `~/.ssh/authorized_keys` additions, config/data tiers.
|
||||
- **DOC/POS.md:** system-uninstall row Tier 1 description updated (factual): adds user units + ScaleTail + flags store.
|
||||
|
||||
### Probes (all pass)
|
||||
1. **H-002/H-003 fixture `.bashrc` (15 lines incl. unrelated `pos` lines + comments) → 3 lines removed** (PATH_LINE, COMPLETION_LINE, hook source); custom `MY_POS_REPO`, user PATH, `# source pos.bash` comments survive.
|
||||
2. **Fixture `.bash_completion` (7 lines incl. `repos`/`dispose`/`compose`) → 1 line removed** (source pos.bash); `repos`/`dispose`/`compose` and comment survive.
|
||||
3. **scan_tier1 (HOME fixture):** finds exact bashrc/bash_completion lines + `pos-aria2.service`, `pos-entertainment-weather.timer` user units; does NOT find `unrelated.service` or unrelated completion lines.
|
||||
4. **remove block replication:** REMOVED_COUNT=6 (3 bashrc + 1 completion + 2 user units); unrelated content + `unrelated.service` survive; `systemctl --user` fails gracefully (`|| true`, no session).
|
||||
5. **Inventory: POS_LIBS == install.sh lib list** (12 names identical); 13th `lib/pos-ai-hook.sh` is not installed by install.sh phase 2 (correctly not in POS_LIBS).
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Gates + hygiene
|
||||
|
||||
### Gate results (final state)
|
||||
- `bash -n scripts/lint-conventions.sh bin/pos-system-uninstall` → OK.
|
||||
- `make gen` ×2 → idempotent (`gen-docs: write OK` both runs; second run produces the same diff, no drift).
|
||||
- `make check` → `check-sync: OK` (exit 0).
|
||||
- `time make lint` → `0 FAIL, 0 WARN (convention lint)`, **4.38 s** (second run 6.26 s; final measured 4.38 s). Baseline was **145.9 s** for the bare script and >120 s (timeout) for `make lint` → ~30x speedup. Rapid repeat runs no longer leave the shell spinning (no more per-line fork storms).
|
||||
- **Gen output note:** `make gen` regenerated `DOC/AGENT_Context_Project.md` + `completions/pos.bash` from the **shared** working tree. The diff includes my expected `bin/pos-system-uninstall` row-count update (435→517 lines) **plus** the other parallel tracks' already-uncommitted changes (`bin/pos-ai-server`, `bin/pos-ai`, `bin/pos-system-backup`, communication listeners, etc.). No POS headers/dirs were touched by me; gen is byte-order deterministic (`LC_ALL=C`), CI's `git diff --exit-code` will see the whole tree's refresh.
|
||||
|
||||
### Hybrid conflict check (shared working tree)
|
||||
Final `git status` shows many files modified by **parallel Builder tracks** (stab-ai, stab-security: `bin/pos-ai*`, `bin/pos-communication-*`, `bin/pos-network-checkport`, `bin/pos-share-smb-client`, `bin/pos-system-backup`, `lib/share-lib.sh`, `lib/ai-providers/llamacpp.sh`, `apps/install.sh`, `templates/app.sh`, `DOC/howto/*`, `DOC/APPS.md`, `AGENT_TODO.md`, `config/*.env`). These were NOT edited by me — my changed-file set is exactly:
|
||||
1. `scripts/lint-conventions.sh` (rewritten; 213 changed lines vs HEAD)
|
||||
2. `bin/pos-system-uninstall` (D-E + H-002/H-003; 150 changed lines vs HEAD)
|
||||
3. `DOC/POS.md` — only the `pos system uninstall` row (all other rows in the file diff belong to other tracks)
|
||||
4. `DOC/AGENT_Context_Project.md` + `completions/pos.bash` — gen output including, among others, my uninstall row count
|
||||
5. `AgentsReport/builder/2026-09-06_stab-tooling.md` (this report)
|
||||
|
||||
No protected/brief-excluded file was touched by me; no out-of-scope change made.
|
||||
|
||||
### DOC/SCRIPTS.md + DOC/DEV.md
|
||||
Not touched: no manifest was created (D-E rejected it), so the brief's "only if the manifest needs documenting" condition does not apply. No new documentation needed.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Final status
|
||||
|
||||
- **Scope:** lint perf (Task 1) + D-E uninstall coverage (Task 2) + H-002/H-003 safe removal (Task 3) + surviving-artifacts docs (D-E 7). All implemented; no scope expansion.
|
||||
- **Timings:** `make lint` 145.9 s → 4.38 s (~30x). Differential: output byte-identical to original.
|
||||
- **Probes:** all pass (H-002/H-003 fixture removal, scan_tier1 with HOME override, remove-block replication, POS_LIBS==install.sh inventory).
|
||||
- **Gates:** `bash -n` OK, `make gen` idempotent, `make check` OK, `make lint` `0 FAIL, 0 WARN`.
|
||||
- **Deferred/risks:**
|
||||
1. The real-machine removal path (removing live `/usr/local/bin`, `/etc/systemd`, ScaleTail dirs) was NOT executed here — validated via fixture/sandbox probes only. A dry-run pass on a real install is doable via the tool's own scan display (`pos system uninstall` interactive scan shows Tier 1 entries before any removal).
|
||||
2. `lib/pos-ai-hook.sh` (13th lib file) is intentionally not in `POS_LIBS` because install.sh phase 2 doesn't install it; the uninstaller already removes `~/.local/bin/pos-ai-hook.sh` separately — no action.
|
||||
3. Other parallel tracks' edits are interleaved in the shared tree; my in-scope files are `scripts/lint-conventions.sh`, `bin/pos-system-uninstall`, DOC/POS.md (one row).
|
||||
- **Recommended next agent:** Reviewer (independent adversarial review of the uninstall diff + lint rewrite), then Orchestrator to coordinate the shared-tree commit once the parallel tracks land.
|
||||
@@ -1,608 +0,0 @@
|
||||
# UI/UX Specification: `pos ai alias`
|
||||
|
||||
**Date:** 2026-08-26
|
||||
**Author:** Designer
|
||||
**Status:** DESIGN_READY
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
| Aspect | Decision |
|
||||
|--------|----------|
|
||||
| Main menu | `menu_run` with 4 items; alias table rendered above the box |
|
||||
| Create flow | 4-step wizard using `step()` headers; name validated in a loop |
|
||||
| Edit flow | Pick → show current values → per-field prompt with Enter=keep |
|
||||
| Remove flow | Pick → detail display → `confirm … n` (default=NO) |
|
||||
| List output | Column-formatted table to stdout, 80-col safe |
|
||||
| Long prompts | Single-line via `menu_ask_value`; full value stored; display truncated |
|
||||
| Error UX | `warn` + re-prompt (recoverable) or `err` + exit (fatal) |
|
||||
| First-run | Auto-create `.env`+`.sh` on first write; show "No aliases yet" |
|
||||
| Color | CYAN box, GREEN success, YELLOW warn, RED error — same as all `pos` tools |
|
||||
|
||||
**Key design decisions:**
|
||||
|
||||
1. **Edit uses Enter-to-keep** — each field defaults to current value; empty = keep.
|
||||
2. **Remove defaults to NO** — irreversible action gets `confirm … n`.
|
||||
3. **No multi-line prompt input** — `menu_ask_value` is single-line; matches every other `pos` tool.
|
||||
4. **Table preview** shows provider + session + truncated prompt — enough to distinguish at a glance.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Main Menu — `pos ai alias` (no args)
|
||||
|
||||
### Screen Purpose
|
||||
Entry point. Shows existing aliases and offers all actions.
|
||||
|
||||
### ASCII Mockup — With Aliases
|
||||
|
||||
```
|
||||
════════════════════════════════════════════
|
||||
AI Agent Aliases
|
||||
════════════════════════════════════════════
|
||||
1) Create new alias
|
||||
2) Edit existing alias
|
||||
3) Remove alias
|
||||
4) List aliases
|
||||
0) Exit
|
||||
----------------------------------------
|
||||
Choose:
|
||||
```
|
||||
|
||||
Above the menu box (also on stderr), render the alias table:
|
||||
|
||||
```
|
||||
Name Provider Session Prompt
|
||||
─────────── ───────────── ───────────── ──────────────────────────
|
||||
devbot gemini devbot You are a Linux dev ass…
|
||||
code openrouter code You are a code reviewer…
|
||||
─────────── ───────────── ───────────── ──────────────────────────
|
||||
2 alias(es)
|
||||
```
|
||||
|
||||
### ASCII Mockup — Empty (No Aliases Yet)
|
||||
|
||||
```
|
||||
════════════════════════════════════════════
|
||||
AI Agent Aliases
|
||||
════════════════════════════════════════════
|
||||
1) Create new alias
|
||||
2) Edit existing alias
|
||||
3) Remove alias
|
||||
4) List aliases
|
||||
0) Exit
|
||||
----------------------------------------
|
||||
Choose:
|
||||
```
|
||||
|
||||
With a line above the box:
|
||||
|
||||
```
|
||||
[!] No aliases defined yet — create one with option 1.
|
||||
```
|
||||
|
||||
### Table Column Spec
|
||||
|
||||
```
|
||||
Column Width Alignment Truncation
|
||||
───────── ───── ───────── ─────────────────────────────────
|
||||
Indent 2 — —
|
||||
Name 12 left hard-cut (alias names max ~20 by validation)
|
||||
Provider 12 left as-is (short: gemini, openrouter)
|
||||
Session 12 left as-is
|
||||
Prompt 42 left ${prompt:0:40}… (if > 42 chars)
|
||||
```
|
||||
|
||||
**Total width:** 2 + 12 + 1 + 12 + 1 + 12 + 1 + 42 = **83 cols** (safe for 80-col with minor overflow on rare cases; alternatively reduce prompt to 40 → 81).
|
||||
|
||||
**Empty prompt cell:** Show `${DIM}(default)${RESET}` in dim.
|
||||
|
||||
**Row limit:** If > 5 aliases, show first 4 + a final row: ` … and N more`.
|
||||
|
||||
### Behavior
|
||||
|
||||
| Condition | Behavior |
|
||||
|-----------|----------|
|
||||
| Zero aliases | No table; dim warning line; menu options still shown (user can create immediately). |
|
||||
| 1+ aliases | Table above menu box. |
|
||||
| EOF / non-tty | `menu_guard` fails → prints to stderr, rc 1. |
|
||||
| `0`/`q`/`Q` on menu | Clean exit rc 0 (standard `menu_run` behavior). |
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Create Flow
|
||||
|
||||
### Entry Points
|
||||
- `pos ai alias` → menu → option 1
|
||||
- `pos ai alias create` (no name arg — full interactive)
|
||||
- `pos ai alias create <name>` (pre-fills name, skips step 1)
|
||||
|
||||
### ASCII Mockup — Full Interactive Create
|
||||
|
||||
```
|
||||
════════════════════════════════════════════
|
||||
Create AI Agent Alias
|
||||
════════════════════════════════════════════
|
||||
|
||||
[1/4] Alias Name
|
||||
─────────────────────────────────────────
|
||||
Alias name: devbot
|
||||
|
||||
[2/4] Provider
|
||||
─────────────────────────────────────────
|
||||
-- 2 available --
|
||||
1) gemini
|
||||
2) openrouter
|
||||
0) Back
|
||||
----------------------------------------
|
||||
Pick provider [1-2], text=filter, 0=back 1
|
||||
|
||||
[3/4] Session Name
|
||||
─────────────────────────────────────────
|
||||
Session name [devbot]:
|
||||
|
||||
[4/4] System Prompt
|
||||
─────────────────────────────────────────
|
||||
System prompt (empty = use built-in): You are a Linux dev assistant.
|
||||
|
||||
────────────────────────────────────────────
|
||||
Create alias 'devbot'?
|
||||
Provider: gemini
|
||||
Session: devbot
|
||||
Prompt: You are a Linux dev assistant.
|
||||
────────────────────────────────────────────
|
||||
Create alias 'devbot'? [Y/n]: y
|
||||
|
||||
[+] Alias 'devbot' created.
|
||||
Reload shell: source ~/.bashrc
|
||||
```
|
||||
|
||||
### Step-by-Step Behavior
|
||||
|
||||
#### Step 1: Alias Name
|
||||
|
||||
| Input | Behavior |
|
||||
|-------|----------|
|
||||
| Prompt | `menu_ask_value "Alias name" ""` — mandatory, no default |
|
||||
| Empty input | `warn "Alias name cannot be empty"` → re-prompt (loop) |
|
||||
| Invalid format | `warn "Invalid name '$input' — use letters, digits, hyphens, underscores (start with a letter)"` → re-prompt |
|
||||
| Duplicate name | `warn "Alias '$input' already exists — use 'pos ai alias edit $input' instead"` → re-prompt |
|
||||
| `<name>` arg given | Validate format + uniqueness; fatal `err` if invalid (non-interactive) |
|
||||
| **Valid name** | Break out of loop, proceed to step 2 |
|
||||
|
||||
**Validation regex:** `^[a-zA-Z][a-zA-Z0-9_-]*$`
|
||||
|
||||
#### Step 2: Provider
|
||||
|
||||
| Input | Behavior |
|
||||
|-------|----------|
|
||||
| Source | Discover from `$PROVIDER_DIR/*.sh` (pattern: `bin/pos-ai` lines 17-18) |
|
||||
| Picker | `menu_pick "Pick provider" "${providers[@]}"` |
|
||||
| Zero providers | `err "No AI providers installed — run 'pos ai' setup first"` → exit 1 |
|
||||
| Single provider | Auto-select, log: `"Using provider: ${provider}"` (skip picker) |
|
||||
| User picks 0/back | Return to main menu (rc 1 from `menu_pick`) |
|
||||
|
||||
#### Step 3: Session Name
|
||||
|
||||
| Input | Behavior |
|
||||
|-------|----------|
|
||||
| Prompt | `menu_ask_value "Session name" "<alias_name>"` — default = alias name |
|
||||
| Empty (Enter) | Accepts default = alias name (the common path) |
|
||||
| Invalid format | `warn` + re-prompt with current default |
|
||||
| **Valid** | Proceed |
|
||||
|
||||
**Design decision:** Default session = alias name. 90%+ of users want 1:1 mapping. The bracket default `[alias_name]` makes this obvious.
|
||||
|
||||
#### Step 4: System Prompt
|
||||
|
||||
| Input | Behavior |
|
||||
|-------|----------|
|
||||
| Prompt | `menu_ask_value "System prompt (empty = use built-in)" ""` |
|
||||
| Empty | Stores empty string → `--system` flag omitted in generated alias |
|
||||
| Contains `\|` | `warn "System prompt must not contain '|' characters"` → re-prompt |
|
||||
| > 500 chars | `warn "Prompt is ${#input} chars — consider keeping it concise"` → still accepts |
|
||||
| Single quotes, `$`, backticks | **Accepted** — `_regen_aliases()` handles escaping |
|
||||
|
||||
**Design decision: single-line only.** `menu_ask_value` uses `read -rp`. Multi-line input is not supported and would require external deps (dialog/whiptail). Users paste prompts on one line — this is consistent with every other `pos` tool.
|
||||
|
||||
#### Confirmation
|
||||
|
||||
| Aspect | Behavior |
|
||||
|--------|----------|
|
||||
| Display | `section`-style box with all 4 values; prompt truncated to 50 chars in display |
|
||||
| Confirm | `confirm "Create alias '<name>'?" y` — **default YES** (constructive action) |
|
||||
| User declines | `log "Aborted."` → return to main menu |
|
||||
| User confirms | `_write_env_file()` + `_regen_aliases()` → `log "Alias '<name>' created."` |
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Edit Flow
|
||||
|
||||
### Entry Points
|
||||
- `pos ai alias` → menu → option 2
|
||||
- `pos ai alias edit` (interactive picker)
|
||||
- `pos ai alias edit <name>` (edit specific alias)
|
||||
|
||||
### ASCII Mockup — Full Interactive Edit
|
||||
|
||||
```
|
||||
════════════════════════════════════════════
|
||||
Edit AI Agent Alias
|
||||
════════════════════════════════════════════
|
||||
|
||||
-- 3 available --
|
||||
1) devbot [gemini] You are a Linux dev assistant.
|
||||
2) code [openrouter] You are a code reviewer.
|
||||
3) dev [gemini] Dev assistant
|
||||
0) Back
|
||||
----------------------------------------
|
||||
Pick alias to edit [1-3], text=filter, 0=back 1
|
||||
|
||||
Current values for 'devbot':
|
||||
Provider: gemini
|
||||
Session: devbot
|
||||
Prompt: You are a Linux dev assistant.
|
||||
|
||||
[1/3] Provider
|
||||
─────────────────────────────────────────
|
||||
-- 2 available --
|
||||
1) gemini
|
||||
2) openrouter
|
||||
0) Back
|
||||
----------------------------------------
|
||||
Pick provider [1-2], text=filter, 0=back 1
|
||||
|
||||
[2/3] Session Name
|
||||
─────────────────────────────────────────
|
||||
Session name [devbot]:
|
||||
|
||||
[3/3] System Prompt
|
||||
─────────────────────────────────────────
|
||||
System prompt [You are a Linux dev assistant.]:
|
||||
|
||||
────────────────────────────────────────────
|
||||
Save changes to 'devbot'?
|
||||
Provider: gemini (unchanged)
|
||||
Session: devbot (unchanged)
|
||||
Prompt: You are a dev assistant
|
||||
────────────────────────────────────────────
|
||||
Save changes? [Y/n]: y
|
||||
|
||||
[+] Alias 'devbot' updated.
|
||||
```
|
||||
|
||||
### Step-by-Step Behavior
|
||||
|
||||
#### Alias Picker
|
||||
|
||||
| Condition | Behavior |
|
||||
|-----------|----------|
|
||||
| No aliases | `warn "No aliases to edit — create one first"` → return to main menu |
|
||||
| Picker | `menu_pick "Pick alias to edit" "${display_items[@]}"` |
|
||||
| Items format | `"${name} [${provider}] ${prompt_preview}"` (prompt preview = first 30 chars) |
|
||||
| `<name>` arg given | Validate existence; `err` if not found, exit 1 |
|
||||
|
||||
#### Show Current Values
|
||||
|
||||
After picking, display a summary block (to stderr):
|
||||
|
||||
```
|
||||
Current values for 'devbot':
|
||||
Provider: gemini
|
||||
Session: devbot
|
||||
Prompt: You are a Linux dev assistant.
|
||||
```
|
||||
|
||||
This gives the user a reference before editing.
|
||||
|
||||
#### Per-Field Editing (3 steps)
|
||||
|
||||
| Field | Mechanism | Default | Validation |
|
||||
|-------|-----------|---------|------------|
|
||||
| **Provider** | `menu_pick` from installed providers | Current shown in list header | Must pick valid provider; 0/back = keep current |
|
||||
| **Session** | `menu_ask_value "Session name" "<current>"` | Current value | Regex; warn + re-prompt on invalid |
|
||||
| **Prompt** | `menu_ask_value "System prompt" "<current>"` | Current value (truncated in display if > 60 chars) | `\|` rejected; > 500 chars warning |
|
||||
|
||||
**Provider edit detail:** `menu_pick` doesn't support pre-selection. Current provider is shown in the header: `"Current provider: gemini"`. If user picks 0/back, current is kept — clean "skip to keep" pattern.
|
||||
|
||||
**Prompt edit detail:** If current prompt > 80 chars, default display is truncated: `[You are a Linux dev assistant. You reply with c…]`. The full value is preserved in the stored default regardless.
|
||||
|
||||
**Step count:** 3 steps (not 4) because alias name cannot be changed — it's the record key.
|
||||
|
||||
#### Save Confirmation
|
||||
|
||||
| Condition | Behavior |
|
||||
|-----------|----------|
|
||||
| **No changes detected** (all values identical) | `log "No changes — nothing to save."` → return to main menu (skip write) |
|
||||
| Changes exist | Show diff summary with `(changed)` / `(unchanged)` tags |
|
||||
| **Confirm** | `confirm "Save changes to '<name>'?" y` — **default YES** |
|
||||
| User declines | `log "Discarded."` → return to main menu |
|
||||
| User confirms | Rewrite `.env` entry, regenerate `.sh`, `log "Alias '<name>' updated."` |
|
||||
|
||||
**Diff summary format:**
|
||||
|
||||
```
|
||||
────────────────────────────────────────────
|
||||
Save changes to 'devbot'?
|
||||
Provider: gemini (unchanged)
|
||||
Session: mybot (changed)
|
||||
Prompt: You are a dev assistant
|
||||
────────────────────────────────────────────
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Remove Flow
|
||||
|
||||
### Entry Points
|
||||
- `pos ai alias` → menu → option 3
|
||||
- `pos ai alias remove` (interactive picker)
|
||||
- `pos ai alias remove <name>` (remove specific alias)
|
||||
|
||||
### ASCII Mockup
|
||||
|
||||
```
|
||||
════════════════════════════════════════════
|
||||
Remove AI Agent Alias
|
||||
════════════════════════════════════════════
|
||||
|
||||
-- 3 available --
|
||||
1) devbot [gemini] You are a Linux dev assistant.
|
||||
2) code [openrouter] You are a code reviewer.
|
||||
3) dev [gemini] Dev assistant
|
||||
0) Back
|
||||
----------------------------------------
|
||||
Pick alias to remove [1-3], text=filter, 0=back 1
|
||||
|
||||
Alias: devbot
|
||||
Provider: gemini
|
||||
Session: devbot
|
||||
Prompt: You are a Linux dev assistant.
|
||||
|
||||
Remove alias 'devbot'? This cannot be undone. [y/N]: n
|
||||
|
||||
[--] Cancelled.
|
||||
```
|
||||
|
||||
### Behavior
|
||||
|
||||
| Condition | Behavior |
|
||||
|-----------|----------|
|
||||
| No aliases | `warn "No aliases to remove"` → return to main menu |
|
||||
| Picker | Same format as edit picker |
|
||||
| `<name>` arg | Validate existence; `err` if not found, exit 1 |
|
||||
| Detail display | Show all fields in full (no truncation) before confirmation |
|
||||
| **Confirm** | `confirm "Remove alias '<name>'? This cannot be undone." n` — **default NO** |
|
||||
| User declines | `log "Cancelled."` → return to main menu |
|
||||
| User confirms | Remove from `.env`, regenerate `.sh`, `log "Alias '<name>' removed."` |
|
||||
|
||||
**Design decision: default=n.** Irreversible destructive action. `confirm` with default `n` means pressing Enter is safe — user must explicitly type `y`.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: List / Show (Non-Interactive)
|
||||
|
||||
### `pos ai alias list`
|
||||
|
||||
#### ASCII Mockup
|
||||
|
||||
```
|
||||
Aliases (3):
|
||||
Name Provider Session Prompt
|
||||
─────────── ───────────── ───────────── ──────────────────────────────
|
||||
devbot gemini devbot You are a Linux dev assistant.
|
||||
code openrouter code You are a code reviewer. Be concise.
|
||||
dev gemini devbot Dev assistant
|
||||
```
|
||||
|
||||
| Aspect | Spec |
|
||||
|--------|------|
|
||||
| Format | `printf " %-12s %-12s %-12s %s\n"` |
|
||||
| Prompt truncation | Full up to 60 chars; longer → append `…` |
|
||||
| Zero aliases | Print `Aliases (0):` with nothing below, exit 0 |
|
||||
| Header | `Aliases (N):` — count included for scripting |
|
||||
| Output | stdout, no color (pipeable) |
|
||||
|
||||
### `pos ai alias show <name>`
|
||||
|
||||
#### ASCII Mockup
|
||||
|
||||
```
|
||||
Alias: devbot
|
||||
Provider: gemini
|
||||
Session: devbot
|
||||
Prompt: You are a Linux dev assistant.
|
||||
Command: pos ai gemini ask --session devbot --system 'You are a Linux dev assistant.'
|
||||
```
|
||||
|
||||
| Aspect | Spec |
|
||||
|--------|------|
|
||||
| `<name>` required | Missing → `err "Usage: pos ai alias show <name>"` exit 1 |
|
||||
| Name not found | `err "Alias '<name>' not found"` exit 1 |
|
||||
| Full values | No truncation on any field |
|
||||
| `Command` field | Exact alias expansion: `pos ai <provider> ask --session <session> [--system '<prompt>']` |
|
||||
| Single quotes in prompt | Display escaped as `'\''` to show actual alias content |
|
||||
| Empty prompt | `Command` omits `--system` entirely |
|
||||
| Output | stdout, no color |
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Error States — Complete Catalog
|
||||
|
||||
| Scenario | Message | Type | Behavior |
|
||||
|----------|---------|------|----------|
|
||||
| **First run (no .env)** | Silent auto-create on first write | — | `_load_aliases()` returns empty; menu shows "No aliases" |
|
||||
| **Alias name empty** | `warn "Alias name cannot be empty"` | Recoverable | Re-prompt (create) |
|
||||
| **Alias name invalid** | `warn "Invalid name '<input>' — use letters, digits, hyphens, underscores"` | Recoverable | Re-prompt |
|
||||
| **Alias name duplicate (create)** | `warn "Alias '<input>' already exists — use 'pos ai alias edit <input>' instead"` | Recoverable | Re-prompt |
|
||||
| **Alias not found (edit/remove)** | `err "Alias '<name>' not found"` | Fatal | Exit 1 |
|
||||
| **Invalid provider** | `err "Unknown provider '<input>'"` | Fatal | Exit 1 (impossible in interactive — menu_pick) |
|
||||
| **No providers installed** | `err "No AI providers installed — run 'pos ai' setup first"` | Fatal | Exit 1 |
|
||||
| **Pipe char in prompt** | `warn "System prompt must not contain '\|' characters"` | Recoverable | Re-prompt |
|
||||
| **Long prompt (> 500 chars)** | `warn "Prompt is <N> chars — consider keeping it concise"` | Advisory | Accepts, continues |
|
||||
| **No aliases to edit** | `warn "No aliases to edit — create one first"` | Info | Return to main menu |
|
||||
| **No aliases to remove** | `warn "No aliases to remove"` | Info | Return to main menu |
|
||||
| **Non-tty / EOF** | `[!] Interactive menu needs a terminal — use a subcommand instead (see --help).` | Fatal | Exit 1 |
|
||||
| **Confirm declined (create)** | `log "Aborted."` | Info | Return to main menu |
|
||||
| **Confirm declined (remove)** | `log "Cancelled."` | Info | Return to main menu |
|
||||
| **Confirm declined (edit)** | `log "Discarded."` | Info | Return to main menu |
|
||||
| **No changes in edit** | `log "No changes — nothing to save."` | Info | Return to main menu |
|
||||
|
||||
### Message Convention
|
||||
|
||||
| Helper | Use For | Destination |
|
||||
|--------|---------|-------------|
|
||||
| `log` | Success, info | stdout |
|
||||
| `warn` | Recoverable problems, advisory | stderr |
|
||||
| `err` | Fatal errors | stderr + exit 1 |
|
||||
| `section` / `step` | Visual structure | stderr (display) |
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Color & Formatting Conventions
|
||||
|
||||
### Color Palette
|
||||
|
||||
| Element | Color | Source |
|
||||
|---------|-------|--------|
|
||||
| Section/box borders | CYAN | `section()`, `menu_run()` |
|
||||
| Step numbers | BOLD | `step()` |
|
||||
| Step underlines | BLUE | `step()` |
|
||||
| Success messages | GREEN | `log()` |
|
||||
| Warnings | YELLOW | `warn()` |
|
||||
| Errors | RED | `err()` |
|
||||
| Dim/placeholder text | DIM | inline `${DIM}...${RESET}` |
|
||||
| Table headers | Plain (no color) | — |
|
||||
| Default values in prompts | Plain | shown in brackets `[value]` |
|
||||
|
||||
### Box Style
|
||||
|
||||
All section boxes use the existing double-line Unicode style — the project standard from `section()` in `common.sh`:
|
||||
|
||||
```
|
||||
════════════════════════════════════════════
|
||||
Title
|
||||
════════════════════════════════════════════
|
||||
```
|
||||
|
||||
### 80-Column Safety
|
||||
|
||||
Final column layout for table output:
|
||||
|
||||
```
|
||||
Indent: 2
|
||||
Name: 12
|
||||
Gap: 1
|
||||
Provider: 12
|
||||
Gap: 1
|
||||
Session: 12
|
||||
Gap: 1
|
||||
Prompt: 42
|
||||
─────────────────────
|
||||
Total: 83 cols (1 col over 80 — acceptable for modern terminals)
|
||||
```
|
||||
|
||||
For strict 80-col: prompt = 40 → total = 81. **Decision: use 42 for prompt width; 83 cols is acceptable** — the project targets 80-col as a guideline, not a hard wall. All other content (menus, prompts, messages) is well within 80.
|
||||
|
||||
---
|
||||
|
||||
## Step 8: Implementation Guidance for Builder
|
||||
|
||||
### Function Map
|
||||
|
||||
| Function | Responsibility | Est. Lines |
|
||||
|----------|---------------|------------|
|
||||
| `_load_aliases()` | Read `.env` → parallel arrays `_ALIAS_NAMES[]`, `_ALIAS_PROVIDERS[]`, `_ALIAS_SESSIONS[]`, `_ALIAS_PROMPTS[]` | ~20 |
|
||||
| `_find_alias <name>` | Linear scan; return index or -1 | ~8 |
|
||||
| `_write_env_file()` | Write arrays → `.env` (with header comments) | ~15 |
|
||||
| `_regen_aliases()` | `.env` → `.sh` with `'`→`'\''` escaping + `bash -n` pre-commit check | ~30 |
|
||||
| `_list_aliases()` | `printf` table to stdout | ~15 |
|
||||
| `_show_alias <name>` | Key-value display to stdout | ~10 |
|
||||
| `_create_alias [name]` | 4-step wizard (name→provider→session→prompt→confirm) | ~60 |
|
||||
| `_edit_alias [name]` | Pick→show→3 field prompts→diff→confirm→write | ~70 |
|
||||
| `_remove_alias [name]` | Pick→detail→confirm(n)→remove+regen | ~30 |
|
||||
| `_main_menu()` | `menu_run` loop dispatching to above | ~30 |
|
||||
| `usage()` | Help text | ~20 |
|
||||
|
||||
### Critical Implementation Patterns
|
||||
|
||||
1. **All display to stderr, all results to stdout** — `menu-lib.sh` contract. `menu_run`/`menu_pick`/`menu_ask_value` already do this; `_create_alias`/`_edit_alias`/`_remove_alias` must follow the same pattern for their `section()`/`step()` output.
|
||||
|
||||
2. **Re-prompt loops** — use `while true` with validation inside; `continue` on `warn`, `break` on valid input. Name validation loops until valid. Provider/session/prompt validation loops until valid.
|
||||
|
||||
3. **Edit diff detection** — compare new values against loaded values before writing; skip the write entirely if all identical. This avoids unnecessary `.sh` regeneration.
|
||||
|
||||
4. **Provider discovery** — copy pattern from `bin/pos-ai` lines 17-18:
|
||||
```bash
|
||||
PROVIDER_DIR="$(dirname "$0")/../lib/ai-providers"
|
||||
[ -d "$PROVIDER_DIR" ] || PROVIDER_DIR="$(dirname "$0")/ai-providers"
|
||||
```
|
||||
|
||||
5. **Pipe char in prompt** — validate in `_create_alias` and `_edit_alias` prompt steps, NOT in `_regen_aliases()` (which trusts its input after validation).
|
||||
|
||||
6. **Generated .sh syntax check** — `bash -n "$sh_file"` before `mv "$tmp" "$sh_file"`; on failure, `warn` and `rm -f "$tmp"` (keep old `.sh`).
|
||||
|
||||
7. **INTERACTIVE_CMDS** — add `ai-alias` to the space-separated list in `bin/pos`.
|
||||
|
||||
### State Transition Diagram
|
||||
|
||||
```
|
||||
pos ai alias (no args)
|
||||
│
|
||||
├─ _main_menu()
|
||||
│ ├─ [1] → _create_alias()
|
||||
│ │ ├─ Step 1: name validation loop
|
||||
│ │ ├─ Step 2: provider picker (or auto if single)
|
||||
│ │ ├─ Step 3: session (default = alias name)
|
||||
│ │ ├─ Step 4: prompt (default = empty)
|
||||
│ │ ├─ confirm → _write_env_file() + _regen_aliases()
|
||||
│ │ └─ return to _main_menu()
|
||||
│ │
|
||||
│ ├─ [2] → _edit_alias()
|
||||
│ │ ├─ picker (or use arg)
|
||||
│ │ ├─ show current values
|
||||
│ │ ├─ Step 1: provider picker (0=back = keep)
|
||||
│ │ ├─ Step 2: session (enter = keep)
|
||||
│ │ ├─ Step 3: prompt (enter = keep)
|
||||
│ │ ├─ diff check → confirm → write
|
||||
│ │ └─ return to _main_menu()
|
||||
│ │
|
||||
│ ├─ [3] → _remove_alias()
|
||||
│ │ ├─ picker (or use arg)
|
||||
│ │ ├─ show detail
|
||||
│ │ ├─ confirm n (default = no)
|
||||
│ │ ├─ remove + regen
|
||||
│ │ └─ return to _main_menu()
|
||||
│ │
|
||||
│ ├─ [4] → _list_aliases() (stdout, then return to _main_menu)
|
||||
│ │
|
||||
│ └─ [0] → exit 0
|
||||
│
|
||||
├─ pos ai alias create [name] → _create_alias "$name"
|
||||
├─ pos ai alias edit [name] → _edit_alias "$name"
|
||||
├─ pos ai alias remove [name] → _remove_alias "$name"
|
||||
├─ pos ai alias list → _list_aliases(); exit 0
|
||||
├─ pos ai alias show <name> → _show_alias "$name"; exit 0
|
||||
└─ -h|--help → usage(); exit 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Multi-line prompt editor (no `dialog`/`whiptail` dependency)
|
||||
- Alias import/export
|
||||
- Alias categories or tags
|
||||
- Alias usage statistics
|
||||
- `pos config` integration (architecture Decision 4: explicitly rejected)
|
||||
- Alias renaming (name is the record key; remove + create is the path)
|
||||
|
||||
---
|
||||
|
||||
## Open Design Questions
|
||||
|
||||
None. All decisions resolved from architecture doc + existing primitives.
|
||||
|
||||
---
|
||||
|
||||
**End of UX specification.**
|
||||
@@ -1,285 +0,0 @@
|
||||
# Designer Report — `pos config <scope>` listing readability
|
||||
|
||||
Date: 2026-08-26 · Agent: Designer · Status: DESIGN_READY (spec complete, ready for Builder)
|
||||
|
||||
## TL;DR
|
||||
|
||||
- Status: **DESIGN_READY** — smallest change-set that fixes "the config not clear readable" while staying 100% generic.
|
||||
- Root cause of complaint: the `ai` scope renders provider-specific keys from BOTH adapters flat and equal-weight (`*providers` expansion, lib/config-ui.sh:192-199), so 4 of 6 rows are noise for the active provider and nothing links `AI_PROVIDER`'s value to the relevant rows.
|
||||
- Fix = 3 coordinated pieces, all inside `lib/config-ui.sh` (+ one header line in `bin/pos-ai`):
|
||||
1. **Group captions** via a new optional field type `@[<KEY>=<alts>] <caption>` in `# POS_CONFIG:` headers — backward compatible, zero per-tool branches.
|
||||
2. **Tagged wildcard** `*providers=<tag>` so per-provider groups are possible from pure metadata.
|
||||
3. **Typography tier** applied to ALL scopes uniformly: reuse existing `BOLD/DIM/CYAN/RESET` from common.sh (guarded fallbacks, menu-lib precedent), dim `(not set)`, hanging-indent wrapping at 80–120 cols, display routed to stderr, honest prompt text (`r=refresh` was invisible).
|
||||
- Inactive groups stay visible-but-dimmed with a textual reason — never hidden — so numbering is stable across edits.
|
||||
- No changes to masking, env-file formats, edit flow, gen-docs or completions output. Gates expected green after routine `make gen && make check && make lint`.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Evidence base
|
||||
|
||||
Read and cited throughout:
|
||||
|
||||
- `lib/config-ui.sh` (385 lines) — full render path: `cfg_scope_keys` (:176-204), `_cfg_key_line` (:92-112), `_cfg_plugin_keys` (:116-137), `_cfg_provider_keys` (:141-173), `cfg_value` (:207-214), `cfg_display` (:243-257), `_cfg_edit_one` (:301-332), `cfg_ui` (:335-385).
|
||||
- Color helpers already defined by `lib/common.sh:3-10`: `CYAN GREEN YELLOW RED BLUE BOLD DIM RESET` (tput). **config-ui.sh uses none of them.**
|
||||
- Guarded-color + stderr-display precedent: `lib/menu-lib.sh:29-31` (`CYAN="${CYAN:-}"` fallbacks) and :60-70 (render block `{ … } >&2`, "display goes to stderr, results to stdout" contract :19-20).
|
||||
- `bin/pos-ai:6` — ai scope declaration: `AI_PROVIDER | *providers | AI_SYSTEM_PROMPT`.
|
||||
- Provider adapters feed the expansion: `lib/ai-providers/gemini.sh:7-8`, `lib/ai-providers/openrouter.sh:7-8` (`# PROVIDER_CONFIG:` → AI_GEMINI_API_KEY/MODEL, OPENROUTER_API_KEY/MODEL). Expansion order follows glob order (`gemini.sh` before `openrouter.sh`), matching the user's live listing.
|
||||
- Contrast scopes: `bin/pos-system-health:4` (scope `system`, 2 keys), `bin/pos-system-backup:6` (scope `notify`, 1 key, long description).
|
||||
- Downstream consumers checked for blast radius:
|
||||
- `scripts/gen-docs.sh:153-164` — reads ONLY the scope field of `# POS_CONFIG:` (completion cache).
|
||||
- `completions/pos.bash:193-208` — same, scopes only.
|
||||
- `lib/registry.sh:107` — stores raw header lines verbatim (passthrough; no key-field parsing).
|
||||
- Prompt string `Variable number [q to quit]` occurs exactly once repo-wide (config-ui.sh:371); no doc quotes it verbatim.
|
||||
- No unit tests exist for config-ui.sh.
|
||||
|
||||
Other active `# POS_CONFIG:` headers scanned for syntax collisions: none contain `@`; wildcards in the wild are only `*plugins` / bare `*providers`. The proposed `@…` caption prefix and `=<tag>` wildcard argument collide with nothing.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 2: Diagnosis — why the listing is unclear
|
||||
|
||||
Ordered by contribution to the complaint:
|
||||
|
||||
1. **Flat mixing of active/inactive provider keys (core defect).** `cfg_scope_keys` expands `*providers` inline (config-ui.sh:192-199); `_cfg_provider_keys` greps all adapters at once (:171). Result: `AI_GEMINI_*` and `OPENROUTER_*` rows interleave with equal visual weight regardless of `AI_PROVIDER`'s value. With openrouter active, rows 2-3 are dead weight; switch providers and different rows become dead weight. Nothing on screen expresses that relationship.
|
||||
2. **No visual link between the selector value and any group.** Even a careful reader must mentally join `AI_PROVIDER=openrouter` (row 1's value column) to which other rows matter. That join lives only in the reader's head.
|
||||
3. **Zero typographic hierarchy.** Keys, values, placeholders, descriptions all render at identical weight (plain printf, :362-368) even though `BOLD/DIM/CYAN` exist one file away in common.sh:3-10 and sibling libs use them (menu-lib). Scanning requires reading every line; nothing pops.
|
||||
4. **`(not set)` looks like data.** cfg_display (:246) emits it with the same weight as a real value, so "which things are actually configured?" — arguably the #1 question a config screen answers — is unanswerable at a glance.
|
||||
5. **Description vs example not differentiated.** Both print at the same 6-space indent and weight (:364, :367), yet they are different kinds of information (prose vs literal format hint).
|
||||
6. **No rhythm / grouping whitespace.** Every entry is equally dense; there is no blank-line separation between logical sections, and the fixed 36-char dash rule (:356) is unrelated to content width or house style (menu-lib uses a CYAN rule).
|
||||
7. **Prompt under-documents the actual affordances.** The loop supports `r/R` refresh and Enter-redraw (:374-375) but the prompt says only `[q to quit]`. "Variable number" is also stilted phrasing for "which row do you want to edit".
|
||||
8. **Long descriptions wrap ragged.** `printf ' %s\n'` does no wrapping; at 80 cols the notify scope's 92-char description wraps into the next row's visual space with no hanging indent (verified against bin/pos-system-backup:6 text).
|
||||
9. **Stream discipline inconsistency.** Menu body goes to stdout (:354-370) while `read -p` prompts go to stderr — under the dispatcher's logging tee the interactive UI can leak into logs. menu-lib's established contract (display→stderr) is violated.
|
||||
|
||||
Not defects: value column alignment via fixed `%-28s` pad is adequate for all declared keys today (longest = 25 chars); masking format is fine and out of scope anyway.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 3: Specification
|
||||
|
||||
### 3.1 Header grammar extension (the only metadata change)
|
||||
|
||||
Current grammar (config-ui.sh:8) gains ONE new field type and ONE optional wildcard argument; everything existing keeps working byte-for-byte:
|
||||
|
||||
```text
|
||||
# POS_CONFIG: <scope> | <env-file> | <field> | ...
|
||||
<field> := <KEY>=<flags>:<desc>[::<example>] unchanged
|
||||
| @<caption> NEW — unconditional group caption
|
||||
| @[<KEY>=<alt>[|<alt>…]] <caption> NEW — conditional group caption
|
||||
| *plugins unchanged
|
||||
| *providers[=<tag>] EXT — optional filter to adapter <tag>.sh
|
||||
```
|
||||
|
||||
Semantics:
|
||||
|
||||
- A field starting with `@` is a **caption**, never a key. Optional condition bracket must be the first token: `@[KEY=alt1|alt2] Caption text`.
|
||||
- **Condition evaluation:** caption is *active* iff current value of KEY (via existing `cfg_value`) equals any non-empty alt, or an empty alt segment is present (`trailing/double pipe`) and the value is unset/empty. One condition per caption — deliberately no boolean logic.
|
||||
- Empty-alt support matters: `@[AI_PROVIDER=gemini|]` means "gemini explicitly, or unset (= default gemini)". Without it, a fresh install showing `AI_PROVIDER=(not set)` would wrongly dim the gemini group.
|
||||
- **Inactive captions/rows are dimmed, never hidden** — see 3.3.
|
||||
- `*providers=<tag>` contributes keys only from `lib/ai-providers/<tag>.sh`. Bare `*providers` behaves exactly as today (all adapters).
|
||||
- Captions parse as whole fields: `::`, pipes-in-desc, examples etc. are untouched; `cfg_scopes`/`cfg_scope_envfile` only ever read fields 1-2, so old parsers (gen-docs, completions, registry passthrough) cannot notice the extension.
|
||||
|
||||
Exact new `bin/pos-ai:6` header:
|
||||
|
||||
```bash
|
||||
# POS_CONFIG: ai | ai.env | AI_PROVIDER=:Provider (gemini or openrouter, default gemini) | @[AI_PROVIDER=gemini|] Gemini | *providers=gemini | @[AI_PROVIDER=openrouter] OpenRouter | *providers=openrouter | @General | AI_SYSTEM_PROMPT=:Custom system prompt (overrides built-in, empty to reset)
|
||||
```
|
||||
|
||||
This is pure declarative metadata — no `if scope == ai` anywhere. Any future scope (e.g. notify platforms) can adopt captions; every scope that doesn't, renders as before plus the uniform typography tier.
|
||||
|
||||
### 3.2 Internal representation (Builder guidance, non-normative)
|
||||
|
||||
- Caption records enter the `keys[]` stream as `>|<cond>|<caption>|` (key position = `>`; keys are uppercase env names, so `>` is unambiguous). `cfg_ui` branches on `${k}` = `>`.
|
||||
- Numbering: assign numbers only to non-caption records; keep a number→index map (~5 lines). Numbers therefore derive from static header order → **stable across renders and across provider switches**.
|
||||
- Lazy caption flush: a pending caption prints only when a key actually follows it — a tag whose adapter is missing suppresses its caption instead of leaving an orphan heading. Additionally, an explicit `*providers=<tag>` matching zero files emits one `warn` (silent emptiness would hide authoring errors; bare `*providers` staying silent preserves today's behavior on installs without adapters).
|
||||
|
||||
### 3.3 Rendering rules (applied uniformly to ALL scopes)
|
||||
|
||||
Reuse tokens `BOLD DIM CYAN RESET` from common.sh with guarded fallbacks at the top of config-ui.sh, mirroring menu-lib.sh:29-31 (`DIM="${DIM:-}"` etc.) so standalone sourcing degrades to plain text, never an error.
|
||||
|
||||
| Element | Treatment |
|
||||
|---|---|
|
||||
| Title `pos config — <scope> (<envfile>)` | BOLD |
|
||||
| Rule under title | CYAN, 40 × `─` (unicode precedent: menu-lib box) |
|
||||
| Row number `%2d)` | DIM |
|
||||
| Key name | BOLD |
|
||||
| Value | default weight |
|
||||
| `(not set)` | DIM |
|
||||
| Masked secret `AIzaSy...fDp8 (39 chars)` | default weight (unchanged semantics) |
|
||||
| Description line | default weight, 6-space hanging indent |
|
||||
| Example line `e.g. …` | DIM, 6-space hanging indent |
|
||||
| Caption | DIM, rendered ` ── <caption>` (2-space indent, dashes sit under the number column), preceded by one blank line |
|
||||
| Caption when condition false | append DIM `— inactive while <KEY>=<value>` (or `— inactive (<KEY> not set)`); ALL rows until the next caption render DIM as well |
|
||||
| Prompt | `Number to edit [r=refresh, q=quit]: ` |
|
||||
|
||||
Wrapping: descriptions, examples and caption lines word-wrap at `W = clamp(${COLUMNS:-80}, 60, 120)` minus the 6-column hang; break at spaces only, no hyphenation, over-long tokens pass through unbroken.
|
||||
|
||||
Stream routing: the whole render block goes to stderr (`{ … } >&2`, menu-lib.sh:60-70 pattern). Prompts already go to stderr via `read -p`. Rationale: dispatcher logging-tee hygiene; results/data on stdout unaffected (this UI returns nothing on stdout).
|
||||
|
||||
Why dim-not-hide inactive groups: hiding would renumber rows the moment `AI_PROVIDER` is edited mid-session (unpredictable, WCAG-cognitive predictability failure) and would hide the fact that those settings *exist*. Dimming + a literal textual reason carries the meaning without relying on color alone (WCAG 1.4.1).
|
||||
|
||||
### 3.4 Interaction specification
|
||||
|
||||
- Trigger: every loop iteration re-renders (entry, after each edit, after `r`, after Enter) — unchanged flow (cfg_ui:353 `while true`). Because conditions are evaluated per render from the env file, editing `AI_PROVIDER` to `gemini` flips group emphasis on the very next redraw automatically. No new keystrokes, no new states.
|
||||
- Edge cases: EOF on prompt → existing clean-exit path (:371) kept; invalid number → existing warn kept; rapid repetition N/A (no async behavior); cancellation = `q` unchanged.
|
||||
- Accessibility (terminal):
|
||||
- DIM reserved for secondary info (numbers, examples, placeholders, inactive labels); meaning never carried by dimness/color alone — inactive state always includes the words "inactive while …".
|
||||
- Monochrome/no-TTY (tee, pipe): tput vars fall back to empty → plain-text hierarchy survives via indentation, blank lines, numbering and wording.
|
||||
- Keyboard-only flow unchanged (digits, r, q); target-size/motor N/A; screen readers traverse linearly — caption lines act as spoken group headings.
|
||||
- WCAG 2.1 AA intent: contrast of body/default text is terminal-native; DIM applies only to redundant-or-secondary strings.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 4: Rendered mockups
|
||||
|
||||
Notation: `[dim]`, `[bold]`, `[cyan]` mark spans that render with the corresponding tput attribute; unmarked = default weight. Column grid matches the current `' %2d) %-28s %s\n'` layout (value starts col ~36). State assumed: `AI_PROVIDER=openrouter`, both API keys set, models/prompt unset.
|
||||
|
||||
### 4a. Scope `ai` — BEFORE (what the user flagged)
|
||||
|
||||
```text
|
||||
|
||||
pos config — ai (ai.env)
|
||||
------------------------------------
|
||||
1) AI_PROVIDER openrouter
|
||||
Provider (gemini or openrouter, default gemini)
|
||||
2) AI_GEMINI_API_KEY AIzaSy...fDp8 (39 chars)
|
||||
Gemini API key from aistudio.google.com
|
||||
3) AI_GEMINI_MODEL (not set)
|
||||
Gemini model id (default: gemini-2.5-flash)
|
||||
4) OPENROUTER_API_KEY sk-or-v...wXyZ (64 chars)
|
||||
OpenRouter API key from openrouter.ai
|
||||
5) OPENROUTER_MODEL (not set)
|
||||
OpenRouter model id (default: openrouter/auto)
|
||||
6) AI_SYSTEM_PROMPT (not set)
|
||||
Custom system prompt (overrides built-in, empty to reset)
|
||||
|
||||
Variable number [q to quit]:
|
||||
```
|
||||
|
||||
Defects visible: 4 of 6 rows irrelevant right now, indistinguishable from load-bearing ones; no grouping; no hierarchy; placeholder looks like data; prompt hides `r`.
|
||||
|
||||
### 4b. Scope `ai` — AFTER
|
||||
|
||||
```text
|
||||
|
||||
[pos bold]pos config — ai (ai.env)
|
||||
[cyan]────────────────────────────────────────
|
||||
[dim]1)[/] [bold]AI_PROVIDER[/] openrouter
|
||||
Provider (gemini or openrouter, default gemini)
|
||||
|
||||
[dim]── Gemini — inactive while AI_PROVIDER=openrouter [/](whole group dim)
|
||||
[dim] 2) AI_GEMINI_API_KEY AIzaSy...fDp8 (39 chars)[/]
|
||||
[dim] Gemini API key from aistudio.google.com[/]
|
||||
[dim] 3) AI_GEMINI_MODEL (not set)[/]
|
||||
[dim] Gemini model id (default: gemini-2.5-flash)[/]
|
||||
|
||||
[dim]── OpenRouter[/]
|
||||
4) [bold]OPENROUTER_API_KEY[/] sk-or-v...wXyZ (64 chars)
|
||||
OpenRouter API key from openrouter.ai
|
||||
5) [bold]OPENROUTER_MODEL[/] (not set)
|
||||
OpenRouter model id (default: openrouter/auto)
|
||||
|
||||
[dim]── General[/]
|
||||
6) [bold]AI_SYSTEM_PROMPT[/] (not set)
|
||||
Custom system prompt (overrides built-in, empty to reset)
|
||||
|
||||
Number to edit [r=refresh, q=quit]:
|
||||
```
|
||||
|
||||
(Indentation above is schematic; normative grid = 3.3 table: numbers `%2d)` at current positions, keys padded as today, captions at 2-space indent.) Reading the AFTER top-to-bottom: selector first; the irrelevant group says in words why it's inactive; the active provider's two rows are the visually normal block; general behavior last. The complaint is resolved without touching edit flow.
|
||||
|
||||
### 4c. Scope `system` — BEFORE (contrast/genericity proof)
|
||||
|
||||
```text
|
||||
|
||||
pos config — system (system.env)
|
||||
------------------------------------
|
||||
1) BACKUP_SERVICE_ROOTS (not set)
|
||||
Roots scanned by backup --service and the health backup-age check (default: /srv $HOME/srv)
|
||||
2) HEALTH_BACKUP_MAX_AGE_DAYS 2
|
||||
Max backup age in days before health warns (default 2)
|
||||
|
||||
Variable number [q to quit]:
|
||||
```
|
||||
|
||||
(desc of row 1 is 92 chars → ragged wrap into row 2's space on an 80-col terminal.)
|
||||
|
||||
### 4d. Scope `system` — AFTER (header UNCHANGED — proves backward compatibility)
|
||||
|
||||
```text
|
||||
|
||||
[bold]pos config — system (system.env)
|
||||
[cyan]────────────────────────────────────────
|
||||
[dim]1)[/] [bold]BACKUP_SERVICE_ROOTS[/] (not set)
|
||||
Roots scanned by backup --service and the health
|
||||
backup-age check (default: /srv $HOME/srv)
|
||||
[dim]2)[/] [bold]HEALTH_BACKUP_MAX_AGE_DAYS[/] 2
|
||||
Max backup age in days before health warns (default 2)
|
||||
|
||||
Number to edit [r=refresh, q=quit]:
|
||||
```
|
||||
|
||||
No captions declared → no group machinery appears; the only differences are the uniform typography tier (title/rule/dim placeholder/wrap/prompt). Same holds for `telegram`, `matrix`, `scrcpy`, `ytsync`, `compose`, `notify`, `entertainment` — all headers stay valid as-is.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 5: Impact map
|
||||
|
||||
**Code — `lib/config-ui.sh` (single implementation file):**
|
||||
|
||||
| Function / region | Change |
|
||||
|---|---|
|
||||
| Header grammar comment :7-15 | document `@caption`, `@[K=v1|v2] caption`, `*providers=<tag>` |
|
||||
| Guarded helper fallbacks :24-28 area | add `BOLD/DIM/CYAN/RESET` guarded fallbacks (menu-lib.sh:29-31 pattern) |
|
||||
| NEW caption parsing (inline in `cfg_scope_keys` field loop :188-199) | branch fields starting with `@` before the wildcard `case`; emit `>|cond|caption|` records; pass `<tag>` through to `_cfg_provider_keys` |
|
||||
| `_cfg_provider_keys` :141-173 | optional tag arg: iterate files individually (or attribute grep output per file) and keep only `<tag>.sh`; warn on explicit-tag-zero-match |
|
||||
| `cfg_value` :207-214 | unchanged, reused for condition evaluation |
|
||||
| `cfg_display` :243-257 | unchanged (masking untouched per constraint) |
|
||||
| `_cfg_edit_one` :301-332 | unchanged (never receives `>` records — numbering map guarantees it) |
|
||||
| `cfg_ui` :335-385 | typography table 3.3, caption rendering + lazy flush + cond eval, number→index map, desc/example/caption wrapping, `{ … } >&2` render block, prompt text :371 |
|
||||
|
||||
Unchanged: `cfg_tools_dir`, `cfg_headers`, `cfg_scopes`, `cfg_scope_envfile`, `_cfg_key_line`, `_cfg_plugin_keys`, `cfg_validate`, `cfg_read_secret`, `cfg_write`, `_cfg_post_write`.
|
||||
|
||||
**Headers:** `bin/pos-ai:6` rewritten as in 3.1 (only tool header change; other scopes intentionally left as-is).
|
||||
|
||||
**Hand-maintained docs (doc-sync class, no GEN involvement):** `bin/pos-config` usage text (:25-30 grammar sample, :32 add "`r` redraws"); `DOC/POS.md` §`pos config` (~:486-492, one sentence on captions); `DOC/DEV.md` config-files bullet (~:158, mention caption/tag grammar); optional follow-up for the owner: add `@Plugins` caption before `*plugins` in `bin/pos-entertainment-config:4`.
|
||||
|
||||
**Generated artifacts:** `make gen` MUST be re-run because `bin/pos-*` changed (AGENTS.md definition of done), but expected diff is **empty**: gen-docs.sh:153-164 and completions/pos.bash:193-208 read only scope names; tree/dispatch tables read `# POS:` descriptions (untouched). `lib/registry.sh:107` passes header lines through verbatim → no consumer impact; its grammar comment may gain one line as courtesy.
|
||||
|
||||
**Gates/lint:** `make check` (bash -n, exec bits, doc-sync, dispatch smoke) — no new files, no header removals → expected green. `make lint` — config-ui.sh stays a sourced lib (no `POS:`-header/help requirements apply, same as today); new helpers follow `cfg_`/`_cfg_` naming; strict-mode-safe parsing required (no new stdin readers → `INTERACTIVE_CMDS` untouched). CI tag gate unaffected beyond the normal four commands.
|
||||
|
||||
**Tests:** none exist; recommend Builder smoke covers: old-style scope (e.g. telegram) renders with typography tier and NO group lines; caption condition true/false/unset-selector-with-empty-alt; `*providers=<unknown-tag>` warn + suppressed caption; numbering stability after switching `AI_PROVIDER` mid-session; installed-layout sourcing (`/usr/local/bin` fallback chain).
|
||||
|
||||
Estimated size: ~+45/−12 lines in config-ui.sh, 1 header line, 3 doc touchpoints.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 6: Out of scope / rejected alternatives
|
||||
|
||||
Considered and rejected, with reasons:
|
||||
|
||||
- **Hiding inactive provider groups entirely** — renumbering churn mid-session + hides that the settings exist; dimming achieves clarity without unpredictability.
|
||||
- **Inferring relevance from naming conventions** (e.g. "keys sharing a `_PROVIDER` suffix prefix") — implicit magic, breaks silently on renames; explicit metadata beats inference.
|
||||
- **Per-key (non-caption) activity flags** like `secret,gemini:` flag values — overloads the flags grammar that validation switches on; caption-level grouping covers the actual complaint with less grammar.
|
||||
- **Color-status encoding (green=active / red=inactive)** — color-only meaning fails monochrome terminals and color-blind users; implies judgement ("red = bad") inappropriate for merely-inactive config; DIM + textual reason is safer.
|
||||
- **Two-column / box-drawing table layout** — fragile across the stated 80–120 col range, hostile to copy-paste and screen readers, high code cost for low gain.
|
||||
- **Dynamic value-column width** (measure longest key) — marginal gain over the adequate fixed `%-28s`; adds a pre-pass over keys for cosmetic parity.
|
||||
- **Alphabetical key sorting** — destroys declared/logical order and future grouping intent.
|
||||
- **Hierarchical numbering (`2.1`, `2.2`)** — input-parsing complexity and wider number column for zero selection benefit.
|
||||
- **Pagination / scrolling for long scopes** — largest scope today is 9 entries + descs ≈ fits a terminal page with scrollback; premature.
|
||||
- **Masking redesign, env-file format changes, new keystrokes/flows** — excluded by task constraints; nothing above touches them.
|
||||
- **Rewriting config-ui onto `lib/registry.sh`** while we're in here — a refactor, not a readability fix; registry passthrough already tolerates the new grammar.
|
||||
|
||||
[DONE]
|
||||
|
||||
---
|
||||
|
||||
## Handoff
|
||||
|
||||
Status: **DESIGN_READY**
|
||||
|
||||
- Objective: make `pos config <scope>` listings clearly readable without breaking genericity.
|
||||
- Specification: §3 (grammar extension, rendering rules, interaction/a11y), mockups §4, impact map §5.
|
||||
- Affected: `lib/config-ui.sh`, `bin/pos-ai` (header), 3 doc touchpoints; generated outputs unchanged.
|
||||
- Constraints for Builder: keep masking/display formats and edit flow byte-compatible; no per-scope branches; reuse common.sh color tokens with guarded fallbacks; run `make gen && make check && make lint` (expect zero gen diff).
|
||||
- Open design questions: none blocking. Risks: DIM legibility on exotic palettes (mitigated: secondary info only, meaning duplicated in text); typo'd condition keys yield a permanently dim group (visible, self-inflicted, documented).
|
||||
- Recommended next agent: **Builder** — spec is implementable without further design decisions.
|
||||
@@ -1,269 +0,0 @@
|
||||
# Detective Report: `pos ai alias` Silent Crash
|
||||
|
||||
**Date:** 2026-08-26
|
||||
**Symptom:** `pos ai alias` exits silently after user enters alias name in create flow
|
||||
**Status:** ROOT_CAUSE_ESTABLISHED
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
The `_alias_find` function returns exit code 1 when a name is not found (which is the normal/not-found case). This return code is captured in a variable assignment `existing="$(_alias_find "$name")"` under `set -euo pipefail`. On bash 5.2, `set -e` is NOT suppressed for command substitutions inside variable assignments, so the non-zero return code causes the script to exit immediately and silently.
|
||||
|
||||
**Root cause:** `bin/pos-ai-alias:103` — `_alias_find()` returns 1 when name is not found; combined with `set -e` at line 2.
|
||||
**Bug is present at 4 call sites** — ALL of them crash when the name doesn't exist.
|
||||
**Fix:** Make `_alias_find` always return 0 (the "not found" signal is the `-1` on stdout, not the exit code), or add `|| true` at each call site.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Reproduce the reported symptom
|
||||
|
||||
**Command:** `pos ai alias` → choose 1 → enter "assist"
|
||||
**Result:** Script exits immediately after input. No step 2/4 header. No error message.
|
||||
|
||||
The tool silently returns to the shell prompt with no output beyond the alias name prompt.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Trace the create flow (static analysis)
|
||||
|
||||
File: `bin/pos-ai-alias`
|
||||
|
||||
| Line | Code | Analysis |
|
||||
|------|------|----------|
|
||||
| 208 | `name="$(menu_ask_value "Alias name" "")"` | ✅ Works correctly — user types "assist", captured on stdout, rc=0 |
|
||||
| 210 | `[ -z "$name" ]` | ✅ False — name is "assist", no warning |
|
||||
| 211 | `if ! _alias_name_valid "$name"` | ✅ Regex matches `assist`, validation passes |
|
||||
| 215 | `_alias_load` | ✅ File doesn't exist → returns 0, arrays stay empty |
|
||||
| **217** | **`existing="$(_alias_find "$name")"`** | **💥 CRASH — this is where the script dies** |
|
||||
| 218 | `if [ "$existing" != "-1" ]; then` | Never reached |
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Confirm root cause with controlled experiments
|
||||
|
||||
### Experiment 1: Isolated `set -e` + function return code
|
||||
|
||||
```bash
|
||||
bash -c '
|
||||
set -euo pipefail
|
||||
f() { echo "-1"; return 1; }
|
||||
x="$(f)"
|
||||
echo "after: $x"
|
||||
'
|
||||
# Result: exits immediately. "after" never printed. EXIT CODE: 1
|
||||
```
|
||||
|
||||
**Conclusion:** `set -e` in bash 5.2 does NOT suppress errexit for command substitutions inside variable assignments. A function returning 1 inside `x="$(f)"` triggers immediate script termination.
|
||||
|
||||
### Experiment 2: Same code without `set -e`
|
||||
|
||||
```bash
|
||||
bash -c '
|
||||
set -uo pipefail
|
||||
alias_names=()
|
||||
alias_find() { echo "-1"; return 1; }
|
||||
existing="$(alias_find "test")"
|
||||
echo "existing=$existing"
|
||||
echo "DONE"
|
||||
'
|
||||
# Result: existing=-1, DONE printed. EXIT CODE: 0
|
||||
```
|
||||
|
||||
**Conclusion:** Without `set -e`, the code works correctly.
|
||||
|
||||
### Experiment 3: Fix with `|| true`
|
||||
|
||||
```bash
|
||||
bash -c '
|
||||
set -euo pipefail
|
||||
alias_names=()
|
||||
alias_find() { echo "-1"; return 1; }
|
||||
result="$(alias_find "assist" || true)"
|
||||
echo "result=$result"
|
||||
echo "DONE"
|
||||
'
|
||||
# Result: result=-1, DONE printed. EXIT CODE: 0
|
||||
```
|
||||
|
||||
**Conclusion:** `|| true` inside the command substitution masks the non-zero return code and prevents `set -e` from firing.
|
||||
|
||||
### Experiment 4: Exact reproduction with real script logic
|
||||
|
||||
```bash
|
||||
bash -c '
|
||||
set -euo pipefail
|
||||
name="assist"
|
||||
# line 217 of pos-ai-alias
|
||||
existing="$(_alias_find "$name")"
|
||||
echo "existing=$existing"
|
||||
' 2>&1
|
||||
# EXIT: 1 — "existing" never printed
|
||||
```
|
||||
|
||||
**Conclusion:** Crash confirmed with the exact code pattern from the source.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Identify ALL affected call sites
|
||||
|
||||
The `_alias_find` function is called at **4 locations** in `bin/pos-ai-alias`:
|
||||
|
||||
| Line | Function | Code | Crashes when? |
|
||||
|------|----------|------|---------------|
|
||||
| **217** | `_alias_create` | `existing="$(_alias_find "$name")"` | **New alias name (not yet in file)** — THIS IS THE REPORTED BUG |
|
||||
| 139 | `_alias_show` | `idx="$(_alias_find "$1")"` | `pos ai alias show <nonexistent>` |
|
||||
| 318 | `_alias_edit` | `idx="$(_alias_find "$name")"` | `pos ai alias edit <nonexistent>` (non-interactive preset) |
|
||||
| 453 | `_alias_remove` | `idx="$(_alias_find "$name")"` | `pos ai alias remove <nonexistent>` (non-interactive preset) |
|
||||
|
||||
**All four are equally broken** — any path where the name isn't found triggers a silent crash.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Root cause analysis
|
||||
|
||||
### The bug
|
||||
|
||||
`bin/pos-ai-alias:94-104` — `_alias_find()`:
|
||||
|
||||
```bash
|
||||
_alias_find() {
|
||||
local name="$1" i
|
||||
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
||||
if [ "${_ALIAS_NAMES[$i]}" = "$name" ]; then
|
||||
echo "$i"
|
||||
return 0 # found → rc 0
|
||||
fi
|
||||
done
|
||||
echo "-1" # ← not-found signal on stdout
|
||||
return 1 # ← PROBLEMATIC: rc 1 under set -e
|
||||
}
|
||||
```
|
||||
|
||||
The function uses **two** signals for "not found":
|
||||
1. stdout: echo `-1` (the value callers check)
|
||||
2. exit code: `return 1` (lethal under `set -e`)
|
||||
|
||||
When combined with `set -euo pipefail` (line 2) and captured in a variable assignment, bash 5.2 treats the non-zero exit code as fatal and terminates the script immediately.
|
||||
|
||||
### Why bash 5.2 matters
|
||||
|
||||
Historically, bash suppressed `set -e` for command substitutions inside variable assignments (`x=$(false)` was safe). Bash 5.1+ tightened this behavior to be more POSIX-compliant. On this system's **bash 5.2.37**, the suppression no longer applies, making `var=$(function_that_returns_1)` fatal under `set -e`.
|
||||
|
||||
### Why this is "silent"
|
||||
|
||||
`set -e` exits the script with the exit code of the failed command, but does NOT print any error message (there's no ERR trap configured). The user sees the prompt return with no output — a "silent crash."
|
||||
|
||||
### The first divergence
|
||||
|
||||
**Expected:** After entering "assist", `_alias_find` should return `-1` as a value, the script should check `existing != "-1"` (false), and break out of the loop to proceed to step 2/4.
|
||||
|
||||
**Actual:** `_alias_find` returns exit code 1 alongside the `-1` value. `set -e` intercepts the non-zero exit code from the command substitution and terminates the script before the `if` check at line 218 is ever reached.
|
||||
|
||||
**First divergence point:** `bin/pos-ai-alias:217` — the moment `_alias_find` returns 1 and `set -e` fires.
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Classification
|
||||
|
||||
**Certainty:** FACT
|
||||
|
||||
This is a deterministic, reproducible bug. The crash path is:
|
||||
1. Script starts with `set -euo pipefail`
|
||||
2. User enters a new alias name (not in the env file)
|
||||
3. `_alias_name_valid` passes → code reaches line 217
|
||||
4. `_alias_load` loads empty arrays (file doesn't exist)
|
||||
5. `_alias_find("assist")` iterates empty array, echoes "-1", returns 1
|
||||
6. `existing="$(_alias_find "assist")"` — bash 5.2 does NOT suppress errexit here
|
||||
7. Script exits with code 1 — silent crash
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Recommended fixes
|
||||
|
||||
**Option A (minimal, targeted):** Change line 103 from `return 1` to `return 0`:
|
||||
|
||||
```bash
|
||||
_alias_find() {
|
||||
...
|
||||
echo "-1"
|
||||
return 0 # was: return 1
|
||||
}
|
||||
```
|
||||
|
||||
The not-found state is already communicated via stdout (`-1`). The exit code adds nothing. All callers check the value, not the exit code.
|
||||
|
||||
**Option B (defensive, all call sites):** Add `|| true` at each call site:
|
||||
|
||||
```bash
|
||||
existing="$(_alias_find "$name" || true)" # line 217
|
||||
idx="$(_alias_find "$1" || true)" # line 139
|
||||
idx="$(_alias_find "$name" || true)" # line 318
|
||||
idx="$(_alias_find "$name" || true)" # line 453
|
||||
```
|
||||
|
||||
**Recommended:** Option A — fix the source of the problem rather than patching all consumers. The `-1` sentinel value is the designed interface; the non-zero exit code is an accidental footgun.
|
||||
|
||||
---
|
||||
|
||||
## Handoff
|
||||
|
||||
```
|
||||
Status: ROOT_CAUSE_ESTABLISHED
|
||||
|
||||
Symptom:
|
||||
`pos ai alias` exits silently after user enters alias name in create flow.
|
||||
No error message. No continuation to step 2/4.
|
||||
|
||||
Expected:
|
||||
After entering a valid alias name, the tool should proceed to step 2/4 (Provider selection).
|
||||
|
||||
Actual:
|
||||
The script exits immediately after the name is captured, returning to the shell prompt.
|
||||
|
||||
Root cause:
|
||||
`_alias_find()` at bin/pos-ai-alias:103 returns exit code 1 when a name is
|
||||
not found. This is captured in a variable assignment (line 217) under
|
||||
`set -euo pipefail`. Bash 5.2 does not suppress errexit for command
|
||||
substitutions in variable assignments, causing immediate script termination.
|
||||
|
||||
Classification: FACT
|
||||
|
||||
Evidence:
|
||||
- Reproduced with bash -x trace (stops at `existing=-1`)
|
||||
- Isolated in controlled experiments (set -e + function return 1 = fatal)
|
||||
- Confirmed fix with `|| true` inside command substitution
|
||||
|
||||
Tests performed:
|
||||
1. bash -x bin/pos-ai-alias (non-interactive) — trace confirms stop at line 217
|
||||
2. Isolated `set -euo pipefail` + `x="$(f)"` where f returns 1 — exits
|
||||
3. Same without set -e — works
|
||||
4. Same with `|| true` — works
|
||||
5. Bash version confirmed: 5.2.37
|
||||
|
||||
Alternatives eliminated:
|
||||
- _alias_name_valid regex failure: "assist" matches ^[a-zA-Z][a-zA-Z0-9_-]*$
|
||||
- menu_ask_value returning non-zero: it returns 0 with value on stdout
|
||||
- _alias_load failure: file doesn't exist → returns 0 cleanly
|
||||
- Missing error handling around read: read succeeds, value captured correctly
|
||||
- pipefail interaction: no pipes in the critical path
|
||||
|
||||
Affected components:
|
||||
- bin/pos-ai-alias (lines 103, 139, 217, 318, 453)
|
||||
- All 4 call sites of _alias_find are equally broken
|
||||
|
||||
Scope / decision boundary:
|
||||
- The fix is within pos-ai-alias only, no architectural changes needed
|
||||
- Single-line fix in _alias_find, or 4-site fix in callers
|
||||
|
||||
Remaining uncertainty:
|
||||
- Whether other tools in the project have the same pattern
|
||||
(functions returning 1 captured in var=$(...) under set -e)
|
||||
|
||||
Recommended next agent: Builder
|
||||
|
||||
Reason:
|
||||
The root cause is a single line change in _alias_find (line 103: change
|
||||
`return 1` to `return 0`). The fix is mechanical and well-understood.
|
||||
No architectural decisions needed.
|
||||
```
|
||||
@@ -1,196 +0,0 @@
|
||||
# Detective Report — `pos-ai-hf` GGUF/jq bug (2026-09-04)
|
||||
|
||||
## TL;DR
|
||||
- **Status:** ROOT_CAUSE_ESTABLISHED
|
||||
- **Symptom:** `pos ai hf download Qwen/Qwen2.5-3B-Instruct-GGUF --gguf --output ~/.models` crashes with `jq: error (at <stdin>:0): endswith() requires string inputs` (jq exit 5, after `[!] No HF_TOKEN set` warning).
|
||||
- **Root cause (FACT):** `hf_repo_files()` primary path (`bin/pos-ai-hf:199-204`) returns the **raw HF tree API response**, whose entries have keys `oid, path, size, type` — **no `rfilename`**. Every consumer of `files_json` reads `.rfilename` → gets `null`. Line 336 (`endswith(".gguf")` on null) is the crash site. **The user's null-guard alone is insufficient**: with the guard, `--gguf` would silently filter everything out → `err "No files to download"` (exit 1) instead of downloading the 10 GGUF files. All other modes are also silently broken for every tree-served repo: single-file mode matches nothing, all-files mode writes `null` into the download URL (404), metadata, and summary.
|
||||
- **Fix:** normalize the tree response in `hf_repo_files()` to `[.[] | select(.type == "file") | {rfilename: .path, size: (.size // 0)}]` (same shape the fallback already emits), plus a defense-in-depth string guard on the `--gguf` filter and mode-aware empty-result messages. Verified: 13 files normalize, `--gguf` selects the 10 `.gguf` files, single-file/all-files/meta/summary all work unchanged. Live API validation passed (10/10, 0 non-gguf, 0 nulls).
|
||||
- Expected net change: raw tree result transformed upstream; no semantics change for already-working repos.
|
||||
- Artifacts: this report; fixtures/harness spec → `/tmp/opencode/hf-test2/` (Builder builds it; harness spec in Step 5). No changes made by Detective (read-only).
|
||||
|
||||
## Step 1: Confirm + quantify the crash and every `.rfilename` read — [DONE]
|
||||
|
||||
Fixture `/tmp/opencode/qwen-tree.json` (live capture of `GET /api/models/Qwen/Qwen2.5-3B-Instruct-GGUF/tree/main`): 13 entries, **all `type:"file"`**, keys per entry `oid, path, size, type`, **no `rfilename`**; 10 entries carry `lfs`. `.path` column: `.gitattributes, LICENSE, README.md, qwen2.5-3b-instruct-{fp16-00001-of-00002,fp16-00002-of-00002,q2_k,q3_k_m,q4_0,q4_k_m,q5_0,q5_k_m,q6_k,q8_0}.gguf`.
|
||||
|
||||
Exact reproduction (the exact code, same exit code as the tool — jq exit 5):
|
||||
```
|
||||
$ jq -c '[.[] | select(.rfilename | endswith(".gguf"))]' /tmp/opencode/qwen-tree.json
|
||||
jq: error (at qwen-tree.json:0): endswith() requires string inputs # exit=5
|
||||
```
|
||||
|
||||
Every downstream `.rfilename` read, observed (not inferred):
|
||||
|
||||
| Line | Code | Observed with qwen tree | Verdict |
|
||||
|---|---|---|---|
|
||||
| 333 | single-file `select(.rfilename == $fn)` | `[]` for any fn (`null == "README.md"` → false; exit 0) | silent no-match → `err "No files to download"` |
|
||||
| 336 | `--gguf` `select(.rfilename \| endswith(".gguf"))` | **jq error exit 5** (the reported crash) | the crash |
|
||||
| 339 | all-files `jq -c '.'` | passes all 13 (no filter) | nothing filtered, but downstream 372 breaks |
|
||||
| 372 | loop `jq -r '.rfilename'` | `null` ×13 | URL `…/resolve/main/null` → 404; `curl -o` left an empty `null` file; loop `warn`ed |
|
||||
| 398 | meta `[.[] \| .rfilename]` | `[null,null,…13]` | `.hf-meta` `files` list all null |
|
||||
| 412 | one-file summary `.[0].rfilename` | `null` | `Downloaded: …/null` |
|
||||
|
||||
`jq empty` (line 181), `.size // 0` (lines 373, 414), `[.[].size // 0] \| add // 0` (line 353): unaffected — size handling is already null-safe.
|
||||
|
||||
**Fallback shape verified live** (`GET /api/models/Qwen/Qwen2.5-3B-Instruct-GGUF` → `jq '[.siblings[] | {rfilename: .rfilename, size: (.size // 0)}]'`): 13 entries, 0 null rfilename, all `size: 0` (metadata API has no per-sibling sizes). Shape `{rfilename, size}` — exactly what the normalization produces for the tree path. **Fallback path needs no change.**
|
||||
|
||||
## Step 2: Unsafe-jq sweep (whole file `bin/pos-ai-human`) — [DONE]
|
||||
|
||||
All jq expressions in `bin/pos-ai-hf`, with assessment (only c/p rfilename-related ones are the bug family):
|
||||
|
||||
| Line | Expression | Assessment |
|
||||
|---|---|---|
|
||||
| 181 | `jq empty` on API body | validation only; safe |
|
||||
| 210 | `[.siblings[]\|{rfilename:.rfilename, size:(.size//0)}]` | correct shape; null-safe; **leave as is** |
|
||||
| 217 | `jq -sRr @uri` (query encode) | safe |
|
||||
| 254 | `.defaultBranch // empty` | null-safe; safe |
|
||||
| 298 | `jq 'length'` (search) | safe |
|
||||
| 302 | `.[] \| "…\(.id)…\(.downloads // 0)…\(.likes // 0)"` | search API provides these; `// 0` guards; safe |
|
||||
| **333** | `select(.rfilename == $fn)` | **AFFECTED**: null vs string → silently `[]`. Fixed by normalization (works after); no other string-op risk. |
|
||||
| **336** | `select(.rfilename \| endswith(".gguf"))` | **THE CRASH**. Category (a): string function on possibly-null field. |
|
||||
| 339 | `jq -c '.'` | passthrough; safe |
|
||||
| 343 | `jq 'length'` | safe |
|
||||
| 353 | `[.[].size // 0] \| add // 0` | null-safe on size; safe |
|
||||
| **372** | `jq -r '.rfilename'` | **Affected**: prints literal `null` → bad URL/404 + empty `null` target file |
|
||||
| 373 | `jq -r '.size // 0'` | null-safe; safe |
|
||||
| **398** | `jq -c '[.[] \| .rfilename]'` | **Affected**: meta list all nulls |
|
||||
| **412** | `jq -r '.[0].rfilename'` | **Affected**: summary prints `null` |
|
||||
| 414 | `jq -r '.[0].size // 0'` | null-safe; safe |
|
||||
| 447 | `jq -r '.downloaded_at // "unknown"'` (meta file) | safe; meta file is JSON |
|
||||
|
||||
Category (a) string-function-on-null type: only line 336 in this file (no `startswith`/`contains`/`test` in `pos-ai-hf` at all — grep confirmed; the other matches above are in other tools/service files, out of scope). Category (b) assumes-field-primary-API-returns: only the rfilename family above (lines 333/336/372/398/412). Category (c) covered in Step 1. Category (d) silent no-match on null: line 333 (only one). **No other crash-class bugs found; the rfilename family is the whole story.**
|
||||
|
||||
Related-but-out-of-scope notes (observations, not part of this fix):
|
||||
- `hf_api` uses `curl -sS` without `-L`; HF redirects some aliases (verified: `runwayml/stable-diffusion-v1-5/tree/main` → 307 → `stable-diffusion-v1-5/stable-diffusion-v1-5`). Such repos fail on BOTH tree and fallback (`API request failed (HTTP 307)`). Pre-existing; unrelated to this bug; would need `-L` or canonical-resolution; flag to Builder/Architect, don't fold in.
|
||||
- Tree endpoint is non-recursive; repos with subdirectories (e.g. SD-v1-5: `feature_extractor/`, …) only list top-level entries + `type:"directory"` markers. Fix filters out directories → all-files mode skips subdir files (same as pre-bug behavior; tree path never listed them). Optional follow-up: `?recursive=true` — requires `# POS_FLAGS`/docs change, NOT part of this minimal fix.
|
||||
- `hf_download_file` URL building concatenates raw `path` into URL; files with spaces would need URL-encoding (`@uri`). Pre-existing; rare for models; not this bug.
|
||||
- Names that are `-`, `.` etc. unaffected.
|
||||
|
||||
## 3. `hf_download_file` / `hf_api` related to THIS bug — [DONE]
|
||||
|
||||
- `hf_download_file` (264-286): no field assumptions of its own; takes URL+target. It is a victim: with raw-tree null rfilename, URL `…/resolve/main/null` returns 404, curl fails → `warn "Download interrupted for null (resume…)"`, and an empty `null` file remains in the model dir (then `cmd_list` counts it as size 0). After normalization the function works as designed (has `-L` for HF's 302→CDN; `-C -` resume; empty-file guard). **No change needed.**
|
||||
- `hf_api` (134-186): 200/401/403/404/429 handling + `jq empty` validation — nothing rfilename-related. **No change needed** (the 307 note in Step 2 is separate).
|
||||
- `hf_resolve_branch` (241-261): live-verified defaultBranch "main" resolves fine; unaffected.
|
||||
|
||||
## 4. Fix spec — Builder-executable — [DONE]
|
||||
|
||||
**Objective:** make the primary tree path emit the same `{rfilename, size}` shape the rest of the file (and the fallback path) already assume. Minimal, CLI semantics preserved (all options, filters, messages keep their meaning; only multi-mode empty-result messages get mode-specific text).
|
||||
|
||||
Where: `hf_repo_files()` body, primary branch, current lines 198-204.
|
||||
|
||||
**Change 1 — normalize tree response (the fix).** Replace:
|
||||
```bash
|
||||
local endpoint="/models/${ns}/${repo}/tree/${branch}"
|
||||
local result
|
||||
if result="$(hf_api "$endpoint" 2>/dev/null)"; then
|
||||
printf '%s' "$result"
|
||||
return 0
|
||||
fi
|
||||
```
|
||||
with (exact code for Builder):
|
||||
```bash
|
||||
local endpoint="/models/${ns}/${repo}/tree/${branch}"
|
||||
local result
|
||||
if result="$(hf_api "$endpoint" 2>/dev/null)"; then
|
||||
# Tree API returns {type,path,size,oid[,lfs]} per entry — normalize to the
|
||||
# {rfilename,size} shape the rest of the pipeline expects (same as fallback).
|
||||
# Skip "directory" entries: they have no resolvable file URL.
|
||||
printf '%s' "$result" | jq '[.[] | select(.type == "file") | {rfilename: .path, size: (.size // 0)}]'
|
||||
return 0
|
||||
fi
|
||||
```
|
||||
- This reuses the exact jq via one pipe; `set -euo pipefail` semantics: if the transform ever fails, `result` was already valid JSON so it fails before the pipe — fine (same failure mode as a jq typo elsewhere).
|
||||
- `.size // 0` covers entries lacking `size` (dirs carry size 0; all observed files carry real size incl. LFS files, whose top-level `size` is the true byte size).
|
||||
- Do NOT change the fallback (lines 210-211) — it already emits `{rfilename, size}` (had the same shape requirement; verified live).
|
||||
- Edge: tree returning a non-array (code-200 error object) — `[.[] | select… | {…}]` yields `[{rfilename:null}]`-style or `[]`; `--gguf` guard + Step-3 messages convert that to a graceful error. Acceptable; no extra guard required.
|
||||
|
||||
**Change 2 — defense-in-depth guard in the `--gguf` filter (line 336).** Recommended, type-check form (strictly safe even if `rfilename` were a non-string non-null):
|
||||
```bash
|
||||
filtered_files="$(printf '%s' "$files_json" | jq -c '[.[] | select((.rfilename | type) == "string" and (.rfilename | endswith(".gguf")))]')"
|
||||
```
|
||||
Equivalent accepted: `select(((.rfilename // "") | endswith(".gguf")))}` — both are purely defensive here (normalized data always strings); must NOT become a replacement for Change-2-Normalization: with normalization, guard-or-not both select the 10 gguf. Verified equivalent on fixture: guarded 10, unguarded 10.
|
||||
|
||||
**Change 3 — mode-aware "no files" message (replaces line 344, `[ "$file_count" -gt 0 ] || err "No files to download"`).** Keep exit-1 semantics, distinct messages per mode:
|
||||
```bash
|
||||
local file_count
|
||||
file_count="$(printf '%s' "$filtered_files" | jq 'length')"
|
||||
if [ "$file_count" -eq 0 ]; then
|
||||
if [ -n "$filename" ]; then
|
||||
err "File not found: $filename in $repo_id (branch: ${branch})"
|
||||
elif [ "$GGUF_ONLY" -eq 1 ]; then
|
||||
err "No .gguf files found in $repo_id${branch:+ (branch: $branch)} — try without --gguf"
|
||||
else
|
||||
err "No files to download"
|
||||
fi
|
||||
fi
|
||||
```
|
||||
Not required for the crash fix; required by edge-case spec (no-gguf repo → graceful, distinct message, not crash), and fixes the misleading "No files to download" in single-file mode.
|
||||
|
||||
**Line content checks after Changes 1-3 (verified by fixture/live):**
|
||||
- single-file line 333: fits; `select(.rfilename == $fn)` on normalized → 1 for exact `README.md` / `qwen2.5-3b-instruct-q4_k_m.gguf`.
|
||||
- all-files line 339: fits; loop line 372 pulls real rfilename; size line 373 real; meta line 398 real list; summary 412 real.
|
||||
- **Lines 372/373/376/377/398/412 need NO change** once normalized (checked on fixture).
|
||||
|
||||
## 5. Edge cases — [DONE]
|
||||
|
||||
| Case | Behavior before fix | After fix |
|
||||
|---|---|---|
|
||||
| Repo w/ only `type:"directory"` (tree) | null → crash/downstream; e.g. gguf mode crashes, all-files nulls | `select(.type=="file")` → `[]` → mode-aware graceful error |
|
||||
| Empty array / empty siblings tree | crash / silent | `[]` → graceful error |
|
||||
| `--gguf` on repo w/o .gguf | crash | `err "No .gguf files found in …"` (exit 1, no crash) |
|
||||
| File entry missing `size` / size:0 | `.size // 0` everywhere → ok | unchanged; normalization also `// 0` |
|
||||
| LFS files (`.gguf` 2GB+) | n/a (never reached) | sizes real (`2104932768`); disk pre-check works |
|
||||
| Repo w/ subdirs (non-recursive tree) | null loop | dirs filtered; subtree files not listed — pre-existing semantics (flag in Step 2, decision boundary for follow-up only) |
|
||||
| `--branch` non-main | field absent regardless | branch is only URL+tree-parameter; normalized same way |
|
||||
|
||||
## 6. Test plan (harness spec for Builder) — [DONE]
|
||||
|
||||
**Location:** `/tmp/opencode/hf-test2/` (workspace must NOT gain test files; repo has no harness for pos-ai-hf; user requirement = new fixture-based harness).
|
||||
|
||||
**Key constraint — DO NOT `source` bin/pos-ai-hf in the harness**: the tool executes flag parsing + `usage`/`cmd_download` at top level (`set -euo pipefail`; no-args → `usage` → `exit 0` is a NAK). The harness must test the **jq transforms in isolation** (option b of the brief). Extraction of the functions via `sed -n` is __not__ recommended (fragile); fix the documented transforms — the transforms ARE the bug.
|
||||
|
||||
**Fixture files (create in harness setup, static content):**
|
||||
- `fixtures/tree-files.json` — copy of `/tmp/opencode/qwen-tree.json` (13 files, rfilename ABSENT; 10 .gguf).
|
||||
- `fixtures/tree-with-dirs.json` — copy of `/tmp/opencode/sd-tree.json` (8 files + 7 `type:"directory"`).
|
||||
- `fixtures/tree-empty.json` — `[]`.
|
||||
- `fixtures/tree-nogguf.json` — e.g. `[{"type":"file","path":"README.md","size":100}]`.
|
||||
- Set at top: `NORM='[.[] | select(.type == "file") | {rfilename: .path, size: (.size // 0)}]'` and `GGUF_FILTER='[.[] | select((.rfilename | type) == "string" and (.rfilename | endswith(".gguf")))]' FN_FILTER='[.[] | select(.rfilename == $fn)]'` — **duplicated strings; if either diverges from the file, tests catch drift when `grep -F` checks below run.**
|
||||
|
||||
**Assertions (each a small `t_<name>` function; count PASS/FAIL; exit non-zero on any fail):**
|
||||
1. `t_tree_normalize`: `jq -c "$NORM" tree-001.json` → length 13; every `.size` is number; no entry has `rfilename == null`.
|
||||
2. `t_gguf_on_normalized`: pipe NORM(tree-001) → GGUF_FILTER → length 10; contains `qwen2.5-3b-instruct-q4_k_m.gguf`; NOT contains `README.md`/`LICENSE`/`.gitattributes`.
|
||||
3. `t_single_file`: `jq -c --arg fn "qwen2.5-3b-instruct-q4_k_m.gguf" "$FN_FILTER"` on NORM(tree-001) → length 1; on `--arg fn "no-such-file"` → 0 (no crash).
|
||||
4. `t_all_files_passthrough`: `jq -c '.'` → 13; loop pipe `.[]` → 13 rows, each with string rfilename (no `null`).
|
||||
5. `t_empty`: NORM on tree-empty.json → `[]` | run the mode-0 guard → error message path (grep the code); i.e. assert `printf '[]' | jq "$NORM"` outputs `[]` and file_count logic (replicate `[ "$(…|jq 'length')" -eq 0 ]`) succeeds.
|
||||
6. `t_nogguf`: NORM(tree-nogguf.json) → GGUF_FILTER → length 0, no crash; assert the code contains the "No .gguf files" branch (grep).
|
||||
7. `t_defense_guard`: pipe **raw** qwen-tree.json (unnormalized) through GGUF_FILTER → length 0, exit 0 (proves guard null-proof and non-weakening: the sole difference 10→0 is caused by normalization, guard itself no-op).
|
||||
8. `t_dirs_excluded`: NORM(tree-with-dirs.json) → length 8 (files only; 7 dirs dropped).
|
||||
9. `t_code_sync` (drift check): `grep -Fq 'select(.type == "file")' <repo>/bin/pos-ai-hf` and `grep -Fq 'endswith(".gguf")'` present — catches D it if Builder changed jq inline, test stays honest.
|
||||
|
||||
**Live smoke (optional; fast, no download):** `curl` tree for Qwen → NORM → GGUF_FILTER → assert 10 rfilenames, 0 README. (This exact pipe was executed in Step 1; pass.)
|
||||
|
||||
## 7. Verification commands for Builder (after implementing)
|
||||
|
||||
- `bash -n bin/pos-ai-hf` (repo copy — the installed /usr/local/bin copy is byte-identical; contract must be fixed in the repo copy).
|
||||
- `bash /tmp/opencode/hf-test2/run-tests.sh` → all PASS.
|
||||
- `make gen && make check && make lint` → doc tables/registry unchanged; expect green, `0 FAIL, 0 WARN`, and `git diff` limited to `bin/pos-ai-hf` (+ any doc touch required by CONVENTION pointers; no GEN:START/END blocks change).
|
||||
- Live no-download validation (already demonstrated passing):
|
||||
```bash
|
||||
curl -sS "https://huggingface.co/api/models/Qwen/Qwen2.5-3B-Instruct-GGUF/tree/main" \
|
||||
| jq '[.[] | select(.type=="file") | {rfilename:.path,size:(.size//0)}]' \
|
||||
| jq '[.[] | select(.rfilename|endswith(".gguf"))]' | jq 'length' # expect 10, no README/LICENSE
|
||||
```
|
||||
- Optional tiny-download proof: `pos ai hf download Qwen/Qwen2.5-3B-Instruct-GGUF LICENSE --output /tmp/hf-small` → expect `.hf-meta` listing `LICENSE` and 1-file summary with real size; deletes nothing else.
|
||||
- Full ~2GB download verified: OUT OF SCOPE (explicit).
|
||||
|
||||
## Handoff
|
||||
|
||||
Status: **ROOT_CAUSE_ESTABLISHED**
|
||||
- Symptom: crash `jq: endswith() requires string inputs` (line 336) on --gguf; silent no-match single-file; `null` URLs/meta/summary in all-files.
|
||||
- Expected vs actual: primary tree response should look like the siblings metadata (`rfilename`-keyed) but arrives free-`.rfilename` keys; first divergence = `hf_repo_files` returns raw tree (line 201-204).
|
||||
- Root cause: missing shape normalization of `/tree` response in `hf_repo_files()` — user's endswith guard insufficient (guarded --gguf would download 0 files / "No files to download").
|
||||
- Classification: FACT (crash & downstream effects reproduced on live fixture; normalization + guard live-validated elsewhere).
|
||||
- Alternatives eliminated: (a) network/API failure — endpoints live 200 & JSON; (b) `rfilename` present but null — keys are absent (but `oid,path,size,type`), confirmed on fetch; (c) fallback path defect — live-verified correct shape; (d) curl/URL issue in hf_download_file — reached only if loop got a non-null name; function itself defect-free.
|
||||
- Affected components: `bin/pos-ai-hf` — `hf_repo_files()` (primary branch), `cmd_download()` lines 333/336/341-344 (message branch), and downstream read sites 372/398/412 (no change needed once normalized).
|
||||
- Recommended next agent: **Builder** — fix is exactly scoped: one transform in `hf_repo_files()`, one defense-in-depth guard line, one message branch; implement per spec Step 4 and run Step 7 verification. **Tester** (after Builder) — no committed harness; new /tmp/opencode/hf-test2 harness + optional repeat live tests; recommend adding to repo test suite if project adopts (decision boundary: Architect).
|
||||
- Remaining uncertainty: none material on the bug; only flagged out-of-scope items (hf_api `-L`/307 for alias repos; non-recursive tree semantics) for maintainers.
|
||||
- Changes made by Detective: none (read-only).
|
||||
@@ -1,208 +0,0 @@
|
||||
# ytsync channel handle bug — root cause investigation
|
||||
|
||||
Date: 2026-09-04
|
||||
Agent: Detective (read-only)
|
||||
Status: ROOT_CAUSE_ESTABLISHED
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Confirmed root cause:** `run_probe()` probes the user's channel URL verbatim; yt-dlp `--flat-playlist -J` on a bare channel URL (`@handle`, `/c/`, `/user/`, `/channel/<ID>`, `music.youtube.com/channel/<ID>`) returns the channel's **TAB structure** (Videos/Live/Shorts: `_type:"playlist"`, `url:null`, `id==channel_id`) instead of videos. `collect_entries()` records **every** entry unfiltered, so the three tabs become three "new videos" that `download_video()` tries to fetch as `watch?v=<channel_id>` → `[youtube] <channel_id>: This video is unavailable`.
|
||||
- **Fix (validated, not implemented):** (a) probe-time canonicalization — new helper `canonical_channel_url()` appends `/videos` to bare channel URLs, called at the top of `run_probe()` (fixes both `add` and `sync` of already-stored bare-handle registry entries, no migration); (b) defense-in-depth filter in `collect_entries()` keeping only watchable entry URLs (`watch?v=`, `youtu.be/`, `/shorts/`), plus a `_type=="video"` guard on the single-object fallback (handles empty channels gracefully).
|
||||
- **Artifacts:** probes under `/tmp/opencode/*.json`; validated stub harness at `/tmp/opencode/ytsync-test/run-tests.sh` (17 assertions; 5 fail on the unfixed script, all must pass after the fix).
|
||||
- **Classification:** FACT (reproduced live + function-level).
|
||||
|
||||
## Step 1: Read evidence + code
|
||||
Read `/tmp/opencode/ytsync-probe.json`, `/tmp/opencode/ytsync-vtab.json`, and all of `bin/pos-media-ytsync` (1191 lines). Key code facts:
|
||||
- `run_probe()` line 316: `yt-dlp --flat-playlist -J --no-warnings -- "$url"`.
|
||||
- `collect_entries()` lines 374-392: records `.id`+`.title` of **every** `.entries[]` element; no `_type`/`url` filter; fallback to single object when no entries.
|
||||
- `is_signin_skipped()` line 401: only skips empty ids / `[Private`/`[Deleted`/`[Unavailable` titles — tab entries pass.
|
||||
- `classify_url()` line 157: video / playlist / channel.
|
||||
- Registry stores the **original** URL (`finish_add` line 773-774). `sync` re-probes the stored URL verbatim (`run_sync_set` line 923 → `pass_prepare` line 559 → `run_probe "$S_URL"`).
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 2: Probe channel URL forms (my own reproductions)
|
||||
All probes: `yt-dlp 2026.08.19 --flat-playlist -J --no-warnings`. Summarized as `shapes → outcome`:
|
||||
|
||||
| Probe URL | Probe shape | Verdict |
|
||||
|---|---|---|
|
||||
| `https://www.youtube.com/@3blue1brown` | `_type:playlist`, `id:@3blue1brown`, `playlist_count:3`; entries = 3 tabs (`_type:"playlist"`, `url:null`, `id==channel_id`) | **TABS — the bug** |
|
||||
| `https://www.youtube.com/@3blue1brown/videos` | `playlist_count:151`; entries = 151 real videos (`_type:"url"`, `url:watch?v=…`) | canonical target OK |
|
||||
| `https://www.youtube.com/@3blue1brown/shorts` | `playlist_count:81`; real entries, `url:youtube.com/shorts/<id>` | OK (needs `/shorts/` in filter) |
|
||||
| `https://www.youtube.com/@3blue1brown/streams` | `playlist_count:10`; real entries `watch?v=` | OK |
|
||||
| `https://www.youtube.com/@3blue1brown/live` | **rc=1**, stdout `null`, stderr `The channel is not currently live` | probe-fail path (unchanged) |
|
||||
| `https://www.youtube.com/@3blue1brown/playlists` | `playlist_count:24`; entries `_type:"url"` **but `url:playlist?list=…`** | false-positive if unfiltered |
|
||||
| `https://www.youtube.com/@3blue1brown/featured` | title "… - Home", 7 entries, `id:null`, playlist/tab urls | false-positive if unfiltered |
|
||||
| `https://www.youtube.com/c/3Blue1Brown` | **tabs** (`playlist_count:3`) | needs canonicalization |
|
||||
| `https://www.youtube.com/c/3Blue1Brown/videos` | 151 real videos | OK |
|
||||
| `https://www.youtube.com/user/3Blue1Brown` (+`/videos`) | **rc=1, HTTP 404** | `user/` dead for this channel |
|
||||
| `https://www.youtube.com/user/pewdiepie` | **tabs** (`playlist_count:2`) | works → needs canonicalization |
|
||||
| `https://www.youtube.com/user/MarquesBrownlee` | **tabs** (`playlist_count:3`) | works → needs canonicalization |
|
||||
| `https://www.youtube.com/channel/UCYO_…` | **tabs** | needs canonicalization |
|
||||
| `https://www.youtube.com/channel/UCYO_…/videos` | 151 real videos | OK |
|
||||
| `https://www.youtube.com/playlist?list=…` | `_type:playlist`; entries real `watch?v=` | OK — must stay untouched |
|
||||
| `https://www.youtube.com/watch?v=…` / `https://youtu.be/…` | `_type:"video"` single object, no entries | OK — fallback path |
|
||||
| `@3blue1brown` (no domain) | **rc=1** `[generic] not a valid URL` | latent ytsync wart (is_youtube_url accepts `@*`) |
|
||||
| `youtube.com/@3blue1brown` (no proto) | tabs | works; canonicalizable |
|
||||
| `youtube.com/@3blue1brown/videos` | 151 real videos | OK |
|
||||
| `…/@3blue1brown/videos/` (trailing slash) | 151 real videos | OK — suffix check must strip slash |
|
||||
| `…/@3blue1brown/videos?view=0&sort=dd` | 151 real videos | OK — suffix check must strip query |
|
||||
| `…/@3blue1brown/VIDEOS` (uppercase) | **rc=1** `channel does not have a VIDEOS tab` | yt-dlp suffix is case-sensitive |
|
||||
| `https://music.youtube.com/channel/UCYO_…` | tabs | needs canonicalization |
|
||||
| `https://music.youtube.com/channel/UCYO_…/videos` | 151 real videos | OK |
|
||||
|
||||
Live functional reproduction (read-only, `--dry-run` writes nothing — registry write is behind `DRY_RUN -eq 0`, history only in non-dry runs):
|
||||
```
|
||||
$ bin/pos-media-ytsync sync --dry-run
|
||||
Source : https://www.youtube.com/@3blue1brown ← stored URL re-probed verbatim
|
||||
Resolved : 3Blue1Brown (channel · 3 videos) ← the 3 TABS
|
||||
New : 3 would be downloaded (0 already present)
|
||||
3Blue1Brown - Videos.mp4 / - Live.mp4 / - Shorts.mp4 ← tab titles as "videos"
|
||||
```
|
||||
Matches the reported `add` output exactly (first divergence: `parse_probe`+`collect_entries` treating tab entries as videos).
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 3: Trace the call surface (who probes what URL)
|
||||
|
||||
`run_probe()` callers:
|
||||
1. `cmd_add` line 876 — `run_probe "$url"` (user-supplied add URL; explicit mode).
|
||||
2. `ask_url_interactive` line 839 — `run_probe "$u"` (interactive add; `finish_add` stores `$ASKED_URL`).
|
||||
3. `pass_prepare` line 559 — `run_probe "$S_URL"`; `S_URL` comes from the registry line (read at `run_sync_set` line 923).
|
||||
|
||||
Mandatory conclusion: **the stored registry URL is re-probed verbatim on every sync** (confirmed by code trace AND the live dry-run above). Therefore canonicalization **inside `run_probe()`** fixes both flows at once — the existing machine registry entry (`3blue1brown … https://www.youtube.com/@3blue1brown`) needs **no migration**; it simply re-canonicalizes each probe.
|
||||
|
||||
`parse_probe`/`P_KEY` impact: both the bare-handle and `/videos` probe shapes carry `uploader_id:"@3blue1brown"` → `P_KEY="@3blue1brown"` → slug `3blue1brown` (matches existing registry slug). `P_TITLE` changes cosmetically (`3Blue1Brown` → `3Blue1Brown - Videos`); it is stored as the registry `S_TITLE` field but never displayed by `cmd_list`/digests. No functional impact.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 4: Fix spec (validated, Builder-executable)
|
||||
|
||||
Recommended combination: **canonicalization (primary) + entry filter + fallback guard (defense-in-depth)**. Canonicalization alone fixes the report end-to-end; the filter alone would degrade a bare-handle add to "0 videos" (graceful but useless). Both are needed; both validated by simulation below.
|
||||
|
||||
### 4.1 New helper — insert after `classify_url()` (after line 176), before `sanitize_component()`
|
||||
|
||||
```bash
|
||||
# ── Channel URL canonicalization ─────────────────────────────────────
|
||||
# yt-dlp --flat-playlist on a bare channel URL returns the channel's
|
||||
# TAB list (Videos/Live/Shorts; _type "playlist", url null, id==channel_id),
|
||||
# not videos. Appending /videos makes the probe return the real videos.
|
||||
canonical_channel_url() { # add /videos to bare channel URLs; echo canonical
|
||||
local u="$1"
|
||||
case "$u" in
|
||||
@*) u="https://www.youtube.com/$u" ;; # bare 'handle' → full URL
|
||||
esac
|
||||
[ "$(classify_url "$u")" = "channel" ] || { printf '%s' "$u"; return 0; }
|
||||
local path="${u%%\?*}"
|
||||
path="${path%%\#*}"
|
||||
path="${path%/}"
|
||||
case "${path##*/}" in
|
||||
videos | shorts | streams | live | playlists | featured | releases | podcasts | search)
|
||||
printf '%s' "$u" ;;
|
||||
*)
|
||||
printf '%s/videos' "$u" ;;
|
||||
esac
|
||||
}
|
||||
```
|
||||
|
||||
Behavior (all 16 cases tested PASS in the harness): bare `@handle` full URL → `…/videos`; bare `@3blue1brown` (no domain, fixes the latent generic-error failure) → `https://www.youtube.com/@3blue1brown/videos`; `youtube.com/@…` no-protocol → `+ /videos`; `/c/NAME`, `/user/NAME`, `/channel/ID` → `+ /videos`; `music.youtube.com/channel/ID` → `+ /videos`; already-suffixed `/videos` `/shorts` `/streams` `/live` `/playlists` `/featured`, with trailing slash or query → untouched; `?list=` / `watch?v=` / `youtu.be/<id>` → untouched.
|
||||
|
||||
### 4.2 `run_probe()` — one line, after `local url="$1"` (line 317)
|
||||
|
||||
```bash
|
||||
url="$(canonical_channel_url "$url")"
|
||||
```
|
||||
|
||||
That is the whole integration point: `add` (both modes) and `sync` (stored URLs) now probe the Videos tab. Nothing downstream changes (S_URL stays the original; display is cosmetic).
|
||||
|
||||
### 4.3 `collect_entries()` — two edits (lines 374-392)
|
||||
|
||||
(a) Line 381 — add a `select` so only watchable entries are collected (exact in-file quoting verified):
|
||||
|
||||
```bash
|
||||
mapfile -t pairs < <(jq -r '.entries[] | select((.url // "") | test("watch\\?v=|youtu\\.be/|/shorts/")) | ((.id // "") + "\u001f" + (.title // ""))' "$PROBE_JSON")
|
||||
```
|
||||
|
||||
(b) Lines 388-391 — guard the single-object fallback so empty tab/playlist probes never become one bogus video:
|
||||
|
||||
```bash
|
||||
elif [ "$(jq -r '._type // ""' "$PROBE_JSON")" = "video" ]; then
|
||||
ENTRY_IDS+=("$(jq -r '.id // ""' "$PROBE_JSON")")
|
||||
ENTRY_TITLES+=("$(jq -r '.title // ""' "$PROBE_JSON")")
|
||||
fi
|
||||
```
|
||||
|
||||
Filter counts (validated): tab probe → 0; videos → 151; shorts → 81; streams → 10; playlists-tab → 0; featured → 0; playlist `?list=` → 16; video `?v=` → 1 (fallback). Empty channel (`entries:[]`, `_type:"playlist"`) → 0 → `pass_execute` line 624 prints `Sync complete: 0 new, 0 already present, 0 failed` (no 3-failure spam).
|
||||
|
||||
### 4.4 Error behavior — empty channel / not-live channel
|
||||
- Channel with zero videos: `/videos` probe → `entries:[]` → 0 valid → graceful "0 new" summary (verified with `empty-videos.json` fixture).
|
||||
- Explicit `/live` when not live: probe rc=1 → existing `notfound`/`unreachable` handling (sync: skip + `FAILED (probe)` history; add: retry prompt). Unchanged.
|
||||
- Explicit `/playlists` / `/featured` URLs: untouched by canonicalization; filter now drops all playlist-URL entries → 0 new / 0 failed (previously N failed downloads). Graceful improvement, no new error path.
|
||||
|
||||
### 4.5 Things deliberately NOT changed
|
||||
- `parse_probe`, `P_KEY`, registry line format, download loop, `classify_url` — untouched.
|
||||
- Registry migration — none required (probe-time canonicalization covers stored URLs).
|
||||
- Storing the canonical URL in the registry for new adds — optional cosmetic enhancement, NOT needed for correctness; recommended to skip to keep the change minimal.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 5: Test plan
|
||||
|
||||
Harness (new, stub-based — no ytsync harness exists in repo): `/tmp/opencode/ytsync-test/run-tests.sh`
|
||||
- Self-contained: generates 6 inline fixture JSONs (tab-probe, videos+shorts-tab, playlist, playlists-tab, single-video, empty-videos), stub `common.sh`/`notify.sh`, a recording fake `yt-dlp`; truncates `bin/pos-media-ytsync` at the `# ── Argument dispatch ──` marker and sources it (function-level tests, no network/state).
|
||||
- 17 assertions: 7× `classify_url` regression; 16× `canonical_channel_url` matrix; 8× `collect_entries` fixture behavior; 1× `run_probe` URL recording.
|
||||
- Baseline on the unfixed script: **12 PASS / 5 FAIL** (exactly the fix-specific assertions fail — proves the harness discriminates the bug). After the fix: all 17 must pass.
|
||||
|
||||
Recommended Builder self-verification (in order):
|
||||
1. `bash -n bin/pos-media-ytsync`
|
||||
2. `bash /tmp/opencode/ytsync-test/run-tests.sh` → 0 failed
|
||||
3. `./bin/pos-media-ytsync sync --dry-run` → `Resolved : 3Blue1Brown (channel · 151 videos)`, real titles, `151 would be downloaded`
|
||||
4. Real limited download into temp dirs (never the real `~/Videos`):
|
||||
`YTSYNC_STATE_DIR=/tmp/yts-state YTSYNC_VIDEOS_DIR=/tmp/yts-vids ./bin/pos-media-ytsync add https://www.youtube.com/@3blue1brown`
|
||||
then interrupt after the first videos (safe: temp dir; yt-dlp renames atomically, archive records completed ones); verify `archive/3blue1brown.txt` grows and flat `<title>.mp4` layout.
|
||||
5. `make check` and `make lint` (must end `0 FAIL, 0 WARN`); commit with conventional prefix + AGENT_TODO.md Done move.
|
||||
|
||||
Edge-case matrix (URL form → after-fix behavior):
|
||||
- bare `@handle` / `/c/` / `/user/` (resolvable) / `/channel/ID` / `music.…/channel/ID` → canonicalized to `/videos` → real videos.
|
||||
- `@handle` (no domain) → full canonical URL → real videos (fixes latent failure).
|
||||
- `/videos`, `/shorts`, `/streams`, `/live`, `/playlists`, `/featured` explicit suffixes → untouched; shorts/streams pass filter; playlists/featured yield graceful 0; live fails gracefully if not live.
|
||||
- `?list=`, `?v=`, `youtu.be/<id>` → untouched, behave as today.
|
||||
- `/user/` URLs that 404 → unchanged notfound failure path (pre-existing; `/user/` is deprecated by YouTube).
|
||||
- Uppercase suffix (`@h/VIDEOS`) → untouched → yt-dlp error (pre-existing; suffix check is intentionally case-sensitive).
|
||||
- Existing stored bare-handle registry entries → work without migration; archive has no stale ids (the 3 broken downloads never reached the archive).
|
||||
|
||||
[DONE]
|
||||
|
||||
## Handoff
|
||||
|
||||
Status: ROOT_CAUSE_ESTABLISHED
|
||||
|
||||
Symptom: `pos media ytsync add https://www.youtube.com/@3blue1brown` (and `sync` of the stored URL) resolves the channel's 3 tabs as videos and fails each download with `[youtube] UCYO_jab_es: This video is unavailable`.
|
||||
|
||||
Expected: resolve the channel's actual videos and download new ones incrementally.
|
||||
|
||||
Actual: probe returns tab entries (Videos/Live/Shorts); `collect_entries` records them unfiltered; 3 failed downloads; summary "0 new … 3 failed".
|
||||
|
||||
Root cause: `run_probe` probes the bare channel URL verbatim; yt-dlp flat-playlist on channel URLs returns channel tabs (`_type:"playlist"`, `url:null`, `id==channel_id`), not videos; `collect_entries` has no non-video entry filter, so tabs are downloaded as `watch?v=<channel_id>` and fail.
|
||||
|
||||
Classification: FACT (live reproduction: sync dry-run shows the 3 tab titles; function-level reproduction: tab fixture yields 3 entries; probe evidence table).
|
||||
|
||||
Evidence: probe JSONs `/tmp/opencode/ytsync-{probe,vtab}.json` + my additional probes; filter counts above; harness baseline 12/17.
|
||||
|
||||
Tests performed: 24 URL-form probes (matrix above); jq filter validation on 6 shapes; fallback guard validation on 4 fixtures; live `sync --dry-run` reproduction; harness mechanics + baseline run.
|
||||
|
||||
Alternatives eliminated:
|
||||
- "yt-dlp version regression" — no; current yt-dlp behavior is inherent for channel URLs (tabs), `/videos` suffix returns videos (probed).
|
||||
- "URL should be classified as playlist" — no; channel classification is correct; the probe target is the problem.
|
||||
- "Filter `_type=="url"` only" — rejected: `/playlists`-tab entries are `_type:"url"` with playlist IDs; the watch-URL test is the correct discriminator (validated).
|
||||
- "Registry migration needed" — no; probe-time canonicalization covers stored URLs (traced + live dry-run).
|
||||
|
||||
Affected components: `bin/pos-media-ytsync` — `run_probe` (line 316), `collect_entries` (lines 374-392); new helper `canonical_channel_url`; no change to registry format.
|
||||
|
||||
Scope / decision boundary: none — pure bug fix within the tool; no architectural decisions required.
|
||||
|
||||
Recommended next agent: Builder
|
||||
|
||||
Reason: root cause and the exact code-level fix (validated by simulation and the stub harness) are established; implementation + `make gen/check/lint` + harness green are Builder work.
|
||||
|
||||
Changes made by Detective: none (repo untouched; scaffolding + harness in `/tmp/opencode/` only).
|
||||
@@ -1,282 +0,0 @@
|
||||
# Detective Report: `pos ai server start` — llama.cpp Post-Install Breakage
|
||||
|
||||
**Date:** 2026-09-06
|
||||
**Investigator:** Detective (read-only)
|
||||
**Symptom (user paste):** After `bash apps/install.sh llamacpp` installed llama.cpp b10822, `pos ai server start Qwen-Qwen3-1.7B-GGUF` fails: version "unknown", default flags rejected, model-not-found, and dbus error at the end.
|
||||
**Severity:** High — complete server-start failure after a clean install; the generated unit is silently corrupted.
|
||||
|
||||
## TL;DR
|
||||
|
||||
| Hyp | Verdict | One-line evidence |
|
||||
|-----|---------|-------------------|
|
||||
| H1 | **CONFIRMED** (wrong mechanism) | `llama-server --version` prints to **stderr** (`common/build-info.h:13` default `FILE* = stderr`); `detect_llama_version` discards it with `2>/dev/null` → always "unknown" |
|
||||
| H2 | **REFUTED as user's cause; latent defect stands** | Archive ships `llama-server`; installer symlinks it (verified); but bare `server` fallback picks unrelated binaries when llama-server is missing (fixture-proven) |
|
||||
| H3 | **REFUTED as stated; REAL bug found** | Word-boundary regex MATCHES all 5 flags in real help; the actual failure is `printf|grep -q` + `set -o pipefail` **SIGPIPE race** (pipeline rc=141) — flaky per-run |
|
||||
| H4 | **CONFIRMED** | `resolve_model` accepts only files; a dir containing one `.gguf` under `HF_DOWNLOAD_DIR` fails with exact user error (reproduced) |
|
||||
| H5 | **CONFIRMED** | Unit written (`:584-602`) BEFORE `systemctl --user daemon-reload` (`:605`); no bus pre-check; SSH without `XDG_RUNTIME_DIR` → exact error, `set -e` aborts, linger hint never runs; orphaned unit remains |
|
||||
| H6 | **CONFIRMED** | llama.cpp default port **8080** (`common/common.h:620`; `--help` default), tool/adapter default **8088**; real orphaned unit on this machine omits `--port` |
|
||||
|
||||
**Root cause chain (one paragraph):** `detect_llama_version` (bin/pos-ai-server:71) always returns "unknown" because llama.cpp's `--version` writes to stderr and the tool discards stderr (`2>/dev/null`). Independently, `validate_default_flags`/`validate_requested_flags` (lines 101, 145) run `printf '%s' "$help_text" | grep -qE ...` under `set -o pipefail`; `grep -q` exits at the first match (flags are at byte offsets 320–39,741 of a 59,000-byte help), the bash-builtin `printf` then hits EPIPE, and pipefail promotes the SIGPIPE (rc=141) — so the `if` is false even though the flag IS in the help. Which flags "fail" each run is a scheduling race (empirically 0–4 flags rejected per run). Separately, `resolve_model` (lines 229-259) rejects the user's directory argument `Qwen-Qwen3-1.7B-GGUF` (HF downloader produces `$HF_DOWNLOAD_DIR/<repo-slug>/<file>.gguf`, not a flat file), and the unit write precedes an unguarded `systemctl --user daemon-reload` that fails under SSH with no `XDG_RUNTIME_DIR`, aborting via `set -e` before the linger hint and leaving an orphaned unit whose ExecStart may already have lost `--port` (real example on this machine: `ExecStart=... --host 127.0.0.1` only), guaranteeing a port mismatch against the adapter's 8088 health/API probes.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: H1 — Version detection "unknown"
|
||||
|
||||
### Hypothesis (as briefed)
|
||||
`detect_llama_version` (`bin/pos-ai-server:65-74`) greps `[0-9]+\.[0-9]+\.[0-9]+` (semver) from `--version`; llama.cpp uses build numbers `bNNNNN`, never X.Y.Z.
|
||||
|
||||
### Actual llama.cpp `--version` output (b10822)
|
||||
Downloaded and executed the real release binary (`llama-b10822-bin-ubuntu-x64.tar.gz`, `https://github.com/ggml-org/llama.cpp/releases/tag/b10822`):
|
||||
|
||||
```
|
||||
$ ./llama-server --version
|
||||
version: 0.4.0-dev (build 10822, commit c457e3bf7)
|
||||
built with GNU 11.4.0 for Linux x86_64
|
||||
```
|
||||
|
||||
**The format DOES contain X.Y.Z (`0.4.0`).** The briefed premise is factually wrong for current builds. The regex would match `0.4.0`.
|
||||
|
||||
### The REAL mechanism — output goes to stderr
|
||||
```
|
||||
$ ./llama-server --version 1>/dev/null # output STILL appears
|
||||
$ ./llama-server --version 2>/dev/null # NOTHING appears
|
||||
```
|
||||
`od -c` confirms the bytes are written to **stderr**. Source citation: `common/build-info.h:13`
|
||||
```cpp
|
||||
void llama_print_build_info(const char *, FILE * = stderr);
|
||||
```
|
||||
called from `common/arg.cpp:1456` (`llama_print_build_info(llama_version()); exit(0);`) — default stream stderr.
|
||||
|
||||
`detect_llama_version` (`bin/pos-ai-server:71`):
|
||||
```bash
|
||||
version="$("$bin" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)"
|
||||
```
|
||||
`2>/dev/null` discards the ONLY output → grep gets empty input → `version=` empty → `"unknown"`. Proven against real binary: with `2>/dev/null` captured nothing; with `2>&1` captured `0.4.0`.
|
||||
|
||||
Note: even with `2>&1`, the regex is brittle against older builds that print only `build 10822` (no semver) — a secondary hazard, not the current blocker.
|
||||
|
||||
```
|
||||
Hypothesis: version always "unknown" because regex can't match
|
||||
Why plausible: builds are tag "b10822"
|
||||
Evidence supporting: user sees "unknown" in all messages
|
||||
Evidence against: actual output has X.Y.Z ("0.4.0"); the regex DOES match
|
||||
Test needed: run detect_llama_version verbatim against real binary
|
||||
Result: empty capture (stderr discarded) → "unknown"
|
||||
Conclusion: CONFIRMED outcome, wrong mechanism — stderr discard, not regex format
|
||||
```
|
||||
|
||||
**[DONE]**
|
||||
|
||||
## Step 2: H2 — Wrong binary found
|
||||
|
||||
### (a) What the release archive ships
|
||||
`llama-b10822-bin-ubuntu-x64.tar.gz` (downloaded and listed) contains `llama-server` (among ~30 `llama*` tools + `.so` libs). **No bare `server` binary.**
|
||||
|
||||
### (b) Installer symlink behavior
|
||||
`apps/ai/llamacpp.sh:52-55`:
|
||||
```bash
|
||||
for bin in \$install_dir/llama*; do
|
||||
[ -f "\$bin" ] && [ -x "\$bin" ] || continue
|
||||
sudo ln -sf "\$bin" /usr/local/bin/\$(basename "\$bin")
|
||||
done
|
||||
```
|
||||
Since the archive ships `llama-server`, the glob `llama*` matches it → `/usr/local/bin/llama-server` IS created. Verified on the target machine (real install): `/usr/local/bin/llama-server -> /usr/local/lib/llama.cpp-b10822/llama-server`, and `llama-server --help` (PATH lookup) returns the full 732-line/59,000-byte help. RUNPATH `$ORIGIN` (`readelf -d`: `Library runpath: [$ORIGIN]`) makes the shared libs resolve through the symlink. **The install is not broken this way — H2 is not the user's cause.**
|
||||
|
||||
### (c) The bare `server` fallback is a genuine latent defect (fixture)
|
||||
`find_llamacpp` (`bin/pos-ai-server:56`) candidates: `llama-server`, `llama.cpp/server`, `server`, `llama-server-cuda`. Fixture: PATH containing ONLY an unrelated `/tmp/h2fixture/bin2/server` (no llama-server):
|
||||
|
||||
```
|
||||
find_llamacpp -> 'server' (command -v -> '/tmp/h2fixture/bin2/server')
|
||||
detect_llama_version -> (depends on the unrelated binary)
|
||||
validate_default_flags:
|
||||
--port: UNSUPPORTED (warn + omit)
|
||||
--host: UNSUPPORTED (warn + omit)
|
||||
--n-gpu-layers: UNSUPPORTED (warn + omit)
|
||||
--ctx-size: UNSUPPORTED (warn + omit)
|
||||
--threads: UNSUPPORTED (warn + omit)
|
||||
```
|
||||
i.e., a wrong-binary scenario produces the same family of user-visible messages — but **not** what this user hit (their install is correct and `llama-server` resolves first because candidate order checks `llama-server` before `server`).
|
||||
|
||||
```
|
||||
Conclusion: REFUTED as user's root cause; PARTIALLY-CONFIRMED as latent defect (bare `server` fallback)
|
||||
```
|
||||
|
||||
**[DONE]**
|
||||
|
||||
## Step 3: H3 — Help-format regex vs real llama-server --help — **actual root cause of the flag rejections**
|
||||
|
||||
### The regex matches real help
|
||||
Real `llama-server --help` (732 lines) contains (`grep -n` on real output):
|
||||
```
|
||||
7: -t, --threads N
|
||||
25: -c, --ctx-size N
|
||||
140: -ngl, --gpu-layers, --n-gpu-layers N
|
||||
503: --host HOST
|
||||
506: --port PORT
|
||||
```
|
||||
Word-boundary regex `(^|[[:space:]])${flag}([[:space:]]|=|$)` matches all five when tested WITHOUT pipefail (verified repeatedly). H3 as stated ("format mismatch misses real flags") is **REFUTED**.
|
||||
|
||||
### The real failure: `printf | grep -q` + `set -o pipefail` → SIGPIPE race
|
||||
`bin/pos-ai-server` line 2: `set -euo pipefail`. Lines 101/145:
|
||||
```bash
|
||||
if printf '%s' "$help_text" | grep -qE -- "(^|[[:space:]])${flag}([[:space:]]|=|$)"; then
|
||||
```
|
||||
`grep -q` exits as soon as it finds a match (closing the pipe's read end). The produced help_text is 59,000 bytes; the pipe buffer is 64 KB. Flags appear at byte offsets:
|
||||
- `--threads` 320, `--ctx-size` 1892, `--n-gpu-layers` 10992 (early)
|
||||
- `--host` 39499, `--port` 39741 (late)
|
||||
|
||||
For early flags, grep matches and exits after reading ≤ a few KB; the bash-builtin `printf` still has ~57 KB to write → EPIPE → under `pipefail` the pipeline returns **141 (128+13=SIGPIPE)** → the `if` is false even though grep found the flag. For late flags, printf usually completes writing into the 64 KB buffer before grep exits → rc=0. It's a **race**, so outcomes vary run to run.
|
||||
|
||||
**Isolated proof** (same capture, same regex, pipefail on):
|
||||
```
|
||||
++ printf '%s' "$help_text" | grep -qE -- '(^|[[:space:]])--ctx-size([[:space:]]|=|$)'
|
||||
pipeline rc=141 (grep -q DID match; printf died of SIGPIPE)
|
||||
```
|
||||
30-run trial: MATCH/NOMATCH alternated ~50/50 for `--ctx-size`.
|
||||
|
||||
**Live `pos-ai-server` flakiness (10 runs, real binary, identical inputs):**
|
||||
```
|
||||
run 1: --ctx-size --threads
|
||||
run 2: --ctx-size --threads
|
||||
run 3: --threads
|
||||
run 4: --threads
|
||||
run 5: --n-gpu-layers --ctx-size --threads
|
||||
run 6: --ctx-size
|
||||
run 7: --port --n-gpu-layers --threads
|
||||
run 8: --host --n-gpu-layers --ctx-size --threads
|
||||
run 9: --n-gpu-layers --ctx-size --threads
|
||||
run 10: (none!)
|
||||
```
|
||||
This explains the user's per-run differences (runs 1-2 vs run 3): **there is no CLI/config/env difference** — `REQUESTED_FLAGS` and `CONFIG_REQUESTED_FLAGS` are empty (no ai.env `LLAMACPP_*` keys, no CLI flags; verified the `requested_from_env_config` calls return early). Flag validation runs identically each time; the outcome is a scheduling race. Both `validate_default_flags` (warn+omit) and `validate_requested_flags` (hard `err`) carry the same bug — a requested `--ctx-size` would randomly hard-fail with "does not expose".
|
||||
|
||||
Verdict: **H3 REFUTED as stated; SIGPIPE+pipefail race is THE root cause of the flag rejections.**
|
||||
|
||||
**[DONE]**
|
||||
|
||||
## Step 4: H4 — Model resolution
|
||||
|
||||
`resolve_model` (`bin/pos-ai-server:229-259`) accepts: absolute FILE path (`[ -f ]`), `$HF_DOWNLOAD_DIR/<name>` **FILE** (`[ -f "$candidate" ]`), or relative FILE. It never treats a DIRECTORY under `$HF_DOWNLOAD_DIR` as a model.
|
||||
|
||||
Downloader layout (`bin/pos-ai-hf`): `hf_repo_dir()` (lines 308-311) → `$HF_DOWNLOAD_DIR/${repo_id//\//-}` — e.g. repo `Qwen/Qwen-Qwen3-1.7B-GGUF` → dir `$HF_DOWNLOAD_DIR/Qwen-Qwen3-1.7B-GGUF/` containing `Qwen3-1.7B-Q8_0.gguf` + `.hf-meta`.
|
||||
|
||||
Reproduction (fixture dir created exactly like the user's; `.gguf` stub inside):
|
||||
```
|
||||
$ pos-ai-server start Qwen-Qwen3-1.7B-GGUF
|
||||
[!] installed llama.cpp unknown does not support default flag --threads — omitting it from the unit
|
||||
ERROR: Model not found: Qwen-Qwen3-1.7B-GGUF (also searched /home/unknown/.local/share/linux_post_install/ai/models)
|
||||
EXIT: 1
|
||||
```
|
||||
Exact user message. The user's `mv Qwen3-1.7B-Q8_0.gguf ../` moved the file into `$HF_DOWNLOAD_DIR/`, but the arg `Qwen-Qwen3-1.7B-GGUF` is still a dir name → still fails (run 2 reproduced identically). The absolute file path (run 3) succeeds at model resolution (reproduced).
|
||||
|
||||
`pick_model` (lines 205-227) uses `find "$HF_DOWNLOAD_DIR" -name '*.gguf' -type f` — RECURSIVE, does NOT miss subdirs (verified). But it is only reached when no explicit arg/config is given; the user passed an explicit arg, so it wasn't involved.
|
||||
|
||||
**H4 CONFIRMED.**
|
||||
|
||||
**[DONE]**
|
||||
|
||||
## Step 5: H5 — dbus/systemctl under SSH
|
||||
|
||||
Code ordering (`bin/pos-ai-server`):
|
||||
- `set -euo pipefail` (line 2)
|
||||
- unit write + `chmod 644` (lines 584-602)
|
||||
- `systemctl --user daemon-reload` (line 605)
|
||||
- `systemctl --user enable --now "$SERVICE"` (line 606)
|
||||
- linger hint (lines 610-615)
|
||||
|
||||
No pre-check of the user bus anywhere in the file. Same pattern in `pos-communication-matrix-listener:341-342` and `pos-network-download:191-192`; NO tool pre-checks the bus (`pos-entertainment-status:55` uses `systemctl --user show-environment` only as a query guard, not an enable guard).
|
||||
|
||||
Reproduced exactly on target machine (SSH-like shell: `XDG_RUNTIME_DIR` and `DBUS_SESSION_BUS_ADDRESS` unset):
|
||||
```
|
||||
$ systemctl --user show-environment
|
||||
Failed to connect to user scope bus via local transport: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined (consider using --machine=<user>@.host --user to connect to bus of other user)
|
||||
rc=0 (exit status shown as 0 because of head pipe; the systemctl command itself fails)
|
||||
```
|
||||
With `set -e`, line 605's failure aborts the script; the linger hint (611-615) never runs.
|
||||
|
||||
Remediation verified: with ONLY `export XDG_RUNTIME_DIR=/run/user/$(id -u)` (dir exists), `systemctl --user show-environment` succeeds. `loginctl enable-linger` is the standard persistence fix.
|
||||
|
||||
Orphaned-unit side effect confirmed: the unit file remains written even though daemon-reload failed (a real orphaned unit exists at `~/.config/systemd/user/pos-ai-server.service` from the failed attempt on this machine — see Step 6 for its corrupted ExecStart).
|
||||
|
||||
**H5 CONFIRMED.**
|
||||
|
||||
**[DONE]**
|
||||
|
||||
## Step 6: H6 — Port mismatch (downstream impact)
|
||||
|
||||
llama.cpp default port: **8080**.
|
||||
- Binary evidence: real `--help` → `--port PORT port to listen (default: 8080)`
|
||||
- Source citation: `common/common.h:620` → `int32_t port = 8080; // server listens on this network port` (also `common/common.h:261` in struct block; `from https://raw.githubusercontent.com/ggml-org/llama.cpp/master/common/common.h`)
|
||||
|
||||
Tool/adapter default: **8088** — `bin/pos-ai-server:329` (`PORT="${LLAMACPP_PORT:-8088}"`), `:281`, `:316`; `lib/ai-providers/llamacpp.sh:15,23,49` (adapter probes `http://$host:$port/v1/models` and `/v1/chat/completions`); health check `bin/pos-ai-server:198` probes `$HOST:$PORT`.
|
||||
|
||||
Real corrupted unit found on this machine (from the failed start attempt — the SIGPIPE race omitted flags):
|
||||
```
|
||||
ExecStart="/usr/local/bin/llama-server" -m "…/Qwen3-1.7B-Q8_0.gguf" --host 127.0.0.1
|
||||
```
|
||||
Only `--host` survived — `--port 8088`, `--n-gpu-layers 0`, `--ctx-size 4096`, `--threads N` were all dropped. If `systemctl` had succeeded, llama-server would bind **8080** while `check_health` and the OpenAI adapter probe **8088** → "not running" / connection refused. Even when the race lets the unit through, H6 guarantees a downstream mismatch whenever `--port` is omitted.
|
||||
|
||||
**H6 CONFIRMED.**
|
||||
|
||||
**[DONE]**
|
||||
|
||||
## Root Cause Statement (final)
|
||||
|
||||
The user's exact messages trace to four independent defects in `bin/pos-ai-server` (plus one in the adapter default):
|
||||
|
||||
1. **"installed llama.cpp unknown"** — `detect_llama_version` (`:71`) discards stderr; real llama.cpp b10822 prints `version: 0.4.0-dev (build 10822, …)` to **stderr** (`common/build-info.h:13`), so the capture is always empty → "unknown".
|
||||
2. **"does not support default flag …"** — `printf '%s' "$help_text" | grep -qE` under `set -o pipefail` races: grep -q exits at first match, printf gets SIGPIPE, pipefail promotes rc=141, so valid flags are randomly judged "unsupported" and omitted from the unit (or hard-errored when user-requested). This is the reason runs 1-2 and run 3 flagged different sets of flags — pure scheduling, not input differences.
|
||||
3. **"Model not found: Qwen-Qwen3-1.7B-GGUF"** — the HF downloader puts weights at `$HF_DOWNLOAD_DIR/<repo-slug>/<file>.gguf`, but `resolve_model` accepts only files; a directory argument fails (`:241-248`), even after the user's `mv` (the arg was still a directory name).
|
||||
4. **"Failed to connect to user scope bus …"** — the unit write (`:584-602`) precedes an unguarded `systemctl --user daemon-reload` (`:605`); under SSH neither `XDG_RUNTIME_DIR` nor `DBUS_SESSION_BUS_ADDRESS` is set, so systemctl fails, `set -e` aborts, and the linger fix hint (`:611-615`) never shows. The orphaned unit left behind can carry a corrupted ExecStart (see H6), e.g. missing `--port 8088` so the server would bind llama.cpp's default 8080 while the adapter/health check probe 8088.
|
||||
|
||||
Classification: **FACT** (mechanisms directly reproduced with the real b10822 binary; source citations for stderr stream and port default; unit artifact inspected).
|
||||
|
||||
## Fix-Point Spec
|
||||
|
||||
| # | File:line | Defect | Minimal change | Design question for Architect |
|
||||
|---|-----------|--------|----------------|-------------------------------|
|
||||
| F1 | `bin/pos-ai-server:71` | version always "unknown" (stderr discarded; regex also brittle for pure-build strings) | Capture `2>&1`; broaden regex to also accept `build [0-9]+`/`b[0-9]+`: `version="$("$bin" --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+|build [0-9]+|b[0-9]+' | head -1 …)"` | Canonical display: semver vs build number vs both? |
|
||||
| F2 | `bin/pos-ai-server:101,145` | `printf|grep -q` + pipefail SIGPIPE race → random flag rejection | Replace the pipeline: `grep` without `-q` writing to `/dev/null`, e.g. `if grep -E -- "…" <<<"$help_text" >/dev/null; then` (non-q grep consumes all input; herestring avoids the pipe and printf EPIPE), **or** bash regex `[[ "$help_text" =~ (^|[[:space:]])${flag}([[:space:]]|=|$) ]]` | Style preference: grep-herestring vs bash `=~`; whether to harden `validate_requested_flags` identically (yes) |
|
||||
| F3 | `bin/pos-ai-server:229-259` (`resolve_model`) | directory under `HF_DOWNLOAD_DIR` (one `.gguf`) rejected | When `$candidate` is a directory: if exactly one `*.gguf` inside → use it; if multiple → list and err/ask | Should auto-expand single-gguf dirs, or require explicit file path? (Recommended: auto-expand, since downloader always produces `<repo>/<file>` layout) |
|
||||
| F4 | `bin/pos-ai-server:605-606` (+ same in matrix-listener:341, network-download:191) | no user-bus pre-check; SSH w/o XDG_RUNTIME_DIR → set -e abort; orphan unit; linger hint skipped | Pre-flight before daemon-reload: `systemctl --user show-environment` (or `printenv XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS`); on failure `err` with remediation: `export XDG_RUNTIME_DIR=/run/user/$(id -u)` (if dir exists) and `sudo loginctl enable-linger $(id -un)`; optionally run the server directly (no unit) as fallback | Fallback strategy: error+hint only, or run-direct fallback? Also: consider removing the orphaned unit on failure (Builder decision) |
|
||||
| F5 | `bin/pos-ai-server:56` (`find_llamacpp`) | bare `server` / `llama.cpp/server` fallbacks can pick unrelated binaries | Drop `"server"` (keep `llama-server`, `llama-server-cuda`; keep or drop `llama.cpp/server`) | Should the fallback list be `llama-server`/`llama-server-cuda` only? |
|
||||
| F6 | `bin/pos-ai-server:528-532` + `lib/ai-providers/llamacpp.sh:15,23,49` | `--port` omission → server on 8080 vs adapter/health on 8088 | Guarantee `--port $PORT` always emitted (F2 fixes the omission); as defense-in-depth, adapter health fallback probe both 8088 and 8080, or derive from the unit | Should the tool ever allow running on llama.cpp's default 8080, or always pin 8088? |
|
||||
| F7 | `apps/ai/llamacpp.sh:46-57` | installer never verifies the installed binary runs (version/help readable) | Post-install sanity: `llama-server --version >/dev/null 2>&1 && llama-server --help >/dev/null`; warn on failure | None (simple hardening) |
|
||||
|
||||
## Design Questions (explicit)
|
||||
|
||||
1. **F2/F3 boundary:** Should unknown-version disable flag *validation* entirely (trust defaults), or retain help-based validation but fix the SIGPIPE race? (Recommended: keep validation, fix race — help-based validation is the correct design once deterministic.)
|
||||
2. **F3:** Should `resolve_model` auto-expand a single-`.gguf` directory; with multiple gguFs, err with the list?
|
||||
3. **F4:** On unresolvable user bus: error+hint only, or a run-direct fallback (no systemd unit) for headless/SSH use?
|
||||
4. **F5:** Drop the bare `server` (and possibly `llama.cpp/server`) fallback candidates?
|
||||
5. **F6:** Pin the port in the unit always (recommended) vs teach the adapter to probe the llama.cpp default 8080 as a fallback?
|
||||
|
||||
## Test-Fixture Spec (for Tester)
|
||||
|
||||
Each fix gets a stub/PATH/case that reproduces it deterministically:
|
||||
|
||||
- **F1 (version):** fake `llama-server` printing `version: 0.4.0-dev (build 10822, commit c457e3bf7)` **to stderr**, help to stdout. Assert `detect_llama_version` returns `0.4.0` (not "unknown") with the fixed `2>&1`; also fixture printing only `version: b10822`/`build 10822` to stderr to assert the broadened regex.
|
||||
- **F2 (race):** fake `llama-server` whose `--help` emits a 59 KB body with tokens `--threads` at line 7, `--ctx-size` at line 25, `--n-gpu-layers` at line 140, `--host`/`--port` near the end (exact real llama.cpp layout). Run `validate_default_flags` 20× under `set -euo pipefail`; fixed code must report all 5 supported on EVERY run (deterministic). Regression tail: pre-fix, the run must fail at least once (demonstrates the race existed).
|
||||
- **F3 (model dir):** fixture `$HF_DOWNLOAD_DIR/Qwen-Qwen3-1.7B-GGUF/Qwen3-1.7B-Q8_0.gguf` (+ `.hf-meta`). Assert `pos ai server start Qwen-Qwen3-1.7B-GGUF` (DRY_RUN=1) resolves to the file; multi-gguf dir case errors with the file list.
|
||||
- **F4 (bus):** environment with `XDG_RUNTIME_DIR`/`DBUS_SESSION_BUS_ADDRESS` unset (or a stub `systemctl` that fails with the dbus message); assert the pre-flight fails with the remediation hint AND the linger hint text when applicable; assert no orphaned unit is left (or is removed on failure).
|
||||
- **F5 (server fallback):** PATH containing ONLY a fake unrelated `server` (prints its own --help/--version) and no llama-server; assert `find_llamacpp` does NOT return `server`.
|
||||
- **F6 (port):** with F2 fixed, generate a unit with `DRY_RUN=1` and assert ExecStart always contains `--port 8088` (and `--host`, `--n-gpu-layers`, `--ctx-size`, `--threads`).
|
||||
- **F7 (installer):** run `apps/ai/llamacpp.sh` against a fixture tar containing a broken binary (e.g., missing shared lib) → assert post-install sanity warns.
|
||||
|
||||
## Evidence Index (concrete artifacts)
|
||||
|
||||
- Real binary downloaded/run: `llama-b10822-bin-ubuntu-x64.tar.gz` from `https://github.com/ggml-org/llama.cpp/releases/tag/b10822` (asset `llama-b10822-bin-ubuntu-x64.tar.gz`).
|
||||
- `llama-server --version` → stderr: `common/build-info.h:13` (`FILE * = stderr`), `common/arg.cpp:1454-1458`, plus byte-level `od -c`/redirect proof.
|
||||
- `--help` flag lines: real output lines 7/25/140/503/506; byte offsets 320/1892/10992/39499/39741; total 59001 bytes (< 64 KB pipe buffer).
|
||||
- Pipeline rc=141 proof: `/tmp/test_sigpipe3.sh` output (rc 0/141 alternating).
|
||||
- 10-run `pos-ai-server` flakiness table (see Step 3).
|
||||
- Real corrupted orphaned unit: `~/.config/systemd/user/pos-ai-server.service` — `ExecStart=… --host 127.0.0.1` (port/ctx/threads/gpu-layers omitted).
|
||||
- Model-not-found exact reproduction and post-`mv` reproduction.
|
||||
- User-bus error reproduced verbatim on target machine; `export XDG_RUNTIME_DIR=/run/user/$(id -u)` remediation verified working.
|
||||
- Default port: `common/common.h:620` (`int32_t port = 8080`); real `--help` `(default: 8080)`.
|
||||
- H2 fixture: fake unrelated `server` → `find_llamacpp` returns `server`, all defaults "unsupported".
|
||||
|
||||
## Next Agent
|
||||
|
||||
**Builder** — five (F1-F5) of the six fix-points are code changes in `bin/pos-ai-server` (plus `apps/ai/llamacpp.sh` for F7); Architect input needed on the design questions before/while implementing F2/F3/F4/F6.
|
||||
|
||||
**Changes made by Detective:** none (read-only; only downloaded/extracted to /tmp and created user-level fixtures under `/home/unknown/.local/share/linux_post_install/ai/models` matching the user's layout for reproduction).
|
||||
@@ -1,347 +0,0 @@
|
||||
# Detective Report — pos ai alias create: silent abort on empty System Prompt
|
||||
|
||||
- Date: 2026-09-06
|
||||
- Scope: interactive UI failure in `pos ai alias` create flow (host `ciya` paste)
|
||||
- HEAD: `0b5043a`
|
||||
- Role: read-only investigation (no project files modified)
|
||||
|
||||
## TL;DR
|
||||
|
||||
The reported abort is **confirmed and reproducible**. In `_alias_create()` the
|
||||
"System prompt (empty = use built-in)" step calls `menu_ask_value` with **no
|
||||
default**, and `menu_ask_value` returns rc 1 for an *empty* answer when there is
|
||||
no default (`lib/menu-lib.sh:357`). The caller's `|| return 0` (`bin/pos-ai-alias:410`)
|
||||
then silently aborts to the menu — exactly the reported symptom. The Session step
|
||||
worked because it passes a default (`$name`), so empty→default→rc 0→proceeded.
|
||||
|
||||
- **Root cause (FACT):** `bin/pos-ai-alias:410` invokes `menu_ask_value "System prompt (empty = use built-in)" ""` — an empty 2nd arg means *no default* — while the UI text explicitly advertises empty as valid ("use built-in"). Empty→`rc 1`→`|| return 0`→silent abort.
|
||||
- **Blast radius:** 7 distinct `menu_ask_value` call sites in `pos-ai-alias`; 6 outside. Per-site classification is in the table below. Only `pos-ai-alias:353` and `:410` show class-a (empty advertised as valid → defect). The edit flow is **NOT** affected (its prompt step explicitly handles empty; see edit verdict).
|
||||
- **Step-count defect (FACT):** create flow has 5 steps but lines `bin/pos-ai-alias:352,383` hardcode `/4` (should be `/5`), while 396/409/424 are `/5`. Introduced by commit `300b742a` (feat: alias trust flag) which bumped the total only on the lines it touched.
|
||||
- **Provenance (FACT):** `pos-ai-alias` is byte-identical to HEAD; clean `git status`. Both defects are pre-existing, NOT regressions from the 2026-09-06 stabilization commits (`d817c37`, `0b5043a` which only touched `pos-network-download`/`pos-system-backup`, not these call sites).
|
||||
- **Fix constraint:** the `menu_ask_value` "empty + no default → cancel" contract must be preserved (6+ callers rely on it). The fix must be **local/opt-in** in `pos-ai-alias` (e.g. treat empty as valid where the UI says so, or pass a sentinel default / use a `menu_ask_value` opt-in variant), never a global semantic change.
|
||||
|
||||
Report path (this file):
|
||||
`AgentsReport/detective/2026-09-06_alias-menu-abort.md`
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Trace the abort path (Task A) `[DONE]`
|
||||
|
||||
### menu_ask_value contract (`lib/menu-lib.sh:344-362`)
|
||||
```
|
||||
350 menu_ask_value() {
|
||||
351 local label="$1" def="${2:-}" val pr="$1"
|
||||
352 [ -n "$def" ] && pr="$pr [$def]"
|
||||
353 if ! val="$(menu_read_value "$pr")"; then
|
||||
354 return 1 # EOF / cancel
|
||||
355 fi
|
||||
356 if [ -z "$val" ]; then
|
||||
357 [ -n "$def" ] || return 1 # <-- empty + no default => rc 1
|
||||
358 echo "$def"
|
||||
359 return 0
|
||||
360 fi
|
||||
361 echo "$val"
|
||||
362 }
|
||||
```
|
||||
The documented contract at line 349 says: `rc 0 value on stdout · rc 1 EOF/cancel,
|
||||
or empty answer with no default`. This is exactly the `f61766b0` lineage (the
|
||||
empty-no-default clause predates the 4306a53 paste-safe reader rewrite;
|
||||
`4306a53` only swapped the `read -rp`/`menu_read_value` internals, leaving the
|
||||
`menu_ask_value` empty clause untouched — see Step 5).
|
||||
|
||||
### menu_read_value (the reader, 4306a53+)
|
||||
`menu_read_value` returns `rc 0 + ""` on a plain empty Enter (submit=1, empty
|
||||
`val`, printed nothing), and `rc 1` only on genuine EOF / Ctrl-C/Z/\ / Ctrl-D-on-empty
|
||||
(lines 272-280, 302-304). So **cancel vs empty is distinguishable at the reader level**
|
||||
— it is `menu_ask_value` that deliberately collapses *empty-with-no-default* into
|
||||
*rc 1*, folding it into the same code path as cancel.
|
||||
|
||||
### _alias_create callers (`bin/pos-ai-alias`)
|
||||
- Line 410 (System Prompt): `prompt="$(menu_ask_value "System prompt (empty = use built-in)" "")" || return 0`
|
||||
- 2nd arg is `""` → `def` empty → **no default**.
|
||||
- UI text promises empty is valid ("use built-in").
|
||||
- empty → `rc 1` → `|| return 0` → **silent abort** to menu. ✔ matches paste.
|
||||
- Line 397 (Session): `session="$(menu_ask_value "Session name" "$name")"` — passes default `$name` (non-empty for any valid name). empty→default→`rc 0`→proceeded. ✔ matches paste (session step proceeded).
|
||||
- Line 353 (Alias name): `name="$(menu_ask_value "Alias name" "")" || return 0` — no default; empty→`rc 1`→abort. The loop's empty-name warn/re-prompt at line 355 is unreachable for the empty case.
|
||||
|
||||
### Empirical confirmation
|
||||
The reader needs a TTY (stty raw mode). I drove a non-TTY fallback path (which
|
||||
shares the exact `menu_ask_value` empty/no-default logic — `stty -g` fails → plain
|
||||
`read` → `menu_ask_value` still hits line 357) to confirm the contract:
|
||||
|
||||
```
|
||||
$ echo "" | menu_ask_value "System prompt (empty = use built-in)" ""
|
||||
empty-no-default -> rc=1 out=<>
|
||||
$ echo "" | menu_ask_value "Session name" "assist"
|
||||
empty-with-default -> rc=0 out=<assist>
|
||||
$ printf '' | menu_ask_value "Alias name" ""
|
||||
EOF-no-default -> rc=1 out=<>
|
||||
```
|
||||
|
||||
This deterministically reproduces the exact divergence: empty-with-default succeeds
|
||||
(rc 0, default returned), empty-no-default aborts (rc 1). A full pty repro of the
|
||||
raw-mode reader is possible (pty available) but adds no discriminating evidence
|
||||
beyond this.
|
||||
|
||||
A note on the harness: a naive `prompt="$(menu_ask_value ...)"` under `set -e` in
|
||||
the harness exits immediately on the rc-1 cmdsubst — itself a live demonstration
|
||||
that the failing assignment is the abort point.
|
||||
|
||||
### Task A conclusion
|
||||
Findings 1-2 **confirmed**. The report's abort path maps exactly to
|
||||
`menu_read_value` returning empty (rc 0) on plain Enter, then `menu_ask_value`
|
||||
returning 1 for empty-with-no-default, then `|| return 0` in `_alias_create`.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Blast radius — every caller of menu_ask_value (Task B) `[DONE]`
|
||||
|
||||
### pos-ai-alias (7 sites)
|
||||
| line | code | class | evidence |
|
||||
|------|------|-------|----------|
|
||||
| 353 | `menu_ask_value "Alias name" ""` | **a (defect)** | UI "Alias name"; loop expects to re-prompt on empty (355 warn) but rc 1 short-circuits. Loop's empty-warn is dead code. |
|
||||
| 397 | `menu_ask_value "Session name" "$name"` | ok | default `$name`; empty→default→rc 0. Correct. |
|
||||
| 410 | `menu_ask_value "System prompt (empty = use built-in)" ""` | **a (defect)** | UI explicitly says empty valid; empty→rc 1→abort. **The reported bug.** |
|
||||
| 433 | `menu_ask_value "Trust this alias? (y/N)" "N"` | ok | default `N`; empty→N→rc 0. Correct. |
|
||||
| 538 | `menu_ask_value "Session name" "$new_session"` (edit) | c (edge) | default = current session. If a stored session were empty (shouldn't happen post-create, create defaults to `$name`), empty→rc 1→`return 0`→abort. Requires an empty-stored-session precondition. See edit verdict. |
|
||||
| 557 | `menu_ask_value "System prompt" "$default_display"` (edit) | **handled** | edit explicitly guards: when `default_prompt` empty, line 560 sets `tmp_prompt=""` instead of aborting. No bug. |
|
||||
| 585 | `menu_ask_value "Trust this alias? (y/N)" "$cur_trust_label"` (edit) | ok | default = current label, non-empty. Correct. |
|
||||
|
||||
### Outside pos-ai-alias (6 call sites, 4 files)
|
||||
| file:line | code | class | evidence |
|
||||
|-----------|------|-------|----------|
|
||||
| pos-system-backup:231 | `menu_ask_value "Folder to back up"` (no default) | **b (cancel, correct)** | empty→rc 1→`return 0`; a real folder must be typed, or user backs out. `menu_backup_folder` returns 0 on empty. Correct today. |
|
||||
| pos-media-sync:67 | `menu_ask_value "Source folder [current: $SRC]"` (no default; `$SRC` is in the **label**, not 2nd arg) | **b (cancel, correct)** | empty→rc 1→`return 0`; empty not a valid folder (checked at 68-70). Correct. |
|
||||
| pos-network-download:949 | `menu_ask_value "$1" "N"` (menu_ask_yn, default N) | ok | empty→N→rc 0; rc 1 only genuine cancel. Correct. |
|
||||
| pos-network-download:1001 | `menu_ask_value "URL to add (download dir: ...)" ` (no default) | **b (cancel, correct)** | empty→rc 1→`return 0`; line 1002 redundant `[ -n "$url" ] || return 0`. Empty means "back out". Correct. |
|
||||
| pos-network-download:1027 | `menu_ask_value "Type purge to clear finished/error history"` (no default) | **b (cancel, correct)** | empty→rc 1→`return 0`; must literally type `purge`. Empty=cancel is intentional (must-type confirmation). Correct. |
|
||||
| pos-docker-vbox:495 | `menu_ask_value "Image ref"` (no default) | **b (cancel, correct)** | comment 106-107: "empty answer and a dead stream are indistinguishable (menu_ask_value has no default there)". Typed-value add loop; empty = cancelled typing → back to picker. Intentional. |
|
||||
| pos-docker-vbox:831 | `menu_ask_value "VM name"` (no default) | **b (cancel, correct)** | comment 107/828-830: empty answer intentionally tears down loudly with nothing created ("both rc 1 ... both take the loud teardown"). Intentional. |
|
||||
|
||||
### Blast-radius conclusion
|
||||
Only `pos-ai-alias:353` and `:410` are class-a defects. The other 6 external call
|
||||
sites and the remaining pos-ai-alias sites are correct by design (empty=cancel or
|
||||
empty=default). **A global change to `menu_ask_value`'s empty-no-default semantics
|
||||
would break at least these correct sites**: pos-system-backup:231,
|
||||
pos-media-sync:67, pos-network-download:1001/1027, and both pos-docker-vbox sites
|
||||
(explicitly documented). The fix must be local to `pos-ai-alias`.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Step-count defect + edit flow (Task C) `[DONE]`
|
||||
|
||||
### Create step numbering (`bin/pos-ai-alias`)
|
||||
Confirmm via `grep -n 'step [0-9]'`:
|
||||
```
|
||||
352: step 1 4 (create Alias Name)
|
||||
383: step 2 4 (create Provider)
|
||||
396: step 3 5 (create Session)
|
||||
409: step 4 5 (create Prompt)
|
||||
424: step 5 5 (create Trust)
|
||||
```
|
||||
Create has **5 steps**, but lines 352/383 print `/4` while 396/409/424 print `/5`.
|
||||
Confirms the cosmetic `[1/4] [2/4]` mismatch in the paste.
|
||||
|
||||
Introducing commit: `git blame` shows 352 = `9f289ba3`, 383 = `9f289ba3`,
|
||||
396/424 = `300b742a`. `300b742a` ("feat: alias trust flag") added the Trust step
|
||||
and set 396/409/424 to `/5` but **did not** update 352/383, leaving them at `/4`.
|
||||
|
||||
### Edit flow
|
||||
`grep`:
|
||||
```
|
||||
520: step 1 4 (edit Provider)
|
||||
537: step 2 4 (edit Session)
|
||||
556: step 3 4 (edit Prompt)
|
||||
581: step 4 4 (edit Trust)
|
||||
```
|
||||
Edit numbering is internally **consistent** (4 steps, all /4). No mis-numbering.
|
||||
|
||||
**Edit empty-input traps:** verified the edit Prompt step (556-573) explicitly
|
||||
guards the empty-original-prompt case:
|
||||
```
|
||||
557 if ! tmp_prompt="$(menu_ask_value "System prompt" "$default_display")"; then
|
||||
559 # EOF/cancel: empty-answer abort only when there IS a default;
|
||||
560 # Enter on an empty original prompt keeps it empty and continues.
|
||||
560 [ -z "$default_prompt" ] && tmp_prompt="" || return 0
|
||||
```
|
||||
So when the stored prompt is empty (`default_display` empty), empty Enter →
|
||||
`tmp_prompt=""` → continues (no abort). When the stored prompt is non-empty,
|
||||
empty Enter → returns the default (rc 0). **Edit prompt step has NO bug** — it is
|
||||
the correct pattern the create flow's Step 4 should have used.
|
||||
|
||||
Minor/edge: edit Session step (538) aborts on empty Enter **only if the stored
|
||||
session is empty**, which the create flow prevents (line 404 defaults session to
|
||||
`$name`). Classified c (ambiguous/edge), low practical impact, no action required.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Provenance — byte-identical to HEAD, pre-existing (Task D) `[DONE]`
|
||||
|
||||
- `git status --short` → **clean** (empty). Working tree matches HEAD `0b5043a`.
|
||||
- `git diff HEAD -- bin/pos-ai-alias lib/menu-lib.sh` → **empty**. No uncommitted edits.
|
||||
- `git log --format=%H ... -- bin/pos-ai-alias` → 5 commits, all Aug 26-27 2026:
|
||||
`9f289ba` (feat: pos ai alias), `e969234`, `59935dc`, `300b742` (trust flag),
|
||||
`4306a5` (paste-safe reader). None are the 2026-09-06 stabilization commits.
|
||||
- The 2026-09-06 commits `d817c37` and `0b5043a` touched `bin/pos-network-download`
|
||||
and `bin/pos-system-backup` **but not** any `menu_ask_value` call site in those
|
||||
files (their diffs contain no `menu_ask_value` additions/deletions/-context edits
|
||||
to those lines — verified by grepping the diffs). They never touched `pos-ai-alias`
|
||||
or `lib/menu-lib.sh`.
|
||||
- **Introducing commit for both defects:**
|
||||
- `menu_ask_value` empty-no-default → rc 1 contract: **`f61766b0`** (Aug 24) in
|
||||
`lib/menu-lib.sh` (lines 356-357 unchanged since then; verified by blame).
|
||||
- The `pos-ai-alias` mis-use (empty advertised as valid + `|| return 0`) for
|
||||
step 4 and the alias-name step: **`9f289ba3`** (Aug 26) originally; the abort
|
||||
remains in tree to HEAD.
|
||||
- The create step-count (`/4` vs `/5`) mismatch: **`300b742a`** (Aug 27, trust flag).
|
||||
|
||||
**Verdict: pre-existing; NOT a regression from the 2026-09-06 stabilization/fix
|
||||
commits.**
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Fix constraints (Task E) `[DONE]`
|
||||
|
||||
A correct fix must preserve:
|
||||
1. **Cancel vs empty must remain distinguishable.** The reader already
|
||||
distinguishes them (Ctrl-D/EOF/Ctrl-C → rc 1; plain Enter → rc 0 + empty). The
|
||||
semantic collapse happens *only* in `menu_ask_value` (empty-no-default → rc 1).
|
||||
The fix must not blur this line.
|
||||
2. **The 6 external callers relying on empty=cancel must keep that behavior:**
|
||||
pos-system-backup:231, pos-media-sync:67, pos-network-download:1001/1027 and
|
||||
both pos-docker-vbox sites (documented comments 106-107 and 828-830).
|
||||
3. **No global semantic change to `menu_ask_value`** default contract ("empty with
|
||||
no default = rc 1"). Any change there ripples to the correct callers above.
|
||||
4. **The fix must be local/opt-in to `pos-ai-alias`:** at lines 353 and 410, the
|
||||
UI explicitly advertises empty as valid, so empty must proceed as an empty
|
||||
value instead of aborting. Candidate shapes for Builder (not decided here —
|
||||
implementation is out of the Detective's read-only scope): pass a sentinel
|
||||
default, add an opt-in `menu_ask_value` flag to keep empty, or restructure the
|
||||
`|| return 0` into `|| if cancel then return; else continue with empty`.
|
||||
5. **Step-count fix (create):** lines 352/383 `/4` → `/5` to match the 5-step flow
|
||||
(edit is already consistent at 4).
|
||||
6. Whatever the fix, the edit-flow Pattern (empty-with-default guard, lines
|
||||
557-560) shows the codebase's intended way to handle optional-input prompts.
|
||||
|
||||
---
|
||||
|
||||
## Confirmed Root Cause Statements
|
||||
|
||||
**ROOT CAUSE (FACT):** In `_alias_create()`, the "System prompt (empty = use
|
||||
built-in)" step (`bin/pos-ai-alias:410`) calls `menu_ask_value` with an empty
|
||||
second argument (no default), and the shared helper `lib/menu-lib.sh:356-357`
|
||||
returns **rc 1** for an empty answer when there is no default — conflating
|
||||
"user typed nothing" with "user cancelled". The caller's `|| return 0`
|
||||
(`bin/pos-ai-alias:410`) then silently aborts the create flow back to the menu with
|
||||
no alias created. The same latent trap exists at `bin/pos-ai-alias:353` (alias
|
||||
name), where the loop's empty-name warn/re-prompt (`:355`) is dead/unreachable; the
|
||||
UI at both 353 and 410 explicitly advertises empty as acceptable ("use built-in").
|
||||
|
||||
**Related defect (FACT):** create-flow step numbering is inconsistent — line 352
|
||||
and 383 print `/4` while lines 396/409/424 print `/5`, so the wizard shows
|
||||
`[1/4] [2/4] [3/5] [4/5] [5/5]` for its 5 steps.
|
||||
|
||||
**Provenance (FACT):** pre-existing (not a 2026-09-06 stabilization regression);
|
||||
`pos-ai-alias` is byte-identical to HEAD on a clean tree. Introducing commits:
|
||||
`9f289ba3` (abort mis-use), `300b742a` (step-count mismatch), with the `menu_ask_value`
|
||||
empty-no-default contract from `f61766b0`.
|
||||
|
||||
## Blast Radius Table
|
||||
|
||||
| caller | line | class | evidence |
|
||||
|--------|------|-------|----------|
|
||||
| pos-ai-alias create Alias name | 353 | **a (defect)** | empty advertised in flow; loop warn (355) unreachable |
|
||||
| pos-ai-alias create Session | 397 | ok | default `$name`; empty→default→rc 0 |
|
||||
| pos-ai-alias create Prompt | 410 | **a (defect — reported)** | UI "(empty = use built-in)" is valid; aborts |
|
||||
| pos-ai-alias create Trust | 433 | ok | default N |
|
||||
| pos-ai-alias edit Session | 538 | c (edge) | aborts only if stored session empty (prevented by create) |
|
||||
| pos-ai-alias edit Prompt | 557 | handled | explicit empty guard at 560 |
|
||||
| pos-ai-alias edit Trust | 585 | ok | default = current label |
|
||||
| pos-system-backup Folder | 231 | **b (cancel, keep)** | empty must not be a folder; empty=cancel correct |
|
||||
| pos-media-sync Source | 67 | **b (cancel, keep)** | label carries `$SRC`, not a default; empty=cancel correct |
|
||||
| pos-network-download menu_ask_yn | 949 | ok | default N |
|
||||
| pos-network-download add URL | 1001 | **b (cancel, keep)** | empty→rc 1→return 0; redundant 1002 guard |
|
||||
| pos-network-download purge | 1027 | **b (cancel, keep)** | must type `purge`; empty=cancel intentional |
|
||||
| pos-docker-vbox image ref | 495 | **b (cancel, keep)** | comments 106-107; empty=cancel intentional |
|
||||
| pos-docker-vbox VM name | 831 | **b (cancel, keep)** | comments 107, 828-830; empty→loud teardown intentional |
|
||||
|
||||
## Step-Count Verdict
|
||||
|
||||
Create flow is 5 steps but prints `/4` on steps 1-2 (353/383). Edit flow is
|
||||
internally consistent at 4. Introduced by `300b742a`.
|
||||
|
||||
## Edit-Flow Verdict
|
||||
|
||||
No bug: edit Prompt (557-560) explicitly handles the empty-original-prompt case
|
||||
and empty-with-default correctly; edit numbering is consistent. Only theoretical
|
||||
edge is a stored-empty session, prevented by create (404) and class-c.
|
||||
|
||||
## Provenance Verdict
|
||||
|
||||
Pre-existing. Clean tree, byte-identical to HEAD. Not a 2026-09-06 regression.
|
||||
Introducing commits: `f61766b0` (contract), `9f289ba3` (abort mis-use),
|
||||
`300b742a` (step-count + trust step).
|
||||
|
||||
## Fix Constraints
|
||||
|
||||
1. Preserve cancel-vs-empty distinction (reader already separates them; do not
|
||||
blur).
|
||||
2. Preserve empty=cancel for the 6 external call sites (pos-system-backup:231,
|
||||
pos-media-sync:67, pos-network-download:1001/1027, pos-docker-vbox:495/831 —
|
||||
two explicitly documented).
|
||||
3. NO global semantic change to `menu_ask_value`'s empty-no-default→rc 1 contract.
|
||||
4. Fix local/opt-in to `pos-ai-alias` 353/410: empty must be accepted as an empty
|
||||
value where the UI says it's valid.
|
||||
5. Fix create step numbering: 352/383 → `/5`.
|
||||
6. Follow the edit-flow pattern (empty-with-default guard) as the codebase's
|
||||
intended shape for optional-input prompts.
|
||||
|
||||
## Handoff
|
||||
|
||||
```
|
||||
Status: ROOT_CAUSE_ESTABLISHED
|
||||
|
||||
Symptom: pos ai alias create aborts back to menu after the empty "System prompt"
|
||||
step — no [5/5] trust step, no confirmation, no alias created; with a
|
||||
cosmetic [1/4] [2/4] vs [3/5]-[5/5] step count.
|
||||
|
||||
Expected: empty system prompt (advertised "use built-in") proceeds to the trust
|
||||
step and creates the alias with an empty prompt.
|
||||
|
||||
Actual: empty + no default → menu_ask_value rc 1 → `|| return 0` → silent abort.
|
||||
|
||||
Root cause: lib/menu-lib.sh:356-357 returns rc 1 for empty-with-no-default,
|
||||
collapsing "empty" into "cancel"; bin/pos-ai-alias:410 (and 353)
|
||||
advertise empty as valid but pass no default, and the `|| return 0`
|
||||
converts rc 1 into a silent abort.
|
||||
|
||||
Classification: FACT
|
||||
|
||||
Evidence: code trace (menu-lib.sh:344-362; pos-ai-alias:344-467) + deterministic
|
||||
non-TTY repro (empty-no-default→rc 1; empty-with-default→rc 0 default);
|
||||
clean HEAD tree; git blame provenance.
|
||||
|
||||
Tests performed: menu_ask_value empty/EOF/with-default matrix; created/edit flow
|
||||
step numbering; provenance (git status/diff/blame, stabilization
|
||||
commit diffs).
|
||||
|
||||
Alternatives eliminated: not a reader (menu_read_value) bug — reader distinguishes
|
||||
empty(rc 0) from cancel(rc 1); not a global menu_ask_value bug needing
|
||||
semantic change — 6 external callers rely on empty=cancel; not a
|
||||
2026-09-06 regression — those commits didn't touch these lines.
|
||||
|
||||
Affected components: bin/pos-ai-alias (create 353/410; step numbers 352/383);
|
||||
lib/menu-lib.sh (contract, no change needed).
|
||||
|
||||
Scope / decision boundary: implementation of the fix (how to make 353/410
|
||||
opts accept empty) is Builder territory; whether to add an opt-in
|
||||
menu_ask_value variant or a sentinel is an implementation decision.
|
||||
|
||||
Recommended next agent: Builder
|
||||
|
||||
Reason: root cause and blast radius are established; a local, opt-in fix in
|
||||
pos-ai-alias is understood and within approved scope, preserving the
|
||||
empty=cancel contract for the 6 external callers.
|
||||
|
||||
Changes made by Detective: none (read-only).
|
||||
```
|
||||
@@ -1,98 +0,0 @@
|
||||
# AI Subsystem Audit — Evidence-Based Verification
|
||||
|
||||
Date: 2026-09-06
|
||||
Explorer: read-only, evidence-first.
|
||||
Scope: `bin/pos-ai`, `bin/pos-ai-hf`, `bin/pos-ai-server`, forwarders (`gemini`/`openrouter`/`llamacpp`), `lib/ai-providers/*.sh`, `ai.env` config + `# POS_CONFIG:`/`pos config ai`, `# POS_FLAGS:`/`# POS_SUBCMDS:` metadata, `DOC/POS.md` ai section, GEN doc rows, `completions/pos.bash`, `bin/pos` usage().
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **The AI subsystem is largely real and coherent.** Providers (gemini/openrouter/llamacpp) adapters implement real API calls with key masking; `pos-ai-server` was genuinely repaired (commit `528b166`) — the old fake `validate_server_features` stub and unquoted multi-line ExecStart are gone, `systemd_quote` quoting is correct, `detect_llama_version` is guarded.
|
||||
- **One HIGH defect:** `pos-ai-hf` **single-file** download failure still exits **0** and writes `.hf-meta` advertising a complete model (only the parallel/multi-file path was fixed for partial-failure rc=1). `bin/pos-ai-hf:716-720, 727-744, 773-775`.
|
||||
- **One MEDIUM coherence defect:** `LLAMACPP_HOST` is honored by the server (ExecStart `--host` + display) but **ignored** by the llamacpp provider adapter and by the server's own probes, all of which hardcode `127.0.0.1` (`lib/ai-providers/llamacpp.sh:14,20,31,46-47`; `bin/pos-ai-server:131,576`).
|
||||
- **pos-ai-server flag validation is NOT complete:** only CLI-explicit flags are validated; config-sourced flags and always-emitted defaults (`--threads`, `--n-gpu-layers`, `--ctx-size`, `--port`, `--host`) are never validated. "Version-aware" is cosmetic (version only interpolated into the error string, never used to branch logic).
|
||||
- **Streaming is NOT implemented** in any provider (llamacpp explicitly sets `stream:false`); **no claim** of streaming exists in docs. Not a defect, just a fact to record.
|
||||
- **Command-extraction** (`_extract_commands` / `_prompt_run_command`) provenance: `trusted=1` comes ONLY from the `--trust` CLI flag or alias wrappers that inject `--trust` (alias env field 5=1). Confirmation default on a tty is **ALLOW** (Enter runs); non-tty is fail-safe (never runs). Extra alarm is warranted only if the operator marks an alias trusted.
|
||||
|
||||
---
|
||||
|
||||
## Evidence table
|
||||
|
||||
| ID | file:line | current behavior | classification | notes |
|
||||
|----|-----------|------------------|----------------|-------|
|
||||
| A1 | bin/pos-ai:649-677 | Parses exactly `--provider --model --session --system --full --last --trust` + `-h` | MATCH | `# POS_FLAGS:` (line 5) matches parse exactly |
|
||||
| A2 | bin/pos-ai:693-706 | Subcommands `ask capture chat models providers sessions` + `llamacpp` shorthand | MATCH for the six; **llamacpp shorthand IMPLEMENTED-BUT-UNLISTED** | `# POS_SUBCMDS:` (line 4) omits `llamacpp`; completions/pos.bash:51 does include it |
|
||||
| A3 | bin/pos-ai:130-160 | `load_config` reads ai.env (env-already-exported wins) + legacy gemini/openrouter files | MATCH | duplicated loader, no shared one (see M3) |
|
||||
| A4 | bin/pos-ai:162-202 | Config precedence: `--model` > AI_MODEL > provider-specific (`AI_GEMINI_MODEL`/`OPENROUTER_MODEL`/`LLAMACPP_MODEL`) > `provider_default_model` | MATCH | llamacpp `resolve_model` passes **basename** of LLAMACPP_MODEL (line 198) |
|
||||
| A5 | bin/pos-ai:363-406, 531-535, 571-572 | `_extract_commands` parses ```bash/sh/shell``` fenced blocks; `_prompt_run_command` prompts/executes | MATCH | provenance of trusted traced to `--trust` flag + alias field 5 (pos-ai-alias:64,433-436,580). tty default = run; non-tty = never runs |
|
||||
| A6 | lib/ai-providers/gemini.sh:24-36 | Real `generateContent` call; `-m 60` timeout; parses error.message, non-200 → rc 1 | MATCH | no streaming tokens (`?alt=sse` absent) |
|
||||
| A7 | lib/ai-providers/openrouter.sh:24-37 | Real `/api/v1/chat/completions`; `-m 60`; error.message parse; rc 1 on non-200 | MATCH | |
|
||||
| A8 | lib/ai-providers/llamacpp.sh:19-42 | Real `/v1/chat/completions`; `-m 120`; **hardcodes `127.0.0.1`**, only reads `LLAMACPP_PORT`; `stream:false` | MATCH for localhost mode; **ignores LLAMACPP_HOST** → coherence defect D2 | error path only prints "API error $code", no error.message |
|
||||
| A9 | bin/pos-ai-server:388-392, 445, 501-508 | ExecStart single-line, binary+model `systemd_quote()`d | MATCH | repair confirmed real |
|
||||
| A10 | bin/pos-ai-server:52-59 | `detect_llama_version` guarded; returns "unknown" safely; takes binary arg | MATCH | repair confirmed; no errexit |
|
||||
| A11 | bin/pos-ai-server:66-87, 407-409 | `validate_requested_flags` only validates `REQUESTED_FLAGS` (CLI-explicit only) | PARTIAL (see D3) | config + defaults never validated; grep -qF substring (see D4) |
|
||||
| A12 | bin/pos-ai-server:262-266, 373-380 | Defaults: PORT 8088, HOST 127.0.0.1, CTX 4096, GPU -1, THREADS nproc; CLI > env > default | MATCH | bind localhost by default ✓ |
|
||||
| A13 | bin/pos-ai-server:128-135, 576 | health probe `/health` + `/v1/models` **hardcoded `127.0.0.1`** | PARTIAL (see D2) | ignores LLAMACPP_HOST for probes |
|
||||
| A14 | bin/pos-ai-hf:185-251 | `hf_api` auth header, 401/403/404/429/other → `err`; JSON validity check; 429 retry-once | MATCH | base URL https://huggingface.co/api |
|
||||
| A15 | bin/pos-ai-hf:254-312 | `hf_paginate` Link rel="next"; `hf_repo_files` tree endpoint with guards + fallback; `hf_search` jq urlencode | MATCH | pagination real; error-object guard via `select(type=="...")` |
|
||||
| A16 | bin/pos-ai-hf:636-700 | Parallel download drain, per-pid wait, honest failure counting, `rc=1` on partial failure, no `.hf-meta` on partial | MATCH for multi-file | **single-file path defect = D1** |
|
||||
| A17 | bin/pos-ai-hf:778-812, 915-992 | `list`/`cache` only list dirs with `.hf-meta`; `cache clear` fail-closed confirm | MATCH | |
|
||||
| A18 | bin/pos-ai-hf:449-469 | `--branch`/`--revision` alias, last-wins; default branch from API else "main" | MATCH | |
|
||||
| A19 | completions/pos.bash:6,7,26,51 | completion flags for ai/ai-server/ai-hf match `# POS_FLAGS:`; subcmds match declared | MATCH for declared | **no `_pos_subcmds[ai-hf]`** (see M1) |
|
||||
| A20 | DOC/POS.md:56-125 | ai/hf/server sections describe behavior congruent with code | MATCH | no streaming claim |
|
||||
| A21 | pos config ai / cfg_display masks `secret` keys | AI_GEMINI_API_KEY, OPENROUTER_API_KEY, HF_TOKEN are `secret`-flagged → masked | MATCH | lib/config-ui.sh:347-361 |
|
||||
|
||||
---
|
||||
|
||||
## Findings (ranked defects)
|
||||
|
||||
### AI-subystem defects
|
||||
|
||||
- **D1 (HIGH)** — `pos-ai-hf` single-file download failure returns **0** and writes `.hf-meta` for a partial model.
|
||||
Evidence: `bin/pos-ai-hf:716-720` — sequential path `warn "Failed to download $fname"; continue` without recording into `failed_files`; `:727-744` — `failed_files` empty ⇒ `.hf-meta` written; `:773-775` — the `return 1` clause only fires for the parallel path. Only if `file_count>1` (parallel branch, `:641`) is a failure non-zero + meta-suppressed. Contradicts commit `528b166` message "rc=1 on partial failure, no .hf-meta for half-downloaded models". A truncated `-C -` partial `.gguf` can then be handed to `pos ai server start`.
|
||||
|
||||
- **D2 (MEDIUM)** — `LLAMACPP_HOST` advertised but only honored by the server, not by the client/probes.
|
||||
Evidence: server emits `--host $HOST` (`bin/pos-ai-server:445`) and displays `endpoint: http://$HOST:$PORT` (`:604`), but health probe `:131` and `/v1/models` probe `:576` hardcode `127.0.0.1`; the llamacpp **adapter** (`lib/ai-providers/llamacpp.sh:14,20,31,46-47`) hardcodes `127.0.0.1` and reads only `LLAMACPP_PORT`. A non-localhost `LLAMACPP_HOST` → server binds elsewhere while `pos ai llamacpp ask/chat/models` and probes talk to loopback. Coherence break across the two tools + the advertised config key.
|
||||
|
||||
- **D3 (MEDIUM)** — `validate_requested_flags` validates **only CLI-explicit** flags.
|
||||
Evidence: `REQUESTED_FLAGS` is populated only in the arg-parsing `case` clauses (`bin/pos-ai-server:295-359`); the gate `if [ "${#REQUESTED_FLAGS[@]}" -gt 0 ]` (`:407-409`) + always-serialized defaults `--n-gpu-layers $gpu_layers --ctx-size $CTX_SIZE --threads $THREADS` (`:446-448`) and config-sourced `--port/--host` (`:445`) are never checked. Also: the `version` argument is used **only** in the error string (`:84`); there is no version-conditional logic — the "version-aware" phrasing is cosmetic. Practically mitigated because the always-on core flags (`--threads`, `--n-gpu-layers`, `--ctx-size`, `--port`, `--host`) are universal across llama.cpp builds, but the guarantee is broader than the implementation.
|
||||
|
||||
- **D4 (LOW)** — `validate_requested_flags` uses loose substring matching `grep -qF -- "$flag"` (`bin/pos-ai-server:83`). A flag token could match inside synthesized `--help` text unrelated to actual support (e.g. a builder that documents a placeholder), giving false-passes; no word/`--flag=` boundary. Minor robustness issue.
|
||||
|
||||
- **D5 (LOW / UNKNOWN-live)** — llamacpp model id: `resolve_model` passes `basename "$LLAMACPP_MODEL"` (`bin/pos-ai:198`) as the `model` field. llama.cpp `/v1/models` `.id` is not guaranteed to equal the GGUF basename (may be the model's internal name or full path). If they differ the server may reply "model not found". UNKNOWN — requires live llama-server to confirm.
|
||||
|
||||
- **D6 (LOW, security-relevant)** — command-execution confirmation default is **ALLOW** on a tty: `_prompt_run_command` prompt `[Y/n]` runs on any value except `n|N` (`bin/pos-ai:391-405`); trusted=1 (`--trust` flag `:667-668`, or alias trusted field via `pos-ai-alias:64,433-436`) runs with **no** confirmation (`:385-389`) via `run eval "$cmd"` (`:388,403`). Non-tty is fail-safe (`:382` — returns without running). Not an auto-exec bug under default settings, but the confirmation default-allow + `eval` of AI-extracted shell is worth a security review's attention.
|
||||
*Provenance of trusted=1 (question 11):* (1) `--trust` CLI flag, default 0 (`:644`); (2) alias wrapper inserts `--trust` when the alias's 5th env field (`name|provider|session|prompt|trusted`) = 1 (`pos-ai-alias:64`), set interactively ("Trust this alias? (y/N)", default N) at create/edit (`:433-436, 580-591`). **No config key** drives trust. Auto-execution paths: only trusted-mode `_prompt_run_command` (`:388`) — nothing else eval's model output.
|
||||
|
||||
### Metadata / doc drift
|
||||
|
||||
- **M1 (MEDIUM, metadata gap)** — `pos-ai-hf` implements subcommands `search/download/list/remove/info/files/cache` but declares **no `# POS_SUBCMDS:` header** (`bin/pos-ai-hf:3-4`). Consequently `_pos_subcmds[ai-hf]` is absent from `completions/pos.bash` and the gen doc tables/tree do not surface them. IMPLEMENTED-BUT-UNLISTED.
|
||||
|
||||
- **M2 (LOW)** — `pos-ai` `llamacpp` shorthand subcommand (parsed, `bin/pos-ai:701-704`) absent from `# POS_SUBCMDS:` (line 4). Completions already include it (`completions/pos.bash:51`). IMPLEMENTED-BUT-UNLISTED (only in the shorthand).
|
||||
|
||||
- **M3 (LOW, maintainability)** — no shared `ai.env` loader. pos-ai (`load_config`, `:130`), pos-ai-server (`load_config`, `:21`), pos-ai-hf (`load_hf_config`, `:30`) each duplicate the env-precedence loop; `lib/common.sh` only provides `load_system_env` (system.env) and `CONFIG_DIR`. All three behave identically, but this is a future-drift seam and contradicts the "shared loader" intent implied by AGENTS.md.
|
||||
|
||||
- **M4 (LOW)** — forwarder `# POS_SUBCMDS:` under-list: `pos-ai-openrouter:4` = `ask chat sessions capture` (omits `models`, which works via passthrough); all three forwarders omit `providers` (`pos-ai-gemini:4`, `pos-ai-openrouter:4`, `pos-ai-llamacpp:4`), which also works. Completions inherit the under-list.
|
||||
|
||||
- **M5 (INFO, not a defect)** — `QUANT_DIR` is a CLI-flag-only variable (`bin/pos-ai-hf:119,134-135`), not an env/config key; it is not in `# POS_CONFIG:` and is correctly absent from `pos config ai`. The audit task listed it among config keys to trace; it is flag-only by design. Configure `HF_TOKEN`/`HF_DOWNLOAD_DIR` instead.
|
||||
|
||||
### Could NOT verify live (read-only / no runtime)
|
||||
|
||||
- llama-server binary behavior: `--version`/`--help` output, whether `--threads`/`--n-gpu-layers`/`--gpu-threads`/`--kv-cache` etc. are all accepted by the installed build; the `/v1/models` id format (D5).
|
||||
- Actual Hugging Face API responses (tree endpoint shape, pagination Link header, 429 Retry-After); `hf_paginate`/`hf_repo_files` correctness with live data.
|
||||
- systemd unit validity: the reviewer report for `528b166` states `systemd-analyze verify` rc=0 was checked; I could not re-run it. Static reading of `systemd_quote` + single-line ExecStart is correct.
|
||||
- Whether a real Gemini/OpenRouter key/reply path works end-to-end (key required).
|
||||
|
||||
---
|
||||
|
||||
## Handoff
|
||||
|
||||
Status: COMPLETE (objective answered to the limit of read-only evidence).
|
||||
Evidence: all file:line cited above; primary source + git commit `528b166` diff.
|
||||
|
||||
- **Recommended next agent: Reviewer (adversarial review)** on the single-file download path (D1) and the `LLAMACPP_HOST` coherence break (D2) — both are concrete, source-provable behavior defects ripe for a focused fix. **Builder** if a fix is decided (D1: route single-file through the failure-counting harness or check `hf_download_with_progress` rc and suppress meta + return 1; D2: thread `LLAMACPP_HOST` into the adapter and probes). **Maintainer** for the metadata gaps (M1, M2, M4) and the duplicated ai.env loader (M3).
|
||||
- Affected areas: `bin/pos-ai-hf`, `bin/pos-ai-server`, `lib/ai-providers/llamacpp.sh`, `bin/pos-ai*` POS_* headers, `completions/pos.bash`, gen doc output.
|
||||
- Decision boundary: adding `# POS_SUBCMDS:` to `pos-ai-hf` is a metadata change requiring `make gen` + `make check` + `make lint` (per AGENTS.md) — that is a Builder/Maintainer action, out of Explorer scope.
|
||||
|
||||
Remaining uncertainty: D5 (llamacpp model-id match) and all binary/API live behavior (listed above) — needs a live llama.cpp/HF/Gemini environment.
|
||||
@@ -1,239 +0,0 @@
|
||||
# Security Audit — Command-Execution Surfaces, Chat Authorization, Secret Handling
|
||||
|
||||
**Date:** 2026-09-06
|
||||
**Explorer:** read-only investigation
|
||||
**Scope:** `bin/`, `lib/`, `scripts/`, `install.sh`, `preinstall.sh`, `postinstall.sh`, `apps/`, `entertainment/`
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Telegram listener skips SENDER authorization entirely.** It filters *only on chat id* (`TELEGRAM_CHAT_ID`), and even that filter is weakened by an OR-clause that also accepts `from_id`. No `from.username`/owner-user-id allowlist exists anywhere; there is no config key for one. Anyone who can get a message into the owner chat (shared chat, forwarded/mention, or a group where the bot sees the message with `from_id != chat_id`) can execute arbitrary mapped bash **and** the Gemini AI bridge as your user. This is the single highest-severity finding.
|
||||
- **Matrix listener DOES implement exactly "sender authorized AND chat authorized"**, but both are *optional-to-configure*: `MATRIX_USER_ID` defaults to a live `/whoami` resolution and `MATRIX_ROOM_ID` to "every room joined". If both are unset at runtime you get "any sender, any room" — remote code execution. Severity depends on which homeserver/rooms the account is in.
|
||||
- **AI command auto-execution (`pos ai` eval path)** is a real RCE primitive. Trusted aliases pass `--trust` (no confirmation). The listener's `<prefix>` bridge passes *unvalidated chat text* into a shell string that is executed by `bash -c` — a direct RCE.
|
||||
- **`/dev/tcp` probes interpolate unvalidated host/port into `bash -c` strings** in 4 places. Most reachable inputs are CLI args (interactive/low risk), but `pos-network-download` (`NET_PROBE`, network-derived) and the `share_port_probe` surface deserve review.
|
||||
- **Backup GPG passphrase** is passed on the gpg **command line** (`--passphrase "$PASS"`) → visible in `ps`/process args, and exported into any `notify`/log context via `set -x` if debugging. No temp files persist the secret; `mktemp` artifacts are cleaned on both success and failure, and the archive is `chmod 600`. The SMB client and Matrix login write credentials to throwaway/templated files correctly (`chmod 600`) and avoid argv exposure, but the SMB persistent credentials dir text is world-readable risk only via file perms (mode 600).
|
||||
|
||||
---
|
||||
|
||||
## 1. Command-Execution Inventory
|
||||
|
||||
All primitives below were located by grep across the specified paths; input provenance chain and classification are traced from source.
|
||||
|
||||
### `/dev/tcp` probes — host/port interpolated into `bash -c` string
|
||||
|
||||
| ID | file:line | primitive | input source | attacker-controlled? | classification |
|
||||
|----|-----------|-----------|--------------|----------------------|----------------|
|
||||
| C01 | `bin/pos-network-checkport:133` | `bash -c "exec 3<>/dev/tcp/$ip/$port"` | `ip` from CLI arg (`$host` from targets, validated no-spaces at :486), `port` validated `[0-9]{1,5}` 1-65535 | port no; host partially (no-space check only, IPv6 brackets stripped) | **REVIEW** — host could contain `;`/`$(...)` if crafted (only `*" "` is rejected; `;`, backticks, `$()` not blocked). Interactive CLI, but the string is unquoted. |
|
||||
| C02 | `bin/pos-network-checkport:157` | `bash -c "exec 3<>/dev/udp/$ip/$port; printf 'x' >&3"` | same provenance | same | **REVIEW** |
|
||||
| C03 | `bin/pos-network-checkport:166` | `bash -c ".../dev/tcp/$ip/$port; printf 'HEAD...'"` | same | same | **REVIEW** |
|
||||
| C04 | `bin/pos-network-checkport:168` | `bash -c ".../dev/tcp/$ip/$port; head -c 200"` | same | same | **REVIEW** |
|
||||
| C05 | `bin/pos-share-smb-client:92` | `timeout 3 bash -c "exec 3<>/dev/tcp/${host}/${SMB_PORT}"` | `host` from `//server/share` CLI arg; `split_share` extracts `SERVER` with no validation | **partially** — no filtering of `;`/`$()`/backticks in SERVER | **VULNERABLE-INPUT** (low exploitability: requires the operator to type such a host; but a malicious remote `//<payload>/share` string reaches shell). |
|
||||
| C06 | `lib/share-lib.sh:59` | `timeout 3 bash -c "exec 3<>/dev/tcp/${1}/${2}"` | `$1`=host, `$2`=port from callers (nfs/smb client mounts) | host unvalidated | **VULNERABLE-INPUT** (shared helper; called from CLI-only paths). |
|
||||
| C07 | `bin/pos-network-download:28,377` | `NET_PROBE=...bash -c '</dev/tcp/8.8.8.8/53>'`; `net_up() { bash -c "$NET_PROBE" }` | **controlled by `NET_PROBE` env var** | **yes** if `NET_PROBE` is attacker-set (e.g. via schedule/daemon env) | **REVIEW** — env-derived command string evaluated; DEFAULT is constant/safe. |
|
||||
|
||||
### Listener command execution (chat-driven)
|
||||
|
||||
| ID | file:line | primitive | input source | attacker-controlled? | classification |
|
||||
|----|-----------|-----------|--------------|----------------------|----------------|
|
||||
| C08 | `bin/pos-communication-telegram-listener:349` | `timeout "$tmo" bash -c "$cmdline"` (in `run_and_reply`) | `$cmdline` = map value (constant, owner-edited) **or** prefix bridge `$cmd $qtext` where `$qtext` = `printf '%q'` of chat text | `$qtext` is the chat text, but `%q`-quoted (safe metachar-wise) | **REVIEW** — map value is constant; prefix path quotes the arg. Map value itself is operator-owned. |
|
||||
| C09 | `bin/pos-communication-telegram-listener:697` | `run_and_reply "$cmd $qtext" ...` | prefix bridge: `$cmd` = map value (operator), `$qtext` = `%q` quoted chat text | chat text via `%q` (safe) | **REVIEW/SAFE** — quoting present but the whole string is one `bash -c`; the chat text is not the only component. |
|
||||
| C10 | `bin/pos-communication-matrix-listener:493` | `timeout 60 bash -c "$value"` | `$value` = map value (operator-owned, /cmd) | no (constant operator string) | **SAFE** (constant owner map). |
|
||||
| C11 | `bin/pos-communication-matrix-listener:214` | `timeout 60 bash -c "$value"` (ui test) | operator input in TUI | no | **SAFE/DESIGNED-INTERACTIVE**. |
|
||||
| C12 | `bin/pos-communication-telegram-listener:368` | `timeout 60 bash -c "$value"` (ui test) | operator input in TUI | no | **SAFE/DESIGNED-INTERACTIVE**. |
|
||||
|
||||
### AI command eval (RCE primitive)
|
||||
|
||||
| ID | file:line | primitive | input source | attacker-controlled? | classification |
|
||||
|----|-----------|-----------|--------------|----------------------|----------------|
|
||||
| C13 | `bin/pos-ai:388,403` | `run eval "$cmd"` (in `_prompt_run_command`) | `$cmd` = `_extract_commands` of the **AI provider's raw response** (gemini/openrouter/llamacpp output) | **yes — AI-generated** | **VULNERABLE-INPUT** (eval of untrusted model output). Trusted mode (`--trust`, `TRUST_MODE=1`) at :385-388 auto-executes with no prompt; non-trusted prompts on tty. Called from `cmd_ask:535` and `cmd_chat:572`. |
|
||||
| C14 | `bin/pos-communication-telegram-listener:734` | `timeout 120 pos ai gemini ask ... "$prompt"` | AI bridge: `$prompt` = chat text after `<prefix> ` | chat text — but only as an **argument** to `pos ai gemini ask` (a subprocess arg, quoted via `"$prompt"`); the AI eval inside pos-ai then acts on the model's *reply*. | **REVIEW** — the bridge feeds arbitrary chat text to the model; if the model echoes back a command, C13 fires. Indirect injection. |
|
||||
| C15 | `bin/pos-communication-matrix-listener:472` | `pos ai gemini ask ... "$prompt"` | same AI bridge (Matrix `ai ` prefix) | same | **REVIEW** (indirect). |
|
||||
|
||||
### `sudo` / `systemctl` / docker / ssh / curl|sh
|
||||
|
||||
- **`apps/**/*.sh`** — `sudo` used for `apt install`/`systemctl`/`usermod`/`curl | sh` installers. These run **once interactively at install time**; inputs are (mostly) constant URLs. Classified **SAFE/DESIGNED-INTERACTIVE**. Notable pipe-to-shell:
|
||||
- `apps/networking/zerotier.sh:9` — `curl -s https://install.zerotier.com | sudo bash` (network→shell, no verification). **Review**.
|
||||
- `apps/networking/netbird.sh:9`, `tailscale.sh:9` — `curl -fsSL ... | sh` (official vendor identical). **Review** (no checksum; standard vendor practice).
|
||||
- `apps/system/docker.sh:9` — `curl -fsSL https://get.docker.com | sh`. **Review**.
|
||||
- `apps/development/opencode.sh:9` — `curl -fsSL https://opencode.ai/install | bash`. **Review**.
|
||||
- `apps/utilities/tsui.sh:9` — `curl -fsSL https://neuralink.com/tsui/install.sh | bash`. **Review** (vendor unknown-ish host).
|
||||
- **`preinstall.sh:45,48,58,65` / `postinstall.sh:101-160` / `install.sh:134-239`** — `sudo apt`, `sudo systemctl`, `sudo install`, `git clone`. Constant/static targets. **SAFE** (interactive bootstrap).
|
||||
- **`bin/pos-network-scan:88,99`** — `sudo -n nmap` / `sudo nmap`; constant. **SAFE**.
|
||||
- **`bin/pos-network-hotspot:60-86`** — `sudo create_ap` with `"$@"` passthrough. CLI args reach sudo. **REVIEW** (interactive tool; operator supplies args).
|
||||
- **`bin/pos-system-backup:173`** — `sudo tar -czvf "$ARCHIVE" -C "$(dirname "$FOLDER")" "$NAME"`; `$FOLDER` CLI arg baked into `$ARCHIVE` name. **REVIEW** (folder name into tar; low risk, arbitrary path backup).
|
||||
- **`bin/pos-share-nfs-client/smb-client`, `lib/usb-lib.sh`, `lib/share-lib.sh`** — `sudo mount`/`umount`/`mkdir` with mount paths; mountpoint paths validated to be absolute and non-system (smb-client `ask_new_mountpoint`/`menu_ask_mountpoint:579,614` deny `/etc,/boot,/bin,...`). **SAFE/REVIEW** — some paths from `findmnt`/menu.
|
||||
- **`bin/pos-docker-vbox:1074,1094,1096`** — `docker exec -it ... bash` (interactive attach). **SAFE/DESIGNED-INTERACTIVE**.
|
||||
- **`scripts/lint-conventions.sh`, `make gen` pipeline** — dev-time `bash -n`/`awk`/`sed`. Out of runtime scope (dev tooling, constant). **SAFE**.
|
||||
- **No `sshpass`, `scp`, or `ssh` remote-command execution** anywhere (only `pos ssh load-keys` adding keys to the agent, and doc references). No remote-shell-over-ssh primitive found. **N/A**.
|
||||
- **`lib/scheduler-lib.sh:180`** — `bash -c "$JOB_COMMAND"`; `JOB_COMMAND` is the literal remainder of a `schedule.d/*.env` line, operator-authored, syntax-checked (POS tool). **SAFE/DESIGNED-INTERACTIVE** (operator-owned config; would be RCE if an attacker could write `schedule.d/`).
|
||||
|
||||
### Secret-in-process-argv (gpg/openssl)
|
||||
|
||||
- **`bin/pos-system-backup:195`** — `gpg --passphrase "$PASS" --symmetric ...`: the backup passphrase is placed on the gpg **command line**, visible to any local user/process via `/proc/<pid>/cmdline` and to `ps as`. **See Section 3.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Authorization Model — Telegram + Matrix
|
||||
|
||||
### Telegram listener (`bin/pos-communication-telegram-listener`)
|
||||
|
||||
**Config keys read (via `load_config` line 86-99, plus direct grep in `ai_bridge_prefix`:625):**
|
||||
| Key | Defined in | Purpose in listener |
|
||||
|-----|-----------|---------------------|
|
||||
| `TELEGRAM_BOT_TOKEN` | `# POS_CONFIG:` in `telegram-sender:6`; `telegram.env` | authenticate Bot API |
|
||||
| `TELEGRAM_CHAT_ID` | same | **the only owner filter** (see below) |
|
||||
| `TELEGRAM_AI_PREFIX` | same | AI bridge trigger word (default `ai`) |
|
||||
|
||||
There is **no** `TELEGRAM_OWNER`, `TELEGRAM_USER_ID`, `TELEGRAM_ALLOWED`, `OWNER_ID`, or any sender-identity allowlist key anywhere in the codebase, config templates, or docs.
|
||||
|
||||
**Message flow & the update loop (lines 766-792):**
|
||||
1. `getUpdates` with `allowed_updates=["message"]` (line 771) — filters update *type* to messages only.
|
||||
2. Per update: extracts `chat` (`.message.chat.id`), `from_id` (`.message.from.id`), `text` (line 780-783).
|
||||
3. **The ONLY authorization gate is line 787:**
|
||||
```
|
||||
if [ -n "$chat" ] && [ "$chat" != "$TELEGRAM_CHAT_ID" ] && [ "$from_id" != "$TELEGRAM_CHAT_ID" ]; then continue; fi
|
||||
```
|
||||
This is: **continue (drop) ONLY IF** chat is set AND chat ≠ owner-chat **AND** from_id ≠ owner-chat.
|
||||
4. `handle_message` (line 674) then dispatches with **no further sender check**: `/command` map (via `map_get`, line 743) → `run_and_reply` → `bash -c` (C08); web-URL detection → `pos media grab` (line 704); prefix bridge (line 697) → `bash -c`; AI bridge (line 734) → `pos ai gemini ask`.
|
||||
|
||||
**Verdict — Telegram does NOT implement "sender authorized AND chat authorized":**
|
||||
- The check is **chat-or-sender OR**, not AND, and the "sender" comparison uses **`from_id` == `TELEGRAM_CHAT_ID`** — i.e. it assumes the owner's Telegram *user id* numerically equals the *chat id*. That is only true for a direct (1:1) private chat with the owner. In a **group or supergroup**, the `chat.id` is negative and differs from any `from.id`; the OR-clause then accepts **any** `from_id` that happens to equal `TELEGRAM_CHAT_ID` (unlikely) OR requires chat==owner. In a **shared/followup private chat** scenarios or when `TELEGRAM_CHAT_ID` is a forwarded context, the model breaks.
|
||||
- **Concretely exploitable:** if the bot's token is added to a group, `chat.id` (group, negative) ≠ `TELEGRAM_CHAT_ID` AND `from_id` (a member) ≠ `TELEGRAM_CHAT_ID` → condition **false** → message is dropped. So plain group members are blocked **only if** `TELEGRAM_CHAT_ID` is truly the owner's 1:1 chat and the bot isn't also filtering otherwise. BUT there is **no sender allowlist**, so the guard is the sole mechanism and it mis-handles the general case. If the owner ever sets `TELEGRAM_CHAT_ID` to a group id (a plausible misconfiguration the tool doesn't prevent), **anyone in that group executes commands**. Also the model treats `from_id == TELEGRAM_CHAT_ID` as allowed even when `chat` is different — so a message where sender id coincidentally equals the configured numeric id (or a bot re-post) is accepted regardless of chat.
|
||||
- **No `from.username`, no `from.first_name/last_name` allowlist, no user-id allowlist** — the requirement "is the SENDER's from.id checked ANYWHERE before a mapped command or AI bridge executes?" is answered: **yes, but only against the chat-id value via an OR with chat-id, and only for one numeric field.** This is not a proper sender authorization.
|
||||
- **Missing/invalid config behavior:** `run_daemon:759-760` `err`s (exits) if token or chat-id are unset. Good fail-closed for *chat* but there is no sender config to miss.
|
||||
|
||||
**Map-file editing surface:** exclusively the **interactive TUI** (`ui()` at line 440, no-arg invocation; `ui_add/ui_edit/ui_remove/ui_test`, lines 378-438) and the `prefix` verb for the prefix map (`prefix_cmd:567`). No network/API path writes `telegram_commands.env`. The map file is `chmod 600` (`map_set:137,144`). **Risk:** it is a **local** file `~/.config/linux_post_install/telegram_commands.env` editable only by the owner at the shell; if the owner account is compromised the map is trivially editable (but that's full-host compromise anyway). The `/cmd::description=` text (user-typed description) flows into `sync_bot_commands` → `setMyCommands` (line 215), so a map *description* is sent to Telegram — a low-level info leak of the operator's own design, not an attacker surface.
|
||||
|
||||
### Matrix listener (`bin/pos-communication-matrix-listener`)
|
||||
|
||||
**Config keys read (via `load_config`:65-78):**
|
||||
| Key | Defined in | Purpose |
|
||||
|-----|-----------|---------|
|
||||
| `MATRIX_HOMESERVER` | `# POS_CONFIG:` `matrix-sender:5`; `matrix.env` | server URL for /sync |
|
||||
| `MATRIX_ACCESS_TOKEN` | same | auth |
|
||||
| `MATRIX_USER_ID` | same | **owner/sender filter** |
|
||||
| `MATRIX_ROOM_ID` | same | **room filter** (optional) |
|
||||
|
||||
**Message flow & /sync loop (lines 509-557):**
|
||||
1. Resolves owner: `MATRIX_USER_ID` if set, else live `/account/whoami` (lines 514-523).
|
||||
2. `room_only="${MATRIX_ROOM_ID:-}"` (line 515) — **empty = watch every join.**
|
||||
3. Per room: skip if `room_only` set and `room != room_only` (line 539).
|
||||
4. Per event: require `type==m.room.message`, `content.msgtype==m.text` (546-547); **sender filter line 550:** `[ "$sender" = "$owner" ] || continue` — message REFUSED unless the sender is the owner.
|
||||
5. `handle_message` (line 447) dispatches with **no further sender check**: `/`/`!` command map (line 483) → `bash -c` (C10); `ai ` bridge (line 472) → `pos ai gemini ask`.
|
||||
|
||||
**Verdict — Matrix:**
|
||||
- `MATRIX_ROOM_ID` set: **sender authorized (== owner) AND chat authorized (== configured room)** — matches the requirement. ✅
|
||||
- `MATRIX_ROOM_ID` unset but `MATRIX_USER_ID` set: sender authorized, chat = **every joined room** — chat is NOT pinned. An owner tweet from any room triggers RCE. ⚠️
|
||||
- Both unset: owner auto-resolved via whoami (still "sender == owner"), room = every room. Still sender-gated, but the room is unbounded. If `whoami` *fails* (line 520-521), it `err`s out (fail-closed). So Matrix is **sender-gated always** (owner == account's own user id), chat optionally restricted. This is a materially **stronger and correct** model than Telegram's.
|
||||
|
||||
**Guard lines that would need to change to satisfy "user AND chat authorized" fully:**
|
||||
- **Telegram:** replace the OR at `bin/pos-communication-telegram-listener:787` with an AND requiring `chat == TELEGRAM_CHAT_ID` **and** a sender check against a new allowlist key (e.g. `TELEGRAM_OWNER_ID`). Minimal sketch:
|
||||
```bash
|
||||
# 1) Chat must be the owner chat
|
||||
[ "$chat" = "$TELEGRAM_CHAT_ID" ] || continue
|
||||
# 2) Sender must be an allowed user id (new key, fail-closed if unset)
|
||||
[ -n "${TELEGRAM_OWNER_ID:-}" ] || { warn "no TELEGRAM_OWNER_ID — refusing"; continue; }
|
||||
case " $TELEGRAM_OWNER_ID " in *" $from_id "*) ;; *) continue ;; esac
|
||||
```
|
||||
plus add the `TELEGRAM_OWNER_ID` key to the `# POS_CONFIG:` registry in `telegram-sender:6` (`pos config telegram`). **Do NOT implement — this is exploration output only.**
|
||||
- **Matrix:** to fully pin chat even when `MATRIX_ROOM_ID` is unset, `run_daemon` should fail-closed (refuse to start) or require a room list; currently line 515 defaults to "watch all." Add validation that `MATRIX_ROOM_ID` is set before `--run` (or an explicit allowlist). **Do NOT implement.**
|
||||
|
||||
### Other listener/plugin paths that execute commands
|
||||
- **Entertainment plugins** (`entertainment/{weather,joke,gold}.sh`) fetch public APIs and print text — **no command execution**; output is sent via `pos-entertainment-send` → `notify_send`. Not an RCE surface via chat; they run on a schedule or explicit `pos entertainment send`.
|
||||
- **`pos-docker-vbox enter`** (`docker exec -it bash`) is operator-attach only.
|
||||
- **No other chat→command executor** found besides the two listeners and the AI bridge.
|
||||
|
||||
---
|
||||
|
||||
## 3. Backup / GPG Secret Handling
|
||||
|
||||
File: `bin/pos-system-backup`. **PASS provenance chain:**
|
||||
1. **Prompted interactively** (lines 183-186): `read -s -rp "Enter backup password:" PASS`, then `read -s -rp "Confirm..." CONFIRM`. Read from the terminal with echo suppressed — **not** from config file, env, or argv. Good.
|
||||
2. Validation (187-191): non-empty and `PASS == CONFIRM`, else loops.
|
||||
3. `unset CONFIRM` (192) immediately after.
|
||||
4. **Every place PASS is used:**
|
||||
- line 195: `gpg --batch --yes --passphrase "$PASS" --symmetric --cipher-algo AES256 "$ARCHIVE"` — **PASS on the command line** → visible in `/proc/<pid>/cmdline` and `ps` output of `gpg`. **This is the exposure.**
|
||||
- line 202: `gpg --batch --quiet --passphrase "$PASS" --decrypt "$ARCHIVE" | tar -tzf -` — verify/decrypt path, **same argv exposure**.
|
||||
- line 204: `unset PASS`.
|
||||
5. **Temp files:** none created for the secret. `mktemp` is **not** used anywhere in backup; the archive is `tar -czvf "$ARCHIVE"` (line 173), then encrypted, then `rm -f "$ARCHIVE"` (line 197) leaving `$ARCHIVE.gpg`, `chmod 600` (line 199). The plain intermediate `.tar.gz` is removed **after** encryption. On **failure** (encrypt/verify fail → `set -euo pipefail` aborts): if gpg fails at 195, the plain `$ARCHIVE.tar.gz` **remains on disk** (sleeped only after the successful encrypt at 197). No trap removes the plain intermediate. This is a **partial-failure plaintext-leak** risk (archive stays unencrypted if `gpg --symmetric` fails). Also the top trap (line 17) sends `notify_send "Backup FAILED..."` on any ERR — does not leak PASS but does announce paths.
|
||||
6. **verify/decrypt path:** line 202 pipes gpg decrypt into `tar -tzf -` and discards (no plaintext written to disk) — good, verify is streaming.
|
||||
7. **notify/msg exposure:** backup success sends `notify_send "Backup completed: $ARCHIVE"` (line 211) and USB copy sends `...: $dest` (line 112) — **artifact paths (not contents) are sent to Telegram/Matrix** via `lib/notify.sh`. These are local absolute paths; harmless unless they reveal structure the owner wants private. The **gpg passphrase is NOT** in any notify message.
|
||||
8. **Shell-history/log exposure:** PASS is read via `read -s` (not echo'd, not in history). It appears only in the gpg argv; not logged to any file by `pos`. However if the operator runs the tool under `set -x` or with shell tracing, `PASS` is a shell variable and would be expanded into stderr; not a repo bug, but the argv placement (not env) is the primary leak vector.
|
||||
|
||||
**Other secrets in process argv repo-wide:**
|
||||
- `bin/pos-network-hotspot:60` — `sudo create_ap ... "$@"`; if a passphrase/SSID is passed as an arg it enters `create_ap` argv. Interactive, no secret stored.
|
||||
- `matrix-sender` `login` (pos-communication-matrix-sender:167-190): password read via `read -rsp ... </dev/tty`, sent **in the HTTP JSON body** (not argv), access token saved to `matrix.env` `chmod 600` (save_config:67). **Good** — no argv leak.
|
||||
- `pos-share-smb-client` `make_creds` (143-152): password `read -rsp`, written to a throwaway `mktemp` file `chmod 600`, `printf '...password=%s'` — **not argv**. SMB persistent creds `sudo install -m 600` to `/etc/samba/credentials/` (line 346). **Good**.
|
||||
- `pos-ai` API keys: sent as HTTP header `x-goog-api-key: ${AI_API_KEY}` (gemini.sh:25) — **not argv**. `AI_API_KEY`/`AI_GEMINI_API_KEY` exported env var; safe.
|
||||
- Telegram bot token: used in URL `.../bot${TELEGRAM_BOT_TOKEN}/...` (listener:636,768; sender:81) — appears in **URLs/argv of curl** (`curl ... https://api.telegram.org/bot<TOKEN>/...`). The token is in curl's argv → visible via `ps`. **This is a real secondary exposure**: the Telegram bot token is a local-process-argv secret. Same class as the gpg passphrase.
|
||||
|
||||
---
|
||||
|
||||
## Severity-ranked concrete vulnerabilities (real, not hypothetical)
|
||||
|
||||
**V1 — HIGH — Telegram listener lacks sender authorization; remote code execution as your user.**
|
||||
`bin/pos-communication-telegram-listener:787` is the only gate and it is an OR over `chat`/`from_id` against the single `TELEGRAM_CHAT_ID` value, with **no sender allowlist key existing anywhere**. Any message that satisfies either `chat == TELEGRAM_CHAT_ID` OR `from_id == TELEGRAM_CHAT_ID` triggers mapped `bash -c` (C08) and the Gemini bridge. In group/shared-chat misconfiguration, any member runs arbitrary commands as the owner. Even in the "correct" 1:1 setup there is no authenticated-sender binding, so a bot-repost or replayed `from_id` is accepted regardless of source chat. Evidence: lines 787, 743, 697, 734.
|
||||
|
||||
**V2 — HIGH — `pos ai` evaluates arbitrary AI-provider output; `--trust` removes confirmation.**
|
||||
`bin/pos-ai:388,403` runs `eval "$cmd"` where `$cmd` is extracted from the provider's raw response (`_extract_commands`:363). The Telegram/Matrix AI bridges (listener:734/:472 / C14/C15) feed arbitrary chat text as the prompt; if the model's reply (or a prompt-injection / model misbehavior) emits a fenced `bash`/`sh` block, it is executed. Non-trust mode prompts on a tty (`[ -w /dev/tty ]`), but **trusted aliases** pass `--trust` (`TRUST_MODE=1`, :385-388) → auto-execute, no confirm. And the **AI bridges run non-interactively (no tty)**, so `_prompt_run_command`'s `[ -w /dev/tty ] || return 0` at line 382 returns 0 **without prompting** → **any** command block in a bridge-AI reply executes automatically even without trust. This makes the chat AI bridge an unconditional RCE on model output. Evidence: pos-ai:382,388,403,535,572; listeners:734/:472.
|
||||
|
||||
**V3 — HIGH — backup GPG passphrase on command line (argv exposure).**
|
||||
`bin/pos-system-backup:195,202` pass `--passphrase "$PASS"` to gpg → secret readable by any local user or leak to syslog/ps. Evidence: lines 195, 202. (Also the plain `.tar.gz` can remain on a failed encrypt: line 197 is only reached after a successful 195.)
|
||||
|
||||
**V4 — MEDIUM — Matrix listener optional chat authorization.**
|
||||
When `MATRIX_ROOM_ID` is unset (default), `run_daemon:515` watches **all joined rooms**; sender still must equal the owner (`:550`), so it's owner-only but unbounded-room. If the account is in any shared room and the owner sends a command there, it executes. Fails closed on unresolved owner (`:521`), so severity is bounded to owner-initiated events. The authorization model is *correct* conceptually but chat-scope defaults too broadly.
|
||||
|
||||
**V5 — MEDIUM — unvalidated host/port interpolated into `bash -c /dev/tcp` strings.**
|
||||
`bin/pos-network-checkport:133,157,166,168`, `bin/pos-share-smb-client:92`, `lib/share-lib.sh:59`. Host strings are not fully validated (no `;`/`$()`/backtick reject) before being embedded in a shell string. Reachable via CLI args (interactive) and `NET_PROBE` env (`pos-network-download:28,377`).
|
||||
|
||||
**V6 — LOW/MEDIUM — `curl | sh` / `curl | sudo bash` installers without checksums.**
|
||||
`apps/{zerotier,netbird,tailscale,docker,opencode,tsui}.sh`. Vendor-standard, but supply-chain risk from the remote script. Interactive install-time only.
|
||||
|
||||
**V7 — LOW — Telegram bot token in curl argv.**
|
||||
`bin/pos-communication-telegram-listener:636,768` and `telegram-sender:81` place `bot<TOKEN>` in a URL passed to curl → token visible in `/proc/<pid>/cmdline`. Same class as V3.
|
||||
|
||||
---
|
||||
|
||||
## Uncertainties / Could-not-verify
|
||||
|
||||
- **Exact live behavior of the Telegram `from_id`/`chat_id` equality in real groups** cannot be established by read-only inspection; the numeric-equality assumption is documented in the code (line 787) but its breakage requires a live group test. This is the crux of V1's real-world exploitability and needs a live check by another agent.
|
||||
- **Whether `ps`/`/proc` argv is considered a real threat model** for this homelab (single-user local machine) is a policy/rationale question the Explorer can't decide — see note in V3.
|
||||
- **`pos-network-download`'s `NET_PROBE`** default is constant; whether any deployment injects an attacker-controlled value is unknown — it honours an env var seam.
|
||||
- **Whether any operator already sets a `MATRIX_ROOM_ID`** (affects V4) is config state not present in the repo (config files are gitignored).
|
||||
- The exact content of runtime `telegram_commands.env` / `matrix_commands.env` map files (what commands are mapped) is unknown — gitignored.
|
||||
|
||||
---
|
||||
|
||||
## Important Files
|
||||
- `bin/pos-communication-telegram-listener` — auth gate (787), dispatch, AI/prefix bridges.
|
||||
- `bin/pos-communication-matrix-listener` — owner filter (550), room gate (539).
|
||||
- `bin/pos-ai` — eval path (388,403,382), trusted mode, bridge call sites.
|
||||
- `bin/pos-system-backup` — passphrase argv usage (195,202).
|
||||
- `bin/pos-network-checkport`, `bin/pos-share-smb-client`, `lib/share-lib.sh`, `bin/pos-network-download` — /dev/tcp interpolation.
|
||||
- `lib/ai-providers/gemini.sh` — API key in HTTP header (not argv).
|
||||
- `bin/pos-communication-{telegram,matrix}-sender` — POS_CONFIG registry, token/creds handling.
|
||||
- `lib/notify.sh` — artifact-path-only notify sends.
|
||||
|
||||
---
|
||||
|
||||
## Handoff
|
||||
|
||||
- **Status:** COMPLETE (investigation objective satisfied; no code changed).
|
||||
- **Objective:** Security audit of command-execution surfaces, chat authorization, secret handling.
|
||||
- **Evidence:** file:line citations throughout; classification by certainty (SAFE / DESIGNED-INTERACTIVE / VULNERABLE-INPUT / REVIEW) with provenance chains.
|
||||
- **Affected areas:** Telegram & Matrix listeners, `pos-ai` eval, backup GPG, /dev/tcp probes, installer curl|sh.
|
||||
- **Scope/decision boundary:** Read-only exploration only; **no changes proposed for implementation.** Minimal change sketch for the Telegram auth gate is provided at Section 2 (as exploration output, explicitly NOT implemented).
|
||||
- **Verification performed:** Full source tracing of both listeners, pos-ai eval, backup, notify, and all grep'd primitives; git blame on the Telegram gate (unchanged since 2026-08-06, b9edd078 / 014d6be).
|
||||
- **Remaining uncertainty:** live Telegram group behavior (V1), NET_PROBE env deploy state, MATRIX_ROOM_ID config state, real `ps`-argv threat model.
|
||||
- **Recommended next agent:** **Architect** — the two listeners embody two different authorization philosophies (Telegram: chat-id-OR, no sender allowlist; Matrix: owner-sender with optional room). Aligning them into one "sender AND chat authorized" contract is a cross-component design decision (config schema + registry keys + both daemons), which is precisely an architectural boundary. Evidence above gives the exact guard lines and a minimal-change sketch to evaluate, not implement.
|
||||
|
||||
### Scope-expansion note
|
||||
Investigating Telegram/Matrix authorization surfaced that the auth contract is **not a single-file bug** but a **cross-component, decision-level** matter (two daemons, the `# POS_CONFIG:` registry, config templates, docs, and a new `TELEGRAM_OWNER_ID`-style key). That is an architectural decision, so the handoff above goes to **Architect** per the Explorer's scope rule.
|
||||
|
||||
```text
|
||||
Status: COMPLETE (with Architect handoff on scope expansion)
|
||||
Reason: Fixing chat authorization correctly spans two listeners + config schema + registry + docs
|
||||
Evidence: listener auth gates telegram:787 / matrix:550,539; POS_CONFIG headers in senders; no owner key anywhere
|
||||
Affected areas: bin/pos-communication-telegram-listener, matrix-listener, telegram-sender (POS_CONFIG), config templates, DOC
|
||||
Decision required: Architect
|
||||
Out-of-scope changes: none
|
||||
```
|
||||
@@ -1,242 +0,0 @@
|
||||
# Explorer Report — 2026-09-06 — Tooling Audit (lint perf, install/uninstall symmetry, config-parsing duplication, shell correctness)
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Lint perf:** `scripts/lint-conventions.sh` (261 lines) has ~17 external-process hotspots; the dominant cost is **two full-file scans that spawn `printf | sed | tail` per line** (`uses_stdin` L67 and the top-level-`local` scan L143) — for a ~45-tool repo with several >1000-line tools this is roughly **60k+ subprocess forks per lint run** just from those two scans. Wall-time measurements are **UNVERIFIED** (sandbox denies `time`/`make`/`bash` execution); static hotspot inventory below is complete.
|
||||
- **Install/uninstall symmetry:** a real uninstaller exists (`pos system uninstall`, 435 lines) but it is **PARTIAL** — 9 of 12 installed `lib/*.sh` have no removal path, the ScaleTail template clone (`/usr/local/share/linux_post_install/scale-tail`) and the feature-flag store (`/usr/local/share/linux_post_install/flags/`) are never removed, `~/.config/rclone/` and `/usr/local/bin/yt-dlp` survive, and **all runtime-created systemd *user* units** (`~/.config/systemd/user/`) are missed by every tier.
|
||||
- **Config duplication:** **9 tools hand-roll byte-similar `load_config()`-style loaders** (env-wins export loop), plus at least 4 bespoke parsers; 2 shared key-value read/write libraries (`config-ui.sh` `cfg_value`/`cfg_write` and `entertainment-lib.sh` `config_value`/`write_config_key`) are duplicates of each other. CRLF-strip behavior splits 5-and-5; the `CONFIG_DIR`/XDG seam is honoured by self-contained tools but **bypassed by several common.sh-sourcing tools** that hardcode `$HOME/.config/linux_post_install/...`. Recommended owner: **`lib/config-ui.sh`**.
|
||||
- **Shell correctness:** no high-confidence unquoted-`rm -rf`, unquoted-`[ $x ]`, or unguarded-`cd` bugs found in `bin/`; the flagged hotspots are `bin/pos:388/403` (`run eval "$cmd"` — AI-extracted command execution, deliberate but security-relevant), `bin/pos-system-uninstall:333` (`sed -i '/pos/d'` on user `.bash_completion`), and the per-line subprocess spawns in the lint script itself.
|
||||
|
||||
---
|
||||
|
||||
## Task 1 — Lint performance (`scripts/lint-conventions.sh`, 261 lines)
|
||||
|
||||
### 1.1 Measured timing
|
||||
|
||||
| Command | Result |
|
||||
|---|---|
|
||||
| `time make lint` (run 1) | **UNVERIFIED** — sandbox denies `make`, `bash`, `time` execution |
|
||||
| `time make lint` (run 2) | **UNVERIFIED** |
|
||||
| `time bash scripts/lint-conventions.sh` | **UNVERIFIED** |
|
||||
|
||||
Static hotspot analysis is complete and is the basis for the estimates (see 1.3).
|
||||
|
||||
### 1.2 Per-line/per-file external-process hotspots (rule → implementation → bash-native alternative)
|
||||
|
||||
F = per-file spawn, L = per-line spawn, 1× = one-off.
|
||||
|
||||
| # | Lint rule | Location | Spawns per unit | Bash-native equivalent (no semantics change) |
|
||||
|---|---|---|---|---|
|
||||
| 1 | shebang check | L92 `head -1 "$f" \| grep -q` | F (2 procs/file) | `IFS= read -r first < "$f"` + `[[ $first == '#!/usr/bin/env bash' ]]` |
|
||||
| 2 | strict-mode check | L95 `has_regex` → `grep -qE` | F (1 proc/file) | fold into the same first-line read as #1 |
|
||||
| 3 | INTERACTIVE_CMDS extraction | L84 `sed -n … \| head -1` | 1× (2 procs) | single `read` with regex |
|
||||
| 4 | `# POS:` header text | L110 `sed -n '/^# POS: /{…;q}'` | F (1 proc/tool) | read up to first `# POS:` line in bash loop |
|
||||
| 5 | em-dash presence | L115 `grep -q ' — ' <<<"$headline"` | F (1 proc/tool, heredoc string) | `[[ $headline == *' — '* ]]` |
|
||||
| 6 | `# POS:` line number | L118 `grep -nE … \| head -1 \| cut -d: -f1` | F (3 procs/tool) | captured in the same loop as #4 |
|
||||
| 7 | deps-guard-before-help | L127 `first_guard_line` | F (0 extra — bash loop) | already bash-native |
|
||||
| 8 | help-line number | L128 `first_line` → `grep -nE … \| while read` | F (1 proc/tool) | same first-match read loop as #4/#6 |
|
||||
| 9 | top-level `local` scan | L137–157 with **L143 `printf '%s\n' "$line" \| sed -nE … \| tail -1`** | **L (3 procs per line of every tool)** | `[[ $line =~ <<-?[[:space:]]*([A-Za-z0-9_]+) ]]` in-bash |
|
||||
| 10 | stdin-reader detection | L59–80 `uses_stdin` with **L67 `printf \| sed \| tail`** | **L (3 procs per line — scans every tool a 2nd time)** | same `[[ =~ ]]` regex; can also merge with #9 into ONE pass |
|
||||
| 11 | POS.md reference | L169 `grep -q "$(basename "$f")" DOC/POS.md` | F (1 proc/tool) | read POS.md into a var once; `[[ $posmd == *$basename* ]]` |
|
||||
| 12 | INTERACTIVE_CMDS entry check | L174–180 | — | fine |
|
||||
| 13 | plugin common.sh / POS_PLUGIN / app uninstall fn+case / systemd / wrapper checks | L184,187,196,197,201,208,211,218,224 `has_regex` | F (1–2 procs/file each) | single-read first-match loop per file |
|
||||
| 14 | wrapper line count | L221–222 `wc -l < "$f"` **twice** | F (2 procs/wrapper) | `mapfile -t lines < "$f"; ${#lines[@]}` |
|
||||
| 15 | secret-literal scan | L229–243 `grep -nE` per file + **L235 `grep -qE … <<<"$body"` per matched line** | F + L (heredoc-string greps) | `[[ $body =~ (TOKEN|PASSWORD|…)= ]]` |
|
||||
| 16 | system-path write scan | L245–258 `grep -nE` per file + **L251 two `grep -qE <<<"$body"` per matched line** | F + L | `[[ $body =~ (>|>>|tee ) ]] && [[ $body =~ (/etc/|\$HOME|/usr/local) ]]` |
|
||||
| 17 | `last_line()` | L39–42 | **dead code — defined, never called** | delete |
|
||||
|
||||
### 1.3 Estimated cost
|
||||
|
||||
- The two per-line scans (#9, #10) each read every line of every `bin/pos-*` tool twice. The repo has 45 tools with several >1000-line files (pos-docker-vbox 1125, pos-media-ytsync 1213, pos-network-download 1108) — total tool lines ≈ 15–20k. At 3 forks/line × 2 scans ≈ **90k–120k `printf|sed|tail` subprocess forks per lint run** just from those two rules.
|
||||
- Remaining rules add ≈ 10–15 forks per tool ≈ 500–700 more forks total. The secret/system-path scans add one grep per file plus per-matched-line heredoc greps.
|
||||
- Expected effect: the lint time is dominated by process creation (fork/exec), not by grep itself. Replacing #9/#10 with `[[ =~ ]]` and merging into one pass should cut lint wall time by the largest factor; the heredoc-string greps (#5, #15, #16) are cheap per call but numerous.
|
||||
|
||||
### 1.4 `scripts/check-sync.sh` (42 lines) — brief
|
||||
|
||||
- `bash -n` per file, exec-bit loop, doc-sync via `gen-docs.sh --check`, 3 dispatch smokes. Per-file spawns are inherent to `bash -n` (must run bash anyway); no obvious perf bug.
|
||||
- Correctness note: L14–16 glob list misses `features/*.sh`? — actually it includes `features/*.sh` (line 15 `features/*.sh`). It does NOT include `completions/*` other than `completions/pos.bash` (fine) and does not `bash -n` `install.sh`'s sourced libs beyond the list — libs are covered. No hotspot.
|
||||
|
||||
### 1.5 `scripts/gen-docs.sh` (254 lines) — brief
|
||||
|
||||
- `sed` per header per tool (L42–47: 6 sed calls/tool) — minor; only runs on `make gen`, not per commit.
|
||||
- L207–210 check mode: `sed` block extract + `cat` + `diff` per block — fine for check.
|
||||
- Correctness hotspot: docmap convergence loop (L244–252) re-runs `regen_block docmap` up to 5 times by design; each iteration re-does a full-file `sed` + `grep -n` + `wc -l` — acceptable (documented convergence), but on a 700-line file it is the single slowest part of gen; an in-memory line accounting would converge in one pass. Not a bug.
|
||||
|
||||
### Task 1 — Ranked change list (no implementation)
|
||||
|
||||
1. Merge the per-line `printf|sed|tail` delimiter extraction into a single bash-native pass using `[[ $line =~ <<-?[[:space:]]*([A-Za-z0-9_]+) ]]` — used by the top-level-`local` scan (L143) and `uses_stdin` (L67). Highest ROI; removes the ~90–120k fork estimate.
|
||||
2. Replace per-line heredoc-string `grep -q <<<"$body"` with `[[ $body =~ … ]]` in the secret (L235) and system-path (L251) scans.
|
||||
3. Replace per-file `grep`/`sed|head|cut`/`wc` with a single read of the first ~6 lines per tool (covers L92/95/110/115/118/128) plus `mapfile` line counts for wrappers (L221).
|
||||
4. Replace `grep -q <basename> DOC/POS.md` (L169) with one preloaded POS.md content check.
|
||||
5. Delete the dead `last_line()` (L39–42).
|
||||
|
||||
---
|
||||
|
||||
## Task 2 — Install/uninstall symmetry
|
||||
|
||||
### 2.1 What exists
|
||||
|
||||
- **Installer:** `install.sh` phases 1–4 + optional apps. Uninstall path documented in `install.sh:68–71` (apps only) and provided as a **CLI tool** `bin/pos-system-uninstall` (not a `make uninstall`, not a scripts/ uninstaller — grep of `Makefile`, `scripts/`, `README.md` shows no `make uninstall`; `apps/install.sh --uninstall` handles optional desktop apps only).
|
||||
- **Uninstaller:** `bin/pos-system-uninstall` — Tier 1 (always): binaries, plugins, known systemd services, shell integration; Tier 2 (`--config`): `~/.config/linux_post_install`; Tier 3 (`--data`): `~/.local/share/linux_post_install`.
|
||||
|
||||
### 2.2 Install inventory vs uninstall coverage
|
||||
|
||||
| Artifact | Installed by | Removal path | Verdict |
|
||||
|---|---|---|---|
|
||||
| `bin/*` → `/usr/local/bin/` (45 pos-*, pos, flag-*, wr-*, mp3/mp4/vbox/ssh-load-all) | Phase 2 (install.sh:136–140) | Tier 1: `/usr/local/bin/pos` + `compgen -G /usr/local/bin/pos-*` + legacy names (L53–83, 229–257) | **SYMMETRIC** |
|
||||
| `lib/common.sh`, `lib/menu-lib.sh`, `lib/share-lib.sh` → `/usr/local/bin/` | Phase 2 (install.sh:143–144) | Tier 1 (pos-system-uninstall:62–64) | **SYMMETRIC** |
|
||||
| `lib/{flags,notify,entertainment-lib,scheduler-lib,config-ui,user-timers-lib,entertainment-plugin-lib,usb-lib,registry}.sh` → `/usr/local/bin/` | Phase 2 (install.sh:143–144) | **none** | **INSTALL-ONLY** (9 of 12 libs) |
|
||||
| `lib/ai-providers/*.sh` → `/usr/local/bin/ai-providers/` | Phase 2 (install.sh:154–161) | Tier 1 `rm -rf /usr/local/bin/ai-providers` (L244) | **SYMMETRIC** |
|
||||
| `entertainment/*.sh` → `/usr/local/bin/` | Phase 2 (install.sh:167–173) | Tier 1 — **hardcoded list** `weather.sh gold.sh joke.sh` (L72, 248) | **SYMMETRIC today**; breaks automatically if a 4th plugin is added |
|
||||
| `x64_bin|arm64_bin/*` → `/usr/local/bin/` | Phase 2 (install.sh:179–194) | Tier 1 hardcoded `wihotspot wihotspot-gui create_ap` (L86, 260) | **SYMMETRIC today**; same hardcode fragility |
|
||||
| `features/*` → `/usr/local/bin/` (--feature) | Phase 2 (install.sh:197–222) + flag set | Tier 1 `autostart.sh usb-automount.sh` (L91, 265) | **SYMMETRIC today**; hardcoded |
|
||||
| **Feature-flag store** `/usr/local/share/linux_post_install/flags/` | Phase 2 `flag_set` (install.sh:216) | **none** (no `flags`/`flag` match in pos-system-uninstall) | **INSTALL-ONLY** |
|
||||
| **ScaleTail clone** `/usr/local/share/linux_post_install/scale-tail` | Phase 4 (install.sh:237–243) | **none** (only bash-completion under /usr/local/share is removed, L108) | **INSTALL-ONLY** |
|
||||
| `completions/pos.bash` → `/usr/local/share/bash-completion/completions/pos.bash` | postinstall.sh:100–107 | Tier 1 (L108, 284) | **SYMMETRIC** |
|
||||
| systemd `*.service`/`*.timer` → `/etc/systemd/system/` + enable | postinstall.sh:139–166 | Tier 1: 3 known + find `-name '*linux_post_install*' -o -name 'pos-*'` (L287–311) | **SYMMETRIC** (system units) |
|
||||
| `config/authorized_keys` → `~/.ssh/authorized_keys` | postinstall.sh:110–137 | **none** (tier 2 only targets `~/.config/linux_post_install`) | **INSTALL-ONLY** (by design — user data) |
|
||||
| `config/rclone.conf` → `~/.config/rclone/rclone.conf` | postinstall.sh:10–17 | **none** (tier 2 path is `linux_post_install` only) | **INSTALL-ONLY** |
|
||||
| `config/{entertainment,system,notify,ai}.env` → `~/.config/linux_post_install/` | postinstall.sh:22–50 | Tier 2 (`--config`) find over the dir (L147–155) | **SYMMETRIC** (opt-in tier) |
|
||||
| `config/schedule.d/*.env` → `~/.config/linux_post_install/schedule.d/` | postinstall.sh:57–77 | Tier 2 + rmdir schedule.d (L351–355) | **SYMMETRIC** (opt-in tier) |
|
||||
| PATH line + completion line in `~/.bashrc` | postinstall.sh:80–98 | Tier 1 sed removals (L320–322) | **SYMMETRIC** |
|
||||
| apt packages (25+) + yt-dlp → `/usr/local/bin/yt-dlp` + cpufreq | preinstall.sh:28–75 | **none** (uninstaller never touches apt or yt-dlp) | **INSTALL-ONLY** (likely deliberate — system packages) |
|
||||
|
||||
### 2.3 Runtime-created state (created by tools at runtime, not install.sh)
|
||||
|
||||
| Artifact | Created by | Uninstall path in pos-system-uninstall | Verdict |
|
||||
|---|---|---|---|
|
||||
| `~/.config/linux_post_install/<tool>.env` (ai, telegram, matrix, scrcpy, download, ytsync, grab, ai-aliases, compose) | tools' config writes | Tier 2 (`--config`) | **RUNTIME-STATE / SYMMETRIC** (removed with --config) |
|
||||
| `~/.local/share/linux_post_install/{logs,ytsync,ai/models,entertainment/last,backups}` | bin/pos logging + tools | Tier 3 (`--data`) | **RUNTIME-STATE / SYMMETRIC** (removed with --data) |
|
||||
| **systemd *user* units** `~/.config/systemd/user/`: `pos-aria2.service`+`pos-aria2-retry.{service,timer}` (pos-network-download:172–190,633–669), telegram-listener unit (pos-communication-telegram-listener:471–521), matrix-listener unit (pos-communication-matrix-listener:315–364), `pos-ai-server.service` (pos-ai-server:500–522), entertainment timers `pos-entertainment-*.timer` (user-timers-lib), scheduler per-job timers (scheduler-lib) | runtime tool subcommands | **none** — Tier 1 only scans `/etc/systemd/system` (L287–311); Tier 2 only `~/.config/linux_post_install`; `~/.config/systemd/user/` is outside both | **INSTALL-ONLY** (from the uninstaller's perspective; each tool's own `stop`/`disable` subcommand does remove its own unit, e.g. `pos network download stop` L209–211) |
|
||||
| `~/.local/bin/pos-ai-hook.sh` + `ai-aliases.sh` wrappers | pos-ai-alias | Tier 1 pos-ai-hook + marker-managed alias scan (L96–105, 270–281) | **SYMMETRIC** |
|
||||
| `~/.config/rclone/rclone.conf` (from postinstall) | postinstall.sh:10–17 | none | **INSTALL-ONLY** |
|
||||
|
||||
### Task 2 — Ranked change list (no implementation)
|
||||
|
||||
1. **Remove the 9 orphaned libs** (`flags.sh`, `notify.sh`, `entertainment-lib.sh`, `scheduler-lib.sh`, `config-ui.sh`, `user-timers-lib.sh`, `entertainment-plugin-lib.sh`, `usb-lib.sh`, `registry.sh`) in Tier 1 — the biggest INSTALL-ONLY gap (hardcoded `common.sh menu-lib.sh share-lib.sh` only, pos-system-uninstall:62–64).
|
||||
2. **Remove ScaleTail templates** `/usr/local/share/linux_post_install/scale-tail` and the **feature-flag store** `/usr/local/share/linux_post_install/flags/` in Tier 1 (documented install outputs in AGENT_Context §3/§10, no removal).
|
||||
3. **Add a user-unit sweep** to Tier 1: disable+remove matching units in `~/.config/systemd/user/` (prefixes `pos-*`, `pos-entertainment-*`, `pos-schedule-*` etc.), or document that per-tool `stop` is the supported path.
|
||||
4. De-hardcode the entertainment-plugin / prebuilt-binary / feature names in the uninstaller to directory-driven discovery (mirror install.sh's loops) so future plugins/bins don't silently become INSTALL-ONLY.
|
||||
5. Decide (and document) whether `~/.config/rclone`, `~/.ssh/authorized_keys` additions, apt packages, and `/usr/local/bin/yt-dlp` are intentionally outside uninstall — currently silent.
|
||||
|
||||
---
|
||||
|
||||
## Task 3 — Config-parsing duplication
|
||||
|
||||
### 3.1 Inventory
|
||||
|
||||
**Shared loaders that exist:**
|
||||
|
||||
| Loader | Location | Used by |
|
||||
|---|---|---|
|
||||
| `load_system_env()` (env-file → export, env-wins) | lib/common.sh:146–159 | pos-system-health, pos-system-backup, pos-media-sync (system.env) |
|
||||
| `cfg_value()` / `cfg_write()` (key read/write, `KEY="value"`, chmod 600) | lib/config-ui.sh:311–344 | pos-config; pos-entertainment-config:L98 sources config-ui dynamically; config-scope registry consumers |
|
||||
| `config_value()` / `write_config_key()` (key read/write, same semantics) | lib/entertainment-lib.sh:28–54 | pos-entertainment-{config,status,enable,disable,send} |
|
||||
| inline `grep '^NOTIFY_PLATFORM=' \| tail -1 \| cut` | lib/notify.sh:41 | notify_send |
|
||||
|
||||
**Hand-rolled near-identical `load_config()`-style loaders (9)** — each is the same ~14-line loop: `grep -E '^[A-Z_]+=' | while IFS='=' read k v` + quote-strip + `[ -z "${!k:-}" ] && export`:
|
||||
|
||||
1. bin/pos-communication-telegram-sender:61–74 (`load_config`)
|
||||
2. bin/pos-communication-matrix-listener:65–77 (`load_config`)
|
||||
3. bin/pos-communication-telegram-listener:86–98 (`load_config`)
|
||||
4. bin/pos-communication-matrix-sender:44–57 (`load_config`)
|
||||
5. bin/pos-communication-scrcpy:14–28 (`load_config`)
|
||||
6. bin/pos-ai:130–160 (`load_config`, plus legacy-file loop)
|
||||
7. bin/pos-ai-server:21–35 (`load_config`)
|
||||
8. bin/pos-ai-hf:30–44 (`load_hf_config`)
|
||||
9. bin/pos-media-grab:10–23 (`load_grab_config`)
|
||||
|
||||
**Bespoke parsers (4+):**
|
||||
|
||||
- bin/pos-network-download:32–37 `load_secret` — single-key `grep'^RPC_SECRET=' | head -1 | cut -d= -f2-`; also duplicated inline at L153–154
|
||||
- bin/pos-docker-compose:9–10 + layered strategy (`template < global compose.env < per-service .env`, documented L31–48) — reads global config via `CONFIG_ENV` and per-service envs
|
||||
- bin/pos-share-smb-server:97 `reload_config` — Samba-specific
|
||||
- bin/pos-media-ytsync:28–30 `_YTSYNC_CFG` via `pos config ytsync` scope; plus the share-client (`pos-share-smb-client`) creds records parsing
|
||||
|
||||
**Counts:** 45 `pos-*` tools; ~20 source `lib/common.sh`; **9 hand-roll their own file parser**; only `pos-config` and the entertainment tools use a shared key-value loader; 3 use `load_system_env`; the 5 self-contained communication tools duplicate the loader because they don't source common.sh (documented convention: guarded inline fallback copies in DEV.md).
|
||||
|
||||
### 3.2 Consistency findings
|
||||
|
||||
- **Precedence order** is `env > config-file > defaults` everywhere the hand-rolled loaders are used (`if [ -z "${!k:-}" ]` before export; defaults applied later via `${VAR:-default}`). CLI-vs-config precedence is declared `CLI > environment > config file` in the three tools that document it (telegram-sender:45, scrcpy:70, matrix-sender:29). pos-docker-compose is the outlier model (per-service file wins over global file; no env) — a different domain, but also the only tool where "config file" beats "global defaults" deliberately.
|
||||
- **CRLF handling diverges:** 5 loaders strip `\r` (matrix-sender:53, scrcpy:23, ai:139, ai-server:30, ai-hf:39) but 5 do NOT (telegram-sender, matrix-listener, telegram-listener, media-grab, and `load_system_env` in common.sh:154). A Windows-edited `.env` parses differently depending on which tool reads it.
|
||||
- **CONFIG_DIR / XDG seam divergence:** self-contained tools (+ config-ui.sh:32, notify.sh:27) carry the guarded `CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"` copy; but several tools that **source** common.sh (which defines `CONFIG_DIR` at line 19) still hardcode `$HOME/.config/linux_post_install/...`: pos-ai:11, pos-ai-hf:26, pos-ai-server:15, pos-media-grab:11, common.sh load_system_env:147, entertainment-plugin-lib:15, pos-docker-compose:10. So `CONFIG_DIR`/`XDG_CONFIG_HOME` overrides work for some tools and are silently ignored by others.
|
||||
- **Key-value writers duplicated** — `cfg_write` (config-ui.sh:323) and `write_config_key` (entertainment-lib.sh:36) are the same algorithm (grep -v + append, `-` deletes, chmod 600); only the value-quoting and the multi-line warning differ.
|
||||
|
||||
### 3.3 Recommendation (no implementation)
|
||||
|
||||
- **Owner: `lib/config-ui.sh`.** It already hosts the `POS_CONFIG` scope registry that `pos config` consumes, has secret masking/validation helpers, and is installed to `/usr/local/bin` alongside the tools.
|
||||
- Add one generic loader there, e.g. `load_env_file <file>` (env-wins export loop with quote + CRLF strip parsed consistently) and have `load_system_env` delegate to it.
|
||||
- Merge `entertainment-lib.sh` `config_value`/`write_config_key` into `cfg_value`/`cfg_write` (keep `config_value` as a thin alias for the entertainment tools, or migrate the 5 call sites).
|
||||
- **Migration targets:** the 9 hand-rolled loaders → `load_env_file` (source `config-ui.sh` in the 5 self-contained communication tools, replacing their guarded inline copies and CONFIG_DIR blocks); `pos-network-download` → `cfg_value "$CONFIG_FILE" RPC_SECRET`; `pos-docker-compose` `config show` → `cfg_value`/`cfg_write` for the global config.
|
||||
- Unify CRLF-strip and the `CONFIG_DIR` path source across every loader during the migration.
|
||||
|
||||
### Task 3 — Ranked change list (no implementation)
|
||||
|
||||
1. Add `load_env_file` to `lib/config-ui.sh`; make `common.sh load_system_env` delegate; fix the CRLF split in the process.
|
||||
2. Migrate the 9 hand-rolled loaders (list in 3.1) to it; make the 5 self-contained communication tools source `config-ui.sh` instead of the inline CONFIG_DIR+load_config copies.
|
||||
3. Fold `entertainment-lib.sh` read/write helpers into `cfg_value`/`cfg_write` (alias or migrate the 5 entertainment call sites).
|
||||
4. Route `pos-network-download` `load_secret` and `pos-docker-compose` global-config reads through `cfg_value`.
|
||||
5. Replace hardcoded `$HOME/.config/linux_post_install/...` in the common.sh-sourcing tools with the sourced `CONFIG_DIR` (pos-ai, pos-ai-hf, pos-ai-server, pos-media-grab, load_system_env, entertainment-plugin-lib, pos-docker-compose).
|
||||
|
||||
---
|
||||
|
||||
## Task 4 — Shell-correctness hotspots (high-confidence only)
|
||||
|
||||
Method: targeted scan of `bin/` and `lib/` for unquoted `$var` in args/array appends, `for x in $list`, `[ $x = … ]`, `rm -rf $VAR`, unguarded `cd`, missing `|| true` in pipelines under `set -euo pipefail`, `eval` of derived strings. Only high-confidence items below.
|
||||
|
||||
### 4.1 High-confidence findings
|
||||
|
||||
- **H-001 (WARN) — `bin/pos-ai:388,403` `run eval "$cmd"`.** `_prompt_run_command` executes a command string extracted from AI output. Interactive path prompts on `/dev/tty`; the `--trust` path (L385–388) auto-executes without confirmation. Deliberate feature, but any AI-output-derived command executed through `eval` is a shell-injection-relevant surface — recommend keeping, but it deserves explicit review of what `trusted=1` callers feed it. Classification: FACT (code), design concern.
|
||||
- **H-002 (WARN) — `bin/pos-system-uninstall:333` `sed -i '/pos/d' "$HOME/.bash_completion"`.** Deletes **every** line containing the substring `pos` from a user-owned file, not just pos-managed lines (unlike the `pos-ai-hook` marker check at L277). A line like `complete -F _git checkout` is safe, but any unrelated completion containing "pos" (e.g. `repos`, `compose-help`, `dispose`) is silently removed — and this runs in default Tier 1. Classification: FACT.
|
||||
- **H-003 (WARN) — `bin/pos-system-uninstall:320–322` `sed -i` on `~/.bashrc`.** Removal of PATH/completion/hook lines is line-based and unanchored at line start (`/source.*pos\.bash/d`, `/linux_post_install.*PATH/d`, `/source.*pos-ai-hook/d`); a user comment mentioning `pos.bash` is deleted. Lower risk than H-002 but same class. Classification: FACT.
|
||||
- **H-004 (WARN) — `scripts/lint-conventions.sh:31,41,67,84,110,118,143,235,251`.** Under `set -euo pipefail`, the `grep | while read` and `... | tail -1 | cut` pipelines are only safe because of the `|| true` / `2>/dev/null` guards and the non-final elements' exit codes. The per-line `printf | sed | tail -1` inside the read loop (L67/L143) is the perf hotspot from Task 1 AND a correctness risk: if `sed` ever exits non-zero for a given line under `pipefail`, the surrounding `while read` loop aborts mid-scan. Classification: FACT (perf measured as static analysis); correctness risk is conditional, not observed.
|
||||
|
||||
### 4.2 Checked and cleared (not bugs)
|
||||
|
||||
- `rm -rf`/`rm -f` in `bin/` are consistently quoted (`pos-ai-hf:644,698,835,989`; `pos-system-uninstall` all lines; `pos-docker-vbox:1115`; app scripts). No unquoted/empty `rm -rf $VAR` found.
|
||||
- Unquoted `[ $x … ]` comparisons: none found in `bin/` (only `"$var"` forms).
|
||||
- `for x in $list` sites (`pos-docker-health:38`, `pos-docker-ps:36`, `pos-tree:71`, `pos-system-health:170`, `pos-network-checkport:408`, `pos-entertainment-status:39`, `pos:123`) intentionally word-split newline/comma-separated IDs or sorted output with no spaces in elements — not bugs at present, but a space in a future element (e.g. a plugin filename) would silently split. Low-priority hardening, not a defect.
|
||||
- `cd` sites are guarded (`pos-docker-vbox:1029,1040` use `(cd "$d" 2>/dev/null && pwd) || …`; `pos-docker-compose:221–260` wrap in subshells with `set -e` context).
|
||||
- `pos-share-smb-client:457` `sudo rm -f "$SMB_CREDS_DIR/$(basename "$where")"` — properly quoted.
|
||||
- `find /etc/systemd/system/ -name '*linux_post_install*' -o -name 'pos-*'` (pos-system-uninstall:311) — `-o` binds both predicates to the stated path; matches both patterns as intended. Not a bug.
|
||||
- `bin/pos` logging tee pipes: INTERACTIVE_CMDS handling verified by lint rule and existing registrations — no new finding.
|
||||
|
||||
### Task 4 — Ranked change list (no implementation)
|
||||
|
||||
1. Restrict `pos-system-uninstall` `.bash_completion`/`.bashrc` removal to anchored, marker-based patterns (e.g. only lines the installer itself added, or apply the `grep -q 'Managed by pos…'`-style marker check used for alias wrappers).
|
||||
2. Review `bin/pos-ai` `_prompt_run_command` trust boundaries: confirm every `trusted=1` caller is user-flagged and document the eval surface (or re-run through `bash -c` with validation).
|
||||
3. Convert lint L67/L143 per-line `printf|sed|tail` to `[[ =~ ]]` (also removes the pipefail-mid-loop abort risk).
|
||||
4. Optional hardening: quote the `for x in $list` sites that consume plugin names/scheduled-job names where elements could contain spaces.
|
||||
|
||||
---
|
||||
|
||||
## Uncertainties
|
||||
|
||||
- **Lint wall time** could not be measured (sandbox denies `time`, `make`, `bash`). Estimates are derived from hotspot counts and file sizes (45 tools, 15–20k total lines); real numbers should be captured by a runner-capable agent (`make lint` ×2 + bare script) — see Handoff.
|
||||
- The exact fork count per run is an INFERENCE (each `printf|sed|tail` is at least 3 forks; actual exec cost depends on PATH lookup and filesystem state).
|
||||
- Whether apt packages / yt-dlp / `~/.ssh/authorized_keys` / `~/.config/rclone` are *supposed* to survive uninstall is a product decision, not verifiable from code.
|
||||
- Whether the runtime-created user units are "expected to persist" is not documented anywhere in the repo; the uninstaller help text ("services") implies coverage, which is not delivered.
|
||||
|
||||
## Important Files
|
||||
|
||||
- `scripts/lint-conventions.sh` — all Task 1 hotspots (L31,41,67,84,92,95,110,115,118,137–157,162,169,221–222,235,251; dead `last_line` L39–42)
|
||||
- `scripts/check-sync.sh`, `scripts/gen-docs.sh` — gates; convergence loop L244–252
|
||||
- `install.sh` — phases, `should_run` (L100–117), copy targets (L136–222), ScaleTail (L237–243)
|
||||
- `preinstall.sh` — apt PACKAGES (L28–43), yt-dlp (L65–68) — no uninstall counterpart
|
||||
- `postinstall.sh` — rclone/entertainment/system/notify/ai env templates, schedule.d, .bashrc, completion, systemd
|
||||
- `bin/pos-system-uninstall` — tiers, lib list L62–64, user-unit gap, H-002/H-003, find L311
|
||||
- `bin/pos`, `bin/pos-ai`, `bin/pos-communication-{telegram,matrix}-{sender,listener}`, `bin/pos-communication-scrcpy`, `bin/pos-ai-server`, `bin/pos-ai-hf`, `bin/pos-media-grab`, `bin/pos-network-download`, `bin/pos-docker-compose`, `bin/pos-share-smb-server` — config-loading inventory (Task 3)
|
||||
- `lib/common.sh` (load_system_env), `lib/config-ui.sh` (cfg_value/cfg_write), `lib/entertainment-lib.sh` (config_value/write_config_key), `lib/notify.sh` — loader candidates
|
||||
- `DOC/DEV.md:182–213` — env-seam rules the loader centralization should preserve
|
||||
|
||||
## Handoff
|
||||
|
||||
- **Status:** OBJECTIVE_SATISFIED (plus measurement note)
|
||||
- **Objective:** evidence audit of lint performance, install/uninstall symmetry, config-parsing duplication, shell-correctness hotspots — completed read-only.
|
||||
- **Evidence / completed work:** this report; hotspot inventory with file:line; install/uninstall matrix; 9-loader duplication census with CRLF and CONFIG_DIR inconsistencies; 3 high-confidence shell hotspots.
|
||||
- **Affected areas:** `scripts/lint-conventions.sh`, `bin/pos-system-uninstall`, `lib/config-ui.sh` + `lib/entertainment-lib.sh` + `lib/common.sh` (loader centralization), 9 tool files, `bin/pos-ai`.
|
||||
- **Scope/decision boundary:** no code changed. Centralizing the loader (Task 3) is a deliberate cross-tool refactor with a doc convention ("guarded inline fallback copies" in DEV.md) — that is an Architect/Designer decision boundary, not a mechanical fix.
|
||||
- **Verification performed:** full reads of lint/check-sync/gen-docs/install/preinstall/postinstall/uninstall/common/config-ui; greps across `bin/`+`lib/` for loaders, rm/cd/eval/for-splitting patterns; shared-memory check of maintainer/architect reports (no overlap: the 2026-09-06 convention-sweep was about POS header/doc drift, not these four areas).
|
||||
- **Remaining uncertainty:** measured lint wall time (needs a runner-capable agent); intended persistence of apt packages/rclone/user units; actual fork count (inference).
|
||||
- **Recommended next agent:** **Architect** (for the loader centralization decision: which library owns `load_env_file`, how self-contained tools source config-ui.sh without breaking the "no shared lib? inline fallbacks" convention) — and/or **Maintainer** for the uninstaller gaps + lint per-line hotspot rewrite if a decision is not needed.
|
||||
- **Reason:** Task 3's fix crosses the documented DEV.md convention and 9 tool files (architectural boundary); Tasks 1/2/4 are mechanical cleanups that a Maintainer can implement once the loader decision is made.
|
||||
@@ -1,57 +0,0 @@
|
||||
# Maintainer Report — 2026-08-26 — pos-ai-alias registration
|
||||
|
||||
## TL;DR
|
||||
|
||||
- Drift: `bin/pos-ai-alias` reads stdin interactively but was missing from `INTERACTIVE_CMDS` in `bin/pos` (lint FAIL), and was undocumented in `DOC/POS.md` (lint WARN).
|
||||
- Correction 1: added token `ai-alias` to `INTERACTIVE_CMDS`, after `ai-openrouter`. Nothing else on the line changed.
|
||||
- Correction 2: documented `pos ai alias …` in the `### ai` section of `DOC/POS.md` (6 table rows + storage/regen paragraph), grounded in `bin/pos-ai-alias` source.
|
||||
- Validation: `bash -n` OK → `make gen` zero drift → `make check` OK → **`make lint`: 0 FAIL, 0 WARN**.
|
||||
- Scope compliance: only `bin/pos` + `DOC/POS.md` touched; pre-existing unrelated working-tree modifications left untouched and attributed below.
|
||||
- No commit made (per brief).
|
||||
|
||||
## Step 1: Register `ai-alias` as interactive command
|
||||
|
||||
- Finding: lint gate `FAIL bin/pos-ai-alias: reads stdin but NOT in INTERACTIVE_CMDS in bin/pos`.
|
||||
- Evidence: `bin/pos-ai-alias` uses `menu_pick`/`menu_ask_value`/`confirm` in `_alias_menu`/`_alias_create`/`_alias_edit`/`_alias_remove`; registry at `bin/pos:262`.
|
||||
- Smallest fix: single-line replacement of line 262 inserting `ai-alias` between `ai-openrouter` and `system-schedule` (keeps the loose `ai*` grouping). Key format matches existing entries (`ai-gemini` ⇒ filename after `pos-`).
|
||||
|
||||
Status: [DONE]
|
||||
|
||||
## Step 2: Document `pos ai alias` in DOC/POS.md ai section
|
||||
|
||||
- Finding: lint gate `WARN bin/pos-ai-alias: file not referenced in DOC/POS.md` (lint greps basename at `scripts/lint-conventions.sh:169`).
|
||||
- Fix: extended the existing Command/Behavior table in `### ai` (rows appended after the last current row) plus one prose paragraph immediately after the table, before "Backward compatibility:". All claims verified against source:
|
||||
- no args → interactive menu loop, alias table shown between picks (`_alias_menu`)
|
||||
- create: 4-step wizard — name validation `^[a-zA-Z][a-zA-Z0-9_-]*$` + uniqueness, provider pick from `lib/ai-providers/*.sh`, session defaulting to alias name, optional system prompt (`|` forbidden, >500 chars warns), confirm default yes (`_alias_create`)
|
||||
- edit: pre-filled prompts, Enter keeps current, per-field changed/unchanged tags, confirm default yes, no-write when unchanged (`_alias_edit`)
|
||||
- remove: confirm default **no**, cannot be undone (`_alias_remove:467`)
|
||||
- list: non-interactive Name/Provider/Session/Prompt table, prompt truncation at 42 chars (`_alias_list`, `_alias_prompt_truncate`)
|
||||
- show `<name>`: details incl. resolved command `pos ai <provider> ask --session <session>[ --system '<prompt>']` (`_alias_show`)
|
||||
- paragraph: `ai-aliases.env` format `name|provider|session|system_prompt`, chmod 600; every write regenerates `ai-aliases.sh` chmod 644 with `alias <name>='pos ai …'`, empty session falls back to name, prompts quote-escaped, file syntax-checked before replace (`_alias_save`/`_alias_regen`); user sources it via shell rc (no repo code auto-sources it yet)
|
||||
- Density/style matched neighboring rows (single-line dense cells, `\|` escaping consistent with existing `(gemini\|openrouter)` row). Literal `bin/pos-ai-alias` string included, satisfying the coverage grep.
|
||||
- Purpose/File header lines of the section were NOT touched (they describe `bin/pos-ai`, not the alias tool) — scope fence.
|
||||
|
||||
Status: [DONE]
|
||||
|
||||
## Step 3: Verify gates
|
||||
|
||||
| Gate | Result |
|
||||
|------|--------|
|
||||
| `bash -n bin/pos` | OK |
|
||||
| `make gen` | `gen-docs: write OK`; md5sums of `completions/pos.bash` + all `DOC/*.md` byte-identical before vs after → zero gen drift introduced |
|
||||
| `git diff --exit-code -- completions/pos.bash DOC/` | exit 1 — **pre-existing** uncommitted mods from other agents (`DOC/AGENT_Context_Project.md` 2 lines, `DOC/DEV.md` 6 lines); attribution: checksums prove gen didn't alter them, I never edited them |
|
||||
| `make check` | `check-sync: OK` |
|
||||
| `make lint` | **0 FAIL, 0 WARN (convention lint)** — both target gates cleared |
|
||||
|
||||
Diff attribution (working tree vs HEAD):
|
||||
- `completions/pos.bash`: identical to HEAD (not in diff stat).
|
||||
- `DOC/POS.md`: exactly my 8 added lines in the ai section, nothing else.
|
||||
- `bin/pos`: exactly 1 insertion / 1 deletion (the INTERACTIVE_CMDS line).
|
||||
- `DOC/AGENT_Context_Project.md`, `DOC/DEV.md`: pre-existing other-agent modifications — out of my scope, untouched by me.
|
||||
|
||||
Status: [DONE]
|
||||
|
||||
## Deferred / notes
|
||||
|
||||
- `postinstall.sh` does not yet append a `.bashrc` source line for `ai-aliases.sh` (the tool only logs "Reload shell: source ~/.bashrc"); the architect report lists this as future work — out of maintenance scope here, noted only.
|
||||
- `AGENT_TODO.md` Done-entry bookkeeping is tied to committing, which this brief forbids — left for whoever commits these changes.
|
||||
@@ -1,113 +0,0 @@
|
||||
# 2026-08-26 — registry-docs sync (Maintainer)
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Drift:** command-registry feature (`lib/registry.sh`, optional `# POS_DEPS:`/`# POS_EXAMPLES:` headers) landed without syncing 3 agent/convention docs — confirmed: AGENTS.md (header enumeration + `pos tree` description), DOC/SCRIPTS.md (Phase-2 lib list, no lib section, stale TOC), MAINTENANCE.md (no record of the convention change).
|
||||
- **Corrections:** all three synced to implemented reality (code + `# POS_*` headers as ground truth); docs-only, nothing else touched.
|
||||
- **Sweep findings (reported):** 2 out-of-fence staleness spots in DOC/AGENT_Context_Project.md (:548 header enumeration, :598 wrongly lists gen-docs.sh as a registry consumer); 1 minor incomplete phrasing in DOC/POS.md:498. In-fence extra fixed: SCRIPTS.md TOC was missing 5 pre-existing lib sections.
|
||||
- **MAINTENANCE.md decision:** no phase-log/conventions-summary section exists → did NOT invent one; used the file's own sanctioned mechanism instead (one appended ticket, M-024, dated Fix line).
|
||||
- **Validation:** `make gen` zero new drift beyond pre-existing uncommitted feature work · `make check` OK · `make lint` **0 FAIL, 0 WARN** · `grep -n "POS_DEPS"` present in AGENTS.md / DEV.md / SCRIPTS.md.
|
||||
|
||||
## Standard established (evidence)
|
||||
|
||||
Ground truth verified before editing:
|
||||
|
||||
- `templates/pos-tool.sh:13-14` documents optional `# POS_DEPS:` / `# POS_EXAMPLES:`.
|
||||
- `DOC/DEV.md:126-134` already documents both headers + semantics; `DOC/DEV.md:34` already lists `registry.sh` → only AGENTS.md / DOC/SCRIPTS.md / MAINTENANCE.md drifted.
|
||||
- `install.sh:143` `lib_names`: registry.sh is LAST (`… usb-lib.sh share-lib.sh menu-lib.sh registry.sh`) — SCRIPTS.md Phase-2 row must append it after `menu-lib.sh`.
|
||||
- Consumers: `bin/pos-tree:7` sources `lib/registry.sh`; `bin/pos:68-90` `_pos_category_help()` lazy-sources it (`reg_scan`/`reg_tools_in`/`reg_lookup`, incl. deps).
|
||||
- `scripts/gen-docs.sh:47-48` parses the same headers independently (predates registry) — must NOT be listed as a registry consumer.
|
||||
- `lib/registry.sh` (199 lines): no shebang, not executable, installed 644; guarded `log/warn/err` fallbacks ("mirrors lib/config-ui.sh", :18-21); key = filename after `pos-`, category split at first dash (empty when category-less, :66-78); fields cat/desc/flags/subcmds/deps/examples (:11); config scope helpers (:171-186); `reg_each` callback contract cb(cat, key, desc) (:189-194); sorts under `LC_ALL=C` inside `reg_scan` (:50-51,120-125).
|
||||
|
||||
## Step 1: AGENTS.md — Tool model bullet (+POS_DEPS/+POS_EXAMPLES/+registry API)
|
||||
|
||||
AGENTS.md:17 — extended the header sentence with both optional headers (deps = space-separated runtime binaries hard-required via `command -v` guards; examples = one per line, `<command> | <description>`) and one clause naming `lib/registry.sh` as the shared query API over all `POS_*` headers (`reg_scan` + `reg_list`/`reg_lookup`/…), new consumers to prefer it. Kept the original terse single-bullet style; nothing else in the bullet touched.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 2: AGENTS.md — Categories bullet (`pos tree` metadata source)
|
||||
|
||||
AGENTS.md:18 — minimal extension: hierarchy still derives from filenames + `# POS:`/`# POS_SUBCMDS:` headers; now also states it reads metadata through `lib/registry.sh` and annotates declared `# POS_DEPS:`. Matches observable behavior of bin/pos-tree (sources registry.sh:7; `[deps: …]` annotations visible in the generated tree block).
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 3: DOC/SCRIPTS.md — Phase 2 lib list + registry section + TOC
|
||||
|
||||
a) Phase-2 row — appended `+ lib/registry.sh` after `+ lib/menu-lib.sh`, i.e. LAST in the list, matching `install.sh:143` `lib_names` actual order (… usb-lib share-lib menu-lib registry).
|
||||
|
||||
b) New section `## lib/registry.sh — tool metadata query API` inserted between `lib/menu-lib.sh` and the features sections (install order), in the neighbors' File/Purpose/Sourced-by dense-paragraph format. Claims grounded: reg_scan LC_ALL=C + lazy (bin/pos:70 comment), key/category convention, six fields, config-scope helpers, reg_each cb(cat,key,desc) contract, reg_tool_exists, guarded log/warn/err "like lib/config-ui.sh" (:18-21 says it verbatim), no shebang/not executable/installed 644. Consumers listed as bin/pos-tree + bin/pos `_pos_category_help()` only; gen-docs.sh explicitly noted as parsing independently (predates registry).
|
||||
|
||||
c) TOC — added `- [lib/registry.sh — tool metadata query API](#libregistrysh--tool-metadata-query-api)` in matching format. Sweep-driven extra (unambiguous staleness INSIDE this item file, authorized by sweep clause): TOC was also missing rows for five sections that already existed in the body (entertainment-lib, user-timers-lib, usb-lib, share-lib, menu-lib) — added those five too, anchors derived by the same slug pattern as existing entries.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 4: MAINTENANCE.md — judgment call
|
||||
|
||||
Structure read: Phase 0 authority order / Baseline / Checklist matrix / Lint results / Findings tickets (M-001..M-023, all resolved 2026-08-14) / Deep-dive notes / Next-session brief / Checked & clean. **No phase log or conventions-summary section exists**, so per the brief's escape clause a new section was NOT invented.
|
||||
|
||||
However the file is not frozen: its own header instructs "Keep the structure below; **append findings as they land**", and its native dated-record mechanism is exactly one ticket entry ending `Fix (date): …`. Docs-category findings belong under `### P2 — doc drift / consistency / style`. → Added ONE entry, ticket M-024, in the established field set, with the required content: template documents the optional headers (pre-existing), registry.sh added as shared query API, consumers migrated (pos-tree, bin/pos category help), lint unchanged. Nothing else in the file touched.
|
||||
|
||||
Deliberately NOT changed: the Checklist line enumerating `# POS_FLAGS:`/`# POS_SUBCMDS:`/`# POS_CONFIG:` — it is a historical snapshot of the audit-time convention matrix ("from AGENTS.md … DEV.md" at audit date), and rewriting completed-audit history isn't standard restoration; M-024 records the current state instead.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 5: Sweep check + gates
|
||||
|
||||
Sweep (grep over `DOC/*.md`, `AGENTS.md`, `README.md`, `MAINTENANCE.md` for stale header/consumer claims):
|
||||
|
||||
**Findings — outside the fence, reported only, NOT touched:**
|
||||
1. `DOC/AGENT_Context_Project.md:548` ("Adding a New Tool" step 2, hand-maintained row) — header enumeration lists only `# POS_FLAGS:`/`# POS_SUBCMDS:`; missing both new optional headers.
|
||||
2. `DOC/AGENT_Context_Project.md:598` (hand-maintained registry.sh filetable row) — says "used by `pos-tree` and `gen-docs.sh`"; **false for gen-docs.sh**, which parses the headers itself (`scripts/gen-docs.sh:47-48`) and never sources the lib (its only "registry" hit is a comment naming the `# POS_CONFIG:` concept). Line count 199 is still correct. This is exactly the "each consumer parses headers itself"-class claim the sweep was told to hunt.
|
||||
3. `DOC/POS.md:498` — minor: describes `pos tree` data as derived from filenames + `# POS_SUBCMDS:` headers annotated with descriptions; incomplete (no deps annotation / registry mention) but not unambiguously false → left.
|
||||
|
||||
**Finding inside the fence — deliberately not fixed:** `MAINTENANCE.md:37` checklist matrix enumerates only the three original headers. It is a historical snapshot of the completed 2026-08-14 audit's convention matrix; current state is recorded by M-024 instead of rewriting audit history.
|
||||
|
||||
**In-fence staleness fixed under the sweep clause:** SCRIPTS.md TOC missing 5 pre-existing lib sections (done in Step 3c).
|
||||
|
||||
Clean: DOC/DEV.md (already fully updated with headers + registry), README.md and DOC/README.md (no lib/header enumerations), howto/*.
|
||||
|
||||
## Gates (tails)
|
||||
|
||||
```
|
||||
$ make gen && git diff --stat → gen-docs: write OK; diff = my 3 docs + AgentsReport +
|
||||
pre-existing uncommitted registry-feature work
|
||||
(bin/*, templates/, scripts/gen-docs.sh, install.sh,
|
||||
AGENT_Context) — zero drift introduced by this session
|
||||
$ make check → check-sync: OK
|
||||
$ make lint → 0 FAIL, 0 WARN (convention lint)
|
||||
$ grep -n "POS_DEPS" … → AGENTS.md:17,18 · DOC/DEV.md:131,134 · DOC/SCRIPTS.md:241 ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Status: MAINTENANCE_COMPLETE
|
||||
|
||||
Maintenance objective:
|
||||
Sync the three agent/convention docs that drifted when the command-registry feature landed; code + `# POS:` headers are ground truth.
|
||||
|
||||
Findings addressed:
|
||||
AGENTS.md Tool-model + Categories bullets (fixed); DOC/SCRIPTS.md Phase-2 lib list + new registry section + TOC (fixed, incl. 5 pre-existing TOC omissions); MAINTENANCE.md record (added as ticket M-024 via the file's own append-findings mechanism).
|
||||
|
||||
Standard enforced:
|
||||
Authority order per MAINTENANCE.md Phase 0 — code + `# POS:` headers ground truth; template/DEV.md codified convention; doc drift in SCRIPTS = doc bug.
|
||||
|
||||
Files changed:
|
||||
AGENTS.md · DOC/SCRIPTS.md · MAINTENANCE.md · AgentsReport/maintainer/2026-08-26_registry-docs-sync.md (nothing else)
|
||||
|
||||
Verification performed:
|
||||
make gen (idempotent, no new drift) · make check OK · make lint 0 FAIL / 0 WARN · POS_DEPS grep present where expected · diffs reviewed line-by-line against intended edits.
|
||||
|
||||
Records updated:
|
||||
This report; MAINTENANCE.md M-024.
|
||||
|
||||
Scope compliance:
|
||||
In-scope corrections + one explicitly-reasoned sweep fix (SCRIPTS.md TOC) inside an item file; out-of-scope changes: none (3 sweep findings reported, untouched).
|
||||
|
||||
Remaining / deferred items:
|
||||
AGENT_CONTEXT :548/:598 + POS.md:498 need a follow-up pass (outside this fence); MAINTENANCE.md:37 checklist left historical by design.
|
||||
|
||||
Recommended next agent:
|
||||
Reviewer — independent adversarial review of the three doc edits before commit (per constraints, nothing is committed).
|
||||
|
||||
Changes made by Maintainer:
|
||||
Smallest corrective doc changes only; no code, no templates, no GEN blocks, no completions.
|
||||
@@ -1,28 +0,0 @@
|
||||
# Maintainer Report — 2026-09-06 AGENT_TODO maintenance
|
||||
|
||||
## TL;DR
|
||||
|
||||
- Fixed stale `DOC/APPS.md 15→16` count in the llamacpp Done entry (filetable is now 18 after stabilization pass).
|
||||
- Added one new Done entry summarizing the full stabilization pass.
|
||||
- Both edits are uncommitted; `AGENT_TODO.md` is not part of `make gen/check/lint`.
|
||||
|
||||
## Step 1: Fix stale count in llamacpp Done entry
|
||||
|
||||
**Finding:** `AGENT_TODO.md:45` says `DOC/APPS.md 15→16` — the security-track work later raised the filetable to 18.
|
||||
**Status:** DONE
|
||||
**Change:** Replaced `DOC/APPS.md 15→16` with `DOC/APPS.md app-table row count updated (15→16 at the time; 18 after the 2026-09-06 stabilization pass)`.
|
||||
[DONE]
|
||||
|
||||
## Step 2: Add stabilization-pass Done entry
|
||||
|
||||
**Finding:** No Done entry summarizes the 17-point stabilization pass executed 2026-09-06.
|
||||
**Status:** DONE
|
||||
**Change:** Inserted a new `- **2026-09-06** — Stabilization pass …` entry at the top of the Done section (newest-last placement).
|
||||
[DONE]
|
||||
|
||||
## Verification
|
||||
|
||||
- `git diff --stat AGENT_TODO.md` → `4 insertions` (new-newest Done entry + the count substitution).
|
||||
- The diff shows exactly the two changed regions: `@@ -42,6 +42,10 @@ ## Done` — new entry inserted at top of Done, and the count substitution inside the llamacpp entry.
|
||||
- No other entries moved/reordered; Later/Next untouched; no commit made.
|
||||
[DONE]
|
||||
@@ -1,55 +0,0 @@
|
||||
# Maintainer Report — AGENT_TODO.md Done entry for AI cost/session-window work
|
||||
|
||||
**Date:** 2026-09-06
|
||||
**Mode:** single-file convention edit (AGENT_TODO.md only); no commit; no gates.
|
||||
|
||||
## TL;DR
|
||||
|
||||
Drift: the AI cost & session-window work (OpenRouter 402 + AI_SESSION_TURNS) was finished but had no **Done** entry — a one-edit-per-commit convention violation. Fix: added ONE dated **Done** bullet at the TOP of the Done section (2026-09-06, above the existing same-day entries), ~5-8 lines, style-matched to existing entries. Corrected, validated against diff (2 insertions), report written. All constraints honored: touched ONLY `AGENT_TODO.md`, ran no gates, made no commit.
|
||||
|
||||
## Step 1: Verify facts before writing
|
||||
|
||||
Read `AgentsReport/architect/2026-09-06_ai-cost-window-design.md` (decisions, defaults, lazy resolution, scope) and `AgentsReport/builder/2026-09-06_ai-cost-window.md` (implementation + F1 hardening + verification numbers). Cross-checked reviewer `2026-09-06_f1-hardening-recheck.md` → Status **ACCEPT** (after `2026-09-06_ai-cost-window-review.md` → **CHANGES_REQUIRED**, 1 REQUIRED + 1 NOTE) → supports "Reviewer ACCEPT (twice)" with the CHANGES_REQUIRED→fixed flow.
|
||||
|
||||
Confirmed the Done-section convention from existing entries (line 6 "newest last", bullet `- **YYYY-MM-DD** — <title> …` wrapping one paragraph).
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 2: Insert the Done entry
|
||||
|
||||
Added one bullet at the top of the `## Done` section (`AGENT_TODO.md:45`), placing it **above** the existing `pos ai alias` same-day entry (per brief: "at the TOP of the Done section … above the existing same-day entries"). Covers:
|
||||
|
||||
- 402 root cause (no `max_tokens` sent; OpenRouter pre-bills worst-case 131072 vs 4511 balance) + user request to bound session to last-5.
|
||||
- Architect decisions: `AI_MAX_TOKENS` (num, default 2048, cost cap) on OpenRouter `max_tokens` + Gemini `generationConfig.maxOutputTokens` (llamacpp skipped); `AI_SESSION_TURNS` (2 msgs/exchange; default 40 back-compat; 10 = last 5) resolved lazily in `session_push` (config loads after line-25 default); both registered in `bin/pos-ai` `# POS_CONFIG:` `@General` → `pos config ai`.
|
||||
- Reviewer hardening (CHANGES_REQUIRED→fixed): unguarded env → jq `tonumber` (0/-5/010/abc) — both providers + `session_push` now guard `^[1-9][0-9]*$` fallback-to-default.
|
||||
- Verified: fake-curl shim (16 provider-body + 12 session-window checks incl. 010-regression proof), gen idempotent, check OK, lint 0/0, test 17 files/299 green, Reviewer ACCEPT (twice). Tester round not run (user's call); permanent coverage = follow-up.
|
||||
|
||||
Line count: the bullet is ~6 wrapped display lines (single logical line / 2 added physical lines).
|
||||
|
||||
[DONE]
|
||||
|
||||
## Step 3: Validate the correction
|
||||
|
||||
`git diff --stat AGENT_TODO.md` → `1 file changed, 2 insertions(+)`; `git diff AGENT_TODO.md` shows only the one added logical line at the top of Done, no other files touched. Verified the entry is above the existing same-day `pos ai alias` entry and style-matches (bullet, `- **2026-09-06** —`, inline prose).
|
||||
|
||||
Per constraints: no gates run, no commit made.
|
||||
|
||||
[DONE]
|
||||
|
||||
## Handoff
|
||||
|
||||
Status: MAINTENANCE_COMPLETE (single-item task, fully satisfied)
|
||||
|
||||
Standard enforced: AGENT_TODO.md **Done** convention — move completed work into **Done** (dated) in the same-language form used by existing entries.
|
||||
|
||||
Files changed: `AGENT_TODO.md` (1 logical line added at top of Done).
|
||||
|
||||
Verification: `git diff --stat` / `git diff` — 2 insertions only; correct placement + style confirmed by read.
|
||||
|
||||
Scope compliance: in-scope (AGENT_TODO.md only) — no other files, no gates, no commit.
|
||||
|
||||
Recommended next agent: Orchestrator (this edit rides the imminent commit; no further Maintainer action).
|
||||
|
||||
Reason: nothing else remains in scope; the entry is ready to be committed with the AI cost/session-window work.
|
||||
|
||||
Changes made by Maintainer: one **Done** entry added at the top of the Done section.
|
||||
@@ -1,75 +0,0 @@
|
||||
# Maintainer — AGENT_TODO.md Done entry for 2026-09-06 AI-server round
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Finding:** AI-server breakage round (llamacpp install fallout) finished, but no
|
||||
AGENT_TODO Done entry captured it yet. Per repo convention ("move completed work
|
||||
into **Done** (dated one-line) in the same commit that finishes the work"), a
|
||||
commit is imminent and the edit must ride it. Added ONE new **Done** entry.
|
||||
- **Correction:** Added a single 2026-09-06 entry at the TOP of Done (newest-last
|
||||
placement), style matching existing entries, ~9 lines, condensing the
|
||||
Detective/Architect/Builder/Tester reports. All other content untouched.
|
||||
- **Validation:** `git diff --stat AGENT_TODO.md` = 1 file changed, 2 insertions.
|
||||
No gates run (file is not part of gen/check/lint).
|
||||
|
||||
## Step 1: Locate Done section + newest-last placement
|
||||
|
||||
Read `AGENT_TODO.md`. Done entries are newest-at-top within the section (e.g. the
|
||||
existing 2026-09-06 entries precede 2026-09-05/04/…). The new entry was inserted
|
||||
immediately after `## Done`, above the existing "Stabilization pass" entry.
|
||||
|
||||
## Step 2: Check the existing 2026-09-06 "Stabilization pass" entry for contradiction
|
||||
|
||||
Reviewed it (lines 45+). It records the 17-point audit's own scope (help-flag
|
||||
validation, security, tooling, config, tests at 12 files/179 checks). The new
|
||||
round found deeper defects (stderr version detection, SIGPIPE race, dir expansion,
|
||||
port mismatch, user-bus) that the pass did not claim to have covered. No statement
|
||||
in the stabilization entry asserts these were already fixed, so **no minimal
|
||||
adjustment is warranted** — the entries are complementary, not contradictory.
|
||||
Per the brief, left everything else untouched.
|
||||
|
||||
## Step 3: Add the new Done entry
|
||||
|
||||
Inserted one 2026-09-06 entry at the top of Done, ~9 lines, matching existing
|
||||
style (`- **YYYY-MM-DD** —` prefix, terse evidence-backed summary), condensing:
|
||||
|
||||
- Detective root cause (b10822 binary: `--version`→STDERR mg07 hidden by
|
||||
`2>/dev/null`; `printf|grep -q` SIGPIPE rc=141 flag race; `resolve_model`
|
||||
file-only vs downloader dirs; port 8080 vs 8088)
|
||||
- Architect DQ1-DQ6 (help-gated stays; no silent dir-expansion pick;
|
||||
`ensure_user_bus` pre-flight; `--no-unit` escape hatch; candidates narrowed;
|
||||
port pinned 8088; installer sanity)
|
||||
- Builder F1-F7 + F1 regex edge (`build 1.2.3`→`1`)
|
||||
- Tester (4 regression files + 3 fixture updates; 16 files / 269 checks)
|
||||
- Verification (gen idempotent, check OK, lint 0 FAIL/0 WARN, test 269/269,
|
||||
bash -n, git diff --check)
|
||||
- Post-fix user outcome (`XDG_RUNTIME_DIR` + linger → Option A or `--no-unit`)
|
||||
|
||||
[DONE]
|
||||
|
||||
## Validation
|
||||
|
||||
`git diff --stat AGENT_TODO.md`:
|
||||
|
||||
```
|
||||
AGENT_TODO.md | 2 ++
|
||||
1 file changed, 2 insertions(+)
|
||||
```
|
||||
|
||||
Only AGENT_TODO.md touched. No gates run (not part of gen/check/lint).
|
||||
|
||||
[DONE]
|
||||
|
||||
## Report status
|
||||
|
||||
Status: MAINTENANCE_COMPLETE
|
||||
|
||||
- Maintenance objective: add ONE Done entry for the AI-server round, newest-at-top,
|
||||
style-matched; adjust stale text only if it contradicts (it did not).
|
||||
- Files changed: AGENT_TODO.md (one entry added).
|
||||
- Verification: git diff stat above; content diff reviewed.
|
||||
- Scope compliance: in-scope corrections only (touched ONLY AGENT_TODO.md); no
|
||||
features, no new standards, no other files.
|
||||
- Remaining / deferred items: none for this objective. The edit rides the imminent
|
||||
commit made by the committer (I did NOT commit).
|
||||
- Recommended next agent: Orchestrator (commit + handoff).
|
||||
@@ -1,289 +0,0 @@
|
||||
# Maintainer Report — 2026-09-06 — POS CLI convention sweep (AI tools)
|
||||
|
||||
## TL;DR
|
||||
|
||||
- Objective: comprehensive convention/maintenance sweep over the POS CLI AI tooling
|
||||
after several AI-tool changes (`bin/pos-ai-llamacpp` forwarder, enhanced
|
||||
`bin/pos-ai-hf`, enhanced `bin/pos-ai-server`, `bin/pos-ai` shorthand,
|
||||
`bin/pos` INTERACTIVE_CMDS) + llamacpp doc drift. Restore the established
|
||||
standard; do not redesign.
|
||||
- Verified-clean (no changes needed): `bin/pos-ai-llamacpp` (shebang, strict-mode,
|
||||
`# POS:` style, `# POS_SUBCMDS:` = actual adapter support, mode 100755,
|
||||
`-h|--help`, exec body — byte-mirror of gemini), `bin/pos-ai-hf` (all headers vs
|
||||
implementation, 11 unique examples, no dupes), `bin/pos-ai-server` (all headers
|
||||
vs implementation; config keys already registered in the `ai` scope via
|
||||
`bin/pos-ai` POS_CONFIG, commit adf88cc), `bin/pos` (INTERACTIVE_CMDS entry
|
||||
format + lint expectation; usage EXAMPLES has no factual provider list).
|
||||
- Corrected 7 factual provider-list omissions (llamacpp is a real provider:
|
||||
`lib/ai-providers/llamacpp.sh` + wired in `bin/pos-ai` resolve/require/model
|
||||
paths): `bin/pos-ai` usage() ×2, `DOC/POS.md` AI_PROVIDER config row,
|
||||
`DOC/howto/ai.md` ×4 (adapter list, `--provider` backend list, backward-compat
|
||||
shorthand, "Available providers" table row).
|
||||
- Ledger: 1 dated Done entry added to `AGENT_TODO.md`.
|
||||
- Validation: `bash -n` all `bin/pos*` OK; `make gen` idempotent (byte-identical
|
||||
before/after — no header changes); `make check` → `check-sync: OK`;
|
||||
`make lint` → **0 FAIL, 0 WARN (convention lint)**; smoke:
|
||||
`pos-ai --help` shows llamacpp lists, `pos-ai llamacpp --help`/`providers`
|
||||
dispatch correctly to provider llamacpp.
|
||||
- No commit (per brief — Orchestrator integrates).
|
||||
|
||||
## Step 1: `bin/pos-ai-llamacpp` (new forwarder) — verify only
|
||||
|
||||
Evidence (`bin/pos-ai-llamacpp:1-7`):
|
||||
- `#!/usr/bin/env bash` + `set -euo pipefail` (lines 1-2) ✓
|
||||
- `# POS: ai llamacpp — Forward to pos ai --provider llamacpp (backward compat)`
|
||||
(line 3) — same style as `bin/pos-ai-gemini:3` / `bin/pos-ai-openrouter:3` ✓
|
||||
- `# POS_SUBCMDS: ask chat models sessions capture` (line 4) — mirrors gemini;
|
||||
verified against `lib/ai-providers/llamacpp.sh`: it implements
|
||||
`provider_generate` (drives `cmd_ask`/`cmd_chat`/`cmd_sessions`/`cmd_capture`
|
||||
in `bin/pos-ai`) and `provider_models_list` (drives `cmd_models`) — so all 5
|
||||
listed subcommands are supported by the adapter. No invented subcommands ✓
|
||||
- mode `755` (100755) via `stat` ✓
|
||||
- `-h|--help` case (line 6) present, same as gemini; no deps guards to order
|
||||
against ✓
|
||||
- exec forward body `exec pos ai --provider llamacpp "$@"` (line 7) ✓
|
||||
- Lint gate cross-check (`scripts/lint-conventions.sh`): shebang/strict-mode for
|
||||
all shell files; exec-bit for `bin/pos-*`; `# POS:` + em-dash; `-h|--help`
|
||||
regex; `uses_stdin` → forwarded tool reads stdin via `pos ai chat`, so
|
||||
`ai-llamacpp` must be in INTERACTIVE_CMDS (it is, see Step 5) — and the reverse
|
||||
lint rule (`INTERACTIVE_CMDS` entry needs matching executable `bin/pos-ai-llamacpp`)
|
||||
is satisfied.
|
||||
|
||||
No changes. Status: [DONE]
|
||||
|
||||
## Step 2: `bin/pos-ai-hf` (enhanced) — verify only
|
||||
|
||||
Evidence vs implementation:
|
||||
- `# POS: ai hf — …` (`bin/pos-ai-hf:3`) ✓
|
||||
- `# POS_FLAGS: --branch --gguf --list --output --quant --include --exclude --revision`
|
||||
(line 4) — every flag is parsed in the arg loop (lines 122-145: `--branch`,
|
||||
`--gguf`, `--list`, `--quant`, `--output`, `--include`, `--exclude`,
|
||||
`--revision`); no stale/duplicated flags ✓
|
||||
- `# POS_DEPS: curl jq` (line 5) — both `command -v` guards sit before `--help`
|
||||
(lines 18-19) ✓
|
||||
- `# POS_CONFIG: ai | ai.env | HF_TOKEN=secret:… | HF_DOWNLOAD_DIR=:…` (line 6) —
|
||||
script reads `HF_TOKEN` (lines 27, 167-168, 183-184) and `HF_DOWNLOAD_DIR`
|
||||
(lines 28, 164) ✓
|
||||
- `# POS_EXAMPLES:` — 11 unique lines (7-17), no duplicate lines; every example
|
||||
maps to an implemented subcommand (search / download variants / list / remove /
|
||||
info / files) ✓
|
||||
- usage() (lines 48-97) lists subcommands `search download list remove info files
|
||||
cache` — dispatch (lines 813-823) implements exactly those (`cmd_search`,
|
||||
`cmd_download`, `cmd_list`, `cmd_remove`, `cmd_info`, `cmd_files`, `cmd_cache`);
|
||||
all 8 flags documented in usage match the parse loop ✓
|
||||
|
||||
No changes (the 3 duplicate examples were already removed by the previous
|
||||
maintainer pass `2026-09-06_restore-cleanup.md`). Status: [DONE]
|
||||
|
||||
## Step 3: `bin/pos-ai-server` (enhanced) — verify only
|
||||
|
||||
Evidence vs implementation (full read of `bin/pos-ai-server`):
|
||||
- `# POS: ai server — llama.cpp local inference server (start, stop, status, models, logs)` (line 3) ✓
|
||||
- `# POS_SUBCMDS: start stop status models logs` (line 4) — dispatch (lines 605-613:
|
||||
`start`, `stop`, `status`, `models`, `logs`) matches exactly ✓
|
||||
- `# POS_FLAGS:` (line 5) — all 23 listed flags are parsed in the arg loop
|
||||
(lines 262-340: `--port --host --model --ctx --gpu --threads --gpu-layers
|
||||
--gpu-threads --tensor-split --n-gpu-layers --batch-size --ubatch-size
|
||||
--temperature --top-k --top-p --repetition-penalty --mmap --mlock --kv-cache
|
||||
--ctx-size --metrics --health --slots`); no extra/missing flags ✓
|
||||
- `# POS_DEPS: curl jq` (line 6) — both guarded before `--help` (lines 11-12) ✓
|
||||
- usage() (lines 174-233): commands, all flags, and the `LLAMACPP_*` config-key
|
||||
block (lines 222-228) match the implementation (reads `LLAMACPP_PORT/HOST/MODEL/
|
||||
CTX_SIZE/GPU_LAYERS/THREADS` from `~/.config/linux_post_install/ai.env`) ✓
|
||||
- POS_CONFIG: the tool itself has no `# POS_CONFIG:` header, but the `LLAMACPP_*`
|
||||
keys it reads are ALREADY registered in the `ai` scope by `bin/pos-ai:6`
|
||||
POS_CONFIG (committed `adf88cc fix: pos config ai splits llamacpp into its own
|
||||
section`), same env file. Adding a header here would duplicate the registration
|
||||
and is a feature add, not drift restoration → left untouched per "smallest safe
|
||||
change" + Do-not-redesign.
|
||||
|
||||
No changes. Status: [DONE]
|
||||
|
||||
## Step 4: `bin/pos-ai` (modified) — shorthand case OK; 2 stale usage() lines fixed
|
||||
|
||||
- New `llamacpp` dispatch case (`bin/pos-ai:701-704`):
|
||||
`llamacpp) exec "$0" --provider llamacpp "${args[@]}" ;;` — matches the
|
||||
gemini/openrouter forwarder semantics (`pos ai llamacpp <subcmd> … == pos ai
|
||||
--provider llamacpp <subcmd> …`); sits with the other parallel cases in the
|
||||
final `case "${cmd:-}"` dispatch, `*)` error still last. Verified behavior:
|
||||
`bin/pos-ai llamacpp --help` and `llamacpp providers` both resolve to provider
|
||||
llamacpp (`llamacpp configured (model: … ) ← active`).
|
||||
- STALE usage() provider lists — FACTUALLY WRONG about supported providers
|
||||
(llamacpp is wired: `lib/ai-providers/llamacpp.sh` exists; `bin/pos-ai`
|
||||
`resolve_key`/`require_key`/`resolve_model` handle `llamacpp` cases; POS_CONFIG
|
||||
line 6 already lists "gemini, openrouter or llamacpp"; POS.md row 72 lists
|
||||
llamacpp):
|
||||
- `bin/pos-ai:42` `AI assistant with pluggable providers (gemini, openrouter).`
|
||||
→ `AI assistant with pluggable providers (gemini, openrouter, llamacpp).`
|
||||
- `bin/pos-ai:59` `--provider <name> Provider to use (gemini|openrouter; default: gemini).`
|
||||
→ `--provider <name> Provider to use (gemini|openrouter|llamacpp; default: gemini).`
|
||||
- Line 81 (`AI_PROVIDER … (gemini|openrouter|llamacpp, default gemini)`) was
|
||||
already correct — untouched.
|
||||
- Headers: `# POS_SUBCMDS: ask chat sessions capture models providers` (line 4)
|
||||
vs dispatch (ask/capture/chat/models/providers/sessions) — same 6, complete ✓;
|
||||
`# POS_FLAGS: --provider --model --session --system --full --last --trust`
|
||||
(line 5) — all 7 parsed in the arg loop (lines 655-680) ✓; POS_CONFIG (line 6)
|
||||
includes llamacpp + `LLAMACPP_*` keys ✓.
|
||||
|
||||
Status: [DONE]
|
||||
|
||||
## Step 5: `bin/pos` (modified) — INTERACTIVE_CMDS verified; EXAMPLES verdict
|
||||
|
||||
- `ai-llamacpp` added to INTERACTIVE_CMDS (`bin/pos:269`) after `ai-openrouter`,
|
||||
byte-consistent with `ai-gemini`/`ai-openrouter` (space-separated in the same
|
||||
string). Lint gate (`scripts/lint-conventions.sh:174-180`) requires each entry
|
||||
to have a matching executable `bin/pos-$entry` — `bin/pos-ai-llamacpp` exists
|
||||
(100755) → the entry is required AND correct. The stdin rule
|
||||
(lint lines 162-167) is satisfied via the INTERACTIVE_CMDS registration
|
||||
(`pos ai llamacpp chat` reads stdin → must skip the logging tee pipe).
|
||||
- usage() EXAMPLES block (`bin/pos:201-204`): showcases `pos ai gemini …`, shows
|
||||
no factual provider list → no change (matches the Builder's judgment; adding a
|
||||
llamacpp line would be inconsistent with openrouter having none).
|
||||
|
||||
No changes. Status: [DONE]
|
||||
|
||||
## Step 6: `DOC/POS.md` — AI_PROVIDER row fixed; Builder's 3 edits verified
|
||||
|
||||
- Builder's 3 hand-edits verified consistent end-to-end:
|
||||
- line 58 file list now includes `bin/pos-ai-llamacpp` ✓
|
||||
- line 72 `--provider <name>` row `(gemini\|openrouter\|llamacpp)` ✓
|
||||
- line 82 backward-compat sentence includes `pos ai llamacpp` ✓
|
||||
- line 59 adapters list already includes `lib/ai-providers/llamacpp.sh` ✓
|
||||
- Remaining staleness fixed: line 90 config table
|
||||
`| AI_PROVIDER | no | gemini | Active provider (gemini\|openrouter) |`
|
||||
→ `Active provider (gemini\|openrouter\|llamacpp)` — llamacpp is a real provider
|
||||
adapter, and the row lists provider values for the other two, so llamacpp must
|
||||
be mentioned (brief's explicit criterion).
|
||||
- Also verified no duplicate/stale `pos ai hf` / `pos ai server` rows elsewhere
|
||||
in the section.
|
||||
|
||||
Status: [DONE]
|
||||
|
||||
## Step 7: `DOC/howto/ai.md` — 4 provider-list fixes (brief lines 29-30 + same root cause)
|
||||
|
||||
Same root cause as Step 6 (provider/shorthand lists omit llamacpp); fixed all
|
||||
instances minimally, facts from `lib/ai-providers/llamacpp.sh`:
|
||||
1. line 5 `pluggable provider adapters (gemini, openrouter)` →
|
||||
`(gemini, openrouter, llamacpp)`.
|
||||
2. lines 19-20 `--provider <name>` backend list `(gemini|openrouter;` →
|
||||
`(gemini|openrouter|llamacpp;` (wrap preserved).
|
||||
3. lines 29-30 backward-compat shorthand sentence (brief's named instance):
|
||||
`pos ai gemini` / `pos ai openrouter` → adds `pos ai llamacpp` as shorthand
|
||||
for `pos ai --provider llamacpp`.
|
||||
4. "Available providers" table (lines 134-137) gains a row:
|
||||
`| llamacpp | Local llama.cpp (OpenAI-compatible) | loaded on the running server | LLAMACPP_MODEL |`
|
||||
— facts verified: OpenAI-compatible `/v1/chat/completions` (llamacpp.sh:3,31),
|
||||
`provider_default_model()` reads the loaded model from the running server via
|
||||
`/v1/models` (llamacpp.sh:11-16), config key `LLAMACPP_MODEL`
|
||||
(llamacpp.sh:7 `PROVIDER_CONFIG:`).
|
||||
|
||||
Terse-but-correct statements left alone (e.g. line 3 "Gemini, OpenRouter, and
|
||||
more", line 10 openrouter-only example rows — the doc's example style, not
|
||||
factual provider enumerations).
|
||||
|
||||
Status: [DONE]
|
||||
|
||||
## Step 8: `bin/pos` usage() EXAMPLES — no factual provider list → no change
|
||||
|
||||
Verified `bin/pos:201-204`: the ai EXAMPLES show `pos ai gemini …` as a usage
|
||||
showcase only (openrouter has no line either) — no provider enumeration to
|
||||
update. Per brief, no change. Status: [DONE]
|
||||
|
||||
## Step 9: Global gates (`bash -n` / `make gen` / `make check` / `make lint`)
|
||||
|
||||
- `bash -n` across every `bin/pos*` file → all OK.
|
||||
- `make gen` → `gen-docs: write OK`; second run byte-identical (md5sums of
|
||||
`DOC/AGENT_Context_Project.md` + `completions/pos.bash` match the pre-edit
|
||||
baseline) → idempotent, no gen drift introduced (no `# POS_*` headers changed).
|
||||
- `make check` → `check-sync: OK`.
|
||||
- `make lint` → `0 FAIL, 0 WARN (convention lint)`.
|
||||
- Smoke: `bin/pos-ai --help` prints the corrected provider lists;
|
||||
`bin/pos-ai llamacpp --help` → usage exit 0; `bin/pos-ai llamacpp providers` →
|
||||
`llamacpp … ← active` (no "Unknown ai subcommand").
|
||||
|
||||
Status: [DONE]
|
||||
|
||||
## Step 10: `AGENT_TODO.md` ledger
|
||||
|
||||
- Added one dated Done one-liner (2026-09-06, convention sweep — llamacpp
|
||||
doc/usage sync) at the top of `## Done`, established style (newest batch at
|
||||
top; single entry this pass). Existing entries untouched.
|
||||
|
||||
Status: [DONE]
|
||||
|
||||
## Remaining drift found but NOT fixed (with reason)
|
||||
|
||||
- `DOC/POS.md:124` `pos ai server` "Flags:" line enumerates only 7 of the 23
|
||||
implemented flags (omits the 16 advanced flags from commit 0856b25); same for
|
||||
`DOC/POS.md:108` `pos ai hf` "Options:" line (omits `--include`/`--exclude`/
|
||||
`--revision`). This is the established terse-summary style of POS.md flag rows
|
||||
(both sections predate the enhanced work), the statements are correct as far as
|
||||
they go, and the sweep brief names only llamacpp-caused drift for docs — the
|
||||
tool usage() help documents all flags. Reason: out of named scope; fixing would
|
||||
require a doc-completeness decision (Architect/Writer), not drift restoration.
|
||||
- `bin/pos-ai-server` has no `# POS_CONFIG:` header. NOT a gap: the `LLAMACPP_*`
|
||||
keys it reads are already registered in the `ai` scope via `bin/pos-ai:6`
|
||||
POS_CONFIG (commit adf88cc), same env file `ai.env` — `pos config ai` already
|
||||
edits them. Adding a header would be a duplicate registration / feature add.
|
||||
- Untracked plan documents in the working tree (`AUDIT.md`, `AUDIT_TABLE.md`,
|
||||
`FINAL_SUMMARY.md`, `IMPLEMENTATION_PLAN.md`) and untracked
|
||||
`AgentsReport/maintainer/2026-09-06_restore-cleanup.md` +
|
||||
`AgentsReport/builder/*.md` reports — noted in Step 1 of the previous
|
||||
maintainer pass; Orchestrator decision (commit/delete), not this brief.
|
||||
|
||||
## Completion handoff
|
||||
|
||||
Status: MAINTENANCE_COMPLETE
|
||||
|
||||
Maintenance objective:
|
||||
- Convention sweep over the POS AI CLI tooling after the AI-tool changes;
|
||||
verify/fix conventions only, restore the established standard, do not redesign.
|
||||
|
||||
Findings addressed:
|
||||
- 7 factual provider-list omissions fixed (llamacpp omitted): `bin/pos-ai`
|
||||
usage() ×2, `DOC/POS.md` AI_PROVIDER row ×1, `DOC/howto/ai.md` ×4.
|
||||
- Verified clean (no changes): `bin/pos-ai-llamacpp`, `bin/pos-ai-hf`,
|
||||
`bin/pos-ai-server`, `bin/pos` INTERACTIVE_CMDS + EXAMPLES, all `# POS_*`
|
||||
headers vs implementations.
|
||||
|
||||
Standard enforced:
|
||||
- `templates/pos-tool.sh` convention + AGENTS.md header rules; provider list
|
||||
claims in docs/help must match the real adapters
|
||||
(`lib/ai-providers/{gemini,openrouter,llamacpp}.sh`); generated blocks only via
|
||||
`make gen`; `pos config` scope registry via `# POS_CONFIG:`.
|
||||
|
||||
Files changed (this pass):
|
||||
- `bin/pos-ai` (+2 lines: usage() provider lists)
|
||||
- `DOC/POS.md` (+1 row edit: AI_PROVIDER)
|
||||
- `DOC/howto/ai.md` (4 edits: adapter list, backend list, backward-compat, table row)
|
||||
- `AGENT_TODO.md` (+1 Done entry)
|
||||
- `AgentsReport/maintainer/2026-09-06_convention-sweep.md` (this report)
|
||||
|
||||
Verification performed:
|
||||
- `bash -n` all `bin/pos*` OK; `make gen` idempotent (byte-identical);
|
||||
`make check` → `check-sync: OK`; `make lint` → 0 FAIL, 0 WARN;
|
||||
smoke: llamacpp shorthand/usage paths live.
|
||||
|
||||
Records updated:
|
||||
- `AGENT_TODO.md` Done ledger (1 dated entry); this report.
|
||||
|
||||
Scope compliance:
|
||||
- In-scope corrections only; no behavior changes; no redesign; no commit.
|
||||
- Out-of-scope changes: none. `README.md`, `lib/ai-providers/*.sh`, `bin/pos`,
|
||||
`bin/pos-ai-hf`, `bin/pos-ai-server`, `bin/pos-ai-llamacpp` untouched by me
|
||||
(working-tree changes from Builder preserved).
|
||||
|
||||
Remaining / deferred items:
|
||||
- POS.md flag rows are terse partial enumerations (established style; tool
|
||||
usage() is complete) — noted, out of named scope.
|
||||
- Untracked plan docs + agent reports awaiting Orchestrator decision.
|
||||
|
||||
Recommended next agent:
|
||||
- Orchestrator
|
||||
|
||||
Reason:
|
||||
- All sweep items verified, gates green, docs synchronized; per the brief no
|
||||
commit was made — Orchestrator integrates and commits.
|
||||
|
||||
Changes made by Maintainer:
|
||||
- 7 minimal factual provider-list corrections (usage + docs) and 1 ledger entry.
|
||||
@@ -1,110 +0,0 @@
|
||||
# Maintainer Report — 2026-09-06 — restore-cleanup (README + pos-ai-hf dedupe + ledger)
|
||||
|
||||
## TL;DR
|
||||
|
||||
- Drift 1: working-tree `README.md` was overwritten with an internal optimization-plan document; must be restored to the committed user-facing README.
|
||||
- Drift 2: `bin/pos-ai-hf` has duplicated `# POS_EXAMPLES:` header lines (3 repeats of the last 3 examples) → duplicated rows in the generated dispatch table (`DOC/AGENT_Context_Project.md`).
|
||||
- Corrections: `git checkout -- README.md`; removed exactly 3 duplicate `# POS_EXAMPLES:` lines; regenerated + verified.
|
||||
- Validation: `make gen` idempotent, `make check` OK, `make lint` 0 FAIL / 0 WARN.
|
||||
- Ledger: added 3 dated Done entries to `AGENT_TODO.md` (09-05 hf parallel+advanced, 09-05 server advanced options, 09-06 llamacpp forwarder).
|
||||
- Scope compliance: README + `bin/pos-ai-hf` (3 lines) + `AGENT_TODO.md` + generated files touched; no behavior changes; no commit (per brief).
|
||||
|
||||
## Step 1: Restore `README.md`
|
||||
|
||||
- Finding: working-tree `README.md` was an internal optimization-plan document ("Linux_post_install - AI Hugging Face Tool Optimization"), not the committed user-facing README.
|
||||
- Evidence: `git diff README.md` showed the entire file replaced (committed 53 lines → plan doc).
|
||||
- Correction: `git checkout -- README.md`; verified `git diff --exit-code -- README.md` rc 0 and `git status --short` no longer lists README.md. First line restored: `# Linux_post_install — Personal Bootstrap & Homelab Toolkit`.
|
||||
|
||||
Status: [DONE]
|
||||
|
||||
## Step 2: Dedupe `bin/pos-ai-hf` `# POS_EXAMPLES:` headers
|
||||
|
||||
- Finding: lines 18–20 duplicated lines 15–17 verbatim (info / files / download-include-exclude), so the generated dispatch table showed each example twice.
|
||||
- Correction: removed exactly the 3 duplicate lines. `git diff bin/pos-ai-hf` shows only that deletion (3 lines, nothing else touched). 11 unique `# POS_EXAMPLES:` lines remain (7–17), order unchanged, `uniq -d` across them = 0.
|
||||
- Body (source `…`, deps guards, subcommands, usage) untouched.
|
||||
|
||||
Status: [DONE]
|
||||
|
||||
## Step 3: Regenerate + gate verification (`make gen` / `make check` / `make lint`)
|
||||
|
||||
- `make gen` → `gen-docs: write OK`.
|
||||
- `make check` → `check-sync: OK`.
|
||||
- `make lint` → **0 FAIL, 0 WARN (convention lint)**, rc 0.
|
||||
- Idempotency: second `make gen` run produced byte-identical `completions/pos.bash` + `DOC/AGENT_Context_Project.md` (md5sum compare OK) → no drift.
|
||||
|
||||
Status: [DONE]
|
||||
|
||||
## Step 4: Confirm dispatch table no longer duplicates examples
|
||||
|
||||
- `DOC/AGENT_Context_Project.md:286` (ai | hf dispatch row) now lists exactly 11 `pos ai hf` example bullets, each once.
|
||||
- Previously-duplicated rows counted across the generated doc: `Show repository information` = 1, `List repository files` = 1, `Download with include/exclude patterns` = 1 (were 2 each).
|
||||
|
||||
Status: [DONE]
|
||||
|
||||
## Step 5: Update `AGENT_TODO.md` Done ledger
|
||||
|
||||
- Added exactly 3 dated Done entries at the top of the `## Done` section (same placement practice as commit 2794122), ordered oldest→newest with the newest last per brief:
|
||||
1. **2026-09-05** — `pos ai hf` parallel downloads (`PARALLEL_DOWNLOADS=4`) + advanced features (`info`/`files`, `--include`/`--exclude`, `--revision`, refactored quant gate, cache stub) from commits 387f23f + 0856b25, with verification.
|
||||
2. **2026-09-05** — `pos ai server` advanced options from commit 0856b25 (GPU offload / batch / sampling / endpoints / memory flags, version awareness), with verification.
|
||||
3. **2026-09-06** — `pos ai llamacpp` forwarder + provider shorthand (`bin/pos-ai-llamacpp`, `bin/pos-ai` dispatch case, INTERACTIVE_CMDS, POS.md hand-edits), with verification.
|
||||
- No existing ledger lines deleted or rewritten. `git diff AGENT_TODO.md` = 6 insertions only.
|
||||
|
||||
Status: [DONE]
|
||||
|
||||
## Deferred / notes
|
||||
|
||||
- Pre-existing working-tree modifications from other agents (`bin/pos`, `bin/pos-ai`, `DOC/POS.md`, `completions/pos.bash`, `DOC/AGENT_Context_Project.md`, untracked `bin/pos-ai-llamacpp` + plan docs) were present before this task; untouched by this maintenance pass (Builder's scope, already green).
|
||||
- `AUDIT.md` / `AUDIT_TABLE.md` / `FINAL_SUMMARY.md` / `IMPLEMENTATION_PLAN.md` in the working tree look like the leftover optimization-plan documents; noted only (out of scope — could be removed by Orchestrator decision, not by this brief).
|
||||
- No commit made (per brief — Orchestrator integrates and commits after verification).
|
||||
|
||||
## Completion handoff
|
||||
|
||||
Status: MAINTENANCE_COMPLETE
|
||||
|
||||
Maintenance objective:
|
||||
- Restore the committed user-facing `README.md`; dedupe the repeated `# POS_EXAMPLES:` header lines in `bin/pos-ai-hf`; regenerate + verify all gates; ledger the completed AI-tool work in `AGENT_TODO.md`.
|
||||
|
||||
Findings addressed:
|
||||
- README.md overwritten in working tree → restored via `git checkout -- README.md`; `git diff --exit-code -- README.md` empty (rc 0).
|
||||
- `bin/pos-ai-hf` 3 duplicated `# POS_EXAMPLES:` lines (18–20 = repeats of 15–17) → removed exactly those 3; 11 unique examples remain, order unchanged.
|
||||
- Generated dispatch table `DOC/AGENT_Context_Project.md:286` now lists each example once (previously 2× for info/files/include-exclude rows).
|
||||
|
||||
Standard enforced:
|
||||
- Committed HEAD is authoritative for `README.md`; `# POS_EXAMPLES:` header rule (one line per example, no verbatim repeats) feeds `make gen` tables.
|
||||
|
||||
Files changed:
|
||||
- `README.md` (restored to HEAD, no diff)
|
||||
- `bin/pos-ai-hf` (−3 duplicate `# POS_EXAMPLES:` lines only)
|
||||
- `DOC/AGENT_Context_Project.md`, `completions/pos.bash` (regenerated via `make gen`)
|
||||
- `AGENT_TODO.md` (+3 Done entries)
|
||||
- `AgentsReport/maintainer/2026-09-06_restore-cleanup.md` (this report)
|
||||
|
||||
Verification performed:
|
||||
- `make gen` → write OK; second run byte-identical (md5sum match) → idempotent, no drift.
|
||||
- `make check` → `check-sync: OK`.
|
||||
- `make lint` → **0 FAIL, 0 WARN (convention lint)**, rc 0.
|
||||
- Dispatch table counts: `Show repository information` = 1, `List repository files` = 1, `Download with include/exclude patterns` = 1; total `pos ai hf` example bullets = 11.
|
||||
|
||||
Records updated:
|
||||
- `AGENT_TODO.md` Done section (3 dated entries, newest last within the batch; 6 insertions, no deletions).
|
||||
- Maintainer report (this file).
|
||||
|
||||
Scope compliance:
|
||||
- In-scope corrections only (`README.md`, 3 header lines in `bin/pos-ai-hf`, generated files, ledger, report). Out-of-scope changes: none. `bin/pos-ai`, `bin/pos-ai-llamacpp`, `bin/pos-ai-server` untouched per constraint.
|
||||
|
||||
Remaining / deferred items:
|
||||
- Plan-document files in the working tree (`AUDIT.md`, `AUDIT_TABLE.md`, `FINAL_SUMMARY.md`, `IMPLEMENTATION_PLAN.md`) — not part of this brief; Orchestrator may delete or commit them.
|
||||
- None from this maintenance objective.
|
||||
|
||||
Recommended next agent:
|
||||
- Orchestrator
|
||||
|
||||
Reason:
|
||||
- All corrections verified and gates green; per the brief no commit was made — the Orchestrator integrates and commits these changes.
|
||||
|
||||
Changes made by Maintainer:
|
||||
- `git checkout -- README.md` (restore committed standard).
|
||||
- Deleted 3 duplicate `# POS_EXAMPLES:` lines in `bin/pos-ai-hf`.
|
||||
- `make gen` regeneration (idempotent) of generated docs/completions.
|
||||
- 3 dated entries appended to `AGENT_TODO.md` Done section.
|
||||
- Report written to `AgentsReport/maintainer/2026-09-06_restore-cleanup.md`.
|
||||
@@ -1,219 +0,0 @@
|
||||
# Philosopher Analysis: Self-Describing Command Registry for POS
|
||||
|
||||
**Date:** 2026-08-26
|
||||
**Author:** Philosopher (big-pickle)
|
||||
**Status:** ANALYSIS_READY
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
**Purpose:** Make POS introspectable — a single source of truth for command metadata that feeds all consumers (tree, help, menu, config, dashboard) without duplication.
|
||||
|
||||
**Core tension:** The project already has a lightweight registry (POS headers + gen-docs.sh). The real value isn't building a new system, but **centralizing the consumption** of what exists. The risk is building a framework that's harder to maintain than the current scattered approach.
|
||||
|
||||
**Key insight:** This is not about making commands "self-describing" — they already are via `# POS:` headers. This is about making POS itself able to answer "what can I do?" as a unified system, not as 40+ disconnected scripts.
|
||||
|
||||
**Recommendation:** Build a **query API** over existing metadata, not a new registry framework. The soul is introspection, not abstraction.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: What Problem Actually Exists?
|
||||
|
||||
The current state is already a registry:
|
||||
|
||||
- Every tool declares `# POS: <cat> <cmd> — <desc>`
|
||||
- Many declare `# POS_SUBCMDS:` (subcommands/actions)
|
||||
- Many declare `# POS_FLAGS:` (flags for completion)
|
||||
- Some declare `# POS_CONFIG:` (config scopes)
|
||||
- `make gen` regenerates docs/completions from these headers
|
||||
- `pos tree` derives hierarchy from filenames + headers
|
||||
- `pos <category> --help` shows tool descriptions from headers
|
||||
|
||||
**What's missing is not a registry, but a shared query mechanism.** Today:
|
||||
|
||||
- `pos tree` reimplements header parsing (pos-tree does its own scan)
|
||||
- `pos <category> --help` reimplements header parsing (pos dispatcher does its own scan)
|
||||
- `make gen` reimplements header parsing (gen-docs.sh does its own scan)
|
||||
- `pos config` reimplements header parsing (config-ui.sh does its own scan)
|
||||
- A future dashboard would need to reimplement header parsing again
|
||||
|
||||
Each consumer independently reads the same files and extracts the same data. That's the duplication: **not the metadata itself, but the logic that reads it.**
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Why Not Just Good --help?
|
||||
|
||||
Good `--help` on each tool is necessary but insufficient. It serves:
|
||||
|
||||
- **Single-tool consumers** (a human running `pos network download --help`)
|
||||
- **No cross-cutting concerns** (can't list all tools, find dependencies, show unified menu)
|
||||
|
||||
A registry enables:
|
||||
|
||||
- **System-wide queries**: "What commands exist?" "What depends on aria2c?" "What has config?"
|
||||
- **Programmatic consumers**: Menu generation, dashboard aggregation, config discovery
|
||||
- **Single source of truth**: Add metadata once, appears everywhere (tree, help, menu, config)
|
||||
|
||||
The soul isn't "each tool describes itself" (that's already true). The soul is **"POS as a system can describe itself."**
|
||||
|
||||
---
|
||||
|
||||
## Step 3: The Soul — Introspection
|
||||
|
||||
What does a self-describing POS enable that wasn't possible before?
|
||||
|
||||
1. **POS can answer "what can I do?"** — not via scattered `--help` calls, but as a unified query. A menu, a dashboard, a Telegram bot, or a human can ask "show me everything" and get a coherent answer.
|
||||
|
||||
2. **POS can answer "what do I need?"** — dependencies are declared. A dashboard can show "these 3 tools won't work without aria2c." A menu can gray out unavailable tools.
|
||||
|
||||
3. **POS can answer "what can I configure?"** — config scopes are discoverable. A unified config UI can show all configurable tools without hardcoding them.
|
||||
|
||||
4. **Adding a new tool is one step** — write the tool, add its headers, done. No updating tree definitions, menu code, dashboard configs, help text in multiple places.
|
||||
|
||||
The soul is **making POS a self-aware system rather than a collection of scripts with documentation.**
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Risks of Over-Engineering
|
||||
|
||||
The spec explicitly warns against this: "Do NOT build a giant abstraction layer." But the risk is real:
|
||||
|
||||
1. **Framework trap**: Building `CommandBase`, `CommandFactory`, etc. that every tool must conform to. This would require rewriting 40+ tools and violating the "existing commands must keep working" principle.
|
||||
|
||||
2. **Metadata burden**: Requiring every tool to define every field (dependencies, examples, arguments). This breaks the "progressive metadata" principle — simple tools should stay simple.
|
||||
|
||||
3. **Staleness risk**: A separate registry file that must be kept in sync with the actual tools. This is worse than the current system where metadata lives with the implementation.
|
||||
|
||||
4. **Complexity creep**: The current system is simple (headers + gen-docs.sh). A "proper" registry could become a maintenance burden that's harder to understand than the problem it solves.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Minimum Viable Registry
|
||||
|
||||
The minimum that delivers real value:
|
||||
|
||||
1. **Centralized query API** — A single function (or small set) that parses all tool headers once and provides lookup/iteration. This eliminates the per-consumer reimplementation.
|
||||
|
||||
2. **Dependency metadata** — Add `# POS_DEPS:` header (optional) so the system can answer "what tools need aria2c?" This is genuinely useful for dashboards and health checks.
|
||||
|
||||
3. **Examples metadata** — Add `# POS_EXAMPLES:` header (optional) so help can show curated usage patterns, not just `--help` output.
|
||||
|
||||
4. **Shared consumption** — `pos tree`, `pos <category> --help`, `make gen`, and future consumers all use the same query API instead of reimplementing header parsing.
|
||||
|
||||
What this does NOT include:
|
||||
|
||||
- A separate registry file (metadata stays in tool headers)
|
||||
- Required fields beyond name/description
|
||||
- Abstract base classes or framework patterns
|
||||
- Rewriting existing tools
|
||||
|
||||
---
|
||||
|
||||
## Step 6: What Should NOT Be Built
|
||||
|
||||
Explicitly NOT:
|
||||
|
||||
1. **A separate registry database** — Metadata stays in tool headers (`# POS:` etc.). A separate file would create synchronization problems.
|
||||
|
||||
2. **Abstract base classes** — No `CommandBase`, `AbstractCommand`, `CommandFactory`. Tools are bash scripts, not OOP objects.
|
||||
|
||||
3. **Required metadata fields** — Tools should not need to define arguments, examples, or dependencies. These are optional enhancements.
|
||||
|
||||
4. **A new configuration language** — No YAML/JSON/TOML for tool metadata. The current comment-header format is simple and works.
|
||||
|
||||
5. **A monolithic registry manager** — No single large script that owns all metadata. The query API should be a small library (like `lib/common.sh`) that tools can source if needed.
|
||||
|
||||
6. **Migration pressure** — Existing tools should not need rewriting to "conform" to the registry. The registry should read what exists, not demand what should exist.
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Recommendations
|
||||
|
||||
### For the Architect
|
||||
|
||||
1. **Start with the query API** — Build a `lib/registry.sh` (or similar) that provides:
|
||||
- `registry_list` — list all tools with metadata
|
||||
- `registry_get <tool>` — get metadata for a specific tool
|
||||
- `registry_find <criteria>` — find tools by dependency, category, etc.
|
||||
- `registry_has_config` — list tools with config scopes
|
||||
|
||||
2. **Extend existing headers** — Add `# POS_DEPS:` and `# POS_EXAMPLES:` as optional headers. Don't require them.
|
||||
|
||||
3. **Refactor consumers** — Update `pos tree`, `pos <category> --help`, `make gen` to use the shared API instead of reimplementing parsing.
|
||||
|
||||
4. **Don't touch tool implementations** — Tools keep their current structure. The registry reads headers; it doesn't modify tools.
|
||||
|
||||
### For the Philosopher's Soul Check
|
||||
|
||||
The soul of this feature is **introspection** — making POS a self-aware system. If the implementation makes POS harder to understand, maintain, or extend, it has failed. If it makes adding new tools easier and cross-cutting queries possible, it has succeeded.
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Where does the query API live?** `lib/registry.sh`? Inside `bin/pos`? A standalone script that `make gen` calls?
|
||||
|
||||
2. **Should `pos tree` be refactored to use the API?** Or is the current implementation (scanning headers directly) simpler and sufficient?
|
||||
|
||||
3. **How much of the current `gen-docs.sh` logic should move to the registry API?** The script already parses headers — should it become the registry, or should the registry be separate?
|
||||
|
||||
4. **Is `# POS_DEPS:` the right header name?** Could be `# POS_REQUIRES:` or `# POS_NEEDS:` for clarity.
|
||||
|
||||
5. **Should examples be inline in headers or in a separate file?** Headers are terse; examples might be better in `DOC/howto/*.md` (which already exists).
|
||||
|
||||
---
|
||||
|
||||
## Assumptions
|
||||
|
||||
1. The current header format (`# POS:`, `# POS_SUBCMDS:`, etc.) is the right abstraction level. It's simple, proven, and works.
|
||||
|
||||
2. The problem is primarily about **consumption duplication**, not **metadata inadequacy**. The metadata exists; the shared access pattern doesn't.
|
||||
|
||||
3. The user wants this to be **incremental** — not a big-bang rewrite. The spec says "existing commands must keep working" and "incremental migration approach."
|
||||
|
||||
4. The soul is **introspection**, not **abstraction**. The goal is making POS self-aware, not building a framework.
|
||||
|
||||
---
|
||||
|
||||
## Decision Principles
|
||||
|
||||
When in doubt, ask:
|
||||
|
||||
1. **Does this make adding new tools easier?** If not, why build it?
|
||||
2. **Does this reduce duplication?** If it adds more code than it removes, it's over-engineering.
|
||||
3. **Does this maintain simplicity?** If a new contributor can't understand the registry in 5 minutes, it's too complex.
|
||||
4. **Does this serve the soul?** If it doesn't make POS more self-aware, it's not serving the purpose.
|
||||
|
||||
---
|
||||
|
||||
## Handoff Recommendation
|
||||
|
||||
**Status:** ANALYSIS_READY
|
||||
|
||||
**Discovery summary:** The project already has a lightweight registry (POS headers + gen-docs.sh). The real value isn't building a new system, but centralizing the consumption of what exists. The soul is introspection — making POS a self-aware system that can answer "what can I do?" as a unified query.
|
||||
|
||||
**Philosophy document:** Not created (this is a feature analysis, not a new project). The design spec already captures the purpose well.
|
||||
|
||||
**Key insights:**
|
||||
1. The problem is consumption duplication, not metadata inadequacy
|
||||
2. The soul is introspection, not abstraction
|
||||
3. The minimum viable registry is a shared query API over existing headers
|
||||
4. The risk is building a framework that's harder to maintain than the current system
|
||||
|
||||
**Open questions:**
|
||||
- Where does the query API live? (lib/registry.sh vs. inside bin/pos)
|
||||
- Should gen-docs.sh become the registry, or should it be separate?
|
||||
- Is the current header format the right abstraction level?
|
||||
|
||||
**Assumptions made:**
|
||||
- The current header format is the right abstraction level
|
||||
- The problem is consumption duplication, not metadata inadequacy
|
||||
- The user wants incremental change, not a big-bang rewrite
|
||||
|
||||
**Recommended next agent:** Architect
|
||||
|
||||
**Reason:** The philosophical analysis is complete. The purpose is clear: introspection via a shared query API. The Architect can now design the implementation details (where the API lives, how consumers refactor, what new headers to add).
|
||||
|
||||
**Changes made by Philosopher:**
|
||||
- Analysis written to `./AgentsReport/philosopher/2026-08-26_registry-purpose.md`
|
||||
@@ -1,254 +0,0 @@
|
||||
# Philosopher Analysis: "Hugging Face Downloader" in `pos ai`
|
||||
|
||||
**Date:** 2026-09-04
|
||||
**Author:** Philosopher (big-pickle)
|
||||
**Status:** ANALYSIS_READY
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
**Purpose:** Provide a one-command convenience wrapper for downloading AI models (and optionally datasets) from the Hugging Face Hub to local disk, integrated into the `pos ai` CLI category.
|
||||
|
||||
**Core tension:** The user said "hugging face downloader" — but the `pos ai` system is currently a cloud-only AI assistant (Gemini, OpenRouter). This feature is NOT a new AI provider adapter; it is a **utility tool** that downloads model files to the local filesystem. The tension is determining the right scope: thin wrapper vs. full HF client, and whether it should also support local inference or remain download-only.
|
||||
|
||||
**Key insight:** This feature belongs under `pos ai` because it manages AI assets (models), not because it performs inference. The soul is **convenience** — replacing `huggingface-cli download meta-llama/Llama-3-8B` with `pos ai hf download meta-llama/Llama-3-8B`.
|
||||
|
||||
**Recommendation:** Build a thin wrapper around `huggingface-cli` (or the HF Hub HTTP API via curl) with subcommands: `download`, `search`, `list`. Do NOT build a provider adapter, a model manager, or a local inference runner. Keep scope narrow.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: What Is Being Downloaded?
|
||||
|
||||
The "downloader" terminology strongly suggests **files from the Hugging Face Hub** — primarily AI models, but potentially datasets, Spaces artifacts, or individual files within repos.
|
||||
|
||||
Hugging Face Hub hosts:
|
||||
- **Models** (the primary use case): Llama, Mistral, Phi, Qwen, Gemma, etc. — downloaded as directories of safetensors/bin/ONNX files
|
||||
- **Datasets**: training/evaluation data in various formats
|
||||
- **Spaces**: Gradio/Streamlit apps (less likely download target)
|
||||
- **Individual files**: tokenizer configs, model cards, etc.
|
||||
|
||||
The most natural reading: **the user wants to download AI models** — either to run locally or to archive/inspect.
|
||||
|
||||
### Evidence
|
||||
- The user said "hugging face downloader" — not "hugging face provider" or "hugging face inference"
|
||||
- The toolkit has `pos media grab` (download media from URLs) — a pattern of "convenience downloaders" exists
|
||||
- No local inference tools exist in the toolkit yet (no ollama, llama.cpp integration)
|
||||
- No prior mention of Hugging Face, ollama, or local models exists in the codebase
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Why Under `pos ai`?
|
||||
|
||||
Three possible interpretations:
|
||||
|
||||
### A) New AI Provider Adapter (like Gemini/OpenRouter)
|
||||
**Verdict: NO.** Provider adapters implement a 4-function interface (`provider_name`, `provider_default_model`, `provider_generate`, `provider_models_list`) that provides inference via remote APIs. A "downloader" does not generate text — it downloads files. This is not an inference provider.
|
||||
|
||||
### B) Utility for Downloading Models to Run Locally
|
||||
**Verdict: MOST LIKELY.** The user has a homelab. Downloading models is the prerequisite for local inference (ollama, llama.cpp, vLLM, text-generation-webui). Even if local inference isn't implemented yet, downloading models is a meaningful standalone utility.
|
||||
|
||||
### C) File Management Tool for HF Repos
|
||||
**Verdict: POSSIBLE BUT UNLIKELY.** The user could want to download specific files (tokenizer, config, single shard) from a repo. This is a valid use case but secondary to downloading entire models.
|
||||
|
||||
### D) Integration of HF into the AI Workflow
|
||||
**Verdict: YES, but as a utility.** HF is an AI-adjacent service. Having `pos ai hf` in the CLI makes it part of the AI workflow without making it an inference provider.
|
||||
|
||||
**The correct framing:** This is a **convenience utility** that downloads AI-related files from Hugging Face. It belongs under `pos ai` because models are AI assets, not because it performs inference.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: What Problem Does This Solve?
|
||||
|
||||
**Primary problem:** Downloading models from Hugging Face requires knowing the HF CLI syntax, having Python/pip installed, and remembering repo IDs. On a Debian homelab, the user may not have Python set up, or may want a one-command experience.
|
||||
|
||||
**Secondary problem:** The user may want to discover models (search/browse) without opening a browser.
|
||||
|
||||
**Tertiary problem (future):** If local inference is added later, having a download tool is the prerequisite step.
|
||||
|
||||
### Who Benefits?
|
||||
- The user (single-user homelab toolkit — this is for personal use)
|
||||
- Anyone who SSHes into the box and wants a quick model download
|
||||
|
||||
### What Happens Without It?
|
||||
- The user manually runs `pip install huggingface_hub && huggingface-cli download <repo>`
|
||||
- Or manually downloads files via the HF web UI
|
||||
- The `pos ai` CLI doesn't know about local models at all
|
||||
|
||||
---
|
||||
|
||||
## Step 4: The Soul of This Feature
|
||||
|
||||
**The soul is convenience.** This feature makes downloading AI models from Hugging Face as easy as typing a single command. It follows the same philosophy as `pos media grab` — a thin wrapper around an external tool that adds discoverability and clean UX.
|
||||
|
||||
### What Makes This Feature Valuable?
|
||||
1. **Discoverability**: `pos ai hf --help` tells you what's available
|
||||
2. **Consistency**: Same CLI pattern as every other `pos` tool
|
||||
3. **Integration**: Part of the AI workflow, not a separate Python tool
|
||||
4. **Simplicity**: No need to remember HF CLI syntax or install Python packages manually
|
||||
|
||||
### What Does NOT Make This Feature Valuable?
|
||||
1. Reimplementing the HF Hub API in bash (the HF CLI already does this well)
|
||||
2. Building a full HF client with every feature
|
||||
3. Making this the entry point for local inference (that's a separate feature)
|
||||
4. Building a model management system (tracking versions, updates, disk usage)
|
||||
|
||||
---
|
||||
|
||||
## Step 5: What This Feature Should NOT Be (Anti-Patterns)
|
||||
|
||||
### NOT a Provider Adapter
|
||||
The HF Inference API exists (you can call HF models via their API), but "downloader" is not "inference." If the user wants HF as an inference provider, that's a separate feature (a new `lib/ai-providers/huggingface.sh`). Do NOT conflate downloading with inference.
|
||||
|
||||
### NOT a Full HF Client
|
||||
The Hugging Face Hub has extensive features: model cards, discussions, versioning, branches, permissions, gated models, OAuth, organization management. A "downloader" should NOT try to replicate all of this. It should download files.
|
||||
|
||||
### NOT a Model Manager
|
||||
Tracking which models are installed, their sizes, versions, update status — that's a model management system. A "downloader" downloads. If model management is needed later, it can be a separate tool.
|
||||
|
||||
### NOT a Local Inference Runner
|
||||
Downloading a model is not the same as running it. The user may use ollama, llama.cpp, vLLM, or something else. The downloader should not assume or dictate the inference runtime.
|
||||
|
||||
### NOT a Dataset Tool (Initially)
|
||||
While HF hosts datasets, the primary use case for "hugging face downloader" in an AI context is models. Datasets can be a later addition.
|
||||
|
||||
### NOT a Python Dependency Nightmare
|
||||
If the tool requires `huggingface_hub` Python package, that's a heavy dependency for a bash toolkit. Prefer the HF Hub HTTP API via `curl` (the Hub has a REST API) or wrap `huggingface-cli` only if it's already installed.
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Recommended Scope
|
||||
|
||||
### What to Build
|
||||
|
||||
**Core tool:** `bin/pos-ai-hf` (category: `ai`, command: `hf`)
|
||||
|
||||
**Subcommands:**
|
||||
1. **`download <repo-id> [filename]`** — Download a model (or specific file) from HF Hub
|
||||
- Default: download the full repo to `~/.local/share/linux_post_install/hf/<repo-id>` (or configurable)
|
||||
- `--output <dir>` — custom download directory
|
||||
- `--revision <branch/tag>` — specific version
|
||||
- Progress display (if terminal)
|
||||
- Size estimate before download
|
||||
|
||||
2. **`search <query>`** — Search HF Hub for models (optional, nice-to-have)
|
||||
- Uses HF API: `https://huggingface.co/api/models?search=<query>`
|
||||
- Displays results: model ID, downloads, likes, last updated
|
||||
|
||||
3. **`list`** — List downloaded models (optional, nice-to-have)
|
||||
- Scans the download directory
|
||||
- Shows: repo ID, size on disk, last downloaded
|
||||
|
||||
**Dependency handling:**
|
||||
- If `huggingface-cli` is installed, wrap it (thin wrapper)
|
||||
- If not, use the HF Hub REST API via `curl` + `jq` (no Python dependency)
|
||||
- Config: `pos config ai` gains `HF_TOKEN` (for gated models) and `HF_DOWNLOAD_DIR`
|
||||
|
||||
### What to Leave Out
|
||||
|
||||
- **Provider adapter** (HF Inference API) — separate feature if needed
|
||||
- **Model management** (versions, updates, disk cleanup) — separate feature
|
||||
- **Local inference integration** (ollama, llama.cpp) — separate feature
|
||||
- **Dataset downloading** — can be added later
|
||||
- **Spaces downloading** — can be added later
|
||||
- **Gated model access flows** — handle token auth, but not the full gated model UX
|
||||
- **Model card rendering** — use HF web UI for browsing
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Architecture Considerations
|
||||
|
||||
### Where Does This Tool Live?
|
||||
|
||||
Option A: **`bin/pos-ai-hf`** — a standalone tool under `pos ai`
|
||||
- Pros: follows existing pattern, auto-discovered, has its own `# POS:` header
|
||||
- Cons: another file in `bin/`
|
||||
|
||||
Option B: **Subcommand of `bin/pos-ai`** — add `hf` to the existing AI tool
|
||||
- Pros: keeps AI tools together
|
||||
- Cons: `bin/pos-ai` is already 692 lines; adding download logic makes it larger
|
||||
|
||||
**Recommendation:** Option A — `bin/pos-ai-hf` as a standalone tool. It's a utility, not an inference command. Keeping it separate follows the project's "one tool, one responsibility" pattern.
|
||||
|
||||
### Backend: HF CLI vs. REST API
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
|----------|------|------|
|
||||
| Wrap `huggingface-cli` | Full HF feature support, handles auth/gating | Python dependency, slower startup |
|
||||
| HF REST API via curl | No Python dependency, fast, bash-native | Must reimplement download logic, handles less edge cases |
|
||||
|
||||
**Recommendation:** Support both — try `huggingface-cli` first (if installed), fall back to curl-based download. This matches the project's pattern of graceful degradation.
|
||||
|
||||
### Download Location
|
||||
|
||||
Default: `~/.local/share/linux_post_install/hf/<org>/<model>` (following XDG conventions and the project's data directory pattern).
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Does the user want to run models locally?** This affects whether the tool should also support loading models into ollama/llama.cpp after download. If yes, that's a larger scope.
|
||||
|
||||
2. **Does the user have Python/huggingface-cli installed?** This determines whether to depend on the CLI or build a curl-based alternative.
|
||||
|
||||
3. **Should this also work for datasets?** The user said "hugging face downloader" — models are the primary case, but datasets are possible.
|
||||
|
||||
4. **How important is gated model support?** Some models (like Llama) require accepting a license agreement. Should the tool handle this flow?
|
||||
|
||||
5. **Should the tool show download progress?** HF CLI shows a progress bar. A curl-based tool would need to compute and display progress manually.
|
||||
|
||||
---
|
||||
|
||||
## Assumptions
|
||||
|
||||
1. The user primarily wants to download **models**, not datasets or Spaces.
|
||||
2. The user values **convenience** over feature completeness.
|
||||
3. The user may not have Python installed — the tool should work without it if possible.
|
||||
4. The download directory should be configurable but have a sensible default.
|
||||
5. This is a **standalone utility**, not the beginning of local inference support (that can come later).
|
||||
6. The HF Hub REST API is sufficient for the core use case (downloading public models).
|
||||
|
||||
---
|
||||
|
||||
## Decision Principles
|
||||
|
||||
When in doubt, ask:
|
||||
|
||||
1. **Does this serve convenience?** If the tool requires more setup than just using `huggingface-cli` directly, it has failed.
|
||||
2. **Is this download-only?** If the scope creeps into inference, model management, or HF web features, pull back.
|
||||
3. **Does this follow existing patterns?** It should feel like `pos media grab` for AI models — same UX philosophy.
|
||||
4. **Is this bash-native?** Prefer curl/jq over Python dependencies. The toolkit is a bash toolkit.
|
||||
|
||||
---
|
||||
|
||||
## Handoff Recommendation
|
||||
|
||||
**Status:** ANALYSIS_READY
|
||||
|
||||
**Discovery summary:** The user wants a convenience tool for downloading AI models from Hugging Face, integrated into the `pos ai` CLI category. This is NOT a provider adapter — it's a utility that downloads files to local disk. The soul is convenience: one command to download a model instead of remembering HF CLI syntax.
|
||||
|
||||
**Philosophy document:** Not created (this is a feature analysis, not a new project). The purpose is clear enough for the Architect to proceed.
|
||||
|
||||
**Key insights:**
|
||||
1. This is a download utility, not an inference provider
|
||||
2. The soul is convenience — same philosophy as `pos media grab`
|
||||
3. Should wrap `huggingface-cli` with curl fallback
|
||||
4. Keep scope narrow: download + search + list
|
||||
5. Do NOT build: provider adapter, model manager, inference runner
|
||||
|
||||
**Open questions:**
|
||||
- Does the user want local inference integration?
|
||||
- Does the user have Python/huggingface-cli installed?
|
||||
- Should datasets be in scope?
|
||||
|
||||
**Assumptions made:**
|
||||
- Primary use case is downloading models
|
||||
- Convenience is the core value
|
||||
- Bash-native (curl/jq) is preferred over Python dependencies
|
||||
|
||||
**Recommended next agent:** Architect
|
||||
|
||||
**Reason:** The philosophical analysis is complete. The purpose is clear: a convenience download tool for HF models. The Architect can now design the implementation (tool structure, backend selection, download directory, auth handling).
|
||||
|
||||
**Changes made by Philosopher:**
|
||||
- Analysis written to `./AgentsReport/philosopher/2026-09-04_hf-downloader-purpose.md`
|
||||
@@ -1,286 +0,0 @@
|
||||
# Reviewer — R5 Chain Adversarial Review
|
||||
|
||||
**Date:** 2026-08-25
|
||||
**Reviewed work:** Four consecutive Builder passes (R5, R5b, R5c, R5d) on `bin/pos-ai-gemini` and docs
|
||||
**Approved scope:** tmp_request.md R5 — terse prompt + markdown rendering, --last + default session, answer separation + staleness transparency, machine context
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
**Status: ACCEPT_WITH_NOTES**
|
||||
|
||||
- 21 checklist items assessed: 18 PASS, 1 FAIL (duplicate line in ai.md doc), 1 WARN (chat REPL extra whitespace — cosmetic), 1 NOT-APPLICABLE (chat subcommands that never existed)
|
||||
- 1 BLOCKING finding: duplicate line in `DOC/howto/ai.md` (lines 135–138)
|
||||
- 0 regressions detected in non-tty output path or pre-R5 behavior
|
||||
- 0 scope creep into share-lib, menu-lib, cmd_unmount, or Telegram bridge
|
||||
- 3 unverified items (require `make gen`, `bash -n`, runtime testing — sandbox blocked)
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Scope compliance
|
||||
|
||||
**Files in R5 scope** (per approved scope):
|
||||
- `bin/pos-ai-gemini` — ✅ Changed, 311→565 lines
|
||||
- `DOC/howto/ai.md` — ✅ Changed
|
||||
- `DOC/POS.md` — ✅ Changed (ai section updated)
|
||||
- `completions/pos.bash` — ✅ Changed (GEN: block, `--full --last` added)
|
||||
- `DOC/AGENT_Context_Project.md` — ✅ Changed (line-count resync only)
|
||||
|
||||
**Out-of-scope files also modified in working tree** (from concurrent work, NOT caused by R5):
|
||||
- `bin/pos-docker-vbox`, `bin/pos-share-nfs-client`, `bin/pos-share-smb-client` — R1/R3/R6 work
|
||||
- `lib/common.sh` — R4 confirm convention
|
||||
- `DOC/DEV.md`, `DOC/howto/docker.md`, `DOC/howto/share.md` — doc sync for R3/R4/R6
|
||||
|
||||
These are concurrent uncommitted changes, not scope creep by R5.
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Rationale | All R5-scope files are modified; no R5-authored changes found in out-of-scope files |
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Default system prompt (R5)
|
||||
|
||||
| Check | Verdict | Evidence |
|
||||
|-------|---------|----------|
|
||||
| Terse clause present | PASS | Line 23: "Be extremely terse: lead with the exact command(s) to run; one-line explanations max; no greetings, no closing offers, no essays." |
|
||||
| Troubleshooting clause present | PASS | Line 23: "diagnose it from that and lead with the fix command(s)." |
|
||||
| Machine context appended dynamically | PASS | `cmd_ask` lines 411–414: `mc="$(machine_context)"; [ -n "$mc" ] && mc=" $mc"; system="$DEFAULT_SYSTEM_PROMPT$mc"` |
|
||||
|
||||
---
|
||||
|
||||
## Step 3: --system replaces wholesale
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | Lines 410–415: `system="$SYSTEM_PROMPT"` then `if [ -z "$system" ] && [ "$FULL_MODE" -eq 0 ]; then ...`. When `--system` sets `SYSTEM_PROMPT`, the condition `[ -z "$system" ]` is false → built-in prompt + machine context are never assembled. |
|
||||
|
||||
---
|
||||
|
||||
## Step 4: --full drops built-in
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | Line 411: `if [ -z "$system" ] && [ "$FULL_MODE" -eq 0 ]`. With `FULL_MODE=1`, the entire block is skipped → `system` stays as `SYSTEM_PROMPT` which defaults to `""`. No built-in or machine context injected. |
|
||||
|
||||
---
|
||||
|
||||
## Step 5: --last resolves + injects + announces
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | `newest_pos_log()` (lines 131–138) finds newest non-pos.log, verifies non-empty. `cmd_ask` (lines 398–403): calls `last_log_annotate` for stderr info, `last_log_context` for the tail, appends `[last command output:]` block. `last_log_annotate` (lines 171–185): prints `[i] attaching last pos output — <basename> (<age>)` + preview line to stderr. |
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Staleness >60min warning
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | Line 182: `if [ "$age_s" -gt "$LAST_LOG_STALE_SECS" ]` (LAST_LOG_SECS=3600). Prints `[!] that log is %s old and may not match your current problem...` to stderr with pipe-fresh hint. |
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Session "default" persists
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | Line 16: `SESSION="default"` (was `SESSION=""` in baseline). `session_load()` / `session_save()` no longer gated on `[ -n "$SESSION" ]` in ask/chat — always writes to `default.json`. |
|
||||
|
||||
---
|
||||
|
||||
## Step 8: --session overrides default
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | Flag parser lines 534–535: `--session) SESSION="$2"; shift 2`. Overrides the default before any session operation. |
|
||||
|
||||
---
|
||||
|
||||
## Step 9: Tty answer separation
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | `render_markdown` tty path (line 318): `printf '\n%s\n' "$rendered"` — exactly one leading blank line + content + one trailing newline. Non-tty path (lines 262–264): `printf '%s\n' "$text"` — raw bytes, zero added newlines. |
|
||||
|
||||
---
|
||||
|
||||
## Step 10: Markdown renderer coverage
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | Fenced blocks: line 280 toggle + line 281 dimmed indent. Inline code: lines 291–295 → yellow. Bold `**…**`: lines 298–302. Bold `__…__`: lines 305–309. Headers `#{1,4}`: lines 282–285 → bold cyan, # stripped. HR `---/***___`: line 287 → thin rule. List markers: no-op (passed through as-is, which is correct). glow: lines 313–314, `command -v glow >/dev/null 2>&1` — opportunistic. |
|
||||
|
||||
---
|
||||
|
||||
## Step 11: Machine context fallback chain
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | `machine_context()` lines 337–386: hostnamectl first (lines 340–356), os-release fallback for distro (lines 359–368), uname fallback for kernel+arch (lines 370–371). `[ -n "$out" ] \|\| return 0` on line 384 — clause omitted when all fail. `mc_clean()` strips ANSI, control chars, collapses whitespace. |
|
||||
|
||||
---
|
||||
|
||||
## Step 12: Non-tty stdout byte-identical
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | Old baseline `cmd_ask`: `printf '%s\n' "$out"`. New `cmd_ask`: `render_markdown "$out"` where non-tty path is `printf '%s\n' "$text"`. Byte-equivalent output format. The system prompt change affects what the model generates but not the print format — opt-in behavioral change within scope. |
|
||||
|
||||
---
|
||||
|
||||
## Step 13: Chat REPL commands intact
|
||||
|
||||
| Verdict | NOT-APPLICABLE |
|
||||
|---------|----------------|
|
||||
| Rationale | Checklist references /history, /export, /help — these never existed in either baseline or current code. Old code only had `/reset` and `q/Q/quit/exit`. Current code preserves both exactly (lines 441–446). No regression. |
|
||||
|
||||
---
|
||||
|
||||
## Step 14: Telegram listener bridge untouched
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | `git diff --name-only` shows no changes to communication listener/sender files. Telegram bridge calls `pos ai gemini ask` — the non-tty stdout path is byte-identical (check 12). |
|
||||
|
||||
---
|
||||
|
||||
## Step 15: Pipe stdin still works
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | Lines 392–394: `elif [ ! -t 0 ]; then prompt="$(cat)"`. Identical to baseline. |
|
||||
|
||||
---
|
||||
|
||||
## Step 16: POS_FLAGS complete
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | Line 5: `# POS_FLAGS: --model --session --system --full --last`. All five flags from checklist present. Completions line 6 matches: `_pos_flags[ai-gemini]="--model --session --system --full --last"`. |
|
||||
|
||||
---
|
||||
|
||||
## Step 17: make gen idempotency
|
||||
|
||||
| Verdict | UNVERIFIED |
|
||||
|---------|------------|
|
||||
| Rationale | Cannot run `make gen` in sandbox. Static evidence: completions posflags block (line 6) matches POS_FLAGS header; filetable row updated to 565 lines (confirmed by `wc -l`). The docker-vbox, nfs-client, smb-client, and completions line-count rows in the GEN block reflect concurrent work, not R5 drift. Strong inference: gen output would be byte-identical, but UNVERIFIED without running it. |
|
||||
|
||||
---
|
||||
|
||||
## Step 18: glow deps guard
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | Line 313: `if command -v glow >/dev/null 2>&1` — standard pattern, never errors when absent, falls back to awk renderer. No other reference to glow in the file. |
|
||||
|
||||
---
|
||||
|
||||
## Step 19: Lint convention compliance
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | Shebang: line 1 `#!/usr/bin/env bash`. Strict mode: line 2 `set -euo pipefail`. Help dispatch: line 529 `-h\|--help) usage ;;` in flag parse loop (before subcommand dispatch). No new external deps required (glow is optional). |
|
||||
|
||||
---
|
||||
|
||||
## Step 20: --last scoped to ask
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | Lines 554–556: `if [ "$LAST_MODE" -eq 1 ] && [ "${cmd:-}" != "ask" ]; then err "--last only applies to 'pos ai gemini ask'"`. Hard error for any other subcommand. |
|
||||
|
||||
---
|
||||
|
||||
## Step 21: Error→stderr, answer→stdout
|
||||
|
||||
| Verdict | PASS |
|
||||
|---------|------|
|
||||
| Evidence | `err()` from common.sh → stderr. `warn()` at line 450 → stderr. `gemini_generate` error echo at line 246 → `>&2`. `last_log_annotate` all output → `>&2`. `render_markdown` → stdout. API errors → stderr + exit 1. |
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1 — Duplicate line in DOC/howto/ai.md
|
||||
- **Severity:** REQUIRED
|
||||
- **Evidence:** Lines 135–138 in `DOC/howto/ai.md`:
|
||||
```
|
||||
- On a non-2xx response the API's `error.message` is shown and the exit code is
|
||||
non-zero — so scripts can rely on `ask` failing loudly.
|
||||
- On a non-2xx response the API's `error.message` is shown and the exit code is
|
||||
non-zero — so scripts can rely on `ask` failing loudly.
|
||||
```
|
||||
The diff confirms: the new line was appended but the old identical line was not removed.
|
||||
- **Relevant files/lines:** `DOC/howto/ai.md:135-138`
|
||||
- **Approved scope reference:** R5 doc updates to ai.md
|
||||
- **Why it matters:** Duplicate text is a doc bug — readers see the same bullet twice. Trivial to fix (delete one).
|
||||
|
||||
### Finding 2 — Chat REPL uses raw SYSTEM_PROMPT (no built-in default)
|
||||
- **Severity:** NOTE
|
||||
- **Evidence:** `cmd_chat` line 449: `gemini_generate "$model" "$contents" "$SYSTEM_PROMPT"` — passes the raw `SYSTEM_PROMPT` var. When no `--system` is given, this is `""` (empty), meaning chat gets NO system instruction — not even the terse prompt. This is consistent with the scope ("chat keeps its neutral behavior, only --system applies") and with `ai.md` line 39: "`chat` keeps its neutral behavior (only `--system` applies)."
|
||||
- **Why it matters:** Not a defect — explicitly documented behavior. But worth noting that chat and ask have asymmetric prompt treatment.
|
||||
|
||||
### Finding 3 — Chat REPL extra leading whitespace on tty
|
||||
- **Severity:** SUGGESTED
|
||||
- **Evidence:** `cmd_chat` lines 458–460: `printf '\n'; render_markdown "$answer"; printf '\n\n'`. On a tty, `render_markdown` emits `\n<rendered>\n`, so total after `> ` prompt close is: `\n` (from printf) + `\n` (render_markdown leading) + content + `\n` (render_markdown trailing) + `\n\n` (printf). This produces 2 leading blank lines and 3 trailing blank lines — more visual whitespace than the baseline's `printf '\n%s\n\n'`.
|
||||
- **Why it matters:** Cosmetic. The extra separation is arguably better for interactive readability. Non-blocking.
|
||||
|
||||
---
|
||||
|
||||
## Verification verified
|
||||
|
||||
1. ✅ Default system prompt has terse + troubleshooting clauses (line 23)
|
||||
2. ✅ Machine context function implements hostnamectl → os-release → uname fallback chain (lines 337–386)
|
||||
3. ✅ --system prevents built-in assembly (line 411 condition)
|
||||
4. ✅ --full prevents built-in assembly (line 411 FULL_MODE check)
|
||||
5. ✅ --last injects `[last command output:]` block (lines 398–403)
|
||||
6. ✅ --last announces to stderr with basename + age + preview (lines 171–185)
|
||||
7. ✅ Staleness warning at >60min (line 182, LAST_LOG_SECS=3600)
|
||||
8. ✅ Session default persists (SESSION="default", no conditional on load/save)
|
||||
9. ✅ Tty rendering: glow opportunistic + awk fallback, non-tty raw (lines 260–318)
|
||||
10. ✅ POS_FLAGS header has all 5 flags (line 5)
|
||||
11. ✅ Completions match POS_FLAGS (line 6)
|
||||
12. ✅ Filetable line count accurate (565 matches wc -l)
|
||||
13. ✅ --last scoped to ask only (lines 554–556)
|
||||
14. ✅ Errors to stderr throughout
|
||||
15. ✅ Pipe stdin preserved (lines 392–394)
|
||||
16. ✅ Telegram bridge path untouched (no file changes)
|
||||
17. ✅ Conventions: shebang, strict mode, help dispatch order
|
||||
|
||||
## Verification unverified
|
||||
|
||||
1. `make gen` byte-idempotency (sandbox cannot execute make)
|
||||
2. `bash -n` syntax check (sandbox cannot execute bash on non-git commands)
|
||||
3. Runtime test: `--last` with actual dispatcher logs, staleness warning timing, glow rendering
|
||||
|
||||
---
|
||||
|
||||
## Scope compliance
|
||||
|
||||
- **In-scope confirmed:** All R5/R5b/R5c/R5d features implemented in `bin/pos-ai-gemini` + docs
|
||||
- **Out-of-scope found:** None authored by R5. Working tree contains concurrent changes from R1/R3/R4/R6 work (separate feature branches mixed into working tree before commit)
|
||||
- **No regressions:** Non-tty output byte-equivalent, chat REPL preserved, stdin pipe preserved, Telegram bridge untouched
|
||||
|
||||
---
|
||||
|
||||
## Remaining uncertainty
|
||||
|
||||
1. Whether `make gen` output is byte-identical to committed GEN blocks — strong inference says yes (flag set matches header, line counts match), but unverified.
|
||||
2. Whether the awk markdown renderer handles all edge cases at runtime (nested bold inside code blocks, unclosed fences, etc.) — static review shows correct structure but edge-case behavior is runtime-only.
|
||||
|
||||
---
|
||||
|
||||
## Recommended next agent
|
||||
|
||||
**Builder**
|
||||
|
||||
**Reason:** One REQUIRED finding (duplicate doc line in `DOC/howto/ai.md`) is a trivial fix within approved scope — delete lines 137–138. The builder can resolve this and then the chain is ready for commit.
|
||||
|
||||
---
|
||||
|
||||
## Changes made by Reviewer
|
||||
|
||||
None — read-only review.
|
||||
@@ -1,165 +0,0 @@
|
||||
# Reviewer Report — Registry Phase 2 (dispatcher migration + metadata headers)
|
||||
|
||||
**Date:** 2026-08-26
|
||||
**Status:** ACCEPT_WITH_NOTES (gates pending Orchestrator execution)
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status:** ACCEPT_WITH_NOTES — Phase 2 delta is correct, faithful to the contract, and free of regressions by code inspection; 0 CRITICAL/HIGH defects.
|
||||
- **Defects:** 1 MEDIUM (stale hand-maintained 4-column table header left above the now self-headed dispatch GEN block, AGENT_Context_Project.md:275–276), 2 LOW (latent `//`-joiner URL ambiguity; pipe-substitution covers only spaced pipes), 4 NOTE/NIT.
|
||||
- **Gates:** ALL execution gates UNVERIFIED — sandbox deny-list blocks `bash -n`, `make *`, and running `bin/pos`. Static analysis supports every Builder claim; Orchestrator MUST run the gate battery before commit.
|
||||
- **Adversarial result on B:** the two gen-docs fixes are genuinely correct in their current form; however the "old paste bug" never existed at HEAD — Builder fixed its own uncommitted intermediate, not a pre-existing repo defect.
|
||||
- Maintainer overlap safe; Phase 1 leftovers cleanly attributed; no out-of-scope edits found.
|
||||
|
||||
---
|
||||
|
||||
## Review Inputs
|
||||
|
||||
- Contract: `AgentsReport/architect/2026-08-26_registry-architecture.md` (Decisions 2–6; Phase 2 = `_pos_category_help()` migration, `pos help` enrichment, representative headers)
|
||||
- Handoff under review: `AgentsReport/builder/2026-08-26_registry-phase2.md`
|
||||
- Prior accepted state: `AgentsReport/reviewer/2026-08-26_registry-review.md` (Phase 1, ACCEPT_WITH_NOTES); `AgentsReport/maintainer/2026-08-26_pos-ai-alias-registration.md`
|
||||
- Sandbox constraint: bash restricted to read-only git/grep-class commands — **execution gates cannot be re-run by this reviewer**; marked UNVERIFIED where applicable.
|
||||
|
||||
## Step 1: Diff attribution & scope compliance
|
||||
|
||||
Working tree = cumulative uncommitted Phase 1 + Maintainer + Phase 2 deltas (HEAD `4fd3c37`). Every entry attributed:
|
||||
|
||||
| git status entry | Attribution | Evidence |
|
||||
|------|-------------|----------|
|
||||
| M `bin/pos` | Phase 2 (dispatcher fn) + Maintainer (INTERACTIVE_CMDS line, uncommitted) | diff has exactly 2 hunks: `_pos_category_help()` rewrite; line 269 adds only `ai-alias` |
|
||||
| M `bin/pos-network-download` (+4), `bin/pos-media-sync` (+3), `bin/pos-system-backup` (+1), `bin/pos-docker-ps` (+1) | Phase 2 headers only | diff shows header lines only |
|
||||
| M `scripts/gen-docs.sh` (58±) | Phase 1 extension + Phase 2 fixes (single uncommitted delta) | HEAD has no examples field at all; see Step 3 |
|
||||
| M `DOC/AGENT_Context_Project.md` (124±) | Phase 2 gen output | tree/dispatch/filetable/docmap GEN blocks |
|
||||
| M `bin/pos-tree` (18±) | **Phase 1 leftover** — matches Phase 1 review items 5–6 byte-for-byte (source line 7, collection loop → reg_*) | `git diff` inspected |
|
||||
| M `install.sh` (2±) | **Phase 1 leftover** — lib_names += registry.sh (Phase 1 review item 10) | `git diff` inspected |
|
||||
| M `templates/pos-tool.sh` (+3) | **Phase 1 leftover** — template header docs (Phase 1 review item 9) | `git diff` inspected |
|
||||
| ?? `lib/registry.sh` | Phase 1 deliverable (199 lines; content matches Phase-1-reviewed landmarks: reg_scan:48, LC_ALL:51, restore:121–125, key=name:70) → **STRONG INFERENCE** unchanged since Phase 1 acceptance | read + compared to Phase 1 review citations |
|
||||
| ?? `AgentsReport/` | expected reports | — |
|
||||
|
||||
Constraint compliance (Architect Decision 5 + brief §E): `lib/config-ui.sh` ✅ untouched · `completions/pos.bash` ✅ untouched (absent from status) · `INTERACTIVE_CMDS` ✅ only the Maintainer's `ai-alias` token · `lib/registry.sh` ✅ no evidence of post-review change · `bin/pos-tree` ✅ not touched in Phase 2 · `usage()` EXAMPLES in bin/pos ✅ untouched. [STEP PASS]
|
||||
|
||||
## Step 2: Dispatcher migration correctness (`_pos_category_help()`)
|
||||
|
||||
**Verified by code inspection (bin/pos:68–130 new vs `git show HEAD:bin/pos` old):**
|
||||
|
||||
- Lazy load: `source "$self/../lib/registry.sh" 2>/dev/null || source "$self/registry.sh"` + `reg_scan "$self"` at bin/pos:71–72 — INSIDE the function only. [FACT]
|
||||
- Fast path clean: dispatch order is usage → `pos help` (exec redirect) → `_pos_category_exists` (glob-based, no registry) → registry touched ONLY when `n==1` or `n==2 && -h/--help` (bin/pos:254–258). `pos network download status` never sources registry. [FACT]
|
||||
- Key mapping: registry keys are full names (`network-download`, lib/registry.sh:70); display shortening via `files+=("${t#"$cat"-}")` (bin/pos:78). Cat derivation `${name%%-*}` / `""` (registry.sh:71–75) matches old glob-strip semantics for every shape incl. nested `share-usb-server` → `usb-server`, `docker-compose-yml` → `compose-yml`. Category-less tools (`config`, `tree`) have `cat=""` and can't reach the function (`_pos_category_exists` requires a `pos-<cat>-*` executable match, bin/pos:59–66). [FACT]
|
||||
- Desc equivalence: old inline `${d#*— }` ≡ registry-side strip at lib/registry.sh:83 (`_reg_desc[key]="${pos_line#*— }"`). Same em-dash-missing fallback behavior. [FACT]
|
||||
- Nested enrichment loop (bin/pos:92–105), `is_nested` skip (113–120), `printf ' %-28s%s\n'` layout: byte-identical to pre-migration. Only addition: deps line bin/pos:122. No double-listing path: nested tools still hidden via is_nested; dedupe via `sort -u` retained (bin/pos:80). Ordering: within a category all keys share the `<cat>-` prefix, so LC_ALL=C full-key sort order ≡ old short-name sort order (remainders compared over [a-z0-9-]). [FACT]
|
||||
- Empty-category robustness: unchanged guard chain; both old glob and new filter yield empty list identically; `mapfile` on empty input identical to before (pre-existing pattern). [FACT]
|
||||
- Maintainer overlap: INTERACTIVE_CMDS (bin/pos:269) retains `ai-openrouter ai-alias system-schedule` exactly as the Maintainer placed it. [FACT]
|
||||
|
||||
Notes: (a) category-help now hard-depends on lib/registry.sh existing (ugly failure if absent post-install — mitigated by install.sh lib_names incl. registry.sh, verified in Phase 1); (b) Builder reports reg_scan ≈1.3 s on this box — help-path latency cost, not a fast-path regression. [STEP PASS]
|
||||
|
||||
## Step 3: gen-docs.sh defect fixes (adversarial)
|
||||
|
||||
**Fix (a) — pipe→arrow in examples cells** (`sed 's/ | / → /g'` applied to the Examples cell only, rich branch of `gen_dispatch`):
|
||||
- Rendered output verified in the generated block: AGENT_Context:300 and :303 show valid 6-column rows with arrows; URLs keep their `//`. [FACT]
|
||||
- Column integrity today: grep over all 41 `# POS:` headers shows **no description contains a raw `|`**, and all current examples use spaced ` | ` per the grammar → no column breaks possible with present data. [FACT]
|
||||
- Field-splitting safety: `IFS='|' read -r … deps examples` puts remainder-pipes into the LAST var verbatim (bash read semantics) → multi-example strings survive re-parse. [FACT]
|
||||
- Latent gaps: (i) an example written `cmd|desc` without spaces bypasses the substitution and would break the table; (ii) descriptions are entirely unguarded. No live instance — LOW.
|
||||
- Cosmetic ambiguity: descriptions already contain literal `→` (`media sync — Incremental Music → USB sync`, matrix-listener `/command → bash`) — indistinguishable from substituted arrows. NOTE.
|
||||
|
||||
**Fix (b) — awk joiner replacing paste:**
|
||||
- Join logic correct: for L1..Ln emits `L1//L2//…//Ln`; single-example case emits L1 unprefixed; empty input yields "" because command substitution strips awk's `END{print ""}` trailing newline — critically this also prevents `_has_deps_examples` false positives on the 39 tools without POS_EXAMPLES. [FACT]
|
||||
- **Adversarial correction of the narrative:** HEAD's gen-docs.sh has NO paste joiner and NO examples field at all (5-field tools array). The `paste -sd'//'` bug existed only inside Builder's own uncommitted Phase 2 intermediate — it was never a repo defect. The fix itself is sound; the framing slightly inflates its provenance. [FACT via `git show HEAD:scripts/gen-docs.sh`]
|
||||
- New wart introduced by the chosen separator: example content itself contains `//` (`https://example.com/file.zip`, rendered at AGENT_Context:303), so the join delimiter is lossy/ambiguous for any future consumer splitting on `//`. Non-breaking today. LOW.
|
||||
- Side benefit: rich mode now emits a proper markdown header+separator row inside the block (resolves Phase 1 review Finding 3 for this state); the `_has_deps_examples=0` else-branch remains header-less (state-dependent format, deterministic either way). NOTE.
|
||||
- Determinism: pure functions of header text; `sort` runs under exported LC_ALL=C (gen-docs.sh:24). Idempotence logically guaranteed (same inputs → same bytes; docmap convergence loop pre-existing). Empirical double-run NOT executed — UNVERIFIED. [STEP PASS with findings]
|
||||
|
||||
## Step 4: Header accuracy vs reality (4 tools)
|
||||
|
||||
| Tool | Header | Actual guards (evidence) | Verdict |
|
||||
|------|--------|--------------------------|---------|
|
||||
| pos-network-download | `aria2c jq curl` + 3 EXAMPLES | Hard `err` guards lines 14–16 = aria2c, jq, curl; tmux conditional behind `--tmux` flag only (lines 282/312/351/451/483/587) → correctly excluded | ✅ ACCURATE |
|
||||
| pos-media-sync | `lsblk jq` + 2 EXAMPLES | Hard `err` guards lines 18–19 = lsblk, jq | ✅ ACCURATE |
|
||||
| pos-system-backup | `tar` only | tar hard `err` line 61; lsblk/jq warn-only "USB copy skipped" + `return 0` lines 73–74; gpg conditional `err` line 180 inside encryption branch (`--no-encrypt` bypasses) → minimal hard-dep declaration is defensible per deps=hard-requirement | ✅ ACCURATE |
|
||||
| pos-docker-ps | `docker` | Single hard guard line 17 | ✅ ACCURATE |
|
||||
|
||||
Examples cross-checked against each tool's usage(): network-download add/status/watch match usage lines 60/59/73; media-sync's two examples are verbatim its own usage() examples (lines 49–50), flags --mp3/--mp4/--dry-run all parsed at lines 207–209. No invented flags/subcommands. [FACT]
|
||||
|
||||
Header placement: all inserted after existing POS_* lines, before first code line (architect constraint satisfied). [FACT] [STEP PASS]
|
||||
|
||||
## Step 5: Maintainer overlap safety
|
||||
|
||||
- `INTERACTIVE_CMDS` (bin/pos:269): `ai-alias` present between `ai-openrouter` and `system-schedule`, exactly as Maintainer placed it; no other token changed (diff hunk = single-line insertion). [FACT]
|
||||
- `DOC/POS.md`: alias table rows + storage paragraph intact at lines 73–80 (committed at HEAD via `6566c83`; working tree does not touch POS.md). [FACT]
|
||||
- Builder's claim "Maintainer's fixes untouched" is supported. [STEP PASS]
|
||||
|
||||
## Step 6: Gates
|
||||
|
||||
| Gate | Builder claim | Reviewer verification |
|
||||
|------|---------------|----------------------|
|
||||
| `bash -n bin/pos` / `bash -n scripts/gen-docs.sh` | ✅ | UNVERIFIED — execution denied. Code inspection: no visible syntax hazards; new constructs (`while … done < <(…)`, herestrings, `%*s` with empty width) are valid bash. |
|
||||
| `make gen && git diff --exit-code` (idempotence) | ✅ | UNVERIFIED empirically. Logically sound: rendering is a pure function of headers under LC_ALL=C; docmap converges via existing loop. |
|
||||
| `make check` | ✅ OK | UNVERIFIED — execution denied. |
|
||||
| `make lint` | ✅ 0 FAIL, 0 WARN | UNVERIFIED — execution denied. |
|
||||
| `bin/pos --help` renders | ✅ | UNVERIFIED; usage()/_pos_category_list() unchanged from accepted state. |
|
||||
| `bin/pos network --help` shows `[deps: aria2c jq curl]` | ✅ | UNVERIFIED runtime; static trace supports it (deps populated bin/pos:88–89, printed :122 only when non-empty). |
|
||||
| `bin/pos-tree` annotations + shape | ✅ | UNVERIFIED runtime; pos-tree unchanged this phase (Phase 1 accepted). |
|
||||
| Fast-path timing ~26 ms unchanged | ✅ | UNVERIFIED. Static proof that fast path never sources registry: dispatch gate at bin/pos:254–258 admits only n==1 or n==2(-h/--help) into `_pos_category_help`; `pos network download status` (n=3) falls straight through to the exec loop. |
|
||||
|
||||
**Orchestrator must run the full battery in F before commit.** [STEP BLOCKED: sandbox]
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1 — Stale hand-maintained table header above the dispatch GEN block
|
||||
**Severity:** MEDIUM · **Certainty:** FACT
|
||||
**Evidence:** DOC/AGENT_Context_Project.md:275–276 (outside markers) still carry the old hand-written 4-column header `| Category | Command | Script | Description |` + separator; the generator now emits its own 6-column header inside the block (:278–279). The section renders as a dangling empty 4-column header immediately followed by a duplicate complete table.
|
||||
**Files:** DOC/AGENT_Context_Project.md:275–276; introduced by gen-docs.sh rich-branch header emission.
|
||||
**Scope ref:** Phase 2 changed gen_dispatch output format; the hand-maintained section around it was not synced (AGENTS.md: drift in hand-maintained doc areas is a doc bug).
|
||||
**Why it matters:** visible malformed doc structure for every agent/consumer reading §4; `make check` cannot catch it because the stale lines sit outside GEN markers — exactly the drift class this project's gates are blind to.
|
||||
**Fix:** delete lines 275–276 (2-line hand edit, then `make gen` no-op).
|
||||
|
||||
### Finding 2 — `//` join delimiter collides with URLs inside examples
|
||||
**Severity:** LOW · **Certainty:** FACT (collision), HYPOTHESIS (future impact)
|
||||
**Evidence:** awk joiner uses literal `//`; network-download example contains `https://example.com/file.zip` — both survive into the cell at AGENT_Context:303. No consumer splits on `//` today.
|
||||
**Why it matters:** any future parser that splits the Examples cell on `//` will mis-split URLs; a rarer sentinel or newline storage would be lossless.
|
||||
|
||||
### Finding 3 — Pipe protection only covers spaced pipes; descriptions unguarded
|
||||
**Severity:** LOW · **Certainty:** FACT (mechanism), no live instance
|
||||
**Evidence:** `sed 's/ | / → /g'` (gen-docs.sh, rich branch) converts only space-delimited pipes; a future `# POS_EXAMPLES: cmd|desc` or any description containing `|` emits a raw pipe into a markdown cell and shifts columns. All 41 current descriptions/examples are clean.
|
||||
**Why it matters:** silent table corruption on first offender; a lint WARN (architect Decision 4 envisioned one) would close it.
|
||||
|
||||
### Finding 4 — Arrow substitution cosmetically ambiguous
|
||||
**Severity:** NOTE · **Certainty:** FACT
|
||||
Descriptions legitimately contain `→` (`media sync`, matrix-listener) — indistinguishable from substituted example pipes. No structural impact.
|
||||
|
||||
### Finding 5 — "Defect fix" provenance: paste bug was Builder's own intermediate
|
||||
**Severity:** NOTE · **Certainty:** FACT
|
||||
HEAD's gen-docs.sh never contained the `paste -sd'//'` joiner (no examples field at all until the uncommitted delta). The fixes improve Builder's own in-flight code, which is fine — but the report reads as if pre-existing repo defects were repaired. Trust-calibration note only.
|
||||
|
||||
### Finding 6 — Stray untracked files in repo root
|
||||
**Severity:** NIT · **Certainty:** FACT
|
||||
`To`, `tmp_request.md`, `reportAgents/`, `opencode_helper/`, `Design-and-implement-a-self-describing-command-registry-for-POS.md` are untracked and unattributed by any report — likely Orchestrator/brief artifacts. Not Builder scope violations ("no new files except report" holds for its own output) but must be cleaned/ignored before commit.
|
||||
|
||||
### Finding 7 — Help-path latency + new lib dependency
|
||||
**Severity:** NOTE · **Certainty:** UNVERIFIED (measurement), FACT (dependency)
|
||||
Builder reports reg_scan ≈1.3 s on this box (vs architect's ~5 ms estimate) — borne only by `pos <cat> [--help]`. Also, category-help now hard-requires lib/registry.sh at runtime; installed layout is covered (install.sh lib_names includes registry.sh).
|
||||
|
||||
## Verdict
|
||||
|
||||
**ACCEPT_WITH_NOTES**
|
||||
|
||||
The Phase 2 delta does what the contract says: the dispatcher migration is provably behavior-preserving except for the intended `[deps: …]` lines; all four header annotations are accurate against actual guards with real-subcommand examples; the two gen-docs fixes render correctly on current data; Maintainer/Phase 1 work is untouched; scope is clean. Findings 1–3 are small follow-ups (Finding 1 should land before commit); none blocks acceptance of the code. Final acceptance is conditional on the Orchestrator running the gate battery this sandbox could not execute.
|
||||
|
||||
**Defect count:** CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 2 · NOTE/NIT 4
|
||||
|
||||
---
|
||||
|
||||
## Handoff
|
||||
|
||||
**Status:** ACCEPT_WITH_NOTES
|
||||
**Objective/problem:** verify Phase 2 (dispatcher migration, metadata headers, gen-docs fixes) against Architect Decisions 2–6 and the review brief.
|
||||
**Evidence/completed work:** Steps 1–6 above; every git-status entry attributed; static equivalence proof for `_pos_category_help()`; adversarial validation of both gen-docs fixes.
|
||||
**Affected areas:** bin/pos, scripts/gen-docs.sh, 4 tool headers, generated DOC blocks (+ pre-existing Phase 1/Maintainer working-tree deltas).
|
||||
**Scope/decision boundary:** all changes within approved Phase 2 scope; no out-of-scope edits found; Reviewer made no changes.
|
||||
**Verification performed:** full static analysis incl. HEAD-vs-worktree diffs, registry semantic checks, guard/header cross-checks, rendered-output inspection.
|
||||
**Remaining uncertainty:** all execution gates (bash -n, make gen/check/lint idempotence round-trip, runtime outputs, timing) UNVERIFIED — sandbox deny-list.
|
||||
**Recommended next agent:** Builder (2-line Finding 1 fix + optionally address Finding 2/3 hardening), then **Orchestrator** runs gates F and commits.
|
||||
**Reason:** defects found are within approved scope and mechanically trivial; nothing requires redesign or investigation.
|
||||
|
||||
Changes made by Reviewer: none (this report file only).
|
||||
@@ -1,206 +0,0 @@
|
||||
# Reviewer Report — Self-Describing Command Registry for POS
|
||||
|
||||
**Date:** 2026-08-26
|
||||
**Status:** ACCEPT_WITH_NOTES
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
The implementation is well-structured, internally consistent, and correctly scoped. One REQUIRED finding (documentation gap in `AGENT_Context_Project.md` installation flow) and two SUGGESTED findings remain. No BLOCKING issues. Gates could not be re-run in this sandbox; the builder's claims about gate results are plausible but UNVERIFIED by this review.
|
||||
|
||||
---
|
||||
|
||||
## Gate Results
|
||||
|
||||
| Gate | Builder Claimed | Reviewer Verified | Notes |
|
||||
|------|----------------|-------------------|-------|
|
||||
| `bash -n lib/registry.sh` | PASS | UNVERIFIED | Sandbox blocks execution; code inspection shows no syntax issues |
|
||||
| `bash -n scripts/gen-docs.sh` | PASS | UNVERIFIED | Sandbox blocks execution |
|
||||
| `bash -n bin/pos-tree` | PASS | UNVERIFIED | Sandbox blocks execution |
|
||||
| `make gen && git diff --exit-code` | PASS (zero drift) | STRONG INFERENCE | No tools have POS_DEPS/POS_EXAMPLES yet; gen_dispatch falls through to unchanged path; gen_tree only adds `[deps:]` when non-empty — both confirmed by code inspection |
|
||||
| `make check` | PASS | UNVERIFIED | Sandbox blocks execution |
|
||||
| `make lint` | 1 FAIL + 1 WARN (pre-existing) | UNVERIFIED | Builder cites pos-ai-alias issues, which are plausible given commit `9f289ba` |
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1: Documentation gap — installation flow missing `registry.sh`
|
||||
|
||||
**Severity:** REQUIRED
|
||||
**Certainty:** FACT
|
||||
|
||||
**Evidence:** `DOC/AGENT_Context_Project.md:211-213` lists the libs copied by Phase 2:
|
||||
```
|
||||
│ └─ Copies lib/*.sh (common, flags, notify, entertainment-lib,
|
||||
│ scheduler-lib, config-ui, user-timers-lib, entertainment-plugin-lib,
|
||||
│ usb-lib, share-lib, menu-lib) → /usr/local/bin/ (chmod 644)
|
||||
```
|
||||
This list does NOT include `registry.sh`. However, `install.sh:143` correctly includes `registry.sh` in the `lib_names` array. The `DEV.md` "Directory Layout" table at line 34 also does not mention `registry.sh` in its lib directory description.
|
||||
|
||||
**Relevant files/lines:** `DOC/AGENT_Context_Project.md:211-213`, `DOC/DEV.md:34`
|
||||
**Approved scope reference:** Arch Decision 5 approved "DOC/AGENT_Context_Project.md — Update line count table for lib/registry.sh"
|
||||
**Why it matters:** The installation flow description is a hand-maintained doc section. A new library was added to the install pipeline, but the description wasn't updated to reflect it. This creates documentation drift — the next agent reading this section would not know registry.sh is installed.
|
||||
|
||||
**Fix:** Add `registry` to the lib list on line 213 of AGENT_Context_Project.md and to the `lib/` directory description in DEV.md line 34.
|
||||
|
||||
---
|
||||
|
||||
### Finding 2: `gen-docs.sh` header comment not updated for new headers
|
||||
|
||||
**Severity:** SUGGESTED
|
||||
**Certainty:** FACT
|
||||
|
||||
**Evidence:** `scripts/gen-docs.sh:8-12` documents the source-of-truth headers:
|
||||
```bash
|
||||
# Sources of truth:
|
||||
# - bin/pos-* filenames → category, subcommand
|
||||
# - "# POS:" header line → one-line description
|
||||
# - "# POS_FLAGS:" line → flag completion list (flag-style tools only)
|
||||
# - "# POS_SUBCMDS:" line → subcommand completion list (multi-command tools)
|
||||
```
|
||||
Missing: `# POS_DEPS:` and `# POS_EXAMPLES:` are parsed at lines 45-46 but not documented in the header comment.
|
||||
|
||||
**Relevant files/lines:** `scripts/gen-docs.sh:8-12`
|
||||
**Why it matters:** The header comment is the first thing a developer reads when modifying the script. Omitting the new headers from the "Sources of truth" list could cause confusion.
|
||||
|
||||
---
|
||||
|
||||
### Finding 3: `AGENT_Context_Project.md` §4 "Available Commands" dispatch table lacks header row
|
||||
|
||||
**Severity:** NOTE
|
||||
**Certainty:** HYPOTHESIS
|
||||
|
||||
**Evidence:** When `_has_deps_examples=0`, `gen_dispatch()` (gen-docs.sh:88-93) outputs only data rows, no markdown table header. The committed dispatch block at `AGENT_Context_Project.md:273-315` also has no header row — the table starts directly with data rows. This means the dispatch table is a list of pipe-separated values rendered by GitHub/Gitea markdown as a table, but it technically lacks `|---|---|` separator lines.
|
||||
|
||||
**Why it matters:** This is pre-existing behavior, not introduced by this change. Noted for completeness. The architect's spec also shows no header rows in the dispatch table example. Markdown renderers may handle this gracefully, but strict parsers would not.
|
||||
|
||||
---
|
||||
|
||||
### Finding 4: Architect pseudocode key format ≠ implementation (resolves correctly)
|
||||
|
||||
**Severity:** NOTE
|
||||
**Certainty:** FACT
|
||||
|
||||
**Evidence:** The architect's pseudocode in `architect/2026-08-26_registry-architecture.md:272` uses `key="$sub"` (sub-only key, e.g., `download` for `pos-network-download`). The actual implementation at `lib/registry.sh:70` uses `key="$name"` (full key after `pos-`, e.g., `network-download`). However, the architect's "Tool Key Convention" section (lines 142-147) specifies `network-download` as the key — matching the implementation. The builder correctly followed the convention, not the pseudocode bug.
|
||||
|
||||
**Why it matters:** Not a defect, but the pseudocode and convention section of the same document contradict each other. Future reference to the pseudocode could cause confusion.
|
||||
|
||||
---
|
||||
|
||||
### Finding 5: Gate results not independently verifiable
|
||||
|
||||
**Severity:** NOTE
|
||||
**Certainty:** UNVERIFIED
|
||||
|
||||
**Evidence:** The sandbox environment blocks `bash -n`, `make gen`, `make check`, `make lint`, and any non-read-only bash commands. The builder's report claims all gates pass, and the code inspection supports these claims being plausible (no syntax errors visible, gen_dispatch backward-compatible when no tools have new headers, etc.). However, I cannot independently confirm them.
|
||||
|
||||
**Why it matters:** The Orchestrator should run the gates before accepting. The builder's claims are well-documented but technically unverified by this review.
|
||||
|
||||
---
|
||||
|
||||
## Scope Compliance
|
||||
|
||||
### In-Scope Items (all verified present in working tree)
|
||||
|
||||
| Item | Status | Evidence |
|
||||
|------|--------|----------|
|
||||
| `lib/registry.sh` — new file (199 lines) | ✅ PRESENT | `git status --short` shows `?? lib/registry.sh` |
|
||||
| `scripts/gen-docs.sh` — extended tools array, conditional columns, deps annotation | ✅ MODIFIED | `git diff HEAD -- scripts/gen-docs.sh` shows all expected changes |
|
||||
| `bin/pos-tree` — migrated to registry API | ✅ MODIFIED | `git diff HEAD -- bin/pos-tree` shows sed→registry migration |
|
||||
| `templates/pos-tool.sh` — documented new headers | ✅ MODIFIED | `git diff HEAD -- templates/pos-tool.sh` adds POS_DEPS/POS_EXAMPLES docs |
|
||||
| `install.sh` — added registry.sh to lib_names | ✅ MODIFIED | `git diff HEAD -- install.sh` shows registry.sh added to loop |
|
||||
| `DOC/DEV.md` — documented POS_DEPS and POS_EXAMPLES | ✅ MODIFIED | `git diff HEAD -- DOC/DEV.md` shows header docs and explanation |
|
||||
| `DOC/AGENT_Context_Project.md` — registry.sh in line-count table | ✅ MODIFIED | `git diff HEAD~1..HEAD` shows new row at line 594 |
|
||||
|
||||
### Out-of-Scope Items (verified NOT modified)
|
||||
|
||||
| Item | Status | Evidence |
|
||||
|------|--------|----------|
|
||||
| `bin/pos` dispatcher logic | ✅ NOT MODIFIED | Not in `git status --short` |
|
||||
| `completions/pos.bash` | ✅ NOT MODIFIED | Not in `git status --short` |
|
||||
| `lib/config-ui.sh` | ✅ NOT MODIFIED | Not in `git status --short` |
|
||||
| `# POS_DEPS:` / `# POS_EXAMPLES:` on existing tools | ✅ NOT ADDED | `grep -c 'POS_DEPS:' bin/pos-*` → 0 |
|
||||
| `scripts/lint-conventions.sh` | ✅ NOT MODIFIED | Not in `git status --short` (was optional in scope) |
|
||||
|
||||
### Scope Deviation
|
||||
|
||||
The architect's approved scope stated "install.sh changes — registry.sh is installed with existing lib/* loop" (Decision 5, "Not in scope"). The builder correctly identified that the `lib_names` loop lists files explicitly (not a glob), requiring an explicit addition. The `install.sh` modification was **necessary and correct** — the architect's scope was slightly inaccurate on this point.
|
||||
|
||||
---
|
||||
|
||||
## Verification Verified
|
||||
|
||||
1. **API function completeness:** All 10 declared API functions (`reg_scan`, `reg_list`, `reg_categories`, `reg_tools_in`, `reg_lookup`, `reg_config_scopes`, `reg_config_keys`, `reg_config_envfile`, `reg_each`, `reg_tool_exists`) are implemented in `lib/registry.sh`. (Lines 48, 129, 131, 150, 158, 172, 174, 179, 189, 197) — **FACT**
|
||||
|
||||
2. **No shebang:** `lib/registry.sh` has no shebang line — starts with a comment. — **FACT** (line 1)
|
||||
|
||||
3. **LC_ALL=C in reg_scan:** Set at line 51, restored at lines 121-125 (with save/restore pattern). — **FACT**
|
||||
|
||||
4. **Guarded fallbacks:** `log`, `warn`, `err` use the `declare -F` pattern matching `lib/config-ui.sh` at lines 19-21. — **FACT**
|
||||
|
||||
5. **pos-tree sources registry.sh:** Line 7 of `bin/pos-tree` adds the source line with fallback chain. — **FACT**
|
||||
|
||||
6. **pos-tree uses registry API:** Lines 48-74 replace direct sed with `reg_scan`/`reg_list`/`reg_lookup`. `add()` and `render()` are unchanged. — **FACT**
|
||||
|
||||
7. **gen-docs.sh backward compatibility:** When `_has_deps_examples=0` (which is true since no tools have POS_DEPS/POS_EXAMPLES):
|
||||
- `gen_dispatch()` falls to `else` branch (lines 88-93) producing identical `| cat | sub | script | desc |` format — **FACT** (code inspection)
|
||||
- `gen_tree()` only adds `[deps:]` annotation when `deps` is non-empty (lines 73-75) — **FACT** (code inspection)
|
||||
|
||||
8. **Key format consistency:** Registry key = `"${f##*/pos-}"` (line 68-70 of registry.sh), same as pos-tree's old `name="${f##*/pos-}"`. Consumer pos-tree passes `tool_key` directly to `reg_lookup` — consistent. — **FACT**
|
||||
|
||||
9. **Templates documentation:** `templates/pos-tool.sh:12-14` documents `POS_SUBCMDS`, `POS_DEPS`, `POS_EXAMPLES`. — **FACT**
|
||||
|
||||
10. **install.sh lib_names:** Line 143 includes `registry.sh` in the for loop. — **FACT**
|
||||
|
||||
---
|
||||
|
||||
## Verification Unverified
|
||||
|
||||
1. **`bash -n` for all three scripts** — sandbox blocks execution.
|
||||
2. **`make gen && git diff --exit-code`** — strongly inferred (code logic supports it) but not executed.
|
||||
3. **`make check` and `make lint`** — sandbox blocks execution.
|
||||
4. **Runtime registry API test** (`source lib/registry.sh; reg_scan; reg_list | wc -l`) — sandbox blocks.
|
||||
5. **`bin/pos-tree` output byte-identical to previous version** — builder claims `diff` is empty; code inspection supports this (registry provides same data, key format matches), but cannot verify.
|
||||
6. **`bin/pos docker --help` and `bin/pos network --help` still work** — no code changes to `bin/pos`, so likely fine, but unverified.
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
### Edge Cases in Header Parsing
|
||||
|
||||
1. **Tool with no `# POS:` header:** `sed` returns empty; `_reg_desc` stores empty string. `reg_tool_exists` uses `${_reg_desc[$1]+x}` which is false for empty — tool would not be "found" by `reg_tool_exists`. However, `reg_list` would still include it (it's in `_reg_tools` from the filename). **Minor inconsistency** between `reg_tool_exists` (checks `_reg_desc`) and `reg_list` (includes all executable pos-* files). **Severity: LOW** — unlikely to matter since all pos-* files in the repo have `# POS:` headers.
|
||||
|
||||
2. **Empty tool directory:** `_reg_tools_dir` fallback works (line 26-34). `reg_scan` with empty glob `"$dir"/pos-*` would expand to literal `pos-*` if `nullglob` is off — the for loop body would fail on the first non-existent file. **Severity: LOW** — `_reg_tools` would be empty (the `[ -x "$f" ] || continue` guard on line 67 protects against this).
|
||||
|
||||
3. **Sourcing registry.sh multiple times:** Re-sourcing re-declares arrays (idempotent `declare`) and redefines functions (harmless overwrite). **No conflict.**
|
||||
|
||||
### `_reg_tools_dir` vs `$self` in pos-tree
|
||||
|
||||
`pos-tree` passes `$self` (the tool's directory) to `reg_scan`, bypassing `_reg_tools_dir`. This is correct — `_reg_tools_dir` is only used when `reg_scan` is called without arguments (future consumers).
|
||||
|
||||
---
|
||||
|
||||
## Verdict
|
||||
|
||||
**ACCEPT_WITH_NOTES**
|
||||
|
||||
The implementation is correct, well-scoped, and internally consistent. The REQUIRED finding (documentation gap in installation flow) should be fixed before final acceptance. The two SUGGESTED findings are non-blocking. Gate results are plausible but could not be independently verified — the Orchestrator should run the gates before closing.
|
||||
|
||||
**Defect count:**
|
||||
- REQUIRED: 1 (documentation gap)
|
||||
- SUGGESTED: 2 (header comment, pseudocode inconsistency)
|
||||
- NOTE: 3 (dispatch table format, key format discrepancy, edge cases)
|
||||
- BLOCKING: 0
|
||||
|
||||
---
|
||||
|
||||
## Handoff
|
||||
|
||||
**Recommended next agent:** Builder
|
||||
|
||||
**Reason:** One REQUIRED documentation fix needed (Finding 1: add `registry` to the installation flow lib list in AGENT_Context_Project.md and the lib directory description in DEV.md). This is a trivial 2-line change within the approved scope.
|
||||
|
||||
**Changes made by Reviewer:** none (read-only)
|
||||
@@ -1,258 +0,0 @@
|
||||
# Reviewer Report — `pos ai hf` (Hugging Face Model Downloader)
|
||||
|
||||
**Date:** 2026-09-04
|
||||
**Status:** CHANGES_REQUIRED
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
**Verdict: CHANGES_REQUIRED**
|
||||
|
||||
Reviewed: `bin/pos-ai-hf` (470 lines), `DOC/POS.md` updates, generated docs/completions, AGENT_TODO entry.
|
||||
|
||||
**1 BLOCKING finding** — `total_size` accumulated inside a pipe subshell is always 0, so multi-file download summaries show incorrect total size. **2 REQUIRED findings** — missing disk space pre-flight check (architect-specified) and missing `hf_repo_files` API fallback (architect-specified). **2 SUGGESTED findings** — rate-limit HEAD request inefficiency and error message format deviation.
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| BLOCKING | 1 |
|
||||
| REQUIRED | 2 |
|
||||
| SUGGESTED | 2 |
|
||||
| NOTE | 3 |
|
||||
|
||||
---
|
||||
|
||||
## Checklist Results
|
||||
|
||||
### Code Quality
|
||||
|
||||
| Item | Status | Evidence |
|
||||
|------|--------|----------|
|
||||
| `set -euo pipefail` present | [PASS] | Line 2: `set -euo pipefail` |
|
||||
| `# POS:` header correct format with em-dash | [PASS] | Line 3: `# POS: ai hf — Download AI models from Hugging Face (search, download, manage)` — em-dash `—` confirmed |
|
||||
| `# POS_FLAGS:` correct | [PASS] | Line 4: `# POS_FLAGS: --branch --gguf --output` — matches actual flag parsing (lines 88-109) |
|
||||
| `# POS_DEPS:` correct | [PASS] | Line 5: `# POS_DEPS: curl jq` — matches deps guards on lines 17-18 |
|
||||
| `# POS_CONFIG:` correct format | [PASS] | Line 6: `# POS_CONFIG: ai \| ai.env \| HF_TOKEN=secret:… \| HF_DOWNLOAD_DIR=:…` — uses `secret:` prefix convention matching other tools (pos-docker-compose, pos-communication-matrix-sender, pos-communication-telegram-sender) |
|
||||
| `# POS_EXAMPLES:` present | [PASS] | Lines 7-12: 6 example lines covering search, download (repo, gguf, single file), list, remove |
|
||||
| Sources `lib/common.sh` via standard fallback | [PASS] | Line 14: `source "$(dirname "$0")/../lib/common.sh" 2>/dev/null \|\| source "$(dirname "$0")/common.sh"` — exact template pattern |
|
||||
| Deps guards BEFORE `-h\|--help` | [PASS] | Lines 17-18 (deps) before line 90 (`-h\|--help` case) |
|
||||
| `usage()` present and comprehensive | [PASS] | Lines 44-79: all 4 subcommands, download options, examples, config keys, exit codes documented |
|
||||
| All 4 subcommands implemented | [PASS] | `cmd_search` (line 279), `cmd_download` (line 303), `cmd_list` (line 402), `cmd_remove` (line 438); dispatch at line 464 |
|
||||
| Config loader reads `ai.env` with env-var precedence | [PASS] | `load_hf_config()` (lines 25-39): reads `CONFIG_FILE`, env-already-set wins (`if [ -z "${!k:-}" ]`), strips quotes, CR, comments |
|
||||
| Auth header: `Authorization: Bearer $HF_TOKEN` | [PASS] | `hf_auth_header()` (lines 128-132): `printf 'Authorization: Bearer %s' "$HF_TOKEN"` |
|
||||
| Rate limit handling: 429 → sleep + retry | [PASS] | Lines 150-167: loop with `attempt < 2`, on 429 extracts `Retry-After` or defaults 60, sleeps, retries once |
|
||||
| Resume: `curl -C -` | [PASS] | Line 259: `curl_args=(-L -C - --progress-bar -o "$target")` |
|
||||
| `.hf-meta` metadata tracking | [PASS] | Lines 368-382: writes JSON with repo_id, branch, timestamp, files array |
|
||||
| Output matches contract (emojis, paths, sizes) | [PASS] | Lines 392-398: 📥 and 📁 emojis, repo-id, size, path format matches Architect Decision 6 |
|
||||
| Error handling: 404 | [PASS] | Line 175: `err "Model not found: ${endpoint#/api/models/}"` |
|
||||
| Error handling: 401/403 | [PASS] | Line 174: `err "Authentication failed — check HF_TOKEN (pos config ai)"` |
|
||||
| Error handling: 429 | [PASS] | Line 176: `err "Rate limit exceeded — try again later"` |
|
||||
| Error handling: timeout | [PASS] | Line 153: `err "Connection timed out — check network"` |
|
||||
| Error handling: jq parse | [PASS] | Line 182: `err "Failed to parse API response — check network or HF status"` |
|
||||
| Error handling: no token | [PASS] | Line 121: `warn "No HF_TOKEN set — using anonymous access"` — continues for public repos per spec |
|
||||
| All file paths seam-guarded | [PASS] | `CONFIG_FILE="${CONFIG_FILE:-$HOME/…}"` (line 21), `HF_TOKEN="${HF_TOKEN:-}"` (line 22), `HF_DOWNLOAD_DIR="${HF_DOWNLOAD_DIR:-$HOME/…}"` (line 23), re-guarded at line 117 after `--output` override |
|
||||
| No `err "msg" 1` pattern | [PASS] | All 19 `err` calls use `err "message"` with no trailing exit code — confirmed by grep |
|
||||
|
||||
### Convention Compliance
|
||||
|
||||
| Item | Status | Evidence |
|
||||
|------|--------|----------|
|
||||
| `bash -n` passes | [UNVERIFIED] | Cannot execute `bash -n` due to sandbox restrictions. Builder claims pass. |
|
||||
| `make gen && make check` passes | [UNVERIFIED] | Cannot execute make. Builder claims pass. Generated files (AGENT_Context, completions) contain correct entries. |
|
||||
| `make lint` passes (0 FAIL, 0 WARN) | [UNVERIFIED] | Cannot execute make. Builder claims 0 FAIL, 0 WARN. |
|
||||
| Tool is executable (chmod 100755) | [UNVERIFIED] | Cannot check permissions. `git ls-files` shows file is tracked. Builder claims chmod 100755. |
|
||||
| POS.md has `ai hf` row + detail block | [PASS] | POS.md line 58: `bin/pos-ai-hf` in `**File:**` line. Lines 103-112: command table + auth/rate-limit/resume detail block. |
|
||||
| No INTERACTIVE_CMDS change needed | [PASS] | `bin/pos` line 269: `pos-ai-hf` NOT in `INTERACTIVE_CMDS` string — tool does not read stdin. |
|
||||
| No changes to `bin/pos-ai` | [PASS] | Grep for "hf" in `bin/pos-ai` returns 0 matches. |
|
||||
| No changes to `bin/pos` | [PASS] | `pos-ai-hf` not in `INTERACTIVE_CMDS`. No other modifications visible. |
|
||||
| No changes to `preinstall.sh` | [PASS] | Not in git diff. |
|
||||
| No changes to `lib/common.sh` | [PASS] | Not in git diff. |
|
||||
| Generated docs contain ai-hf | [PASS] | AGENT_Context: tree line 66, dispatch line 283, filetable line 613. Completions line 6: `_pos_flags[ai-hf]="--branch --gguf --output"` |
|
||||
| AGENT_TODO Done entry added | [PASS] | Git diff shows new entry at top of Done section, dated 2026-09-04. |
|
||||
|
||||
### Security
|
||||
|
||||
| Item | Status | Evidence |
|
||||
|------|--------|----------|
|
||||
| Token never printed in output | [PASS] | `$HF_TOKEN` referenced only at lines 22, 120, 129-130, 138, 257 — none in any `printf`/`echo` output path. Token warning (line 121) only prints the literal string "No HF_TOKEN set". |
|
||||
| Token passed via header, not URL | [PASS] | Lines 128-132: `hf_auth_header()` constructs `Authorization: Bearer …` header. Lines 144-146, 260-262: added as `-H` arg to curl. Never in URL string. |
|
||||
| Config file permissions (ai.env chmod 600) | [UNVERIFIED] | Tool reads from `~/.config/linux_post_install/ai.env`. The chmod is set by `postinstall.sh` (not by this tool). The tool does NOT change permissions — correct behavior. |
|
||||
| No command injection via repo-id | [PASS] | `repo_id` is user input. Used in: API endpoint construction (line 198-199, passed as curl URL arg — safe), `hf_repo_dir()` (line 214: string substitution `${repo_id//\//-}` — safe), jq filter argument (line 322: `--arg fn "$filename"` — safe), `.hf-meta` heredoc (line 375-382: unquoted heredoc — variables expanded but context is JSON file, not shell execution). No `eval`, no `exec` with user-controlled path. |
|
||||
| No command injection via filenames | [PASS] | Filenames from API response are parsed by `jq -r` and used in path construction (`$target="${target_dir}/${fname}"`). Passed to `mkdir -p` and curl `-o` — no shell interpretation of the filename value itself. |
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Subshell Variable Loss in Multi-File Downloads
|
||||
|
||||
[FAIL — BLOCKING]
|
||||
|
||||
**Finding:** In `cmd_download`, the multi-file download loop at lines 345-366 runs inside a pipe (`printf … | jq … | while IFS= read -r file_json; do … done`). In bash, a pipe creates a subshell, so variables modified inside the `while` loop — specifically `$total_size` (line 349) and `$downloaded` (line 355) — are lost when the pipe exits. The summary section at line 396 reads `$total_size` which is still `0` from its initialization at line 340.
|
||||
|
||||
**Severity:** BLOCKING
|
||||
**Certainty:** FACT — provable from bash subshell semantics. `cmd | while read; do var=...; done` runs the while body in a subshell. Variables set inside do not propagate back.
|
||||
**Evidence:**
|
||||
- Line 340: `local total_size=0`
|
||||
- Line 345: `printf … | jq … | while IFS= read -r file_json; do` — pipe creates subshell
|
||||
- Line 349: `total_size=$((total_size + fsize))` — modified inside subshell, lost
|
||||
- Line 396: `total_human="$(hf_human_size "$total_size")"` — reads `0`
|
||||
**Relevant files/lines:** `bin/pos-ai-hf:340-398`
|
||||
**Approved scope reference:** Architect Decision 6 (Output Contract) specifies multi-file summary as "📥 Downloaded: meta-llama/Llama-3.1-8B-Instruct (7 files, 4.7 GB)" — the size should be the correct total.
|
||||
**Why it matters:** Every multi-file download (the common case for large models) will print "0 B" as the total size. This is a user-visible incorrect output and directly violates the Architect's output contract.
|
||||
**Test mask:** The test harness (Test 22, line 151) checks for `[0-9] B)` which matches "0 B)" — the test passes on the bug. The test needs to check the actual expected sum (618 + 8500000000 + 9000000 + 5000 + 200 + 500 = 8509017318 bytes ≈ "8.5 GB").
|
||||
**Suggested fix:** Replace the pipe with process substitution (`while IFS= read -r file_json; do … done < <(printf '%s' "$filtered_files" | jq -c '.[]')`) to keep the loop in the main shell, or accumulate total_size via a temp file or another jq pass on `$filtered_files` before the loop.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Missing Disk Space Pre-Flight Check
|
||||
|
||||
[FAIL — REQUIRED]
|
||||
|
||||
**Finding:** The architecture (Decision 5, "Key implementation details" table, and Decision 7, "Error matrix" row) specifies a pre-flight disk space check: `df` available space vs estimated total (from `/tree/` endpoint), with a warning when space is critically low. The implementation has no disk space check at all.
|
||||
|
||||
**Severity:** REQUIRED
|
||||
**Certainty:** FACT — no `df` or `stat`-based space check anywhere in the file (confirmed by grep).
|
||||
**Relevant files/lines:** `bin/pos-ai-hf` — absent between line 333 (file count check) and line 337 (mkdir).
|
||||
**Approved scope reference:** Architect Decision 5: "Disk space | Pre-flight check: `df` available space vs estimated total (from `/tree/` endpoint)" and Decision 7: "Disk space | `df` pre-flight | `warn "Low disk space: need {N} GB, only {M} GB available"` then continue (user's call)".
|
||||
**Why it matters:** Downloading a 7-8 GB model on a near-full disk is a waste of time and leaves partial files. The architecture explicitly chose a non-blocking warning (not an error) — the user makes the final call. Without this, users discover the problem only after curl fails mid-file.
|
||||
**Suggested fix:** Before the download loop, sum the sizes from `$filtered_files` (this also solves the subshell bug if using jq for the sum), compare with `df --output=avail "$target_dir"`, and `warn` if insufficient. This is a ~5 line addition.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Missing `hf_repo_files` API Fallback
|
||||
|
||||
[FAIL — REQUIRED]
|
||||
|
||||
**Finding:** The architecture specifies `hf_repo_files()` should call `/api/models/{ns}/{repo}/tree/{branch}/` for file sizes, and fall back to `/api/models/{ns}/{repo}` for file list if the tree endpoint fails. The implementation only calls the tree endpoint (line 198) with no fallback.
|
||||
|
||||
**Severity:** REQUIRED
|
||||
**Certainty:** FACT — line 198: `local endpoint="/models/${ns}/${repo}/tree/${branch}"` followed by a single `hf_api "$endpoint"` call. No fallback logic.
|
||||
**Relevant files/lines:** `bin/pos-ai-hf:188-200`
|
||||
**Approved scope reference:** Architect Decision 5 (`hf_repo_files()` signature): "Calls: GET /api/models/{ns}/{repo}/tree/{branch}/ for sizes, **falls back to /api/models/{ns}/{repo} for file list**"
|
||||
**Why it matters:** Some HF repositories (e.g., datasets, some model repos) may not respond to the `/tree/` endpoint (404 or empty). The fallback to `/api/models/{ns}/{repo}` provides a file list (without sizes) so the user can still download. Without it, those repos fail entirely with a 404 error.
|
||||
**Suggested fix:** Wrap the tree call in a conditional; on 404, call `/api/models/{ns}/{repo}` and construct a minimal `[{"rfilename": "<name>", "size": 0}]` array from the `siblings` array. `size` being 0 is acceptable (displays as "0 B") since the primary goal is getting the download list.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Rate-Limit Extra HEAD Request
|
||||
|
||||
[NOTE — SUGGESTED]
|
||||
|
||||
**Finding:** Line 158 makes a second `curl -sI` HEAD request specifically to extract the `Retry-After` header value after receiving a 429. This is an extra HTTP call that could itself be rate-limited, and the `Retry-After` header was already present in the original request's response (line 151 uses `-w '%{http_code}'` but does not capture response headers).
|
||||
|
||||
**Severity:** SUGGESTED
|
||||
**Certainty:** FACT — line 158: `retry_after="$(curl -sI -H "${auth_header:-}" "$url" 2>/dev/null | grep -i 'retry-after:' | tr -d '\r' | awk '{print $2}')"`
|
||||
**Relevant files/lines:** `bin/pos-ai-hf:156-161`
|
||||
**Approved scope reference:** Architect Decision 5: "Rate limiting | Sleep 1s between files; on 429, wait `Retry-After` header value or 60s default"
|
||||
**Why it matters:** Minor inefficiency. When already rate-limited, making another request is suboptimal. Could use `curl -sS -D -` (dump headers to stdout) in the original request to capture `Retry-After` directly, or simply default to 60s without the extra call.
|
||||
**Suggested fix:** Change the original curl in `hf_api()` to use `-D -` (or a header dump file) so the `Retry-After` header is available from the first response without a second call.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Error Message Format Deviation
|
||||
|
||||
[NOTE — SUGGESTED]
|
||||
|
||||
**Finding:** The error message for invalid repo format at line 195 (`hf_repo_files`) says `"Invalid repo format: use namespace/model-name"` while the Architect specified `"Invalid repo format: use namespace/model-name"` at Decision 7. However, line 308 (`cmd_download`) also says the same message. The Architect's error matrix entry says `err "Invalid repo format: use namespace/model-name"` — which matches. This is consistent.
|
||||
|
||||
However, the Architect's error matrix says the model-not-found message should reference the full `repo-id` (e.g., `err "Model not found: {repo-id}"`), while the implementation at line 175 constructs the message from the API endpoint: `err "Model not found: ${endpoint#/api/models/}"`. The stripped endpoint value is the same as repo-id when the endpoint is `/models/{ns}/{repo}`, but if the endpoint is `/models/{ns}/{repo}/tree/{branch}`, the stripped value would be `{ns}/{repo}/tree/{branch}` — which is confusing.
|
||||
|
||||
**Severity:** SUGGESTED
|
||||
**Certainty:** HYPOTHESIS — only manifests when 404 is returned from `/tree/{branch}` endpoint (which strips to `{ns}/{repo}/tree/{branch}` in the message). The normal `/models/{ns}/{repo}` path produces the correct repo-id in the message.
|
||||
**Relevant files/lines:** `bin/pos-ai-hf:175`
|
||||
**Why it matters:** Minor UX: a 404 from the tree endpoint would show a confusing path in the error message instead of the clean repo-id. The Architect's spec just says `{repo-id}`.
|
||||
**Suggested fix:** Capture the `repo_id` and pass it to `hf_api` or handle the error at the caller level where `repo_id` is available. Or, in `hf_api`, accept an optional display-name parameter for error messages.
|
||||
|
||||
---
|
||||
|
||||
## Architect Compliance
|
||||
|
||||
| Decision | Implemented? | Notes |
|
||||
|----------|-------------|-------|
|
||||
| Decision 1: File location `bin/pos-ai-hf` | [YES] | Created at correct path |
|
||||
| Decision 2: Subcommands (download, search, list, remove) | [YES] | All 4 implemented |
|
||||
| Decision 3: Config scope extends `ai` | [YES] | `# POS_CONFIG: ai \| ai.env` — correct |
|
||||
| Decision 4: Download directory layout | [YES] | `<namespace>-<model-name>/` under XDG data dir, `.hf-meta` metadata |
|
||||
| Decision 5: Download logic | [PARTIAL] | Download flow correct; missing disk space check; missing API fallback |
|
||||
| Decision 6: Output contract | [PARTIAL] | Emojis, paths correct; multi-file size is always 0 (subshell bug) |
|
||||
| Decision 7: Error handling | [PARTIAL] | All error matrix cases handled; missing disk space pre-flight |
|
||||
| Decision 8: Deps/lint compliance | [YES] | POS headers correct, deps before help, source chain |
|
||||
| Decision 9: Testing strategy | [YES] | Stub-PATH harness exists at expected location with 46 tests |
|
||||
|
||||
**Deviations from Architect design:**
|
||||
1. Disk space pre-flight check not implemented (architect-specified, REQUIRED).
|
||||
2. `hf_repo_files` API fallback not implemented (architect-specified, REQUIRED).
|
||||
3. Rate-limit handling uses extra HEAD request instead of extracting from original response (architect did not specify implementation detail — minor deviation).
|
||||
4. Config loader is an inline pattern rather than copying from `bin/pos-ai` (functionally equivalent, not a deviation in behavior).
|
||||
|
||||
---
|
||||
|
||||
## Verification Verified
|
||||
|
||||
| Claim | Evidence | Status |
|
||||
|-------|----------|--------|
|
||||
| Builder: "46/46 tests green" | Test harness exists at `/tmp/opencode/hf-test/run-tests.sh` with 45 numbered tests visible (tests 1-45). Could not execute to confirm count. | UNVERIFIED |
|
||||
| Builder: "make gen && make check OK" | Generated files (AGENT_Context line 66/283/613, completions line 6) contain correct ai-hf entries. | STRONG INFERENCE |
|
||||
| Builder: "make lint 0 FAIL, 0 WARN" | All conventions verified by static analysis (POS headers, deps guards, source chain, exec bits claimed). | UNVERIFIED |
|
||||
| Builder: "bash -n OK" | Cannot execute. No syntax errors visible by manual inspection. | UNVERIFIED |
|
||||
| Builder: "POS.md updated" | Git diff confirms: ai File line updated (line 58), command table + detail block added (lines 103-112). | FACT |
|
||||
| Builder: "No changes to bin/pos, bin/pos-ai, preinstall.sh, lib/common.sh" | `git ls-files` confirms these files tracked; grep confirms no hf-related changes. | FACT |
|
||||
|
||||
---
|
||||
|
||||
## Verification Unverified
|
||||
|
||||
| Claim | Reason |
|
||||
|-------|--------|
|
||||
| `bash -n` passes | Sandbox prevents execution |
|
||||
| `make gen && make check` passes | Sandbox prevents execution |
|
||||
| `make lint` 0 FAIL 0 WARN | Sandbox prevents execution |
|
||||
| Tool is executable (100755) | Sandbox prevents `ls -la` |
|
||||
| Test harness 46/46 passes | Sandbox prevents execution |
|
||||
| Config file ai.env chmod 600 | Controlled by postinstall.sh, not this tool |
|
||||
|
||||
---
|
||||
|
||||
## Scope Compliance
|
||||
|
||||
**In-scope (confirmed):**
|
||||
- `bin/pos-ai-hf` created with all 4 subcommands
|
||||
- `DOC/POS.md` ai hf row + detail block
|
||||
- Generated docs/completions updated via `make gen`
|
||||
- `AGENT_TODO.md` Done entry
|
||||
|
||||
**Out-of-scope (confirmed NOT present):**
|
||||
- No ollama integration
|
||||
- No parallel downloads
|
||||
- No changes to `bin/pos-ai`, `bin/pos`, `preinstall.sh`, `lib/common.sh`
|
||||
- No new config scope (extends `ai`)
|
||||
- No `INTERACTIVE_CMDS` change
|
||||
|
||||
**Unexpected changes:** None detected.
|
||||
|
||||
---
|
||||
|
||||
## Remaining Uncertainty
|
||||
|
||||
1. Whether `bash -n`, `make gen`, `make check`, and `make lint` actually pass — Builder claims they do, and generated artifacts are consistent with this, but execution was blocked.
|
||||
2. Whether the 46 test cases actually all pass — the test harness exists with the correct structure, but Test 22 masks the subshell bug (checks for `[0-9] B)` which matches "0 B)").
|
||||
3. Whether the tool is truly `chmod 100755` — Builder claims it is, file is in git index.
|
||||
4. Live network behavior against real `huggingface.co` — only stub-tested, not end-to-end verified.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Next Agent
|
||||
|
||||
**Builder**
|
||||
|
||||
**Reason:** The 1 BLOCKING finding (subshell variable loss) and 2 REQUIRED findings (disk space check, API fallback) are all within the approved scope and well-understood fixes. The Builder should:
|
||||
1. Fix the subshell bug (replace pipe with process substitution or pre-compute sum via jq)
|
||||
2. Add the disk space pre-flight check (~5 lines, `df` + `warn`)
|
||||
3. Add the `hf_repo_files` fallback to `/api/models/{ns}/{repo}` on 404
|
||||
4. Update Test 22 to check actual expected size value instead of regex `[0-9] B)`
|
||||
5. Re-run `bash -n`, tests, and gates
|
||||
|
||||
---
|
||||
|
||||
## Changes Made by Reviewer
|
||||
|
||||
None — read-only review.
|
||||
@@ -1,426 +0,0 @@
|
||||
# Independent Review: `pos ai server` — llama.cpp Inference Server
|
||||
|
||||
## TL;DR
|
||||
|
||||
**Status: ACCEPT_WITH_NOTES**
|
||||
**Verdict:** Implementation faithfully satisfies the approved architecture. All 4 case additions to pos-ai are correct; the new tool and provider adapter follow established project conventions. One REQUIRED finding (test harness outside repo scope) and several notes. No BLOCKING or CRITICAL issues.
|
||||
|
||||
**Defect count:**
|
||||
- BLOCKING: 0
|
||||
- REQUIRED: 1 (test harness artifacts at `/tmp/opencode/` — not in repo, cannot be run independently)
|
||||
- SUGGESTED: 2
|
||||
- NOTE: 4
|
||||
|
||||
---
|
||||
|
||||
## Step 1: pos-ai-server — Tool Structure & POS Headers
|
||||
|
||||
- `set -euo pipefail` present: `bin/pos-ai-server:2` — **FACT**
|
||||
- POS header correct format with em-dash: `bin/pos-ai-server:3` — `# POS: ai server — llama.cpp local inference server (start, stop, status, models, logs)` — **FACT**
|
||||
- POS_SUBCMDS: `start stop status models logs` — matches architect Decision 1:44 — **FACT**
|
||||
- POS_FLAGS: `--port --host --model --ctx --gpu --threads` — matches architect Decision 1:48 — **FACT**
|
||||
- POS_DEPS: `curl jq` — matches architect Decision 1:47 — **FACT**
|
||||
- Sources `lib/common.sh` via standard fallback chain: `bin/pos-ai-server:8` — same pattern as pos-ai-hf, pos-network-download — **FACT**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 2: pos-ai-server — Deps Guards & Help
|
||||
|
||||
- Deps guards (`curl`, `jq`) at lines 11–12, BEFORE the `-h|--help` case at line 215 — correct ordering per AGENTS.md conventions — **FACT**
|
||||
- `command -v` pattern matches existing tools — **FACT**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 3: pos-ai-server — All 5 Subcommands Implemented
|
||||
|
||||
- `cmd_start()` at line 258 — generates systemd unit, enables, health-checks — **FACT**
|
||||
- `cmd_stop()` at line 341 — disable, remove unit, daemon-reload — **FACT**
|
||||
- `cmd_status()` at line 356 — service state, model from `/v1/models`, config, health — **FACT**
|
||||
- `cmd_models()` at line 407 — scans `HF_DOWNLOAD_DIR` for `.gguf` files — **FACT**
|
||||
- `cmd_logs()` at line 429 — `journalctl --user -u pos-ai-server -n <lines>` — **FACT**
|
||||
- Dispatch at line 436: empty→usage, start/stop/status/models/logs→respective functions, `*`→error — **FACT**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 4: pos-ai-server — Config Loader
|
||||
|
||||
- `load_config()` at line 21–35, env-var precedence via `if [ -z "${!k:-}" ]` — matches pos-ai-hf pattern (architect Decision 11:1) — **FACT**
|
||||
- Called at line 37 (top-level, before dispatch) — **FACT**
|
||||
- Pattern: reads `[A-Z_]+=` lines, strips quotes, strips CR, skips comments — identical to `bin/pos-ai:130–160` — **FACT**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 5: pos-ai-server — Seam-Guarded Paths
|
||||
|
||||
- `CONFIG_FILE="${CONFIG_FILE:-$HOME/.config/linux_post_install/ai.env}"` — env-overridable — **FACT**
|
||||
- `USER_SYSTEMD_DIR="${USER_SYSTEMD_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user}"` — env-overridable, matches pos-network-download:23 — **FACT**
|
||||
- `HF_DOWNLOAD_DIR="${HF_DOWNLOAD_DIR:-$HOME/.local/share/linux_post_install/ai/models}"` — env-overridable — **FACT**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 6: pos-ai-server — Binary Detection Fallback
|
||||
|
||||
- `find_llamacpp()` at line 40–47: candidates `llama-server`, `llama.cpp/server`, `server`, `llama-server-cuda` — matches architect Decision 9:454–460 exactly — **FACT**
|
||||
- Called in `cmd_start()` with `|| err "..."` on failure — **FACT**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 7: pos-ai-server — GPU Detection
|
||||
|
||||
- `detect_gpu()` at line 50–56: `nvidia-smi` check with proper stderr suppression — matches architect Decision 5 — **FACT**
|
||||
- `resolve_gpu_layers()` at line 58–71: configured→use value; `-1`→auto-detect → cuda→`-1`, cpu→`0` — matches architect Decision 5:270–285 — **FACT**
|
||||
- Warning at line 277–279: "No NVIDIA GPU detected — running in CPU mode" — matches architect Decision 9 error table — **FACT**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 8: pos-ai-server — Systemd Unit Generation
|
||||
|
||||
- Generated at runtime via heredoc (`cat > "$USER_SYSTEMD_DIR/$SERVICE" <<EOF`) — matches pos-network-download:173 pattern — **FACT**
|
||||
- Unit fields:
|
||||
- `Type=simple` — matches architect Decision 2:75 — **FACT**
|
||||
- `Restart=on-failure` — matches architect Decision 2:76 — **FACT**
|
||||
- `RestartSec=5` — matches architect Decision 2:77 — **FACT**
|
||||
- `TimeoutStopSec=10` — matches architect Decision 2:78 — **FACT**
|
||||
- `KillMode=control-group` — matches architect Decision 2:79 — **FACT**
|
||||
- `EnvironmentFile=-%h/.config/linux_post_install/ai.env` (dash prefix = optional) — matches architect Decision 2:81 — **FACT**
|
||||
- `WantedBy=default.target` — matches architect Decision 2:84 — **FACT**
|
||||
- ExecStart: uses `$llamacpp_full` (resolved full path) — matches architect Decision 11:3 (full path requirement) — **FACT**
|
||||
- Unit does NOT hardcode `$HOME` — confirmed no `$HOME` or `~` in the heredoc — matches architect Decision 11:2 — **FACT**
|
||||
- `chmod 644` on generated unit — matches pos-network-download:188 — **FACT**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 9: pos-ai-server — Start Flow
|
||||
|
||||
Per architect Decision 2:98–111:
|
||||
1. Load config — line 37 (`load_config`) — **FACT**
|
||||
2. Resolve model (argument → config → interactive pick) — line 270, `resolve_model()` — **FACT**
|
||||
3. Resolve port, host, ctx, gpu, threads — lines 246–254 (flag overrides to env vars) — **FACT**
|
||||
4. Validate model file exists — inside `resolve_model()` lines 129, 145 — **FACT**
|
||||
5. Auto-detect GPU if LLAMACPP_GPU_LAYERS=-1 — line 274, `resolve_gpu_layers()` — **FACT**
|
||||
6. Check port availability — lines 282–287 (`ss -tlnp`, warn only) — **FACT**
|
||||
7. Generate systemd unit — lines 297–314 — **FACT**
|
||||
8. `systemctl --user daemon-reload` — line 318 — **FACT**
|
||||
9. `systemctl --user enable --now` — line 319 — **FACT**
|
||||
10. Wait + health check — lines 331–338 (2s sleep + `check_health()`) — **FACT**
|
||||
- Linger warning — lines 324–328 — **FACT**
|
||||
- Dry-run mode at lines 289–293 — **FACT**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 10: pos-ai-server — Model Resolution
|
||||
|
||||
Per architect Decision 6:
|
||||
1. Explicit argument → absolute path check → HF_DOWNLOAD_DIR relative → original path — lines 126–141 — **FACT**
|
||||
2. Config (`LLAMACPP_MODEL`) — lines 144–148 — **FACT**
|
||||
3. Interactive pick (TTY only) — lines 150–155 — reads `/dev/tty`, not stdin — **FACT**
|
||||
4. Error if non-TTY and no model — line 156 — **FACT**
|
||||
|
||||
`pick_model()`:
|
||||
- Scans `HF_DOWNLOAD_DIR` for `.gguf` — line 103 — **FACT**
|
||||
- Reads `/dev/tty` — line 117 — **FACT**
|
||||
- Validates numeric selection — line 118 — **FACT**
|
||||
- Does NOT read stdin — no `INTERACTIVE_CMDS` needed — confirmed architect Decision 1:40 — **FACT**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 11: pos-ai-server — Health Check & Status
|
||||
|
||||
- `check_health()` at line 88–95: curl `/health`, jq parse, fallback "not running" — matches architect Decision 7:397–406 — **FACT**
|
||||
- `cmd_status()` output format matches architect Decision 7:382–392 (service, model, port, host, gpu, context, threads, autostart, endpoint, health) — **FACT**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 12: pos-ai-server — Error Handling
|
||||
|
||||
Per architect Decision 9 error table:
|
||||
- `llama-server` not found → `err "llama-server not found — install llama.cpp ..."` — line 261 — **FACT**
|
||||
- Port in use → `warn "Port $PORT may already be in use — check with 'ss -tlnp'"` — lines 283–286 — **FACT**
|
||||
- Model not found → `err "Model not found: $explicit"` — line 129, and `err "Configured model not found: $LLAMACPP_MODEL"` — line 145 — **FACT**
|
||||
- GPU not detected → `warn "No NVIDIA GPU detected — running in CPU mode"` — line 278 — **FACT**
|
||||
- Server start fails → `systemctl --user enable --now` propagates failure (set -e) — **FACT**
|
||||
- Server unhealthy → `warn "Server may not be ready yet — check with 'pos ai server status'"` — line 337 — **FACT**
|
||||
- No `err "msg" 1` anti-pattern found — **FACT**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 13: pos-ai-server — Human-readable Size
|
||||
|
||||
- `human_size()` at line 74–85 — uses `awk` for GB/MB/KB, `printf` for bytes — **FACT**
|
||||
- Called from `cmd_models()` and `pick_model()` — **FACT**
|
||||
- Note: The architect code (Decision 6) used `local human_size; human_size="$(human_size "$size")"` which shadows the function name. Builder correctly renamed the variable to `hsize` (line 421) — **GOOD CATCH**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 14: llamacpp.sh — Provider Adapter
|
||||
|
||||
- 4 functions present:
|
||||
- `provider_name()` line 9 — `printf 'Local llama.cpp'` — **FACT**
|
||||
- `provider_default_model()` line 11–16 — queries live server, fallback `(no model loaded)` — **FACT**
|
||||
- `provider_generate()` line 19–42 — OpenAI-compatible `/v1/chat/completions`, `stream:false` — **FACT**
|
||||
- `provider_models_list()` line 45–60 — lists models from `/v1/models`, marks loaded — **FACT**
|
||||
- `# PROVIDER_CONFIG: LLAMACPP_MODEL=:Default model path (GGUF file)` at line 7 — **FACT**
|
||||
- No stdout pollution: all response text goes to stdout, errors to stderr — matches gemini.sh and openrouter.sh patterns — **FACT**
|
||||
- Error handling for curl failures: `|| { echo "request failed (curl exit $?)" >&2; return 1; }` — **FACT**
|
||||
- Port from `LLAMACPP_PORT` config — line 20, 12, 46 — all `${LLAMACPP_PORT:-8088}` — **FACT**
|
||||
- Provider adapter format matches existing adapters (gemini.sh, openrouter.sh) — **FACT**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 15: pos-ai Modifications — Exactly 4 Case Additions
|
||||
|
||||
Git diff of `bin/pos-ai` shows exactly 4 `llamacpp)` case additions (plus 1 POS_CONFIG header update):
|
||||
|
||||
1. **`resolve_key()`** line 170: `llamacpp) return 0 ;; # No API key needed for local server`
|
||||
- Returns 0 without key — matches architect Decision 4:224–229 — **FACT**
|
||||
|
||||
2. **`require_key()`** line 181: `llamacpp) ;; # No key needed for local server`
|
||||
- Empty case arm (dead code since `resolve_key` returns 0) — matches architect Decision 4:431 — **FACT**
|
||||
- Harmless defensive coding — **NOTE**
|
||||
|
||||
3. **`resolve_model()`** line 198: `llamacpp) [ -n "${LLAMACPP_MODEL:-}" ] && printf '%s' "$(basename "$LLAMACPP_MODEL")" && return ;;`
|
||||
- Reads `LLAMACPP_MODEL`, returns basename — matches architect Decision 4:233–235 — **FACT**
|
||||
|
||||
4. **`cmd_providers()`** line 626: `llamacpp) configured="configured" ;; # Local server — always configured`
|
||||
- Always "configured" — matches architect Decision 4:430 final form — **FACT**
|
||||
|
||||
- No other code changes to pos-ai beyond these 4 cases + POS_CONFIG header — **FACT**
|
||||
- POS_CONFIG header correctly extended with `llamacpp | *providers=llamacpp` and all `LLAMACPP_*` keys — **FACT**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 16: Convention Compliance
|
||||
|
||||
- **`bash -n` syntax check**: Sandbox permissions denied `bash` execution except allowed git/read commands. **UNVERIFIED** — the builder report claims these passed; static inspection of all three files shows no syntax issues.
|
||||
- **`make gen`**: Git diff confirms `completions/pos.bash` and `DOC/AGENT_Context_Project.md` updated with correct entries for pos-ai-server (flags, subcmds, dispatch table, filetable, docmap line counts). Output appears byte-order deterministic. **STRONG INFERENCE** that `make gen` ran successfully.
|
||||
- **`make check` / `make lint`**: Cannot run due to sandbox restrictions. **UNVERIFIED**.
|
||||
- **Tool executable**: Cannot verify via `ls -la` due to sandbox. File begins with `#!/usr/bin/env bash` shebang. Builder claims `chmod 100755`. **UNVERIFIED** (shebang present: FACT).
|
||||
- **POS.md documentation**: `DOC/POS.md:114–124` documents `pos ai server` with all 5 subcommands, flags, and config keys. Matches the implementation. — **FACT**
|
||||
- **No INTERACTIVE_CMDS change needed**: Tool reads from `/dev/tty` not stdin; architect Decision 1:40 confirms this. — **FACT**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 17: Security
|
||||
|
||||
- **No hardcoded paths that could be exploited**: All paths use `$HOME`, env seams, or XDG dirs. — **FACT**
|
||||
- **Config file permissions**: `ai.env` is a template in the repo (gitignored at runtime). `chmod 600` is set by `postinstall.sh` at install time. — **FACT** (template permissions not checked in sandbox; runtime permission set by existing install flow)
|
||||
- **systemd unit doesn't expose API to network by default**: `LLAMACPP_HOST` defaults to `127.0.0.1`. — **FACT**
|
||||
- **No secrets in unit file**: No API keys needed for local llama.cpp server. — **FACT**
|
||||
- **Unit uses `EnvironmentFile=-` (dash prefix)**: Missing file is not an error. — **FACT**
|
||||
- **ExecStart uses heredoc with variable expansion**: Values come from user-controlled config and flag parsing; no injection vector in normal use. — **STRONG INFERENCE**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 18: Architect Compliance
|
||||
|
||||
Every decision in the Architect report maps to implemented code:
|
||||
|
||||
| Decision | Status |
|
||||
|----------|--------|
|
||||
| D1: Tool structure (5 subcommands, POS headers) | Implemented — **FACT** |
|
||||
| D2: Runtime-generated systemd unit (all fields) | Implemented — **FACT** |
|
||||
| D3: Config keys in ai.env (6 LLAMACPP_* keys) | Implemented — **FACT** |
|
||||
| D4: Provider adapter (4-function contract) | Implemented — **FACT** |
|
||||
| D5: GPU auto-detection (CUDA only, deferred ROCm) | Implemented — **FACT** |
|
||||
| D6: Model selection (find, resolution order, interactive pick) | Implemented — **FACT** |
|
||||
| D7: Health check & status (full output format) | Implemented — **FACT** |
|
||||
| D8: Changes to bin/pos-ai (4 case additions) | Implemented — **FACT** |
|
||||
| D9: Error handling (all matrix cases) | Implemented — **FACT** |
|
||||
| D10: File list & responsibilities | Matches — **FACT** |
|
||||
| D11: Implementation constraints (config seam, no $HOME in unit, full path, make gen) | All implemented — **FACT** |
|
||||
|
||||
Approved scope respected. No out-of-scope changes found. No missing in-scope items.
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Step 19: config/ai.env Documentation
|
||||
|
||||
- All 6 `LLAMACPP_*` keys documented as commented examples: `config/ai.env:20–26` — **FACT**
|
||||
- Section header: `# llama.cpp local inference server (pos ai server):` — **FACT**
|
||||
- Defaults match implementation values — **FACT**
|
||||
|
||||
`[PASS]`
|
||||
|
||||
---
|
||||
|
||||
## Independent Gate Results
|
||||
|
||||
| Gate | Result | Notes |
|
||||
|------|--------|-------|
|
||||
| `bash -n bin/pos-ai-server` | UNVERIFIED | Sandbox denied. Static inspection: no syntax issues found. |
|
||||
| `bash -n lib/ai-providers/llamacpp.sh` | UNVERIFIED | Sandbox denied. Static inspection: no syntax issues found. |
|
||||
| `bash -n bin/pos-ai` | UNVERIFIED | Sandbox denied. Static inspection: no syntax issues found. |
|
||||
| Test suite `/tmp/opencode/llamacpp-test/run-tests.sh` | UNVERIFIED | Sandbox denied `ls`; test directory existence cannot be confirmed. Builder claims 87/87 passing. |
|
||||
| `make gen && make check && make lint` | PARTIALLY VERIFIED | `make gen` output verified via git diff (pos.bash, AGENT_Context_Project.md updated correctly). `make check` and `make lint` cannot be run in sandbox — UNVERIFIED. |
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1: Test Harness Located Outside Repository
|
||||
|
||||
**Finding:** Builder report references test suite at `/tmp/opencode/llamacpp-test/run-tests.sh`. This is outside the repository and will not survive a reboot, workspace reset, or CI run. It cannot be run independently to verify the implementation claim.
|
||||
|
||||
**Severity:** REQUIRED
|
||||
|
||||
**Evidence:** Builder report Step 6 references `/tmp/opencode/llamacpp-test/run-tests.sh` (87/87 passing). Cannot confirm directory exists (sandbox restrictions).
|
||||
|
||||
**Relevant files:** `/tmp/opencode/llamacpp-test/run-tests.sh` (external)
|
||||
|
||||
**Approved scope reference:** Architect Decision 11:6 states "The tool must pass `make check && make lint`". The project convention is that CI runs `make check && make lint` on every push.
|
||||
|
||||
**Why it matters:** If the test suite cannot be re-run by other agents or CI, its verification claim is transient. The implementation should be validated by the standard `make check && make lint` gates before the Orchestrator marks it complete. Since those gates could not be independently run by this reviewer, this finding stands.
|
||||
|
||||
**Certainty:** FACT
|
||||
|
||||
### Finding 2: `require_key()` llamacpp Case is Unreachable Dead Code
|
||||
|
||||
**Finding:** The `llamacpp) ;;` case inside the `if ! resolve_key` block in `require_key()` (pos-ai:181) is unreachable. `resolve_key()` returns 0 for llamacpp (line 170), so the `if` condition is never true for llamacpp, and the case block is never entered.
|
||||
|
||||
**Severity:** SUGGESTED (non-blocking — architect explicitly requested this case)
|
||||
|
||||
**Evidence:** `bin/pos-ai:170` returns 0 unconditionally; `bin/pos-ai:176` enters the block only when `! resolve_key` (non-zero); `bin/pos-ai:181` is inside that block.
|
||||
|
||||
**Relevant files:** `bin/pos-ai:170, 175–185`
|
||||
|
||||
**Approved scope reference:** Architect Decision 4:431 — `require_key(): llamacpp) ;; — No key needed, just return`. The architect intended this as defensive fallback.
|
||||
|
||||
**Why it matters:** Minor. The dead code is harmless but could confuse future maintainers. If someone refactored `resolve_key` to fail for llamacpp, this error path would have an empty message before falling through to the generic `err "No API key for provider '$p'"` on line 183 — which is actually a reasonable fallback.
|
||||
|
||||
**Certainty:** FACT
|
||||
|
||||
### Finding 3: `provider_generate()` stderr Message Could Leak if Pos-ai Wraps It
|
||||
|
||||
**Finding:** In `llamacpp.sh:34`, a curl failure echoes `"request failed (curl exit $?)"` to stderr. The existing `pos-ai` `cmd_ask` captures stderr via `2>&1` (line 520: `provider_generate ... 2>&1`), which is the existing pattern for all providers. This is not a defect — just noting the behavior is consistent with gemini.sh and openrouter.sh.
|
||||
|
||||
**Severity:** NOTE
|
||||
|
||||
**Evidence:** `lib/ai-providers/llamacpp.sh:34`, `bin/pos-ai:520`
|
||||
|
||||
**Certainty:** FACT
|
||||
|
||||
### Finding 4: `check_health()` Says "not running" During Model Loading
|
||||
|
||||
**Finding:** The llama.cpp `/health` endpoint returns HTTP 503 with `{"status": "loading model"}` while the model is loading. `curl -sf` fails on non-2xx, so `check_health()` returns "not running" during the loading phase. This is a known limitation of the architecture (architect Decision 7:400 uses the same `curl -sf` approach).
|
||||
|
||||
**Severity:** NOTE
|
||||
|
||||
**Evidence:** `bin/pos-ai-server:91` — `curl -sf` (fails on non-2xx); architect Decision 7:400 uses identical logic.
|
||||
|
||||
**Relevant files:** `bin/pos-ai-server:88–95`
|
||||
|
||||
**Why it matters:** A 2s sleep before health check (line 331) may not be enough for large models. The warning at line 337 ("Server may not be ready yet — check with 'pos ai server status'") partially mitigates this. For larger models, `pos ai server status` would show the accurate state since it queries the live health endpoint with its own check.
|
||||
|
||||
**Certainty:** STRONG INFERENCE
|
||||
|
||||
---
|
||||
|
||||
## Verification Verified
|
||||
|
||||
| Claim | Evidence |
|
||||
|-------|----------|
|
||||
| `set -euo pipefail` present | `bin/pos-ai-server:2` — FACT |
|
||||
| POS header correct | `bin/pos-ai-server:3` — FACT |
|
||||
| 5 subcommands implemented | `bin/pos-ai-server:258,341,356,407,429,436` — FACT |
|
||||
| Config loader with env-var precedence | `bin/pos-ai-server:21–37` — FACT |
|
||||
| find_llamacpp fallback chain | `bin/pos-ai-server:40–47` — FACT |
|
||||
| detect_gpu checks nvidia-smi | `bin/pos-ai-server:50–56` — FACT |
|
||||
| Systemd unit generated at runtime | `bin/pos-ai-server:297–314` — FACT |
|
||||
| Unit fields match architect spec | `bin/pos-ai-server:298–313` — FACT |
|
||||
| Model resolution: arg → config → interactive | `bin/pos-ai-server:123–157` — FACT |
|
||||
| Health check via curl | `bin/pos-ai-server:88–95` — FACT |
|
||||
| 4 case additions to pos-ai | `git diff HEAD -- bin/pos-ai` — 4 `llamacpp)` lines — FACT |
|
||||
| No `err "msg" 1` pattern | grep: 0 matches — FACT |
|
||||
| llamacpp.sh 4-function contract | `lib/ai-providers/llamacpp.sh:9,11,19,45` — FACT |
|
||||
| PROVIDER_CONFIG header present | `lib/ai-providers/llamacpp.sh:7` — FACT |
|
||||
| POS.md documentation complete | `DOC/POS.md:114–124` — FACT |
|
||||
| ai.env LLAMACPP_* docs | `config/ai.env:20–26` — FACT |
|
||||
| make gen output correct | git diff: pos.bash + AGENT_Context_Project.md updated — FACT |
|
||||
|
||||
## Verification Unverified
|
||||
|
||||
| Claim | Reason |
|
||||
|-------|--------|
|
||||
| `bash -n` passes on all 3 files | Sandbox denied `bash` execution |
|
||||
| `make gen && make check && make lint` passes | Sandbox denied `make` execution |
|
||||
| Test suite 87/87 passing | Test directory at `/tmp/opencode/` cannot be confirmed |
|
||||
| `bin/pos-ai-server` is chmod 100755 | Sandbox denied `ls` execution |
|
||||
|
||||
---
|
||||
|
||||
## Scope Compliance
|
||||
|
||||
- **In-scope confirmed:**
|
||||
- `bin/pos-ai-server` (new) — created, 444 lines
|
||||
- `lib/ai-providers/llamacpp.sh` (new) — created, 61 lines
|
||||
- `bin/pos-ai` (modified) — 4 case additions + POS_CONFIG header
|
||||
- `config/ai.env` (modified) — LLAMACPP_* documentation
|
||||
- `DOC/POS.md` (modified) — ai server documentation
|
||||
- `completions/pos.bash` (auto-gen) — flags, subcmds updated
|
||||
- `DOC/AGENT_Context_Project.md` (auto-gen) — tree, dispatch, filetable, docmap updated
|
||||
- **Out-of-scope found:** None
|
||||
- **Additional files in git status:**
|
||||
- `AgentsReport/builder/2026-09-04_hf-downloader-implementation.md` — previous task artifact (not in this change's scope, already tracked as untracked)
|
||||
- `AgentsReport/reviewer/2026-09-04_hf-downloader-review.md` — previous task artifact
|
||||
|
||||
---
|
||||
|
||||
## Remaining Uncertainty
|
||||
|
||||
1. **bash -n / make check / make lint gates**: Could not be run in sandbox. Builder claims all pass. Static inspection finds no issues but this is not a substitute for execution.
|
||||
2. **Test suite existence and results**: Cannot confirm the test harness exists at `/tmp/opencode/llamacpp-test/`.
|
||||
3. **Executable bit on bin/pos-ai-server**: Cannot verify from sandbox.
|
||||
4. **Commit status**: No llamacpp-server commit in git log. The implementation files are untracked. This may be normal workflow (builder creates, reviewer reviews, then commit happens) but should be confirmed.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Next Agent
|
||||
|
||||
**Orchestrator**
|
||||
|
||||
**Reason:** All findings are either SUGGESTED (dead code note, cosmetic wording) or NOTE-level observations. The single REQUIRED finding is about test harness portability, not code quality. The implementation satisfies the approved architecture. The Orchestrator should:
|
||||
1. Verify the gates (`bash -n`, `make check`, `make lint`) locally or via CI — the Reviewer could not run them due to sandbox restrictions.
|
||||
2. Verify the test harness runs and passes.
|
||||
3. If gates pass, accept and proceed with commit + AGENT_TODO.md update.
|
||||
|
||||
---
|
||||
|
||||
## Changes made by Reviewer
|
||||
|
||||
none
|
||||
@@ -1,203 +0,0 @@
|
||||
# Reviewer Report — `pos media grab` Implementation
|
||||
|
||||
**Date:** 2026-09-04
|
||||
**Status:** ACCEPT_WITH_NOTES
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status:** ACCEPT_WITH_NOTES
|
||||
- **Verdict:** Implementation faithfully matches the approved architecture. One minor cosmetic defect, two unverified gates (sandbox prevented execution), one missing doc entry (howto/media.md).
|
||||
- **Findings:** 0 BLOCKING, 0 REQUIRED, 2 SUGGESTED, 1 NOTE
|
||||
- **Gates:** UNVERIFIED (sandbox restriction — see details below)
|
||||
|
||||
---
|
||||
|
||||
## Checklist Results
|
||||
|
||||
### Code Quality
|
||||
|
||||
| # | Check | Result | Evidence |
|
||||
|---|-------|--------|----------|
|
||||
| 1 | `set -euo pipefail` present | ✅ PASS | `bin/pos-media-grab:2` — `set -euo pipefail` |
|
||||
| 2 | `# POS:` header correct format | ✅ PASS | `bin/pos-media-grab:3` — `# POS: media grab — Auto-download URL as audio or video (classify + route)` |
|
||||
| 3 | `# POS_FLAGS:` header correct | ✅ PASS | `bin/pos-media-grab:4` — declares all 8 flags matching implemented case arms |
|
||||
| 4 | `# POS_CONFIG:` header correct | ✅ PASS | `bin/pos-media-grab:5` — `grab \| grab.env \| GRAB_DEFAULT=:...` matches Architect Decision 3 exactly |
|
||||
| 5 | Sources `lib/common.sh` via standard fallback | ✅ PASS | `bin/pos-media-grab:7` — standard `$(dirname "$0")/../lib/common.sh` fallback chain, identical to `pos-media-mp3:6` |
|
||||
| 6 | No deps guards (Decision 8) | ✅ PASS | No `command -v` guard present; deps are delegated to mp3/mp4 per spec |
|
||||
| 7 | `classify_url()` matches Decision 2 table | ✅ PASS | `bin/pos-media-grab:28-39` — exact match: `music.youtube.com→audio`, `soundcloud.com→audio`, `bandcamp.com→audio`, `youtube.com/youtu.be→video`, `vimeo.com→video`, `twitch.tv→video`, `*→$GRAB_DEFAULT`. Priority order correct (music.youtube.com matches before youtube.com). |
|
||||
| 8 | `usage()` present | ✅ PASS | `bin/pos-media-grab:41-67` — full help with options and examples |
|
||||
| 9 | Arg parsing handles all declared flags | ✅ PASS | `bin/pos-media-grab:78-97` — all 8 flags + `-h\|--help` + catch-all for unknown options |
|
||||
| 10 | `--audio` / `--video` mutual exclusion | ✅ PASS | `bin/pos-media-grab:108-109` — `FORCE_AUDIO -eq 1 && FORCE_VIDEO -eq 1` → error |
|
||||
| 11 | `--best` / `--worst` mutual exclusion | ✅ PASS | `bin/pos-media-grab:110-111` — `BEST -eq 1 && WORST -eq 1` → error |
|
||||
| 12 | Single positional arg (URL) enforced | ✅ PASS | `bin/pos-media-grab:94-95` — first non-flag sets URL, second triggers error |
|
||||
| 13 | Non-HTTP URLs rejected | ✅ PASS | `bin/pos-media-grab:102-105` — case match on `http://*\|https://*`, else → error |
|
||||
| 14 | `--dry-run` works without downloading | ✅ PASS | `bin/pos-media-grab:138-141` — prints command and exits before delegation |
|
||||
| 15 | Output format matches spec (emoji, title, duration, path, size) | ✅ PASS | `bin/pos-media-grab:208-227` — `🎵/🎬 Downloaded: $title ($duration)` + `📁 $rel_path ($size_human)` |
|
||||
| 16 | Error handling: stderr captured, clean message | ✅ PASS | `bin/pos-media-grab:144-157` — `2>&1` capture, grep for error summary, `err` with clean message |
|
||||
|
||||
### Listener Integration
|
||||
|
||||
| # | Check | Result | Evidence |
|
||||
|---|-------|--------|----------|
|
||||
| 17 | `url_detect` function exists | ✅ PASS | `bin/pos-communication-telegram-listener:660-672` — matches Architect spec exactly |
|
||||
| 18 | URL routing step position (after prefix map, before AI bridge) | ✅ PASS | `bin/pos-communication-telegram-listener:700-706` — between prefix map block (lines 688-699) and AI bridge (lines 707+) |
|
||||
| 19 | Timeout is 600s | ✅ PASS | `bin/pos-communication-telegram-listener:704` — `run_and_reply ... "$msg_id" 600` |
|
||||
| 20 | `run_and_reply` called with correct args | ✅ PASS | `bin/pos-communication-telegram-listener:704` — `run_and_reply "pos media grab --best \"$grab_url\"" "$msg_id" 600` |
|
||||
|
||||
### Convention Compliance
|
||||
|
||||
| # | Check | Result | Evidence |
|
||||
|---|-------|--------|----------|
|
||||
| 21 | `bash -n` on `pos-media-grab` | ⚠️ UNVERIFIED | Sandbox denied execution; Builder reports ✅ |
|
||||
| 22 | `bash -n` on `pos-communication-telegram-listener` | ⚠️ UNVERIFIED | Sandbox denied execution; Builder reports ✅ |
|
||||
| 23 | `make gen && make check` passes | ⚠️ UNVERIFIED | Sandbox denied execution; Builder reports ✅. Code review of gen output: `DOC/AGENT_Context_Project.md` tree/dispatch/filetable entries present (lines 83, 297, 626), `completions/pos.bash` has `media-grab` flags (line 12) and `grab` config scope (line 50). |
|
||||
| 24 | `make lint` passes (0 FAIL, 0 WARN) | ⚠️ UNVERIFIED | Sandbox denied execution; Builder reports ✅ |
|
||||
| 25 | Tool is executable (chmod 100755) | ⚠️ UNVERIFIED | Sandbox denied `stat`; Builder reports ✅ |
|
||||
| 26 | POS.md has grab row | ✅ PASS | `DOC/POS.md:240` — `pos media grab <url>` row in media table with full configuration column |
|
||||
|
||||
### Security
|
||||
|
||||
| # | Check | Result | Evidence |
|
||||
|---|-------|--------|----------|
|
||||
| 27 | No command injection via URL (proper quoting) | ✅ PASS | Listener: `"$grab_url"` inside double-quoted cmdline string → URL is properly quoted for `bash -c`. Tool: `"${DELEGATE_ARGS[@]}" "$URL"` → proper array expansion. |
|
||||
| 28 | No path traversal risks | ✅ PASS | No user-controlled path manipulation; output dirs are `$HOME/Music` or `$HOME/Videos` with optional `--output` override. `find` uses `-maxdepth 1` preventing traversal. |
|
||||
| 29 | Config file permissions | ✅ PASS | Config file at `~/.config/linux_post_install/grab.env` — file is only read (line 11-23), never written by this tool. Existing project convention for `.env` files is `chmod 600` at write time. |
|
||||
|
||||
### Architect Compliance
|
||||
|
||||
| # | Decision | Result | Evidence |
|
||||
|---|----------|--------|----------|
|
||||
| 30 | D1: `bin/pos-media-grab` as standard tool | ✅ PASS | File created at `bin/pos-media-grab`, not `features/` |
|
||||
| 31 | D2: Classification logic matches table | ✅ PASS | `classify_url()` at lines 28-39 — exact match (see #7 above) |
|
||||
| 32 | D3: Config scope `grab` with `GRAB_DEFAULT` | ✅ PASS | `POS_CONFIG` header at line 5, `load_grab_config()` at lines 10-23, called at line 25 |
|
||||
| 33 | D4: No INTERACTIVE_CMDS change; `--best` default for mp4 | ✅ PASS | `pos` INTERACTIVE_CMDS (line 269) does not include `media-grab`. mp4 route adds `--best` by default (line 133) |
|
||||
| 34 | D5: 600s timeout in listener | ✅ PASS | `run_and_reply ... 600` at line 704 |
|
||||
| 35 | D6: Clean output contract (emoji, title, duration, path, size) | ✅ PASS | Lines 198-227 — matches spec format exactly |
|
||||
| 36 | D7: Stub-based test harness | ✅ PASS (per Builder) | Builder reports 28 cases / 70 assertions / 0 failed at `/tmp/opencode/media-grab-test/` |
|
||||
| 37 | D8: No deps guards in grab | ✅ PASS | No `command -v` guards present; delegates to mp3/mp4 |
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1: Trailing "1" in error messages
|
||||
|
||||
**Severity:** SUGGESTED
|
||||
**Certainty:** FACT
|
||||
|
||||
**Evidence:** `bin/pos-media-grab` calls `err` with a trailing `1` argument in 6 places (lines 86, 90, 93, 95, 104, 109, 111, 156). The `err()` function from `lib/common.sh` is defined as:
|
||||
```bash
|
||||
err() { echo "${RED}ERROR:${RESET} $*" >&2; exit 1; }
|
||||
```
|
||||
Since `$*` joins all arguments, the trailing `1` becomes part of the visible error message. For example, line 95:
|
||||
```bash
|
||||
err "pos media grab: Unexpected argument: $1" 1
|
||||
```
|
||||
Would print: `ERROR: pos media grab: Unexpected argument: foo 1`
|
||||
|
||||
**Relevant files/lines:** `bin/pos-media-grab:86,90,93,95,104,109,111,156`
|
||||
**Approved scope reference:** Architect Decision 8 (conventions) — follow established patterns.
|
||||
**Why it matters:** Every other tool in the codebase calls `err "message"` without a trailing exit code (e.g., `pos-media-mp3:52,57,61,63`). The `1` is redundant (exit code is hardcoded to 1) and pollutes the user-facing error output.
|
||||
|
||||
---
|
||||
|
||||
### Finding 2: `howto/media.md` not updated with grab section
|
||||
|
||||
**Severity:** SUGGESTED
|
||||
**Certainty:** FACT
|
||||
|
||||
**Evidence:** `DOC/howto/media.md` line 5 lists tools as `mp3`, `mp4`, `sync`, `ytsync` — `grab` is missing from this list. The Architect scope explicitly listed `DOC/howto/media.md — Add grab usage example` as a file to update. The Builder did not mention updating this file. The `make lint` gate apparently does not check howto coverage (it checks POS.md), so this passed silently.
|
||||
|
||||
**Relevant files/lines:** `DOC/howto/media.md:5,7-12`
|
||||
**Approved scope reference:** Architect "Files to Update (Docs)" table: `DOC/howto/media.md — Add grab usage example`
|
||||
**Why it matters:** Users consulting the how-to guide won't find `pos media grab` documented there. Minor doc completeness gap.
|
||||
|
||||
---
|
||||
|
||||
### Finding 3: Gates could not be independently verified
|
||||
|
||||
**Severity:** NOTE
|
||||
**Certainty:** UNVERIFIED
|
||||
|
||||
**Evidence:** The sandbox restricted bash execution to only git/grep/sort/wc/head/tail commands. I could not independently run `bash -n`, `make gen`, `make check`, `make lint`, the test suite, or `stat` to verify file permissions. Builder reports all green; code inspection is consistent with this (generated files appear correct in `DOC/AGENT_Context_Project.md` and `completions/pos.bash`).
|
||||
|
||||
**Relevant files/lines:** N/A
|
||||
**Approved scope reference:** Architect "Post-Implementation Gates"
|
||||
**Why it matters:** Independent verification is a core reviewer responsibility. The Orchestrator should run these gates before final acceptance.
|
||||
|
||||
---
|
||||
|
||||
### Finding 4: `url_detect` trades off against `ai`-prefixed text with URLs
|
||||
|
||||
**Severity:** NOTE
|
||||
**Certainty:** FACT (known Architect design trade-off)
|
||||
|
||||
**Evidence:** `bin/pos-communication-telegram-listener:700-706` — URL detection runs before the AI bridge (lines 707+). A message like `ai what is https://example.com` will be routed to `pos media grab` instead of the AI bridge. The Architect explicitly documented this trade-off in Decision 5: "This is a minor trade-off — the user is more likely asking AI about the URL content than wanting to download it. However, this is a rare edge case."
|
||||
|
||||
**Relevant files/lines:** `bin/pos-communication-telegram-listener:700-706`
|
||||
**Approved scope reference:** Architect Decision 5, Edge Cases table
|
||||
**Why it matters:** No action needed — this is an acknowledged design decision, not a Builder deviation.
|
||||
|
||||
---
|
||||
|
||||
## Verification Verified
|
||||
|
||||
- **classify_url() matches Decision 2 exactly** — FACT (code comparison)
|
||||
- **url_detect() matches Architect spec exactly** — FACT (character-by-character comparison of `bin/pos-communication-telegram-listener:660-672` vs Architect Decision 5 pseudocode)
|
||||
- **URL routing step is between prefix map and AI bridge** — FACT (line positions confirmed: prefix map ends ~699, URL detect 700-706, AI bridge starts ~707)
|
||||
- **600s timeout** — FACT (line 704)
|
||||
- **POS.md row present and correct** — FACT (`DOC/POS.md:240`)
|
||||
- **Config scope in completions** — FACT (`completions/pos.bash:50` has `grab`)
|
||||
- **Flags in completions** — FACT (`completions/pos.bash:12` has `media-grab` flags)
|
||||
- **AGENT_Context_Project.md updated** — FACT (tree: line 83, dispatch: line 297, filetable: line 626)
|
||||
- **AGENT_TODO.md updated** — FACT (line 45, dated 2026-09-04)
|
||||
- **No INTERACTIVE_CMDS change** — FACT (line 269 does not include `media-grab`)
|
||||
|
||||
## Verification Unverified
|
||||
|
||||
- `bash -n` syntax check on both files — UNVERIFIED (sandbox)
|
||||
- `make gen && make check` — UNVERIFIED (sandbox)
|
||||
- `make lint` — UNVERIFIED (sandbox)
|
||||
- Test suite (`/tmp/opencode/media-grab-test/run-tests.sh`) — UNVERIFIED (sandbox)
|
||||
- File permissions (`chmod 100755`) — UNVERIFIED (sandbox)
|
||||
- `url_detect` edge case behavior — UNVERIFIED (would require runtime execution)
|
||||
|
||||
## Scope Compliance
|
||||
|
||||
**In-scope confirmed:**
|
||||
- `bin/pos-media-grab` — new tool ✅
|
||||
- `bin/pos-communication-telegram-listener` — `url_detect` + routing step ✅
|
||||
- `DOC/POS.md` — media table row ✅
|
||||
- `DOC/AGENT_Context_Project.md` — regenerated via make gen ✅
|
||||
- `completions/pos.bash` — regenerated via make gen ✅
|
||||
- `AGENT_TODO.md` — moved to Done ✅
|
||||
|
||||
**Out-of-scope changes found:** None.
|
||||
|
||||
**Missing from Architect scope:**
|
||||
- `DOC/howto/media.md` — grab section not added (Finding 2)
|
||||
|
||||
## Remaining Uncertainty
|
||||
|
||||
1. All runtime gates (bash -n, make gen/check/lint, test suite) are unverified due to sandbox restrictions. Orchestrator should run them before final close.
|
||||
2. The `find`-based file-location heuristic (lines 192-196) could pick a stale file in multi-download scenarios. This is an Architect design decision (Decision 6), not a Builder deviation.
|
||||
|
||||
## Deviations from Architect Design
|
||||
|
||||
| Area | Architect spec | Implementation | Assessment |
|
||||
|------|---------------|----------------|------------|
|
||||
| `err` call pattern | Not specified (implicit — follow conventions) | `err "msg" 1` with trailing exit code | Minor convention violation — all other tools use `err "msg"` without exit code. Cosmetic only. |
|
||||
| POS.md row | "Add `pos media grab` row to media table + detail block" | Added row to media table | ✅ Matches — Builder correctly identified this as lint-required |
|
||||
| howto/media.md | "Add `grab` usage example" | Not updated | Gap — listed in Architect scope but not implemented |
|
||||
|
||||
## Recommended next agent
|
||||
|
||||
**Orchestrator**
|
||||
|
||||
Reason: Implementation is complete and architecturally faithful. One SUGGESTED fix (trailing "1" in err calls) could be handed to Builder, and the howto gap is a doc update. The remaining uncertainty is gate verification — the Orchestrator should run `bash -n`, `make gen && make check && make lint`, and the test suite to close the verification gap before final acceptance.
|
||||
|
||||
## Changes made by Reviewer
|
||||
|
||||
None — read-only review.
|
||||
@@ -1,118 +0,0 @@
|
||||
# Review Report — `pos-ai-hf` GGUF/jq bug fix (2026-09-04)
|
||||
|
||||
## TL;DR
|
||||
- **Verdict:** APPROVE_WITH_NOTES — no BLOCKING or REQUIRED findings. The 3-hunk fix matches the Detective spec exactly; diff is scoped; doc diff is a clean single gen line-count change; harness design is sound; user acceptance evidence confirms the real flow.
|
||||
- **Findings:** 1 SUGGESTED (harness lacks an error-object/non-array shape test for the very adversarial case this review probed), plus NOTES on AGENT_TODO.md and harness message-checks being code-presence greps.
|
||||
- **UNVERIFIED (sandbox):** cannot execute bash beyond read-only git/grep — harness 9/9 and `bash -n`/`make check`/`make lint` claims NOT re-run by me; Orchestrator must run them (prior-review convention).
|
||||
- **Diff verdict:** PASS (3 sanctioned hunks, byte-for-byte per spec; doc = gen row only 495→506).
|
||||
- **Harness verdict:** PASS on design/structure/fixture integrity; execution UNVERIFIED here.
|
||||
|
||||
## Step 1: Inputs & scope — [DONE]
|
||||
- Spec of record: `AgentsReport/detective/2026-09-04_pos-ai-hf-gguf-jq-bug.md` (Changes 1–3 at Step 4, lines 80–126).
|
||||
- Builder report: `AgentsReport/builder/2026-09-04_pos-ai-hf-gguf-jq-bug-fix.md`.
|
||||
- Diff (`git diff bin/pos-ai-hf`): exactly 3 hunks:
|
||||
- `bin/pos-ai-hf:201-207` — normalization + comments in `hf_repo_files` primary path.
|
||||
- `bin/pos-ai-hf:339` — guarded `--gguf` filter.
|
||||
- `bin/pos-ai-hf:346-355` — mode-aware empty-message branch, replaces the old `-gt 0 || err` one-liner.
|
||||
- Doc diff (`git diff DOC/AGENT_Context_Project.md`): single line, `bin/pos-ai-hf` filetable row 495→506 (`:613`); row sits inside `GEN:START filetable` (612)–`GEN:END` (659) → legitimate `make gen` output; 506 matches actual file length (read: file ends at line 506). No other DOC/GEN changes. Full file integrity further confirmed via `git diff --stat`: only `bin/pos-ai-hf` (15 ins/3 del) + doc (1/1) modified; no other working-tree files.
|
||||
- POS headers (lines 1–12) untouched; exec bit `100755` (git ls-files -s).
|
||||
|
||||
**Step 1 verdict:** [PASS]
|
||||
|
||||
## 2: Change 1 — normalization in hf_repo_files — [DONE]
|
||||
`bin/pos-ai-hf:205`:
|
||||
`printf '%s' "$result" | jq '[.[] | select(.type == "file") | {rfilename: .path, size: (.size // 0)}]'`
|
||||
- Literally matches spec line 97 (including comment lines 202-204). Dirs dropped via `select(.type == "file")`; `size` defaulted via `.size // 0`; output shape `{rfilename, size}` per entry.
|
||||
- Adversarial probe — non-array (HTTP 200 error object `{"error":"x"}`): reasoned (cannot run jq): `.[]` on an object iterates its **values**; each value (string/object) fails `select(.type == "file")` → result `[]`, no jq error → file_count 0 → mode-aware `err` (exit 1). Only a top-level **number/boolean** 200-body would make `.[]` error ("Cannot iterate over number"); the HF tree endpoint never returns that, and `hf_api` (`:172-183`) errs on non-200 and validates JSON. This is exactly the residual accepted in spec line 104 ("graceful error"). No crash path for realistic inputs.
|
||||
- `set -euo pipefail` interaction: pipe failure would abort the script (spec line 101 explicitly accepts this) — an impossible failure here since `result` passed `jq empty` validation in `hf_api` (`:181`).
|
||||
- Verified on fixture data (counts, read-only): `fixtures/tree-files.json` = 13 `type:"file"` / 0 dirs / 10 `.gguf` entries; `fixtures/tree-with-dirs.json` = 8 files + 7 dirs — matches spec claims; dirs have `size: 0`, files carry real sizes (incl. LFS 3.98 GB fp16). Schema keys `oid/path/size/type`, no `rfilename` — confirms the bug's premise in the fixture.
|
||||
|
||||
**Step 2 verdict**: [PASS] (error-object behavior: STRONG INFERENCE from jq semantics; empirical jq run UNVERIFIED — Orchestrator to run `printf '%s' '{"error":"x"}' | jq '[.[] | select(.type == "file") | {rfilename: .path, size: (.size // 0)}]'` — expected `[]`, rc 0.)
|
||||
|
||||
## 3: Change 2 — guarded --gguf filter — [DONE]
|
||||
`bin/pos-ai-hf:339` matches spec line 108 verbatim. Non-weakening — FACT by construction: `(type)=="string" and endswith(...)` is true-passthrough for every valid-domain input the old `endswith` accepted (strings), and converts the former crash (null/number) into a no-match. With normalized data both guard-on and guard-off select the same 10 — guard is purely defense-in-depth, verified conceptually on the fixture (10 gguf paths present).
|
||||
|
||||
**Step 3 verdict:** [PASS]
|
||||
|
||||
## 4: Change 3 — mode-aware empty messages — [DONE]
|
||||
`bin/pos-ai-hf:347-355` matches spec lines 115–125 exactly:
|
||||
- single-file: `err "File not found: $filename in $repo_id (branch: ${branch})"` — `$filename` only referenced under `-n "$filename"` guard (line 348), no unset risk (`local filename="${SUBCMD_ARGS[1]:-}"` at line 324 keeps it set/empty). Em-dash/text per spec.
|
||||
- `--gguf`: `err "No .gguf files found in $repo_id${branch:+ (branch: $branch)} — try without --gguf"` — does NOT reference `$filename` (safe in --gguf mode); `${branch:+...}` defensive on empty branch.
|
||||
- generic: `err "No files to download"` (unchanged).
|
||||
- Exit semantics preserved: `err` in `lib/common.sh:24` → `exit 1` (verified read-only). No double-printing; single `err` call per branch.
|
||||
|
||||
**Step 4 verdict:** [PASS]
|
||||
|
||||
## 5: Regression surface — [DONE]
|
||||
- `git diff` shows hunks only at 201-207, 339, 346-356 — search/list/remove, `hf_search` (216-223), `hf_api` (134-186), `hf_resolve_branch` (244-264), `hf_download_file` (267-289), URL building (387), `--branch` (326), `--output` (114-116) — all byte-identical.
|
||||
- Only consumer of `files_json`/`hf_repo_files`: `cmd_download` line 330 (`grep hf_repo_files` → definition :188, call :330 — no other caller repo-wide).
|
||||
- Downstream read sites now receive `{rfilename,size}`: loop (`:383-387`), size `// 0` (`:384`, `:364), meta `[.[]|.rfilename]` (`:409`), summary `.[0].rfilename` (`:423,:425`) — all fit the normalized shape; no downstream edit needed. Shape parity with fallback (`:213` `[.siblings[] | {rfilename, size:(.size//0))}]`) — identical keys `{rfilename, size}`, both numeric sizes. Fallback sizes are 0 (HF metadata API has no sizes — detective Step 1 verified); that's a data, not a shape, difference.
|
||||
- No `INTERACTIVE_CMDS` impact (pos-ai-hf doesn't read stdin; not in the stdin family).
|
||||
|
||||
**Step 5 verdict:** [PASS]
|
||||
|
||||
## 6: Harness quality — [DONE]
|
||||
`/tmp/opencode/hf-test2/run-tests.sh` (read in full; counting/verification of fixtures via wc/grep — I could not *execute* anything, sandbox policy):
|
||||
- 9 assertions (lines 132-140), all behavioral jq-on-fixture checks except the intent-documented message-branch greps (t_empty:83-95, t_nogguf:98-105 — code-presence because sourcing the tool is a NAK per spec line 149; replicated `file_count` logic; design accepted).
|
||||
- Not tautological: each asserts a numeric/string result (13/0-nulls sizes, 10 gguf, 1/0 single-file, 13 rows no nulls, `[]`, count 0 rc 0 raw-guard, 8 files 0-`/` paths).
|
||||
- Drift-guards t_code_sync (124-129) pin all four jq expressions with `grep -F` — they pin presence, not location; divergence from the file breaks the test. Adequate per spec.
|
||||
- t_defense_guard (108-112) correctly demonstrates non-weakening AND null-proofing on the raw unnormalized fixture (`exit 0`, `[]`).
|
||||
- Fixture integrity: verified on-disk (13/0/10 and 8+7; `[]`; `[{"type":"file","path":"README.md","size":100}]`).
|
||||
- **Gap:** no assertion for the adversarial non-array shape (`{"error": "x"}` → normalize → `[]` rc 0, and `--gguf` guard on it → `[]`); also no static fixture for it. Recommend adding (a good SUGGESTED).
|
||||
- Note: `REPO` hard-coded to the home checkout path (line 10) — fine in place, would need param if the harness is ever committed for CI (out of today's decision boundary).
|
||||
|
||||
**Step 6 verdict:** [PASS] (execution UNVERIFIED; design/FACT-checks passed)
|
||||
|
||||
## 7: Style / conventions — [DONE]
|
||||
- `set -euo pipefail` line 2 intact; `# POS:`/POS_FLAGS/DEPS/CONFIG/EXAMPLES headers lines 3-12 unchanged; exec bit 100755 (ls-files -s, pre-commit gate intact by chmod).
|
||||
- No new dependencies; no unrelated files (add/modify status = only the two expected paths + 2 report artifacts).
|
||||
- Doc-sync: single gen line-count row, in-GEN-block, actual-sync (506 = line count) — no hand-edit violation.
|
||||
- **`AGENT_TODO.md` not touched — no task entry was created/moved for this multi-agent task; AGENTS.md asks to move finished tasks to Done. Minor; the Orchestrator can fold a dated Done line into the fix commit. (NOTE)**
|
||||
- Not runnable here (bash restricted): `bash-nn`, `make gen/check/lint` claimed green by Builder (0 FAIL, 0 WARN) — UNVERIFIED; prior-review convention: Orchestrator runs these.
|
||||
|
||||
**Step 7 verdict:** [PASS] with 2 NOTES
|
||||
|
||||
## 8: Panic-check the user-visible flows — [DONE] (read-verified; user evidence)
|
||||
| Flow | Expected after fix | Evidence |
|
||||
|---|---|---|
|
||||
| Single-file (`README.md`) | `1` match, real URL | fixture: `select(.rfilename == $fn)` matches a real `path` after NORM; builder live-verified `LICENSE` (7.2 KB, `.hf-meta` correct) |
|
||||
| All-files | 13 rows, string rfilename | normalized fixture row count 13, 0 nulls in harness; loop `:381-402` safe |
|
||||
| `--gguf` with gguf repos | 10 files, `[1/10]` progress | user acceptance: `pos ai hf download Qwen/Qwen2.5-3B-Instruct-GGUF --gguf --output ~/.models` → `[1/10] Downloading qwen2.5-3b-instruct-fp16-00001-of-00002.gguf…` (no jq crash) — matches `file_count=10` progress format (`:390-392`) |
|
||||
| `--gguf` no gguf | `err "No .gguf files found in … — try without --gguf"`, exit 1 | code `:351`; Builder live-verified distilbert |
|
||||
| Empty repo `[]` | `err "No files to download"` | `:353`; harness t_empty |
|
||||
| Dirs-only | filtered → `[]` → graceful | NORM `select(.type=="file")`; t_dirs_excluded |
|
||||
|
||||
**Step 8 verdict:** [PASS]
|
||||
|
||||
## 9: Findings (numbered)
|
||||
1. **SUGGESTED** — Harness lacks a non-array / error-object shape test (the adversarial probe Section 2). Add a static fixture, e.g. `{"error":"unauthorized"}` (and optionally `null`), with assertions: NORM → `[]`, rc 0; GGUF_FILTER on it → `[]`, rc 0 — the harness would then also pin the graceful-shape property it currently only watches through NORM. Evidence: `run-tests.sh` contains no such case. `Relevant file: /tmp/opencode/hf-test2/run-tests.sh` (lines 36-129). Approved reference: Detective Step 5 ("Tree returning a non-array (code-200 error object) … acceptable; no extra guard"), which the harness should lock in. Why it matters: this review's adversarial probe could only be **reasoned** (cannot run jq), and it is the one untested branch of the new code; low cost to pin.
|
||||
- **NOTE** — `AGENT_TODO.md` has no entry for this task (head shows empty Now); per AGENTS.md convention a Done-line should be folded into the fix commit. Not defect-scope (the fix commit doesn't exist yet); Orchestrator to fold in at commit.
|
||||
- **NOTE** — the follow-up `AGENT_TODO.md` / message-branch tests in the harness are grep-presence checks rather than full command runs by design (sourcing is top-level-NAK); behavioral side is covered by Builder+user live evidence.
|
||||
- **UNVERIFIED** — harness 9/9, `bash -n`, `make gen/check/lint` (green) — this sandbox denies non-git bash execution; couldn't re-run. Builder's claims are internally consistent with the diff and fixtures; user's live run independently corroborates the core fix. Orchestrator runs: `bash /tmp/opencode/hf-test2/run-tests.sh`, `bash -n bin/pos-ai-hf`, `make gen && make check && make lint`, and the error-object jq one-liner from Step 2.
|
||||
|
||||
## Verification verified
|
||||
- Statements contract: Change 1/2/3 match spec line-for-line (verified to file content).
|
||||
- Shape parity primary↔fallback (identical `{rfilename, size}` keys).
|
||||
- Scope containment: 15/3 lines in `bin/pos-ai-hf` + 1 DOC row; 3 hunks; no out-of-scope code.
|
||||
- Exec bit, headers, deps, convention surface unchanged.
|
||||
- Fixture integrity on disk: 13 (0 dirs, 10 gguf) + 8 files/7 dirs + `[]` + no-gguf; schema proofs the bug premise.
|
||||
- Exit-1 semantics through `err` (`lib/common.sh:24`).
|
||||
|
||||
## Verification unverified
|
||||
- jq behavior on `{"error":"x"}` (reasoned only: `[]`, rc 0).
|
||||
- Harness 9/9 run + `make check` and `make lint` (needs Orchestrator).
|
||||
- Installed `/usr/local/bin` copy byte-identity (user-run success implies fixed code; path check not assessable here).
|
||||
|
||||
## Scope compliance
|
||||
- In-scope: the 3 Changes 1-3 from Detective Step 4 + harness per Step 6. **All present, nothing extra.**
|
||||
- Out-of-scope found: none (docs/out-of-scope flags — `hf_api` NO `-L` 307 alias handling, non-recursive tree — correctly not touched, deferred following spec Step 2).
|
||||
|
||||
## Remaining uncertainty
|
||||
- Empirical jq semantics on the error-object case judged safe but not executed by me (read-only boundary); one-liner for Orchestrator.
|
||||
- Orchestrator-significant matters: harness/make execution results as acceptance evidence.
|
||||
|
||||
## Recommended next agent
|
||||
**Orchestrator** — approve-and-commit: stage `bin/pos-ai-hf` + `DOC/AGENT_Context_Project.md` + the two reports; fold a dated AGENT_TODO Done line per convention (NOTE 2); run the 3 harness/gates; optionally attach the error-object jq one-liner (Step 2) to close uncertainty. If any gate candidate genuinely fails, return to Builder within this exact scope (defects would be mechanical, not design).
|
||||
|
||||
## Changes made by Reviewer
|
||||
none (read-only; no repo file modified; only this report written)
|
||||
@@ -1,340 +0,0 @@
|
||||
# Reviewer Report: ytsync channel-handle fix
|
||||
|
||||
Date: 2026-09-04
|
||||
Agent: Reviewer (read-only)
|
||||
Status: VERDICT — **APPROVE_WITH_NOTES**
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Verdict:** APPROVE_WITH_NOTES — the fix implements the Detective's spec exactly, no BLOCKING or REQUIRED defects found.
|
||||
- **Diff:** 2 files changed (bin/pos-media-ytsync +25/-3; DOC/AGENT_Context_Project.md line-count 1191→1213). No out-of-scope changes.
|
||||
- **Findings:** 2 SUGGESTED (query retained on `/videos` append; `/live/` URL shape not in filter), 3 NOTE (uppercase-suffix prose deviation, harness coverage gaps, bare-handle-with-query unpinned).
|
||||
- **Spec conformance:** every element of the Detective's fix spec implemented exactly as specified (canonical_channel_url helper, run_probe integration, .entries[] filter, _type=="video" fallback guard).
|
||||
- **Test harness:** 32 assertions structurally sound and behavior-discriminating (not tautological) on the core fix paths; cannot independently verify execution due to read-only sandbox (bash execution denied), so the harness-pass claim is marked UNVERIFIED — the assertions themselves are meaningful and the baseline 12/17 vs fixed 32/0 story is internally consistent with the fix's blast radius.
|
||||
- **Runtime:** live `--dry-run` and single-entry probe claims are physically unverifiable in the read-only sandbox (execution is blocked); marked UNVERIFIED, per the fixed boundaries.
|
||||
|
||||
[PENDING: runtime verification by Orchestrator]
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Approved scope / contract
|
||||
|
||||
Source: `AgentsReport/detective/2026-09-04_ytsync-channel-handle.md` (spec of record).
|
||||
|
||||
Four required elements (Detective §4.1–4.3):
|
||||
1. New helper `canonical_channel_url()` inserted after `classify_url()`, before `sanitize_component()`.
|
||||
2. One-line integration in `run_probe()` after `local url="$1"`.
|
||||
3. `.entries[]` filter in `collect_entries()` keeping only `watch?v=|youtu.be/|/shorts/` URL entries.
|
||||
4. `_type == "video"` guard on the single-object fallback in `collect_entries()`.
|
||||
|
||||
Explicitly NOT changed (Detective §4.5): `parse_probe`, `P_KEY`, registry format, download loop, `classify_url`, registry migration.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 2: Verify each spec element against the diff
|
||||
|
||||
### 2.1 `canonical_channel_url()` helper — inserted at bin/pos-media-ytsync:178-197
|
||||
|
||||
- Inserted AFTER `classify_url()` (ends at line 176) and BEFORE `sanitize_component()` (starts line 199). ✓ spec location.
|
||||
- Logic matches the spec exactly:
|
||||
- `@*` bare handle → prepend `https://www.youtube.com/` (line 185). ✓
|
||||
- `classify_url` channel check before canonicalizing (line 187). ✓
|
||||
- Strips query `?` and fragment `#` before the suffix test (lines 188-189). ✓
|
||||
- Strips trailing `/` (line 190). ✓
|
||||
- Suffix whitelist: `videos|shorts|streams|live|playlists|featured|releases|podcasts|search` (line 192) — exactly the spec list plus `releases|podcasts|search` which are additional tab types. ✓ no scope creep (they're valid channel tabs that don't need /videos).
|
||||
- Non-whitelist → append `/videos` (line 195). ✓
|
||||
|
||||
Let me verify the edge cases manually:
|
||||
|
||||
**a. `@handle` with no domain** → line 185: `@*` matches → `https://www.youtube.com/@h` → classify_url → channel → path = `https://www.youtube.com/@h` → last component `@h` not in whitelist → append `/videos` → `https://www.youtube.com/@h/videos`. ✓ CANONICALIZED.
|
||||
|
||||
**b. `youtube.com/@h` no protocol** → classify_url → channel → path=`youtube.com/@h` → suffix `@h` → append `/videos` → `youtube.com/@h/videos`. ✓ CANONICALIZED (matches spec behavior table and harness).
|
||||
|
||||
**c. Channel with explicit tab (`/videos`, `/shorts`, etc., INCLUDING `/live`)**:
|
||||
- Path strip query/fragment/trailing-slash → last component in whitelist → untouched. So `/videos`, `/shorts`, `/streams`, `/live`, `/playlists`, `/featured` all untouched. ✓ per spec.
|
||||
- **Uppercase `/VIDEOS`** → last component `VIDEOS` (uppercase) NOT in the whitelist (case-sensitive) → append `/videos` → `@h/VIDEOS/videos`. This is a BEHAVIORAL DIFFERENCE from the spec.
|
||||
|
||||
Detective §4.5 and §168-171: "Uppercase suffix (`@h/VIDEOS`) → untouched → yt-dlp error (pre-existing)". But the implementation does NOT leave `@h/VIDEOS` untouched — it appends `/videos` because the case-sensitive whitelist doesn't match `VIDEOS`.
|
||||
|
||||
**Impact analysis:** In the pre-fix world, `@h/VIDEOS` → probe rc=1 → graceful notfound failure. In the fixed world, `@h/VIDEOS` → `@h/VIDEOS/videos` → this URL actually does NOT give a valid videos tab either (yt-dlp's VIDEOS tab doesn't exist; it would return an error for case-sensitive miss, per the probe evidence for uppercase suffix: "rc=1 `channel does not have a VIDEOS tab`"). So the net outcome is the same failure path, just at a different URL. The append of `/videos` to a non-matching suffix is arguably MORE correct than leaving it — it degrades to `@h/VIDEOS/videos` which yt-dlp treats as "no such tab" → probe-fail → same graceful notfound catch. Not a regression, and arguably an improvement. Recorded as **NOTE**.
|
||||
|
||||
**d. Query/fragment stripping before append** → For bare `@h?tab=foo`: path = `https://www.youtube.com/@h` (query stripped) → suffix `@h` → append `/videos` → `https://www.youtube.com/@h/videos`. The original query is NOT preserved (it's dropped because we append to the full `$u` which includes the query... wait let me recheck.
|
||||
|
||||
Actually looking at line 188: `path="${u%%\?*}"` — this computes `path` with query stripped, **but the `printf '%s/videos' "$u"` at line 195 appends to the ORIGINAL `$u` including the query**. So for `@h?tab=foo`, the output is `https://www.youtube.com/@h?tab=foo/videos` — the query `?tab=foo` is RETAINED because the append is on the full URL `$u`, not on `path`.
|
||||
|
||||
Wait — is that a bug? For `@h?tab=foo`, the result `@h?tab=foo/videos` puts `/videos` AFTER the query. That is a malformed URL: `?tab=foo/videos` — `videos` becomes part of the `tab` parameter value. youtube would interpret `tab` = `foo/videos` which is not a valid tab, so it would fall back to the Videos tab anyway (probably). But it's subtly wrong.
|
||||
|
||||
Let me re-read the actual code:
|
||||
```bash
|
||||
local path="${u%%\?*}"
|
||||
path="${path%%\#*}"
|
||||
path="${path%/}"
|
||||
case "${path##*/}" in
|
||||
videos | shorts | ...) printf '%s' "$u" ;;
|
||||
*) printf '%s/videos' "$u" ;;
|
||||
esac
|
||||
```
|
||||
|
||||
So for `https://www.youtube.com/@3blue1brown?tab=foo`:
|
||||
- `path` = `https://www.youtube.com/@3blue1brown` (query stripped)
|
||||
- last component = `@3blue1brown` (not in whitelist)
|
||||
- Append → `https://www.youtube.com/@3blue1brown?tab=foo/videos`
|
||||
|
||||
The query is retained in `$u`. So the canonical URL becomes `@3blue1brown?tab=foo/videos`. This could be wrong — `tab=foo/videos` is not a valid value. The spec §4.1 (line 108) says: "already-suffixed `/videos` … with trailing slash **or query** → untouched; … `?list=` / `watch?v=` / `youtu.be/<id>` → untouched." Those are cases where the suffix is intact. The spec behavior table does NOT explicitly cover `@h?tab=foo` (bare handle WITH a query but no known tab).
|
||||
|
||||
The harness tests `@h/videos?view=0&sort=dd` (suffix intact + query) → untouched, which is handled correctly because `path` strips query → last component `videos` → whitelist → output `$u` unchanged. ✓.
|
||||
|
||||
For `@h?tab=foo`, there's no explicit test. But since the handler appends to `$u` (with query), the query ends up mid-URL. This is a latent edge case of questionable behavior. In practice, YouTube treats `?tab=foo/videos` as an unknown tab → falls back to the default Videos tab. The behavior is *probably* correct in practice (probe returns Videos), but it's not clean. Recorded as **SUGGESTED** (could strip query when appending).
|
||||
|
||||
Actually wait — the spec's own code (Detective §4.1, lines 96-104) is IDENTICAL to what the Builder implemented. The spec itself uses `printf '%s/videos' "$u"` (full URL including query). So the Builder faithfully implemented the spec. The query-edge is inherent in the spec — not a Builder deviation. I'll record it as a NOTE against the spec, not the implementation.
|
||||
|
||||
**e. `music.youtube.com/channel/<ID>`** → classify_url: no `youtu.be/`, no `v=`, no `list=`, and `music.youtube.com/*` is in `is_youtube_url` OR pattern (line 150) → channel → canonicalize. ✓ per spec (probe evidence table line 51-52).
|
||||
|
||||
**f. `youtu.be/<id>` classification** → `classify_url` extracts the video id at lines 158-166 and returns `video`. So `canonical_channel_url` returns it untouched. ✓ (harness tests line 110).
|
||||
|
||||
**g. Empty channel** — `canonical_channel_url` doesn't touch empty channels; the `collect_entries` fallback guard handles that. ✓.
|
||||
|
||||
[PASS]
|
||||
|
||||
### 2.2 `run_probe()` integration — bin/pos-media-ytsync:339
|
||||
|
||||
```bash
|
||||
run_probe() { # run_probe <url>
|
||||
local url="$1"
|
||||
url="$(canonical_channel_url "$url")"
|
||||
```
|
||||
|
||||
Single line, immediately after `local url="$1"` — exactly what Detective §4.2 specified. The canonicalized `$url` is then passed to `yt-dlp --flat-playlist -J --no-warnings -- "$url"` at line 344. ✓
|
||||
|
||||
All three `run_probe` callers (`cmd_add` line 898, `ask_url_interactive` line 861, `pass_prepare`/sync line 581) benefit because the canonicalization lives inside `run_probe` — no shared-code-path regression, registry entries untouched (probe-time only). ✓
|
||||
|
||||
[PASS]
|
||||
|
||||
### 2.3 `.entries[]` filter — bin/pos-media-ytsync:403
|
||||
|
||||
```bash
|
||||
mapfile -t pairs < <(jq -r '.entries[] | select((.url // "") | test("watch\\?v=|youtu\\.be/|/shorts/")) | ((.id // "") + "\u001f" + (.title // ""))' "$PROBE_JSON")
|
||||
```
|
||||
|
||||
Exact jq quoting from the spec (Detective §4.3a). Let me analyze the regex:
|
||||
|
||||
`test("watch\\?v=|youtu\\.be/|/shorts/")`:
|
||||
- `watch\?v=` — matches URL containing `watch?v=` (the literal `?` escaped)
|
||||
- `youtu\.be/` — matches `youtu.be/`
|
||||
- `/shorts/` — matches path containing `/shorts/`
|
||||
|
||||
**Live-entry URL concern (the adversarial check):** In **flat-playlist mode**, what URL shape does a live-tab entry carry? The spec is explicit that real entries in `/videos`, `/shorts`, and `/streams` tabs carry `watch?v=` or `youtube.com/shorts/` URLs, per the probe evidence table (lines 30-32). The `/live` tab when a channel is actually live — yt-dlp's probe of `/live` (line 33) returns rc=1 "channel is not currently live" when nothing is live. When a channel IS live, what would `--flat-playlist -J` on `/live` return? The spec doesn't directly address this because the primary path (/videos) is the fix target.
|
||||
|
||||
Key point: the Detective's matrix (lines 30-32) shows that videos/shorts/streams tabs produce real `watch?v=` or `youtu.be/shorts/` entries. If a `/live` tab were probed when a channel is actually live, the live-video entries in flat-playlist mode would likely carry `watch?v=` URLs too (live videos are still videos accessible by watch?v), OR they might carry a `/live/<id>` form. The filter's `watch\?v=|youtu\.be/` pattern would match `watch?v=` or `youtu.be/` — but NOT a hypothetical `youtube.com/live/<id>` URL.
|
||||
|
||||
However: the primary fix path canonicalizes bare channels to `/videos`, which — when a live video is also the most recent upload — appears in the Videos tab with a standard `watch?v=` URL. Live entries carried through other tabs (e.g. a user explicitly adds `@h/live`) are edge cases. The `test` text has no `/live/` alternative. The spec DELIBERATELY only kept shorts/streams in the filter, treating live as outside scope (the `/live` URL already fails gracefully when not live). This is consistent with the spec's decisions, and a live-but-`watch?v=` entry WOULD pass the filter. The only case that would be silently dropped is a hypothetical `youtube.com/live/<id>` URL shape in a `/live` probe — which the spec didn't require. Recorded as **NOTE** (possible future refinement, not a spec violation).
|
||||
|
||||
**Playlists-tab entries** → `url:playlist?list=...` → no `watch?v=`, no `youtu.be/`, no `/shorts/` → dropped. ✓ (harness tests line 120; playlists-tab → 0 entries, spec lines 34-35).
|
||||
|
||||
**`_type:"url"` with null/new fields** — The Detective's matrix (line 35, featured tab) shows `id:null`, tab URLs → the filter drops them via the url test (null → `// ""` → empty string → `test("")` returns false → dropped). ✓ per spec.
|
||||
|
||||
[PASS]
|
||||
|
||||
### 2.4 `_type=="video"` fallback guard — bin/pos-media-ytsync:410-413
|
||||
|
||||
```bash
|
||||
elif [ "$(jq -r '._type // ""' "$PROBE_JSON")" = "video" ]; then
|
||||
```
|
||||
|
||||
Exactly the spec (§4.3b). Behavior:
|
||||
- Empty channel probe (`entries:[]`, `_type:"playlist"`) → `n=0` → `_type` is `"playlist"` ≠ `"video"` → skip. → 0 entries → 0 new (graceful). ✓
|
||||
- Tab probe (`_type:"playlist"`, 3 tabs) → `n=3`, but filter drops all → 0 entries. ✓
|
||||
- Single `?v=` probe (`_type:"video"`) → `n=0` (no entries array) → `_type=="video"` → fallback records 1 entry. ✓
|
||||
- If a probe ever returns `_type:"playlist"` with no entries (empty channel) → skipped gracefully. ✓
|
||||
|
||||
**Real single-video probe shape:** The Detective's probe evidence (step 2, line 44) confirms: `watch?v=` → `_type:"video"` single object, no entries array. The `video.json` fixture (`_type:"video"`, id, title, no entries) models this. The fallback fires and records the object. ✓
|
||||
|
||||
**Sub-case:** a probe returning `_type:"playlist"` with NO entries IS skipped (correct — nothing to download), while a probe returning `_type:"video"` (single) IS recorded. This is the exact intended behavior.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 3: Scope compliance / out-of-scope
|
||||
|
||||
- `git diff --stat`: 2 files — `bin/pos-media-ytsync` (+25/-3) and `DOC/AGENT_Context_Project.md` (+1/-1).
|
||||
- The DOC change is only the hand-maintained line-count row for `bin/pos-media-ytsync` 1191→1213 (exactly matches the +22/-3 = +22 lines → 1213). ✓ expected regeneration-only change, no GEN-block drift.
|
||||
- No changes to `parse_probe`, `classify_url`, registry write paths, download loop. ✓
|
||||
- No new dependencies (uses jq which was already required). ✓
|
||||
- `# POS:`, `# POS_CONFIG:`, `# POS_SUBCMDS:`, `# POS_FLAGS:` headers unchanged (diff shows no header hunk). ✓
|
||||
- Executable bit preserved (git diff --stat shows mode unchanged, file is executable). ✓
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 4: Regression risk analysis
|
||||
|
||||
- `canonical_channel_url` is called ONLY inside `run_probe`, which is the single choke point for all probes (add explicit, add interactive, sync). The canonicalization is purely probe-time — the registry `S_URL` (stored original) and the `finish_add` stored URL are unchanged. ✓
|
||||
- Playlist (`?list=`) and single-video (`?v=` / `youtu.be/`) URLs are classified non-channel by `classify_url` and bypass canonicalization entirely. ✓
|
||||
- The `.entries[]` filter only affects what `collect_entries` records — existing playlist probes use `?list=` entries which carry `watch?v=` URLs and pass the filter. ✓
|
||||
- The `_type=="video"` guard only changes the `else` branch (empty-entries fallback); the `n>0` branch (normal playlists/channels) is unaffected for entries that have valid URLs. Since the pre-fix script never had an `entries:[]` case that was functionally meaningful (it always fell into the single-object fallback populating bogus ids for playlist `_type` objects), the guard is strictly a correctness improvement.
|
||||
- `run_probe` failure path (`rc != 0`) is unchanged; canonicalization happens before probe so the failure semantics for user/404/music URLs are unchanged.
|
||||
- **`grab` flow:** `bin/pos-media-grab*` and `pos-media-grab` are separate tools; grep confirms no shared code path (only the run_probe/collect_entries functions in ytsync are touched, and grab doesn't invoke them).
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 5: Test harness review
|
||||
|
||||
Read `/tmp/opencode/ytsync-test/run-tests.sh` (132 lines) in full.
|
||||
|
||||
**Does it test what it claims?**
|
||||
|
||||
The harness:
|
||||
1. Generates 6 inline fixture JSONs (tab-probe, video-tab, playlist, playlists-tab, video, empty-videos) — §1 (lines 21-45).
|
||||
2. Stubs `common.sh`, `notify.sh`, and a recording fake `yt-dlp` — §2 (lines 48-67).
|
||||
3. Truncates `bin/pos-media-ytsync` at the `# ── Argument dispatch ──` marker and sources it — §3 (lines 70-78).
|
||||
4. Tests `classify_url` regression (7 checks) — §4.
|
||||
5. Tests `canonical_channel_url` (16 checks) — §5.
|
||||
6. Tests `collect_entries` filter against the fixtures (8 checks) — §6.
|
||||
7. Tests `run_probe` records the canonical URL (1 check) — §7.
|
||||
|
||||
**Are the assertions meaningful or tautological?**
|
||||
|
||||
The `canonical_channel_url` checks call the function and compare actual output to expected — **behavioral, not tautological** (lines 95-110). If the function were missing, we get `fail: canonical_channel_url is not defined` (line 93).
|
||||
|
||||
The `collect_entries` checks set `PROBE_JSON` to a fixture, call `collect_entries`, and compare actual `ENTRY_IDS`/`ENTRY_TITLES` — **behavioral**. The filter's effect is verified: tab-probe → 0, video-tab → 2 with the right ids, playlists-tab → 0, playlist → 1, empty → 0. These are meaningful discriminations of the fix.
|
||||
|
||||
The `run_probe` check (line 127-128) exports a urllog and fixture, calls the real `run_probe` (via the stub yt-dlp), and asserts the yt-dlp received the canonical `/videos` URL. **Behavioral.**
|
||||
|
||||
The `classify_url` checks are regression guards that the fix didn't change URL classification — **meaningful** (they'd catch if heuristic changes broke the primary classification).
|
||||
|
||||
**Gaps in the harness:**
|
||||
- It does NOT test the interactive-add flow end-to-end (only function-level canonical/probe/filter behaviors) — acceptable for a unit-style harness.
|
||||
- It does NOT test a `/port`-style URL with query stripped and appended — a minor edge not covered by the 16 checks.
|
||||
- It does NOT test an *uppercase* suffix (`@h/VIDEOS`) — which the implementation treats as non-whitelist and appends `/videos`. This matches the spec's stated case-sensitivity intent but the harness doesn't pin the behavior.
|
||||
- It does NOT test a channel with explicit `/live` and a real live entry (`watch?v=` or `/live/<id>` URL shape). This is the NOTE from Step 2.3 — the filter may or may not handle a hypothetical `/live/<id>` URL shape.
|
||||
|
||||
**Cannot verify execution:** The sandbox blocks running the harness (bash execution deny). The Builder's reported `32 PASS / 0 FAIL` (17 logical checks × 2 check calls each in some cases producing more than 17 lines) is self-consistent with the harness structure (17 logical check blocks; each block emits PASS/FAIL lines; the harness counts the actual check() invocations which are more than 17 — the Builder counted 32). The claim "baseline 12 PASS / 5 FAIL on unfixed script" is a testable assertion I couldn't run here. Marked **UNVERIFIED (execution)** — Orchestrator should run it as part of gate verification.
|
||||
|
||||
[PASS — with unverified execution]
|
||||
|
||||
## Step 6: Verification / gate claims
|
||||
|
||||
Per the fixed read-only boundary I could not run `bash -n`, `make check`, `make lint`, or the live dry-run (bash execution is denied). These are the Builder's claims:
|
||||
|
||||
1. `bash -n bin/pos-media-ytsync` — [UNVERIFIED — needs Orchestrator]
|
||||
2. harness `32 PASS / 0 FAIL` — [UNVERIFIED — needs Orchestrator]
|
||||
3. `make gen/check/lint` green (`0 FAIL, 0 WARN`) — [UNVERIFIED — needs Orchestrator]
|
||||
4. live dry-run `Resolved : 3Blue1Brown (channel · 151 videos)` + real titles — [UNVERIFIED — needs Orchestrator]
|
||||
5. single-entry download proof (GlYgs6v2YfU) began streaming before 180s timeout — [UNVERIFIED — needs Orchestrator]
|
||||
|
||||
None of these claims are contradicted by evidence I can see. The DOC line-count (1191→1213) matches exactly with +22 lines in the script (§7.4 of the Builder's report). Static analysis shows `# POS:` header, `set -euo pipefail` (line 2), and the diff touches no other files.
|
||||
|
||||
**Mark the runtime claims PENDING** — the Orchestrator should run the gates + harness + dry-run to convert them from UNVERIFIED to FACT.
|
||||
|
||||
[PENDING]
|
||||
|
||||
## Step 7: Findings
|
||||
|
||||
**Finding 1 — SUGGESTED**
|
||||
- Finding: `canonical_channel_url` appends `/videos` to `$u` (the full URL with any query) rather than to the query-stripped `path`. For a bare handle WITH a query but no tab (e.g. `@h?tab=foo`), the output is `@h?tab=foo/videos` — malformed, with `/videos` embedded in the query value. Impact is low (YouTube falls back to Videos on invalid tab values) but it is unclean.
|
||||
- Severity: SUGGESTED
|
||||
- Evidence: bin/pos-media-ytsync:195 — `printf '%s/videos' "$u"` after the query/fragment/trailing-slash stripping only on `path`.
|
||||
- Relevant files/lines: bin/pos-media-ytsync:188-196
|
||||
- Approved scope reference: Detective §4.1 (the spec's own code has this same construction — Builder matched it faithfully)
|
||||
- Why it matters: Cosmetic edge; not spec-violating since the spec used the identical `$u`-append.
|
||||
|
||||
**Finding 2 — SUGGESTED**
|
||||
- Finding: The `.entries[]` filter's URL test has no `youtube.com/live/` alternative. If a `/live` tab probe (when a channel IS live) ever returned live entries with a `youtube.com/live/<id>` URL shape, they would be silently dropped. Far more likely, live entries carry `watch?v=` URLs (still watchable), which would pass. No evidence of a regression.
|
||||
- Severity: SUGGESTED
|
||||
- Evidence: bin/pos-media-ytsync:403 — `test("watch\\?v=|youtu\\.be/|/shorts/")`; no `/live/` alternative.
|
||||
- Relevant files/lines: bin/pos-media-ytsync:403
|
||||
- Approved scope reference: Detective §4.3 — the filter kept shorts/streams deliberately; live was deemed out of scope and the `/live` URL fails probe gracefully when not live.
|
||||
- Why it matters: Future-proofing; not a defect under any observed fixture or the spec's stated edge matrix.
|
||||
|
||||
**Finding 3 — NOTE**
|
||||
- Finding: The uppercase-suffix (`@h/VIDEOS`) case does NOT leave the URL untouched as the spec's prose (§4.5, §168-171) describes. Because the whitelist match is case-sensitive, `@h/VIDEOS` → `@h/VIDEOS/videos` (append). The net runtime outcome is the same as the pre-fix world (yt-dlp errors on the bad tab → probe-fail → graceful notfound), so no regression. But the implementation deviates from the prose description in Detective §4.5.
|
||||
- Severity: NOTE
|
||||
- Evidence: bin/pos-media-ytsync:192 (case-sensitive `case` match) vs Detective line 171 ("`@h/VIDEOS` → untouched").
|
||||
- Relevant files/lines: bin/pos-media-ytsync:192
|
||||
- Approved scope reference: Detective §4.5 / line 171.
|
||||
- Why it matters: A doc/spec prose vs code mismatch. The behavior is benign but should be reconciled in the fix record if this is preserved.
|
||||
|
||||
**Finding 4 — NOTE**
|
||||
- Finding: The harness's `run_probe` check (line 127) sources the script in-band, but because it exports `YTSYNC_TEST_FIXTURE=video-tab.json`, the stub emits the fixture and `run_probe` exits 0. The check is meaningful but only asserts URL recording — it doesn't assert the probe result flow (parse_probe / collect_entries end-to-end). This gap is acceptable for a focused unit harness.
|
||||
- Severity: NOTE
|
||||
- Evidence: /tmp/opencode/ytsync-test/run-tests.sh:124-128
|
||||
- Required verification: none — informational.
|
||||
|
||||
**Finding 5 — NOTE**
|
||||
- Finding: The `@h?tab=foo` case (bare handle with query but no tab) is not pinned by the harness. It's a rare user input; the canonicalization's behavior is probable-but-untested.
|
||||
- Severity: NOTE
|
||||
- Evidence: run-tests.sh test matrix (lines 95-110) covers `@h/videos?view=0`, `@h/videos/`, bare `@h`, but not `@h?tab=foo`.
|
||||
- Required verification: none — informational.
|
||||
|
||||
## Findings summary
|
||||
|
||||
No BLOCKING. No REQUIRED. 2 SUGGESTED + 3 NOTE.
|
||||
|
||||
## Verification verified vs unverified
|
||||
|
||||
**Verified by static evidence (FACT):**
|
||||
- Spec conformance of the diff (all four elements implemented exactly).
|
||||
- No scope creep / two files only.
|
||||
- DOC line-count change is exactly the expected +22.
|
||||
- No new dependencies, headers unchanged, executable bit preserved.
|
||||
- `set -euo pipefail` intact (line 2).
|
||||
- All `run_probe` callers benefit from the single integration point.
|
||||
- Playlist/video URLs guaranteed untouched by classification.
|
||||
|
||||
**UNVERIFIED (needs state-changing execution — Orchestrator):**
|
||||
- `bash -n` result.
|
||||
- Harness execution (32 PASS / 0 FAIL claim).
|
||||
- `make check` / `make lint` (`0 FAIL, 0 WARN` claim).
|
||||
- Live `--dry-run` showing `Resolved : 3Blue1Brown (channel · 151 videos)`.
|
||||
- Single-entry download proof.
|
||||
|
||||
## Scope compliance
|
||||
|
||||
- In-scope confirmed: all four spec elements implemented. ✓
|
||||
- Out-of-scope found: none. The only extra beyond the literal spec prose is `releases|podcasts|search` in the suffix whitelist — these are valid channel tabs that correctly bypass `/videos`; not a deviation, just a completeness of the tab list.
|
||||
|
||||
## Remaining uncertainty
|
||||
|
||||
- Exact `/live/<id>` URL shape in a real live-tab flat-playlist probe (Finding 2) — untested, low risk.
|
||||
- Query-without-tab URL canonicalization cleanliness (Finding 1).
|
||||
- Execution claims (harness, gates, dry-run) require Orchestrator confirmation.
|
||||
|
||||
## Handoff
|
||||
|
||||
Status: **APPROVE_WITH_NOTES**
|
||||
|
||||
Reviewed work:
|
||||
- uncommitted diff: bin/pos-media-ytsync (+25/-3), DOC/AGENT_Context_Project.md (1191→1213 line count only)
|
||||
- spec: AgentsReport/detective/2026-09-04_ytsync-channel-handle.md
|
||||
- implementation report: AgentsReport/builder/2026-09-04_ytsync-channel-handle-fix.md
|
||||
- harness: /tmp/opencode/ytsync-test/run-tests.sh + fixtures
|
||||
|
||||
Approved scope / contract:
|
||||
- Four spec elements (helper, run_probe integration, filter, fallback guard) — all present and exact.
|
||||
|
||||
Findings:
|
||||
- SUGGESTED #1: query retained when appending `/videos` to bare-handle-with-query.
|
||||
- SUGGESTED #2: filter lacks `/live/` URL alternative (low risk).
|
||||
- NOTE #3: uppercase suffix behavior deviates from spec prose (benign).
|
||||
- NOTE #4: harness coverage gaps (end-to-end flow, regex edge).
|
||||
- NOTE #5: bare-handle-with-query not pinned by harness.
|
||||
|
||||
Verification verified:
|
||||
- Static conformance of diff to spec. No scope creep. Single integration point. No regression paths.
|
||||
|
||||
Verification unverified (needs Orchestrator):
|
||||
- bash -n, harness run, make check/lint, live dry-run (151 videos), single-entry download.
|
||||
|
||||
Scope compliance:
|
||||
- In-scope: all. Out-of-scope: none.
|
||||
|
||||
Remaining uncertainty:
|
||||
- Execution claims pending Orchestrator; `/live/<id>` URL shape untested; query edge cosmetic.
|
||||
|
||||
Recommended next agent:
|
||||
- **Orchestrator**
|
||||
|
||||
Reason:
|
||||
- The implementation is spec-conformant and no BLOCKING/REQUIRED defects were found. The remaining evidence gaps (execution of the harness, gates, and the live dry-run) are self-performing by the Orchestrator as the designated verification step in the AGENTS.md gate chain. If any claim fails at that point, hand to Builder for a scoped fix. No Builder fix is warranted from static evidence.
|
||||
|
||||
Changes made by Reviewer:
|
||||
- Created this report under AgentsReport/reviewer/2026-09-04_ytsync-channel-handle-review.md
|
||||
- No source/config/data file modified.
|
||||
@@ -1,138 +0,0 @@
|
||||
# Reviewer Report — AI cost & session-window change review
|
||||
|
||||
**Date:** 2026-09-06
|
||||
**Reviewer:** big-pickle (read-only, adversarial)
|
||||
**HEAD reviewed:** 8ce5479 (working tree has uncommitted Builder changes)
|
||||
**Design contract:** `AgentsReport/architect/2026-09-06_ai-cost-window-design.md`
|
||||
**Builder handoff:** `AgentsReport/builder/2026-09-06_ai-cost-window.md`
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status: CHANGES_REQUIRED** — 1 REQUIRED finding (token-cap input guard), 1 NOTE.
|
||||
- **Scope:** diff is exactly the 6 approved files (+2 journal reports); no llamacpp.sh / pos-ai-server / aliases / tests / AGENT_TODO edits. Verified.
|
||||
- **Core fix verified statically:** openrouter + gemini now send a hard `max_tokens` cap (default 2048 on unset/empty); `session_push` resolves `AI_SESSION_TURNS` lazily with a numeric guard; help text and all three docs are truthful and consistent; registry/header format is key-driven so both new keys are discoverable in scope `ai`.
|
||||
- **Defect:** `AI_MAX_TOKENS=0` / `-5` / `010` are *accepted by `pos config ai`* (`num:` validation regex is `^-?[0-9]+$`) and reach jq with no guard → provider 400s (0/-5) or a raw jq abort (leading zeros, non-numeric via hand-edit). The sibling `AI_SESSION_TURNS` var is guarded; `AI_MAX_TOKENS` is not, and the session guard itself still misses leading-zero values.
|
||||
- **UNVERIFIED (sandbox denies execution):** `make gen` idempotence, `make check`, `make lint`, `make test` (Builder claims gen x3 no-op, check OK, lint 0 FAIL/0 WARN, 17 files/299 checks green), live `reg_config_keys ai` / `pos config ai` rendering, and the 402 live repro. Static evidence is consistent with all of these; the Orchestrator should run the gates before merge.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Diff scope — [PASS]
|
||||
|
||||
`git diff HEAD --stat` shows exactly:
|
||||
|
||||
- `bin/pos-ai` (+10/−6 net +4 — header line 6, help lines 94-95, `session_push` 288-293)
|
||||
- `lib/ai-providers/openrouter.sh` (+2/−2, body 22-23)
|
||||
- `lib/ai-providers/gemini.sh` (+3/−2, body 17-20)
|
||||
- `DOC/POS.md`, `DOC/HOWTO.md`, `DOC/AGENT_Context_Project.md`
|
||||
|
||||
`git status --porcelain` = 6 modified files + 2 untracked journal reports only. **No** llamacpp.sh (`lib/ai-providers/llamacpp.sh:32-33` verified unchanged), no pos-ai-server, no alias wrappers, no tests, no AGENT_TODO, no `completions/pos.bash`. Matches design §Decision 5 file list exactly. `git diff --check` clean.
|
||||
|
||||
## Step 2: max_tokens correctness — [PASS] with edge finding
|
||||
|
||||
- Default applied: `--arg mt "${AI_MAX_TOKENS:-2048}"` → unset **or empty-exported** env → jq gets `"2048"` → `tonumber` → 2048. Applies in both providers. Var name matches the `# POS_CONFIG:` declaration (`AI_MAX_TOKENS`, bin/pos-ai:6); `cfg_write` writes the header key verbatim, so `pos config ai` writes `AI_MAX_TOKENS`/`AI_SESSION_TURNS` into ai.env; `load_env_file` (config-ui.sh:336-357) exports with env-wins. Providers read the same names.
|
||||
- Ordering safe: `session_push`/`provider_generate` are only reached after `require_key` → `resolve_key` → `load_config` (cmd_ask:503→524-529, cmd_chat:542→560-565), so ai.env values are loaded before lazy reads.
|
||||
- **Findings 1 & 2** below cover `AI_MAX_TOKENS=0`/`-5`/`010`/`abc` (no guard; jq `tonumber` semantics) and `AI_SESSION_TURNS=010` slipping through the guard.
|
||||
|
||||
## Step 3: session window correctness — [PASS] (with NOTE)
|
||||
|
||||
- `session_push` (pos-ai:288-293): `local n="${AI_SESSION_TURNS:-$MAX_SESSION_TURNS}"`; guard `^[0-9]+$` + `(( n >= 1 ))` else 40; jq `--argjson n` + `.messages |= .[-$n:]` (no string interpolation into the filter — improvement over the old `'"$MAX_SESSION_TURNS"'` splice).
|
||||
- n=10 → append user+assistant then keep exactly last 10 messages = 5 exchanges. ✓ matches user goal.
|
||||
- Very large arrays / n > length → jq slice clamps to whole array, no error. ✓
|
||||
- `SESSION` is always `default`/`--session` value (pos-ai:23, 658-660); `session_push` does not depend on SESSION at all and `session_load`/`session_save` early-return on empty SESSION (252-253, 277). No unsafe path.
|
||||
- Help text: pos-ai:67 renders "capped at 40 turns" (default 40 — truthful); the new Config: lines (94-95) document both vars with the "10 = last 5" semantics. DOC/POS.md:64 adds "(configurable via `AI_SESSION_TURNS`)". Truthful.
|
||||
- NOTE: `AI_SESSION_TURNS=010` passes the guard (regex matches; bash octal eval 8 ≥ 1) but `--argjson n "010"` is an invalid JSON numeric literal → jq abort under `set -e`. Pathological but config-ui-enterable. Covered by the same fix as Findings 1/2.
|
||||
|
||||
## Step 4: Config surface — [PASS: static] / [UNVERIFIED: runtime]
|
||||
|
||||
- Registry (`lib/registry.sh`) is format-driven: `reg_config_keys <scope>` echoes the whole `# POS_CONFIG:` line(s) per scope; the `ai` scope line (pos-ai:6) now contains `AI_MAX_TOKENS=num:…` and `AI_SESSION_TURNS=num:…` inside `@General`, exactly matching the proven `LLAMACPP_CTX_SIZE=num:…` pattern. config-ui renders `@`-captioned sections.
|
||||
- Static conclusion: both keys ARE discoverable under scope `ai` in `@General`. Runtime `reg_config_keys ai` / `pos config ai` display could not be executed (sandbox denies `source`/bash) → **UNVERIFIED**; Orchestrator/next agent can re-run.
|
||||
- Note: `bin/pos-ai-hf` also declares scope `ai` (pre-existing); both lines merge under `ai` — unchanged behavior.
|
||||
|
||||
## Step 5: Doc sync — [PASS]
|
||||
|
||||
- `DOC/POS.md:94-95` new rows: `AI_MAX_TOKENS` no/`2048`/"Max output tokens per request (OpenRouter/Gemini cost cap)"; `AI_SESSION_TURNS` no/`40`/"Session message cap — 2 per exchange; 10 = last 5 exchanges" — consistent with header + code (2048 default; 40 messages = 20 exchanges; 10 = 5 exchanges). `:64` parenthetical true (default unchanged).
|
||||
- `DOC/HOWTO.md:45` and `DOC/AGENT_Context_Project.md:491` append both vars. ✓
|
||||
- GEN blocks: only the filetable row `bin/pos-ai 705→709` changed (inside `GEN:START filetable`/`GEN:END`, AGENT_Context 614-662); `wc -l bin/pos-ai` = 709 → the generated row is byte-consistent with the file, so a `make gen` rerun would be a no-op for that row. Other GEN blocks and `completions/pos.bash` untouched (no key-level completion table exists — config is read live from headers). Hand-maintained Key-File rows (AGENT_Context:593-613) do not include `lib/ai-providers/*`, so "no bump needed" is correct.
|
||||
|
||||
## Step 6: Gates — [UNVERIFIED]
|
||||
|
||||
Sandbox permission rules deny `make`, `bash`, `source`, `jq`, `mkdir` (allowlist: git read commands, head/tail/wc/sort/grep/rg only). Builder claims: `make gen` idempotent x3, `make check` OK, `make lint` 0 FAIL 0 WARN, `make test` 17 files/299 checks green, `bash -n` clean, `git diff --check` clean (the last confirmed independently). **Orchestrator must run `make gen && git diff --exit-code`, `make check`, `make lint`, `make test` before merge** to confirm; static review found no gen-drift predicate (no new commands/flags; header text doesn't feed the generated tables).
|
||||
|
||||
## Step 7: Behavioral sanity (user's 402 goal) — [PASS with caveats]
|
||||
|
||||
- Pre-fix: no `max_tokens` → OpenRouter pre-check bills worst-case 131072 output tokens → 402 at balance-4511. Post-fix the request body carries `max_tokens: 2048`, so the pre-check estimate ≈ input_tokens + 2048 (output) instead of + 131072. With the terse `ask` prompt (~200 input tokens) the estimate is far under the ~4511-token affordance → 402 resolved for typical requests. Math is plausible.
|
||||
- Caveats for the user (also in Findings → caveats below): (1) if the auto-routed model's *output* price per token is steep, 2048 output tokens can still exceed the balance → 402 persists; set an explicit cheaper `OPENROUTER_MODEL` or switch `AI_PROVIDER=gemini`/`llamacpp`. (2) `max_tokens:2048` truncates long `--full` answers (raise the var). (3) Live OpenRouter/Gemini repro was not run (no network, no key) → UNVERIFIED.
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
**Finding 1 — REQUIRED**
|
||||
- **Evidence:** `lib/ai-providers/openrouter.sh:22-23` and `lib/ai-providers/gemini.sh:17-19` pass `${AI_MAX_TOKENS:-2048}` straight into jq `($mt|tonumber)` with no range/format guard. `lib/config-ui.sh:419` `num:` validation accepts `^-?[0-9]+$` — so `AI_MAX_TOKENS=0`, `-5`, `010` are all savable via `pos config ai`. Consequences: `0`/`-5` → provider 400 on every request (OpenRouter `max_tokens` must be ≥ 1; Gemini `maxOutputTokens` ≥ 1); `010` → jq invalid numeric literal → command substitution fails → `set -euo pipefail` (pos-ai:2) aborts the whole CLI with a raw jq error; same raw abort for a non-numeric value hand-set/exported (e.g. `AI_MAX_TOKENS=abc`). The sibling `AI_SESSION_TURNS` got an explicit guard (pos-ai:290-291) — the asymmetry shows the guard pattern was intended but not applied to the token cap, and the session guard itself misses leading-zero values (see Finding 2).
|
||||
- **Relevant files/lines:** openrouter.sh:22-23; gemini.sh:17-19; config-ui.sh:419; bin/pos-ai:290-291.
|
||||
- **Approved scope reference:** design §Decision 1 (providers read `${AI_MAX_TOKENS:-2048}`), §Decision 5 ("empty/unset values fall back to documented defaults" — satisfied, but 0/negative/leading-zero are neither empty nor unset).
|
||||
- **Why it matters:** a value the project's own config UI accepts (0 — a plausible "no cap" attempt, valid on some OpenAI-compatible backends) hard-breaks both remote providers until manually cleared; the new var is the 402 fix, so its input path should fail closed to the safe default, matching the guarded sibling var.
|
||||
- **Fix demand (Builder, in scope):** in both providers, before jq: `mt="${AI_MAX_TOKENS:-2048}"; [[ "$mt" =~ ^[1-9][0-9]*$ ]] || mt=2048` (also rejects 0, negatives, leading zeros, empty, non-numeric) then `--arg mt "$mt"`. 2-3 lines each.
|
||||
|
||||
**Finding 2 — NOTE (folded into Finding 1's fix)**
|
||||
- **Evidence:** `bin/pos-ai:290-291` guard is `^[0-9]+$` + `(( n >= 1 ))`; `AI_SESSION_TURNS=010` passes both (octal 8) then `--argjson n "010"` is an invalid JSON numeric literal → jq abort. Pathological, but config-ui-enterable. Fix: switch the session guard to the same `^[1-9][0-9]*$` pattern.
|
||||
|
||||
**Finding 3 — NOTE (unverified runtime claims)**
|
||||
- Gates/suite/live-402 claims from the Builder report could not be re-run under the sandbox (deny rules). The Orchestrator must run `make gen`/`make check`/`make lint`/`make test` and, if desired, a live OpenRouter repro before merge. Static evidence is consistent with the claims (row 709 == `wc -l`; header change adds no commands → gen tables unaffected).
|
||||
|
||||
---
|
||||
|
||||
## Verification verified (by evidence)
|
||||
|
||||
- Diff scope exact (6 approved files; `git status`/`git diff --stat`/`git diff`).
|
||||
- `git diff --check` clean.
|
||||
- Default 2048 applied when `AI_MAX_TOKENS` unset or empty (`:-2048` → jq 2048).
|
||||
- Var-name match across POS_CONFIG header, providers, and config-ui writer.
|
||||
- Lazy `AI_SESSION_TURNS` resolution after `load_config` in all 4 `session_push` call sites; `n>=1` guard; `--argjson` numeric slicing; n=10 → exactly 10 messages = 5 exchanges; large arrays safe; help text truthful.
|
||||
- Docs consistent (POS.md 94-95 + :64, HOWTO:45, AGENT_Context:491, filetable 709).
|
||||
- No GEN block hand-edits (only the generated filetable row; value matches `wc -l`).
|
||||
- llamacpp.sh / pos-ai-server / aliases / tests / AGENT_TODO untouched.
|
||||
|
||||
## Verification unverified
|
||||
|
||||
- `make gen` idempotence (and `git diff --exit-code` after gen), `make check`, `make lint` (0 FAIL/0 WARN), `make test` (17/299), `bash -n` — sandbox denies execution; need Orchestrator run.
|
||||
- Live `reg_config_keys ai` / `pos config ai` display (static evidence strong; runtime render unverified).
|
||||
- Live OpenRouter/Gemini request + 402 reproduction (no network/key; fake-curl smoke was temp, not committed; design §Testing defers permanent coverage to Tester).
|
||||
|
||||
## Scope compliance
|
||||
|
||||
- In-scope confirmed: header @General additions; session_push lazy resolution + guard; Config: help lines; openrouter `max_tokens`; gemini `generationConfig.maxOutputTokens`; three doc files; generated filetable row.
|
||||
- Out-of-scope found: none.
|
||||
|
||||
## Remaining uncertainty
|
||||
|
||||
- Gate results (Builder's claims unconfirmed in-sandbox).
|
||||
- Whether the auto-routed OpenRouter model's output price per token is low enough for 2048 output tokens within the ~4511-token balance (live repro needed).
|
||||
- Leading-zero / 0 / negative `AI_MAX_TOKENS` behavior is *certain* from jq/bash semantics (jq `tonumber` on `"abc"` errors; `"010"` is an invalid numeric literal; `0`/`-5` are valid numbers the providers will reject).
|
||||
|
||||
## Verdict
|
||||
|
||||
**CHANGES_REQUIRED** — one REQUIRED finding (token-cap input guard), fixable in ~4 lines within the approved scope (Builder), after which the same reviewer step re-verifies; then the Orchestrator runs the four gates and Tester adds permanent provider-body + session-pruning coverage per design §Testing.
|
||||
|
||||
## Handoff
|
||||
|
||||
**Status:** CHANGES_REQUIRED
|
||||
**Objective/problem:** one REQUIRED robustness finding on the new `AI_MAX_TOKENS` input path (and the same leading-zero hole in the session guard).
|
||||
**Evidence:** config-ui:419 accepts 0/-5/010; providers pass them unguarded to jq; provider 400 or raw jq abort under `set -e`; sibling guard at pos-ai:290-291 proves the intended pattern.
|
||||
**Affected areas:** `lib/ai-providers/openrouter.sh:22-23`, `lib/ai-providers/gemini.sh:17-19`, optionally `bin/pos-ai:290-291` guard regex.
|
||||
**Scope/decision boundary:** Builder may only add the numeric guard (default-fallback on invalid); no design change.
|
||||
**Verification performed:** full static diff/code/doc/registry audit; `git diff --check`; `wc -l` consistency; gates UNVERIFIED (sandbox).
|
||||
**Remaining uncertainty:** gate results; live 402 repro.
|
||||
**Recommended next agent:** **Builder**
|
||||
**Reason:** the fix is a scoped, fully-specified 4-line hardening within the approved change; after it lands, Orchestrator runs gates and Tester adds the permanent regression tests (provider-body cap, session pruning) the design already budgets.
|
||||
|
||||
**Changes made by Reviewer:** none (report file only).
|
||||
|
||||
---
|
||||
|
||||
## Caveats for the user (present regardless of the REQUIRED finding)
|
||||
|
||||
1. **402 may persist if the routed model is pricey:** 2048 output tokens at a high per-token price can still exceed the ~4511-token balance. Recommended: set an explicit cheaper `OPENROUTER_MODEL`, lower `AI_MAX_TOKENS` (e.g. 512), or switch `AI_PROVIDER=gemini`/`llamacpp`.
|
||||
2. **Cap truncates long answers:** `--full` requests are cut at `AI_MAX_TOKENS` (default 2048); raise the var if you need long-form output.
|
||||
3. **Env beats config file:** an exported `AI_MAX_TOKENS`/`AI_SESSION_TURNS` in your shell overrides `ai.env` (documented `load_env_file` semantics).
|
||||
4. **Session bound is opt-in:** default stays 40 messages (20 exchanges) for backward compatibility; set `AI_SESSION_TURNS=10` for "last 5 req/response".
|
||||
@@ -1,137 +0,0 @@
|
||||
# Reviewer Re-check — Finding 1 (input-guard hardening)
|
||||
|
||||
**Date:** 2026-09-06
|
||||
**Reviewer:** big-pickle (read-only, adversarial)
|
||||
**HEAD:** 8ce5479 + uncommitted Builder changes
|
||||
**Re-verifies:** `AgentsReport/reviewer/2026-09-06_ai-cost-window-review.md` → Finding 1 (REQUIRED) + Finding 2 (NOTE)
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status: ACCEPT** — all three guard sites match the exact fix spec; no findings remain.
|
||||
- Guard regex `^[1-9][0-9]*$` rejects 0, -5, 010, abc, empty; legitimate values (512, 2048, 4096) pass.
|
||||
- `local mt` does not shadow any existing local in either provider; jq plumbing (`--arg mt` + `tonumber`; `--argjson n`) is correct.
|
||||
- Scope: only the three code files changed for F1; no unrelated diffs; jq response parsing untouched.
|
||||
- Caveats: (1) runtime gate suite (`make gen/check/lint/test`) still needs Orchestrator execution; (2) live 402 repro unverified.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: openrouter.sh guard — [PASS]
|
||||
|
||||
**Lines 22-25 (actual):**
|
||||
```
|
||||
local mt="${AI_MAX_TOKENS:-2048}"
|
||||
[[ "$mt" =~ ^[1-9][0-9]*$ ]] || mt=2048
|
||||
body="$(printf '%s' "$body" | jq -nc --arg m "$model" --argjson msgs "$body" --arg mt "$mt" \
|
||||
'{model:$m, messages:$msgs, max_tokens:($mt|tonumber)}')"
|
||||
```
|
||||
|
||||
| Check | Result |
|
||||
|-------|--------|
|
||||
| Guard BEFORE jq executes | ✅ lines 22-23 precede jq on line 24 |
|
||||
| `local mt` no shadow | ✅ line 15 declares `model messages system body resp code body_out errmsg` — `mt` not present |
|
||||
| `$mt` used consistently | ✅ declared line 22, guarded line 23, passed `--arg mt "$mt"` line 24, consumed `($mt\|tonumber)` line 25 |
|
||||
| Regex rejects 0 | ✅ `0` fails `^[1-9]…` (first char must be `[1-9]`) → falls back to 2048 |
|
||||
| Regex rejects -5 | ✅ `-` fails `^[1-9]…` |
|
||||
| Regex rejects 010 | ✅ `0` fails `^[1-9]…` |
|
||||
| Regex rejects abc | ✅ `a` fails `^[1-9]…` |
|
||||
| Regex rejects empty | ✅ empty string fails `^[1-9]…` |
|
||||
| Default on unset/empty | ✅ `${AI_MAX_TOKENS:-2048}` covers both; regex re-confirms |
|
||||
| Legitimate values pass | ✅ `512`, `2048`, `4096` all match `^[1-9][0-9]*$` |
|
||||
| jq `tonumber` safe | ✅ only guaranteed-positive-integer strings reach `tonumber` |
|
||||
|
||||
## Step 2: gemini.sh guard — [PASS]
|
||||
|
||||
**Lines 17-22 (actual):**
|
||||
```
|
||||
local mt="${AI_MAX_TOKENS:-2048}"
|
||||
[[ "$mt" =~ ^[1-9][0-9]*$ ]] || mt=2048
|
||||
body="$(printf '%s' "$messages" | jq -c --arg mt "$mt" '{
|
||||
contents: [.messages[]? | {role: (.role | gsub("assistant";"model")), parts: [{text: .content}]}],
|
||||
generationConfig: {maxOutputTokens: ($mt|tonumber)}
|
||||
}')"
|
||||
```
|
||||
|
||||
| Check | Result |
|
||||
|-------|--------|
|
||||
| Guard BEFORE jq executes | ✅ lines 17-18 precede jq on line 19 |
|
||||
| `local mt` no shadow | ✅ line 15 declares `model messages system body resp code body_out errmsg` — `mt` not present |
|
||||
| Identical guard pattern to openrouter | ✅ exact same two lines |
|
||||
| All regex rejection cases | ✅ same analysis as Step 1 |
|
||||
| jq plumbing correct | ✅ `--arg mt` string → `tonumber` → integer in `generationConfig` |
|
||||
|
||||
## Step 3: session_push guard — [PASS]
|
||||
|
||||
**Lines 288-293 (actual):**
|
||||
```
|
||||
session_push() {
|
||||
local messages="$1" role="$2" text="$3"
|
||||
local n="${AI_SESSION_TURNS:-$MAX_SESSION_TURNS}"
|
||||
[[ "$n" =~ ^[1-9][0-9]*$ ]] || n="$MAX_SESSION_TURNS"
|
||||
printf '%s' "$messages" | jq -c --arg r "$role" --arg t "$text" --argjson n "$n" \
|
||||
'.messages += [{role:$r, content:$t}] | .messages |= .[-$n:]'
|
||||
}
|
||||
```
|
||||
|
||||
| Check | Result |
|
||||
|-------|--------|
|
||||
| Guard regex is `^[1-9][0-9]*$` | ✅ line 291 — matches spec, rejects leading zeros |
|
||||
| Old redundant `>= 1` check removed | ✅ gone; regex alone suffices |
|
||||
| Guard BEFORE jq executes | ✅ lines 290-291 precede jq on line 292 |
|
||||
| `--argjson n "$n"` correct | ✅ valid JSON integer after regex; jq variable `$n` used in slice `.-$n:` |
|
||||
| Fallback is `$MAX_SESSION_TURNS` | ✅ line 291, not a hardcoded literal |
|
||||
| `MAX_SESSION_TURNS=40` untouched | ✅ line 25 of pos-ai confirmed |
|
||||
|
||||
## Step 4: Scope — [PASS]
|
||||
|
||||
`git diff --stat HEAD` shows exactly 6 files:
|
||||
- `bin/pos-ai` — F1 session_push guard + earlier-round header/help/config
|
||||
- `lib/ai-providers/openrouter.sh` — F1 guard only
|
||||
- `lib/ai-providers/gemini.sh` — F1 guard only
|
||||
- `DOC/AGENT_Context_Project.md`, `DOC/HOWTO.md`, `DOC/POS.md` — earlier-round docs
|
||||
|
||||
No other files changed. No jq response-parsing shapes altered from the earlier round.
|
||||
|
||||
## Step 5: Consistency across all sites — [PASS]
|
||||
|
||||
Both providers use the identical pattern:
|
||||
```bash
|
||||
local mt="${AI_MAX_TOKENS:-2048}"
|
||||
[[ "$mt" =~ ^[1-9][0-9]*$ ]] || mt=2048
|
||||
```
|
||||
|
||||
session_push uses the same regex pattern for its sibling variable:
|
||||
```bash
|
||||
local n="${AI_SESSION_TURNS:-$MAX_SESSION_TURNS}"
|
||||
[[ "$n" =~ ^[1-9][0-9]*$ ]] || n="$MAX_SESSION_TURNS"
|
||||
```
|
||||
|
||||
The asymmetry that existed before (session had a guard, token cap did not) is now resolved. All user-configurable numeric values that reach jq/`--argjson` are guarded with the same `^[1-9][0-9]*$` pattern.
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
None. All demands from the original Finding 1 and Finding 2 are satisfied.
|
||||
|
||||
## Verification verified
|
||||
|
||||
- All three guard sites exist at exact claimed lines (FACT — direct file read).
|
||||
- Guard placement before jq confirmed by line-number ordering (FACT).
|
||||
- No local-name shadowing in either provider (FACT — line 15 declarations compared).
|
||||
- Regex rejects all specified bad inputs (FACT — `^[1-9][0-9]*$` does not match 0, -5, 010, abc, empty).
|
||||
- `MAX_SESSION_TURNS=40` at line 25 is untouched (FACT).
|
||||
- No scope creep (FACT — `git diff --stat`).
|
||||
|
||||
## Verification unverified
|
||||
|
||||
- Runtime gate suite execution (make gen/check/lint/test) — Orchestrator responsibility.
|
||||
- Live OpenRouter/Gemini 402 fix confirmation — requires network + API key.
|
||||
|
||||
## Verdict
|
||||
|
||||
**ACCEPT**
|
||||
|
||||
## Caveats
|
||||
|
||||
1. Orchestrator must still run `make gen && git diff --exit-code && make check && make lint` before merge.
|
||||
2. Live 402 reproduction unverified (design §Testing defers to Tester).
|
||||
@@ -1,129 +0,0 @@
|
||||
# Reviewer Report — 2026-09-06 — llamacpp optional-app installer + `ai` category
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status:** REQUEST_CHANGES — 2 REQUIRED, 3 SUGGESTED, 3 NOTE. All functional/structure claims verified; two fixable defects block acceptance.
|
||||
- **REQUIRED-1:** `apps/ai/llamacpp.sh` is **not executable** (mode 100644, untracked); every tracked `apps/*` file is 100755. Fix: `chmod +x` before `git add`.
|
||||
- **REQUIRED-2:** `DOC/APPS.md:3` says "16 optional desktop application installers"; the catalog table (rows 73–90) and the filesystem both hold **18** app installers (17 pre-existing tracked + the new one). The 15→16 change propagated a stale count (correct: 18).
|
||||
- **SUGGESTED:** (S1) failures inside the embedded `bash -c` install block are masked as success (no `set -e` in child — same class pattern as `scrcpy.sh`, so fix across the class); (S2) `DOC/DEV.md:292` categories list misses `ai`; (S3) unguarded `python3` dependency yields a misleading error when absent.
|
||||
- Every other item in the brief verified [PASS]: shebang/strict-mode/source path, idempotence guard before network/sudo, `/releases?per_page=10` scanning, `.tar.gz` suffix matcher, `--strip-components=1`, symlink-loop + readlink-target uninstall (no blanket `rm llama*`), temp cleanup on success + matcher-failure path, `case` dispatch with `uninstall`, CAT_NAMES `[ai]`, template comment, exactly 3 text lines in `bin/pos-ai-server` (no logic change), 1-line `DOC/POS.md` hint (accurate — `install.sh --apps/--full` verified), AGENT_TODO Done entry, scope/hygiene clean.
|
||||
- **Unverified (read-only boundary):** live GitHub API probe results, and `make gen/check/lint` runs (I did not re-run them); static evidence is consistent with the Builder's claims.
|
||||
|
||||
## Step 1: `apps/ai/llamacpp.sh` — structure, idempotence, release discovery, cleanup, dispatch
|
||||
|
||||
- [x] Shebang `#!/usr/bin/env bash` (line 1), `set -euo pipefail` (line 2), sources `lib/common.sh` via `$(dirname "$0")/../../lib/common.sh` (line 3) — correct depth for `apps/ai/`.
|
||||
- [x] `install_llamacpp()` idempotence guard **first**: `command -v llama-server &>/dev/null && { log "llama.cpp already installed"; return 0; }` (line 16) — before any network (lines 25–27) and before any sudo (lines 49–55).
|
||||
- [x] Arch mapping `x86_64→x64`, `aarch64→arm64`, unsupported arch errors (lines 19–23).
|
||||
- [x] Discovery: fetches `/releases?per_page=10` (line 13); python3 matcher scans `r['assets']` for `a['name'].endswith('-bin-ubuntu-$arch.tar.gz')` (lines 30–40). `endswith` on the exact suffix ⇒ no false positives on `.sha256`/`.txt` assets. No-match path removes the temp JSON and `err`s with a clear message + GitHub URL (lines 41–44).
|
||||
- [x] `--strip-components=1` extraction (line 51) — consistent with the claimed top-level-dir archive layout (Builder live probe; cannot re-probe read-only → see unverified).
|
||||
- [x] Symlink loop links every `llama*` binary from the install dir to `/usr/local/bin`, guarded by `[ -f ] && [ -x ]` (lines 52–55).
|
||||
- [x] Temp cleanup on success (line 56, both files) and on the matcher no-match path (line 42). No `trap`; remaining early-failure edge → NOTE-3.
|
||||
- [x] `uninstall_llamacpp()`: idempotent guard (line 63), removes `/usr/local/lib/llama.cpp-*` (line 65), and removes **only** symlinks whose `readlink` target matches `/usr/local/lib/llama.cpp-*` (lines 69–77) — **no** blanket `rm -f /usr/local/bin/llama*`; unrelated `/usr/local/bin/llama*` files are left alone. Extra edge → NOTE-2.
|
||||
- [x] `case "${1:-}"` dispatch with `uninstall` arm (lines 82–85). Matches lint rule `lint-conventions.sh:192-204` (function name + `uninstall)` dispatch present).
|
||||
- [x] Dry-run: every mutation is inside `spawn` (common.sh `spawn` logs and returns 0 under `DRY_RUN=1`, lines 79–82) — no mutation under `DRY_RUN=1`. Nuance → NOTE-1.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 2: `apps/install.sh` — CAT_NAMES + discovery
|
||||
|
||||
- [x] `CAT_NAMES` gains `[ai]="AI / ML"` (line 45); alphabetical position consistent.
|
||||
- [x] Discovery loop (lines 59–78) is directory-driven; no per-category registration — `apps/ai/` is picked up automatically; `bash apps/install.sh llamacpp` resolves via `find_app_category`; `--uninstall llamacpp` path works the same.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 3: `templates/app.sh` — category comment
|
||||
|
||||
- [x] Comment now reads "Categories: ai, browsers, development, media, networking, remote-access, system, utilities." (line 8) — `ai` added, nothing else drifted (single-hunk diff).
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 4: `bin/pos-ai-server` — exactly 3 text lines
|
||||
|
||||
- [x] `git diff bin/pos-ai-server`: 3 hunks, +3/-3, string-only:
|
||||
- help "Requires:" line (now line 256),
|
||||
- `cmd_start` `err` (line 397),
|
||||
- `cmd_status` `err` (line 563).
|
||||
- [x] All three name the installer (`apps/ai/llamacpp.sh`), give the invocation (`bash apps/install.sh llamacpp`, `--apps`/`--full`), and keep the GitHub URL `https://github.com/ggerganov/llama.cpp`.
|
||||
- [x] `--apps`/`--full` claim verified against root `install.sh:57-58,78-79` and `DOC/APPS.md:26` — accurate.
|
||||
- [x] Phrasing mirrors the scrcpy model (`bin/pos-communication-scrcpy:11`: "… not found — install … with the app installer: 'apps/media/scrcpy.sh' … see 'pos help …'"). No logic changed — diff is text-only in message strings.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 5: `DOC/APPS.md` — count, categories, row
|
||||
|
||||
- [x] Categories line now includes `ai` (line 37); llama.cpp catalog row added (line 73) with method "GitHub release → `/usr/local/lib/llama.cpp-<tag>` + `/usr/local/bin` symlinks" and category `ai` — matches the installer (GitHub release `.tar.gz` → `/usr/local/lib`, cat `ai`).
|
||||
- [FAIL] Count line (line 3): says "16 optional desktop application installers". Actual = **18**. Evidence: catalog rows 73–90 = 18 rows; `apps/**/*.sh` glob = 19 files incl. `apps/install.sh` (18 app installers); `git ls-files 'apps/*/*.sh'` = 17 tracked + 1 new. The pre-existing "15" was already stale (17 real installers at HEAD); the change kept the arithmetic wrong (correct value 18). REQUIRED-2.
|
||||
|
||||
[FAIL]
|
||||
|
||||
## Step 6: `DOC/POS.md` — single-line hint
|
||||
|
||||
- [x] Diff shows exactly one line changed (line 125, `pos ai server` Flags line): appends "(install via `bash apps/install.sh llamacpp`)" to the existing sentence. Accurate.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 7: `AGENT_TODO.md` — Done entry
|
||||
|
||||
- [x] New entry dated **2026-09-06** inserted at the top of `## Done` (line 45), before the prior 2026-09-06 entries — accurate, no duplication, matches the implementation (including the "15→16" claim which carries the same count defect as the DOC — noted in R2).
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 8: Hygiene + executable mode
|
||||
|
||||
- [x] No debugging artifacts, no hardcoded machine paths, no secrets, no committed temp files. `apps/ai/` contains only `llamacpp.sh`.
|
||||
- [FAIL] Executable mode: **existing apps are tracked 100755** (`git ls-files -s apps` → every `apps/*` including `apps/install.sh` is `100755`; `templates/app.sh` also 100755). The new `apps/ai/llamacpp.sh` is **100644** (`git diff --no-index --summary /dev/null apps/ai/llamacpp.sh` → "create mode 100644"). The repo convention stores app exec bits; the new file breaks that uniformity and `git add` would persist 100644. Note: gates won't catch it (`check-sync.sh` exec loop covers only `bin/pos*`; `lint-conventions.sh` `executable_files()` covers `bin/pos-*` + `entertainment/*.sh` only). REQUIRED-1.
|
||||
|
||||
[FAIL]
|
||||
|
||||
## Step 9: Scope
|
||||
|
||||
- [x] `git status --porcelain`: modified = AGENT_TODO.md, DOC/APPS.md, DOC/POS.md, apps/install.sh, bin/pos-ai-server, templates/app.sh; untracked = AgentsReport/builder/2026-09-06_llamacpp-app.md, apps/ai/. `git diff --stat` = 6 files, +11/-7 — consistent with the claimed change set.
|
||||
- [x] No `lib/`, `config/`, `install.sh`/`preinstall.sh`/`postinstall.sh`, `completions/`, or GEN: block changes. Builder's "make gen changed nothing" is consistent with the absense of generated-block diffs.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Findings
|
||||
|
||||
| # | Severity | File:line | Finding | Evidence |
|
||||
|---|----------|-----------|---------|----------|
|
||||
| R1 | REQUIRED | `apps/ai/llamacpp.sh` (untracked, whole file) | New app installer is not executable (100644); all tracked app installers/template are 100755. `git add` will persist 100644 | `git diff --no-index --summary /dev/null apps/ai/llamacpp.sh` → "create mode 100644"; `git ls-files -s apps` → all 100755; `check-sync.sh:22-24` + `lint-conventions.sh:100-105` only enforce exec bits for `bin/pos*`/`entertainment/*.sh`, so gates are blind to this |
|
||||
| R2 | REQUIRED | `DOC/APPS.md:3` | Count says 16; actual installer count is 18 (17 pre-existing tracked + llamacpp). Pre-existing "15" was already stale; change should have gone to 18 | Catalog table rows 73–90 (18 rows, read of DOC/APPS.md); glob `apps/**/*.sh` → 19 files incl. `apps/install.sh` (18 installers); `git ls-files 'apps/*/*.sh'` = 17 |
|
||||
| S1 | SUGGESTED | `apps/ai/llamacpp.sh:46-57` | Embedded `bash -c` install block has no `set -e`; a failed `curl -fsSL`/`tar` continues to `rm` (rc 0) → `spawn` prints OK and line 59 logs "installed" on a failed install. Same class pattern as `apps/media/scrcpy.sh:26-34` — recommend adding `set -e` inside the child (or `&&`-chaining), ideally fixed across both installers | read of llamacpp.sh:46-57 vs scrcpy.sh:26-34; common.sh:90-115 (`spawn` runs `bash -c` child, no errexit inheritance) |
|
||||
| S2 | SUGGESTED | `DOC/DEV.md:292` | "Adding an Optional App" categories list omits the new `ai` category (updated in `templates/app.sh:8` and `DOC/APPS.md:37` but not DEV.md) | read of DEV.md:292; diff of templates/app.sh + DOC/APPS.md |
|
||||
| S3 | SUGGESTED | `apps/ai/llamacpp.sh:30-43` | `python3` is an undeclared dependency; if absent, `python3 -c` fails and the script prints the misleading "No llama.cpp Ubuntu x64 binary release found" error. A `command -v python3` guard (or using the `curl`+`jq` already required by `pos ai server`) would give a clearer error. Same pattern as scrcpy.sh:15 | llamacpp.sh:30-44; no deps guard for python3 anywhere in file |
|
||||
| N1 | NOTE | `apps/ai/llamacpp.sh:30-44` + `lib/common.sh:79-82` | Under `DRY_RUN=1` no mutation occurs (spawn no-ops), but the python3 matcher runs outside spawn and hard-errors ("No … release found") because the probe file was never fetched — dry-run does not print a would-be trace. Mirrors scrcpy's dry-run behavior; no defect in mutation-safety | common.sh:79-82 (spawn DRY_RUN branch), llamacpp.sh:30-44 |
|
||||
| N2 | NOTE | `apps/ai/llamacpp.sh:63` | Uninstall guard keys on `llama-server` on PATH; a partial install with no symlink leaves `/usr/local/lib/llama.cpp-*` behind on uninstall. Reinstall is safe (line 49 removes the dir first) — edge case only | llamacpp.sh:63, 49 |
|
||||
| N3 | NOTE | `apps/ai/llamacpp.sh:25-27` + `lib/common.sh:114` | First-spawn (release JSON fetch) failure exits via `spawn`'s `exit "$rc"` before the JSON cleanup runs (line 42/56); a mid-transfer abort can leave a partial `/tmp/llamacpp-releases.json`. No `trap` for the process. Minor | common.sh:114, llamacpp.sh:25-27 |
|
||||
|
||||
## Verification verified
|
||||
|
||||
- All structure/idempotence/dispatch claims for `apps/ai/llamacpp.sh` (Step 1 list) — read-only inspection.
|
||||
- `apps/install.sh` CAT_NAMES + auto-discovery; `templates/app.sh` one-comment-line diff; `bin/pos-ai-server` exactly 3 text lines (256/397/563), no logic change, GitHub URL kept, `--apps`/`--full` accurate (root `install.sh:57-58,78-79`); `DOC/POS.md` one-line hint; AGENT_TODO entry correct and non-duplicated; scope = exactly the claimed file set; hygiene clean; uninstall symlink-scoped removal (no blanket `rm llama*`); temp cleanup on success + matcher-failure paths; lint apps-class rules satisfied (`lint-conventions.sh:192-204`).
|
||||
|
||||
## Verification unverified
|
||||
|
||||
- Live GitHub API probe (nightly `bNNNNN` asset naming, `.tar.gz` not `.zip`, top-level dir layout, `/releases/latest` having no binaries) — external network probe; not re-run within read-only boundary. Static construction (per_page=10 scan, endswith matcher, strip-components=1) is consistent with the Builder's claimed evidence.
|
||||
- `make gen` idempotence, `make check`, `make lint` runs — not re-run (do-not-run-make instruction); Builder-reported outputs are consistent with the script rules I inspected (gen scans only `bin/pos-*` `# POS:` headers; none changed).
|
||||
- Runtime behavior of DRY_RUN and failure paths — analyzed statically only.
|
||||
|
||||
## Scope compliance
|
||||
|
||||
- In-scope confirmed: new `apps/ai/llamacpp.sh`; `apps/install.sh` line; `templates/app.sh` line; `DOC/APPS.md` 3-line edit; `bin/pos-ai-server` 3-line text edit; `DOC/POS.md` 1-line edit; `AGENT_TODO.md` 1 entry; builder report.
|
||||
- Out-of-scope found: none. No lib/config/core-installer/generated-block changes.
|
||||
|
||||
## Remaining uncertainty
|
||||
|
||||
- Whether `chmod +x` is applied before commit (R1) and whether the count is corrected to 18 (R2).
|
||||
- Whether the failure-masking (S1) should be addressed in this change or deferred as a class-level fix (scrcpy shares it). Not a blocker; owner decision.
|
||||
|
||||
## Recommended next agent
|
||||
|
||||
**Builder**
|
||||
|
||||
## Reason
|
||||
|
||||
Two REQUIRED findings within the approved scope of this change: (1) `chmod +x apps/ai/llamacpp.sh` before `git add` (100755 uniformity with all tracked apps); (2) correct `DOC/APPS.md:3` count from 16 → 18 (and, if touched, the AGENT_TODO wording). Both are small, understood fixes — no design/architecture input needed. After fixing, re-run `bash -n`, `make check`, `make lint`.
|
||||
|
||||
## Changes made by Reviewer
|
||||
|
||||
none
|
||||
@@ -1,124 +0,0 @@
|
||||
# Reviewer Report — Acceptance review of Builder R1/R2 fixes + whole pending diff
|
||||
|
||||
Date: 2026-09-06
|
||||
Reviewer: independent (read-only) reviewer — final acceptance gate
|
||||
Reviewed refs: working tree over HEAD `0856b25` + untracked `bin/pos-ai-llamacpp`
|
||||
Inputs read: Builder R1/R2 fix report (`AgentsReport/builder/2026-09-06_r1r2-fixes.md`), prior reviews (rereview + full review), the full `git diff HEAD`, `bin/pos-ai-server`, `bin/pos-ai-hf` (full reads), `bin/pos-ai-llamacpp`, `bin/pos`, `bin/pos-ai`, `completions/pos.bash`, `AGENT_TODO.md`, doc diffs (`DOC/POS.md`, `DOC/howto/ai.md`, `DOC/AGENT_Context_Project.md`), AGENTS.md / `DOC/DEV.md` conventions.
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status:** APPROVE_WITH_NOTES
|
||||
- **Verdict:** Both REQUIRED findings from the prior re-review are **FIXED with supporting evidence**. R1 (systemd ExecStart quoting) and R2 (partial-failure honesty) both PASS by direct code read + diff. The whole pending diff (wire-up, docs, hygiene, completions) is internally consistent and committable. No BLOCKING or REQUIRED findings remain.
|
||||
- **Defect counts this pass:** 0 BLOCKING, 0 REQUIRED, 0 SUGGESTED, 3 NOTE (1 carryover-scope item recorded, 2 low-risk observations).
|
||||
- **Read-only constraint:** gates (`bash -n`, `make gen`/`check`/`lint`) and the live probes were executed by the Builder/Orchestrator, not independently reproduced here (sandbox denies execution). Accepted per the Orchestrator's instruction not to re-run make; the live evidence (real `systemd-analyze verify` rc 0 + 16-token word-split assertion; 2-file forced-failure probe) is consistent with the code as read.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: R1 — systemd ExecStart quoting (`bin/pos-ai-server`)
|
||||
|
||||
- `systemd_quote()` (`bin/pos-ai-server:388-392`): `value="${value//\"/\\\"}"` then `printf '"%s"'` — wraps in systemd double quotes and escapes any embedded `"` as `\"`. This matches systemd.service(5) double-quote rules (double quotes preserve whitespace; `\"` escapes a quote).
|
||||
- Use (`bin/pos-ai-server:445`): `exec_cmd="$(systemd_quote "$llamacpp_full") -m $(systemd_quote "$model") --port $PORT --host $HOST"`. Quoting applies **only** to the executable and the model path — the two tokens that may legally contain spaces. Plain flag/number tokens are appended unquoted (`--n-gpu-layers`, `--ctx-size`, `--threads`, and conditionally `--gpu-threads`, `--tensor-split`, `--batch-size`, `--ubatch-size`, `--temperature`, `--top-k`, `--top-p`, `--repetition-penalty`, `--kv-cache`, `--slots`, and bare `--mmap`/`--mlock`/`--metrics`/`--health`). No over-quoting.
|
||||
- Dry-run parity: `log "(dry-run) ExecStart: $exec_cmd"` (`bin/pos-ai-server:494`) uses the exact same `$exec_cmd` string later written to the unit (`ExecStart=$exec_cmd`, `bin/pos-ai-server:508`). Byte-identical by construction.
|
||||
- Unit validity: `ExecStart="/usr/bin/llama-server" -m "/home/me/My Models/m.gguf" --port 8088 --host 127.0.0.1 ...` is a single valid systemd ExecStart line. systemd splits on unquoted whitespace and honors the double-quoted tokens as single args, so a model path with spaces survives. This is the precise failure class R1 targeted — now resolved.
|
||||
- Sanity check on OTHER user-provided values appended raw: `$PORT`, `$HOST`, `$gpu_layers`, `$GPU_THREADS`, `$TENSOR_SPLIT`, `$BATCH_SIZE`, `$UBATCH_SIZE`, `$TEMPERATURE`, `$TOP_K`, `$TOP_P`, `$REPETITION_PENALTY`, `$KV_CACHE_SIZE`, `$SLOTS`. Each is a numeric or address/split token where a space is not a legal value (host = IP/hostname; tensor-split = comma/semicolon GPU list; the rest numeric). The only tokens where spaces are legitimate (filesystem paths) are the two that ARE quoted. No realistic spaced-value breakage remains. (Defense-in-depth could quote all of them, but that is not required and would not change behavior for legal inputs — NOTE 2.)
|
||||
- `systemd-analyze verify`: Builder reports rc 0 on a spaced-path unit and an asserted word-split of 16 tokens. Not re-run here (execution denied); the unit text as read is plausible and valid.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 2: R2 — partial-failure honesty (`bin/pos-ai-hf`)
|
||||
|
||||
- **`failed_files` scoping**: `local failed_files=()` declared at `bin/pos-ai-hf:632` inside `cmd_download` (function scope). Populated ONLY in the parallel branch (`:671`, `:685`). The sequential branch never touches it, so it stays empty there.
|
||||
- **`.hf-meta` gated on zero failures**: `if [ "${#failed_files[@]}" -eq 0 ]; then` (`:727`) writes meta; `else` (`:742-744`) `warn "Not writing .hf-meta — ${repo_id} is incomplete (N file(s) failed)"`. No complete-meta is written after partial failure. Confirmed.
|
||||
- **Summary honesty**: failure branch (`:757-761`) `success_count=$((file_count - ${#failed_files[@]}))` and prints `📥 Downloaded: %s (%d of %d files, %d failed: %s)`. The success-only summary `(%d files, %s)` (`:762-766`) is in the `else`, so it cannot appear when any file failed. No misleading "Downloaded:" success line on partial failure. Confirmed.
|
||||
- **Exit rc 1**: `if [ "${#failed_files[@]}" -gt 0 ]; then return 1; fi` (`:773-775`). Reachable only from the parallel path (sequential never populates the array). Confirmed.
|
||||
- **Sequential path unchanged**: the `else` sequential single-file branch (`:700-721`) is not among the diff's changed lines — `if ! hf_download_with_progress ...; then warn "Failed to download $fname"; continue; fi` retains its warn-and-continue, rc-0 semantics. The Builder's "byte-for-byte unchanged" claim is consistent with the diff. Confirmed.
|
||||
- Builder's live probe output (`[2/2] Failed: model-fail.bin`; `Not writing .hf-meta`; `1 of 2 files, 1 failed: model-fail.bin`; `rc=1`; dir lacks `.hf-meta`; `list` empty; single-file sequential success/failure unchanged) matches all of the above by code read.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 3: Whole pending diff — wire-up
|
||||
|
||||
- `bin/pos-ai-llamacpp` (7 lines) is a byte-for-byte mirror of `bin/pos-ai-gemini` (`bin/pos-ai-gemini:1-7`) with only the provider name changed (`gemini`→`llamacpp`); `# POS_SUBCMDS: ask chat models sessions capture` matches the gemini forwarder and `lib/ai-providers/llamacpp.sh` capabilities.
|
||||
- `bin/pos-ai:701-704` `llamacpp)` case → `exec "$0" --provider llamacpp "${args[@]}"`, mirroring the gemini/openrouter forwarder cases. usage() provider lists updated (`bin/pos-ai:42`, `:59`).
|
||||
- `bin/pos:269` INTERACTIVE_CMDS adds `ai-llamacpp` (chat reads stdin → tee-pipe guard) — required and consistent with the reverse lint rule.
|
||||
- `completions/pos.bash`: `_pos_subcmds[ai-llamacpp]="ask chat models sessions capture"` and `[ai]` list includes `llamacpp`; `_pos_flags[ai-hf]`/`[ai-server]` expanded to match the headers. All consistent.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 4: Whole pending diff — docs & completions
|
||||
|
||||
- `DOC/POS.md`: ai section references the three forwarders incl. llamacpp (`:55`); `--provider` row `(gemini\|openrouter\|llamacpp)` (`:69`); backward-compat sentence (`:82`); AI_PROVIDER config row (`:87`); hf download row documents `--branch`/`--revision` alias + `--include`/`--exclude` glob (`:105`); hf `cache [status|clear]` row added (`:108`); server flags row expanded + version-aware validation note (`:122`). All factual vs the code.
|
||||
- `DOC/howto/ai.md`: adapter list, `--provider` backend list, backward-compat shorthand, and "Available providers" table row (llamacpp, `LLAMACPP_MODEL`) — factual vs `lib/ai-providers/llamacpp.sh`.
|
||||
- `DOC/AGENT_Context_Project.md`: generated blocks (docmap line shifts, tree row, dispatch row, selfcontained, filetable) internally consistent with headers. Filetable line counts match `wc -l` exactly: `pos-ai-hf` 1004, `pos-ai-server` 659, `pos-ai` 706, `pos-ai-llamacpp` 7, `completions/pos.bash` 313, `bin/pos` 302.
|
||||
- `AGENT_TODO.md`: all Done entries dated (2026-09-06 / 2026-09-05). No un-dated entries introduced.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 5: Hygiene / secrets / exec bits
|
||||
|
||||
- No `/tmp/opencode` references in any source file (grep clean).
|
||||
- No stray debugging/temporary code: grep for `DEBUG|print_r|console.log|TODO|FIXME|HACK|probe|stub` hits only comment words ("version probe", "probe the resolved binary") and a pre-existing unrelated alias tmpfile — no debug blocks or commented-out code in the changed files.
|
||||
- No secret literals: `HF_TOKEN` is read at runtime from env/config (`bin/pos-ai-hf:27,187`) — no embedded key/token. The `# POS_CONFIG: ... HF_TOKEN=secret:...` header is the declared masking classification, not a value. No `sk-`/`AIza`/`gh*_` style literals in the changed files.
|
||||
- Tracked exec bits confirmed 100755 via `git ls-files -s` for `bin/pos`, `bin/pos-ai-hf`, `bin/pos-ai-server`; `bin/pos-ai`, `bin/pos`, `completions` tracked normally. The untracked `bin/pos-ai-llamacpp` exec bit **cannot be confirmed via git** (untracked; `stat` denied by sandbox) — see NOTE 1.
|
||||
|
||||
[PASS]
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
1. **NOTE** — untracked `bin/pos-ai-llamacpp` exec bit is UNVERIFIED via git (untracked file; `stat` denied by sandbox). It is a byte-for-byte mirror of `bin/pos-ai-gemini` (a tracked 100755 forwarder), and both the Maintainer's earlier `stat` claim and the Builder report assert 100755. LOW RISK; the dispatcher's `make gen`/check gates (already green per Orchestrator) would catch a missing exec bit. No action required if `git add` preserves exec-bit from the on-disk mode.
|
||||
2. **NOTE** — additional user-provided flag tokens (`--host`, `--tensor-split`, numeric flags) are appended unquoted. Each is a token type where a space is not legal, so no realistic breakage; this matches the brief's intended scope (quote only path-capable tokens). Optional hardening, not required.
|
||||
3. **NOTE** — the two SUGGESTED items carried from the prior re-review (`--list` ignores `--include/--exclude`; bash glob case-sensitivity) remain outstanding. They were NOT REQUIRED findings in either the re-review or this acceptance brief, and the R1/R2 scope was intentionally constrained to the two REQUIRED defects. Recorded for a future pass, not a blocker.
|
||||
|
||||
---
|
||||
|
||||
## Per-item status
|
||||
|
||||
| Item | Status | Evidence |
|
||||
|------|--------|----------|
|
||||
| R1 — systemd ExecStart quoting | **PASS** | `systemd_quote` (:388-392) escapes+quotes; binary+model only (:445); dry-run shares exact `$exec_cmd` (:494); unit writes same (:508); other tokens space-legal-check (NOTE 2) |
|
||||
| R2 — partial-failure honesty | **PASS** | `failed_files` function-scoped (:632); meta gated (:727-744); honest summary (:757-761); rc 1 (:773-775); sequential path untouched |
|
||||
| Wire-up (llamacpp) | **PASS** | Byte mirror of gemini forwarder; dispatch case; INTERACTIVE_CMDS; completions |
|
||||
| Docs & completions | **PASS** | POS.md / howto/ai.md / AGENT_Context factual; filetable line counts all match `wc -l` |
|
||||
| Hygiene / secrets | **PASS** | No `/tmp/opencode`, no stray debug, no embedded secrets; tracked exec bits 100755 (NOTE 1 for untracked exec bit) |
|
||||
| Gates | **Verified via Orchestrator handoff** | bash -n OK, make gen idempotent, make check `check-sync: OK`, make lint `0 FAIL, 0 WARN` (not re-run here per instruction) |
|
||||
|
||||
---
|
||||
|
||||
## Verification verified
|
||||
|
||||
- R1 `systemd_quote` implementation, use-site, dry-run/unit byte-parity, and systemd-valid ExecStart form — FACT by code read + diff (all in-scope lines cited above).
|
||||
- R2 failed_files scoping, meta gating, honest summary, rc-1 exit, and unchanged sequential path — FACT by code read + diff.
|
||||
- Wire-up (forwarder mirror, dispatch case, INTERACTIVE_CMDS, completions) — FACT by read.
|
||||
- Docs factual vs code + adapters; filetable line counts match `wc -l` exactly — FACT.
|
||||
- No `/tmp/opencode`, no stray debug, no secret literals in changed files — FACT by grep.
|
||||
- Tracked exec bits 100755 — FACT via `git ls-files -s`.
|
||||
- FINAL_SUMMARY/IMPLEMENTATION_PLAN + audit files remain untracked (Orchestrator commit decision, out of review scope) — consistent with prior passes.
|
||||
|
||||
## Verification unverified
|
||||
|
||||
- Gate commands (`bash -n`, `make gen` x2, `make check`, `make lint`, `systemd-analyze verify`) and the live probes — NOT independently reproduced in this sandbox (execution denied). Accepted as Orchestrator-handoff evidence: claims are internally consistent, match the code as read, and the probe outputs match the expected post-fix behavior exactly.
|
||||
- Exec bit of untracked `bin/pos-ai-llamacpp` — UNVERIFIED via git (stat denied); asserted 100755 by Maintainer/Builder, healthy risk.
|
||||
|
||||
## Scope compliance
|
||||
|
||||
- In-scope, delivered: R1 (ExecStart quoting) and R2 (partial-failure honesty) — both confirmed.
|
||||
- In-scope, unchanged/documented: sequential single-file failure behavior preserved per the "don't touch" constraint.
|
||||
- Out-of-scope changes: none found in source. The two carried SUGGESTED items are recorded as NOTE 3 (outside R1/R2 scope).
|
||||
- No new flags/subcommands/config keys introduced by the R1/R2 pass (docs/completions refreshed only by `make gen`).
|
||||
|
||||
## Remaining uncertainty
|
||||
|
||||
- Gate/probe results rest on the Builder/Orchestrator's reported runs rather than an observed run in this review sandbox. The code-level evidence independently confirms each claim to the extent a static read allows; the only residue is empirical (a real `systemd-analyze verify` on this tree, a real forced-failure parallel download, a real spaced-path `server start`), which the Builder reports green.
|
||||
- Untracked `bin/pos-ai-llamacpp` exec bit.
|
||||
|
||||
## Recommended next agent
|
||||
|
||||
**Orchestrator**
|
||||
|
||||
**Reason:** The verdict is final — APPROVE_WITH_NOTES. R1 and R2 are fixed and verified at the code level; the pending diff is committable. The Orchestrator should commit the reviewed source set (decision on untracked plan/report files as previously), then close the workflow. The two carried SUGGESTED items (NOTE 3) can be scheduled as a future builder pass; neither blocks this commit.
|
||||
|
||||
## Changes made by Reviewer
|
||||
|
||||
none
|
||||
@@ -1,208 +0,0 @@
|
||||
# Reviewer Report — POS AI tooling (hf / server enhancement + llamacpp forwarder)
|
||||
|
||||
Date: 2026-09-06
|
||||
Reviewer: independent (read-only) reviewer
|
||||
Reviewed refs: working tree `0856b25` (HEAD = master baseline `0856b25`) + uncommitted maintainer changes
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status:** CHANGES_REQUIRED
|
||||
- **Verdict:** The `llamacpp` forwarder + dispatch + docs + completions work is APPROVABLE. The enhanced `pos-ai-hf` and `pos-ai-server` contain two BLOCKING correctness failures and several REQUIRED defects. The approval gates (`make gen` / `make check` / `make lint`) were NOT RUN in this review (see Step 6 — sandbox blocked, UNVERIFIED).
|
||||
- **Defect counts:** 2 BLOCKING, 5 REQUIRED, 4 SUGGESTED, 2 NOTE.
|
||||
- Primary defects: (1) `pos ai hf download --include/--exclude` can never complete a download (jq stream-vs-array + glob-vs-regex + raw interpolation); (2) `pos ai server start` generates a malformed systemd unit (flags appended as non-continued lines) — `enable --now` cannot work; regression vs the pre-enhancement inline heredoc.
|
||||
- **Next agent:** Builder (fixes understood, in-scope), then Orchestrator to run gates + live verification.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Contract & scope
|
||||
|
||||
Read and cross-checked against the named inputs: `AUDIT.md`, `AUDIT_TABLE.md`, `IMPLEMENTATION_PLAN.md`, `FINAL_SUMMARY.md`, `AgentsReport/builder/2026-09-05_enhanced_pos_ai_tools.md`, `AgentsReport/builder/2026-09-06_llamacpp-forwarder.md`, `AgentsReport/maintainer/2026-09-06_restore-cleanup.md`, `AgentsReport/maintainer/2026-09-06_convention-sweep.md`.
|
||||
|
||||
- Every hunk in the tree diff traces to an approved plan item (hf patterns, parallel download, cache, info/files, server GPU/flags/version-awareness, llamacpp forwarder, docs/completions regen).
|
||||
- FINAL_SUMMARY honestly discloses `cache` and version-feature-validation as stubs.
|
||||
- No invented out-of-scope files: the tree diff touches exactly `bin/pos`, `bin/pos-ai`, `bin/pos-ai-hf`, `bin/pos-ai-llamacpp` (new), `DOC/*`, `completions/pos.bash`, `AGENT_TODO.md`.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 2: Work tree & git history
|
||||
|
||||
- `git status --short`: modified `AGENT_TODO.md`, `DOC/AGENT_Context_Project.md`, `DOC/POS.md`, `DOC/howto/ai.md`, `bin/pos`, `bin/pos-ai`, `bin/pos-ai-hf`, `completions/pos.bash`; untracked `AUDIT{,.md,_TABLE.md}`, `FINAL_SUMMARY.md`, `IMPLEMENTATION_PLAN.md`, `AgentsReport/*`, `bin/pos-ai-llamacpp`. Matches the maintainer/builder reports; `bin/pos-ai-server` unchanged in tree (committed in `0856b25`).
|
||||
- `git log --oneline -15`: `0856b25` enhance pos-ai-server/hf, `387f23f` parallel download, `2794122` server flags, `99c033c` base hf, `5e312b3` base server.
|
||||
- `git diff 0856b25~1..0856b25` (ablated) and `git diff 2794122 387f23f`:
|
||||
- Confirmed regression: pre-enhancement `branch="$(hf_resolve_branch "$repo_id" "$BRANCH")"` was changed to `"$REVISION"` (now dead `--branch` flag, see F3).
|
||||
- Confirmed include/exclude block, parallel block, cache stub, dead `err_with_context`/`run_parallel_download`, and the duplicate `# POS_EXAMPLES:` lines (3) later removed by the maintainer.
|
||||
- Confirmed server-side: `5e312b3` had a correct **single-line** `ExecStart=… --n-gpu-layers $gpu_layers --ctx-size $CTX_SIZE --threads $THREADS` inside the heredoc; the enhancement replaced it with a truncated heredoc + line-by-line `echo >>` flag appends (F2).
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 3: llamacpp forwarder, dispatch, completions, docs sync
|
||||
|
||||
- `bin/pos-ai-llamacpp` (7 lines) matches `bin/pos-ai-gemini` byte-for-byte except provider name; `-h|--help` → `exec pos ai --provider llamacpp --help`; exec fallthrough. `# POS_SUBCMDS: ask chat models sessions capture` exactly matches `lib/ai-providers/llamacpp.sh` capabilities (provider_generate ask/chat/sessions/capture; provider_models_list models).
|
||||
- `bin/pos-ai:701-704` adds the `llamacpp)` shorthand case → `exec "$0" --provider llamacpp "${args[@]}"`; usage lines 42/59/81 mention `llamacpp`; POS_CONFIG lists `AI_PROVIDER` incl. `llamacpp` + `LLAMACPP_*` vars (line 6).
|
||||
- `bin/pos:269` INTERACTIVE_CMDS includes `ai-llamacpp` (stdin/tee gotcha respected; dispatcher longest-prefix resolution picks `pos-ai-llamacpp` length 12 > `pos-ai` length 2).
|
||||
- `completions/pos.bash:33` `_pos_subcmds[ai-llamacpp]="ask chat models sessions capture"`, line 51 `[ai]` includes `llamacpp` — matches the forwarder header.
|
||||
- `DOC/AGENT_Context_Project.md`: tree row 68, dispatch row 287, selfcontained row 374, filetable row 619 all consistent; hf dispatch row 286 lists exactly 11 examples matching the 11 `# POS_EXAMPLES:` lines in `bin/pos-ai-hf`; filetable rows 618/619/621 (hf 912 / llamacpp 7 / server 613) match `wc -l`; docmap + line-count rows updated.
|
||||
- `DOC/POS.md:58-59,72,82,90` and `DOC/howto/ai.md:5,19,30-31,139` include llamacpp consistently.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 4: pos-ai-hf correctness
|
||||
|
||||
Read the full file (912 lines). Verified working paths: search API/retry, explicit filename (`--arg fn`), GGUF filter + quant gate + pagination, list/remove/info/files, `.hf-meta`. FAILURES listed in Findings F1, F3, F4 (and S7/S8).
|
||||
|
||||
[FAIL]
|
||||
|
||||
## Step 5: pos-ai-server correctness
|
||||
|
||||
Read the full file (613 lines). Verified working logic: GPU precedence is correct (`--gpu-layers` flag > env/config > auto, lines 376-379; `resolve_gpu_layers` 72-85), health-check, model picker (reads `/dev/tty`, not stdin). FAILURES listed in Findings F2, F5 (S8).
|
||||
|
||||
[FAIL]
|
||||
|
||||
## Step 6: Conventions, gates, maintainability
|
||||
|
||||
Static convention checks (by reading `scripts/lint-conventions.sh`, `scripts/check-sync.sh`, `make gen` inputs):
|
||||
|
||||
- Shebang, `set -euo pipefail`, `# POS:` headers with `—`, exec-bit (100755), `-h|--help` after deps guards: all present in the touched files; `DOC/POS.md` references all four scripts; `ai-llamacpp` in INTERACTIVE_CMDS; no stdin readers missing; no `local` at top level; heredoc delimiters balanced; no secret literals; no raw `/etc/`/`/usr/local` writes found statically.
|
||||
- Generated blocks (`tree`, `dispatch`, `selfcontained`, `filetable`, `docmap`, `completions`) are internally consistent with headers — no hand-edit evidence.
|
||||
- **BLOCKED:** `bash -n`, `make gen --check`, `make check`, `make lint` CANNOT be run in this sandbox (bash tool permission denies every command outside the git/head/tail/wc/sort/grep/rg allow-list). Claims of green gates are UNVERIFIED.
|
||||
- Maintainability: F4 dead code (`run_parallel_download`, `hf_download_file`, `err_with_context`, `temp_dir`) is avoidable complexity introduced by the change.
|
||||
|
||||
[BLOCKED: sandbox denies `bash`/`make` execution — required verification must be run by Orchestrator/Builder outside this review]
|
||||
|
||||
## Step 7: Documentation accuracy
|
||||
|
||||
- llamacpp/ai docs: accurate (see Step 3).
|
||||
- hf docs: `DOC/POS.md:108` documents `--branch <rev>` as a working option — **false**, the flag is dead (F3). `DOC/POS.md` omits `--include/--exclude/--revision`; `DOC/AGENT_Context_Project.md:286` showcases `--include "*.gguf" --exclude "*Q4_*"` — the showcased example cannot work (F1).
|
||||
|
||||
[FAIL]
|
||||
|
||||
## Step 8: Verdict synthesis
|
||||
|
||||
The approved scope is only partially met: the llamacpp forwarder work is complete and correct; the hf/server enhancements carry BLOCKING correctness defects and shortfalls in the plan's own matrix (cache stub, version-feature-validation stub). Both BLOCKING items are in-scope implementation defects with understood fixes → Builder.
|
||||
|
||||
[FAIL]
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### F1 — BLOCKING — `pos ai hf download --include/--exclude` can never complete a download
|
||||
|
||||
- Severity: BLOCKING
|
||||
- Evidence: `bin/pos-ai-hf:600-614` builds filters as jq **streams**: `include_filter=".[] | select(.rfilename | match(\"$INCLUDE_PATTERN\"; \"i\") | length > 0)"` (line 606) and the exclude analog (612). A stream of objects is then consumed by array-expecting code:
|
||||
- Line 621 `file_count="$(… | jq 'length')"` — `length` applied per input object → key count ("2\n2" for ≥ 2 matches) → `[ "$file_count" -eq 0 ]` at 622 errors "integer expression expected" → `set -e` exit; with exactly 1 match `file_count="2"` → line 662 `[ "$file_count" -gt 1 ]` misroutes the single file into the parallel branch.
|
||||
- Parallel branch (661-702) reads via `jq -c '.[]'` which, on an object-stream input, yields the object **values** (strings/numbers); `fname="$(… | jq -r '.rfilename')"` then fails ("Cannot index string with .rfilename") → `set -e` exit.
|
||||
- `total_size` at 644 also assumes an array (`[.[].size // 0]`).
|
||||
- Interpolation: patterns are embedded verbatim into the double-quoted jq program (no `--arg`, lines 606/612) — quotes/`$`/backslashes in a pattern corrupt the program.
|
||||
- Glob-vs-regex: usage/`# POS_FLAGS:`/POS.md/AGENT_Context all advertise glob patterns (`*.gguf`); `match()` applies **regex** semantics, and `*.gguf` is an invalid regex (leading quantifier) under jq's regex engine → jq error even before the stream issue.
|
||||
- Relevant files/lines: `bin/pos-ai-hf:600-614, 621-622, 644, 661-702`; `DOC/AGENT_Context_Project.md:286`.
|
||||
- Approved scope reference: IMPLEMENTATION_PLAN "Filtering (include/exclude patterns)"; FINAL_SUMMARY claims "pattern-based filtering".
|
||||
- Why it matters: the flagship documented example of the enhancement crashes on every invocation; core download feature partially unusable; misleading docs.
|
||||
|
||||
### F2 — BLOCKING — `pos ai server start` generates a malformed systemd unit (flags never reach llama-server)
|
||||
|
||||
- Severity: BLOCKING
|
||||
- Evidence: `bin/pos-ai-server:403-411` heredoc writes only `ExecStart=$llamacpp_full -m $model --port $PORT --host $HOST` and closes with `EOF` at line 411. Lines 414-464 then append each flag as its own line with a 2-space indent and **no trailing `\`**: `echo " --n-gpu-layers $gpu_layers" >> …` (415), `--ctx-size` (418), `--threads` (421), `--gpu-threads` (424), `--tensor-split` (427), `--batch-size` (430), `--ubatch-size` (433), `--temperature` (436), `--top-k` (439), `--top-p` (442), `--repetition-penalty` (445), `--mmap` (448), `--mlock` (451), `--kv-cache` (454), `--metrics` (457), `--health` (460), `--slots` (463). systemd.service(5) requires a trailing `\` for continuation; these lines are invalid unit syntax and are never passed to llama-server. `gpu_layers` is always non-empty (`resolve_gpu_layers` returns a number/layer count; 377-379), so the first broken line is always appended; line 467 `Restart=on-failure` is similarly misplaced outside `[Service]`-continuation.
|
||||
- Regression: `git show 5e312b3:bin/pos-ai-server` had a single valid inline `ExecStart=… --n-gpu-layers $gpu_layers --ctx-size $CTX_SIZE --threads $THREADS` inside the heredoc; the enhancement (0856b25, confirmed in its diff) replaced it with this broken scheme.
|
||||
- Dry-run output (line 396) shows the *intended* single-line command, masking the defect.
|
||||
- Relevant files/lines: `bin/pos-ai-server:403-467`.
|
||||
- Approved scope reference: IMPLEMENTATION_PLAN "systemd unit generation with new flags".
|
||||
- Why it matters: `systemctl --user enable --now` fails or starts a flagless server; GPU layers/ctx/threads/sampling options silently never apply — the core feature of the enhancement.
|
||||
|
||||
### F3 — REQUIRED — `pos ai hf --branch` regressed to a dead flag; docs still claim it works
|
||||
|
||||
- Severity: REQUIRED
|
||||
- Evidence: `bin/pos-ai-hf:112,126` parse `--branch` into `BRANCH`, but the only two consumers use `REVISION`: lines 566 and 881 `branch="$(hf_resolve_branch "$repo_id" "$REVISION")"`. `git diff 0856b25~1..0856b25` shows the regression (`$BRANCH` → `$REVISION`). `DOC/POS.md:108` documents `--branch <rev>` as functional.
|
||||
- Related guard quirk: the parse-loop guard errors only when **both** `--include` and `--exclude` are set with `--gguf`; `--gguf --include "pat"` (include only) silently drops filtering (code at ~107-110 guard; GGUF branch 595-599 runs, include ignored) — include-only and exclude-only are each valid intents.
|
||||
- Relevant files/lines: `bin/pos-ai-hf:112,126,566,881`; `DOC/POS.md:108`.
|
||||
- Approved scope reference: existing documented option; convention "no stale flags".
|
||||
- Why it matters: doc-vs-code contradiction; users passing `--branch` silently get the default branch; violates stale-flag convention.
|
||||
|
||||
### F4 — REQUIRED — parallel download aborts the entire batch on the first failed file and orphans remaining jobs; dead machinery shipped
|
||||
|
||||
- Severity: REQUIRED
|
||||
- Evidence: `bin/pos-ai-hf:686` `wait "${job_pids[0]}"` (and 696 `wait "$pid"`) return the background job's exit status; under `set -euo pipefail` a single failed download (404 shard, network blip) terminates the whole command at 686, leaving the remaining background jobs running detached and `.hf-meta` unwritten. Helpers `run_parallel_download` (509-517), `hf_download_file` (449-470), `err_with_context` (400-409), and `temp_dir` (663-664, 702) are dead code; progress text (688-690 area) prints the just-started `$fname`, not the completed job.
|
||||
- Relevant files/lines: `bin/pos-ai-hf:661-702, 400-409, 449-470, 509-517`.
|
||||
- Approved scope reference: IMPLEMENTATION_PLAN "parallel downloads with failure handling".
|
||||
- Why it matters: failure path is exactly what a downloader must survive; misleading progress; avoidable complexity (lint/maintainability).
|
||||
|
||||
### F5 — REQUIRED — `pos ai server status` crashes when `llama-server` isn't installed (or isn't named literally); version-feature validation is a print-only stub
|
||||
|
||||
- Severity: REQUIRED
|
||||
- Evidence: `detect_llama_version` (50-54) is `version="$(llama-server --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)"`; under `set -euo pipefail` a missing binary (127) or non-matching output (1) makes the substitution fail and, via the caller `version="$(detect_llama_version)"` at 568 (cmd_status) and 363 (cmd_start), the script exits instead of printing `version: unknown` (569-573 intended). `find_llamacpp` (40-47) can resolve to `server`/`llama.cpp/server`/`llama-server-cuda`, but the version probe still hardcodes `llama-server` → `start` too can crash even when a server binary exists. `validate_server_features` (57-61) prints "Feature validation would occur here" — the plan's "reject unsupported flags" behavior is not implemented for any flag.
|
||||
- Relevant files/lines: `bin/pos-ai-server:40-61, 363-366, 568-573`.
|
||||
- Approved scope reference: IMPLEMENTATION_PLAN/AUDIT_TABLE "version detection + validate feature support".
|
||||
- Why it matters: status must never crash (esp. on a fresh box without llama.cpp); claimed safety gate is cosmetic.
|
||||
|
||||
### F6 — REQUIRED — approved-scope shortfalls: `hf cache` and server feature-validation are stubs
|
||||
|
||||
- Severity: REQUIRED
|
||||
- Evidence: `bin/pos-ai-hf:897-900` `cmd_cache` prints "not fully implemented yet" (and is indented 4 spaces at top level — style drift that the `local`-depth lint rule tolerates but should not ship); version validation stub (F5). Disclosed honestly in FINAL_SUMMARY/AGENT_TODO, but the plan named both as deliverables.
|
||||
- Relevant files/lines: `bin/pos-ai-hf:897-900`; `bin/pos-ai-server:57-61`.
|
||||
- Approved scope reference: IMPLEMENTATION_PLAN (cache management, feature validation).
|
||||
- Why it matters: scope is incomplete; either implement or record explicit deferral with owner.
|
||||
|
||||
### S7 — SUGGESTED — pattern-filter UX gaps and doc incompleteness
|
||||
|
||||
- Severity: SUGGESTED
|
||||
- Evidence: `--list`/`files` modes ignore `--include/--exclude` (573-581, 876-895); single-file mode ignores patterns too (586-594); guard requires both flags (F3); `DOC/POS.md` omits the new flags (only usage + AGENT_Context carry them); `hf_gguf_quant_gate` added an unreachable empty-json guard (333-358).
|
||||
- Why it matters: feature boundaries are undocumented and inconsistent; does not block acceptance of the happy paths.
|
||||
|
||||
### S8 — SUGGESTED — minor code cleanliness in pos-ai-server
|
||||
|
||||
- Severity: SUGGESTED
|
||||
- Evidence: trailing `echo " "` (466) appends a blank line to the unit; `resolve_gpu_layers` auto-CUDA returns `-1` which the docs describe as "(-1=auto)" — consistent but worth a comment; dry-run log (396) diverges from what the unit contains (already F2-related).
|
||||
- Why it matters: none blocking; housekeeping.
|
||||
|
||||
### N9 — NOTE — approval gates unverified in this review
|
||||
|
||||
- Severity: NOTE
|
||||
- Evidence: sandbox denies `bash`/`make` (Step 6). Claims "check green / lint 0 WARN" come from maintainer reports, not from an observed run here.
|
||||
- Why it matters: merge-blocker status cannot be confirmed until gates are actually run.
|
||||
|
||||
### N10 — NOTE — GPU precedence logic itself is correct
|
||||
|
||||
- Severity: NOTE
|
||||
- Evidence: `bin/pos-ai-server:376-379` + 72-85: flag > env/config > auto, matching docs. Currently unreachable in effect because of F2.
|
||||
- Why it matters: builder's claim "GPU precedence works" is TRUE as logic; it is the unit file that breaks the outcome.
|
||||
|
||||
---
|
||||
|
||||
## Verification verified
|
||||
|
||||
- Working-tree diff exactly matches maintainer reports (positional, content, line counts).
|
||||
- `--branch` regression introduced by 0856b25; include/exclude + parallel + cache blocks introduced by 0856b25/387f23f.
|
||||
- Forwarder = byte-for-byte mirror of gemini pattern; SUBCMDS consistent with `lib/ai-providers/llamacpp.sh`; dispatch case; INTERACTIVE_CMDS entry; completions; AGENT_Context tree/dispatch/selfcontained/filetable/docmap; POS.md/howto ai-llamacpp rows.
|
||||
- `bin/pos``/bin/pos-ai` llamacpp wiring (lines 269, 701-704) verified by direct read.
|
||||
- Static lint-relevant conventions (headers, guards, `-h|--help` position, heredocs, no stdin gaps, no secret literals) hold for the touched files.
|
||||
|
||||
## Verification unverified
|
||||
|
||||
- `bash -n` on `bin/pos-ai`, `bin/pos-ai-hf`, `bin/pos-ai-server`, `bin/pos-ai-llamacpp` (sandbox denies bash).
|
||||
- `make gen` idempotency (`git diff --exit-code` after regen).
|
||||
- `make check` (check-sync: bash -n + exec-bit + doc-sync + dispatch smoke).
|
||||
- `make lint` (0 FAIL / 0 WARN claim).
|
||||
- Live behavior of F1/F2/F4/F5 (jq stream/glob semantics, systemd unit parse, `wait`+errexit) — my certainty levels: F1 stream/array and F2 unit syntax are FACT by code reading and bash/jq/systemd semantics; F4 `wait` semantics FACT per bash manual; F5 errexit-in-substitution STRONG INFERENCE. All four can be empirically confirmed via the gates + one `pos ai hf download --include "*.gguf"` and one `pos ai server start` probe.
|
||||
|
||||
## Scope compliance
|
||||
|
||||
- In-scope, correctly implemented: llamacpp forwarder + dispatch + docs/table/completions; hf search/explicit-file/GGUF/list/remove/info/files happy paths; server GPU precedence logic, autostart/model-picker helpers.
|
||||
- In-scope, defective: hf include/exclude (F1), hf parallel failure path (F4), server unit generation (F2), server version handling (F5).
|
||||
- In-scope, not delivered: cache command, server feature-validation (F6).
|
||||
- Out-of-scope changes: none found.
|
||||
|
||||
## Remaining uncertainty
|
||||
|
||||
- Gate results (Step 6) — pending Orchestrator/CI run.
|
||||
- Whether pattern matching should be glob or regex going forward — decision for Architect/Builder; current code contradicts its own docs either way.
|
||||
- Exact `wait`/errexit or `match()` behavior under the machine's jq/bash version — empirical confirmations pending.
|
||||
|
||||
## Recommended next agent
|
||||
|
||||
**Builder**
|
||||
|
||||
**Reason:** Two BLOCKING defects (F1, F2) are clear in-scope implementation bugs with understood fixes (array-preserving jq filters + `--arg` + glob-vs-regex decision; single-line heredoc ExecStart with `\` continuations as in the 5e312b3 baseline). F3/F4/F5 are fixable in the same pass. After fixes, re-run `make gen`/`make check`/`make lint`, then Orchestrator performs the live probes (include/exclude download, `server start` unit parse + `--user` enable, one forced-failure parallel download, `server status` without llama-server) and re-hands to Reviewer for final sign-off.
|
||||
|
||||
## Changes made by Reviewer
|
||||
|
||||
none
|
||||
@@ -1,206 +0,0 @@
|
||||
# Reviewer Report — Re-review of Builder fixes F1–F6 + llamacpp wiring + maintainer doc corrections
|
||||
|
||||
Date: 2026-09-06
|
||||
Reviewer: independent (read-only) reviewer
|
||||
Reviewed refs: working tree over HEAD `0856b25` (diff above) + untracked `bin/pos-ai-llamacpp`
|
||||
Inputs read: my previous review (`2026-09-06_pos_ai_full_review.md`), Builder fix report (`2026-09-06_review-fixes.md`), Maintainer sweep (`2026-09-06_convention-sweep.md`), AGENTS.md, `DOC/DEV.md` via lint/check scripts, `lib/ai-providers/llamacpp.sh`, `lib/common.sh`, `scripts/lint-conventions.sh`, `scripts/check-sync.sh`.
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status:** REQUEST_CHANGES (this is a re-review of in-progress work before commit — not final acceptance)
|
||||
- **Verdict:** F1, F3, F5, F6 are FIXED; F2 is PARTIAL (core BLOCKING unit defect fixed, but ExecStart does not quote the model path — paths with spaces still break); F4's failure handling is FIXED but a new REQUIRED honesty defect (misleading summary/`.hf-meta` after partial failure, exit 0) must be fixed. llamacpp wiring and maintainer doc corrections PASS.
|
||||
- **Defect counts this pass:** 2 REQUIRED, 2 SUGGESTED, 2 NOTE. No BLOCKING findings remain.
|
||||
- **Gates:** `bash -n`, `make gen` idempotency, `make check`, `make lint`, `systemd-analyze verify` — **UNVERIFIED** (sandbox denies bash/make; denial quoted in Step 8). Static reading of lint/check rules shows no violation in the touched files.
|
||||
- **Next agent:** Builder (two small, understood, in-scope fixes), then Orchestrator to run gates + live probes and re-hand for final sign-off.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Contract scope re-check
|
||||
|
||||
Named review inputs: previous review findings F1–F6; Builder's fix report; Maintainer convention sweep; AGENTS.md/DEV.md conventions; the FULL pending diff (`git diff HEAD` = working tree over baseline `0856b25`).
|
||||
|
||||
- Diff touches exactly: `AGENT_TODO.md`, `DOC/AGENT_Context_Project.md`, `DOC/POS.md`, `DOC/howto/ai.md`, `bin/pos`, `bin/pos-ai`, `bin/pos-ai-hf`, `bin/pos-ai-server`, `completions/pos.bash` + untracked `bin/pos-ai-llamacpp` and plan/report files (Orchestrator decision, out of review scope). No out-of-scope source changes found.
|
||||
- Builder scope claim ("no changes to bin/pos-ai, bin/pos-ai-llamacpp, bin/pos, README, AGENT_TODO") — **contradicted by the actual diff**: `bin/pos`, `bin/pos-ai`, `AGENT_TODO.md` ARE modified. These are the Maintainer's corrections (usage provider lists, INTERACTIVE_CMDS, AGENT_TODO ledger) rather than Builder changes, so the *combined* tree is consistent — but the Builder report's "did not touch" list is stale. NOTE (report accuracy, not code).
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 2: F1 — include/exclude glob filtering (pos-ai-hf)
|
||||
|
||||
Verified by reading `bin/pos-ai-hf` + grep:
|
||||
|
||||
- (a) **Array shape end-to-end**: `hf_apply_patterns` (`bin/pos-ai-hf:422-447`) consumes the input via `jq -c '.[]'` (array iteration), accumulates entries, and yields `[]` on no match or `jq -c -s '.'` (array) on matches. Downstream consumers all see an array again: `file_count` via `jq 'length'` (593), `total_size` via `[.[].size // 0]` (618), parallel/sequential iterators via `jq -c '.[]'` (674/713), `.hf-meta` via `[.[] | .rfilename]` (721). The old object-stream breakage is gone.
|
||||
- (b) **Composition order**: gguf/filename filter runs first (`bin/pos-ai-hf:566-584`), then the pattern block `gguf/filename → include → exclude` (588-590), matching usage/POS.md wording. The `--gguf + --include/--exclude` pre-check error was removed (541-544 now only guards filename+both-patterns), so patterns compose with `--gguf`.
|
||||
- (c) **No-match error**: count-0 branch errors cleanly `No files match include/exclude patterns in <repo> (branch: <branch>)` (599-600); `err()` exits 1 (`lib/common.sh:24`).
|
||||
- (d) **Quote safety**: patterns never enter a jq program — `hf_apply_patterns` passes them as positional args into bash `case "$fname" in $include)` glob matching (429-439); no `match(`/interpolation remains (grep: no `match(`/`include_filter`/`exclude_filter` in pos-ai-hf). `hf_apply_patterns` defaults `include="${2:-}" exclude="${3:-}"` → no unbound vars under `set -u`; `INCLUDE_PATTERN`/`EXCLUDE_PATTERN` initialized at top (120-121).
|
||||
- (e) **Dead code**: `err_with_context`, `hf_download_file`, `run_parallel_download` — grep across `bin/` finds zero occurrences.
|
||||
|
||||
Carried-over SUGGESTED (from S7, unchanged): `--list` mode (553-563) returns before the pattern block, so `--list --include "*.gguf"` lists everything although POS.md:108 promises "shows exactly what download would fetch". Also new behavior note: bash `case` glob is case-sensitive (old broken regex used `"i"`); docs don't promise case-insensitivity, and the gguf filter itself is case-insensitive — flagging for doc consistency only.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 3: F2 — systemd unit generation (pos-ai-server)
|
||||
|
||||
- ExecStart is ONE line with the full resolved command: `exec_cmd` built at `bin/pos-ai-server:431-477`; unit heredoc writes `ExecStart=$exec_cmd` (495) with no `echo >>` appends anywhere (git diff confirms removal of the entire append block); unit structure `[Unit]/[Service]/[Install]` valid (488-504).
|
||||
- Dry-run prints the same string (`(dry-run) ExecStart: $exec_cmd`, 481).
|
||||
- **NOT FIXED — model path with spaces**: `exec_cmd="$llamacpp_full -m $model --port $PORT --host $HOST"` (432) concatenates the raw path, and the heredoc writes it unquoted. systemd.service(5) splits ExecStart arguments on unquoted whitespace; `resolve_model` (163-197) accepts space-containing paths (`[ -f "$explicit" ]`), so a model under a spaced dir (or `HF_DOWNLOAD_DIR` with a space) produces a unit whose args are split (`-m /home/user/My` + `Models/model.gguf`), and the server silently fails to load the model. The brief's check item "systemd quoting of model path with spaces is correct (quote the ExecStart value properly)" is **not** satisfied. → REQUIRED R1.
|
||||
- `systemd-analyze verify` claim: plausible from the unit text (all keys valid, ExecStart absolute path), but **UNVERIFIED** here — sandbox denies execution. Note: `systemd-analyze verify` would not catch the space issue anyway (it validates syntax/literal paths, not runtime arg semantics).
|
||||
|
||||
[FAIL → flagged as REQUIRED R1]
|
||||
|
||||
## Step 4: F3 — --branch/--revision alias (pos-ai-hf)
|
||||
|
||||
- `BRANCH` variable removed (grep: no `BRANCH` reference anywhere in pos-ai-hf); both `--branch` (127-129) and `--revision` (146-148) set the same `REVISION`, last-arg-wins by loop overwrite.
|
||||
- Both consumers use `REVISION`: `bin/pos-ai-hf:547` (cmd_download) and `871` (cmd_files).
|
||||
- usage() documents the alias (67, 77-79); `# POS_FLAGS:` line 4 lists both; `DOC/POS.md:108` documents "alias `--revision`, when both are given the later one wins".
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 5: F4 — parallel download failure handling (pos-ai-hf)
|
||||
|
||||
- Per-pid reap: `if ! wait "${job_pids[0]}"` batch reap (666-668) and drain loop `if ! wait "${job_pids[$i]}"` (677-683) — a failed job is recorded, never fatal to the batch (checked: no unguarded `wait` remains).
|
||||
- Failure collection + reporting after the batch (667, 679, 687-689) — same `warn` style as the sequential path.
|
||||
- No orphaned jobs: drain loop waits for every started pid; temp dir cleaned by `trap 'rm -rf "$temp_dir"' EXIT` (641) + explicit `rm -rf` + `trap - EXIT` (691-692).
|
||||
- Dead machinery removed (see Step 2e).
|
||||
- **NEW REQUIRED — misleading success reporting**: on partial failure the script continues and: (i) summary prints `Downloaded: <repo> (<file_count> files, <total_size>)` (745) counting **attempted** files; (ii) `.hf-meta` "files" records ALL filtered files (721) even failed ones; (iii) exit status is 0. The builder disclosed this ("summary line counts attempted files — pre-existing, noted not in review scope"), but the re-review brief says "summary counts are honest (flag anything misleading)" and the parallel failure path is exactly this rewrite's scope. Consequence: a partially-failed model is marked complete in meta and `pos ai server start` can attempt incomplete weights. → REQUIRED R2. Also minor: the progress line prints `Completed: <failed-file>` for failures before the batch-end warning (670/682) — fold into R2.
|
||||
|
||||
[PASS for original F4 defect; FAIL on honesty item → REQUIRED R2]
|
||||
|
||||
## Step 6: F5 — version/feature validation (pos-ai-server)
|
||||
|
||||
- `detect_llama_version` (52-59): `command -v "$bin"` guard **before** the pipeline; pipeline terminated `|| true`; empty → `unknown`. Cannot crash on missing binary.
|
||||
- `validate_requested_flags` (66-87) validates only `REQUESTED_FLAGS` — populated exclusively in the parse loop when the user explicitly passes the flag (288-359); config/env-derived defaults are never validated.
|
||||
- Error message names flag + version (84): `installed llama.cpp <version> does not expose <flag> — remove it or upgrade llama.cpp`.
|
||||
- Warn-and-proceed on unreadable `--help` (72-74) is deliberate and documented in `DOC/POS.md:125` ("if `--help` cannot be read the tool warns and proceeds").
|
||||
- `cmd_status` errors cleanly before the version probe (549-551) and probes the *resolved* binary (604-605).
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 7: F6 — hf cache + /dev/tty deviation (pos-ai-hf)
|
||||
|
||||
- `cmd_cache {status|clear}` real implementation (`bin/pos-ai-hf:887-964`): default `status`, bad action → usage error; indent fixed (no more 4-space top-level).
|
||||
- `status`: cache dir + model count + on-disk size via existing `hf_human_size` (926), empty → `Models: 0 (nothing downloaded yet)` rc 0; discovery identical to `list` via `hf_cache_models` (897-905).
|
||||
- `clear`: lists models, confirmation prompt, fail-closed — only `[Yy]` proceeds; EOF/invalid → `Aborted — nothing removed` rc 0 (948-953).
|
||||
- **/dev/tty deviation assessment — acceptable**:
|
||||
- Convention (AGENTS.md) covers tools that read **stdin**; `cache clear` reads `/dev/tty`, not stdin, so the `pos` logging-tee cannot hang or swallow the prompt.
|
||||
- Lint rule `uses_stdin` (scripts/lint-conventions.sh:59-80) explicitly skips lines containing `/dev/tty` (74) — `pos-ai-hf` is statically lint-clean and correctly NOT in INTERACTIVE_CMDS.
|
||||
- Precedent: `pos-ai-server` `pick_model` reads `/dev/tty` (157) and is likewise not in INTERACTIVE_CMDS — the deviation matches an established pattern.
|
||||
- No controlling terminal: `IFS= read -r yn 2>/dev/null </dev/tty || yn=""` fails closed (deny) instead of hanging — consistent with the builder's setsid probe claim.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 8: llamacpp wiring + maintainer corrections + gates
|
||||
|
||||
- `bin/pos-ai-llamacpp` (7 lines) is a byte-for-byte mirror of `bin/pos-ai-gemini` except the provider name; `# POS_SUBCMDS: ask chat models sessions capture` matches `lib/ai-providers/llamacpp.sh` capabilities (`provider_generate` drives ask/chat/sessions/capture; `provider_models_list` drives models).
|
||||
- `bin/pos-ai:701-704` `llamacpp)` case routes `exec "$0" --provider llamacpp "${args[@]}"` like the gemini/openrouter forwarders; usage() provider lists corrected (42, 59).
|
||||
- `bin/pos:269` INTERACTIVE_CMDS includes `ai-llamacpp` — required (chat reads stdin) and the reverse lint rule (entry ⇒ matching executable) is satisfied by the untracked forwarder (exec bit claimed 100755 by Maintainer `stat`; **UNVERIFIED** here — sandbox denies `stat`, file is untracked so `git ls-files -s` cannot confirm).
|
||||
- `DOC/POS.md` (58-59, 72, 82, 90, 108, 111, 125) and `DOC/howto/ai.md` (5, 19-20, 29-31, 134-137) llamacpp edits are factual — verified against `lib/ai-providers/llamacpp.sh` (OpenAI-compatible `/v1/chat/completions` line 31; default model from `/v1/models` lines 11-16; `LLAMACPP_MODEL` line 7).
|
||||
- Generated blocks (AGENT_Context tree rows 66-70, dispatch 286-289, selfcontained 374, filetable 618-621) internally consistent with headers; filetable line counts match `wc -l` exactly (pos-ai-hf 976, pos-ai-server 646, pos-ai 706, pos-ai-llamacpp 7, completions 313, pos 302). Regen idempotency itself UNVERIFIED.
|
||||
- AGENT_TODO.md: both 2026-09-06 entries (convention sweep; llamacpp forwarder) dated and consistent with the file's loose "readable summary" convention (convention sweep entry is newest-at-top, forwarder entry predates it — pre-existing placement, not this pass's defect).
|
||||
- **Gates UNVERIFIED — denial quoted**: sandbox permission rules allow only `git status/log/diff/show/blame/reflog/merge-base/rev-parse/branch --list/branch -a/branch -r/ls-files/ls-tree/head/tail/wc/sort/grep/rg`; `bash -n`, `make gen`, `make check`, `make lint`, `systemd-analyze` are denied (`{"permission":"bash","pattern":"*","action":"deny"}`). Static cross-checks against `scripts/lint-conventions.sh`/`scripts/check-sync.sh` show no obvious gate violation in the touched files (shebang, strict-mode, POS header + em-dash, exec-bit claims, `-h|--help` after deps guards, `uses_stdin`/INTERACTIVE_CMDS consistency, no top-level `local`, docs referenced in POS.md).
|
||||
|
||||
[BLOCKED: gates require execution — must be run by Orchestrator/Builder outside this sandbox]
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### R1 — REQUIRED — ExecStart does not quote the model/binary path; paths with spaces produce a broken unit
|
||||
|
||||
- Severity: REQUIRED
|
||||
- Evidence: `bin/pos-ai-server:432` `exec_cmd="$llamacpp_full -m $model --port $PORT --host $HOST"` and 495 `ExecStart=$exec_cmd` (raw heredoc). systemd.service(5) splits ExecStart on unquoted whitespace; `resolve_model` (163-197) accepts spaced paths (`[ -f "$explicit" ]`), and `HF_DOWNLOAD_DIR` (18) becomes the relative-model base. A model like `--model "/home/me/My Models/m.gguf"` yields `ExecStart=/usr/bin/llama-server -m /home/me/My Models/m.gguf …` → args split, model load fails silently.
|
||||
- Relevant files/lines: `bin/pos-ai-server:432, 495` (+ `18`, `163-197`).
|
||||
- Approved scope reference: re-review brief F2 item "systemd quoting of model path with spaces is correct (quote the ExecStart value properly)".
|
||||
- Why it matters: exactly the F2 failure class this fix was meant to eliminate — a valid input produces a unit that doesn't do what the user asked, without any error. Fix is understood: emit quoted systemd tokens (`ExecStart="$llamacpp_full" -m "$model" …`) inside the unquoted heredoc, escaping embedded quotes as needed.
|
||||
|
||||
### R2 — REQUIRED — partial-failure reporting overstates success: summary counts attempted files, `.hf-meta` lists failed files, exit code is 0
|
||||
|
||||
- Severity: REQUIRED
|
||||
- Evidence: `bin/pos-ai-hf:745` `printf '📥 Downloaded: %s (%d files, %s)'` uses `file_count` (= all filtered files) even after `failed_files` is non-empty; 721 writes `.hf-meta` "files" = all filtered files; no code path exits non-zero when `failed_files` is non-empty (parallel 638-692 and summary 716-747 both run to completion; dispatch exits 0). Progress lines 670/682 also print `Completed:` for failed files before the batch-end warning.
|
||||
- Relevant files/lines: `bin/pos-ai-hf:687-689, 716-747` (esp. 721, 745).
|
||||
- Approved scope reference: re-review brief F4 item "summary counts are honest (flag anything misleading)". Builder disclosed the limitation in their report (Step 4 "Known limit (pre-existing…)") — disclosed is not fixed; the failure path is this rewrite's scope.
|
||||
- Why it matters: a partially-downloaded model is reported as fully downloaded, recorded complete in `.hf-meta`, and can then be handed to `pos ai server start` (incomplete weights) — an operational hazard from exactly the failure mode F4 was meant to handle. Fix is understood: count successes for the summary, exclude failed files from `.hf-meta` (or record per-file status), and exit non-zero when any file failed.
|
||||
|
||||
### S3 — SUGGESTED — `--list` mode ignores `--include/--exclude` (carried from S7)
|
||||
|
||||
- Severity: SUGGESTED
|
||||
- Evidence: `bin/pos-ai-hf:553-563` returns before the pattern block at 588-590; `DOC/POS.md:108` and usage (line 74) promise `--list` "shows exactly what download would fetch".
|
||||
- Why it matters: doc-vs-behavior inconsistency for the flagship documented example; small fix (apply patterns inside the `--list` branch or move the early return after the pattern block).
|
||||
|
||||
### S4 — SUGGESTED — pattern glob is case-sensitive; docs silent, gguf filter is case-insensitive
|
||||
|
||||
- Severity: SUGGESTED
|
||||
- Evidence: `hf_apply_patterns` bash `case` glob (429-439) is case-sensitive; `HF_GGUF_FILTER` uses `ascii_downcase` (181). `--include "*.GGUF"` won't match `.gguf` files.
|
||||
- Why it matters: consistency note only; no working behavior regressed (old regex path was broken), but one line of docs ("supports glob") would remove ambiguity.
|
||||
|
||||
### N5 — NOTE — approval gates and systemd-analyze verification could not be run in this sandbox
|
||||
|
||||
- Severity: NOTE
|
||||
- Evidence: permission rules deny all bash except the git/read allow-list (quoted in Step 8). Builder/Maintainer claim `bash -n` OK, `make gen` idempotent, `make check` green, `make lint` 0 FAIL / 0 WARN, `systemd-analyze verify` RC=0 — plausible but not observed here. Also NOTE: `systemd-analyze verify` success does not cover R1 (it cannot see the runtime space-splitting).
|
||||
- Why it matters: merge-blocker status (gen drift / gate failure) cannot be confirmed until the Orchestrator/Builder re-runs the gates on this exact tree.
|
||||
|
||||
### N6 — NOTE — Builder report's "files not touched" list is stale
|
||||
|
||||
- Severity: NOTE
|
||||
- Evidence: Builder report (lines 10, 99) claims `bin/pos`, `bin/pos-ai`, `AGENT_TODO.md` untouched; actual diff shows all three modified — by the Maintainer's sweep, not the Builder, so combined work is consistent. Report-accuracy nit only.
|
||||
|
||||
---
|
||||
|
||||
## Per-finding status vs original list
|
||||
|
||||
| Finding | Status | Evidence |
|
||||
|---------|--------|----------|
|
||||
| F1 (BLOCKING — include/exclude never completes) | **FIXED** | bash-case glob, array shape, composition order, no-match rc 1, no jq interpolation, dead helpers removed |
|
||||
| F2 (BLOCKING — malformed unit) | **PARTIAL** | Single-line ExecStart + dry-run parity + no echo>> appends: FIXED. Path-with-spaces quoting: NOT FIXED → R1 |
|
||||
| F3 (REQUIRED — dead --branch) | **FIXED** | Single `REVISION` var, both aliases, last-wins, docs/headers updated, both consumers use REVISION |
|
||||
| F4 (REQUIRED — parallel failure path) | **FIXED** (primary) + **R2** | Per-pid reap, failure collection, drain, EXIT-trap cleanup, dead machinery removed — FIXED. Misleading counts/meta/exit on partial failure — new REQUIRED R2 |
|
||||
| F5 (REQUIRED — version/validation) | **FIXED** | Guard before pipe, unknown-safe, explicit-flags-only validation, flag+version error, documented warn-and-proceed, clean status error |
|
||||
| F6 (REQUIRED — cache stub) | **FIXED** | Real status/clear, sizes via hf_human_size, fail-closed confirm; /dev/tty deviation acceptable (lint-exempt, precedent, fail-closed) |
|
||||
| llamacpp wiring (brief) | **PASS** | Forwarder mirror, dispatch case, INTERACTIVE_CMDS, docs factual, AGENT_TODO dated |
|
||||
| Maintainer llamacpp doc corrections | **PASS** | 7 provider-list fixes factual vs adapter; no drift introduced |
|
||||
| Gates | **UNVERIFIED** | Sandbox denial; static lint/check analysis clean |
|
||||
|
||||
## Gate outcomes
|
||||
|
||||
- `bash -n` all in-scope scripts — UNVERIFIED (denied).
|
||||
- `make gen` idempotency (x2) — UNVERIFIED (denied); filetable line counts independently match `wc -l`; GEN blocks internally consistent with headers.
|
||||
- `make check` / `make lint` — UNVERIFIED (denied); static reading of `scripts/lint-conventions.sh` (shebang/strict-mode/POS headers/`-h|--help` position/`uses_stdin` tty exemption/INTERACTIVE_CMDS reverse rule/`local`-at-top warning) and of `check-sync.sh` shows no violation in the touched files.
|
||||
- `systemd-analyze verify` — UNVERIFIED (denied); unit text (pos-ai-server:488-504) is plausible: valid keys, absolute ExecStart, `EnvironmentFile=-%h/...` accepted syntax.
|
||||
- Denial quote: `{"permission":"bash","pattern":"*","action":"deny"}` with an allow-list of git read commands, `head/tail/wc/sort/grep/rg` only.
|
||||
|
||||
## Verification verified
|
||||
|
||||
- F1 array/composition/no-match/quote-safety/dead-code by direct code read + grep (FACT).
|
||||
- F2 single-line ExecStart + dry-run parity + no append writes by code read + diff (FACT); space-quoting gap by systemd.service(5) semantics + code read (FACT).
|
||||
- F3 aliasing by grep + code read (FACT).
|
||||
- F4 failure handling by code read (FACT); misleading summary/meta/exit by code read (FACT).
|
||||
- F5 guards and validation scope by code read (FACT).
|
||||
- F6 cache behavior + /dev/tty fail-closed by code read + lint rule read (FACT).
|
||||
- llamacpp wiring: forwarder byte-mirror (read), dispatch (read), INTERACTIVE_CMDS (read), docs vs adapter facts (read), line counts (wc).
|
||||
- Git baseline HEAD = `0856b25`; tree diff matches the combined Builder+Maintainer reports.
|
||||
|
||||
## Verification unverified
|
||||
|
||||
- `bash -n`, `make gen` idempotency, `make check` (incl. `gen-docs --check`), `make lint`, `systemd-analyze verify` — sandbox denies execution; must be run by Orchestrator/Builder on this exact tree.
|
||||
- Exec bit of untracked `bin/pos-ai-llamacpp` (claimed 100755 via `stat` by Maintainer; untracked so not confirmable via git).
|
||||
- Live probes (a real include/exclude download, one forced-failure parallel download, a real `server start` with a spaced model path, `status` without llama-server) — empirical confirmation pending.
|
||||
|
||||
## Scope compliance
|
||||
|
||||
- In-scope, correctly implemented: F1, F3, F5, F6; F4 failure handling; llamacpp forwarder + dispatch + docs + completions; maintainer doc corrections.
|
||||
- In-scope, defective: F2 space-quoting (R1); F4 summary/meta honesty (R2).
|
||||
- Out-of-scope changes: none in source; untracked plan docs/reports remain for the Orchestrator's commit decision.
|
||||
|
||||
## Remaining uncertainty
|
||||
|
||||
- Gate results on the exact tree (Builder/Maintainer claims unverified here).
|
||||
- Whether `R1` (space quoting) and `R2` (honest failure reporting) are fixed per the recommendations — requires a Builder pass and another review round, plus one live spaced-path probe and one forced-failure probe by the Orchestrator.
|
||||
|
||||
## Recommended next agent
|
||||
|
||||
**Builder**
|
||||
|
||||
**Reason:** Two REQUIRED defects with understood, in-scope fixes: (1) quote the ExecStart tokens (`"$llamacpp_full"`/`"$model"`) so spaced paths survive systemd's argument splitting (pos-ai-server:432/495); (2) make the failure path honest — count successes in the summary, exclude failed files from `.hf-meta` (or record status), and exit non-zero when any file failed (pos-ai-hf:687-747). After fixes: re-run `bash -n`, `make gen` x2, `make check`, `make lint`; then Orchestrator runs the live probes (include/exclude download, forced-failure parallel download, `server start` with a spaced model path + `systemd-analyze verify`, `server status` without llama-server) and re-hands to Reviewer for final acceptance. S3/S4 can ride along in the same pass.
|
||||
|
||||
## Changes made by Reviewer
|
||||
|
||||
none
|
||||
@@ -1,176 +0,0 @@
|
||||
# Stability Pass Acceptance Review — 2026-09-06
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status: CHANGES_REQUIRED** (reject with block list).
|
||||
- **Reviewed:** uncommitted stabilization pass (`git status` 33 modified + untracked `AgentsReport/`, `apps/ai/`, `config/{telegram,matrix}.env`, `tests/`) against architect `AgentsReport/architect/2026-09-06_stabilization-design.md` (D-A…D-F), explorer audits (V1-V7, D1-D4, M1-M4, H-002/H-003, dup-loaders), and llamacpp-app REQUIRED items.
|
||||
- **Defects: 2 BLOCKING, 1 REQUIRED, 1 SUGGESTED, 3 NOTE** (below).
|
||||
- **BLOCKING 1:** neither chat bridge passes `--no-command-execution` to `pos ai` (D-B criteria 5/6 fail; docs falsely claim the bridges rely on it).
|
||||
- **BLOCKING 2:** D-A contract not implemented — owner/room unset must `err` + exit (acceptance criteria 1/5 fail); implemented as run-degraded fail-closed; needs per-contract fix **or** explicit Architect amendment.
|
||||
- **Gates:** static verification only — **all empirical gate runs (make gen×2, make check, make lint, make test, bash -n, probes, `git diff --check`) are UNVERIFIED** from this sandbox (bash execution denied); Orchestrator must execute before merge.
|
||||
- **Recommended next agent: Builder** for F1+F3 (and F2 if the per-contract `err` path is chosen); **Architect** if F2's soft-fail model is to be ratified instead.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Scope and diff inventory
|
||||
|
||||
Checked `git status --short`, `git diff --stat` (33 files, +685/−403), per-file diff mapping, and untracked files.
|
||||
|
||||
- All 17 modified `bin/pos-*` trace to an approved decision area (D-A listeners/sender + templates; D-B pos-ai + `# POS_FLAGS:`/usage/completions + M4 adapter headers; D-D nine migrated tools + media-grab; D-E uninstall; V3 backup; V5 checkport/download/smb-client).
|
||||
- `bin/pos` **not** modified (dispatcher/INTERACTIVE_CMDS untouched — `pos-ai` already listed at `bin/pos:269`).
|
||||
- `bin/pos-docker-*` not modified → docker-compose loader **not** migrated (consistent with the accepted "no compose change" boundary).
|
||||
- No accidental deletions, no stray files. Untracked files all expected (report dirs, `apps/ai/llamacpp.sh`, env templates, `tests/`).
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 2: D-A — owner/room AND-gates
|
||||
|
||||
- `TELEGRAM_OWNER_ID` present in sender `# POS_CONFIG:` registry (`bin/pos-communication-telegram-sender:6`) and `config/telegram.env`; `pos config telegram` scope wired (sender line 46; template).
|
||||
- Actual auth logic: telegram checks both chat and owner (`bin/pos-communication-telegram-listener:761,790`); matrix checks owner + room (`bin/pos-communication-matrix-listener:511`).
|
||||
- **FAIL:** architect D-A required *refuse to start* on unset (`err` in `run_daemon`, decision lines 31/38; acceptance criteria 1/5 at lines 59/63). Implementation instead starts the daemon, warns, and ignores commands. Fail-closed security property holds (no unauthorized execution), but the approved contract is not met — **Finding 2 (BLOCKING)**.
|
||||
|
||||
[FAIL]
|
||||
|
||||
## Step 3: D-B — AI eval posture (`--no-command-execution`)
|
||||
|
||||
- Flag implemented in `bin/pos-ai`: `# POS_FLAGS:` line 5, usage lines 43/78/83, parser 666-669 (last-wins vs `--trust`), guard in `_prompt_run_command` 370-404; completions/pos.bash:26 regenerated.
|
||||
- **FAIL:** D-B acceptance criteria 5/6 (decision lines 137-138) — bridge invocations must include the flag. `grep -c "no-command-execution"` = **0** in both `bin/pos-communication-telegram-listener` and `bin/pos-communication-matrix-listener`; both call `pos ai gemini ask` without it (telegram:731, matrix:466). Security still holds today only via the non-tty guard (`pos-ai:376`), which is exactly the defense-in-depth the decision required to be made explicit — **Finding 1 (BLOCKING)**.
|
||||
- **FAIL (minor):** criterion 4 ("no output of the command block") — implementation prints the block + "command execution disabled" before returning (`pos-ai:377-382`) — **Finding 3 (REQUIRED)**.
|
||||
- Docs (DOC/POS.md `Command execution posture`; DOC/howto/ai.md same section) describe the print behavior and claim the bridges rely on the flag — the claim is false against the working tree (folded into F1).
|
||||
|
||||
[FAIL]
|
||||
|
||||
## Step 4: D-C — test suite
|
||||
|
||||
Read `tests/run-tests.sh`, `tests/test-lib.sh`, `tests/README.md`, and 4 of the 12 t-*.sh files.
|
||||
|
||||
- Harness: strict-mode runner in per-test subshell; `test_run`/`test_run_env` capture rc without triggering errexit; SKIP counted separately; **aborted test without FAIL is itself FAIL**; zero-assertion file → FAIL ("no assertions run — harness broken").
|
||||
- Tests are genuine behavioral tests (real production scripts, stub PATH/curl/llama-server/nvidia-smi/gpg, exact rc/output asserts) — not tautologies.
|
||||
- `t-ai-server-flags.sh`: dedupe (`--ctx`+`--ctx-size` → one token), precedence, default+requested emission — meaningful.
|
||||
- `t-matrix-auth.sh`: owner+room gating, exactly-one-reply (reply-loop detection), fail-closed no-exec — meaningful.
|
||||
- `t-uninstall-manifest.sh`: install.sh ↔ POS_LIBS set-equality + every lib exists; XDG_CONFIG_HOME scan_tier1 extraction + behavioral run — meaningful.
|
||||
- Makefile `test:` target added.
|
||||
|
||||
[PASS] statically. Execution results (counts/timing/red-green) **UNVERIFIED** — Orchestrator must run `make test`.
|
||||
|
||||
## Step 5: D-D — config loader migration
|
||||
|
||||
- `lib/config-ui.sh:336-357` `load_env_file`: env-wins export, CRLF strip, quote-pair trim, quiet on missing file, `LOADED_ENV_KEYS` append, bare-basename resolution under CONFIG_DIR; precedence CLI > env > file > defaults documented and implemented (file only when env var unset/empty).
|
||||
- Exactly 9 tools migrated (pos-ai, pos-ai-hf, pos-ai-server, telegram sender+listener, matrix sender+listener, scrcpy, media-grab); `load_secret` via loader in network-download; entertainment-lib thin wrappers `config_value`/`write_config_key` → `cfg_value`/`cfg_write` (`lib/entertainment-lib.sh:35-43`, CONFIG_FILE set at line 6).
|
||||
- Legacy `load_system_env` remains in common.sh:146 with exactly the 3 unchanged callers (pos-system-backup, pos-system-health, pos-media-sync) — matches the accepted note.
|
||||
- `cfg_write`/`cfg_value` semantics verified (env-file source of truth, grep-v+append value-safe writes, chmod 600).
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 6: D-E — uninstall manifest
|
||||
|
||||
- `POS_LIBS` matches `install.sh` phase-2 lib list byte-for-byte (12 libs; `bin/pos-system-uninstall`; also asserted by `t-uninstall-manifest.sh`).
|
||||
- ScaleTail path `/usr/local/share/linux_post_install/scale-tail` matches `install.sh:237`; flags dir matches `lib/flags.sh`; user-unit scan_tier1 honors XDG_CONFIG_HOME + `pos-*` prefix only; plugin removal stays POS_PLUGIN-marker driven; tier structure documented in DOC/POS.md.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 7: D-F — llama-server flag validation
|
||||
|
||||
Read `bin/pos-ai-server` end-to-end (743 lines):
|
||||
|
||||
- Parse loop records `REQUESTED_FLAGS` (canonical tokens), `requested_from_env_config` captures config/env-sourced flags pre-parse (lines ~56-68), merged + deduped at 476-483, hard error for unsupported requested flags (validate_requested_flags) vs warn+omit for unsupported defaults (validate_default_flags; DEFAULT_*_OK gates in exec_cmd 527-574).
|
||||
- Behavior matrix matches decision: readable `--help` → word-boundary version-aware matching; unreadable → warn + accept-all; unsupported default → one warning + omitted, never in the unit; unsupported requested → hard error naming flag+version.
|
||||
- `LLAMACPP_HOST` honored (HOST default 127.0.0.1, used by health probe + exec line); dry-run prints exact ExecStart; systemd_quote for binary+model tokens.
|
||||
- Docs (DOC/POS.md ai-server row, usage) updated to describe requested-vs-default semantics.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 8: Security sweep (V1-V7, D1-D4, M1-M4, NET_PROBE, gpg, /dev/tcp)
|
||||
|
||||
- NET_PROBE: `NET_PROBE="${NET_PROBE:-timeout 3 bash -c 'exec 3<>/dev/tcp/\$1/\$2' _ 8.8.8.8 53}"` — host/port escaped in default (`bin/pos-network-download:31`); all other `/dev/tcp` uses positional-arg composition (network-checkport 135/159/168/170 incl. udp fix, share-smb-client:94, share-lib.sh:61).
|
||||
- gpg: only `--passphrase-fd 3` + `3<<<"$PASS"` in backup (197/207) with failure cleanup; **no** `--passphrase <value>` anywhere in bin/lib/scripts/apps/install.sh.
|
||||
- D1 (`failed_files` init 624, single-file failure 706, meta suppressed 733, rc 1 738-742), M1 (`# POS_SUBCMDS:` incl. new verbs), M2/M4 (pos-ai subcmds + adapters), D2 (llamacpp provider honors LLAMACPP_HOST) — all present.
|
||||
- Secrets: runtime token masking by `pos config`; templates carry no real secrets; `config/telegram.env`/`matrix.env` tracked by convention (like ai.env), no secret material.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 9: llamacpp app REQUIRED items
|
||||
|
||||
- `apps/ai/llamacpp.sh` mode 100755 (`git diff --no-index --summary` → create mode 100755); DOC/APPS.md row present + count 18; `apps/install.sh` CAT_NAMES has `[ai]`; `templates/app.sh` categories comment += ai; pos-ai-server install-hint strings present in help + both err lines (start/status).
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 10: Gates
|
||||
|
||||
- `make gen` ×2 byte-identical, `make check`, `make lint` (0 FAIL/0 WARN on current repo), `make test` results, `bash -n` of changed scripts, planted-violation lint negative, test-mutation failure probes, `systemd-analyze` skip-path, `git diff --check`: **cannot be executed from this sandbox (bash denied). All UNVERIFIED — Orchestrator must run before merge.**
|
||||
- Static lint comparison (HEAD vs working `scripts/lint-conventions.sh`): 23 fail/warn messages, 21 byte-identical; **two WARN messages lost the `:num` location** (secret-literal-assignment; system-path write) → "byte-identical output" claim holds only vacuously on a clean repo (0 WARN) — **Finding 4 (SUGGESTED)**.
|
||||
- Deterministic generators: completion reorder observed (`llamacpp` moved in `_pos_subcmds[ai]`) consistent with LC_ALL=C sort.
|
||||
|
||||
[BLOCKED: empirical gate execution requires Orchestrator]
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1 — BLOCKING — chat bridges missing `--no-command-execution`; docs falsely claim they use it
|
||||
|
||||
- Severity: BLOCKING · Certainty: FACT
|
||||
- Evidence: `grep -c "no-command-execution"` = 0 in `bin/pos-communication-telegram-listener` / `bin/pos-communication-matrix-listener`; bridge invocations `timeout 120 pos ai gemini ask --session … --system … "$prompt"` at telegram:731 and matrix:466 carry no flag. Architect decision `AgentsReport/architect/2026-09-06_stabilization-design.md:112-113,126-127` and acceptance criteria 5/6 (lines 137-138) require the flag in the actual command string. DOC/POS.md and DOC/howto/ai.md state "this is the structural guard the chat bridges rely on" — false against the implementation.
|
||||
- Why it matters: the approved D-B deliverable is not implemented; the accepted defense-in-depth (explicit flag so a future refactor cannot introduce bridge execution) does not exist; documentation misrepresents the implementation.
|
||||
- Remediation: add `--no-command-execution` to both `pos ai gemini ask` invocations (telegram:731, matrix:466) and re-run `make check`.
|
||||
|
||||
### Finding 2 — BLOCKING — D-A unset-owner/room contract not implemented (err+exit) — soft-fail substituted without amendment
|
||||
|
||||
- Severity: BLOCKING · Certainty: FACT
|
||||
- Evidence: architect decision lines 31/38 require `err` in `run_daemon` and acceptance criteria 1/5 (lines 59/63) require `--run` to exit with an error naming the key. Implementation: daemon starts, `warn` at telegram-listener:761/790 and matrix-listener:511, commands ignored. Docs (DOC/POS.md, howto/communication.md, config/telegram.env, config/matrix.env) and `t-matrix-auth.sh` consistently describe the soft-fail model — the whole round silently implements a different accepted decision. Security property (fail-closed, no unauthorized execution) is preserved by both designs.
|
||||
- Why it matters: an approved, testable acceptance criterion is not met; the implementer changed design without a decision; unset-owner upgrades now keep the daemon alive (operator may not notice commands are dead).
|
||||
- Remediation (either):
|
||||
1. Implement per contract: `[ -n "${TELEGRAM_OWNER_ID:-}" ] || err …` in run_daemon next to the token/chat-id guards; same for MATRIX_ROOM_ID; update docs/tests, or
|
||||
2. Architect formally amends D-A to the soft-fail model (then this finding downgrades and F2's code is accepted as-is).
|
||||
|
||||
### Finding 3 — REQUIRED — `--no-command-execution` prints the command block (D-B criterion 4 not met)
|
||||
|
||||
- Severity: REQUIRED · Certainty: FACT
|
||||
- Evidence: `bin/pos-ai:377-382` prints "Command detected:" + cmd + "command execution disabled" before returning under NO_EXEC; decision line 136 ("no prompt, no output of the command block"). Docs document the print behavior (no doc bug); the deviation is the accepted criterion.
|
||||
- Why it matters: contract mismatch on output semantics; per criterion the flag should return without printing. Harmless informationally, but violates the letter of the accepted decision.
|
||||
- Remediation: in `_prompt_run_command`, check NO_EXEC before the "Command detected:" print and return 0; align DOC/POS.md/howto text.
|
||||
|
||||
### Finding 4 — SUGGESTED — lint rewrite dropped `:num` locations from two WARN messages
|
||||
|
||||
- Severity: SUGGESTED · Certainty: FACT
|
||||
- Evidence: working `scripts/lint-conventions.sh` vs HEAD — `warn_ "$f: secret-like literal assignment (…)"` and `warn_ "$f: writes to a system path (…)"` lost `:num`; all other 21 messages byte-identical; no FAIL rule changed. Claim "byte-identical output" holds only when the repo has 0 WARNs.
|
||||
- Why it matters: the two least-actionable warnings become file-only; brief's "output is actionable (file:line)" requirement degrades for those classes.
|
||||
- Remediation: restore `:num` in those two messages; update the rewrite's verification claim to "identical on clean repo".
|
||||
|
||||
### Notes
|
||||
|
||||
- **N1 (NOTE):** `t-matrix-auth.sh` run 1 has no explicit `check_rc` after `timeout 5` — a daemon-hang regression (TERM trap broken, rc 124) would not fail the test. Add `check_rc` for run 1 and the matrix-room-unset run.
|
||||
- **N2 (NOTE):** AGENT_TODO.md new Done entry says "DOC/APPS.md 15→16" — stale; the llamacpp bump was 17→18 (current count 18). Also the stabilization pass itself has no Done entry yet (acceptable pre-commit; add it in the commit per AGENTS.md).
|
||||
- **N3 (NOTE):** `git diff --check` (whitespace) unobtainable here — folded into Step 10 UNVERIFIED.
|
||||
|
||||
---
|
||||
|
||||
## Verification verified (static, fact-level)
|
||||
|
||||
- D-B flag fully wired in pos-ai (header/usage/parser/last-wins guard) and completions; absent in bridges (F1).
|
||||
- D-A registry + templates present; auth logic checks both gates; unset behavior deviates (F2).
|
||||
- D-D loader/migration complete incl. legacy caller boundary; entertainment wrappers correct.
|
||||
- D-E manifest/flags/ScaleTail/scan_tier1/plugin-marker; D-F full validation matrix; V1-V7 panels; D1/D2/M1/M2/M4; NET_PROBE + /dev/tcp positional hygiene; gpg fd-only passphrase; llamacpp app REQUIRED items; test-suite strictness (skip contract, aborted=Fail, zero-assertion=Fail).
|
||||
- Scope: 33 modified + untracked files all trace to approved areas; no out-of-scope change found; bin/pos and docker-compose untouched.
|
||||
|
||||
## Verification unverified (needs Orchestrator execution)
|
||||
|
||||
- `make gen` ×2 byte-identical; `make check`; `make lint` (0 FAIL / 0 WARN); `make test` (12 files, counts, elapsed); `bash -n` on changed scripts; planted-violation lint negative; test-mutation failure probes; `systemd-analyze verify` skip-path; live bridge/`pos ai` behavioral probes; `git diff --check`.
|
||||
|
||||
## Scope compliance
|
||||
|
||||
- In-scope confirmed: all D-A..D-F areas, explorer audits, llamacpp REQUIRED items.
|
||||
- Out-of-scope found: none (F1/F2 are *missing* accepted scope, not additions).
|
||||
- Deviations from approved decisions: F1 (criterion 5/6 unmet), F2 (criterion 1/5 unmet, design substituted), F3 (criterion 4 unmet).
|
||||
|
||||
## Remaining uncertainty
|
||||
|
||||
- All empirical gate and behavioral results (Step 10). Whether F2 resolves to code (err+exit) or Architect amendment. Whether F3's print is acceptable after amendment.
|
||||
|
||||
## Recommended next agent
|
||||
|
||||
**Builder** (primary) — F1+F3 are well-understood scoped fixes (add flag to two invocations; move the NO_EXEC check before the print); F2 fix per contract also Builder. If the Orchestrator prefers to keep the soft-fail design, route F2 to **Architect** to amend D-A explicitly (then F2 downgrades and the round can be accepted after gates).
|
||||
|
||||
## Changes made by Reviewer
|
||||
|
||||
none
|
||||
@@ -1,139 +0,0 @@
|
||||
# Stabilization Pass — Final Re-Verification (F1–F4 + test hardening) — 2026-09-06
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status: APPROVE_WITH_NOTES** (all four findings F1–F4 resolved; test hardening present; no BLOCKING/REQUIRED findings remain).
|
||||
- **Reviewed (read-only):** re-verification of prior `CHANGES_REQUIRED` findings F1/F2/F3/F4 plus the t-matrix-auth test-hardening, against the working tree + architect amendment (`AgentsReport/architect/2026-09-06_da-failmode-decision.md`, `2026-09-06_stabilization-design.md` D-A "AMENDED" blocks).
|
||||
- **Findings resolved:** F1 (BLOCKING→OK), F2 (BLOCKING→OK via Architect amendment), F3 (REQUIRED→OK), F4 (SUGGESTED→OK). Test hardening (item 5) present.
|
||||
- **New defects: 0.** New NOTEs: 2 residual-verification notes (N1 rc=2 set-e-in-trap dependency; N2 lint perf-rewrite equivalence).
|
||||
- **Gates:** static verification only. Empirical `make lint` result, `make test` (incl. rc=2), and behavioral probes of the lint rewrite are **UNVERIFIED-BY-REVIEWER** (bash execution denied in this sandbox) — Orchestrator must run before merge, as in the prior round.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: F1 — `--no-command-execution` in real bridge invocations
|
||||
|
||||
**Evidence (grep of actual command strings, not help text):**
|
||||
- `bin/pos-communication-telegram-listener:731`:
|
||||
`timeout 120 pos ai gemini ask --no-command-execution --session "$session" --system "$AI_SYSTEM" "$prompt"`
|
||||
- `bin/pos-communication-matrix-listener:466`: identical string with the flag.
|
||||
|
||||
**Flag-ordering valid — walked the dispatch/parse chain:**
|
||||
- `pos ai gemini ask ...` → dispatcher longest-prefix routes `pos ai gemini` → `bin/pos-ai-gemini` (thin forwarder, `bin/pos-ai-gemini:7`: `exec pos ai --provider gemini "$@"`), so `pos-ai` receives `--provider gemini ask --no-command-execution --session … --system … "$prompt"`.
|
||||
- `bin/pos-ai` parse loop (`bin/pos-ai:645-677`) handles flags at ANY position (case arms; `--no-command-execution` → `NO_EXEC=1; TRUST_MODE=0` at 666-667; non-flag tokens become `cmd`/`args`). So a flag after the `ask` subcommand is accepted. `NO_EXEC` is set globally before `_prompt_run_command` runs.
|
||||
- Last-wins vs `--trust` unchanged; bridges never pass `--trust` (nothing in the two bridge strings references it).
|
||||
|
||||
**Docs now match implementation:**
|
||||
- `DOC/POS.md:103` and `DOC/howto/ai.md:270` state the flag is "the structural guard the chat bridges rely on" — now true against the working tree (both bridges pass it). No false-claim remains.
|
||||
|
||||
[PASS] — F1 resolved.
|
||||
|
||||
## Step 2: F2 — D-A soft-fail ratified by Architect amendment
|
||||
|
||||
**Inputs:** `AgentsReport/architect/2026-09-06_da-failmode-decision.md` (DECIDED) + `2026-09-06_stabilization-design.md` D-A amended blocks.
|
||||
|
||||
**(a) Amendment coherent/self-consistent:** `2026-09-06_stabilization-design.md:33` (Telegram) and `:40` (Matrix) are marked **"AMENDED at review (2026-09-06): soft-fail ratified as shipped"**, present-tense soft-fail rules. TL;DR (`:11`), acceptance criteria (`:59-63`), and risk note (`:70`) all describe soft-fail. The only remaining strict-`err`/"requires" wording sits inside the amendment's past-tense descriptive paragraphs ("The original wording required…") — framed as what changed, not as the operative contract. No contradiction remains.
|
||||
|
||||
**(b) No other design decision silently depends on old strict D-A:** the decision report (`da-failmode-decision.md`, Decision 2 + Files-changed) states no doc/template/test change was needed because the entire shipped surface already encodes soft-fail: `tests/t-telegram-auth.sh:5,76-78`, `tests/t-matrix-auth.sh:78-87`, `config/*.env`, `DOC/POS.md`, `DOC/howto/communication.md`. Spot-read confirms those describe soft-fail, not fail-stop. No dependent decision references the strict wording.
|
||||
|
||||
**(c) Security property (fail-closed either way) — spot-check confirmed guards don't route around it:**
|
||||
- Telegram: unset owner → `bin/pos-communication-telegram-listener:789-791` warns + `continue` **before** `handle_message` (line 800). AND-gate at `:796` requires `chat == TELEGRAM_CHAT_ID` AND `from_id == TELEGRAM_OWNER_ID`.
|
||||
- Matrix: `bin/pos-communication-matrix-listener:510-512` warns on unset room; `:538` skips any room not matching `room_only` (empty when unset → every room skipped → fail-closed); sender gate `:549` (`sender == owner`). Dispatch only reached after both pass.
|
||||
- Unauthorized/unset → command never executed; no hint reply. Fail-closed invariant holds in both models.
|
||||
|
||||
[PASS] — F2 resolved by amendment (no code change required).
|
||||
|
||||
## Step 3: F3 — NO_EXEC returns before any print
|
||||
|
||||
**Evidence (`bin/pos-ai:370-402`):**
|
||||
- `:376` tty guard (`[ -w /dev/tty ] || return 0`).
|
||||
- `:377-379` `if [ "${NO_EXEC:-0}" -eq 1 ]; then return 0; fi` — placed immediately after the guard and **before** the `printf` at `:380-381`.
|
||||
- So under `NO_EXEC=1` the function returns 0 with zero stdout/stderr (no "Command detected:", no cmd echo).
|
||||
- Normal path (tty, NO_EXEC=0): `:380-381` prints the command; `y|Y` executes, `*` declines→history. Unchanged.
|
||||
- Exit semantics: NO_EXEC path returns 0; matches prior behavior (rc 0). Builder probe (PROBE-A/B) claims 0 bytes both streams under NO_EXEC, prints on normal path.
|
||||
|
||||
Docs (`DOC/POS.md:103`, `DOC/howto/ai.md:261-273`) updated to "neither printed nor run" — matches. No stale "still printed" text.
|
||||
|
||||
[PASS] statically. (Empirical 0-byte probe is Orchestrator/UNVERIFIED.)
|
||||
|
||||
## Step 4: F4 — lint WARN messages carry `:num` again
|
||||
|
||||
**Evidence (`git diff HEAD -- scripts/lint-conventions.sh`):**
|
||||
- Secret-literal: `warn_ "$f:$num: secret-like literal assignment (…)"` — working `:247`, and HEAD `:239` => **byte-identical format**.
|
||||
- System-path: `warn_ "$f:$num: writes to a system path (…)"` — working `:297`, HEAD `:254` => **byte-identical**.
|
||||
- `num` counter incremented first in-loop (before `continue` gates), so skipped lines still get correct numbers.
|
||||
|
||||
**Rule list/count parity vs HEAD (no other lint behavior changed):**
|
||||
- FAIL rules: 14/14 identical (shebang, set-euo, executable, POS-header, em-dash, -h|--help, deps-guard ordering, stdin-INTERACTIVE, INTERACTIVE_CMDS round-trip, plugin-no-common.sh, POS_PLUGIN, uninstall func, uninstall case, legacy-forward).
|
||||
- WARN rules: 9/9 identical (POS-header line, local top-level, not-in-POS.md, TimeoutStopSec, WantedBy, legacy-lines, legacy-case, secret-literal, system-path). All message strings match HEAD byte-for-byte.
|
||||
|
||||
[PASS] for rule/message parity and the two `:num` fixes.
|
||||
|
||||
**NOTE (N2):** this file is a substantial *behavior-equivalence* rewrite (subprocess grep/sed → pure-bash single-pass: `_syspath_outer`, `_reads_stdin` + caller heredoc/depth state, POS.md preload). Rule parity is statically confirmed, but behavioral equivalence of the rewritten scan internals on edge cases needs empirical `make lint` (clean repo → 0 FAIL/0 WARN) plus the planted-violation negative — Orchestrator, UNVERIFIED-BY-REVIEWER.
|
||||
|
||||
## Step 5: Test hardening (t-matrix-auth) + no-regression spot-read
|
||||
|
||||
**Evidence (`tests/t-matrix-auth.sh`):**
|
||||
- Run 1 (`:71-72`): `test_run_env … -- timeout --preserve-status -k 2 5 "$listener" --run` + `check_rc "daemon terminated via TERM trap, not killed (no hang)" 2 "$TR_RC"`.
|
||||
- Run 2 / room-unset (`:82-83`): same `--preserve-status -k 2` pattern + `check_rc … 2`.
|
||||
- `check_rc` helper exists unchanged (`tests/test-lib.sh:19`, delegating to `check_eq`; `test_run_env` captures `TR_RC` at `test-lib.sh:88-91`).
|
||||
- rc=2 rationale: listeners run `set -euo pipefail` (`matrix-listener:2`, `telegram-listener:2`) with trap `kill $(jobs -p) 2>/dev/null; exit 0` (`matrix:525`, `telegram:767`). On empty `jobs`, bash `kill` hits a usage error (rc 2) and, under `set -e`, aborts before `exit 0` → rc 2. `--preserve-status` surfaces it; on a true daemon-hang the SIGKILL after `-k 2` yields 137 → the rc-2 assert genuinely fails → the mutation-probe hang claim is credible.
|
||||
|
||||
**No-regression spot-read:** `tests/t-telegram-auth.sh` unchanged semantics, aligned with ratified soft-fail (`:5` "unset → fail-closed", `:76-78` assert warning + no marker + no sendMessage; no fail-stop assertion). No test asserts the old strict-D-A behavior.
|
||||
|
||||
[PASS] statically. **N1 (NOTE):** the exact rc value 2 depends on bash `set -e` firing inside the TERM trap on the empty `kill`. If a given bash does not errexit-abort in a trap, the trap reaches `exit 0` → rc 0, which would make `check_rc … 2` **false-FAIL** a healthy run (the reverse failure direction). The 137-vs-2 hang discrimination is sound by design; the precise value must be confirmed by `make test` (Orchestrator). UNVERIFIED-BY-REVIEWER.
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Resolved (prior round)
|
||||
|
||||
**F1 (was BLOCKING) — RESOLVED.** `--no-command-execution` present in the actual `pos ai gemini ask` command strings (telegram:731, matrix:466); ordering valid through `pos-ai-gemini` → `pos-ai` parse loop (flag accepted post-subcommand); docs now truthful. Certainty: FACT.
|
||||
|
||||
**F2 (was BLOCKING) — RESOLVED via Architect amendment.** D-A soft-fail ratified (decision report + amended design `:33/:40`); coherent and self-consistent; no dependent decision on old strict wording; fail-closed security verified (telegram AND-gate + unset-skip, matrix room-gate + sender gate; no route around). Certainty: FACT.
|
||||
|
||||
**F3 (was REQUIRED) — RESOLVED.** `_prompt_run_command` returns 0 before any print under `NO_EXEC` (pos-ai:377-379); zero output; normal path unchanged; rc 0 both ways. Certainty: FACT (empirical 0-byte probe UNVERIFIED).
|
||||
|
||||
**F4 (was SUGGESTED) — RESOLVED.** Both lint WARN messages carry `$f:$num:`, byte-identical to HEAD; 14 FAIL + 9 WARN rules and all message strings match HEAD. Certainty: FACT (rewrite behavior-equivalence UNVERIFIED).
|
||||
|
||||
### New (this round)
|
||||
|
||||
No new BLOCKING / REQUIRED / SUGGESTED findings.
|
||||
|
||||
- **N1 (NOTE):** t-matrix-auth rc-2 assert depends on `set -e` aborting inside the TERM trap on empty `kill` (bash-version-sensitive). If errexit does not fire in-trap, healthy run yields rc 0 → check_rc(2) false-FAILs. Design intent (137-hang discrimination) is sound; the exact value is UNVERIFIED → Orchestrator `make test`.
|
||||
- **N2 (NOTE):** `scripts/lint-conventions.sh` perf-rewrite (subprocess→pure-bash) is rule/message-identical per static diff, but behavior equivalence on edge cases is UNVERIFIED → Orchestrator `make lint` on clean repo + planted-violation negative.
|
||||
|
||||
---
|
||||
|
||||
## Verification verified (static, fact-level)
|
||||
|
||||
- F1 flag in both real bridge command strings (grep); dispatch+parse chain accepts it; docs accurate.
|
||||
- F2 amendment coherent + no strict leftover + fail-closed guards verified (read of both listeners).
|
||||
- F3 NO_EXEC early-return before print; rc semantics; docs aligned.
|
||||
- F4 `$f:$num:` restored byte-identical to HEAD; rule/count parity (14 FAIL, 9 WARN) vs HEAD.
|
||||
- t-matrix-auth both runs `timeout --preserve-status -k 2` + `check_rc 2`; check_rc unchanged (test-lib.sh:19); t-telegram-auth aligned with soft-fail.
|
||||
|
||||
## Verification unverified (needs Orchestrator execution)
|
||||
|
||||
- `make gen` ×2 byte-identical; `make check`; `make lint` (0 FAIL/0 WARN) incl. planted-violation negative; `make test` (all files, including the rc=2 asserts in t-matrix-auth and the no-hang 137 discrimination); `bash -n`; NO_EXEC 0-byte probe; `git diff --check`.
|
||||
- Empirical rc=2 (set-e-in-trap) — N1.
|
||||
- Lint rewrite behavior-equivalence on edge cases — N2.
|
||||
|
||||
## Scope compliance
|
||||
|
||||
- In-scope: F1, F2 (amendment), F3, F4, test hardening all trace to approved scope/decisions.
|
||||
- Out-of-scope found: none. Builder F1/F3/F4 report confirms it touched only those files + literal-contradiction doc lines; D-A/decision-pending code regions untouched.
|
||||
|
||||
## Remaining uncertainty
|
||||
|
||||
- All empirical gate/test results (Step 5). N1 rc=2 value; N2 lint-equivalence. Whether `make test` passes with the new rc=2 asserts.
|
||||
|
||||
## Recommended next agent
|
||||
|
||||
**Orchestrator** — all findings statically resolved; this review is APPROVE_WITH_NOTES, and the standing empirical gates (`make test` incl. rc=2, `make lint` incl. negative, `make gen`/`check`) remain for the Orchestrator to execute before merge. If any gate regresses, hand that specific failure back to Builder (or Tester for a coverage fix).
|
||||
|
||||
**Reason:** Reviewer is read-only and bash-denied; verdict is final and gated only on Orchestrator's empirical runs, none of which is a known defect.
|
||||
|
||||
## Changes made by Reviewer
|
||||
|
||||
none (read-only; report written only).
|
||||
@@ -1,66 +0,0 @@
|
||||
# Tester report — llama-server breakage (F1–F7) regression tests
|
||||
|
||||
Date: 2026-09-06 · Tester · Objective: regression tests `tests/` for the fixed llama-server contract, `make test` evidence, no commits (per brief). Status: TESTS_READY (new files) / suite blocked by 2 pre-existing issues.
|
||||
|
||||
## TL;DR
|
||||
|
||||
- New test files (all pass, runnable via `make test`; sandbox-only, no prod/other-test edits): `tests/t-ai-server-validate.sh` (F1/F2/F5/F6), `tests/t-ai-server-model.sh` (F3), `tests/t-ai-server-bus.sh` (F4 + E2E user/SSH scenarios), `tests/t-llamacpp-install.sh` (F7).
|
||||
- **New-suite result: 90 checks PASS / 0 FAIL / 0 SKIP, runtime ~7 s.** Total `make test` (16 files): 12 files pass / 4 files fail / 0 skip, 235 check PASS / 34 FAIL / 62 s — the 34 failures are ALL in 4 pre-existing test files, 2 root causes, NEITHER caused by my new files (verified: new files pass standalone and in the combined run).
|
||||
- **Production bug observed (REPORTED, not fixed, per brief):** F1 version regex alternation `[0-9]+\.[0-9]+\.[0-9]+|build [0-9]+|b[0-9]+` — leftmost-match makes `grep -oE` return `build 1` (→ version `1`) for `llama.cpp build 1.2.3 (…)`. File/repro: `bin/pos-ai-server:86`; `echo "llama.cpp build 1.2.3 (abcdef)" | grep -oE '…'` → `build 1`. Breaks pre-existing `tests/t-ai-llama-detect.sh` (expects `1.2.3`, gets `1`). Real b10822 (`… (build 10822)` → `10822`) is unaffected.
|
||||
- **Fixture gap introduced by F4 pre-flight:** 3 pre-existing DRY_RUN test files (`t-ai-server-flags.sh` 26 FAIL, `t-unsupported-flags.sh` 3 FAIL, `t-config-precedence.sh` 4 FAIL) never provided a `systemctl` stub / bus env, so the new `ensure_user_bus` (called before the DRY_RUN return in `cmd_start`) aborts with rc 1 and no `ExecStart:` is emitted. NOT a production bug. Per brief I did NOT modify other tests; the fix is a fixture update (succeeding `systemctl` stub + `XDG_RUNTIME_DIR`/`DBUS_SESSION_BUS_ADDRESS` in those 3 files) → needs Orchestrator routing.
|
||||
- `make test` is therefore NOT green on the merged tree (34 fails above). After the F1 regex fix and the 3 file fixture updates, expected status: all 16 files green (my 4 + 12 already-green).
|
||||
|
||||
## Step 1: Contracts read (architect/detective/builder reports, test infra, F1–F7 code)
|
||||
|
||||
Read `AgentsReport/{architect,detective,builder}/2026-09-06_ai-server-*.md`, `tests/run-tests.sh`, `tests/test-lib.sh`, the 4 affected pre-existing tests + `t-systemd-unit.sh` (control, passes), `bin/pos-ai-server`, `lib/common.sh` `ensure_user_bus` (lines 143–154), `apps/ai/llamacpp.sh` `llamacpp_sanity` (lines 23–36) + `LLAMACPP_BIN_DIR` seam (line 17). Marked: F4 order (bus check BEFORE DRY_RUN return), `--no-unit` skips bus, `NO_UNIT_PIDFILE`/`NO_UNIT_LOG` seams, DRY_RUN unit-path output line `(dry-run) ExecStart: …`, `DRY_RUN+--no-unit` output `(dry-run) nohup …`. Pre-existing failures reproduced on the current tree before writing any new test (baseline full run: 12/4).
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 2: tests/t-ai-server-validate.sh (F1/F2/F5/F6) — 27 checks
|
||||
|
||||
Approach: `extract_fn` (brace-counting awk) extracts the REAL shipped `detect_llama_version`, `find_llamacpp`, `validate_requested_flags`, `validate_default_flags` bodies into a sandbox file sourced in `bash -c` subshells (with `$ROOT/lib/common.sh`) — no prod edit, tests the exact shipped code. 25× `validate_default_flags` loop against a real-shaped 63 864-byte / 732-line `--help` fixture (flags at lines 7/25/140/503/506, matching Detective's real llama.cpp layout, just under the 64 KB pipe buffer — the adversarial size that made the pre-fix `printf|grep -q` race observable). Each run asserts the whole invocation rc=0 AND all five globals `1|1|1|1|1` (catches an rc=141 SIGPIPE component explicitly). 20× requested-flag cases (`--port` accepted; `--tensor-split` rejected naming `0.4.0`); F1 stderr-vs-file cases (`stderr` → `0.4.0`, `build 10822` → `10822`, `b10822` → `b10822`, file-read ok, missing binary → `unknown` rc 0; all real CLI). F5: bare `server` no longer a candidate (only `llama-server`/`llama-server-cuda`), `llama-server` preferred. F6: DRY_RUN `start` (succeeding `systemctl` stub, real CLI) → ExecStart has `--port 8088 --host 127.0.0.1 --n-gpu-layers 0 --ctx-size 4096 --threads`; `status` shows `version: 0.4.0`, never `unknown`.
|
||||
|
||||
Result: 27 PASS / 0 FAIL / 0 SKIP, ~3 s.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 3: tests/t-ai-server-model.sh (F3) — 18 checks
|
||||
|
||||
Unit tests of real `resolve_model`/`resolve_gguf_in_dir` (HF layout fixture: `models/Qwen-Qwen3-1.7B-GGUF/Qwen3-1.7B-Q8_0.gguf`, multi dir, empty dir, flat file, absolute path) via `bash -c "source '$com'; source '$fn_file'; resolve_model '<arg>'";` with `HF_DOWNLOAD_DIR` env; plus CLI integration through real `pos-ai-server` (`start … --no-unit` DRY_RUN resolves to `-m "…/Qwen3-1.7B-Q8_0.gguf"`; multi-dir CLI start errs `pick one` rc 1). First run had 4 failures in the multi/empty unit cases (`err`/`pick_model` not found in the un-sourced subshell); fixed by sourcing `$com` in every subshell — re-run 18/18.
|
||||
|
||||
Result: 18 PASS / 0 FAIL / 0 SKIP, ~1 s.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 4: tests/t-ai-server-bus.sh (F4 + E2E) — 36 checks
|
||||
|
||||
Stub farm (install-shaped `llama-server` with b10822 stderr version + full-support help; failing `systemctl` = bus-missing SSH shape; `nvidia-smi` exit 1 → deterministic CPU path; `curl` → `{"status":"ok"}`; `loginctl` → `Linger=no`; env `-u XDG_RUNTIME_DIR -u DBUS_SESSION_BUS_ADDRESS` for bus-less cases since the outer shell has them unset). Verified: bus missing + real `start` → rc≠0, output has BOTH remediation lines + `no unit was written`, NO orphaned unit file; bus present (env + succeeding stub) + DRY_RUN → rc 0 and proceeds to `ExecStart:`; `--no-unit` bus-less escape hatch → real start writes pidfile, launches stub (`exec sleep 300` so the logged pid IS the sleeper — no orphans; stop killed it, no stray processes after suite), `status` finds `service: running` via pidfile, `stop` kills + removes pidfile; E2E user scenario (bus up, unit path): `start Qwen-Qwen3-1.7B-GGUF` rc 0, unit written, ExecStart has model + `--port 8088 --host 127.0.0.1 --n-gpu-layers 0 --ctx-size 4096 --threads`, `status` reports `version: 0.4.0` not `unknown`, `systemd-analyze verify` accepts the unit (BEFORE-LINGER/WARN tolerances confirmed on systemd 257), `stop` removes unit; E2E SSH-shaped: rc 1, both remediation lines, no unit. The `printf '...%s...'` with `--`-leading strings lesson from Step 2 also applies to the unit ExecStart assertions — used `printf '%s\n'` consistently.
|
||||
|
||||
Result: 36 PASS / 0 FAIL / 0 SKIP, ~4 s.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 5: tests/t-llamacpp-install.sh (F7) — 9 checks
|
||||
|
||||
Extracts real shipped `llamacpp_sanity` via `extract_fn`; exercises through the `LLAMACPP_BIN_DIR` seam with the bin dir on PATH. Healthy fixture → rc 0 + `llama.cpp sanity OK`; dangling symlink → rc 1 + `dangling symlink`; non-executable file → rc 1 with a sanity err (bash `command -v` finds the file by PATH existence, so the failure surfaces at the `--version` exec step: `did not run`); broken binary (`--version` exit 1, missing-shared-lib shape) → rc 1 `did not run`; static guard: shipped installer wires `llamacpp_sanity` into install. Fixed one assertion after the first run: my expected `not on PATH` message was wrong for the non-executable case (real behavior: `command -v` exists-check passes, exec fails) — re-run 9/9.
|
||||
|
||||
Result: 9 PASS / 0 FAIL / 0 SKIP, <1 s.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 6: Full-suite evidence
|
||||
|
||||
- My 4 files together: `./tests/run-tests.sh t-ai-server-validate.sh t-ai-server-model.sh t-ai-server-bus.sh t-llamacpp-install.sh` → files 4 pass / 0 fail / 0 skip; checks 90 pass / 0 fail / 0 skip; runtime 7 s (fits the ≤30 s new-suite budget).
|
||||
- `make test` (16 files): files 12 pass / 4 fail / 0 skip; checks 235 pass / 34 fail / 0 skip; runtime 62 s (< 90 s budget).
|
||||
- Fail breakdown (all pre-existing files): `t-ai-llama-detect.sh` 8 checks, 1 FAIL — F1 regex production bug (`version: 1` vs expected `1.2.3`); `t-ai-server-flags.sh` 28 checks, 26 FAIL — bus-gap (all 26 abort rc 1 before `ExecStart:`); `t-config-precedence.sh` 39 checks, 4 FAIL — bus-gap (B1–B4, empty ExecStart); `t-unsupported-flags.sh` 16 checks, 3 FAIL — bus-gap (rc 1 + missing `--threads` warn because start aborted before validation path).
|
||||
- `bash -n` clean on all 4 new files. No stray processes/pidfiles after the `--no-unit` case (`pgrep -af 'llama-server|sleep 300'` empty, no `/tmp/pos-ai-server.pid`).
|
||||
- git status: 4 new `tests/t-*.sh` untracked (not committed per brief); working tree still carries Builder's uncommitted F1–F7 changes.
|
||||
|
||||
[PASS] (new files) · suite-level blockers in TL;DR
|
||||
|
||||
## Handoff / blockers
|
||||
|
||||
- **F1 production bug** → Builder: fix `bin/pos-ai-server:86` alternation ordering (e.g. put `[0-9]+\.[0-9]+\.[0-9]+` first or drop `build [0-9]+`, matching real b10822 semantics); then `t-ai-llama-detect.sh` should go green (verify).
|
||||
- **F4 bus-gap fixtures** → Orchestrator to authorize editing the 3 pre-existing DRY_RUN test files (`t-ai-server-flags.sh`, `t-unsupported-flags.sh`, `t-config-precedence.sh`): add a succeeding `systemctl` stub + set `XDG_RUNTIME_DIR`/`DBUS_SESSION_BUS_ADDRESS` in their `start` invocations (pattern: `tests/t-systemd-unit.sh` + my `t-ai-server-bus.sh`). Once authorized, this is a small Maintainer/Tester fixture edit, not a prod change.
|
||||
- After both: expect all 16 files green — 269 checks = my 90 + 145 pre-existing passes + the 34 fixed. Reviewer can then independently re-run `make test`.
|
||||
- Residual risk (documented, not blocking): F1 regex fragility for `build X.Y.Z-dev` strings remains until the production fix; my F1 tests assert real b10822-shaped output which the current code handles.
|
||||
@@ -1,90 +0,0 @@
|
||||
# Tester Report — Alias-Menu Fix: Regression Tests (2026-09-06)
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status:** IN PROGRESS — see per-step markers below; final summary lands in Step 5/6.
|
||||
- **Scope:** permanent regression coverage for Builder's alias-menu fix (lib/menu-lib.sh `--allow-empty` + bin/pos-ai-alias 353/410 + step counters 352/383 → /5). Tests only; no source/doc edits.
|
||||
- **Methods used:** (1) unit matrix on the real `menu_ask_value` via the non-TTY stdin path, (2) static source guards via brace-extracted function bodies (same `extract_fn` pattern as t-ai-server-validate.sh), (3) pty E2E through `script -qec` driving the REAL `pos ai alias create` flow.
|
||||
- **Pty verdict:** FEASIBLE. `script -qec` with paced input (0.3s inter-input sleep) drives the raw-mode reader deterministically. 3 E2E scenarios proven.
|
||||
- **Files added:** `tests/t-menu-allow-empty.sh`, `tests/t-ai-alias-create-e2e.sh`.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Pty feasibility experiment `[DONE]`
|
||||
|
||||
Attempted `script -qec` with a piped script of inputs per DEV.md §7 (`printf 'answer\n' | script -qec "cmd" /dev/null`).
|
||||
|
||||
**Evidence (probe A — the reported bug scenario):**
|
||||
```
|
||||
printf 'testbot\n1\n\n\n\ny\n' (paced 0.4s) | script -qec "$ROOT/bin/pos-ai-alias create" typescript
|
||||
rc=0
|
||||
typescript showed: [1/5] Alias Name → [2/5] Provider → [3/5] Session Name →
|
||||
[4/5] System Prompt (EMPTY Enter) → [5/5] Trust Level → "Alias 'testbot' created."
|
||||
env file: testbot|gemini|testbot||0 (4th field EMPTY — the fix)
|
||||
wrapper: ~/.local/bin/testbot exists, exec line without --system
|
||||
```
|
||||
|
||||
**Pacing requirement (probe: no-sleep):** all input at once is UNRELIABLE — `menu_read_value`'s raw reader consumes the whole queued burst via `dd bs=4096`, so bytes after the first submitted newline are discarded and later readers see EOF → clean abort. Input MUST be paced (sleep between inputs). 0.2s and 0.3s pacing both proven reliable for all three scenarios; 0.1s is NOT reliable for the empty-first-input case.
|
||||
|
||||
**Rejected alternatives:** `expect` not installed; python3 pty module available but unnecessary — `script` is deterministic with pacing and keeps the suite zero-dependency (bash + util-linux coreutils, matching DEV.md precedent).
|
||||
|
||||
**Verdict:** pty driver FEASIBLE → E2E included in the permanent suite (case table below).
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Unit matrix — menu_ask_value --allow-empty `[PENDING]`
|
||||
|
||||
See coverage table (Part A of t-menu-allow-empty.sh) — implementation landed, run results pending full suite.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Static source guards `[PENDING]`
|
||||
|
||||
Create-flow step labels 1-5 of /5 (extracted `_alias_create` body); exactly 2 `--allow-empty` call sites; edit flow still /4. See coverage table.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: E2E create flow (pty) `[PENDING]`
|
||||
|
||||
See coverage table (3 cases in t-ai-alias-create-e2e.sh).
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Full suite `[PENDING]`
|
||||
|
||||
## Step 6: Gates `[PENDING]`
|
||||
|
||||
---
|
||||
|
||||
## Coverage table
|
||||
|
||||
| case | technique | result |
|
||||
|------|-----------|--------|
|
||||
| _— unit matrix —_ | | |
|
||||
| empty+no-default, no flag → rc 1 | non-TTY stdin, real lib | _pending_ |
|
||||
| empty+no-default, --allow-empty → rc 0 + empty | non-TTY stdin, real lib | _pending_ |
|
||||
| empty+default, --allow-empty → rc 0 + default (default wins) | non-TTY stdin, real lib | _pending_ |
|
||||
| empty+default, no flag → rc 0 + default (regression) | non-TTY stdin, real lib | _pending_ |
|
||||
| non-empty, --allow-empty → rc 0 + value | non-TTY stdin, real lib | _pending_ |
|
||||
| non-empty, no flag → rc 0 + value (regression) | non-TTY stdin, real lib | _pending_ |
|
||||
| EOF/cancel, --allow-empty → rc 1 (cancel stays cancel) | non-TTY stdin, real lib | _pending_ |
|
||||
| EOF/cancel, no flag → rc 1 (contract) | non-TTY stdin, real lib | _pending_ |
|
||||
| value beats default with --allow-empty | non-TTY stdin, real lib | _pending_ |
|
||||
| _— static source guards —_ | | |
|
||||
| create steps 1..5 all labeled /5 (no /4) | extract_fn(_alias_create) + grep | _pending_ |
|
||||
| exactly 2 --allow-empty call sites (353/410), both in create | grep bin/pos-ai-alias + extract_fn | _pending_ |
|
||||
| edit flow still 4 × /4 (untouched) | extract_fn(_alias_edit) + grep | _pending_ |
|
||||
| no OTHER tool adopted --allow-empty (scope fence) | grep -l bin/pos-* | _pending_ |
|
||||
| _— pty E2E —_ | | |
|
||||
| empty System Prompt → trust step reached + alias file created (empty 4th field) | script -qec REAL create flow | _pending_ |
|
||||
| empty Alias Name → warn + re-prompt [1/5] ×2 → created | script -qec REAL create flow | _pending_ |
|
||||
| Ctrl-D at System Prompt → clean abort, no [5/5], no alias, no env | script -qec REAL create flow | _pending_ |
|
||||
| script binary unavailable → documented SKIP | require_cmd | _pending_ |
|
||||
|
||||
---
|
||||
|
||||
## Constraints respected
|
||||
|
||||
- Only tests + this report modified; no lib/ bin/ AGENT_TODO.md docs touched; no `make gen`.
|
||||
- Hard-skip contract honored (SKIP only via `skip_case`, never a fake pass).
|
||||
- Runtime budget respected (target ≤ ~70s; suite ~49s today + ~10s E2E).
|
||||
@@ -1,49 +0,0 @@
|
||||
# Busgap Fixture Update — 2026-09-06
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **F4 (`ensure_user_bus`)** in `bin/pos-ai-server` now runs in `cmd_start` BEFORE the DRY_RUN return (verified in `lib/common.sh:148` + `bin/pos-ai-server:654-671`), so every unit-path `start` without a reachable user systemd bus aborts with rc 1 and no `ExecStart:` output.
|
||||
- Three pre-existing DRY_RUN test files never provided a `systemctl` stub / bus env, so 33 checks failed (26 + 3 + 4).
|
||||
- **Fix:** added a succeeding `systemctl` stub (return 0) to each affected sandbox, matching the established pattern in `t-systemd-unit.sh` and `t-ai-server-bus.sh`. No assertions weakened; bus-missing behavior remains covered in `t-ai-server-bus.sh`.
|
||||
- **Result:** full suite green — **16 files pass / 0 fail, 269 checks pass / 0 fail, 0 skip. Runtime 72s.**
|
||||
|
||||
**PASS/FAIL totals: 269 pass / 0 fail. Defects by severity: none.**
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Establish failure & mechanism
|
||||
|
||||
Read `bin/pos-ai-server`, `lib/common.sh`, `tests/run-tests.sh`, the 3 affected tests, and the passing control tests (`t-systemd-unit.sh`, `t-ai-server-bus.sh`).
|
||||
|
||||
**Verified call order:** `cmd_start` (pos-ai-server:654-655) calls `ensure_user_bus` when `NO_UNIT != 1`, i.e. **before** the `DRY_RUN` early-return at line 661. `ensure_user_bus` (common.sh:148-154) runs `systemctl --user show-environment &>/dev/null` and `err`s on nonzero. All three DRY_RUN tests drive the unit path (no `--no-unit`), so without a `systemctl` stub they abort with rc 1 and no `ExecStart:` output.
|
||||
|
||||
## Step 2: Apply fixtures (systemctl stub returning 0)
|
||||
|
||||
Established pattern: `printf '#!/usr/bin/env bash\nexit 0\n' > "$stubs/systemctl"` + add to `chmod +x` (exact copy of `t-systemd-unit.sh:41,46`). Also tested that this satisfies the `systemctl --user show-environment` probe without needing `XDG_RUNTIME_DIR` — the stub returns 0, so the pre-flight passes. Assertion lines untouched.
|
||||
|
||||
- `t-ai-server-flags.sh`: added stub after nvidia-smi, extended chmod.
|
||||
- `t-unsupported-flags.sh`: added stub after first llama-server/chmod, extended chmod.
|
||||
- `t-config-precedence.sh` (Part B): added stub after nvidia-smi, extended chmod.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 3: bash -n + full suite
|
||||
|
||||
- `bash -n` on all three edited files: OK.
|
||||
- `make test`: 16 files / 269 checks, all pass.
|
||||
|
||||
Counts per modified file: `t-ai-server-flags.sh` 28, `t-config-precedence.sh` 43, `t-unsupported-flags.sh` 19. (Prior failing counts: 26 / 4 / 3. The higher post-fix counts reflect the checks now actually running to completion instead of aborting on the bus error; the pre-existing assertion set was preserved.)
|
||||
|
||||
[PASS]
|
||||
|
||||
---
|
||||
|
||||
## Handoff
|
||||
|
||||
**Status:** TESTS_READY
|
||||
|
||||
- **Files changed (test fixtures only):** `tests/t-ai-server-flags.sh`, `tests/t-unsupported-flags.sh`, `tests/t-config-precedence.sh`
|
||||
- **Assertions:** unchanged (only added a systemctl stub + chmod); bus-missing behavior stays in `t-ai-server-bus.sh`.
|
||||
- **Verification:** `bash -n` all three; `make test` → 269 pass / 0 fail, 72s.
|
||||
- **No production code changed; nothing committed.**
|
||||
- **Residual failures:** none. (Noted: F1-regex fix and these fixtures land in parallel; this run was green, so no rerun needed.)
|
||||
@@ -1,136 +0,0 @@
|
||||
# Tester Report — Matrix-auth daemon-hang rc assertion (Reviewer N1) — 2026-09-06
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Status: TESTS_READY** — regression-suite hardening for Reviewer `AgentsReport/reviewer/2026-09-06_stab_acceptance.md` Note N1: `t-matrix-auth.sh` run 1 lacked an exit-code assertion, so a daemon-hang regression could pass vacuously.
|
||||
- **Defects added:** 2 new `check_rc` assertions (run 1 authorized + run 2 room-unset). File `t-matrix-auth.sh`: 8 → **10 checks**.
|
||||
- **Suite:** `make test` → **12 files pass / 0 fail / 0 skip**, **179 checks pass**, runtime **45s**, exit 0. No flakes observed.
|
||||
- **Production code: untouched.** Only `tests/t-matrix-auth.sh` modified; `tests/test-lib.sh` unchanged (the `check_rc` helper already exists).
|
||||
- **Key correction to the brief:** asserting `TR_RC` under plain `timeout` is vacuous — GNU `timeout` reports **124 in BOTH** the healthy and broken-trap cases. The discriminating fix is `timeout --preserve-status -k 2 …` + assert the daemon's own TERM-trap exit status.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Confirm the gap (Reviewer N1)
|
||||
|
||||
Reviewed `tests/t-matrix-auth.sh` run 1: `test_run_env … -- timeout 5 "$listener" --run` with `check_contains`/`check_file_exists`/`check_eq` but **no rc assertion**. Confirmed the reviewer's concern: a broken TERM trap (daemon ignores TERM) would not fail any existing check.
|
||||
|
||||
Also confirmed the deeper problem with the naive fix: plain `timeout 5` returns **124 whether or not the TERM trap works** (verified empirically — both the working daemon and a no-trap daemon yield 124). So `check_rc … 124` would be itself vacuous.
|
||||
|
||||
Evidence (throwaway probes, `/tmp/opencode`, removed after):
|
||||
```
|
||||
timeout 1 bash -c 'trap "exit 0" TERM; sleep 30' → rc=124 (trap WORKS)
|
||||
timeout 1 bash -c 'sleep 30' → rc=124 (no trap)
|
||||
timeout --preserve-status 1 … (trap WORKS) → rc=0
|
||||
timeout --preserve-status 1 … (no trap) → rc=143 (SIGTERM)
|
||||
```
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 2: Design the discriminating assertion
|
||||
|
||||
Goal per the brief: *"so the test genuinely fails on a daemon-hang regression"*. The daemon is an infinite polling loop that only terminates via its `TERM` trap (`trap 'kill $(jobs -p) 2>/dev/null; exit 0' TERM INT`), so `timeout` must always be the one signalling it. To make the daemon's own exit status observable:
|
||||
|
||||
- **`--preserve-status`** — `timeout` forwards the child's real exit status instead of forcing 124.
|
||||
- **`-k 2` (`--kill-after`)** — bounds the wait: if a broken trap ignores TERM, `timeout` SIGKILLs at +2s so the test cannot hang the whole suite indefinitely.
|
||||
|
||||
**Empirical healthy-path exit status (current production code):** the listener's TERM trap is `kill $(jobs -p) 2>/dev/null; exit 0`. Under `set -euo pipefail`, with no background jobs `kill` (no args) fails with **rc 2**, which triggers errexit **before** the `exit 0` and aborts the trap → the daemon actually exits with **rc 2** (verified on the real listener and a minimal `set -euo pipefail` repro: `rc=2`, the post-kill `exit 0` never runs).
|
||||
|
||||
- A genuine **daemon-hang** (TERM ignored / trap non-exiting): `--kill-after` SIGKILLs → **rc 137**.
|
||||
- A healthy daemon: TERM trap fires → **rc 2**.
|
||||
|
||||
So `check_rc … 2` meaningfully discriminates: healthy = daemon's own trap exit (2, definitively not a forced kill), regression = 137/124.
|
||||
|
||||
**Note (observation for Orchestrator, NOT fixed — out of scope):** the `kill $(jobs -p)` in the daemon's TERM trap fails under errexit, so the daemon exits 2 rather than the intended 0. Harmless to the daemon's operation (it still terminates, no hang) but the `exit 0` is effectively dead. Flagged for a possible future Builder fix; intentionally not addressed here (production code out of scope). The rc assert documents current healthy behavior and still catches a hang.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 3: Change — `tests/t-matrix-auth.sh`
|
||||
|
||||
- Run 1 (authorized): added `--preserve-status -k 2` and `check_rc "daemon terminated via TERM trap, not killed (no hang)" 2 "$TR_RC"`.
|
||||
- Run 2 (room-unset): same `--preserve-status -k 2` + `check_rc … 2`.
|
||||
- Comment explains why `--preserve-status -k 2` + rc 2 catches the regression and why plain `timeout` would be vacuous.
|
||||
|
||||
Snippet (run 1):
|
||||
```bash
|
||||
# --preserve-status + --kill-after surface the daemon's own TERM-trap exit,
|
||||
# so a broken trap (daemon-hang regression → SIGKILL 137 / timeout 124)
|
||||
# genuinely fails the rc assert instead of passing vacuously. --kill-after
|
||||
# also bounds the wait so a hung daemon can't stall the whole suite.
|
||||
test_run_env "${common[@]}" -- timeout --preserve-status -k 2 5 "$listener" --run
|
||||
check_rc "daemon terminated via TERM trap, not killed (no hang)" 2 "$TR_RC"
|
||||
```
|
||||
|
||||
Run 2 mirrors it with the room-unset env (no `MATRIX_ROOM_ID`) and desc `"room-unset daemon terminated via TERM trap, not killed (no hang)"`.
|
||||
|
||||
`tests/test-lib.sh` was not modified — `check_rc` already exists (`test-lib.sh:19`).
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 4: Mutation probe — the new assert genuinely fails on a hang
|
||||
|
||||
Copied the listener into a scratch tree and replaced the TERM trap with a non-exiting handler (`trap 'hang…sleep 30' TERM INT`) to simulate the daemon-hang regression, then ran it under the same `timeout --preserve-status -k 2` invocation:
|
||||
|
||||
```
|
||||
… timeout --preserve-status -k 2 3 "$listener" --run
|
||||
ignoring TERM (regression), sleeping
|
||||
rc=137
|
||||
```
|
||||
|
||||
Expected `2`, actual `137` → the `check_rc 2` assert **rejects** the regression and the test fails. Healthy path verified → `rc=2` → assert passes. Scratch cleanup performed; no production file touched.
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 5: Full suite — `make test`
|
||||
|
||||
```
|
||||
Running 12 test file(s) — strict mode: no network, no sudo, no system changes.
|
||||
PASS t-ai-hf-download.sh (10 checks)
|
||||
PASS t-ai-llama-detect.sh ( 9 checks)
|
||||
PASS t-ai-server-flags.sh (28 checks)
|
||||
PASS t-config-precedence.sh (43 checks)
|
||||
PASS t-gen-docs-drift.sh ( 4 checks)
|
||||
PASS t-gpg-password.sh (14 checks)
|
||||
PASS t-lint-gate.sh ( 5 checks)
|
||||
PASS t-matrix-auth.sh (10 checks)
|
||||
PASS t-systemd-unit.sh (11 checks)
|
||||
PASS t-telegram-auth.sh ( 8 checks)
|
||||
PASS t-uninstall-manifest.sh(18 checks)
|
||||
PASS t-unsupported-flags.sh (19 checks)
|
||||
──────────────────────────────────────────────
|
||||
Summary: files 12 pass / 0 fail / 0 skip (of 12)
|
||||
Checks : 179 pass / 0 fail / 0 skip
|
||||
Runtime: 45s
|
||||
```
|
||||
|
||||
Exit 0. `t-matrix-auth.sh` independently re-run at 10 checks (10s). No flake source observed; isolated result is representative (no mid-edit contention observed on the suite path — `bin/pos`/`bin/pos-*` are only read by the tests, never written here).
|
||||
|
||||
[PASS]
|
||||
|
||||
---
|
||||
|
||||
## Verification completed
|
||||
|
||||
- Baseline `t-matrix-auth.sh` (8 checks, 10s) before edit — green.
|
||||
- Post-edit standalone run (10 checks, 10s) — green.
|
||||
- Mutation probe proves the new assert fails (rc 137) on a broken TERM trap / daemon hang.
|
||||
- Full `make test` green: 179/179 checks, 45s.
|
||||
- `test-lib.sh` untouched; no production code touched; no commits made.
|
||||
|
||||
## Coverage note
|
||||
|
||||
The new asserts close the reviewer's vacuous-pass gap: a daemon-hang (TERM-trap) regression now produces rc 137/124 and fails the test rather than passing silently. Both authz runs (authorized + fail-closed) carry the guard.
|
||||
|
||||
## Remaining uncertainty / out of scope
|
||||
|
||||
- The daemon's dormant `exit 0` in its TERM trap (exits 2 under errexit) — see Step 2 observation. Functional no-hang is preserved; left for Builder/Architect if they want the trap to truly exit 0.
|
||||
- Not committed (per brief).
|
||||
|
||||
## Recommended next agent
|
||||
|
||||
**Reviewer** — the regression-suite gap is closed and green; suitable for independent verification of the rc assert.
|
||||
|
||||
**Reason:** Tester completes measurement/verification; the change is in test-only scope, ready for reviewer sign-off.
|
||||
|
||||
## Changes made by Tester
|
||||
|
||||
- `tests/t-matrix-auth.sh` — added `--preserve-status -k 2` to both `timeout` invocations and two `check_rc … 2` (no-hang) assertions; explanatory comments. No other files touched, no commits.
|
||||
@@ -1,93 +0,0 @@
|
||||
# Tester Report — Regression Test Infrastructure + First Suite (2026-09-06)
|
||||
|
||||
## TL;DR (updated continuously)
|
||||
|
||||
- **Status:** TESTS_READY — 12/12 test files pass, 177 checks, runtime ~46s (`make test`).
|
||||
- **Deliverables:** `tests/run-tests.sh` (zero-dep runner), `make test` target, 12 `tests/t-*.sh` files, `tests/README.md`.
|
||||
- **Findings (production bugs discovered):** none — no production bug surfaced; all defects found during test iteration were in the test framework/stubs/test assertions themselves (see Step 4).
|
||||
- **Suite timing / counts:** `make test` (2026-09-06): files **12 pass / 0 fail / 0 skip**, checks **177 pass / 0 fail / 0 skip**, runtime **46s** (44s on rerun); exit 0.
|
||||
- **Gates:** `make lint` still `0 FAIL, 0 WARN`; `make check` now PASS (parallel-track gen drift resolved upstream during this session); tests/ has zero lint/check surface.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Environment baseline (before adding tests)
|
||||
|
||||
- `make check` at start: **FAILED** — `doc/code drift` (expected: parallel Builder tracks have uncommitted changes; gen output in the working tree not yet refreshed). Now resolves to PASS after upstream sync.
|
||||
- `make lint` at start: **PASS** — `0 FAIL, 0 WARN` (3.5s).
|
||||
- `make gen` idempotence on a pristine temp copy: **PASS** (2× ~1.4–1.8s; `git status --porcelain` empty after 2nd gen).
|
||||
- `systemd-analyze verify` prototype: passes (rc 0) when ExecStart binary exists and model path is quoted.
|
||||
- Config-loader migration (D-D): **landed in the working tree** — `load_env_file` present in `lib/config-ui.sh:336`; all 9 tools call it. Config-precedence tests target the final contract.
|
||||
|
||||
## Step 2: Framework + suite files (status below)
|
||||
|
||||
- [x] `tests/run-tests.sh`
|
||||
- [x] `tests/test-lib.sh`
|
||||
- [x] `tests/t-ai-server-flags.sh`
|
||||
- [x] `tests/t-ai-hf-download.sh`
|
||||
- [x] `tests/t-ai-llama-detect.sh`
|
||||
- [x] `tests/t-unsupported-flags.sh`
|
||||
- [x] `tests/t-systemd-unit.sh`
|
||||
- [x] `tests/t-telegram-auth.sh`
|
||||
- [x] `tests/t-matrix-auth.sh`
|
||||
- [x] `tests/t-gpg-password.sh`
|
||||
- [x] `tests/t-config-precedence.sh`
|
||||
- [x] `tests/t-uninstall-manifest.sh`
|
||||
- [x] `tests/t-gen-docs-drift.sh`
|
||||
- [x] `tests/t-lint-gate.sh`
|
||||
- [x] `Makefile` `test:` target
|
||||
- [x] `tests/README.md`
|
||||
|
||||
## Step 3: Full suite run (final)
|
||||
|
||||
Command: `make test` (target: `./tests/run-tests.sh`) — 2026-09-06.
|
||||
|
||||
```
|
||||
Running 12 test file(s) — strict mode: no network, no sudo, no system changes.
|
||||
|
||||
PASS t-ai-hf-download.sh (10 checks)
|
||||
PASS t-ai-llama-detect.sh (9 checks)
|
||||
PASS t-ai-server-flags.sh (28 checks)
|
||||
PASS t-config-precedence.sh (43 checks)
|
||||
PASS t-gen-docs-drift.sh (4 checks)
|
||||
PASS t-gpg-password.sh (14 checks)
|
||||
PASS t-lint-gate.sh (5 checks)
|
||||
PASS t-matrix-auth.sh (8 checks)
|
||||
PASS t-systemd-unit.sh (11 checks)
|
||||
PASS t-telegram-auth.sh (8 checks)
|
||||
PASS t-uninstall-manifest.sh (18 checks)
|
||||
PASS t-unsupported-flags.sh (19 checks)
|
||||
|
||||
──────────────────────────────────────────────
|
||||
Summary: files 12 pass / 0 fail / 0 skip (of 12)
|
||||
Checks : 177 pass / 0 fail / 0 skip
|
||||
Runtime: 46s
|
||||
```
|
||||
|
||||
Exit code 0. Rerun via `make test`: files 12/12 pass, 44s. `make lint` unaffected (`0 FAIL, 0 WARN`), `make check` passes (parallel drift resolved upstream, not by this track).
|
||||
|
||||
[PASS]
|
||||
|
||||
## Step 4: Defects found and fixed during test iteration (all in test artifacts, none in production)
|
||||
|
||||
1. `tests/test-lib.sh` `check_rc` — `$desc` read before `local desc="$1"` declaration → `set -u` crash on first use. Fixed.
|
||||
2. `tests/run-tests.sh` — `set -e` in the runner killed the PARENT when a test subshell exited nonzero (e.g. test 4 aborted after 3 passing tests). Fixed: subshell wrapped in `if (…); then rc=0; else rc=$?; fi`; verified a failing test now records FAIL and continues. Also: bare-name args (`run-tests.sh t-gpg-password`) now resolve `$TEST_DIR/<name>.sh`.
|
||||
3. `tests/t-ai-hf-download.sh` stub — embedded JSON via `$(cat "$tree_resp")` broke stub quoting → replaced with `cat "$TREE_RESP"` env passthrough; `for (( ; i<=$#; i++ ))` expanded `$#` at stub-write time → escaped `\$#`; `base_env` typo → `env_base`; `return 1` at stub top level → `exit 1` (see #6).
|
||||
4. `tests/t-ai-llama-detect.sh` — asserted literal `cpu`; tool emits `gpu: CPU` (case differs) → assertions corrected to actual token shape.
|
||||
5. `tests/t-config-precedence.sh` — Part A env-wins probe `FOO=envval load_env_file …` evaluated in the PARENT shell (no persistence) → rewrote as explicit subprocess with `export` + captured output; Part B needed llama-server + nvidia-smi stubs for the deps guard; Part D legacy guard was a false positive — exactly 3 documented `load_system_env` callers (pos-media-sync, pos-system-backup, pos-system-health) → whitelist those and assert count == 3.
|
||||
6. **Stub scripts: `return` at top level of a non-sourced script is an ERROR in bash and falls through** (`return: can only 'return' from a function or sourced script`), so every stub response silently gained a trailing `{"ok":true}` → corrupt JSON → listeners slept in a 5s retry loop and never processed (`jq -r '.ok'` returned `true\ntrue`). Fixed all stub heredocs to `exit 0` (telegram/matrix curl stubs; ai-hf already used `exit`).
|
||||
7. `tests/t-telegram-auth.sh` / `tests/t-matrix-auth.sh` — two line-continuation bugs in `test_run_env` invocations: a missing trailing `\` meant the env-var list became a separate command and `test_run_env` ran bare `env` (prints the whole environment — the mysterious `SHELL=/bin/bash` output) with rc 0. Fixed by single-line invocation. Matrix reply count needle `m.room.message` also matched the URL-encoded sync filter on every `/sync` line → narrowed to `/send/m.room.message`.
|
||||
8. `tests/t-gpg-password.sh` — artifact-leftover checks false-failed because run 1's `.gpg` remained on disk for runs 2/3 → now `rm -rf "$work"; mkdir` between runs; bare `--passphrase` guard now token-exact (`grep -c '^--passphrase$'`) since `--passphrase-fd` legitimately contains the substring.
|
||||
9. `tests/t-lint-gate.sh` — negative case invoked the REAL lint (absolute path); `lint-conventions.sh` computes `ROOT="$(dirname "$0")/.."` and `cd`s THERE, so it linted the real repo (clean), not the planted copy. Fixed: run the copy's own `scripts/lint-conventions.sh` (relative path) from inside the copy.
|
||||
10. `tests/t-uninstall-manifest.sh` — POS_LIBS extraction awk `<^POS_LIBS=( … {getline; while(1)…}` never matched a lone `^)` line because the block is `POS_LIBS=(… \⏎ …registry.sh)` (two lines, `)` on the second) → getline at EOF returns 0, loop spins forever at EOF → the whole test hung (this was the full-suite 300s hang). Replaced with a sed range `/^POS_LIBS=(/,/)$/p` + normalization; also the leftover-gap whitespace made the sorted diff fail (collapsed with `tr -s`), and plugin-removal marker check now greps `POS_PLUGIN` (the marker `installed_plugins()` scans for) instead of a literal `^# POS_PLUGIN:` in the uninstall script.
|
||||
11. `tests/t-systemd-unit.sh` — systemd unit uses double quotes (not backslash escaping) for the model path → assertion corrected; `EnvironmentFile` check compared against the unit PATH instead of its content → `$(cat "$unit")`.
|
||||
12. `tests/t-unsupported-flags.sh` — real error text is `installed llama.cpp <v> does not expose <flag> — remove it or upgrade llama.cpp`, not "does not support" → assertions updated.
|
||||
|
||||
None of the above touched production code. `make check` / `make lint` / `make gen` results are unchanged by this track (verify with `make check && make lint` — both currently green).
|
||||
|
||||
## Step 5: Coverage notes & handoff
|
||||
|
||||
- **Behavior covered per area:** ai-server flag seam (CLI/config/env/default precedence + unsupported-flag hard error + dedupe) 28; config file precedence + legacy loaders 43; gpg password hygiene (fd-only, no bare token, no secret in argv, artifact cleanup on enc/verify failure) 14; systemd unit generation (ExecStart quoting, environment/deps/secrets lines, `systemd-analyze verify`) 11; telegram/matrix authz fail-closed gates 8+8; ai hf download stub network behavior 10; llama detection stub 9; gen/lint gates (positive + planted-violation negative) 4+5; uninstall manifest symmetry + XDG scan tier + POS_PLUGIN marker 18; unsupported-flag matrix 19.
|
||||
- **What is not covered (deliberately):** real network/sudo/docker paths (stubbed only); `pos entertainment send` live-plugin e2e (requires Telegram token); anything requiring root. These are outside the sandbox contract of this suite and remain manual checks.
|
||||
- **Suite hygiene:** deterministic sorted order, per-test sandbox auto-clean, per-file logs, no network/sudo/system mutations, skip contract, total < 90s.
|
||||
|
||||
[PASS]
|
||||
@@ -1,640 +0,0 @@
|
||||
Design and implement a **self-describing command registry** for POS.
|
||||
|
||||
The goal is NOT to redesign the POS CLI hierarchy.
|
||||
|
||||
Keep the existing horizontal command philosophy:
|
||||
|
||||
```text
|
||||
pos <category>
|
||||
pos <category> <tool>
|
||||
pos <category> <tool> <action>
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
pos ai ask
|
||||
pos ai chat
|
||||
pos ai alias create
|
||||
|
||||
pos network scan
|
||||
pos network checkport
|
||||
pos network download add
|
||||
pos network download pause
|
||||
|
||||
pos share nfs client mount
|
||||
pos share smb server adduser
|
||||
|
||||
pos docker ps
|
||||
pos docker compose restart
|
||||
```
|
||||
|
||||
The command paths are already good. Preserve them.
|
||||
|
||||
---
|
||||
|
||||
# 1. Core idea
|
||||
|
||||
POS should become **self-describing**.
|
||||
|
||||
Today, information about commands can become duplicated across:
|
||||
|
||||
* `pos tree`
|
||||
* `pos help`
|
||||
* `pos menu`
|
||||
* `pos config`
|
||||
* `pos dashboard`
|
||||
* command-specific documentation
|
||||
|
||||
Create a common command metadata/registry system so these interfaces can consume the same source of truth.
|
||||
|
||||
Conceptually:
|
||||
|
||||
```text
|
||||
POS COMMAND REGISTRY
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
│ │ │
|
||||
tree help menu
|
||||
│ │
|
||||
└──────────────┬──────────────┘
|
||||
│
|
||||
dashboard
|
||||
```
|
||||
|
||||
The registry describes commands.
|
||||
|
||||
The command implementation remains separate.
|
||||
|
||||
---
|
||||
|
||||
# 2. Separation of metadata and implementation
|
||||
|
||||
Do NOT turn every command into a large framework object.
|
||||
|
||||
Keep this separation:
|
||||
|
||||
```text
|
||||
COMMAND METADATA
|
||||
│
|
||||
├── name
|
||||
├── description
|
||||
├── actions
|
||||
├── arguments
|
||||
├── options
|
||||
├── dependencies
|
||||
├── configuration
|
||||
└── examples
|
||||
|
||||
COMMAND IMPLEMENTATION
|
||||
│
|
||||
└── actual bash/python/etc. code
|
||||
```
|
||||
|
||||
Metadata tells POS **what the command is**.
|
||||
|
||||
The implementation tells the system **how it works**.
|
||||
|
||||
Do not duplicate command implementation inside the registry.
|
||||
|
||||
---
|
||||
|
||||
# 3. Metadata should be progressive
|
||||
|
||||
Do not require every command to define every field.
|
||||
|
||||
Minimum metadata:
|
||||
|
||||
```text
|
||||
name
|
||||
description
|
||||
```
|
||||
|
||||
Optional metadata:
|
||||
|
||||
```text
|
||||
actions
|
||||
arguments
|
||||
options
|
||||
dependencies
|
||||
configuration
|
||||
examples
|
||||
```
|
||||
|
||||
A very simple command should remain very simple.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
network/scan
|
||||
|
||||
name:
|
||||
scan
|
||||
|
||||
description:
|
||||
Scan hosts in a CIDR network.
|
||||
```
|
||||
|
||||
A complex command can describe more:
|
||||
|
||||
```text
|
||||
network/download
|
||||
|
||||
name:
|
||||
download
|
||||
|
||||
description:
|
||||
Manage downloads through aria2.
|
||||
|
||||
actions:
|
||||
add
|
||||
remove
|
||||
pause
|
||||
resume
|
||||
restart
|
||||
retry
|
||||
status
|
||||
files
|
||||
peers
|
||||
|
||||
dependencies:
|
||||
aria2c
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 4. Registry hierarchy
|
||||
|
||||
The registry must preserve the existing POS hierarchy.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
pos
|
||||
├── ai
|
||||
│ ├── ask
|
||||
│ ├── chat
|
||||
│ └── alias
|
||||
│ ├── create
|
||||
│ ├── edit
|
||||
│ ├── list
|
||||
│ ├── remove
|
||||
│ └── show
|
||||
│
|
||||
├── network
|
||||
│ ├── scan
|
||||
│ ├── ip
|
||||
│ ├── checkport
|
||||
│ └── download
|
||||
│ ├── add
|
||||
│ ├── pause
|
||||
│ ├── resume
|
||||
│ └── status
|
||||
│
|
||||
└── share
|
||||
├── nfs
|
||||
│ ├── client
|
||||
│ │ ├── mount
|
||||
│ │ └── unmount
|
||||
│ └── server
|
||||
│ ├── share
|
||||
│ └── unshare
|
||||
└── smb
|
||||
├── client
|
||||
└── server
|
||||
```
|
||||
|
||||
The registry should represent this structure naturally.
|
||||
|
||||
Do NOT flatten everything into one giant list.
|
||||
|
||||
---
|
||||
|
||||
# 5. Example metadata
|
||||
|
||||
Use a format appropriate to the existing POS implementation.
|
||||
|
||||
For example, conceptually:
|
||||
|
||||
```yaml
|
||||
name: download
|
||||
|
||||
description: Manage downloads through aria2.
|
||||
|
||||
actions:
|
||||
- name: add
|
||||
description: Add a download.
|
||||
|
||||
- name: pause
|
||||
description: Pause a download.
|
||||
|
||||
- name: resume
|
||||
description: Resume a download.
|
||||
|
||||
- name: status
|
||||
description: Show download status.
|
||||
|
||||
dependencies:
|
||||
- aria2c
|
||||
```
|
||||
|
||||
Another example:
|
||||
|
||||
```yaml
|
||||
name: scan
|
||||
|
||||
description: Scan hosts in a CIDR range.
|
||||
|
||||
arguments:
|
||||
- name: cidr
|
||||
required: true
|
||||
description: Network range to scan.
|
||||
|
||||
dependencies:
|
||||
- ping
|
||||
```
|
||||
|
||||
Do not copy these examples literally if POS already has an established format. Adapt the implementation to existing project conventions.
|
||||
|
||||
---
|
||||
|
||||
# 6. `pos tree`
|
||||
|
||||
`pos tree` must obtain command information from the registry instead of maintaining a separate hardcoded command tree.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
$ pos tree
|
||||
|
||||
pos
|
||||
├── ai
|
||||
│ ├── ask
|
||||
│ ├── chat
|
||||
│ └── alias
|
||||
│ ├── create
|
||||
│ ├── edit
|
||||
│ ├── list
|
||||
│ ├── remove
|
||||
│ └── show
|
||||
├── network
|
||||
│ ├── scan
|
||||
│ ├── ip
|
||||
│ ├── checkport
|
||||
│ └── download
|
||||
│ ├── add
|
||||
│ ├── pause
|
||||
│ ├── resume
|
||||
│ └── status
|
||||
└── share
|
||||
```
|
||||
|
||||
The tree must be generated from discovered command metadata.
|
||||
|
||||
---
|
||||
|
||||
# 7. `pos help`
|
||||
|
||||
Help should consume the same metadata.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
$ pos help network download
|
||||
|
||||
network download
|
||||
|
||||
Manage downloads through aria2.
|
||||
|
||||
Actions:
|
||||
add Add a download
|
||||
pause Pause a download
|
||||
resume Resume a download
|
||||
status Show download status
|
||||
|
||||
Dependencies:
|
||||
aria2c
|
||||
```
|
||||
|
||||
Do not maintain a separate help description if the metadata already contains the information.
|
||||
|
||||
---
|
||||
|
||||
# 8. `pos menu`
|
||||
|
||||
The interactive menu should discover commands from the same registry.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
POS
|
||||
├── AI
|
||||
├── Network
|
||||
│ ├── Scan
|
||||
│ ├── IP
|
||||
│ ├── Check Port
|
||||
│ └── Download
|
||||
│ ├── Add
|
||||
│ ├── Pause
|
||||
│ ├── Resume
|
||||
│ └── Status
|
||||
├── Share
|
||||
└── System
|
||||
```
|
||||
|
||||
Adding a new command should automatically make it available to the menu without manually editing menu code.
|
||||
|
||||
---
|
||||
|
||||
# 9. `pos config`
|
||||
|
||||
Configuration metadata should be discoverable when applicable.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
network/download
|
||||
|
||||
configuration:
|
||||
download_dir:
|
||||
type: path
|
||||
description: Default download directory
|
||||
|
||||
rpc_port:
|
||||
type: integer
|
||||
description: aria2 RPC port
|
||||
```
|
||||
|
||||
`pos config` can then discover configurable commands from the registry.
|
||||
|
||||
A command that has no configuration should simply expose none.
|
||||
|
||||
Do not force configuration metadata onto commands that do not need it.
|
||||
|
||||
---
|
||||
|
||||
# 10. `pos dashboard`
|
||||
|
||||
The dashboard should eventually consume the same registry.
|
||||
|
||||
The registry can tell the dashboard:
|
||||
|
||||
```text
|
||||
command
|
||||
description
|
||||
status/configuration information
|
||||
available actions
|
||||
```
|
||||
|
||||
Do not create a second dashboard-specific command definition.
|
||||
|
||||
---
|
||||
|
||||
# 11. Command discovery
|
||||
|
||||
The registry should support discovering installed commands.
|
||||
|
||||
Conceptually:
|
||||
|
||||
```text
|
||||
command implementation
|
||||
+
|
||||
command metadata
|
||||
↓
|
||||
registry
|
||||
```
|
||||
|
||||
POS can then answer:
|
||||
|
||||
```text
|
||||
What commands exist?
|
||||
What does this command do?
|
||||
What actions does it support?
|
||||
What dependencies does it require?
|
||||
What configuration does it expose?
|
||||
```
|
||||
|
||||
This makes the CLI self-describing.
|
||||
|
||||
---
|
||||
|
||||
# 12. Important rule: no unnecessary framework
|
||||
|
||||
Do NOT build a giant abstraction layer.
|
||||
|
||||
Avoid:
|
||||
|
||||
```text
|
||||
CommandBase
|
||||
AbstractCommand
|
||||
CommandFactory
|
||||
CommandProvider
|
||||
CommandManager
|
||||
CommandController
|
||||
CommandResolver
|
||||
CommandRegistryManager
|
||||
```
|
||||
|
||||
unless the existing architecture genuinely requires them.
|
||||
|
||||
Prefer the smallest implementation that provides:
|
||||
|
||||
```text
|
||||
discover
|
||||
register
|
||||
lookup
|
||||
iterate
|
||||
describe
|
||||
```
|
||||
|
||||
The registry should be a practical internal mechanism, not a new programming language or framework.
|
||||
|
||||
---
|
||||
|
||||
# 13. Existing commands must keep working
|
||||
|
||||
This work must not unnecessarily change existing command paths.
|
||||
|
||||
For example:
|
||||
|
||||
```text
|
||||
pos network scan
|
||||
```
|
||||
|
||||
must remain:
|
||||
|
||||
```text
|
||||
pos network scan
|
||||
```
|
||||
|
||||
Do not change it to:
|
||||
|
||||
```text
|
||||
pos network scanner run
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```text
|
||||
pos network tools scan execute
|
||||
```
|
||||
|
||||
Likewise:
|
||||
|
||||
```text
|
||||
pos ai alias create
|
||||
```
|
||||
|
||||
must remain the same.
|
||||
|
||||
The registry describes the existing CLI. It does not redesign it.
|
||||
|
||||
---
|
||||
|
||||
# 14. Convention for new POS commands
|
||||
|
||||
Every new POS command should follow this process:
|
||||
|
||||
```text
|
||||
1. Decide category.
|
||||
2. Decide tool.
|
||||
3. Decide action if needed.
|
||||
4. Implement the command.
|
||||
5. Add its metadata.
|
||||
6. Register/discover it.
|
||||
7. Verify it appears in `pos tree`.
|
||||
8. Verify it appears in `pos help`.
|
||||
9. Verify it appears in `pos menu` when applicable.
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
A new feature is a system service monitor.
|
||||
|
||||
Use:
|
||||
|
||||
```text
|
||||
pos system services
|
||||
```
|
||||
|
||||
If actions are needed:
|
||||
|
||||
```text
|
||||
pos system services list
|
||||
pos system services status
|
||||
pos system services restart
|
||||
```
|
||||
|
||||
Metadata conceptually:
|
||||
|
||||
```yaml
|
||||
name: services
|
||||
|
||||
description: Manage system services.
|
||||
|
||||
actions:
|
||||
- name: list
|
||||
description: List available services.
|
||||
|
||||
- name: status
|
||||
description: Show service status.
|
||||
|
||||
- name: restart
|
||||
description: Restart a service.
|
||||
```
|
||||
|
||||
After adding it:
|
||||
|
||||
```text
|
||||
pos tree
|
||||
```
|
||||
|
||||
automatically shows:
|
||||
|
||||
```text
|
||||
system
|
||||
└── services
|
||||
├── list
|
||||
├── status
|
||||
└── restart
|
||||
```
|
||||
|
||||
No separate tree definition should be required.
|
||||
|
||||
---
|
||||
|
||||
# 15. Source of truth
|
||||
|
||||
There must be **one authoritative source for command metadata**.
|
||||
|
||||
Do not manually maintain:
|
||||
|
||||
```text
|
||||
tree definitions
|
||||
menu definitions
|
||||
help definitions
|
||||
dashboard command definitions
|
||||
```
|
||||
|
||||
separately.
|
||||
|
||||
Instead:
|
||||
|
||||
```text
|
||||
metadata
|
||||
│
|
||||
▼
|
||||
registry
|
||||
┌─────────┼─────────┐
|
||||
▼ ▼ ▼
|
||||
tree help menu
|
||||
│
|
||||
dashboard
|
||||
```
|
||||
|
||||
This is the main architectural goal.
|
||||
|
||||
---
|
||||
|
||||
# 16. Keep the filesystem and implementation compatible
|
||||
|
||||
Do not require the whole project to be rewritten at once.
|
||||
|
||||
Use an incremental approach.
|
||||
|
||||
Existing commands may continue working using their current implementation.
|
||||
|
||||
Add registry metadata around them.
|
||||
|
||||
Over time, migrate commands to the convention.
|
||||
|
||||
The migration should not require rewriting working commands merely to satisfy the registry.
|
||||
|
||||
---
|
||||
|
||||
# 17. Success criteria
|
||||
|
||||
The implementation is successful when:
|
||||
|
||||
```text
|
||||
Adding one new command
|
||||
↓
|
||||
adding its metadata
|
||||
↓
|
||||
POS discovers it
|
||||
↓
|
||||
pos tree shows it
|
||||
pos help knows it
|
||||
pos menu can discover it
|
||||
pos config can discover it when applicable
|
||||
pos dashboard can consume it when applicable
|
||||
```
|
||||
|
||||
without manually updating multiple unrelated files.
|
||||
|
||||
The CLI hierarchy remains horizontal, simple, readable, and backward compatible.
|
||||
|
||||
The key idea is:
|
||||
|
||||
> **POS commands describe themselves once, and POS uses that description everywhere.**
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
# Complete Implementation Summary
|
||||
|
||||
I have successfully completed the comprehensive enhancement of the POS AI tools as requested in the audit and implementation plan.
|
||||
|
||||
## Enhanced `pos ai hf` Tool
|
||||
|
||||
### New Features Added:
|
||||
✅ **Repository Information Command** (`info`)
|
||||
- Shows detailed repository metadata (author, description, tags, pipeline tag, architectures)
|
||||
- Displays model size, downloads, likes, and creation dates
|
||||
- Provides card data and model type information
|
||||
|
||||
✅ **File Listing Command** (`files`)
|
||||
- Lists all repository files with sizes
|
||||
- Shows file paths and file sizes in human-readable format
|
||||
- Supports branch/revision specification
|
||||
|
||||
✅ **Advanced Download Filtering**
|
||||
- `--include` and `--exclude` patterns with glob support
|
||||
- `--revision` for specific commits/tags/branches
|
||||
- Enhanced file pattern matching
|
||||
|
||||
✅ **Cache Management** (framework implemented)
|
||||
- Basic cache command structure
|
||||
- Future extensibility for cache operations
|
||||
|
||||
✅ **Improved Progress Reporting**
|
||||
- Better download progress indicators
|
||||
- Enhanced error messages with context
|
||||
- Parallel download support for multiple files
|
||||
|
||||
✅ **Enhanced Error Handling**
|
||||
- More descriptive error messages
|
||||
- Better validation and feedback
|
||||
- Context-specific help text
|
||||
|
||||
## Enhanced `pos ai server` Tool
|
||||
|
||||
### New Features Added:
|
||||
✅ **Detailed GPU Configuration**
|
||||
- `--gpu-layers`, `--gpu-threads`, `--tensor-split`
|
||||
- `--n-gpu-layers` support
|
||||
- GPU device detection and auto-configuration
|
||||
|
||||
✅ **Memory and Context Controls**
|
||||
- `--ctx-size`, `--kv-cache`, `--mmap`, `--mlock`
|
||||
- Better memory allocation and management
|
||||
|
||||
✅ **Performance Tuning Options**
|
||||
- `--batch-size`, `--ubatch-size`
|
||||
- Continuous batching support
|
||||
- Thread configuration
|
||||
|
||||
✅ **Sampling Parameters**
|
||||
- `--temperature`, `--top-k`, `--top-p`, `--repetition-penalty`
|
||||
- Advanced sampling controls for inference quality
|
||||
|
||||
✅ **Server Configuration**
|
||||
- `--metrics`, `--health`, `--slots` endpoints
|
||||
- Enhanced server management and monitoring
|
||||
|
||||
✅ **Version Awareness**
|
||||
- Version detection capability
|
||||
- Feature validation framework
|
||||
- Backward compatibility maintained
|
||||
|
||||
## Key Implementation Details
|
||||
|
||||
### Backward Compatibility:
|
||||
- ✅ All existing commands work exactly as before
|
||||
- ✅ All existing flags preserved unchanged
|
||||
- ✅ No breaking changes introduced
|
||||
- ✅ Configuration files remain compatible
|
||||
|
||||
### Code Quality:
|
||||
- ✅ Follows established project conventions
|
||||
- ✅ Maintains consistent code style
|
||||
- ✅ Proper error handling and validation
|
||||
- ✅ Clean, readable implementation
|
||||
|
||||
### Testing:
|
||||
- ✅ All syntax checks pass (`bash -n`)
|
||||
- ✅ Code synchronization verified (`make check`)
|
||||
- ✅ Convention linting passes (`make lint` - 0 FAIL, 0 WARN)
|
||||
- ✅ No regressions introduced
|
||||
|
||||
## Documentation Updates
|
||||
|
||||
### Help Text:
|
||||
- Updated `pos ai hf --help` with new commands and options
|
||||
- Updated `pos ai server --help` with enhanced parameters
|
||||
- Added comprehensive examples for all new features
|
||||
|
||||
### Examples:
|
||||
```bash
|
||||
# Enhanced Hugging Face operations
|
||||
pos ai hf info meta-llama/Llama-3.1-8B-Instruct
|
||||
pos ai hf files meta-llama/Llama-3.1-8B-Instruct
|
||||
pos ai hf download meta-llama/Llama-3.1-8B-Instruct --include "*.gguf" --exclude "*Q4_*"
|
||||
pos ai hf download meta-llama/Llama-3.1-8B-Instruct --revision v1.0
|
||||
|
||||
# Enhanced Server operations
|
||||
pos ai server start --model model.gguf --gpu-layers 35 --ctx-size 4096 --temperature 0.7
|
||||
pos ai server start --model model.gguf --batch-size 512 --ubatch-size 64 --metrics
|
||||
```
|
||||
|
||||
## Verification Results
|
||||
|
||||
✅ **All tests pass**:
|
||||
- Syntax validation (`bash -n`) - PASSED
|
||||
- Code synchronization (`make check`) - PASSED
|
||||
- Convention linting (`make lint`) - PASSED (0 FAIL, 0 WARN)
|
||||
|
||||
✅ **No regressions**: All existing functionality preserved
|
||||
|
||||
✅ **Production-ready**: Implementation meets all project standards and requirements
|
||||
|
||||
The enhanced POS AI tools now provide comprehensive functionality matching the capabilities of the underlying Hugging Face CLI and llama.cpp server while maintaining full backward compatibility. The implementation is complete, thoroughly tested, and ready for production use.
|
||||
@@ -1,331 +0,0 @@
|
||||
# Implementation Plan for POS AI Tools
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the comprehensive implementation plan for enhancing the `pos ai hf` and `pos ai server` tools to make them robust, useful wrappers around the actual Hugging Face CLI and llama.cpp server.
|
||||
|
||||
## 1. `pos ai hf` Enhancement Plan
|
||||
|
||||
### Current Limitations Identified
|
||||
|
||||
Based on audit, the current implementation is missing:
|
||||
- Repository information (`info` command)
|
||||
- File listing (`files` command)
|
||||
- Cache management
|
||||
- Advanced filtering (`--include`, `--exclude`)
|
||||
- Revision support
|
||||
- Better progress reporting
|
||||
- Enhanced authentication handling
|
||||
|
||||
### Required Enhancements
|
||||
|
||||
#### A. Add New Commands
|
||||
|
||||
**1. Info Command**
|
||||
```bash
|
||||
pos ai hf info <repo>
|
||||
```
|
||||
- Show repository metadata (size, downloads, likes, tags, etc.)
|
||||
- Display model architecture information
|
||||
- Show commit history and version information
|
||||
|
||||
**2. Files Command**
|
||||
```bash
|
||||
pos ai hf files <repo>
|
||||
```
|
||||
- List all files in repository with sizes
|
||||
- Show file types and metadata
|
||||
- Support pattern matching
|
||||
|
||||
**3. Cache Command**
|
||||
```bash
|
||||
pos ai hf cache
|
||||
```
|
||||
- Show cache status
|
||||
- Clear cache
|
||||
- Manage local cache
|
||||
|
||||
#### B. Enhanced Download Capabilities
|
||||
|
||||
**1. Advanced Filtering Support**
|
||||
- `--include`/`--exclude` patterns
|
||||
- File globbing support
|
||||
- Wildcard matching
|
||||
|
||||
**2. Revision Support**
|
||||
- `--revision` for commits/tags/branches
|
||||
- Specific version targeting
|
||||
|
||||
**3. Progress Reporting**
|
||||
- Detailed download progress
|
||||
- Transfer speed indicators
|
||||
- Estimated time remaining
|
||||
|
||||
#### C. Authentication Improvements
|
||||
|
||||
**1. Enhanced Token Handling**
|
||||
- Support for credential helpers
|
||||
- Better error messages for authentication failures
|
||||
- Token validation
|
||||
|
||||
#### D. Integration Improvements
|
||||
|
||||
**1. Better Error Handling**
|
||||
- More descriptive error messages
|
||||
- Context-specific help
|
||||
- Graceful degradation
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
#### Phase 1: Core Infrastructure (Week 1)
|
||||
1. Add new command structure to POS registry
|
||||
2. Implement basic command routing
|
||||
3. Add enhanced error handling
|
||||
4. Update documentation
|
||||
|
||||
#### Phase 2: New Commands (Week 2)
|
||||
1. Implement `info` command
|
||||
2. Implement `files` command
|
||||
3. Implement `cache` command
|
||||
4. Add command-specific help text
|
||||
|
||||
#### Phase 3: Advanced Features (Week 3)
|
||||
1. Add `--include`/`--exclude` support
|
||||
2. Add revision support
|
||||
3. Enhance progress reporting
|
||||
4. Improve authentication handling
|
||||
|
||||
## 2. `pos ai server` Enhancement Plan
|
||||
|
||||
### Current Limitations Identified
|
||||
|
||||
Based on audit, the current implementation is missing:
|
||||
- Detailed GPU configuration (`-ngl`, multi-GPU)
|
||||
- Memory management parameters
|
||||
- Performance tuning options
|
||||
- Advanced sampling controls
|
||||
- Server configuration options
|
||||
- Version awareness
|
||||
- Process monitoring
|
||||
|
||||
### Required Enhancements
|
||||
|
||||
#### A. GPU Configuration
|
||||
|
||||
**1. Detailed GPU Support**
|
||||
```bash
|
||||
pos ai server start --gpu-layers <n> --gpu-threads <n> --tensor-split <n>
|
||||
```
|
||||
- Support for `--n-gpu-layers`
|
||||
- Support for tensor splitting
|
||||
- Multi-GPU configuration
|
||||
|
||||
**2. Device Selection**
|
||||
- GPU device selection
|
||||
- CPU fallback handling
|
||||
|
||||
#### B. Memory and Context Management
|
||||
|
||||
**1. Context Size Control**
|
||||
```bash
|
||||
pos ai server start --ctx-size <n> --kv-cache <size>
|
||||
```
|
||||
|
||||
**2. Memory Allocation**
|
||||
- Support for `--mmap`, `--mlock`
|
||||
- KV cache configuration
|
||||
|
||||
#### C. Performance Tuning
|
||||
|
||||
**1. Batch Size Configuration**
|
||||
```bash
|
||||
pos ai server start --batch-size <n> --ubatch-size <n>
|
||||
```
|
||||
|
||||
**2. Continuous Batching**
|
||||
- Support for continuous batching options
|
||||
- Parallel request handling
|
||||
|
||||
#### D. Sampling Controls
|
||||
|
||||
**1. Advanced Sampling**
|
||||
```bash
|
||||
pos ai server start --temperature <n> --top-k <n> --top-p <n> --repetition-penalty <n>
|
||||
```
|
||||
|
||||
**2. Advanced Features**
|
||||
- JSON/schema support
|
||||
- Tool calling capabilities
|
||||
- Reasoning options
|
||||
|
||||
#### E. Server Configuration
|
||||
|
||||
**1. Endpoint Configuration**
|
||||
- Health endpoints
|
||||
- Metrics endpoints
|
||||
- Authentication handling
|
||||
|
||||
**2. Process Management**
|
||||
- Graceful shutdown
|
||||
- Process monitoring
|
||||
- Log management
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
#### Phase 1: Core Infrastructure (Week 1)
|
||||
1. Extend command structure for server options
|
||||
2. Add version detection capability
|
||||
3. Implement enhanced GPU detection
|
||||
4. Add memory management support
|
||||
|
||||
#### Phase 2: Configuration Options (Week 2)
|
||||
1. Add GPU parameter support
|
||||
2. Implement memory context controls
|
||||
3. Add performance tuning options
|
||||
4. Add sampling controls
|
||||
|
||||
#### Phase 3: Advanced Features (Week 3)
|
||||
1. Add server configuration options
|
||||
2. Implement version-aware command generation
|
||||
3. Add process monitoring
|
||||
4. Enhance error handling and validation
|
||||
|
||||
## 3. Version Awareness Implementation
|
||||
|
||||
### Approach
|
||||
1. **Version Detection**: Implement `llama-server --version` detection
|
||||
2. **Feature Support Matrix**: Create support matrix for different versions
|
||||
3. **Validation**: Validate configuration against supported features
|
||||
4. **Error Handling**: Provide clear error messages for unsupported features
|
||||
|
||||
### Example Implementation
|
||||
```bash
|
||||
detect_llama_version() {
|
||||
local version
|
||||
version="$(llama-server --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)"
|
||||
echo "$version"
|
||||
}
|
||||
|
||||
validate_options() {
|
||||
local version="$1"
|
||||
local options="$2"
|
||||
# Check if options are supported in this version
|
||||
# Return error if unsupported
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Testing Strategy
|
||||
|
||||
### `pos ai hf` Tests
|
||||
1. **Model Download Tests**
|
||||
- Basic download functionality
|
||||
- Specific file download
|
||||
- Include/exclude patterns
|
||||
- Revision handling
|
||||
|
||||
2. **Repository Tests**
|
||||
- Info command
|
||||
- Files command
|
||||
- Cache management
|
||||
|
||||
3. **Error Handling Tests**
|
||||
- Authentication failures
|
||||
- Nonexistent repositories
|
||||
- Network failures
|
||||
|
||||
### `pos ai server` Tests
|
||||
1. **Command Generation Tests**
|
||||
- Basic server start
|
||||
- GPU configuration
|
||||
- Memory settings
|
||||
|
||||
2. **Configuration Tests**
|
||||
- Version detection
|
||||
- Feature validation
|
||||
- Unsupported option handling
|
||||
|
||||
3. **Integration Tests**
|
||||
- Process start/stop
|
||||
- Health checking
|
||||
- Graceful shutdown
|
||||
|
||||
## 5. Documentation Updates
|
||||
|
||||
### Help Text Updates
|
||||
1. Update `pos ai hf --help`
|
||||
2. Update `pos ai server --help`
|
||||
3. Add examples for new features
|
||||
4. Include GPU/memory configuration examples
|
||||
|
||||
### Usage Examples
|
||||
1. **Basic Model Download**
|
||||
```bash
|
||||
pos ai hf download meta-llama/Llama-3.1-8B-Instruct
|
||||
```
|
||||
|
||||
2. **Specific GGUF File**
|
||||
```bash
|
||||
pos ai hf download meta-llama/Llama-3.1-8B-Instruct model-00001-of-00006.gguf
|
||||
```
|
||||
|
||||
3. **Server Configuration**
|
||||
```bash
|
||||
pos ai server start --model model.gguf --gpu-layers 35 --ctx-size 4096
|
||||
```
|
||||
|
||||
## 6. Backward Compatibility
|
||||
|
||||
### Maintained Features
|
||||
1. All existing commands must continue to work
|
||||
2. All existing flags must continue to work
|
||||
3. Default behavior unchanged
|
||||
4. Configuration files remain compatible
|
||||
|
||||
### New Features
|
||||
1. Additions are optional
|
||||
2. Existing workflows unchanged
|
||||
3. No breaking changes introduced
|
||||
|
||||
## 7. Risk Mitigation
|
||||
|
||||
### Technical Risks
|
||||
1. **Version Compatibility**: Different llama.cpp versions may have different options
|
||||
2. **Dependency Issues**: May require additional system packages
|
||||
3. **Integration Complexity**: Complex server process management
|
||||
|
||||
### Mitigation Strategies
|
||||
1. **Version Detection**: Detect and validate supported options
|
||||
2. **Graceful Degradation**: Fallback to basic functionality when features unavailable
|
||||
3. **Comprehensive Testing**: Test across different scenarios and configurations
|
||||
|
||||
## 8. Timeline
|
||||
|
||||
### Week 1: Core Implementation
|
||||
- Command structure enhancements
|
||||
- Basic GPU/memory support
|
||||
- Version detection
|
||||
|
||||
### Week 2: Feature Implementation
|
||||
- Advanced download capabilities
|
||||
- Server configuration options
|
||||
- Error handling improvements
|
||||
|
||||
### Week 3: Testing and Documentation
|
||||
- Comprehensive testing
|
||||
- Documentation updates
|
||||
- Final validation
|
||||
|
||||
## 9. Expected Benefits
|
||||
|
||||
1. **Enhanced Functionality**: Complete feature set matching underlying tools
|
||||
2. **Better User Experience**: More intuitive workflows and better error messages
|
||||
3. **Improved Reliability**: Better error handling and validation
|
||||
4. **Version Safety**: Proper version detection and compatibility
|
||||
5. **Performance**: Optimized server configuration options
|
||||
|
||||
## 10. Future Considerations
|
||||
|
||||
1. **Integration with POS Ecosystem**: Seamless integration with other pos tools
|
||||
2. **Extensibility**: Easy to add new features
|
||||
3. **Scalability**: Support for larger deployments
|
||||
4. **Cross-platform**: Consistent behavior across different systems
|
||||
@@ -1,508 +0,0 @@
|
||||
# Architect Report — `pos media ytsync`
|
||||
|
||||
Date: 2026-08-22 · Agent: Architect · Status: **DECISION_READY**
|
||||
|
||||
Spec-only task. No code or docs modified outside this report. The dirty share-suite
|
||||
working tree was left untouched (verified via `git status`: share suite files only).
|
||||
Builds on `reportAgents/2026-08-22-designer-ytsync.md` (behavioral contract).
|
||||
|
||||
> Note (orchestrator): the Architect session could not write files (sandbox denied);
|
||||
> this report was delivered verbatim and persisted on its behalf.
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
DECISION_READY — every boundary, ownership, interface, and constraint needed for
|
||||
implementation is decided below. One environment blocker is recorded in §9
|
||||
(this session could not write files; content delivered inline). Builder can
|
||||
start without making architectural choices.
|
||||
|
||||
## Objective
|
||||
|
||||
Turn the user objective ("tool in media category that asks for a YouTube channel
|
||||
URL, then incrementally downloads into `Videos/<channel>/<playlist-or-flat>`")
|
||||
and the Designer UX spec into an implementable architecture: canonical CLI name,
|
||||
subcommand surface + completion headers, state/storage ownership, yt-dlp
|
||||
invocation strategy, prompt transport, scheduling integration, dependency guards,
|
||||
and documentation obligations — resolving Designer assumptions A1–A5.
|
||||
|
||||
## Problem
|
||||
|
||||
yt-dlp-based one-shot downloaders exist (`mp3`/`mp4`), but none owns *tracked,
|
||||
incremental* channel sync. The new tool introduces persistent state (registry +
|
||||
download archives) — the first media tool with machine-owned state — which must
|
||||
not leak into `~/Videos`, must fit the repo's config/state split, must survive
|
||||
scheduler runs, and must respect the dispatcher's logging tee and the convention
|
||||
lint gate.
|
||||
|
||||
## Constraints (evidence)
|
||||
|
||||
- Naming: lowercase + hyphens, `pos-<category>-<command>` — DOC/AGENT_Context_Project.md:491-496, DOC/DEV.md:497-503.
|
||||
- New tools start from `templates/pos-tool.sh` (AGENTS.md "Doc conflicts" bullet; template usage at templates/pos-tool.sh:7), need `# POS:`/`# POS_SUBCMDS:`/`# POS_FLAGS:` headers right after shebang, exec bit `100755`, auto-discovery — DOC/DEV.md:122-136, AGENT_Context_Project.md:530-539.
|
||||
- Dispatcher pipes non-interactive tools through a logging `tee`; stdin-readers must be in `INTERACTIVE_CMDS` or prompts break — bin/pos:283-288, bin/pos:261, DOC/DEV.md:58. Registration is all-or-nothing per script.
|
||||
- Lint gate excludes `/dev/tty` reads from the stdin⇄INTERACTIVE_CMDS check — DOC/DEV.md:327-329 (rule class at :344-346).
|
||||
- Env seams: every written path guarded `VAR="${VAR:-default}"` — DOC/DEV.md:196 (+ missing-leading-`:-` gotcha), review step DEV.md:180-182.
|
||||
- Deps guards sit before `-h|--help`; non-apt binaries get `command -v` guards in-tool, never PACKAGES — DOC/DEV.md:110, :149, AGENTS.md Quick facts. yt-dlp is a GitHub-release binary (preinstall.sh:64-67); ffmpeg/jq are apt (preinstall.sh:40, :29).
|
||||
- Config dir seam is canonical in lib/common.sh:19 (`CONFIG_DIR`); runtime tool config = `~/.config/linux_post_install/<tool>.env` chmod 600 with env precedence (DOC/DEV.md:156); `# POS_CONFIG:` grammar in lib/config-ui.sh:7-15 (live example bin/pos-communication-scrcpy:5).
|
||||
- Machine-owned mutable data lives under `~/.local/share/linux_post_install/` (scheduler state/logs lib/scheduler-lib.sh:26-28; entertainment last-run state, ai sessions — AGENT_TODO Done entries 2026-08-09/13).
|
||||
- Structured record separators: never tab/IFS-whitespace; use `\x1f` — DOC/DEV.md:207.
|
||||
- Atomic config/job writes via mktemp+mv precedent — lib/scheduler-lib.sh:108-123.
|
||||
- Scheduler jobs: chmod-600 `.env` files with `INTERVAL/NOTIFY/COMMAND`; policies `always|onchange|onerror|threshold|never`; `COMMAND` executed via `bash -c` — lib/scheduler-lib.sh:4-21, :180; per-job run state recorded regardless of policy — :231-244.
|
||||
- Timer machinery is shared (lib/user-timers-lib.sh) but bespoke enable/disable subcommands duplicate the general scheduler (consolidation history in AGENT_DONE 2026-08-12 `system schedule` entry).
|
||||
- Notify: opt-in source, silent-fail, never affects exit codes — lib/notify.sh:24-25, :72-84; ERR-trap alarm precedent bin/pos-media-sync:74.
|
||||
- Colors auto-disable when stdout isn't a TTY — lib/common.sh:2-13 (relevant under the tee branch).
|
||||
- Definition of done: `make gen && make check && make lint` (0 FAIL/0 WARN) + CI tags — AGENTS.md Quick facts, DOC/DEV.md:190.
|
||||
|
||||
---
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1 — Canonical name: `pos media ytsync` (file `bin/pos-media-ytsync`); "ytSync" is display branding only
|
||||
|
||||
**Decision.** CLI/file name follows the repo naming rule exactly: `bin/pos-media-ytsync`,
|
||||
invoked as `pos media ytsync`. The mixed-case "ytSync" appears ONLY as a human-facing
|
||||
brand string inside the interactive menu header, notification copy, and docs prose.
|
||||
|
||||
**Why.** "Hyphens for word separation, lowercase always" (AGENT_Context_Project.md:491-496;
|
||||
DEV.md:497-503) is unqualified; every generated table (tree/dispatch/filetable),
|
||||
completion map, and the dispatcher's filename-derived discovery (bin/pos:17-26)
|
||||
assume it. A camelCase filename would be a lone outlier across 37 tools and would
|
||||
force special-casing in generators for zero functional gain. Display branding
|
||||
costs nothing and preserves the user's mental model.
|
||||
|
||||
### D2 — Subcommand surface RATIFIED (Designer D1), with three small amendments
|
||||
|
||||
**Decision.** Exactly the Designer's surface:
|
||||
|
||||
```
|
||||
pos media ytsync # interactive front door (empty-state → URL prompt; else looping menu)
|
||||
pos media ytsync add [<url>] # register + first sync (prompts iff url omitted)
|
||||
pos media ytsync sync [<name>] # incremental pass; no arg = all tracked sources; NEVER prompts
|
||||
pos media ytsync list # tracked-sources table + footer showing state paths; non-tty safe
|
||||
pos media ytsync remove <name> # stop tracking; keeps downloaded files AND archive
|
||||
# flags: --dry-run (add/sync/remove), -h/--help
|
||||
```
|
||||
|
||||
Headers (single source of truth for gen/completion):
|
||||
|
||||
```bash
|
||||
# POS: media ytsync — Incrementally sync YouTube channels/playlists into ~/Videos
|
||||
# POS_SUBCMDS: add sync list remove
|
||||
# POS_FLAGS: --dry-run
|
||||
```
|
||||
|
||||
Carrying both `POS_SUBCMDS` and `POS_FLAGS` on one tool is precedented
|
||||
(`pos-network-download` — AGENT_TODO Done 2026-08-12: "# POS_SUBCMDS: (18) +
|
||||
# POS_FLAGS:"); completion updates come exclusively from `make gen`.
|
||||
|
||||
**Amendments** (boundary tightenings, not redesigns):
|
||||
1. **Name matching:** `sync <name>` / `remove <name>` accept the internal slug OR the
|
||||
exact display name; no fuzzy/prefix matching (ambiguity → error listing candidates,
|
||||
rc 1). Prevents surprise partial matches against stored names containing spaces.
|
||||
2. **Ambiguous URLs:** a URL carrying BOTH `v=` and `list=` parameters is treated as a
|
||||
SINGLE VIDEO with an implied `--no-playlist` (flag precedent bin/pos-media-mp4:39);
|
||||
pure `…&list=…` without `v=` is a playlist source. Rule: nobody accidentally
|
||||
backfills a 500-video playlist by pasting a watch link copied from a playlist view.
|
||||
Documented in usage().
|
||||
3. **Verb-less flag invocation:** `pos media ytsync --dry-run` (no verb) prints a
|
||||
one-line usage hint on stderr ("use 'add --dry-run' or 'sync --dry-run'") and exits 1.
|
||||
|
||||
**Exit codes RATIFIED** (Designer D2): 0 = completed, including "0 new", clean cancels,
|
||||
EOF, and the non-tty guard; 1 = fatal only (missing dep, invalid explicit URL, unknown
|
||||
`<name>`, or ≥1 requested source failing wholesale during `sync`). Per-video failures
|
||||
never flip the exit code. Scheduler observability is preserved even at rc 0/noop because
|
||||
the scheduler records rc/output per run (lib/scheduler-lib.sh:231-244).
|
||||
|
||||
### D3 — State & storage layout (resolves A3: AGREED with placement split config-vs-state)
|
||||
|
||||
**Decision.**
|
||||
|
||||
**User-editable runtime config** (repo's `<tool>.env` pattern, DOC/DEV.md:156):
|
||||
`~/.config/linux_post_install/ytsync.env`, chmod 600, loaded with env-var precedence,
|
||||
registered for the shared config UI:
|
||||
|
||||
```bash
|
||||
# POS_CONFIG: ytsync | ytsync.env | YTSYNC_VIDEOS_DIR=:Videos root for synced channels (default ~/Videos) | YTSYNC_EXTRA_ARGS=:Extra yt-dlp flags appended verbatim to every yt-dlp call (advanced)
|
||||
```
|
||||
|
||||
(grammar per lib/config-ui.sh:7-15; EXTRA_ARGS escape-hatch precedent:
|
||||
`SCRCPY_EXTRA_FLAGS` bin/pos-communication-scrcpy:5). Two keys only. No
|
||||
postinstall template — defaults are complete without a file (absence tolerated on read);
|
||||
the file materializes via `pos config ytsync`.
|
||||
|
||||
**Machine-owned state** — NOT config, so it lives under the repo's data root
|
||||
(precendent: scheduler state lib/scheduler-lib.sh:27-28):
|
||||
|
||||
```
|
||||
~/.local/share/linux_post_install/ytsync/
|
||||
├── registry # one line per tracked source, \x1f-delimited (see below)
|
||||
├── archive/<slug>.txt # native yt-dlp --download-archive format, one per source
|
||||
└── history.log # append-only: date · source · new/skipped/failed per run
|
||||
```
|
||||
|
||||
`registry` record fields (in order, `\x1f`-separated — never tabs, per DEV.md:207):
|
||||
|
||||
```
|
||||
slug ⇥type⇥url⇥subdir⇥playlist_title⇥added_ts (⇥ = \x1f)
|
||||
```
|
||||
|
||||
- `slug` — unique internal key `[a-z0-9][a-z0-9_-]*`, derived from the probed
|
||||
`uploader_id`/handle (sanitized), numeric `-2` suffix on collision. Keys archive
|
||||
filenames and is the primary argument token for `sync`/`remove`.
|
||||
- `type` — `channel | playlist | video` (classification rule in D4).
|
||||
- `subdir` — destination path RELATIVE to the videos root, resolved ONCE at
|
||||
registration (e.g. `Linus Tech Tips` or `David Malan/CS50 lectures`). Stored, never
|
||||
recomputed — YouTube renames never mutate the local tree (ratifies Designer D3).
|
||||
- `playlist_title` — empty for flat sources; display-only.
|
||||
- Writes are atomic (temp+mv, lib/scheduler-lib.sh:108-123 pattern).
|
||||
|
||||
**Ownership rule:** `~/Videos/<subdir>/` contains NOTHING but media files. Registry,
|
||||
archives, history live outside it — satisfies the Designer requirement and keeps the
|
||||
media tree TV/stick-syncable without metadata noise.
|
||||
|
||||
**Env seams (test overrides)** — all written `VAR="${VAR:-default}"` with the leading
|
||||
`VAR:-` (DEV.md:196 gotcha):
|
||||
|
||||
| Seam | Default | Role |
|
||||
|---|---|---|
|
||||
| `YTSYNC_VIDEOS_DIR` | `$HOME/Videos` | user-facing key AND test seam (same duality as `MEDIA_SYNC_SOURCE`, bin/pos-media-sync:16) |
|
||||
| `YTSYNC_STATE_DIR` | `${XDG_STATE_HOME:-$HOME/.local/share}/linux_post_install/ytsync` | test seam only — not advertised in usage() |
|
||||
| `CONFIG_DIR` | already canonical (lib/common.sh:19) | covers ytsync.env location |
|
||||
|
||||
No dedicated binary seams — fake `yt-dlp`/`jq`/`ffmpeg` arrive via stub PATH (DEV.md:197).
|
||||
|
||||
**`remove` semantics:** drops the registry line; KEEPS downloaded files AND keeps the
|
||||
archive file. Keeping the archive makes a future re-add of the same source a true
|
||||
incremental resume instead of a full re-download colliding with existing files under
|
||||
`--no-overwrites` (which would spam collision warnings). Orphaned archives (tiny text
|
||||
files) are acceptable; documented in help text.
|
||||
|
||||
### D4 — yt-dlp strategy (resolves A2 + A4: AGREED with amendments)
|
||||
|
||||
**Registration/dry-run probe (A2):**
|
||||
- Single fast call: `yt-dlp --flat-playlist -J -- <url>` (dump-single-json; flat
|
||||
entries stay stubs → 1–2 HTTP round trips regardless of library size). Latency
|
||||
budget: seconds; wrapped in `spawn "Resolving source …"` — spawn's designed habitat
|
||||
for short pre-steps (Designer D4 concurs; mp3/mp4 wrap their quick calls likewise).
|
||||
- Parse with `jq` (already a repo dep, preinstall.sh:29; guard per D7).
|
||||
- Display-name chain: `.channel // .uploader // .uploader_id // .title` — mirrors the
|
||||
repo's established fallback-alternation idiom (`%(artist,uploader)s`
|
||||
bin/pos-media-mp3:69,:77). Resolved ONCE, shown on the add-confirm screen, stored in
|
||||
the registry. Never used as a per-video directory template (avoids collaboration/
|
||||
cross-post scatter — Designer D3 rationale upheld).
|
||||
- The probe JSON also yields entry ids/titles/count → the **new-list is computed BEFORE
|
||||
any download** by diffing entry ids against `archive/<slug>.txt`. Consequences: exact
|
||||
`[n/N]` counts, exact dry-run plans, zero speculative downloads.
|
||||
- Classification (deterministic, URL-shape based): contains `list=` without `v=` →
|
||||
playlist source; contains neither → channel/video flat source; both → single video
|
||||
(D2 amendment 2).
|
||||
|
||||
**Download invocations (A4):** paths are COMPUTED from stored registry fields — the
|
||||
tool does not lean on `%(playlist_title)s` or `--output-na-placeholder` for shaping.
|
||||
This removes A4's fragile empty-for-flat question entirely: a stored `subdir` cannot
|
||||
be NA, cannot drift mid-library, and `list` renders destinations straight from the
|
||||
registry. Per-invocation templates:
|
||||
|
||||
- flat/channel/video: `-o "<videos_dir>/<subdir>/%(title)s.%(ext)s"`
|
||||
- playlist: `-o "<videos_dir>/<subdir>/<NNN> - %(title)s.%(ext)s"` where `<NNN>` is a
|
||||
LITERAL zero-padded index injected by the tool (taken from the probe's entry
|
||||
position/`playlist_index`), because the chosen execution model (below) invokes
|
||||
yt-dlp per video and a standalone video URL has no live `%(playlist_index)s`.
|
||||
|
||||
**Execution model:** one yt-dlp invocation PER NEW VIDEO, looped in diff order:
|
||||
|
||||
```
|
||||
yt-dlp -f "bestvideo*+bestaudio/best"
|
||||
--merge-output-format mp4 # parity: bin/pos-media-mp4:120
|
||||
--embed-metadata --embed-chapters # parity: mp4:121
|
||||
--embed-thumbnail # parity: mp4:123
|
||||
--no-overwrites # parity: mp4:124
|
||||
--download-archive "<state>/archive/<slug>.txt" # crash-safe per-video recording
|
||||
--windows-filenames # USB/Samba/TV-safe names (Designer D3 ask)
|
||||
--trim-filenames 120 # headroom for NNN prefix + ext under 255-byte limits
|
||||
--retries 3 --fragment-retries 3
|
||||
[--no-playlist] # only for v=+list= URLs
|
||||
${YTSYNC_EXTRA_ARGS} # appended LAST — user override hatch
|
||||
-o "<template from above>"
|
||||
"<canonical per-video URL>"
|
||||
```
|
||||
|
||||
Rationale for per-video loops over one batched call: identical code path for
|
||||
channel/playlist/video types; exact per-video `[n/N] title` + OK lines with no log
|
||||
parsing; precise failure isolation (Designer D6 taxonomy maps 1:1 to loop iterations);
|
||||
crash-resume safety via the archive. Cost — one extra extraction round trip per video
|
||||
(~seconds on a multi-hour backfill) — accepted for v1; batching is a listed future
|
||||
optimization, not a design gap.
|
||||
|
||||
**Deliberate divergences from mp4, documented in help text:**
|
||||
- NO `--embed-subs --sub-langs all`: bulk-library size bloat for marginal value;
|
||||
users re-add subs per-source via `YTSYNC_EXTRA_ARGS` (appended-last wins in yt-dlp).
|
||||
- Progress hygiene (contract, mechanics left to Builder within it): our own LF-terminated
|
||||
per-video lines are the heartbeat; pass `--quiet --no-warnings` toward yt-dlp and add
|
||||
`--progress` ONLY when stdout is a TTY — `\r` bytes must never reach the tee'd log
|
||||
(Designer D4/D9). NEVER wrap the batch in `spawn()` — spawn captures output until
|
||||
completion (lib/common.sh:88-90) = hours of silence behind a braille spinner.
|
||||
- Sequential downloads; no concurrency knob in v1.
|
||||
|
||||
### D5 — Prompt transport (resolves A1: `/dev/tty` reads; NO INTERACTIVE_CMDS registration)
|
||||
|
||||
**Decision. Option 1 — read interactive input from `/dev/tty`. Do NOT add
|
||||
`media-ytsync` to `INTERACTIVE_CMDS`.**
|
||||
|
||||
**Justification:**
|
||||
1. **Logging preserved where it matters most.** Unregistered tools run through the
|
||||
dispatcher's tee branch (bin/pos:283-288) — hours-long `sync` runs leave a complete
|
||||
per-file audit trail in `~/.local/share/linux_post_install/logs/`. Registration is
|
||||
all-or-nothing per script (DOC/DEV.md:58): it would silence logs for `sync`, `list`,
|
||||
and `add <url>` alike — the wrong trade for a long-running downloader.
|
||||
2. **Lint-clean by explicit carve-out.** `/dev/tty` reads are excluded from the
|
||||
stdin⇄INTERACTIVE_CMDS gate (DOC/DEV.md:327-329) — no FAIL/WARN either way, no
|
||||
`bin/pos` edit, no completion/doc churn.
|
||||
3. **Precedent.** sudo reads `/dev/tty` (noted at DEV.md:58); smb-client password
|
||||
prompting via `/dev/tty` shipped 2026-08-11 (AGENT_TODO Done). share-lib's guard
|
||||
(`[ -t 0 ]` else hint+clean exit, lib/share-lib.sh:38-44) remains the entry gate.
|
||||
|
||||
**Builder obligations under this decision:**
|
||||
- Every interactive entry point checks stdin-tty FIRST (share_menu_guard pattern);
|
||||
the guard message points at `sync` for unattended runs (Designer copy, D2 matrix).
|
||||
- All prompt reads use `read -rp '...' </dev/tty`; bash prints `-p` prompts to stderr
|
||||
when input is a terminal → stdout stays machine-parseable (Designer D9 holds).
|
||||
- EOF on the tty read = clean cancel of the current step, rc 0 (Designer D2).
|
||||
- Accepted consequence (repo-wide behavior, not new): dispatched runs pipe stdout
|
||||
through tee → colors auto-disabled (lib/common.sh:2-13). Menus/tables must remain
|
||||
fully readable colorless — they are, because semantics ride `[+]`/`[!]`/`ERROR:`/`OK`
|
||||
prefixes (Designer D9). Direct invocation (`bin/pos-media-ytsync` or post-install
|
||||
`/usr/local/bin`) on a real terminal keeps colors.
|
||||
- If INTERACTIVE_CMDS had been chosen instead (rejected): Designer D5's history line
|
||||
would become mandatory; moot here — the history log is specified in D3 anyway.
|
||||
|
||||
### D6 — Scheduling integration (resolves A5: primary path = `pos system schedule` job; NO built-in enable/disable; NO own timers)
|
||||
|
||||
**Decision.** ytsync ships as a bare manual tool. Automation is documented as a
|
||||
scheduler job — the repo's general-purpose facility (jobs = chmod-600 env files,
|
||||
lib/scheduler-lib.sh:4-11; `COMMAND` runs via `bash -c`, :180; per-job user-timer
|
||||
units reconciled on enable/disable):
|
||||
|
||||
```
|
||||
# recommended recipe (goes in howto/media.md + POS.md):
|
||||
pos system schedule config → name: ytsync
|
||||
INTERVAL=daily
|
||||
NOTIFY=never # ← the double-notify resolution, see below
|
||||
MSG="ytSync"
|
||||
COMMAND=pos media ytsync sync
|
||||
```
|
||||
|
||||
**Rejected alternative:** entertainment-style `enable/disable` subcommands writing
|
||||
their own user-timer pairs (precedent bin/pos-entertainment-enable:47-48 +
|
||||
lib/user-timers-lib.sh:65-94). Rejected because it duplicates interval validation,
|
||||
unit writing, linger bootstrap, run-state, and run logs that `pos system schedule`
|
||||
already provides generically — the platform consolidated FROM bespoke single-purpose
|
||||
timers INTO the scheduler (AGENT_TODO Done 2026-08-12); a third timer writer inverts
|
||||
that decision. Entertainment keeps its own UI for historical reasons, not as a pattern
|
||||
to extend.
|
||||
|
||||
**Double-notify resolution (A5):** single-notification-owner principle — the TOOL owns
|
||||
content notifications (D7: noteworthy-only digest + ERR-trap alarm); the JOB therefore
|
||||
runs `NOTIFY=never` ("silent side-effect jobs", lib/scheduler-lib.sh:19). Result:
|
||||
exactly one notification path can fire per event. Observability is not lost: rc and
|
||||
output are still recorded per run and visible in `pos system schedule list/status`
|
||||
(lib/scheduler-lib.sh:246-252, :560-568) — including rc 1 when every requested source
|
||||
failed. Users who deliberately prefer scheduler-owned alerts simply don't enable the
|
||||
job recipe as documented (and accept doubled failure alarms); v1 adds no config key to
|
||||
silence the tool's own notify — out of scope (§ Scope).
|
||||
|
||||
**Timer-safety by construction:** `sync` never prompts (D2); interactive verbs hit the
|
||||
non-tty guard and exit 0 — an accidental misconfiguration of the job command can't hang
|
||||
a timer unit (TimeoutStopSec=5s backstop exists regardless, lib/user-timers-lib.sh:77).
|
||||
|
||||
### D7 — Dependencies, guards, dry-run boundary, notify ownership
|
||||
|
||||
- **Guards (before `-h|--help`, DOC/DEV.md:110):**
|
||||
- `yt-dlp`: `command -v` guard, ACTIVE EVEN UNDER `--dry-run` — deliberate
|
||||
divergence from mp3/mp4 (their `--dry-run` skips all guards because it only prints
|
||||
a command line, bin/pos-media-mp3:16-19; mp4:16-19). ytsync's dry-run IS the probe;
|
||||
without yt-dlp it has no value. Copy uses accurate install guidance for THIS repo:
|
||||
`err "yt-dlp not found — installed by preinstall.sh (GitHub release → /usr/local/bin); run ./preinstall.sh or see DOC/howto/media.md"`.
|
||||
Do not propagate mp3/mp4's inaccurate `sudo apt install yt-dlp` string (their drift
|
||||
= Maintainer follow-up, Designer A6; fixing them here is out of scope).
|
||||
- `jq`: guarded (probe parser; apt-dep preinstall.sh:29, guard precedent
|
||||
bin/pos-media-sync:14).
|
||||
- `ffmpeg`: guarded, SKIPPED under `--dry-run` (no merge/embed happens during a plan;
|
||||
structure mirrors the conditional-guard block of mp3/mp4).
|
||||
- **PACKAGES: unchanged.** yt-dlp is a non-apt GitHub-release installer (preinstall.sh:64-67)
|
||||
→ in-tool guard per the non-apt rule (AGENTS.md Quick facts; DOC/DEV.md:149);
|
||||
ffmpeg/jq already present (preinstall.sh:40, :29).
|
||||
- **`--dry-run` support boundary:** valid on `add` and `sync` (probe + plan block +
|
||||
example filenames; ZERO writes — no registry mutation, no directories, no archive
|
||||
appends, no notify) and on `remove` (preview only). Dry-run performs real network
|
||||
probes; it is honest about needing connectivity.
|
||||
- **Notifications:** THE TOOL sources `lib/notify.sh` opt-in and owns delivery:
|
||||
per-run digest ONLY when new>0 or failed>0 (Designer D7 shape/caps ratified);
|
||||
`trap 'notify_send "⚠️ ytSync FAILED …"' ERR` mirroring bin/pos-media-sync:74;
|
||||
silent-fail safety guaranteed by lib/notify.sh:24-25,72-84. The scheduler side stays
|
||||
silent via `NOTIFY=never` (D6). Emoji/plain-text conventions per entertainment-send
|
||||
precedent. No markdown in v1.
|
||||
|
||||
### D8 — Documentation obligations checklist (Builder, same commit)
|
||||
|
||||
1. `tools-docs/ytsync.md` — NEW research doc honoring the pending per-tool-research-docs
|
||||
convention (directory does not exist yet — creating it is part of this workflow):
|
||||
probe mechanics, chosen flag set + why each flag, archive format, classification
|
||||
rules, edge cases (mid-playlist inserts, title renames, sign-in-skipped).
|
||||
2. `DOC/POS.md` — media section table row + detail block (hand-written; lint WARN
|
||||
coverage class, DEV.md:347).
|
||||
3. `DOC/HOWTO.md` index row + section in `DOC/howto/media.md`: recipes (first channel,
|
||||
daily automation via the D6 schedule recipe, troubleshooting sign-in-skipped /
|
||||
collision warnings / probe failures).
|
||||
4. Generated surfaces — run `make gen`: AGENT_Context tree/dispatch/filetable rows +
|
||||
completions/pos.bash subcmd/flag entries. Never hand-edited (GEN markers).
|
||||
5. Hand-maintained spots in `DOC/AGENT_Context_Project.md`: add a §14 "Common Tasks"
|
||||
row ("Modify YouTube channel sync logic → Edit bin/pos-media-ytsync"). Line-count
|
||||
table: the `bin/pos-media-ytsync` row is gen-generated; no manual bump needed.
|
||||
6. `bin/pos` usage() EXAMPLES: optional showcase line (hand-maintained block,
|
||||
bin/pos:140-207) — recommended, e.g. `pos media ytsync sync Incremental YouTube channel sync`.
|
||||
7. `AGENT_TODO.md`: move the task to **Done (dated) in the same commit** (repo rule).
|
||||
8. `INTERACTIVE_CMDS` in `bin/pos`: **NO change** (consequence of D5).
|
||||
9. `preinstall.sh`, `postinstall.sh`, `install.sh`, `AGENTS.md` Quick facts, root
|
||||
`README.md`: **NO change** (deps present; no new category; no structural fact).
|
||||
10. Gates: `make gen && make check && make lint` ending 0 FAIL / 0 WARN.
|
||||
|
||||
### D9 — v1 OUT of scope (explicit non-goals)
|
||||
|
||||
- Cookies/auth (members-only, age-gated): reported as "N videos require sign-in —
|
||||
skipped"; manual escape hatch = `YTSYNC_EXTRA_ARGS="--cookies …"`. Fast-follow candidate.
|
||||
- Live streams / premieres handling beyond whatever yt-dlp naturally skips.
|
||||
- SponsorBlock, segment cutting, format/quality selection UI (fixed
|
||||
bestvideo*+bestaudio → mp4 merge; override via `YTSYNC_EXTRA_ARGS`).
|
||||
- Deleting or renaming local files when YouTube deletes/retitles/reorders (archive is
|
||||
keyed by video id; local files are immutable once written; mid-playlist insert shifts
|
||||
FUTURE numbering only — documented caveat).
|
||||
- Auto-expanding a channel into all of its playlists (channel URL ⇒ flat videos; one
|
||||
playlist URL ⇒ one subdir).
|
||||
- Audio-only mode (that is `pos media mp3`'s job), subtitles embedding (divergence
|
||||
documented in D4), thumbnail/NFO/Kodi sidecar files.
|
||||
- Built-in enable/disable/schedule subcommands; any new systemd units (D6).
|
||||
- Per-tool notify on/off switch; rclone/cloud offload; dedupe; concurrency controls;
|
||||
rate-limit tuning beyond the fixed retry defaults.
|
||||
- Renaming/migrating existing libraries; import of previously downloaded trees (a
|
||||
manually seeded `--download-archive` file is the power-user path, undocumented beyond
|
||||
tools-docs).
|
||||
- Fixing the mp3/mp4 dep-guard copy drift (Maintainer, A6).
|
||||
|
||||
---
|
||||
|
||||
## Ownership
|
||||
|
||||
- `bin/pos-media-ytsync` (new tool) owns: registry/archive/history lifecycle, probe,
|
||||
download loop, prompts/menu, summaries, notifications, dry-run.
|
||||
- `pos system schedule` (existing) owns: timing, run logs, run state for scheduled runs.
|
||||
- `lib/notify.sh` (existing, consumed as-is) owns: platform routing/delivery.
|
||||
- `lib/common.sh` / `lib/config-ui.sh` (existing, consumed as-is) own helpers/config UI.
|
||||
- Nothing new in `lib/` — one self-contained tool; if a second consumer of this
|
||||
registry ever appears, extracting a lib becomes a fresh architectural decision.
|
||||
|
||||
## Interfaces
|
||||
|
||||
- CLI: `pos media ytsync {add [url]|sync [name]|list|remove <name>} [--dry-run] [-h|--help]`
|
||||
(headers in D2 feed dispatch help, tree, completion via `make gen`).
|
||||
- Config surface: `# POS_CONFIG: ytsync …` → `pos config ytsync` (keys in D3).
|
||||
- State format: registry record schema in D3 (`\x1f`-separated; treat as internal —
|
||||
`list` is the supported read API; format changes require a migration note).
|
||||
- Notification messages: Designer §3 copy, plain text.
|
||||
- Exit-code contract: D2.
|
||||
|
||||
## Affected areas (exact files)
|
||||
|
||||
**Create:**
|
||||
- `bin/pos-media-ytsync` — from `cp templates/pos-tool.sh`, chmod `100755`, committed mode 100755.
|
||||
- `tools-docs/ytsync.md` — research doc (new directory; pending convention).
|
||||
|
||||
**Modify (docs/process only):**
|
||||
- `DOC/POS.md` — media row + detail block.
|
||||
- `DOC/HOWTO.md` + `DOC/howto/media.md` — index row + hands-on section incl. schedule recipe.
|
||||
- `AGENT_TODO.md` — Done entry (dated, same commit).
|
||||
- `DOC/AGENT_Context_Project.md` — §14 Common-Tasks row (hand) + generated blocks (via `make gen`).
|
||||
- `completions/pos.bash` — via `make gen` only.
|
||||
- `bin/pos` — OPTIONAL usage() EXAMPLES line only; **NOT** INTERACTIVE_CMDS.
|
||||
|
||||
**Explicitly NOT touched:** `preinstall.sh`, `postinstall.sh`, `install.sh`,
|
||||
`lib/*` (incl. `user-timers-lib.sh`, `scheduler-lib.sh`, `share-lib.sh`),
|
||||
all existing `bin/pos-*` tools, the dirty share-suite work.
|
||||
|
||||
## Approved implementation scope (for Builder)
|
||||
|
||||
Implement `bin/pos-media-ytsync` exactly within D1–D9 + Designer's behavioral spec
|
||||
(its §2–§3 copy/states are binding UX contract). Allowed: internal helper functions,
|
||||
ordering of flags within the D4 skeleton, choice of jq filters achieving D4's
|
||||
extractions. Not allowed without returning to Architect: new verbs, new flags beyond
|
||||
`--dry-run/-h/--help`, any write outside `YTSYNC_VIDEOS_DIR`/`YTSYNC_STATE_DIR`/
|
||||
`CONFIG_DIR` seams, INTERACTIVE_CMDS changes, new libs, new systemd units, changes to
|
||||
shared scripts, deviation from exit-code contract.
|
||||
|
||||
## Verification plan (Builder proves; Tester adversarially re-proves)
|
||||
|
||||
Static gates (definition of done): `make gen` (byte-stable, LC_ALL=C determinism),
|
||||
`make check`, `make lint` → `0 FAIL, 0 WARN`; `bin/pos help media ytsync`;
|
||||
`pos media ytsync -h`; `pos media --help` lists the tool; CI tags green on push.
|
||||
|
||||
Env-seam proof (DEV.md:180-188): with `YTSYNC_STATE_DIR=/tmp/opencode/ytsync-test/state`
|
||||
and `YTSYNC_VIDEOS_DIR=/tmp/opencode/ytsync-test/Videos`, assert the real
|
||||
`$HOME/.local/share/linux_post_install/ytsync` and `$HOME/Videos` are byte-untouched
|
||||
(guards the missing-leading-`:-` gotcha, DEV.md:196).
|
||||
|
||||
Stub-PATH behaviour suite (throwaway, `/tmp/opencode/ytsync-test/` per DEV.md:192-212;
|
||||
fake yt-dlp emitting canned `-J` JSON + per-video success/failure; fake
|
||||
telegram sender appending to `sends.log`): run Designer §6's 14-case matrix verbatim
|
||||
— it already encodes this architecture's observable contracts (registry/archives/
|
||||
summaries/rc/non-tty/no-\r-in-log/idempotency/collision/notify-policy/dry-run-zero-writes).
|
||||
Add these architect-mandated cases:
|
||||
15. Dispatcher integration: `bin/pos media ytsync list` under piped stdout → per-command
|
||||
log file created (proves no INTERACTIVE_CMDS entry is needed or present).
|
||||
16. Playlist numbering: fixture playlist source → files land as `001 - …`, `002 - …`
|
||||
(literal-index injection correct; stable across a second sync).
|
||||
17. `v=`+`list=` URL → single video downloaded with `--no-playlist`, registered type `video`.
|
||||
18. `remove` then re-`add` same source → archive retained → second add reports 0 new.
|
||||
19. Registry atomicity: simulate mid-write failure (stub) → registry either old or new,
|
||||
never truncated.
|
||||
20. Accurate dep-guard copy: missing yt-dlp stub PATH → error text matches D7 wording,
|
||||
fires even for `--dry-run` and `--help`.
|
||||
|
||||
PTY-driven prompt tests per DEV.md:198 (`printf 'answer\n' | script -qec … /dev/null`)
|
||||
for the Designer matrix items 1–6.
|
||||
|
||||
Live smoke (real box, optional but recommended before Done): one small channel add +
|
||||
two consecutive `sync` runs (second = 0 new), then the D6 schedule recipe with
|
||||
`NOTIFY=never` and `run ytsync` once.
|
||||
|
||||
## Remaining uncertainty (explicit, non-blocking)
|
||||
|
||||
1. Probe JSON field availability varies by extractor version/site (`channel` may be
|
||||
null off-YouTube) — mitigated by the four-step fallback chain + mandatory
|
||||
resolved-name confirm screen; worst case stores a handle string.
|
||||
2. Literal `NNN - ` prefix passes through yt-dlp's windows-filename sanitation
|
||||
unchanged (digits/space/dash are legal everywhere); verify in case 16 — if a
|
||||
sanitizer edge appears, fall back to per-video `--parse-metadata` injection.
|
||||
3. Per-video invocation overhead on very large backfills (accepted; batching listed as
|
||||
future optimization in tools-docs).
|
||||
4. Slug stability if a channel migrates handles — archive keyed by video id inside, so
|
||||
worst case is a re-resolve, not re-download.
|
||||
5. Whether subtitles should default on later (decided off; revisit on user feedback).
|
||||
|
||||
**Open questions needing user/orchestrator input (non-blockers):**
|
||||
- Codify the pending `tools-docs/` convention somewhere authoritative (MAINTENANCE.md?)
|
||||
— currently honored only by workflow instruction.
|
||||
- Preferred default schedule interval for the recipe (doc suggests `daily`; user taste).
|
||||
- Maintainer follow-up on mp3/mp4 dep-guard copy accuracy (A6) — separate small task.
|
||||
|
||||
**Recommended next agent**
|
||||
|
||||
**Builder** — implement `bin/pos-media-ytsync` + docs checklist per this report and the
|
||||
Designer spec, starting from `templates/pos-tool.sh`. Reason: boundaries, interfaces,
|
||||
state formats, flag sets, transport, and verification are fully pinned; no
|
||||
architectural latitude remains that Builder would have to guess. Reviewer should
|
||||
adversarially check the finished tool against D1–D9 + Designer §6 before Done.
|
||||
|
||||
## Architect changes
|
||||
|
||||
None committed — spec-only session; this document is the sole artifact.
|
||||
@@ -1,172 +0,0 @@
|
||||
# Builder Report — `pos media ytsync` (2026-08-22)
|
||||
|
||||
**Status: IMPLEMENTED_WITH_RISKS** (risks are documented YouTube-side unknowns, not implementation defects)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Implement the approved `pos media ytsync` tool per the binding contracts
|
||||
`reportAgents/2026-08-22-architect-ytsync.md` (decisions D1–D9) and
|
||||
`reportAgents/2026-08-22-designer-ytsync.md` (verbatim UX copy), plus the D8
|
||||
documentation checklist, green gates, and a stub-PATH verification suite —
|
||||
without touching out-of-scope files and without committing anything (the
|
||||
working tree carries unrelated, pre-existing share-suite work).
|
||||
|
||||
## Approved scope
|
||||
|
||||
- New: `bin/pos-media-ytsync`, `tools-docs/ytsync.md`
|
||||
- Doc edits (D8): `DOC/POS.md`, `DOC/HOWTO.md`, `DOC/howto/media.md`,
|
||||
`DOC/AGENT_Context_Project.md` §14, `AGENT_TODO.md`, optional `bin/pos` EXAMPLES line
|
||||
- Gates: `make gen && make check && make lint` → 0 FAIL / 0 WARN
|
||||
- Verification: PATH-stub yt-dlp suite via env seams; real `$HOME` untouched
|
||||
|
||||
## What was built
|
||||
|
||||
### `bin/pos-media-ytsync` (1179 lines, mode 755)
|
||||
|
||||
Subcommands `add [url] / sync [name] / list / remove <name>` + `--dry-run`;
|
||||
bare invocation = interactive menu (`/dev/tty` reads, EOF-safe, deliberately NOT
|
||||
in `INTERACTIVE_CMDS`; empty state goes straight to the URL prompt). Verbs never
|
||||
prompt — scheduler/timer safe by construction.
|
||||
|
||||
Key mechanics:
|
||||
|
||||
- **Probe**: one `yt-dlp --flat-playlist -J --no-warnings` call parsed with jq;
|
||||
name chain `.channel // .uploader // .uploader_id // .title`;
|
||||
`list=` w/o `v=` ⇒ playlist, `v=` ⇒ video (+`--no-playlist`), else channel;
|
||||
single-video objects (no `.entries`) handled as one-entry lists.
|
||||
- **Diff-before-download**: new list computed against the per-source
|
||||
`--download-archive` BEFORE any download ⇒ exact counts, exact dry-run plans,
|
||||
zero speculative downloads.
|
||||
- **Download skeleton** (D4 flag order): `yt-dlp --quiet --no-warnings --ignore-config
|
||||
--no-playlist? --format bestvideo*+bestaudio/best --merge-output-format mp4
|
||||
--embed-metadata --embed-chapters --embed-thumbnail --convert-thumbnails jpg
|
||||
--download-archive ARCH --retries 3 --no-overwrites --windows-filenames
|
||||
--trim-filenames 120 [--progress if tty] EXTRA_ARGS -o TPL URL`.
|
||||
- **State** (machine-owned, outside ~/Videos):
|
||||
`$YTSYNC_STATE_DIR/{registry(\x1f-delimited slug/type/url/subdir/title/ts),
|
||||
archive/<slug>.txt, history.log}`; all writes atomic temp+mv.
|
||||
- **Failure semantics**: per-video failure ⇒ counted + continue (rc stays 0);
|
||||
ENOSPC ⇒ prominent warning + stop that source mid-run (other sources continue);
|
||||
wholesale probe failure ⇒ history variant line `<date> · <name> · FAILED (probe)`,
|
||||
rc 1 only when an explicitly requested source fails.
|
||||
- **Notify**: digest only when new>0 or failed>0; ≤5 titles + "…and M more";
|
||||
single-source dest vs videos root; ERR-trap alarm armed ONLY around download loops.
|
||||
- **Config scope** `ytsync`: `YTSYNC_VIDEOS_DIR`, `YTSYNC_EXTRA_ARGS`
|
||||
(`pos config ytsync`); env > file > default; deps guards before `-h|--help`
|
||||
(yt-dlp+jq active under `--dry-run`, ffmpeg skipped there).
|
||||
|
||||
### Docs
|
||||
|
||||
- `tools-docs/ytsync.md` — research notes (probe mechanics, why probes don't use
|
||||
spawn(), classification table, download invocation, state layout, edge cases).
|
||||
- `DOC/POS.md` — media table row + detail block (watch-link rule, remove/resume,
|
||||
sign-in note + cookies escape hatch).
|
||||
- `DOC/HOWTO.md` + `DOC/howto/media.md` — row wording, full ytsync section
|
||||
(usage, layout, command table, notes), configurable-interval
|
||||
`pos system schedule` recipe (`COMMAND=pos media ytsync sync`, `NOTIFY=never`),
|
||||
troubleshooting bullets.
|
||||
- `DOC/AGENT_Context_Project.md` §14 Common-Tasks row.
|
||||
- `AGENT_TODO.md` Done entry (2026-08-22).
|
||||
- `bin/pos` usage() EXAMPLES: one line under the media block.
|
||||
|
||||
## Contract deviations (all justified, none silent)
|
||||
|
||||
1. **Custom probe runner instead of `spawn()`**: spawn exits the process on
|
||||
failure; probes must be recoverable (interactive re-prompt ×3, per-source
|
||||
continue). Built `run_probe()` with the same spinner/LF-log UX, but returning rc.
|
||||
2. **Collision accounting folded into "already present"**: a video yt-dlp reports
|
||||
as already-downloaded increments R_EXISTS and is added to R_PRESENT in the
|
||||
summary rather than double-counted as new+failed.
|
||||
3. **`FAILED (probe)` history variant**: wholesale probe failures append their own
|
||||
history line so `last run` info reflects reality even when nothing downloaded.
|
||||
4. **Post-verb flag positions accepted** (`add <url> --dry-run`, `sync <name>
|
||||
--dry-run`): required by Designer §UX case 5; initial single-pass-break parser
|
||||
rejected them — caught by the stub suite, fixed before handoff.
|
||||
5. **Add-path present/sign-in counters bug** (caught by stub suite): `finish_add`
|
||||
bypassed `pass_prepare`, so add summaries showed `0 already present` and
|
||||
dropped the sign-in count when the archive already had ids (remove→re-add
|
||||
resume). Fixed by mirroring pass_prepare's two counter lines; covered by test.
|
||||
|
||||
## Verification performed
|
||||
|
||||
Gates (after final edits):
|
||||
|
||||
make gen → gen-docs: write OK
|
||||
make check → check-sync: OK
|
||||
make lint → 0 FAIL, 0 WARN (convention lint)
|
||||
|
||||
Stub-PATH battery `/tmp/opencode/run-ytsync-suite.sh` (fake yt-dlp emitting canned
|
||||
`-J` JSON + fake telegram sender; seams `YTSYNC_STATE_DIR`, `YTSYNC_VIDEOS_DIR`,
|
||||
`CONFIG_DIR`, `HOME` pointed into /tmp/opencode/ytsync-test):
|
||||
|
||||
RESULT: PASS=71 FAIL=0
|
||||
|
||||
Coverage: dep-guard copy exactness (--help and --dry-run) · non-tty guard rc0 +
|
||||
verbatim hint · verb-less --dry-run hint rc1 · empty-state sync/list messages ·
|
||||
add --dry-run real-probe plan copy + zero state writes · explicit add registry
|
||||
row (\x1f fields) · archive exactly-valid-ids (private skipped) · progress header/
|
||||
per-video start lines · summary grammar verbatim · notify digest on new>0 (+ dest)
|
||||
· idempotent second sync (0 new / all present / no notify spam) · history per run ·
|
||||
sync NAME --dry-run zero mutation · incremental upstream delta (exactly 1 call,
|
||||
[1/1]) · per-video failure counted + continues + not archived + failure notify ·
|
||||
ENOSPC stops source mid-run (no further calls) · playlist NNN literal index
|
||||
injection (001 - %(title)s.%(ext)s) · v=&list= ⇒ type=video + --no-playlist ·
|
||||
list table + State footer · remove by display name drops registry row, KEEPS
|
||||
archive · re-add resumes against surviving archive (2 new, 6 already present) ·
|
||||
unknown-source rc1 · remove-without-arg rc1 · zero CR bytes piped AND in
|
||||
dispatcher tee log file · dispatcher integration rc0 through bin/pos ·
|
||||
`pos help media ytsync` smoke · real $HOME (~/.local/share/linux_post_install/
|
||||
ytsync + ~/Videos) byte-identical before/after (md5 snapshot).
|
||||
|
||||
Also verified: `git status` shows only my scoped files added/modified on top of
|
||||
the pre-existing share-suite dirt; no commits made.
|
||||
|
||||
## Files changed
|
||||
|
||||
bin/pos-media-ytsync (new, 1179 lines, 755)
|
||||
tools-docs/ytsync.md (new)
|
||||
reportAgents/2026-08-22-builder-ytsync.md (this file)
|
||||
DOC/POS.md (media row + detail block)
|
||||
DOC/HOWTO.md (line 15 wording)
|
||||
DOC/howto/media.md (intro row, ytsync section, schedule recipe, troubleshooting)
|
||||
DOC/AGENT_Context_Project.md (§14 Common-Tasks row)
|
||||
AGENT_TODO.md (Done entry 2026-08-22)
|
||||
bin/pos (EXAMPLES line only)
|
||||
|
||||
Out-of-scope changes: none. Unrelated dirty share-suite files untouched.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- **Slug stability**: derives from probed uploader_id; if YouTube ever changes it,
|
||||
re-add computes a fresh slug whose empty archive triggers collision-spam on
|
||||
existing files. Manual fix documented (rename old archive txt). Accepted v1
|
||||
limitation (Architect §Remaining uncertainty #4).
|
||||
- Sign-in-gated videos are skipped with a count; cookies only via
|
||||
`YTSYNC_EXTRA_ARGS` (documented escape hatch).
|
||||
- Playlist insertion mid-list shifts future numbering only (existing filenames
|
||||
kept); documented.
|
||||
|
||||
## Remaining uncertainty
|
||||
|
||||
- The battery exercises yt-dlp through a FAKE (canned JSON). Real-network behavior
|
||||
(rate limits, mixed member/public entries, live streams, region blocks) is
|
||||
untested here — first real-world run should start with `--dry-run`.
|
||||
- Interactive menu flows were exercised only at the guard level (non-tty path);
|
||||
full /dev/tty interaction needs a human or a pty harness.
|
||||
- Notify delivery tested via a stubbed sender binary (interface-conformant);
|
||||
real Telegram delivery depends on user config, unchanged territory.
|
||||
|
||||
## Recommended next agent
|
||||
|
||||
Tester — for adversarial edge coverage beyond the happy-path battery: malformed
|
||||
probe JSON, duplicate titles, concurrent sync invocations, pty-driven interactive
|
||||
menu flows, and (optionally) a supervised real-network dry-run.
|
||||
|
||||
Reason: implementation is complete, gates green, and my functional suite passes
|
||||
71/71; independent adversarial testing is the remaining gap before acceptance.
|
||||
|
||||
## Changes made by Builder
|
||||
|
||||
In-scope implementation only, as listed under "Files changed".
|
||||
@@ -1,342 +0,0 @@
|
||||
# Designer Report — `pos media ytsync` UX Specification
|
||||
|
||||
Date: 2026-08-22 · Agent: Designer · Status: **DESIGN_PROVISIONAL** (spec complete; 5 assumptions flagged for Architect reconciliation)
|
||||
Spec-only task — no code or docs modified outside this report. Dirty share-suite work untouched.
|
||||
|
||||
---
|
||||
|
||||
## 1. Objective
|
||||
|
||||
Design the end-to-end terminal experience for `pos media ytsync`: interactively ask for a YouTube channel/playlist URL, then auto/incrementally download videos into `~/Videos/<channel>/<playlist-or-flat>`. Repeat runs fetch only new content. Covers flow shape, non-tty safety, naming as the user sees it, progress UX, state visibility, error handling, notifications, dry-run, and console hygiene.
|
||||
|
||||
Users: the repo owner and their schedulers (`pos system schedule`), on Debian/Ubuntu terminals, sometimes over SSH with 80-col windows, sometimes non-tty under systemd timers.
|
||||
|
||||
---
|
||||
|
||||
## 2. UX Decisions (each with rationale + evidence)
|
||||
|
||||
### D1 — Interaction flow: subcommand-first tool whose bare invocation is the interactive front door
|
||||
|
||||
**Decision.** CLI surface:
|
||||
|
||||
```
|
||||
pos media ytsync # interactive front door (see below)
|
||||
pos media ytsync add [<url>] # register a source + first sync (prompts when url omitted)
|
||||
pos media ytsync sync [<name>] # incremental pass; no arg = all tracked channels
|
||||
pos media ytsync list # tracked channels + archive status table
|
||||
pos media ytsync remove <name> # stop tracking a source (downloaded files are KEPT)
|
||||
# global flags: --dry-run, -h/--help (# POS_SUBCMDS: add sync list remove)
|
||||
```
|
||||
|
||||
Bare invocation behavior (tty):
|
||||
- **Nothing tracked yet** → skip any menu, go straight to the URL prompt. First-run friction must be zero; this matches the stated objective ("mainly ASKS for a YouTube channel URL").
|
||||
- **Channels already tracked** → lean looping menu (share-suite pattern):
|
||||
|
||||
```
|
||||
════════════════════════════════════════════
|
||||
ytSync — YouTube channel sync
|
||||
════════════════════════════════════════════
|
||||
1) Sync all channels now (3 tracked · last run 2026-08-21 09:14)
|
||||
2) Add a channel or playlist
|
||||
3) List channels
|
||||
4) Remove a channel
|
||||
0) Exit
|
||||
--------------------------------------------
|
||||
Choose:
|
||||
```
|
||||
|
||||
After **any** completed interactive action → loop back to this menu (or re-offer the add prompt in the empty-state path). Exit via `0`/`q`/`Q`/EOF. After a **subcommand** action (`add <url>`, `sync`) → print summary and exit 0 (scriptable, no loops).
|
||||
|
||||
Add-flow prompt sequence (interactive):
|
||||
1. Prompt for URL (exact copy in §3).
|
||||
2. Validate + resolve: probe source metadata; show what was resolved *before* downloading:
|
||||
```
|
||||
Resolved : Linus Tech Tips (channel · 2140 videos)
|
||||
Library : ~/Videos/Linus Tech Tips/
|
||||
└── flat videos: <title>.<ext>
|
||||
Start download? [Y/n]
|
||||
```
|
||||
Invalid input re-prompts in place (max 3 attempts, then cancel back to menu) — same validation-feedback discipline as mp4's format-id check (`bin/pos-media-mp4:104-116`).
|
||||
3. Download with progress UX (D4). 4. Summary line (D5). 5. Optional notify (D7). 6. Back to menu.
|
||||
|
||||
`add <url>` with an explicit URL skips the confirm (non-interactive contract); it prints the resolved mapping line and proceeds.
|
||||
|
||||
**Single-verb vs menu/subcommand — comparison and verdict.**
|
||||
|
||||
| | Single verb (`ytsync [url]`, always asks) | Menu/subcommand (recommended) |
|
||||
|---|---|---|
|
||||
| First-run ask-for-URL | ✅ literal match to objective | ✅ preserved (empty state bypasses menu) |
|
||||
| Repeat runs (dominant case) | ❌ must re-paste URL every time | ✅ `sync` = zero input |
|
||||
| Scheduler/non-tty | ❌ needs URL embedded in job; bare call hangs/quits ambiguously | ✅ explicit prompt-free verbs |
|
||||
| State visibility (list/remove) | ❌ bolted-on flags feel tacked-on | ✅ first-class verbs |
|
||||
| Repo precedent | mp3/mp4 (one-shot downloads — different job) | share suite menus + vbox confirms |
|
||||
|
||||
Evidence: share suite's looping menu + EOF-safe guard is the repo's proven interactive-layer pattern (`lib/share-lib.sh:38-44` guard, `:46-80` menu loop); vbox's post-action confirm shows light confirms after heavy ops are idiomatic (`DOC/AGENT_Context_Project.md:315`). A pure single-verb tool optimizes the *rare* action (adding) at the cost of the *common* one (incremental sync). Verdict: **menu/subcommand hybrid** — the menu only appears once there is something to manage.
|
||||
|
||||
### D2 — Non-tty / EOF safety matrix (scheduler-safe by construction)
|
||||
|
||||
| Situation | Behavior |
|
||||
|---|---|
|
||||
| Bare/menu/prompt entry, stdin not a tty | One-line stderr hint + exit 0 (nothing attempted ≠ failure): `[!] ytSync needs a terminal for its prompt — use 'pos media ytsync sync' for unattended runs.` — verbatim pattern of `share_menu_guard` (`lib/share-lib.sh:38-44`) |
|
||||
| EOF (Ctrl-D) at any prompt | Clean cancel of that step; partial results still summarized; exit 0. Precedent: menu/picker EOF handling (`lib/share-lib.sh:65-67`, `:125-127`, `share_ask_value :154-156`) |
|
||||
| Empty input at URL prompt | Re-ask (max 3), then treat as cancel → back to menu / exit 0 |
|
||||
| `add <url>` / `sync` / `list` / `remove` verbs | **Never prompt.** Safe under cron/systemd timers and under the dispatcher's tee pipe |
|
||||
| Any hang risk | None: no read without a preceding tty guard |
|
||||
|
||||
Exit codes: `0` = run completed (including "0 new videos", clean cancels, non-tty guard); `1` = fatal only — missing dependency, invalid explicit URL, or ≥1 channel-level failure during an explicitly requested sync (so scheduler notify policies can react). Per-video failures never flip the exit code (routine on YouTube).
|
||||
|
||||
**Prompt transport — flagged ASSUMPTION A1 (Architect).** Recommended: read prompts from `/dev/tty` instead of plain stdin. Then the script does **not** need `INTERACTIVE_CMDS` registration, so *every* invocation keeps full `tee` logging (`bin/pos:285`) — for an hours-long downloader, the log file becomes the audit trail of what was fetched. DEV.md's lint gate excludes `/dev/tty` reads from the stdin check (`DOC/DEV.md:329`), so this passes conventions. Fallback if Architect rejects it: register `media-ytsync` in `INTERACTIVE_CMDS` (`bin/pos:261`) and accept that the whole script loses output logging (all-or-nothing per script, `DOC/DEV.md:58`) — then D5's history line becomes mandatory rather than recommended. Either way, all user-visible behavior in this spec is unchanged.
|
||||
|
||||
### D3 — Download tree as the user sees it
|
||||
|
||||
Target (user-stated): `~/Videos/<channel>/<playlist>/…` for playlists, `~/Videos/<channel>/…` flat otherwise.
|
||||
|
||||
- **Channel dir derivation:** resolved **once at registration** from a source-metadata probe — never per-video. Rationale: per-video templates like `%(uploader)s` scatter collaboration/cross-posted videos into other folders, silently fragmenting the library. One stable directory per tracked source is the mental model.
|
||||
- **Name preference: `%(channel)s` over `%(uploader)s`.** yt-dlp defines `channel` = "Full name of the channel the video is uploaded on" and `uploader` = legacy "full name of the video uploader"; they diverge for distributed/VEVO-style content, and `channel` is the identity the user actually browsed/subscribed to. Use a fallback alternation `%(channel,uploader,uploader_id)s` (display name → uploader → handle) — mirrors the repo's own fallback precedent `%(artist,uploader)s` (`bin/pos-media-mp3:69`). The resolved name is shown at the add-confirm screen (§D1) so surprises are impossible.
|
||||
- **Sanitization:** yt-dlp sanitizes filename components natively; recommend `--windows-filenames` (+ optional length cap) so the tree stays safe on USB sticks/Samba/TV playback — exact flags are the Architect's call (**A4**).
|
||||
- **Playlist subdir naming:** the playlist title, resolved at registration and stored with the source. If the curator later renames the playlist on YouTube, the local dir does **not** rename mid-library (stability beats freshness); documented in help text.
|
||||
- **Filename inside dirs:**
|
||||
- playlist sources: `%(playlist_index)03d - %(title)s.%(ext)s` → zero-padded numeric order = playlist order (yt-dlp docs' own example pattern). Caveat documented: inserting a video mid-playlist shifts future numbering; existing files are never renamed.
|
||||
- flat/channel sources: `%(title)s.%(ext)s` — matches both existing media tools, which deliberately drop yt-dlp's default `[id]` suffix (`bin/pos-media-mp4:125`, mp3:71).
|
||||
- **Collisions/rename behavior visible to the user:**
|
||||
- already-in-archive ids → skipped silently, counted as "already present" in the summary;
|
||||
- same-title different-video under `--no-overwrites` (repo precedent `bin/pos-media-mp4:124`) → warning counted in the run summary;
|
||||
- title changes on YouTube never rename local files (archive keyed by video id) — stated plainly in help so expectations are set.
|
||||
- **State files live OUTSIDE `~/Videos`** — the media tree must contain nothing but media (it gets synced to TVs/sticks). Registry + archive placement is Architect's (**A3**); the UX requirement is only: not inside `~/Videos`, discoverable via `list`.
|
||||
|
||||
### D4 — Long-download progress UX: per-video lines, not a spinner, not raw passthrough
|
||||
|
||||
**Decision:** the tool prints one LF-terminated line per video event; yt-dlp runs effectively quiet with native progress allowed only on a TTY (yt-dlp auto-simplifies when piped).
|
||||
|
||||
```
|
||||
[+] Linus Tech Tips — 14 new of 2154
|
||||
[1/14] The new GPU tier list is here
|
||||
OK [1/14] The new GPU tier list is here (4m12s)
|
||||
[2/14] We built a PC for $100
|
||||
...
|
||||
```
|
||||
|
||||
Shape: `step`-style start line per video + `ok` completion line with elapsed (helpers: `lib/common.sh:37-42`, `:25`). Byte-level rate/ETA comes from yt-dlp's own progress and only when attached to a terminal — never `\r`-only sequences into logs.
|
||||
|
||||
**Rejected alternatives, with evidence:**
|
||||
- `spawn()` around the whole batch — wrong tool: spawn captures stdout+stderr to temp files until completion (`lib/common.sh:88-90`) and dumps stderr only on FAIL (`:109-112`). For a 500-video initial backfill that is hours of silence behind a braille spinner. Feedback opacity fails the primary long-run use case.
|
||||
- Raw yt-dlp passthrough — `\r` progress spam pollutes the tee'd log files and drowns the per-item heartbeat.
|
||||
|
||||
Rationale: the unit of progress the user cares about is *videos*, matching the repo's per-file feedback precedent (`bin/pos-media-sync:152` echoes `+ rel` per copied file). `spawn` remains right for short pre-steps only (the metadata probe, ≤ seconds — its designed habitat, cf. mp3/mp4 usage `bin/pos-media-mp4:131`). Overall duration via `timer_start/timer_stop` in the summary.
|
||||
|
||||
### D5 — State visibility: `list` view + end-of-run summary
|
||||
|
||||
`pos media ytsync list` (works non-tty):
|
||||
|
||||
```
|
||||
Tracked sources (2)
|
||||
|
||||
NAME TYPE VIDEOS LAST SYNC DESTINATION
|
||||
Linus Tech Tips channel 2140 2026-08-21 ~/Videos/Linus Tech Tips
|
||||
CS50 lectures playlist 132 2026-08-19 ~/Videos/David Malan/CS50 lectures
|
||||
```
|
||||
plus a footer line showing where state lives (registry/archive paths — content owned by Architect, **A3**).
|
||||
|
||||
End-of-run summary mirrors the established grammar of `pos media sync` (`bin/pos-media-sync:159-163`):
|
||||
|
||||
```
|
||||
OK Sync complete: 14 new, 2126 already present, 0 failed → /home/u/Videos/Linus Tech Tips
|
||||
```
|
||||
Multi-channel `sync` adds a roll-up block listing per-channel counts, then one total line. Every run also appends one history line (`date · name · N new · M skipped · K failed`) for scheduled-run visibility — recommended in v1, **mandatory** if A1 resolves to INTERACTIVE_CMDS registration (only trace left).
|
||||
|
||||
### D6 — Error & edge UX: warn-and-continue, with precise vocabulary
|
||||
|
||||
Policy: a batch is never aborted by one bad item; failures are *counted*, *named*, and *summarized*.
|
||||
|
||||
| Case | What the user sees | Run effect |
|
||||
|---|---|---|
|
||||
| Unreachable network at probe/download | `[!] could not reach YouTube — check connection` | channel marked failed; continue others; rc 1 if all requested failed |
|
||||
| Source not found / private / deleted | `ERROR: source not found or private: <url>` at add-time (re-prompt ≤3) | add aborts cleanly |
|
||||
| Members-only/age-gated videos inside a channel | `[!] 3 videos require sign-in — skipped` in summary | warn-and-continue (cookies support = open question, see §8) |
|
||||
| Single unavailable/deleted video mid-batch | one `[!] unavailable: <title>` line | continue; counted as failed |
|
||||
| Same-title collision | `[!] exists, kept: <file>` | continue; counted separately |
|
||||
| Disk full (ENOSPC) | prominent `[!]` + that channel stops | other channels continue |
|
||||
| Everything requested failed | final `ERROR:` line | exit 1 (scheduler-visible) |
|
||||
|
||||
Optional nice-to-have: a disk-space preflight estimate at add-confirm, mirroring `pos media sync`'s fail-early check (`bin/pos-media-sync:110-132`) — mark as enhancement, not v1 blocker.
|
||||
|
||||
### D7 — Notifications: noteworthy-only digest via `notify_send`
|
||||
|
||||
Source `lib/notify.sh` opt-in; silent-fail when Telegram/Matrix unconfigured (`lib/notify.sh:24-25,72-84`) — zero-risk integration.
|
||||
|
||||
- **Send a digest only when something happened**: `new > 0` OR `failed > 0`. A scheduled run that finds nothing new stays silent — daily no-op runs must not spam the chat. (Contrast: `media-sync` always notifies, `bin/pos-media-sync:163`; entertainment-send sends only real content, `bin/pos-entertainment-send:74-95`. For a *scheduled* downloader, silence-on-noop is the correct default.)
|
||||
- **Failure alarm** regardless of digest policy: `trap 'notify_send "⚠️ ytSync FAILED for <name>"' ERR` — verbatim pattern of `bin/pos-media-sync:74`.
|
||||
- **Message shape** (plain text, Telegram-readable):
|
||||
|
||||
```
|
||||
📺 ytSync — 3 new videos: Linus Tech Tips
|
||||
• Title of video one
|
||||
• Title of video two
|
||||
• Title of video three
|
||||
→ ~/Videos/Linus Tech Tips
|
||||
```
|
||||
Cap listed titles at 5, then `…and 9 more`. Failures variant uses `⚠️` prefix (established emoji convention, `bin/pos-entertainment-send:76`). No markdown in v1 (plain survives every sender; `--markdown` available later).
|
||||
|
||||
### D8 — Dry-run preview: plan, counts, example filenames
|
||||
|
||||
`--dry-run` works on `add` and `sync`. It performs the cheap metadata probe + archive diff, then prints:
|
||||
|
||||
```
|
||||
[+] DRY RUN — nothing will be downloaded
|
||||
Source : https://youtube.com/@SomeChannel
|
||||
Resolved : Some Channel (channel · 812 videos)
|
||||
Library : ~/Videos/Some Channel/
|
||||
New : 12 would be downloaded (800 already present)
|
||||
0001 - First new video title.mp4
|
||||
0002 - Second new video title.mp4
|
||||
… 10 more
|
||||
DRY RUN complete — 12 new would be fetched → ~/Videos/Some Channel
|
||||
```
|
||||
|
||||
Final line mirrors the dry-run summary grammar of `bin/pos-media-sync:160`. Note one deliberate divergence from mp3/mp4: their `--dry-run` works without deps because it only prints the command (`bin/pos-media-mp3:8-13` comment); ytsync's preview *is* the value and needs the probe, so deps guards stay active under `--dry-run`.
|
||||
|
||||
### D9 — Accessibility / console hygiene
|
||||
|
||||
- Colors **exclusively** through `lib/common.sh` variables — tput-based, auto-disabled when piped (`lib/common.sh:2-13`). No raw ANSI escapes anywhere.
|
||||
- Meaning never carried by color alone: `[+]` / `[!]` / `ERROR:` / `OK` / `FAIL` prefixes carry all semantics (`lib/common.sh:22-25,105-108`) → fully readable without color, and screen-reader/braille-friendly (spinners are decorative only; all state is plain text lines).
|
||||
- All prompts via `read -rp` (stderr) so stdout stays machine-parseable — the "ui_pick lesson": menus/tables on stderr, data on stdout (`bin/pos-media-mp4:82-84`, `lib/share-lib.sh:11-13`).
|
||||
- No cursor games, no `clear` (scrollback preserved), rules ≤ 80 cols matching `section()` width (`lib/common.sh:31`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Prompt/message copy proposals (exact strings)
|
||||
|
||||
**usage() block**
|
||||
|
||||
```text
|
||||
Usage: pos media ytsync [command] [args]
|
||||
|
||||
Incrementally download YouTube channels/playlists into ~/Videos.
|
||||
First run asks for a channel URL; repeat runs fetch only new videos.
|
||||
|
||||
Commands:
|
||||
add [url] Register a source and download it (asks for URL if omitted)
|
||||
sync [name] Incremental sync of tracked sources (all, if no name given)
|
||||
list Show tracked sources and their status
|
||||
remove <name> Stop tracking a source (keeps downloaded files)
|
||||
|
||||
Options:
|
||||
--dry-run Show what would be downloaded, fetch nothing
|
||||
-h, --help This help
|
||||
|
||||
Layout:
|
||||
Videos/<channel>/<playlist>/<NNN> - <title>.<ext> (playlist sources)
|
||||
Videos/<channel>/<title>.<ext> (channel/video sources)
|
||||
|
||||
Notes:
|
||||
Existing files are never overwritten; renamed/retitled videos keep their
|
||||
local filename. Unattended/scheduled use: 'pos media ytsync sync'.
|
||||
|
||||
Examples:
|
||||
pos media ytsync # interactive
|
||||
pos media ytsync add https://youtube.com/@SomeChannel
|
||||
pos media ytsync sync # cron/timer entry point
|
||||
pos media ytsync sync --dry-run # preview only
|
||||
pos media ytsync list
|
||||
```
|
||||
|
||||
**Prompts and confirmations**
|
||||
|
||||
| Context | Exact string |
|
||||
|---|---|
|
||||
| URL ask | `Channel or playlist URL: ` |
|
||||
| Re-prompt after invalid input | `Try again (2 of 3), or press Enter to cancel: ` |
|
||||
| Add confirm | `Start download? [Y/n]` |
|
||||
| Menu remove pick | `Remove which channel? [1-N], 0=cancel ` |
|
||||
| Remove confirm | `Stop tracking '<name>'? Files stay in ~/Videos. [y/N]: ` |
|
||||
|
||||
**Errors and warnings**
|
||||
|
||||
| Context | Exact string |
|
||||
|---|---|
|
||||
| Deps guard | `yt-dlp not found — install it with: sudo apt install yt-dlp` (matches `bin/pos-media-mp4:17`; see note on accuracy in §8) |
|
||||
| Bad URL shape | `[!] Not a YouTube URL — expected a channel (@handle, /c/, /user/), a playlist (?list=…), or a video link` |
|
||||
| Probe unreachable | `[!] could not reach YouTube — check your connection and try again` |
|
||||
| Not found/private | `ERROR: source not found or private: <url>` |
|
||||
| Requires sign-in | `[!] N videos require sign-in — skipped` |
|
||||
| Non-tty guard | `[!] ytSync needs a terminal for its prompt — use 'pos media ytsync sync' for unattended runs.` |
|
||||
| EOF cancel | `[+] Cancelled — nothing changed` |
|
||||
|
||||
**Progress / summary lines**
|
||||
|
||||
```text
|
||||
[+] Some Channel — 12 new of 812
|
||||
[1/12] Video title here
|
||||
OK [1/12] Video title here (4m12s)
|
||||
[!] unavailable: Some old video (deleted or private)
|
||||
OK Sync complete: 12 new, 800 already present, 1 failed → /home/u/Videos/Some Channel
|
||||
DRY RUN complete — 12 new would be fetched → ~/Videos/Some Channel
|
||||
```
|
||||
|
||||
**Notification messages**
|
||||
|
||||
```text
|
||||
📺 ytSync — 3 new videos: Linus Tech Tips
|
||||
• Video title one
|
||||
• Video title two
|
||||
• Video title three
|
||||
→ ~/Videos/Linus Tech Tips
|
||||
```
|
||||
```text
|
||||
⚠️ ytSync FAILED for Linus Tech Tips
|
||||
⚠️ ytSync — 2 videos failed: Some Channel (run 'pos media ytsync sync' to retry)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Affected areas (UX surface only)
|
||||
|
||||
- **`bin/pos-media-ytsync`** (new) — everything specified above: verbs, menu, prompts, copy, states, summaries.
|
||||
- **`bin/pos`** — only if A1 resolves to Option 2: append `media-ytsync` to `INTERACTIVE_CMDS` (`bin/pos:261`). Optional EXAMPLES line in `usage()`.
|
||||
- **Completions** — via `# POS_SUBCMDS:` / `# POS_FLAGS:` headers + `make gen` (no hand edits).
|
||||
- **Docs** — `DOC/POS.md` media row + detail block; `DOC/HOWTO.md` index row; section in `DOC/howto/media.md`; AGENT_TODO Done entry.
|
||||
- **No changes required** to `lib/common.sh` or `lib/notify.sh` — both consumed as-is.
|
||||
|
||||
## 5. Scope boundary
|
||||
|
||||
Out of scope (explicitly): writing/implementing any code; yt-dlp flag selection and state-file mechanics (Architect — A1–A5); cookies/members-only auth support (v1 reports them as skipped; parity with mp3/mp4's `--cookies` is a fast-follow candidate); whole-channel "every playlist" trees (v1 = channel→flat, playlist URL→one subdir); SponsorBlock/format selection beyond bestvideo+bestaudio merge parity; scheduling integration docs beyond pointing at `pos system schedule` with the `sync` verb.
|
||||
|
||||
## 6. Usability verification plan (for Tester)
|
||||
|
||||
Harness rules (per `DOC/DEV.md:191-212`): stub-PATH fake `yt-dlp` emitting canned metadata/progress; env-seam overrides for every written path (`YTSYNC_*` names = Architect's, A3); throwaway `/tmp/opencode/ytsync-*`; assert **side effects, not prompt strings**, when piped (`DOC/DEV.md:209`).
|
||||
|
||||
PTY-driven prompt tests — `printf 'answer\n' | script -qec "cmd" /dev/null` (`DOC/DEV.md:198`):
|
||||
|
||||
1. Fresh add happy path: feed URL + Enter → assert registry entry, dir created, archive lines appended, summary string exact-match, rc 0.
|
||||
2. EOF at URL prompt (empty PTY input) → guard-free clean exit 0, nothing registered.
|
||||
3. Invalid input ×3 → cancel message, rc 0, nothing registered.
|
||||
4. Decline add-confirm (`n`) → no dirs, no download, back-to-menu/exit 0.
|
||||
5. Menu loop: pick Sync-all, then 0 → two actions, one process, rc 0.
|
||||
6. Non-tty matrix: bare call with stdin closed/pipe under `timeout 10` → exactly the guard line on stderr, rc 0, **no hang**; same for EOF mid-menu.
|
||||
7. `sync` non-tty full run → completes with zero prompts, summary + history line present.
|
||||
8. Log hygiene: simulate dispatcher tee → assert no `\r` bytes in captured log (`grep -c $'\r'` = 0).
|
||||
9. Incremental idempotency: second `sync` → `0 new`, all "already present", mtimes unchanged.
|
||||
10. Collision fixture: pre-existing same-name file → warning counted, existing file byte-identical afterwards.
|
||||
11. Failure paths: fake yt-dlp failing per-video → run continues, count correct, rc 0; failing at channel level → rc 1 + `⚠️` notify.
|
||||
12. Notify capture: fake `pos-communication-telegram-sender` appending to `sends.log` (DEV.md:205 pattern) → digest sent only when new>0 or failed>0; silent noop verified.
|
||||
13. Dry-run: matches plan block above; zero writes anywhere (assert tree absent).
|
||||
14. Gates after implementation: `make gen && make check && make lint` (0 FAIL / 0 WARN).
|
||||
|
||||
## 7. Remaining uncertainty (assumptions for reconciliation)
|
||||
|
||||
| # | Assumption | Owner |
|
||||
|---|---|---|
|
||||
| A1 | Prompt transport `/dev/tty` (recommended, keeps logging) vs `INTERACTIVE_CMDS` registration (loses all logs for script) | Architect |
|
||||
| A2 | Metadata-probe mechanics (`--flat-playlist`-style single probe) and acceptable latency (~seconds; shown under a `spawn` label) | Architect |
|
||||
| A3 | State layout: registry file + per-channel archives; location **outside** `~/Videos`; env-seam names for tests | Architect |
|
||||
| A4 | yt-dlp flag ownership: `--download-archive`, `--windows-filenames`, `--trim-filenames`, merge format parity with mp4, retry flags | Architect |
|
||||
| A5 | Digest policy interplay with `pos system schedule` NOTIFY policies (avoid double-notifications when run *as* a scheduled job) | Architect |
|
||||
| A6 | Dep-guard copy says `sudo apt install yt-dlp`, but `preinstall.sh:64-67` installs yt-dlp from GitHub binaries — pre-existing drift shared with mp3/mp4 (`bin/pos-media-mp4:17`); consistency kept for now, Maintainer may want one accurate canonical string | Maintainer |
|
||||
|
||||
Open product questions: cookies/auth demand (fast-follow); whether `remove` should also offer deletion (v1: never deletes).
|
||||
|
||||
## 8. Recommended next agent
|
||||
|
||||
**Architect** — reconcile A1–A5 (state layout, prompt transport vs dispatcher logging, yt-dlp flag set), then hand to Builder with this spec as the behavioral contract; Reviewer may pre-check spec completeness against §6's test matrix.
|
||||
|
||||
*Changes made by Designer:* this report only (`reportAgents/2026-08-22-designer-ytsync.md`). No other files touched.
|
||||
@@ -1,87 +0,0 @@
|
||||
# Builder Report — share clients: mountpoint picker enhancements + unmount-by-pick
|
||||
|
||||
## TL;DR
|
||||
- Status: IN PROGRESS
|
||||
- Files to change: `bin/pos-share-nfs-client`, `bin/pos-share-smb-client`, `DOC/howto/share.md`
|
||||
- Features: (A) mountpoint picker gains same-as-server synthetic candidate + `n=new` create-dir flow; (B) interactive unmount becomes pick-from-active-mounts with y/N confirm, typed fallback when none.
|
||||
- Verification: bash -n ×2 · `make gen` idempotent · `make check` · `make lint` (0 FAIL, 0 WARN) · pty probes A1–A3, B1–B3 incl. zero-mutation proof.
|
||||
|
||||
## Step 1: Context read + design lock
|
||||
[DONE]
|
||||
- Read survey report (Pattern B contracts), `lib/share-lib.sh`, `lib/menu-lib.sh`, both clients, smb-server consumption (:378–408), common.sh helpers (`confirm` default-deny, `run` dry-run-aware non-exiting).
|
||||
- Constraint conflict resolved: `share_pick` treats `n` as a filter string and owns its read loop; lib is off-limits → each client gets a local `pick_mountpoint` port of `menu_pick` with exactly two deltas: hint line `[1-N], n=new, text=filter, 0=back` and an `n|N` case delegating to a new-dir flow (success → created dir echoed as chosen value; failure/decline/EOF → redraw picker). Numbered picks / filter / `0=back` rendering byte-identical.
|
||||
- Feature B interpretation: "interactive path" = menu item 4 (`menu_unmount`, reached via no-args+tty front door). Bare CLI verb dispatch (`unmount <dir>` required arg) untouched → scripted behavior byte-identical (B3).
|
||||
|
||||
## Step 2: nfs-client — Feature A (picker capabilities)
|
||||
[DONE]
|
||||
- Added `pick_mountpoint` (local menu_pick port; deltas: hint `[1-N], n=new, text=filter, 0=back`, `n|N` → new-dir flow) and `ask_new_mountpoint` (validate absolute + no trailing slash + system-path parity guard, `confirm` y/N naming full path, `run sudo mkdir -p`; any failure/decline/EOF = warn + rc 1 → picker redraws, tool never aborts).
|
||||
- `menu_ask_mountpoint` now takes optional `[server_path]`: strips annotations from candidates for the membership test (`grep -qxF` on bare paths), appends synthetic `<path> (as on server)` only when absent. Fallback to manual typed prompt preserved byte-identically (incl. cancel→manual-entry fall-through).
|
||||
- `menu_mount` passes `${what#*:}` (server-side export path) into the picker for both ephemeral and persist modes.
|
||||
- bash -n OK.
|
||||
|
||||
## Step 3: smb-client — Feature A (picker capabilities + server-path resolution)
|
||||
[DONE]
|
||||
- Same `pick_mountpoint` / `ask_new_mountpoint` / `menu_ask_mountpoint(server_path)` trio as nfs-client.
|
||||
- Added `smb_server_path <host> <share>`: resolves the underlying dir ONLY when the target host is this machine (localhost/loopbacks/hostname/`hostname -f`/`hostname -I`), via `testparm -s --parameter-name=path --section-name=<share> "$SMB_CONF"`; anything else → rc 1 → suggestion silently skipped (remote SMB paths are not remotely discoverable). Invocation verified empirically on this box (`print$` → `/var/lib/samba/printers`, unknown share rc 1). NOTE: bare-share-name positional is WRONG (testparm treats it as a config file); `--section-name` is required. New `SMB_CONF="${SMB_CONF:-/etc/samba/smb.conf}"` env seam matches smb-server's constant.
|
||||
- `menu_mount` resolves after share pick and passes the result into `menu_ask_mountpoint`.
|
||||
- bash -n OK.
|
||||
|
||||
## Step 4: nfs-client — Feature B (unmount by pick)
|
||||
[DONE]
|
||||
- `menu_unmount` rewritten: enumerates via `findmnt -rn -o TARGET,SOURCE -t nfs,nfs4` (same source as `cmd_list`'s view, reduced to TARGET|SOURCE rows via the house `$NF` awk split, cf. share-lib's candidates parser).
|
||||
- Zero mounts → `log "No active NFS mounts"` + existing manual typed prompt fallback. One+ → `share_pick` over `<mountpoint> ← <source>` entries, then default-deny `confirm "Unmount <mp> (from <src>)?" n`; decline/EOF → `[+] Cancelled` + rc 1, zero umount issued.
|
||||
- Deliberate correction inside the rewritten enumeration: old code ran `findmnt -r -n … | tail -n +2`, but `-n` already suppresses the header — so `tail` dropped the FIRST active mount from the picker. Verified empirically (`findmnt -rn -o TARGET -t tmpfs` prints data rows only). Keeping the tail would contradict "enumerate using the same source as list".
|
||||
- Scripted path untouched: dispatch still requires an explicit `<local-dir>` arg for the `unmount` verb; `cmd_unmount` unchanged.
|
||||
- bash -n OK.
|
||||
|
||||
## Step 5: smb-client — Feature B (unmount by pick)
|
||||
[DONE]
|
||||
- Mirror of Step 4 against the cifs source: `findmnt -rn -o TARGET,SOURCE -t cifs`, items `<mountpoint> ← <source>`, confirm-gated, `[+] Cancelled` on decline, typed fallback when none ("No active SMB mounts").
|
||||
- Replaces the old `${row##* }` last-field parse of `findmnt -rnf -t cifs -o SOURCE,TARGET`.
|
||||
- bash -n OK ×2.
|
||||
|
||||
## Step 6: DOC/howto/share.md hints
|
||||
[DONE]
|
||||
- NFS + SMB "Interactive menu" paragraphs now mention: `n=new` create-dir (y/N confirmed, failure returns to picker), the "(as on server)" pick (SMB: only when this machine is the server), and unmount-by-pick `<mountpoint> ← <source>` with confirmation + typed fallback. Pre-existing tolerance sentence preserved.
|
||||
|
||||
## Step 7: Gates
|
||||
[DONE]
|
||||
- `bash -n` ×2 → OK.
|
||||
- `make gen` twice → byte-identical outputs (GEN-IDEMPOTENT); `DOC/AGENT_Context_Project.md` drift = exactly the two filetable line-count rows (343→467, 576→726), no header changes.
|
||||
- `make check` → `check-sync: OK`.
|
||||
- `make lint` → `0 FAIL, 0 WARN (convention lint)`.
|
||||
|
||||
## Step 8: Bug found by own probe — display lines swallowed under $( )
|
||||
[DONE]
|
||||
- First pty probe round exposed that `ask_new_mountpoint`'s warn/log lines were invisible: it runs inside `made="$(ask_new_mountpoint)"`, so stdout-captured substitution ate them. Fixed by sending all display lines to stderr (`>&2`), matching menu-lib's "display → stderr, result → stdout" contract. Gates re-run green after the fix.
|
||||
|
||||
## Step 9: pty probes (throwaway env /tmp/opencode/share-probe, cleaned)
|
||||
[DONE]
|
||||
- Harness: PATH-shimmed `findmnt`/`showmount`/`sudo`/`mount.nfs`/`mount.cifs`/`smbclient`; fixture-driven; `sudo` shim logs every call and EXECUTES ONLY `mkdir` targets inside the probe root — zero mutation guaranteed outside it; real pty via `script(1)`; SMB port-probe satisfied via the tool's `SMB_PORT=18445` env seam against a throwaway listener.
|
||||
- **A1/A3** — nfs synthetic appears when absent (`-- 1 of 8 match 'as on' --` → picked → `Mounted 100.100.100.1:/exports/media at /exports/media`); absent when already listed (7 items, no suggestion); smb resolution via testparm works end-to-end (`/tmp/…/mp/serveronly (as on server)` suggested for share `datashare`) and is silently skipped when the underlying dir is already a candidate (share `testshare`). Filter still works incl. matching the synthetic item.
|
||||
- **A2** — `n`+path+`y` → dir really created inside probe root + `Created mount point …` + mount completes; decline (Enter=N) → no dir, picker redraws, `0=back` → manual typed prompt preserved; relative path → warned, back at picker; mkdir failure in read-only prefix → `Could not create …` warning + stays in picker (then recovers by creating a good dir); EOF at path prompt → straight back to picker. Picker hint renders exactly `Mountpoint [1-N], n=new, text=filter, 0=back`. All flows exit rc 0.
|
||||
- **B1** — nfs + smb: 2 active mounts rendered as `<mountpoint> ← <source>`; confirm names both; decline → `[+] Cancelled` with **zero umount calls logged** (zero-mutation proof from shim log); happy path issues exactly one umount.
|
||||
- **B2** — empty list → `[+] No active NFS mounts` / `[+] No active SMB mounts` + existing typed prompt fallback; typed target handled idempotently ("not mounted as NFS — nothing to do").
|
||||
- **B3** — scripted `unmount <dir>` HEAD vs worktree: stdout+stderr byte-identical and identical rc 0 for both tools.
|
||||
- Excerpt (B1a, decline path):
|
||||
```
|
||||
-- 2 available --
|
||||
1) /mnt/nfs/media ← 100.100.100.1:/exports/media
|
||||
2) /mnt/nfs/data ← 100.100.100.1:/exports/data
|
||||
Unmount which NFS mount? [1-2], text=filter, 0=back 1
|
||||
Unmount /mnt/nfs/media (from 100.100.100.1:/exports/media)? [y/N]:
|
||||
[+] Cancelled
|
||||
```
|
||||
- Harness lessons (not product bugs): HEAD copies must sit beside a `lib/` mirror for their dirname-based sourcing; literal `"[+] "` assertions need ANSI stripping; smb `probe_server` hard-exits (no try-anyway) so the port seam was required.
|
||||
|
||||
## Per-tool diff summary
|
||||
- `bin/pos-share-nfs-client` (+~120/−~18): `pick_mountpoint`, `ask_new_mountpoint`, `menu_ask_mountpoint[server_path]` w/ `(as on server)` synthesis, `menu_mount` passes `${what#*:}`, `menu_unmount` rewritten (list-view enumeration, `<mp> ← <src>` picks, confirm-gate, typed fallback).
|
||||
- `bin/pos-share-smb-client` (+~170/−~18): same trio + `smb_server_path` (testparm via new `SMB_CONF` env seam, self-host-only) + `menu_mount` wiring + mirrored `menu_unmount`.
|
||||
- `DOC/howto/share.md`: NFS + SMB interactive-menu paragraphs updated (n=new, as-on-server, unmount-by-pick).
|
||||
- `DOC/AGENT_Context_Project.md`: GEN line-count rows only (343→467, 576→726).
|
||||
|
||||
## Scope compliance
|
||||
- Only the four allowed files touched (2 tools + howto + GEN regen). lib/, servers, usb-server, dispatch tables, INTERACTIVE_CMDS untouched. No commits made.
|
||||
|
||||
REPORT_PATH: ./reportAgents/2026-08-23-builder-share-clients-picker-unmount.md
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
# Builder Report — T1: Extract category-neutral menu primitives into `lib/menu-lib.sh`
|
||||
|
||||
## TL;DR
|
||||
- Status: **IMPLEMENTED** — all gates green, behavior byte-stable.
|
||||
- Files changed: `lib/menu-lib.sh` (new, 169 ln) · `lib/share-lib.sh` (436→318) · `install.sh` (+1 entry in explicit lib list) · `DOC/AGENT_Context_Project.md`, `DOC/DEV.md`, `DOC/SCRIPTS.md` (hand-maintained lib rows/sections).
|
||||
- Verification: `bash -n` ✓ · `make gen` idempotent/no new drift ✓ · `make check` OK ✓ · `make lint` 0 FAIL 0 WARN ✓ · pty probes before/after byte-identical on all 5 cases ✓ · shim delegation asserted ✓.
|
||||
- install.sh verdict: **needed the addition** — Phase-2 copies libs from an EXPLICIT list (install.sh:143), not a glob; `menu-lib.sh` appended (pre-authorized by brief).
|
||||
- Zero edits to the five `bin/pos-share-*` tools and DOC/POS.md; no commits.
|
||||
|
||||
## Step 0: Scope confirmation + plan [DONE]
|
||||
- Extraction set (per explorer survey :73–80): `share_menu_guard`→`menu_guard`, `share_menu_run`→`menu_run`, `share_pick`→`menu_pick`, `share_ask_value`→`menu_ask_value`. Everything else in share-lib stays (domain).
|
||||
- Verified from source before touching anything:
|
||||
- The four functions use common.sh helpers only via `${CYAN}`/`${RESET}` (share_menu_run :56–58) — no log/warn/err/run/confirm calls. So menu-lib sources nothing; it gets guarded color fallbacks instead (repo idiom: common.sh:16–18 "standalone-sourced files keep an identical guarded copy").
|
||||
- Internal cross-call: none between the four except `share_menu_run` → `share_menu_guard` (:49) — rename-consistent inside menu-lib.
|
||||
- One domain helper calls into the extraction set: `share_smb_shares` uses `share_ask_value` (:300) — keeps working through the shim.
|
||||
- lib→lib sourcing precedent: scheduler-lib.sh:47–50 (4-way BASH_SOURCE/$0 fallback chain) — replicated for share-lib → menu-lib.
|
||||
- install.sh Phase-2 copies libs from an EXPLICIT list (install.sh:143) → `menu-lib.sh` must be added there (brief pre-authorizes this).
|
||||
- Constraints honored: no edits to `bin/pos-share-*`; no commits; no behavior change (rc semantics preserved by 1:1 delegating shims).
|
||||
|
||||
## Step 1: BEFORE byte-baseline probes [DONE]
|
||||
- Harness `/tmp/opencode/t1-probe/t1-probe.sh` (sources repo `lib/common.sh` + `lib/share-lib.sh`; cases: menu-run, menu-quit, guard-nontty, pick-filter, ask-default), pty runs via `script -qec`, logs in `/tmp/opencode/t1-probe/before/`.
|
||||
- Results (all as-contracted):
|
||||
- `menu-run` ("2"): boxed menu → stderr, `STDOUT_INDEX=2 RC=0`
|
||||
- `menu-quit` ("q"): render then `STDOUT=[] RC=1`
|
||||
- `guard-nontty` (`< /dev/null`, no pty): `[!] Interactive menu needs a terminal — use a subcommand instead (see --help).` + `GUARD_RC=1`
|
||||
- `pick-filter` ("al","1"): `-- 1 of 3 match 'al' --` → `PICK_INDEX=1 RC=0` (index into FULL list)
|
||||
- `ask-default` ("" w/ default): `VALUE=[fallback] RC=0`
|
||||
- These files are the byte-comparison baseline for Step 7 (before/after spot-check).
|
||||
|
||||
## Step 2: New lib/menu-lib.sh [DONE]
|
||||
- Created `lib/menu-lib.sh` (~170 lines): header contracts + guarded color fallbacks (`CYAN="${CYAN:-}"; RESET="${RESET:-}"` — standalone-safe, sourced common.sh wins) + `menu_guard`/`menu_run`/`menu_pick`/`menu_ask_value`.
|
||||
- Sources nothing (the four functions never used log/warn/err/run/confirm — only `${CYAN}`/`${RESET}`).
|
||||
- Byte-stability proof: extracted each function body from old/new and diffed after reverse-renaming — 3 of 4 IDENTICAL; `menu_run` differs ONLY in the internal cross-call (`share_menu_guard` → `menu_guard`), which is the mandated rename consistency.
|
||||
|
||||
## Step 3: lib/share-lib.sh thin layer + shims [DONE]
|
||||
- Removed the four function bodies; added the 4-way sourcing fallback chain (scheduler-lib.sh:47–50 precedent) + four 1:1 delegating shims (`lib/share-lib.sh:37–45`).
|
||||
- Header updated: purpose line + function index now mark the four as shims → menu-lib. Domain section byte-untouched (old :165–436 == new :47–318, 272 lines).
|
||||
- share-lib.sh: 436 → 318 lines. menu-lib.sh: 169 lines.
|
||||
- `bash -n` green on both libs.
|
||||
|
||||
## Step 4: install.sh lib list [DONE]
|
||||
- **Verdict: install.sh NEEDED the new lib added** — Phase 2 copies libs from an EXPLICIT list (`install.sh:143`), not a glob. `menu-lib.sh` appended after `share-lib.sh`; `bash -n install.sh` green.
|
||||
|
||||
## Step 5: Hand-maintained doc rows [DONE]
|
||||
- `DOC/AGENT_Context_Project.md`: Installation-Flow Phase-2 prose (+`menu-lib`) · §13 Key File table: share-lib row updated (436→318 + description now "domain layer + compat shims") and new menu-lib row (169 lines) added after it.
|
||||
- `DOC/DEV.md:34`: lib listing gains `menu-lib.sh` (category-neutral menu primitives); share-lib description corrected to domain probes/listings + compat shims.
|
||||
- `DOC/SCRIPTS.md`: Phase-2 lib list (+`menu-lib.sh`) · share-lib section rewritten to domain role + shim note · new short `## lib/menu-lib.sh` section (mirrors the per-lib section convention).
|
||||
- Finding vs brief assumption: the generated filetable contains NO lib rows at all (`gen-docs.sh` emits only pos-tool-derived blocks), so nothing lib-related appears via `make gen`; every spot above is hand-maintained.
|
||||
|
||||
## Step 6: Gates (bash -n, make gen/check/lint) [DONE]
|
||||
- `bash -n lib/share-lib.sh lib/menu-lib.sh install.sh` — all green.
|
||||
- `make gen` — write OK; re-ran and byte-compared the resulting `git diff` of both gen-target files: **idempotent, zero new drift** (expected — no `pos-*` file changed; the visible diff vs HEAD is the other track's pre-existing WIP staleness).
|
||||
- `make check` — **OK**.
|
||||
- `make lint` — **0 FAIL, 0 WARN**.
|
||||
|
||||
## Step 7: AFTER probes + before/after diff + shim assertions [DONE]
|
||||
- Re-ran the identical harness post-change; **all five logs BYTE-IDENTICAL to before/** (`cmp` clean): menu-run (render + `STDOUT_INDEX=2 RC=0`), menu-quit (`q` → `STDOUT=[] RC=1`), guard-nontty (warning + rc 1), pick-filter (`-- 1 of 3 match 'al' --` → index into full list), ask-default (`VALUE=[fallback] RC=0`).
|
||||
- Brief's named checks: (a) `share_menu_run` renders + returns chosen index ✓, `q` exits rc 1 ✓; (b) `menu_guard` (new name) under non-tty prints `[!] Interactive menu needs a terminal — use a subcommand instead (see --help).` + rc 1 ✓; (c) `share_pick` filter→select ✓.
|
||||
- Shim delegation assertion — `declare -f` output:
|
||||
```
|
||||
share_menu_run () { menu_run "$@"; } # likewise share_menu_guard → menu_guard,
|
||||
share_ask_value () { menu_ask_value "$@"; } # share_pick → menu_pick
|
||||
```
|
||||
- New names resolvable after sourcing only `share-lib.sh`: `declare -F` finds all four `menu_*` ✓. Before/after spot-check function: `share_menu_run` (byte-identical log = behavior identical pre/post).
|
||||
- Temps cleaned (`/tmp/opencode/t1-probe/` removed).
|
||||
|
||||
## Final diff summary (this task only)
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `lib/menu-lib.sh` | NEW · 169 lines (header contracts, guarded CYAN/RESET fallbacks, `menu_guard`/`menu_run`/`menu_pick`/`menu_ask_value`) |
|
||||
| `lib/share-lib.sh` | 436 → 318 lines (−118): four bodies+their comment blocks out, header purpose/index updated, sourcing chain + 4 shims in (~13 new lines); domain section byte-untouched |
|
||||
| `install.sh` | 1 line modified: Phase-2 explicit lib list += `menu-lib.sh` (:143) |
|
||||
| `DOC/AGENT_Context_Project.md` | Phase-2 prose list += `menu-lib`; §13 table: share-lib row updated (436→318 + description), menu-lib row added (+1 line net) |
|
||||
| `DOC/DEV.md` | lib listing row rewritten (+menu-lib.sh, share-lib described as domain+shims) |
|
||||
| `DOC/SCRIPTS.md` | Phase-2 lib list += menu-lib.sh; share-lib section rewritten to domain role; new `## lib/menu-lib.sh` section (~+7 lines net) |
|
||||
| `reportAgents/2026-08-23-builder-t1-menu-lib-extraction.md` | this report |
|
||||
|
||||
Note on `git diff` noise: HEAD is stale against the tree for several files (other track's uncommitted WIP — five `bin/pos-share-*`, docs, install.sh, untracked `lib/share-lib.sh`). The table above is MY complete change set; the five share tools were never opened for editing (zero edits).
|
||||
|
||||
## Scope compliance
|
||||
- In-scope changes confirmed: exactly the brief's allowed set (new lib, share-lib, doc rows, install.sh explicit-list addition).
|
||||
- Out-of-scope changes: none. No commits made. `bin/pos-share-*`, DOC/POS.md, preinstall.sh untouched.
|
||||
|
||||
## Remaining risks / notes for next agent
|
||||
- `INTERACTIVE_CMDS`, dispatcher, completions: correctly unaffected (no tool surface changed).
|
||||
- The six future P1/P2 consumers (designer Step 4) can now source `lib/menu-lib.sh` directly; they must add themselves to install.sh's lib chain via their own consumer sourcing (menu-lib is already installed).
|
||||
- usb-server E-004 legacy prompts remain owned by the other track (untouched, per constraint).
|
||||
|
||||
REPORT_PATH: ./reportAgents/2026-08-23-builder-t1-menu-lib-extraction.md
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user