Design: agtag CLI v0.1 scaffold

Status: Approved (brainstorm 2026-05-09) Target version: 0.1.0 Approach: Single-PR scaffold + verbs (Section 6).

1. Purpose

Stand up agtag as a published, agent-first Python CLI on PyPI under the agentculture GitHub org, with the noun/verb surface and release pipeline its sibling projects (steward, daria, afi-cli) already use. v0.1 ships:

The culture agent message migration becomes agtag message in v0.2 and is explicitly out of scope here. PR-related agent tooling (cicd/pr-reply.sh etc.) is the future domain of agex-cli, a separate sibling project, and is also out of scope.

2. Module layout

agtag/
├── pyproject.toml          # name="agtag", hatchling, single-source version
├── CHANGELOG.md            # Keep-a-Changelog, starts at 0.1.0
├── culture.yaml            # agents: [{suffix: agtag, backend: claude}]
├── README.md               # install via `uv tool install agtag`; quickstart
├── agtag/
│   ├── __init__.py         # __version__ via importlib.metadata
│   ├── __main__.py         # delegates to cli.main
│   ├── nick.py             # resolve_nick(cwd) — culture.yaml → repo basename
│   ├── cli/
│   │   ├── __init__.py     # parser + _dispatch (shape-adapt from afi)
│   │   ├── _errors.py      # AgtagError + EXIT_SUCCESS/USER/ENV (stable-contract)
│   │   ├── _output.py      # emit_result/emit_error/emit_diagnostic, --json (stable-contract)
│   │   └── _commands/
│   │       ├── __init__.py
│   │       ├── learn.py
│   │       ├── explain.py
│   │       ├── issue_post.py
│   │       ├── issue_fetch.py
│   │       └── issue_reply.py
│   ├── explain/
│   │   ├── __init__.py     # resolve() + known_paths() (stable-contract)
│   │   └── catalog.py      # ENTRIES dict for agtag's grammar
│   └── issue/
│       ├── __init__.py
│       ├── post.py         # post(repo, title, body, *, nick) -> dict
│       ├── fetch.py        # fetch(repo, number) -> dict
│       ├── reply.py        # reply(repo, number, body, *, nick) -> dict
│       └── _gh.py          # ONLY file that shells out to `gh`
├── docs/
│   ├── skill-sources.md      # vendored-skill ledger (existing; updated)
│   ├── purpose.md            # what agtag is, ecosystem map
│   ├── features.md           # release-keyed feature list + roadmap
│   ├── commands.md           # human-readable CLI reference (mirrors `agtag explain`)
│   └── culture.md            # how agtag fits the AgentCulture mesh
└── tests/
    ├── __init__.py
    ├── test_cli.py             # smoke
    ├── test_nick.py
    ├── test_issue_post.py
    ├── test_issue_fetch.py
    ├── test_issue_reply.py
    └── test_explain_catalog.py

Boundary rule. agtag/issue/ is pure: takes args, returns dicts, raises AgtagError. agtag/cli/_commands/ only translates argparse → kwargs and dict → stdout. agtag/issue/_gh.py is the only file that shells out via subprocess. Verb-level tests mock at _gh._run_gh, never at the verb function itself, so the test suite never invokes a real gh binary.

3. CLI surface

All commands honor --json. In JSON mode, machine output goes to stdout; errors go to stderr as {"code","message","remediation"}. Exit codes: 0 success, 1 user-input error, 2 environment/setup error.

CommandArgsExit triggersstdout (text)stdout (--json)
agtag --versionagtag 0.1.0{"version":"0.1.0"}
agtag learn[--json]self-teaching prompt (markdown)structured prompt
agtag explain <path…>[--json]unknown path → 1catalog entry markdowncatalog entry dict
agtag issue post--repo OWNER/REPO --title T (--body B | --body-file F) [--as NICK] [--json]missing gh → 2; auth fail → 2; bad repo → 1https://github.com/.../issues/N{"url":…, "number":N, "signed_as":"agtag"}
agtag issue fetch--repo OWNER/REPO --number N [--json] (or single positional issue URL)missing gh → 2; not found → 1## #N: <title> + body + per-comment ### @user — date{"number":N,"title":…,"body":…,"author":…,"comments":[…]}
agtag issue reply--repo OWNER/REPO --number N (--body B | --body-file F) [--as NICK] [--json]missing gh → 2; auth fail → 2; not found → 1comment URL{"url":…,"signed_as":"agtag"}

Signing rule

For issue post and issue reply: the supplied body is taken verbatim, then a trailing blank line and \n- <nick> (Claude) are appended. <nick> resolves via agtag.nick.resolve_nick(cwd). --as NICK overrides. issue fetch is read-only and emits no signature.

Idempotent. If the supplied body already ends with the exact signature line that would be appended, no second copy is added. Matches the bash pr-reply.sh line 33–37 behavior so an agent can pre-sign and re-pipe without doubling.

Issue fetch — URL positional form

