feat(cli): round-tables subcommand group with watch daemon#21
Conversation
Adds `admp round-tables` command group with 6 subcommands:
create POST /api/round-tables — create session with topic, goal, participants
list GET /api/round-tables — list mine, filterable by status
get GET /api/round-tables/:id — full session + thread dump
speak POST /api/round-tables/:id/speak — contribute a message
resolve POST /api/round-tables/:id/resolve — close with outcome (facilitator)
watch Daemon loop — polls GET /:id every N ms, emits new thread entries
as they arrive, fires --on-speak <cmd> hook (entry JSON piped to stdin),
auto-exits on resolved (0) / expired (1), handles SIGTERM/SIGINT cleanly
watch design mirrors the Teleportation daemon:
- setInterval-style sleep loop (not setInterval, to avoid drift on slow polls)
- Cursor = lastLength (thread array index, append-only, no server cursor needed)
- Transient errors: warn + continue (network blip should not kill a long-running watch)
- --on-speak: spawn /bin/sh -c <cmd> per entry, fire-and-forget, non-blocking
- --json mode: all events as { event, ... } lines to stdout, warnings to stderr
- Initial fetch before loop sets cursor so history is not replayed on first poll
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code ReviewOverall this is a clean, well-structured addition. The daemon design is thoughtful (signal handling, cursor-based replay avoidance, transient-error resilience) and the help text / examples are genuinely useful. A few issues worth fixing before merge: Bug:
|
| Severity | Issue |
|---|---|
| Bug | entries_seen off-by-initial-thread-length |
| Convention | validateRtId should live in validate.ts |
| Required (CLAUDE.md) | No automated tests |
| Low | child.stdin null-safety |
| Low | No consecutive-error limit in poll loop |
| Nit | Hook concurrency / --no-exit-on-close docs |
Happy to help with any of these if useful.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e409465a19
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
cli/src/commands/round-tables.ts
Outdated
| child.stdin.write(JSON.stringify(entry) + '\n'); | ||
| child.stdin.end(); |
There was a problem hiding this comment.
Handle hook stdin EPIPE to avoid crashing watch loop
Writing each entry to child.stdin without handling stream errors can terminate the entire watch process when the hook exits before reading stdin (for example --on-speak 'true' or other short-lived commands). In that case Node emits EPIPE on the stdin socket, and because no error listener is attached to child.stdin, the process crashes instead of continuing to poll.
Useful? React with 👍 / 👎.
Greptile SummaryAdds Key additions:
Integration notes:
Confidence Score: 4/5
|
| Filename | Overview |
|---|---|
| cli/src/index.ts | Adds import and registration for round-tables command module, following existing pattern |
| cli/src/commands/round-tables.ts | Implements complete CRUD operations plus watch daemon for Round Tables with proper validation, error handling, and signal management |
Sequence Diagram
sequenceDiagram
participant User
participant CLI as admp watch
participant API as /api/round-tables/:id
participant Hook as --on-speak command
User->>CLI: admp round-tables watch rt_123 --on-speak 'handler'
CLI->>API: GET initial state
API-->>CLI: RoundTable + thread
Note over CLI: Initialize cursor = thread.length<br/>Skip existing entries
CLI->>User: Print watch banner
loop Poll every intervalMs
CLI->>CLI: sleep(intervalMs)
CLI->>API: GET current state
API-->>CLI: RoundTable + thread
alt New entries detected
Note over CLI: newEntries = thread.slice(lastLength)
loop For each new entry
CLI->>User: printEntry(entry)
CLI->>Hook: spawn /bin/sh -c handler<br/>pipe entry JSON to stdin
Hook-->>CLI: (fire-and-forget)
end
Note over CLI: lastLength = thread.length
end
alt status !== 'open' && exitOnClose
CLI->>User: printClosure(status, outcome)
CLI->>CLI: exit(status === 'resolved' ? 0 : 1)
end
end
User->>CLI: Ctrl+C (SIGINT)
CLI->>User: Print summary (entries_seen)
CLI->>CLI: exit(0)
Last reviewed commit: e409465
…header Three bugs found and fixed during adversarial review + local smoke testing: 1. src/routes/round-tables.js — Round Table routes used authenticateAgent which requires an :agentId URL param or X-Agent-ID header. Since /api/round-tables paths have no :agentId segment, all requests returned AGENT_ID_REQUIRED. Fixed by switching to authenticateHttpSignature, which extracts identity from the Signature headers keyId — the correct middleware for agent-auth routes without an agent ID in the URL. 2. cli/src/client.ts — url.hostname omits the port on non-standard URLs (e.g. localhost:8099 becomes localhost). The server reads the actual Host header (including port) when verifying Ed25519 signatures, causing a mismatch and SIGNATURE_INVALID for any non-standard-port deployment. Fixed: url.host includes port for non-standard ports, omits it for 80/443 (matching HTTP spec). 3. cli/src/commands/round-tables.ts — watch poll loop caught all errors and retried, including fatal 4xx responses (403 removed from session, 404 session deleted). The daemon would spin indefinitely printing warnings. Fixed: AdmpError with status < 500 is treated as fatal, log once and exit(1). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code ReviewOverall this is a well-structured addition. The daemon design is thoughtful, the input validation is thorough, and the JSON mode support is consistent throughout. A few issues to address before merging. Bug: child.stdin null dereference riskIn fireHook (cli/src/commands/round-tables.ts), child.stdin is typed as Writable | null. While stdio: pipe guarantees it is non-null at runtime, the compiler may flag it and an explicit guard is good practice. Add a check before writing to child.stdin. Bug: Entries seen reports total thread length, not entries observedIn the watch signal handler, lastLength is initialised to rt.thread.length (the existing entries explicitly skipped at startup). The summary therefore overstates entries seen. For example if there were 5 entries before watching and 3 new ones arrived, it prints 8 instead of 3. Fix: track an initialLength at startup and subtract it in the summary: Security note: --on-speak shell hookfireHook passes the operator-supplied command string directly to /bin/sh -c. This is intentional, and the entry data is safely piped to stdin rather than interpolated into the command string, so there is no server-data injection vector. Worth a brief note in docs/AGENT-GUIDE.md that --on-speak executes arbitrary shell commands. Auth middleware change is correct and backward-compatibleSwitching from authenticateAgent to authenticateHttpSignature in src/routes/round-tables.js is the right call. authenticateHttpSignature verifies Ed25519 signatures when the Signature header is present and falls back to the legacy authenticateAgent path when absent, so no existing callers are broken. client.ts host header fix is correcturl.hostname strips the port; url.host preserves it for non-standard ports. Required for HTTP Signature verification where the signed Host value must match exactly (RFC 7230 section 5.4). Good catch. No backoff on transient poll errorsOn a sustained outage the daemon hammers the server at the full --interval rate. A simple capped exponential backoff (e.g. double sleep up to ~60 s, reset on success) would be kinder to a degraded server. Not a blocker, but worth a follow-up issue. Thread cap not surfaced in watch modeThe service caps threads at 200 entries. Once that cap is reached, the watch loop keeps polling but will never see new entries (and will not auto-exit unless the session closes). Adding a warning when current.thread.length equals 200 would help users understand why activity appears to have stopped. Test coverageServer-side API coverage in server.test.js looks solid (13+ cases covering the full lifecycle, auth, participant caps, and edge cases). There are no CLI-level tests for the new subcommands. The watch daemon signal-handling and hook-firing logic would benefit most from testing. This matches existing patterns (no other commands have unit tests), so not a merge blocker, but worth tracking. Minor nits
|
Fixes four issues raised in PR review: 1. entries_seen off-by-initial-thread-length (bug) lastLength was initialized to rt.thread.length (pre-existing entries explicitly skipped at startup). Signal handler now subtracts initialLength so entries_seen reflects only entries observed during this watch session. 2. child.stdin null-check + EPIPE guard (bug / P1) TypeScript types child.stdin as Writable|null. Added null guard before writing. Added error listener on child.stdin to swallow EPIPE when a short-lived hook exits before reading stdin, preventing an uncaught exception from crashing the watch loop. 3. Consecutive poll-error ceiling (important) Watch loop previously retried transient errors indefinitely. After 10 consecutive failures the daemon now exits with POLL_FAILED to give an actionable signal rather than silently stalling. 4. Thread-cap warning (important) Server caps threads at 200 entries. A warning is now emitted when thread.length reaches 200 so users understand why activity appears to have stopped. Also removed redundant running=false before process.exit() in the auto-exit path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code ReviewOverall this is a solid addition — the daemon loop design is thoughtful (cursor-based, error backoff, clean signal handling), and the Bug: ANSI codes ignore
|
|
All four issues from the review have been addressed in commit fd355ed (pushed to main after merge): 1. NO_COLOR / raw ANSI escapes — Replaced all raw 2. Thread-cap warning repeating — Added 3. 4. Dead Build clean (33 modules), tests passing ✓ |
Summary
Adds
admp round-tablesto the CLI — a full CRUD subcommand group plus a daemon-style watch loop.watch daemon design
Modeled on the Teleportation daemon pattern:
Test plan
Generated with Claude Code