Merge pull request 'feat(agents-cleanup): reverse an agents-init scaffold and opt a project out' (#183) from feat/agents-cleanup into main
This commit was merged in pull request #183.
This commit is contained in:
+170
-5
@@ -2470,10 +2470,89 @@ functions). They are active in all interactive sessions.
|
|||||||
|
|
||||||
## 5.12 AI and Developer Tools
|
## 5.12 AI and Developer Tools
|
||||||
|
|
||||||
|
### agents-cleanup
|
||||||
|
|
||||||
|
Synopsis: agents-cleanup [-n | --dry-run] [--drop-extras] [--marker-file]
|
||||||
|
[-v | --verbose] [-q | --quiet] [-s | --silent] [-h | --help]
|
||||||
|
|
||||||
|
Reverses agents-init in the current project, and marks the project so
|
||||||
|
agents-init -- and therefore every claude/agy launch -- leaves it
|
||||||
|
alone from then on.
|
||||||
|
|
||||||
|
Every symlink that resolves into AGENTS/ is replaced by the real file
|
||||||
|
or directory it points to. When two links share a target (docs/plans
|
||||||
|
and docs/superpowers/plans), the shallower one receives the content
|
||||||
|
and the other is removed; a link to a target holding only .gitkeep, a
|
||||||
|
dangling link, or a link to AGENTS/ itself is removed with nothing put
|
||||||
|
in its place. Dangling links are removed even when AGENTS/ is already
|
||||||
|
gone. An AGENTS.md that is exactly
|
||||||
|
the stub agents-init writes is deleted; any other AGENTS.md loses only
|
||||||
|
the SYSTEM DIRECTIVE blockquote that pointed agents at AGENTS/AGENTS.md.
|
||||||
|
No CLAUDE.md is recreated.
|
||||||
|
|
||||||
|
Before anything is moved, pending AGENTS/ changes are committed and
|
||||||
|
the full history is written to a verified git bundle under
|
||||||
|
$XDG_STATE_HOME/agents-cleanup/ (default ~/.local/state). AGENTS/ is
|
||||||
|
then removed, along with docs/superpowers/ and docs/ if left empty,
|
||||||
|
and every "Added by agents-init" block is stripped from .gitignore.
|
||||||
|
Nothing is committed to the outer repository.
|
||||||
|
|
||||||
|
Files inside AGENTS/ that no project symlink points to -- other than
|
||||||
|
agents-init's own .version, .agents-tools/ and .gitkeep files -- stop
|
||||||
|
the cleanup before anything changes. They are listed; --drop-extras
|
||||||
|
discards them instead. A nested git repository inside AGENTS/ is always
|
||||||
|
refused, --drop-extras or not: the bundle keeps only a pointer to it, so
|
||||||
|
move it out first.
|
||||||
|
|
||||||
|
The disabled marker is the per-clone git config key
|
||||||
|
agents-init.disabled, set on every run. --marker-file also writes
|
||||||
|
.agents-disabled in the project root, which agents-init honors too and
|
||||||
|
which may be committed to opt every clone out; it is the only marker
|
||||||
|
available outside a git repository, where the project root is taken to
|
||||||
|
be the current directory -- run it from there. In a project with no
|
||||||
|
AGENTS/, only the marker is set -- a pre-emptive opt-out. agents-init --enable
|
||||||
|
clears the git key again.
|
||||||
|
|
||||||
|
Re-running is safe: an interrupted cleanup resumes where it stopped,
|
||||||
|
and a finished one only confirms the marker.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
-n, --dry-run Print the plan and change nothing
|
||||||
|
--drop-extras Discard unlinked files in AGENTS/ instead of refusing
|
||||||
|
--marker-file Also write .agents-disabled (required outside git)
|
||||||
|
-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
|
||||||
|
-h, --help Show this help message and exit
|
||||||
|
|
||||||
|
Exit Status:
|
||||||
|
0 Cleanup finished, or nothing was left to do
|
||||||
|
1 Refused (outside git without --marker-file, unresolved rebase in
|
||||||
|
AGENTS/, unlinked files or a nested repository in AGENTS/) or a step
|
||||||
|
failed
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
Restore an archived AGENTS/ with: git clone <bundle> AGENTS, then
|
||||||
|
agents-init --enable. The full write-up -- what is kept, what is
|
||||||
|
removed, and how the markers interact -- is in
|
||||||
|
docs/manual/16-agent-tooling.md. Update that section in the same
|
||||||
|
change whenever this function's behavior changes.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
agents-cleanup --dry-run
|
||||||
|
agents-cleanup
|
||||||
|
agents-cleanup --marker-file
|
||||||
|
|
||||||
|
**Dependencies:** `_agents_init_find`, `_agents_init_stub`, `_agents_repo_slug`, `_agents_repo_sync`
|
||||||
|
|
||||||
|
**Classification:** `destructive`, `self-limiting(rm,mkdir,grep)`, `bypasses-shadow(mv)`, `manual-section(16-agent-tooling)`
|
||||||
|
|
||||||
|
**See also:** 16. AI AGENT TOOLING (`docs/manual/16-agent-tooling.md`)
|
||||||
|
|
||||||
### agents-init
|
### agents-init
|
||||||
|
|
||||||
Synopsis: agents-init [-a | --agents] [-p | --plugins] [-v | --verbose]
|
Synopsis: agents-init [-a | --agents] [-p | --plugins] [-e | --enable]
|
||||||
[-q | --quiet] [-s | --silent] [-h | --help]
|
[-v | --verbose] [-q | --quiet] [-s | --silent] [-h | --help]
|
||||||
|
|
||||||
Scaffolds an AGENTS/ sub-repository inside a project directory. Creates
|
Scaffolds an AGENTS/ sub-repository inside a project directory. Creates
|
||||||
a self-contained git repo for agent specifications, moves any existing
|
a self-contained git repo for agent specifications, moves any existing
|
||||||
@@ -2495,6 +2574,13 @@ functions). They are active in all interactive sessions.
|
|||||||
no-op, so running an agent CLI in an arbitrary directory does not
|
no-op, so running an agent CLI in an arbitrary directory does not
|
||||||
create a repository there.
|
create a repository there.
|
||||||
|
|
||||||
|
A project marked disabled is skipped entirely. agents-cleanup sets the
|
||||||
|
per-clone git config key agents-init.disabled; a .agents-disabled file
|
||||||
|
in the project root, which a team may commit, has the same effect.
|
||||||
|
Either one turns every wrapper launch into a silent no-op there.
|
||||||
|
--enable clears the git key and scaffolds; the file has to be deleted
|
||||||
|
by hand, because it is a decision shared with every clone.
|
||||||
|
|
||||||
File layout after setup:
|
File layout after setup:
|
||||||
AGENTS/AGENTS.md canonical root agent spec (real file)
|
AGENTS/AGENTS.md canonical root agent spec (real file)
|
||||||
AGENTS/<subdir>/AGENTS.md canonical spec for any subdir with its own
|
AGENTS/<subdir>/AGENTS.md canonical spec for any subdir with its own
|
||||||
@@ -2555,6 +2641,8 @@ functions). They are active in all interactive sessions.
|
|||||||
-a, --agents Set up AGENTS/ repo + AGENTS.md symlinks (root and every
|
-a, --agents Set up AGENTS/ repo + AGENTS.md symlinks (root and every
|
||||||
discovered subdirectory) only
|
discovered subdirectory) only
|
||||||
-p, --plugins Set up AGENTS/ repo + plans/specs/devlogs dirs + docs/ symlinks only
|
-p, --plugins Set up AGENTS/ repo + plans/specs/devlogs dirs + docs/ symlinks only
|
||||||
|
-e, --enable Clear the git key agents-cleanup set, then scaffold as
|
||||||
|
normal (refused while .agents-disabled exists)
|
||||||
-v, --verbose Print all per-step output (default)
|
-v, --verbose Print all per-step output (default)
|
||||||
-q, --quiet Print one summary line only if changes were made
|
-q, --quiet Print one summary line only if changes were made
|
||||||
-s, --silent Suppress all output; errors only (standard UNIX convention)
|
-s, --silent Suppress all output; errors only (standard UNIX convention)
|
||||||
@@ -2563,7 +2651,8 @@ functions). They are active in all interactive sessions.
|
|||||||
Exit Status:
|
Exit Status:
|
||||||
0 Setup completed successfully
|
0 Setup completed successfully
|
||||||
1 Fatal error (git init failed, move failed, the AGENTS/ commit was
|
1 Fatal error (git init failed, move failed, the AGENTS/ commit was
|
||||||
rejected, or an unresolved rebase blocked it)
|
rejected, or an unresolved rebase blocked it), or --enable refused
|
||||||
|
because .agents-disabled exists
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
This header covers usage only. The full concept/behavior/purpose
|
This header covers usage only. The full concept/behavior/purpose
|
||||||
@@ -2580,7 +2669,7 @@ functions). They are active in all interactive sessions.
|
|||||||
agents-init --plugins
|
agents-init --plugins
|
||||||
agents-init --quiet
|
agents-init --quiet
|
||||||
|
|
||||||
**Dependencies:** `_agents_init_sync_instructions`, `_agents_repo_install_tools`, `_agents_repo_sync`, `_agents_init_ensure_gitignore`
|
**Dependencies:** `_agents_init_find`, `_agents_init_sync_instructions`, `_agents_repo_install_tools`, `_agents_repo_sync`, `_agents_init_ensure_gitignore`
|
||||||
|
|
||||||
**Classification:** `self-limiting(rm,mkdir,grep)`, `bypasses-shadow(mv)`, `manual-section(16-agent-tooling)`
|
**Classification:** `self-limiting(rm,mkdir,grep)`, `bypasses-shadow(mv)`, `manual-section(16-agent-tooling)`
|
||||||
|
|
||||||
@@ -5214,7 +5303,7 @@ Antigravity/agy) working in a project checked out from this configuration:
|
|||||||
where their instructions live, how they get there, and the safety rules
|
where their instructions live, how they get there, and the safety rules
|
||||||
that keep an agent's launch-time bookkeeping from touching a repository's
|
that keep an agent's launch-time bookkeeping from touching a repository's
|
||||||
own tracked history. Command-line usage for the functions named here
|
own tracked history. Command-line usage for the functions named here
|
||||||
(`agents-init`, `agents-vault`) is generated from their own doc headers —
|
(`agents-init`, `agents-cleanup`, `agents-vault`) is generated from their own doc headers —
|
||||||
see Section 5.
|
see Section 5.
|
||||||
|
|
||||||
|
|
||||||
@@ -5479,6 +5568,82 @@ real directory by that name — nothing forces those paths to exist for a
|
|||||||
project that never used them.
|
project that never used them.
|
||||||
|
|
||||||
|
|
||||||
|
## Opting a project out: agents-cleanup
|
||||||
|
|
||||||
|
`agents-cleanup` reverses everything `agents-init` did in a project and
|
||||||
|
stops it from happening again. Run it from anywhere inside the project
|
||||||
|
(from the project root when it is not a git repository):
|
||||||
|
|
||||||
|
agents-cleanup --dry-run
|
||||||
|
agents-cleanup
|
||||||
|
|
||||||
|
Every symlink that points into `AGENTS/` is replaced by the real file or
|
||||||
|
directory it points to, so the project ends up with ordinary files where
|
||||||
|
the links were. Where two links shared one directory (`docs/plans` and
|
||||||
|
`docs/superpowers/plans`), the shallower one — the location that existed
|
||||||
|
before `agents-init` — gets the content and the other link is removed. A
|
||||||
|
link to a directory holding nothing but `.gitkeep`, a dangling link, and a
|
||||||
|
link to `AGENTS/` itself are simply removed; dangling links go even if you
|
||||||
|
already deleted `AGENTS/` by hand. No `CLAUDE.md` is recreated.
|
||||||
|
|
||||||
|
A root `AGENTS.md` that is exactly the seed file `agents-init` writes for
|
||||||
|
a fresh project is deleted, since it never held anything of yours. Any
|
||||||
|
other `AGENTS.md` keeps its content and loses only the "SYSTEM DIRECTIVE"
|
||||||
|
blockquote telling agents to edit `AGENTS/AGENTS.md` — a directory that no
|
||||||
|
longer exists. If the directive was all it held, the file is deleted.
|
||||||
|
|
||||||
|
Before anything moves, pending changes in `AGENTS/` are committed and the
|
||||||
|
whole history is written to a verified git bundle under
|
||||||
|
`~/.local/state/agents-cleanup/` (or `$XDG_STATE_HOME/agents-cleanup/`).
|
||||||
|
Then `AGENTS/` is removed, along with `docs/superpowers/` and `docs/` if
|
||||||
|
they are left empty, and every `Added by agents-init` block is stripped
|
||||||
|
from `.gitignore`. A `.gitignore` that held only those blocks is deleted,
|
||||||
|
unless it is tracked, in which case it is emptied and the change shows in
|
||||||
|
`git status`. Nothing is committed to the project itself: the restored
|
||||||
|
files show up as ordinary changes for you to commit or not.
|
||||||
|
|
||||||
|
WARNING: Files inside `AGENTS/` that no project link points to — notes,
|
||||||
|
scratch files, anything you put there by hand — stop the cleanup before
|
||||||
|
it changes anything, and are listed. Move them out yourself, or pass
|
||||||
|
`--drop-extras` to discard them. `--dry-run` refuses the same way, unless
|
||||||
|
you also give it `--drop-extras`. A git repository nested inside `AGENTS/`
|
||||||
|
is always refused, even with `--drop-extras`: the bundle records only a
|
||||||
|
pointer to it, so move it out first. `agents-cleanup` also refuses to run
|
||||||
|
when `AGENTS/` is itself a symlink to a directory elsewhere, since removing
|
||||||
|
it would remove that directory; replace the link with a real directory
|
||||||
|
first. Discarded files survive only in the bundle, and files
|
||||||
|
`AGENTS/.gitignore` ignores are not in the bundle at all; the listing marks
|
||||||
|
those. An `AGENTS/` that is not a git repository has no bundle, so
|
||||||
|
`--drop-extras` there deletes the files for good.
|
||||||
|
|
||||||
|
### The disabled marker
|
||||||
|
|
||||||
|
`agents-init` skips a project, silently on every `claude`/`agy` launch,
|
||||||
|
when either marker is present:
|
||||||
|
|
||||||
|
- The git config key `agents-init.disabled`, which `agents-cleanup` always
|
||||||
|
sets. It lives in `.git/config`: it is never committed, applies to this
|
||||||
|
clone only, and survives moving or renaming the project. A fresh clone
|
||||||
|
is scaffolded again on its first launch.
|
||||||
|
- A `.agents-disabled` file in the project root, written by
|
||||||
|
`agents-cleanup --marker-file`. It is not committed for you; commit it
|
||||||
|
to opt every clone out. Outside a git repository it is the only marker
|
||||||
|
available, so there `--marker-file` is required.
|
||||||
|
|
||||||
|
Running `agents-cleanup` in a project that was never scaffolded only sets
|
||||||
|
the marker — a way to opt a project out in advance.
|
||||||
|
|
||||||
|
### Undoing a cleanup
|
||||||
|
|
||||||
|
git clone ~/.local/state/agents-cleanup/<slug>-<stamp>.bundle AGENTS
|
||||||
|
agents-init --enable
|
||||||
|
|
||||||
|
`agents-init --enable` clears the git key and scaffolds as usual,
|
||||||
|
re-linking the files restored from the bundle. It refuses while
|
||||||
|
`.agents-disabled` exists: that file is a decision shared with every
|
||||||
|
clone, so delete it (and commit the deletion) by hand.
|
||||||
|
|
||||||
|
|
||||||
## The launch lifecycle
|
## The launch lifecycle
|
||||||
|
|
||||||
The `claude` and `agy` wrapper functions each run `agents-init --quiet`
|
The `claude` and `agy` wrapper functions each run `agents-init --quiet`
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ sidebar:
|
|||||||
helpKeywords:
|
helpKeywords:
|
||||||
- agent
|
- agent
|
||||||
- agents-init
|
- agents-init
|
||||||
|
- agents-cleanup
|
||||||
- agents-vault
|
- agents-vault
|
||||||
- AGENTS.md
|
- AGENTS.md
|
||||||
- claude-code
|
- claude-code
|
||||||
@@ -17,7 +18,7 @@ Antigravity/agy) working in a project checked out from this configuration:
|
|||||||
where their instructions live, how they get there, and the safety rules
|
where their instructions live, how they get there, and the safety rules
|
||||||
that keep an agent's launch-time bookkeeping from touching a repository's
|
that keep an agent's launch-time bookkeeping from touching a repository's
|
||||||
own tracked history. Command-line usage for the functions named here
|
own tracked history. Command-line usage for the functions named here
|
||||||
(`agents-init`, `agents-vault`) is generated from their own doc headers —
|
(`agents-init`, `agents-cleanup`, `agents-vault`) is generated from their own doc headers —
|
||||||
see Section 5.
|
see Section 5.
|
||||||
|
|
||||||
|
|
||||||
@@ -282,6 +283,82 @@ real directory by that name — nothing forces those paths to exist for a
|
|||||||
project that never used them.
|
project that never used them.
|
||||||
|
|
||||||
|
|
||||||
|
## Opting a project out: agents-cleanup
|
||||||
|
|
||||||
|
`agents-cleanup` reverses everything `agents-init` did in a project and
|
||||||
|
stops it from happening again. Run it from anywhere inside the project
|
||||||
|
(from the project root when it is not a git repository):
|
||||||
|
|
||||||
|
agents-cleanup --dry-run
|
||||||
|
agents-cleanup
|
||||||
|
|
||||||
|
Every symlink that points into `AGENTS/` is replaced by the real file or
|
||||||
|
directory it points to, so the project ends up with ordinary files where
|
||||||
|
the links were. Where two links shared one directory (`docs/plans` and
|
||||||
|
`docs/superpowers/plans`), the shallower one — the location that existed
|
||||||
|
before `agents-init` — gets the content and the other link is removed. A
|
||||||
|
link to a directory holding nothing but `.gitkeep`, a dangling link, and a
|
||||||
|
link to `AGENTS/` itself are simply removed; dangling links go even if you
|
||||||
|
already deleted `AGENTS/` by hand. No `CLAUDE.md` is recreated.
|
||||||
|
|
||||||
|
A root `AGENTS.md` that is exactly the seed file `agents-init` writes for
|
||||||
|
a fresh project is deleted, since it never held anything of yours. Any
|
||||||
|
other `AGENTS.md` keeps its content and loses only the "SYSTEM DIRECTIVE"
|
||||||
|
blockquote telling agents to edit `AGENTS/AGENTS.md` — a directory that no
|
||||||
|
longer exists. If the directive was all it held, the file is deleted.
|
||||||
|
|
||||||
|
Before anything moves, pending changes in `AGENTS/` are committed and the
|
||||||
|
whole history is written to a verified git bundle under
|
||||||
|
`~/.local/state/agents-cleanup/` (or `$XDG_STATE_HOME/agents-cleanup/`).
|
||||||
|
Then `AGENTS/` is removed, along with `docs/superpowers/` and `docs/` if
|
||||||
|
they are left empty, and every `Added by agents-init` block is stripped
|
||||||
|
from `.gitignore`. A `.gitignore` that held only those blocks is deleted,
|
||||||
|
unless it is tracked, in which case it is emptied and the change shows in
|
||||||
|
`git status`. Nothing is committed to the project itself: the restored
|
||||||
|
files show up as ordinary changes for you to commit or not.
|
||||||
|
|
||||||
|
WARNING: Files inside `AGENTS/` that no project link points to — notes,
|
||||||
|
scratch files, anything you put there by hand — stop the cleanup before
|
||||||
|
it changes anything, and are listed. Move them out yourself, or pass
|
||||||
|
`--drop-extras` to discard them. `--dry-run` refuses the same way, unless
|
||||||
|
you also give it `--drop-extras`. A git repository nested inside `AGENTS/`
|
||||||
|
is always refused, even with `--drop-extras`: the bundle records only a
|
||||||
|
pointer to it, so move it out first. `agents-cleanup` also refuses to run
|
||||||
|
when `AGENTS/` is itself a symlink to a directory elsewhere, since removing
|
||||||
|
it would remove that directory; replace the link with a real directory
|
||||||
|
first. Discarded files survive only in the bundle, and files
|
||||||
|
`AGENTS/.gitignore` ignores are not in the bundle at all; the listing marks
|
||||||
|
those. An `AGENTS/` that is not a git repository has no bundle, so
|
||||||
|
`--drop-extras` there deletes the files for good.
|
||||||
|
|
||||||
|
### The disabled marker
|
||||||
|
|
||||||
|
`agents-init` skips a project, silently on every `claude`/`agy` launch,
|
||||||
|
when either marker is present:
|
||||||
|
|
||||||
|
- The git config key `agents-init.disabled`, which `agents-cleanup` always
|
||||||
|
sets. It lives in `.git/config`: it is never committed, applies to this
|
||||||
|
clone only, and survives moving or renaming the project. A fresh clone
|
||||||
|
is scaffolded again on its first launch.
|
||||||
|
- A `.agents-disabled` file in the project root, written by
|
||||||
|
`agents-cleanup --marker-file`. It is not committed for you; commit it
|
||||||
|
to opt every clone out. Outside a git repository it is the only marker
|
||||||
|
available, so there `--marker-file` is required.
|
||||||
|
|
||||||
|
Running `agents-cleanup` in a project that was never scaffolded only sets
|
||||||
|
the marker — a way to opt a project out in advance.
|
||||||
|
|
||||||
|
### Undoing a cleanup
|
||||||
|
|
||||||
|
git clone ~/.local/state/agents-cleanup/<slug>-<stamp>.bundle AGENTS
|
||||||
|
agents-init --enable
|
||||||
|
|
||||||
|
`agents-init --enable` clears the git key and scaffolds as usual,
|
||||||
|
re-linking the files restored from the bundle. It refuses while
|
||||||
|
`.agents-disabled` exists: that file is a decision shared with every
|
||||||
|
clone, so delete it (and commit the deletion) by hand.
|
||||||
|
|
||||||
|
|
||||||
## The launch lifecycle
|
## The launch lifecycle
|
||||||
|
|
||||||
The `claude` and `agy` wrapper functions each run `agents-init --quiet`
|
The `claude` and `agy` wrapper functions each run `agents-init --quiet`
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Copyright (C) 2026 Rootiest
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
# SYNOPSIS
|
||||||
|
# _agents_init_find <root> <find-expression>...
|
||||||
|
#
|
||||||
|
# DESCRIPTION
|
||||||
|
# Runs find over <root> with the prune set agents-init discovery uses,
|
||||||
|
# then applies <find-expression> to every entry that survives. Pruned:
|
||||||
|
# dot-directories (.git, .claude, .github -- tool state, not scoped
|
||||||
|
# project dirs), any AGENTS/ (a mirror, never a source), node_modules,
|
||||||
|
# generated-output directories (build, dist, out, target -- an
|
||||||
|
# instruction file there is a build artifact), and nested repositories,
|
||||||
|
# submodules and worktrees (their own .git marks another project).
|
||||||
|
# -mindepth 1 keeps <root> itself, which has a .git, from pruning the
|
||||||
|
# whole walk.
|
||||||
|
#
|
||||||
|
# Shared by agents-init (instruction-file discovery) and agents-cleanup
|
||||||
|
# (symlinks into AGENTS/), so the two never disagree about which part of
|
||||||
|
# the tree belongs to the project.
|
||||||
|
#
|
||||||
|
# ARGUMENTS
|
||||||
|
# root Absolute path to the project root
|
||||||
|
# find-expression find primaries applied to every unpruned entry; must
|
||||||
|
# carry its own action (for example -print)
|
||||||
|
#
|
||||||
|
# EXIT STATUS
|
||||||
|
# 0 find completed
|
||||||
|
# 1 No root given, or find failed
|
||||||
|
#
|
||||||
|
# RETURNS
|
||||||
|
# Whatever <find-expression> prints.
|
||||||
|
#
|
||||||
|
# EXAMPLE
|
||||||
|
# _agents_init_find /path/to/project \( -name AGENTS.md -o -name CLAUDE.md \) -print
|
||||||
|
# _agents_init_find /path/to/project -type l -print
|
||||||
|
function _agents_init_find --argument-names root
|
||||||
|
test -n "$root"; or return 1
|
||||||
|
find "$root" -mindepth 1 \
|
||||||
|
-type d \( -name '.*' -o -name AGENTS -o -name node_modules \
|
||||||
|
-o -name build -o -name dist -o -name out -o -name target \
|
||||||
|
-o -exec test -e '{}/.git' \; \) -prune -o \
|
||||||
|
$argv[2..]
|
||||||
|
end
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Copyright (C) 2026 Rootiest
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
# SYNOPSIS
|
||||||
|
# _agents_init_stub
|
||||||
|
#
|
||||||
|
# DESCRIPTION
|
||||||
|
# Prints the AGENTS.md that agents-init writes for a project with no
|
||||||
|
# agent instructions of its own: a heading and a directive telling
|
||||||
|
# agents to edit AGENTS/AGENTS.md rather than the root symlink.
|
||||||
|
#
|
||||||
|
# One source for the text: _agents_init_sync_instructions writes it, and
|
||||||
|
# agents-cleanup compares against it byte for byte to recognize a stub
|
||||||
|
# that never held user content.
|
||||||
|
#
|
||||||
|
# EXIT STATUS
|
||||||
|
# 0 Always
|
||||||
|
#
|
||||||
|
# RETURNS
|
||||||
|
# The stub, on stdout.
|
||||||
|
#
|
||||||
|
# EXAMPLE
|
||||||
|
# _agents_init_stub >AGENTS/AGENTS.md
|
||||||
|
function _agents_init_stub
|
||||||
|
printf '%s\n' \
|
||||||
|
'# AGENTS.md' \
|
||||||
|
'' \
|
||||||
|
'> ⚠️ **SYSTEM DIRECTIVE FOR AI AGENTS: FILE EDITING**' \
|
||||||
|
'> You may be reading this file via a symlink (`AGENTS.md`) in' \
|
||||||
|
'> the root of the project. Your environment'\''s file-editing tools cannot write' \
|
||||||
|
'> through symlinks and will throw an error.' \
|
||||||
|
'>' \
|
||||||
|
'> **DO NOT** attempt to write to or edit `AGENTS.md` in the' \
|
||||||
|
'> project root. If you need to update these instructions, you **MUST write' \
|
||||||
|
'> directly to `AGENTS/AGENTS.md`**.'
|
||||||
|
end
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
# DEPENDENCIES
|
# DEPENDENCIES
|
||||||
# _agents_init_path_is_protected
|
# _agents_init_path_is_protected, _agents_init_stub
|
||||||
#
|
#
|
||||||
# CLASSIFICATION
|
# CLASSIFICATION
|
||||||
# self-limiting(rm,mkdir), bypasses-shadow(mv)
|
# self-limiting(rm,mkdir), bypasses-shadow(mv)
|
||||||
@@ -169,17 +169,7 @@ function _agents_init_sync_instructions --argument-names root agents_dir rel
|
|||||||
end
|
end
|
||||||
echo "→ Moved $disp_claude → $mirror_rel/AGENTS.md"
|
echo "→ Moved $disp_claude → $mirror_rel/AGENTS.md"
|
||||||
else if test "$rel" = "."
|
else if test "$rel" = "."
|
||||||
printf '%s\n' \
|
_agents_init_stub >"$mirror_agents"
|
||||||
'# AGENTS.md' \
|
|
||||||
'' \
|
|
||||||
'> ⚠️ **SYSTEM DIRECTIVE FOR AI AGENTS: FILE EDITING**' \
|
|
||||||
'> You may be reading this file via a symlink (`AGENTS.md`) in' \
|
|
||||||
'> the root of the project. Your environment'\''s file-editing tools cannot write' \
|
|
||||||
'> through symlinks and will throw an error.' \
|
|
||||||
'>' \
|
|
||||||
'> **DO NOT** attempt to write to or edit `AGENTS.md` in the' \
|
|
||||||
'> project root. If you need to update these instructions, you **MUST write' \
|
|
||||||
'> directly to `AGENTS/AGENTS.md`**.' >"$mirror_agents"
|
|
||||||
echo "→ Created AGENTS/AGENTS.md with agent directive"
|
echo "→ Created AGENTS/AGENTS.md with agent directive"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
#
|
#
|
||||||
# DESCRIPTION
|
# DESCRIPTION
|
||||||
# Stages everything in <dir> and commits it with <message>. Shared by
|
# Stages everything in <dir> and commits it with <message>. Shared by
|
||||||
# agents-init and agents-vault.
|
# agents-init, agents-vault and agents-cleanup. <dir> must be the root of
|
||||||
|
# its own repository: a .git directory that is not a valid gitdir is
|
||||||
|
# refused, because git would otherwise commit the enclosing repository.
|
||||||
#
|
#
|
||||||
# It never touches the network, and that is the point rather than an
|
# It never touches the network, and that is the point rather than an
|
||||||
# omission. Both callers run on every agent launch, synchronously, ahead
|
# omission. Both callers run on every agent launch, synchronously, ahead
|
||||||
@@ -36,7 +38,8 @@
|
|||||||
#
|
#
|
||||||
# EXIT STATUS
|
# EXIT STATUS
|
||||||
# 0 Committed, or nothing needed committing
|
# 0 Committed, or nothing needed committing
|
||||||
# 1 <dir> is not a git repository, arguments were missing, or the commit
|
# 1 <dir> is not the root of its own git repository (a broken .git
|
||||||
|
# directory inside another repository counts as not), arguments were missing, or the commit
|
||||||
# itself failed (e.g. a pre-commit/commit-msg hook rejected it)
|
# itself failed (e.g. a pre-commit/commit-msg hook rejected it)
|
||||||
# 2 A rebase is in progress; nothing committed, nothing touched
|
# 2 A rebase is in progress; nothing committed, nothing touched
|
||||||
#
|
#
|
||||||
@@ -48,9 +51,12 @@
|
|||||||
# _agents_repo_sync /path/to/AGENTS "chore: sync AGENTS repository"
|
# _agents_repo_sync /path/to/AGENTS "chore: sync AGENTS repository"
|
||||||
function _agents_repo_sync --argument-names dir msg
|
function _agents_repo_sync --argument-names dir msg
|
||||||
test -n "$dir" -a -n "$msg"; or return 1
|
test -n "$dir" -a -n "$msg"; or return 1
|
||||||
test -d "$dir/.git"; or return 1
|
# Not `test -d "$dir/.git"`: a half-deleted .git directory passes that, and
|
||||||
|
# git then walks up, finds the enclosing repository and commits *that*.
|
||||||
|
# A valid repository rooted at <dir> reports its git dir as plain ".git".
|
||||||
|
test "$(git -C "$dir" rev-parse --git-dir 2>/dev/null)" = .git; or return 1
|
||||||
|
|
||||||
# The guard above proved .git is a directory, so these are the same two
|
# The guard above proved <dir> has its own .git, so these are the same two
|
||||||
# paths `agents-vault --status` reports an unresolved rebase from.
|
# paths `agents-vault --status` reports an unresolved rebase from.
|
||||||
if test -d "$dir/.git/rebase-merge"; or test -d "$dir/.git/rebase-apply"
|
if test -d "$dir/.git/rebase-merge"; or test -d "$dir/.git/rebase-apply"
|
||||||
echo "_agents_repo_sync: unresolved rebase in $dir; nothing committed" >&2
|
echo "_agents_repo_sync: unresolved rebase in $dir; nothing committed" >&2
|
||||||
|
|||||||
@@ -0,0 +1,548 @@
|
|||||||
|
# Copyright (C) 2026 Rootiest
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
# CATEGORY
|
||||||
|
# 12-ai-and-developer-tools
|
||||||
|
#
|
||||||
|
# DEPENDENCIES
|
||||||
|
# _agents_init_find, _agents_init_stub, _agents_repo_slug, _agents_repo_sync
|
||||||
|
#
|
||||||
|
# CLASSIFICATION
|
||||||
|
# destructive, self-limiting(rm,mkdir,grep), bypasses-shadow(mv), manual-section(16-agent-tooling)
|
||||||
|
#
|
||||||
|
# SYNOPSIS
|
||||||
|
# agents-cleanup [-n | --dry-run] [--drop-extras] [--marker-file]
|
||||||
|
# [-v | --verbose] [-q | --quiet] [-s | --silent] [-h | --help]
|
||||||
|
#
|
||||||
|
# DESCRIPTION
|
||||||
|
# Reverses agents-init in the current project, and marks the project so
|
||||||
|
# agents-init -- and therefore every claude/agy launch -- leaves it
|
||||||
|
# alone from then on.
|
||||||
|
#
|
||||||
|
# Every symlink that resolves into AGENTS/ is replaced by the real file
|
||||||
|
# or directory it points to. When two links share a target (docs/plans
|
||||||
|
# and docs/superpowers/plans), the shallower one receives the content
|
||||||
|
# and the other is removed; a link to a target holding only .gitkeep, a
|
||||||
|
# dangling link, or a link to AGENTS/ itself is removed with nothing put
|
||||||
|
# in its place. Dangling links are removed even when AGENTS/ is already
|
||||||
|
# gone. An AGENTS.md that is exactly
|
||||||
|
# the stub agents-init writes is deleted; any other AGENTS.md loses only
|
||||||
|
# the SYSTEM DIRECTIVE blockquote that pointed agents at AGENTS/AGENTS.md.
|
||||||
|
# No CLAUDE.md is recreated.
|
||||||
|
#
|
||||||
|
# Before anything is moved, pending AGENTS/ changes are committed and
|
||||||
|
# the full history is written to a verified git bundle under
|
||||||
|
# $XDG_STATE_HOME/agents-cleanup/ (default ~/.local/state). AGENTS/ is
|
||||||
|
# then removed, along with docs/superpowers/ and docs/ if left empty,
|
||||||
|
# and every "Added by agents-init" block is stripped from .gitignore.
|
||||||
|
# Nothing is committed to the outer repository.
|
||||||
|
#
|
||||||
|
# Files inside AGENTS/ that no project symlink points to -- other than
|
||||||
|
# agents-init's own .version, .agents-tools/ and .gitkeep files -- stop
|
||||||
|
# the cleanup before anything changes. They are listed; --drop-extras
|
||||||
|
# discards them instead. A nested git repository inside AGENTS/ is always
|
||||||
|
# refused, --drop-extras or not: the bundle keeps only a pointer to it, so
|
||||||
|
# move it out first.
|
||||||
|
#
|
||||||
|
# The disabled marker is the per-clone git config key
|
||||||
|
# agents-init.disabled, set on every run. --marker-file also writes
|
||||||
|
# .agents-disabled in the project root, which agents-init honors too and
|
||||||
|
# which may be committed to opt every clone out; it is the only marker
|
||||||
|
# available outside a git repository, where the project root is taken to
|
||||||
|
# be the current directory -- run it from there. In a project with no
|
||||||
|
# AGENTS/, only the marker is set -- a pre-emptive opt-out. agents-init --enable
|
||||||
|
# clears the git key again.
|
||||||
|
#
|
||||||
|
# Re-running is safe: an interrupted cleanup resumes where it stopped,
|
||||||
|
# and a finished one only confirms the marker.
|
||||||
|
#
|
||||||
|
# ARGUMENTS
|
||||||
|
# -n, --dry-run Print the plan and change nothing
|
||||||
|
# --drop-extras Discard unlinked files in AGENTS/ instead of refusing
|
||||||
|
# --marker-file Also write .agents-disabled (required outside git)
|
||||||
|
# -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
|
||||||
|
# -h, --help Show this help message and exit
|
||||||
|
#
|
||||||
|
# EXIT STATUS
|
||||||
|
# 0 Cleanup finished, or nothing was left to do
|
||||||
|
# 1 Refused (outside git without --marker-file, unresolved rebase in
|
||||||
|
# AGENTS/, unlinked files or a nested repository in AGENTS/) or a step
|
||||||
|
# failed
|
||||||
|
#
|
||||||
|
# EXAMPLE
|
||||||
|
# agents-cleanup --dry-run
|
||||||
|
# agents-cleanup
|
||||||
|
# agents-cleanup --marker-file
|
||||||
|
#
|
||||||
|
# NOTES
|
||||||
|
# Restore an archived AGENTS/ with: git clone <bundle> AGENTS, then
|
||||||
|
# agents-init --enable. The full write-up -- what is kept, what is
|
||||||
|
# removed, and how the markers interact -- is in
|
||||||
|
# docs/manual/16-agent-tooling.md. Update that section in the same
|
||||||
|
# change whenever this function's behavior changes.
|
||||||
|
function agents-cleanup --description 'undo agents-init: restore real files, archive and remove AGENTS/, disable agents-init'
|
||||||
|
__fish_palette
|
||||||
|
|
||||||
|
argparse h/help n/dry-run drop-extras marker-file v/verbose q/quiet s/silent -- $argv
|
||||||
|
or return 1
|
||||||
|
|
||||||
|
if set -q _flag_help
|
||||||
|
echo "$c_head""Usage:$c_reset $c_cmd""agents-cleanup$c_reset $c_flag""[-n] [--drop-extras] [--marker-file] [-v] [-q] [-s] [-h | --help]$c_reset"
|
||||||
|
echo
|
||||||
|
echo " Undo agents-init here: restore real files, archive and remove AGENTS/,"
|
||||||
|
echo " and stop agents-init from scaffolding this project again."
|
||||||
|
echo
|
||||||
|
echo "$c_head""Options:$c_reset"
|
||||||
|
echo " $c_flag-h$c_reset, $c_flag--help$c_reset Show this help message"
|
||||||
|
echo " $c_flag-n$c_reset, $c_flag--dry-run$c_reset Print the plan and change nothing"
|
||||||
|
echo " $c_flag--drop-extras$c_reset Discard unlinked files in AGENTS/ instead of refusing"
|
||||||
|
echo " $c_flag--marker-file$c_reset Also write .agents-disabled (required outside git)"
|
||||||
|
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"
|
||||||
|
echo
|
||||||
|
echo " Re-enable later with $c_cmd""agents-init --enable$c_reset."
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
set -l verbose 1
|
||||||
|
set -l quiet 0
|
||||||
|
if set -q _flag_silent
|
||||||
|
set verbose 0
|
||||||
|
else if set -q _flag_quiet
|
||||||
|
set verbose 0
|
||||||
|
set quiet 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# ──────────────────────────── Project root ────────────────────────────
|
||||||
|
set -l root (git rev-parse --show-toplevel 2>/dev/null)
|
||||||
|
set -l in_git 1
|
||||||
|
if test -z "$root"
|
||||||
|
set in_git 0
|
||||||
|
if not set -q _flag_marker_file
|
||||||
|
echo "$c_err""Error: not a git repository, so there is no .git/config to hold the marker; re-run with --marker-file to write .agents-disabled instead$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
set root (pwd)
|
||||||
|
end
|
||||||
|
|
||||||
|
# ──────────────────────── Phase 1: preflight ─────────────────────────
|
||||||
|
# Read-only. Every refusal below returns before the first change.
|
||||||
|
set -l agents_dir "$root/AGENTS"
|
||||||
|
set -l has_agents 0
|
||||||
|
set -l has_repo 0
|
||||||
|
if test -L "$agents_dir"
|
||||||
|
echo "$c_err""Error: AGENTS is a symlink to "(readlink "$agents_dir")"; replace it with a real directory (or remove the link) first$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
if test -d "$agents_dir"
|
||||||
|
set has_agents 1
|
||||||
|
# Resolved, because link targets are compared after realpath, which
|
||||||
|
# also resolves any symlinked parent of $root.
|
||||||
|
set agents_dir (realpath -- "$agents_dir")
|
||||||
|
# A .git that is not a valid gitdir (half-removed) is "not a repository";
|
||||||
|
# git would otherwise resolve to the enclosing project.
|
||||||
|
test "$(git -C "$agents_dir" rev-parse --git-dir 2>/dev/null)" = .git; and set has_repo 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if test $has_repo -eq 1
|
||||||
|
if test -d "$agents_dir/.git/rebase-merge"; or test -d "$agents_dir/.git/rebase-apply"
|
||||||
|
echo "$c_err""Error: AGENTS/ has an unresolved rebase; finish or abort it first$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Links into AGENTS/, shallowest first (then lexical): when two links
|
||||||
|
# share a target, the original location -- docs/plans rather than
|
||||||
|
# docs/superpowers/plans -- is the one that receives the content.
|
||||||
|
set -l keep # links to materialize
|
||||||
|
set -l keep_tgt # their resolved targets, index-aligned with $keep
|
||||||
|
set -l drop # links removed with nothing put in their place
|
||||||
|
# Also runs with no AGENTS/ (deleted by hand): every such link then
|
||||||
|
# dangles and is dropped. Not outside git, though: there (pwd) is only a
|
||||||
|
# guess at the project root, so without an AGENTS/ in it the walk could
|
||||||
|
# cover an arbitrary tree.
|
||||||
|
if test $has_agents -eq 1; or test $in_git -eq 1
|
||||||
|
test $has_agents -eq 1; or set agents_dir (realpath -m -- "$agents_dir")
|
||||||
|
set -l agents_re '^'(string escape --style=regex -- "$agents_dir")'(/|$)'
|
||||||
|
set -l links
|
||||||
|
for l in (_agents_init_find "$root" -type l -print)
|
||||||
|
string match -qr -- $agents_re (realpath -m -- "$l"); and set -a links "$l"
|
||||||
|
end
|
||||||
|
set links (for l in $links
|
||||||
|
printf '%s\t%s\n' (count (string split / -- "$l")) "$l"
|
||||||
|
end | sort -t\t -k1,1n -k2 | string replace -r '^\d+\t' '')
|
||||||
|
|
||||||
|
for l in $links
|
||||||
|
set -l t (realpath -m -- "$l")
|
||||||
|
if not test -e "$t"
|
||||||
|
set -a drop "$l" # dangling
|
||||||
|
else if test "$t" = "$agents_dir"
|
||||||
|
# A link to AGENTS/ itself: materializing it would move the
|
||||||
|
# whole repository. Drop-only, like any link with nothing to restore.
|
||||||
|
set -a drop "$l"
|
||||||
|
else if contains -- "$t" $keep_tgt
|
||||||
|
set -a drop "$l" # duplicate of a shallower link
|
||||||
|
else if test -d "$t"; and test -z "$(find "$t" ! -type d ! -name .gitkeep -print -quit)"
|
||||||
|
set -a drop "$l" # only .gitkeep: nothing to restore
|
||||||
|
else
|
||||||
|
set -a keep "$l"
|
||||||
|
set -a keep_tgt "$t"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Files in AGENTS/ that no kept link carries out, minus agents-init's
|
||||||
|
# own tooling. Refused by default: nothing says where they belong.
|
||||||
|
set -l extras
|
||||||
|
set -l extras_ignored
|
||||||
|
set -l nested
|
||||||
|
if test $has_agents -eq 1
|
||||||
|
set -l cover_re
|
||||||
|
for t in $keep_tgt
|
||||||
|
set -a cover_re '^'(string escape --style=regex -- "$t")'(/|$)'
|
||||||
|
end
|
||||||
|
# A nested repository is recorded by `git add -A` as a bare gitlink,
|
||||||
|
# so neither the bundle nor --drop-extras treats it as files: it is
|
||||||
|
# listed on its own and never walked into.
|
||||||
|
for f in (find "$agents_dir" -mindepth 1 -name .git -prune -o -type d -exec test -e '{}/.git' \; -print -prune)
|
||||||
|
set -l covered 0
|
||||||
|
for re in $cover_re
|
||||||
|
if string match -qr -- $re "$f"
|
||||||
|
set covered 1
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
test $covered -eq 1; or set -a nested (string replace -- "$agents_dir/" "" "$f")
|
||||||
|
end
|
||||||
|
for f in (find "$agents_dir" -mindepth 1 \( -name .git -o -type d -exec test -e '{}/.git' \; \) -prune -o ! -type d -print)
|
||||||
|
set -l rel (string replace -- "$agents_dir/" "" "$f")
|
||||||
|
switch "$rel"
|
||||||
|
case .version '.agents-tools/*' .gitkeep '*/.gitkeep'
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
set -l covered 0
|
||||||
|
for re in $cover_re
|
||||||
|
if string match -qr -- $re "$f"
|
||||||
|
set covered 1
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
test $covered -eq 1; and continue
|
||||||
|
set -a extras "$rel"
|
||||||
|
if test $has_repo -eq 1; and git -C "$agents_dir" check-ignore -q -- "$rel" 2>/dev/null
|
||||||
|
set -a extras_ignored "$rel"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if set -q nested[1]
|
||||||
|
echo "$c_err""Error: AGENTS/ holds nested git repositories; the history bundle records only a pointer to them, so they would be lost:$c_reset" >&2
|
||||||
|
for n in $nested
|
||||||
|
echo " AGENTS/$n/ (nested repository -- not in bundle)" >&2
|
||||||
|
end
|
||||||
|
echo "Move them out of AGENTS/ first; --drop-extras does not cover them." >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if set -q extras[1]; and not set -q _flag_drop_extras
|
||||||
|
echo "$c_err""Error: AGENTS/ holds files no project symlink points to:$c_reset" >&2
|
||||||
|
for e in $extras
|
||||||
|
if contains -- "$e" $extras_ignored
|
||||||
|
echo " AGENTS/$e (ignored -- not in bundle)" >&2
|
||||||
|
else if test $has_repo -eq 0
|
||||||
|
echo " AGENTS/$e (not in bundle)" >&2
|
||||||
|
else
|
||||||
|
echo " AGENTS/$e" >&2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if test $has_repo -eq 1
|
||||||
|
echo "Move them out of AGENTS/ by hand, or re-run with --drop-extras (the history bundle keeps every file not marked ignored)." >&2
|
||||||
|
else
|
||||||
|
echo "Move them out of AGENTS/ by hand, or re-run with --drop-extras (AGENTS/ is not a git repository, so there is no bundle and they are lost)." >&2
|
||||||
|
end
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
set -l state_dir $XDG_STATE_HOME
|
||||||
|
test -n "$state_dir"; or set state_dir "$HOME/.local/state"
|
||||||
|
set -l bundle_base "$state_dir/agents-cleanup/"(_agents_repo_slug "$root")"-"(date +%Y%m%d-%H%M%S)
|
||||||
|
# `git bundle create` overwrites: never reuse a name from the same second.
|
||||||
|
set -l bundle "$bundle_base.bundle"
|
||||||
|
set -l n 0
|
||||||
|
while test -e "$bundle"
|
||||||
|
set n (math $n + 1)
|
||||||
|
set bundle "$bundle_base-$fish_pid-$n.bundle"
|
||||||
|
end
|
||||||
|
set -l gitignore "$root/.gitignore"
|
||||||
|
|
||||||
|
if set -q _flag_dry_run
|
||||||
|
set -q _flag_silent; and return 0
|
||||||
|
echo "$c_head""Dry run -- nothing changed. Plan:$c_reset"
|
||||||
|
test $in_git -eq 1; and echo " set git config agents-init.disabled true"
|
||||||
|
set -q _flag_marker_file; and echo " write .agents-disabled"
|
||||||
|
test $has_repo -eq 1; and echo " archive AGENTS/ history to $bundle"
|
||||||
|
for i in (seq (count $keep))
|
||||||
|
echo " restore "(string replace -- "$root/" "" "$keep[$i]")" from AGENTS/"(string replace -- "$agents_dir/" "" "$keep_tgt[$i]")
|
||||||
|
end
|
||||||
|
for l in $drop
|
||||||
|
echo " remove link "(string replace -- "$root/" "" "$l")
|
||||||
|
end
|
||||||
|
for e in $extras
|
||||||
|
echo " discard AGENTS/$e"
|
||||||
|
end
|
||||||
|
test $has_agents -eq 1; and echo " remove AGENTS/"
|
||||||
|
if test -f "$gitignore"; and grep -q 'Added by agents-init' "$gitignore"
|
||||||
|
echo " remove agents-init blocks from .gitignore"
|
||||||
|
end
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
# ─────────────────────────── Phase 2: mark ───────────────────────────
|
||||||
|
# Before any destructive step: a claude/agy launch mid-cleanup must not
|
||||||
|
# re-scaffold, and a re-run after a failure resumes instead of fighting
|
||||||
|
# agents-init.
|
||||||
|
set -l changed 0
|
||||||
|
set -l archived
|
||||||
|
if test $in_git -eq 1
|
||||||
|
set -l cur (git -C "$root" config --local --type=bool --get agents-init.disabled 2>/dev/null)
|
||||||
|
if test "$cur" != true
|
||||||
|
if not git -C "$root" config --local agents-init.disabled true
|
||||||
|
echo "$c_err""Error: could not set git config agents-init.disabled$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
set changed 1
|
||||||
|
test $verbose -eq 1; and echo "$c_ok→ Set git config agents-init.disabled true$c_reset"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if set -q _flag_marker_file; and not test -e "$root/.agents-disabled"
|
||||||
|
if not printf '%s\n' \
|
||||||
|
'# agents-init skips this project while this file exists.' \
|
||||||
|
'# Written by agents-cleanup. Commit it to opt every clone out;' \
|
||||||
|
'# delete it (and commit the deletion) to re-enable.' >"$root/.agents-disabled"
|
||||||
|
echo "$c_err""Error: could not write .agents-disabled$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
set changed 1
|
||||||
|
test $verbose -eq 1; and echo "$c_ok→ Wrote .agents-disabled$c_reset"
|
||||||
|
end
|
||||||
|
|
||||||
|
# ────────────────────────── Phase 3: archive ─────────────────────────
|
||||||
|
# Nothing is moved until the history is safely out of AGENTS/.
|
||||||
|
if test $has_repo -eq 1
|
||||||
|
# 2>/dev/null: the helper's own message would leak past --silent (a
|
||||||
|
# command substitution's stderr ignores the caller's redirect); the
|
||||||
|
# error below says the same thing.
|
||||||
|
set -l sync_out (_agents_repo_sync "$agents_dir" "chore: final sync before agents-cleanup" 2>/dev/null)
|
||||||
|
if test $status -ne 0
|
||||||
|
echo "$c_err""Error: could not commit pending AGENTS/ changes; nothing moved or removed$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
if test -n "$sync_out"
|
||||||
|
set changed 1
|
||||||
|
test $verbose -eq 1; and echo "$c_ok$sync_out$c_reset"
|
||||||
|
end
|
||||||
|
|
||||||
|
if git -C "$agents_dir" rev-parse -q --verify HEAD >/dev/null
|
||||||
|
if not mkdir -p (path dirname "$bundle")
|
||||||
|
echo "$c_err""Error: could not create "(path dirname "$bundle")"; nothing moved or removed$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
if not git -C "$agents_dir" bundle create -q "$bundle" --all 2>/dev/null
|
||||||
|
echo "$c_err""Error: could not write $bundle; nothing moved or removed$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
if not git -C "$agents_dir" bundle verify -q "$bundle" >/dev/null 2>&1
|
||||||
|
echo "$c_err""Error: $bundle failed verification; nothing moved or removed$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
set changed 1
|
||||||
|
set archived "$bundle"
|
||||||
|
if test $verbose -eq 1
|
||||||
|
echo "$c_ok→ Archived AGENTS/ history to $bundle$c_reset"
|
||||||
|
echo "$c_dim restore with: git clone $bundle AGENTS$c_reset"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
test $verbose -eq 1; and echo "$c_dim→ AGENTS/ has no commits; nothing to archive$c_reset"
|
||||||
|
end
|
||||||
|
else if test $has_agents -eq 1
|
||||||
|
test $verbose -eq 1; and echo "$c_dim→ AGENTS/ is not a git repository; nothing to archive$c_reset"
|
||||||
|
end
|
||||||
|
|
||||||
|
# ──────────────────────── Phase 4: materialize ───────────────────────
|
||||||
|
set -l directive 'SYSTEM DIRECTIVE FOR AI AGENTS: FILE EDITING'
|
||||||
|
for i in (seq (count $keep))
|
||||||
|
set -l link $keep[$i]
|
||||||
|
set -l rel (string replace -- "$root/" "" "$link")
|
||||||
|
set -l was (readlink "$link")
|
||||||
|
if not test -e "$keep_tgt[$i]"
|
||||||
|
echo "$c_err""Error: AGENTS/ target for $rel is missing; nothing changed for it$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
if not rm -f "$link"
|
||||||
|
echo "$c_err""Error: could not restore $rel from AGENTS/; re-run to resume$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
if not command mv "$keep_tgt[$i]" "$link"
|
||||||
|
# Put the link back: without it the target looks like an unlinked
|
||||||
|
# extra and a re-run would refuse.
|
||||||
|
if ln -s -- "$was" "$link"
|
||||||
|
echo "$c_err""Error: could not restore $rel from AGENTS/; link left in place$c_reset" >&2
|
||||||
|
else
|
||||||
|
set -l where "AGENTS/"(string replace -- "$agents_dir/" "" "$keep_tgt[$i]")
|
||||||
|
test -n "$archived"; and set where "$where and in the bundle"
|
||||||
|
echo "$c_err""Error: could not restore $rel and could not put its link back; the content is still at $where$c_reset" >&2
|
||||||
|
end
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
set changed 1
|
||||||
|
test $verbose -eq 1; and echo "$c_ok→ Restored $rel$c_reset"
|
||||||
|
|
||||||
|
if test -d "$link"
|
||||||
|
rm -f "$link/.gitkeep"
|
||||||
|
else if test (path basename "$link") = AGENTS.md
|
||||||
|
if _agents_init_stub | cmp -s - "$link"
|
||||||
|
rm -f "$link"
|
||||||
|
test $verbose -eq 1; and echo "$c_ok→ Removed $rel (agents-init stub, no user content)$c_reset"
|
||||||
|
else if grep -qF -- "$directive" "$link"
|
||||||
|
# Drop the first run of '>' lines carrying the directive, and
|
||||||
|
# the blank lines after it. Not "the first lines": real files
|
||||||
|
# put a heading above it.
|
||||||
|
set -l body (awk -v d="$directive" '
|
||||||
|
{ line[NR] = $0 }
|
||||||
|
END {
|
||||||
|
s = 0
|
||||||
|
for (i = 1; i <= NR; i++) {
|
||||||
|
if (line[i] !~ /^>/) continue
|
||||||
|
j = i; hit = 0
|
||||||
|
while (j <= NR && line[j] ~ /^>/) { if (index(line[j], d)) hit = 1; j++ }
|
||||||
|
if (hit) { s = i; e = j; break }
|
||||||
|
i = j
|
||||||
|
}
|
||||||
|
if (!s) { for (i = 1; i <= NR; i++) print line[i]; exit }
|
||||||
|
while (e <= NR && line[e] ~ /^[ \t]*$/) e++
|
||||||
|
for (i = 1; i < s; i++) print line[i]
|
||||||
|
for (i = e; i <= NR; i++) print line[i]
|
||||||
|
}' "$link")
|
||||||
|
if test $status -ne 0
|
||||||
|
echo "$c_err""Error: could not rewrite $rel$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
if set -q body[1]; and string match -qr -- '\S' $body
|
||||||
|
if test "$(printf '%s\n' $body | string collect)" != "$(string collect <"$link")"
|
||||||
|
if not printf '%s\n' $body >"$link"
|
||||||
|
echo "$c_err""Error: could not rewrite $rel$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
test $verbose -eq 1; and echo "$c_ok→ Removed the AGENTS/ directive from $rel$c_reset"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
rm -f "$link"
|
||||||
|
test $verbose -eq 1; and echo "$c_ok→ Removed $rel (only the directive, no user content)$c_reset"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for link in $drop
|
||||||
|
set -l rel (string replace -- "$root/" "" "$link")
|
||||||
|
if not rm -f "$link"
|
||||||
|
echo "$c_err""Error: could not remove link $rel$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
set changed 1
|
||||||
|
test $verbose -eq 1; and echo "$c_ok→ Removed link $rel$c_reset"
|
||||||
|
end
|
||||||
|
|
||||||
|
# ─────────────────────── Phase 5: remove AGENTS/ ─────────────────────
|
||||||
|
if test $has_agents -eq 1
|
||||||
|
if not rm -rf "$agents_dir"
|
||||||
|
echo "$c_err""Error: could not remove AGENTS/$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
set changed 1
|
||||||
|
test $verbose -eq 1; and echo "$c_ok→ Removed AGENTS/$c_reset"
|
||||||
|
end
|
||||||
|
# agents-init creates these; drop them only if nothing else lives there.
|
||||||
|
# Also after dropping links with no AGENTS/ left, which empties them too.
|
||||||
|
if test $has_agents -eq 1; or set -q drop[1]
|
||||||
|
for d in "$root/docs/superpowers" "$root/docs"
|
||||||
|
if test -d "$d"; and rmdir "$d" 2>/dev/null
|
||||||
|
test $verbose -eq 1; and echo "$c_ok→ Removed empty "(string replace -- "$root/" "" "$d")"/$c_reset"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ───────────────────────── Phase 6: .gitignore ───────────────────────
|
||||||
|
# Matched by pattern, not the exact strings _agents_init_ensure_gitignore
|
||||||
|
# writes, so blocks from older header variants go too. A header with no
|
||||||
|
# footer before the next header is left in place (and reported).
|
||||||
|
if test -f "$gitignore"; and grep -q 'Added by agents-init' "$gitignore"
|
||||||
|
# The unterminated-block warning is verbose-only, so a quiet or silent
|
||||||
|
# re-run stays silent. awk writes /dev/stderr itself: a fish-level
|
||||||
|
# 2>/dev/stderr reopens the path, which truncates a log file when
|
||||||
|
# stderr is one (`agents-cleanup &>log`).
|
||||||
|
set -l kept (awk -v warn=$verbose '
|
||||||
|
{ line[NR] = $0 }
|
||||||
|
END {
|
||||||
|
n = 0
|
||||||
|
for (i = 1; i <= NR; i++) {
|
||||||
|
if (line[i] ~ /^#.*Added by agents-init/) {
|
||||||
|
for (j = i + 1; j <= NR; j++) {
|
||||||
|
if (line[j] ~ /Added by agents-init/) { j = NR + 1; break }
|
||||||
|
if (line[j] ~ /^#[ \t]*(─)+[ \t]*$/) break
|
||||||
|
}
|
||||||
|
if (j <= NR) {
|
||||||
|
if (n > 0 && out[n] ~ /^[ \t]*$/) n--
|
||||||
|
i = j
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (warn) print "agents-cleanup: unterminated agents-init block in .gitignore left in place" > "/dev/stderr"
|
||||||
|
}
|
||||||
|
out[++n] = line[i]
|
||||||
|
}
|
||||||
|
for (i = 1; i <= n; i++) print out[i]
|
||||||
|
}' "$gitignore")
|
||||||
|
if test $status -ne 0
|
||||||
|
echo "$c_err""Error: could not rewrite .gitignore$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
# Identical output means only unterminated blocks were found: nothing
|
||||||
|
# to write, nothing to report, and a re-run stays silent.
|
||||||
|
if test "$(printf '%s\n' $kept | string collect)" != "$(string collect <"$gitignore")"
|
||||||
|
if set -q kept[1]; and string match -qr -- '\S' $kept
|
||||||
|
if not printf '%s\n' $kept >"$gitignore"
|
||||||
|
echo "$c_err""Error: could not rewrite .gitignore$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
else if test $in_git -eq 1; and git -C "$root" ls-files --error-unmatch -- .gitignore >/dev/null 2>&1
|
||||||
|
# Tracked: someone committed it, so it is not agents-init's to
|
||||||
|
# delete. Leave it empty; the change shows in git status.
|
||||||
|
if not true >"$gitignore"
|
||||||
|
echo "$c_err""Error: could not rewrite .gitignore$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Untracked and nothing but agents-init's blocks: agents-init
|
||||||
|
# created it.
|
||||||
|
rm -f "$gitignore"
|
||||||
|
end
|
||||||
|
set changed 1
|
||||||
|
test $verbose -eq 1; and echo "$c_ok→ Removed agents-init blocks from .gitignore$c_reset"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ────────────────────────────── Summary ───────────────────────────────
|
||||||
|
if test $changed -eq 0
|
||||||
|
test $verbose -eq 1; and echo "$c_dim→ Nothing to clean up; agents-init is already disabled here$c_reset"
|
||||||
|
else if test $quiet -eq 1
|
||||||
|
if test -n "$archived"
|
||||||
|
echo "$c_ok→ Cleaned up AGENTS scaffolding (history: $archived; restore with: git clone $archived AGENTS)$c_reset"
|
||||||
|
else
|
||||||
|
echo "$c_ok→ Cleaned up AGENTS scaffolding$c_reset"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return 0
|
||||||
|
end
|
||||||
+48
-20
@@ -5,14 +5,14 @@
|
|||||||
# 12-ai-and-developer-tools
|
# 12-ai-and-developer-tools
|
||||||
#
|
#
|
||||||
# DEPENDENCIES
|
# DEPENDENCIES
|
||||||
# _agents_init_sync_instructions, _agents_repo_install_tools, _agents_repo_sync, _agents_init_ensure_gitignore
|
# _agents_init_find, _agents_init_sync_instructions, _agents_repo_install_tools, _agents_repo_sync, _agents_init_ensure_gitignore
|
||||||
#
|
#
|
||||||
# CLASSIFICATION
|
# CLASSIFICATION
|
||||||
# self-limiting(rm,mkdir,grep), bypasses-shadow(mv), manual-section(16-agent-tooling)
|
# self-limiting(rm,mkdir,grep), bypasses-shadow(mv), manual-section(16-agent-tooling)
|
||||||
#
|
#
|
||||||
# SYNOPSIS
|
# SYNOPSIS
|
||||||
# agents-init [-a | --agents] [-p | --plugins] [-v | --verbose]
|
# agents-init [-a | --agents] [-p | --plugins] [-e | --enable]
|
||||||
# [-q | --quiet] [-s | --silent] [-h | --help]
|
# [-v | --verbose] [-q | --quiet] [-s | --silent] [-h | --help]
|
||||||
#
|
#
|
||||||
# DESCRIPTION
|
# DESCRIPTION
|
||||||
# Scaffolds an AGENTS/ sub-repository inside a project directory. Creates
|
# Scaffolds an AGENTS/ sub-repository inside a project directory. Creates
|
||||||
@@ -35,6 +35,13 @@
|
|||||||
# no-op, so running an agent CLI in an arbitrary directory does not
|
# no-op, so running an agent CLI in an arbitrary directory does not
|
||||||
# create a repository there.
|
# create a repository there.
|
||||||
#
|
#
|
||||||
|
# A project marked disabled is skipped entirely. agents-cleanup sets the
|
||||||
|
# per-clone git config key agents-init.disabled; a .agents-disabled file
|
||||||
|
# in the project root, which a team may commit, has the same effect.
|
||||||
|
# Either one turns every wrapper launch into a silent no-op there.
|
||||||
|
# --enable clears the git key and scaffolds; the file has to be deleted
|
||||||
|
# by hand, because it is a decision shared with every clone.
|
||||||
|
#
|
||||||
# File layout after setup:
|
# File layout after setup:
|
||||||
# AGENTS/AGENTS.md canonical root agent spec (real file)
|
# AGENTS/AGENTS.md canonical root agent spec (real file)
|
||||||
# AGENTS/<subdir>/AGENTS.md canonical spec for any subdir with its own
|
# AGENTS/<subdir>/AGENTS.md canonical spec for any subdir with its own
|
||||||
@@ -95,6 +102,8 @@
|
|||||||
# -a, --agents Set up AGENTS/ repo + AGENTS.md symlinks (root and every
|
# -a, --agents Set up AGENTS/ repo + AGENTS.md symlinks (root and every
|
||||||
# discovered subdirectory) only
|
# discovered subdirectory) only
|
||||||
# -p, --plugins Set up AGENTS/ repo + plans/specs/devlogs dirs + docs/ symlinks only
|
# -p, --plugins Set up AGENTS/ repo + plans/specs/devlogs dirs + docs/ symlinks only
|
||||||
|
# -e, --enable Clear the git key agents-cleanup set, then scaffold as
|
||||||
|
# normal (refused while .agents-disabled exists)
|
||||||
# -v, --verbose Print all per-step output (default)
|
# -v, --verbose Print all per-step output (default)
|
||||||
# -q, --quiet Print one summary line only if changes were made
|
# -q, --quiet Print one summary line only if changes were made
|
||||||
# -s, --silent Suppress all output; errors only (standard UNIX convention)
|
# -s, --silent Suppress all output; errors only (standard UNIX convention)
|
||||||
@@ -103,7 +112,8 @@
|
|||||||
# EXIT STATUS
|
# EXIT STATUS
|
||||||
# 0 Setup completed successfully
|
# 0 Setup completed successfully
|
||||||
# 1 Fatal error (git init failed, move failed, the AGENTS/ commit was
|
# 1 Fatal error (git init failed, move failed, the AGENTS/ commit was
|
||||||
# rejected, or an unresolved rebase blocked it)
|
# rejected, or an unresolved rebase blocked it), or --enable refused
|
||||||
|
# because .agents-disabled exists
|
||||||
#
|
#
|
||||||
# EXAMPLE
|
# EXAMPLE
|
||||||
# agents-init
|
# agents-init
|
||||||
@@ -122,11 +132,11 @@
|
|||||||
function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec files and plugin dirs'
|
function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec files and plugin dirs'
|
||||||
__fish_palette
|
__fish_palette
|
||||||
|
|
||||||
argparse h/help a/agents p/plugins v/verbose q/quiet s/silent -- $argv
|
argparse h/help a/agents p/plugins e/enable v/verbose q/quiet s/silent -- $argv
|
||||||
or return 1
|
or return 1
|
||||||
|
|
||||||
if set -q _flag_help
|
if set -q _flag_help
|
||||||
echo "$c_head""Usage:$c_reset $c_cmd""agents-init$c_reset $c_flag""[-a] [-p] [-v] [-q] [-s] [-h | --help]$c_reset"
|
echo "$c_head""Usage:$c_reset $c_cmd""agents-init$c_reset $c_flag""[-a] [-p] [-e] [-v] [-q] [-s] [-h | --help]$c_reset"
|
||||||
echo
|
echo
|
||||||
echo " Scaffold an AGENTS/ sub-repository for tracking agent specifications."
|
echo " Scaffold an AGENTS/ sub-repository for tracking agent specifications."
|
||||||
echo
|
echo
|
||||||
@@ -134,6 +144,7 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
|
|||||||
echo " $c_flag-h$c_reset, $c_flag--help$c_reset Show this help message"
|
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 symlinks only"
|
echo " $c_flag-a$c_reset, $c_flag--agents$c_reset Set up AGENTS.md 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-p$c_reset, $c_flag--plugins$c_reset Set up plans/specs/devlogs dirs and docs/ symlinks only"
|
||||||
|
echo " $c_flag-e$c_reset, $c_flag--enable$c_reset Re-enable a project agents-cleanup disabled"
|
||||||
echo " $c_flag-v$c_reset, $c_flag--verbose$c_reset Print all per-step output (default)"
|
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-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"
|
echo " $c_flag-s$c_reset, $c_flag--silent$c_reset Suppress all output; only errors are printed"
|
||||||
@@ -179,6 +190,34 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# ─────────────────────────── Opt-out marker ───────────────────────────
|
||||||
|
# agents-cleanup marks a project it has undone so launches stop
|
||||||
|
# re-scaffolding it: a per-clone git config key, or a .agents-disabled
|
||||||
|
# file a team may commit. The file is a shared decision, so --enable
|
||||||
|
# refuses rather than silently overriding it.
|
||||||
|
set -l marker_file "$root/.agents-disabled"
|
||||||
|
set -l key_set 0
|
||||||
|
if test $in_git -eq 1
|
||||||
|
set -l key (git -C "$root" config --type=bool --get agents-init.disabled 2>/dev/null)
|
||||||
|
test "$key" = true; and set key_set 1
|
||||||
|
end
|
||||||
|
if set -q _flag_enable
|
||||||
|
if test -e "$marker_file"
|
||||||
|
echo "$c_err""Error: .agents-disabled disables agents-init for every clone; delete it (and commit the deletion) to re-enable$c_reset" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
if test $in_git -eq 1; and git -C "$root" config --local --get agents-init.disabled >/dev/null 2>&1
|
||||||
|
git -C "$root" config --local --unset agents-init.disabled
|
||||||
|
test $verbose -eq 1; and echo "$c_ok→ Re-enabled agents-init (unset git config agents-init.disabled)$c_reset"
|
||||||
|
end
|
||||||
|
else if test -e "$marker_file"
|
||||||
|
test $verbose -eq 1; and echo "$c_dim→ agents-init is disabled here by .agents-disabled; delete that file to re-enable$c_reset"
|
||||||
|
return 0
|
||||||
|
else if test $key_set -eq 1
|
||||||
|
test $verbose -eq 1; and echo "$c_dim→ agents-init is disabled here (git config agents-init.disabled); run agents-init --enable to re-enable$c_reset"
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
set -l agents_dir "$root/AGENTS"
|
set -l agents_dir "$root/AGENTS"
|
||||||
set -l plugins_dir "$agents_dir/plugins"
|
set -l plugins_dir "$agents_dir/plugins"
|
||||||
|
|
||||||
@@ -238,22 +277,11 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
|
|||||||
#
|
#
|
||||||
# Discovery stays inside this project: a non-git root (a lone
|
# Discovery stays inside this project: a non-git root (a lone
|
||||||
# agent file in, say, ~) syncs only itself -- walking it would
|
# agent file in, say, ~) syncs only itself -- walking it would
|
||||||
# reach into every unrelated tree below. In a git root, pruned:
|
# reach into every unrelated tree below. In a git root the walk
|
||||||
# any AGENTS/ (a mirror, never a source), dot-directories (.git,
|
# uses the shared prune set; see _agents_init_find.
|
||||||
# .claude, .github: tool state, not scoped project dirs),
|
|
||||||
# node_modules, generated-output directories (build, dist, out,
|
|
||||||
# target: an instruction file there is a build artifact, never a
|
|
||||||
# source -- pruned outright, before tracked-file protection would
|
|
||||||
# even be consulted), and nested repos/submodules/worktrees (their
|
|
||||||
# own .git marks another project). -mindepth 1 keeps the root
|
|
||||||
# itself, which has a .git, from pruning the whole walk.
|
|
||||||
set -l found
|
set -l found
|
||||||
if test $in_git -eq 1
|
if test $in_git -eq 1
|
||||||
set found (find "$root" -mindepth 1 \
|
set found (_agents_init_find "$root" \( -name AGENTS.md -o -name CLAUDE.md \) -print)
|
||||||
-type d \( -name '.*' -o -name AGENTS -o -name node_modules \
|
|
||||||
-o -name build -o -name dist -o -name out -o -name target \
|
|
||||||
-o -exec test -e '{}/.git' \; \) -prune -o \
|
|
||||||
\( -name AGENTS.md -o -name CLAUDE.md \) -print)
|
|
||||||
end
|
end
|
||||||
set -l rels "."
|
set -l rels "."
|
||||||
for f in $found
|
for f in $found
|
||||||
|
|||||||
@@ -0,0 +1,499 @@
|
|||||||
|
#!/usr/bin/env fish
|
||||||
|
# Copyright (C) 2026 Rootiest
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# Hermetic tests for agents-cleanup: every case builds its own throwaway
|
||||||
|
# git repo under mktemp, scaffolds it with the real agents-init, and runs
|
||||||
|
# agents-cleanup against it. XDG_STATE_HOME is a fresh temp dir per case,
|
||||||
|
# so bundles never land in the real state dir. Nothing touches this
|
||||||
|
# checkout.
|
||||||
|
#
|
||||||
|
# Runs isolated (no `# MODE:` marker, which means isolated).
|
||||||
|
#
|
||||||
|
# Usage: fish tests/test-agents-cleanup.fish
|
||||||
|
|
||||||
|
source (realpath (dirname (status filename)))/lib.fish
|
||||||
|
set -p fish_function_path $repo_root/functions
|
||||||
|
|
||||||
|
set -gx GIT_AUTHOR_NAME t
|
||||||
|
set -gx GIT_AUTHOR_EMAIL t@t
|
||||||
|
set -gx GIT_COMMITTER_NAME t
|
||||||
|
set -gx GIT_COMMITTER_EMAIL t@t
|
||||||
|
# No user or system config: a developer's global hooks, signing or
|
||||||
|
# init.defaultBranch must not reach these repos.
|
||||||
|
set -gx GIT_CONFIG_GLOBAL /dev/null
|
||||||
|
set -gx GIT_CONFIG_NOSYSTEM 1
|
||||||
|
set -gx GIT_CONFIG_COUNT 2
|
||||||
|
set -gx GIT_CONFIG_KEY_0 commit.gpgsign
|
||||||
|
set -gx GIT_CONFIG_VALUE_0 false
|
||||||
|
set -gx GIT_CONFIG_KEY_1 init.defaultBranch
|
||||||
|
set -gx GIT_CONFIG_VALUE_1 main
|
||||||
|
|
||||||
|
set -g TMPDIRS
|
||||||
|
|
||||||
|
function new_repo
|
||||||
|
set -l d (mktemp -d)
|
||||||
|
set -ga TMPDIRS $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
|
||||||
|
git -C $d config core.hooksPath /dev/null
|
||||||
|
printf '%s\n' $d
|
||||||
|
end
|
||||||
|
|
||||||
|
function cleanup
|
||||||
|
for d in $TMPDIRS
|
||||||
|
test -n "$d"; and rm -rf $d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# A fresh bundle destination per case, so bundle counts never leak.
|
||||||
|
function fresh_state
|
||||||
|
set -gx XDG_STATE_HOME (mktemp -d)
|
||||||
|
set -ga TMPDIRS $XDG_STATE_HOME
|
||||||
|
end
|
||||||
|
|
||||||
|
# A project with a user .gitignore line, a root and a scoped AGENTS.md,
|
||||||
|
# and a real docs/plans -- scaffolded by the real agents-init, so it
|
||||||
|
# carries every link shape: root and subdir AGENTS.md, docs/plans (the
|
||||||
|
# original location) and docs/superpowers/plans (its duplicate), and
|
||||||
|
# docs/superpowers/specs (a .gitkeep-only target).
|
||||||
|
function scaffolded_repo
|
||||||
|
set -l d (new_repo)
|
||||||
|
echo keep-me >$d/.gitignore
|
||||||
|
echo user-root >$d/AGENTS.md
|
||||||
|
mkdir -p $d/functions $d/docs/plans
|
||||||
|
echo user-scoped >$d/functions/AGENTS.md
|
||||||
|
echo a-plan >$d/docs/plans/p.md
|
||||||
|
pushd $d >/dev/null
|
||||||
|
agents-init --silent 2>/dev/null
|
||||||
|
popd >/dev/null
|
||||||
|
printf '%s\n' $d
|
||||||
|
end
|
||||||
|
|
||||||
|
# Content hash of a tree, .git directories excluded: proves a refusal or
|
||||||
|
# a dry run changed nothing on disk.
|
||||||
|
function tree_hash --argument-names d
|
||||||
|
tar --sort=name --exclude=.git -C $d -cf - . | sha256sum
|
||||||
|
end
|
||||||
|
|
||||||
|
section "agents-cleanup: outside git"
|
||||||
|
fresh_state
|
||||||
|
set -l n1 (mktemp -d)
|
||||||
|
set -a TMPDIRS $n1
|
||||||
|
pushd $n1 >/dev/null
|
||||||
|
set -l nrc1 (set -lx GIT_CEILING_DIRECTORIES (path dirname $n1); agents-cleanup --silent 2>/dev/null; echo $status)
|
||||||
|
set -l nfile1 (test -e $n1/.agents-disabled; and echo true; or echo false)
|
||||||
|
set -l nrc2 (set -lx GIT_CEILING_DIRECTORIES (path dirname $n1); agents-cleanup --marker-file --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "non-git without --marker-file: exits 1" 1 "$nrc1"
|
||||||
|
check "non-git without --marker-file: writes nothing" false "$nfile1"
|
||||||
|
check "non-git with --marker-file: exits 0" 0 "$nrc2"
|
||||||
|
check "non-git with --marker-file: .agents-disabled written" true (test -f $n1/.agents-disabled; and echo true; or echo false)
|
||||||
|
|
||||||
|
section "agents-cleanup: never-scaffolded project"
|
||||||
|
fresh_state
|
||||||
|
set -l p1 (new_repo)
|
||||||
|
pushd $p1 >/dev/null
|
||||||
|
set -l prc (agents-cleanup --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "no AGENTS/: exits 0" 0 "$prc"
|
||||||
|
check "no AGENTS/: git key set" true (git -C $p1 config --type=bool --get agents-init.disabled)
|
||||||
|
check "no AGENTS/: nothing created" "" (command ls -A $p1 | string match -v .git)
|
||||||
|
|
||||||
|
section "agents-cleanup: unlinked files refuse"
|
||||||
|
fresh_state
|
||||||
|
set -l x1 (scaffolded_repo)
|
||||||
|
echo notes >$x1/AGENTS/notes.md
|
||||||
|
set -l xbefore (tree_hash $x1)
|
||||||
|
pushd $x1 >/dev/null
|
||||||
|
set -l xerr (agents-cleanup 2>&1 >/dev/null)
|
||||||
|
set -l xrc $status
|
||||||
|
popd >/dev/null
|
||||||
|
check "extras: exits 1" 1 "$xrc"
|
||||||
|
check "extras: tree unchanged" "$xbefore" (tree_hash $x1)
|
||||||
|
check "extras: git key not set" 1 (git -C $x1 config --get agents-init.disabled >/dev/null; echo $status)
|
||||||
|
check "extras: names the file" true (string match -q -- '*AGENTS/notes.md*' "$xerr"; and echo true; or echo false)
|
||||||
|
|
||||||
|
section "agents-cleanup: --dry-run"
|
||||||
|
fresh_state
|
||||||
|
set -l d1 (scaffolded_repo)
|
||||||
|
set -l dbefore (tree_hash $d1)
|
||||||
|
pushd $d1 >/dev/null
|
||||||
|
set -l dout (agents-cleanup --dry-run 2>/dev/null)
|
||||||
|
set -l drc $status
|
||||||
|
popd >/dev/null
|
||||||
|
check "dry-run: exits 0" 0 "$drc"
|
||||||
|
check "dry-run: tree unchanged" "$dbefore" (tree_hash $d1)
|
||||||
|
check "dry-run: git key not set" 1 (git -C $d1 config --get agents-init.disabled >/dev/null; echo $status)
|
||||||
|
check "dry-run: plans restoring docs/plans" true (string match -q -- '*restore docs/plans from AGENTS/plans*' "$dout"; and echo true; or echo false)
|
||||||
|
check "dry-run: plans dropping the duplicate link" true (string match -q -- '*remove link docs/superpowers/plans*' "$dout"; and echo true; or echo false)
|
||||||
|
check "dry-run: plans dropping the .gitkeep-only link" true (string match -q -- '*remove link docs/superpowers/specs*' "$dout"; and echo true; or echo false)
|
||||||
|
check "dry-run: plans removing AGENTS/" true (string match -q -- '*remove AGENTS/*' "$dout"; and echo true; or echo false)
|
||||||
|
|
||||||
|
section "agents-cleanup: unresolved rebase refuses"
|
||||||
|
fresh_state
|
||||||
|
set -l r1 (scaffolded_repo)
|
||||||
|
mkdir $r1/AGENTS/.git/rebase-merge
|
||||||
|
set -l rbefore (tree_hash $r1)
|
||||||
|
pushd $r1 >/dev/null
|
||||||
|
agents-cleanup >/dev/null 2>&1
|
||||||
|
set -l rrc $status
|
||||||
|
popd >/dev/null
|
||||||
|
check "rebase: exits 1" 1 "$rrc"
|
||||||
|
check "rebase: tree unchanged" "$rbefore" (tree_hash $r1)
|
||||||
|
check "rebase: git key not set" 1 (git -C $r1 config --get agents-init.disabled >/dev/null; echo $status)
|
||||||
|
|
||||||
|
section "agents-cleanup: --marker-file inside git"
|
||||||
|
fresh_state
|
||||||
|
set -l m1 (scaffolded_repo)
|
||||||
|
pushd $m1 >/dev/null
|
||||||
|
set -l mout (agents-cleanup --dry-run --marker-file 2>/dev/null)
|
||||||
|
popd >/dev/null
|
||||||
|
check "marker dry-run: plans writing the file" true (string match -q -- '*write .agents-disabled*' "$mout"; and echo true; or echo false)
|
||||||
|
set -l m2 (new_repo)
|
||||||
|
pushd $m2 >/dev/null
|
||||||
|
set -l mrc (agents-cleanup --marker-file --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "marker run: exits 0" 0 "$mrc"
|
||||||
|
check "marker run: .agents-disabled written" true (test -f $m2/.agents-disabled; and echo true; or echo false)
|
||||||
|
check "marker run: git key set" true (git -C $m2 config --type=bool --get agents-init.disabled)
|
||||||
|
|
||||||
|
section "agents-cleanup: --dry-run --drop-extras"
|
||||||
|
fresh_state
|
||||||
|
set -l e1 (scaffolded_repo)
|
||||||
|
echo notes >$e1/AGENTS/notes.md
|
||||||
|
set -l ebefore (tree_hash $e1)
|
||||||
|
pushd $e1 >/dev/null
|
||||||
|
set -l eout (agents-cleanup --dry-run --drop-extras 2>/dev/null)
|
||||||
|
set -l erc $status
|
||||||
|
popd >/dev/null
|
||||||
|
check "drop-extras dry-run: exits 0" 0 "$erc"
|
||||||
|
check "drop-extras dry-run: plans discarding the file" true (string match -q -- '*discard AGENTS/notes.md*' "$eout"; and echo true; or echo false)
|
||||||
|
check "drop-extras dry-run: tree unchanged" "$ebefore" (tree_hash $e1)
|
||||||
|
|
||||||
|
section "agents-cleanup: glob characters in the project path"
|
||||||
|
fresh_state
|
||||||
|
set -l g0 (mktemp -d)
|
||||||
|
set -a TMPDIRS $g0
|
||||||
|
set -l g1 "$g0/p[x]"
|
||||||
|
mkdir $g1
|
||||||
|
git -C $g1 init -q
|
||||||
|
git -C $g1 config user.email t@t
|
||||||
|
git -C $g1 config user.name t
|
||||||
|
git -C $g1 config commit.gpgsign false
|
||||||
|
git -C $g1 config core.hooksPath /dev/null
|
||||||
|
echo a-plan >$g1/README.md
|
||||||
|
pushd $g1 >/dev/null
|
||||||
|
agents-init --silent 2>/dev/null
|
||||||
|
set -l gout (agents-cleanup --dry-run 2>/dev/null)
|
||||||
|
set -l grc $status
|
||||||
|
popd >/dev/null
|
||||||
|
check "glob path: AGENTS/ scaffolded" true (test -d $g1/AGENTS/.git; and echo true; or echo false)
|
||||||
|
check "glob path: dry-run exits 0" 0 "$grc"
|
||||||
|
check "glob path: plan removes AGENTS/" true (string match -q -- '*remove AGENTS/*' "$gout"; and echo true; or echo false)
|
||||||
|
check "glob path: plan never lists .git/" false (string match -q -- '*.git/*' "$gout"; and echo true; or echo false)
|
||||||
|
|
||||||
|
section "agents-cleanup: full cleanup"
|
||||||
|
fresh_state
|
||||||
|
set -l c1 (scaffolded_repo)
|
||||||
|
pushd $c1 >/dev/null
|
||||||
|
set -l crc (agents-cleanup --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "full: exits 0" 0 "$crc"
|
||||||
|
check "full: AGENTS/ removed" false (test -e $c1/AGENTS; and echo true; or echo false)
|
||||||
|
check "full: root AGENTS.md is a real file" false (test -L $c1/AGENTS.md; and echo true; or echo false)
|
||||||
|
check "full: root content kept" user-root (string collect <$c1/AGENTS.md)
|
||||||
|
check "full: subdir AGENTS.md real, content kept" user-scoped (test -L $c1/functions/AGENTS.md; or string collect <$c1/functions/AGENTS.md)
|
||||||
|
check "full: docs/plans restored as a real dir" a-plan (test -L $c1/docs/plans; or string collect <$c1/docs/plans/p.md)
|
||||||
|
check "full: no .gitkeep in docs/plans" false (test -e $c1/docs/plans/.gitkeep; and echo true; or echo false)
|
||||||
|
check "full: docs/superpowers removed" false (test -e $c1/docs/superpowers -o -L $c1/docs/superpowers; and echo true; or echo false)
|
||||||
|
check "full: git key set" true (git -C $c1 config --type=bool --get agents-init.disabled)
|
||||||
|
check "full: .gitignore keeps only user lines" keep-me (string collect <$c1/.gitignore)
|
||||||
|
check "full: nothing committed to the outer repo" "" (git -C $c1 rev-list --all 2>/dev/null)
|
||||||
|
set -l cb $XDG_STATE_HOME/agents-cleanup/*.bundle
|
||||||
|
check "full: one bundle written" 1 (count $cb)
|
||||||
|
set -l restored (mktemp -d)
|
||||||
|
set -a TMPDIRS $restored
|
||||||
|
git clone -q $cb[1] $restored/AGENTS 2>/dev/null
|
||||||
|
check "full: bundle restores the history" user-root (string collect <$restored/AGENTS/AGENTS.md)
|
||||||
|
|
||||||
|
pushd $c1 >/dev/null
|
||||||
|
agents-init --silent 2>/dev/null
|
||||||
|
popd >/dev/null
|
||||||
|
check "full: agents-init no longer scaffolds" false (test -e $c1/AGENTS; and echo true; or echo false)
|
||||||
|
|
||||||
|
pushd $c1 >/dev/null
|
||||||
|
set -l c2out (agents-cleanup --quiet 2>&1)
|
||||||
|
set -l c2rc $status
|
||||||
|
popd >/dev/null
|
||||||
|
check "rerun: exits 0" 0 "$c2rc"
|
||||||
|
check "rerun: quiet prints nothing" "" "$c2out"
|
||||||
|
|
||||||
|
section "agents-cleanup: stub AGENTS.md"
|
||||||
|
fresh_state
|
||||||
|
set -l s1 (new_repo)
|
||||||
|
pushd $s1 >/dev/null
|
||||||
|
agents-init --silent 2>/dev/null
|
||||||
|
agents-cleanup --silent 2>/dev/null
|
||||||
|
popd >/dev/null
|
||||||
|
check "stub: deleted" false (test -e $s1/AGENTS.md -o -L $s1/AGENTS.md; and echo true; or echo false)
|
||||||
|
check "stub: untracked blocks-only .gitignore deleted" false (test -e $s1/.gitignore; and echo true; or echo false)
|
||||||
|
|
||||||
|
section "agents-cleanup: tracked blocks-only .gitignore is emptied, not deleted"
|
||||||
|
fresh_state
|
||||||
|
set -l g1 (new_repo)
|
||||||
|
pushd $g1 >/dev/null
|
||||||
|
agents-init --silent 2>/dev/null
|
||||||
|
git add .gitignore
|
||||||
|
git commit -qm gi
|
||||||
|
agents-cleanup --silent 2>/dev/null
|
||||||
|
popd >/dev/null
|
||||||
|
check "tracked .gitignore: still exists" true (test -f $g1/.gitignore; and echo true; or echo false)
|
||||||
|
check "tracked .gitignore: emptied" "" (string collect <$g1/.gitignore)
|
||||||
|
check "tracked .gitignore: index untouched (unstaged change only)" " M .gitignore" (git -C $g1 status --porcelain -- .gitignore)
|
||||||
|
|
||||||
|
section "agents-cleanup: directive stripped from a user AGENTS.md"
|
||||||
|
fresh_state
|
||||||
|
set -l t1 (new_repo)
|
||||||
|
printf '%s\n' '# Title' '' '> ⚠️ **SYSTEM DIRECTIVE FOR AI AGENTS: FILE EDITING**' '> write to AGENTS/AGENTS.md' '' body >$t1/AGENTS.md
|
||||||
|
pushd $t1 >/dev/null
|
||||||
|
agents-init --silent 2>/dev/null
|
||||||
|
agents-cleanup --silent 2>/dev/null
|
||||||
|
popd >/dev/null
|
||||||
|
check "directive: blockquote and its trailing blank line removed" (printf '%s\n' '# Title' '' body | string collect) (string collect <$t1/AGENTS.md)
|
||||||
|
|
||||||
|
section "agents-cleanup: --drop-extras"
|
||||||
|
fresh_state
|
||||||
|
set -l x2 (scaffolded_repo)
|
||||||
|
echo notes >$x2/AGENTS/notes.md
|
||||||
|
pushd $x2 >/dev/null
|
||||||
|
set -l x2rc (agents-cleanup --drop-extras --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "drop-extras: exits 0" 0 "$x2rc"
|
||||||
|
check "drop-extras: AGENTS/ removed" false (test -e $x2/AGENTS; and echo true; or echo false)
|
||||||
|
set -l xb $XDG_STATE_HOME/agents-cleanup/*.bundle
|
||||||
|
set -l xr (mktemp -d)
|
||||||
|
set -a TMPDIRS $xr
|
||||||
|
git clone -q $xb[1] $xr/AGENTS 2>/dev/null
|
||||||
|
check "drop-extras: dropped file is in the bundle" notes (string collect <$xr/AGENTS/notes.md)
|
||||||
|
|
||||||
|
section "agents-cleanup: rejected final commit"
|
||||||
|
fresh_state
|
||||||
|
set -l f1 (scaffolded_repo)
|
||||||
|
set -l hooks (mktemp -d)
|
||||||
|
set -a TMPDIRS $hooks
|
||||||
|
printf '%s\n' '#!/bin/sh' 'exit 1' >$hooks/pre-commit
|
||||||
|
chmod +x $hooks/pre-commit
|
||||||
|
git -C $f1/AGENTS config core.hooksPath $hooks
|
||||||
|
echo change >>$f1/AGENTS/AGENTS.md
|
||||||
|
pushd $f1 >/dev/null
|
||||||
|
set -l frc (agents-cleanup --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "rejected commit: exits 1" 1 "$frc"
|
||||||
|
check "rejected commit: AGENTS/ kept" true (test -d $f1/AGENTS/.git; and echo true; or echo false)
|
||||||
|
check "rejected commit: root link untouched" AGENTS/AGENTS.md (readlink $f1/AGENTS.md)
|
||||||
|
set -l fb $XDG_STATE_HOME/agents-cleanup/*.bundle
|
||||||
|
check "rejected commit: no bundle" 0 (count $fb)
|
||||||
|
|
||||||
|
section "agents-cleanup: failed move keeps the link, re-run resumes"
|
||||||
|
fresh_state
|
||||||
|
if test (id -u) -eq 0
|
||||||
|
echo " SKIP failed move: chmod cannot block root"
|
||||||
|
else
|
||||||
|
set -l w1 (scaffolded_repo)
|
||||||
|
chmod a-w $w1/AGENTS/functions
|
||||||
|
pushd $w1 >/dev/null
|
||||||
|
set -l w1rc (agents-cleanup --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "failed mv: exits 1" 1 "$w1rc"
|
||||||
|
check "failed mv: link still a symlink" true (test -L $w1/functions/AGENTS.md; and echo true; or echo false)
|
||||||
|
chmod u+w $w1/AGENTS/functions
|
||||||
|
pushd $w1 >/dev/null
|
||||||
|
set -l w2rc (agents-cleanup --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "failed mv re-run: exits 0" 0 "$w2rc"
|
||||||
|
check "failed mv re-run: real file, content kept" user-scoped (test -L $w1/functions/AGENTS.md; or string collect <$w1/functions/AGENTS.md)
|
||||||
|
end
|
||||||
|
|
||||||
|
section "agents-cleanup: failed directive rewrite keeps AGENTS/"
|
||||||
|
fresh_state
|
||||||
|
if test (id -u) -eq 0
|
||||||
|
echo " SKIP failed rewrite: chmod cannot block root"
|
||||||
|
else
|
||||||
|
set -l k1 (new_repo)
|
||||||
|
printf '%s\n' '# Title' '' '> **SYSTEM DIRECTIVE FOR AI AGENTS: FILE EDITING**' '> x' '' body >$k1/AGENTS.md
|
||||||
|
pushd $k1 >/dev/null
|
||||||
|
agents-init --silent 2>/dev/null
|
||||||
|
popd >/dev/null
|
||||||
|
chmod a-w $k1/AGENTS/AGENTS.md
|
||||||
|
pushd $k1 >/dev/null
|
||||||
|
set -l k1rc (agents-cleanup --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
chmod u+w $k1/AGENTS/AGENTS.md $k1/AGENTS.md 2>/dev/null
|
||||||
|
check "failed rewrite: exits 1" 1 "$k1rc"
|
||||||
|
check "failed rewrite: AGENTS/ kept" true (test -d $k1/AGENTS; and echo true; or echo false)
|
||||||
|
end
|
||||||
|
|
||||||
|
section "agents-cleanup: unterminated .gitignore block is left alone, quietly"
|
||||||
|
fresh_state
|
||||||
|
set -l u1 (new_repo)
|
||||||
|
printf '%s\n' user-line '' '# ──── Added by agents-init ────' AGENTS/ >$u1/.gitignore
|
||||||
|
set -l ubefore (string collect <$u1/.gitignore)
|
||||||
|
pushd $u1 >/dev/null
|
||||||
|
set -l u1rc (agents-cleanup --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "unterminated: exits 0" 0 "$u1rc"
|
||||||
|
check "unterminated: file byte-identical" "$ubefore" (string collect <$u1/.gitignore)
|
||||||
|
pushd $u1 >/dev/null
|
||||||
|
set -l u2out (agents-cleanup --quiet 2>&1)
|
||||||
|
popd >/dev/null
|
||||||
|
check "unterminated: quiet re-run prints nothing" "" "$u2out"
|
||||||
|
|
||||||
|
section "agents-cleanup: symlinked AGENTS refuses"
|
||||||
|
fresh_state
|
||||||
|
set -l l1 (new_repo)
|
||||||
|
set -l ext (mktemp -d)
|
||||||
|
set -a TMPDIRS $ext
|
||||||
|
echo precious >$ext/file
|
||||||
|
ln -s $ext $l1/AGENTS
|
||||||
|
pushd $l1 >/dev/null
|
||||||
|
set -l l1rc (agents-cleanup --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "symlinked AGENTS: exits 1" 1 "$l1rc"
|
||||||
|
check "symlinked AGENTS: target file survives" precious (string collect <$ext/file)
|
||||||
|
|
||||||
|
section "agents-cleanup: extras without a bundle"
|
||||||
|
fresh_state
|
||||||
|
set -l n2 (new_repo)
|
||||||
|
mkdir $n2/AGENTS
|
||||||
|
echo stray >$n2/AGENTS/stray.md
|
||||||
|
pushd $n2 >/dev/null
|
||||||
|
set -l n2err (agents-cleanup 2>&1 >/dev/null)
|
||||||
|
popd >/dev/null
|
||||||
|
check "no-repo extras: labelled not in bundle" true (string match -q -- '*stray.md (not in bundle)*' "$n2err"; and echo true; or echo false)
|
||||||
|
check "no-repo extras: hint does not promise a bundle" false (string match -q -- '*bundle keeps*' "$n2err"; and echo true; or echo false)
|
||||||
|
|
||||||
|
section "agents-cleanup: user AGENTS.md variants"
|
||||||
|
fresh_state
|
||||||
|
set -l v1 (new_repo)
|
||||||
|
printf '%s\n' '> ⚠️ **SYSTEM DIRECTIVE FOR AI AGENTS: FILE EDITING**' '> x' >$v1/AGENTS.md
|
||||||
|
set -l v2 (new_repo)
|
||||||
|
printf '%s\n' '# Mine' '' 'no directive here' >$v2/AGENTS.md
|
||||||
|
set -l v2before (string collect <$v2/AGENTS.md)
|
||||||
|
for v in $v1 $v2
|
||||||
|
pushd $v >/dev/null
|
||||||
|
agents-init --silent 2>/dev/null
|
||||||
|
agents-cleanup --silent 2>/dev/null
|
||||||
|
popd >/dev/null
|
||||||
|
end
|
||||||
|
check "directive-only: deleted" false (test -e $v1/AGENTS.md -o -L $v1/AGENTS.md; and echo true; or echo false)
|
||||||
|
check "no directive: content identical" "$v2before" (string collect <$v2/AGENTS.md)
|
||||||
|
|
||||||
|
section "agents-cleanup: invalid AGENTS/.git never commits the outer repo"
|
||||||
|
fresh_state
|
||||||
|
set -l i1 (scaffolded_repo)
|
||||||
|
echo secret >$i1/untracked-secret.txt
|
||||||
|
rm -rf $i1/AGENTS/.git/HEAD $i1/AGENTS/.git/objects
|
||||||
|
set -l i1before (git -C $i1 rev-list --all | count)
|
||||||
|
pushd $i1 >/dev/null
|
||||||
|
set -l i1rc (agents-cleanup --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "invalid .git: exits 0" 0 "$i1rc"
|
||||||
|
check "invalid .git: outer repo untouched" "$i1before" (git -C $i1 rev-list --all | count)
|
||||||
|
check "invalid .git: no bundle" 0 (count $XDG_STATE_HOME/agents-cleanup/*.bundle 2>/dev/null)
|
||||||
|
check "invalid .git: AGENTS/ removed" false (test -e $i1/AGENTS; and echo true; or echo false)
|
||||||
|
|
||||||
|
section "agents-cleanup: verbose output survives a log-file stderr"
|
||||||
|
fresh_state
|
||||||
|
set -l o1 (scaffolded_repo)
|
||||||
|
set -l o1log (mktemp)
|
||||||
|
set -a TMPDIRS $o1log
|
||||||
|
# A subshell: only a shell whose own stderr is the file reproduces the
|
||||||
|
# truncation (an in-process redirect is emulated and never reopens it).
|
||||||
|
fish -c "set -p fish_function_path $repo_root/functions; cd $o1; agents-cleanup" >$o1log 2>&1
|
||||||
|
check "log file: archive line kept" true (string match -q -- '*Archived AGENTS/ history*' (string collect <$o1log); and echo true; or echo false)
|
||||||
|
check "log file: restore hint kept" true (string match -q -- '*restore with: git clone*' (string collect <$o1log); and echo true; or echo false)
|
||||||
|
|
||||||
|
section "agents-cleanup: nested repository under AGENTS/ is never dropped"
|
||||||
|
fresh_state
|
||||||
|
set -l q1 (scaffolded_repo)
|
||||||
|
mkdir -p $q1/AGENTS/scratch/tool
|
||||||
|
git -C $q1/AGENTS/scratch/tool init -q
|
||||||
|
echo precious >$q1/AGENTS/scratch/tool/file.txt
|
||||||
|
set -l q1before (tree_hash $q1)
|
||||||
|
pushd $q1 >/dev/null
|
||||||
|
set -l q1err (agents-cleanup --drop-extras 2>&1 >/dev/null)
|
||||||
|
set -l q1rc $status
|
||||||
|
popd >/dev/null
|
||||||
|
check "nested repo: exits 1 even with --drop-extras" 1 "$q1rc"
|
||||||
|
check "nested repo: tree unchanged" "$q1before" (tree_hash $q1)
|
||||||
|
check "nested repo: file survives" precious (string collect <$q1/AGENTS/scratch/tool/file.txt)
|
||||||
|
check "nested repo: message names it" true (string match -q -- '*AGENTS/scratch/tool/*nested repository*' "$q1err"; and echo true; or echo false)
|
||||||
|
check "nested repo: git key not set" 1 (git -C $q1 config --get agents-init.disabled >/dev/null; echo $status)
|
||||||
|
|
||||||
|
section "agents-cleanup: a link to AGENTS/ itself is dropped, not moved"
|
||||||
|
fresh_state
|
||||||
|
set -l z1 (scaffolded_repo)
|
||||||
|
ln -s ../AGENTS $z1/docs/agents
|
||||||
|
pushd $z1 >/dev/null
|
||||||
|
set -l z1rc (agents-cleanup --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "self link: exits 0" 0 "$z1rc"
|
||||||
|
check "self link: docs/agents gone" false (test -e $z1/docs/agents -o -L $z1/docs/agents; and echo true; or echo false)
|
||||||
|
check "self link: docs/plans restored real" a-plan (test -L $z1/docs/plans; or string collect <$z1/docs/plans/p.md)
|
||||||
|
|
||||||
|
section "agents-cleanup: AGENTS/ deleted by hand, dangling links still go"
|
||||||
|
fresh_state
|
||||||
|
set -l h1 (scaffolded_repo)
|
||||||
|
rm -rf $h1/AGENTS
|
||||||
|
pushd $h1 >/dev/null
|
||||||
|
set -l h1dry (agents-cleanup --dry-run 2>/dev/null)
|
||||||
|
set -l h1rc (agents-cleanup --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "no AGENTS/, dangling: dry-run plans removing links" true (string match -q -- '*remove link AGENTS.md*' "$h1dry"; and echo true; or echo false)
|
||||||
|
check "no AGENTS/, dangling: exits 0" 0 "$h1rc"
|
||||||
|
check "no AGENTS/, dangling: AGENTS.md link gone" false (test -e $h1/AGENTS.md -o -L $h1/AGENTS.md; and echo true; or echo false)
|
||||||
|
check "no AGENTS/, dangling: docs/superpowers gone" false (test -e $h1/docs/superpowers -o -L $h1/docs/superpowers; and echo true; or echo false)
|
||||||
|
check "no AGENTS/, dangling: git key set" true (git -C $h1 config --type=bool --get agents-init.disabled)
|
||||||
|
|
||||||
|
section "agents-cleanup: outside git, no AGENTS/ here: links are not inventoried"
|
||||||
|
fresh_state
|
||||||
|
set -l b1 (mktemp -d)
|
||||||
|
set -a TMPDIRS $b1
|
||||||
|
mkdir -p $b1/sub
|
||||||
|
ln -s ../AGENTS/x $b1/sub/dangling
|
||||||
|
pushd $b1 >/dev/null
|
||||||
|
set -l b1rc (set -lx GIT_CEILING_DIRECTORIES (path dirname $b1); agents-cleanup --marker-file --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "non-git no AGENTS/: exits 0" 0 "$b1rc"
|
||||||
|
check "non-git no AGENTS/: stray link untouched" true (test -L $b1/sub/dangling; and echo true; or echo false)
|
||||||
|
|
||||||
|
section "agents-cleanup: quiet summary carries the restore command"
|
||||||
|
fresh_state
|
||||||
|
set -l y1 (scaffolded_repo)
|
||||||
|
pushd $y1 >/dev/null
|
||||||
|
set -l y1out (agents-cleanup --quiet 2>&1)
|
||||||
|
popd >/dev/null
|
||||||
|
check "quiet: names the restore command" true (string match -q -- '*git clone *.bundle AGENTS*' "$y1out"; and echo true; or echo false)
|
||||||
|
|
||||||
|
section "agents-cleanup: user .gitignore lines around two agents-init blocks"
|
||||||
|
fresh_state
|
||||||
|
set -l w9 (new_repo)
|
||||||
|
pushd $w9 >/dev/null
|
||||||
|
agents-init --silent 2>/dev/null
|
||||||
|
popd >/dev/null
|
||||||
|
set -l blk (string collect <$w9/.gitignore)
|
||||||
|
printf '%s\n' before1 before2 >$w9/.gitignore
|
||||||
|
printf '%s\n' "$blk" >>$w9/.gitignore
|
||||||
|
printf '%s\n' between >>$w9/.gitignore
|
||||||
|
printf '%s\n' "$blk" >>$w9/.gitignore
|
||||||
|
printf '%s\n' after1 after2 >>$w9/.gitignore
|
||||||
|
pushd $w9 >/dev/null
|
||||||
|
agents-cleanup --silent 2>/dev/null
|
||||||
|
popd >/dev/null
|
||||||
|
check "gitignore: exactly the user lines remain, in order" (printf '%s\n' before1 before2 between after1 after2 | string collect) (string collect <$w9/.gitignore)
|
||||||
|
|
||||||
|
cleanup
|
||||||
|
report
|
||||||
@@ -55,6 +55,7 @@ check "fresh root: mirror AGENTS.md created" true (test -f $r1/AGENTS/AGENTS.md;
|
|||||||
check "fresh root: no mirror CLAUDE.md" false (test -e $r1/AGENTS/CLAUDE.md; and echo true; or echo false)
|
check "fresh root: no mirror CLAUDE.md" false (test -e $r1/AGENTS/CLAUDE.md; and echo true; or echo false)
|
||||||
check "fresh root: project AGENTS.md links to mirror" AGENTS/AGENTS.md (readlink $r1/AGENTS.md)
|
check "fresh root: project AGENTS.md links to mirror" AGENTS/AGENTS.md (readlink $r1/AGENTS.md)
|
||||||
check "fresh root: no project CLAUDE.md" false (test -e $r1/CLAUDE.md; and echo true; or echo false)
|
check "fresh root: no project CLAUDE.md" false (test -e $r1/CLAUDE.md; and echo true; or echo false)
|
||||||
|
check "fresh root: mirror AGENTS.md is exactly the shared stub" (_agents_init_stub | string collect) (string collect <$r1/AGENTS/AGENTS.md)
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "== _agents_init_sync_instructions: idempotent second run =="
|
echo "== _agents_init_sync_instructions: idempotent second run =="
|
||||||
@@ -461,5 +462,64 @@ _agents_init_path_is_protected $g1 "$g1/a[1]/AGENTS.md"
|
|||||||
set -l protected $status
|
set -l protected $status
|
||||||
check "glob false-match: untracked a[1]/AGENTS.md is not protected" 1 "$protected"
|
check "glob false-match: untracked a[1]/AGENTS.md is not protected" 1 "$protected"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "== _agents_init_find: shared prune set =="
|
||||||
|
|
||||||
|
set -l pf (new_repo)
|
||||||
|
mkdir -p $pf/src $pf/node_modules/pkg $pf/.claude $pf/AGENTS $pf/build $pf/nested
|
||||||
|
git -C $pf/nested init -q
|
||||||
|
ln -s ../x $pf/src/link
|
||||||
|
ln -s x $pf/node_modules/pkg/link
|
||||||
|
ln -s x $pf/.claude/link
|
||||||
|
ln -s x $pf/AGENTS/link
|
||||||
|
ln -s x $pf/build/link
|
||||||
|
ln -s x $pf/nested/link
|
||||||
|
check "find: only the unpruned symlink" $pf/src/link (_agents_init_find $pf -type l -print | string join ,)
|
||||||
|
check "find: no root -> status 1" 1 (_agents_init_find ""; echo $status)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "== agents-init: opt-out marker =="
|
||||||
|
|
||||||
|
set -l m1 (new_repo)
|
||||||
|
git -C $m1 config agents-init.disabled true
|
||||||
|
pushd $m1 >/dev/null
|
||||||
|
set -l mrc1 (agents-init --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "git key: exits 0" 0 "$mrc1"
|
||||||
|
check "git key: nothing scaffolded" false (test -e $m1/AGENTS -o -L $m1/AGENTS.md; and echo true; or echo false)
|
||||||
|
|
||||||
|
set -l m2 (new_repo)
|
||||||
|
touch $m2/.agents-disabled
|
||||||
|
pushd $m2 >/dev/null
|
||||||
|
set -l mrc2 (agents-init --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "marker file: exits 0" 0 "$mrc2"
|
||||||
|
check "marker file: nothing scaffolded" false (test -e $m2/AGENTS -o -L $m2/AGENTS.md; and echo true; or echo false)
|
||||||
|
|
||||||
|
set -l m3 (new_repo)
|
||||||
|
git -C $m3 config agents-init.disabled true
|
||||||
|
pushd $m3 >/dev/null
|
||||||
|
set -l mrc3 (agents-init --enable --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "--enable: exits 0" 0 "$mrc3"
|
||||||
|
check "--enable: git key unset" 1 (git -C $m3 config --get agents-init.disabled >/dev/null; echo $status)
|
||||||
|
check "--enable: scaffold created" AGENTS/AGENTS.md (readlink $m3/AGENTS.md)
|
||||||
|
|
||||||
|
set -l m4 (new_repo)
|
||||||
|
touch $m4/.agents-disabled
|
||||||
|
pushd $m4 >/dev/null
|
||||||
|
set -l mrc4 (agents-init --enable --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "--enable with marker file: exits 1" 1 "$mrc4"
|
||||||
|
check "--enable with marker file: nothing scaffolded" false (test -e $m4/AGENTS -o -L $m4/AGENTS.md; and echo true; or echo false)
|
||||||
|
check "--enable with marker file: file kept" true (test -e $m4/.agents-disabled; and echo true; or echo false)
|
||||||
|
|
||||||
|
set -l m5 (new_repo)
|
||||||
|
git -C $m5 config agents-init.disabled true
|
||||||
|
pushd $m5 >/dev/null
|
||||||
|
set -l mout5 (agents-init 2>/dev/null)
|
||||||
|
popd >/dev/null
|
||||||
|
check "git key, verbose: note names the marker" true (string match -q -- '*agents-init.disabled*' "$mout5"; and echo true; or echo false)
|
||||||
|
|
||||||
cleanup
|
cleanup
|
||||||
report
|
report
|
||||||
|
|||||||
Reference in New Issue
Block a user