@ahood/cli 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -189,9 +189,9 @@ Shared alice/pdf-tools with design-team.
189
189
  Sharing is additive -- it doesn't change `alice/pdf-tools`'s own public/private visibility, it just makes it visible to everyone in `design-team` too. On the other side, whoever received the link runs `ahood group join <the link>` to join.
190
190
  </details>
191
191
 
192
- ## Kinds: skills, agents, MCP servers, docs, and project guides
192
+ ## Kinds: skills, agents, MCP servers, docs, project guides, and loops
193
193
 
194
- The registry holds five kinds of artifact, each with its own command group. All five take the same verbs (`search`, `list`, `view`/`show`, `read`, `versions`, `diff`, `add`, `update`, `outdated`, `remove`, `edit`, `unpublish`, `star`, `unstar`, `share`, `unshare`, `init`, `publish`), run by the same code:
194
+ The registry holds six kinds of artifact, each with its own command group. All six take the same verbs (`search`, `list`, `view`/`show`, `read`, `versions`, `diff`, `add`, `update`, `outdated`, `remove`, `edit`, `unpublish`, `star`, `unstar`, `share`, `unshare`, `init`, `publish`), run by the same code:
195
195
 
196
196
  | Kind | Root file | `add` installs to | Group |
197
197
  | --- | --- | --- | --- |
@@ -200,6 +200,7 @@ The registry holds five kinds of artifact, each with its own command group. All
200
200
  | mcp | `server.json` | an entry merged into `.mcp.json` | `ahood mcp <verb>` |
201
201
  | doc | `DOC.md` | `.claude/docs/<owner>@<doc>.md` | `ahood doc <verb>` |
202
202
  | project | `PROJECT.md` | `.claude/project.md` (one per repository) | `ahood project <verb>` |
203
+ | loop | `LOOP.md` | `.claude/loops/<owner>@<loop>.md` | `ahood loop <verb>` |
203
204
 
204
205
  ```
205
206
  $ ahood agent search review
@@ -212,15 +213,18 @@ $ ahood doc add alice/setup-guide # ./.claude/docs/alice@setup-guide.md, p
212
213
  $ ahood project # project guides for this repo's git remote
213
214
  $ ahood project add alice/acme-api # ./.claude/project.md, pinned
214
215
  $ ahood project init acme-api # ./acme-api/PROJECT.md, `project:` filled from git origin
216
+ $ ahood loop read alice/code-review-pass # print a loop without installing it
217
+ $ ahood loop add alice/code-review-pass # ./.claude/loops/alice@code-review-pass.md, pinned
215
218
  $ ahood agent add alice/pdf-tools
216
219
  alice/pdf-tools is a skill, not an agent -- refusing to install it. Use `ahood skill add alice/pdf-tools` instead.
217
220
  ```
218
221
 
