guild-cli 0.7.3__tar.gz → 0.8.0__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.
Files changed (103) hide show
  1. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/guild/scripts/create.sh +13 -5
  2. {guild_cli-0.7.3 → guild_cli-0.8.0}/CHANGELOG.md +32 -0
  3. {guild_cli-0.7.3 → guild_cli-0.8.0}/CLAUDE.md +1 -1
  4. {guild_cli-0.7.3 → guild_cli-0.8.0}/PKG-INFO +1 -1
  5. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/cli/_commands/_provision_template.py +9 -1
  6. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/cli/_commands/create.py +90 -29
  7. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/scaffold/__init__.py +3 -3
  8. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/scaffold/instantiate.py +187 -52
  9. {guild_cli-0.7.3 → guild_cli-0.8.0}/pyproject.toml +1 -1
  10. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_cli_create.py +215 -0
  11. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_scaffold_instantiate.py +217 -0
  12. {guild_cli-0.7.3 → guild_cli-0.8.0}/uv.lock +1 -1
  13. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/agent-config/SKILL.md +0 -0
  14. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
  15. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/agent-config/scripts/show.sh +0 -0
  16. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
  17. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
  18. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/cicd/SKILL.md +0 -0
  19. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
  20. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
  21. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
  22. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
  23. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
  24. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/communicate/SKILL.md +0 -0
  25. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  26. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  27. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  28. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  29. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
  30. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
  31. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
  32. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
  33. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/guild/SKILL.md +0 -0
  34. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/guild/scripts/configure-repo.sh +0 -0
  35. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/guild/scripts/overview.sh +0 -0
  36. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/onboard/SKILL.md +0 -0
  37. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/onboard/scripts/onboard.sh +0 -0
  38. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
  39. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
  40. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/run-tests/SKILL.md +0 -0
  41. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
  42. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
  43. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
  44. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
  45. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
  46. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/teach/SKILL.md +0 -0
  47. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/teach/scripts/teach.sh +0 -0
  48. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/think/SKILL.md +0 -0
  49. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/think/scripts/think.sh +0 -0
  50. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/version-bump/SKILL.md +0 -0
  51. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
  52. {guild_cli-0.7.3 → guild_cli-0.8.0}/.claude/skills.local.yaml.example +0 -0
  53. {guild_cli-0.7.3 → guild_cli-0.8.0}/.devague/frames/guildmaster-ships-teach-and-onboard-two-agent-firs.json +0 -0
  54. {guild_cli-0.7.3 → guild_cli-0.8.0}/.devague/plans/guildmaster-ships-teach-and-onboard-two-agent-firs.json +0 -0
  55. {guild_cli-0.7.3 → guild_cli-0.8.0}/.flake8 +0 -0
  56. {guild_cli-0.7.3 → guild_cli-0.8.0}/.github/workflows/publish.yml +0 -0
  57. {guild_cli-0.7.3 → guild_cli-0.8.0}/.github/workflows/tests.yml +0 -0
  58. {guild_cli-0.7.3 → guild_cli-0.8.0}/.gitignore +0 -0
  59. {guild_cli-0.7.3 → guild_cli-0.8.0}/.markdownlint-cli2.yaml +0 -0
  60. {guild_cli-0.7.3 → guild_cli-0.8.0}/LICENSE +0 -0
  61. {guild_cli-0.7.3 → guild_cli-0.8.0}/README.md +0 -0
  62. {guild_cli-0.7.3 → guild_cli-0.8.0}/culture.yaml +0 -0
  63. {guild_cli-0.7.3 → guild_cli-0.8.0}/docs/cutover.md +0 -0
  64. {guild_cli-0.7.3 → guild_cli-0.8.0}/docs/plans/2026-05-24-guildmaster-ships-teach-and-onboard-two-agent-firs.md +0 -0
  65. {guild_cli-0.7.3 → guild_cli-0.8.0}/docs/skill-sources.md +0 -0
  66. {guild_cli-0.7.3 → guild_cli-0.8.0}/docs/specs/2026-05-24-guildmaster-ships-teach-and-onboard-two-agent-firs.md +0 -0
  67. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/__init__.py +0 -0
  68. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/__main__.py +0 -0
  69. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/cli/__init__.py +0 -0
  70. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/cli/_commands/__init__.py +0 -0
  71. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/cli/_commands/_broadcast.py +0 -0
  72. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/cli/_commands/explain.py +0 -0
  73. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/cli/_commands/learn.py +0 -0
  74. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/cli/_commands/onboard.py +0 -0
  75. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/cli/_commands/overview.py +0 -0
  76. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/cli/_commands/show.py +0 -0
  77. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/cli/_commands/teach.py +0 -0
  78. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/cli/_commands/whoami.py +0 -0
  79. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/cli/_errors.py +0 -0
  80. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/cli/_output.py +0 -0
  81. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/cli/_repo.py +0 -0
  82. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/skills/__init__.py +0 -0
  83. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/skills/identity.py +0 -0
  84. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/skills/ledger.py +0 -0
  85. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/skills/render.py +0 -0
  86. {guild_cli-0.7.3 → guild_cli-0.8.0}/guild/skills/sources.py +0 -0
  87. {guild_cli-0.7.3 → guild_cli-0.8.0}/sonar-project.properties +0 -0
  88. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/__init__.py +0 -0
  89. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_broadcast_post.py +0 -0
  90. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_cli.py +0 -0
  91. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_cli_explain.py +0 -0
  92. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_cli_learn.py +0 -0
  93. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_cli_onboard.py +0 -0
  94. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_cli_overview.py +0 -0
  95. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_cli_show.py +0 -0
  96. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_cli_teach.py +0 -0
  97. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_cli_whoami.py +0 -0
  98. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_skills_convention.py +0 -0
  99. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_skills_identity.py +0 -0
  100. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_skills_ledger.py +0 -0
  101. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_skills_render.py +0 -0
  102. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_skills_sources.py +0 -0
  103. {guild_cli-0.7.3 → guild_cli-0.8.0}/tests/test_version_fallback.py +0 -0
