Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,14 @@ agents-shipgate bootstrap --json
`.github/workflows/agents-shipgate.yml`; orthogonal to `--write`. Use
`--minimal` for the pre-v0.6 CHANGE_ME-heavy template.
`--agent-instructions=all` (or a comma-separated subset of
`agents-md,claude-md,cursor,pr-template`) renders agent-facing snippets to
stdout; combined with `--write` it commits them to the target repo via
managed `<!-- agents-shipgate:start -->` markers (idempotent — safe to
rerun). Strict CI and baselines remain opt-in human decisions; the flag
emits advisory guidance only.
`agents-md,codex-skill,claude-code-skill,claude-md,cursor,pr-template`)
renders agent-facing snippets to stdout; combined with `--write` it commits
them to the target repo via managed `<!-- agents-shipgate:start -->` markers
(idempotent for managed-block hosts; full-file and skill-bundle targets use
safe-update checks). The `codex-skill` and `claude-code-skill` targets write
multi-file skill bundles under `.agents/skills/agents-shipgate/` and
`.claude/skills/agents-shipgate/` respectively. Strict CI and baselines
remain opt-in human decisions; the flag emits advisory guidance only.
- **`scan --suggest-patches`** — attaches Patch objects to every active
finding. `Finding.patches` is absent without the flag.
- **`apply-patches`** — file-grouped, dry-run by default. Containment-
Expand Down
33 changes: 28 additions & 5 deletions docs/target-repo-agent-snippets.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ to agents reading the Agents Shipgate source repo.

> The CLI plants these snippets for you. Run
> `agents-shipgate init --write --agent-instructions=all` (or pass a subset
> like `--agent-instructions=agents-md,codex-skill,cursor`) to emit them into
> the target repo. Shared host files use managed `<!-- agents-shipgate:start -->`
> blocks; full-file and skill-bundle targets use safe-update checks. Idempotent
> — safe to rerun. The raw content below is the canonical reference and the
> source the renderers in
> like `--agent-instructions=agents-md,codex-skill,claude-code-skill,cursor`)
> to emit them into the target repo. Shared host files use managed
> `<!-- agents-shipgate:start -->` blocks; full-file and skill-bundle targets
> use safe-update checks. Idempotent — safe to rerun. The raw content below is
> the canonical reference and the source the renderers in
> `src/agents_shipgate/cli/discovery/agent_instructions/renderers/` lift from.

## When To Run
Expand Down Expand Up @@ -89,6 +89,29 @@ implicitly by Codex when the task matches its frontmatter. It carries a compact
`SKILL.md`, on-demand references for recipes and report reading, and an
advisory GitHub Action template.

## Claude Code Skill

For Claude Code, generate the repo-scoped skill into
`.claude/skills/agents-shipgate/`:

```bash
agents-shipgate init --workspace . --write --agent-instructions=claude-code-skill
```

Pair it with the `AGENTS.md` block and the `CLAUDE.md` managed-block for the
strongest trigger surface:

```bash
agents-shipgate init --workspace . --write \
--agent-instructions=agents-md,claude-md,claude-code-skill
```

The skill is invoked by typing `/agents-shipgate` in Claude Code, or auto-loaded
when the session is in a repo that matches its frontmatter. It bundles `SKILL.md`,
eight recipe prompts (bootstrap, relevance decision, finding fixes, strict-mode
promotion, false-positive triage, version upgrades, finding explanation), and an
advisory GitHub Action template under `ci-recipes/`.

## `CLAUDE.md`