219
- - **`ahood agent`, `ahood mcp`, `ahood doc` and `ahood project` are strict.** A target of another kind is refused -- exit code `2`, naming the kind it found -- before anything is downloaded, prompted for, written, or sent. That covers `add`, `update`, `remove`, `edit`, `unpublish`, `share`/`unshare`, `star`/`unstar`, and `publish` (an existing entry of another kind is refused before anything is packed or uploaded); the read-only verbs refuse too, rather than show the wrong kind. `search` asks the registry for that kind only, `list` keeps only that kind of your own entries, and `update`/`outdated` with no argument consider only installed pins of that kind. `agent publish`/`mcp publish`/`doc publish`/`project publish` imply their kind and reject a contradictory `--kind`.
220
- - **Missing or unexpected kind metadata is refused, never guessed.** If the registry's answer carries no kind (or one this CLI doesn't handle), a kind-scoped command stops with exit `1` and points at the legacy form. `remove` reads the kind from the project's own files (the skill directory, the agent file, the mcp fingerprint in the lockfile, or a doc or project pin's recorded `kind`) and asks the registry only when those can't tell; if neither can, nothing is removed.
221
- - **`ahood skill` is legacy and cross-kind in this release.** `ahood skill search` and `ahood skill list` include every kind, and `ahood skill add`/`update`/`remove`/... act on whatever kind the target is, exactly as before -- including `--json` output and exit codes -- so existing scripts keep working. The exceptions are a doc and a project guide: `ahood skill add` refuses them and points at `ahood doc add` / `ahood project add`. Add `--kind skill|agent|mcp|doc|project` to any `ahood skill` verb (except `publish`, whose `--kind` keeps its original meaning) to scope it, or `--kind all` to spell out the cross-kind behavior.
222
- - **Docs install as one pinned file.** `ahood doc add` saves a doc's `DOC.md` to `.claude/docs/<owner>@<doc>.md` and pins it in `.claude/skills.lock.json` with `"kind": "doc"`. It refuses to overwrite a file at that path that isn't pinned as that doc. `ahood doc update`/`outdated`/`remove` manage those pins like any other kind. Kind dispatch is table-driven (`src/kind-info.ts`).
222
+ - **`ahood agent`, `ahood mcp`, `ahood doc`, `ahood project` and `ahood loop` are strict.** A target of another kind is refused -- exit code `2`, naming the kind it found -- before anything is downloaded, prompted for, written, or sent. That covers `add`, `update`, `remove`, `edit`, `unpublish`, `share`/`unshare`, `star`/`unstar`, and `publish` (an existing entry of another kind is refused before anything is packed or uploaded); the read-only verbs refuse too, rather than show the wrong kind. `search` asks the registry for that kind only, `list` keeps only that kind of your own entries, and `update`/`outdated` with no argument consider only installed pins of that kind. `agent publish`/`mcp publish`/`doc publish`/`project publish`/`loop publish` imply their kind and reject a contradictory `--kind`.
223
+ - **Missing or unexpected kind metadata is refused, never guessed.** If the registry's answer carries no kind (or one this CLI doesn't handle), a kind-scoped command stops with exit `1` and points at the legacy form. `remove` reads the kind from the project's own files (the skill directory, the agent file, the mcp fingerprint in the lockfile, or a doc, project, or loop pin's recorded `kind`) and asks the registry only when those can't tell; if neither can, nothing is removed.
224
+ - **`ahood skill` is legacy and cross-kind in this release.** `ahood skill search` and `ahood skill list` include every kind, and `ahood skill add`/`update`/`remove`/... act on whatever kind the target is, exactly as before -- including `--json` output and exit codes -- so existing scripts keep working. The exceptions are a doc, a project guide, and a loop: `ahood skill add` refuses them and points at `ahood doc add` / `ahood project add` / `ahood loop add`. Add `--kind skill|agent|mcp|doc|project|loop` to any `ahood skill` verb (except `publish`, whose `--kind` keeps its original meaning) to scope it, or `--kind all` to spell out the cross-kind behavior.
225
+ - **Docs and loops install as one pinned file per owner/slug.** `ahood doc add` saves a doc's `DOC.md` to `.claude/docs/<owner>@<doc>.md` and pins it in `.claude/skills.lock.json` with `"kind": "doc"`; `ahood loop add` does the same for a loop's `LOOP.md`, to `.claude/loops/<owner>@<loop>.md` with `"kind": "loop"`. Both refuse to overwrite a file at that path that isn't pinned as that entry. `update`/`outdated`/`remove` manage those pins like any other kind. Kind dispatch is table-driven (`src/kind-info.ts`).
223
226
  - **A project guide is one pinned file per repository.** `ahood project add` saves the guide's `PROJECT.md` to `.claude/project.md` and pins it with `"kind": "project"`. It never touches `CLAUDE.md` or `AGENTS.md` (point those at `.claude/project.md` yourself), refuses to overwrite an unpinned `.claude/project.md`, and refuses a second guide while another is pinned (`ahood project remove` it first). `ahood project init` fills the optional `project` frontmatter field from `git remote get-url origin`, normalized to `host/owner/repo` exactly as the registry does (credentials stripped; `--project <remote>` or `--no-project` to override). Bare `ahood project` lists the guides you can see for this repository's remote, through the hosted `get_project_guide` MCP tool.
227
+ - **A loop is a reusable procedure, not scoped to one repository.** It is pseudocode and rules for running a recurring kind of agentic session (a code review pass, a migration sweep, ...), meant to be installed the same way in many different projects -- unlike a project guide, `LOOP.md` has no `project` remote field, just `DOC.md`'s plain `name`/`description` frontmatter. `ahood loop init` has no `--project`/`--no-project` flags for that reason.
224
228
 
225
229
  **Migration plan for a strict `ahood skill`.** A future major version will make `ahood skill <verb>` skills-only by default, matching `agent` and `mcp`. Until then: (1) this release adds the strict groups and `--kind`, and labels `ahood skill` as legacy in `--help`; (2) a following minor release prints a stderr deprecation note when an `ahood skill` command actually touches a non-skill entry (stdout and `--json` unchanged); (3) the major release flips the default, keeping `ahood skill <verb> --kind all` as the explicit cross-kind escape hatch. Scripts that want today's behavior forever can pass `--kind all` now.
226
230
 
