diff --git a/.agents/skills/axi-tool-intake/SKILL.md b/.agents/skills/axi-tool-intake/SKILL.md new file mode 100644 index 0000000000..3b20a81d75 --- /dev/null +++ b/.agents/skills/axi-tool-intake/SKILL.md @@ -0,0 +1,106 @@ +--- +name: axi-tool-intake +description: >- + This fleet's four rules for acquiring an agent-ergonomic CLI, carrying no design guidance of its own - the `axi` skill owns the design contract in full and this one never restates it. + Use before filing, scoping, or briefing work that would build, adopt, derive, or extend such a tool for this fleet, and before telling the captain that none exists for a domain. + Covers checking both indexes because they disagree, the credential rule, the licence and provenance rules for deriving from someone else's tool, and the open question of how a finished tool reaches every seat rather than one. +user-invocable: false +metadata: + internal: true +--- + +# axi-tool-intake + +**This skill contains no design guidance and never will.** +The `axi` skill in this repo owns the AXI contract in full - all ten principles, worked examples, and the `--version` fast path - and it is vendored verbatim from upstream, so nothing here may restate, summarize, or paraphrase it. +Load `axi` for how a tool should behave; load this for what this fleet does before and around building one. +`docs/axi-skill-provenance.md` records where that skill came from and carries its licence notice. + +Verified 2026-08-19 against its text: the AXI specification covers none of the four rules below. + +## 1. Check both indexes before building, and state which you checked + +This is the first step, before any design work and before a build task is filed at all. + +There are two indexes and **neither is a superset of the other**, so check both: + +- The **axi.md catalogue**: `catalog.yaml` in , which the published catalogue page is generated from. +- The **package registry**, by exact name first: `npm view -axi`, then `npm search ` for tools not named to the pattern. + +Also check this home's own installed set under `.local/axi/bin` and the self-updating list in `bin/fm-axi-suite.sh`, since a tool the fleet already carries is the cheapest answer of all. +A forge repository search for `-axi` is a useful third pass when both indexes come back empty. + +Then **state the result and name which indexes you checked, in the record that drives the decision.** +"Absent from the catalogue and absent from npm, checked " is a finding and belongs in the task note exactly as much as "one exists and covers two thirds of what we call". +An unrecorded search is indistinguishable from no search, and a result that does not say what was searched cannot be re-checked later. + +### Why one index is not enough, measured 2026-08-19 + +The two indexes disagree in **both** directions: + +- `forgejo-axi` is on npm at 1.2.0 and is **not** in the axi.md catalogue. +- `docker-axi` and `jj-axi` are **in** the axi.md catalogue and return a flat 404 on npm under those names. + +So checking one index and finding nothing proves nothing at all. + +Absence from the catalogue means only that nobody added an entry to `catalog.yaml`. +It is not evidence that a tool is unofficial, unmaintained, or unsuitable: `forgejo-axi` is MIT, at its third release, and absent purely because no one filed the pull request. + +### What it cost, both directions + +- A task to write a Forgejo client was filed 2026-08-17, when `forgejo-axi` 1.0.0 had been on npm since 2026-08-03, fourteen days earlier, reaching 1.2.0 on 2026-08-16. + Nobody looked. +- The same day, a plan said "adopt sc1's archive tool, do not rebuild" - and that tool had never been persisted at all. + That is the opposite error, from the same missing habit. + +This fleet's own note recorded the Forgejo miss as one day late; the registry says fourteen, and the registry is the authority. + +### A tool's description is not its coverage + +Never decide build, adopt, or derive from a candidate's one-sentence description. +Enumerate the verbs this fleet actually calls - grep the real call sites, do not recall them - then check each against the candidate's own `--help` output, and report the delta as a number before the decision is made. + +`forgejo-axi` describes itself around pull request lifecycles and in fact also carries issues, labels, and a raw `api` path, while omitting review submission entirely. +It was both narrower and wider than its own sentence, in ways that only an enumeration surfaces. + +## 2. Credentials: this fleet's rule, not the specification's + +The AXI specification is silent on secret handling. +This fleet is not, and a tool built or derived here carries this rule regardless: + +- **A credential is never accepted as a value on the command line and never emitted.** + Resolve it from an environment variable or a configuration file inside the process. +- **A credential is verified by its effect** - a successful authenticated read - never by displaying it. + +`forgejo-axi` already does exactly this, so it is a worked example rather than a theory. + +An argument value is readable by any account on the host through the process listing; `fm-axi-nomistakes-guidance-off-argv` in the backlog is this fleet's own live instance of that defect. +Load `secrets-handling` for the mechanics; this section states only that the rule binds tools we build, and does not restate it. + +## 3. Deriving from an existing tool + +When the decision is to start from someone else's tool rather than from nothing: + +- **Read the actual licence file of that specific upstream.** + Do not infer it from the ecosystem, from a sibling project, or from the catalogue. + This fleet shipped a fix on 2026-08-19 for exactly this defect: licensing asserted beyond what was evidenced. +- **The copyright notice travels with the derived work.** + MIT requires it, and a derivation that drops it is broken in a way nothing at runtime will ever reveal. +- **The provenance record names the upstream, its version, its commit, and what changed.** + A derivation whose parent cannot be identified cannot be updated when the parent fixes something. + +Build on `packages/axi-sdk-js` from the AXI repository rather than hand-rolling the contract's primitives. +It is MIT and maintained, and a hand-written copy of what it already provides drifts the first time the specification moves. + +## 4. How a finished tool reaches every seat - OPEN + +Publishing to the public catalogue is a solved question, and not this one: it is a pull request adding one entry to `catalog.yaml`, with the generated tables regenerated in the same commit, per `CONTRIBUTING.md` in . +Read that procedure there rather than from memory. + +**How a tool reaches every seat in this fleet has no settled answer, and inventing one is worse than naming the gap.** + +Who owns this fleet's AXI suite, and what the accepted route is for adding a tool to it, is tracked as `fleet-forgejo-axi` and `fm-axi-nomistakes-guidance-off-argv` - two records asking the same ownership question about the same suite, meant to be answered once for both. + +Two things follow while it is open. +A tool that exists only on the seat that built it is not what was asked for, so "it works here" is not a completed deliverable. +And the route out - upstream contribution, fleet-local package, vendored copy - is a captain decision, not a builder's; escalate it rather than choosing by default. diff --git a/.agents/skills/axi/LICENSE b/.agents/skills/axi/LICENSE new file mode 100644 index 0000000000..5043b3a58a --- /dev/null +++ b/.agents/skills/axi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Kun Chen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.agents/skills/axi/SKILL.md b/.agents/skills/axi/SKILL.md new file mode 100644 index 0000000000..b6e0819a5a --- /dev/null +++ b/.agents/skills/axi/SKILL.md @@ -0,0 +1,273 @@ +--- +name: axi +description: > + Agent eXperience Interface (AXI) — ergonomic standards for building CLI tools that agents + use via shell execution. Use when building, modifying, or reviewing any agent-facing CLI. +--- + +# Agent eXperience Interface (AXI) + +AXI defines ergonomic standards for building CLI tools that autonomous agents interact with through shell execution. + +## Before you start + +Read the [TOON specification](https://toonformat.dev/reference/spec.html) before building any AXI output. + +## 1. Token-efficient output + +Use [TOON](https://toonformat.dev/) (Token-Oriented Object Notation) as the output format on stdout. +TOON provides ~40% token savings over equivalent JSON while remaining readable by agents. +Convert to TOON at the output boundary — keep internal logic on JSON. + +``` +tasks[2]{id,title,status,assignee}: + "1",Fix auth bug,open,alice + "2",Add pagination,closed,bob +``` + +## 2. Minimal default schemas + +Every field in stdout costs tokens — multiplied by row count in collections. +Default to the smallest schema that lets the agent decide what to do next: typically an identifier, a title, and a status. + +- Default list schemas: 3-4 fields, not 10 +- Default limits: high enough to cover common cases in one call (if most repos have <100 labels, default to 100, not 30) +- Long-form content (bodies, descriptions) belongs in detail views, not lists +- Offer a `--fields` flag to let agents request additional fields explicitly + +## 3. Content truncation + +Detail views often contain large text fields. Omitting them forces agents to hunt; including them wastes tokens. +Truncate by default and tell the agent how to get the full version. + +``` +task: + number: 42 + title: Fix auth bug + state: open + body: First 500 chars of the issue body... + ... (truncated, 8432 chars total) +help[1]: Run `tasks view 42 --full` to see complete body +``` + +- Never omit large fields entirely — include a truncated preview +- Show the total size so the agent knows how much it's missing +- Suggest the escape hatch (`--full`) only when content is actually truncated +- Choose a truncation limit that covers most use cases (500-1500 chars) + +## 4. Pre-computed aggregates + +The most expensive token cost is often not a longer response — it's a follow-up call. If your backend has data that agents commonly need as a next step, compute it and include it. + +**Aggregate counts**: include the **total count** in list output, not just the page size. Agents need "how many are there?" and will paginate if the answer isn't definitive. + +``` +count: 30 of 847 total +tasks[30]{number,title,state}: + 1,Fix auth bug,open + ... +``` + +**Derived status fields**: when the next step almost always involves checking related state, include a lightweight summary inline. + +``` +task: + number: 42 + title: Deploy pipeline fix + state: open + checks: 3/3 passed + comments: 7 +``` + +Only include derived fields your backend can provide cheaply — a summary ("3/3 passed"), not the full data. + +## 5. Definitive empty states + +When the answer is "nothing", say so explicitly. Ambiguous empty output causes agents to re-run with different flags to verify. + +``` +$ tasks list --state closed +tasks: 0 closed tasks found in this repository +``` + +State the zero with context. Make it clear the command succeeded — the absence of results is the answer. + +## 6. Structured errors & exit codes + +### Idempotent mutations + +Don't error when the desired state already exists. If the agent closes something already closed, acknowledge and move on with exit code 0. Reserve non-zero exit codes for situations where the agent's intent genuinely cannot be satisfied. + +``` +$ tasks close 42 +task: #42 already closed (no-op) # exit 0 +``` + +### Structured errors on stdout + +Errors go to **stdout** in the same structured format as normal output, so the agent can read and act on them. Include what went wrong and an actionable suggestion. Never let raw dependency output (API errors, stack traces) leak through. + +``` +error: --title is required +help: tasks create --title "..." [--body "..."] +``` + +- Validate required flags before calling any dependency +- Translate errors — extract actionable meaning, discard noise +- Never leak dependency names — suggestions reference your CLI's commands, not the underlying tool + +### No interactive prompts + +Every operation must be completable with flags alone. If a required value is missing, fail immediately with a clear error — don't prompt for it. Suppress prompts from wrapped tools. + +### Fail loud on unrecognized input + +Reject unknown flags and arguments — never silently ignore them. A dropped flag is worse than an error: the agent gets plausible-looking output it believes is scoped or filtered, then proceeds confidently on wrong data. This is the same guarantee a CLI already owes for an unknown _command_; extend it to flags. + +``` +$ tasks list --stat closed +error: unknown flag --stat for `list` +help: valid flags for `list`: --state, --assignee, --limit (--help always allowed) +``` + +- **Validate before any dependency call**, with exit code 2 — the same as a missing required flag. Each command declares its own known flags; an unrecognized one is rejected by name and the command's valid flags are listed. +- **`--help` always passes** — it's the one universal flag. Beyond it, a CLI may standardize its own always-allowed globals (e.g. an `--account` selector); whatever the set, those flags pass on every command and are never reported as unknown. +- **Renamed or removed flags get a targeted hint**, not the generic list — point at what replaced them (`--status was renamed; use --state instead`) so the agent self-corrects in one step. +- **Per-subcommand flag sets.** For grouped nouns where one command dispatches to subcommands (a `list` vs a `create` under the same noun), validate against the _subcommand's_ flags — they differ, and only the subcommand layer knows which is in play. +- **Make the error self-correcting in one turn.** The agent's deterministic next move after an unknown-flag error is to run ` --help` (e.g. `tasks list --help`) — so fold that lookup into the error: list the valid flags inline, or print the command's concise `--help` block directly beneath it. Per §4 the expensive cost is the follow-up call, and per §10 per-command help is already concise, so inlining it collapses the two-turn correction into one. + +### Output channels + +- **stdout**: all structured output the agent consumes — data, errors, suggestions +- **stderr**: debug logging, progress indicators, diagnostics (agents don't read this) +- **Exit codes**: 0 = success (including no-ops), 1 = error, 2 = usage error + +Never mix progress messages into stdout. An agent that reads "Fetching data..." will try to interpret it as data. + +## 7. Ambient context via session integrations + +Register your tool into the agent's session lifecycle so every conversation starts with relevant state already visible — before the agent takes any action. + +**Pattern:** + +1. Provide an explicit setup command that installs or repairs a session hook or plugin after user intent is clear +2. At session start, the integration runs your tool and provides a compact dashboard as context +3. The agent receives this as initial context and can act immediately + +``` +# Agent sees this at session start — no invocation needed: +specs[2]{id,title,status}: + 1,Fix auth bug,open + 2,Add pagination,in-progress + +help[2]: + Run `mytool specs view 1` for details + Run `mytool specs create --title "..."` to add a spec +``` + +**Rules:** + +- **Default app targets**: by default, support Claude Code, Codex, and OpenCode. Do not hard-code a single agent integration when the tool can reasonably support multiple agents +- **Explicit opt-in**: register hooks or plugins only from a user-invoked setup command, not from ordinary CLI commands +- **Portable commands**: hook commands should use a PATH-verified binary name when it resolves to the current executable, and fall back to the full absolute path otherwise. This keeps global installs portable while ensuring hooks do not accidentally run a different binary +- **Path repair**: setup commands should check existing hooks and update the executable path if it has changed (e.g., after reinstall or relocation) +- **Idempotent**: repeated installs with the same path are silent no-ops +- **Directory-scoped**: show only state relevant to the current working directory +- **Token-budget-aware**: this context loads on _every_ session — ruthlessly minimize it. Include just enough for the agent to orient and act; deep data belongs in explicit invocations +- **Lifecycle capture**: use session-end hooks to capture what happened (transcripts, files touched, specs referenced) so future session-start context gets richer over time + +**How to integrate with each app:** + +- **Claude Code**: use native hooks in `~/.claude/settings.json` or project `.claude/settings.json`. Prefer `SessionStart` to inject compact context via stdout +- **Codex**: use native hooks in `~/.codex/hooks.json` or `/.codex/hooks.json`, and ensure `[features].hooks = true` in `config.toml`. Prefer `SessionStart` for ambient context via stdout +- **OpenCode**: use a managed plugin in `~/.config/opencode/plugins/`. Prefer ambient system-context injection for the home view rather than adding a custom tool + +**Also ship an installable skill (secondary recommendation):** + +The session hook is the primary integration, but it only helps agents whose harness supports hooks, and it loads on _every_ session. +Offer an installable [Agent Skill](https://agentskills.io) as a secondary discovery path. +It loads on demand when the agent recognizes a matching task, carries no per-session token cost, and works in any agent that supports the skill format. +Recommend the hook first (ambient context plus live state) and the skill second (lower overhead, broader agent support) - they are complementary, and a user installs whichever fits, or both. + +```sh +npx skills add / --skill +``` + +- **Single source of truth**: generate `SKILL.md` from the same content your no-args home view prints, so the skill never drifts from the CLI's own guidance. Add a `--check` build step to CI that fails if the committed skill is stale +- **Strip live state**: a skill is static, so omit dynamic data (open sessions, current items) that only the hook can show +- **Non-interactive commands**: rewrite command examples to a form the agent can run without a global install (e.g. `npx -y mytool ...`), since a skill may be installed without the binary on PATH +- **Trigger-shaped frontmatter**: include `name` and a `description` written as a trigger — terse and outcome-focused so the agent loads it on the right intent +- **Document both paths**: in your README, present the hook and the skill as two ways to achieve the same thing, and make clear the user only needs one + +## 8. Content first + +Running your CLI with no arguments should show the most relevant live content — not a usage manual. +When an agent sees actual state it can act immediately. When it sees help text, it has to make a second call. + +``` +$ tasks +tasks[3]{id,title,status}: + 1,Fix auth bug,open + 2,Add pagination,open + 3,Update docs,closed +help[2]: + Run `tasks view ` to see full details + Run `tasks create --title "..."` to add a task +``` + +## 9. Contextual disclosure + +Include **a few next steps** that follow logically from the current output. +The agent discovers your CLI's surface area organically by using it, not by reading a manual upfront. + +Rules: + +- **Relevant**: after an open item → suggest closing; after an empty list → suggest creating; after a list → suggest viewing +- **Actionable**: every suggestion is a complete command (or template) carrying forward any disambiguating flags from the current invocation (e.g., `--repo`, `--source`) +- **Parameterize dynamic values**: when a suggested command needs a runtime value such as an ID, title, branch, URL, or path, use placeholders like `` or `""` instead of guessing a concrete value that may mislead the agent +- **Omit when self-contained**: when the output fully answers the query (a detail view, a count, a confirmation), suggestions are noise — leave them out. Include them on list and mutation responses where the next step isn't obvious. +- **Guide discovery, not workflows**: suggest a variety of possible next actions, don't prescribe a fixed sequence. An agent that already knows what it wants should never be nudged into an extra step. +- **Reveal truncated lists**: when a list shows only the most recent N items out of a larger total, add a help hint telling the agent how to see all of them (e.g., `Run 'mytool list' for all 47 items`). Don't encode pagination into TOON array headers — use help hints instead. +- **Resolve errors**: on errors, suggest the specific command that fixes the problem, not "see `--help`" + +## 10. Consistent way to get help + +The top-level home view should also identify the tool itself before the live data: + +- Include the absolute path of the current executable, with the user's home directory collapsed to `~` +- Include a one-sentence description of what this AXI does + +``` +$ tasks +bin: ~/.local/bin/tasks +description: Manage project tasks in the current workspace +... +``` + +Every subcommand should support `--help` with a concise, complete reference: available flags with defaults, required arguments, and 2-3 usage examples. Keep it focused on the requested subcommand — don't dump the entire CLI's manual. + +### Identify yourself instantly: the `--version` fast path + +`-v`, `-V`, and `--version` must all print the bare version and exit 0. Agents and their harnesses probe `--version` constantly - to confirm a tool is installed, to check whether a fix has shipped, to decide whether to suggest `update`. That makes latency an ergonomics property, not just a perf tweak: a probe that takes 80 ms is 80 ms of every session start, paid before any useful work happens. + +The trap is ESM static imports. If `bin/<tool>.js` statically imports the module that builds the command graph, every dependency in that graph is fully evaluated _before_ the version check runs. One heavy import anywhere in the tree - an SDK, a server framework - is then paid on every `--version`. + +Answer the version before the graph loads: keep the version in a leaf module that imports only node builtins, and defer the real CLI to a dynamic `import()`. + +```js +#!/usr/bin/env node +import { tryFastPath } from "axi-sdk-js/fast-path"; +import { VERSION } from "../src/version.js"; // leaf module - node builtins only + +if (!tryFastPath(process.argv.slice(2), { version: VERSION })) { + const { main } = await import("../src/cli.js"); // heavy graph loads only here + await main(); +} +``` + +`axi-sdk-js/fast-path` is a dedicated subpath export that imports nothing at all, so pulling it in never drags in `runAxiCli` or its dependencies. `tryFastPath` handles only a bare, single-argument version flag and returns `false` for everything else, so all other argv - including version flags in trailing positions - falls through to `runAxiCli`, which stays the single owner of the general case. Its accepted flags and output are identical to the SDK's own version handling, so adopting it changes nothing an agent can observe except the latency. + +Two things keep this honest: + +- The version must come from a **leaf** module. If `VERSION` is defined inside `cli.ts`, importing it re-pulls the whole graph and the fast path buys nothing. +- Guard it with a test that measures the version path against the `node -e "console.log(1)"` floor measured in the same process, rather than an absolute millisecond budget that goes flaky across machines. diff --git a/.agents/skills/firstmate-coding-guidelines/SKILL.md b/.agents/skills/firstmate-coding-guidelines/SKILL.md index fefc6c6906..b7fc02c208 100644 --- a/.agents/skills/firstmate-coding-guidelines/SKILL.md +++ b/.agents/skills/firstmate-coding-guidelines/SKILL.md @@ -63,6 +63,7 @@ Every new skill needs its load trigger declared inline: section 13 for agent-onl State the trigger as a condition ("load before X", "load on Y wake"), never as a vague pointer. The `SKILL.md` frontmatter `description` is a second, independent arrival route - it is the trigger text the harness shows in its skill listing, and the only route left in a deployment where no instruction-surface trigger line is possible - so state a condition there too, for user-invocable and agent-only skills alike. `test_every_skill_declares_a_load_trigger` in `tests/fm-instruction-owners.test.sh` enumerates `.agents/skills/` and enforces the floor: a frontmatter `name` that is not the directory name, an undeclared `user-invocable`, an agent-only skill without exactly one section 13 entry, a section 13 entry naming a skill that no longer exists, or a description that is empty or states no condition each fail it. +Skills installed from upstream and listed in `skills-lock.json` are exempt from the locally authored frontmatter requirements so their recorded content stays untouched, but each still needs exactly one section 13 entry. That check proves a condition is stated, not that it is the right one, so keep writing precise triggers rather than writing to the check. Briefs for tasks that touch firstmate's own tracked material should tell the crewmate to load this skill. `bin/fm-brief.sh`'s `REPO` argument is a caller-supplied string with no reliable signal that it names firstmate's own repo, unlike a project registered in `data/projects.md`, so there is no clean point inside the scaffold to detect this case automatically. diff --git a/AGENTS.md b/AGENTS.md index a463d92b8c..f362d56ae4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,7 +51,7 @@ Verify credentials only through their effect, such as a successful authenticated Load `secrets-handling` for safe mechanics, dangerous-command alternatives, and exposure response. You may maintain this repo's private operational state directly. -Shared tracked material is `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.codex/`, `.github/workflows/`, `bin/`, `roles/`, `.agents/skills/`, and public `skills/`. +Shared tracked material is `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.codex/`, `.github/workflows/`, `bin/`, `roles/`, `.agents/skills/`, `skills-lock.json`, and public `skills/`. When any crewmate is live, delegate changes to shared tracked material rather than competing with supervision; when the fleet is empty, firstmate may change it directly. This repo is a shared template, while `.env`, `data/`, `state/`, `config/`, `projects/`, `.local/axi/`, `.no-mistakes/`, and `graphify-out/` are captain-private and gitignored. Ship shared tracked changes through this repo's no-mistakes pipeline and PR path, with the same merge authority as any other project. @@ -509,6 +509,8 @@ These skills are not captain-invocable; load them only at their precise triggers - `fmx-respond` - load on an `x-mention <request_id>` `check:` wake to handle the mention, on an `x-mode-error ...` `check:` wake to report the X-mode configuration blocker, and on any milestone or terminal wake for an X-mode-linked task before posting its completion follow-up; relevant only when X mode is on. - `firstmate-codexapp` - load before coordinating a visible Codex Desktop thread, evaluating a Codex App backend request, or reconciling Codex Desktop host-tool smoke evidence for Firstmate work. - `firstmate-coding-guidelines` - load before changing firstmate's shared, tracked material, as defined by section 1's list, whether editing directly or briefing a crewmate for a firstmate-repo task. +- `axi` - load before building, modifying, or reviewing any agent-facing CLI; it is the official AXI skill, installed verbatim from upstream and never edited here, and `docs/axi-skill-provenance.md` carries its licence notice and update route. +- `axi-tool-intake` - load before filing, scoping, or briefing work that would build, adopt, derive, or extend an agent-ergonomic CLI for this fleet, and before telling the captain that none exists for a domain; it carries only what the AXI specification does not, and `axi` remains the sole owner of the design contract. ## 14. X mode diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d202a035b0..5d53340040 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,8 +35,9 @@ See the [no-mistakes quick start](https://kunchenguid.github.io/no-mistakes/star - This repo is a template for running a firstmate orchestrator agent. `AGENTS.md` is the agent's main job description and names when to load bundled firstmate skills; `CLAUDE.md` is a symlink to it, and `.claude/skills` is a symlink to `.agents/skills`. -- Only shared material is tracked: `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.codex/`, `.github/workflows/`, `bin/`, `roles/`, `.agents/skills/`, and `skills/`. - `.agents/skills/` holds agent-loaded skills that assume a live firstmate home and carry `metadata.internal: true` so installers such as [skills.sh](https://skills.sh) hide them from discovery; `skills/` holds standalone, installer-facing public skills with no firstmate dependency (see the README's "Two-tier skill layout"). +- Only shared material is tracked: `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.codex/`, `.github/workflows/`, `bin/`, `roles/`, `.agents/skills/`, `skills-lock.json`, and `skills/`. + `.agents/skills/` holds agent-loaded skills; skills authored here assume a live firstmate home and carry `metadata.internal: true` so installers such as [skills.sh](https://skills.sh) hide them from discovery, while `skills-lock.json` identifies upstream-installed exceptions whose frontmatter stays untouched. + `skills/` holds standalone, installer-facing public skills with no firstmate dependency (see the README's "Two-tier skill layout"). `roles/` holds per-role instruction overlays; a home's gitignored `config/role` selects which one amends `AGENTS.md` there, with the contract owned by [`docs/configuration.md`](docs/configuration.md) ("Vessel role"). Everything personal to one captain's fleet (`.env`, `data/`, `state/`, `config/`, `projects/`, `.no-mistakes/`, `graphify-out/`) is gitignored; never commit it. The root `.tasks.toml` is tracked `tasks-axi` config for `data/backlog.md`; compatible `tasks-axi` is the default backend for routine backlog mutations, with the compatibility definition owned by [`docs/configuration.md`](docs/configuration.md) ("Backlog backend"). @@ -54,10 +55,12 @@ See the [no-mistakes quick start](https://kunchenguid.github.io/no-mistakes/star - In Markdown, put each full sentence on its own line. - `README.md` stays a concise overview plus pointers: it never carries a wall of inline detail. Route detail to the most specific `docs/` file (architecture, configuration, or a backend guide) and link to it instead. +- A skill listed in `skills-lock.json` was installed from upstream, not written here, and is never edited - not to add frontmatter fields this repo invented, and not to normalise its punctuation. + Editing it diverges it from the hash its installer recorded and turns the next update into a conflict; update it by re-running the installer and re-recording its provenance page in the same commit. ## Development -Tracked changes to firstmate itself - `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.github/workflows/`, `bin/`, `roles/`, `.agents/skills/`, and `skills/` - ship through the `no-mistakes` pipeline on a feature branch and require an explicit merge approval. +Tracked changes to firstmate itself - `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.github/workflows/`, `bin/`, `roles/`, `.agents/skills/`, `skills-lock.json`, and `skills/` - ship through the `no-mistakes` pipeline on a feature branch and require an explicit merge approval. Before making any such change, load the agent-only `firstmate-coding-guidelines` skill (`.agents/skills/firstmate-coding-guidelines/SKILL.md`). It has the knowledge-placement rules that keep `AGENTS.md` from regrowing after each diet pass. There is no reliable way for `bin/fm-brief.sh`'s scaffold to detect that a task's repo is firstmate itself, so firstmate adds this skill's load line to firstmate-repo briefs by hand. diff --git a/README.md b/README.md index dcd423fe96..3ea4cabee2 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,9 @@ Agent-only reference skills live under `.agents/skills/` and are loaded by first Firstmate's skills live in two separate places with different audiences: -- `.agents/skills/` - agent-loaded skills (this section's table, plus firstmate's agent-only reference skills). Every one of these assumes a live firstmate home and is meaningless, or actively misleading, installed anywhere else, so each carries `metadata.internal: true` in its frontmatter. That flag hides them from installer discovery (tools like the [skills.sh](https://skills.sh) `npx skills add` installer) without affecting how firstmate itself loads them - frontmatter metadata is inert to the agent's own skill loader. +- `.agents/skills/` - agent-loaded skills (this section's table, plus firstmate's agent-only reference skills). Every skill written here assumes a live firstmate home and is meaningless, or actively misleading, installed anywhere else, so each carries `metadata.internal: true` in its frontmatter. That flag hides them from installer discovery (tools like the [skills.sh](https://skills.sh) `npx skills add` installer) without affecting how firstmate itself loads them - frontmatter metadata is inert to the agent's own skill loader. + The exception is a skill *installed* here from upstream rather than written here, which carries upstream's frontmatter unchanged and is generally useful outside a firstmate home. + `skills-lock.json` records which ones those are, and they are never edited to match the local convention - see [`docs/axi-skill-provenance.md`](docs/axi-skill-provenance.md). - `skills/` - public, installer-facing skills meant to be installed standalone into any project, independent of firstmate. Each one is a self-contained skill with no dependency on firstmate's paths, tools, or vocabulary. Today that is `skills/stow`, a generic session-knowledge-sweep skill that routes findings by explicit instruction first, then existing local conventions, then a private `.stow-notes.md` fallback in the current directory, and closes with a resume pointer for the next session. @@ -260,6 +262,13 @@ Third-party material, all from [`mattpocock/skills`](https://github.com/mattpoco - `/to-backlog` and `bin/fm-to-backlog.sh` adopt the to-tickets skill. [`docs/to-backlog-provenance.md`](docs/to-backlog-provenance.md) carries the same notice and licence text, along with what was kept, changed, and dropped with the cost of each omission. - `/domain-modeling` adopts the domain-modeling skill. [`docs/domain-modeling-provenance.md`](docs/domain-modeling-provenance.md) carries the same notice and licence text, along with what was kept, changed, and dropped with the cost of each omission. +Third-party material from a different upstream, on different terms: + +- `.agents/skills/axi/` is the official AXI skill by Kun Chen, from [`kunchenguid/axi`](https://github.com/kunchenguid/axi), used under the MIT licence. + It is installed verbatim through `npx skills add` rather than adapted, so it is never edited here and this repository's own Markdown conventions do not apply to it. + [`docs/axi-skill-provenance.md`](docs/axi-skill-provenance.md) carries that copyright notice and licence text, records the installed commit and content hash, and states why the file stays untouched; `.agents/skills/axi/LICENSE` carries the notice beside the copy itself. + This fleet's own additions, which the AXI specification does not cover, live separately in `.agents/skills/axi-tool-intake/` and restate none of it. + `/codebase-sweep` is not on that list: it loads the `codebase-design` plugin skill and copies nothing from it, and its sweep subjects are traced to a talk by the same author rather than derived from his code. [`docs/codebase-sweep-provenance.md`](docs/codebase-sweep-provenance.md) records what came from that talk, what did not, and the measurement behind the difference. Operational provenance records for installed local tools and Codex-managed bundles that are not vendored into this repository live in [`docs/provenance-metadata-followups.md`](docs/provenance-metadata-followups.md). diff --git a/docs/axi-skill-provenance.md b/docs/axi-skill-provenance.md new file mode 100644 index 0000000000..ba51604a97 --- /dev/null +++ b/docs/axi-skill-provenance.md @@ -0,0 +1,79 @@ +# AXI skill provenance: installed from `kunchenguid/axi` + +`.agents/skills/axi/SKILL.md` is the **official AXI skill**, by **Kun Chen**, from **[`kunchenguid/axi`](https://github.com/kunchenguid/axi)**, under the **MIT licence**. +It is not an internal invention and it is not an adaptation: it is a verbatim copy. + +That upstream is the same one firstmate itself comes from - `CONTRIBUTING.md` names `kunchenguid/firstmate` as this repository's parent - so this is one ecosystem rather than an outside dependency. +It is still handled as third-party material below, because sharing an author grants no licence relief and an unattributed copy is unattributed either way. + +MIT permits exactly what was done here - use and redistribute - and asks one thing in return: the copyright notice and the permission notice travel with the work. +`.agents/skills/axi/LICENSE` carries them alongside the copy, and this page carries them again below, and both land in the same commit as the skill, so the notice is never absent from a tree that contains the work. +That ordering is deliberate: an earlier adoption from a different upstream shipped without attribution and had to be corrected afterwards (`docs/sea-chart-provenance.md`). + +## Why it is here at all + +This fleet runs six AXI tools and twice in one day planned to build one that already existed. +The remedy that was first proposed was a fleet-written skill describing the AXI contract. +That was the wrong remedy: an official skill already exists, it is 273 lines working all ten principles with examples, and `principles.yaml` upstream states outright that the full specification of each principle lives in it. +A second description of the same contract would have been a copy that drifts. + +So the contract is installed rather than restated, and this fleet's own additions - which the specification does not cover - live separately in `.agents/skills/axi-tool-intake/`. +That skill carries no design guidance by construction, and `tests/fm-axi-tool-intake.test.sh` enforces the separation in both directions. + +## What was installed + +| | | +| --- | --- | +| Source | `kunchenguid/axi`, skill path `.agents/skills/axi/SKILL.md` | +| Install command | `npx skills add kunchenguid/axi`, run 2026-08-19 | +| Upstream commit read | `408a653` (2026-08-16), the repository head at fetch time on 2026-08-19 | +| Licence | MIT, `LICENSE` at that repository's root, copied to `.agents/skills/axi/LICENSE` | +| Content | Byte-identical to upstream; `sha256` `59f62cd5c6eff01516cb3fcd6b1fce1e097f92df4eee2f444183b5039d20aef2` | +| Manifest | `skills-lock.json` at this repository's root, written by the installer | + +The installer wrote `.agents/skills/axi/SKILL.md` and `skills-lock.json`, and it resolved the Claude Code symlink against this repository's existing tracked `.claude/skills -> ../.agents/skills` link, so it created no third copy. + +An independent shallow clone of the upstream repository, read into a scratch location outside this repository, produced a SKILL.md byte-identical to the installed one. +That is what establishes the commit above as the content's origin: the installer records a source and a content hash but not a commit. + +## Do not edit the installed file + +`.agents/skills/axi/SKILL.md` is upstream's, and this repository's rules do not apply to it. + +It uses em dashes, which this repository forbids in its own tracked Markdown, and its frontmatter carries only `name` and `description`, without the `user-invocable` and `metadata.internal` fields every skill this repository authors must declare. +**Both are correct and neither is to be fixed.** +Editing the file diverges it from `skills-lock.json`'s recorded hash, which is how the installer detects a local modification, and turns every future `npx skills` update into a conflict. + +Two consequences follow, and both are enforced by `tests/fm-axi-tool-intake.test.sh`: + +- The frontmatter trigger floor in `tests/fm-instruction-owners.test.sh` skips skills listed in `skills-lock.json`, because they cannot declare fields this repository invented. It still requires each of them to be reachable from `AGENTS.md` section 13, so an installed skill can never become dead weight. +- The recorded `sha256` above is asserted against the file on disk. A well-meaning local edit is otherwise invisible: the skill still loads and still works. + +## Updating it + +Update through the installer, never by hand: re-run `npx skills add kunchenguid/axi`, then re-record the commit, the hash, and the date in the table above in the same commit. +A provenance record that is not re-taken at update time is a record of a version that is no longer installed. + +## Notice + + MIT License + + Copyright (c) 2026 Kun Chen + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000000..2f235a5dba --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "skills": { + "axi": { + "source": "kunchenguid/axi", + "sourceType": "github", + "skillPath": ".agents/skills/axi/SKILL.md", + "computedHash": "7de23a6b8171a06b7885712f3dd971e64d9301543d1b8c46f21660494303df95" + } + } +} diff --git a/tests/fm-axi-tool-intake.test.sh b/tests/fm-axi-tool-intake.test.sh new file mode 100755 index 0000000000..40aee177ab --- /dev/null +++ b/tests/fm-axi-tool-intake.test.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# Contract tests for the installed `axi` skill, its provenance record, and the +# `axi-tool-intake` overlay that carries this fleet's own additions. +# +# The installed file hash and MIT notices are owned byte contracts. +# The overlay's forbidden design terms are its non-competition contract. +# Its four numbered headings and external identifiers are its structural and +# actionable-reference contracts, and the section 13 entries are the delivered +# instruction surface's reachability contract. +# shellcheck disable=SC2016 +set -u + +# shellcheck source=tests/lib.sh +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +AXI="$ROOT/.agents/skills/axi/SKILL.md" +AXI_LICENSE="$ROOT/.agents/skills/axi/LICENSE" +INTAKE="$ROOT/.agents/skills/axi-tool-intake/SKILL.md" +PROV="$ROOT/docs/axi-skill-provenance.md" +LOCK="$ROOT/skills-lock.json" +AGENTS="$ROOT/AGENTS.md" + +# --- the installed skill ---------------------------------------------------- + +# The remedy first proposed for this whole problem was a fleet-written skill +# describing the AXI contract. An official one already existed. If a later edit +# reintroduces a second description, the copy drifts the first time upstream +# moves, which is the defect the installation exists to avoid. +test_the_contract_is_installed_rather_than_restated() { + local source skill_path + assert_present "$AXI" "the official axi skill is not installed" + assert_present "$LOCK" "skills-lock.json is missing; the install is unrecorded" + if command -v jq >/dev/null 2>&1; then + source=$(jq -er '.skills.axi.source | strings' "$LOCK") \ + || fail "the manifest must decode the axi skill's upstream source" + skill_path=$(jq -er '.skills.axi.skillPath | strings' "$LOCK") \ + || fail "the manifest must decode where the axi skill was installed" + elif command -v python3 >/dev/null 2>&1; then + source=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1], encoding="utf-8"))["skills"]["axi"]["source"])' "$LOCK") \ + || fail "the manifest must decode the axi skill's upstream source" + skill_path=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1], encoding="utf-8"))["skills"]["axi"]["skillPath"])' "$LOCK") \ + || fail "the manifest must decode where the axi skill was installed" + else + fail "this contract test requires jq or python3 to decode $LOCK" + fi + [ "$source" = "kunchenguid/axi" ] \ + || fail "the decoded axi source must be kunchenguid/axi, found $source" + [ "$skill_path" = ".agents/skills/axi/SKILL.md" ] \ + || fail "the decoded axi skillPath is wrong: $skill_path" + fm_installed_skill_dirs | grep -qx axi \ + || fail "axi must be resolvable as an installed skill from skills-lock.json" + pass "the AXI contract is installed from upstream, not restated here" +} + +# The installed file is upstream's. It carries em dashes this repository forbids +# and frontmatter fields it does not declare, and both are correct. An edit that +# tidies either one diverges the file from the hash the installer recorded and +# turns the next update into a conflict - and nothing at load time would ever +# reveal it, because the skill still loads and still works. +test_the_installed_file_is_unmodified() { + local recorded actual + recorded=$(grep -o '`sha256` `[0-9a-f]\{64\}`' "$PROV" | grep -o '[0-9a-f]\{64\}') + [ -n "$recorded" ] || fail "the provenance record must carry the installed file's sha256" + actual=$(sha256sum "$AXI" | awk '{print $1}') + [ "$recorded" = "$actual" ] \ + || fail "the installed axi skill has been edited locally: recorded $recorded, on disk $actual" + pass "the installed axi skill is byte-for-byte what was installed" +} + +# MIT asks one thing in return, and a dropped notice is the one licence defect +# that never surfaces at runtime. This fleet has already had to correct it once +# after the fact, which is why the notice is asserted rather than trusted. +test_the_licence_notice_travels_with_the_copy() { + assert_present "$AXI_LICENSE" "the MIT licence must sit beside the installed copy" + assert_grep "Copyright (c) 2026 Kun Chen" "$AXI_LICENSE" \ + "the licence beside the copy must carry the upstream copyright line" + assert_grep "Copyright (c) 2026 Kun Chen" "$PROV" \ + "the provenance record must carry the upstream copyright line" + assert_grep "shall be included in all" "$PROV" \ + "the provenance record must carry the MIT permission notice, not just the copyright line" + assert_grep "MIT" "$PROV" "the provenance record must name the licence" + pass "the MIT notice travels with the installed copy" +} + +# --- the separation --------------------------------------------------------- + +# The overlay's entire justification is that it does not compete. The moment it +# starts explaining a principle, there are two owners for one contract and the +# fleet is back where it started, with the added cost of a file that looks +# authoritative. +test_the_overlay_carries_no_design_guidance() { + assert_present "$INTAKE" "the axi-tool-intake skill is missing" + # The principles' own names are the shape a restatement arrives in. + local term + for term in "TOON" "exit code" "stdout" "truncat" "empty state" "aggregate"; do + assert_no_grep "$term" "$INTAKE" \ + "the overlay restates '$term', which belongs to the installed axi skill alone" + done + pass "the overlay carries no design guidance" +} + +# --- the four fleet additions ----------------------------------------------- + +# The whole reason this work was ordered: twice in one day this fleet decided to +# build without looking. A check that reads as an appendix is the same failure +# with better documentation, so its position is part of the contract. +test_the_overlay_has_four_ordered_fleet_contract_sections() { + local -a headings expected + mapfile -t headings < <(grep '^## [0-9]\+\.' "$INTAKE") + expected=( + "## 1. Check both indexes before building, and state which you checked" + "## 2. Credentials: this fleet's rule, not the specification's" + "## 3. Deriving from an existing tool" + "## 4. How a finished tool reaches every seat - OPEN" + ) + [ "${#headings[@]}" -eq 4 ] \ + || fail "the overlay must have exactly four top-level numbered sections" + [ "$(printf '%s\n' "${headings[@]}")" = "$(printf '%s\n' "${expected[@]}")" ] \ + || fail "the overlay's four numbered sections are missing or out of order" + pass "the overlay has exactly four ordered fleet contract sections" +} + +test_the_overlay_retains_actionable_external_identifiers() { + assert_grep "catalog.yaml" "$INTAKE" \ + "the overlay must identify the AXI catalogue source" + assert_grep "npm" "$INTAKE" \ + "the overlay must identify the package registry source" + assert_grep "fleet-forgejo-axi" "$INTAKE" \ + "the overlay must name the record tracking the ownership question" + assert_grep "fm-axi-nomistakes-guidance-off-argv" "$INTAKE" \ + "the overlay must name the second record asking the same ownership question" + pass "the overlay retains its actionable external identifiers" +} + +# --- reachability ----------------------------------------------------------- + +test_both_skills_are_reachable_from_the_instruction_surface() { + local name count + for name in axi axi-tool-intake; do + count=$(grep -Fc -- "- \`$name\` - " "$AGENTS") + [ "$count" -eq 1 ] \ + || fail "$name must have exactly one AGENTS.md section 13 trigger, found $count" + done + pass "both skills are reachable from the instruction surface" +} + +test_the_contract_is_installed_rather_than_restated +test_the_installed_file_is_unmodified +test_the_licence_notice_travels_with_the_copy +test_the_overlay_carries_no_design_guidance +test_the_overlay_has_four_ordered_fleet_contract_sections +test_the_overlay_retains_actionable_external_identifiers +test_both_skills_are_reachable_from_the_instruction_surface diff --git a/tests/fm-instruction-owners.test.sh b/tests/fm-instruction-owners.test.sh index f0c39f701a..afb729d443 100755 --- a/tests/fm-instruction-owners.test.sh +++ b/tests/fm-instruction-owners.test.sh @@ -386,17 +386,36 @@ test_compressed_agents_retains_authority_and_supervision_safety() { # firstmate reading its own instruction surface. The SKILL.md description is the # route for the harness skill listing, and it is the ONLY route where no # instruction-surface trigger line is possible, so it is required of every skill -# rather than only the harness-listed ones. +# this repository AUTHORS. +# +# A skill INSTALLED from upstream is exempt from the frontmatter half and held +# to the AGENTS.md half alone: its file is verbatim and editing it to satisfy a +# convention this repository invented would diverge it from the installer's +# recorded hash. The installed set is read from skills-lock.json rather than +# listed here, for the same staleness reason the loop enumerates the directory. test_every_skill_declares_a_load_trigger() { - local dir name invocable count section desc + local dir name invocable count section desc installed local missing="" toneless="" dangling="" section=$(awk '/^## 13\. /{f=1} f && /^## 14\. /{exit} f' "$AGENTS") [ -n "$section" ] || fail "AGENTS.md section 13 is missing or unparseable" + installed=" $(fm_installed_skill_dirs | tr '\n' ' ')" for dir in "$ROOT"/.agents/skills/*/; do name=$(basename "$dir") [ -f "$dir/SKILL.md" ] || fail "skill $name has no SKILL.md" fm_skill_frontmatter "$dir" | grep -qx "name: $name" \ || fail "skill $name declares a metadata name that is not its directory" + # A skill installed from upstream is held to reachability only. It cannot + # declare frontmatter fields this repository invented, and editing it to add + # them would diverge it from the hash the installer recorded, turning every + # future update into a conflict (docs/axi-skill-provenance.md). Its + # description is upstream's wording and is not ours to shape either. + case "$installed" in + *" $name "*) + count=$(printf '%s\n' "$section" | grep -Fc -- "- \`$name\` - ") + [ "$count" -eq 1 ] || missing="$missing $name(section-13-entries=$count)" + continue + ;; + esac invocable=$(fm_skill_frontmatter "$dir" | grep -m1 '^user-invocable:' | awk '{print $2}') case "$invocable" in false) diff --git a/tests/lib.sh b/tests/lib.sh index 7176fd6644..81c288ea6d 100644 --- a/tests/lib.sh +++ b/tests/lib.sh @@ -508,6 +508,50 @@ assert_present() { # fm_skill_frontmatter <skill-dir>: print the YAML frontmatter block only, from # the opening `---` on line 1 to the next `---`, so a column-0 key anywhere in # the SKILL.md body can never satisfy a frontmatter probe. +# fm_installed_skill_dirs: print the directory name of every skill this +# repository INSTALLED from upstream rather than authored, one per line, read +# from the installer's own manifest. Derived from skills-lock.json on purpose: +# a hand-maintained list of installed skills goes stale the moment someone runs +# the installer again, which is the same silent-staleness failure the checks +# that use it exist to catch. Prints nothing when no manifest exists, but an +# unreadable or invalid manifest is an error rather than an empty installed set. +fm_installed_skill_dirs() { + local lock="$ROOT/skills-lock.json" + [ -f "$lock" ] || return 0 + if command -v jq >/dev/null 2>&1; then + jq -r ' + .skills | + if type != "object" then error("skills must be an object") else . end | + to_entries[] | + if (.value | type) != "object" then + error("skills." + .key + " must be an object") + elif .value.skillPath == (".agents/skills/" + .key + "/SKILL.md") then + .key + else + empty + end + ' "$lock" || fail "could not decode installed skills from $lock" + elif command -v python3 >/dev/null 2>&1; then + python3 - "$lock" <<'PY' || fail "could not decode installed skills from $lock" +import json +import sys + +with open(sys.argv[1], encoding="utf-8") as handle: + manifest = json.load(handle) +skills = manifest.get("skills") +if not isinstance(skills, dict): + raise ValueError("skills must be an object") +for name, record in skills.items(): + if not isinstance(record, dict): + raise ValueError(f"skills.{name} must be an object") + if record.get("skillPath") == f".agents/skills/{name}/SKILL.md": + print(name) +PY + else + fail "fm_installed_skill_dirs requires jq or python3 to decode $lock" + fi +} + fm_skill_frontmatter() { awk ' NR == 1 { if ($0 !~ /^---[[:space:]]*$/) exit; next }