Skip to content

Latest commit

 

History

History
266 lines (222 loc) · 14.6 KB

AGENTS.md

File metadata and controls

266 lines (222 loc) · 14.6 KB

Agent Guide

This file orients coding agents (Claude Code and others) working in this repository. It is intentionally short — the real guidance lives elsewhere.

Start here

  1. Read CONTRIBUTING.md first. It covers the project structure, the build/test/lint commands, coding and testing conventions, and the commit/PR expectations every change must follow.

  2. Then read, only as the task needs them, the documents under docs/:

    • docs/technical-design.md — architecture, the package layout, the API-client/flavor abstraction, the config and error models, and the rendering pipeline. Read before changing core behavior.
    • docs/installation.md — install methods, shell completion, and the companion Skill. Read for distribution/UX changes.
    • docs/releasing.md — versioning, the changelog step, tagging, and the release/CI workflows. Read before cutting a release or touching .github/workflows/.

Pull in a document when the task touches its area; do not read all of docs/ up front.

Ground rules

  • Run make test and make e2e before claiming a change is complete.
  • Keep commits scoped to one logical change; follow the commit and PR conventions in CONTRIBUTING.md.
  • Never commit .env, credentials, tokens, or build artifacts.
  • Output rendering, argv correction, the update check, the credential store, credential resolution, the configuration engine, and the code behind pkg/errors and pkg/transport come from agent-cli-kit, shared by every CLI in the family. Fix bugs in those there, not here. internal/auth declares confluence-cli's auth schemes and recovery wording and maps config onto the shared resolver. internal/config registers confluence-cli's settings, file locations, command wording and flavor rules with the shared config engine (schema.go) and keeps the YAML shape of config.yaml, including the legacy flat layout. pkg/errors re-exports the shared CLIError model and exit-code map (0–11) and installs confluence-cli's hints (hints.go); pkg/transport re-exports the shared retrying HTTP client with confluence-cli's User-Agent.

Cloud / Data Center parity — touch both branches, always

This CLI targets two backends (Confluence Cloud and Data Center / Server). Writes go through the v1 REST API on both, but the flavors still diverge on endpoints, pagination, and field shapes — and the recurring bug class is a branch (often Cloud) honouring an input the other branch silently drops. A real example: Cloud user search ignored the page cursor and capped at the first page while the DC branch paged correctly.

When you touch any path that branches on c.flavor:

  1. Handle both branches or consciously diverge — every input the command accepts (filters, cursors, version, expand) must be consumed by both flavor branches, or the gap must be deliberate and documented. Compiling proves nothing; a branch that never reads a field is silently wrong.
  2. Verify pagination + optimistic-lock symmetry. If one branch returns a next cursor or sends a version, the other almost certainly must too.
  3. Add a test that exercises both flavors for the changed path so a one-sided change is caught.

Discoverability — no dead-end inputs

Every non-trivial identifier a command accepts as input must be discoverable through another command in this CLI. Examples of inputs covered by this rule: space keys, page IDs, comment IDs, attachment IDs, label names, version numbers, user identifiers (accountId on Cloud, username on Data Center), and file paths within an attachment.

The rule is symmetric: an input is only discoverable if (a) the CLI has a command that lists / searches values of that kind, and (b) that listing command itself is reachable without already knowing some other value the CLI can't tell you about. A comment list that demands a page ID is only useful if search (or another path-to-page command) also exists.

When you add a command or flag that takes a new kind of input:

  1. Walk every parameter the new surface accepts. For each, answer: "Where does the caller (especially an AI agent) get this value?"
  2. If the answer is another command in this CLI, you are done.
  3. If the answer is an existing identifier the user already had in hand (e.g. they pasted a Confluence page URL), that counts too — pkg/urlref already parses page / comment IDs out of URLs.
  4. If the answer is out-of-band (a web UI, another tool, the API directly), that is a gap. Add the missing discovery command in the same PR, or surface it as a follow-up issue and document the dead end in CHANGELOG.md under Known gaps.

