agentleFS
ice-lfernandesice-lfernandes/claude-spring-architect/.claude/skills/git-publish/SKILL.md

git-publish

Skill11 starsChanged 7 days agoApache-2.0View on GitHub

Offers to git init/commit the current project and, optionally, create a GitHub repo (via `gh`) and push — always behind two explicit confirmations, one for the local commit and one for the remote create+push. Use right after a project was just generated by `/init-project`, or whenever the user types `/git-publish` — the line `/new-feature` and `/new-feature-implement` print at the end of a run — or asks to create a git repo, commit, or push the current project. The two feature flows never invoke it.

  • Reads credentials
  • Deletes or force-pushes
  • Commits and pushes

1 other project uses a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

6 rules. About 0 are common, 2 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  • could be automatic: a setting could enforce it
  1. Very specific:

    Input (optional): paths to stage: When the invoking context or the typed argument names paths — /new-feature prints them for an approved …

    line 59
  2. Very specific:

    Input (optional): project directory: When the invoking context names project: <absolute path> — project-initializer does, because it runs …

    line 70
  3. Distinctive:

    Never writes project source files.

    line 81
  4. Distinctive:

    Before gate 1's question, show the diff-file list and ask explicitly whether it actually matches the feature just built.

    line 109
  5. Very specific:

    Only reached after gate 1 commits successfully, or when TRACKED+clean with unpushed commits/no remote.

    line 189
  6. Very specific:

    Never git push --force, never --no-verify, never delete an existing remote without the user naming that as the explicit choice.

    line 205

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

git-publish — offer to commit and push, with confirmation

Why this is a skill (Form 1) and not Form 2

One piece chains into this one by name: project-initializer (after /init-project finishes). /new-feature and /new-feature-implement used to as well, and print the typed command instead since @.claude/decisions/0182-git-publish-printed-not-chained-by-the-feature-flows.md: the chained call ran at the 250–370k context of the flow and committed before the run's audit trail existed. disable-model-invocation: true (Form 2) blocks exactly that — the model can't call a Form 2 skill via the Skill tool at all, only a human typing /git-publish can, the same restriction java-spring-boot-developer.md documents for why it can't re-invoke gof-design-patterns. Precedent: @.claude/decisions/0007-pipeline-skills-invocation.md (D17) — the five /new-feature pipeline skills stay without disable-model-invocation for the same reason, and rely on an entry guard in the body instead of the flag. This file's guard is the two AskUserQuestion gates below: no git side effect ever runs without an explicit yes, regardless of what triggered the invocation.

Form 3 (agent) was rejected: the confirmation dialogue with the user is the heart of this task, the context it needs fits entirely in this file, and the final output is short — all three fail the counter-test in references/decision-matrix.md § 5.

Decision record: @.claude/decisions/0034-git-publish-skill.md.

Runs on sonnet with effort: low: a fixed procedure whose every side effect waits behind the two gates (@.claude/decisions/0080-procedural-skills-on-sonnet.md).

Contract

Class: ops — the territory is skill_classes.ops in @.claude/schemas/extensions.json, and it is empty: this skill writes no file. Its whole effect is git over Bash, which the guard deliberately does not cover — a commit is not a file write, and the two confirmations below are what gate it. allowed-tools pre-approves only the local git subcommands and gh auth status; the remote step is left to permissions.ask (decision 0076).

Input (optional, from the invoking context or the typed argument): a short description of what was just done — e.g. "initial scaffold, blueprint hexagonal, maven, features: rest,jpa" or "UC-001-order: order management feature". Used to build the commit message. No input → generic message from what git status shows.

The typed argument. /new-feature and /new-feature-implement end by printing /git-publish <message>[ — paths: <p…>][ — pre-existing: <p…>]. The message is everything before the first literal — paths: or — pre-existing:; each marker, when present, opens one of the two inputs below. Paths are space-separated.

Input (optional): paths to stage. When the invoking context or the typed argument names paths — /new-feature prints them for an approved spec not implemented yet (docs/use-cases/UC-NNN-<slug>/, docs/use-cases/BACKLOG.md) — gate 1 lists and stages only those paths. Anything else dirty in the tree stays unstaged and is named in the report.

