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.
Files changed (46) hide show
  1. local_agent_harness-0.1.0/PKG-INFO +91 -0
  2. local_agent_harness-0.1.0/README.md +61 -0
  3. local_agent_harness-0.1.0/pyproject.toml +83 -0
  4. local_agent_harness-0.1.0/src/local_agent_harness/__init__.py +4 -0
  5. local_agent_harness-0.1.0/src/local_agent_harness/__main__.py +4 -0
  6. local_agent_harness-0.1.0/src/local_agent_harness/cli/__init__.py +0 -0
  7. local_agent_harness-0.1.0/src/local_agent_harness/cli/app.py +34 -0
  8. local_agent_harness-0.1.0/src/local_agent_harness/cli/cmd_assess.py +33 -0
  9. local_agent_harness-0.1.0/src/local_agent_harness/cli/cmd_check.py +29 -0
  10. local_agent_harness-0.1.0/src/local_agent_harness/cli/cmd_init.py +26 -0
  11. local_agent_harness-0.1.0/src/local_agent_harness/cli/cmd_refresh.py +24 -0
  12. local_agent_harness-0.1.0/src/local_agent_harness/cli/cmd_report.py +49 -0
  13. local_agent_harness-0.1.0/src/local_agent_harness/cli/cmd_setup.py +92 -0
  14. local_agent_harness-0.1.0/src/local_agent_harness/cli/cmd_validate.py +36 -0
  15. local_agent_harness-0.1.0/src/local_agent_harness/cli/cmd_version.py +9 -0
  16. local_agent_harness-0.1.0/src/local_agent_harness/core/__init__.py +4 -0
  17. local_agent_harness-0.1.0/src/local_agent_harness/core/_paths.py +29 -0
  18. local_agent_harness-0.1.0/src/local_agent_harness/core/assess_repo.py +222 -0
  19. local_agent_harness-0.1.0/src/local_agent_harness/core/diff_manifests.py +214 -0
  20. local_agent_harness-0.1.0/src/local_agent_harness/core/manifest_regression.py +123 -0
  21. local_agent_harness-0.1.0/src/local_agent_harness/core/readiness_report.py +126 -0
  22. local_agent_harness-0.1.0/src/local_agent_harness/core/redaction_smoke.py +85 -0
  23. local_agent_harness-0.1.0/src/local_agent_harness/core/scaffold_manifests.py +199 -0
  24. local_agent_harness-0.1.0/src/local_agent_harness/py.typed +0 -0
  25. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/SKILL.md +251 -0
  26. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/AGENTS.md.tmpl +83 -0
  27. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/GROUNDING.md.tmpl +51 -0
  28. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/ci/governance.yml.tmpl +27 -0
  29. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/ci/verify.yml.tmpl +30 -0
  30. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/devcontainer.json.tmpl +28 -0
  31. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/plan.md.tmpl +37 -0
  32. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/pre-commit-config.yaml.tmpl +15 -0
  33. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/readiness-report.md.tmpl +43 -0
  34. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/repo-skill.SKILL.md.tmpl +36 -0
  35. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/runtime-overlays/CLAUDE.md.tmpl +25 -0
  36. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/runtime-overlays/codex.config.tmpl +23 -0
  37. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/runtime-overlays/copilot-cli.tmpl +21 -0
  38. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/assets/runtime-overlays/cursor-rules.tmpl +21 -0
  39. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/references/ai-readiness-rubric.md +70 -0
  40. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/references/manifest-anti-patterns.md +16 -0
  41. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/references/memory-governance.md +44 -0
  42. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/references/runtime-overlays.md +29 -0
  43. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/references/source-mapping.md +41 -0
  44. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/references/stages.md +155 -0
  45. local_agent_harness-0.1.0/src/local_agent_harness/skill_data/local-agent-harness/references/tool-dag-patterns.md +36 -0
  46. 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"
@@ -0,0 +1,4 @@
1
+ """local-agent-harness — maturity-aware harness manager."""
2
+ from __future__ import annotations
3
+
4
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from local_agent_harness.cli.app import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -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,9 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from local_agent_harness import __version__
6
+
7
+
8
+ def run() -> None:
9
+ typer.echo(f"local-agent-harness {__version__}")
@@ -0,0 +1,4 @@
1
+ """Core library — pure functions implementing the harness operations.
2
+
3
+ CLI commands in :mod:`local_agent_harness.cli` delegate to these modules.
4
+ """
@@ -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"