guild-cli 0.26.22__tar.gz → 0.27.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.
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/version-bump/SKILL.md +22 -3
- guild_cli-0.27.0/.claude/skills/version-bump/scripts/bump.py +365 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/CHANGELOG.md +25 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/PKG-INFO +1 -1
- {guild_cli-0.26.22 → guild_cli-0.27.0}/pyproject.toml +1 -1
- guild_cli-0.27.0/tests/test_version_bump_script.py +346 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/uv.lock +1 -1
- guild_cli-0.26.22/.claude/skills/version-bump/scripts/bump.py +0 -178
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/agent-config/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/agent-config/scripts/show.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/ask-colleague/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/ask-colleague/prompts/explore.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/ask-colleague/prompts/review.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/ask-colleague/prompts/write.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/ask-colleague/scripts/ask-colleague.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/challenge/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/challenge/scripts/challenge.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/cicd/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/communicate/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/deviate/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/deviate/scripts/deviate.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/guild/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/guild/scripts/configure-repo.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/guild/scripts/create.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/guild/scripts/overview.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/onboard/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/onboard/scripts/onboard.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/outsource/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/outsource/prompts/explore.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/outsource/prompts/review.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/outsource/prompts/write.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/outsource/scripts/outsource.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/recall/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/recall/scripts/recall.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/remember/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/remember/scripts/remember.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/run-tests/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/scope/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/scope/scripts/scope.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/summarize-delivery/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/summarize-delivery/scripts/summarize-delivery.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/teach/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/teach/scripts/teach.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/think/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/think/scripts/think.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/validate-delivery/SKILL.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills/validate-delivery/scripts/validate-delivery.sh +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.claude/skills.local.yaml.example +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.devague/current +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.devague/current_plan +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.devague/deliveries/template-supports-four-harnesses.json +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.devague/frames/guildmaster-ships-teach-and-onboard-two-agent-firs.json +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.devague/frames/template-supports-four-harnesses.json +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.devague/plans/guildmaster-ships-teach-and-onboard-two-agent-firs.json +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.devague/plans/template-supports-four-harnesses.json +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.eidetic/memory/culture-agent-template__public.jsonl +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.eidetic/memory/default__public.jsonl +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.eidetic/memory/guildmaster__public.jsonl +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.flake8 +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.github/workflows/publish.yml +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.github/workflows/tests.yml +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.gitignore +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/.markdownlint-cli2.yaml +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/AGENTS.override.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/CLAUDE.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/LICENSE +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/QWEN.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/README.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/culture.yaml +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/docs/cutover.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/docs/deliveries/2026-09-05-template-supports-four-harnesses.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/docs/onboarding/reachy-mini-mcp.json +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/docs/plans/2026-05-24-guildmaster-ships-teach-and-onboard-two-agent-firs.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/docs/plans/2026-09-05-template-supports-four-harnesses-split.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/docs/plans/2026-09-05-template-supports-four-harnesses.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/docs/skill-sources.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/docs/specs/2026-05-24-guildmaster-ships-teach-and-onboard-two-agent-firs.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/docs/specs/2026-09-05-template-supports-four-harnesses.md +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/__init__.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/__main__.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/cli/__init__.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/cli/_commands/__init__.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/cli/_commands/_broadcast.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/cli/_commands/_provision_template.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/cli/_commands/create.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/cli/_commands/explain.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/cli/_commands/harness.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/cli/_commands/learn.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/cli/_commands/onboard.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/cli/_commands/overview.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/cli/_commands/show.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/cli/_commands/teach.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/cli/_commands/whoami.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/cli/_errors.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/cli/_output.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/cli/_repo.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/scaffold/__init__.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/scaffold/harness.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/scaffold/harness_use.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/scaffold/instantiate.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/skills/__init__.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/skills/identity.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/skills/ledger.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/skills/render.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/guild/skills/sources.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/sonar-project.properties +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/__init__.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_broadcast_post.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_cli.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_cli_create.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_cli_explain.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_cli_harness.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_cli_learn.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_cli_onboard.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_cli_overview.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_cli_show.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_cli_teach.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_cli_whoami.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_configure_repo_sonar.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_create_gate.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_scaffold_harness.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_scaffold_harness_use.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_scaffold_instantiate.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_skills_convention.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_skills_identity.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_skills_ledger.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_skills_render.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_skills_sources.py +0 -0
- {guild_cli-0.26.22 → guild_cli-0.27.0}/tests/test_version_fallback.py +0 -0
|
@@ -24,8 +24,10 @@ Run from the repo root.
|
|
|
24
24
|
echo '{"added":["New X"],"changed":["Refactored Y"],"fixed":["Bug in Z"]}' \
|
|
25
25
|
| python3 .claude/skills/version-bump/scripts/bump.py minor
|
|
26
26
|
|
|
27
|
-
# Without changelog content (inserts empty ### Added/Changed/Fixed stubs)
|
|
28
|
-
|
|
27
|
+
# Without changelog content (inserts empty ### Added/Changed/Fixed stubs).
|
|
28
|
+
# From scripts, daemons and agent harnesses, pass --no-stdin so the bump
|
|
29
|
+
# never waits on a stdin it was handed but nobody writes to:
|
|
30
|
+
python3 .claude/skills/version-bump/scripts/bump.py patch --no-stdin
|
|
29
31
|
|
|
30
32
|
# Check current version without bumping:
|
|
31
33
|
python3 .claude/skills/version-bump/scripts/bump.py show
|
|
@@ -43,6 +45,17 @@ python3 .claude/skills/version-bump/scripts/bump.py show
|
|
|
43
45
|
## Changelog JSON Format
|
|
44
46
|
|
|
45
47
|
Pass via stdin. All fields are optional — only non-empty sections are rendered.
|
|
48
|
+
The only keys are `added`, `changed` and `fixed`, each a list of strings.
|
|
49
|
+
|
|
50
|
+
Stdin is read until it closes. Any pause longer than `BUMP_STDIN_IDLE_SECONDS`
|
|
51
|
+
(default 5), **including before the first byte**, stops the bump with an
|
|
52
|
+
error; raise it for a slow producer (an LLM call, a cold `git log`).
|
|
53
|
+
`--no-stdin` (or `BUMP_NO_STDIN=1`) skips stdin and writes the empty stub.
|
|
54
|
+
|
|
55
|
+
Invalid JSON, an unknown key (e.g. `removed`), a value that isn't a list of
|
|
56
|
+
strings, or bytes that don't decode are errors too, never a silent empty
|
|
57
|
+
stub. Every check runs before any file is written, so a failed bump changes
|
|
58
|
+
nothing; fix the input and run it again.
|
|
46
59
|
|
|
47
60
|
```json
|
|
48
61
|
{
|
|
@@ -57,7 +70,13 @@ Pass via stdin. All fields are optional — only non-empty sections are rendered
|
|
|
57
70
|
- `pyproject.toml` — the `version = "x.y.z"` field (single source of truth;
|
|
58
71
|
`steward/__init__.py` reads it via `importlib.metadata`, so there's no
|
|
59
72
|
separate `__version__` literal to keep in sync).
|
|
60
|
-
- `CHANGELOG.md` — inserts a new `## [x.y.z] - YYYY-MM-DD` entry
|
|
73
|
+
- `CHANGELOG.md` — inserts a new `## [x.y.z] - YYYY-MM-DD` entry above the
|
|
74
|
+
first `## [` release heading (headings inside fenced code blocks don't
|
|
75
|
+
count). A leading `## [Unreleased]` section stays on top, untouched; its
|
|
76
|
+
content is never moved into the release for you. A CHANGELOG with no `##`
|
|
77
|
+
heading yet gets the entry at the end, or after an Unreleased-only section.
|
|
78
|
+
One whose `##` headings aren't `## [...]` release headings (e.g. `## 1.2.3`)
|
|
79
|
+
stops the bump with an error.
|
|
61
80
|
|
|
62
81
|
The script does the rest. Pick a bump type from the diff (patch for fixes,
|
|
63
82
|
minor for new features, major for breaking changes), summarize the diff into
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Bump the version in pyproject.toml and CHANGELOG.md.
|
|
3
|
+
|
|
4
|
+
If the package's ``__init__.py`` contains a literal ``__version__ = "..."``
|
|
5
|
+
assignment, the script also rewrites that value. In repos that read
|
|
6
|
+
``__version__`` from package metadata (the steward-cli convention), the
|
|
7
|
+
``__init__.py`` step is a no-op.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
bump.py major # 0.1.0 -> 1.0.0
|
|
11
|
+
bump.py minor # 0.1.0 -> 0.2.0
|
|
12
|
+
bump.py patch # 0.1.0 -> 0.1.1
|
|
13
|
+
bump.py show # print current version
|
|
14
|
+
bump.py patch --no-stdin # don't read stdin; insert the empty stub
|
|
15
|
+
|
|
16
|
+
Changelog entries are passed via stdin as a JSON object:
|
|
17
|
+
{
|
|
18
|
+
"added": ["New CLI command", "Observer module"],
|
|
19
|
+
"changed": ["Restructured namespace"],
|
|
20
|
+
"fixed": ["WHO reply index bug"]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
If stdin is a terminal or empty, or --no-stdin (or BUMP_NO_STDIN=1) is given,
|
|
24
|
+
an empty stub is inserted. Otherwise stdin is read until it closes. Any pause
|
|
25
|
+
longer than BUMP_STDIN_IDLE_SECONDS (default 5), including before the first
|
|
26
|
+
byte, stops the bump with an error; raise it for a slow producer. Stdin that
|
|
27
|
+
isn't valid JSON in the shape above (an object whose keys are added / changed /
|
|
28
|
+
fixed, each a list of strings) is an error too. Every check runs before any
|
|
29
|
+
file is written, so a failed bump changes nothing.
|
|
30
|
+
|
|
31
|
+
The new section goes above the first ``## [`` release heading, ignoring
|
|
32
|
+
headings inside fenced code blocks. A leading ``## [Unreleased]`` section stays
|
|
33
|
+
on top, untouched. A CHANGELOG with no ``##`` heading yet gets the section at
|
|
34
|
+
the end, or after an Unreleased-only section; one whose ``##`` headings aren't
|
|
35
|
+
``## [...]`` release headings (e.g. ``## 1.2.3``) is an error.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import argparse
|
|
39
|
+
import json
|
|
40
|
+
import math
|
|
41
|
+
import os
|
|
42
|
+
import queue
|
|
43
|
+
import re
|
|
44
|
+
import sys
|
|
45
|
+
import threading
|
|
46
|
+
from datetime import date
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def find_pyproject() -> Path:
|
|
51
|
+
"""Walk up from cwd to find pyproject.toml."""
|
|
52
|
+
current = Path.cwd()
|
|
53
|
+
while current != current.parent:
|
|
54
|
+
candidate = current / "pyproject.toml"
|
|
55
|
+
if candidate.exists():
|
|
56
|
+
return candidate
|
|
57
|
+
current = current.parent
|
|
58
|
+
print("ERROR: pyproject.toml not found", file=sys.stderr)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def read_version(path: Path) -> str:
|
|
63
|
+
"""Extract version string from pyproject.toml."""
|
|
64
|
+
text = path.read_text()
|
|
65
|
+
match = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
|
|
66
|
+
if not match:
|
|
67
|
+
print("ERROR: version field not found in pyproject.toml", file=sys.stderr)
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
return match.group(1)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def bump(version: str, part: str) -> str:
|
|
73
|
+
"""Bump the specified part of a semver version."""
|
|
74
|
+
parts = version.split(".")
|
|
75
|
+
if len(parts) != 3:
|
|
76
|
+
print(f"ERROR: version '{version}' is not semver (x.y.z)", file=sys.stderr)
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
|
|
79
|
+
major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
|
|
80
|
+
|
|
81
|
+
if part == "major":
|
|
82
|
+
return f"{major + 1}.0.0"
|
|
83
|
+
elif part == "minor":
|
|
84
|
+
return f"{major}.{minor + 1}.0"
|
|
85
|
+
elif part == "patch":
|
|
86
|
+
return f"{major}.{minor}.{patch + 1}"
|
|
87
|
+
else:
|
|
88
|
+
print(f"ERROR: unknown bump type '{part}' (use major, minor, or patch)", file=sys.stderr)
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def write_version(path: Path, old: str, new: str) -> None:
|
|
93
|
+
"""Replace old version with new in pyproject.toml."""
|
|
94
|
+
text = path.read_text()
|
|
95
|
+
updated = text.replace(f'version = "{old}"', f'version = "{new}"', 1)
|
|
96
|
+
path.write_text(updated)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
STDIN_IDLE_SECONDS_DEFAULT = 5.0
|
|
100
|
+
SECTIONS = ("added", "changed", "fixed")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _env_flag(name: str) -> bool:
|
|
104
|
+
return os.environ.get(name, "").strip().lower() in ("1", "true", "yes")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _stdin_idle_seconds() -> float:
|
|
108
|
+
raw = os.environ.get("BUMP_STDIN_IDLE_SECONDS")
|
|
109
|
+
if raw is None:
|
|
110
|
+
return STDIN_IDLE_SECONDS_DEFAULT
|
|
111
|
+
try:
|
|
112
|
+
value = float(raw)
|
|
113
|
+
except ValueError:
|
|
114
|
+
value = 0.0
|
|
115
|
+
if not math.isfinite(value) or value <= 0:
|
|
116
|
+
print(
|
|
117
|
+
f"ERROR: BUMP_STDIN_IDLE_SECONDS must be a positive number, got {raw!r}",
|
|
118
|
+
file=sys.stderr,
|
|
119
|
+
)
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
return value
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _fail(message: str) -> None:
|
|
125
|
+
print(f"ERROR: {message}; nothing was changed.", file=sys.stderr)
|
|
126
|
+
sys.exit(1)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _stdin_encoding() -> str:
|
|
130
|
+
return getattr(sys.stdin, "encoding", None) or "utf-8"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _pump_stdin(out: queue.Queue) -> None:
|
|
134
|
+
"""Thread body: put stdin's chunks (b"" at EOF) or the read error on ``out``.
|
|
135
|
+
|
|
136
|
+
Reads the raw fd with os.read, not the buffered sys.stdin, so a thread
|
|
137
|
+
still blocked in a read at exit can't hold the stdin buffer's lock during
|
|
138
|
+
interpreter shutdown.
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
fd = sys.stdin.fileno()
|
|
142
|
+
except (OSError, ValueError): # a text-only stdin, e.g. io.StringIO
|
|
143
|
+
encoding = _stdin_encoding()
|
|
144
|
+
|
|
145
|
+
def read() -> bytes:
|
|
146
|
+
return sys.stdin.read(65536).encode(encoding)
|
|
147
|
+
|
|
148
|
+
else:
|
|
149
|
+
|
|
150
|
+
def read() -> bytes:
|
|
151
|
+
return os.read(fd, 65536)
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
while True:
|
|
155
|
+
chunk = read()
|
|
156
|
+
out.put(chunk)
|
|
157
|
+
if not chunk:
|
|
158
|
+
return
|
|
159
|
+
except (OSError, ValueError) as exc:
|
|
160
|
+
out.put(exc)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _read_stdin_until_eof(idle: float) -> bytes | None:
|
|
164
|
+
"""Read stdin to EOF; None if no new input arrives within ``idle`` s."""
|
|
165
|
+
chunks: queue.Queue = queue.Queue()
|
|
166
|
+
threading.Thread(target=_pump_stdin, args=(chunks,), daemon=True).start()
|
|
167
|
+
data = []
|
|
168
|
+
while True:
|
|
169
|
+
try:
|
|
170
|
+
chunk = chunks.get(timeout=idle)
|
|
171
|
+
except queue.Empty:
|
|
172
|
+
return None
|
|
173
|
+
if isinstance(chunk, Exception):
|
|
174
|
+
_fail(f"could not read stdin ({chunk})")
|
|
175
|
+
if not chunk:
|
|
176
|
+
return b"".join(data)
|
|
177
|
+
data.append(chunk)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _parse_entries(raw: bytes) -> dict:
|
|
181
|
+
"""Decode and validate changelog JSON; an error rather than a dropped entry."""
|
|
182
|
+
try:
|
|
183
|
+
text = raw.decode(_stdin_encoding()).strip()
|
|
184
|
+
except (UnicodeDecodeError, LookupError) as exc:
|
|
185
|
+
_fail(f"could not decode stdin as {_stdin_encoding()} ({exc})")
|
|
186
|
+
if not text:
|
|
187
|
+
return {}
|
|
188
|
+
try:
|
|
189
|
+
entries = json.loads(text)
|
|
190
|
+
except json.JSONDecodeError as exc:
|
|
191
|
+
_fail(f"stdin is not valid changelog JSON ({exc})")
|
|
192
|
+
if not isinstance(entries, dict):
|
|
193
|
+
_fail('changelog JSON must be an object like {"fixed": ["..."]}')
|
|
194
|
+
unknown = sorted(set(entries) - set(SECTIONS))
|
|
195
|
+
if unknown:
|
|
196
|
+
_fail(
|
|
197
|
+
f"unknown changelog key(s) {', '.join(map(repr, unknown))}; use {', '.join(SECTIONS)}"
|
|
198
|
+
)
|
|
199
|
+
for key, items in entries.items():
|
|
200
|
+
if not isinstance(items, list) or not all(isinstance(i, str) for i in items):
|
|
201
|
+
_fail(f"changelog key {key!r} must be a list of strings")
|
|
202
|
+
return entries
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def read_changelog_entries(no_stdin: bool = False) -> dict:
|
|
206
|
+
"""Read changelog entries from stdin as JSON."""
|
|
207
|
+
if no_stdin or sys.stdin is None or sys.stdin.isatty():
|
|
208
|
+
return {}
|
|
209
|
+
idle = _stdin_idle_seconds()
|
|
210
|
+
raw = _read_stdin_until_eof(idle)
|
|
211
|
+
if raw is None:
|
|
212
|
+
_fail(
|
|
213
|
+
f"stdin stayed open with no input for {idle:g}s and never closed. Pipe the "
|
|
214
|
+
"changelog JSON (echo '{...}' | bump.py patch), raise BUMP_STDIN_IDLE_SECONDS "
|
|
215
|
+
"for a slow producer, or pass --no-stdin (or set BUMP_NO_STDIN=1) for an "
|
|
216
|
+
"empty stub"
|
|
217
|
+
)
|
|
218
|
+
return _parse_entries(raw)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def format_changelog_section(new: str, entries: dict) -> str:
|
|
222
|
+
"""Format a changelog section from entries dict.
|
|
223
|
+
|
|
224
|
+
Output uses single blank lines between elements (markdownlint MD012
|
|
225
|
+
compliant). Trailing blank line before the next existing entry is
|
|
226
|
+
included.
|
|
227
|
+
"""
|
|
228
|
+
today = date.today().isoformat()
|
|
229
|
+
chunks = [f"## [{new}] - {today}\n"]
|
|
230
|
+
|
|
231
|
+
for section in SECTIONS:
|
|
232
|
+
items = entries.get(section, [])
|
|
233
|
+
if items:
|
|
234
|
+
chunks.append(f"\n### {section.capitalize()}\n\n")
|
|
235
|
+
chunks.extend(f"- {item}\n" for item in items)
|
|
236
|
+
|
|
237
|
+
# If no entries at all, emit empty section stubs.
|
|
238
|
+
if not any(entries.get(s) for s in SECTIONS):
|
|
239
|
+
chunks.append("\n### Added\n\n### Changed\n\n### Fixed\n")
|
|
240
|
+
|
|
241
|
+
chunks.append("\n") # blank line before the next existing entry
|
|
242
|
+
return "".join(chunks)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
_FENCE = re.compile(r"^ {0,3}(`{3,}|~{3,})(.*)$")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _section_headings(lines: list[str]) -> list[tuple[int, str]]:
|
|
249
|
+
"""``## `` headings outside fenced code blocks, as (line index, line).
|
|
250
|
+
|
|
251
|
+
A fence opens with 3+ backticks or tildes (a backtick fence's info string
|
|
252
|
+
can't contain a backtick, so ```` ```x``` text ```` is inline code, not a
|
|
253
|
+
fence) and closes only on a line of the same character, at least as long,
|
|
254
|
+
with nothing after it.
|
|
255
|
+
"""
|
|
256
|
+
headings, fence = [], None
|
|
257
|
+
for i, line in enumerate(lines):
|
|
258
|
+
match = _FENCE.match(line.rstrip("\r\n"))
|
|
259
|
+
if fence:
|
|
260
|
+
marker, info = match.groups() if match else ("", "x")
|
|
261
|
+
if marker[:1] == fence[0] and len(marker) >= len(fence) and not info.strip():
|
|
262
|
+
fence = None
|
|
263
|
+
elif match and not (match.group(1)[0] == "`" and "`" in match.group(2)):
|
|
264
|
+
fence = match.group(1)
|
|
265
|
+
elif line.startswith("## "):
|
|
266
|
+
headings.append((i, line))
|
|
267
|
+
return headings
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _insertion_index(lines: list[str]) -> int | None:
|
|
271
|
+
"""Line index for the new section: above the first ``## [`` release heading
|
|
272
|
+
that isn't Unreleased, so a leading Unreleased section stays on top; after
|
|
273
|
+
an Unreleased-only section, or at the end of a file with no ``##`` heading.
|
|
274
|
+
None for ``##`` headings this script doesn't recognise (e.g. ``## 1.2.3``)."""
|
|
275
|
+
headings = _section_headings(lines)
|
|
276
|
+
unreleased = re.compile(r"^## \[Unreleased\]", re.IGNORECASE)
|
|
277
|
+
releases = [i for i, line in headings if line.startswith("## [") and not unreleased.match(line)]
|
|
278
|
+
if releases:
|
|
279
|
+
return releases[0]
|
|
280
|
+
after = [i for i, line in headings if unreleased.match(line)]
|
|
281
|
+
if after:
|
|
282
|
+
return next((i for i, _ in headings if i > after[0]), len(lines))
|
|
283
|
+
return None if headings else len(lines)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def plan_changelog(project_root: Path, new: str, entries: dict) -> tuple[Path, str] | None:
|
|
287
|
+
"""The CHANGELOG path and its new text, without writing; None if there is none."""
|
|
288
|
+
changelog = project_root / "CHANGELOG.md"
|
|
289
|
+
if not changelog.exists():
|
|
290
|
+
return None
|
|
291
|
+
lines = changelog.read_text().splitlines(keepends=True)
|
|
292
|
+
idx = _insertion_index(lines)
|
|
293
|
+
if idx is None:
|
|
294
|
+
_fail(
|
|
295
|
+
"CHANGELOG.md has ## headings but no '## [x.y.z]' release heading to insert "
|
|
296
|
+
"above (Keep a Changelog format)"
|
|
297
|
+
)
|
|
298
|
+
before, after = "".join(lines[:idx]), "".join(lines[idx:])
|
|
299
|
+
new_entry = format_changelog_section(new, entries)
|
|
300
|
+
if not after: # appending at the end of the file
|
|
301
|
+
before = before.rstrip("\n") + "\n\n" if before.strip() else ""
|
|
302
|
+
new_entry = new_entry.rstrip("\n") + "\n"
|
|
303
|
+
return changelog, before + new_entry + after
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _parse_args(argv: list[str]) -> argparse.Namespace:
|
|
307
|
+
parser = argparse.ArgumentParser(
|
|
308
|
+
prog="bump.py",
|
|
309
|
+
description=__doc__,
|
|
310
|
+
allow_abbrev=False,
|
|
311
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
312
|
+
)
|
|
313
|
+
parser.add_argument("part", type=str.lower, choices=("major", "minor", "patch", "show"))
|
|
314
|
+
parser.add_argument(
|
|
315
|
+
"--no-stdin", action="store_true", help="don't read stdin; insert the empty stub"
|
|
316
|
+
)
|
|
317
|
+
if not argv:
|
|
318
|
+
parser.print_help()
|
|
319
|
+
sys.exit(0)
|
|
320
|
+
return parser.parse_args(argv)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def main():
|
|
324
|
+
args = _parse_args(sys.argv[1:])
|
|
325
|
+
path = find_pyproject()
|
|
326
|
+
current = read_version(path)
|
|
327
|
+
|
|
328
|
+
if args.part == "show":
|
|
329
|
+
print(current)
|
|
330
|
+
sys.exit(0)
|
|
331
|
+
|
|
332
|
+
# Every check runs before any write, so a failed bump changes nothing.
|
|
333
|
+
new = bump(current, args.part)
|
|
334
|
+
entries = read_changelog_entries(args.no_stdin or _env_flag("BUMP_NO_STDIN"))
|
|
335
|
+
changelog = plan_changelog(path.parent, new, entries)
|
|
336
|
+
|
|
337
|
+
write_version(path, current, new)
|
|
338
|
+
|
|
339
|
+
# Also update __init__.py if it has __version__
|
|
340
|
+
init_candidates = [
|
|
341
|
+
path.parent / path.parent.name / "__init__.py",
|
|
342
|
+
]
|
|
343
|
+
for init in init_candidates:
|
|
344
|
+
if init.exists():
|
|
345
|
+
init_text = init.read_text()
|
|
346
|
+
if f'__version__ = "{current}"' in init_text:
|
|
347
|
+
init.write_text(
|
|
348
|
+
init_text.replace(
|
|
349
|
+
f'__version__ = "{current}"',
|
|
350
|
+
f'__version__ = "{new}"',
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
print(f"Updated {init.relative_to(path.parent)}")
|
|
354
|
+
|
|
355
|
+
if changelog is None:
|
|
356
|
+
print("No CHANGELOG.md found — skipping")
|
|
357
|
+
else:
|
|
358
|
+
changelog[0].write_text(changelog[1])
|
|
359
|
+
print(f"Updated CHANGELOG.md with [{new}]")
|
|
360
|
+
|
|
361
|
+
print(f"{current} -> {new}")
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
if __name__ == "__main__":
|
|
365
|
+
main()
|
|
@@ -5,6 +5,31 @@ 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.27.0] - 2026-10-30
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- `version-bump`: every check runs before any file is written, so a failed bump changes nothing. That covers the bump type, stdin, the changelog JSON and where the new CHANGELOG section goes. A mistyped bump type now fails at once with argparse's "invalid choice" instead of first waiting on stdin, and unknown options such as `--nostdin` or abbreviations such as `--no` are rejected.
|
|
13
|
+
- `version-bump`: invalid changelog JSON, a non-object, an unknown key (e.g. `removed`), a value that isn't a list of strings, and stdin bytes that don't decode now stop the bump with an error instead of a warning plus an empty stub, so entries are never dropped silently.
|
|
14
|
+
- `version-bump`: SKILL.md now says plainly that a pause longer than `BUMP_STDIN_IDLE_SECONDS`, including before the first byte, is an error, and to raise it for a slow producer.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `version-bump`: fenced code blocks are recognised properly. A line like ```` ```bash``` one-liner ```` no longer opens a fence, a different or shorter fence marker no longer closes one, and the new section can no longer end up appended below the oldest release (regression in 0.26.23).
|
|
19
|
+
- `version-bump`: a CHANGELOG whose `##` headings aren't `## [...]` release headings (e.g. `## 1.2.3`) stops the bump with an error before `pyproject.toml` is touched. 0.26.23 appended the section at the end of the file; before that, the script warned but still bumped `pyproject.toml`.
|
|
20
|
+
- `version-bump`: stdin is read by a background thread with the same idle timeout instead of `select()`, so the no-hang behaviour also holds on Windows and for a text-only stdin such as `io.StringIO`, which used to crash on `fileno()`.
|
|
21
|
+
|
|
22
|
+
## [0.26.23] - 2026-10-30
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `version-bump`: `--no-stdin` (or `BUMP_NO_STDIN=1`) skips reading stdin and writes the empty changelog stub, for scripts, daemons and agent harnesses that hand the script a stdin nobody writes to. `BUMP_STDIN_IDLE_SECONDS` (default 5) sets how long stdin may stay silent before the bump gives up.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- `version-bump`: `bump.py` no longer hangs on a stdin that stays open, whether it sends nothing or only part of the JSON. Stdin is read until it closes, however slowly the JSON arrives; if it goes silent for `BUMP_STDIN_IDLE_SECONDS` first, the bump stops with an error and changes nothing, instead of hanging or silently dropping the entries. Piped JSON (`echo … | bump.py patch`) works as before.
|
|
31
|
+
- `version-bump`: the new `CHANGELOG.md` section goes above the first `## [` release heading, ignoring headings inside fenced code blocks. A leading `## [Unreleased]` section now stays on top, untouched; before, the release was inserted above it. A file whose first line is a `## [` heading is updated instead of refused (culture's vendored copy already did this), and a CHANGELOG with no release heading gets the section appended instead of a warning.
|
|
32
|
+
|
|
8
33
|
## [0.26.22] - 2026-10-24
|
|
9
34
|
|
|
10
35
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: guild-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.27.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
|