Files
Linux_post_install/AgentsReport/architect/2026-08-26_registry-architecture.md
T
Your Name 06a05f0567
gates / consistency-and-conventions (push) Successful in 1m24s
ai continue ...
2026-09-04 11:09:21 -04:00

809 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 16) 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 68123): reads `# POS:` and `# POS_SUBCMDS:` per tool
- `bin/pos-tree` (lines 4768): reads `# POS:` and `# POS_SUBCMDS:` per tool
- `scripts/gen-docs.sh` (lines 3046): reads `# POS:`, `# POS_FLAGS:`, `# POS_SUBCMDS:`, `# POS_CONFIG:` per tool
- `lib/config-ui.sh` (lines 5057): 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 4768): 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 4768) with registry calls. The tree structure is already correct from filenames + `POS_SUBCMDS`.
#### 3.2: `scripts/gen-docs.sh` (Priority: HIGH)
**Current state** (lines 3046): 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 68123): 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 50203): 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 35 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 46 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 4768) 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 3046):
- 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 4768 (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 13 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).*