Input (optional): pre-existing changes. — pre-existing: <paths> is the list the feature flow's guardrail took at the start of its run (IGNORED counts as an entry). It is the PRE_EXISTING_WORK list of step 1, handed over instead of rediscovered: a /git-publish typed after /clear has none of that run's context, so what the guardrail saw is what the line carries. Without the marker, step 1 discovers it from git status --porcelain as before.

Input (optional): project directory. When the invoking context names project: <absolute path> — project-initializer does, because it runs in a session opened somewhere else — that directory is the project. Run every command of this procedure as (cd "<path>" && git …), gh repo create included, so --source=. is the project and git init never lands in the session's own repository. Without it, the project is the session's directory.

Reads: git status, git remote -v, gh auth status. Nothing in the project files.

Writes: .git/ of the current project, and — only after the second confirmation — a new GitHub repository via gh repo create, or a push to a remote the user names. Never writes project source files.

Integration:

  • Invoked by project-initializer (agent) right after a green build, via the Skill tool, with a scaffold summary as context.
  • Typed by the user at the end of each of the two feature flows, after /clear, from the line the flow printed: after /new-feature approves a spec, with the approved spec's paths as the only paths to stage; after /new-feature-implement succeeds, with the UC name/summary as the message. Neither flow invokes it.
  • Invocable directly by the user (/git-publish, or by asking in plain language).

Procedure

1 · Read state, never assume it

git rev-parse --is-inside-work-tree 2>/dev/null && echo TRACKED || echo UNTRACKED
git status --porcelain 2>/dev/null
git diff --cached --stat 2>/dev/null
git remote -v 2>/dev/null
git log -1 --oneline 2>/dev/null
git check-ignore -q . && echo ROOT_IGNORED_BY_PARENT

