local-agent-harness 0.1.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.
- local_agent_harness-0.1.0/PKG-INFO +91 -0
- local_agent_harness-0.1.0/README.md +61 -0
- local_agent_harness-0.1.0/pyproject.toml +83 -0
- local_agent_harness-0.1.0/src/local_agent_harness/__init__.py +4 -0
- local_agent_harness-0.1.0/src/local_agent_harness/__main__.py +4 -0
- local_agent_harness-0.1.0/src/local_agent_harness/cli/__init__.py +0 -0
- local_agent_harness-0.1.0/src/local_agent_harness/cli/app.py +34 -0
- local_agent_harness-0.1.0/src/local_agent_harness/cli/cmd_assess.py +33 -0
- local_agent_harness-0.1.0/src/local_agent_harness/cli/cmd_check.py +29 -0
- local_agent_harness-0.1.0/src/local_agent_harness/cli/cmd_init.py +26 -0
- local_agent_harness-0.1.0/src/local_agent_harness/cli/cmd_refresh.py +24 -0
- local_agent_harness-0.1.0/src/local_agent_harness/cli/cmd_report.py +49 -0
- local_agent_harness-0.1.0/src/local_agent_harness/cli/cmd_setup.py +92 -0
- local_agent_harness-0.1.0/src/local_agent_harness/cli/cmd_validate.py +36 -0
- local_agent_harness-0.1.0/src/local_agent_harness/cli/cmd_version.py +9 -0
- local_agent_harness-0.1.0/src/local_agent_harness/core/__init__.py +4 -0
- local_agent_harness-0.1.0/src/local_agent_harness/core/_paths.py +29 -0
- local_agent_harness-0.1.0/src/local_agent_harness/core/assess_repo.py +222 -0
- local_agent_harness-0.1.0/src/local_agent_harness/core/diff_manifests.py +214 -0
- local_agent_harness-0.1.0/src/local_agent_harness/core/manifest_regression.py +123 -0
- local_agent_harness-0.1.0/src/local_agent_harness/core/readiness_report.py +126 -0
- local_agent_harness-0.1.0/src/local_agent_harness/core/redaction_smoke.py +85 -0
- local_agent_harness-0.1.0/src/local_agent_harness/core/scaffold_manifests.py +199 -0
- local_agent_harness-0.1.0/src/local_agent_harness/py.typed +0 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/SKILL.md +251 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/AGENTS.md.tmpl +83 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/GROUNDING.md.tmpl +51 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/ci/governance.yml.tmpl +27 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/ci/verify.yml.tmpl +30 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/devcontainer.json.tmpl +28 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/plan.md.tmpl +37 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/pre-commit-config.yaml.tmpl +15 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/readiness-report.md.tmpl +43 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/repo-skill.SKILL.md.tmpl +36 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/runtime-overlays/CLAUDE.md.tmpl +25 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/runtime-overlays/codex.config.tmpl +23 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/runtime-overlays/copilot-cli.tmpl +21 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/runtime-overlays/cursor-rules.tmpl +21 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/references/ai-readiness-rubric.md +70 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/references/manifest-anti-patterns.md +16 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/references/memory-governance.md +44 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/references/runtime-overlays.md +29 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/references/source-mapping.md +41 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/references/stages.md +155 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/references/tool-dag-patterns.md +36 -0
- local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/references/verify-gate-catalog.md +39 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: local-agent-harness
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Maturity-aware harness manager for local AI coding agents (Claude Code, Codex CLI, Copilot CLI, Cursor). Audit, init, and refresh AGENTS.md / GROUNDING.md / CI / sandbox at the right S0–S3 stage; ships an installable skill.
|
|
5
|
+
Keywords: ai-agents,claude-code,codex-cli,copilot-cli,cursor,agents-md,harness,skill,ai-readiness
|
|
6
|
+
Author: Grammy Jiang
|
|
7
|
+
Author-email: Grammy Jiang <grammy.jiang@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development
|
|
17
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: typer>=0.12,<1
|
|
20
|
+
Requires-Dist: pytest>=8.0 ; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-cov>=5.0 ; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff>=0.6 ; extra == 'dev'
|
|
23
|
+
Requires-Dist: mypy>=1.10 ; extra == 'dev'
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Project-URL: Homepage, https://github.com/grammy-jiang/local-agent-harness
|
|
26
|
+
Project-URL: Repository, https://github.com/grammy-jiang/local-agent-harness
|
|
27
|
+
Project-URL: Issues, https://github.com/grammy-jiang/local-agent-harness/issues
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# local-agent-harness
|
|
32
|
+
|
|
33
|
+
Maturity-aware harness manager for local AI coding agents (Claude Code,
|
|
34
|
+
Codex CLI, GitHub Copilot CLI, Cursor).
|
|
35
|
+
|
|
36
|
+
`local-agent-harness` makes a repository ready for AI-agent-assisted
|
|
37
|
+
development from two directions:
|
|
38
|
+
|
|
39
|
+
1. **Make the agent work better.** Generate `AGENTS.md` / `GROUNDING.md`,
|
|
40
|
+
per-runtime overlays (`CLAUDE.md`, `.codex/config`,
|
|
41
|
+
`.github/copilot-cli.md`, `.cursor/rules`), tool DAGs, permission
|
|
42
|
+
ladders, governed memory, and cost/context budgets.
|
|
43
|
+
2. **Make the repository ready.** Render sandbox/devcontainer,
|
|
44
|
+
`.pre-commit-config.yaml`, verify CI, governance CI, secrets/SAST/dep
|
|
45
|
+
scans, and a machine-readable AI-readiness score.
|
|
46
|
+
|
|
47
|
+
The harness *evolves with the repo*. A blank S0 skeleton receives a
|
|
48
|
+
minimal kit; a mature S3 codebase gets the full set of governance gates.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pipx install local-agent-harness
|
|
54
|
+
local-agent-harness setup # install the bundled skill into ~/.claude, ~/.copilot, ~/.codex
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`setup` only installs into agent skill roots whose parent directory
|
|
58
|
+
already exists. Override with `--target PATH` (repeatable) to install
|
|
59
|
+
into project-local locations like `.github/skills/`.
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
local-agent-harness assess # detect maturity stage + AI-readiness score
|
|
65
|
+
local-agent-harness check # audit manifests for drift (read-only)
|
|
66
|
+
local-agent-harness init --runtime claude-code --runtime copilot-cli
|
|
67
|
+
local-agent-harness refresh --apply # rewrite stale manifests (backups written)
|
|
68
|
+
local-agent-harness report --out .agent/readiness.md
|
|
69
|
+
local-agent-harness validate # regression + redaction smoke checks
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Three modes:
|
|
73
|
+
|
|
74
|
+
| Mode | Writes? | Use when |
|
|
75
|
+
|-----------|---------|----------|
|
|
76
|
+
| `check` | no | Audit-only — CI gate or quick diagnosis. |
|
|
77
|
+
| `init` | yes | Render *missing* manifests; never overwrites. |
|
|
78
|
+
| `refresh` | yes (with `--apply`) | Back up + rewrite *stale or relaxed* manifests. |
|
|
79
|
+
|
|
80
|
+
## Stages (S0 → S3)
|
|
81
|
+
|
|
82
|
+
| Stage | Repo signal | Default kit |
|
|
83
|
+
|-------|----------------------------------------------|---------------------------------------|
|
|
84
|
+
| S0 | empty / no source / no tests / no CI | AGENTS.md, GROUNDING.md, plan.md |
|
|
85
|
+
| S1 | source + tests OR CI | + pre-commit, devcontainer, verify CI |
|
|
86
|
+
| S2 | source + tests + CI | + governance CI, redaction smoke |
|
|
87
|
+
| S3 | + tags/releases | + readiness gate, no-regression check |
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# local-agent-harness
|
|
2
|
+
|
|
3
|
+
Maturity-aware harness manager for local AI coding agents (Claude Code,
|
|
4
|
+
Codex CLI, GitHub Copilot CLI, Cursor).
|
|
5
|
+
|
|
6
|
+
`local-agent-harness` makes a repository ready for AI-agent-assisted
|
|
7
|
+
development from two directions:
|
|
8
|
+
|
|
9
|
+
1. **Make the agent work better.** Generate `AGENTS.md` / `GROUNDING.md`,
|
|
10
|
+
per-runtime overlays (`CLAUDE.md`, `.codex/config`,
|
|
11
|
+
`.github/copilot-cli.md`, `.cursor/rules`), tool DAGs, permission
|
|
12
|
+
ladders, governed memory, and cost/context budgets.
|
|
13
|
+
2. **Make the repository ready.** Render sandbox/devcontainer,
|
|
14
|
+
`.pre-commit-config.yaml`, verify CI, governance CI, secrets/SAST/dep
|
|
15
|
+
scans, and a machine-readable AI-readiness score.
|
|
16
|
+
|
|
17
|
+
The harness *evolves with the repo*. A blank S0 skeleton receives a
|
|
18
|
+
minimal kit; a mature S3 codebase gets the full set of governance gates.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pipx install local-agent-harness
|
|
24
|
+
local-agent-harness setup # install the bundled skill into ~/.claude, ~/.copilot, ~/.codex
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`setup` only installs into agent skill roots whose parent directory
|
|
28
|
+
already exists. Override with `--target PATH` (repeatable) to install
|
|
29
|
+
into project-local locations like `.github/skills/`.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
local-agent-harness assess # detect maturity stage + AI-readiness score
|
|
35
|
+
local-agent-harness check # audit manifests for drift (read-only)
|
|
36
|
+
local-agent-harness init --runtime claude-code --runtime copilot-cli
|
|
37
|
+
local-agent-harness refresh --apply # rewrite stale manifests (backups written)
|
|
38
|
+
local-agent-harness report --out .agent/readiness.md
|
|
39
|
+
local-agent-harness validate # regression + redaction smoke checks
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Three modes:
|
|
43
|
+
|
|
44
|
+
| Mode | Writes? | Use when |
|
|
45
|
+
|-----------|---------|----------|
|
|
46
|
+
| `check` | no | Audit-only — CI gate or quick diagnosis. |
|
|
47
|
+
| `init` | yes | Render *missing* manifests; never overwrites. |
|
|
48
|
+
| `refresh` | yes (with `--apply`) | Back up + rewrite *stale or relaxed* manifests. |
|
|
49
|
+
|
|
50
|
+
## Stages (S0 → S3)
|
|
51
|
+
|
|
52
|
+
| Stage | Repo signal | Default kit |
|
|
53
|
+
|-------|----------------------------------------------|---------------------------------------|
|
|
54
|
+
| S0 | empty / no source / no tests / no CI | AGENTS.md, GROUNDING.md, plan.md |
|
|
55
|
+
| S1 | source + tests OR CI | + pre-commit, devcontainer, verify CI |
|
|
56
|
+
| S2 | source + tests + CI | + governance CI, redaction smoke |
|
|
57
|
+
| S3 | + tags/releases | + readiness gate, no-regression check |
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.10.12,<0.12.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "local-agent-harness"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Maturity-aware harness manager for local AI coding agents (Claude Code, Codex CLI, Copilot CLI, Cursor). Audit, init, and refresh AGENTS.md / GROUNDING.md / CI / sandbox at the right S0–S3 stage; ships an installable skill."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Grammy Jiang", email = "grammy.jiang@gmail.com"}
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.11"
|
|
15
|
+
keywords = [
|
|
16
|
+
"ai-agents",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"codex-cli",
|
|
19
|
+
"copilot-cli",
|
|
20
|
+
"cursor",
|
|
21
|
+
"agents-md",
|
|
22
|
+
"harness",
|
|
23
|
+
"skill",
|
|
24
|
+
"ai-readiness"
|
|
25
|
+
]
|
|
26
|
+
classifiers = [
|
|
27
|
+
"Development Status :: 4 - Beta",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Programming Language :: Python :: 3.13",
|
|
34
|
+
"Topic :: Software Development",
|
|
35
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
36
|
+
"Typing :: Typed"
|
|
37
|
+
]
|
|
38
|
+
dependencies = [
|
|
39
|
+
"typer>=0.12,<1"
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
dev = [
|
|
44
|
+
"pytest>=8.0",
|
|
45
|
+
"pytest-cov>=5.0",
|
|
46
|
+
"ruff>=0.6",
|
|
47
|
+
"mypy>=1.10"
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.scripts]
|
|
51
|
+
local-agent-harness = "local_agent_harness.cli.app:app"
|
|
52
|
+
|
|
53
|
+
[project.urls]
|
|
54
|
+
Homepage = "https://github.com/grammy-jiang/local-agent-harness"
|
|
55
|
+
Repository = "https://github.com/grammy-jiang/local-agent-harness"
|
|
56
|
+
Issues = "https://github.com/grammy-jiang/local-agent-harness/issues"
|
|
57
|
+
|
|
58
|
+
[tool.uv.build-backend]
|
|
59
|
+
module-name = "local_agent_harness"
|
|
60
|
+
module-root = "src"
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
line-length = 100
|
|
64
|
+
target-version = "py311"
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint]
|
|
67
|
+
ignore = ["E701", "E731"]
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint.per-file-ignores]
|
|
70
|
+
"tests/**" = ["E402"]
|
|
71
|
+
"src/local_agent_harness/core/_paths.py" = ["E402"]
|
|
72
|
+
"src/local_agent_harness/core/manifest_regression.py" = ["E402"]
|
|
73
|
+
"src/local_agent_harness/core/diff_manifests.py" = ["E402"]
|
|
74
|
+
"src/local_agent_harness/core/scaffold_manifests.py" = ["E402"]
|
|
75
|
+
|
|
76
|
+
[tool.mypy]
|
|
77
|
+
python_version = "3.11"
|
|
78
|
+
strict = true
|
|
79
|
+
files = ["src/local_agent_harness"]
|
|
80
|
+
|
|
81
|
+
[tool.pytest.ini_options]
|
|
82
|
+
testpaths = ["tests"]
|
|
83
|
+
addopts = "-ra"
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Typer entry point wiring all subcommands."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from . import (
|
|
7
|
+
cmd_assess,
|
|
8
|
+
cmd_check,
|
|
9
|
+
cmd_init,
|
|
10
|
+
cmd_refresh,
|
|
11
|
+
cmd_report,
|
|
12
|
+
cmd_setup,
|
|
13
|
+
cmd_validate,
|
|
14
|
+
cmd_version,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
add_completion=False,
|
|
19
|
+
no_args_is_help=True,
|
|
20
|
+
help="Maturity-aware harness manager for local AI coding agents.",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
app.command("setup", help="Install the bundled skill into agent skill directories.")(cmd_setup.run)
|
|
24
|
+
app.command("assess", help="Detect maturity stage and AI-readiness score.")(cmd_assess.run)
|
|
25
|
+
app.command("check", help="Audit harness manifests for drift (read-only).")(cmd_check.run)
|
|
26
|
+
app.command("init", help="Render missing manifests at the appropriate stage.")(cmd_init.run)
|
|
27
|
+
app.command("refresh", help="Back up + rewrite stale/relaxed manifests (with --apply).")(cmd_refresh.run)
|
|
28
|
+
app.command("report", help="Write a machine-readable AI-readiness report.")(cmd_report.run)
|
|
29
|
+
app.command("validate", help="Run manifest regression and redaction smoke checks.")(cmd_validate.run)
|
|
30
|
+
app.command("version", help="Print version.")(cmd_version.run)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
app()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from local_agent_harness.core import assess_repo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run(
|
|
12
|
+
repo: Path = typer.Option(Path("."), "--repo", help="Repository path."),
|
|
13
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON instead of text."),
|
|
14
|
+
) -> None:
|
|
15
|
+
repo = repo.resolve()
|
|
16
|
+
if not repo.exists():
|
|
17
|
+
typer.echo(f"error: {repo} does not exist", err=True)
|
|
18
|
+
raise typer.Exit(code=2)
|
|
19
|
+
result = assess_repo.detect(repo)
|
|
20
|
+
if json_output:
|
|
21
|
+
typer.echo(json.dumps(result, indent=2, sort_keys=True))
|
|
22
|
+
return
|
|
23
|
+
typer.echo(f"Repository: {repo}")
|
|
24
|
+
typer.echo(f"Stage: {result['stage']}")
|
|
25
|
+
typer.echo(f"Total: {result['total']} / 25")
|
|
26
|
+
for k, v in result["axes"].items():
|
|
27
|
+
typer.echo(f" {k:16s} {v}/5")
|
|
28
|
+
if result["detected_runtimes"]:
|
|
29
|
+
typer.echo(f"Runtimes: {', '.join(result['detected_runtimes'])}")
|
|
30
|
+
if result["missing_artifacts"]:
|
|
31
|
+
typer.echo("Missing:")
|
|
32
|
+
for m in result["missing_artifacts"]:
|
|
33
|
+
typer.echo(f" - {m}")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from local_agent_harness.core import diff_manifests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run(
|
|
12
|
+
repo: Path = typer.Option(Path("."), "--repo", help="Repository path."),
|
|
13
|
+
stage: Optional[str] = typer.Option(None, "--stage", help="Override stage (S0|S1|S2|S3)."),
|
|
14
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON."),
|
|
15
|
+
) -> None:
|
|
16
|
+
repo = repo.resolve()
|
|
17
|
+
if not repo.exists():
|
|
18
|
+
typer.echo(f"error: {repo} does not exist", err=True)
|
|
19
|
+
raise typer.Exit(code=2)
|
|
20
|
+
result = diff_manifests.diff(repo, stage=stage)
|
|
21
|
+
if json_output:
|
|
22
|
+
import json
|
|
23
|
+
typer.echo(json.dumps(result, indent=2, sort_keys=True))
|
|
24
|
+
else:
|
|
25
|
+
diff_manifests._print_human(result)
|
|
26
|
+
if result.get("relaxed"):
|
|
27
|
+
raise typer.Exit(code=2)
|
|
28
|
+
if result.get("drift"):
|
|
29
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from local_agent_harness.core import assess_repo, scaffold_manifests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run(
|
|
12
|
+
repo: Path = typer.Option(Path("."), "--repo", help="Repository path."),
|
|
13
|
+
stage: Optional[str] = typer.Option(None, "--stage", help="Override stage (S0|S1|S2|S3)."),
|
|
14
|
+
runtime: List[str] = typer.Option(
|
|
15
|
+
[], "--runtime", help="Runtime overlay(s) to render: claude-code|codex-cli|copilot-cli|cursor."
|
|
16
|
+
),
|
|
17
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be written."),
|
|
18
|
+
) -> None:
|
|
19
|
+
repo = repo.resolve()
|
|
20
|
+
if not repo.exists():
|
|
21
|
+
typer.echo(f"error: {repo} does not exist", err=True)
|
|
22
|
+
raise typer.Exit(code=2)
|
|
23
|
+
if stage is None:
|
|
24
|
+
stage = assess_repo.detect(repo)["stage"]
|
|
25
|
+
rc = scaffold_manifests.cmd_init(repo, stage, list(runtime), dry_run)
|
|
26
|
+
raise typer.Exit(code=rc)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from local_agent_harness.core import assess_repo, scaffold_manifests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run(
|
|
12
|
+
repo: Path = typer.Option(Path("."), "--repo", help="Repository path."),
|
|
13
|
+
stage: Optional[str] = typer.Option(None, "--stage", help="Override stage (S0|S1|S2|S3)."),
|
|
14
|
+
runtime: List[str] = typer.Option([], "--runtime", help="Runtime overlay(s) to render."),
|
|
15
|
+
apply: bool = typer.Option(False, "--apply", help="Actually write changes (default is dry-run)."),
|
|
16
|
+
) -> None:
|
|
17
|
+
repo = repo.resolve()
|
|
18
|
+
if not repo.exists():
|
|
19
|
+
typer.echo(f"error: {repo} does not exist", err=True)
|
|
20
|
+
raise typer.Exit(code=2)
|
|
21
|
+
if stage is None:
|
|
22
|
+
stage = assess_repo.detect(repo)["stage"]
|
|
23
|
+
rc = scaffold_manifests.cmd_refresh(repo, stage, list(runtime), apply, dry=not apply)
|
|
24
|
+
raise typer.Exit(code=rc)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from local_agent_harness.core import assess_repo, readiness_report
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run(
|
|
12
|
+
repo: Path = typer.Option(Path("."), "--repo", help="Repository path."),
|
|
13
|
+
out: Optional[Path] = typer.Option(None, "--out", help="Write report to this path (default stdout)."),
|
|
14
|
+
check_no_regression: Optional[Path] = typer.Option(
|
|
15
|
+
None, "--check-no-regression",
|
|
16
|
+
help="Compare a fresh assessment against an existing readiness file; fail on per-axis regressions.",
|
|
17
|
+
),
|
|
18
|
+
) -> None:
|
|
19
|
+
repo = repo.resolve()
|
|
20
|
+
if not repo.exists():
|
|
21
|
+
typer.echo(f"error: {repo} does not exist", err=True)
|
|
22
|
+
raise typer.Exit(code=2)
|
|
23
|
+
result = assess_repo.detect(repo)
|
|
24
|
+
|
|
25
|
+
if check_no_regression:
|
|
26
|
+
prev_text = check_no_regression.read_text(encoding="utf-8")
|
|
27
|
+
prev = readiness_report.parse_machine_block(prev_text)
|
|
28
|
+
if prev is None:
|
|
29
|
+
typer.echo("error: previous readiness file has no machine block", err=True)
|
|
30
|
+
raise typer.Exit(code=2)
|
|
31
|
+
regressed = []
|
|
32
|
+
for axis, score in result["axes"].items():
|
|
33
|
+
old = int(prev.get(axis, 0))
|
|
34
|
+
if score < old:
|
|
35
|
+
regressed.append(f"{axis}: {old} -> {score}")
|
|
36
|
+
if regressed:
|
|
37
|
+
for line in regressed:
|
|
38
|
+
typer.echo(f"REGRESSION {line}", err=True)
|
|
39
|
+
raise typer.Exit(code=1)
|
|
40
|
+
typer.echo("OK no regression")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
text = readiness_report.render_report(result, repo)
|
|
44
|
+
if out:
|
|
45
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
out.write_text(text, encoding="utf-8")
|
|
47
|
+
typer.echo(f"wrote {out}")
|
|
48
|
+
else:
|
|
49
|
+
typer.echo(text)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Install the bundled `local-agent-harness` skill into agent skill directories.
|
|
2
|
+
|
|
3
|
+
Default targets are the three known agent skill roots, but only those whose
|
|
4
|
+
parent directory already exists on disk:
|
|
5
|
+
|
|
6
|
+
* ``~/.claude/skills/local-agent-harness/`` (Claude Code)
|
|
7
|
+
* ``~/.copilot/skills/local-agent-harness/`` (GitHub Copilot CLI)
|
|
8
|
+
* ``~/.codex/skills/local-agent-harness/`` (Codex CLI)
|
|
9
|
+
|
|
10
|
+
Use ``--target PATH`` (repeatable) to install elsewhere, e.g. into a
|
|
11
|
+
project-local ``.github/skills/<name>/`` directory.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import shutil
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import List
|
|
18
|
+
|
|
19
|
+
import typer
|
|
20
|
+
|
|
21
|
+
from local_agent_harness.core._paths import skill_data_root
|
|
22
|
+
|
|
23
|
+
_SKILL_NAME = "local-agent-harness"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _default_targets() -> list[Path]:
|
|
27
|
+
"""Return skill dirs whose *parent* (e.g. ~/.claude/skills) already exists.
|
|
28
|
+
|
|
29
|
+
Falls back to ``~/.claude/skills/<name>`` if none exist, so first-time
|
|
30
|
+
users on a fresh box still get a sensible default.
|
|
31
|
+
"""
|
|
32
|
+
home = Path.home()
|
|
33
|
+
candidates = [
|
|
34
|
+
home / ".claude" / "skills",
|
|
35
|
+
home / ".copilot" / "skills",
|
|
36
|
+
home / ".codex" / "skills",
|
|
37
|
+
]
|
|
38
|
+
existing = [p / _SKILL_NAME for p in candidates if p.is_dir()]
|
|
39
|
+
if existing:
|
|
40
|
+
return existing
|
|
41
|
+
return [candidates[0] / _SKILL_NAME]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _install(src: Path, dst: Path, *, symlink: bool, force: bool) -> str:
|
|
45
|
+
if dst.exists() or dst.is_symlink():
|
|
46
|
+
if not force:
|
|
47
|
+
return f"SKIP {dst} (exists; use --force to overwrite)"
|
|
48
|
+
if dst.is_symlink() or dst.is_file():
|
|
49
|
+
dst.unlink()
|
|
50
|
+
else:
|
|
51
|
+
shutil.rmtree(dst)
|
|
52
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
if symlink:
|
|
54
|
+
dst.symlink_to(src, target_is_directory=True)
|
|
55
|
+
return f"LINK {dst} -> {src}"
|
|
56
|
+
shutil.copytree(src, dst)
|
|
57
|
+
return f"COPY {dst}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run(
|
|
61
|
+
target: List[Path] = typer.Option(
|
|
62
|
+
[],
|
|
63
|
+
"--target",
|
|
64
|
+
"-t",
|
|
65
|
+
help=(
|
|
66
|
+
"Destination directory for the skill (repeatable). "
|
|
67
|
+
"Defaults to the existing agent skill roots: "
|
|
68
|
+
"~/.claude/skills, ~/.copilot/skills, ~/.codex/skills."
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
symlink: bool = typer.Option(
|
|
72
|
+
False, "--symlink", help="Symlink instead of copying (good for development)."
|
|
73
|
+
),
|
|
74
|
+
force: bool = typer.Option(
|
|
75
|
+
False, "--force", "-f", help="Overwrite the destination if it already exists."
|
|
76
|
+
),
|
|
77
|
+
list_only: bool = typer.Option(
|
|
78
|
+
False, "--list", help="Print the resolved targets and exit without writing."
|
|
79
|
+
),
|
|
80
|
+
) -> None:
|
|
81
|
+
src = skill_data_root()
|
|
82
|
+
targets = [t.expanduser().resolve() for t in target] if target else _default_targets()
|
|
83
|
+
|
|
84
|
+
if list_only:
|
|
85
|
+
typer.echo(f"source: {src}")
|
|
86
|
+
for t in targets:
|
|
87
|
+
typer.echo(f"target: {t}")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
for t in targets:
|
|
91
|
+
msg = _install(src, t, symlink=symlink, force=force)
|
|
92
|
+
typer.echo(msg)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from local_agent_harness.core import manifest_regression, redaction_smoke
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run(
|
|
11
|
+
repo: Path = typer.Option(Path("."), "--repo", help="Repository path."),
|
|
12
|
+
) -> None:
|
|
13
|
+
repo = repo.resolve()
|
|
14
|
+
if not repo.exists():
|
|
15
|
+
typer.echo(f"error: {repo} does not exist", err=True)
|
|
16
|
+
raise typer.Exit(code=2)
|
|
17
|
+
|
|
18
|
+
typer.echo("== manifest regression ==")
|
|
19
|
+
results = manifest_regression.check(repo)
|
|
20
|
+
failed = 0
|
|
21
|
+
for name, ok, msg in results:
|
|
22
|
+
marker = "PASS" if ok else "FAIL"
|
|
23
|
+
typer.echo(f" [{marker}] {name}: {msg}")
|
|
24
|
+
if not ok:
|
|
25
|
+
failed += 1
|
|
26
|
+
|
|
27
|
+
typer.echo("== redaction smoke ==")
|
|
28
|
+
findings = redaction_smoke.scan(repo)
|
|
29
|
+
for path, kind in findings:
|
|
30
|
+
typer.echo(f" HIT {kind}: {path}")
|
|
31
|
+
logs_ok = redaction_smoke.check_logs_ignored(repo)
|
|
32
|
+
typer.echo(f" .agent/.env in .gitignore: {'yes' if logs_ok else 'no'}")
|
|
33
|
+
|
|
34
|
+
if failed or findings or not logs_ok:
|
|
35
|
+
raise typer.Exit(code=1)
|
|
36
|
+
typer.echo("validate: OK")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Locate packaged skill data (assets/, references/, SKILL.md).
|
|
2
|
+
|
|
3
|
+
The package ships a copy of the skill under
|
|
4
|
+
``local_agent_harness/skill_data/local-agent-harness/``. This module finds
|
|
5
|
+
the assets directory whether installed as a wheel or used in editable mode.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib.resources
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
_SKILL_NAME = "local-agent-harness"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def skill_data_root() -> Path:
|
|
16
|
+
"""Return the directory containing SKILL.md / assets/ / references/."""
|
|
17
|
+
ref = importlib.resources.files("local_agent_harness") / "skill_data" / _SKILL_NAME
|
|
18
|
+
p = Path(str(ref))
|
|
19
|
+
if not p.is_dir():
|
|
20
|
+
raise RuntimeError(f"skill_data not found at {p}")
|
|
21
|
+
return p
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def assets_dir() -> Path:
|
|
25
|
+
return skill_data_root() / "assets"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def references_dir() -> Path:
|
|
29
|
+
return skill_data_root() / "references"
|