The same rule applies to error messages. When a command rejects an invocation for missing a required identifier, the resulting CLIError's next_steps must include the discovery command (e.g. space list, search, comment list, attachment list). Errors that say "Pass --space <key>" without showing the user how to find a valid <key> are defects.

Recovery steps must name commands that exist in this CLI. The context listing is config get-contexts; UNKNOWN_CONTEXT names it through the wording registered in internal/config/schema.go. Resolve an advertised command against the command tree in a test.

The e2e harness should exercise this contract: when adding a "missing input" error path, also add a scripts/e2e.sh assertion that the error output contains the discovery command's name (use a stderr-capturing helper when needed, since structured errors are written to stderr).

Safety modes — --dry-run and read-only posture

Two orthogonal protections guard every operation that mutates remote state:

  1. --dry-run is a per-command flag on every mutating command. It must resolve the request via Client.DescribeWrite(ctx, op) and emit the resulting WriteRequestPlan{Method, URL, Payload} instead of sending the HTTP request. Use the shared emitDryRun(s, client, ctx, op) helper — never re-implement the dispatch inline. Every write request type that reaches a command must also have a DescribeWrite case; the read path (build helper) must be the same code path the live write uses so the preview cannot drift from the actual request.
  2. Read-only posture is a session-level switch: defaults.read_only in the config file, or CONFLUENCE_CLI_READ_ONLY in the environment. When active, appState.newClient() wraps the client in apiclient.NewReadOnly(...), which returns a structured READONLY_BLOCKED (category=permission) error from every mutating method before any HTTP request is sent. --allow-writes (root persistent flag) overrides the posture for one invocation.

Client is the union of Reader and Writer (pkg/apiclient/client.go). Declare every new method in exactly one of them: anything that can change remote state goes in Writer, never in Reader, because the read-only wrapper delegates Reader unchanged. When you add a method to Writer:

  • Add its blocking override on readOnlyClient in pkg/apiclient/readonly.go; the build fails until you do.
  • Add a DescribeWrite case + a --dry-run branch on the calling command.
  • Add an e2e assertion for both the --dry-run happy path and the READONLY_BLOCKED rejection inside the read-only block of scripts/e2e.sh, where the mock server's write log must stay empty.
  • Add a row to the wrapper's write table in pkg/apiclient/readonly_test.go; the test fails until you do.

A method added to Reader needs a canned answer on recordingReader and a row in the delegation test in the same file, which also fails until it has one.

--dry-run must not be blocked by read-only mode. DescribeWrite may perform reads needed to build the request, but never sends the mutation. It is a Reader method, so the wrapper passes it through. Keep preview/live payload parity tests for both flavors, including AddCommentReq and threaded storage/wiki bodies.

Preserve content during write recovery

  • Page body edits must use the current raw storage body and its version; preserve unrelated human content and rich markup. Conflict recovery must merge the intended edit into newly fetched content before retrying with --version. Metadata-only reads followed by a version bump are not safe recovery.
  • A transient or decoding error does not prove a write failed. Skill and runtime guidance must verify remote state before replaying an uncertain mutation.
  • Keep the Skill's human-comment approval gate and reason-once explanation. Technical previews do not introduce another approval gate for an authorized change; attribution describes the actual AI contribution.

