agentharnesses-cli 0.1.2__tar.gz → 0.1.4__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.
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentharnesses-cli
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: CLI tools for agentharnesses.io
5
5
  Project-URL: Homepage, https://agentharnesses.io
6
6
  Project-URL: Repository, https://github.com/agentharnesses/cli
7
7
  License-Expression: Apache-2.0
8
8
  Requires-Python: >=3.10
9
9
  Requires-Dist: click>=8.1
10
+ Requires-Dist: harnesses-ref>=0.1.0
10
11
  Description-Content-Type: text/markdown
11
12
 
12
13
  # agentharnesses-cli
@@ -64,6 +65,35 @@ When using the `claude` preset (default), also installs:
64
65
  └── SKILL.md
65
66
  ```
66
67
 
68
+ The metaskill is cloned fresh from [agentharnesses/metaskill](https://github.com/agentharnesses/metaskill) at init time, so you always get the latest version.
69
+
70
+ ### `ahar validate`
71
+
72
+ Validate a harness directory structure:
73
+
74
+ ```bash
75
+ ahar validate ./my-harness
76
+ ```
77
+
78
+ ### `ahar read`
79
+
80
+ Read a property from a harness's `HARNESS.md` frontmatter:
81
+
82
+ ```bash
83
+ ahar read ./my-harness name
84
+ ahar read ./my-harness description
85
+ ```
86
+
87
+ ### `ahar prompt`
88
+
89
+ Render a harness as prompt XML for agent injection:
90
+
91
+ ```bash
92
+ ahar prompt ./my-harness
93
+ ```
94
+
95
+ These commands are backed by [harnesses-ref](https://pypi.org/project/harnesses-ref/), the reference implementation for the Agent Harnesses standard.
96
+
67
97
  ## Publishing
68
98
 
69
99
  Releases are published to PyPI automatically when a version tag is pushed:
@@ -53,6 +53,35 @@ When using the `claude` preset (default), also installs:
53
53
  └── SKILL.md
54
54
  ```
55
55
 
