guild-cli 0.6.4__tar.gz → 0.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/guild/SKILL.md +10 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/guild/scripts/create.sh +6 -1
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.github/workflows/tests.yml +5 -6
- {guild_cli-0.6.4 → guild_cli-0.7.1}/CHANGELOG.md +40 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/CLAUDE.md +1 -1
- {guild_cli-0.6.4 → guild_cli-0.7.1}/PKG-INFO +1 -1
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/cli/_commands/_provision_template.py +5 -1
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/cli/_commands/create.py +32 -2
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/scaffold/instantiate.py +117 -5
- {guild_cli-0.6.4 → guild_cli-0.7.1}/pyproject.toml +1 -1
- guild_cli-0.7.1/sonar-project.properties +24 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_cli_create.py +77 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_scaffold_instantiate.py +111 -1
- {guild_cli-0.6.4 → guild_cli-0.7.1}/uv.lock +1 -1
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/agent-config/SKILL.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/agent-config/scripts/show.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/cicd/SKILL.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/cicd/scripts/workflow.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/communicate/SKILL.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/guild/scripts/configure-repo.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/guild/scripts/overview.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/onboard/SKILL.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/onboard/scripts/onboard.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/run-tests/SKILL.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/sonarclaude/SKILL.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/spec-to-plan/SKILL.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/teach/SKILL.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/teach/scripts/teach.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/think/SKILL.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/think/scripts/think.sh +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/version-bump/SKILL.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/version-bump/scripts/bump.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills.local.yaml.example +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.devague/frames/guildmaster-ships-teach-and-onboard-two-agent-firs.json +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.devague/plans/guildmaster-ships-teach-and-onboard-two-agent-firs.json +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.flake8 +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.github/workflows/publish.yml +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.gitignore +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/.markdownlint-cli2.yaml +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/LICENSE +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/README.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/culture.yaml +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/docs/cutover.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/docs/plans/2026-05-24-guildmaster-ships-teach-and-onboard-two-agent-firs.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/docs/skill-sources.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/docs/specs/2026-05-24-guildmaster-ships-teach-and-onboard-two-agent-firs.md +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/__init__.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/__main__.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/cli/__init__.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/cli/_commands/__init__.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/cli/_commands/_broadcast.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/cli/_commands/explain.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/cli/_commands/learn.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/cli/_commands/onboard.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/cli/_commands/overview.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/cli/_commands/show.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/cli/_commands/teach.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/cli/_commands/whoami.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/cli/_errors.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/cli/_output.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/cli/_repo.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/scaffold/__init__.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/skills/__init__.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/skills/identity.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/skills/ledger.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/skills/render.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/guild/skills/sources.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/__init__.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_broadcast_post.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_cli.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_cli_explain.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_cli_learn.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_cli_onboard.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_cli_overview.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_cli_show.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_cli_teach.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_cli_whoami.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_skills_convention.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_skills_identity.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_skills_ledger.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_skills_render.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_skills_sources.py +0 -0
- {guild_cli-0.6.4 → guild_cli-0.7.1}/tests/test_version_fallback.py +0 -0
|
@@ -116,6 +116,13 @@ Provision a new sibling by instantiating the GitHub template and customising it:
|
|
|
116
116
|
--template myorg/my-template \
|
|
117
117
|
--apply
|
|
118
118
|
|
|
119
|
+
# Custom PyPI dist name (command + import package stay the repo name)
|
|
120
|
+
.claude/skills/guild/scripts/create.sh \
|
|
121
|
+
--agent agentculture/jetson \
|
|
122
|
+
--desc "Edge-AI ops agent." \
|
|
123
|
+
--dist jetson-cli \
|
|
124
|
+
--apply
|
|
125
|
+
|
|
119
126
|
# JSON output (works in both dry-run and apply)
|
|
120
127
|
.claude/skills/guild/scripts/create.sh \
|
|
121
128
|
--agent agentculture/new-agent \
|
|
@@ -135,6 +142,9 @@ Provision a new sibling by instantiating the GitHub template and customising it:
|
|
|
135
142
|
- rewrites `README.md` first heading + intro
|
|
136
143
|
- overwrites `CLAUDE.md` with a self-init seed (names the agent, embeds the
|
|
137
144
|
description, carries a `/init` re-init instruction)
|
|
145
|
+
- if `--dist` differs from the repo name, retargets the PyPI dist
|
|
146
|
+
(`[project].name`, the `importlib.metadata` lookup, the TestPyPI install
|
|
147
|
+
pin) — the command + import package stay the repo name
|
|
138
148
|
4. Runs `configure-repo.sh` to apply GitHub settings (branch protection,
|
|
139
149
|
environments, `SONAR_TOKEN` placeholder).
|
|
140
150
|
5. Stages, commits (`scaffold <bare> from culture-agent-template`), and pushes.
|
|
@@ -12,7 +12,12 @@ set -euo pipefail
|
|
|
12
12
|
# Usage:
|
|
13
13
|
# create.sh --agent OWNER/REPO --desc TEXT [--backend claude|acp]
|
|
14
14
|
# [--workspace-root DIR] [--template OWNER/REPO]
|
|
15
|
-
# [--org agentculture] [--apply] [--json]
|
|
15
|
+
# [--org agentculture] [--dist NAME] [--apply] [--json]
|
|
16
|
+
#
|
|
17
|
+
# --dist NAME sets the PyPI distribution name (default: the repo name); pass
|
|
18
|
+
# e.g. --dist jetson-cli to ship the dist as jetson-cli while keeping the
|
|
19
|
+
# command + import package as the repo name. Retargets [project].name, the
|
|
20
|
+
# importlib.metadata lookup, and the TestPyPI install pin.
|
|
16
21
|
#
|
|
17
22
|
# Exit codes:
|
|
18
23
|
# 0 success (delegates to `guild create`; its exit code propagates)
|
|
@@ -33,12 +33,11 @@ jobs:
|
|
|
33
33
|
- run: uv run pytest -n auto --cov=guild --cov-report=xml:coverage.xml --cov-report=term -v
|
|
34
34
|
|
|
35
35
|
- name: SonarCloud Scan
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
if: env.SONAR_TOKEN != '' && hashFiles('sonar-project.properties') != ''
|
|
36
|
+
# Reads sonar-project.properties (projectKey agentculture_guildmaster,
|
|
37
|
+
# coverage.xml path, qualitygate.wait). Gated only on the token so fork
|
|
38
|
+
# PRs and token-less repos skip the scan and stay green — `secrets.*`
|
|
39
|
+
# isn't allowed in `if:`, hence the job-level env promotion above.
|
|
40
|
+
if: env.SONAR_TOKEN != ''
|
|
42
41
|
uses: SonarSource/sonarqube-scan-action@fd88b7d7ccbaefd23d8f36f73b59db7a3d246602 # v6
|
|
43
42
|
env:
|
|
44
43
|
SONAR_HOST_URL: https://sonarcloud.io
|
|
@@ -5,6 +5,46 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
Format follows [Keep a Changelog](https://keepachangelog.com/). This project
|
|
6
6
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.7.1] - 2026-05-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **SonarCloud coverage wiring** — committed `sonar-project.properties`
|
|
13
|
+
(`agentculture_guildmaster`, `sonar.python.coverage.reportPaths=coverage.xml`,
|
|
14
|
+
`sonar.qualitygate.wait=true`). CI already produced `coverage.xml`
|
|
15
|
+
(`--cov=guild --cov-report=xml:coverage.xml`) but the SonarCloud scan was
|
|
16
|
+
gated off because the config file was absent — guildmaster was the only repo
|
|
17
|
+
in the fleet without it. The project is already provisioned on SonarCloud, so
|
|
18
|
+
this activates the scan and feeds coverage to the quality gate.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **`tests.yml` SonarCloud Scan gate** — dropped the now-redundant
|
|
23
|
+
`hashFiles('sonar-project.properties')` clause (the file is committed) so the
|
|
24
|
+
step gates only on `env.SONAR_TOKEN != ''`, matching steward and the
|
|
25
|
+
`culture-agent-template`. Refreshed the stale "not provisioned yet" comment.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
## [0.7.0] - 2026-05-28
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **`guild create --dist NAME`** — set the PyPI **distribution** name at
|
|
34
|
+
provision time. Defaults to the repo token (unchanged behaviour); pass e.g.
|
|
35
|
+
`--dist jetson-cli` to ship the dist as `jetson-cli` while the console command
|
|
36
|
+
and import package stay the repo token (`jetson`), matching the
|
|
37
|
+
`guild-cli`/`guild` convention. The transform's new step 6 retargets only the
|
|
38
|
+
three dist-name surfaces — `[project].name`, the `importlib.metadata` version
|
|
39
|
+
lookup, and the TestPyPI install pin — and is a strict no-op when `--dist`
|
|
40
|
+
equals (or is omitted and defaults to) the repo token, so the bare-name case
|
|
41
|
+
(e.g. `dominion-breaker`) is preserved. The name is validated as a PEP 503
|
|
42
|
+
distribution name before any external act. This removes the manual
|
|
43
|
+
post-`create` dist rename that `agenda`, `convertible`, and `jetson` each
|
|
44
|
+
needed. Note: renaming the dist still requires a Trusted Publisher registered
|
|
45
|
+
for that project name on PyPI/TestPyPI — `guild create` configures the GitHub
|
|
46
|
+
side only.
|
|
47
|
+
|
|
8
48
|
## [0.6.4] - 2026-05-28
|
|
9
49
|
|
|
10
50
|
### Added
|
|
@@ -108,7 +108,7 @@ two live broadcasters.
|
|
|
108
108
|
|
|
109
109
|
**Provisioning verb — `guild create` (template-instantiate a new sibling).**
|
|
110
110
|
|
|
111
|
-
- `guild create --agent OWNER/REPO --desc TEXT [--backend claude|acp] [--workspace-root DIR] [--template agentculture/culture-agent-template] [--apply] [--json]` — provision a brand-new AgentCulture sibling by instantiating the GitHub template, renaming identifiers throughout the clone, writing a self-init CLAUDE.md seed, configuring the GitHub repo (branch protection, environments, SONAR_TOKEN placeholder), pushing the genesis commit, and registering the agent in `docs/skill-sources.md`. Dry-run by default; `--apply` executes. The transform is **pure** (unit-testable against a fixture dir; no network): `culture_agent_template` → pkg (underscore form, e.g. `my_agent`), `culture-agent-template` → repo token (hyphen form, e.g. `my-agent`). The CLAUDE.md seed is a `/init`-style bootstrap placeholder — run `/init` in the new repo after provisioning to expand it into a full runtime prompt. Implementation: `guild/scaffold/instantiate.py` (pure transform), `guild/cli/_commands/create.py` (CLI), `guild/cli/_commands/_provision_template.py` (injectable-runner executor), `.claude/skills/guild/scripts/create.sh` (thin wrapper).
|
|
111
|
+
- `guild create --agent OWNER/REPO --desc TEXT [--backend claude|acp] [--workspace-root DIR] [--template agentculture/culture-agent-template] [--dist NAME] [--apply] [--json]` — provision a brand-new AgentCulture sibling by instantiating the GitHub template, renaming identifiers throughout the clone, writing a self-init CLAUDE.md seed, configuring the GitHub repo (branch protection, environments, SONAR_TOKEN placeholder), pushing the genesis commit, and registering the agent in `docs/skill-sources.md`. Dry-run by default; `--apply` executes. The transform is **pure** (unit-testable against a fixture dir; no network): `culture_agent_template` → pkg (underscore form, e.g. `my_agent`), `culture-agent-template` → repo token (hyphen form, e.g. `my-agent`). `--dist NAME` sets the **PyPI distribution name** (default: the repo token); pass e.g. `--dist jetson-cli` to publish as `jetson-cli` while keeping the command and import package as the repo token (`jetson`) — this retargets only `[project].name`, the `importlib.metadata` lookup, and the TestPyPI install pin, matching the `guild-cli`/`guild` convention without a manual post-create edit. (Renaming the dist needs a matching Trusted Publisher registered for that project name on PyPI/TestPyPI — `guild create` configures the GitHub side only.) The CLAUDE.md seed is a `/init`-style bootstrap placeholder — run `/init` in the new repo after provisioning to expand it into a full runtime prompt. Implementation: `guild/scaffold/instantiate.py` (pure transform), `guild/cli/_commands/create.py` (CLI), `guild/cli/_commands/_provision_template.py` (injectable-runner executor), `.claude/skills/guild/scripts/create.sh` (thin wrapper).
|
|
112
112
|
|
|
113
113
|
**Inventory verbs — `overview` & `show` (the read-only surfaces).** guildmaster
|
|
114
114
|
owns the mesh's *inventory* surfaces per
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: guild-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: guildmaster — an agent and CLI that manages skills for the AgentCulture mesh.
|
|
5
5
|
Project-URL: Homepage, https://github.com/agentculture/guildmaster
|
|
6
6
|
Project-URL: Issues, https://github.com/agentculture/guildmaster/issues
|
|
@@ -241,6 +241,7 @@ def apply(
|
|
|
241
241
|
guildmaster_root: Path,
|
|
242
242
|
runner: Runner | None = None,
|
|
243
243
|
template: str = DEFAULT_TEMPLATE,
|
|
244
|
+
dist: str | None = None,
|
|
244
245
|
poll_attempts: int = 30,
|
|
245
246
|
poll_delay: float = 2.0,
|
|
246
247
|
) -> dict:
|
|
@@ -265,6 +266,9 @@ def apply(
|
|
|
265
266
|
Callable ``(cmd, cwd=None) -> RunResult``. Defaults to real subprocess.
|
|
266
267
|
template:
|
|
267
268
|
The GitHub template repo to instantiate (default: ``DEFAULT_TEMPLATE``).
|
|
269
|
+
dist:
|
|
270
|
+
PyPI distribution name to set in the clone. ``None`` leaves it as the
|
|
271
|
+
repo name; a different value retargets the dist (see ``transform_clone``).
|
|
268
272
|
poll_attempts / poll_delay:
|
|
269
273
|
``gh repo create --template`` copies the template content
|
|
270
274
|
**asynchronously**; cloning immediately yields an empty repo. After
|
|
@@ -315,7 +319,7 @@ def apply(
|
|
|
315
319
|
_check(clone, f"git clone {agent} failed", EXIT_ENV_ERROR)
|
|
316
320
|
|
|
317
321
|
# Step 4 — pure transform (no runner needed).
|
|
318
|
-
transform_clone(clone_dest, bare, desc, backend)
|
|
322
|
+
transform_clone(clone_dest, bare, desc, backend, dist=dist)
|
|
319
323
|
|
|
320
324
|
# Step 5 — stage everything.
|
|
321
325
|
add = runner(["git", "-C", str(clone_dest), "add", "-A"])
|
|
@@ -15,7 +15,7 @@ Usage
|
|
|
15
15
|
guild create --agent OWNER/REPO --desc TEXT [--org agentculture]
|
|
16
16
|
[--backend claude|acp] [--workspace-root DIR]
|
|
17
17
|
[--template agentculture/culture-agent-template]
|
|
18
|
-
[--apply] [--json]
|
|
18
|
+
[--dist NAME] [--apply] [--json]
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
21
|
from __future__ import annotations
|
|
@@ -24,6 +24,7 @@ import argparse
|
|
|
24
24
|
import difflib
|
|
25
25
|
import json
|
|
26
26
|
import keyword
|
|
27
|
+
import re
|
|
27
28
|
from pathlib import Path
|
|
28
29
|
|
|
29
30
|
from guild.cli._commands import _broadcast
|
|
@@ -86,6 +87,17 @@ def register(sub: argparse._SubParsersAction) -> None:
|
|
|
86
87
|
default=_provision.DEFAULT_TEMPLATE,
|
|
87
88
|
help=f"GitHub template repo to instantiate (default: {_provision.DEFAULT_TEMPLATE}).",
|
|
88
89
|
)
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"--dist",
|
|
92
|
+
metavar="NAME",
|
|
93
|
+
default=None,
|
|
94
|
+
help=(
|
|
95
|
+
"PyPI distribution name. Defaults to the repo name; pass e.g. "
|
|
96
|
+
"'jetson-cli' to ship the dist as 'jetson-cli' while keeping the "
|
|
97
|
+
"command and import package as the repo name ('jetson'). Retargets "
|
|
98
|
+
"[project].name, the importlib.metadata lookup, and the TestPyPI pin."
|
|
99
|
+
),
|
|
100
|
+
)
|
|
89
101
|
parser.add_argument(
|
|
90
102
|
"--apply",
|
|
91
103
|
action="store_true",
|
|
@@ -127,12 +139,26 @@ def _handle(args: argparse.Namespace) -> None:
|
|
|
127
139
|
),
|
|
128
140
|
)
|
|
129
141
|
|
|
142
|
+
# Validate the optional PyPI dist name (fail fast, before any external act).
|
|
143
|
+
# PyPI normalises [-_.] runs, but the raw name must still be a non-empty run
|
|
144
|
+
# of letters/digits/.-_ that starts and ends alphanumerically (PEP 503).
|
|
145
|
+
if args.dist is not None and not re.match(
|
|
146
|
+
r"^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$", args.dist
|
|
147
|
+
):
|
|
148
|
+
raise GuildError(
|
|
149
|
+
code=EXIT_USER_ERROR,
|
|
150
|
+
message=f"--dist {args.dist!r} is not a valid PyPI distribution name",
|
|
151
|
+
remediation=(
|
|
152
|
+
"use letters/digits/.-_ starting and ending alphanumerically (e.g. 'jetson-cli')"
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
|
|
130
156
|
# Resolve workspace root (parent of guildmaster if not supplied).
|
|
131
157
|
workspace_root = args.workspace_root if args.workspace_root is not None else root.parent
|
|
132
158
|
clone_dest = workspace_root / bare
|
|
133
159
|
|
|
134
160
|
# Build the dry-run plan (pure — no writes, no subprocess).
|
|
135
|
-
plan = _transform_plan(bare, args.desc)
|
|
161
|
+
plan = _transform_plan(bare, args.desc, dist=args.dist)
|
|
136
162
|
|
|
137
163
|
# Ledger diff (pure — reads only).
|
|
138
164
|
skills = _broadcast.canonical_skills(root)
|
|
@@ -171,6 +197,7 @@ def _handle(args: argparse.Namespace) -> None:
|
|
|
171
197
|
clone_dest=clone_dest,
|
|
172
198
|
guildmaster_root=root,
|
|
173
199
|
template=args.template,
|
|
200
|
+
dist=args.dist,
|
|
174
201
|
)
|
|
175
202
|
|
|
176
203
|
# Register in the ledger idempotently.
|
|
@@ -186,6 +213,7 @@ def _handle(args: argparse.Namespace) -> None:
|
|
|
186
213
|
"repo": apply_result["repo"],
|
|
187
214
|
"clone_dest": apply_result["clone_dest"],
|
|
188
215
|
"pushed": apply_result["pushed"],
|
|
216
|
+
"dist": plan["dist"],
|
|
189
217
|
"ledger_written": ledger_written,
|
|
190
218
|
}
|
|
191
219
|
emit_result(json.dumps(result, indent=2) if args.json else _render_apply(result))
|
|
@@ -207,6 +235,8 @@ def _render_dry_run(result: dict) -> str:
|
|
|
207
235
|
f" clone dest : {result['clone_dest']}",
|
|
208
236
|
f" pkg : {plan['pkg']}",
|
|
209
237
|
f" repo token : {plan['repo_token']}",
|
|
238
|
+
f" dist (PyPI): {plan['dist']}"
|
|
239
|
+
+ ("" if plan["dist"] == plan["repo_token"] else " (command + import stay repo token)"),
|
|
210
240
|
"",
|
|
211
241
|
"── rename map ──",
|
|
212
242
|
]
|
|
@@ -10,12 +10,13 @@ rename_map(bare) -> dict[str, str]
|
|
|
10
10
|
The two identifier substitutions: ``culture_agent_template`` → ``pkg`` and
|
|
11
11
|
``culture-agent-template`` → ``repo_token``. Pure function; no I/O.
|
|
12
12
|
|
|
13
|
-
transform_plan(bare, desc) -> dict
|
|
13
|
+
transform_plan(bare, desc, dist=None) -> dict
|
|
14
14
|
A human/JSON-renderable description of what ``transform_clone`` WOULD do
|
|
15
15
|
(the rename map, package directory rename, pyproject description, README
|
|
16
|
-
headline change,
|
|
16
|
+
headline change, CLAUDE.md seed replacement, and — when *dist* differs from
|
|
17
|
+
the repo token — the dist-name retarget). No I/O.
|
|
17
18
|
|
|
18
|
-
transform_clone(dest, bare, desc, backend) -> None
|
|
19
|
+
transform_clone(dest, bare, desc, backend, dist=None) -> None
|
|
19
20
|
The actual, irreversible in-place transform. Walks *dest*, skipping
|
|
20
21
|
``.git/``:
|
|
21
22
|
1. Global text replace across every file: ``culture_agent_template`` → pkg,
|
|
@@ -29,6 +30,14 @@ transform_clone(dest, bare, desc, backend) -> None
|
|
|
29
30
|
5. Overwrite ``CLAUDE.md`` with a self-init seed (prompt-file-present +
|
|
30
31
|
backend-consistency compliant; carries an explicit ``/init`` re-init
|
|
31
32
|
instruction; names the agent, embeds *desc*, uses *backend*).
|
|
33
|
+
6. **Only when** *dist* is given and differs from the repo token: retarget
|
|
34
|
+
the PyPI distribution name in the three places that name the *dist* (not
|
|
35
|
+
the import package or the console command) — ``[project].name``, the
|
|
36
|
+
``importlib.metadata`` lookup, and the TestPyPI install pin.
|
|
37
|
+
|
|
38
|
+
*dist* ``None`` means "leave the dist as the repo token" (the default). An
|
|
39
|
+
explicitly empty/whitespace *dist* is rejected (``ValueError``) so a stray
|
|
40
|
+
programmatic ``""`` is not silently treated as "unset".
|
|
32
41
|
|
|
33
42
|
This module is PURE w.r.t. the filesystem: it only writes within *dest*, and
|
|
34
43
|
only after the caller hands it a real directory. No subprocess, no network.
|
|
@@ -39,6 +48,7 @@ from __future__ import annotations
|
|
|
39
48
|
|
|
40
49
|
import re
|
|
41
50
|
from pathlib import Path
|
|
51
|
+
from typing import Callable
|
|
42
52
|
|
|
43
53
|
# ---------------------------------------------------------------------------
|
|
44
54
|
# Identifier derivation
|
|
@@ -70,6 +80,21 @@ def _derive(bare: str) -> tuple[str, str]:
|
|
|
70
80
|
return pkg, repo_token
|
|
71
81
|
|
|
72
82
|
|
|
83
|
+
def _effective_dist(dist: str | None, repo_token: str) -> str:
|
|
84
|
+
"""Resolve the distribution name, distinguishing ``None`` from ``""``.
|
|
85
|
+
|
|
86
|
+
``None`` means "not provided" → default to *repo_token*. An explicitly
|
|
87
|
+
empty or whitespace-only *dist* is a programming error (the CLI rejects it
|
|
88
|
+
before this point) and raises ``ValueError`` rather than silently defaulting,
|
|
89
|
+
so plan and transform reason identically about their inputs.
|
|
90
|
+
"""
|
|
91
|
+
if dist is None:
|
|
92
|
+
return repo_token
|
|
93
|
+
if not dist.strip():
|
|
94
|
+
raise ValueError("dist must be a non-empty string or None")
|
|
95
|
+
return dist
|
|
96
|
+
|
|
97
|
+
|
|
73
98
|
# ---------------------------------------------------------------------------
|
|
74
99
|
# Public helpers — no I/O
|
|
75
100
|
# ---------------------------------------------------------------------------
|
|
@@ -95,7 +120,7 @@ def rename_map(bare: str) -> dict[str, str]:
|
|
|
95
120
|
return {_TEMPLATE_PKG: pkg, _TEMPLATE_REPO: repo_token}
|
|
96
121
|
|
|
97
122
|
|
|
98
|
-
def transform_plan(bare: str, desc: str) -> dict:
|
|
123
|
+
def transform_plan(bare: str, desc: str, dist: str | None = None) -> dict:
|
|
99
124
|
"""Return a human/JSON-renderable description of what ``transform_clone`` would do.
|
|
100
125
|
|
|
101
126
|
Parameters
|
|
@@ -104,6 +129,13 @@ def transform_plan(bare: str, desc: str) -> dict:
|
|
|
104
129
|
The bare repo/agent name.
|
|
105
130
|
desc:
|
|
106
131
|
The short description for the new agent.
|
|
132
|
+
dist:
|
|
133
|
+
The PyPI distribution name to set. ``None`` (the default) means leave it
|
|
134
|
+
as the global-replace result (``repo_token``) — the pure default. When it
|
|
135
|
+
differs from ``repo_token``, an extra retarget step renames just the dist
|
|
136
|
+
(``[project].name``, the ``importlib.metadata`` lookup, and the TestPyPI
|
|
137
|
+
install pin); the importable package and the console command stay
|
|
138
|
+
``repo_token``.
|
|
107
139
|
|
|
108
140
|
Returns
|
|
109
141
|
-------
|
|
@@ -111,10 +143,12 @@ def transform_plan(bare: str, desc: str) -> dict:
|
|
|
111
143
|
``rename_map`` — ``{old: new}`` substitution pairs
|
|
112
144
|
``package_dir_rename`` — ``{old_dir: new_dir}``
|
|
113
145
|
``pyproject_desc`` — the value that would be written to ``description``
|
|
146
|
+
``dist`` — the effective distribution name
|
|
114
147
|
``claude_md_seed`` — the first ~3 lines of the seed (synopsis only)
|
|
115
148
|
``steps`` — ordered list of step descriptions
|
|
116
149
|
"""
|
|
117
150
|
pkg, repo_token = _derive(bare)
|
|
151
|
+
effective_dist = _effective_dist(dist, repo_token)
|
|
118
152
|
m = rename_map(bare)
|
|
119
153
|
steps = [
|
|
120
154
|
f"global text replace in every file (skip .git/): "
|
|
@@ -124,6 +158,12 @@ def transform_plan(bare: str, desc: str) -> dict:
|
|
|
124
158
|
"replace README.md first heading + intro with new agent name and description",
|
|
125
159
|
"overwrite CLAUDE.md with a self-init seed (names agent, embeds desc, /init instruction)",
|
|
126
160
|
]
|
|
161
|
+
if effective_dist != repo_token:
|
|
162
|
+
steps.append(
|
|
163
|
+
f"retarget PyPI distribution name {repo_token!r} → {effective_dist!r} "
|
|
164
|
+
"([project].name, importlib.metadata lookup, TestPyPI install pin); "
|
|
165
|
+
f"command + import package stay {repo_token!r}"
|
|
166
|
+
)
|
|
127
167
|
return {
|
|
128
168
|
"bare": bare,
|
|
129
169
|
"pkg": pkg,
|
|
@@ -131,6 +171,7 @@ def transform_plan(bare: str, desc: str) -> dict:
|
|
|
131
171
|
"rename_map": m,
|
|
132
172
|
"package_dir_rename": {_TEMPLATE_PKG: pkg},
|
|
133
173
|
"pyproject_desc": desc,
|
|
174
|
+
"dist": effective_dist,
|
|
134
175
|
"claude_md_seed_synopsis": f"# CLAUDE.md seed for {bare} — run /init to expand",
|
|
135
176
|
"steps": steps,
|
|
136
177
|
}
|
|
@@ -258,6 +299,61 @@ def _set_pyproject_description(pyproject: Path, desc: str) -> None:
|
|
|
258
299
|
pyproject.write_text(new_text, encoding="utf-8")
|
|
259
300
|
|
|
260
301
|
|
|
302
|
+
def _rewrite_text_file(path: Path, transform: Callable[[str], str]) -> None:
|
|
303
|
+
"""Read *path*, apply *transform* to its text, and write back if it changed.
|
|
304
|
+
|
|
305
|
+
Degrades gracefully: a missing or non-UTF-8 file is left untouched. Centralises
|
|
306
|
+
the read/guard/write boilerplate so callers describe only the substitution.
|
|
307
|
+
"""
|
|
308
|
+
if not path.is_file():
|
|
309
|
+
return
|
|
310
|
+
try:
|
|
311
|
+
text = path.read_text(encoding="utf-8")
|
|
312
|
+
except (UnicodeDecodeError, OSError):
|
|
313
|
+
return
|
|
314
|
+
new_text = transform(text)
|
|
315
|
+
if new_text != text:
|
|
316
|
+
path.write_text(new_text, encoding="utf-8")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _retarget_dist(dest: Path, pkg: str, repo_token: str, dist: str) -> None:
|
|
320
|
+
"""Rename only the PyPI **distribution** from *repo_token* to *dist*.
|
|
321
|
+
|
|
322
|
+
Called after the global token replace has set every ``culture-agent-template``
|
|
323
|
+
occurrence to *repo_token*. This narrowly rewrites the three places that name
|
|
324
|
+
the *distribution* (as opposed to the importable package or the console
|
|
325
|
+
command, which stay *repo_token*):
|
|
326
|
+
|
|
327
|
+
1. ``pyproject.toml`` — the ``[project].name = "<repo_token>"`` line (matched
|
|
328
|
+
by value, so ``authors = [{name = "..."}]`` and other ``name =`` lines are
|
|
329
|
+
never touched).
|
|
330
|
+
2. ``<pkg>/__init__.py`` — the ``importlib.metadata`` version lookup argument
|
|
331
|
+
(the ``"<repo_token>"`` string literal).
|
|
332
|
+
3. ``.github/workflows/publish.yml`` — the TestPyPI install hint's version
|
|
333
|
+
pin (``<repo_token>==`` → ``<dist>==``; the ``==`` anchors it so other
|
|
334
|
+
``repo_token`` mentions are untouched).
|
|
335
|
+
|
|
336
|
+
Each step degrades gracefully if the file or pattern is absent. No-op is the
|
|
337
|
+
caller's responsibility (skip when ``dist`` equals ``repo_token``).
|
|
338
|
+
"""
|
|
339
|
+
name_line = re.compile(r'(?m)^(\s*name\s*=\s*)["\']' + re.escape(repo_token) + r'["\']')
|
|
340
|
+
|
|
341
|
+
_rewrite_text_file(
|
|
342
|
+
dest / "pyproject.toml",
|
|
343
|
+
lambda text: name_line.sub(lambda m: m.group(1) + '"' + dist + '"', text),
|
|
344
|
+
)
|
|
345
|
+
_rewrite_text_file(
|
|
346
|
+
dest / pkg / "__init__.py",
|
|
347
|
+
lambda text: text.replace(f'"{repo_token}"', f'"{dist}"').replace(
|
|
348
|
+
f"'{repo_token}'", f"'{dist}'"
|
|
349
|
+
),
|
|
350
|
+
)
|
|
351
|
+
_rewrite_text_file(
|
|
352
|
+
dest / ".github" / "workflows" / "publish.yml",
|
|
353
|
+
lambda text: text.replace(f"{repo_token}==", f"{dist}=="),
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
261
357
|
def _is_desc_stub(line: str) -> bool:
|
|
262
358
|
"""Return True if *line* looks like a short plain-text description placeholder.
|
|
263
359
|
|
|
@@ -380,6 +476,7 @@ def transform_clone(
|
|
|
380
476
|
bare: str,
|
|
381
477
|
desc: str,
|
|
382
478
|
backend: str = "claude",
|
|
479
|
+
dist: str | None = None,
|
|
383
480
|
) -> None:
|
|
384
481
|
"""Customise the cloned template tree at *dest* into the new agent *bare*.
|
|
385
482
|
|
|
@@ -392,6 +489,10 @@ def transform_clone(
|
|
|
392
489
|
3. **Set description** in ``pyproject.toml``.
|
|
393
490
|
4. **Rewrite README.md** first heading + intro.
|
|
394
491
|
5. **Overwrite CLAUDE.md** (or ``AGENTS.md`` for acp) with a self-init seed.
|
|
492
|
+
6. **Retarget the PyPI dist name** to *dist* (only when it differs from
|
|
493
|
+
``repo_token``) — ``[project].name``, the ``importlib.metadata`` lookup,
|
|
494
|
+
and the TestPyPI install pin. The importable package and the console
|
|
495
|
+
command stay ``repo_token``.
|
|
395
496
|
|
|
396
497
|
Parameters
|
|
397
498
|
----------
|
|
@@ -403,6 +504,9 @@ def transform_clone(
|
|
|
403
504
|
Short description for the new agent.
|
|
404
505
|
backend:
|
|
405
506
|
``"claude"`` (default) or ``"acp"``.
|
|
507
|
+
dist:
|
|
508
|
+
PyPI distribution name. ``None`` leaves it as ``repo_token`` (the pure
|
|
509
|
+
default); a different value retargets only the dist (step 6).
|
|
406
510
|
|
|
407
511
|
Raises
|
|
408
512
|
------
|
|
@@ -418,7 +522,10 @@ def transform_clone(
|
|
|
418
522
|
if not dest.is_dir():
|
|
419
523
|
raise FileNotFoundError(f"dest is not a directory: {dest}")
|
|
420
524
|
|
|
421
|
-
pkg,
|
|
525
|
+
pkg, repo_token = _derive(bare)
|
|
526
|
+
# Resolve the dist up front so an explicitly-empty value is rejected before
|
|
527
|
+
# any file is touched (None → repo_token; "" / whitespace → ValueError).
|
|
528
|
+
effective_dist = _effective_dist(dist, repo_token)
|
|
422
529
|
replacements = rename_map(bare) # underscore first, then hyphen
|
|
423
530
|
|
|
424
531
|
# Step 1 — global text replace (visit all files before the directory rename
|
|
@@ -442,3 +549,8 @@ def transform_clone(
|
|
|
442
549
|
prompt_file = _BACKEND_PROMPT_FILE[backend]
|
|
443
550
|
seed_content = _seed_prompt(bare, desc, backend)
|
|
444
551
|
(dest / prompt_file).write_text(seed_content, encoding="utf-8")
|
|
552
|
+
|
|
553
|
+
# Step 6 — retarget the PyPI dist name (only when it differs from repo_token,
|
|
554
|
+
# so the default is a true no-op and the bare-name case is unchanged).
|
|
555
|
+
if effective_dist != repo_token:
|
|
556
|
+
_retarget_dist(dest, pkg, repo_token, effective_dist)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SonarCloud project identity. Matches the project key registered on
|
|
2
|
+
# SonarCloud and used by the cicd skill's Sonar status queries
|
|
3
|
+
# (`agentculture_guildmaster`).
|
|
4
|
+
sonar.projectKey=agentculture_guildmaster
|
|
5
|
+
sonar.organization=agentculture
|
|
6
|
+
|
|
7
|
+
# Source layout — package lives at the top level (no src/ wrapper).
|
|
8
|
+
sonar.sources=guild
|
|
9
|
+
sonar.tests=tests
|
|
10
|
+
|
|
11
|
+
# Coverage report produced in CI by `uv run pytest --cov-report=xml:coverage.xml`
|
|
12
|
+
# (see .github/workflows/tests.yml). Path is relative to the repo root.
|
|
13
|
+
sonar.python.coverage.reportPaths=coverage.xml
|
|
14
|
+
sonar.python.version=3.12
|
|
15
|
+
|
|
16
|
+
# Don't analyze noise (vendored skills are cite-don't-import copies, not source).
|
|
17
|
+
sonar.exclusions=**/__pycache__/**,**/.venv/**,uv.lock,.claude/skills/**
|
|
18
|
+
|
|
19
|
+
# Block CI on a failing quality gate. The scanner polls SonarCloud and exits
|
|
20
|
+
# non-zero on a red gate (coverage regression, new bugs/vulnerabilities). Only
|
|
21
|
+
# takes effect when the SonarCloud Scan step runs — that step is guarded by
|
|
22
|
+
# `if: env.SONAR_TOKEN != ''` in .github/workflows/tests.yml, so token-less
|
|
23
|
+
# repos and fork PRs (no injected secret) skip the scan and stay green.
|
|
24
|
+
sonar.qualitygate.wait=true
|
|
@@ -159,6 +159,36 @@ def test_create_dry_run_custom_template(tmp_path, monkeypatch, capsys):
|
|
|
159
159
|
assert out["template"] == "myorg/my-template"
|
|
160
160
|
|
|
161
161
|
|
|
162
|
+
def test_create_dry_run_dist_flag(tmp_path, monkeypatch, capsys):
|
|
163
|
+
monkeypatch.chdir(_seed(tmp_path))
|
|
164
|
+
monkeypatch.setattr(subprocess, "run", _never_called)
|
|
165
|
+
|
|
166
|
+
rc = main(["create", "--agent", "newsib", "--desc", "D.", "--dist", "newsib-cli", "--json"])
|
|
167
|
+
assert rc == 0
|
|
168
|
+
out = json.loads(capsys.readouterr().out)
|
|
169
|
+
assert out["plan"]["dist"] == "newsib-cli"
|
|
170
|
+
assert any("retarget" in s.lower() for s in out["plan"]["steps"])
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_create_dry_run_dist_defaults_to_repo_token(tmp_path, monkeypatch, capsys):
|
|
174
|
+
monkeypatch.chdir(_seed(tmp_path))
|
|
175
|
+
monkeypatch.setattr(subprocess, "run", _never_called)
|
|
176
|
+
|
|
177
|
+
rc = main(["create", "--agent", "newsib", "--desc", "D.", "--json"])
|
|
178
|
+
assert rc == 0
|
|
179
|
+
out = json.loads(capsys.readouterr().out)
|
|
180
|
+
assert out["plan"]["dist"] == "newsib"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_create_rejects_invalid_dist_name(tmp_path, monkeypatch, capsys):
|
|
184
|
+
monkeypatch.chdir(_seed(tmp_path))
|
|
185
|
+
monkeypatch.setattr(subprocess, "run", _never_called)
|
|
186
|
+
|
|
187
|
+
rc = main(["create", "--agent", "newsib", "--desc", "D.", "--dist", "bad name!"])
|
|
188
|
+
assert rc != 0
|
|
189
|
+
assert "not a valid PyPI distribution name" in capsys.readouterr().err
|
|
190
|
+
|
|
191
|
+
|
|
162
192
|
def test_create_dry_run_bare_agent_gets_org(tmp_path, monkeypatch, capsys):
|
|
163
193
|
monkeypatch.chdir(_seed(tmp_path))
|
|
164
194
|
|
|
@@ -223,9 +253,18 @@ class _FakeRunner:
|
|
|
223
253
|
(clone_dest / "culture_agent_template").mkdir()
|
|
224
254
|
(clone_dest / "culture_agent_template" / "__init__.py").write_text(
|
|
225
255
|
"# culture_agent_template\n"
|
|
256
|
+
"from importlib.metadata import version as _pkg_version\n"
|
|
257
|
+
'__version__ = _pkg_version("culture-agent-template")\n'
|
|
226
258
|
)
|
|
227
259
|
(clone_dest / "pyproject.toml").write_text(
|
|
228
260
|
'[project]\nname = "culture-agent-template"\ndescription = "Template desc."\n'
|
|
261
|
+
"\n[project.scripts]\n"
|
|
262
|
+
'culture-agent-template = "culture_agent_template.cli:main"\n'
|
|
263
|
+
)
|
|
264
|
+
wf = clone_dest / ".github" / "workflows"
|
|
265
|
+
wf.mkdir(parents=True)
|
|
266
|
+
(wf / "publish.yml").write_text(
|
|
267
|
+
'run: echo "install culture-agent-template==${DEV_VERSION}"\n'
|
|
229
268
|
)
|
|
230
269
|
(clone_dest / "culture.yaml").write_text(
|
|
231
270
|
"agents:\n- suffix: culture-agent-template\n backend: claude\n"
|
|
@@ -640,3 +679,41 @@ def test_create_apply_transform_renames_in_clone(tmp_path, monkeypatch):
|
|
|
640
679
|
claude_md = (clone_dest / "CLAUDE.md").read_text()
|
|
641
680
|
assert "newsib" in claude_md
|
|
642
681
|
assert "/init" in claude_md
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def test_create_apply_dist_retargets_clone(tmp_path, monkeypatch):
|
|
685
|
+
"""--apply with --dist renames the dist in the clone, not the command/pkg."""
|
|
686
|
+
root = _seed(tmp_path)
|
|
687
|
+
workspace = tmp_path / "ws"
|
|
688
|
+
workspace.mkdir()
|
|
689
|
+
monkeypatch.chdir(root)
|
|
690
|
+
|
|
691
|
+
runner = _FakeRunner()
|
|
692
|
+
import guild.cli._commands._provision_template as _prov
|
|
693
|
+
|
|
694
|
+
monkeypatch.setattr(_prov, "default_runner", runner)
|
|
695
|
+
|
|
696
|
+
main(
|
|
697
|
+
[
|
|
698
|
+
"create",
|
|
699
|
+
"--agent",
|
|
700
|
+
"newsib",
|
|
701
|
+
"--desc",
|
|
702
|
+
"Dist test.",
|
|
703
|
+
"--workspace-root",
|
|
704
|
+
str(workspace),
|
|
705
|
+
"--dist",
|
|
706
|
+
"newsib-cli",
|
|
707
|
+
"--apply",
|
|
708
|
+
]
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
clone_dest = workspace / "newsib"
|
|
712
|
+
pyproject = (clone_dest / "pyproject.toml").read_text()
|
|
713
|
+
assert 'name = "newsib-cli"' in pyproject
|
|
714
|
+
# Command stays the bare repo name.
|
|
715
|
+
assert 'newsib = "newsib.cli:main"' in pyproject
|
|
716
|
+
init = (clone_dest / "newsib" / "__init__.py").read_text()
|
|
717
|
+
assert '_pkg_version("newsib-cli")' in init
|
|
718
|
+
wf = (clone_dest / ".github" / "workflows" / "publish.yml").read_text()
|
|
719
|
+
assert "newsib-cli==${DEV_VERSION}" in wf
|
|
@@ -30,12 +30,27 @@ def _build_fixture(tmp_path: Path) -> Path:
|
|
|
30
30
|
(tmp_path / ".git").mkdir()
|
|
31
31
|
(tmp_path / ".git" / "config").write_text("[core]\n\trepositoryformatversion = 0\n")
|
|
32
32
|
|
|
33
|
-
# Package directory with __init__.py that imports from the package
|
|
33
|
+
# Package directory with __init__.py that imports from the package and reads
|
|
34
|
+
# its version from the *distribution* metadata (hyphen form, like the real
|
|
35
|
+
# template's `importlib.metadata` lookup).
|
|
34
36
|
pkg_dir = tmp_path / _TEMPLATE_PKG
|
|
35
37
|
pkg_dir.mkdir()
|
|
36
38
|
(pkg_dir / "__init__.py").write_text(
|
|
37
39
|
'"""culture_agent_template package."""\n'
|
|
40
|
+
"from importlib.metadata import version as _pkg_version\n"
|
|
38
41
|
"from culture_agent_template import cli # noqa: F401\n"
|
|
42
|
+
'\n__version__ = _pkg_version("culture-agent-template")\n'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# .github/workflows/publish.yml — the TestPyPI install hint pins the dist.
|
|
46
|
+
wf_dir = tmp_path / ".github" / "workflows"
|
|
47
|
+
wf_dir.mkdir(parents=True)
|
|
48
|
+
(wf_dir / "publish.yml").write_text(
|
|
49
|
+
"name: publish\n"
|
|
50
|
+
"jobs:\n"
|
|
51
|
+
" test-publish:\n"
|
|
52
|
+
" steps:\n"
|
|
53
|
+
' - run: echo "install culture-agent-template==${DEV_VERSION}"\n'
|
|
39
54
|
)
|
|
40
55
|
|
|
41
56
|
# pyproject.toml
|
|
@@ -375,6 +390,101 @@ def test_transform_readme_consumes_wrapped_inline_code_prose(tmp_path):
|
|
|
375
390
|
assert "## More" in readme
|
|
376
391
|
|
|
377
392
|
|
|
393
|
+
# ---------------------------------------------------------------------------
|
|
394
|
+
# --dist retarget tests
|
|
395
|
+
# ---------------------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_transform_default_dist_leaves_repo_token(tmp_path):
|
|
399
|
+
"""No dist → the dist name stays the global-replace result (repo_token)."""
|
|
400
|
+
dest = _build_fixture(tmp_path)
|
|
401
|
+
transform_clone(dest, "appsec", "AppSec agent.", "claude")
|
|
402
|
+
pyproject = (dest / "pyproject.toml").read_text()
|
|
403
|
+
assert 'name = "appsec"' in pyproject
|
|
404
|
+
init = (dest / "appsec" / "__init__.py").read_text()
|
|
405
|
+
assert '_pkg_version("appsec")' in init
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def test_transform_dist_equal_repo_token_is_noop(tmp_path):
|
|
409
|
+
"""Explicitly passing dist == repo_token changes nothing (no-op guard)."""
|
|
410
|
+
dest = _build_fixture(tmp_path)
|
|
411
|
+
transform_clone(dest, "appsec", "AppSec agent.", "claude", dist="appsec")
|
|
412
|
+
pyproject = (dest / "pyproject.toml").read_text()
|
|
413
|
+
assert 'name = "appsec"' in pyproject
|
|
414
|
+
assert "appsec-cli" not in pyproject
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def test_transform_custom_dist_retargets_pyproject_name(tmp_path):
|
|
418
|
+
dest = _build_fixture(tmp_path)
|
|
419
|
+
transform_clone(dest, "appsec", "AppSec agent.", "claude", dist="appsec-cli")
|
|
420
|
+
pyproject = (dest / "pyproject.toml").read_text()
|
|
421
|
+
assert 'name = "appsec-cli"' in pyproject
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def test_transform_custom_dist_retargets_init_metadata(tmp_path):
|
|
425
|
+
dest = _build_fixture(tmp_path)
|
|
426
|
+
transform_clone(dest, "appsec", "AppSec agent.", "claude", dist="appsec-cli")
|
|
427
|
+
init = (dest / "appsec" / "__init__.py").read_text()
|
|
428
|
+
assert '_pkg_version("appsec-cli")' in init
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def test_transform_custom_dist_retargets_publish_pin(tmp_path):
|
|
432
|
+
dest = _build_fixture(tmp_path)
|
|
433
|
+
transform_clone(dest, "appsec", "AppSec agent.", "claude", dist="appsec-cli")
|
|
434
|
+
wf = (dest / ".github" / "workflows" / "publish.yml").read_text()
|
|
435
|
+
assert "appsec-cli==${DEV_VERSION}" in wf
|
|
436
|
+
assert "appsec==${DEV_VERSION}" not in wf
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_transform_custom_dist_keeps_command_and_package(tmp_path):
|
|
440
|
+
"""The dist rename must NOT touch the console command or the package dir."""
|
|
441
|
+
dest = _build_fixture(tmp_path)
|
|
442
|
+
transform_clone(dest, "appsec", "AppSec agent.", "claude", dist="appsec-cli")
|
|
443
|
+
# Package dir stays the import name.
|
|
444
|
+
assert (dest / "appsec").is_dir()
|
|
445
|
+
pyproject = (dest / "pyproject.toml").read_text()
|
|
446
|
+
# The console script (under [project.scripts]) stays the bare command.
|
|
447
|
+
assert 'appsec = "appsec.cli:main"' in pyproject
|
|
448
|
+
# packages list stays the import name.
|
|
449
|
+
assert 'packages = ["appsec"]' in pyproject
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def test_transform_custom_dist_hyphenated_repo(tmp_path):
|
|
453
|
+
"""A hyphenated repo (my-agent) with a custom dist still retargets correctly."""
|
|
454
|
+
dest = _build_fixture(tmp_path)
|
|
455
|
+
transform_clone(dest, "my-agent", "Hyphenated.", "claude", dist="my-agent-cli")
|
|
456
|
+
pyproject = (dest / "pyproject.toml").read_text()
|
|
457
|
+
assert 'name = "my-agent-cli"' in pyproject
|
|
458
|
+
# command + package (underscore) untouched.
|
|
459
|
+
assert (dest / "my_agent").is_dir()
|
|
460
|
+
assert 'my-agent = "my_agent.cli:main"' in pyproject
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def test_transform_plan_includes_dist_default(tmp_path):
|
|
464
|
+
plan = transform_plan("appsec", "AppSec agent.")
|
|
465
|
+
assert plan["dist"] == "appsec"
|
|
466
|
+
# No retarget step when dist == repo_token.
|
|
467
|
+
assert not any("retarget" in s.lower() for s in plan["steps"])
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def test_transform_plan_includes_dist_custom(tmp_path):
|
|
471
|
+
plan = transform_plan("appsec", "AppSec agent.", dist="appsec-cli")
|
|
472
|
+
assert plan["dist"] == "appsec-cli"
|
|
473
|
+
assert any("retarget" in s.lower() and "appsec-cli" in s for s in plan["steps"])
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def test_transform_clone_empty_dist_raises(tmp_path):
|
|
477
|
+
"""An explicitly-empty dist is a programming error, not "use the default"."""
|
|
478
|
+
dest = _build_fixture(tmp_path)
|
|
479
|
+
with pytest.raises(ValueError, match="dist"):
|
|
480
|
+
transform_clone(dest, "appsec", "AppSec agent.", "claude", dist="")
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def test_transform_plan_whitespace_dist_raises():
|
|
484
|
+
with pytest.raises(ValueError, match="dist"):
|
|
485
|
+
transform_plan("appsec", "AppSec agent.", dist=" ")
|
|
486
|
+
|
|
487
|
+
|
|
378
488
|
def test_transform_seed_avoids_md036_standalone_emphasis(tmp_path):
|
|
379
489
|
"""The CLAUDE.md seed must not place the agent name on a standalone
|
|
380
490
|
emphasized line (markdownlint MD036 no-emphasis-as-heading — this broke
|
|
File without changes
|
{guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/agent-config/data/backend-fingerprints.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{guild_cli-0.6.4 → guild_cli-0.7.1}/.claude/skills/communicate/scripts/templates/skill-new-brief.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|