feat(ship-it): add tea-ci-watch and background CI verification phases #12

Merged
rootiest merged 2 commits from feat/ship-it-ci-watch into main 2026-10-01 00:14:41 +00:00
4 changed files with 19 additions and 3 deletions
Showing only changes of commit d23651850d - Show all commits

No files matched your search

+5
View File
@@ -359,6 +359,7 @@ generator. A plugin can bundle any subset of:
plugins/<name>/
├── plugin.json # required: name, description, version, author
├── skills/<skill>/SKILL.md # 0+ skills (YAML frontmatter: name, description, version, author)
├── skills/<skill>/scripts/ # optional: helper scripts, shipped with the skill
├── hooks.json # optional: lifecycle hooks, Claude-shaped event → matcher groups
├── mcp.json # optional: {"mcpServers": {...}}
├── rules/AGENTS.md # optional: agy-only, always-on project rules
@@ -366,6 +367,10 @@ plugins/<name>/
└── agents/*.md # optional: Claude Code-only subagents
```
Anything else at the plugin root fails validation, since the generator would
not ship it. Each skill directory is copied whole, so a skill reaches its
helper scripts by a path relative to its own `SKILL.md`.
`hooks.json` and `mcp.json` are translated per target rather than copied
verbatim where the two tools' schemas diverge:
+2 -2
View File
@@ -28,9 +28,9 @@ Execute the following phases sequentially. Do not proceed to the next phase unle
- Wait for CI on the PR head commit with ONE blocking watcher. Never poll with `sleep` or repeated status checks.
- Run the watcher as a background command (Claude Code: Bash `run_in_background: true`); you are re-invoked when it exits. Without background support, run it in the foreground with a tool timeout longer than the watcher's.
- Derive `<owner>/<repo>` and the platform from `git remote get-url origin`.
- Gitea:
- Gitea: run the bundled watcher, `scripts/tea-ci-watch.sh` in this skill's directory (the directory holding this SKILL.md). Use its absolute path; it is not on `PATH`. Requires `tea` (logged in) and `jq`.
```bash
tea-ci-watch "<owner>/<repo>" "$(git rev-parse HEAD)"
bash "<skill-dir>/scripts/tea-ci-watch.sh" "<owner>/<repo>" "$(git rev-parse HEAD)"
```
| Exit | Meaning | Action |
|------|---------|--------|
+12 -1
View File
@@ -7,7 +7,9 @@ A "plugin" is a directory under `plugins/<name>/` that may bundle any mix of:
optional targets: ["claude-code", "agy", ...] — restricts the
WHOLE plugin, including its hooks/mcp/rules/commands/agents, to
only the listed targets; default is every target in manifest.yaml)
- skills/<name>/SKILL.md (0+ skills; a skill's own frontmatter may also declare
- skills/<name>/SKILL.md (0+ skills; the whole skill directory is shipped verbatim,
so helper scripts belong in skills/<name>/scripts/; a
skill's own frontmatter may also declare
`targets:` to further restrict just that one skill, independent
of its sibling skills — narrowed to, never wider than, the
plugin's own `targets`)
@@ -76,6 +78,9 @@ REQUIRED_SKILL_FIELDS = ("name", "description")
AGY_GROUPED_EVENTS = ("PreToolUse", "PostToolUse")
AGY_FLAT_EVENTS = ("PreInvocation", "PostInvocation", "Stop")
# Everything else at a plugin's root would be silently dropped from dist/, so reject it.
PLUGIN_ENTRIES = {"plugin.json", "skills", "hooks.json", "mcp.json", "rules", "commands", "agents"}
class ValidationError(Exception):
pass
@@ -197,6 +202,12 @@ def discover_plugins(merged_root: Path, valid_targets: list[str]) -> list[dict]:
raise ValidationError(
f"{manifest_path}: name '{meta.get('name')}' does not match directory name '{plugin_dir.name}'"
)
unknown = sorted(e.name for e in plugin_dir.iterdir() if e.name not in PLUGIN_ENTRIES)
if unknown:
raise ValidationError(
f"plugins/{plugin_dir.name}: unsupported entries {unknown} would not be shipped; "
"put helper files inside skills/<skill>/ instead"
)
for field in REQUIRED_PLUGIN_FIELDS:
if not meta.get(field):
raise ValidationError(f"{manifest_path}: missing required field '{field}'")