diff --git a/.agents/skills/migrating-fleet-context/SKILL.md b/.agents/skills/migrating-fleet-context/SKILL.md index fd17f456..77447642 100644 --- a/.agents/skills/migrating-fleet-context/SKILL.md +++ b/.agents/skills/migrating-fleet-context/SKILL.md @@ -1,240 +1,349 @@ --- name: migrating-fleet-context -description: Safely migrates repository agent context from legacy or drifted structures to the Two-Tier Agent Context architecture (Tier 1 router + Tier 2 domain guidelines + 4-store docs layout) using LLM semantic merge on a dedicated feature branch. +description: Use when `agents doctor` reports a `scaffold:*` warning, `agents drift` exits non-zero, or a repository keeps domain rules in a root `AGENTS.md`/`CLAUDE.md`, lacks `.agents/AGENTS.md`, is missing `docs/` stores, carries plans or designs in the wrong store, or has a bundled skill that no longer matches the installed binary. --- # Migrating Fleet Context -Migrates repository agent context from legacy single-file or drifted structures to the **Two-Tier Agent Context Architecture** with **4-store documentation layout**. +Moves a repository onto the Two-Tier Agent Context architecture: a canonical +root router at `AGENTS.md`, domain rules in `.agents/AGENTS.md`, durable +knowledge in the four `docs/` stores. -## Why this skill exists +**The deterministic tools know the states; only you can read the prose.** +`agents drift` tells you exactly what is wrong and never guesses at meaning. +Your job is the part it cannot do — deciding which sentence is a repository's +own rule and which is scaffold boilerplate — and proving you moved every one. -Earlier iterations of repository scaffolding treated `AGENTS.md` (or `CLAUDE.md`) as a single monolithic context file. Over time, repositories accumulated human-authored domain rules, test protocols, and language conventions mixed directly with machine wiring and router instructions. +**Nothing is staged or committed until a human approves the diff.** -Deterministic tools (`agents init`, `agents update`) cannot safely disentangle reordered, edited, or appended domain rules from router boilerplate without risking silent deletion of repository guidelines. Conversely, leaving legacy files unmaintained freezes repositories on obsolete scaffold conventions. +--- + +## Step 0: Refresh yourself first + +This skill is an `agents`-owned asset embedded in the binary. The copy you are +reading can be older than the tool you are about to run. + +```bash +agents doctor +``` -This skill provides an authoritative, LLM-in-the-loop migration protocol to safely partition context into two isolated tiers: -1. **Tier 1 (Root Router)**: Standardized root `AGENTS.md` (and relative symlink `CLAUDE.md -> AGENTS.md`) acting solely as a durable docs and machine wiring pointer. -2. **Tier 2 (Domain Context)**: Dedicated repository-specific engineering guidelines, test mandates, architectural invariants, and safety constraints in `.agents/AGENTS.md`. -3. **4-Store Documentation**: Durable repository knowledge in `docs/` partitioned across `design/`, `plans/`, `journal/`, and `qna/`. +If `scaffold:skill-migrating` is anything but `ok`, your instructions are stale: + +```bash +agents update --apply +``` + +Then re-read this file before continuing. Do not migrate from a stale copy. --- -## When to Run +## Step 1: Pick the mode and read the right JSON shape + +The two invocations do not return the same type. A parser written for one +breaks on the other. + +| Mode | Command | Returns | +|---|---|---| +| Single repository | `agents drift --json` | one **object** | +| Fleet | `agents ls`, then `agents drift --all --json` | an **array** of objects | + +Fleet mode migrates one repository at a time, each on its own branch. Skip and +name in the final report, rather than migrating: + +- registry entries reported `missing` or `unknown` +- any repository whose working tree is dirty +- any repository already `clean_current` with every other field `ok` + +Fields to read from each report: + +| field | use | +|---|---| +| `router_state` | picks your procedure — see Step 4 | +| `symlink_state` | `ok` \| `not_symlink` \| `broken` \| `missing` — see Step 5 | +| `domain_state` | `ok` \| `missing` — whether `.agents/AGENTS.md` exists | +| `skills` | embedded skills only: `ok` \| `clean_legacy` \| `customized` \| `missing` | +| `local_skills` | the repository's own skills. **Never touch these.** | +| `docs_stores` | which of `design`/`plans`/`journal`/`qna` exist | +| `misplaced_docs` | plans and designs in the wrong live store | +| `diff` | unified diff of the root router against canonical | + +--- + +## Step 2: Preflight + +```bash +git status --porcelain +``` + +Any output at all: stop. Report it and let the human commit or stash. A dirty +tree makes the approval diff in Step 10 unreadable, which is the one artifact +this whole procedure exists to produce. + +## Step 3: Branch isolation -Run this skill when: -- `agents doctor` reports warnings or info for any `scaffold:*` check (`scaffold:router`, `scaffold:symlink`, `scaffold:domain`, `scaffold:skill-recording`, `scaffold:skill-migrating`). -- `agents drift` or `agents drift --json` detects `router_state: clean_legacy`, `router_state: drifted`, `domain_state: missing`, or `misplaced_docs`. -- Migrating a repository from legacy monolithic `AGENTS.md` or `CLAUDE.md` to Two-Tier context. -- Realigning misplaced plan or design documents across `docs/`. +```bash +git branch --show-current +git checkout -b feat/two-tier-context-migration +``` + +Never migrate on `master`, `main`, or any protected branch. --- -## Invariants & Safety Constraints +## Step 4: Reconcile the root, one procedure per state + +Four states, four different correct actions. Treating them alike is the single +most damaging thing you can do here — it partitions a file that has nothing to +partition, or invents content for a file that has none. + +| `router_state` | What it means | What to do | +|---|---|---| +| `clean_current` | matches the installed binary's canonical router | nothing. Do not "improve" it. | +| `clean_legacy` | a known older canonical template, **no repository content** | replace wholesale with the canonical router. There is nothing to extract — the digest already proved that. | +| `drifted` | canonical text plus, or reworded into, repository content | semantic reconcile, below | +| `missing` | no root `AGENTS.md` at all | do not invent one. Go to Step 5 and read `CLAUDE.md`. If that is absent too, **stop and ask** what this repository's rules are. | + +### Semantic reconcile (`drifted` only) + +Two inputs — the current root file and the canonical router — and one output +per block. This is not a three-way merge; there is no base. Read the `diff` +field to see what the repository added. + +Classify **every** block: + +- **Boilerplate**, to be replaced by the canonical router: pointer tables to + `docs/` or `.agents/memory/`, old single-line `agents doctor` instructions, + references to retired commands (handoff writing, `save`, `index`, and the + memory tooling — none of which the CLI defines any more). +- **Domain rules**, to be preserved in `.agents/AGENTS.md`: tech stack + conventions, test mandates, safety constraints, architecture invariants, PR + and workflow policy, commenting standards. -1. **Zero Rule Dropping**: Every domain guideline, architectural rule, test mandate, commenting standard, and safety constraint in the existing files MUST be preserved in `.agents/AGENTS.md`. Never discard domain context during migration. -2. **Feature Branch Isolation**: Never perform migrations directly on `master`, `main`, or protected branches. Always execute on a dedicated feature branch (`feat/migrate-agent-context` or `feat/two-tier-context-migration`). -3. **Deterministic Preflight Cleanliness**: Require a clean working tree (`git status --porcelain`) before modifying any files. -4. **Canonical Router Exactness**: Root `AGENTS.md` must match the canonical Tier 1 router template verbatim. -5. **Relative Symlink**: `CLAUDE.md` must be a relative symlink pointing to `AGENTS.md` (`CLAUDE.md -> AGENTS.md`). -6. **Archive Immutability**: Active work and modern plans belong in `docs/plans/`. Never write new plans or active context to `docs/archive/`. +Write the domain rules to `.agents/AGENTS.md`. If it already exists, append +into the matching section without duplicating what is there. Then overwrite the +root `AGENTS.md` with the canonical router verbatim: + +```markdown +# Agent context + +Durable context for this repo lives in `docs/`. Read it before assuming; +it is the record, and this file is only the pointer to it. + +- `docs/qna/` — answers indexed by the question you would ask again +- `docs/plans/` — implementation plans +- `docs/journal/` — dated record of what happened +- `docs/design/` — the design still in force + +## Repository Architecture & Guidelines +- Domain engineering guidelines, commenting standards, and safety constraints + are defined in `.agents/AGENTS.md`. +- Repo-specific procedures and skills are located in `.agents/skills/`. + +## Machine Wiring +`.agents/` holds machine wiring and local skills. A hook cannot install itself +and a missing hook fails silently. +- If the `agents` CLI is installed, run `agents doctor` early and report any warnings before relying on this context. +- If `agents` is not installed on this machine, skip machine wiring checks and adhere directly to the repository instructions above. + +Recording is covered by the global instruction and the `recording-what-you-learn` +skill; it is not repo-specific and is not restated here. +``` --- -## Phased Migration Workflow +## Step 5: The two root files, before any symlink -Follow all five phases sequentially. Do not skip phases or verification gates. +`CLAUDE.md` is about to become a symlink, which destroys whatever it holds. +`stat` both paths before you read or write either one. +```bash +ls -la AGENTS.md CLAUDE.md ``` -Phase 1: Deterministic Preflight & Assessment - │ (agents drift --json, git status --porcelain) - ▼ -Phase 2: Feature Branch Isolation - │ (git checkout -b feat/migrate-agent-context) - ▼ -Phase 3: Semantic Un-Nesting & Partitioning - │ (extract domain rules -> .agents/AGENTS.md, - │ restore root AGENTS.md router, relink CLAUDE.md) - ▼ -Phase 4: Docs Store Realignment & Skill Refresh - │ (relocate misplaced plans -> docs/plans/, - │ ensure 4 docs stores & bundled skills) - ▼ -Phase 5: Automated Verification & Diagnostic Gate - (agents drift, agents doctor, repo test suite, commit) + +| Topology | Meaning | Action | +|---|---|---| +| `AGENTS.md` regular, `CLAUDE.md -> AGENTS.md` | current | nothing to preserve | +| `AGENTS.md` regular, `CLAUDE.md` regular | both may carry rules | reconcile **both** as Step 4 sources; a legacy repository may hold its only copy of a rule in `CLAUDE.md` | +| `AGENTS.md -> CLAUDE.md`, `CLAUDE.md` regular | **inverted** — the pre-2026-08-19 topology | see below | + +**The inverted case destroys the repository if handled blind.** Running +`rm -f` on `CLAUDE.md` and then `ln -s AGENTS.md CLAUDE.md` deletes the only real file +and leaves `AGENTS.md -> CLAUDE.md -> AGENTS.md`: a symlink loop, and every line +of context gone. Invert deliberately instead: + +```bash +cat CLAUDE.md # read the real content FIRST +rm AGENTS.md # remove the symlink, not the file +# write reconciled content to AGENTS.md as a regular file +rm CLAUDE.md +ln -s AGENTS.md CLAUDE.md ``` +Only create the symlink once the content has a destination. + --- -### Phase 1: Deterministic Preflight & Assessment - -1. **Verify working tree cleanliness**: - ```bash - git status --porcelain - ``` - If untracked or uncommitted changes exist, stop immediately and report to the user or commit/stash before proceeding. - -2. **Inspect repository drift state**: - ```bash - agents drift --json - ``` - Evaluate the JSON output fields: - - `router_state`: `clean_current`, `clean_legacy`, `drifted`, or `missing`. - - `symlink_state`: `ok`, `not_symlink`, `broken`, or `missing`. - - `domain_state`: `ok` or `missing`. - - `skills`: status of bundled skills (`recording-what-you-learn`, `migrating-fleet-context`). - - `docs_stores`: presence of `design`, `plans`, `journal`, `qna`. - - `misplaced_docs`: list of files needing relocation (e.g. `*-plan.md` in `docs/journal/`). - - `diff`: unified diff highlighting custom additions in root `AGENTS.md`. +## Step 6: Skills + +**Embedded skills** (`skills` in the report) are the only ones you touch. + +| state | action | +|---|---| +| `ok` | nothing | +| `missing` | populate from the binary: `agents init`, or `agents update --apply` for `migrating-fleet-context` | +| `clean_legacy` | replace with the current version; the digest proved there are no local edits | +| `customized` | three-way merge, below — except `migrating-fleet-context`, which is `agents`-owned: refresh it with `agents update --apply` and keep no local edits | + +### Three-way merge (`recording-what-you-learn`) + +Name the three inputs before you start; an unnamed merge is a guess: + +- **upstream** — the version embedded in the running binary +- **base** — the canonical or legacy template the local file last matched, + identified by the digest catalog that produced the `clean_legacy` state +- **local** — the working file on disk + +Apply upstream's changes to local, keeping local's additions. If the digest +catalog cannot identify a **base**, there is no three-way merge to perform — +**stop and ask** rather than inventing one. + +**`local_skills` are not yours.** They are the repository's own procedures, +which is exactly what `.agents/skills/` is for. Do not merge, rewrite, move or +delete them. --- -### Phase 2: Feature Branch Isolation +## Step 7: Docs stores and retired stores + +Create any store missing from `docs_stores`, with its `README.md`. `agents init` +scaffolds them non-destructively. -1. Check current branch: - ```bash - git branch --show-current - ``` -2. If on `master`, `main`, or any protected branch, create and switch to a dedicated migration branch: - ```bash - git checkout -b feat/migrate-agent-context - ``` +Relocate each entry in `misplaced_docs`: + +```bash +git mv docs/journal/-plan.md docs/plans/ +``` + +Then fix relative markdown links inside the moved files. + +**`docs/archive/` is immutable and is never a source or a destination.** It +holds executed plans and retired specs, and a record edited to stay true is not +a record. `agents drift` does not report anything under it, and neither should +you relocate out of it. + +### Retired stores + +A repository predating 2026-08-19 may carry `.agents/memory/` and +`.agents/reports/`. Do not relocate them wholesale — much of it is +machine-generated and belongs nowhere. Triage each file: + +| content | destination | +|---|---| +| a topic-indexed finding | `docs/qna/` | +| a design still in force | `docs/design/` | +| an unexecuted plan | `docs/plans/` | +| generated indexes, handoff scaffolding, trace pointers, stale summaries | drop | + +Name everything you dropped in the Step 10 report. --- -### Phase 3: Semantic Un-Nesting & Partitioning - -1. **Inspect and Read Root Context**: - Read root `AGENTS.md` (and `CLAUDE.md` if it is a regular file rather than a symlink). - -2. **Disentangle Domain Knowledge from Router Boilerplate**: - Identify and separate: - - **Router Boilerplate (to be replaced by canonical router)**: - - Old pointer tables to `docs/` or `.agents/memory/`. - - Outdated `agents doctor` single-line instructions. - - Retired commands (such as legacy handoff, review, index, or memory tools). - - **Repository Domain Rules (to be preserved in `.agents/AGENTS.md`)**: - - Tech stack conventions (Go idioms, Python `uv`, Node/TypeScript rules, Fish scripts, etc.). - - Safety constraints and testing mandates (TDD rules, pre-commit checks, required flags). - - Architecture invariants and subsystem documentation. - - Platform workflow guidelines (shadowing platform native planning, PR policies). - -3. **Author or Merge `.agents/AGENTS.md`**: - - If `.agents/AGENTS.md` does not exist: create it with the extracted domain rules organized under standard sections: - ```markdown - # Repository Guidelines & Domain Context - - ## 1. Tech Stack & Standards - - ... - - ## 2. Safety & Verification Mandates - - ... - - ## 3. Workflow & Harness Guidelines - - ... - - ## 4. Architecture & Engineering Standards - - ... - ``` - - If `.agents/AGENTS.md` already exists: perform a semantic 3-way merge. Retain existing content and append any new extracted guidelines into appropriate sections without duplication. - -4. **Replace Root `AGENTS.md` with Canonical Tier 1 Router**: - Overwrite root `AGENTS.md` with the exact canonical template: - - ```markdown - # Agent context - - Durable context for this repo lives in `docs/`. Read it before assuming; - it is the record, and this file is only the pointer to it. - - - `docs/qna/` — answers indexed by the question you would ask again - - `docs/plans/` — implementation plans - - `docs/journal/` — dated record of what happened - - `docs/design/` — the design still in force - - ## Repository Architecture & Guidelines - - Domain engineering guidelines, commenting standards, and safety constraints - are defined in `.agents/AGENTS.md`. - - Repo-specific procedures and skills are located in `.agents/skills/`. - - ## Machine Wiring - `.agents/` holds machine wiring and local skills. A hook cannot install itself - and a missing hook fails silently. - - If the `agents` CLI is installed, run `agents doctor` early and report any warnings before relying on this context. - - If `agents` is not installed on this machine, skip machine wiring checks and adhere directly to the repository instructions above. - - Recording is covered by the global instruction and the `recording-what-you-learn` - skill; it is not repo-specific and is not restated here. - ``` - -5. **Ensure Relative `CLAUDE.md` Symlink**: - Ensure `CLAUDE.md` is a relative symlink pointing to `AGENTS.md`: - ```bash - rm -f CLAUDE.md - ln -s AGENTS.md CLAUDE.md - ``` +## Step 8: Build the traceability table + +Zero rule dropping is not verifiable by looking at the result. Before you ask +for approval, produce a row for every non-boilerplate block in every source file +you read: + +``` +source quote (verbatim) | classification | destination +-------------------------------------------------|----------------|--------------------- +"All tests must pass before commit; use uv, not pip" | domain rule | .agents/AGENTS.md §2 +"Run `agents doctor` early and surface what it says" | boilerplate | replaced by router +"docs/sessions/... halt and resumption plan" | misplaced doc | docs/plans/ +``` + +Every block gets exactly one destination. Then state the count: *N blocks in, N +blocks placed, 0 unaccounted*. That sentence is the evidence for the invariant. +Without the table the invariant is an assertion, and this is precisely the +failure deterministic tools cannot catch for you. --- -### Phase 4: Docs Store Realignment & Skill Refresh - -1. **Relocate Misplaced Documentation**: - Review `misplaced_docs` flagged during Phase 1: - - Move misplaced plan files (`*-plan.md` in `docs/journal/` or legacy archive paths) into `docs/plans/`: - ```bash - git mv docs/journal/-plan.md docs/plans/ - ``` - - Move design specifications out of `docs/journal/` into `docs/design/`. - - Fix any broken internal relative markdown links within relocated files. - -2. **Ensure 4 Documentation Stores Exist**: - Ensure directories and starter `README.md` files exist for all 4 stores: - - `docs/design/README.md` - - `docs/plans/README.md` - - `docs/journal/README.md` - - `docs/qna/README.md` - If any are missing, run `agents init` or scaffold them non-destructively. - -3. **Verify Bundled Skills**: - - Ensure `.agents/skills/recording-what-you-learn/SKILL.md` is present. If customized, perform 3-way merge; if missing, populate from canonical template. - - Ensure `.agents/skills/migrating-fleet-context/SKILL.md` is present and matches the binary's embedded asset. +## Step 9: Verify + +```bash +agents drift # expect exit 0 +agents doctor # expect all five scaffold:* checks ok +``` + +Then the repository's own suite — `go test ./...`, `pytest`, `npm test`, +whatever it uses. A migration that breaks the build is not done. --- -### Phase 5: Automated Verification & Diagnostic Gate - -1. **Verify Context Cleanliness with `agents drift`**: - ```bash - agents drift - ``` - Must return exit code `0` (`Router: clean_current`, `Symlink: ok`, `Domain: ok`). - -2. **Verify Granular Diagnostics with `agents doctor`**: - ```bash - agents doctor - ``` - Verify that all 5 `scaffold:*` checks pass with status `ok`: - - `scaffold:router` (OK) - - `scaffold:symlink` (OK) - - `scaffold:domain` (OK) - - `scaffold:skill-recording` (OK) - - `scaffold:skill-migrating` (OK) - -3. **Run Repository Test Suite**: - Run the repository's native test commands if applicable (e.g. `go test ./...`, `npm test`, `pytest`). Ensure full completion with exit code 0. - -4. **Commit Changes**: - Stage and commit all migration changes: - ```bash - git add AGENTS.md CLAUDE.md .agents/ docs/ - git commit -m "refactor(context): migrate to two-tier agent context and 4-store layout" - ``` +## Step 10: Stop for the human + +**Do not stage anything yet.** Present: + +1. The traceability table from Step 8, with the *N in, N placed, 0 unaccounted* line. +2. `git status --porcelain` and `git diff --stat`. +3. Anything you dropped in Step 7, named. +4. Every stop-and-ask you resolved and how. + +Then wait for an explicit approval. Silence is not approval, and neither is a +clean verification in Step 9. + +## Step 11: Commit and open the pull request + +Only after approval, and staging the exact paths you changed — never `git add .` +and never a broad directory that sweeps up unrelated work: + +```bash +git add AGENTS.md CLAUDE.md .agents/AGENTS.md docs/ +git diff --cached --stat +git commit -m "refactor(context): migrate to two-tier agent context and 4-store layout" +git push -u origin feat/two-tier-context-migration +gh pr create --fill +``` + +In fleet mode, repeat from Step 2 for the next repository. --- +## Stop and ask when + +- A block cannot be confidently classified as domain rule or boilerplate. +- Two destinations are both plausible for the same block. +- `router_state` is `missing` and there is no `CLAUDE.md` to read either. +- A `customized` skill has no identifiable **base**. +- The working tree is dirty, or the repository is mid-rebase or mid-merge. +- `misplaced_docs` names a file whose correct store is genuinely unclear. + +Asking costs one message. Guessing costs a rule nobody notices is gone. + +## Red flags + +- About to remove `CLAUDE.md` without having `stat`-ed `AGENTS.md` first. +- About to apply the `drifted` procedure to a `clean_legacy` router. +- About to commit before presenting the traceability table. +- About to touch a skill listed in `local_skills`. +- About to relocate something out of `docs/archive/`. +- Parsing `agents drift --all --json` as an object. + ## Where this comes from -- `docs/design/2026-08-29-two-tier-context-and-llm-migration-architecture.md` (Architecture specification) -- `docs/plans/2026-08-31-two-tier-context-and-llm-migration-plan.md` (Implementation plan) -- `docs/qna/why-does-agents-init-never-update-existing-instructions.md` (Scaffold immutability rationale) -- `docs/qna/how-does-two-tier-agent-context-prevent-scaffold-drift.md` (Two-Tier isolation rationale) +This skill is owned and maintained by the `agents` CLI, not by the repository it +is sitting in. `agents update --apply` overwrites it from the installed binary, +so local edits here do not survive — if this repository needs different +behaviour, that belongs in `.agents/AGENTS.md`. + +**The tool is the authority on state, not this document.** `agents drift` and +`agents doctor` report what a repository actually is; where they and this skill +disagree, they are right and this copy is stale. Step 0 is how you find out. + +The architecture rationale — why the router is a fixed template, why domain +rules live one level down, what each digest state proves — lives with the +`agents` project's own design documents, upstream. It is deliberately not +restated or linked here: this file is scaffolded into repositories that have no +copy of those documents, and a pointer to a path that does not exist is worse +than no pointer at all. diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 31895600..796ebd40 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -221,7 +221,11 @@ jobs: TestNoUsageLineNamesAFlagThatDoesNotExist TestReadmeCommandBlockIsCurrent TestLivingDocumentsNameOnlyRealCommands - TestHarnessSkillCoversAgentCommands" + TestHarnessSkillCoversAgentCommands + TestMigrationSkillCoversItsSpecifiedProtocol + TestMigrationSkillMatchesEmbeddedAsset + TestMigrationSkillPastesTheCanonicalRouter + TestMigrationSkillNamesNoRepoLocalDocuments" filter="$(echo $tests | tr ' ' '|')" go test -count=1 -v -run "^(${filter})$" ./... | tee /tmp/docs.log # `go test -run` exits 0 when the filter matches NOTHING -- it prints diff --git a/agents/README.md b/agents/README.md index 4783caed..18397646 100644 --- a/agents/README.md +++ b/agents/README.md @@ -7,7 +7,7 @@ A developer harness manager, repository context framework, and transcript record ## Features - **Multi-Harness Wiring**: Automatically configures and keeps in sync hook configurations for Claude Code (`.claude/settings.json`), Codex (`.codex/hooks.json`), and Antigravity (`.agents/hooks.json`). -- **Two-Tier Context & Drift Detection**: Enforces clean separation between canonical machine routing (`AGENTS.md`, `CLAUDE.md`) and repository domain guidelines (`.agents/AGENTS.md`). `agents drift` inspects context layout, canonical diffs, domain context, skills, and misplaced documentation across repositories. +- **Two-Tier Context & Drift Detection**: Enforces clean separation between canonical machine routing (`AGENTS.md`, `CLAUDE.md`) and repository domain guidelines (`.agents/AGENTS.md`). `agents drift` inspects context layout, canonical diffs, domain context, bundled skills, and misplaced documentation across repositories. Repository-specific skills under `.agents/skills/` are listed as `local_skills` and never classified as drift. - **Fleet Maintenance & Skill Refresh**: `agents update` rewires machine hooks across registered repositories, refreshes the authoritative `migrating-fleet-context` skill, and emits advisory notices if any repository exhibits context drift. - **Durable Transcript Caching**: Captures and preserves subagent conversation transcripts before harnesses delete them, storing them in `.agents/transcripts/` with retention and size bounding. - **Repository Guardrails & Pre-Commit Secret Scanning**: Integrates `gitleaks` into `agents guard --staged` to catch secret leaks before commit. @@ -112,8 +112,8 @@ When installed via Homebrew or downloaded from releases, `agents` operates as a - `scaffold:router`: Validates that root `AGENTS.md` matches the canonical router template without unpartitioned domain drift. - `scaffold:symlink`: Verifies that `CLAUDE.md` is a valid relative symlink to `AGENTS.md`. - `scaffold:domain`: Confirms presence of `.agents/AGENTS.md` for repository-specific domain rules. - - `scaffold:skill-recording`: Checks status and customization state of `.agents/skills/recording-what-you-learn/`. - - `scaffold:skill-migrating`: Checks status and customization state of `.agents/skills/migrating-fleet-context/`. + - `scaffold:skill-recording`: Checks status and customization state of `.agents/skills/recording-what-you-learn/`. This skill is repository-customizable, so local edits are reported without warning. + - `scaffold:skill-migrating`: Checks that `.agents/skills/migrating-fleet-context/` matches the installed binary. This skill is `agents`-owned, so any divergence is staleness and warns; run `agents update --apply` to refresh it. - Git hook dispatching executes repository-level hooks and built-in guards. ### 2. Dotfiles Operator Mode diff --git a/agents/cmd_drift.go b/agents/cmd_drift.go index ef78c167..42e2f224 100644 --- a/agents/cmd_drift.go +++ b/agents/cmd_drift.go @@ -168,6 +168,12 @@ func printDriftReport(w io.Writer, rep drift.DriftReport) { for _, name := range skillNames { fmt.Fprintf(w, " %-26s %s\n", name+":", rep.Skills[name]) } + if len(rep.LocalSkills) > 0 { + fmt.Fprintln(w, " Local skills (not managed by agents):") + for _, name := range rep.LocalSkills { + fmt.Fprintf(w, " %s\n", name) + } + } fmt.Fprintln(w, " Docs stores:") var storeNames []string for name := range rep.DocsStores { diff --git a/agents/docs_test.go b/agents/docs_test.go index 5e76419d..0ce7e517 100644 --- a/agents/docs_test.go +++ b/agents/docs_test.go @@ -8,6 +8,8 @@ import ( "regexp" "strings" "testing" + + "github.com/nilbot/dotfiles/agents/internal/scaffold" ) const ( @@ -166,3 +168,146 @@ func TestHarnessSkillCoversAgentCommands(t *testing.T) { t.Errorf("agent-facing commands absent from the skill:\n %s", strings.Join(missing, "\n ")) } } + +// The migrating-fleet-context skill is the one deliverable of the two-tier work +// that is prose rather than code, and it shipped contradicting the tooling it +// drives: no fleet mode, one procedure for four router states, an unconditional +// `rm -f CLAUDE.md`, and a commit with no approval gate. +// +// TestLivingDocumentsNameOnlyRealCommands already scans this file, but only in +// one direction -- it catches a command that does not exist, never a required +// command that is absent. That asymmetry is exactly why the omissions survived +// review. This is the other direction, the way TestHarnessSkillCoversAgentCommands +// is the other direction for claude/skills/agents-tool. +// +// See docs/journal/2026-09-01-why-the-migration-skill-shipped-hollow.md and +// Amendment 1 of docs/design/2026-08-29-two-tier-context-and-llm-migration-architecture.md. +func TestMigrationSkillCoversItsSpecifiedProtocol(t *testing.T) { + root := task18RepoRoot(t) + rel := filepath.Join(".agents", "skills", "migrating-fleet-context", "SKILL.md") + data, err := os.ReadFile(filepath.Join(root, rel)) + if err != nil { + t.Fatalf("the migration skill is missing: %v", err) + } + text := string(data) + + // Required by the amended design section 7. Each entry names the section + // that requires it, so a future edit that drops one can find out why. + required := []struct{ substr, why string }{ + {"agents ls", "7.1.2 target discovery over the registered fleet"}, + {"agents drift --json", "7.1.2 single-repository inspection"}, + {"agents drift --all --json", "7.6 fleet inspection, which returns an array"}, + {"agents update --apply", "7.1.1 self-currency check before trusting itself"}, + {"agents doctor", "7.1.7 verification gate"}, + {"clean_current", "7.3 router state table"}, + {"clean_legacy", "7.3 router state table"}, + {"drifted", "7.3 router state table"}, + {"missing", "7.3 router state table"}, + {"upstream", "7.3 named merge sources"}, + {"base", "7.3 named merge sources"}, + {"local", "7.3 named merge sources"}, + {"stop and ask", "7.5 unclassifiable blocks are not a judgement call"}, + {"traceability", "7.5 evidence for zero rule dropping"}, + {"gh pr create", "7.1.9 the migration ends in a pull request"}, + } + lower := strings.ToLower(text) + for _, r := range required { + if !strings.Contains(lower, strings.ToLower(r.substr)) { + t.Errorf("%s does not mention %q, required by %s", rel, r.substr, r.why) + } + } + + // Forbidden: the unconditional symlink replacement. On the pre-2026-08-19 + // topology (AGENTS.md -> CLAUDE.md, content in CLAUDE.md) this deletes the + // only real file and leaves AGENTS.md -> CLAUDE.md -> AGENTS.md, a symlink + // loop with every line of repository context gone. playground/desktop_pet + // was in exactly that state on 2026-09-01. + if strings.Contains(text, "rm -f CLAUDE.md") { + t.Errorf("%s still carries the unconditional `rm -f CLAUDE.md`; design 7.4 requires "+ + "stat-ing both root paths and preserving content before the symlink", rel) + } + + // Forbidden: a command that relocates out of the immutable archive. Prose + // forbidding the move is fine and expected; a `git mv` with an archive + // source is not. + for _, line := range strings.Split(text, "\n") { + if strings.Contains(line, "git mv") && strings.Contains(line, "docs/archive/") { + t.Errorf("%s relocates out of docs/archive/, which is immutable: %q", rel, strings.TrimSpace(line)) + } + } +} + +// The skill exists twice: the repository's own copy and the embedded asset the +// binary scaffolds into every other repository. Nothing bound them together, +// so they could diverge silently and the fleet would be migrated by whichever +// copy the reader happened to open. +func TestMigrationSkillMatchesEmbeddedAsset(t *testing.T) { + root := task18RepoRoot(t) + repoCopy, err := os.ReadFile(filepath.Join(root, ".agents", "skills", "migrating-fleet-context", "SKILL.md")) + if err != nil { + t.Fatalf("repository copy: %v", err) + } + asset, err := os.ReadFile(filepath.Join(root, "agents", "internal", "scaffold", + "assets", "skills", "migrating-fleet-context", "SKILL.md")) + if err != nil { + t.Fatalf("embedded asset: %v", err) + } + if !bytes.Equal(repoCopy, asset) { + t.Errorf(".agents/skills/migrating-fleet-context/SKILL.md and its embedded asset differ; "+ + "they are scaffolded into every other repository from the asset, so they must be identical "+ + "(repo copy %d bytes, asset %d bytes)", len(repoCopy), len(asset)) + } +} + +// The skill pastes the canonical router so a migrating agent can restore it +// without a second tool. That is a second copy of DefaultAgentsMD, and the two +// ship in the same binary -- so nothing except this test stops a change to one +// from silently leaving the other behind, telling every migrated repository to +// adopt a router the tool then reports as drifted. +func TestMigrationSkillPastesTheCanonicalRouter(t *testing.T) { + root := task18RepoRoot(t) + data, err := os.ReadFile(filepath.Join(root, ".agents", "skills", + "migrating-fleet-context", "SKILL.md")) + if err != nil { + t.Fatalf("the migration skill is missing: %v", err) + } + + const fence = "```markdown\n# Agent context\n" + i := strings.Index(string(data), fence) + if i < 0 { + t.Fatal("the skill no longer pastes a canonical router block; if that is deliberate, " + + "delete this test, and if it is not, restore the block") + } + body := string(data)[i+len("```markdown\n"):] + j := strings.Index(body, "\n```") + if j < 0 { + t.Fatal("unterminated router code fence in the skill") + } + pasted := body[:j+1] + + if pasted != scaffold.DefaultAgentsMD { + t.Errorf("the router pasted into the skill does not match scaffold.DefaultAgentsMD\n"+ + "pasted %d bytes, canonical %d bytes", len(pasted), len(scaffold.DefaultAgentsMD)) + } +} + +// The skill is scaffolded into other people's repositories, which do not have +// this repository's documents. A "where this comes from" list of dated design +// and Q&A paths reads as a working reference and resolves to nothing there -- +// worse than no pointer, because an agent will try to follow it. +// +// Bare dates describing an era ("the pre-2026-08-19 topology") are fine; a dated +// *filename* is a path into this repository and is not. +func TestMigrationSkillNamesNoRepoLocalDocuments(t *testing.T) { + root := task18RepoRoot(t) + rel := filepath.Join(".agents", "skills", "migrating-fleet-context", "SKILL.md") + data, err := os.ReadFile(filepath.Join(root, rel)) + if err != nil { + t.Fatalf("the migration skill is missing: %v", err) + } + datedDoc := regexp.MustCompile(`[0-9]{4}-[0-9]{2}-[0-9]{2}-[A-Za-z0-9-]+\.md`) + for _, m := range datedDoc.FindAllString(string(data), -1) { + t.Errorf("%s names %q, a document that exists only in this repository; "+ + "the skill ships into repositories that have no copy of it", rel, m) + } +} diff --git a/agents/internal/doctor/doctor.go b/agents/internal/doctor/doctor.go index 015bd3d5..c554ab72 100644 --- a/agents/internal/doctor/doctor.go +++ b/agents/internal/doctor/doctor.go @@ -1118,10 +1118,15 @@ func checkScaffold(repoRoot string) []Check { Detail: ".agents/skills/migrating-fleet-context/ matches legacy template", }) case string(drift.ComponentCustomized): + // Unlike recording-what-you-learn, this skill is authoritative and + // agents-owned (design 5.1): a local divergence is staleness, not a + // customization to respect. Reporting it ok let a skill carrying + // obsolete migration instructions pass its own health check. checks = append(checks, Check{ Name: "scaffold:skill-migrating", - Status: OK, - Detail: ".agents/skills/migrating-fleet-context/ carries repository customizations", + Status: Warn, + Detail: ".agents/skills/migrating-fleet-context/ does not match the installed binary", + Remedy: "run 'agents update --apply' to refresh infrastructure skills", }) default: checks = append(checks, Check{ diff --git a/agents/internal/doctor/doctor_test.go b/agents/internal/doctor/doctor_test.go index d4ecccbe..c90aa78b 100644 --- a/agents/internal/doctor/doctor_test.go +++ b/agents/internal/doctor/doctor_test.go @@ -1390,9 +1390,16 @@ func TestCheckScaffoldGranularChecks(t *testing.T) { if err := os.WriteFile(skillPath, []byte("# customized migration skill\n"), 0o644); err != nil { t.Fatal(err) } + // migrating-fleet-context is 100% agents-owned (design 5.1), so a copy + // that does not match the embedded asset is stale, not customized -- + // and a stale migration skill reported `ok` is a skill that passes its + // own health check while carrying obsolete instructions. cCustom := checkByName(t, checkScaffold(rootCustom), "scaffold:skill-migrating") - if cCustom.Status != OK || cCustom.Detail != ".agents/skills/migrating-fleet-context/ carries repository customizations" { - t.Errorf("customized migrating = %+v", cCustom) + if cCustom.Status != Warn || cCustom.Detail != ".agents/skills/migrating-fleet-context/ does not match the installed binary" { + t.Errorf("stale migrating = %+v", cCustom) + } + if cCustom.Remedy != "run 'agents update --apply' to refresh infrastructure skills" { + t.Errorf("stale migrating remedy = %q", cCustom.Remedy) } // Missing diff --git a/agents/internal/drift/drift.go b/agents/internal/drift/drift.go index 6f47206c..a7f505ce 100644 --- a/agents/internal/drift/drift.go +++ b/agents/internal/drift/drift.go @@ -17,7 +17,8 @@ type DriftReport struct { RouterState RouterState `json:"router_state"` SymlinkState string `json:"symlink_state"` // "ok" | "broken" | "not_symlink" | "missing" DomainState string `json:"domain_state"` // "ok" | "missing" - Skills map[string]string `json:"skills"` // skill_name -> ComponentState + Skills map[string]string `json:"skills"` // embedded skill_name -> ComponentState + LocalSkills []string `json:"local_skills"` // repo-specific skills: listed, never judged DocsStores map[string]bool `json:"docs_stores"` // design, plans, journal, qna MisplacedDocs []string `json:"misplaced_docs"` // e.g. plans living in docs/journal/ Diff string `json:"diff,omitempty"` // Unified diff against canonical router @@ -28,6 +29,7 @@ func InspectRepo(root string) (DriftReport, error) { report := DriftReport{ RepoPath: root, Skills: make(map[string]string), + LocalSkills: []string{}, DocsStores: map[string]bool{"design": false, "plans": false, "journal": false, "qna": false}, MisplacedDocs: []string{}, } @@ -109,6 +111,11 @@ func InspectRepo(root string) (DriftReport, error) { } } + // Repository-specific skills are listed, never classified. `.agents/skills/` + // is where design section 2 says they belong, so `agents` owning the whole + // directory made a repository dirty for using the feature as intended -- + // and no migration could clear it, because there is nothing to fix. The + // tool judges only what it embeds. skillsDir := filepath.Join(root, ".agents", "skills") if entries, err := os.ReadDir(skillsDir); err == nil { for _, e := range entries { @@ -116,15 +123,16 @@ func InspectRepo(root string) (DriftReport, error) { continue } name := e.Name() - if _, already := report.Skills[name]; already { + if _, tracked := report.Skills[name]; tracked { continue } skillFile := filepath.Join(skillsDir, name, "SKILL.md") if _, err := os.Stat(skillFile); err == nil { - report.Skills[name] = string(ComponentCustomized) + report.LocalSkills = append(report.LocalSkills, name) } } } + sort.Strings(report.LocalSkills) // 5. Docs stores inspection for store := range report.DocsStores { @@ -148,6 +156,14 @@ func InspectRepo(root string) (DriftReport, error) { relSlash := filepath.ToSlash(rel) name := d.Name() + // docs/archive/ is immutable by repository rule, so nothing in it + // can be "misplaced": a report here is an instruction to move a + // file that must not move. Excluding it keeps this classifier and + // the migrating-fleet-context skill agreeing on one definition. + if strings.HasPrefix(relSlash, "docs/archive/") { + return nil + } + if strings.HasSuffix(name, "-plan.md") && !strings.HasPrefix(relSlash, "docs/plans/") { report.MisplacedDocs = append(report.MisplacedDocs, relSlash) } diff --git a/agents/internal/drift/drift_test.go b/agents/internal/drift/drift_test.go index 1c509db6..c7b6907c 100644 --- a/agents/internal/drift/drift_test.go +++ b/agents/internal/drift/drift_test.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strings" "testing" @@ -258,7 +259,10 @@ func TestInspectMisplacedDocs(t *testing.T) { if err := os.MkdirAll(filepath.Join(dir, "docs", "archive", "plans"), 0o755); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(dir, "docs", "archive", "plans", "2026-08-30-old-plan.md"), []byte("# Misplaced Plan in Archive"), 0o644); err != nil { + // Archived, and therefore NOT misplaced: docs/archive/ is immutable, so + // this file is deliberately kept out of wantMisplaced below. See + // Amendment 1 of the 2026-08-29 two-tier design. + if err := os.WriteFile(filepath.Join(dir, "docs", "archive", "plans", "2026-08-30-old-plan.md"), []byte("# Archived Plan"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "docs", "journal", "2026-08-30-test-design.md"), []byte("# Misplaced Design in Journal"), 0o644); err != nil { @@ -271,7 +275,6 @@ func TestInspectMisplacedDocs(t *testing.T) { } wantMisplaced := []string{ - "docs/archive/plans/2026-08-30-old-plan.md", "docs/journal/2026-08-30-test-design.md", "docs/journal/2026-08-30-test-plan.md", } @@ -364,8 +367,13 @@ func TestInspectSkillStates(t *testing.T) { if err != nil { t.Fatal(err) } - if report.Skills["my-custom-skill"] != string(ComponentCustomized) { - t.Errorf("got my-custom-skill state %q, want customized", report.Skills["my-custom-skill"]) + // A repository-specific skill is listed, not classified: the tool owns + // only the skills it embeds. See TestRepoSpecificSkillsAreListedNotJudged. + if _, judged := report.Skills["my-custom-skill"]; judged { + t.Errorf("my-custom-skill was classified as %q; it should only be listed", report.Skills["my-custom-skill"]) + } + if !slices.Contains(report.LocalSkills, "my-custom-skill") { + t.Errorf("local_skills = %v, want my-custom-skill listed", report.LocalSkills) } }) } @@ -426,3 +434,88 @@ func TestInspectEmptyRepo(t *testing.T) { } } } + +// docs/archive/ is strictly immutable: `.agents/AGENTS.md` says so, and +// docs/design/README.md says nothing in it is rewritten to stay true. A file +// reported as misplaced is a file the migration skill is told to `git mv`, so +// reporting an archived plan is an instruction to violate that rule. +func TestMisplacedDocsExcludesArchive(t *testing.T) { + dir := newRepo(t) + if err := scaffold.Create(dir, false); err != nil { + t.Fatal(err) + } + mustWrite := func(rel, body string) { + t.Helper() + p := filepath.Join(dir, filepath.FromSlash(rel)) + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(p, []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } + // Archived: immutable, must never be reported. + mustWrite("docs/archive/plans/2026-08-01-old-plan.md", "# old\n") + mustWrite("docs/archive/specs/2026-08-01-old-design.md", "# old\n") + // Live stores: genuinely misplaced, must still be reported. + mustWrite("docs/journal/2026-08-30-stray-plan.md", "# stray\n") + + report, err := InspectRepo(dir) + if err != nil { + t.Fatalf("InspectRepo failed: %v", err) + } + for _, m := range report.MisplacedDocs { + if strings.HasPrefix(m, "docs/archive/") { + t.Errorf("archive file reported as misplaced: %s", m) + } + } + // The positive control: without this the check passes on a classifier that + // reports nothing at all. + if !slices.Contains(report.MisplacedDocs, "docs/journal/2026-08-30-stray-plan.md") { + t.Errorf("live-store misplaced plan was not reported; got %v", report.MisplacedDocs) + } +} + +// `.agents/skills/` is where repository-specific skills are supposed to live +// (design section 2). The tool owns only the skills it embeds; classifying every +// other directory as `customized` made isDriftClean report a repository as +// dirty for using the feature exactly as designed, and no migration could ever +// clear it. playground/autogo-mlx carries two such skills and could not report +// clean on 2026-09-01. +func TestRepoSpecificSkillsAreListedNotJudged(t *testing.T) { + dir := newRepo(t) + if err := scaffold.Create(dir, false); err != nil { + t.Fatal(err) + } + for _, name := range []string{"human-ranked-sft", "llm-in-the-loop-rl-discovery"} { + p := filepath.Join(dir, ".agents", "skills", name) + if err := os.MkdirAll(p, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(p, "SKILL.md"), []byte("# "+name+"\n"), 0o644); err != nil { + t.Fatal(err) + } + } + + report, err := InspectRepo(dir) + if err != nil { + t.Fatalf("InspectRepo failed: %v", err) + } + + // Not judged: absent from the state map the cleanliness check reads. + for _, name := range []string{"human-ranked-sft", "llm-in-the-loop-rl-discovery"} { + if state, ok := report.Skills[name]; ok { + t.Errorf("repo-specific skill %s classified as %q; the tool owns only the skills it embeds", name, state) + } + } + // Still tracked: the embedded skills are judged as before. Without this the + // test would pass on an inspector that classified nothing at all. + if report.Skills["recording-what-you-learn"] != string(ComponentOK) { + t.Errorf("embedded skill state = %q, want ok", report.Skills["recording-what-you-learn"]) + } + // Listed: a migrating agent needs to know they exist so it leaves them alone. + if !slices.Contains(report.LocalSkills, "human-ranked-sft") || + !slices.Contains(report.LocalSkills, "llm-in-the-loop-rl-discovery") { + t.Errorf("local_skills = %v, want both repo-specific skills listed", report.LocalSkills) + } +} diff --git a/agents/internal/scaffold/assets/skills/migrating-fleet-context/SKILL.md b/agents/internal/scaffold/assets/skills/migrating-fleet-context/SKILL.md index fd17f456..77447642 100644 --- a/agents/internal/scaffold/assets/skills/migrating-fleet-context/SKILL.md +++ b/agents/internal/scaffold/assets/skills/migrating-fleet-context/SKILL.md @@ -1,240 +1,349 @@ --- name: migrating-fleet-context -description: Safely migrates repository agent context from legacy or drifted structures to the Two-Tier Agent Context architecture (Tier 1 router + Tier 2 domain guidelines + 4-store docs layout) using LLM semantic merge on a dedicated feature branch. +description: Use when `agents doctor` reports a `scaffold:*` warning, `agents drift` exits non-zero, or a repository keeps domain rules in a root `AGENTS.md`/`CLAUDE.md`, lacks `.agents/AGENTS.md`, is missing `docs/` stores, carries plans or designs in the wrong store, or has a bundled skill that no longer matches the installed binary. --- # Migrating Fleet Context -Migrates repository agent context from legacy single-file or drifted structures to the **Two-Tier Agent Context Architecture** with **4-store documentation layout**. +Moves a repository onto the Two-Tier Agent Context architecture: a canonical +root router at `AGENTS.md`, domain rules in `.agents/AGENTS.md`, durable +knowledge in the four `docs/` stores. -## Why this skill exists +**The deterministic tools know the states; only you can read the prose.** +`agents drift` tells you exactly what is wrong and never guesses at meaning. +Your job is the part it cannot do — deciding which sentence is a repository's +own rule and which is scaffold boilerplate — and proving you moved every one. -Earlier iterations of repository scaffolding treated `AGENTS.md` (or `CLAUDE.md`) as a single monolithic context file. Over time, repositories accumulated human-authored domain rules, test protocols, and language conventions mixed directly with machine wiring and router instructions. +**Nothing is staged or committed until a human approves the diff.** -Deterministic tools (`agents init`, `agents update`) cannot safely disentangle reordered, edited, or appended domain rules from router boilerplate without risking silent deletion of repository guidelines. Conversely, leaving legacy files unmaintained freezes repositories on obsolete scaffold conventions. +--- + +## Step 0: Refresh yourself first + +This skill is an `agents`-owned asset embedded in the binary. The copy you are +reading can be older than the tool you are about to run. + +```bash +agents doctor +``` -This skill provides an authoritative, LLM-in-the-loop migration protocol to safely partition context into two isolated tiers: -1. **Tier 1 (Root Router)**: Standardized root `AGENTS.md` (and relative symlink `CLAUDE.md -> AGENTS.md`) acting solely as a durable docs and machine wiring pointer. -2. **Tier 2 (Domain Context)**: Dedicated repository-specific engineering guidelines, test mandates, architectural invariants, and safety constraints in `.agents/AGENTS.md`. -3. **4-Store Documentation**: Durable repository knowledge in `docs/` partitioned across `design/`, `plans/`, `journal/`, and `qna/`. +If `scaffold:skill-migrating` is anything but `ok`, your instructions are stale: + +```bash +agents update --apply +``` + +Then re-read this file before continuing. Do not migrate from a stale copy. --- -## When to Run +## Step 1: Pick the mode and read the right JSON shape + +The two invocations do not return the same type. A parser written for one +breaks on the other. + +| Mode | Command | Returns | +|---|---|---| +| Single repository | `agents drift --json` | one **object** | +| Fleet | `agents ls`, then `agents drift --all --json` | an **array** of objects | + +Fleet mode migrates one repository at a time, each on its own branch. Skip and +name in the final report, rather than migrating: + +- registry entries reported `missing` or `unknown` +- any repository whose working tree is dirty +- any repository already `clean_current` with every other field `ok` + +Fields to read from each report: + +| field | use | +|---|---| +| `router_state` | picks your procedure — see Step 4 | +| `symlink_state` | `ok` \| `not_symlink` \| `broken` \| `missing` — see Step 5 | +| `domain_state` | `ok` \| `missing` — whether `.agents/AGENTS.md` exists | +| `skills` | embedded skills only: `ok` \| `clean_legacy` \| `customized` \| `missing` | +| `local_skills` | the repository's own skills. **Never touch these.** | +| `docs_stores` | which of `design`/`plans`/`journal`/`qna` exist | +| `misplaced_docs` | plans and designs in the wrong live store | +| `diff` | unified diff of the root router against canonical | + +--- + +## Step 2: Preflight + +```bash +git status --porcelain +``` + +Any output at all: stop. Report it and let the human commit or stash. A dirty +tree makes the approval diff in Step 10 unreadable, which is the one artifact +this whole procedure exists to produce. + +## Step 3: Branch isolation -Run this skill when: -- `agents doctor` reports warnings or info for any `scaffold:*` check (`scaffold:router`, `scaffold:symlink`, `scaffold:domain`, `scaffold:skill-recording`, `scaffold:skill-migrating`). -- `agents drift` or `agents drift --json` detects `router_state: clean_legacy`, `router_state: drifted`, `domain_state: missing`, or `misplaced_docs`. -- Migrating a repository from legacy monolithic `AGENTS.md` or `CLAUDE.md` to Two-Tier context. -- Realigning misplaced plan or design documents across `docs/`. +```bash +git branch --show-current +git checkout -b feat/two-tier-context-migration +``` + +Never migrate on `master`, `main`, or any protected branch. --- -## Invariants & Safety Constraints +## Step 4: Reconcile the root, one procedure per state + +Four states, four different correct actions. Treating them alike is the single +most damaging thing you can do here — it partitions a file that has nothing to +partition, or invents content for a file that has none. + +| `router_state` | What it means | What to do | +|---|---|---| +| `clean_current` | matches the installed binary's canonical router | nothing. Do not "improve" it. | +| `clean_legacy` | a known older canonical template, **no repository content** | replace wholesale with the canonical router. There is nothing to extract — the digest already proved that. | +| `drifted` | canonical text plus, or reworded into, repository content | semantic reconcile, below | +| `missing` | no root `AGENTS.md` at all | do not invent one. Go to Step 5 and read `CLAUDE.md`. If that is absent too, **stop and ask** what this repository's rules are. | + +### Semantic reconcile (`drifted` only) + +Two inputs — the current root file and the canonical router — and one output +per block. This is not a three-way merge; there is no base. Read the `diff` +field to see what the repository added. + +Classify **every** block: + +- **Boilerplate**, to be replaced by the canonical router: pointer tables to + `docs/` or `.agents/memory/`, old single-line `agents doctor` instructions, + references to retired commands (handoff writing, `save`, `index`, and the + memory tooling — none of which the CLI defines any more). +- **Domain rules**, to be preserved in `.agents/AGENTS.md`: tech stack + conventions, test mandates, safety constraints, architecture invariants, PR + and workflow policy, commenting standards. -1. **Zero Rule Dropping**: Every domain guideline, architectural rule, test mandate, commenting standard, and safety constraint in the existing files MUST be preserved in `.agents/AGENTS.md`. Never discard domain context during migration. -2. **Feature Branch Isolation**: Never perform migrations directly on `master`, `main`, or protected branches. Always execute on a dedicated feature branch (`feat/migrate-agent-context` or `feat/two-tier-context-migration`). -3. **Deterministic Preflight Cleanliness**: Require a clean working tree (`git status --porcelain`) before modifying any files. -4. **Canonical Router Exactness**: Root `AGENTS.md` must match the canonical Tier 1 router template verbatim. -5. **Relative Symlink**: `CLAUDE.md` must be a relative symlink pointing to `AGENTS.md` (`CLAUDE.md -> AGENTS.md`). -6. **Archive Immutability**: Active work and modern plans belong in `docs/plans/`. Never write new plans or active context to `docs/archive/`. +Write the domain rules to `.agents/AGENTS.md`. If it already exists, append +into the matching section without duplicating what is there. Then overwrite the +root `AGENTS.md` with the canonical router verbatim: + +```markdown +# Agent context + +Durable context for this repo lives in `docs/`. Read it before assuming; +it is the record, and this file is only the pointer to it. + +- `docs/qna/` — answers indexed by the question you would ask again +- `docs/plans/` — implementation plans +- `docs/journal/` — dated record of what happened +- `docs/design/` — the design still in force + +## Repository Architecture & Guidelines +- Domain engineering guidelines, commenting standards, and safety constraints + are defined in `.agents/AGENTS.md`. +- Repo-specific procedures and skills are located in `.agents/skills/`. + +## Machine Wiring +`.agents/` holds machine wiring and local skills. A hook cannot install itself +and a missing hook fails silently. +- If the `agents` CLI is installed, run `agents doctor` early and report any warnings before relying on this context. +- If `agents` is not installed on this machine, skip machine wiring checks and adhere directly to the repository instructions above. + +Recording is covered by the global instruction and the `recording-what-you-learn` +skill; it is not repo-specific and is not restated here. +``` --- -## Phased Migration Workflow +## Step 5: The two root files, before any symlink -Follow all five phases sequentially. Do not skip phases or verification gates. +`CLAUDE.md` is about to become a symlink, which destroys whatever it holds. +`stat` both paths before you read or write either one. +```bash +ls -la AGENTS.md CLAUDE.md ``` -Phase 1: Deterministic Preflight & Assessment - │ (agents drift --json, git status --porcelain) - ▼ -Phase 2: Feature Branch Isolation - │ (git checkout -b feat/migrate-agent-context) - ▼ -Phase 3: Semantic Un-Nesting & Partitioning - │ (extract domain rules -> .agents/AGENTS.md, - │ restore root AGENTS.md router, relink CLAUDE.md) - ▼ -Phase 4: Docs Store Realignment & Skill Refresh - │ (relocate misplaced plans -> docs/plans/, - │ ensure 4 docs stores & bundled skills) - ▼ -Phase 5: Automated Verification & Diagnostic Gate - (agents drift, agents doctor, repo test suite, commit) + +| Topology | Meaning | Action | +|---|---|---| +| `AGENTS.md` regular, `CLAUDE.md -> AGENTS.md` | current | nothing to preserve | +| `AGENTS.md` regular, `CLAUDE.md` regular | both may carry rules | reconcile **both** as Step 4 sources; a legacy repository may hold its only copy of a rule in `CLAUDE.md` | +| `AGENTS.md -> CLAUDE.md`, `CLAUDE.md` regular | **inverted** — the pre-2026-08-19 topology | see below | + +**The inverted case destroys the repository if handled blind.** Running +`rm -f` on `CLAUDE.md` and then `ln -s AGENTS.md CLAUDE.md` deletes the only real file +and leaves `AGENTS.md -> CLAUDE.md -> AGENTS.md`: a symlink loop, and every line +of context gone. Invert deliberately instead: + +```bash +cat CLAUDE.md # read the real content FIRST +rm AGENTS.md # remove the symlink, not the file +# write reconciled content to AGENTS.md as a regular file +rm CLAUDE.md +ln -s AGENTS.md CLAUDE.md ``` +Only create the symlink once the content has a destination. + --- -### Phase 1: Deterministic Preflight & Assessment - -1. **Verify working tree cleanliness**: - ```bash - git status --porcelain - ``` - If untracked or uncommitted changes exist, stop immediately and report to the user or commit/stash before proceeding. - -2. **Inspect repository drift state**: - ```bash - agents drift --json - ``` - Evaluate the JSON output fields: - - `router_state`: `clean_current`, `clean_legacy`, `drifted`, or `missing`. - - `symlink_state`: `ok`, `not_symlink`, `broken`, or `missing`. - - `domain_state`: `ok` or `missing`. - - `skills`: status of bundled skills (`recording-what-you-learn`, `migrating-fleet-context`). - - `docs_stores`: presence of `design`, `plans`, `journal`, `qna`. - - `misplaced_docs`: list of files needing relocation (e.g. `*-plan.md` in `docs/journal/`). - - `diff`: unified diff highlighting custom additions in root `AGENTS.md`. +## Step 6: Skills + +**Embedded skills** (`skills` in the report) are the only ones you touch. + +| state | action | +|---|---| +| `ok` | nothing | +| `missing` | populate from the binary: `agents init`, or `agents update --apply` for `migrating-fleet-context` | +| `clean_legacy` | replace with the current version; the digest proved there are no local edits | +| `customized` | three-way merge, below — except `migrating-fleet-context`, which is `agents`-owned: refresh it with `agents update --apply` and keep no local edits | + +### Three-way merge (`recording-what-you-learn`) + +Name the three inputs before you start; an unnamed merge is a guess: + +- **upstream** — the version embedded in the running binary +- **base** — the canonical or legacy template the local file last matched, + identified by the digest catalog that produced the `clean_legacy` state +- **local** — the working file on disk + +Apply upstream's changes to local, keeping local's additions. If the digest +catalog cannot identify a **base**, there is no three-way merge to perform — +**stop and ask** rather than inventing one. + +**`local_skills` are not yours.** They are the repository's own procedures, +which is exactly what `.agents/skills/` is for. Do not merge, rewrite, move or +delete them. --- -### Phase 2: Feature Branch Isolation +## Step 7: Docs stores and retired stores + +Create any store missing from `docs_stores`, with its `README.md`. `agents init` +scaffolds them non-destructively. -1. Check current branch: - ```bash - git branch --show-current - ``` -2. If on `master`, `main`, or any protected branch, create and switch to a dedicated migration branch: - ```bash - git checkout -b feat/migrate-agent-context - ``` +Relocate each entry in `misplaced_docs`: + +```bash +git mv docs/journal/-plan.md docs/plans/ +``` + +Then fix relative markdown links inside the moved files. + +**`docs/archive/` is immutable and is never a source or a destination.** It +holds executed plans and retired specs, and a record edited to stay true is not +a record. `agents drift` does not report anything under it, and neither should +you relocate out of it. + +### Retired stores + +A repository predating 2026-08-19 may carry `.agents/memory/` and +`.agents/reports/`. Do not relocate them wholesale — much of it is +machine-generated and belongs nowhere. Triage each file: + +| content | destination | +|---|---| +| a topic-indexed finding | `docs/qna/` | +| a design still in force | `docs/design/` | +| an unexecuted plan | `docs/plans/` | +| generated indexes, handoff scaffolding, trace pointers, stale summaries | drop | + +Name everything you dropped in the Step 10 report. --- -### Phase 3: Semantic Un-Nesting & Partitioning - -1. **Inspect and Read Root Context**: - Read root `AGENTS.md` (and `CLAUDE.md` if it is a regular file rather than a symlink). - -2. **Disentangle Domain Knowledge from Router Boilerplate**: - Identify and separate: - - **Router Boilerplate (to be replaced by canonical router)**: - - Old pointer tables to `docs/` or `.agents/memory/`. - - Outdated `agents doctor` single-line instructions. - - Retired commands (such as legacy handoff, review, index, or memory tools). - - **Repository Domain Rules (to be preserved in `.agents/AGENTS.md`)**: - - Tech stack conventions (Go idioms, Python `uv`, Node/TypeScript rules, Fish scripts, etc.). - - Safety constraints and testing mandates (TDD rules, pre-commit checks, required flags). - - Architecture invariants and subsystem documentation. - - Platform workflow guidelines (shadowing platform native planning, PR policies). - -3. **Author or Merge `.agents/AGENTS.md`**: - - If `.agents/AGENTS.md` does not exist: create it with the extracted domain rules organized under standard sections: - ```markdown - # Repository Guidelines & Domain Context - - ## 1. Tech Stack & Standards - - ... - - ## 2. Safety & Verification Mandates - - ... - - ## 3. Workflow & Harness Guidelines - - ... - - ## 4. Architecture & Engineering Standards - - ... - ``` - - If `.agents/AGENTS.md` already exists: perform a semantic 3-way merge. Retain existing content and append any new extracted guidelines into appropriate sections without duplication. - -4. **Replace Root `AGENTS.md` with Canonical Tier 1 Router**: - Overwrite root `AGENTS.md` with the exact canonical template: - - ```markdown - # Agent context - - Durable context for this repo lives in `docs/`. Read it before assuming; - it is the record, and this file is only the pointer to it. - - - `docs/qna/` — answers indexed by the question you would ask again - - `docs/plans/` — implementation plans - - `docs/journal/` — dated record of what happened - - `docs/design/` — the design still in force - - ## Repository Architecture & Guidelines - - Domain engineering guidelines, commenting standards, and safety constraints - are defined in `.agents/AGENTS.md`. - - Repo-specific procedures and skills are located in `.agents/skills/`. - - ## Machine Wiring - `.agents/` holds machine wiring and local skills. A hook cannot install itself - and a missing hook fails silently. - - If the `agents` CLI is installed, run `agents doctor` early and report any warnings before relying on this context. - - If `agents` is not installed on this machine, skip machine wiring checks and adhere directly to the repository instructions above. - - Recording is covered by the global instruction and the `recording-what-you-learn` - skill; it is not repo-specific and is not restated here. - ``` - -5. **Ensure Relative `CLAUDE.md` Symlink**: - Ensure `CLAUDE.md` is a relative symlink pointing to `AGENTS.md`: - ```bash - rm -f CLAUDE.md - ln -s AGENTS.md CLAUDE.md - ``` +## Step 8: Build the traceability table + +Zero rule dropping is not verifiable by looking at the result. Before you ask +for approval, produce a row for every non-boilerplate block in every source file +you read: + +``` +source quote (verbatim) | classification | destination +-------------------------------------------------|----------------|--------------------- +"All tests must pass before commit; use uv, not pip" | domain rule | .agents/AGENTS.md §2 +"Run `agents doctor` early and surface what it says" | boilerplate | replaced by router +"docs/sessions/... halt and resumption plan" | misplaced doc | docs/plans/ +``` + +Every block gets exactly one destination. Then state the count: *N blocks in, N +blocks placed, 0 unaccounted*. That sentence is the evidence for the invariant. +Without the table the invariant is an assertion, and this is precisely the +failure deterministic tools cannot catch for you. --- -### Phase 4: Docs Store Realignment & Skill Refresh - -1. **Relocate Misplaced Documentation**: - Review `misplaced_docs` flagged during Phase 1: - - Move misplaced plan files (`*-plan.md` in `docs/journal/` or legacy archive paths) into `docs/plans/`: - ```bash - git mv docs/journal/-plan.md docs/plans/ - ``` - - Move design specifications out of `docs/journal/` into `docs/design/`. - - Fix any broken internal relative markdown links within relocated files. - -2. **Ensure 4 Documentation Stores Exist**: - Ensure directories and starter `README.md` files exist for all 4 stores: - - `docs/design/README.md` - - `docs/plans/README.md` - - `docs/journal/README.md` - - `docs/qna/README.md` - If any are missing, run `agents init` or scaffold them non-destructively. - -3. **Verify Bundled Skills**: - - Ensure `.agents/skills/recording-what-you-learn/SKILL.md` is present. If customized, perform 3-way merge; if missing, populate from canonical template. - - Ensure `.agents/skills/migrating-fleet-context/SKILL.md` is present and matches the binary's embedded asset. +## Step 9: Verify + +```bash +agents drift # expect exit 0 +agents doctor # expect all five scaffold:* checks ok +``` + +Then the repository's own suite — `go test ./...`, `pytest`, `npm test`, +whatever it uses. A migration that breaks the build is not done. --- -### Phase 5: Automated Verification & Diagnostic Gate - -1. **Verify Context Cleanliness with `agents drift`**: - ```bash - agents drift - ``` - Must return exit code `0` (`Router: clean_current`, `Symlink: ok`, `Domain: ok`). - -2. **Verify Granular Diagnostics with `agents doctor`**: - ```bash - agents doctor - ``` - Verify that all 5 `scaffold:*` checks pass with status `ok`: - - `scaffold:router` (OK) - - `scaffold:symlink` (OK) - - `scaffold:domain` (OK) - - `scaffold:skill-recording` (OK) - - `scaffold:skill-migrating` (OK) - -3. **Run Repository Test Suite**: - Run the repository's native test commands if applicable (e.g. `go test ./...`, `npm test`, `pytest`). Ensure full completion with exit code 0. - -4. **Commit Changes**: - Stage and commit all migration changes: - ```bash - git add AGENTS.md CLAUDE.md .agents/ docs/ - git commit -m "refactor(context): migrate to two-tier agent context and 4-store layout" - ``` +## Step 10: Stop for the human + +**Do not stage anything yet.** Present: + +1. The traceability table from Step 8, with the *N in, N placed, 0 unaccounted* line. +2. `git status --porcelain` and `git diff --stat`. +3. Anything you dropped in Step 7, named. +4. Every stop-and-ask you resolved and how. + +Then wait for an explicit approval. Silence is not approval, and neither is a +clean verification in Step 9. + +## Step 11: Commit and open the pull request + +Only after approval, and staging the exact paths you changed — never `git add .` +and never a broad directory that sweeps up unrelated work: + +```bash +git add AGENTS.md CLAUDE.md .agents/AGENTS.md docs/ +git diff --cached --stat +git commit -m "refactor(context): migrate to two-tier agent context and 4-store layout" +git push -u origin feat/two-tier-context-migration +gh pr create --fill +``` + +In fleet mode, repeat from Step 2 for the next repository. --- +## Stop and ask when + +- A block cannot be confidently classified as domain rule or boilerplate. +- Two destinations are both plausible for the same block. +- `router_state` is `missing` and there is no `CLAUDE.md` to read either. +- A `customized` skill has no identifiable **base**. +- The working tree is dirty, or the repository is mid-rebase or mid-merge. +- `misplaced_docs` names a file whose correct store is genuinely unclear. + +Asking costs one message. Guessing costs a rule nobody notices is gone. + +## Red flags + +- About to remove `CLAUDE.md` without having `stat`-ed `AGENTS.md` first. +- About to apply the `drifted` procedure to a `clean_legacy` router. +- About to commit before presenting the traceability table. +- About to touch a skill listed in `local_skills`. +- About to relocate something out of `docs/archive/`. +- Parsing `agents drift --all --json` as an object. + ## Where this comes from -- `docs/design/2026-08-29-two-tier-context-and-llm-migration-architecture.md` (Architecture specification) -- `docs/plans/2026-08-31-two-tier-context-and-llm-migration-plan.md` (Implementation plan) -- `docs/qna/why-does-agents-init-never-update-existing-instructions.md` (Scaffold immutability rationale) -- `docs/qna/how-does-two-tier-agent-context-prevent-scaffold-drift.md` (Two-Tier isolation rationale) +This skill is owned and maintained by the `agents` CLI, not by the repository it +is sitting in. `agents update --apply` overwrites it from the installed binary, +so local edits here do not survive — if this repository needs different +behaviour, that belongs in `.agents/AGENTS.md`. + +**The tool is the authority on state, not this document.** `agents drift` and +`agents doctor` report what a repository actually is; where they and this skill +disagree, they are right and this copy is stale. Step 0 is how you find out. + +The architecture rationale — why the router is a fixed template, why domain +rules live one level down, what each digest state proves — lives with the +`agents` project's own design documents, upstream. It is deliberately not +restated or linked here: this file is scaffolded into repositories that have no +copy of those documents, and a pointer to a path that does not exist is worse +than no pointer at all. diff --git a/docs/design/2026-08-29-two-tier-context-and-llm-migration-architecture.md b/docs/design/2026-08-29-two-tier-context-and-llm-migration-architecture.md index d2f5c600..c05e7a7e 100644 --- a/docs/design/2026-08-29-two-tier-context-and-llm-migration-architecture.md +++ b/docs/design/2026-08-29-two-tier-context-and-llm-migration-architecture.md @@ -1,13 +1,38 @@ # Design: Two-Tier Agent Context and LLM-in-the-Loop Migration Architecture -**Date:** 2026-08-29 (Updated 2026-08-31) -**Status:** Approved in Brainstorming (Ready for Implementation Planning) +**Date:** 2026-08-29 (Updated 2026-08-31; **amended 2026-09-01**, see Amendment 1) +**Status:** Implemented 2026-08-31; §7 amended 2026-09-01 after the skill it specifies shipped contradicting it **Applies to:** `agents` CLI (`scaffold`, `drift`, `doctor`, `fleet`), `AGENTS.md` / `CLAUDE.md`, `.agents/AGENTS.md`, `.agents/skills/`, `.agents/skills/migrating-fleet-context/` **Depends on:** [Spec 1](2026-08-07-agents-repo-context-design.md) (harness adapters, exit codes), [Knowledge is Documentation](2026-08-19-knowledge-is-documentation.md) (2026-08-19), [Contributor Guardrails](2026-08-28-contributor-guardrails-and-scaffold-decoupling.md) (2026-08-28), [Binary Identity & Standalone Resolution](2026-08-28-binary-identity-and-standalone-resolution.md) (2026-08-28) **Reads against:** [`docs/qna/why-does-agents-init-never-update-existing-instructions.md`](../qna/why-does-agents-init-never-update-existing-instructions.md), [`docs/qna/how-does-two-tier-agent-context-prevent-scaffold-drift.md`](../qna/how-does-two-tier-agent-context-prevent-scaffold-drift.md) --- +## Amendment 1 — 2026-09-01 + +The `migrating-fleet-context` skill authored against §7 of this document shipped +contradicting the tooling it drives. The review is in +[`docs/journal/2026-09-01-why-the-migration-skill-shipped-hollow.md`](../journal/2026-09-01-why-the-migration-skill-shipped-hollow.md); +four of the defects traced to this spec rather than to the plan or the skill. +The corrections are applied in the body below. This section is the record of +what they replaced, because the spec is the design still in force and the +journal alone would not be read by the next author of the skill. + +| § | Was | Now | Why | +|---|---|---|---| +| 7.1.4 | "Moves plan files (`*-plan.md` in `docs/journal/` **or `docs/archive/plans/`**) into `docs/plans/`" | archive is excluded from relocation, in the skill and in `internal/drift` alike | `.agents/AGENTS.md` declares `docs/archive/` strictly immutable and this store's own README says nothing in it is rewritten to stay true. §7.1 and §7.2 contradicted each other; §7.2 wins. | +| 7.1.4 | "Semantic 3-Way Un-Nesting" / "3-way merge", sources never named | §7.3 names the sources for each operation, and the root operation is renamed **semantic reconcile** | Three-way merge requires upstream, base and local. The root router operation has only two inputs, so it was never a 3-way merge; the skill merge has three and never said where they came from. An instruction an LLM can only guess at is not a specification. | +| 7.1 | Symlink handling assumed `CLAUDE.md` carries nothing | §7.4 requires reading and preserving `CLAUDE.md` before it is replaced | A legacy repository may hold its only copy of a domain rule in `CLAUDE.md`. The skill's `rm -f CLAUDE.md` deleted it with no extraction step. | +| 7.2 | "Zero Rule Dropping" asserted with no mechanism | §7.5 requires a traceability table, and makes an unclassifiable block a stop-and-ask | An invariant with no verification is a hope. This is the failure the spec says deterministic tools cannot handle, so the LLM path needs stronger proof than they get, not weaker. | +| 4.2 | every directory under `.agents/skills/` classified, unrecognised ones as `customized` | only embedded skills are classified; repository-specific skills are listed in a new `local_skills` field and never judged | §2 says `.agents/skills/` is where repository-specific skills belong, so classifying them made a repository dirty for using the feature as designed — and no migration could clear it, because there was nothing to fix. `playground/autogo-mlx` carries two and could not report clean. The tool owns what it embeds. | +| 6 | `scaffold:skill-migrating` — `warn` (missing or outdated) | unchanged as written; `doctor.go` implemented `customized` as `ok` and is corrected to `warn` | The skill is 100% `agents`-owned per §5.1. For an authoritative asset, "customized" *is* "outdated" — reporting it `ok` means a stale migration skill passes its own health check. | + +Two further gaps were absent from this spec entirely and are added, not corrected: +**§7.6** (fleet mode and the `--all` output shape) and **§7.7** (legacy store +triage). Neither was in the plan either; see the journal for where each entered. + +--- + ## 1. Executive Summary & Problem Formulation ### 1.1 The Deterministic Migration Dilemma @@ -30,7 +55,7 @@ This specification establishes a robust **Two-Tier Agent Context Architecture** - `agents update`: Deterministic machine wiring and authoritative refresh of `agents`-owned infrastructural skills. 3. **Model A LLM-in-the-Loop Migration Engine (`migrating-fleet-context`)**: - An authoritative, 100% `agents`-owned agent skill embedded in the Go binary. - - Executes inside AI agent harnesses (Antigravity, Claude Code, Codex) on a dedicated feature branch with git dirty checks, semantic 3-way un-nesting of domain rules, 3-way skill merges, document relocation, and an interactive human approval gate. + - Executes inside AI agent harnesses (Antigravity, Claude Code, Codex) on a dedicated feature branch with git dirty checks, semantic reconcile of domain rules, three-way skill merges, document relocation, and an interactive human approval gate that blocks the commit. --- @@ -150,7 +175,8 @@ type DriftReport struct { RouterState RouterState `json:"router_state"` SymlinkState string `json:"symlink_state"` // "ok" | "broken" | "not_symlink" | "missing" DomainState string `json:"domain_state"` // "ok" | "missing" - Skills map[string]string `json:"skills"` // skill_name -> ComponentState + Skills map[string]string `json:"skills"` // embedded skill_name -> ComponentState + LocalSkills []string `json:"local_skills"` // repo-specific skills: listed, never judged DocsStores map[string]bool `json:"docs_stores"` // design, plans, journal, qna MisplacedDocs []string `json:"misplaced_docs"` // e.g. plans living in docs/journal/ Diff string `json:"diff,omitempty"` // Unified diff against canonical router @@ -209,32 +235,135 @@ digraph model_a_migration { node [shape=box, style=rounded, fontname="Helvetica"]; A [label="1. Drift Detected\n(agents doctor or agents drift)"]; + A0 [label="0. Self-Currency Check\n- skill matches installed binary"]; B [label="2. AI Agent Harness Invokes\n'migrating-fleet-context' Skill"]; C [label="3. Git Safety & Branch Isolation\n- Assert working tree is clean\n- Create 'feat/two-tier-context-migration'"]; - D [label="4. Semantic 3-Way Un-Nesting\n- Extract domain rules -> .agents/AGENTS.md\n- Reconcile root -> Canonical AGENTS.md\n- 3-way merge customized skills\n- Relocate misplaced docs -> docs/plans/"]; + D [label="4. Reconcile by Router State\n- One action per state (7.3)\n- Preserve CLAUDE.md first (7.4)\n- 3-way merge skills: upstream/base/local\n- Relocate docs, excluding docs/archive/"]; E [label="5. Verification\n- Run 'agents doctor'\n- Run repo test suite (go test, etc.)"]; - F [label="6. Interactive Human Approval Gate\n- Present structured diff & summary"]; + F [label="6. Interactive Human Approval Gate\n- Present traceability table (7.5)\n- Nothing is staged before approval"]; G [label="7. Commit & Open Pull Request"]; - A -> B -> C -> D -> E -> F -> G; + A -> B -> A0 -> C -> D -> E -> F -> G; } ``` ### 7.1 Skill Operational Protocol -1. **Target Discovery**: Runs `agents ls` and `agents drift --json`. -2. **Git Safety Check**: Asserts the working tree is clean (`git status --porcelain`). Refuses to proceed if uncommitted changes exist. -3. **Branch Isolation**: Creates dedicated feature branch: `feat/two-tier-context-migration`. -4. **Semantic 3-Way Un-Nesting**: - - **Root `AGENTS.md`**: Identifies custom engineering rules, tech stack constraints, and safety guidelines -> appends them to `.agents/AGENTS.md` without duplicating existing entries. Restores root `AGENTS.md` to `scaffold.DefaultAgentsMD`. - - **User Skills (`recording-what-you-learn`)**: Performs 3-way merge between upstream embedded skill and local customized changes. - - **Document Relocation**: Moves plan files (`*-plan.md` in `docs/journal/` or `docs/archive/plans/`) into `docs/plans/` and updates internal relative markdown links. -5. **Verification Gate**: Executes `agents doctor` and repository test suites (`go test ./...`). -6. **Interactive Human Approval Gate**: Presents a structured diff summary in chat. On explicit approval, commits changes and prepares PR. +1. **Self-Currency Check**: The skill is an `agents`-owned asset (§5.1) and can be read stale. Before acting, confirm the running copy matches the installed binary — `agents doctor` reporting `scaffold:skill-migrating` as `ok`. If it does not, run `agents update --apply` and re-read. +2. **Target Discovery**: `agents ls` for the registered fleet, then `agents drift --json` for one repository or `agents drift --all --json` for the fleet. See §7.6 for the two output shapes. +3. **Git Safety Check**: Asserts the working tree is clean (`git status --porcelain`). Refuses to proceed if uncommitted changes exist. +4. **Branch Isolation**: Creates dedicated feature branch: `feat/two-tier-context-migration`. +5. **Root Context Reconcile**: One action per router state (§7.3), preserving `CLAUDE.md` content first (§7.4). +6. **Skill Merge & Docs Realignment**: Three-way merge of user-owned skills (§7.3), relocation of misplaced documents excluding `docs/archive/` (§7.1 amendment), triage of retired stores (§7.7). +7. **Verification Gate**: Executes `agents drift`, `agents doctor` and repository test suites (`go test ./...`). +8. **Interactive Human Approval Gate**: Presents the traceability table (§7.5) and a scoped `git diff --stat`. **The skill stops here and waits.** It does not stage or commit before an explicit human approval. +9. **Commit & Open Pull Request**: On approval, stages the exact changed paths, commits, pushes, and opens a PR via `gh`. ### 7.2 Invariant Guarantees -- **Zero Rule Dropping**: All domain constraints, test rules, and guidelines present in the original file are preserved in `.agents/AGENTS.md`. -- **Archive Immutability**: Active work and modern plans are never written to `docs/archive/`. +- **Zero Rule Dropping**: All domain constraints, test rules, and guidelines present in the original file are preserved in `.agents/AGENTS.md`, and the traceability table of §7.5 is what shows it. +- **Archive Immutability**: `docs/archive/` is neither written to nor moved out of. Relocation applies to the live stores only. - **Branch Protection Compliance**: All migrations are authored on dedicated feature branches and merged via Pull Requests after passing CI gates. +- **No Unapproved Commit**: Steps 1-7 mutate the working tree; only step 9 touches the index, and only after step 8 returns approval. + +### 7.3 Router State Table and Merge Sources + +Migration is not one procedure. `internal/drift` classifies four router states +and each has a different correct action; treating them alike is what makes a +`clean_legacy` router get partitioned as though it held domain rules. + +| `router_state` | Meaning | Action | +|---|---|---| +| `clean_current` | matches the running binary's `scaffold.DefaultAgentsMD` | no-op | +| `clean_legacy` | matches a known older canonical template, carries no custom content | replace wholesale; there is nothing to extract | +| `drifted` | canonical text plus, or reworded into, repository content | semantic reconcile (below), then restore the canonical router | +| `missing` | no root `AGENTS.md` | do not invent one; inspect `CLAUDE.md` (§7.4), and if it too is absent, stop and ask | + +Two distinct operations were both called "3-way merge". They are not the same +shape and only one of them is a merge: + +- **Semantic reconcile (root router)** — two inputs: the current root file, and + the canonical router from the running binary. Output: domain rules moved to + `.agents/AGENTS.md`, canonical router restored. Two inputs is not a three-way + merge and calling it one invited an operation nobody had defined. +- **Three-way merge (user-owned skills, e.g. `recording-what-you-learn`)** — + three inputs, named explicitly: **upstream** is the embedded asset in the + running binary; **base** is the canonical or legacy template the local file + last matched, identified by the digest catalog of §4.1; **local** is the + working file. Where the digest catalog cannot identify a base, there is no + three-way merge available — stop and ask rather than guessing one. + +### 7.4 Root File Preservation + +`CLAUDE.md` becomes a symlink, which destroys whatever it held. Before +`ln -s AGENTS.md CLAUDE.md`: + +1. `stat` both paths before reading either. Three topologies exist in the + fleet today and they are not interchangeable: + + | Topology | Meaning | Handling | + |---|---|---| + | `AGENTS.md` regular, `CLAUDE.md -> AGENTS.md` | current | nothing to preserve | + | `AGENTS.md` regular, `CLAUDE.md` regular | both carry content | reconcile both (step 2) | + | **`AGENTS.md -> CLAUDE.md`, `CLAUDE.md` regular** | **inverted**, the pre-2026-08-19 Spec 1 topology | see below | + +2. If both exist as regular files and differ, treat **both** as sources of + domain rules. A legacy repository may hold its only copy of a rule in + `CLAUDE.md`. + +3. **The inverted case is destructive if handled blind.** Where `AGENTS.md` is + a symlink *to* `CLAUDE.md`, the sequence `rm -f CLAUDE.md && ln -s AGENTS.md + CLAUDE.md` deletes the only real file and leaves `AGENTS.md -> CLAUDE.md -> + AGENTS.md`: a symlink loop, and every line of repository context gone. + Invert deliberately instead — read `CLAUDE.md`, remove the `AGENTS.md` + symlink, write the reconciled content to `AGENTS.md` as a regular file, then + replace `CLAUDE.md` with the symlink. + + `playground/desktop_pet` is in exactly this state as of 2026-09-01: + `AGENTS.md -> CLAUDE.md`, 18 lines of real content in `CLAUDE.md`, + `symlink_state: not_symlink`, `domain_state: missing`. + +4. Extract to `.agents/AGENTS.md` first. Replace with the symlink only once the + content has a destination. + +### 7.5 Traceability Requirement + +Zero Rule Dropping is not verifiable by inspection of the result. Before the +approval gate the skill produces a table over every non-boilerplate block in the +source files: + +``` +source quote (verbatim) -> classification -> destination +``` + +Every block reaches exactly one destination: `.agents/AGENTS.md`, the canonical +router (as boilerplate being replaced), or a named `docs/` store. A block that +cannot be confidently classified, or that has two plausible destinations, is a +**stop-and-ask** — not a judgement call the skill makes alone. The table goes to +the human in step 8; it is the evidence for the invariant, and without it the +invariant is an assertion. + +### 7.6 Fleet Mode and Output Shapes + +`agents drift --json` and `agents drift --all --json` do not return the same +JSON type, and a consumer written against one breaks on the other: + +| Invocation | Returns | +|---|---| +| `agents drift --json` | a single `DriftReport` **object** | +| `agents drift --all --json` | an **array** of `DriftReport` | + +Fleet mode iterates the array with per-repository branch isolation. Registry +entries reported `missing` or `unknown`, and repositories with a dirty working +tree, are skipped and named in the final report rather than migrated. + +### 7.7 Retired Store Triage + +Repositories predating the 2026-08-19 redesign may carry `.agents/memory/` and +`.agents/reports/`. These are **not** relocated wholesale: much of their content +is machine-generated and belongs nowhere. Durable findings move to the store +that matches their retrieval axis — `docs/qna/` for a topic-indexed finding, +`docs/design/` for a design still in force, `docs/plans/` for an unexecuted +plan. Everything else is dropped, and what was dropped is named in the report to +the human. --- diff --git a/docs/design/README.md b/docs/design/README.md index 34191ab6..38e69edf 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -31,7 +31,7 @@ Numbers are stable catalog identifiers, not priority or order. | — | [knowledge is documentation](2026-08-19-knowledge-is-documentation.md) | **executed 2026-08-20** — the retired code and stores are deleted | | — | [antigravity multi-harness onboarding](2026-08-28-antigravity-multi-harness-onboarding.md) | **implemented 2026-08-28** — adapter, dialect, dual-layer instruction topology | | — | [contributor guardrails and scaffold decoupling](2026-08-28-contributor-guardrails-and-scaffold-decoupling.md) | **implemented 2026-08-28** — conditional doctor, standalone support | -| — | [two-tier context and llm migration architecture](2026-08-29-two-tier-context-and-llm-migration-architecture.md) | **designed 2026-08-29** — two-tier context, 4-store layout, bundled skills, LLM migration | +| — | [two-tier context and llm migration architecture](2026-08-29-two-tier-context-and-llm-migration-architecture.md) | **implemented 2026-08-31; §7 amended 2026-09-01** — two-tier context, 4-store layout, bundled skills, LLM migration. Read Amendment 1 before touching the migration skill: the original §7 contradicted §7.2 on archive immutability and left "3-way merge" undefined. | **Spec 1 is the foundation** — terminology, the placement rule, the pointer diff --git a/docs/journal/2026-09-01-why-the-migration-skill-shipped-hollow.md b/docs/journal/2026-09-01-why-the-migration-skill-shipped-hollow.md new file mode 100644 index 00000000..405148a4 --- /dev/null +++ b/docs/journal/2026-09-01-why-the-migration-skill-shipped-hollow.md @@ -0,0 +1,221 @@ +# 2026-09-01 — Why the migration skill shipped hollow + +Reviewing [PR #40](https://github.com/nilbot/dotfiles/pull/40) after merge. The +Go side of the Two-Tier work — `internal/drift`, `agents drift`, five +`scaffold:*` doctor checks, the refined `agents update` — is sound and tested. +The one deliverable that is *prose*, `.agents/skills/migrating-fleet-context/SKILL.md`, +came out as an architecture summary with shell commands attached: it contradicts +the tooling it is supposed to drive, and it drops requirements the design and the +plan both stated. + +The question this entry answers is not "what is wrong with the skill" — that was +enumerated separately. It is **where each defect entered**, and whether the +`superpowers:writing-plans` skill or the authoring harness (Antigravity) is +responsible. + +## Where each defect entered + +Read design = `docs/design/2026-08-29-two-tier-context-and-llm-migration-architecture.md`, +plan = `docs/plans/2026-08-31-two-tier-context-and-llm-migration-plan.md`, +both in commit `60e5d22`; skill authored in `285627e`, same day, same author. + +| # | Defect | Design | Plan | Entered at | +|---|---|---|---|---| +| 1 | No fleet mode; never runs `agents ls` | §7.1 names it | Task 6 *Interfaces* names it, Step 1 body does not | plan step body | +| 1b | `--all --json` emits an array, `--json` an object | absent (§4.2 gives one object) | absent | Task 3 implementation, never reconciled with Task 6 | +| 2 | Four router states collapsed to one procedure | §4.1 defines all four | Task 6 names one behaviour | plan | +| 3 | `rm -f CLAUDE.md` can delete canonical content | absent | absent | design omission | +| 4 | "3-way merge" with undefined sources | used 4× undefined | repeated verbatim | design vocabulary | +| 5 | Archive relocation vs archive immutability | §7.1 says move from `docs/archive/plans/`; §7.2 + `.agents/AGENTS.md:10` say **STRICTLY IMMUTABLE** | Task 2 narrows the classifier to `docs/journal/` | design, then widened by implementation | +| 6 | Zero-rule-dropping has no verification | §7.2 asserts it | no test step at all | plan | +| 7 | Human approval gate and PR missing | §7 nodes 6–7 | Task 6 Step 1 **does** say "Interactive human diff review before commit/PR" | skill authoring | +| 8 | Skill can run while stale | absent | absent | — (and see below) | +| 9 | No triage for `.agents/memory/`, `.agents/reports/` | absent | absent | design omission | +| 10 | Canonical router pasted as 24 literal lines | §2.1 embeds it too | Task 6 says restore `DefaultAgentsMD` — a *symbol* | skill authoring | + +Rows 7 and 10 are the only pure transmission losses: the plan carried the +requirement and the skill dropped it. The skill contains **0** occurrences of +`agents ls`, `--all`, `gh pr`, `approval`, or `approve`, and its Phase 5 ends by +staging four broad paths and committing. + +Two of these are worth pinning with numbers. + +**Row 5 is a three-way disagreement, currently latent.** `drift.go:151-155` +flags any `*-plan.md` outside `docs/plans/` and any `*-design.md` outside +`docs/design/`, walking all of `docs/` — including `docs/archive/`. The plan +(Task 2, Step 3) had specified only `docs/journal/`. It does not fire in +`dotfiles` today because the archived plans are named +`2026-08-07-agents-repo-context.md`, without the `-plan.md` suffix: +`agents drift --json` returns `"misplaced_docs": []`. Any fleet repo that +archived with the suffix would have the skill told to `git mv` out of a store +`.agents/AGENTS.md` declares immutable. + +**Row 8 has a deterministic half that is also green when it should not be.** +The design's §6 table specifies `warn` for `scaffold:skill-migrating` when the +skill is "missing **or outdated**". `doctor.go:1120-1125` reports +`ComponentCustomized` as **`OK`** — "carries repository customizations". So a +stale or hand-edited authoritative skill passes doctor. The plan's Task 4 never +restated the design's remedy table; it said only "generate the 5 diagnostic +checks with descriptive status and remedies", and the implementation chose. + +Row 10 is currently harmless and unbound: the 23-line router block pasted into +the skill is byte-identical to the live `AGENTS.md`, and no test asserts that it +stays so. + +## The plan is its own control + +The tempting attribution is "Antigravity wrote a sloppy plan". The plan itself +refutes that, because it holds the harness constant and varies only the +deliverable type: + +- **5 of 7 tasks carry a red-first gate** (`Expected: FAIL` at lines 96, 200, + 278, 345, 404). Tasks 1–5 contain real Go, real assertions, real file:line + targets. +- **Task 7 has no failing-test step but does invoke existing gates** — + `docs_test.go` and `exitcode_doc_test.go`, which are genuine content + assertions over prose. +- **Task 6 has neither.** Its Step 1 is a nine-bullet list of topics. Its Step 3 + runs `go test -v ./agents/...`, which proves only that the file embeds. + +Same author, same session, same document. Output quality tracks whether the +`writing-plans` template supplied a shape, not who was driving. That is a +within-document control, and it points at the template. + +The template's blind spot is structural, not accidental. Its entire Task +Structure example is `pytest`; its Bite-Sized Task Granularity section is +literally "write the failing test / run it / implement / run / commit"; and its +No Placeholders rule ends with "code blocks required for **code** steps" — +which by omission licenses a prose step that only describes. Its Self-Review has +exactly three checks: spec coverage, placeholder scan, type consistency. A +hollow prose task passes all three. + +And it did. The plan's own Self-Review Checklist asserts: + +> **No Placeholders:** All steps contain explicit Go code snippets, test +> assertions, file paths, and shell commands. + +Task 6 Step 1 contains none of those. The self-review produced a false green on +the one task that needed it — the same failure shape already recorded in +[can this check actually fail](../qna/can-this-check-actually-fail.md). + +## The repository already owns the fix, aimed elsewhere + +Prose *can* carry a test cycle here; two already do. + +`TestLivingDocumentsNameOnlyRealCommands` (`agents/docs_test.go:95`) walks +`.agents/skills/**/*.md`, so the migration skill **is** scanned — and passes, +because every command it names (`agents drift`, `agents doctor`, `agents init`) +exists. The check is one-directional: it catches invented commands, never +omitted ones. Defect 1 is precisely an omitted command, so this test could not +have caught it. + +The other direction exists too. `TestHarnessSkillCoversAgentCommands` +(`docs_test.go:140`) asserts every `Audience: Agent` command appears in the +fleet skill — and `drift` is registered `Audience: []Audience{Human, Agent}` +(`commands.go:46`). But that test is hard-bound to +`claude/skills/agents-tool/SKILL.md`, which does name both `agents ls` (line +108) and `agents drift` (line 126). The reverse check works; it was simply never +pointed at the skill Task 6 produced. + +Both tests pass right now (`go test . -run 'TestLivingDocuments|TestHarnessSkillCovers'` → `ok`), +and `agents doctor` reports all five `scaffold:*` checks `ok`. Every gate this +change owns is green while the skill is wrong. That is the finding. + +## Verdict, and what would settle the rest + +**The `writing-plans` template is the necessary condition.** It has no shape for +a deliverable that is prose, so Task 6 was written without content and without a +gate, and nothing downstream could detect either. + +**The harness is not thereby exonerated.** Rows 7 and 10 are content the plan +explicitly carried and the skill silently dropped — an execution-side loss. The +template's blind spot is what removed the detector that would have caught it. + +What is *not* settled is whether a different harness executing the same template +would have produced a hollow Task 6. This entry asserts a template blind spot +from a within-document control, not from a probe, and the repository's standard +for harness claims is higher than that — see +[why didn't Antigravity apply my rules](../qna/why-didnt-antigravity-apply-my-rules.md), +where a sound-looking inference about `agy` was wrong in two independent ways +and only a fixture with a positive control made the run readable. + +The probe, if it is worth running: hand the same design §7 and the same +`writing-plans` skill to Claude Code, and read whether its Task 6 comes out with +a gate. The positive control is a code task in the same plan — if Tasks 1–5 also +degrade, the run says nothing about prose. Until that is run, "the template is +the culprit" is the best-supported reading of one document, not a measured fact +about two harnesses. + +## What to change + +1. Give `agents drift` and the skill one definition of misplaced: exclude + `docs/archive/` in `drift.go`, or drop archive relocation from the skill. + They disagree today. +2. Make `scaffold:skill-migrating` `warn` on `customized`, per design §6. + Presence is not currency. +3. Point a reverse-coverage test at the migration skill, the way + `TestHarnessSkillCoversAgentCommands` points at `agents-tool`. +4. Bind the pasted 24-line router to `scaffold.DefaultAgentsMD` with a test, or + stop pasting it. +5. When `writing-plans` produces a task whose deliverable is a document, write + the acceptance assertion into the task anyway. The template will not ask. + +## Resolution, same day + +All five items landed on `feat/migration-skill-rework`, each red before green. + +`TestMigrationSkillCoversItsSpecifiedProtocol` is the gate that was missing. +Against the old skill it failed **nine** times — once per real defect, including +every omission `TestLivingDocumentsNameOnlyRealCommands` structurally could not +see. It asserts required substrings with the spec section that requires each, +forbids the literal `rm -f` on `CLAUDE.md`, and forbids a `git mv` with an +archive source. Two more bind the skill to its context: one asserts the +repository copy and the embedded asset are byte-identical, one asserts the +router pasted into the skill equals `scaffold.DefaultAgentsMD`. All three are +registered in the `docs` job of `verify.yml`, which fails if a named test stops +existing. + +Both of the repository's existing prose gates caught real errors in the rewrite +draft — the living-documents scan rejected `agents handoff` in a code span +listing retired commands, and the new gate rejected my own forbidden literal in +a warning. The one-directional check is weak, not useless. + +**A correction to the analysis above.** This entry implied the whole exercise +was a no-op on an already-migrated repository. That was wrong, and the reasoning +is worth keeping. `CanonicalSkillDigest` hashes the asset *in the running +binary*, and `LegacySkillDigests` returns `nil` for `migrating-fleet-context` — +only `recording-what-you-learn` has a legacy entry. So rewriting the embedded +asset makes every already-migrated repository report `customized`, and +`isDriftClean` requires every skill to be `ComponentOK`. Measured on the new +binary: `cowork` goes from exit 0 to **exit 1** with `migrating-fleet-context: +customized`, while its router, symlink, domain and all four stores stay clean. +The semantic un-nesting has nothing to do there; the staleness path fires. Those +are different claims and this entry ran them together. + +**Two defects found while fixing, neither in the original review.** + +*The inverted symlink.* `playground/desktop_pet` is on the pre-2026-08-19 +topology: `AGENTS.md -> CLAUDE.md`, with 866 bytes over 18 lines in `CLAUDE.md` +as the only real file. The old skill's unconditional replacement would have +deleted it and left `AGENTS.md -> CLAUDE.md -> AGENTS.md`, a symlink loop with +every line of context gone. Defect 3 was worse than "may lose content": on this +one repository it is total loss plus an unreadable tree. Now design §7.4 with a +three-row topology table. + +*The tool owned a directory it did not own.* `drift.go` classified every +directory under `.agents/skills/` and reported unrecognised ones as +`customized`, which `isDriftClean` treats as dirty. But §2 says `.agents/skills/` +is exactly where repository-specific skills belong, so a repository using the +feature as designed could never report clean and no migration could fix it — +there was nothing to fix. `playground/autogo-mlx` carries `human-ranked-sft` and +`llm-in-the-loop-rl-discovery` and was permanently dirty for it. The tool now +judges only what it embeds; the rest is listed in a new `local_skills` field and +never classified. + +**One process note.** The control for `TestMigrationSkillPastesTheCanonicalRouter` +appeared to pass with the router deliberately sabotaged. It had not run: `go +test` served a cached result, because the file it reads lives outside the +package. `-count=1` showed both tests failing as they should. `verify.yml:101` +already carries the comment that `-count=1` is load-bearing for exactly this, +so CI was never exposed — only the local check was, and a control that silently +does not run is the failure this repository has recorded twice before.