Documentation — keep it current

  • Keep list pagination in agent-cli-kit's output.EmitList. NDJSON emits rows on stdout and a structured _notice.pagination on stderr only after successful row output when more results remain. Preserve cursors through field projection and empty filtered pages; completed --all results emit no pagination notice. Options.NoticeWriter defaults to stderr and NextFlag defaults to --cursor, shared with the table footer. Test cursor replay through the real CLI for both Cloud and Data Center when changing this path.

  • Actively maintain the docs. When a change affects architecture, installation, commands, flags, or the release process, update the relevant file under docs/ in the same commit. Stale docs are a defect.

  • This includes the GitHub Pages site. docs/index.html is the published landing page (served at https://angelmsger.github.io/confluence-cli/) and .github/workflows/pages.yml redeploys it on every push to main that touches docs/. When commands, the feature list, or install instructions change, update docs/index.html to match — do not let the landing page drift from the README and the CLI.

  • Keep the companion Skill in sync — it is the agent-facing source of truth. The embedded Skill under skills/confluence/ (SKILL.md

    • references/) is what coding agents read instead of --help, so a capability the Skill omits effectively does not exist for them. Any new command, subcommand, flag, or alias — and any change to flavor-specific behavior (Cloud vs Data Center / Server) — must be reflected in the Skill's ## Commands list and the relevant references/ file in the same commit. In particular: a flag whose help text points at another command (e.g. --author saying "find IDs with user search") must have that command listed in the Skill; and never leave a Skill claim that contradicts the code.

Changelog & versioning — required

  • Actively maintain CHANGELOG.md. Whenever a change is user-facing (a flag, command, output, behavior, or bug fix), add an entry to the [Unreleased] section in the same commit — do not leave it for later.

  • If you bump the version, you must tag the commit. "Bumping the version" means renaming [Unreleased] in CHANGELOG.md to the new version with today's date and updating build/npm/package.json. The CLI's own version is derived from the git tag via -ldflags, so a version bump is not real until the commit carrying it is tagged:

    git tag vX.Y.Z <commit>
    git push origin vX.Y.Z

    See docs/releasing.md for the full release procedure.

Team setup contract

Keep service presets separate from personal credentials. config set-context must resolve the named destination (including a new one), ignore personal variables before scheme inference, remain offline and credential-store-free, and share one merge for dry-run and execution. Preserve other contexts, personal usernames and shared defaults. Login must persist its identity as well as its secret and reject a different complete service URL before credential writes. Use the common acquisition guide in all prompt styles and missing-credential recovery; never request a guide URL with credentials. Cover a fresh config reload, conflict/idempotent setup, and partial persistence failures when changing this flow. The canonical behavior is in the installation guide's team setup section.

Time-window flags

--since excludes --from and --to, and --to requires --from; the window is the half-open [from, to). Resolve it with resolveHistoryWindow before any credential or network access and report a rejected one through badHistoryWindow: BAD_TIME_RANGE, usage category, the violated rule as the message. Reuse both for any new event or history filter rather than adding a second parser. search --after/--before are server-side lastmodified filters, not an event window, and stay outside this contract.

Credential reuse

Keep auth reuse separate from public service setup. The decision belongs to agent-cli-kit's reuse package; this repository only describes its contexts, resolves, verifies and writes. Match complete URLs and the effective deployment flavor before credential access, keep ValidateService as the scheme check (Cloud never adopts pat, not even when selected explicitly), never replace a working login automatically, and retain operational failures. A username is an identity only under basic. Verification reports the canonical account from the same user/current call (accountId on Cloud, userKey or the username on Data Center); credentials of one account are one login. Never copy secrets, infer identity from environment variables or activate a context. Cover dry-run, ambiguity, scope mismatch, concurrent edits, fresh-load credential resolution and both flavors. Native self-configuration follows the existing read-only exception.

Credential cleanup

The keychain account is <host>:<scheme>, so contexts on one host with one scheme share a secret: a team preset and the personal context whose login it reuses, or two spellings of one URL. Forget a stored secret only through forgetUnusedCredential, after the config write has succeeded, and only when no remaining context resolves the same account. Resolve a missing scheme as a request does (basic on Cloud, pat elsewhere) instead of assuming one. auth logout is the explicit exception. Cover a URL edit that keeps the host, a shared and an unshared deletion, a scheme change, a context without a scheme and a failed write; see internal/app/config_credentials_test.go. internal/app tests run against the in-memory keyring that TestMain installs; never add a test that can reach the OS keychain.