@@ -362,6 +366,29 @@ alice/pdf-tools is a skill, not an agent -- refusing to install it. Use `ahood s
362
366
  | `ahood project init [name] [--project <git-remote> \| --no-project]` | Scaffold a new project guide folder with a minimal, valid PROJECT.md. |
363
367
  | `ahood project publish <owner>/<slug>@<version> [--path <dir>] [--name <text>] [--tagline <text>] [--tags <comma,separated>] [--license <id>] [--homepage <url>] [--repository <url>] [--changelog <text>] [--json]` | Publish a new version of a project guide from a folder containing PROJECT.md, creating it first if it doesn't exist yet. |
364
368
 
369
+ ### Loop
370
+
371
+ | Command | What it does |
372
+ | --- | --- |
373
+ | `ahood loop search <query> [--json] [--limit <n>]` | Search published loops. |
374
+ | `ahood loop view\|show <owner>/<loop> [--json] [--web]` | Show a single loop's details without installing it (alias: ahood loop show). |
375
+ | `ahood loop read <owner>/<loop> [--json]` | Print a published loop's LOOP.md, without installing it. |
376
+ | `ahood loop versions <owner>/<loop> [--json]` | List a loop's published-version history. |
377
+ | `ahood loop diff <owner>/<loop> <versionA> <versionB> [--json]` | Show what changed between two published versions of a loop. |
378
+ | `ahood loop list [--json]` | List your own loops, public and private. |
379
+ | `ahood loop add <owner>/<loop>[@version]` | Install a loop into .claude/loops/<owner>@<loop>.md, pinned in the lockfile. |
380
+ | `ahood loop update [<owner>/<loop> ...] [--dry-run] [--json]` | Move loop pins forward to the latest version. |
381
+ | `ahood loop outdated [<owner>/<loop> ...] [--json]` | Read-only staleness check for installed loops. |
382
+ | `ahood loop remove <owner>/<loop> [--yes]` | Uninstall and unpin a loop (local only, prompts unless --yes is passed). |
383
+ | `ahood loop edit <owner>/<loop> [--tagline] [--tags] [--license] [--visibility] [--homepage] [--repository]` | Update a loop you own, changing only the flags you pass. |
384
+ | `ahood loop unpublish <owner>/<loop>[@version] [--yes]` | Delete a loop from the registry for every consumer, or yank one version (prompts unless --yes is passed). |
385
+ | `ahood loop star <owner>/<loop>` | Star a loop. |
386
+ | `ahood loop unstar <owner>/<loop>` | Remove your star from a loop. |
387
+ | `ahood loop share <owner>/<loop> --group <group>` | Share a loop you own with a group, without changing its visibility. |
388
+ | `ahood loop unshare <owner>/<loop> --group <group>` | Stop sharing a loop you own with a group. |
389
+ | `ahood loop init [name]` | Scaffold a new loop folder with a minimal, valid LOOP.md. |
390
+ | `ahood loop publish <owner>/<loop>@<version> [--path <dir>] [--name <text>] [--tagline <text>] [--tags <comma,separated>] [--license <id>] [--homepage <url>] [--repository <url>] [--changelog <text>] [--json]` | Publish a new version of a loop from a folder containing LOOP.md, creating it first if it doesn't exist yet. |
391
+
365
392
  ### Group
366
393
 
367
394
  | Command | What it does |
@@ -1,8 +1,8 @@
1
- import { TOP_LEVEL_COMMANDS_HELP, SKILL_COMMANDS_HELP, AGENT_COMMANDS_HELP, MCP_COMMANDS_HELP, DOC_COMMANDS_HELP, PROJECT_COMMANDS_HELP, GROUP_COMMANDS_HELP, SNAP_COMMANDS_HELP, COMMAND_ALIASES, } from "../help.js";
1
+ import { TOP_LEVEL_COMMANDS_HELP, SKILL_COMMANDS_HELP, AGENT_COMMANDS_HELP, MCP_COMMANDS_HELP, DOC_COMMANDS_HELP, PROJECT_COMMANDS_HELP, LOOP_COMMANDS_HELP, GROUP_COMMANDS_HELP, SNAP_COMMANDS_HELP, COMMAND_ALIASES, } from "../help.js";
2
2
  import { UsageError } from "../usage-error.js";
3
3
  const USAGE = "Usage: ahood completion <bash|zsh|fish>";
4
4
  // Every entity noun with its own verb list, in the order completion offers
5
- // them. Registry nouns (skill, agent, mcp -- ahood-cli#172; doc -- #182; project -- #185) come first; group
5
+ // them. Registry nouns (skill, agent, mcp -- ahood-cli#172; doc -- #182; project -- #185; loop -- ahood#858) come first; group
6
6
  // and snap were already real nouns but had no completion at all before.
7
7
  const NOUNS = [
8
8
  ["skill", SKILL_COMMANDS_HELP],
@@ -10,6 +10,7 @@ const NOUNS = [
10
10
  ["mcp", MCP_COMMANDS_HELP],
11
11
  ["doc", DOC_COMMANDS_HELP],
12
12
  ["project", PROJECT_COMMANDS_HELP],
13
+ ["loop", LOOP_COMMANDS_HELP],
13
14
  ["group", GROUP_COMMANDS_HELP],
14
15
  ["snap", SNAP_COMMANDS_HELP],
15
16
  ];
@@ -139,12 +139,32 @@ TODO -- how to work in this repository: conventions, how to verify a change, and
139
139
  traps. Plain Markdown; \`ahood project add\` saves this whole file to .claude/project.md.
140
140
  `;
141
141
  }
