Six bundled default skills with skills install target (1.40.0)
- BUNDLED_SKILLS embeds deep-research, seriousdevelopment, git, plan, requirements-clarification, taste (21 files), adapted for tai: load_skill, ask_user, fork/poll workers, web_fetch (never curl), single-line descriptions, no em dashes; seriousdevelopment verbatim - new skills install target (12th): install writes missing only, upgrade refreshes pristine and loudly keeps user-modified files, reinstall rebuilds tracked files keeping extras, uninstall removes only unmodified content; sidecar records upstream provenance - bare --install includes skills; run_install refreshes live skills - README: Skills section bundled paragraph, twelve targets
This commit is contained in:
@@ -80,7 +80,7 @@ Any other input is sent to the agent.
|
||||
/install status
|
||||
/install upgrade scheduler-service
|
||||
|
||||
Eleven install targets exist side by side: `binary` (`~/.local/bin/tai.py`); a command-not-found hook per shell -- `bash-hook` (`~/.bashrc`), `zsh-hook` (`~/.zshrc`), `fish-hook` (`~/.config/fish/config.fish`, respecting `$XDG_CONFIG_HOME`) -- each a guarded block (backed up once to `<rc file>.bak-tai` before the first write) adding a `tai` alias/function so the bare `tai` command resolves to the REPL instead of falling through, plus the shell's own not-found mechanism (`command_not_found_handle` for bash, `command_not_found_handler` for zsh, a `fish_command_not_found` function for fish) so any other unknown shell command is answered by the agent instead; a plexer auto-wrap hook per shell -- `plexer-hook` (bash), `zsh-plexer-hook`, `fish-plexer-hook` -- that transparently re-execs a fresh interactive shell into a managed, scriptable plexer session (`tai.py --plexer-wrap`; skipped automatically inside tmux, inside another plexer-wrapped shell, or when `~/.tai-no-plexer` exists) using whatever that shell can actually express (`[`/`case "$-"` for bash and zsh -- byte-identical content, since both are POSIX enough for it; `status is-interactive`/`test`/`isatty` for fish, which has neither); `venv` (`~/.tai/venv`, created whenever the venv module exists); `scheduler-service` and `telegram-service` (systemd user units that prefer the venv python); and `container` (the sandbox, whose image carries its own `/box/venv`). Installing a zsh/fish-specific target on a machine without that shell skips loudly (`"zsh-hook: zsh not found, skipped"`) rather than writing a dangling hook into a config file nothing will ever source; `uninstall`/`reinstall` still act on the file regardless, so a hook installed before a shell was later removed still gets cleaned up. `/install status` (or the `install` tool) reports exactly which of these exist, with versions and service states. `install` adds missing pieces, `upgrade` refreshes in place, `reinstall` rebuilds artifacts from scratch, `uninstall` removes them; service changes restart or start units immediately. Data handling is explicit: no action ever touches the vault (`memory.db`, secrets, schedules, records, backups). `./tai.py --install` (the one-shot CLI bootstrap) always installs the bash hook and, in addition, detects `$SHELL` and installs that shell's matching hook too when it's zsh or fish.
|
||||
Twelve install targets exist side by side: `binary` (`~/.local/bin/tai.py`); a command-not-found hook per shell -- `bash-hook` (`~/.bashrc`), `zsh-hook` (`~/.zshrc`), `fish-hook` (`~/.config/fish/config.fish`, respecting `$XDG_CONFIG_HOME`) -- each a guarded block (backed up once to `<rc file>.bak-tai` before the first write) adding a `tai` alias/function so the bare `tai` command resolves to the REPL instead of falling through, plus the shell's own not-found mechanism (`command_not_found_handle` for bash, `command_not_found_handler` for zsh, a `fish_command_not_found` function for fish) so any other unknown shell command is answered by the agent instead; a plexer auto-wrap hook per shell -- `plexer-hook` (bash), `zsh-plexer-hook`, `fish-plexer-hook` -- that transparently re-execs a fresh interactive shell into a managed, scriptable plexer session (`tai.py --plexer-wrap`; skipped automatically inside tmux, inside another plexer-wrapped shell, or when `~/.tai-no-plexer` exists) using whatever that shell can actually express (`[`/`case "$-"` for bash and zsh -- byte-identical content, since both are POSIX enough for it; `status is-interactive`/`test`/`isatty` for fish, which has neither); `venv` (`~/.tai/venv`, created whenever the venv module exists); `scheduler-service` and `telegram-service` (systemd user units that prefer the venv python); `container` (the sandbox, whose image carries its own `/box/venv`); and `skills` (six default skills into `~/.tai/skills`, see "Skills" below). Installing a zsh/fish-specific target on a machine without that shell skips loudly (`"zsh-hook: zsh not found, skipped"`) rather than writing a dangling hook into a config file nothing will ever source; `uninstall`/`reinstall` still act on the file regardless, so a hook installed before a shell was later removed still gets cleaned up. `/install status` (or the `install` tool) reports exactly which of these exist, with versions and service states. `install` adds missing pieces, `upgrade` refreshes in place, `reinstall` rebuilds artifacts from scratch, `uninstall` removes them; service changes restart or start units immediately. Data handling is explicit: no action ever touches the vault (`memory.db`, secrets, schedules, records, backups). `./tai.py --install` (the one-shot CLI bootstrap) always installs the bash hook and, in addition, detects `$SHELL` and installs that shell's matching hook too when it's zsh or fish.
|
||||
|
||||
## Plexer
|
||||
|
||||
@@ -233,6 +233,8 @@ Its quieter cousin, `get_current_terminal_content`, grabs just the visible pane
|
||||
|
||||
Standard agent skill files (`SKILL.md` with `name` plus `description` frontmatter, optional `scripts/`, `references/`, `assets/`, per the open Agent Skills format) are discovered from four places, in ascending priority so the most tai-specific location always wins a name collision: read-only interop with `~/.claude/skills` and `./.agents/skills` first (so a skill another tool already dropped next to your project is visible with *zero* install step), then tai's own `~/.tai/skills`, then `./.tai/skills`. Descriptions stay in the system prompt catalog at all times; full instructions load only through `load_skill`, on demand — the same lazy-loading philosophy as tools, applied to skills.
|
||||
|
||||
Six skills ship inside `tai.py` itself and land in `~/.tai/skills` with `./tai.py --install` (or `/install skills`): `deep-research` (parallel evidence-gathering method with per-domain source routing, adapted to `fork`/`poll` and `web_fetch`), `seriousdevelopment` (spec-first delivery method with ADRs and build tracking, verbatim), `git` (history-write safety plus a recovery script, with a note reconciling it with tai's approval settings), `plan` (explicit planning requests with research-before-drafting, `go` to execute), `requirements-clarification` (narrow pre-commit questions via `ask_user`), and `taste` (frontend anti-slop filter). They are installed, never forced: `install` writes only missing skills, `upgrade` refreshes pristine files and loudly keeps user-modified ones, `reinstall` rebuilds tracked files while keeping user-added extras, and `uninstall` removes only unmodified content. Edit an installed skill freely; its `.installs/` sidecar records the upstream provenance.
|
||||
|
||||
Two independent, complementary ways to get a skill:
|
||||
|
||||
- **`create_skill`** authors a brand-new one from a brief: a dedicated deep-research subagent runs `sysinfo` first so the skill matches the actual machine, then researches with web search and shell until the material is independently verified, then writes `SKILL.md` plus supporting files — project-scoped by default. Six blueprints ship built in (`bot-creator`, `api-client`, `web-researcher`, `pdf-forms`, `data-wrangler`, `home-sysadmin`); `load_skill` builds a missing one through this exact same path the first time it's actually needed, so a skill finalizes lazily at the moment of use, not at install time.
|
||||
|
||||
+149
-1
@@ -1956,7 +1956,7 @@ class InstallOpsTests(unittest.TestCase):
|
||||
report = tai.install_report("status", list(tai.INSTALL_TARGETS), self.config.home, self.store)
|
||||
for label in (
|
||||
"binary:", "bash-hook:", "zsh-hook:", "fish-hook:", "plexer-hook:", "zsh-plexer-hook:",
|
||||
"fish-plexer-hook:", "venv:", "scheduler-service:", "telegram-service:", "container:", "vault:",
|
||||
"fish-plexer-hook:", "venv:", "scheduler-service:", "telegram-service:", "container:", "skills:", "vault:",
|
||||
):
|
||||
self.assertIn(label, report)
|
||||
self.assertIn("not installed", report)
|
||||
@@ -2182,6 +2182,154 @@ class InstallOpsTests(unittest.TestCase):
|
||||
self.assertIn("use /install", printed)
|
||||
|
||||
|
||||
class BundledSkillsTests(unittest.TestCase):
|
||||
NAMES = ("deep-research", "seriousdevelopment", "git", "plan", "requirements-clarification", "taste")
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.home = os.path.join(self.tmp.name, "home")
|
||||
os.makedirs(self.home)
|
||||
self.env = mock.patch.dict(os.environ, {"HOME": self.home, "TAI_HOME": os.path.join(self.home, ".tai")})
|
||||
self.env.start()
|
||||
|
||||
class FakeArgs:
|
||||
profile = "t"
|
||||
yes = True
|
||||
|
||||
self.config = tai.Config(FakeArgs())
|
||||
self.store = tai.Store(self.config, tai.Seal(self.config.home, "skills-test-1"))
|
||||
self.app = FakeSecretApp(self.store)
|
||||
self.app.config = self.config
|
||||
self.app.skills = {}
|
||||
self.tools = tai.Tools(self.app)
|
||||
|
||||
def tearDown(self):
|
||||
self.store.close()
|
||||
self.env.stop()
|
||||
self.tmp.cleanup()
|
||||
os.environ.pop("TAI_HOME", None)
|
||||
|
||||
def skill_path(self, name, rel="SKILL.md"):
|
||||
return os.path.join(self.config.home, "skills", name, rel)
|
||||
|
||||
def test_embedded_skills_parse(self):
|
||||
self.assertEqual(sorted(tai.BUNDLED_SKILLS), sorted(self.NAMES))
|
||||
for name in self.NAMES:
|
||||
files = tai.BUNDLED_SKILLS[name]
|
||||
self.assertIn("SKILL.md", files)
|
||||
skill = tai.parse_skill_front(files["SKILL.md"].split("\n---", 1)[0].splitlines()[1:])
|
||||
self.assertEqual(skill.get("name"), name)
|
||||
self.assertTrue(len(skill.get("description", "")) > 100, name)
|
||||
self.assertTrue(len(files["SKILL.md"]) > 1500, name)
|
||||
|
||||
def test_embedded_content_guards(self):
|
||||
for name in self.NAMES:
|
||||
self.assertIn(name, tai.BUNDLED_SKILL_UPSTREAM)
|
||||
self.assertTrue(tai.BUNDLED_SKILL_UPSTREAM[name], name)
|
||||
for rel, text in tai.BUNDLED_SKILLS[name].items():
|
||||
label = "%s/%s" % (name, rel)
|
||||
self.assertNotIn("\u2014", text, label)
|
||||
self.assertNotIn('"""', text, label)
|
||||
self.assertNotIn("read_skill", text, label)
|
||||
self.assertNotIn("request_user_input", text, label)
|
||||
self.assertNotIn("Claude Code", text, label)
|
||||
self.assertNotIn("curl -", text, label)
|
||||
self.assertNotIn("curl commands", text, label)
|
||||
self.assertNotIn("`/plan`", text, label)
|
||||
self.assertNotIn("/skill plan", text, label)
|
||||
|
||||
def test_install_discovers_all(self):
|
||||
report = tai.op_skills("install", self.config.home)
|
||||
self.assertIn("installed 6", report)
|
||||
for name in self.NAMES:
|
||||
self.assertIn(name, report)
|
||||
self.assertTrue(os.path.isfile(self.skill_path(name)), name)
|
||||
skills = tai.discover_skills(self.config.home, self.tmp.name)
|
||||
self.assertEqual(sorted(skills), sorted(self.NAMES))
|
||||
meta = tai.load_skill_install_meta(os.path.join(self.config.home, "skills"), "git")
|
||||
self.assertEqual(meta["mode"], "bundled")
|
||||
self.assertEqual(meta["scope"], "home")
|
||||
self.assertEqual(meta["source"], "tai-bundled")
|
||||
self.assertEqual(len(meta["files"]), len(tai.BUNDLED_SKILLS["git"]))
|
||||
self.assertEqual(tai.skill_provenance_note(skills["git"]), "[installed bundled, home scope, from tai-bundled]")
|
||||
extras = tai.skill_extra_files(os.path.join(self.config.home, "skills", "git"))
|
||||
self.assertEqual(extras, [os.path.join(self.config.home, "skills", "git", "scripts", "workspace-recovery.sh")])
|
||||
|
||||
def test_install_idempotent_and_never_overwrites(self):
|
||||
tai.op_skills("install", self.config.home)
|
||||
path = self.skill_path("taste")
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
handle.write("user rewrite")
|
||||
report = tai.op_skills("install", self.config.home)
|
||||
self.assertNotIn("installed 6", report)
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
self.assertEqual(handle.read(), "user rewrite")
|
||||
|
||||
def test_upgrade_keeps_modified_loudly(self):
|
||||
tai.op_skills("install", self.config.home)
|
||||
path = self.skill_path("taste")
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
handle.write("user rewrite")
|
||||
report = tai.op_skills("upgrade", self.config.home)
|
||||
self.assertIn("already current 5", report)
|
||||
self.assertIn("kept modified 1 (taste (SKILL.md))", report)
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
self.assertEqual(handle.read(), "user rewrite")
|
||||
self.assertIn("modified: taste", "\n".join(tai.status_lines(self.config.home, self.store)))
|
||||
|
||||
def test_reinstall_refreshes_but_keeps_extras(self):
|
||||
tai.op_skills("install", self.config.home)
|
||||
path = self.skill_path("taste")
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
handle.write("user rewrite")
|
||||
extra = os.path.join(self.config.home, "skills", "taste", "mynotes.md")
|
||||
with open(extra, "w", encoding="utf-8") as handle:
|
||||
handle.write("mine")
|
||||
report = tai.op_skills("reinstall", self.config.home)
|
||||
self.assertIn("reinstalled 1 (taste)", report)
|
||||
self.assertTrue(tai.bundled_skill_pristine(self.config.home, "taste"))
|
||||
self.assertTrue(os.path.isfile(extra))
|
||||
|
||||
def test_uninstall_keeps_user_content(self):
|
||||
tai.op_skills("install", self.config.home)
|
||||
path = self.skill_path("git")
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
handle.write("user rewrite")
|
||||
extra = os.path.join(self.config.home, "skills", "git", "mynotes.md")
|
||||
with open(extra, "w", encoding="utf-8") as handle:
|
||||
handle.write("mine")
|
||||
report = tai.op_skills("uninstall", self.config.home)
|
||||
self.assertIn("removed 5", report)
|
||||
self.assertIn("kept modified 1 (git (SKILL.md, mynotes.md))", report)
|
||||
self.assertTrue(os.path.isfile(path))
|
||||
self.assertTrue(os.path.isfile(extra))
|
||||
self.assertTrue(os.path.isfile(tai.skill_install_meta_path(os.path.join(self.config.home, "skills"), "git")))
|
||||
self.assertFalse(os.path.isdir(os.path.join(self.config.home, "skills", "taste")))
|
||||
report = tai.op_skills("uninstall", self.config.home)
|
||||
self.assertIn("kept modified 1", report)
|
||||
os.remove(path)
|
||||
os.remove(extra)
|
||||
self.assertIn("removed 1 (git)", tai.op_skills("uninstall", self.config.home))
|
||||
self.assertIn("not present", tai.op_skills("uninstall", self.config.home))
|
||||
|
||||
def test_status_counts(self):
|
||||
report = "\n".join(tai.status_lines(self.config.home, self.store))
|
||||
self.assertIn("skills: 0/6 bundled present, missing: deep-research, git, plan", report)
|
||||
tai.op_skills("install", self.config.home)
|
||||
report = "\n".join(tai.status_lines(self.config.home, self.store))
|
||||
self.assertIn("skills: 6/6 bundled present", report)
|
||||
self.assertNotIn("missing:", report.split("skills:")[1].split("\n")[0])
|
||||
|
||||
def test_run_install_refreshes_live_skills(self):
|
||||
self.assertEqual(self.app.skills, {})
|
||||
report = self.tools.dispatch("install", json.dumps({"action": "install", "targets": ["skills"]}))
|
||||
self.assertIn("installed 6", report)
|
||||
self.assertEqual(sorted(self.app.skills), sorted(self.NAMES))
|
||||
report = self.tools.dispatch("install", json.dumps({"action": "uninstall", "targets": ["skills"]}))
|
||||
self.assertIn("removed 6", report)
|
||||
self.assertEqual(self.app.skills, {})
|
||||
|
||||
|
||||
class BoxCapsTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
tai._BOX_CAPS_STAMP["done"] = False
|
||||
|
||||
Reference in New Issue
Block a user