@@ -12,12 +12,20 @@ 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] [--dist NAME] [--apply] [--json]
15
+ # [--org agentculture] [--command NAME] [--pkg NAME]
16
+ # [--dist NAME] [--apply] [--json]
16
17
  #
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.
18
+ # The repo name is the repo/agent identity (README, culture.yaml suffix,
19
+ # CLAUDE.md seed, repo URL). --command / --pkg / --dist each retarget one facet
20
+ # independently and default to the repo name (or, for --pkg, the underscore form
21
+ # of --command), so with no overrides the result is identical to before:
22
+ # --command NAME console-command name (the [project.scripts] key only); e.g.
23
+ # --command reachy for a repo reachy-mini-cli.
24
+ # --pkg NAME importable package; defaults to the underscore form of
25
+ # --command (pass only to decouple command from package).
26
+ # --dist NAME PyPI distribution name; e.g. --dist jetson-cli. Retargets
27
+ # [project].name, the importlib.metadata lookup, and the
28
+ # TestPyPI install pin.
21
29
  #
22
30
  # Exit codes:
23
31
  # 0 success (delegates to `guild create`; its exit code propagates)
@@ -5,6 +5,38 @@ 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.8.0] - 2026-05-30
9
+
10
+ ### Added
11
+
12
+ - **`guild create`: repo≠command split via `--command` / `--pkg`**
13
+ ([#43](https://github.com/agentculture/guildmaster/issues/43)). A new sibling
14
+ can now ship its CLI under a shorter name than the repo — the guildmaster
15
+ pattern (repo `guildmaster` → command/package `guild` → dist `guild-cli`) —
16
+ without a manual post-create edit.
17
+ - `--command NAME` sets the console-command (binary) name (default: the repo
18
+ token) and retargets **only** the `[project.scripts]` entry-point key.
19
+ - `--pkg NAME` sets the importable Python package (default: the underscore
20
+ form of `--command`, so command + import package stay in lock-step); pass it
21
+ only to decouple the two.
22
+ - The **repo token stays the repo/agent identity** (README heading,
23
+ `culture.yaml` suffix, CLAUDE.md seed, repo URL). `--command` / `--pkg` /
24
+ `--dist` are each independent; with no overrides the output is byte-identical
25
+ to before.
26
+ - Validation is fail-fast (before any external act): `--command` must be a PEP
27
+ 503-ish name and the *effective* import package must be a valid, non-keyword
28
+ Python identifier (the error attributes the bad value to `--pkg`, else
29
+ `--command`, else the repo name).
30
+ - A `--dist`/`--command` rename still does **not** register a PyPI/TestPyPI
31
+ Trusted Publisher — `guild create` configures only the GitHub side.
32
+ - Pure transform: new `_resolve_identifiers` (single source of truth shared by
33
+ `transform_plan` / `transform_clone`) and `_retarget_command` (narrow
34
+ scripts-key rewrite, mirroring `_retarget_dist`).
35
+
36
+ ### Changed
37
+
38
+ ### Fixed
39
+
8
40
  ## [0.7.3] - 2026-05-29
9
41
 
10
42
  ### 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] [--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).
111
+ - `guild create --agent OWNER/REPO --desc TEXT [--backend claude|acp] [--workspace-root DIR] [--template agentculture/culture-agent-template] [--command NAME] [--pkg NAME] [--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`). **The repo token is the repo/agent identity only** — it always drives the README heading, `culture.yaml` suffix, CLAUDE.md seed, and repo URL. The **command**, **import package**, and **dist** are each independently retargetable and default to the repo token (or, for `--pkg`, the underscore form of `--command`); with no overrides the output is byte-identical to a plain rename. `--command NAME` sets the **console-command (binary) name** (default: the repo token) and retargets **only** the `[project.scripts]` entry-point key — e.g. `--command reachy` for repo `reachy-mini-cli` ships the command as `reachy`. `--pkg NAME` sets the **importable Python package** (default: the underscore form of `--command`, so command and import package stay in lock-step like `guild`/`guild`); pass it only to decouple the two. `--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 or command does **not** register a Trusted Publisher — that needs a matching Trusted Publisher registered for the 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: `_resolve_identifiers` + `_retarget_command` / `_retarget_dist`), `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.7.3
3
+ Version: 0.8.0
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
@@ -242,6 +242,8 @@ def apply(
242
242
  runner: Runner | None = None,
243
243
  template: str = DEFAULT_TEMPLATE,
244
244
  dist: str | None = None,
245
+ command: str | None = None,
246
+ pkg: str | None = None,
245
247
  poll_attempts: int = 30,
246
248
  poll_delay: float = 2.0,
247
249
  ) -> dict:
@@ -269,6 +271,12 @@ def apply(
269
271
  dist:
270
272
  PyPI distribution name to set in the clone. ``None`` leaves it as the
271
273
  repo name; a different value retargets the dist (see ``transform_clone``).
274
+ command:
275
+ Console-command name to set in the clone. ``None`` leaves it as the repo
276
+ name; a different value retargets the ``[project.scripts]`` key.
277
+ pkg:
278
+ Importable package name. ``None`` defaults to the underscore form of the
279
+ effective command (see ``transform_clone``).
272
280
  poll_attempts / poll_delay:
273
281
  ``gh repo create --template`` copies the template content
274
282
  **asynchronously**; cloning immediately yields an empty repo. After
@@ -319,7 +327,7 @@ def apply(
319
327
  _check(clone, f"git clone {agent} failed", EXIT_ENV_ERROR)
320
328
 
321
329
  # Step 4 — pure transform (no runner needed).
322
- transform_clone(clone_dest, bare, desc, backend, dist=dist)
330
+ transform_clone(clone_dest, bare, desc, backend, dist=dist, command=command, pkg=pkg)
323
331
 
324
332
  # Step 5 — stage everything.
325
333
  add = runner(["git", "-C", str(clone_dest), "add", "-A"])
@@ -15,7 +15,8 @@ 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
- [--dist NAME] [--apply] [--json]
18
+ [--command NAME] [--pkg NAME] [--dist NAME]
19
+ [--apply] [--json]
19
20
  """
20
21
 
21
22
  from __future__ import annotations
@@ -35,6 +36,11 @@ from guild.cli._repo import repo_root
35
36
  from guild.scaffold.instantiate import transform_plan as _transform_plan
36
37
  from guild.skills import ledger as _ledger
37
38
 
39
+ # A PEP 503-ish name: a non-empty run of letters/digits/.-_ that starts and ends
40
+ # alphanumerically. PyPI normalises [-_.] runs, but the raw value must still look
41
+ # like a name. Shared by the --dist and --command validators.
42
+ _PEP503 = re.compile(r"^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$")
43
+
38
44
 
39
45
  def register(sub: argparse._SubParsersAction) -> None:
40
46
  parser = sub.add_parser(
@@ -87,6 +93,30 @@ def register(sub: argparse._SubParsersAction) -> None:
87
93
  default=_provision.DEFAULT_TEMPLATE,
88
94
  help=f"GitHub template repo to instantiate (default: {_provision.DEFAULT_TEMPLATE}).",
89
95
  )
96
+ parser.add_argument(
97
+ "--command",
98
+ # Explicit dest: the bare "command" dest collides with the subparsers'
99
+ # ``dest="command"`` (which holds the chosen subcommand name).
100
+ dest="command_name",
101
+ metavar="NAME",
102
+ default=None,
103
+ help=(
104
+ "Console-command (binary) name. Defaults to the repo name; pass e.g. "
105
+ "'reachy' for a repo 'reachy-mini-cli' to ship the command as 'reachy'. "
106
+ "Retargets only the [project.scripts] entry-point key. The import "
107
+ "package follows it (underscore form) unless --pkg overrides."
108
+ ),
109
+ )
110
+ parser.add_argument(
111
+ "--pkg",
112
+ metavar="NAME",
113
+ default=None,
114
+ help=(
115
+ "Importable Python package name. Defaults to the underscore form of "
116
+ "--command, so command and import package stay in lock-step (like "
117
+ "guild/guild). Pass it only to decouple the two."
118
+ ),
119
+ )
90
120
  parser.add_argument(
91
121
  "--dist",
92
122
  metavar="NAME",
@@ -115,50 +145,77 @@ def register(sub: argparse._SubParsersAction) -> None:
115
145
 
116
146
 
117
147
  # ---------------------------------------------------------------------------
118
- # Handler
148
+ # Validation (fail-fast, before any external act)
119
149
  # ---------------------------------------------------------------------------
120
150
 
121
151
 
122
- def _handle(args: argparse.Namespace) -> None:
123
- root = repo_root()
124
- agent = _broadcast.normalize_target(args.agent, args.org)
125
- bare = agent.rsplit("/", 1)[-1]
126
-
127
- # Fail fast (before any external act) if the repo name can't derive a valid
128
- # Python package identifier — the derived pkg is both a directory name and a
129
- # global rename token, so an invalid value (dots, leading digit) breaks the
130
- # generated sibling.
131
- pkg = bare.lower().replace("-", "_")
132
- if not pkg.isidentifier() or keyword.iskeyword(pkg):
152
+ def _validate_pep503(value: str | None, flag: str, what: str, example: str) -> None:
153
+ """Reject *value* (when given) if it is not a PEP 503-ish name."""
154
+ if value is not None and not _PEP503.match(value):
133
155
  raise GuildError(
134
156
  code=EXIT_USER_ERROR,
135
- message=f"repo name {bare!r} derives an invalid Python package name {pkg!r}",
157
+ message=f"{flag} {value!r} is not a valid {what}",
136
158
  remediation=(
137
- "use a repo name of letters/digits/hyphens that maps to a valid, "
138
- "non-keyword Python identifier (e.g. not starting with a digit)"
159
+ "use letters/digits/.-_ starting and ending alphanumerically " f"(e.g. {example!r})"
139
160
  ),
140
161
  )
141
162
 
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
- ):
163
+
164
+ def _effective_pkg_for_validation(args: argparse.Namespace, repo_token: str) -> str:
165
+ """Mirror the resolver: the import package the transform will actually use."""
166
+ effective_command = args.command_name if args.command_name is not None else repo_token
167
+ return args.pkg if args.pkg is not None else effective_command.replace("-", "_")
168
+
169
+
170
+ def _validate_identifiers(args: argparse.Namespace, bare: str, repo_token: str) -> None:
171
+ """Fail-fast validation of --command / --pkg / --dist before any external act.
172
+
173
+ The *effective* import package is both a directory name and a global rename
174
+ token, so an invalid value (dots, leading digit, keyword) breaks the
175
+ generated sibling. Attribute a bad value to --pkg, else --command, else the
176
+ repo name.
177
+ """
178
+ _validate_pep503(args.command_name, "--command", "command name", "reachy")
179
+
180
+ effective_pkg = _effective_pkg_for_validation(args, repo_token)
181
+ if not effective_pkg.isidentifier() or keyword.iskeyword(effective_pkg):
182
+ if args.pkg is not None:
183
+ source = f"--pkg {args.pkg!r}"
184
+ elif args.command_name is not None:
185
+ source = f"--command {args.command_name!r}"
186
+ else:
187
+ source = f"repo name {bare!r}"
148
188
  raise GuildError(
149
189
  code=EXIT_USER_ERROR,
150
- message=f"--dist {args.dist!r} is not a valid PyPI distribution name",
190
+ message=f"{source} derives an invalid Python package name {effective_pkg!r}",
151
191
  remediation=(
152
- "use letters/digits/.-_ starting and ending alphanumerically (e.g. 'jetson-cli')"
192
+ "the import package must be a valid, non-keyword Python identifier "
193
+ "(letters/digits/underscore, not starting with a digit); pass --pkg to set it"
153
194
  ),
154
195
  )
155
196
 
197
+ _validate_pep503(args.dist, "--dist", "PyPI distribution name", "jetson-cli")
198
+
199
+
200
+ # ---------------------------------------------------------------------------
201
+ # Handler
202
+ # ---------------------------------------------------------------------------
203
+
204
+
205
+ def _handle(args: argparse.Namespace) -> None:
206
+ root = repo_root()
207
+ agent = _broadcast.normalize_target(args.agent, args.org)
208
+ bare = agent.rsplit("/", 1)[-1]
209
+ repo_token = bare.lower()
210
+
211
+ _validate_identifiers(args, bare, repo_token)
212
+
156
213
  # Resolve workspace root (parent of guildmaster if not supplied).
157
214
  workspace_root = args.workspace_root if args.workspace_root is not None else root.parent
158
215
  clone_dest = workspace_root / bare
159
216
 
160
217
  # Build the dry-run plan (pure — no writes, no subprocess).
161
- plan = _transform_plan(bare, args.desc, dist=args.dist)
218
+ plan = _transform_plan(bare, args.desc, dist=args.dist, command=args.command_name, pkg=args.pkg)
162
219
 
163
220
  # Ledger diff (pure — reads only).
164
221
  skills = _broadcast.canonical_skills(root)
@@ -198,6 +255,8 @@ def _handle(args: argparse.Namespace) -> None:
198
255
  guildmaster_root=root,
199
256
  template=args.template,
200
257
  dist=args.dist,
258
+ command=args.command_name,
259
+ pkg=args.pkg,
201
260
  )
202
261
 
203
262
  # Register in the ledger idempotently.
@@ -213,6 +272,8 @@ def _handle(args: argparse.Namespace) -> None:
213
272
  "repo": apply_result["repo"],
214
273
  "clone_dest": apply_result["clone_dest"],
215
274
  "pushed": apply_result["pushed"],
275
+ "command": plan["command"],
276
+ "pkg": plan["pkg"],
216
277
  "dist": plan["dist"],
217
278
  "ledger_written": ledger_written,
218
279
  }
@@ -233,10 +294,10 @@ def _render_dry_run(result: dict) -> str:
233
294
  f" agent : {result['agent']}",
234
295
  f" backend : {result['backend']}",
235
296
  f" clone dest : {result['clone_dest']}",
236
- f" pkg : {plan['pkg']}",
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)"),
297
+ f" repo token : {plan['repo_token']} (identity: README, culture.yaml, seed, URL)",
298
+ f" command : {plan['command']}",
299
+ f" pkg import : {plan['pkg']}",
300
+ f" dist (PyPI): {plan['dist']}",
240
301
  "",
241
302
  "── rename map ──",
242
303
  ]
@@ -8,7 +8,7 @@ new sibling agent.
8
8
  Public surface
9
9
  --------------
10
10
  ``instantiate`` — the transform module:
11
- transform_clone(dest, pkg, repo_token, desc, backend) -> None
12
- rename_map(bare) -> dict[str, str]
13
- transform_plan(bare, desc) -> dict
11
+ transform_clone(dest, bare, desc, backend="claude", dist=None, command=None, pkg=None) -> None
12
+ rename_map(bare, pkg=None) -> dict[str, str]
13
+ transform_plan(bare, desc, dist=None, command=None, pkg=None) -> dict
14
14
  """