142
+ // LOOP.md, for `ahood loop init` (ahood-cli, ahood#858). The backend's
143
+ // validator (lib/publish/parse-loop-frontmatter.ts) is DOC.md's exact flat
144
+ // reader -- no extra field, unlike PROJECT.md's optional `project` remote,
145
+ // since a loop is explicitly portable across repositories rather than
146
+ // scoped to one. Same rules as buildDocMd apply: nothing above the opening
147
+ // `---`, and no colon in a comment line inside the frontmatter.
148
+ function buildLoopMd(name) {
149
+ return `---
150
+ name: ${name}
151
+ description: TODO -- one line saying what kind of recurring agentic session this loop is for.
152
+ ---
153
+
154
+ # ${name}
155
+
156
+ TODO -- the pseudocode and rules for running this kind of session: the steps, the order,
157
+ and what to do at each decision point. Plain Markdown; \`ahood loop add\` saves this whole
158
+ file to .claude/loops/<owner>@<loop>.md.
159
+ `;
160
+ }
142
161
  const TEMPLATES = {
143
162
  skill: buildSkillMd,
144
163
  agent: buildAgentMd,
145
164
  mcp: buildServerJson,
146
165
  doc: buildDocMd,
147
166
  project: buildProjectMd,
167
+ loop: buildLoopMd,
148
168
  };
149
169
  const NEXT_STEPS = {
150
170
  skill: ["Fill in the description, then flesh out the ## Instructions section."],
@@ -156,6 +176,7 @@ const NEXT_STEPS = {
156
176
  ],
157
177
  doc: ["Fill in the description, then write the doc below the frontmatter."],
158
178
  project: ["Fill in the description, then write the guide below the frontmatter."],
179
+ loop: ["Fill in the description, then write the loop's pseudocode and rules below the frontmatter."],
159
180
  };
160
181
  function parseProjectFlags(args) {
161
182
  const raw = flagValue(args, "--project");
package/dist/help.js CHANGED
@@ -386,6 +386,10 @@ function registryCommandsHelp(kind) {
386
386
  }
387
387
  export const AGENT_COMMANDS_HELP = registryCommandsHelp("agent");
388
388
  export const DOC_COMMANDS_HELP = registryCommandsHelp("doc");
389
+ // `ahood loop` (ahood-cli, ahood#858): the shared verbs, no extras -- a loop
390
+ // is portable across repositories (unlike `project`), so it needs none of
391
+ // PROJECT_COMMANDS_HELP's bare-listing or `--project` flag additions below.
392
+ export const LOOP_COMMANDS_HELP = registryCommandsHelp("loop");
389
393
  // `ahood project` (ahood-cli#185): the shared verbs, with init's `project`
390
394
  // field flags, plus bare `ahood project` -- the guides for this repository.
391
395
  export const PROJECT_COMMANDS_HELP = [
@@ -619,6 +623,7 @@ const ENTITY_COMMANDS_HELP = {
619
623
  mcp: MCP_COMMANDS_HELP,
620
624
  doc: DOC_COMMANDS_HELP,
621
625
  project: PROJECT_COMMANDS_HELP,
626
+ loop: LOOP_COMMANDS_HELP,
622
627
  group: GROUP_COMMANDS_HELP,
623
628
  snap: SNAP_COMMANDS_HELP,
624
629
  };
@@ -801,12 +806,22 @@ export function formatProjectHelp() {
801
806
  "this repository's git remote.",
802
807
  ], PROJECT_COMMANDS_HELP, "project");
803
808
  }
809
+ export function formatLoopHelp() {
810
+ return formatRegistryKindHelp("ahood loop -- loops (LOOP.md) in the ahood registry", [
811
+ "A loop is a pseudocode-and-rules procedure for running a recurring kind of agentic session",
812
+ "(a code review pass, a migration sweep, ...), reusable across repositories -- unlike a project",
813
+ "guide, it names no single repository. Every verb here acts on loops only: a target of another",
814
+ "kind is refused before anything is downloaded, prompted for, written, or sent. Installs land in",
815
+ ".claude/loops/<owner>@<loop>.md. To look at a loop without installing it, use `ahood loop read`.",
816
+ ], LOOP_COMMANDS_HELP, "loop");
817
+ }
804
818
  const KIND_HELP = {
805
819
  skill: formatSkillHelp,
806
820
  agent: formatAgentHelp,
807
821
  mcp: formatMcpHelp,
808
822
  doc: formatDocHelp,
809
823
  project: formatProjectHelp,
824
+ loop: formatLoopHelp,
810
825
  };