````md
Expand Down
13 changes: 8 additions & 5 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,14 @@ agents-shipgate bootstrap --json
`.github/workflows/agents-shipgate.yml`; orthogonal to `--write`. Use
`--minimal` for the pre-v0.6 CHANGE_ME-heavy template.
`--agent-instructions=all` (or a comma-separated subset of
`agents-md,claude-md,cursor,pr-template`) renders agent-facing snippets to
stdout; combined with `--write` it commits them to the target repo via
managed `<!-- agents-shipgate:start -->` markers (idempotent — safe to
rerun). Strict CI and baselines remain opt-in human decisions; the flag
emits advisory guidance only.
`agents-md,codex-skill,claude-code-skill,claude-md,cursor,pr-template`)
renders agent-facing snippets to stdout; combined with `--write` it commits
them to the target repo via managed `<!-- agents-shipgate:start -->` markers
(idempotent for managed-block hosts; full-file and skill-bundle targets use
safe-update checks). The `codex-skill` and `claude-code-skill` targets write
multi-file skill bundles under `.agents/skills/agents-shipgate/` and
`.claude/skills/agents-shipgate/` respectively. Strict CI and baselines
remain opt-in human decisions; the flag emits advisory guidance only.
- **`scan --suggest-patches`** — attaches Patch objects to every active
finding. `Finding.patches` is absent without the flag.
- **`apply-patches`** — file-grouped, dry-run by default. Containment-
Expand Down
7 changes: 4 additions & 3 deletions src/agents_shipgate/cli/_register_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ def init(
"--agent-instructions=none to opt out. "
"Without --write, snippets are printed to stdout (or returned in "
"--json). With --write, snippets are written to AGENTS.md, "
".agents/skills/agents-shipgate/, CLAUDE.md, "
".agents/skills/agents-shipgate/, "
".claude/skills/agents-shipgate/, CLAUDE.md, "
".cursor/rules/agents-shipgate.mdc, and the PR template "
"via managed `<!-- agents-shipgate:start -->` markers (idempotent "
"where host files are shared, full-file/skill-bundle safe-update "
Expand Down Expand Up @@ -115,8 +116,8 @@ def init(
why=str(exc),
expects=(
"Snippets render for every supported target "
"(AGENTS.md, Codex skill, CLAUDE.md, Cursor rule, "
"PR template)."
"(AGENTS.md, Codex skill, Claude Code skill, "
"CLAUDE.md, Cursor rule, PR template)."
),
).model_dump(mode="json")
],
Expand Down
78 changes: 53 additions & 25 deletions src/agents_shipgate/cli/discovery/agent_instructions/apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@
outside the workspace.
- ``skipped_directory_template`` — the directory form
``.github/PULL_REQUEST_TEMPLATE/`` exists; v1 only handles the file form.
- ``created_file_tree`` — created the repo-scoped Codex skill bundle.
- ``migrated_and_repaired`` — rewrote prior-version Codex skill files and
- ``created_file_tree`` — created a repo-scoped skill bundle (Codex or
Claude Code).
- ``migrated_and_repaired`` — rewrote prior-version skill files and
recreated at least one missing file in the same pass.

