This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,165 @@
|
||||
# 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).
|
||||
@@ -0,0 +1,206 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,203 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user