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 afi-template universals —
learn,explain,--version,--json, structured errors, exit-code policy. - An
issuenoun group with three verbs (post,fetch,reply) that wraps theghCLI and signs every authored body with the repo’s nick. - The release pipeline (lint, test, version-check, TestPyPI dev publish on PR, PyPI publish on push to main).
- Three sibling skills vendored from
steward(version-bump,run-tests,pypi-maintainer) so the CI gates the workflows enforce have the agent-side tooling to satisfy them.
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.
| Command | Args | Exit triggers | stdout (text) | stdout (--json) |
|---|---|---|---|---|
agtag --version | — | — | agtag 0.1.0 | {"version":"0.1.0"} |
agtag learn | [--json] | — | self-teaching prompt (markdown) | structured prompt |
agtag explain <path…> | [--json] | unknown path → 1 | catalog entry markdown | catalog 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 → 1 | https://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 → 1 | comment 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:
test—uv sync→uv run pytest -n auto --cov=agtag --cov-report=xml:coverage.xml --cov-report=term -v→ SonarCloud scan (gated on aSONAR_TOKEN_PRESENTflag promoted to job env; the realSONAR_TOKENis injected only at the scan step’senv:to keep it out ofpytest’s environment). The current standalonesonar.ymlis deleted and its scan step folds into this job.lint—black --check,isort --check-only,flake8,bandit -c pyproject.toml -r agtag,markdownlint-cli2 "**/*.md".version-check— PR-only; comparespyproject.tomlversion againstorigin/main. Sticky comment if equal. Lifted verbatim from steward; only project name interpolation changes.
.github/workflows/publish.yml
Runs on push: main and PR, paths-filtered to pyproject.toml + agtag/**. Three jobs:
test— minimal smoke (same as above).test-publish— PR-only, non-fork-only,environment: testpypi, OIDC trusted publishing. Sets dev version${BASE}.dev${{ github.run_number }},uv build,uv publish --publish-url https://test.pypi.org/legacy/ --trusted-publishing always --check-url https://test.pypi.org/simple/. Prints a notice with the install hint:uv tool install --index-url https://test.pypi.org/simple/ --index-strategy unsafe-best-match agtag==${DEV_VERSION}.publish— push-to-main only,environment: pypi,uv build+uv publish --trusted-publishing always --check-url https://pypi.org/simple/.
Pre-merge gate
Verify on (Test)PyPI:
agtagproject exists or is reservable under theagentcultureGitHub org’s trusted-publisher claim.- Trusted-publisher records list workflow
publish.ymland environmentstestpypi(TestPyPI) /pypi(PyPI). - GitHub repo has the matching
testpypiandpypideployment 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
| Skill | Status | Adaptation |
|---|---|---|
cicd | already vendored | untouched. No file changes. |
communicate | already vendored — modified | Delete 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-bump | newly vendored | scripts/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-tests | newly vendored | scripts/test.sh is package-agnostic (reads [tool.coverage.run]); copy verbatim. SKILL.md unchanged. Add ledger row. |
pypi-maintainer | newly vendored | scripts/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
- File tracking issue on
agentculture/agtagvia the current bashpost-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). - Branch
scaffold/v0.1offmain. - 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. - Apply scaffold per Sections 2–6.
- 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-testagtag issue post --repo agentculture/agtag --title test --body test --jsonagainst a throwaway issue (then close). - Pre-merge gate — verify trusted-publisher / environment configuration per §5.
- Open PR via
cicdskill (create-pr-and-wait.sh). PR body includesCloses #<tracking issue>. - Merge →
publish.ymlruns on push-to-main → first PyPI release ofagtag 0.1.0. - Steward broadcast (post-merge follow-up, separate work): announce the
post-issue.sh→agtag issue postmigration to othercommunicate-skill consumers viasteward 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].
tests/test_cli.py—--version(text + json),learn(parseable JSON in--jsonmode),explain issue post(returns the catalog entry),explain bogus path(exit 1, json-mode-aware error),agtagwith no args (prints help, exit 0).tests/test_nick.py—culture.yamlpresent →"agtag"; absent →Path.cwd().name; malformed YAML → fallback + diagnostic on stderr.tests/test_issue_post.py— happy path: mockedghreturns issue URL, verb appends\n\n- agtag (Claude). Error:ghmissing →AgtagError(code=ENV). Override:--as steward→ suffix\n\n- steward (Claude). Idempotent suffix: passing a body that already ends with the signature does not double-append (matchespr-reply.shline 33–37 behavior).tests/test_issue_fetch.py— mockedgh issue view --jsonreturns a canned issue + comments; assert dict shape; assert positional URL parsing equivalent to--repo+--number.tests/test_issue_reply.py— symmetric to post: signature appended, idempotent, env-error path,--asoverride.tests/test_explain_catalog.py— every catalog entry resolves;known_paths()lists all of them;--jsonoutput for each entry has the documented keys.
9. Out of scope
- Mesh transport (
agtag messagemigratingculture agent message) → v0.2. - PR review-thread tooling (migrating
cicd/pr-reply.shandcicd/pr-batch.sh) →agex-clirepo, when it lands.cicdis untouched in this PR. communicateskill’sfetch-issues.shandmesh-message.sh— left as-is for now; future symmetric migration is possible but not required by this PR.
10. Risks / unknowns
- Trusted-publisher state on (Test)PyPI is unverified. First publish run fails if the OIDC claim is missing. Pre-merge verification gate exists in §5 to catch this.
- PR size. This is a large single PR. Mitigation: most files are template-substitution from the afi reference tree, not original logic; reviewers can audit Section 2–6 of this design first and use it as a checklist.
- Coverage gate at 80% for a small surface. With ~6 verbs and small modules, hitting 80% is achievable but tight; may require explicit
# pragma: no coveron the__main__.pyshim and theif TYPE_CHECKING:blocks already excluded by[tool.coverage.report].