feat(agents): AGENTS/ structure versioning + plans/specs/devlogs consolidation #57
@@ -293,3 +293,8 @@ pyrightconfig.json
|
||||
# agents-init --agents
|
||||
AGENTS/
|
||||
# ────────────────────────────────────────────────────────
|
||||
|
||||
# ──────────────── Added by agents-init ──────────────────
|
||||
# agents-init --plugins
|
||||
docs/devlogs
|
||||
# ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -135,6 +135,14 @@ smart-exit=### smart_exit
|
||||
ai=## 5.12 AI and Developer Tools
|
||||
antigravity-ide=### antigravity-ide
|
||||
agy=### agy
|
||||
agents-init=### agents-init
|
||||
agents=### agents-init
|
||||
agents-tools=### agents-init
|
||||
version-bump=### agents-init
|
||||
.version=### agents-init
|
||||
structure-version=### agents-init
|
||||
core.hooksPath=### agents-init
|
||||
devlogs=### agents-init
|
||||
claude-cli=### claude
|
||||
claude-docs=### claude-docs
|
||||
claude-pr=### claude-pr
|
||||
|
||||
+24
-11
@@ -1127,17 +1127,30 @@ Add -i (interactive confirmation) to destructive commands:
|
||||
### agents-init
|
||||
|
||||
Synopsis: agents-init [--agents | --plugins]
|
||||
Scaffold an AGENTS/ sub-repository for tracking agent specs and plugin
|
||||
directories. Creates AGENTS/ as a standalone git repo, initializes
|
||||
standard plugin subdirectories (superpowers/, plans/, specs/), moves any
|
||||
existing AGENTS.md and docs/plugin dirs into the sub-repo, and replaces
|
||||
them with relative symlinks. Also adds CLAUDE.md -> AGENTS/AGENTS.md so
|
||||
Claude Code picks up the shared agent instructions. Adds all managed
|
||||
paths to .gitignore and auto-commits every change inside the AGENTS/
|
||||
sub-repo. Fully idempotent: a second run produces no output and no new
|
||||
commits. Flags: --agents re-runs only the AGENTS.md / symlink step;
|
||||
--plugins re-runs only the plugin-directory wiring step. Called
|
||||
automatically by the claude and agy wrappers on every invocation.
|
||||
Scaffold an AGENTS/ sub-repository for tracking agent specs, plans, specs,
|
||||
and dev logs. Creates AGENTS/ as a standalone git repo, moves any existing
|
||||
AGENTS.md into it, and replaces it with a relative symlink (plus
|
||||
CLAUDE.md -> AGENTS/AGENTS.md so Claude Code picks up the shared agent
|
||||
instructions). Consolidates plans/ and specs/ directly under AGENTS/
|
||||
(merging any legacy docs/plans, docs/superpowers/plans, or old
|
||||
AGENTS/plugins/ locations into the canonical AGENTS/<tgt>), creates
|
||||
AGENTS/devlogs/, and wires docs/superpowers/{plans,specs} symlinks back to
|
||||
them. Adds managed paths to .gitignore and auto-commits every change inside
|
||||
the AGENTS/ sub-repo; pulls first when the sub-repo has an upstream.
|
||||
Fully idempotent: a second run produces no output and no new commits.
|
||||
Flags: --agents re-runs only the AGENTS.md / symlink step; --plugins
|
||||
re-runs only the plans/specs/devlogs wiring step. Called automatically by
|
||||
the claude and agy wrappers on every invocation.
|
||||
|
||||
Structure versioning: each AGENTS/ repo carries a self-contained version
|
||||
bumper. AGENTS/.version holds MAJOR.MINOR.PATCH (seeded 1.0.0). Committed
|
||||
git hooks under AGENTS/.agents-tools/ (wired via core.hooksPath) bump it on
|
||||
every commit: MINOR (resetting PATCH) when the tracked directory set
|
||||
changes, PATCH otherwise; MAJOR is manual-only. A prepare-commit-msg hook
|
||||
appends "(vX.Y.Z)" to the commit subject. Downstream tooling can read
|
||||
AGENTS/.version - a changed MINOR field signals a structure change. The
|
||||
script and hooks are shipped from scripts/agents-tools/ and refreshed when
|
||||
their version marker is stale.
|
||||
|
||||
agents-init
|
||||
agents-init --agents
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# _agents_init_install_tools <agents_dir>
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Copies the canonical version-bump script and git hook shims from
|
||||
# fish-config's scripts/agents-tools/ into <agents_dir>/.agents-tools/,
|
||||
# refreshing them when the shipped `agents-tools-version:` marker is newer
|
||||
# than the installed copy. Files are made executable. Idempotent: prints
|
||||
# nothing when the installed tooling is already current, or a short summary
|
||||
# line when it installed or updated the tooling.
|
||||
#
|
||||
# ARGUMENTS
|
||||
# agents_dir Absolute path to the AGENTS/ sub-repo root
|
||||
#
|
||||
# RETURNS
|
||||
# 0 Tooling is current or was installed/updated successfully
|
||||
# 1 Canonical source missing or a copy failed
|
||||
#
|
||||
# EXAMPLE
|
||||
# set -l msg (_agents_init_install_tools /path/to/AGENTS)
|
||||
# test -n "$msg"; and echo $msg
|
||||
function _agents_init_install_tools --argument-names agents_dir
|
||||
test -n "$agents_dir"; or return 1
|
||||
set -l src (path resolve (status dirname)/../scripts/agents-tools)
|
||||
test -f "$src/version-bump"; or return 1
|
||||
set -l dest "$agents_dir/.agents-tools"
|
||||
|
||||
set -l want (command grep -m1 -oE 'agents-tools-version: *[0-9]+' "$src/version-bump" 2>/dev/null | command grep -oE '[0-9]+$')
|
||||
set -l have ""
|
||||
test -f "$dest/version-bump"; and set have (command grep -m1 -oE 'agents-tools-version: *[0-9]+' "$dest/version-bump" 2>/dev/null | command grep -oE '[0-9]+$')
|
||||
|
||||
test "$want" = "$have"; and return 0
|
||||
|
||||
mkdir -p "$dest/hooks"; or return 1
|
||||
command cp "$src/version-bump" "$dest/version-bump"; or return 1
|
||||
command cp "$src/hooks/pre-commit" "$dest/hooks/pre-commit"; or return 1
|
||||
command cp "$src/hooks/prepare-commit-msg" "$dest/hooks/prepare-commit-msg"; or return 1
|
||||
chmod +x "$dest/version-bump" "$dest/hooks/pre-commit" "$dest/hooks/prepare-commit-msg"; or return 1
|
||||
|
||||
if test -z "$have"
|
||||
echo "→ Installed AGENTS/.agents-tools/ (version-bump v$want)"
|
||||
else
|
||||
echo "→ Updated AGENTS/.agents-tools/ (v$have → v$want)"
|
||||
end
|
||||
end
|
||||
+182
-42
@@ -17,7 +17,27 @@
|
||||
# or symlink → AGENTS.md (single-source case)
|
||||
# <root>/AGENTS.md → AGENTS/AGENTS.md
|
||||
# <root>/CLAUDE.md → AGENTS/CLAUDE.md
|
||||
# docs/<plug> → ../AGENTS/plugins/<plug>
|
||||
# AGENTS/plans superpowers plans (real dir, .gitkeep)
|
||||
# AGENTS/specs superpowers specs (real dir, .gitkeep)
|
||||
# AGENTS/devlogs agent development logs (real dir, .gitkeep)
|
||||
# AGENTS/.version MAJOR.MINOR.PATCH structure version (seed 1.0.0)
|
||||
# AGENTS/.agents-tools/ committed version-bump script + git hook shims
|
||||
# docs/superpowers/plans → ../../AGENTS/plans (always)
|
||||
# docs/superpowers/specs → ../../AGENTS/specs (always)
|
||||
# docs/plans → ../AGENTS/plans (only if docs/plans existed)
|
||||
# docs/specs → ../AGENTS/specs (only if docs/specs existed)
|
||||
# docs/devlogs → ../AGENTS/devlogs (only if docs/devlogs existed)
|
||||
#
|
||||
# plans/ and specs/ are merged from every legacy location (docs/<tgt>,
|
||||
# docs/superpowers/<tgt>, and the old AGENTS/plugins/ layout) into the
|
||||
# canonical AGENTS/<tgt>; the AGENTS/plugins/ layer is removed.
|
||||
#
|
||||
# Each AGENTS repo carries a self-contained version bumper wired via
|
||||
# core.hooksPath: a pre-commit hook bumps AGENTS/.version on every commit
|
||||
# (MINOR when the tracked directory set changes, PATCH otherwise; MAJOR is
|
||||
# manual-only), and a prepare-commit-msg hook appends "(vX.Y.Z)" to the
|
||||
# commit subject. The script/hooks are version-managed from
|
||||
# scripts/agents-tools/ and refreshed when their marker is stale.
|
||||
#
|
||||
# With no flags, runs both --agents and --plugins setup. At the end of
|
||||
# every invocation, commits any uncommitted changes in the AGENTS/ sub-repo
|
||||
@@ -25,7 +45,7 @@
|
||||
#
|
||||
# ARGUMENTS
|
||||
# -a, --agents Set up AGENTS/ repo + AGENTS.md / CLAUDE.md symlinks only
|
||||
# -p, --plugins Set up AGENTS/ repo + plugins dirs + docs/ symlinks only
|
||||
# -p, --plugins Set up AGENTS/ repo + plans/specs/devlogs dirs + docs/ symlinks only
|
||||
# -v, --verbose Print all per-step output (default)
|
||||
# -q, --quiet Print one summary line only if changes were made
|
||||
# -s, --silent Suppress all output; errors only (standard UNIX convention)
|
||||
@@ -61,7 +81,7 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
|
||||
echo "$c_head""Options:$c_reset"
|
||||
echo " $c_flag-h$c_reset, $c_flag--help$c_reset Show this help message"
|
||||
echo " $c_flag-a$c_reset, $c_flag--agents$c_reset Set up AGENTS.md / CLAUDE.md symlinks only"
|
||||
echo " $c_flag-p$c_reset, $c_flag--plugins$c_reset Set up plugins dirs and docs/ symlinks only"
|
||||
echo " $c_flag-p$c_reset, $c_flag--plugins$c_reset Set up plans/specs/devlogs dirs and docs/ symlinks only"
|
||||
echo " $c_flag-v$c_reset, $c_flag--verbose$c_reset Print all per-step output (default)"
|
||||
echo " $c_flag-q$c_reset, $c_flag--quiet$c_reset Print one summary line only if changes were made"
|
||||
echo " $c_flag-s$c_reset, $c_flag--silent$c_reset Suppress all output; only errors are printed"
|
||||
@@ -123,6 +143,26 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
|
||||
test $verbose -eq 1; and echo "$c_ok→ Initialized git repo in AGENTS/$c_reset"
|
||||
end
|
||||
|
||||
# ─────────────── Always: version tracking (.version + hooks) ───────────────
|
||||
if not test -f "$agents_dir/.version"
|
||||
echo 1.0.0 >"$agents_dir/.version"
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok→ Created AGENTS/.version (1.0.0)$c_reset"
|
||||
end
|
||||
|
||||
set -l _tools (_agents_init_install_tools "$agents_dir")
|
||||
if test -n "$_tools"
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok$_tools$c_reset"
|
||||
end
|
||||
|
||||
set -l _hp (git -C "$agents_dir" config --local core.hooksPath 2>/dev/null)
|
||||
if test "$_hp" != .agents-tools/hooks
|
||||
git -C "$agents_dir" config --local core.hooksPath .agents-tools/hooks
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok→ Set core.hooksPath → .agents-tools/hooks$c_reset"
|
||||
end
|
||||
|
||||
# ──────────────────────────── --agents mode ──────────────────────────────
|
||||
if test $do_agents -eq 1
|
||||
# Detect which root-level files are real (not symlinks)
|
||||
@@ -248,19 +288,6 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
|
||||
|
||||
# ─────────────────────────── --plugins mode ──────────────────────────────
|
||||
if test $do_plugins -eq 1
|
||||
# Ensure AGENTS/plugins/ dirs exist with .gitkeep
|
||||
for dir in "$plugins_dir" "$plugins_dir/superpowers" "$plugins_dir/plans" "$plugins_dir/specs"
|
||||
if not test -d "$dir"
|
||||
if not mkdir -p "$dir"
|
||||
echo "$c_err""Error: could not create "(string replace "$root/" "" "$dir")"/$c_reset" >&2
|
||||
return 1
|
||||
end
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok→ Created "(string replace "$root/" "" "$dir")"/$c_reset"
|
||||
end
|
||||
test -f "$dir/.gitkeep"; or touch "$dir/.gitkeep"
|
||||
end
|
||||
|
||||
set -l docs_dir "$root/docs"
|
||||
|
||||
if not test -d "$docs_dir"
|
||||
@@ -272,39 +299,146 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
|
||||
test $verbose -eq 1; and echo "$c_ok→ Created docs/$c_reset"
|
||||
end
|
||||
|
||||
for plug in superpowers plans specs
|
||||
set -l docs_target "$docs_dir/$plug"
|
||||
set -l agents_target "$plugins_dir/$plug"
|
||||
# Consolidate plans/ and specs/ directly under AGENTS/ (no plugins/ layer).
|
||||
# Real source dirs from any known layout are merged into the canonical
|
||||
# AGENTS/<tgt> and removed: docs/<tgt>, docs/superpowers/<tgt>,
|
||||
# AGENTS/plugins/<tgt>, AGENTS/plugins/superpowers/<tgt> (legacy).
|
||||
# Symlinks: docs/superpowers/<tgt> always (superpowers skills expect it);
|
||||
# docs/<tgt> only when it had existed as a real directory.
|
||||
for tgt in plans specs
|
||||
set -l canonical "$agents_dir/$tgt"
|
||||
|
||||
# If a real directory exists in docs/, migrate its contents to AGENTS/plugins/
|
||||
if test -d "$docs_target"; and not test -L "$docs_target"
|
||||
set -l contents (ls -A "$docs_target" 2>/dev/null)
|
||||
if test (count $contents) -gt 0
|
||||
if not command cp -rn "$docs_target/." "$agents_target/"
|
||||
echo "$c_err""Error: could not copy docs/$plug → AGENTS/plugins/$plug$c_reset" >&2
|
||||
return 1
|
||||
end
|
||||
end
|
||||
if not rm -rf "$docs_target"
|
||||
echo "$c_err""Error: could not remove docs/$plug after copy$c_reset" >&2
|
||||
return 1
|
||||
end
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok→ Moved docs/$plug → AGENTS/plugins/$plug$c_reset"
|
||||
# Remember whether docs/<tgt> was a real dir (drives its symlink below)
|
||||
set -l docs_tgt_was_real 0
|
||||
if test -d "$docs_dir/$tgt"; and not test -L "$docs_dir/$tgt"
|
||||
set docs_tgt_was_real 1
|
||||
end
|
||||
|
||||
# Create symlink docs/<plug> → ../AGENTS/plugins/<plug>
|
||||
if not test -L "$docs_target"
|
||||
if not ln -s "../AGENTS/plugins/$plug" "$docs_target"
|
||||
echo "$c_err""Error: could not create docs/$plug symlink$c_reset" >&2
|
||||
return 1
|
||||
test -d "$canonical"; or mkdir -p "$canonical"
|
||||
|
||||
# Candidate real source dirs to merge into the canonical dir
|
||||
set -l srcs "$docs_dir/$tgt" "$plugins_dir/$tgt" "$plugins_dir/superpowers/$tgt"
|
||||
# Only follow docs/superpowers/<tgt> when docs/superpowers is a real
|
||||
# dir; if it is a legacy symlink, its target is already covered by the
|
||||
# AGENTS/plugins/superpowers/<tgt> source above.
|
||||
if not test -L "$docs_dir/superpowers"
|
||||
set -a srcs "$docs_dir/superpowers/$tgt"
|
||||
end
|
||||
|
||||
for src in $srcs
|
||||
if test -d "$src"; and not test -L "$src"
|
||||
set -l rel (string replace "$root/" "" "$src")
|
||||
set -l contents (command ls -A "$src" 2>/dev/null)
|
||||
if test (count $contents) -gt 0
|
||||
if not command cp -rn "$src/." "$canonical/"
|
||||
echo "$c_err""Error: could not merge $rel → AGENTS/$tgt$c_reset" >&2
|
||||
return 1
|
||||
end
|
||||
end
|
||||
if not rm -rf "$src"
|
||||
echo "$c_err""Error: could not remove $rel after merge$c_reset" >&2
|
||||
return 1
|
||||
end
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok→ Merged $rel → AGENTS/$tgt$c_reset"
|
||||
end
|
||||
end
|
||||
|
||||
test -f "$canonical/.gitkeep"; or touch "$canonical/.gitkeep"
|
||||
|
||||
# docs/<tgt> symlink: only recreated when it had been a real directory
|
||||
set -l docs_link "$docs_dir/$tgt"
|
||||
if test $docs_tgt_was_real -eq 1
|
||||
if not test -L "$docs_link"
|
||||
if not ln -s "../AGENTS/$tgt" "$docs_link"
|
||||
echo "$c_err""Error: could not create docs/$tgt symlink$c_reset" >&2
|
||||
return 1
|
||||
end
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok→ Linked docs/$tgt → AGENTS/$tgt$c_reset"
|
||||
end
|
||||
else if test -L "$docs_link"; and test (readlink "$docs_link") != "../AGENTS/$tgt"
|
||||
# Stale legacy symlink (pointed into AGENTS/plugins) → drop it
|
||||
rm -f "$docs_link"
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok→ Linked docs/$plug → AGENTS/plugins/$plug$c_reset"
|
||||
test $verbose -eq 1; and echo "$c_warn→ Removed stale docs/$tgt symlink$c_reset"
|
||||
end
|
||||
end
|
||||
|
||||
set -l _gi (_agents_init_ensure_gitignore "$root" "agents-init --plugins" "docs/superpowers" "docs/plans" "docs/specs")
|
||||
# docs/superpowers/ must be a real dir holding the plans/ + specs/ symlinks.
|
||||
# Replace any legacy docs/superpowers symlink (content merged above).
|
||||
if test -L "$docs_dir/superpowers"
|
||||
rm -f "$docs_dir/superpowers"
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_warn→ Removed legacy docs/superpowers symlink$c_reset"
|
||||
end
|
||||
if not test -d "$docs_dir/superpowers"
|
||||
if not mkdir -p "$docs_dir/superpowers"
|
||||
echo "$c_err""Error: could not create docs/superpowers/$c_reset" >&2
|
||||
return 1
|
||||
end
|
||||
end
|
||||
|
||||
# docs/superpowers/<tgt> symlinks — always present (superpowers default)
|
||||
for tgt in plans specs
|
||||
set -l sp_link "$docs_dir/superpowers/$tgt"
|
||||
if not test -L "$sp_link"
|
||||
if not ln -s "../../AGENTS/$tgt" "$sp_link"
|
||||
echo "$c_err""Error: could not create docs/superpowers/$tgt symlink$c_reset" >&2
|
||||
return 1
|
||||
end
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok→ Linked docs/superpowers/$tgt → AGENTS/$tgt$c_reset"
|
||||
else if test (readlink "$sp_link") != "../../AGENTS/$tgt"
|
||||
rm -f "$sp_link"
|
||||
ln -s "../../AGENTS/$tgt" "$sp_link"
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok→ Relinked docs/superpowers/$tgt → AGENTS/$tgt$c_reset"
|
||||
end
|
||||
end
|
||||
|
||||
# Drop the now-empty legacy AGENTS/plugins/ layer entirely
|
||||
if test -d "$plugins_dir"
|
||||
rm -rf "$plugins_dir"
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_warn→ Removed legacy AGENTS/plugins/$c_reset"
|
||||
end
|
||||
|
||||
# ── AGENTS/devlogs (standard location for agent dev logs) ─────────────
|
||||
set -l devlogs_dir "$agents_dir/devlogs"
|
||||
set -l docs_devlogs "$docs_dir/devlogs"
|
||||
|
||||
if test -d "$docs_devlogs"; and not test -L "$docs_devlogs"
|
||||
# Migrate an existing docs/devlogs into AGENTS/devlogs, then symlink
|
||||
test -d "$devlogs_dir"; or mkdir -p "$devlogs_dir"
|
||||
set -l contents (command ls -A "$docs_devlogs" 2>/dev/null)
|
||||
if test (count $contents) -gt 0
|
||||
if not command cp -rn "$docs_devlogs/." "$devlogs_dir/"
|
||||
echo "$c_err""Error: could not copy docs/devlogs → AGENTS/devlogs$c_reset" >&2
|
||||
return 1
|
||||
end
|
||||
end
|
||||
if not rm -rf "$docs_devlogs"
|
||||
echo "$c_err""Error: could not remove docs/devlogs after copy$c_reset" >&2
|
||||
return 1
|
||||
end
|
||||
if not ln -s "../AGENTS/devlogs" "$docs_devlogs"
|
||||
echo "$c_err""Error: could not create docs/devlogs symlink$c_reset" >&2
|
||||
return 1
|
||||
end
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok→ Moved docs/devlogs → AGENTS/devlogs$c_reset"
|
||||
else if not test -d "$devlogs_dir"
|
||||
if not mkdir -p "$devlogs_dir"
|
||||
echo "$c_err""Error: could not create AGENTS/devlogs$c_reset" >&2
|
||||
return 1
|
||||
end
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok→ Created AGENTS/devlogs/$c_reset"
|
||||
end
|
||||
test -f "$devlogs_dir/.gitkeep"; or touch "$devlogs_dir/.gitkeep"
|
||||
|
||||
set -l _gi (_agents_init_ensure_gitignore "$root" "agents-init --plugins" "docs/superpowers" "docs/plans" "docs/specs" "docs/devlogs")
|
||||
if test -n "$_gi"
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo $_gi
|
||||
@@ -312,6 +446,11 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
|
||||
end
|
||||
|
||||
# ──────────────────────── Auto-commit AGENTS/ ────────────────────────────
|
||||
# Pull first when an upstream is configured so the local .version reflects
|
||||
# any remote bumps before we add to it (no-op for local-only repos).
|
||||
if git -C "$agents_dir" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1
|
||||
git -C "$agents_dir" pull --rebase --autostash -q 2>/dev/null
|
||||
end
|
||||
git -C "$agents_dir" add -A 2>/dev/null
|
||||
set -l status_out (git -C "$agents_dir" status --porcelain 2>/dev/null)
|
||||
if test -n "$status_out"
|
||||
@@ -321,7 +460,8 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
|
||||
set changed 1
|
||||
if test $verbose -eq 1
|
||||
set -l sha (git -C "$agents_dir" rev-parse --short HEAD 2>/dev/null)
|
||||
echo "$c_ok→ Committed AGENTS/ ($sha) $c_dim$msg$c_reset"
|
||||
set -l realmsg (git -C "$agents_dir" log -1 --pretty=%s 2>/dev/null)
|
||||
echo "$c_ok→ Committed AGENTS/ ($sha) $c_dim$realmsg$c_reset"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# agents-tools-version: 1
|
||||
# Delegates to the repo's committed .agents-tools/version-bump. Guarded so a
|
||||
# missing script or failed rev-parse never blocks the commit (always exit 0).
|
||||
root="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
|
||||
[ -x "$root/.agents-tools/version-bump" ] && "$root/.agents-tools/version-bump" precommit
|
||||
exit 0
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# agents-tools-version: 1
|
||||
# Delegates to the repo's committed .agents-tools/version-bump. Guarded so a
|
||||
# missing script or failed rev-parse never blocks the commit (always exit 0).
|
||||
root="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
|
||||
[ -x "$root/.agents-tools/version-bump" ] && "$root/.agents-tools/version-bump" prepmsg "$1"
|
||||
exit 0
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
#
|
||||
# Self-contained tests for version-bump. Run: bash test-version-bump.sh
|
||||
set -u
|
||||
# Neutralize an inherited CDPATH: bash `cd` echoes the resolved path when a
|
||||
# target is found via CDPATH, which would corrupt the command substitutions below.
|
||||
CDPATH=
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
SCRIPT="$HERE/version-bump"
|
||||
fail() { echo "FAIL: $1"; exit 1; }
|
||||
|
||||
new_repo() {
|
||||
local d; d="$(mktemp -d)"
|
||||
git -C "$d" init -q
|
||||
git -C "$d" config user.email t@t
|
||||
git -C "$d" config user.name t
|
||||
git -C "$d" config commit.gpgsign false
|
||||
# Isolate from any global core.hooksPath so the test is hermetic. The e2e
|
||||
# block re-points hooksPath at the repo's own .agents-tools/hooks.
|
||||
git -C "$d" config core.hooksPath /dev/null
|
||||
printf '%s\n' "$d"
|
||||
}
|
||||
|
||||
# ── precommit: baseline + PATCH + MINOR ──────────────────────────────────────
|
||||
r="$(new_repo)"; cd "$r"
|
||||
mkdir -p plans; : > plans/.gitkeep
|
||||
echo 1.0.0 > .version
|
||||
git add -A; "$SCRIPT" precommit; git commit -q -m init
|
||||
[ "$(cat .version)" = 1.0.0 ] || fail "first commit: want 1.0.0 got $(cat .version)"
|
||||
|
||||
echo hi > plans/a.md
|
||||
git add -A; "$SCRIPT" precommit; git commit -q -m content
|
||||
[ "$(cat .version)" = 1.0.1 ] || fail "content: want 1.0.1 got $(cat .version)"
|
||||
|
||||
mkdir -p specs; : > specs/.gitkeep
|
||||
git add -A; "$SCRIPT" precommit; git commit -q -m structure
|
||||
[ "$(cat .version)" = 1.1.0 ] || fail "structure: want 1.1.0 got $(cat .version)"
|
||||
|
||||
echo x > specs/s.md
|
||||
git add -A; "$SCRIPT" precommit; git commit -q -m content2
|
||||
[ "$(cat .version)" = 1.1.1 ] || fail "content2: want 1.1.1 got $(cat .version)"
|
||||
|
||||
cd /; rm -rf "$r"
|
||||
echo "precommit OK"
|
||||
|
||||
# ── prepmsg: subject suffix + idempotency ────────────────────────────────────
|
||||
r="$(new_repo)"; cd "$r"
|
||||
echo 1.2.3 > .version
|
||||
msg="$(mktemp)"
|
||||
printf 'chore: do thing\n' > "$msg"
|
||||
"$SCRIPT" prepmsg "$msg"
|
||||
head -n1 "$msg" | grep -qF 'chore: do thing (v1.2.3)' || fail "prepmsg: subject not suffixed"
|
||||
"$SCRIPT" prepmsg "$msg"
|
||||
[ "$(grep -c '(v1.2.3)' "$msg")" -eq 1 ] || fail "prepmsg: not idempotent"
|
||||
|
||||
# body preserved
|
||||
printf 'subject\n\nbody line\n' > "$msg"
|
||||
echo 2.0.0 > .version
|
||||
"$SCRIPT" prepmsg "$msg"
|
||||
head -n1 "$msg" | grep -qF 'subject (v2.0.0)' || fail "prepmsg: body-case subject"
|
||||
grep -qF 'body line' "$msg" || fail "prepmsg: body lost"
|
||||
|
||||
rm -f "$msg"; cd /; rm -rf "$r"
|
||||
echo "prepmsg OK"
|
||||
|
||||
# ── end-to-end via real hooks + core.hooksPath ───────────────────────────────
|
||||
r="$(new_repo)"; cd "$r"
|
||||
mkdir -p .agents-tools/hooks
|
||||
cp "$HERE/version-bump" .agents-tools/version-bump
|
||||
cp "$HERE/hooks/pre-commit" .agents-tools/hooks/pre-commit
|
||||
cp "$HERE/hooks/prepare-commit-msg" .agents-tools/hooks/prepare-commit-msg
|
||||
chmod +x .agents-tools/version-bump .agents-tools/hooks/pre-commit .agents-tools/hooks/prepare-commit-msg
|
||||
git config core.hooksPath .agents-tools/hooks
|
||||
echo 1.0.0 > .version
|
||||
mkdir -p plans; : > plans/.gitkeep
|
||||
git add -A; git commit -q -m "init"
|
||||
[ "$(cat .version)" = 1.0.0 ] || fail "e2e baseline: want 1.0.0 got $(cat .version)"
|
||||
git log -1 --pretty=%s | grep -qF 'init (v1.0.0)' || fail "e2e baseline message: $(git log -1 --pretty=%s)"
|
||||
|
||||
mkdir -p specs; : > specs/.gitkeep
|
||||
git add -A; git commit -q -m "add specs"
|
||||
[ "$(cat .version)" = 1.1.0 ] || fail "e2e structure: want 1.1.0 got $(cat .version)"
|
||||
git log -1 --pretty=%s | grep -qF 'add specs (v1.1.0)' || fail "e2e structure message: $(git log -1 --pretty=%s)"
|
||||
|
||||
echo z > specs/z.md
|
||||
git add -A; git commit -q -m "edit spec"
|
||||
[ "$(cat .version)" = 1.1.1 ] || fail "e2e content: want 1.1.1 got $(cat .version)"
|
||||
git log -1 --pretty=%s | grep -qF 'edit spec (v1.1.1)' || fail "e2e content message: $(git log -1 --pretty=%s)"
|
||||
|
||||
cd /; rm -rf "$r"
|
||||
echo "e2e OK"
|
||||
|
||||
# ── corrupt .version recovers to a clean baseline ────────────────────────────
|
||||
r="$(new_repo)"; cd "$r"
|
||||
mkdir -p plans; : > plans/.gitkeep
|
||||
echo 1.0.0 > .version
|
||||
git add -A; "$SCRIPT" precommit; git commit -q -m init
|
||||
printf '1.\n' > .version # partial/corrupt
|
||||
echo hi > plans/a.md
|
||||
git add -A; "$SCRIPT" precommit
|
||||
[ "$(cat .version)" = 1.0.1 ] || fail "corrupt recovery: want 1.0.1 got $(cat .version)"
|
||||
cd /; rm -rf "$r"
|
||||
echo "corrupt-recovery OK"
|
||||
|
||||
# ── hook never blocks a commit when version-bump is missing ──────────────────
|
||||
r="$(new_repo)"; cd "$r"
|
||||
mkdir -p .agents-tools/hooks
|
||||
cp "$HERE/hooks/pre-commit" .agents-tools/hooks/pre-commit
|
||||
cp "$HERE/hooks/prepare-commit-msg" .agents-tools/hooks/prepare-commit-msg
|
||||
chmod +x .agents-tools/hooks/pre-commit .agents-tools/hooks/prepare-commit-msg
|
||||
# note: .agents-tools/version-bump intentionally NOT copied
|
||||
git config core.hooksPath .agents-tools/hooks
|
||||
echo hi > file.txt
|
||||
git add -A
|
||||
git commit -q -m "no bumper present" || fail "missing version-bump blocked the commit"
|
||||
git rev-parse -q --verify HEAD >/dev/null || fail "commit did not happen"
|
||||
cd /; rm -rf "$r"
|
||||
echo "hook-resilience OK"
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# agents-tools-version: 1
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
#
|
||||
# version-bump <precommit|prepmsg> [msgfile]
|
||||
#
|
||||
# Maintains <repo>/.version (MAJOR.MINOR.PATCH). MAJOR is manual-only.
|
||||
# MINOR bumps when the tracked directory set changes between HEAD and the
|
||||
# staged index; PATCH bumps on any other commit. Never blocks a commit:
|
||||
# all hook paths exit 0 even on unexpected errors.
|
||||
|
||||
mode="${1:-}"
|
||||
root="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
|
||||
vfile="$root/.version"
|
||||
|
||||
read_dirset() {
|
||||
# $1: HEAD|INDEX — print sorted unique content dirs (dot-dirs excluded)
|
||||
local listing
|
||||
if [ "$1" = HEAD ]; then
|
||||
listing="$(git ls-tree -r --name-only HEAD 2>/dev/null || true)"
|
||||
else
|
||||
listing="$(git ls-files 2>/dev/null || true)"
|
||||
fi
|
||||
printf '%s\n' "$listing" \
|
||||
| while IFS= read -r f; do [ -n "$f" ] && dirname "$f"; done \
|
||||
| { grep -v '^\.' || true; } \
|
||||
| sort -u
|
||||
}
|
||||
|
||||
bump_precommit() {
|
||||
[ -f "$vfile" ] || printf '1.0.0\n' > "$vfile"
|
||||
|
||||
# First commit: establish the baseline, no bump.
|
||||
if ! git rev-parse --verify -q HEAD >/dev/null 2>&1; then
|
||||
grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' "$vfile" || printf '1.0.0\n' > "$vfile"
|
||||
git add -- "$vfile" 2>/dev/null || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
local ver major minor patch
|
||||
ver="$(head -n1 "$vfile")"
|
||||
if printf '%s' "$ver" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
IFS=. read -r major minor patch <<< "$ver"
|
||||
else
|
||||
# Corrupt/partial .version (e.g. "1." or "abc") → reset to a clean baseline.
|
||||
major=1; minor=0; patch=0
|
||||
fi
|
||||
|
||||
if [ "$(read_dirset HEAD)" != "$(read_dirset INDEX)" ]; then
|
||||
minor=$((minor + 1)); patch=0
|
||||
else
|
||||
patch=$((patch + 1))
|
||||
fi
|
||||
printf '%s.%s.%s\n' "$major" "$minor" "$patch" > "$vfile"
|
||||
git add -- "$vfile" 2>/dev/null || true
|
||||
}
|
||||
|
||||
bump_prepmsg() {
|
||||
local msgfile="${1:-}" version first rest
|
||||
[ -n "$msgfile" ] && [ -f "$msgfile" ] || return 0
|
||||
[ -f "$vfile" ] || return 0
|
||||
version="$(head -n1 "$vfile")"
|
||||
[ -n "$version" ] || return 0
|
||||
head -n1 "$msgfile" | grep -qF "(v$version)" && return 0
|
||||
first="$(head -n1 "$msgfile")"
|
||||
rest="$(tail -n +2 "$msgfile")"
|
||||
if [ -n "$rest" ]; then
|
||||
{ printf '%s (v%s)\n' "$first" "$version"; printf '%s\n' "$rest"; } > "$msgfile"
|
||||
else
|
||||
printf '%s (v%s)\n' "$first" "$version" > "$msgfile"
|
||||
fi
|
||||
}
|
||||
|
||||
case "$mode" in
|
||||
precommit) bump_precommit ;;
|
||||
prepmsg) bump_prepmsg "${2:-}" ;;
|
||||
*) echo "version-bump: unknown mode '$mode'" >&2; exit 2 ;;
|
||||
esac
|
||||
exit 0
|
||||
Reference in New Issue
Block a user