agentsquire 0.2.2__tar.gz → 0.3.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.
- {agentsquire-0.2.2 → agentsquire-0.3.0}/.gitignore +4 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/CHANGELOG.md +33 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/PKG-INFO +37 -1
- {agentsquire-0.2.2 → agentsquire-0.3.0}/README.md +36 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/pyproject.toml +20 -1
- {agentsquire-0.2.2 → agentsquire-0.3.0}/src/agentsquire/__init__.py +1 -1
- agentsquire-0.3.0/src/agentsquire/console.py +90 -0
- agentsquire-0.3.0/src/agentsquire/skills/developing-with-agentsquire/SKILL.md +101 -0
- agentsquire-0.3.0/tests/test_bundled_skill.py +73 -0
- agentsquire-0.3.0/tests/test_console.py +51 -0
- agentsquire-0.3.0/tests/test_console_skills.py +81 -0
- agentsquire-0.3.0/tests/test_console_staleness.py +120 -0
- agentsquire-0.3.0/tests/test_guide.py +116 -0
- agentsquire-0.3.0/tests/test_guide_packaging.py +77 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/LICENSE +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/docs/api.md +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/docs/harnesses.md +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/src/agentsquire/cli.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/src/agentsquire/harnesses.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/src/agentsquire/hashing.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/src/agentsquire/py.typed +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/src/agentsquire/roots.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/src/agentsquire/skills.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/src/agentsquire/sources.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/src/agentsquire/staleness.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/src/agentsquire/stamping.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/src/agentsquire/verbs.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_api_lifecycle.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_backends.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_bundled_source.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_cli_group.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_docs_snippets.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_entry_point.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_harness_registry.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_hash.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_install.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_package.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_reference_doc.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_root_overrides.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_skill_model.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_source_seam.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_staleness_check.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_status.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_uninstall.py +0 -0
- {agentsquire-0.2.2 → agentsquire-0.3.0}/tests/test_update.py +0 -0
|
@@ -10,3 +10,7 @@ checkouts/
|
|
|
10
10
|
# Local scratch / dev notes (never committed, never published)
|
|
11
11
|
bugs.md
|
|
12
12
|
feedback.md
|
|
13
|
+
|
|
14
|
+
# specflo local state: the active-project pointer is per-clone session state,
|
|
15
|
+
# not shared config. The durable record lives under docs/projects/.
|
|
16
|
+
.specflo/
|
|
@@ -8,6 +8,39 @@ The version is declared in two places that are kept in sync by the release
|
|
|
8
8
|
tooling: `__version__` in `src/agentsquire/__init__.py` and `version` in
|
|
9
9
|
`pyproject.toml`.
|
|
10
10
|
|
|
11
|
+
## [0.3.0]
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **agentsquire now ships its own runnable CLI, `squire` (long alias
|
|
15
|
+
`agentsquire`).** It dogfoods the library's consumer contract end to end,
|
|
16
|
+
carrying one bundled skill, mounting the ready-made skills group, emitting the
|
|
17
|
+
proactive staleness notice, and serving its own reference docs.
|
|
18
|
+
- `squire skills install|status|update|uninstall` - the standard group,
|
|
19
|
+
mounted the production way at user scope by default, operating on
|
|
20
|
+
agentsquire's one bundled skill.
|
|
21
|
+
- `squire guide [TOPIC]` - with no argument lists the reference topics (api,
|
|
22
|
+
harnesses, integration); with a topic, pages that canonical doc as raw
|
|
23
|
+
markdown. An unknown topic is a clean, non-zero error naming the valid
|
|
24
|
+
topics.
|
|
25
|
+
- `squire --version` prints the installed agentsquire version; `squire --help`
|
|
26
|
+
and `agentsquire --help` are identical below the usage line.
|
|
27
|
+
- A proactive staleness notice on stderr points at `squire skills update` when
|
|
28
|
+
an installed copy is out of date; suppressed under CI or
|
|
29
|
+
`AGENTSQUIRE_NO_UPDATE_CHECK`, and it never changes stdout or the exit code.
|
|
30
|
+
- **`developing-with-agentsquire` bundled skill.** agentsquire carries one Agent
|
|
31
|
+
Skill as package data and marks itself skill-carrying under the
|
|
32
|
+
`agentsquire.skills` entry-point group - the same contract it documents for
|
|
33
|
+
consumers.
|
|
34
|
+
- **The three reference docs are force-included into the wheel.** `docs/api.md`,
|
|
35
|
+
`docs/harnesses.md`, and `README.md` are packaged under `agentsquire/_docs/`
|
|
36
|
+
(as `api.md`, `harnesses.md`, `integration.md`) so `guide` has one
|
|
37
|
+
authoritative source that works from an installed wheel, with no committed
|
|
38
|
+
in-package duplicate.
|
|
39
|
+
- **README Installation section.** Documents `pip install agentsquire`,
|
|
40
|
+
installing from a source clone, and - for developers building on agentsquire -
|
|
41
|
+
how to install the bundled `developing-with-agentsquire` skill and reference
|
|
42
|
+
docs into their own harness (`squire skills install`, `squire guide`).
|
|
43
|
+
|
|
11
44
|
## [0.2.2]
|
|
12
45
|
|
|
13
46
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentsquire
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Let a Python package carry its own agent integrations and install them into whatever agent harness is present.
|
|
5
5
|
Author: TacoTakumi
|
|
6
6
|
License-Expression: MIT
|
|
@@ -42,6 +42,42 @@ is by marker directory; every operation is local (no network in any verb). Each
|
|
|
42
42
|
harness's directories, scopes, and behaviour are recorded in
|
|
43
43
|
[docs/harnesses.md](docs/harnesses.md).
|
|
44
44
|
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
pip install agentsquire
|
|
48
|
+
|
|
49
|
+
This installs the library and its own CLI, `squire` (aliased `agentsquire`). As
|
|
50
|
+
a consumer you normally add `agentsquire` to your package's dependencies rather
|
|
51
|
+
than having users install it directly - it rides in your wheel, and your users
|
|
52
|
+
only ever see your tool.
|
|
53
|
+
|
|
54
|
+
agentsquire dogfoods its own contract: it carries the
|
|
55
|
+
`developing-with-agentsquire` skill and its reference docs as package data. When
|
|
56
|
+
you build an app on agentsquire, install that skill into your own agent harness
|
|
57
|
+
so the harness has the integration know-how on hand while it works:
|
|
58
|
+
|
|
59
|
+
squire skills install
|
|
60
|
+
|
|
61
|
+
That copies `developing-with-agentsquire` (how to ship skills, mount the group,
|
|
62
|
+
and wire the staleness hook) into every detected harness. The same reference
|
|
63
|
+
docs are served at the terminal from the installed wheel - no checkout needed:
|
|
64
|
+
|
|
65
|
+
squire guide # topics: api, harnesses, integration
|
|
66
|
+
squire guide api # the Python API reference
|
|
67
|
+
squire guide integration # this consumer integration guide
|
|
68
|
+
|
|
69
|
+
### From source
|
|
70
|
+
|
|
71
|
+
Clone the repository and install it into your environment:
|
|
72
|
+
|
|
73
|
+
git clone https://github.com/TacoTakumi/AgentSquire.git
|
|
74
|
+
cd AgentSquire
|
|
75
|
+
pip install .
|
|
76
|
+
|
|
77
|
+
Add `-e` (`pip install -e .`) for an editable install if you plan to work on
|
|
78
|
+
agentsquire itself. The repo uses [uv](https://docs.astral.sh/uv/): `uv sync`
|
|
79
|
+
provisions a dev environment and `uv run pytest` runs the test suite.
|
|
80
|
+
|
|
45
81
|
## Consumer integration guide
|
|
46
82
|
|
|
47
83
|
### 1. Ship skills as package data
|
|
@@ -18,6 +18,42 @@ is by marker directory; every operation is local (no network in any verb). Each
|
|
|
18
18
|
harness's directories, scopes, and behaviour are recorded in
|
|
19
19
|
[docs/harnesses.md](docs/harnesses.md).
|
|
20
20
|
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
pip install agentsquire
|
|
24
|
+
|
|
25
|
+
This installs the library and its own CLI, `squire` (aliased `agentsquire`). As
|
|
26
|
+
a consumer you normally add `agentsquire` to your package's dependencies rather
|
|
27
|
+
than having users install it directly - it rides in your wheel, and your users
|
|
28
|
+
only ever see your tool.
|
|
29
|
+
|
|
30
|
+
agentsquire dogfoods its own contract: it carries the
|
|
31
|
+
`developing-with-agentsquire` skill and its reference docs as package data. When
|
|
32
|
+
you build an app on agentsquire, install that skill into your own agent harness
|
|
33
|
+
so the harness has the integration know-how on hand while it works:
|
|
34
|
+
|
|
35
|
+
squire skills install
|
|
36
|
+
|
|
37
|
+
That copies `developing-with-agentsquire` (how to ship skills, mount the group,
|
|
38
|
+
and wire the staleness hook) into every detected harness. The same reference
|
|
39
|
+
docs are served at the terminal from the installed wheel - no checkout needed:
|
|
40
|
+
|
|
41
|
+
squire guide # topics: api, harnesses, integration
|
|
42
|
+
squire guide api # the Python API reference
|
|
43
|
+
squire guide integration # this consumer integration guide
|
|
44
|
+
|
|
45
|
+
### From source
|
|
46
|
+
|
|
47
|
+
Clone the repository and install it into your environment:
|
|
48
|
+
|
|
49
|
+
git clone https://github.com/TacoTakumi/AgentSquire.git
|
|
50
|
+
cd AgentSquire
|
|
51
|
+
pip install .
|
|
52
|
+
|
|
53
|
+
Add `-e` (`pip install -e .`) for an editable install if you plan to work on
|
|
54
|
+
agentsquire itself. The repo uses [uv](https://docs.astral.sh/uv/): `uv sync`
|
|
55
|
+
provisions a dev environment and `uv run pytest` runs the test suite.
|
|
56
|
+
|
|
21
57
|
## Consumer integration guide
|
|
22
58
|
|
|
23
59
|
### 1. Ship skills as package data
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentsquire"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Let a Python package carry its own agent integrations and install them into whatever agent harness is present."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -30,6 +30,16 @@ dependencies = [
|
|
|
30
30
|
"click",
|
|
31
31
|
]
|
|
32
32
|
|
|
33
|
+
[project.scripts]
|
|
34
|
+
squire = "agentsquire.console:main"
|
|
35
|
+
agentsquire = "agentsquire.console:main"
|
|
36
|
+
|
|
37
|
+
# agentsquire dogfoods its own consumer contract: it carries one bundled skill
|
|
38
|
+
# (developing-with-agentsquire) and marks itself skill-carrying under the same
|
|
39
|
+
# entry-point group it tells consumers to use. Nothing reads this at launch.
|
|
40
|
+
[project.entry-points."agentsquire.skills"]
|
|
41
|
+
agentsquire = "agentsquire"
|
|
42
|
+
|
|
33
43
|
[dependency-groups]
|
|
34
44
|
dev = [
|
|
35
45
|
"build",
|
|
@@ -53,6 +63,15 @@ include = [
|
|
|
53
63
|
"/docs/harnesses.md",
|
|
54
64
|
]
|
|
55
65
|
|
|
66
|
+
# Force-include the three canonical docs into the wheel so `guide` has a single
|
|
67
|
+
# authoritative source with no hand-maintained in-package duplicate. The sources
|
|
68
|
+
# live only under docs/ and README.md; nothing is committed under the package.
|
|
69
|
+
# README.md becomes the `integration` topic.
|
|
70
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
71
|
+
"docs/api.md" = "agentsquire/_docs/api.md"
|
|
72
|
+
"docs/harnesses.md" = "agentsquire/_docs/harnesses.md"
|
|
73
|
+
"README.md" = "agentsquire/_docs/integration.md"
|
|
74
|
+
|
|
56
75
|
[tool.pytest.ini_options]
|
|
57
76
|
testpaths = ["tests"]
|
|
58
77
|
pythonpath = ["src"]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""The ``squire`` root CLI - agentsquire's own runnable executable.
|
|
2
|
+
|
|
3
|
+
``squire`` (and its long alias ``agentsquire``) is itself an agentsquire
|
|
4
|
+
consumer: it carries one bundled skill (developing-with-agentsquire), mounts the
|
|
5
|
+
skills subcommand group and a ``guide`` command, and emits the proactive
|
|
6
|
+
staleness notice. This module is that root command group; the two console
|
|
7
|
+
scripts in ``[project.scripts]`` both point here.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from importlib.metadata import version
|
|
13
|
+
from importlib.resources import files as resource_files
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
|
|
18
|
+
import agentsquire
|
|
19
|
+
from agentsquire import BundledPackageDataSource, check_stale
|
|
20
|
+
from agentsquire.cli import skills_command_group
|
|
21
|
+
|
|
22
|
+
# The installed agentsquire version, and the single source of truth for
|
|
23
|
+
# ``--version`` (equals agentsquire.__version__, kept in sync by release tooling).
|
|
24
|
+
_VERSION = version("agentsquire")
|
|
25
|
+
|
|
26
|
+
# The `guide` topics, one per canonical doc. Content is force-included into the
|
|
27
|
+
# wheel under agentsquire/_docs/<topic>.md (see pyproject); nothing is committed
|
|
28
|
+
# under the package, so a source checkout falls back to the repo docs/ + README.
|
|
29
|
+
_GUIDE_TOPICS = ("api", "harnesses", "integration")
|
|
30
|
+
_GUIDE_SOURCE_FALLBACK = {
|
|
31
|
+
"api": ("docs", "api.md"),
|
|
32
|
+
"harnesses": ("docs", "harnesses.md"),
|
|
33
|
+
"integration": ("README.md",),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _read_guide_doc(topic: str) -> str | None:
|
|
38
|
+
"""Content for a guide topic: the packaged agentsquire/_docs/<topic>.md when
|
|
39
|
+
installed from a wheel, else the repo docs/ + README in a source checkout."""
|
|
40
|
+
packaged = resource_files("agentsquire") / "_docs" / f"{topic}.md"
|
|
41
|
+
if packaged.is_file():
|
|
42
|
+
return packaged.read_text(encoding="utf-8")
|
|
43
|
+
# Source checkout: <repo>/src/agentsquire/__init__.py -> repo root is parents[2].
|
|
44
|
+
repo_root = Path(agentsquire.__file__).resolve().parents[2]
|
|
45
|
+
fallback = repo_root.joinpath(*_GUIDE_SOURCE_FALLBACK[topic])
|
|
46
|
+
if fallback.is_file():
|
|
47
|
+
return fallback.read_text(encoding="utf-8")
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@click.group()
|
|
52
|
+
@click.version_option(version=_VERSION, prog_name="squire", message="%(version)s")
|
|
53
|
+
def main() -> None:
|
|
54
|
+
"""squire - install agentsquire's own agent skills into your harness."""
|
|
55
|
+
# Proactive skill-staleness notice, wired the production way (no home=/
|
|
56
|
+
# project=, so the real roots resolve). Safe by design: notice-only on
|
|
57
|
+
# stderr, never prompts, never mutates, never changes the exit code.
|
|
58
|
+
check_stale(
|
|
59
|
+
BundledPackageDataSource("agentsquire"),
|
|
60
|
+
prog_name="squire",
|
|
61
|
+
update_command="squire skills update",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@main.command()
|
|
66
|
+
@click.argument("topic", required=False)
|
|
67
|
+
def guide(topic: str | None) -> None:
|
|
68
|
+
"""Show agentsquire's reference docs. Run with no TOPIC to list them."""
|
|
69
|
+
if topic is None:
|
|
70
|
+
click.echo("Available guide topics:")
|
|
71
|
+
for name in _GUIDE_TOPICS:
|
|
72
|
+
click.echo(f" {name}")
|
|
73
|
+
return
|
|
74
|
+
if topic not in _GUIDE_TOPICS:
|
|
75
|
+
raise click.BadParameter(
|
|
76
|
+
f"unknown topic {topic!r}. Available topics: "
|
|
77
|
+
f"{', '.join(_GUIDE_TOPICS)}.",
|
|
78
|
+
param_hint="TOPIC",
|
|
79
|
+
)
|
|
80
|
+
content = _read_guide_doc(topic)
|
|
81
|
+
if content is None:
|
|
82
|
+
raise click.ClickException(f"the {topic!r} guide doc is not available.")
|
|
83
|
+
# Raw markdown via click.echo (no pager) for deterministic, capturable output.
|
|
84
|
+
click.echo(content)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Dogfood the ready-made group agentsquire publishes: mount it the production
|
|
88
|
+
# way (no home=/project=), so each invocation resolves the real roots. Tests
|
|
89
|
+
# redirect those roots with AGENTSQUIRE_HOME / AGENTSQUIRE_PROJECT.
|
|
90
|
+
main.add_command(skills_command_group("agentsquire", default_scope="user"))
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: developing-with-agentsquire
|
|
3
|
+
description: "Use when building a Python CLI that carries its own Agent Skills with agentsquire - wiring the skills subcommand group, the staleness notice, console entry points, and package-data layout into a consumer package."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Developing with agentsquire
|
|
7
|
+
|
|
8
|
+
agentsquire lets your Python package carry its own Agent Skills and install them
|
|
9
|
+
into whatever agent harness is present (Claude Code, pi, Hermes, opencode). Your
|
|
10
|
+
users only ever run your tool - `your-cli skills install` - never a second CLI.
|
|
11
|
+
|
|
12
|
+
Add `agentsquire` as a plain dependency, then wire the four points below. This is
|
|
13
|
+
a checklist, not the reference: run `squire guide` (topics `api`, `harnesses`,
|
|
14
|
+
`integration`) or read `docs/api.md` for full signatures and behaviour.
|
|
15
|
+
|
|
16
|
+
## 1. Ship skills as package data
|
|
17
|
+
|
|
18
|
+
Lay each skill out as a directory with a `SKILL.md` (agentskills.io format)
|
|
19
|
+
under a `skills/` resource inside your importable package:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
your_pkg/
|
|
23
|
+
__init__.py
|
|
24
|
+
skills/
|
|
25
|
+
my-skill/
|
|
26
|
+
SKILL.md
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The skills ride inside your wheel, so no source checkout is needed at run time.
|
|
30
|
+
hatchling includes package data by default; setuptools needs
|
|
31
|
+
`include-package-data`. This skills-as-package-data layout is what
|
|
32
|
+
`BundledPackageDataSource("your_pkg")` reads from.
|
|
33
|
+
|
|
34
|
+
## 2. Mount the skills subcommand group
|
|
35
|
+
|
|
36
|
+
`skills_command_group` returns a click group with `install`, `status`, `update`,
|
|
37
|
+
and `uninstall` subcommands, parameterized by your package name and default
|
|
38
|
+
scope. Mount it on your root group:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from agentsquire.cli import skills_command_group
|
|
42
|
+
|
|
43
|
+
cli.add_command(skills_command_group("your_pkg", default_scope="user"))
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For typer, mount onto `typer.main.get_command(app)`. Every subcommand takes
|
|
47
|
+
`--scope user|project` and `--harness NAME`. Choose `user` scope for
|
|
48
|
+
general-purpose tools, `project` for repo-specific skills.
|
|
49
|
+
|
|
50
|
+
## 3. Expose a console entry point
|
|
51
|
+
|
|
52
|
+
Give your CLI a `[project.scripts]` console entry in `pyproject.toml` so users
|
|
53
|
+
invoke it by name:
|
|
54
|
+
|
|
55
|
+
```toml
|
|
56
|
+
[project.scripts]
|
|
57
|
+
your-cli = "your_pkg.console:main"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 4. Surface updates proactively (optional)
|
|
61
|
+
|
|
62
|
+
Call the `check_stale` hook at your entry point. When an installed skill has a
|
|
63
|
+
newer shipped copy it prints one advisory line on stderr and nothing else - it
|
|
64
|
+
never prompts, never writes stdout, never changes your exit code:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from agentsquire import BundledPackageDataSource, check_stale
|
|
68
|
+
|
|
69
|
+
def main():
|
|
70
|
+
check_stale(
|
|
71
|
+
BundledPackageDataSource("your_pkg"),
|
|
72
|
+
prog_name="your-cli",
|
|
73
|
+
update_command="your-cli skills update",
|
|
74
|
+
)
|
|
75
|
+
# ... rest of your entry point
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Suppressed when `CI` or `AGENTSQUIRE_NO_UPDATE_CHECK` is set to any non-empty
|
|
79
|
+
value.
|
|
80
|
+
|
|
81
|
+
## 5. Mark the package as skill-carrying (optional)
|
|
82
|
+
|
|
83
|
+
One pyproject line registers your package under the `agentsquire.skills`
|
|
84
|
+
entry-point marker. Nothing reads it today; it is reserved for a future
|
|
85
|
+
environment-wide listing and changes no behaviour:
|
|
86
|
+
|
|
87
|
+
```toml
|
|
88
|
+
[project.entry-points."agentsquire.skills"]
|
|
89
|
+
your_pkg = "your_pkg"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## The provenance and update model
|
|
93
|
+
|
|
94
|
+
Installs are plain copies - no symlinks, no lockfile - so an installed skill
|
|
95
|
+
survives upgrade or removal of your package. Each installed `SKILL.md` carries a
|
|
96
|
+
provenance stamp in its frontmatter `metadata.agentsquire` map (installer,
|
|
97
|
+
versions, source package, content hash). Staleness detection is a local hash
|
|
98
|
+
compare: `status` classifies each skill as not-installed, up-to-date,
|
|
99
|
+
update-available, or locally-modified, and `update` refreshes only the stale
|
|
100
|
+
ones. User-modified installs and pre-existing symlinks are reported and skipped,
|
|
101
|
+
never clobbered. See `squire guide integration` for the full model.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""T-01 / REQ-11..13: the one bundled skill is a valid, wheel-shipped skill
|
|
2
|
+
whose body covers every agentsquire wiring point and points at the reference."""
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import zipfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from agentsquire.skills import load_skill, validate_skill_dir
|
|
12
|
+
|
|
13
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
14
|
+
SKILL_DIR = REPO_ROOT / "src" / "agentsquire" / "skills" / "developing-with-agentsquire"
|
|
15
|
+
SKILL_MD = SKILL_DIR / "SKILL.md"
|
|
16
|
+
|
|
17
|
+
# REQ-13: the body must cover each wiring point. Grep-checkable tokens, one per
|
|
18
|
+
# point the consumer guide names.
|
|
19
|
+
WIRING_TOKENS = [
|
|
20
|
+
"package data", # skills-as-package-data layout
|
|
21
|
+
"skills_command_group", # the mount call
|
|
22
|
+
"check_stale", # the staleness notice hook
|
|
23
|
+
"[project.scripts]", # console entry
|
|
24
|
+
"agentsquire.skills", # entry-point marker
|
|
25
|
+
"provenance", # provenance/update (staleness) model
|
|
26
|
+
"staleness",
|
|
27
|
+
]
|
|
28
|
+
# ...and it must point at the full reference rather than duplicate it.
|
|
29
|
+
DOCS_POINTER_TOKENS = ["squire guide", "docs/"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_skill_dir_is_structurally_valid():
|
|
33
|
+
# REQ-11: validate_skill_dir reports no violations.
|
|
34
|
+
assert validate_skill_dir(SKILL_DIR) == []
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_frontmatter_name_matches_directory():
|
|
38
|
+
# REQ-11: frontmatter name is developing-with-agentsquire.
|
|
39
|
+
assert load_skill(SKILL_DIR).name == "developing-with-agentsquire"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_body_covers_every_wiring_point():
|
|
43
|
+
# REQ-13: token checklist over the body.
|
|
44
|
+
body = SKILL_MD.read_text()
|
|
45
|
+
missing = [token for token in WIRING_TOKENS if token not in body]
|
|
46
|
+
assert not missing, f"SKILL.md body is missing wiring tokens: {missing}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_body_points_to_guide_or_docs():
|
|
50
|
+
# REQ-13: a pointer to the full reference, not a duplicate of it.
|
|
51
|
+
body = SKILL_MD.read_text()
|
|
52
|
+
assert any(token in body for token in DOCS_POINTER_TOKENS)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.fixture(scope="module")
|
|
56
|
+
def wheel_record(tmp_path_factory):
|
|
57
|
+
"""Build the real agentsquire wheel and return its RECORD text (REQ-12)."""
|
|
58
|
+
outdir = tmp_path_factory.mktemp("dist")
|
|
59
|
+
subprocess.run(
|
|
60
|
+
[sys.executable, "-m", "build", "--wheel", "--no-isolation", "--outdir", str(outdir)],
|
|
61
|
+
cwd=REPO_ROOT,
|
|
62
|
+
check=True,
|
|
63
|
+
capture_output=True,
|
|
64
|
+
)
|
|
65
|
+
wheel = next(outdir.glob("*.whl"))
|
|
66
|
+
with zipfile.ZipFile(wheel) as zf:
|
|
67
|
+
record_name = next(n for n in zf.namelist() if n.endswith(".dist-info/RECORD"))
|
|
68
|
+
return zf.read(record_name).decode()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_bundled_skill_ships_in_the_wheel(wheel_record):
|
|
72
|
+
# REQ-12: the skill's SKILL.md is listed in the built wheel's RECORD.
|
|
73
|
+
assert "agentsquire/skills/developing-with-agentsquire/SKILL.md" in wheel_record
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""T-02 / REQ-01..03: the squire root CLI, its agentsquire alias, and --version."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import entry_points, version
|
|
4
|
+
|
|
5
|
+
import agentsquire
|
|
6
|
+
from agentsquire.console import main
|
|
7
|
+
from click.testing import CliRunner
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_help_exits_zero_and_renders_the_root_group():
|
|
11
|
+
# REQ-01: `squire --help` exits 0 and prints the root command group.
|
|
12
|
+
result = CliRunner().invoke(main, ["--help"])
|
|
13
|
+
assert result.exit_code == 0
|
|
14
|
+
assert result.output.startswith("Usage:")
|
|
15
|
+
assert "install agentsquire's own agent skills" in result.output
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_version_prints_the_installed_agentsquire_version():
|
|
19
|
+
# REQ-03: `squire --version` prints exactly importlib.metadata.version('agentsquire').
|
|
20
|
+
result = CliRunner().invoke(main, ["--version"])
|
|
21
|
+
assert result.exit_code == 0
|
|
22
|
+
assert result.output.strip() == version("agentsquire")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_reported_version_equals_dunder_version():
|
|
26
|
+
# REQ-03: the two declared version sources agree.
|
|
27
|
+
assert version("agentsquire") == agentsquire.__version__
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_both_console_scripts_resolve_to_the_same_callable():
|
|
31
|
+
# REQ-01/REQ-02: squire and its agentsquire alias are the identical CLI.
|
|
32
|
+
scripts = {
|
|
33
|
+
ep.name: ep.value
|
|
34
|
+
for ep in entry_points(group="console_scripts")
|
|
35
|
+
if ep.name in ("squire", "agentsquire")
|
|
36
|
+
}
|
|
37
|
+
assert scripts == {
|
|
38
|
+
"squire": "agentsquire.console:main",
|
|
39
|
+
"agentsquire": "agentsquire.console:main",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_alias_help_is_identical_below_the_usage_line():
|
|
44
|
+
# REQ-02: `agentsquire --help` is the same root-group help as `squire --help`;
|
|
45
|
+
# the two invocations differ only in the program name on the Usage line.
|
|
46
|
+
runner = CliRunner()
|
|
47
|
+
squire = runner.invoke(main, ["--help"], prog_name="squire").output.splitlines()
|
|
48
|
+
alias = runner.invoke(main, ["--help"], prog_name="agentsquire").output.splitlines()
|
|
49
|
+
assert squire[0].startswith("Usage: squire")
|
|
50
|
+
assert alias[0].startswith("Usage: agentsquire")
|
|
51
|
+
assert squire[1:] == alias[1:]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""T-03 / REQ-04, REQ-05, REQ-11, REQ-01: the squire CLI mounts the skills group
|
|
2
|
+
and installs its one bundled skill into a detected harness end-to-end.
|
|
3
|
+
|
|
4
|
+
The group is wired the production way (no home=/project=); these CLI-level tests
|
|
5
|
+
redirect the roots with AGENTSQUIRE_HOME / AGENTSQUIRE_PROJECT (0.2.0 overrides)
|
|
6
|
+
rather than monkeypatching Path.home / chdir."""
|
|
7
|
+
|
|
8
|
+
import agentsquire
|
|
9
|
+
from agentsquire.console import main
|
|
10
|
+
from agentsquire.stamping import read_stamp
|
|
11
|
+
from click.testing import CliRunner
|
|
12
|
+
|
|
13
|
+
SKILL = "developing-with-agentsquire"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def invoke(*args, **env):
|
|
17
|
+
return CliRunner().invoke(main, list(args), env=env, catch_exceptions=False)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def fixture_roots(tmp_path):
|
|
21
|
+
"""A home carrying the Claude Code marker, and an empty project dir."""
|
|
22
|
+
home = tmp_path / "home"
|
|
23
|
+
project = tmp_path / "project"
|
|
24
|
+
(home / ".claude").mkdir(parents=True)
|
|
25
|
+
project.mkdir()
|
|
26
|
+
return home, project
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_skills_help_lists_exactly_the_four_verbs():
|
|
30
|
+
# REQ-04: the mounted group exposes exactly install/status/update/uninstall.
|
|
31
|
+
skills_group = main.commands["skills"]
|
|
32
|
+
assert sorted(skills_group.commands) == ["install", "status", "uninstall", "update"]
|
|
33
|
+
|
|
34
|
+
result = invoke("skills", "--help")
|
|
35
|
+
assert result.exit_code == 0
|
|
36
|
+
for verb in ("install", "status", "update", "uninstall"):
|
|
37
|
+
assert verb in result.output
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_install_help_shows_scope_default_user_and_harness():
|
|
41
|
+
# REQ-04: install exposes --scope (shown default user) and --harness.
|
|
42
|
+
result = invoke("skills", "install", "--help")
|
|
43
|
+
assert result.exit_code == 0
|
|
44
|
+
assert "--scope" in result.output
|
|
45
|
+
assert "[default: user]" in result.output
|
|
46
|
+
assert "--harness" in result.output
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_install_writes_stamped_skill_then_reports_up_to_date(tmp_path):
|
|
50
|
+
# REQ-05/REQ-11: install writes the one bundled skill into the user-scope
|
|
51
|
+
# Claude Code dir with an agentsquire provenance stamp, idempotently.
|
|
52
|
+
home, project = fixture_roots(tmp_path)
|
|
53
|
+
env = {"AGENTSQUIRE_HOME": str(home), "AGENTSQUIRE_PROJECT": str(project)}
|
|
54
|
+
|
|
55
|
+
first = invoke("skills", "install", **env)
|
|
56
|
+
assert first.exit_code == 0, first.output
|
|
57
|
+
|
|
58
|
+
installed = home / ".claude" / "skills" / SKILL / "SKILL.md"
|
|
59
|
+
assert installed.is_file()
|
|
60
|
+
stamp = read_stamp(installed.read_text())
|
|
61
|
+
assert stamp is not None
|
|
62
|
+
assert stamp["installer"] == "agentsquire"
|
|
63
|
+
assert stamp["source_package"] == "agentsquire"
|
|
64
|
+
assert stamp["installer_version"] == agentsquire.__version__
|
|
65
|
+
|
|
66
|
+
second = invoke("skills", "install", **env)
|
|
67
|
+
assert second.exit_code == 0, second.output
|
|
68
|
+
assert "up-to-date" in second.output
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_status_lists_exactly_the_one_bundled_skill(tmp_path):
|
|
72
|
+
# REQ-11: status over the user scope names exactly developing-with-agentsquire.
|
|
73
|
+
home, project = fixture_roots(tmp_path)
|
|
74
|
+
env = {"AGENTSQUIRE_HOME": str(home), "AGENTSQUIRE_PROJECT": str(project)}
|
|
75
|
+
|
|
76
|
+
result = invoke("skills", "status", "--scope", "user", **env)
|
|
77
|
+
assert result.exit_code == 0, result.output
|
|
78
|
+
|
|
79
|
+
lines = [line for line in result.output.splitlines() if line.strip()]
|
|
80
|
+
assert len(lines) == 1
|
|
81
|
+
assert SKILL in lines[0]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""T-04 / REQ-06: the squire root callback wires check_stale, so any subcommand
|
|
2
|
+
emits exactly one stderr update notice - naming 'squire skills update' - without
|
|
3
|
+
touching stdout or the exit code, and the suppression env vars silence it.
|
|
4
|
+
|
|
5
|
+
The hook is wired the production way (no home=/project=); these tests redirect
|
|
6
|
+
the roots with AGENTSQUIRE_HOME / AGENTSQUIRE_PROJECT."""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from agentsquire.console import main
|
|
14
|
+
from agentsquire.harnesses import CLAUDE_CODE
|
|
15
|
+
from agentsquire.sources import DirectorySource
|
|
16
|
+
from agentsquire.verbs import install
|
|
17
|
+
from click.testing import CliRunner
|
|
18
|
+
|
|
19
|
+
SKILL = "developing-with-agentsquire"
|
|
20
|
+
SRC = Path(__file__).parent.parent / "src"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def stale_home(tmp_path):
|
|
24
|
+
"""Home + project fixtures with an installed-but-stale developing-with-agentsquire.
|
|
25
|
+
|
|
26
|
+
An 'old' copy (content differing from the real bundled skill) is installed
|
|
27
|
+
with a correct stamp, so status against BundledPackageDataSource('agentsquire')
|
|
28
|
+
- the source the console wires - classifies it UPDATE_AVAILABLE."""
|
|
29
|
+
home = tmp_path / "home"
|
|
30
|
+
project = tmp_path / "project"
|
|
31
|
+
(home / ".claude").mkdir(parents=True)
|
|
32
|
+
project.mkdir()
|
|
33
|
+
|
|
34
|
+
bundle = tmp_path / "old-bundle"
|
|
35
|
+
old = bundle / SKILL
|
|
36
|
+
old.mkdir(parents=True)
|
|
37
|
+
(old / "SKILL.md").write_text(
|
|
38
|
+
f"---\nname: {SKILL}\ndescription: An older bundled copy.\n---\n\nold body\n"
|
|
39
|
+
)
|
|
40
|
+
install(
|
|
41
|
+
DirectorySource(bundle), CLAUDE_CODE, scope="user",
|
|
42
|
+
home=home, project=project,
|
|
43
|
+
source_package="agentsquire", source_version="0.0.1",
|
|
44
|
+
)
|
|
45
|
+
return home, project
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def env_for(home, project, **extra):
|
|
49
|
+
return {"AGENTSQUIRE_HOME": str(home), "AGENTSQUIRE_PROJECT": str(project), **extra}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run(home, project, **extra_env):
|
|
53
|
+
return CliRunner().invoke(
|
|
54
|
+
main, ["skills", "status", "--scope", "user"],
|
|
55
|
+
env=env_for(home, project, **extra_env), catch_exceptions=False,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_stale_install_emits_exactly_one_update_notice_on_stderr(tmp_path):
|
|
60
|
+
home, project = stale_home(tmp_path)
|
|
61
|
+
|
|
62
|
+
result = run(home, project)
|
|
63
|
+
|
|
64
|
+
assert result.exit_code == 0
|
|
65
|
+
notices = result.stderr.splitlines()
|
|
66
|
+
assert len(notices) == 1
|
|
67
|
+
assert "squire skills update" in notices[0]
|
|
68
|
+
assert SKILL in notices[0]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_notice_is_stderr_only_and_leaves_stdout_intact(tmp_path):
|
|
72
|
+
home, project = stale_home(tmp_path)
|
|
73
|
+
|
|
74
|
+
result = run(home, project)
|
|
75
|
+
|
|
76
|
+
# stdout is exactly the subcommand's own output; the notice never leaks in.
|
|
77
|
+
assert "a skills update is available" not in result.stdout
|
|
78
|
+
assert SKILL in result.stdout
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_ci_env_suppresses_the_notice(tmp_path):
|
|
82
|
+
home, project = stale_home(tmp_path)
|
|
83
|
+
|
|
84
|
+
result = run(home, project, CI="1")
|
|
85
|
+
|
|
86
|
+
assert result.exit_code == 0
|
|
87
|
+
assert result.stderr == ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_no_update_check_env_suppresses_the_notice(tmp_path):
|
|
91
|
+
home, project = stale_home(tmp_path)
|
|
92
|
+
|
|
93
|
+
result = run(home, project, AGENTSQUIRE_NO_UPDATE_CHECK="1")
|
|
94
|
+
|
|
95
|
+
assert result.exit_code == 0
|
|
96
|
+
assert result.stderr == ""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_real_entry_point_keeps_stdout_stderr_and_exit_code_separate(tmp_path):
|
|
100
|
+
# Strongest evidence: the wired callback fires before dispatch in a real
|
|
101
|
+
# process, the notice lands on stderr, and stdout/exit code are untouched.
|
|
102
|
+
home, project = stale_home(tmp_path)
|
|
103
|
+
env = {k: v for k, v in os.environ.items() if k not in ("CI", "AGENTSQUIRE_NO_UPDATE_CHECK")}
|
|
104
|
+
env.update({
|
|
105
|
+
"PYTHONPATH": str(SRC),
|
|
106
|
+
"AGENTSQUIRE_HOME": str(home),
|
|
107
|
+
"AGENTSQUIRE_PROJECT": str(project),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
proc = subprocess.run(
|
|
111
|
+
[sys.executable, "-c",
|
|
112
|
+
"import sys; sys.argv = ['squire', 'skills', 'status', '--scope', 'user'];"
|
|
113
|
+
" from agentsquire.console import main; main()"],
|
|
114
|
+
stdin=subprocess.DEVNULL, capture_output=True, timeout=60, env=env,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
assert proc.returncode == 0
|
|
118
|
+
assert SKILL.encode() in proc.stdout
|
|
119
|
+
assert b"squire skills update" in proc.stderr
|
|
120
|
+
assert b"a skills update is available" not in proc.stdout
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""T-06 / REQ-08, REQ-09, REQ-10, REQ-01: the `squire guide` command.
|
|
2
|
+
|
|
3
|
+
Bare `guide` lists the topics; `guide <topic>` pages the canonical doc via
|
|
4
|
+
click.echo; an unknown topic is a clean non-zero error naming the valid topics.
|
|
5
|
+
Content resolves from the packaged agentsquire/_docs (so it works from an
|
|
6
|
+
installed wheel), falling back to the repo docs/ + README only in a source
|
|
7
|
+
checkout. The wheel-resolution claim is proved by build-and-install, following
|
|
8
|
+
the tests/test_bundled_source.py pattern.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from agentsquire.console import main
|
|
16
|
+
from click.testing import CliRunner
|
|
17
|
+
|
|
18
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
19
|
+
|
|
20
|
+
# Topic -> a heading known to appear only in that topic's canonical source.
|
|
21
|
+
TOPIC_HEADINGS = {
|
|
22
|
+
"api": "# AgentSquire Python API reference",
|
|
23
|
+
"harnesses": "# Harness reference",
|
|
24
|
+
"integration": "## Consumer integration guide",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def invoke(*args):
|
|
29
|
+
# Suppress the root staleness notice so guide output is deterministic.
|
|
30
|
+
return CliRunner().invoke(
|
|
31
|
+
main, list(args), env={"AGENTSQUIRE_NO_UPDATE_CHECK": "1"}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_root_help_lists_the_guide_command():
|
|
36
|
+
# REQ-01: guide is mounted on the root group alongside skills.
|
|
37
|
+
result = invoke("--help")
|
|
38
|
+
assert result.exit_code == 0
|
|
39
|
+
assert "guide" in result.output
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_bare_guide_lists_the_three_topics():
|
|
43
|
+
# REQ-08: no argument -> exit 0, listing api, harnesses, integration.
|
|
44
|
+
result = invoke("guide")
|
|
45
|
+
assert result.exit_code == 0
|
|
46
|
+
for topic in TOPIC_HEADINGS:
|
|
47
|
+
assert topic in result.output
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_guide_topic_pages_its_canonical_doc():
|
|
51
|
+
# REQ-09: each topic prints its own doc's known heading.
|
|
52
|
+
for topic, heading in TOPIC_HEADINGS.items():
|
|
53
|
+
result = invoke("guide", topic)
|
|
54
|
+
assert result.exit_code == 0, result.output
|
|
55
|
+
assert heading in result.output
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_unknown_topic_is_a_clean_error_naming_valid_topics():
|
|
59
|
+
# REQ-09: a bogus topic exits non-zero and names the valid topics.
|
|
60
|
+
result = invoke("guide", "bogus")
|
|
61
|
+
assert result.exit_code != 0
|
|
62
|
+
for topic in TOPIC_HEADINGS:
|
|
63
|
+
assert topic in result.output
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_guide_resolves_from_the_installed_wheel(tmp_path):
|
|
67
|
+
"""REQ-10: with only the built wheel on the path (no repo checkout),
|
|
68
|
+
`squire guide api` still prints api.md content - proving it resolves from
|
|
69
|
+
the packaged agentsquire/_docs, not the source tree."""
|
|
70
|
+
outdir = tmp_path / "dist"
|
|
71
|
+
subprocess.run(
|
|
72
|
+
[
|
|
73
|
+
sys.executable, "-m", "build", "--wheel", "--no-isolation",
|
|
74
|
+
"--outdir", str(outdir),
|
|
75
|
+
],
|
|
76
|
+
cwd=PROJECT_ROOT,
|
|
77
|
+
check=True,
|
|
78
|
+
capture_output=True,
|
|
79
|
+
)
|
|
80
|
+
wheel = next(outdir.glob("*.whl"))
|
|
81
|
+
|
|
82
|
+
site = tmp_path / "site"
|
|
83
|
+
subprocess.run(
|
|
84
|
+
[
|
|
85
|
+
sys.executable, "-m", "pip", "install",
|
|
86
|
+
"--no-deps", "--no-index", "--target", str(site), str(wheel),
|
|
87
|
+
],
|
|
88
|
+
check=True,
|
|
89
|
+
capture_output=True,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Runner asserts agentsquire imports from the wheel-only site (not the
|
|
93
|
+
# editable source install), then runs `guide api`. Running outside the repo
|
|
94
|
+
# with PYTHONPATH=site means the source-checkout fallback cannot reach the
|
|
95
|
+
# repo docs/, so a printed heading can only come from the packaged _docs.
|
|
96
|
+
runner = tmp_path / "run_guide.py"
|
|
97
|
+
runner.write_text(
|
|
98
|
+
"import sys\n"
|
|
99
|
+
"import agentsquire\n"
|
|
100
|
+
"assert agentsquire.__file__.startswith(sys.argv[1]), agentsquire.__file__\n"
|
|
101
|
+
"from agentsquire.console import main\n"
|
|
102
|
+
"main(args=sys.argv[2:], prog_name='squire', standalone_mode=False)\n"
|
|
103
|
+
)
|
|
104
|
+
result = subprocess.run(
|
|
105
|
+
[sys.executable, str(runner), str(site), "guide", "api"],
|
|
106
|
+
cwd=tmp_path,
|
|
107
|
+
env={
|
|
108
|
+
"PYTHONPATH": str(site),
|
|
109
|
+
"AGENTSQUIRE_NO_UPDATE_CHECK": "1",
|
|
110
|
+
"PATH": "/usr/bin:/bin",
|
|
111
|
+
},
|
|
112
|
+
check=True,
|
|
113
|
+
capture_output=True,
|
|
114
|
+
text=True,
|
|
115
|
+
)
|
|
116
|
+
assert TOPIC_HEADINGS["api"] in result.stdout
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""T-05 / REQ-07, REQ-10: the wheel force-includes the three canonical docs.
|
|
2
|
+
|
|
3
|
+
The `guide` command needs a single authoritative source for its pages with no
|
|
4
|
+
hand-maintained in-package duplicate. The build force-includes docs/api.md,
|
|
5
|
+
docs/harnesses.md, and README.md into the wheel under agentsquire/_docs/, and the
|
|
6
|
+
package source commits no copy of them. Verified by build-and-install, following
|
|
7
|
+
the tests/test_bundled_source.py pattern.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import zipfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
18
|
+
|
|
19
|
+
# Force-include maps the three sources to these packaged paths.
|
|
20
|
+
PACKAGED_DOCS = {
|
|
21
|
+
"agentsquire/_docs/api.md": PROJECT_ROOT / "docs" / "api.md",
|
|
22
|
+
"agentsquire/_docs/harnesses.md": PROJECT_ROOT / "docs" / "harnesses.md",
|
|
23
|
+
"agentsquire/_docs/integration.md": PROJECT_ROOT / "README.md",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture(scope="module")
|
|
28
|
+
def built_wheel(tmp_path_factory):
|
|
29
|
+
"""Build the agentsquire wheel into a temp dir and hand back its path."""
|
|
30
|
+
outdir = tmp_path_factory.mktemp("dist")
|
|
31
|
+
subprocess.run(
|
|
32
|
+
[
|
|
33
|
+
sys.executable, "-m", "build", "--wheel", "--no-isolation",
|
|
34
|
+
"--outdir", str(outdir),
|
|
35
|
+
],
|
|
36
|
+
cwd=PROJECT_ROOT,
|
|
37
|
+
check=True,
|
|
38
|
+
capture_output=True,
|
|
39
|
+
)
|
|
40
|
+
return next(outdir.glob("*.whl"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _record_paths(wheel: Path) -> set[str]:
|
|
44
|
+
"""The path column of the wheel's dist-info RECORD."""
|
|
45
|
+
with zipfile.ZipFile(wheel) as zf:
|
|
46
|
+
record = next(n for n in zf.namelist() if n.endswith(".dist-info/RECORD"))
|
|
47
|
+
text = zf.read(record).decode()
|
|
48
|
+
return {line.split(",", 1)[0] for line in text.splitlines() if line}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_wheel_record_lists_the_three_packaged_docs(built_wheel):
|
|
52
|
+
paths = _record_paths(built_wheel)
|
|
53
|
+
for packaged in PACKAGED_DOCS:
|
|
54
|
+
assert packaged in paths, f"{packaged} missing from wheel RECORD"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_packaged_docs_carry_their_source_bytes(built_wheel):
|
|
58
|
+
with zipfile.ZipFile(built_wheel) as zf:
|
|
59
|
+
for packaged, source in PACKAGED_DOCS.items():
|
|
60
|
+
assert zf.read(packaged).decode() == source.read_text()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_no_canonical_doc_is_committed_under_the_package_source():
|
|
64
|
+
out = subprocess.run(
|
|
65
|
+
["git", "ls-files", "src/agentsquire"],
|
|
66
|
+
cwd=PROJECT_ROOT,
|
|
67
|
+
check=True,
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True,
|
|
70
|
+
)
|
|
71
|
+
tracked = out.stdout.splitlines()
|
|
72
|
+
offenders = [
|
|
73
|
+
p for p in tracked
|
|
74
|
+
if Path(p).name in {"api.md", "harnesses.md", "integration.md"}
|
|
75
|
+
or "/_docs/" in p
|
|
76
|
+
]
|
|
77
|
+
assert offenders == [], f"canonical docs must live in docs/ only, not: {offenders}"
|
|
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
|