# AGENTS.md — Discourse CLI tooling This directory holds the `discourse-cli` Docker container — a thin Python CLI over the Discourse REST API. Every Crush session that works on or with this tool should read this file first: how to invoke it, what commands exist, what gotchas to avoid, and the patterns to follow for common forum operations. ## Connection & invocation Invoke the real container with `docker run`. Credentials come from the centralized credential store (see `tooling-cli/KNELCredsManager`): ```bash docker run --rm --env-file ~/.creds/discourse.env \ git.knownelement.com/reachableceo/discourse-cli:latest ``` There is intentionally **no `bin/` wrapper** — invoke the real container, as the project-wide rules require. For readability in an interactive session you may define a throwaway shell alias (do not commit one): ```bash alias discourse='docker run --rm --env-file ~/.creds/discourse.env \ git.knownelement.com/reachableceo/discourse-cli:latest' ``` The examples below write `discourse ` for brevity; expand it to the full `docker run` line (or set the alias) before running. | Item | Value | |------|-------| | Instance | `https://community.turnsys.com` | | API user | Charles N Wyble (id 2, username `reachableceo`, trust level 4) | | Admin? | **No** — trust 4 (Leader) but not staff/admin. Admin-only endpoints will 403. | | Credentials | `~/.creds/discourse.env` (`DISCOURSE_URL`, `DISCOURSE_API_KEY`, `DISCOURSE_API_USERNAME`) | | CLI image | `git.knownelement.com/reachableceo/discourse-cli:latest` | | Source | `src/discourse_cli.py` in this directory | To use a locally built image instead of the registry image, either set `DISCOURSE_CLI_IMAGE` or swap the tag for `kneldevstack-aimiddleware-discourse-cli:latest`. ## Quick start ```bash # Connection sanity check (run this first in any session): discourse whoami # List categories: discourse categories # Latest topics (or in a category): discourse ls discourse ls -c general # Look at a topic: discourse show 42 ``` ## Command reference | Command | Description | |---------|-------------| | `whoami` | Authenticated user + connection test. | | `categories` | List categories (id, slug, topic count, name). | | `cat-info ` | Show details of a single category. | | `topics` (`ls`) | List topics (latest, or filtered). Filters below. | | `show ` | Full topic detail incl. all posts (HTML stripped to text). | | `create` | Create a new topic (first post). | | `reply ` | Reply to a topic. | | `update ` | Edit an existing post's body. | | `delete ` | Delete a post. | | `search ` | Search the forum. | | `notifications` | List your notifications. | ### `topics` / `ls` filters - `-c, --category ` — only topics in a category - `-n, --new` — only new (to you) topics - `-u, --unread` — only unread topics - `-p, --page ` — page number (default 0) ### `create` options - `-t, --title` **required** - `-b, --body` **required** (raw markdown/text — this becomes the first post) - `-c, --category ` - `--tags` — comma-separated tag list ### `reply` options - `topic_id` (positional, **required**) — the topic to reply in - `-b, --body` **required** (raw markdown/text) - `-r, --reply-to ` — reply to a specific **post number** (not id) ### `update` / `delete` - Both operate on a **post id** (positional, required), not a topic id or post number. Find the post id via `show` or the raw API (see gotcha below). - `update -b, --body` **required** (new raw body). ### `search` - `` (positional, required) - `-p, --page ` — page number (default 1) ### `notifications` - `-l, --limit ` — max results (default 20) ## Key categories | ID | Slug | Name | Notes | |----|------|------|-------| | 4 | `general` | General | Open discussion | | 23 | `reachableceo` | ReachableCEO | Personal | | 6 | `chiefoperationsandfinanceofficer` | ChiefOperationsOfficer | COO seat — parent for VP subcategories | | 3 | `staff` | Staff | May be restricted | | 74 | `vp-techops` | VP TechOps | ChiefOperationsOfficer (6) — 11 wiki topics migrated from PFVCluster | | 75 | `vp-compliance` | VP Compliance | ChiefOperationsOfficer (6) — awaiting content | | 76 | `board` | Board | (top-level, future) | Run `discourse categories` for the full, current list. ### Key topics (VP TechOps — category 74) Eleven wiki topics migrated from PFVCluster (Discourse is source of truth): | Topic | Title | Pattern | |-------|-------|---------| | 296 | PFVCluster Project Overview | Pinned wiki | | 297 | Operations Status | Pinned wiki (updated in place) | | 298 | Infrastructure Audit Log | Wiki index + dated replies | | 299 | Network Topology | Wiki | | 300 | Storage Architecture | Wiki | | 301 | Data Center Infrastructure | Wiki | | 302 | Automation and Provisioning | Wiki | | 303 | Security Architecture and Hardening | Wiki | | 304 | Proxmox Fleet Reference | Wiki + replies | | 305 | Kubernetes Platform | Wiki + replies | | 306 | DNS and DHCP Services | Wiki + replies | ## Gotcha: post id vs post number Discourse distinguishes a **post number** (1, 2, 3... within a topic — the first post is always #1) from a **post id** (a globally unique integer). - `reply -r` takes a **post number**. - `update` / `delete` take a **post id**. `show` prints headers like `--- #3 [3] author ...` where the value in brackets is the post number (here they often coincide for simple topics, but they are **not** the same thing). To reliably get a post's **id**, either read the raw JSON via the escape hatch below, or note that `create`/`reply` print the id of the post they just made (`Posted reply # (id=)`). ## Gotcha: not an admin (user key) The default API user is trust level 4 (Leader) but **not** an admin/staff member. With the user-level key: - **Cannot** create categories, set wiki posts, configure site settings, or access `/admin/...` endpoints (all 403). - **Can** create topics, reply, edit own posts, search, list notifications. For admin operations (category creation, wiki flagging, docs plugin config), an **admin-scoped API key** is needed. Set it via `DISCOURSE_ADMIN_KEY` in the credential file and pass it explicitly in raw `requests` calls. Things that will fail or be restricted without admin: - Deleting other users' posts. - Creating topics in staff-only or restricted categories. - Moving/merging/recategorizing topics. - Setting the wiki flag on posts. If an operation 403s, that's expected — surface it to the user rather than retrying. ## Gotcha: bulk operations & raw API For anything beyond a single `create`/`reply`/`update`, or to read fields the CLI doesn't print (e.g. exact post ids, category permissions, user lists), run Python directly inside the container with `requests`. The CLI uses raw `requests` against the Discourse REST API (no heavyweight SDK). Pattern (mount a script and run it in the same image): ```bash cat > /tmp/script.py <<'PY' import os, requests URL = os.environ["DISCOURSE_URL"].rstrip("/") H = { "Api-Key": os.environ["DISCOURSE_API_KEY"], "Api-Username": os.environ["DISCOURSE_API_USERNAME"], "Accept": "application/json", } # Example: get post ids for topic 42 r = requests.get(f"{URL}/t/42.json", headers=H, timeout=30) r.raise_for_status() for p in r.json()["post_stream"]["posts"]: print(p["post_number"], "id=", p["id"], "by", p["username"]) PY docker run --rm --env-file ~/.creds/discourse.env \ --entrypoint python \ -v /tmp/script.py:/tmp/script.py \ git.knownelement.com/reachableceo/discourse-cli:latest \ /tmp/script.py ``` Useful raw endpoints: - `GET /t/.json` — full topic incl. `post_stream.posts[]` (each has `id`, `post_number`, `username`, `cooked`, `raw`). - `GET /categories.json` — all categories. - `GET /c/.json` — topics in a category. - `POST /posts.json` — create topic (`title`+`raw`+`category`) or reply (`topic_id`+`raw`). - `PUT /posts/.json` — edit (`{"post":{"raw":"..."}}`). - `DELETE /posts/.json` — delete. - `GET /search.json?q=` — search. - `GET /notifications.json` — your notifications. ## Patterns ### Pattern: review & respond to a topic 1. `discourse show ` — read the topic and all replies. 2. Identify the post you're responding to; note its **post number** (for `-r`) and the overall context. 3. Draft a reply. `create`/`reply` take **raw markdown** — links, lists, code fences all work. 4. `discourse reply -b "your markdown" [-r ]`. 5. Verify with `discourse show `. ### Pattern: before you act - Always `discourse show ` before replying/editing — confirm the current state so you don't duplicate or contradict prior posts. - **Never delete a post** unless the user explicitly asks. - Prefer replying over editing someone else's post (and editing others' posts will likely 403 anyway as a non-admin). - When unsure which category to post in, `discourse categories` and pick the closest match, or ask the user. ### Pattern: posting conventions - Bodies are **raw markdown** — Discourse renders them. Use fenced code blocks for commands/output, headings, tables, and bullet lists freely. - Keep titles concise and descriptive. - Use tags where the category supports them (`--tags a,b,c`). ### Pattern: wiki topics (living documents) Wiki topics are the core anti-sprawl primitive. A wiki topic's **first post is editable by anyone** with permission (not just the original author), and Discourse preserves the full edit history automatically. When to use a wiki topic: - **Living references** — inventories, host lists, network topology, storage maps. Updated in place as facts change. - **Collaborative documents** — policies, design docs, specs where multiple agents/humans contribute. First post = the document; replies = discussion. - **Status/index pages** — operations status, documentation indexes. When NOT to use a wiki topic: - **Discussion threads** — regular topics where each reply is a distinct voice. Wiki-editing the first post would destroy the conversation. - **Point-in-time reports** — these go as dated replies inside a wiki "log" topic (see audit pattern below). To mark a post as wiki via the API (requires admin/moderator): ```python requests.put(f"{URL}/posts/{post_id}.json", headers=H, json={"wiki": True}) ``` ### Pattern: post lifecycle (comment vs replace vs new) The golden rule: **never create a new topic for an update to existing knowledge.** One wiki topic per subsystem, updated in place. | Change type | Action | Why | |-------------|--------|-----| | Fact update (new VM, IP changed) | Edit the wiki post in place | Edit history preserves old state | | New snapshot (audit, drift report) | New reply in the existing log topic | One topic accumulates history | | Question/discussion about content | Reply to the relevant post | Threaded, doesn't mutate the doc | | Major restructure | Edit wiki + reply noting why | Edit log = what; reply = why | ### Pattern: audit/snapshot logs Point-in-time reports (audits, drift reports, status snapshots) do NOT each get their own topic. Instead, create ONE wiki "log" topic per domain and append each snapshot as a dated reply: ``` [Wiki post #1] Index table (date | scope | link | superseded-by) + latest snapshot summary [Reply #2] Audit 2026-07-29 — full content [Reply #3] Audit 2026-07-30 — full content (supersedes #2) [Reply #4] Audit 2026-08-05 — full content (current) ``` When a new snapshot arrives: add a reply with full content, then edit the wiki first post to point at the latest reply as "current." ### Pattern: tag taxonomy Tags are cross-cutting classifiers that prevent category multiplication: | Tag | Meaning | |-----|---------| | `reference` | Living reference doc (inventories, topology, host lists) | | `runbook` | Operational procedure (deploy, recover, configure) | | `architecture` | Design doc, system architecture, capacity model | | `policy` | Naming conventions, security policies, standards | | `decision` | Resolved decision (ADR, distro choice, analysis outcome) | | `audit` | Point-in-time snapshot | | `security` | Security/hardening topic | ### Pattern: category taxonomy Org-based hierarchy mirroring Known Element's VP seats: ``` ChiefOperationsOfficer (id 6) ├── vp-techops — infrastructure, network, compute, security ops └── vp-compliance — frameworks, evidence, audit response Board (future) ``` Security operations (SecOps) lives under vp-techops (security is operational). Compliance covers frameworks (CMMC/STIG), evidence, and audit response. ## Git workflow ### Atomic commits Each commit is **one logical change** — one feature, one fix, one doc update. If you're tempted to write "and also..." in a commit message, that's a sign to split it into two commits. Stage precisely (`git add `, not `git add -A`) so unrelated changes don't get bundled. ### Conventional commit messages Use the [Conventional Commits](https://www.conventionalcommits.org/) format: ``` (): ``` Types used in this repo: `feat`, `fix`, `docs`, `refactor`, `chore`, `style`. Rules: - Subject line **under 72 chars**, lowercase, imperative mood. - No period at end of subject. - Body wrapped at 72 chars, explains **why** the change exists. ### Commit cadence - **Commit early and often.** Don't accumulate a pile of unrelated changes. - Commit **without asking** — if you made a coherent change, commit it. - Every task (topic review, reply, script addition, doc update) ends with the relevant files committed. - Run `git status` before committing to stage only what belongs together.