35 KiB
Architect Report — Self-Describing Command Registry for POS
Date: 2026-08-26 Status: DECISION_READY
TL;DR
- Decision: Add a thin
lib/registry.shlibrary 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+grepheader 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 helpandpos menuare 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
# 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 (whatcommand -vchecks, 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):
# POS: <category> <command> — <description>
# POS_FLAGS: --flag1 --flag2
# POS_SUBCMDS: sub1 sub2 sub3
# POS_CONFIG: <scope> | <env-file> | <KEY>=<flags>:<desc> | ...
New headers (optional):
# 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)
#!/usr/bin/env bash
set -euo pipefail
# POS: ssh load-keys — Load all SSH keys into the agent
# ... rest of script
Example: Rich Tool
#!/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 toolbin/pos-tree(lines 47–68): reads# POS:and# POS_SUBCMDS:per toolscripts/gen-docs.sh(lines 30–46): reads# POS:,# POS_FLAGS:,# POS_SUBCMDS:,# POS_CONFIG:per toollib/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.
# 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→ keynetwork-download, categorynetworkbin/pos-config→ keyconfig, category""(category-less)bin/pos-ai-alias→ keyai-alias, categoryai
API
# ── 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
# 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:
- Headers are already the source of truth — no sync risk.
- The scan takes ~5ms for 40 tools — no performance concern.
- Works from
/usr/local/bin/(all libs live there after install). - Matches the project's "no unnecessary framework" philosophy.
- Follows the pattern established by
lib/config-ui.sh.
Source Chain
# 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-treeneeds tree-building from tool databin/pos_pos_category_help()needs category → tool listingscripts/gen-docs.shneeds all metadata for code generationlib/config-ui.shneeds 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:
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:
# 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
depsandexamplescolumns togen_tree(),gen_dispatch(),gen_filetable(). - These become visible in
DOC/AGENT_Context_Project.mdonce 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:
_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:
- config-ui.sh already works correctly and is well-tested.
- Its parsing is specialized (multi-line key fields,
*pluginsexpansion,*providersexpansion). - Integrating it with the registry would require the registry to handle all config-ui's edge cases, bloating the library.
- 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):
- Create
lib/registry.shwith full API. - Update
scripts/gen-docs.shto parse new headers (degrades gracefully when absent). - Migrate
bin/pos-treeto use registry (proves runtime value). - Update
templates/pos-tool.shto document new headers. - Update
DOC/DEV.mdandDOC/AGENT_Context_Project.mdwith new header format.
Phase 2 (future, out of scope):
- Migrate
bin/pos_pos_category_help()to use registry. - Enhance
pos helpto show registry metadata (deps, examples). - Add
# POS_DEPS:and# POS_EXAMPLES:to tools incrementally (start with 3–5 representative tools per category).
Phase 3 (future, out of scope):
pos menu— interactive menu from registry (new tool).pos dashboard— status dashboard from registry (new tool).
Incremental Adoption for Tool Authors
- Add
# POS_DEPS: docker jqto your tool's header block. Done — deps appear in registry. - Add
# POS_EXAMPLES: pos <tool> <args> | Descriptionlines. Done — examples appear in registry. - No other changes required. The tool keeps working exactly as before.
- When you run
make gen, the new metadata appears in generated docs.
Tool Template Update
templates/pos-tool.sh gets new header documentation:
# ────────────────────────────────────────────────────────────────
# 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
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
- Do NOT add
# POS_DEPS:or# POS_EXAMPLES:to any existing tool in this PR (that's incremental migration, separate commits). - Do NOT change the
# POS:header format or the em-dash convention. - Do NOT change
bin/posdispatch logic orINTERACTIVE_CMDS. - Do NOT add any new files beyond
lib/registry.sh. - Do NOT modify the completion script (
completions/pos.bash). - Do NOT refactor
lib/config-ui.shto use the registry. - Do NOT add a shebang to
lib/registry.sh(it's a library, not executable). - 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-codebefore committing. Zero diff = safe. - The tools array format change (adding
$deps|$examplesfields) 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_scancould read each file once and parse all headers in a singleawkpass. 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.shdoes NOT definerun,spawn,confirm, or any other common.sh functions.registry.shdoes NOT callerrduring normal operation — only ifreg_scanis 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-treebefore 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_scanwrites 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_scanuses 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 -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
make gen && git diff --exit-code # Zero diff (no tools have new headers yet)
Gate 3: Self-Consistency
make check # Must pass (syntax + exec bits + doc/code sync + dispatch smoke)
Gate 4: Convention Lint
make lint # Must pass (0 FAIL, 0 WARN)
Gate 5: Functional
# 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
# 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
-
Create
lib/registry.sh(~180 lines). Start from the API spec in Decision 2. Uselib/config-ui.shas a structural reference for the guarded fallbacks and source pattern. -
Update
scripts/gen-docs.sh. In the tools collection loop (lines 30–46):- Add
depsandexamplesfields to thetoolsarray format:"$cat|$sub|$desc|$flags|$subcmds|$deps|$examples" - Parse new headers with the same
sedpattern as existing ones. - In
gen_tree(),gen_dispatch(),gen_filetable(): add deps/examples columns ONLY when non-empty. Empty fields = identical output to current.
- Add
-
Migrate
bin/pos-tree. Replace lines 47–68 (the file-scanning loop) with registry calls. Keepadd(),render(), and the rest unchanged. Test that output is identical for existing tools. -
Update
templates/pos-tool.sh. Add# POS_DEPS:and# POS_EXAMPLES:to the header documentation block. Add them after the existing# POS_FLAGS:example. -
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).
- Document the new
-
Update
lib/registry.shline count inDOC/AGENT_Context_Project.mdfiletable (the hand-maintained rows above the GEN marker). -
Run gates:
make gen && make check && make lint. Verify 0 FAIL, 0 WARN.
Critical Constraints
lib/registry.shmust NOT have a shebang (library, not executable).lib/registry.shmust be added to thelib_nameslist ininstall.shPhase 2 (line 143).reg_scanmust setLC_ALL=Cfor deterministic sort.- gen-docs.sh changes must produce zero diff when no tools have new headers.
pos-treeoutput must be byte-identical before/after migration (for existing headers).
Open Questions (for Orchestrator)
-
Should
pos <category> --help(inbin/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. -
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. -
Should
reg_scansupport a--cachedmode? Recommendation: no, not yet — premature optimization for 40 tools.
Report written by Architect agent. Next recommended agent: Builder (to implement the approved scope).