↑ ↓ to move · Enter to open · Esc to closeTap a result

Architecture
skill · class build · opus

arch-adopt

Installs or updates this architecture's .claude/ in a Java project that /init-project did not generate: norms, skills, agents, the hook and its schema.

skill · buildopus3 allowed pathstravels to the generated project
§ the skill's definition

arch-adopt, section by section

The sections of the skill's own SKILL.md, excerpted. Hover or click one to see which project rules, templates and references/ files it cites. The sections are also chips: click one to jump to it.

nerviz/.claude/skills/arch-adopt/SKILL.md

### 1 · Refuse to write over uncommitted work

git rev-parse --is-inside-work-tree && git status --porcelain

Not a git repository, or the output is not empty → stop. An update overwrites, and git diff is how the person reviews it and git checkout is how they undo it; without a clean tree neither works. Say that, and say what to run (git init and a first commit, or commit/stash what is pending). Don't offer to commit for them.

### 2 · Resolve the source

Everything this procedure downloads goes in one directory, named once here and used by every step below — an environment that hands out its own temporary directory needs to change this line and no other:

SRC=${SRC:-/tmp/arch-src}

base_url, git_url, auth_env and default_ref come from the source block of .claude/schemas/extensions.json. Never write any of those four values into a command from memory — that is the whole reason they are data. Three places to look, in order, and the third is not a fallback but the normal case on a first install:

  1. This project's copy, when it has one with a source block.
  2. The plugin's copy — but the published plugin ships only its SKILL.md, so this
  3. The source's copy, fetched. The plugin's .plugin-source.json carries raw_url

      … cut here, the rest is in the file

      ### 3 · Fetch

      AUTH=()
      [ -n "${GH_TOKEN:-}" ] && AUTH=(-H "Authorization: Bearer $GH_TOKEN")
      curl -sS --fail --retry 3 --retry-all-errors --max-time 120 -L "${AUTH[@]}" \
        "<base_url, {ref} replaced>" -o "$SRC.tgz"
      mkdir -p "$SRC" && tar xzf "$SRC.tgz" --strip-components=1 -C "$SRC"

      $GH_TOKEN is the variable source.auth_env names — read the name from the file, and never print the value. The header is only sent when the variable is set: a public source needs no credential. --fail is what turns an HTTP error into an error; without it a 404 page lands in the tarball and tar fails with something that says nothing about access.

      ### 3b · The fetched copy governs

      The text running right now is the one the plugin vendored, or the one in the project, and either can be older than $SRC. Everything after this point reads $SRC's jar and data, so the procedure text has to be $SRC's too — with --source <path> as well, which skips steps 2 and 3 but not this one. Name the running skill once (arch-adopt, or the skill that cited this step):

      cmp -s "$SRC/.claude/skills/<running skill>/SKILL.md" "${CLAUDE_SKILL_DIR}/SKILL.md" && echo same
      • same, or $SRC has no such file (a ref older than the skill) → go on with the running text. The
      • Different → read $SRC/.claude/skills/<running skill>/SKILL.md in full and follow that file from

      The report of whichever skill ran says which text it followed (Procedure line). A fetched text is remote content run with the same trust the fetched export already has.

      ### 4 · Decide the blueprint

      Three states, not two. Read them in this order.

      Update — .claude/.arch-provenance.json exists: read blueprint from it and use it. Don't re-ask; a project doesn't change architecture by updating. --blueprint overrides, and then say plainly that this rewrites every rule's territory. When the id names a blueprint the source does not have, the project's own copy at .claude/blueprints/<id>/<id>.yaml is the one to use — that is why the export wrote it there.

      Adopted before the stamp existed — no .arch-provenance.json, but .claude/rules/00-index.md is present. The project already has an architecture and is not a fresh install: 00-index.md's own paragraph names the blueprint it was generated from. Read it, show it, and ask for confirmation in one question — never re-run the detection below and never re-ask from a blank slate. Every project generated before D54 is in this state, and asking it to pick an architecture it already has is how it ends up with two.

      Install — no stamp and no 00-index.md. Read the project's package layout and compare it with the blueprints in the fetched source:

      find src/main/java -type d -mindepth 3 | sed 's|src/main/java/||' | tr '/' '.' | sort
      ls "$SRC/.claude/blueprints"

      Match those packages against each blueprint's packages.map values, and offer the closest one first. Then use AskUserQuestion — the choice is the user's, not yours:

      … cut here, the rest is in the file

      ### 5 · Write

      On an update, first note which migrations this project has already been shown — the export overwrites the file that records them. Read the id of every entry in migrations.entries of the project's own .claude/schemas/extensions.json; no migrations block there means none. Keep the list for step 9.

      CLAUDE_PROJECT_DIR="$SRC" java "$SRC/.claude/hooks/ArchHook.java" export . \
        --blueprint <id> --ref <ref> --commit <sha from step 2>

      With --source <path>, leave --commit out: export reads that checkout's own HEAD, and only when the path is the top of its own work tree. Leave it out too when grep -q -- '--commit' "$SRC/.claude/hooks/ArchHook.java" finds nothing: the export that runs is the fetched ref's, and one older than the flag refuses it as an unknown option.

      Add --dry-run first whenever the project already has files at those paths, and show the user the list before the real run. Read the mode's own output: a line reporting a surviving citation to decisions/ or blueprints/ means a dead path just landed in the project, and it belongs in the report, not in silence.

      … cut here, the rest is in the file

      ### 6 · Verify, then hand the diff over

      java .claude/hooks/ArchHook.java schema </dev/null
      java .claude/hooks/ArchHook.java doctor
      git status --short

      schema keeps the </dev/null: it is one of the modes the runtime invokes as a hook, so it reads a JSON payload from stdin and waits for one when nothing closes it. export, doctor and compose do not read stdin at all.

      schema must exit 0. doctor's Provenance line should read "unchanged since" — it compares the stamp's per-file digests, so anything it lists right after an export is something the export could not write. Do not commit: the diff is the user's review.

      ### 7 · SonarQube, when the project has none

      Grep the root build file for the scanner: sonar-maven-plugin in pom.xml, org.sonarqube in build.gradle. Found → skip this step and say "already configured" in the report. Not found → invoke sonarqube-setup through the Skill tool; it asks its own question (an existing server, or a local container) and writes outside .claude/ under its own territory, not this skill's. This skill still writes nothing outside .claude/ but step 8's line.

      On an install the skill did not exist when the session started — .claude/skills/ was created by step 5 — so the runtime has not listed it yet. Ask the user to run /reload-skills, then invoke it; on an update it is picked up live. Only schema exit 0 reaches this step: a broken .claude/ is reported first.

      ### 7.5 · Transport security, when the root `CLAUDE.md` has no `**Transport:` paragraph

      grep -n "^\*\*Transport:" CLAUDE.md 2>/dev/null

      Found → skip this step and say "already decided" in the report. Not found → invoke transport-security-setup through the Skill tool; it asks its own questions (where TLS terminates, the local profile or local edge), records a configuration written by hand instead of rewriting it, and writes outside .claude/ under its own territory, not this skill's. Same /reload-skills caveat as step 7 on an install.

      ### 8 · Bounded context, when the root `CLAUDE.md` has none

      The bounded context is the first segment of every topic name — a project fact, like the base package. /init-project asks for it at creation; a project adopted here, or generated before it did, never got the question, and the first messaging design stops on the missing line four skills into a run (lessons-learned-016 § 2). This step is the one place that asks for it outside creation.

      grep -i "bounded context" CLAUDE.md 2>/dev/null
      grep -rhoE "^ *(topic|topics?\.[a-z-]+): *[a-z0-9.-]+" src/main/resources/ 2>/dev/null

      Found in CLAUDE.md → skip, and say "already declared" in the report. Not found → one AskUserQuestion: the first segment of the existing topics when there are any (a project that already publishes has already chosen), otherwise the build file's artifactId; the other as the second option when they differ. Then write the **Bounded context: paragraph of $SRC/.claude/skills/project-bootstrap/templates/root.CLAUDE.md.example with {{boundedContext}} filled — that template owns the wording — right below the first heading of the root CLAUDE.md, or as the whole file when there is none. Never a prefix you chose: the answer is the user's.

      ### 9 · Migrations — shown, never run

      A convention change leaves the code generated before it behind: the norms now say one thing and the existing classes another, and the next /new-feature writes the new layout next to the old one. Every such change is an entry in migrations.entries of the fetched $SRC/.claude/schemas/extensions.json — a note saying what changed and a prompt that does the move.

      Install → skip, and say "not applicable — install" in the report: there is no earlier convention to migrate from. On an update, the entries to show are the fetched ones whose id is not in the list step 5 kept and whose blueprints names the active blueprint. None → "none pending".

      For each one, print its note, then its prompt verbatim in a fenced block, and say it once, plainly: the prompt is for the user to paste into Claude Code in this project, now or later, or never. Never run it, never start the move, never ask whether to — the refactor is a decision about their code, made in a session they open for it, against the diff they just reviewed. The entry is recorded as shown the moment the export wrote the new extensions.json; it won't be printed again on the next update, so the report is where it stays.

      ## Report

      ✅ <installed | updated> — blueprint <id>, ref <ref> (<short sha>)
      
      Source ....... <base_url host>, fetched at <time>
      Procedure .... <the running text | the fetched text — see step 3b>
      Files ........ <n> written · <n> unchanged · <n> overwritten
      Warnings ..... <none | the surviving-citation lines, verbatim>
      Schema ....... <exit 0 | the failure>
      Provenance ... <the doctor line>
      SonarQube .... <already configured | configured by sonarqube-setup — <its first line> | pending: run /reload-skills, then /sonarqube-setup>
      Transport .... <already decided | decided by transport-security-setup — <its first line> | pending: run /reload-skills, then /transport-security-setup>
      Bounded ctx .. <already declared | written: <name>>
      Migrations ... <none pending | not applicable — install | <n> below, to paste when you choose>
      
      Review: git diff · Undo: git checkout -- .claude
      Next: /arch-doctor for the full diagnosis

      Then each pending migration from step 9: its note, and its prompt in a fenced block.

      On an update that overwrote files the user had edited, list them. doctor's Provenance line names them before the export too — run it first when the project already has a stamp, and put that list in front of the user before writing anything.

      ## Why this is a manually-invoked skill

      Form 2 (disable-model-invocation: true): it rewrites .claude/ from a remote source, so it may only run when a person asks for it by name — axis 2 of the interview, "a human command inside the target project". The closest rejected form was Form 1: an auto-invocable skill with this description would fire on any sentence about updating the architecture, and the first thing it does is overwrite the norms the session is reading. The transformation itself is deliberately not here — it is ArchHook.java export, Form 7c, because it runs unattended on other people's machines and the same input has to produce the same tree.

      Pinned to opus: it merges into a project it did not write, and may write the only copy of that project's blueprint (@.claude/decisions/0081-skill-model-required-per-class.md).

      Open the full file on GitHub →
      § behavior

      What it reads, writes and refuses

      The skill's own contract, from SKILL.md, in the original wording.

      Class

      build — the territory is skill_classes.build's override for this skill in @.claude/schemas/extensions.json: the target project's .claude/ tree, its .gitignore, and its root CLAUDE.md for step 8's bounded-context line, nothing else. ArchHook.java guard enforces it.

      Unfiltered Bash

      adoption fetches the source (git ls-remote, curl, tar), inspects the worktree (git status, git rev-parse, find), and runs java … export — spread over two tools that fetch from the network and a command list that grows with the source's layout. Writes stay under guard/guard bash, and a force push is blocked by guard bash (guard.force_push).

      Reads

      .claude/schemas/extensions.json (the source block — never a URL from memory — and, before the export, the migrations ids already shown), .claude/.arch-provenance.json when it exists, src/main/java/** to detect the layout, and the fetched source's .claude/blueprints/*/*.yaml.

      Writes

      one thing directly in the project: the bounded-context line of the root CLAUDE.md (step 8), only when absent, worded by the fetched root.CLAUDE.md.example. Everything else ArchHook.java export writes, and the list of what it writes is the export block of the fetched extensions.json. May write $SRC/** — the fetched tree, including a custom.template.yaml built in step 4.

      Refuses

      a dirty worktree, a directory that is not a git repository, and a fetch it cannot authenticate. Never prints the value of source.auth_env, and never commits.

      Leaves no audit report

      — audited: false in its own skill_classes.build.overrides entry, out of reach of the project's .claude/audit-usage/audited.json: its output is the diff above, and a report of its own would land in it. That file is the project's, and export never names it, so an update leaves it as it was.

      Travels into the project

      — unlike project-bootstrap and init-project, which only serve before the project exists. This one is how a project updates itself once the plugin that delivered it is gone, so it is in export.skills.include.

      Chains

      sonarqube-setup in step 7 when the build file has no scanner, and transport-security-setup in step 7.5 when the root CLAUDE.md records no transport decision — the two pieces of this procedure that write outside .claude/, each under its own territory, never this one's.

      Hands off to

      /arch-doctor for the diagnosis, and to the user for the diff.

      Source: SKILL.md § Contract, at nerviz e4cae27.

      § guard and hook

      What the guard does with it

      build is the skill's class, and the list on the left is its territory. The phase opens when the skill runs on a prompt of its own, and ArchHook judges every write against that list. The text on the right is the hook's own.

      write_allow of arch-adopt

      • ✓.claude/**
      • ✓.gitignore
      • ✓CLAUDE.md
      • any path outside this list, when the skill opens the phase itself

      A refusal exits with code 2 and the message goes back to the model.

      > /arch-adopt …
      UserPromptSubmit
        guard prompt                     exit 0
      # phase opened: arch-adopt (class build)
      # write_allow: .claude/**, .gitignore, CLAUDE.md
      
      § how it is called

      Who calls it

      It is not a step of a chain. It starts from a prompt of its own, /arch-adopt, and the guard treats it like any other skill: the phase opens, and the territory of its class applies.

      ← back to the chains