@a2f0/agent-tool 0.1.49 → 0.1.51
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/package.json +1 -1
- package/skills/explicit-state-machines/SKILL.md +186 -0
- package/skills/explicit-state-machines/references/lessons.md +36 -0
- package/skills/explicit-state-machines/references/patterns.md +224 -0
- package/src/cli.ts +4 -1
- package/src/permissions/catalog.ts +1 -2
- package/src/pr/squashMerge.ts +1 -1
- package/src/review/reviewPrompt.ts +1 -1
- package/src/runAgentToolAction.ts +8 -15
- package/src/skills/bundled.ts +9 -1
- package/src/skills/install.ts +17 -10
- package/src/stateMachines/command.ts +226 -0
- package/src/stateMachines/scan.ts +306 -0
- package/src/workflows/pr.ts +9 -10
package/src/skills/bundled.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Generated by scripts/build/bundle-skills.ts; edit skills/*/SKILL.md.
|
|
1
|
+
// Generated by scripts/build/bundle-skills.ts; edit skills/*/SKILL.md and skills/*/references/*.md.
|
|
2
2
|
export const BUNDLED_SKILLS: Readonly<Record<string, string>> = {
|
|
3
3
|
"cross-agent-review": "---\nname: cross-agent-review\ndescription: Review committed branch changes with an independent Claude Code, Codex, or OpenCode reviewer, and repair actionable findings when requested.\n---\n\n# Cross-Agent Review\n\nUse the installed `agent-tool` executable. Read the repository's development and\nreview policy and `agent-tool.json` when present. Keep project validation commands\nand release rules in the repository; do not assume Bun, a workspace layout, or\nparticular CI job names.\n\nSelect an independent reviewer using a different model/provider from the author\nwhen possible, not merely a different CLI: Hermes and OpenCode can use the same\nmodel as another harness. Hermes can coordinate this skill but is not a `review`\nadapter. Honor a requested reviewer or effort; otherwise use an available\nindependent reviewer.\nRun `agent-tool doctor` if CLI compatibility is uncertain.\n\nRun noisy validation or repair checks through\n`agent-tool run --label <check> -- <command> [args...]`. Read its bounded failure\ntail and use `agent-tool logs <log>` or a targeted search for more evidence.\nKeep `agent-tool review` direct: it retains reviewer stderr in a private local\nlog and displays the complete final review. Never hide, truncate, or substitute\na command success receipt for findings and the final verdict.\n\nReview committed changes with `agent-tool review claude`, `agent-tool review codex`,\nor `agent-tool review opencode`. Optional effort is `low`, `medium`, `high`,\n`xhigh`, or `max`. For a local or non-GitHub repository, add `--base <commit-or-ref>`.\nFor a coordinated GitHub review, pin the fetched base using\n`AGENT_TOOL_REVIEW_BASE_REF`, the base branch name such as `main` (not a\nremote-tracking ref like `origin/main`), and `AGENT_TOOL_REVIEW_BASE_OID`, its\nfetched commit. Fetch from the repository that owns the PR, which can differ\nfrom the checkout's origin.\n\nUse `agent-tool repo context --fetch` to resolve and fetch the PR's own base,\nthen record the exact base and HEAD before reviewing. The tool reviews raw committed\nfiles and excludes worktree edits. A zero exit means a complete review was\nreturned; it does not mean the findings are non-blocking. Read the final\n`VERDICT: BLOCKER|MAJOR|MINOR|SUGGESTION|CLEAN` and the findings. A reviewer\nthat labels findings `[P0]` through `[P3]` uses the same scale: `[P0]` and\n`[P1]` are BLOCKER and MAJOR, `[P2]` and `[P3]` are MINOR and SUGGESTION.\n\nIf a CLI is unavailable, out of credits, or returns an unusable review, try another\navailable reviewer and disclose the fallback. Do not silently convert a failed\nreview into CLEAN or call an in-session review independent.\n\nFor report-only requests, report findings without changing files or history.\nWhen repairs are authorized, address actionable BLOCKER and MAJOR findings,\nvalidate with the repository's relevant checks, commit the repairs, and review\nthe new HEAD. Continue while meaningful progress is possible; report an impasse\nwhen progress requires a material user decision. A repaired commit is unreviewed\nuntil the next review completes. Preserve unrelated work.\n\nBefore reporting success, confirm HEAD still matches the reviewed commit and\nthat an existing PR points to that same commit. If the base moved, refresh it and\nreview again before shipping. Report reviewer, exact base and head IDs, verdict,\nremaining findings, validation, and any repairs performed. This skill does not\nauthorize pushing, opening a PR, or merging unless the user requested that work.\n",
|
|
4
|
+
"explicit-state-machines": "---\nname: explicit-state-machines\ndescription: Find implicit state machines in a repository, or across many repositories, and refactor them into explicit ones (a closed set of states, typed events, one pure transition function) without changing behavior. Use when the user asks to \"find all of the implicit state machines\", to replace tangled boolean flags or scattered status writes, or to dogfood this pass across repositories; not for adding a state-machine library or redesigning features.\n---\n\n# Explicit State Machines\n\nAn implicit state machine is code that moves through modes (idle, loading,\nfailed, done) without naming them: several booleans that are only valid in\nsome combinations, a status string written from many places, nullable fields\nwhose presence means \"we are in phase X\", or effects that chain off each other\nto advance a process. Bugs live in the combinations nobody named:\n`isLoading && error`, a retry that fires after `done`, a dialog both open and\nsubmitting twice.\n\nAn explicit machine names every state once, carries each state's data only in\nthat state, and changes state only through one function of `(state, event)`.\nThe pass finds the implicit ones, converts those where the conversion removes\nimpossible states or scattered transitions, and proves behavior did not change.\n\n## Find candidates\n\n`agent-tool state-machines` ranks tracked source files by two signals:\nlifecycle booleans toggled together (`isLoading`, `hasError`,\n`_running`, `busy.current`, `this.#rendered`), and status-like fields assigned\nthree or more string values from three or more places (`step.status = ...`).\nIt reads extensionless scripts by their shebang and is read-only.\n\n- `agent-tool state-machines [path...] [--limit N] [--min-score N] [--json]`\n scans the current repository; each path names the repository holding it.\n If the repository's pinned agent-tool lacks the command, run\n `bunx --bun @a2f0/agent-tool@latest state-machines`.\n- `--limit N` (default 25 per repository, 0 for all), `--min-score N`\n (default 3), `--json` for agents.\n\nThe script finds leads, not verdicts, and it misses whole families. Also read\nfor these by hand, starting from the places state lives (stores, providers,\nhooks, workers, sync loops, connection handlers, upload and checkout flows,\nwizards, CLI subcommands that resume):\n\n- `loading`/`error`/`data` held as separate fields, or a nullable `error`\n beside a non-null `data`; any nullable value paired with a boolean or a\n second nullable (`nowPlaying` with `interruptedNoise`, `reservation` with\n `editing`). The scanner sees booleans only.\n- State split between a context and per-component state that an effect\n re-syncs, and \"already handled\" dedupe fields that quietly mean \"confirmed\n started\".\n- Fields that are set together and cleared together (`startedAt`, `jobId`,\n `abortController`) and so encode \"running\".\n- `useEffect` chains where one effect sets state that triggers the next.\n- Retry, polling, and reconnect loops guarded by counters and flags.\n- Status columns or enums whose transitions are enforced nowhere (any writer\n may set any value), in the database or in API handlers.\n- Shell, Python, or Expect scripts that track progress with several\n variables.\n\n## Decide what to convert\n\nConvert a candidate only when at least one holds:\n\n1. Two or more of its variables can hold a combination that is meaningless\n (`isLoading && isError`, `data` set while `status === \"failed\"`).\n2. Its transitions are written in several places, so no single place says\n which moves are legal.\n3. A bug or missing guard follows from either (an event handled in a state\n where it must be ignored). Fix the bug in the same change, with a test that\n fails before it, and say so in the PR.\n\nLeave it alone when the flags are independent (a theme toggle and a sidebar\ntoggle are a product of two machines, not one), when there is a single\nboolean, when a status is only read (an API response's field mirrored into\nUI), when it already is a discriminated union with one writer, or when the\nmodule is about to be deleted. Also leave alone:\n\n- Status columns whose writes all live in one module, each a conditional\n update naming the states it may leave (`UPDATE ... WHERE status = $from`),\n so that module reads as the transition table. Where writers are spread out\n or name no from-state, add one `ALLOWED` table and check it in the update\n instead; a value `CHECK` alone does not enforce transitions. A database\n transition trigger only restates a guarded module and breaks fixtures that\n simulate races by setting states.\n- Monotone flags with one writer each that move one way and are read only\n by cleanup (lock held, applying, committed), and append-only accumulators.\n- Static records checked by one validator, and before/after comparisons that\n validate rather than track progress.\n- Effect and promise guards (`cancelled`, `mounted`) on their own.\n- One request in flight plus one \"run again\" flag, both written from one\n function (`pending` with `started`): three reachable phases, one writer.\n- A ref mirroring a state to block a double click before re-render\n (`isSubmitting` with `isSubmittingRef`), written together. The missing\n mirror is the bug: a `busy` guard read from a click handler's render lets\n two clicks start the work twice.\n- Flags in different components (a parent's `editing`, and `busy` in a child\n that only exists while editing): the mount already excludes the bad pair.\n\nRecord each one you leave and why, so the next pass does not reread it.\n\n## Convert\n\nFollow `references/patterns.md` for the language at hand. In every language:\n\n1. List the states and the data each one carries; list the events. Write the\n transition table before the code, and keep it in the PR body.\n2. Model states as a closed type: a TypeScript discriminated union on a\n `status` (or the repository's existing tag name), a Swift enum with\n associated values, a Kotlin sealed interface, a Python `Enum` or tagged\n dataclasses, a Go typed constant with a transition function.\n3. Write one pure `transition(state, event)` (a reducer). Make the switch\n exhaustive (`never` check, `assertNever`, `@unique` enums, sealed `when`).\n Say explicitly what an event does in a state where it does not apply:\n ignore it (return the same state) or throw in development; never fall\n through silently.\n4. Keep every guard the old code had. A `cancelled` flag or an unmount check\n that drops a stale response must become a request tag carried by the\n state and its events (only the current request may finish), never a bare\n \"is it loading?\" check, which accepts `start(A) -> reset -> start(B) ->\n succeed(A)`. Test that sequence. A cleaned-up effect's callbacks (its own\n abort's rejection included) must not dispatch either; see `patterns.md`.\n5. Move side effects out of the transition: effects run on entering a state\n (a worker loop that switches on it, or an effect keyed on the state's whole\n identity: `status` plus its request tag), not inside the reducer. An\n effect keyed on `status` alone misses `loading(A) -> loading(B)` and never\n starts B; test a replacement request.\n6. Keep call sites small: derive old booleans from the state\n (`const isLoading = state.status === \"loading\"`) where many readers use\n them, rather than rewriting every consumer in the same change.\n7. Derive old booleans so they stay stable across intermediate states: an\n effect keyed on `editing` must not refire when a replacement passes\n through `reserving`. Keep effects tied to an event, not a state (resuming\n audio on a press), in the handler.\n8. Use no new dependency. Use a state-machine library (XState, Robot) only if\n the repository already depends on it.\n9. Test the transition table directly: every legal transition, a sample of\n ignored events per state, and the impossible combinations that the old\n code allowed. Keep the existing behavior tests passing unchanged; a test\n that must change shows a behavior change, which belongs in its own PR.\n Run the new behavior tests against the old file (`git show <base>:<file>`\n into a copy kept aside) to prove the refactor changed nothing and that a\n bug's regression test fails before its fix. For output that is printed or\n saved as JSON, assert key order too.\n10. Check every new guard against existing listeners: a guard that only acts\n in `ready` breaks a listener reacting to an event fired mid-render.\n11. Treat \"initial\" props (`initialEditing`) as one-time requests, on mount or\n when they turn true, not as levels that re-apply whenever another input\n changes. Fold a mirrored prop the machine depends on (`canWrite`) into the\n state, synced by one effect declared before the request effect.\n\n## Ship\n\n- Work on a `refactor/<topic>` branch (`fix/<topic>` when the change fixes a\n bug) and ship with the repository's `ship-pr` skill: its validation, its\n independent review, its merge policy.\n- Check file-count or naming budgets (ls-lint) before adding a machine module\n and its test; a full directory may need the tests in an existing file.\n- One PR per repository per pass, unless the diff passes about 600 lines or\n spans unrelated areas; then one PR per area, each independently valid. In\n large repositories plan one PR per machine: formatted transition tables\n and their tests grow fast.\n- The PR body lists each converted machine (file, states, events, the\n impossible combinations removed, any bug fixed) and each candidate left\n alone with its reason.\n- Repository-specific findings (where machines cluster, conventions such as\n the tag name or reducer location) go in that repository's agent guidance.\n Cross-repository lessons go in `references/lessons.md` here, by PR to\n agent-tool.\n\n## Across many repositories\n\nA pass over many repositories follows the coordinating workspace's sweep\nrules (status, ledger, one delegate per repository, `ship-pr` each). Run\n`agent-tool state-machines --json` in each repository first, attach its slice\nto that repository's delegate brief, and remind delegates to read beyond the\nscript's list.\nRepositories whose push hook or ship policy runs a full browser, simulator,\nor package gate need a heavy slot to ship: a delegate without one prepares a\nreviewed commit or open PR and hands off to a finisher that holds one.\n\n## Ownership\n\nagent-tool (`a2f0/agent-tool`) owns this skill in\n`skills/explicit-state-machines/`, and the `state-machines` scanner with its\ntests in `src/stateMachines/`. Repositories get it from\n`agent-tool skills install --apply`; do not edit the installed copy. Change it\nin agent-tool by PR, and send cross-repository lessons to\n`references/lessons.md` there. Repository-specific conventions still go in\nthat repository's `AGENTS.md`.\n",
|
|
4
5
|
"open-issue": "---\nname: open-issue\ndescription: Open a GitHub issue the user explicitly requested, assigned to the authenticated GitHub user, with a reviewable title and body.\n---\n\n# Open Issue\n\nOpen an issue only when the user explicitly asks for one; finding a bug,\nrecording evidence, or following another skill does not authorize it. Follow\nrepository guidance for where issues belong, labels, and templates.\n\nResolve the target repository with `agent-tool repo context` (its `repo`\nfield) from the checkout, or the `owner/repo` the user named; never infer it\nfrom a folder name. Before creating, search open issues for a duplicate:\n\n```sh\ngh issue list --repo <owner/repo> --state open --search '<key words>' --json number,title,url\n```\n\nIf one matches, report it instead of opening another, and assign it with\n`gh issue edit <number> --repo <owner/repo> --add-assignee @me` when the user\nwants it tracked.\n\nWrite a specific title and a body that states the observed behavior, the\nexpected behavior, and reproduction steps or evidence (commands, versions,\nlinks). Never include secret values. Use only labels that already exist\n(`gh label list --repo <owner/repo>`). Put the body in a temporary file, then\ncreate the issue assigned to the authenticated GitHub user:\n\n```sh\ngh issue create --repo <owner/repo> --title '<title>' --body-file /path/to/issue-body.md --assignee @me\n```\n\nAlways pass `--assignee @me`; assign other people only when the user names\nthem, in addition to `@me` unless the user says otherwise. Confirm the result\nwith `gh issue view <url> --json url,assignees` and, if the assignee is missing,\nadd it with `gh issue edit <url> --add-assignee @me`. Report the issue URL and\nits assignees, or the exact error if assignment failed.\n",
|
|
5
6
|
"open-pr": "---\nname: open-pr\ndescription: Open a GitHub pull request for the current feature branch with a validated title and a reviewable description.\n---\n\n# Open PR\n\nUse the installed `agent-tool`. Follow repository policy for validation, commits,\nand branch naming. Resolve the repository and default branch with `agent-tool repo context`;\ndo not assume the branch is named main. Preserve unrelated work and finish\nrequested changes and appropriate checks before publishing them.\n\nEnsure the intended commit is on a feature branch and use\n`agent-tool branch push <reviewed-head> --remote <remote>` through normal hooks.\nIt names the destination explicitly and verifies local/remote HEAD afterward;\na changed result needs another review. Respect existing push authorization.\nIf the user only requested a draft description, prepare it without publishing.\n\nWrite a title that satisfies the data-only `agent-tool.json` subject policy.\nDescribe the concrete resulting behavior and relevant validation. When the\nbranch changes package manifests, run\n`agent-tool dependencies diff --base <base-oid>` and name every `newPackages`\nentry in the body with its manifest and version, so a reviewer sees new\nthird-party code however it was added. Put multiline body text in a temporary\nfile, then run:\n\n```sh\nagent-tool pr open 'feat: describe the change' --body-file /path/to/pr-body.txt\n```\n\nOmitting the title uses the latest commit subject. The tool refuses duplicate\nopen PRs and requires the branch to be pushed at its local HEAD. It does not\npush for you. Resolve a fork's head/base identities explicitly if the repository\nuses fork PRs; the initial open helper supports same-repository branches.\n\nReport the created PR link and any remaining validation or review work. Run\n`agent-tool teardown` first, stop what the work started, and report what remains.\n",
|
|
6
7
|
"reset": "---\nname: reset\ndescription: Return a clean checkout to its updated default branch after confirmed shipping or an explicit checkout cleanup request.\n---\n\n# Reset\n\nThis is checkout cleanup, not a destructive Git reset. Read repository guidance\nfor setup and hooks. Determine the default branch through the repository's\nremote metadata rather than assuming main.\n\nFor post-merge cleanup, preview with\n`agent-tool repo cleanup <PR-number> <shipped-head>`, then run it with `--apply`\nwithin the authorized cleanup scope. It requires a confirmed MERGED PR, refuses\nthe default and `branch.exempt` branches, preserves local edits, checks exact\nlocal/remote feature commits, verifies the merge is on the default branch, and\nfast-forwards that branch. By default it prunes the\nshipped local feature branch and deletes the matching remote branch if present.\nLocal pruning also applies after a squash merge or when GitHub already deleted\nthe remote branch; a missing remote is not evidence that local cleanup is done.\nPass `--keep-branch` when the user asked to preserve both feature refs.\nRead the receipt's completed steps if a later operation fails; do not claim full\ncleanup from a partial result. Verify the final branch and clean worktree, and\ncheck that `git branch --list <feature-branch>` is empty unless keeping the branch.\nIf the feature branch is checked out in another worktree, shared cleanup reports\na partial failure after updating the default branch and leaves both feature refs\nin place; preserve that worktree and report the remaining cleanup.\n\nRemote branch deletion uses GitHub REST after checking the live ref to avoid\npre-push hooks. The API has no atomic expected-OID delete guard; the check and\ndeletion retain a small concurrent-update window. Fork PRs require manual remote\nhandling and are refused by the shared cleanup command.\nUse a repository's stronger cleanup procedure when required, including atomic\nremote deletion or additional local verification; do not weaken it to use the\nshared command. The same local pruning and keep-branch outcomes still apply.\n\nFor an explicit checkout cleanup without a merged PR, preserve all feature refs:\ncheck worktree/index, resolve the default branch with `repo context`, fetch it,\nswitch, and update by fast-forward only. Never discard or automatically stash\nedits, force a diverged default branch, or infer shipping from Git ancestry after\na squash merge.\n\nRun `agent-tool teardown` and stop what the finished work left running\n(`teardown --apply` for recorded processes); report anything that remains.\nRun the repository's documented dependency/hook setup if required. When copied\nhooks are configured, use `agent-tool hooks check` and `hooks install --apply`;\ninspect unmanaged hooks before an explicit `--adopt`, which backs them up.\nDo not assume a package manager. Report final branch, commit, whether the shipped\nlocal branch was pruned or preserved, and unfinished cleanup or setup.\n",
|
|
@@ -8,3 +9,10 @@ export const BUNDLED_SKILLS: Readonly<Record<string, string>> = {
|
|
|
8
9
|
"squash-merge": "---\nname: squash-merge\ndescription: Squash-merge a reviewed GitHub pull request while binding the mutation to the reviewed commit and checking CI.\n---\n\n# Squash Merge\n\nUse the installed `agent-tool` and repository merge policy. Merge only within\nthe user's authorized scope. Have the exact reviewed HEAD, base commit, and base\nbranch available, and read the verdict and unresolved findings. BLOCKER or MAJOR\nfindings require repair and a new review before merging.\nFollow the repository's review-bot policy and require all actionable blocking\nreview feedback to be addressed before merging.\n\nWait with `agent-tool pr checks --wait --head <reviewed-head>`, which waits for\nchecks to appear and for every workflow/job/event entry to finish, and stops\nearly with `behind: true` once the live base moves past the head; then cancel\nthat head's runs with `agent-tool pr cancel-runs --head <reviewed-head>` and\nintegrate, validate, review, and push again. Run\n`agent-tool pr preflight <reviewed-head> <base-branch> <reviewed-base>` immediately\nbefore merging. It checks local and PR identity, live base, ancestry, explicit\nversion policy, merge readiness, configured strict rulesets, and passing checks.\nIf an identity changed, refresh, integrate, validate, and review again. Read\nunresolved feedback with `pr threads list` and follow project review policy.\n\nUse a title that satisfies `agent-tool.json` and pass both reviewed HEAD and\nbase branch:\n\n```sh\nagent-tool pr merge 'feat: describe the change' <reviewed-head-oid> <base-branch>\n```\n\nThe helper enforces HEAD atomically through GitHub's `expectedHeadOid`, checks\nCI, rejects queued or automatic merges, and creates a subject-only squash with\nthe PR reference. GitHub has no atomic expected-base argument in this mutation;\nrepository branch protection must enforce any required base freshness. With\n`merge.requireStrictBaseFreshness`, the helper refuses to merge unless an\nactive strict status ruleset that the actor cannot bypass protects the base. Do\nnot retarget the PR concurrently with merging.\n\nConfirm GitHub reports MERGED before any cleanup. A failed or uncertain mutation\nis not evidence of a merge. Preserve the branch on failure. After a confirmed\nmerge, perform checkout cleanup only when requested or as part of an authorized\nshipping workflow, with `agent-tool repo cleanup <PR-number> <reviewed-head>\n--apply` rather than raw `git branch -D`. Report the PR and merge result.\n",
|
|
9
10
|
"update-dependencies": "---\nname: update-dependencies\ndescription: Update dependencies or update everything in a repository, including runtimes, tools, and CI pins, with compatibility checks and safe previews; ship through review, CI, merge, and release verification unless a narrower scope is requested.\n---\n\n# Update Dependencies\n\nUse the repository's package managers and installed `agent-tool`. Read `AGENTS.md`,\nREADME, existing dependency-maintenance notes, linked setup/release documents,\nand repository-owned upgrade skills before editing. Follow their hooks, version\npolicy, checks, and requested stopping point.\nUpdate applicable dependencies throughout the repository, including workspaces\nand examples. When asked to perform dependency updates, invoke the installed\n[`ship-pr`](../ship-pr/SKILL.md) to complete review, PR, CI, and merge, then verify\nthe repository's normal merge-triggered publication or deployment. An explicit\nnarrower scope, such as inventory-only, local changes, or open-PR-only, takes\nprecedence. This default does not authorize standalone infrastructure applies,\ndeployments, or global tool changes. Default dependency-update merge subject:\n`chore: update dependencies`, subject to repository title policy and any explicit\nuser subject.\n\nRun noisy installs, checks, tests, and builds with\n`agent-tool run --label <check> -- <command> [args...]` to retain full private\nlogs and return the original status with compact output. Inspect bounded excerpts\nusing `agent-tool logs <log>` and targeted searches for warnings and evidence;\na successful receipt does not establish a warning-free upgrade. Keep structured\naudit/plan results, deployment previews, and review findings directly visible\nwhen they drive a safety decision. Do not dump full suite logs into context,\nsilence warnings at their source, or change check scope to reduce output.\n\n## Checkout setup\n\nThis shared workflow also handles natural-language requests to \"update everything\".\nIf the repository owns an `update-everything` wrapper, read and follow its extra\nrequirements; do not create an alias or overwrite that skill. The workflow works\nfrom a standalone checkout with no workspace coordinator.\n\nAfter a fetch, branch integration, or dependency pin change, check that the local\n`agent-tool --version` matches the repository's declared pin before trusting its\ncommands or generated skills. A stale CLI can reject newer configuration keys.\nAfter screening install scripts and hooks as below, run the documented frozen\ninstall with the repository's pinned runtime. For Bun projects, an already working\nCLI can run `agent-tool bun install`; if it is missing or cannot parse the new\nconfig, run the documented package-manager install directly. A known working\nexecutable may capture it with `agent-tool --repo <checkout> run --label setup --\n<install-command> [args...]`. Do not fetch an unpinned CLI, alter global tools, or\nassume a sibling workspace exists. Verify the installed version afterward; if it\nstill differs, report the mismatch rather than deleting arbitrary dependencies.\n\nAfter upgrading agent-tool, run the repository's skill/permission sync commands\n(or `agent-tool skills install --harness <existing-scope> --apply`), preserve its\nharness scope, and commit ownership manifests with generated files. Hermes reads\nthe shared `.agents/skills` in a trusted project; the installer does not grant\ntrust. If discovery is unavailable, read those files directly rather than silently\nusing a stale global copy.\n\n## Nonblocking execution\n\nUse the harness's managed background execution for long finite checks, reviews,\npush hooks, CI waits, and release verification; request completion notification\nwhen available and retain its process handle plus agent-tool's log/receipt path.\nRun `run-group` and reviews directly so their reports remain visible. A started\nprocess is not a passed check: read its final status and required evidence before\nshipping, and do not repeatedly poll when completion events are available.\nWithout harness background support, `proc start --label <name> -- agent-tool run\n--label <check> -- <command> [args...]` records the outer process and captures\nits output; inspect `proc list` and `logs` plus the inner run receipt to establish\ncompletion and exit status. An exited process record alone does not prove success.\nUse `proc stop`/`teardown` for owned leftovers; never stop another session's work.\nThese commands need no matrix workspace or global configuration change.\n\nKeep independent compatibility groups moving while a producer release or an\nexternal gate is pending. Only final adoption of that exact published release\nmust wait. Bound CPU/process-heavy concurrency to the machine's available budget;\na failed full suite needs diagnosis and a full-scope rerun, not larger timeouts,\nfewer assertions, or a focused retry presented as suite recovery. Background\nexecution never bypasses an approval, a deployment preview, or exact-head review.\n\n## Safety before execution\n\n**Always dry-run before any infrastructure apply or deployment. Never destroy,\nreplace, or recreate a resource during an upgrade.** This includes Terraform,\nOpenTofu, Wrangler, Ansible, other infrastructure tools, and effects triggered by\ninstallation, tests, hooks, Git push, opening a PR, CI, or merging. Greenfield\nprojects and approval to ship do not waive this rule.\n\nBefore running project commands or pushing, trace their scripts, hooks, workflow\ntriggers, reusable workflows, and deployment commands. A dry-run flag alone is\ninsufficient: inspect tasks, provisioners, plugins, external programs, and actions\nthat can mutate despite preview mode. Avoid such a preview unless its effects\nare proven read-only. Do not run destroy/delete, taint/replace, state removal,\nbackend migration, forced database reset, or an automatic \"repair\" that bypasses\nthe rule. Do not hide destruction with targeting, disabled refresh, ignored\nchanges, new resource identities, or weakened policy.\n\nPreview with the proposed versions and complete configuration for each affected\nenvironment, using its real backend/workspace/account, variables, inventory, and\nresource identities. An empty local state, mocked credentials, validate-only run,\nor bundle build cannot establish safety for production. Check the current baseline\nas well when needed to distinguish existing drift from the upgrade. Keep state,\nbinary plans, JSON plans, and secret-bearing diffs out of Git and public reports.\n\nIf a preview proposes deletion or replacement, fails, is incomplete, omits\naffected resources, or cannot establish safety, skip that dependency and its\ncoupled migrations. Restore only this task's edits to the affected compatibility\ngroup, retain independent safe upgrades, and record the reason and affected\nresources. Never apply to find out. A preview is not deployment authorization.\n\nBefore an authorized apply/deploy, verify the exact validated commit, toolchain,\nconfiguration, environment, and current state still match the preview. Re-preview\nafter relevant changes or drift. CI must enforce the same order and stop on an\nunsafe preview before mutation; a successful local preview does not excuse an\nunguarded push- or merge-triggered deployment. Establish a narrowly scoped guard\nwhen authorized and reviewable, or hold the affected upgrade before triggering\nthat workflow. Do not disable protections to ship. A plan must be inspected before\nthe operation; inspecting deployment logs afterward is too late.\nAn unsafe workflow triggered on every branch push also blocks pushing otherwise\nindependent upgrades until that trigger has a proven safe guard.\n\n### Terraform and OpenTofu\n\nRead core, provider, and module upgrade guides, including intermediate major\nmigrations. Regenerate provider locks with the pinned CLI, preserving supported\nplatform hashes. Backend initialization/migration is also subject to the safety\ngate. Inspect external data programs and provisioners before planning.\n\nUse a full refreshed saved plan with locking and the real variables, for example\n`terraform plan -input=false -out=<private-plan>`; if using `-detailed-exitcode`,\n0 and 2 are success, 1 is failure. Inspect `terraform show -json <private-plan>`.\nRun `agent-tool dependencies check-terraform-plan <private-plan.json>` to check\nsupported JSON resource actions. It fails for every action containing `delete`,\nincluding both replacement orders, unknown actions, incomplete/deferred plans,\nfailed checks, or opaque provider action invocations. It prints actions and\ndiagnostics without resource attribute values; it does not prove that a plan is\nfresh, comprehensive, authentic, or that provider code has no hidden effects.\nOlder/unsupported plan formats that cannot establish completeness are skipped.\nOpenTofu currently omits `complete` in its JSON format and is rejected by this\nhelper; report that limitation rather than adding a synthetic completeness flag.\nAlso inspect resource identity, drift, warnings, provider semantics, and any\neffects outside resource actions. For an authorized apply, consume the exact\napproved saved plan, not an uninspected newly generated plan.\n\nSource: [Terraform JSON plan format](https://developer.hashicorp.com/terraform/internals/json-format).\n\n### Wrangler and other deployment tools\n\nUse the proposed pinned Wrangler for `wrangler deploy --dry-run` with the same\nbuild/configuration/environment used by deployment. This checks bundling, not a\ncomplete remote resource plan. Separately verify account, worker name, routes,\nbindings and resource IDs against the existing deployment and read migration\nsemantics. Review Durable Object deleted/renamed classes, D1 schema/data\nmigrations, storage resources, queues, containers, and deployment integrations.\nNever infer \"no destruction\" from a successful bundle. Resource removals,\nreplacement, or unprovable remote effects skip the affected group. Apply the\nequivalent preview and identity checks to other tools rather than assuming they\nshare Terraform's guarantees.\n\nDo not assume `wrangler d1 migrations list --remote` is read-only. The\nimplementation inspected for this skill initializes migration tracking with\n`CREATE TABLE IF NOT EXISTS`; verify the pinned Wrangler version before using\nthe command. Verify database identity and compare migration files with the\ndatabase's configured migration tracking table through audited read-only\nqueries. A missing table, pending migration, or unverifiable schema/data\neffects hold the affected upgrade/deployment group. Never initialize schema\nmerely to establish preview safety.\n\nWrangler runs a custom `build.command` again during deployment. Use supported\nconfiguration referencing the same validated artifacts for both preview and\ndeployment, preventing another unvalidated build before upload. Bind the\nartifacts, configuration, and pinned tool to the authorized commit, and recheck\ntheir identities before upload and after relevant changes. Hold the affected\ngroup if the pinned Wrangler cannot upload the validated output without\nrebuilding it, or if the upload cannot be proven to match the preview.\n\nSources: [Wrangler command documentation](https://developers.cloudflare.com/workers/wrangler/commands/),\n[D1 migration listing implementation](https://github.com/cloudflare/workers-sdk/blob/e3d186776fcc2e4b5cc4cfd7155591778a375957/packages/wrangler/src/d1/migrations/list.ts),\n[migration table initialization](https://github.com/cloudflare/workers-sdk/blob/e3d186776fcc2e4b5cc4cfd7155591778a375957/packages/wrangler/src/d1/migrations/helpers.ts),\nand [Wrangler custom builds](https://developers.cloudflare.com/workers/wrangler/custom-builds/).\n\n### Ansible\n\nCheck controller Python, ansible-core, collections/roles, and target Python support\ntogether; read porting guides. Inspect all tasks and invoked scripts before\n`ansible-playbook --check --diff` with the real inventory and variables.\n`check_mode: false` forces mutation even with `--check`; unsupported modules and\nconditions based on registered results can omit effects. Inspect deletion,\nrecreation, service/data reset, and package removal semantics beyond the diff.\nSkipped or opaque tasks are a safety gap, not a clean preview. Redact diffs.\n\nSource: [Ansible check mode limits](https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_checkmode.html).\n\n## Inventory and choose compatible targets\n\nRecord each dependency source, current resolved version, latest stable candidate,\nselected target, compatibility constraints, upstream migration links, and checks\nin the run ledger described under Report.\nQuery authoritative registries, official releases, support matrices, and migration\nguides at execution time; do not treat remembered versions or prereleases as\nlatest stable. Inspect runtime requirements, peer dependencies, ABI/API changes,\nplatform availability, maintained release lines, and changed defaults, including\nsecurity, privacy/telemetry, data retention, and postinstall/native behavior.\nCheck transitive dependencies' peer/runtime constraints as well as direct ones. Unsupported\nor unpublished targets stay pinned with an explanation.\n\nInventory manifests, locks, tool configuration, CI/reusable actions, containers,\nscripts, deployment files, and executable documentation. Search duplicate version\npins and aliases, not just filenames; exclude vendored/generated snapshots unless\nthe repository owns their update process. Examples of sources to inspect:\n\n| Ecosystem | Sources and compatibility group |\n| --- | --- |\n| JavaScript/TypeScript | All package.json dependency fields, overrides/resolutions, workspace catalogs, packageManager/engines, npm/Bun/pnpm/Yarn locks, Node/Bun pins, compiler and lint/build/test plugins |\n| Toolchains | mise.toml/.mise.toml, mise.lock, .tool-versions, runtime version files, tool URLs/checksums, CI pins and container images; include every tool declared by mise, not just mise itself |\n| Python/Ansible | pyproject/requirements, uv/Poetry/pip locks, Python pins, ansible-core, collection/role requirements, lint plugins and managed host constraints |\n| Ruby/iOS | .ruby-version, Gemfile/lock, Bundler, CocoaPods, Podfile/lock, podspecs, Swift packages and Xcode/iOS deployment requirements |\n| Infrastructure | Terraform/OpenTofu CLI constraints, provider locks/constraints, module versions/Git refs, Wrangler and Cloudflare integration dependencies |\n| CI/system/native | Actions/reusable workflow refs, Docker tags/digests, OS images/packages, Rust/Cargo toolchain and locks, Go modules, Gradle/AGP/Kotlin/JDK and other repository-specific manifests |\n\nBuild connected compatibility groups before editing. Select the newest supported\ncombination, which can be older than an individual package's latest. Upgrade the\nframework first or keep its compatible toolchain pinned; do not force peer\nresolution, drop compatibility checks, or add blanket overrides to make installs\npass. Preserve source pin policy (exact version, digest, full Git SHA) and resolve\nimmutable pins from verified upstream releases. Preserve intentional runtime\nminimums unless migration requires changing the support contract.\n\nFor React Native, use its version-specific upgrade guide/template diffs and\nrepository upgrade skills. Couple React and its renderer, native modules,\nMetro/Babel, CLI, codegen, Hermes/New Architecture, CocoaPods/Bundler/Ruby,\nXcode/iOS deployment targets, and Android Gradle/AGP/Kotlin/JDK/SDK/NDK. For Expo,\nuse the chosen SDK's supported React Native/dependency matrix and its dependency\nchecks. Do not independently update framework-managed pods or generated native\nfiles; use the framework's regeneration workflow and prove iOS/Android builds.\nPermissive peer ranges alone do not prove native renderer or toolchain compatibility;\ncheck the framework's actual supported versions and native implementation.\nAn unavailable native toolchain is a reported validation limit; do not claim the\ngroup verified. See [React Native upgrading](https://reactnative.dev/docs/upgrading).\n\nCheck developer tools' own dependencies too: a compiler release without its old\nprogrammatic API may break test/lint runners that import it even when `tsc` passes.\nAction runtime upgrades can require newer self-hosted runners; preserve workflow\npermissions, action inputs/outputs, caching, artifact semantics, and job names.\n\nCheck current advisories with read-only ecosystem audits against the baseline and\nproposed lockfiles, using the repository's configured registry/advisory sources.\nDo not send private dependency metadata to an unconfigured or unauthorized service;\nreport the resulting coverage limit. Cover direct and resolved transitive dependencies,\nincluding native/platform-specific binaries. Latest direct packages can still\npin vulnerable transitive versions. Trace each finding to its owning dependency,\nverify affected versions, runtime conditions, and remediation in the maintainer's\nadvisory and fixed release. When the owner's range permits a fixed version, use a\ntargeted refresh with the lockfile's owning manager and preserve unrelated\nresolutions; otherwise prefer a supported owner upgrade.\nIf the owner still pins a vulnerable version, a narrowly scoped security patch\noverride may be used only with API/ABI/runtime compatibility evidence and focused\nregression checks of the actual affected integration. Document its rationale and\nremoval condition; a passing install or bundle alone is insufficient. Do not run\nblind audit fixes or use blanket overrides to bypass compatibility constraints.\nReport unresolved advisories and unavailable audit coverage accurately; current\ndirect pins or a partial clean audit do not\nestablish that the whole dependency graph is safe.\n\n## Migrate and validate\n\nCapture relevant baseline checks and warnings first, after screening their side\neffects. Update one compatibility group at a time. Read the upstream instructions\nfor every breaking step; implement required source/config/schema migrations and\nremoved API replacements. Use the manager that owns each lockfile, on the selected\nruntime, and inspect the resulting diff. For Bun lockfiles, change declared\nversions with `agent-tool bun update <name[@version]>` and drop packages with\n`agent-tool bun remove <name>`; both use the pinned Bun and refuse to add a\npackage. A new dependency goes through `agent-tool bun add <name@version>`, which\nasks for approval; never introduce one by editing `package.json` and\nreinstalling. For existing Bun catalog entries use\n`agent-tool bun update --catalog default <name@version>...` (or the existing named\ncatalog). Name an explicit exact/ranged registry spec; keep `catalog:` references,\nworkspace membership, other entries, and peer compatibility contracts intact.\nThe helper refuses intake and ambiguous layouts and uses the owning manager to\nrefresh the lock; do not replace catalog references with direct versions.\n\nFor an already-resolved transitive registry package with an eligible owner range,\nuse `agent-tool bun update --resolved <name>...` with the owning Bun 1.4.4 or newer.\nIt selects every locked copy of those names, never a requested version or glob,\nand accepts only a result with identical manifests and unchanged non-target lock\nentries/metadata. It preserves release-age, overrides and patches. If an older\nmanager is pinned, or a required new child prevents a target-only result, this\npath is unsupported: prefer an owning-dependency upgrade through the normal\nhelper, or obtain approval for an explicitly reviewed manager workflow. Do not\nsilently substitute a broad refresh or add a transitive package directly. Inspect\nthe receipt/logs and re-audit; a no-op or warning can leave an advisory unresolved.\nNever hand-edit integrity hashes or lock entries.\nReview intentional patches, overrides, and pins before removal; retain them until\nupstream fixes and regression checks demonstrate they are unnecessary.\nAvoid unrelated global installations or environment changes. Use task-local or\nrepository-scoped tools for testing changed runtime pins.\nFor published packages, also validate tarballs and consumer installs with their\ndocumented supported package managers, including manifest/override syntax and\nruntime requirements. A successful owning-manager install does not prove that\npublished metadata works for those consumers.\n\nRun the repository's setup/hooks, frozen installs, lint, typechecks, tests, builds,\npackage/platform smoke checks, and relevant safe integration previews. Resolve\nupgrade-related deprecation warnings at their source. Do not suppress warnings,\nweaken tests, or disable checks; report remaining upstream/pre-existing warnings\nwith evidence and keep a target pinned when migration cannot be completed.\nAdd focused regression coverage when a migration changes behavior; avoid tests\nthat merely assert version strings. Re-run checks after repairs. Report checks\nblocked by missing credentials, platforms, or external services accurately.\nRe-audit the final resolved graph after migrations and repairs.\n\nConfirm every inventory row is upgraded, already current, intentionally constrained,\nor skipped with a concrete reason. Do not present an unresolved group as complete.\nWhen upgrading agent-tool itself, install its managed skills with the same harness\nscope and commit the ownership manifest; never edit generated managed skill copies.\n\nWhen changes depend on another repository's release, ship the producer first and\nconfirm publishing and registry availability before pinning the consumer; include\nits exact version/commit and API migration notes. Independent groups may proceed.\nComplete the installed `ship-pr` workflow within the requested scope, including\nindependent review of every changed HEAD, required CI and review feedback, merge,\nand verification of the merge's normal publication or deployment. Do not stop at\nan open PR unless the user requested that stopping point or a concrete blocker\nprevents continuing. Check deployment safety again before push/open/merge;\nshipping defaults do not waive the preview gates above.\n\n## Report\n\nKeep run-specific evidence in the upgrade PR body or a linked PR comment: the\ninventory and old/selected versions, completed migrations, validation and warnings,\nbaseline/final audits and coverage limits, preview environment/commit/tool versions\nand safe action summary, and skipped/constrained dependencies with reasons. Include\nsecurity overrides, their rationale and removal condition, and unresolved advisories.\nIf the user requested a scope that stops before a PR, provide this ledger in the\ntask report and honor that limit. For a sweep, keep one entry per repository,\nincluding unready and unchanged checkouts, and use an existing tracking issue only\nwhen explicitly requested. Never create an issue merely to store upgrade evidence.\n\nKeep durable repository guidance focused on what the next upgrade needs: current\ncompatibility constraints, intentional pins/overrides, unresolved advisories, and\nreusable repository-specific procedures. Prefer the existing maintenance document;\notherwise create `docs/dependency-upgrades.md` (or the repository's equivalent)\nonly when there is substantive guidance to preserve. For each constraint, record\nits rationale, upstream advisory/migration/issue link or originating PR, and the\ncondition and check for removing or revisiting it. Do not copy version inventories,\naudit output, validation/preview receipts, or dated run journals into that document.\n\nOn every upgrade, reconcile the existing maintenance document against the final\ndependency graph and checks: update constraints in place, remove resolved entries,\nand link the PR for historical evidence. Keep the root README focused on purpose,\nsetup, supported requirements, and normal usage, with a concise maintenance link\nwhen needed. Keep dependency-maintenance guidance in `AGENTS.md` to a short\nrouting rule and link; do not accumulate upgrade reports in either entrypoint\nor duplicate the maintenance document.\n\nFor shipping, report the branch, reviewed commit/verdict, PR, merge, publish/deploy\nresult, and final checkout state. Keep full logs private; never publish credentials,\nstate, or plan contents in any of these locations.\n"
|
|
10
11
|
};
|
|
12
|
+
/** Markdown files a skill ships beside its SKILL.md, by skill and then by path within the skill directory. */
|
|
13
|
+
export const BUNDLED_SKILL_REFERENCES: Readonly<Record<string, Readonly<Record<string, string>>>> = {
|
|
14
|
+
"explicit-state-machines": {
|
|
15
|
+
"references/lessons.md": "# Lessons\n\nCross-repository lessons from explicit-state-machine passes. Add one when a\npass learns something the next repository needs; keep repository-specific\nfindings in that repository's guidance.\n\n- The script's `flags` signal skips `cancelled` and `settled`: effect cleanups\n and promise races set them once as guards, which is not a machine.\n- Type members and returned object literals (`status: \"idle\"`) are how an\n explicit machine names its states, so the `status` signal counts only\n assignments and setter calls.\n- The first sweep (16 repositories) found real machines the scanner missed:\n React refs (`busy.current`), `#private` fields, in-place writes to nested\n objects (`record.steps[i].status`), lifecycle booleans not named `is*`\n (`booting`, `paging`), extensionless and Expect scripts, and nullable values\n paired with flags. The first five are now signals; read for the last by hand.\n- A ref mirroring a state (`isSubmitting` with `isSubmittingRef`) now shows as\n two flags; it is worth reading, since the pair can disagree.\n- Server status columns in balebase and devopsrockstars were already guarded:\n every write, in one module, is a conditional update naming the states it\n may leave. The scanner still lists them; the skill says when that suffices.\n- Repositories with no runtime state (seed data, one-shot CLIs, dotfiles)\n closed as `unchanged` within minutes; let them, and record what was read.\n- Converting found real bugs: a step that stayed `running` forever after its\n runner failed to start, a gallery acting on a detached render, a bootstrap\n gate blocked forever when prerequisites vanished mid-run, two menus closing\n on one press, duplicate stop logs on a second focus loss.\n- Second and third rounds in balebase and tearleads, driven by the widened\n scanner, found one more real bug (a checkout link minting two Stripe\n sessions on a double click) and several false positives now named in the\n skill's leave-alone list: in-flight-plus-rerun flags, ref mirrors, and flag\n pairs split across a parent and a conditionally mounted child.\n- Bug fixes the sweep surfaced outside state machines shipped as their own\n `fix/` PRs: a swallowed settings-write error, a partial download cached\n forever, an audio session never released, a verify step failing during\n registry propagation, and a CI runner image that broke browser tests.\n",
|
|
16
|
+
"references/patterns.md": "# Patterns\n\nBefore and after shapes per language. Match the repository's existing names\n(tag field, reducer file layout, error helpers) over these.\n\n## TypeScript and React\n\nBefore: three booleans and two nullable fields, eight combinations, four valid.\n\n```ts\nconst [isLoading, setIsLoading] = useState(false);\nconst [error, setError] = useState<Error | null>(null);\nconst [data, setData] = useState<Item[] | null>(null);\n```\n\nAfter: one union, one reducer, booleans derived for readers.\n\n```ts\nexport type LoadState =\n | { status: \"idle\" }\n | { status: \"loading\"; request: number }\n | { status: \"failed\"; error: Error }\n | { status: \"loaded\"; items: Item[] };\n\nexport type LoadEvent =\n | { type: \"start\"; request: number }\n | { type: \"succeed\"; request: number; items: Item[] }\n | { type: \"fail\"; request: number; error: Error }\n | { type: \"reset\" };\n\n/** Only the request that is still current may finish the load. */\nconst current = (state: LoadState, request: number) =>\n state.status === \"loading\" && state.request === request;\n\nexport function transition(state: LoadState, event: LoadEvent): LoadState {\n switch (event.type) {\n case \"start\":\n return { status: \"loading\", request: event.request };\n case \"succeed\":\n return current(state, event.request) ? { status: \"loaded\", items: event.items } : state;\n case \"fail\":\n return current(state, event.request) ? { status: \"failed\", error: event.error } : state;\n case \"reset\":\n return { status: \"idle\" };\n default:\n return assertNever(event);\n }\n}\n\nfunction assertNever(value: never): never {\n throw new Error(`Unhandled event: ${JSON.stringify(value)}`);\n}\n\n// In the component:\nconst [state, dispatch] = useReducer(transition, { status: \"idle\" });\nconst isLoading = state.status === \"loading\";\n// Keyed on the request, not just the status, so a replacement request starts.\nconst request = state.status === \"loading\" ? state.request : undefined;\nuseEffect(() => {\n if (request === undefined) return;\n const abort = new AbortController();\n // The cleanup's own abort rejects the fetch; that rejection must not\n // dispatch, or a re-run of this effect for the same request (Strict Mode,\n // a dependency change) records a failure the user never had.\n let active = true;\n fetchItems(abort.signal).then(\n items => { if (active) dispatch({ type: \"succeed\", request, items }); },\n error => { if (active) dispatch({ type: \"fail\", request, error }); },\n );\n return () => { active = false; abort.abort(); };\n}, [request]);\n```\n\nThe request tag rejects results from a superseded request; the `active` flag\nrejects callbacks from a cleaned-up effect for the same request. Keep both, and\ntest the replay: mount, unmount, mount again (Strict Mode), then resolve, and\nexpect `loaded`, not `failed`.\n\n- Never drop a cancellation guard without replacing it. A bare\n `status === \"loading\"` check accepts a stale result in\n `start(A) -> reset -> start(B) -> succeed(A)`; tag each request (a counter\n or the `AbortController` itself) and let only the current one finish, as\n above, with that sequence as a test. Keep aborting the network work too.\n- Stores (Zustand, Redux, signals): keep the union as the store's state and\n the reducer as its only writer; actions dispatch events.\n- Classes and long-lived loops (sync workers, socket connections): a private\n `state` field of the union type, a `send(event)` method that applies\n `transition`, and the loop `switch`es on `state.status`.\n- Server status columns: a `ALLOWED: Record<Status, readonly Status[]>` table\n and one `moveTo(row, next)` that checks it inside the update's transaction\n (`UPDATE ... WHERE status = $from`), so concurrent writers cannot skip a\n state.\n\nTest with a table:\n\n```ts\ntest.each([\n [{ status: \"idle\" }, { type: \"succeed\", request: 1, items: [] }, { status: \"idle\" }],\n [{ status: \"loading\", request: 1 }, { type: \"succeed\", request: 1, items: [] }, { status: \"loaded\", items: [] }],\n [{ status: \"loading\", request: 2 }, { type: \"succeed\", request: 1, items: [] }, { status: \"loading\", request: 2 }],\n [{ status: \"loaded\", items: [] }, { type: \"fail\", request: 1, error }, { status: \"loaded\", items: [] }],\n] as const)(\"%o + %o\", (state, event, next) => {\n expect(transition(state, event)).toEqual(next);\n});\n\ntest(\"a stale request cannot finish a newer one\", () => {\n const events: LoadEvent[] = [\n { type: \"start\", request: 1 }, { type: \"reset\" }, { type: \"start\", request: 2 },\n { type: \"succeed\", request: 1, items: stale },\n ];\n expect(events.reduce(transition, { status: \"idle\" })).toEqual({ status: \"loading\", request: 2 });\n});\n```\n\n### More React and class shapes\n\n- **A handler, not an effect, starts the work** (a sign-in button): take the\n request number from a `useRef` counter, dispatch `start` with it, and have\n the async handler dispatch `succeed`/`fail` with the same number.\n- **Synchronous re-entrance** (two clicks before a render): keep the current\n state in a ref that one `send(event)` updates and that returns the next\n state; the caller starts work only if its own request entered `loading`.\n- **A queue that spans effect runs** (read one page at a time, queue one\n refresh): keep the machine in a ref with a `useState` copy for rendering,\n written by one `send`. A superseded request that must still finish (to start\n the queued refresh) carries `current: false` instead of being dropped.\n- **A handler that both changes state and decides an output** (open a menu now,\n defer it, or ignore the press): make the step a Mealy function returning\n `{ state, output }`, so every decision lives in one place.\n- **Long-lived controllers that log or notify on change**: have `send` return\n the previous state too, so the caller acts only on the change that happened\n (log on `starting -> playing`, not on every event).\n- **Classes holding resources** (sockets, timers, watchdogs): give each\n resource to exactly one state and release it in one `leave(previous, next)`\n step; guard `if (next === this.state) return` because leave callbacks can\n re-enter (`onDisconnect` calling `stop()`).\n- **Testing async controllers**: move the React controller into its own hook\n with the async work injected, so `renderHook` can drive stale, replacement\n and Strict Mode sequences. In React Native Testing Library, start a press\n without awaiting it to observe the pending state, then resolve inside\n `await act(...)`.\n- **Dismissals name what they dismiss** (outside click, escape): otherwise the\n listener of the menu that just closed can close the menu that replaced it.\n\n### Placement and lint\n\n- Put the reducer beside the code that runs it when a new module costs a\n network request or a build allowlist entry (bundler-free static sites), and\n in Swift in a file already compiled into the app target, to avoid an Xcode\n project edit.\n- A reducer can exceed Biome's cognitive-complexity limit; one type guard such\n as `inAttempt(state, attempt, ...statuses)` usually brings it back.\n\n## Swift\n\n```swift\nenum Upload: Equatable {\n case idle\n case uploading(progress: Double)\n case failed(message: String)\n case done(url: URL)\n\n func handling(_ event: UploadEvent) -> Upload {\n switch (self, event) {\n case (.idle, .start), (.failed, .start): return .uploading(progress: 0)\n case (.uploading, .progress(let p)): return .uploading(progress: p)\n case (.uploading, .finish(let url)): return .done(url: url)\n case (.uploading, .fail(let message)): return .failed(message: message)\n default: return self\n }\n }\n}\n```\n\nAvoid a `default` that hides a new case where the compiler could check it:\nswitch on `self` first and handle every case.\n\n## Kotlin\n\nA `sealed interface State` with `data object Idle : State` and\n`data class Failed(val error: Throwable) : State`, and a\n`fun State.on(event: Event): State = when (this) { ... }` with no `else`\nbranch, so a new state fails to compile until it is handled.\n\n## Python\n\n```python\nfrom dataclasses import dataclass\nfrom typing import assert_never\n\n@dataclass(frozen=True)\nclass Idle: ...\n@dataclass(frozen=True)\nclass Running:\n job_id: str\n@dataclass(frozen=True)\nclass Failed:\n error: str\n\nState = Idle | Running | Failed\n\ndef transition(state: State, event: Event) -> State:\n match state, event:\n case Idle(), Start(job_id=job_id):\n return Running(job_id)\n case Running(), Fail(error=error):\n return Failed(error)\n case _:\n return state\n```\n\nWhere a plain enum suffices (no per-state data), use `enum.Enum` and an\n`ALLOWED: dict[State, frozenset[State]]` table.\n\n## Go\n\nA `type State int` with `const` values and a `String()` method, a\n`func (s State) On(e Event) (State, error)` that returns an error for an\nillegal transition, and an `exhaustive` lint if the repository runs one.\n\n## Shell\n\nOne `state` variable and a `case \"$state\" in` loop instead of several\n`done=0`/`failed=1` variables; print the state when the script exits early.\n"
|
|
17
|
+
}
|
|
18
|
+
};
|
package/src/skills/install.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { harnessNamed } from "../harnesses";
|
|
5
|
-
import { BUNDLED_SKILLS } from "./bundled";
|
|
5
|
+
import { BUNDLED_SKILL_REFERENCES, BUNDLED_SKILLS } from "./bundled";
|
|
6
6
|
|
|
7
7
|
const MANIFEST = ".agent-tool-skills.json";
|
|
8
8
|
const hash = (source: string) => createHash("sha256").update(source).digest("hex");
|
|
@@ -35,6 +35,11 @@ function atomicWrite(file: string, contents: string): void {
|
|
|
35
35
|
|
|
36
36
|
export interface SkillChange { path: string; status: "create" | "update" | "unchanged" }
|
|
37
37
|
|
|
38
|
+
/** Every file each bundled skill installs, relative to its skill directory: `SKILL.md`, then its references. */
|
|
39
|
+
export function bundledSkillFiles(): [name: string, files: [relative: string, source: string][]][] {
|
|
40
|
+
return Object.entries(BUNDLED_SKILLS).map(([name, source]) => [name, [["SKILL.md", source], ...Object.entries(BUNDLED_SKILL_REFERENCES[name] ?? {})]]);
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
function readManifest(root: string): Record<string, string> {
|
|
39
44
|
const file = safeDestination(root, MANIFEST);
|
|
40
45
|
if (!existsSync(file)) return {};
|
|
@@ -53,16 +58,18 @@ export function installSkills(rootDir: string, target: string, apply = false): S
|
|
|
53
58
|
const previous = readManifest(root);
|
|
54
59
|
const pending: { file: string; relative: string; source: string; change: SkillChange }[] = [];
|
|
55
60
|
for (const directory of directories) {
|
|
56
|
-
for (const [name,
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
for (const [name, sources] of bundledSkillFiles()) {
|
|
62
|
+
for (const [inside, source] of sources) {
|
|
63
|
+
const relative = `${directory}/${name}/${inside}`;
|
|
64
|
+
const file = safeDestination(root, relative);
|
|
65
|
+
let current: string | undefined;
|
|
66
|
+
if (existsSync(file)) {
|
|
67
|
+
if (!lstatSync(file).isFile()) throw new Error(`Skill destination is not a regular file: ${relative}`);
|
|
68
|
+
current = readFileSync(file, "utf8");
|
|
69
|
+
if (current !== source && hash(current) !== previous[relative]) throw new Error(`Locally edited or unmanaged skill: ${relative}. Preserve it before installing.`);
|
|
70
|
+
}
|
|
71
|
+
pending.push({ file, relative, source, change: { path: relative, status: current === source ? "unchanged" : current === undefined ? "create" : "update" } });
|
|
64
72
|
}
|
|
65
|
-
pending.push({ file, relative, source, change: { path: relative, status: current === source ? "unchanged" : current === undefined ? "create" : "update" } });
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
75
|
// Preflight every destination before writing any files.
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agent-tool state-machines`: lists files that look like they hold implicit
|
|
3
|
+
* state machines: several lifecycle booleans toggled together, or a status
|
|
4
|
+
* string assigned many values from many places. Read-only; it ranks files for
|
|
5
|
+
* a person or agent to read, and never edits anything.
|
|
6
|
+
*
|
|
7
|
+
* Scope is one repository at a time: the repository holding the working
|
|
8
|
+
* directory, or the repository holding each named path.
|
|
9
|
+
*/
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { lstatSync, readFileSync, realpathSync, statSync, writeSync } from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { assertOwnCheckout } from "../workflows/core";
|
|
14
|
+
import { type FileReport, language, MAX_BYTES, scanFile } from "./scan";
|
|
15
|
+
|
|
16
|
+
export interface Options {
|
|
17
|
+
readonly names: string[];
|
|
18
|
+
readonly json: boolean;
|
|
19
|
+
readonly limit: number;
|
|
20
|
+
readonly minScore: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RepoReport {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly root: string;
|
|
26
|
+
readonly scanned: number;
|
|
27
|
+
readonly files: FileReport[];
|
|
28
|
+
/** Candidates beyond --limit, so a reader knows the list was cut. */
|
|
29
|
+
readonly more: number;
|
|
30
|
+
readonly error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Target {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly root: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const USAGE = `Usage: agent-tool state-machines [path...] [--limit N] [--min-score N] [--json]
|
|
39
|
+
|
|
40
|
+
Ranks tracked source files by signals of implicit state machines: lifecycle
|
|
41
|
+
booleans toggled together (isLoading + hasError) and status strings assigned
|
|
42
|
+
many values from many places. Read-only. Scans the repository holding the
|
|
43
|
+
working directory, or the repository holding each named path.
|
|
44
|
+
|
|
45
|
+
--limit N files listed per repository (default 25; 0 lists all)
|
|
46
|
+
--min-score N skip files scoring below N (default 3: one status with three values)
|
|
47
|
+
--json JSON output for agents`;
|
|
48
|
+
|
|
49
|
+
// From `git rev-parse --local-env-vars`: variables that bind Git to one repository,
|
|
50
|
+
// such as those a hook sets, and that would override `-C`.
|
|
51
|
+
const REPOSITORY_VARIABLES = [
|
|
52
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
53
|
+
"GIT_CONFIG",
|
|
54
|
+
"GIT_CONFIG_PARAMETERS",
|
|
55
|
+
"GIT_CONFIG_COUNT",
|
|
56
|
+
"GIT_OBJECT_DIRECTORY",
|
|
57
|
+
"GIT_DIR",
|
|
58
|
+
"GIT_WORK_TREE",
|
|
59
|
+
"GIT_IMPLICIT_WORK_TREE",
|
|
60
|
+
"GIT_GRAFT_FILE",
|
|
61
|
+
"GIT_INDEX_FILE",
|
|
62
|
+
"GIT_NO_REPLACE_OBJECTS",
|
|
63
|
+
"GIT_REPLACE_REF_BASE",
|
|
64
|
+
"GIT_PREFIX",
|
|
65
|
+
"GIT_SHALLOW_FILE",
|
|
66
|
+
"GIT_COMMON_DIR",
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/** Git bound to the repository it is asked about, whatever the caller's environment. */
|
|
70
|
+
function git(root: string, args: string[]): { ok: boolean; stdout: string } {
|
|
71
|
+
const env: NodeJS.ProcessEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
72
|
+
for (const name of REPOSITORY_VARIABLES) delete env[name];
|
|
73
|
+
const result = spawnSync("git", ["-C", root, ...args], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024, env });
|
|
74
|
+
return { ok: result.status === 0, stdout: result.stdout ?? "" };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function count(flag: string, value: string | undefined): number {
|
|
78
|
+
if (value === undefined || value.startsWith("--")) throw new Error(`${flag} needs a value.`);
|
|
79
|
+
const number = Number(value);
|
|
80
|
+
if (!Number.isInteger(number) || number < 0) throw new Error(`${flag} must be a non-negative integer.`);
|
|
81
|
+
return number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function parseOptions(argv: string[]): Options {
|
|
85
|
+
const names: string[] = [];
|
|
86
|
+
let json = false,
|
|
87
|
+
limit = 25,
|
|
88
|
+
minScore = 3;
|
|
89
|
+
for (let index = 0; index < argv.length; index++) {
|
|
90
|
+
const arg = argv[index]!;
|
|
91
|
+
if (arg === "--json") json = true;
|
|
92
|
+
else if (arg === "--limit") limit = count(arg, argv[++index]);
|
|
93
|
+
else if (arg === "--min-score") minScore = count(arg, argv[++index]);
|
|
94
|
+
else if (arg.startsWith("-")) throw new Error(`Unknown option ${arg}.`);
|
|
95
|
+
else names.push(arg);
|
|
96
|
+
}
|
|
97
|
+
return { names, json, limit, minScore };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The real top level of the repository holding `directory`, never an enclosing one; undefined outside Git. */
|
|
101
|
+
function toplevel(directory: string): string | undefined {
|
|
102
|
+
const result = git(directory, ["rev-parse", "--show-toplevel"]);
|
|
103
|
+
if (!result.ok) return undefined;
|
|
104
|
+
const top = realpathSync(result.stdout.trim());
|
|
105
|
+
assertOwnCheckout(top, directory);
|
|
106
|
+
return top;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Repositories to scan: the one holding `cwd`, or the one holding each named
|
|
111
|
+
* path (relative to `invoked`), deduplicated by real root.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveTargets(cwd: string, names: string[], invoked = cwd): Target[] {
|
|
114
|
+
if (names.length === 0) {
|
|
115
|
+
const here = toplevel(cwd);
|
|
116
|
+
if (!here) throw new Error(`${cwd} is not inside a git repository; name one`);
|
|
117
|
+
return [{ name: path.basename(here), root: here }];
|
|
118
|
+
}
|
|
119
|
+
const roots = names.map(name => {
|
|
120
|
+
const full = path.resolve(invoked, name);
|
|
121
|
+
let top: string | undefined;
|
|
122
|
+
try {
|
|
123
|
+
top = toplevel(statSync(full).isDirectory() ? full : path.dirname(full));
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
126
|
+
}
|
|
127
|
+
if (!top) throw new Error(`${name} is not a path inside a git repository`);
|
|
128
|
+
return top;
|
|
129
|
+
});
|
|
130
|
+
return [...new Set(roots)].map(root => ({ name: path.basename(root), root }));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A tracked file's text, or undefined when it or any parent directory is a
|
|
135
|
+
* symlink, or it lies outside the repository: a tracked link must never make the scan read an ignored
|
|
136
|
+
* secret or another tree.
|
|
137
|
+
*/
|
|
138
|
+
export function readInside(root: string, file: string): string | undefined {
|
|
139
|
+
const full = path.join(root, file);
|
|
140
|
+
try {
|
|
141
|
+
const stat = lstatSync(full);
|
|
142
|
+
// Check the size before reading, so a huge tracked file is never loaded.
|
|
143
|
+
if (!stat.isFile() || stat.size > MAX_BYTES) return undefined;
|
|
144
|
+
// The resolved path must be exactly root/file: any symlinked component,
|
|
145
|
+
// a parent directory included, changes it, even one pointing back inside.
|
|
146
|
+
const real = realpathSync(full);
|
|
147
|
+
if (real !== path.join(realpathSync(root), path.normalize(file)) || file.split(/[\\/]/).includes("..")) return undefined;
|
|
148
|
+
return readFileSync(real, "utf8");
|
|
149
|
+
} catch {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Scans the files git tracks in one repository (submodules and nested checkouts excluded). */
|
|
155
|
+
export function scanRepository(name: string, root: string, options: Pick<Options, "limit" | "minScore">): RepoReport {
|
|
156
|
+
const listed = git(root, ["ls-files", "-z", "--cached"]);
|
|
157
|
+
if (!listed.ok) return { name, root, scanned: 0, files: [], more: 0, error: "git ls-files failed" };
|
|
158
|
+
const files: FileReport[] = [];
|
|
159
|
+
let scanned = 0;
|
|
160
|
+
for (const file of listed.stdout.split("\0")) {
|
|
161
|
+
if (!file || !language(file)) continue;
|
|
162
|
+
const text = readInside(root, file);
|
|
163
|
+
if (text === undefined) continue;
|
|
164
|
+
scanned++;
|
|
165
|
+
const report = scanFile(file, text);
|
|
166
|
+
if (report && report.score >= options.minScore) files.push(report);
|
|
167
|
+
}
|
|
168
|
+
return { name, root, scanned, ...ranked(files, options.limit) };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Highest scores first, ties by path, cut to `limit` files (0 keeps all) with the count left out. */
|
|
172
|
+
function ranked(files: FileReport[], limit: number): Pick<RepoReport, "files" | "more"> {
|
|
173
|
+
files.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
174
|
+
const shown = limit === 0 ? files : files.slice(0, limit);
|
|
175
|
+
return { files: shown, more: files.length - shown.length };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function describe(report: FileReport): string[] {
|
|
179
|
+
return report.signals.map(signal => {
|
|
180
|
+
const lines = signal.lines.slice(0, 8).join(",") + (signal.lines.length > 8 ? ",..." : "");
|
|
181
|
+
return signal.kind === "flags"
|
|
182
|
+
? ` flags ${signal.names.join(", ")} (lines ${lines})`
|
|
183
|
+
: ` status ${signal.names[0]} = ${signal.values!.join(" | ")} (lines ${lines})`;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function render(reports: RepoReport[]): string {
|
|
188
|
+
const out: string[] = [];
|
|
189
|
+
for (const repo of reports) {
|
|
190
|
+
out.push(`${repo.name} (${repo.root}): ${repo.error ?? `${repo.files.length + repo.more} candidate files of ${repo.scanned} scanned`}`);
|
|
191
|
+
for (const file of repo.files) out.push(` ${file.score.toString().padStart(3)} ${file.path}`, ...describe(file));
|
|
192
|
+
if (repo.more) out.push(` ... ${repo.more} more (raise --limit)`);
|
|
193
|
+
}
|
|
194
|
+
out.push("", "Candidates are leads, not findings: read each file and apply the skill's criteria before changing it.");
|
|
195
|
+
return out.join("\n");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Exit status: 0, 1 when a repository could not be listed, 2 for usage errors. */
|
|
199
|
+
export function stateMachinesCommand(
|
|
200
|
+
argv: string[],
|
|
201
|
+
cwd = process.cwd(),
|
|
202
|
+
print: (text: string) => void = text => { writeSync(1, `${text}\n`); },
|
|
203
|
+
invoked = cwd,
|
|
204
|
+
): number {
|
|
205
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
206
|
+
print(USAGE);
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
let options: Options;
|
|
210
|
+
try {
|
|
211
|
+
options = parseOptions(argv);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
print(`${(error as Error).message}\n\n${USAGE}`);
|
|
214
|
+
return 2;
|
|
215
|
+
}
|
|
216
|
+
let targets: Target[];
|
|
217
|
+
try {
|
|
218
|
+
targets = resolveTargets(cwd, options.names, invoked);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
print((error as Error).message);
|
|
221
|
+
return 2;
|
|
222
|
+
}
|
|
223
|
+
const reports = targets.map(target => scanRepository(target.name, target.root, options));
|
|
224
|
+
print(options.json ? JSON.stringify({ repos: reports }, null, 2) : render(reports));
|
|
225
|
+
return reports.some(report => report.error) ? 1 : 0;
|
|
226
|
+
}
|