811
826
  export function formatKindHelp(noun) {
812
827
  return KIND_HELP[noun]();
@@ -850,6 +865,8 @@ export function formatHelp() {
850
865
  const docGroupSummary = "The same verbs, for docs (DOC.md) only -- run `ahood doc --help` for the full list.";
851
866
  const projectGroupUsage = "ahood project <command>";
852
867
  const projectGroupSummary = "The same verbs, for project guides (PROJECT.md) only; bare `ahood project` lists this repository's guides.";
868
+ const loopGroupUsage = "ahood loop <command>";
869
+ const loopGroupSummary = "The same verbs, for loops (LOOP.md) only -- run `ahood loop --help` for the full list.";
853
870
  const groupGroupUsage = "ahood group <command>";
854
871
  const groupGroupSummary = "Create private groups and share skills with them -- run `ahood group --help` for the full list.";
855
872
  const snapGroupUsage = "ahood snap <command>";
@@ -861,6 +878,7 @@ export function formatHelp() {
861
878
  { usage: mcpGroupUsage, summary: mcpGroupSummary },
862
879
  { usage: docGroupUsage, summary: docGroupSummary },
863
880
  { usage: projectGroupUsage, summary: projectGroupSummary },
881
+ { usage: loopGroupUsage, summary: loopGroupSummary },
864
882
  { usage: groupGroupUsage, summary: groupGroupSummary },
865
883
  { usage: snapGroupUsage, summary: snapGroupSummary },
866
884
  ]);
@@ -882,7 +900,7 @@ export function formatHelp() {
882
900
  "",
883
901
  "Run `ahood <command> --help` (or `ahood help <command>`) for a single command's flags and examples.",
884
902
  "Run `ahood skill --help` for the full list of skill commands.",
885
- "Run `ahood agent|mcp|doc|project --help` for the kind-scoped agent, MCP, doc and project guide commands.",
903
+ "Run `ahood agent|mcp|doc|project|loop --help` for the kind-scoped agent, MCP, doc, project guide and loop commands.",
886
904
  "Run `ahood group --help` for the full list of group commands.",
887
905
  "Run `ahood snap --help` for the full list of snap commands.",
888
906
  "Run `ahood --version` to print the installed CLI version.",
package/dist/index.js CHANGED
@@ -99,6 +99,7 @@ const COMMANDS = {
99
99
  agent: dispatchAgent,
100
100
  doc: dispatchDoc,
101
101
  project: dispatchProject,
102
+ loop: dispatchLoop,
102
103
  group: dispatchGroup,
103
104
  snap: dispatchSnap,
104
105
  };
@@ -111,7 +112,7 @@ const COMMANDS = {
111
112
  // sense "skill" already used before "group" (the entity) existed. "snap"
112
113
  // joins this set for the exact same reason (ahood-cli#107).
113
114
  // Every registry noun joins it (ahood-cli#172), taken from the kind table so
114
- // a new kind (doc #182, project #185) needs no edit here.
115
+ // a new kind (doc #182, project #185, loop ahood#858) needs no edit here.
115
116
  const GROUP_COMMANDS = new Set([...CLI_KINDS, "group", "snap"]);
116
117
  function levenshtein(a, b) {
117
118
  const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
@@ -141,6 +142,19 @@ function closestOf(input, candidates) {
141
142
  function closestCommand(input) {
142
143
  return closestOf(input, Object.keys(COMMANDS));
143
144
  }
145
+ // Plural alias (ahood-cli#190): `ahood skills`, `ahood agents`, `ahood mcps`,
146
+ // `ahood docs`, `ahood projects`, `ahood loops` dispatch exactly as the
147
+ // singular kind would. A straight trailing-`s` strip-and-match
148
+ // against the KINDS registry is enough -- every current and planned kind
149
+ // pluralizes this way, and no COMMANDS key ends in "s" (checked below), so a
150
+ // real top-level command can never be shadowed by this. Scoped to CLI_KINDS
151
+ // only, not "group"/"snap" (those are separate entities, not kinds).
152
+ function pluralKindAlias(word) {
153
+ if (!word.endsWith("s"))
154
+ return undefined;
155
+ const singular = word.slice(0, -1);
156
+ return isCliKind(singular) ? singular : undefined;
157
+ }
144
158
  function closestSkillCommand(input) {
145
159
  return closestOf(input, Object.keys(SKILL_COMMANDS));
146
160
  }
@@ -217,6 +231,13 @@ async function dispatchAgent(args) {
217
231
  async function dispatchDoc(args) {
218
232
  await dispatchRegistry("doc", args);
219
233
  }
234
+ // `ahood loop` (ahood-cli, ahood#858): a loop is portable across
235
+ // repositories (unlike `project`), so it has no bare-command listing of its
236
+ // own -- every verb is the shared registry dispatch, help included, the
237
+ // same shape as dispatchDoc above.
238
+ async function dispatchLoop(args) {
239
+ await dispatchRegistry("loop", args);
240
+ }
220
241
  // `ahood project` (ahood-cli#185): with no verb -- nothing, or only its own
221
242
  // flags -- it lists the project guides for this repository's git remote
222
243
  // (commands/project-guides.ts). Everything else is the shared registry
@@ -359,7 +380,13 @@ async function main() {
359
380
  console.log(formatHelp());
360
381
  return;
361
382
  }
362
- const handler = COMMANDS[command];
383
+ // Resolve a plural kind alias (ahood-cli#190) before the generic lookup, so
384
+ // everything downstream -- the handler itself and the GROUP_COMMANDS
385
+ // --help interception below -- treats `ahood projects` exactly like
386
+ // `ahood project`. Only applies when `command` isn't already a real
387
+ // command (so it never shadows one).
388
+ const resolvedCommand = COMMANDS[command] ? command : (pluralKindAlias(command) ?? command);
389
+ const handler = COMMANDS[resolvedCommand];
363
390
  if (!handler) {
364
391
  console.error(`Unknown command: ${command}`);
365
392
  // Every previously-flat skill command moved under `ahood skill <verb>`
@@ -381,8 +408,8 @@ async function main() {
381
408
  // GROUP_COMMANDS despite the name, unrelated to the "group" entity itself)
382
409
  // own their own --help handling at both the entity and per-verb level --
383
410
  // don't intercept here.
384
- if (!GROUP_COMMANDS.has(command) && (args.includes("--help") || args.includes("-h"))) {
385
- const entry = findCommandHelp(command);
411
+ if (!GROUP_COMMANDS.has(resolvedCommand) && (args.includes("--help") || args.includes("-h"))) {
412
+ const entry = findCommandHelp(resolvedCommand);
386
413
  console.log(entry ? formatCommandHelp(entry) : formatHelp());
387
414
  return;
388
415
  }
@@ -413,4 +440,4 @@ if (isEntrypoint) {
413
440
  main();
414
441
  }
415
442
  // Exported for tests only -- the CLI itself only ever calls main() above.
416
- export { main, dispatchSkill, dispatchAgent, dispatchDoc, dispatchProject, dispatchMcp, dispatchRegistry, dispatchGroup, dispatchSnap, COMMANDS, SKILL_COMMANDS, GROUP_VERBS, SNAP_VERBS };
443
+ export { main, dispatchSkill, dispatchAgent, dispatchDoc, dispatchProject, dispatchLoop, dispatchMcp, dispatchRegistry, dispatchGroup, dispatchSnap, COMMANDS, SKILL_COMMANDS, GROUP_VERBS, SNAP_VERBS };
package/dist/kind-info.js CHANGED
@@ -4,15 +4,20 @@
4
4
  //
5
5
  // Kind-aware registry commands (ahood-cli#172).
6
6
  //
7
- // The registry stores five artifact kinds (the backend's
8
- // lib/skills/validation.ts ALLOWED_KINDS: skill, agent, mcp, doc, project), and a
7
+ // The registry stores six artifact kinds (the backend's
8
+ // lib/artifact-kinds.ts ALLOWED_KINDS: skill, agent, mcp, doc, project, loop), and a
9
9
  // kind is fixed when an entry is created -- PATCH's allow-list has no `kind`
10
10
  // -- so the kind an entry reports now is the kind it has always had. Each
11
11
  // kind has a noun group (`ahood skill|agent|mcp|doc <verb>`), all run by one
12
12
  // shared dispatcher. `doc` joined as the fourth in ahood-cli#182: it installs
13
13
  // as one flat Markdown file, its DOC.md, the same shape an agent has.
14
14
  // `project` joined as the fifth in ahood-cli#185: one PROJECT.md per
15
- // repository, installed to the fixed path .claude/project.md.
15
+ // repository, installed to the fixed path .claude/project.md. `loop` joined
16
+ // as the sixth (ahood-cli, ahood#858): a portable, reusable procedure for a
17
+ // recurring kind of agentic session, installed many-per-project like `doc`
18
+ // (.claude/loops/<owner>@<loop>.md), with DOC.md's exact frontmatter shape --
19
+ // no extra field, unlike `project`'s optional `project` remote, since a loop
20
+ // is explicitly not scoped to one repository.
16
21
  //
17
22
  // Per-kind facts live here, not in per-command branches: `placeholder` and
18
23
  // `example` feed help and init, `about` the legacy `skill add` refusal.
@@ -64,16 +69,27 @@ export const KINDS = {
64
69
  example: { slug: "alice/acme-api", name: "Acme API" },
65
70
  about: "a guide to working in one repository",
66
71
  },
72
+ loop: {
73
+ label: "loop",
74
+ article: "a",
75
+ plural: "loops",
76
+ rootDoc: "LOOP.md",
77
+ installsTo: ".claude/loops/<owner>@<loop>.md",
78
+ placeholder: "<loop>",
79
+ example: { slug: "alice/code-review-pass", name: "Code Review Pass" },
80
+ about: "a reusable procedure for a recurring kind of agentic session, portable across repositories",
81
+ },
67
82
  };
68
83
  export const CLI_KINDS = Object.keys(KINDS);
69
84
  export function isCliKind(value) {
70
85
  return typeof value === "string" && CLI_KINDS.includes(value);
71
86
  }
72
87
  // Kinds that install as one root-doc file pinned with its kind in the
73
- // lockfile (doc: ahood-cli#182, project: #185). The recorded kind is how
74
- // remove/update recognise the pin offline, and how a project guide's shared
75
- // path is claimed by exactly one pin. Legacy `skill add` refuses these.
76
- export const PINNED_FILE_KINDS = ["doc", "project"];
88
+ // lockfile (doc: ahood-cli#182, project: #185, loop: ahood#858). The
89
+ // recorded kind is how remove/update recognise the pin offline, and how a
90
+ // project guide's shared path is claimed by exactly one pin. Legacy
91
+ // `skill add` refuses these.
92
+ export const PINNED_FILE_KINDS = ["doc", "project", "loop"];
77
93
  export function isPinnedFileKind(value) {
78
94
  return typeof value === "string" && PINNED_FILE_KINDS.includes(value);
79
95
  }
package/dist/spec.js CHANGED
@@ -134,7 +134,23 @@ export function docPath(owner, doc) {
134
134
  // file the user owns. Being shared, the path is claimed by exactly one pin:
135
135
  // add.ts refuses a second guide while another is pinned.
136
136
  export const PROJECT_GUIDE_PATH = join(".claude", "project.md");
137
+ // A loop installs as one flat file, .claude/loops/<owner>@<loop>.md
138
+ // (ahood-cli, ahood#858) -- the same shape as docPath above, not
139
+ // PROJECT_GUIDE_PATH's single fixed path, since a loop is explicitly
140
+ // portable across repositories rather than scoped to one (see
141
+ // docs/adr/backend/0015-loop-artifact-kind.md in the ahood repo).
142
+ export const LOOPS_ROOT = join(".claude", "loops");
143
+ export function loopPath(owner, loop) {
144
+ const spec = `${owner}/${loop}`;
145
+ validateSegment(owner, "owner", spec);
146
+ validateSegment(loop, "skill", spec);
147
+ return join(LOOPS_ROOT, `${owner}@${loop}.md`);
148
+ }
137
149
  // Where a pinned-file kind's install lives (kind-info.ts PINNED_FILE_KINDS).
138
150
  export function pinnedFilePath(kind, owner, slug) {
139
- return kind === "doc" ? docPath(owner, slug) : PROJECT_GUIDE_PATH;
151
+ if (kind === "doc")
152
+ return docPath(owner, slug);
153
+ if (kind === "loop")
154
+ return loopPath(owner, slug);
155
+ return PROJECT_GUIDE_PATH;
140
156
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ahood
3
- description: How to use the ahood CLI (@ahood/cli), the registry for reusable AI-agent skills, subagent definitions (AGENT.md), MCP server manifests (server.json), docs (DOC.md), and project guides (PROJECT.md). Load BEFORE running any ahood command; before writing a non-trivial reusable skill, subagent, or MCP integration from scratch (search the registry first); when installing, updating, removing, publishing, sharing, or unpublishing one; when wiring ahood into an MCP host; or when setting up headless/CI access with AHOOD_TOKEN. Triggers - ahood, skill registry, is there a skill for this, install a skill, install an agent, add an MCP server, read a doc, project guide, publish a skill, ahood mcp, ahood doc, ahood project, AHOOD_TOKEN.
3
+ description: How to use the ahood CLI (@ahood/cli), the registry for reusable AI-agent skills, subagent definitions (AGENT.md), MCP server manifests (server.json), docs (DOC.md), project guides (PROJECT.md), and loops (LOOP.md). Load BEFORE running any ahood command; before writing a non-trivial reusable skill, subagent, MCP integration, or recurring-session procedure from scratch (search the registry first); when installing, updating, removing, publishing, sharing, or unpublishing one; when wiring ahood into an MCP host; or when setting up headless/CI access with AHOOD_TOKEN. Triggers - ahood, skill registry, is there a skill for this, install a skill, install an agent, add an MCP server, read a doc, project guide, loop, recurring agentic session procedure, publish a skill, ahood mcp, ahood doc, ahood project, ahood loop, AHOOD_TOKEN.
4
4
  ---
5
5
 
6
6
  # ahood
@@ -13,7 +13,7 @@ and only moves when someone runs an update.
13
13
  This guide ships inside the CLI itself (`ahood help useme`), so it matches the installed
14
14
  version (`ahood --version`). It needs no login or network to print.
15
15
 
16
- ## Five kinds, five command groups
16
+ ## Six kinds, six command groups
17
17
 
18
18
  | Kind | Root file | Installs to | Commands |
19
19
  |---|---|---|---|
@@ -22,19 +22,20 @@ version (`ahood --version`). It needs no login or network to print.
22
22
  | mcp | `server.json` | an entry merged into `.mcp.json` | `ahood mcp <verb>` |
23
23
  | doc | `DOC.md` | `.claude/docs/<owner>@<doc>.md` | `ahood doc <verb>` |
24
24
  | project | `PROJECT.md` | `.claude/project.md` (one per repository) | `ahood project <verb>` |
25
+ | loop | `LOOP.md` | `.claude/loops/<owner>@<loop>.md` | `ahood loop <verb>` |
25
26
 
26
- All five groups take the same verbs: `search`, `list`, `view`/`show`, `read`, `versions`,
27
+ All six groups take the same verbs: `search`, `list`, `view`/`show`, `read`, `versions`,
27
28
  `diff`, `add`, `update`, `outdated`, `remove`, `edit`, `unpublish`, `star`, `unstar`,
28
29
  `share`, `unshare`, `init`, `publish`.
29
30
 
30
- - `ahood agent <verb>`, `ahood mcp <verb>`, `ahood doc <verb>` and `ahood project <verb>` act on their own kind only.
31
+ - `ahood agent <verb>`, `ahood mcp <verb>`, `ahood doc <verb>`, `ahood project <verb>` and `ahood loop <verb>` act on their own kind only.
31
32
  A target of another kind is refused (exit 2) before anything is downloaded, prompted for,
32
33
  written, or sent, and the error names the kind it found and the right command.
33
34
  - `ahood skill <verb>` is the **legacy, cross-kind** group in this CLI release: `skill search`
34
35
  and `skill list` include every kind, and `skill add`/`skill update` install whatever kind
35
- they resolve, except a doc or project guide: `skill add` refuses one and points at
36
- `ahood doc add` / `ahood project add`.
37
- Add `--kind skill` (or `agent`, `mcp`, `doc`, `project`) to any verb to scope it to one kind;
36
+ they resolve, except a doc, project guide, or loop: `skill add` refuses one and points at
37
+ `ahood doc add` / `ahood project add` / `ahood loop add`.
38
+ Add `--kind skill` (or `agent`, `mcp`, `doc`, `project`, `loop`) to any verb to scope it to one kind;
38
39
  `--kind all` spells out the legacy behavior. Prefer the kind-specific form in anything new --
39
40
  a future major version makes `ahood skill` skills-only.
40
41
  - A doc is a documentation page. `ahood doc read alice/setup-guide` prints it without
@@ -44,6 +45,11 @@ All five groups take the same verbs: `search`, `list`, `view`/`show`, `read`, `v
44
45
  `ahood project` inside a repository to list the guides naming its git remote;
45
46
  `ahood project add alice/acme-api` saves one to `.claude/project.md` (pinned, one per
46
47
  repository) and never touches `CLAUDE.md` or `AGENTS.md` -- point those at it yourself.
48
+ - A loop is a pseudocode-and-rules procedure for running a recurring kind of agentic session
49
+ (a code review pass, a migration sweep, ...), reusable across repositories -- unlike a
50
+ project guide, `LOOP.md` has no repository field. `ahood loop read alice/code-review-pass`
51
+ prints it without installing anything; `ahood loop add alice/code-review-pass` saves it as
52
+ one pinned file under `.claude/loops/`, managed like any other pin.
47
53
 
48
54
  ## Find, inspect, then install
49
55
 
@@ -93,7 +99,7 @@ ahood skill update --dry-run --json # preview, machine-readable
93
99
  ahood agent remove alice/code-reviewer # local uninstall; prompts unless --yes
94
100
  ```
95
101
 
96
- With no target, `agent`/`mcp`/`doc`/`project` `update` and `outdated` consider only installed pins of that
102
+ With no target, `agent`/`mcp`/`doc`/`project`/`loop` `update` and `outdated` consider only installed pins of that
97
103
  kind; the legacy `skill update` covers every pin. Updates only affect the current project.
98
104
  An `.mcp.json` entry you edited by hand is refused rather than overwritten.
99
105
 
@@ -107,13 +113,14 @@ ahood agent init code-reviewer # ./code-reviewer/AGENT.md
107
113
  ahood mcp init github-server # ./github-server/server.json (placeholder URL)
108
114
  ahood doc init setup-guide # ./setup-guide/DOC.md
109
115
  ahood project init acme-api # ./acme-api/PROJECT.md, `project:` filled from git origin
116
+ ahood loop init code-review-pass # ./code-review-pass/LOOP.md
110
117
 
111
118
  ahood agent publish alice/code-reviewer@1.0.0 --path code-reviewer --name "Code Reviewer"
112
119
  ahood skill publish alice/pdf-tools@1.1.0 --path pdf-tools --changelog "Adds split"
113
120
  ```
114
121
 
115
122
  - `--name` is required only on the first publish, which creates the entry under your
116
- account. `agent publish`/`mcp publish`/`doc publish`/`project publish` imply the kind and refuse a contradictory `--kind`
123
+ account. `agent publish`/`mcp publish`/`doc publish`/`project publish`/`loop publish` imply the kind and refuse a contradictory `--kind`
117
124
  or an existing entry of another kind before uploading anything.
118
125
  - **New entries are private.** Make one discoverable explicitly:
119
126
  `ahood skill edit alice/pdf-tools --visibility public`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahood/cli",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "CLI for ahood, a registry for installing and publishing Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/AlexKay28/ahood-cli",