agtag issue fetch https://github.com/agentculture/agtag/issues/12 is equivalent to agtag issue fetch --repo agentculture/agtag --number 12. Parsing the URL is done with a single regex; on no match, exit 1 with a JSON-mode-aware error pointing to the explicit --repo/--number form.

explain catalog (v0.1)

Entries: agtag, learn, explain, issue (group page that lists its verbs), issue post, issue fetch, issue reply. The catalog is the source of truth for command discovery; docs/commands.md is a hand-tended mirror that links each section back to agtag explain <path>.

4. Nick resolution + culture.yaml

def resolve_nick(cwd: Path | None = None) -> str:
    """Resolve the signing nick.

    Order:
      1. <cwd>/culture.yaml → agents[0].suffix (if present and non-empty)
      2. Path(cwd).name (repo dir basename)
    cwd defaults to Path.cwd().
    """

Uses pyyaml. Malformed YAML: silent fallback to basename, with a diagnostic on stderr explaining the parse failure. Matches the bash _resolve-nick.sh contract behaviorally.

This repo’s culture.yaml:

agents:
- suffix: agtag
  backend: claude

Once present, both the python agtag/nick.py and the bash cicd/_resolve-nick.sh (used by other cicd scripts) read the same source of truth.

5. CI / release pipeline

Three workflows, ported from steward and adapted to agtag:

.github/workflows/tests.yml

Runs on PR and push to main. Three jobs:

.github/workflows/publish.yml

Runs on push: main and PR, paths-filtered to pyproject.toml + agtag/**. Three jobs:

Pre-merge gate

Verify on (Test)PyPI:

  1. agtag project exists or is reservable under the agentculture GitHub org’s trusted-publisher claim.
  2. Trusted-publisher records list workflow publish.yml and environments testpypi (TestPyPI) / pypi (PyPI).
  3. GitHub repo has the matching testpypi and pypi deployment environments configured.

Without these, the first publish run fails. The brainstorm flagged this as unverified — the user said the project name is “already prepared” but pip index versions agtag returns nothing on either index. The implementation plan must verify before merge.

Versioning

Initial version 0.1.0. CHANGELOG.md gets a single ## [0.1.0] - 2026-05-09 entry under “Added” listing scaffold, verbs, vendored skills, and workflows.

6. Skills vendored + adaptations

SkillStatusAdaptation
cicdalready vendoreduntouched. No file changes.
communicatealready vendored — modifiedDelete scripts/post-issue.sh. Rewrite SKILL.md so the “post an issue” path tells the agent to run agtag issue post --repo … --title … --body-file …. Preserve gh env-error remediation hints, just relocate them. fetch-issues.sh + mesh-message.sh untouched. Update docs/skill-sources.md ledger entry.
version-bumpnewly vendoredscripts/bump.py is generic on package name; copy verbatim. SKILL.md: replace steward/__init__.py reference with agtag/__init__.py (importlib.metadata-driven, same shape). Add docs/skill-sources.md row.
run-testsnewly vendoredscripts/test.sh is package-agnostic (reads [tool.coverage.run]); copy verbatim. SKILL.md unchanged. Add ledger row.
pypi-maintainernewly vendoredscripts/switch-source.sh takes pkg name as first arg; copy verbatim. SKILL.md unchanged. Add ledger row.

After this PR docs/skill-sources.md has 5 rows.

7. PR plan + tracking issue

  1. File tracking issue on agentculture/agtag via the current bash post-issue.sh (last call before deletion). Title: Scaffold agtag CLI v0.1 (afi template + issue verbs + release pipeline). Body summarizes scope, surface, skills, version, deferred items. Signed - agtag (Claude).
  2. Branch scaffold/v0.1 off main.
  3. Run afi cli cite . to drop the reference tree into .afi/reference/python-cli/. Not committed (added to .gitignore); used as source-of-truth template for token substitution.
  4. Apply scaffold per Sections 2–6.
  5. Local verify: uv sync && uv run pytest -n auto -v --cov=agtag, uv run black --check agtag tests, uv run flake8 agtag tests, markdownlint-cli2 "**/*.md", agtag --version, agtag explain issue post, smoke-test agtag issue post --repo agentculture/agtag --title test --body test --json against a throwaway issue (then close).
  6. Pre-merge gate — verify trusted-publisher / environment configuration per §5.
  7. Open PR via cicd skill (create-pr-and-wait.sh). PR body includes Closes #<tracking issue>.
  8. Mergepublish.yml runs on push-to-main → first PyPI release of agtag 0.1.0.
  9. Steward broadcast (post-merge follow-up, separate work): announce the post-issue.shagtag issue post migration to other communicate-skill consumers via steward announce-skill-update.

8. Testing strategy

agtag/issue/_gh.py is the only file that shells out. Tests mock at that boundary, so verb-level tests are fast and require no gh binary. No live network calls in the suite. Coverage gate fail_under = 80 in [tool.coverage.report].

9. Out of scope

10. Risks / unknowns