ROOT_IGNORED_BY_PARENT — this project's own root is gitignored by an outer repo (e.g. a demo under this meta-repo's own examples/). git status --porcelain reflects the outer repo's tracked files, which have no relation to the feature this run just implemented — src/, docs/use-cases/, all of it invisible to that status. Before gate 1's question, show the diff-file list and ask explicitly whether it actually matches the feature just built; don't let the caller's commit-message context imply it does. If the caller (the feature guardrail's worktree step, in /new-feature or /new-feature-implement, through the — pre-existing: IGNORED of the typed line) already flagged this, skip re-discovering it — just carry the warning into gate 1's question.

Four states, four different phrasings for gate 1:

StateGate 1 asks
UNTRACKED (no .git)"Initialize git and create the first commit?"
TRACKED, dirty (git status --porcelain non-empty)"Commit these changes now?"
TRACKED, cleanSkip gate 1 — nothing to commit. Go straight to gate 2 only if there are unpushed commits or no remote configured; otherwise report "nothing to do" and stop
TRACKED, and git status --porcelain shows paths outside the ones the caller named"Commit only this run's paths, or everything that is dirty?" — two options, and the question comes before gate 1's own

PRE_EXISTING_WORK — the fourth state. Changes that belong to no caller of this skill: someone else's cleanup, a deletion made by hand, a half-finished edit, an untracked file from a previous run. Read from git status --porcelain, not from the index alone — an unstaged edit and an untracked file are invisible to git diff --cached and git add -A commits them anyway (lessons-learned-014 § 11). .claude/audit-usage/** never counts toward this state: step 2.3 stages it with every run, and the previous run's report lands after that run's commit by construction (lessons-learned-016 § 8). A worktree whose only extra paths are under it is the plain dirty state, not this one. Deciding it silently is what went wrong in lessons-learned-012 § 7 — 1.503 staged deletions rode along with a feature commit, and only a manual read of the list caught it. Show the paths, say which of them the caller named, and ask with those two options. On "only this run's paths", stage with git add -- <paths> and never git add -A; the rest stays dirty and is reported as left behind, not reverted. On "everything that is dirty", two commits, one gate, one push: first the run's paths plus .claude/audit-usage/ with the run's message (step 2.4), then the rest with git add -A and chore: commit changes from before this run, whose body lists every path it carries. One commit for both gives the run's type to content the run did not write — a docs(UC-…): approved spec once shipped a build-plugin change and a new compose service (lessons-learned-016 § 9). The secret scan of step 2.2 covers both sets before either is staged. The feature guardrail's worktree step (.claude/skills/new-feature/references/entry-guardrail.md § 2) already flags this at the start of the run and prints the list itself in the — pre-existing: part of the typed line — when it did, use that list instead of rediscovering it, and diff it against what is dirty now: what the guardrail saw is what the run did not produce.

2 · Gate 1 — local commit

AskUserQuestion, options: Yes, commit now / No, skip. "No" stops here — report what would have been committed and don't touch anything.

On "Yes":

  1. git init only if UNTRACKED.
  2. Show git status and scan the file list for anything that looks like a secret before staging — .env, *.pem, *.key, credentials*, *secret*. If any match, stop and ask the user to confirm or exclude them; never stage a likely secret silently.
  3. git add -A (respecting .gitignore, already generated by the Initializr) — or, when the input named paths, git add -- <paths> with only the paths that exist, and nothing else. .claude/audit-usage/ is one of those paths, always, whether or not the caller named it: the trail of the run belongs to the commit of the run that produced it. Only .state/ is ignored, and .gitignore already handles that. Staging it in one run and leaving it out of the next is what makes every later diff start dirty (lessons-learned-012 § 15). This skill leaves no report of its own — its class, ops, declares audited: false in skill_classes of .claude/schemas/extensions.json — because that report could only be written after this commit, and every publish would end dirty.
  4. git commit -m "<message>" — build the message from the input context following Conventional Commits: chore: initial project scaffold — <blueprint/build/features> for a project-initializer call, feat(UC-NNN-slug): <summary> for a /new-feature-implement call, docs(UC-NNN-slug): approved spec for a /new-feature one, or a generic chore: commit pending changes with no context.

    Attribution: append exactly the trailer the session provides, and nothing else. The running session states its own attribution lines; use them verbatim. When it states none, the commit carries none — no Co-Authored-By at all.

    Never write a model name from memory. Same discipline @CLAUDE.md invariant 8 applies to Java and Spring Boot versions, and for a sharper reason here: this file is copied into every generated project, so a literal pinned in it goes stale in every one of them, silently and forever. A run under one model committed with another model's name because this step spelled one out, and the audit trail then recorded two models on one feature (lessons-learned-014 § 10). A wrong attribution is worse than an absent one — it is in the git history for good.

3 · Gate 2 — remote create/push

Only reached after gate 1 commits successfully, or when TRACKED+clean with unpushed commits/no remote. AskUserQuestion, options:

  • Create a new GitHub repo (gh) and push — needs gh. Run gh auth status first; not authenticated → report the exact command to fix it (gh auth login) and stop, never attempt to log in on the user's behalf. Ask visibility (public/private) as part of the same question. Then:
    gh repo create <artifactId> --<public|private> --source=. --remote=origin --push
  • Push to an existing remote — ask for the URL if git remote -v showed none; git remote add origin <url> (or set-url if origin exists and the user confirms overwriting it), then git push -u origin <current-branch>.
  • Skip, keep local only — stop, report the local commit is done and nothing was pushed.

Never git push --force, never --no-verify, never delete an existing remote without the user naming that as the explicit choice.

The push itself raises a permission prompt after the gate's yes — git push and gh repo create are in permissions.ask and deliberately absent from this skill's allowed-tools, and ask is evaluated before any allow. That is the intended second look at the one irreversible step, not a misconfiguration. A force push is refused outright by guard bash (guard.force_push in @.claude/schemas/extensions.json), in every spelling it can read.

4 · Report

✅ Committed <short-hash> — "<message>"
<✅ Committed <short-hash> — "chore: commit changes from before this run" (<n> paths) — only on "everything that is dirty">
<✅ Pushed to <remote-url> (branch <name>) | ⏭️ Local only, not pushed>

Failure modes

gh missing or not authenticated:

❌ gh not authenticated. Run `gh auth login`, then re-invoke this skill to push.
Local commit is already done — nothing lost.

Suspicious file staged:

⚠️ .env matched the secret-file scan. Excluded from `git add`.
Add it to .gitignore, or confirm explicitly if it must be committed.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.