acumen 0.0.1.dev0__py3-none-any.whl

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.
@@ -0,0 +1,53 @@
1
+ # `acumen ship` — put a proven skill inside the package
2
+
3
+ ```bash
4
+ acumen ship --skill v2 [--force] [--model M] [--max-turns N] [--max-usd X]
5
+ [--stream] [--log-dir logs] [--auth auto|session|api]
6
+ ```
7
+
8
+ `--skill VERSION` is **required** — there is no implicit "latest" or "best". Run it only
9
+ after the report shows that version earning its place *and* the user has agreed to ship it:
10
+ it writes to the target's real checkout and can open a PR.
11
+
12
+ ## What it produces
13
+
14
+ An agent modifies the target checkout so the package gains a `<dist-name>-install-skills`
15
+ console script. The package's users then run, e.g.:
16
+
17
+ ```bash
18
+ scanpy-install-skills --agent claude # or codex | agents | claude-science
19
+ scanpy-install-skills --dest ./somewhere # explicit destination
20
+ scanpy-install-skills --print-path # where the bundled skill lives
21
+ ```
22
+
23
+ There is **no default framework** — `--agent` or `--dest` is required. The same
24
+ `SKILL.md` + `references/` bundle installs verbatim into every framework; there is no
25
+ per-framework conversion. Destinations are `<root>/skills/<skill_name>`, with roots
26
+ `~/.claude` (`CLAUDE_CONFIG_DIR`), `~/.codex` (`CODEX_HOME`), `~/.agents`, and the active
27
+ org's directory for `claude-science`.
28
+
29
+ Inside the package the agent creates `<import-pkg>/_skills/` with `install.py` (a canonical
30
+ template it must copy verbatim) and `data/` holding the skill files, then wires the console
31
+ script and — the step that silently fails — the **build-backend packaging** so those
32
+ non-`.py` data files actually ship in the wheel. It verifies by building a wheel, installing
33
+ it into a fresh (non-editable) venv, and confirming `SKILL.md` lands.
34
+
35
+ ## Delivery depends on `repo`
36
+
37
+ - **GitHub URL** → the agent branches, commits, pushes, and opens a PR with `gh` (it does
38
+ not merge). Needs write access; a rejected push is reported, not worked around.
39
+ - **Local path** → the change is written straight into your working tree, no branch, no
40
+ commit. Review with `git diff`.
41
+
42
+ ## Non-obvious behaviour
43
+
44
+ - **The ship agent is deliberately NOT isolated.** Unlike every other acumen agent it runs
45
+ in your real environment: real `HOME`, real network, real git/`gh` credentials, real `uv`,
46
+ with `permission_mode="bypassPermissions"`. Only the model credential is constrained (by
47
+ `--auth`). Run it when you are ready for it to touch your checkout and remote.
48
+ - It refuses to run if the target already declares a `*-install-skills` console script or
49
+ has a `_skills/install.py` — pass `--force` to ship anyway.
50
+ - It ships exactly one version, copied verbatim; it never authors or edits skill text, and
51
+ it writes no tests.
52
+ - Unbounded turns/cost by default. Its final summary (PR URL, packaging changes,
53
+ build-verify result) is printed and returned.
@@ -0,0 +1,185 @@
1
+ """Install the bundled ``acumen`` skill into an agentic framework's skills directory.
2
+
3
+ Console script (wired as ``<dist>-install-skills`` in ``pyproject.toml``): copy the skill that
4
+ ships inside this package into a chosen framework's skills directory, so an agent can load it.
5
+ The same ``SKILL.md`` + ``references/`` bundle is a cross-framework standard, so it installs
6
+ verbatim — no per-framework conversion.
7
+
8
+ Frameworks (``--agent``):
9
+
10
+ - ``claude`` -> ``~/.claude/skills`` (honours ``CLAUDE_CONFIG_DIR``)
11
+ - ``codex`` -> ``~/.codex/skills`` (honours ``CODEX_HOME``)
12
+ - ``agents`` -> ``~/.agents/skills``
13
+ - ``claude-science`` -> the active org's skills dir, resolved from
14
+ ``~/.claude-science/active-org.json``
15
+
16
+ ``--dest`` overrides all of them. There is **no default framework**: pass ``--agent`` or
17
+ ``--dest``. The skill files live in the ``data/`` directory next to this module and are read via
18
+ ``importlib.resources``, so this works from an installed wheel, not just an editable checkout.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import json
25
+ import os
26
+ import shutil
27
+ import sys
28
+ from importlib import resources
29
+ from pathlib import Path
30
+
31
+ #: The skill name; it installs to ``<framework-skills-dir>/<SKILL_NAME>/``.
32
+ SKILL_NAME = "acumen"
33
+
34
+ #: Framework -> (env var that overrides the config root, default config root). The skills
35
+ #: directory is ``<root>/skills``. ``claude-science`` is resolved separately.
36
+ _AGENT_ROOTS = {
37
+ "codex": ("CODEX_HOME", "~/.codex"),
38
+ "claude": ("CLAUDE_CONFIG_DIR", "~/.claude"),
39
+ "agents": (None, "~/.agents"),
40
+ }
41
+
42
+ #: The frameworks ``--agent`` accepts.
43
+ AGENTS = (*sorted(_AGENT_ROOTS), "claude-science")
44
+
45
+
46
+ def source_dir() -> Path:
47
+ """Return the package-owned skill directory (the bundle that gets copied)."""
48
+ source = Path(str(resources.files(__package__).joinpath("data")))
49
+ if not (source / "SKILL.md").is_file():
50
+ raise RuntimeError(f"packaged skill data is missing: {source}")
51
+ return source
52
+
53
+
54
+ def _claude_science_skills_dir() -> Path:
55
+ """Resolve the active Claude Science org's skills directory."""
56
+ root = Path("~/.claude-science").expanduser()
57
+ active_org_path = root / "active-org.json"
58
+ try:
59
+ active_org = json.loads(active_org_path.read_text(encoding="utf-8"))
60
+ except (OSError, json.JSONDecodeError) as error:
61
+ raise ValueError(
62
+ "cannot resolve the Claude Science active organization from "
63
+ f"{active_org_path}; pass --dest instead"
64
+ ) from error
65
+ org_uuid = active_org.get("org_uuid") if isinstance(active_org, dict) else None
66
+ if (
67
+ not isinstance(org_uuid, str)
68
+ or not org_uuid
69
+ or Path(org_uuid).name != org_uuid
70
+ or org_uuid in {".", ".."}
71
+ ):
72
+ raise ValueError(f"invalid Claude Science org_uuid in {active_org_path}; pass --dest instead")
73
+ return root / "orgs" / org_uuid / "skills"
74
+
75
+
76
+ def _skills_dir(agent: str) -> Path:
77
+ """Return the skills directory (parent of the install dir) for a framework."""
78
+ if agent == "claude-science":
79
+ return _claude_science_skills_dir()
80
+ variable, fallback = _AGENT_ROOTS[agent]
81
+ root = os.environ.get(variable) if variable is not None else None
82
+ return Path(root or fallback).expanduser() / "skills"
83
+
84
+
85
+ def resolve_dest(agent: str | None, dest: Path | None) -> Path:
86
+ """Resolve the install destination from ``--agent`` / ``--dest``.
87
+
88
+ ``--dest`` wins. With neither, raise ``ValueError`` — there is no default framework.
89
+ """
90
+ if dest is not None:
91
+ return dest.expanduser()
92
+ if agent is None:
93
+ raise ValueError("pass --agent {" + ",".join(AGENTS) + "} or --dest to choose where to install")
94
+ return _skills_dir(agent) / SKILL_NAME
95
+
96
+
97
+ def _snapshot(root: Path) -> dict[str, bytes]:
98
+ """Map each file under ``root`` to its bytes, for exact tree comparison."""
99
+ return {
100
+ str(item.relative_to(root)): item.read_bytes()
101
+ for item in root.rglob("*")
102
+ if item.is_file()
103
+ }
104
+
105
+
106
+ def _matches(source: Path, target: Path) -> bool:
107
+ """Report whether ``target`` is a byte-for-byte copy of ``source``."""
108
+ return target.is_dir() and _snapshot(source) == _snapshot(target)
109
+
110
+
111
+ def main(argv: list[str] | None = None) -> int:
112
+ """Install the bundled skill; entry point for the ``<dist>-install-skills`` script."""
113
+ parser = argparse.ArgumentParser(
114
+ description=f"Install the {SKILL_NAME} skill bundled with this package into an agent's skills directory.",
115
+ )
116
+ parser.add_argument(
117
+ "--agent",
118
+ choices=AGENTS,
119
+ default=None,
120
+ help="framework to install into (no default — pass this or --dest)",
121
+ )
122
+ parser.add_argument(
123
+ "--dest",
124
+ type=Path,
125
+ default=None,
126
+ help="exact skills directory to install into (overrides --agent)",
127
+ )
128
+ parser.add_argument(
129
+ "--force",
130
+ action="store_true",
131
+ help="overwrite an existing installation that differs from the bundled skill",
132
+ )
133
+ action = parser.add_mutually_exclusive_group()
134
+ action.add_argument(
135
+ "--check",
136
+ action="store_true",
137
+ help="report whether the installed skill matches the bundled one; do not install",
138
+ )
139
+ action.add_argument(
140
+ "--print-path",
141
+ action="store_true",
142
+ help="print the bundled skill's location inside the package and exit",
143
+ )
144
+ args = parser.parse_args(argv)
145
+
146
+ try:
147
+ source = source_dir()
148
+ if args.print_path:
149
+ print(source)
150
+ return 0
151
+ if args.check:
152
+ target = resolve_dest(args.agent, args.dest)
153
+ if not target.exists():
154
+ print(f"{SKILL_NAME} skill is not installed at {target}", file=sys.stderr)
155
+ return 1
156
+ if _matches(source, target):
157
+ print(f"{SKILL_NAME} skill at {target} matches the bundled copy")
158
+ return 0
159
+ print(f"{SKILL_NAME} skill at {target} differs from the bundled copy", file=sys.stderr)
160
+ return 1
161
+
162
+ dest = resolve_dest(args.agent, args.dest)
163
+ if dest.exists():
164
+ if _matches(source, dest):
165
+ print(f"{SKILL_NAME} skill already up to date at {dest}")
166
+ return 0
167
+ if not args.force:
168
+ print(f"{dest} already exists and differs; pass --force to overwrite", file=sys.stderr)
169
+ return 1
170
+ if dest.is_dir():
171
+ shutil.rmtree(dest)
172
+ else:
173
+ dest.unlink()
174
+ dest.parent.mkdir(parents=True, exist_ok=True)
175
+ shutil.copytree(source, dest)
176
+ except (OSError, RuntimeError, ValueError) as error:
177
+ print(f"error: {error}", file=sys.stderr)
178
+ return 1
179
+
180
+ print(f"installed the {SKILL_NAME} skill to {dest}")
181
+ return 0
182
+
183
+
184
+ if __name__ == "__main__":
185
+ raise SystemExit(main())