56
+ The metaskill is cloned fresh from [agentharnesses/metaskill](https://github.com/agentharnesses/metaskill) at init time, so you always get the latest version.
57
+
58
+ ### `ahar validate`
59
+
60
+ Validate a harness directory structure:
61
+
62
+ ```bash
63
+ ahar validate ./my-harness
64
+ ```
65
+
66
+ ### `ahar read`
67
+
68
+ Read a property from a harness's `HARNESS.md` frontmatter:
69
+
70
+ ```bash
71
+ ahar read ./my-harness name
72
+ ahar read ./my-harness description
73
+ ```
74
+
75
+ ### `ahar prompt`
76
+
77
+ Render a harness as prompt XML for agent injection:
78
+
79
+ ```bash
80
+ ahar prompt ./my-harness
81
+ ```
82
+
83
+ These commands are backed by [harnesses-ref](https://pypi.org/project/harnesses-ref/), the reference implementation for the Agent Harnesses standard.
84
+
56
85
  ## Publishing
57
86
 
58
87
  Releases are published to PyPI automatically when a version tag is pushed:
@@ -7,6 +7,7 @@ readme = "README.md"
7
7
  requires-python = ">=3.10"
8
8
  dependencies = [
9
9
  "click>=8.1",
10
+ "harnesses-ref>=0.1.0",
10
11
  ]
11
12
 
12
13
  [project.urls]
@@ -25,3 +26,8 @@ source = "vcs"
25
26
 
26
27
  [tool.hatch.build.targets.wheel]
27
28
  packages = ["src/ahar"]
29
+
30
+ [dependency-groups]
31
+ dev = [
32
+ "pytest>=7.0",
33
+ ]
@@ -14,39 +14,25 @@ _MAINTAIN_SKILLS_INDEX_DEST = "skills/maintenance/SKILLS.md"
14
14
  _MAINTAIN_SKILL = """\
15
15
  ---
16
16
  name: modify-harness
17
- description: Use when asked to maintain, update, or extend this harness updating HARNESS.md, adding skills and references, managing the skill index.
17
+ description: Update harness structure files HARNESS.md, SKILLS.md indexes, REFERENCES.mdto keep routing and descriptions accurate as the harness evolves.
18
18
  ---
19
19
 
20
- ## Maintaining the Harness
20
+ ## Role
21
21
 
22
- When asked to maintain, update, or extend this harness, follow these conventions.
22
+ Keep the harness self-consistent when skills or references are added, renamed, or removed.
23
23
 
24
- When **maintaining the harness** (adding, moving, or renaming files), use `reverse_disclose.py` to find every `.md` file above the target that links to it — so you can update all references that would break:
24
+ ## What to do
25
25
 
26
- ```
27
- python3 .claude/skills/agent-harnesses/scripts/reverse_disclose.py <target_path>
28
- ```
26
+ 1. Use reverse progressive disclosure (via the `agent-harnesses` skill) to find which index files reference the target path
27
+ 2. Read the current state of each affected file
28
+ 3. Apply the change: add, update, or remove the relevant entry
29
+ 4. Ensure descriptions remain accurate and routing summaries reflect actual contents
29
30
 
30
- Run this before and after any structural change. The output lists each ancestor `.md` file that references the target, with line numbers and link text, so nothing is left pointing to a stale path.
31
+ ## Conventions
31
32
 
32
- ### HARNESS.md
33
- - Keep the `## Skills` section in sync with entries in `skills/SKILLS.md`
34
- - Keep the `## References` section in sync with entries in `references/REFERENCES.md`
35
- - Update the `description` frontmatter field when the harness scope changes
36
-
37
- ### Adding a skill bucket
38
- 1. Create `skills/<bucket-name>/<skill-name>/SKILL.md` with a frontmatter `name` and `description`
39
- 2. Add an entry to `skills/<bucket-name>/SKILLS.md` summarizing when to use the skill
40
- 3. Ensure `skills/SKILLS.md` references the bucket
41
- 4. Add a bullet to the `## Skills` section in `HARNESS.md`
42
-
43
- ### Adding a reference document
44
- 1. Add the document to `references/`
45
- 2. Add an entry to `references/REFERENCES.md` describing the document's purpose
46
- 3. Add a bullet to the `## References` section in `HARNESS.md`
47
-
48
- ### General conventions
49
- - Keep skill descriptions actionable: "Use when..." not "This skill..."
33
+ - Keep `HARNESS.md` `## Skills` and `## References` sections in sync with `skills/SKILLS.md` and `references/REFERENCES.md`
34
+ - Update the `description` frontmatter in `HARNESS.md` when the harness scope changes
35
+ - Skill descriptions should be actionable: "Use when..." not "This skill..."
50
36
  - Reference documents should be stable facts; skill buckets contain executable guidance
51
37
  - Prefer updating existing skill buckets over creating new ones when scope overlaps
52
38
  """
@@ -81,17 +67,9 @@ TODO: write the entry message Claude should internalize when this harness loads.
81
67
 
82
68
  ## How to Find Information for Claude
83
69
 
84
- Use the `agent-harnesses` skill to explore the harness, just in time, based on prompts from the user. Run `disclose.py` with `python3` against this harness directory to progressively explore its contents — select only what is relevant and repeat until the session is complete, then read the returned resources.
85
-
86
- Do not load skills or references speculatively. Use `disclose.py` to find resources when necessary. Any time you need to find anything in the harness, and you don't already know where it exists, use `disclose.py`.
87
-
88
- When **maintaining the harness** (adding, moving, or renaming files), use `reverse_disclose.py` to find every `.md` file above the target that links to it — so you can update all references that would break:
89
-
90
- ```
91
- python3 .claude/skills/agent-harnesses/scripts/reverse_disclose.py <target_path>
92
- ```
70
+ Use the `agent-harnesses` skill to explore the harness just in time, based on prompts from the user. Select only what is relevant and repeat until the session is complete, then read the returned resources.
93
71
 
94
- Run this before and after any structural change. The output lists each ancestor `.md` file that references the target, with line numbers and link text, so nothing is left pointing to a stale path.
72
+ When **maintaining the harness** (adding, moving, or renaming files), consult the `agent-harnesses` skill for reverse progressive disclosure to keep routing files in sync.
95
73
 
96
74
  ## Skills
97
75
 
File without changes
@@ -0,0 +1,137 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+ from click.testing import CliRunner
6
+
7
+ from ahar.commands.init import scaffold
8
+ from ahar.main import cli
9
+
10
+
11
+ # --- scaffold unit tests ---
12
+
13
+ def test_scaffold_creates_required_files(tmp_path):
14
+ scaffold(str(tmp_path), "my-harness")
15
+ assert (tmp_path / "HARNESS.md").exists()
16
+ assert (tmp_path / "README.md").exists()
17
+ assert (tmp_path / ".gitignore").exists()
18
+ assert (tmp_path / ".claude" / "settings.json").exists()
19
+ assert (tmp_path / "skills" / "SKILLS.md").exists()
20
+ assert (tmp_path / "references" / "REFERENCES.md").exists()
21
+
22
+
23
+ def test_scaffold_harness_md_contains_name(tmp_path):
24
+ scaffold(str(tmp_path), "my-harness")
25
+ content = (tmp_path / "HARNESS.md").read_text()
26
+ assert "name: my-harness" in content
27
+
28
+
29
+ def test_scaffold_settings_registers_marketplace(tmp_path):
30
+ scaffold(str(tmp_path), "my-harness")
31
+ settings = json.loads((tmp_path / ".claude" / "settings.json").read_text())
32
+ assert "my-harness" in settings["extraKnownMarketplaces"]
33
+ assert settings["extraKnownMarketplaces"]["my-harness"]["source"]["source"] == "directory"
34
+ assert settings["extraKnownMarketplaces"]["my-harness"]["source"]["path"] == str(tmp_path)
35
+
36
+
37
+ def test_scaffold_settings_enables_plugin(tmp_path):
38
+ scaffold(str(tmp_path), "my-harness")
39
+ settings = json.loads((tmp_path / ".claude" / "settings.json").read_text())
40
+ assert "my-harness@my-harness" in settings["enabledPlugins"]
41
+ assert settings["enabledPlugins"]["my-harness@my-harness"] is True
42
+
43
+
44
+ def test_scaffold_gitignore_excludes_settings(tmp_path):
45
+ scaffold(str(tmp_path), "my-harness")
46
+ content = (tmp_path / ".gitignore").read_text()
47
+ assert ".claude/settings.json" in content
48
+
49
+
50
+ # --- init CLI tests ---
51
+
52
+ def test_init_empty_preset_creates_scaffold(tmp_path):
53
+ runner = CliRunner()
54
+ with runner.isolated_filesystem(temp_dir=tmp_path):
55
+ result = runner.invoke(cli, ["init", "test-harness"], input="empty\n")
56
+ assert result.exit_code == 0
57
+ assert Path("HARNESS.md").exists()
58
+ assert Path("skills/SKILLS.md").exists()
59
+ assert Path("references/REFERENCES.md").exists()
60
+
61
+
62
+ def test_init_uses_directory_name_as_default(tmp_path):
63
+ runner = CliRunner()
64
+ with runner.isolated_filesystem(temp_dir=tmp_path):
65
+ result = runner.invoke(cli, ["init"], input="empty\n")
66
+ assert result.exit_code == 0
67
+ content = Path("HARNESS.md").read_text()
68
+ assert f"name: {Path.cwd().name}" in content
69
+
70
+
71
+ def test_init_fails_if_already_initialized(tmp_path):
72
+ runner = CliRunner()
73
+ with runner.isolated_filesystem(temp_dir=tmp_path):
74
+ Path("HARNESS.md").write_text("---\nname: existing\ndescription: x\n---\n")
75
+ result = runner.invoke(cli, ["init"], input="empty\n")
76
+ assert result.exit_code == 1
77
+
78
+
79
+ def test_init_empty_preset_skips_metaskill(tmp_path):
80
+ runner = CliRunner()
81
+ with runner.isolated_filesystem(temp_dir=tmp_path):
82
+ result = runner.invoke(cli, ["init", "test-harness"], input="empty\n")
83
+ assert result.exit_code == 0
84
+ assert not Path(".claude/skills/agent-harnesses").exists()
85
+
86
+
87
+ # --- validate CLI tests ---
88
+
89
+ def _make_valid_harness(path: Path) -> Path:
90
+ (path / "HARNESS.md").write_text(
91
+ "---\nname: Test Harness\ndescription: A test harness.\n---\nBody.\n"
92
+ )
93
+ return path
94
+
95
+
96
+ def test_validate_valid_harness(tmp_path):
97
+ _make_valid_harness(tmp_path)
98
+ runner = CliRunner()
99
+ result = runner.invoke(cli, ["validate", str(tmp_path)])
100
+ assert result.exit_code == 0
101
+ assert "valid" in result.output
102
+
103
+
104
+ def test_validate_invalid_harness(tmp_path):
105
+ (tmp_path / "HARNESS.md").write_text("no frontmatter")
106
+ runner = CliRunner()
107
+ result = runner.invoke(cli, ["validate", str(tmp_path)])
108
+ assert result.exit_code == 1
109
+
110
+
111
+ # --- read CLI tests ---
112
+
113
+ def test_read_name(tmp_path):
114
+ _make_valid_harness(tmp_path)
115
+ runner = CliRunner()
116
+ result = runner.invoke(cli, ["read", str(tmp_path), "name"])
117
+ assert result.exit_code == 0
118
+ assert result.output.strip() == "Test Harness"
119
+
120
+
121
+ def test_read_description(tmp_path):
122
+ _make_valid_harness(tmp_path)
123
+ runner = CliRunner()
124
+ result = runner.invoke(cli, ["read", str(tmp_path), "description"])
125
+ assert result.exit_code == 0
126
+ assert result.output.strip() == "A test harness."
127
+
128
+
129
+ # --- prompt CLI tests ---
130
+
131
+ def test_prompt_renders_xml(tmp_path):
132
+ _make_valid_harness(tmp_path)
133
+ runner = CliRunner()
134
+ result = runner.invoke(cli, ["prompt", str(tmp_path)])
135
+ assert result.exit_code == 0
136
+ assert "<harness" in result.output
137
+ assert "Test Harness" in result.output