Every ``skipped_*`` status contributes 2 to the exit code (matching the
Expand All @@ -36,14 +37,17 @@
from __future__ import annotations

import hashlib
from collections.abc import Iterable
from collections.abc import Callable, Iterable
from dataclasses import dataclass
from pathlib import Path

from agents_shipgate.cli.discovery.agent_instructions.managed_block import (
UpsertStatus,
upsert,
)
from agents_shipgate.cli.discovery.agent_instructions.renderers import (
claude_code_skill as claude_code_skill_renderer,
)
from agents_shipgate.cli.discovery.agent_instructions.renderers import (
codex_skill as codex_skill_renderer,
)
Expand All @@ -52,6 +56,8 @@
)
from agents_shipgate.cli.discovery.agent_instructions.renderers import (
render_agents_md,
render_claude_code_skill_bundle_text,
render_claude_code_skill_files,
render_claude_md,
render_codex_skill_bundle_text,
render_codex_skill_files,
Expand Down Expand Up @@ -173,6 +179,8 @@ def _rendered_inner(name: str) -> str:
return render_claude_md()
if name == "codex-skill":
return render_codex_skill_bundle_text()
if name == "claude-code-skill":
return render_claude_code_skill_bundle_text()
if name == "pr-template":
return render_pr_template()
if name == "cursor":
Expand All @@ -197,10 +205,7 @@ def render_targets(workspace: Path, requested: Iterable[str]) -> list[TargetOutc
status="would_render",
rendered=_rendered_inner(name),
files=(
[
{"path": str(workspace / rel), "content": content}
for rel, content in render_codex_skill_files().items()
]
_file_payload(workspace, _FILE_TREE_RENDERERS[name]())
if spec.is_file_tree
else None
),
Expand Down Expand Up @@ -402,16 +407,33 @@ def _apply_cursor(path: Path, workspace: Path) -> TargetOutcome:
)


def _apply_codex_skill(path: Path, workspace: Path) -> TargetOutcome:
rendered_files = render_codex_skill_files()
_FILE_TREE_RENDERERS: dict[str, Callable[[], dict[str, str]]] = {
"codex-skill": render_codex_skill_files,
"claude-code-skill": render_claude_code_skill_files,
}

_FILE_TREE_MODULES = {
"codex-skill": codex_skill_renderer,
"claude-code-skill": claude_code_skill_renderer,
}


def _apply_file_tree(
name: str,
path: Path,
workspace: Path,
render_fn: Callable[[], dict[str, str]],
prior_sha: dict[str, tuple[str, ...]],
) -> TargetOutcome:
rendered_files = render_fn()
target_paths = {rel: workspace / rel for rel in rendered_files}
files = _file_payload(workspace, rendered_files)

for target in target_paths.values():
symlink = _first_symlink_in_chain(target, workspace)
if symlink is not None:
return TargetOutcome(
name="codex-skill",
name=name,
path=str(path),
status="skipped_symlink",
files=files,
Expand All @@ -424,7 +446,7 @@ def _apply_codex_skill(path: Path, workspace: Path) -> TargetOutcome:

existing: list[str] = []
missing: list[str] = []
prior: list[str] = []
prior_version: list[str] = []
for rel, content in rendered_files.items():
target = target_paths[rel]
if not target.exists():
Expand All @@ -433,7 +455,7 @@ def _apply_codex_skill(path: Path, workspace: Path) -> TargetOutcome:
existing.append(rel)
if not target.is_file():
return TargetOutcome(
name="codex-skill",
name=name,
path=str(path),
status="skipped_user_modified",
files=files,
Expand All @@ -443,16 +465,16 @@ def _apply_codex_skill(path: Path, workspace: Path) -> TargetOutcome:
if current == content:
continue
current_sha = hashlib.sha256(current.encode("utf-8")).hexdigest()
if current_sha in codex_skill_renderer.PRIOR_RENDER_SHA256.get(rel, ()):
prior.append(rel)
if current_sha in prior_sha.get(rel, ()):
prior_version.append(rel)
continue
return TargetOutcome(
name="codex-skill",
name=name,
path=str(path),
status="skipped_user_modified",
files=files,
message=(
f"{target} does not match a shipped Agents Shipgate Codex skill file; "
f"{target} does not match a shipped Agents Shipgate {name} file; "
"refusing to overwrite user edits. Edit the file manually or remove "
"the skill directory before re-running."
),
Expand All @@ -465,21 +487,21 @@ def _apply_codex_skill(path: Path, workspace: Path) -> TargetOutcome:

if not existing:
status = "created_file_tree"
message = f"Wrote Codex skill bundle to {path}"
elif prior and missing:
message = f"Wrote {name} skill bundle to {path}"
elif prior_version and missing:
status = "migrated_and_repaired"
message = f"Updated Codex skill bundle and repaired missing file(s) at {path}"
elif prior:
message = f"Updated {name} skill bundle and repaired missing file(s) at {path}"
elif prior_version:
status = "migrated"
message = f"Updated Codex skill bundle at {path}"
message = f"Updated {name} skill bundle at {path}"
elif missing:
status = "updated"
message = f"Repaired missing Codex skill file(s) under {path}"
message = f"Repaired missing {name} skill file(s) under {path}"
else:
status = "unchanged"
message = f"Codex skill bundle already current at {path}"
message = f"{name} skill bundle already current at {path}"
return TargetOutcome(
name="codex-skill",
name=name,
path=str(path),
status=status,
files=files,
Expand Down Expand Up @@ -551,7 +573,13 @@ def apply_agent_instructions(
path = workspace / spec.relative_path

if spec.is_file_tree:
outcomes.append(_apply_codex_skill(path, workspace))
outcomes.append(
_apply_file_tree(
name, path, workspace,
_FILE_TREE_RENDERERS[name],
_FILE_TREE_MODULES[name].PRIOR_RENDER_SHA256,
)
)
elif name == "cursor":
outcomes.append(_apply_cursor(path, workspace))
else:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
from agents_shipgate.cli.discovery.agent_instructions.renderers.agents_md import (
render_block as render_agents_md,
)
from agents_shipgate.cli.discovery.agent_instructions.renderers.claude_code_skill import (
PRIOR_RENDER_SHA256 as CLAUDE_CODE_SKILL_PRIOR_RENDER_SHA256,
)
from agents_shipgate.cli.discovery.agent_instructions.renderers.claude_code_skill import (
render_bundle_text as render_claude_code_skill_bundle_text,
)
from agents_shipgate.cli.discovery.agent_instructions.renderers.claude_code_skill import (
render_files as render_claude_code_skill_files,
)
from agents_shipgate.cli.discovery.agent_instructions.renderers.claude_md import (
render_block as render_claude_md,
)
Expand All @@ -37,9 +46,12 @@
)

__all__ = [
"CLAUDE_CODE_SKILL_PRIOR_RENDER_SHA256",
"CODEX_SKILL_PRIOR_RENDER_SHA256",
"CURSOR_PRIOR_RENDER_SHA256",
"render_agents_md",
"render_claude_code_skill_bundle_text",
"render_claude_code_skill_files",
"render_claude_md",
"render_codex_skill_bundle_text",
"render_codex_skill_files",
Expand Down
Loading