From 1ebee79741427e4cd4087e0684370381d0458bfd Mon Sep 17 00:00:00 2001 From: Ersi Ni Date: Tue, 1 Sep 2026 11:32:17 +0100 Subject: [PATCH 1/5] fix(drift): stop reporting immutable archives and repo-owned skills as drift Two classifications told the migration skill to do things the repository forbids. docs/archive/ is strictly immutable per .agents/AGENTS.md, but the misplaced-docs walk covered all of docs/, so an archived *-plan.md was reported as misplaced -- and a misplaced file is one the skill is instructed to git mv. The classifier and the skill disagreed about what "misplaced" means; the archive rule wins. .agents/skills/ is where design section 2 says repository-specific skills belong, yet every unrecognised directory there was classified customized, which isDriftClean treats as dirty. A repository using the feature as designed could never report clean and no migration could clear it, because there was nothing to fix. The tool now judges only the skills it embeds; the rest are listed in a new local_skills field and never classified. Both existing assertions encoded the old behaviour and are updated deliberately; see Amendment 1 of the 2026-08-29 two-tier design. --- agents/cmd_drift.go | 6 ++ agents/internal/drift/drift.go | 22 +++++- agents/internal/drift/drift_test.go | 101 ++++++++++++++++++++++++++-- 3 files changed, 122 insertions(+), 7 deletions(-) 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/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) + } +} From 327f081e7fe6059a169e7c3e400010cb232828dd Mon Sep 17 00:00:00 2001 From: Ersi Ni Date: Tue, 1 Sep 2026 11:32:23 +0100 Subject: [PATCH 2/5] fix(doctor): warn when the agents-owned migration skill is stale The design's check table specifies warn for scaffold:skill-migrating when the skill is "missing or outdated", but the implementation reported a diverged copy as ok with the detail "carries repository customizations". migrating-fleet-context is 100% agents-owned (design section 5.1). Unlike recording-what-you-learn there is no customization to respect: a copy that does not match the embedded asset is stale, and reporting it ok let a skill carrying obsolete migration instructions pass its own health check. That is the same skill an agent then follows to migrate the fleet. --- agents/internal/doctor/doctor.go | 9 +++++++-- agents/internal/doctor/doctor_test.go | 11 +++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) 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 From 2255d229b9e0787a6985d73683cd6e0b8093a50d Mon Sep 17 00:00:00 2001 From: Ersi Ni Date: Tue, 1 Sep 2026 11:32:32 +0100 Subject: [PATCH 3/5] feat(skills): rewrite migrating-fleet-context and gate it with tests The skill was the one deliverable of the two-tier work that is prose rather than code, and it shipped contradicting the tooling it drives: single-repo only, one procedure for four router states, an unconditional CLAUDE.md replacement, no traceability for its own zero-rule-dropping invariant, and a commit with no approval gate. The rewrite follows the amended design section 7: a self-currency check first, the two JSON shapes named, a state table with one action per router state, the three root-file topologies including the destructive inverted case, explicitly named merge sources, retired-store triage, a traceability table, and a stop before anything is staged. TestMigrationSkillCoversItsSpecifiedProtocol is the gate that did not exist. Against the old skill it failed nine times, once per real defect. TestLivingDocumentsNameOnlyRealCommands already scanned this file but only catches commands that do not exist, never required commands that are absent -- which is why the omissions survived review. Two further tests bind the repository copy to the embedded asset and the pasted router to scaffold.DefaultAgentsMD, since both pairs ship in the same binary and nothing else stopped them diverging. All three are registered in the docs job, which fails if a named test stops existing. --- .../skills/migrating-fleet-context/SKILL.md | 486 +++++++++++------- .github/workflows/verify.yml | 5 +- agents/docs_test.go | 124 +++++ .../skills/migrating-fleet-context/SKILL.md | 486 +++++++++++------- 4 files changed, 712 insertions(+), 389 deletions(-) diff --git a/.agents/skills/migrating-fleet-context/SKILL.md b/.agents/skills/migrating-fleet-context/SKILL.md index fd17f456..6b755457 100644 --- a/.agents/skills/migrating-fleet-context/SKILL.md +++ b/.agents/skills/migrating-fleet-context/SKILL.md @@ -1,240 +1,338 @@ --- 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/2026-08-30-something-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) +- `docs/design/2026-08-29-two-tier-context-and-llm-migration-architecture.md` §7 — read Amendment 1 first +- `docs/journal/2026-09-01-why-the-migration-skill-shipped-hollow.md` — why the previous version of this skill was wrong +- `docs/qna/why-does-agents-init-never-update-existing-instructions.md` +- `docs/qna/how-does-two-tier-agent-context-prevent-scaffold-drift.md` diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 31895600..22cfd178 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -221,7 +221,10 @@ jobs: TestNoUsageLineNamesAFlagThatDoesNotExist TestReadmeCommandBlockIsCurrent TestLivingDocumentsNameOnlyRealCommands - TestHarnessSkillCoversAgentCommands" + TestHarnessSkillCoversAgentCommands + TestMigrationSkillCoversItsSpecifiedProtocol + TestMigrationSkillMatchesEmbeddedAsset + TestMigrationSkillPastesTheCanonicalRouter" 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/docs_test.go b/agents/docs_test.go index 5e76419d..c095c2b9 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,125 @@ 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)) + } +} 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..6b755457 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,338 @@ --- 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/2026-08-30-something-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) +- `docs/design/2026-08-29-two-tier-context-and-llm-migration-architecture.md` §7 — read Amendment 1 first +- `docs/journal/2026-09-01-why-the-migration-skill-shipped-hollow.md` — why the previous version of this skill was wrong +- `docs/qna/why-does-agents-init-never-update-existing-instructions.md` +- `docs/qna/how-does-two-tier-agent-context-prevent-scaffold-drift.md` From 62cf00eca7476d715070ec39eb735e56d1b4f14b Mon Sep 17 00:00:00 2001 From: Ersi Ni Date: Tue, 1 Sep 2026 11:32:41 +0100 Subject: [PATCH 4/5] docs: amend the two-tier spec and record why the migration skill was wrong Amendment 1 records what section 7 said before the corrections, because the spec is the design still in force and a body edited silently would leave the next author of the skill with no reason for any of it. Four defects traced to this spec rather than to the plan or the skill: the archive contradiction between 7.1 and 7.2, "3-way merge" used four times with its sources never named, no preservation step for CLAUDE.md, and an invariant asserted with no mechanism. New sections cover the router state table and named merge sources (7.3), the three root-file topologies including the inverted symlink measured on playground/desktop_pet (7.4), the traceability requirement (7.5), fleet mode and the two JSON shapes (7.6), and retired-store triage (7.7). The journal entry carries the attribution analysis: which defects entered at the spec, which at the plan, and which at the skill, and why the plan is its own control for the claim that writing-plans has no shape for a prose deliverable. The plan itself is deliberately unedited -- it is a dated record of what was intended and executed, and this entry is its correction. --- agents/README.md | 6 +- ...-context-and-llm-migration-architecture.md | 165 +++++++++++-- docs/design/README.md | 2 +- ...-why-the-migration-skill-shipped-hollow.md | 221 ++++++++++++++++++ 4 files changed, 372 insertions(+), 22 deletions(-) create mode 100644 docs/journal/2026-09-01-why-the-migration-skill-shipped-hollow.md 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/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. From 8ff5553105729144294daaf0e892c57198108ad3 Mon Sep 17 00:00:00 2001 From: Ersi Ni Date: Tue, 1 Sep 2026 11:36:20 +0100 Subject: [PATCH 5/5] fix(skills): stop the migration skill pointing at documents only dotfiles has The skill is embedded in the binary and scaffolded into arbitrary repositories, including on machines with no copy of this checkout. Its closing section listed four dated design and Q&A paths from this repository, which read as a working reference and resolve to nothing anywhere else -- worse than no pointer, because an agent will try to follow it. It now says where the skill is owned, that the tool rather than the document is the authority on state, and that the architecture rationale lives upstream -- deliberately without paths. The one dated example filename in a git mv sample is now a placeholder. TestMigrationSkillNamesNoRepoLocalDocuments rejects any dated markdown filename. Bare dates describing an era are still allowed; a dated filename is a path into this repository. --- .../skills/migrating-fleet-context/SKILL.md | 21 ++++++++++++++----- .github/workflows/verify.yml | 3 ++- agents/docs_test.go | 21 +++++++++++++++++++ .../skills/migrating-fleet-context/SKILL.md | 21 ++++++++++++++----- 4 files changed, 55 insertions(+), 11 deletions(-) diff --git a/.agents/skills/migrating-fleet-context/SKILL.md b/.agents/skills/migrating-fleet-context/SKILL.md index 6b755457..77447642 100644 --- a/.agents/skills/migrating-fleet-context/SKILL.md +++ b/.agents/skills/migrating-fleet-context/SKILL.md @@ -221,7 +221,7 @@ scaffolds them non-destructively. Relocate each entry in `misplaced_docs`: ```bash -git mv docs/journal/2026-08-30-something-plan.md docs/plans/ +git mv docs/journal/-plan.md docs/plans/ ``` Then fix relative markdown links inside the moved files. @@ -332,7 +332,18 @@ Asking costs one message. Guessing costs a rule nobody notices is gone. ## Where this comes from -- `docs/design/2026-08-29-two-tier-context-and-llm-migration-architecture.md` §7 — read Amendment 1 first -- `docs/journal/2026-09-01-why-the-migration-skill-shipped-hollow.md` — why the previous version of this skill was wrong -- `docs/qna/why-does-agents-init-never-update-existing-instructions.md` -- `docs/qna/how-does-two-tier-agent-context-prevent-scaffold-drift.md` +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 22cfd178..796ebd40 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -224,7 +224,8 @@ jobs: TestHarnessSkillCoversAgentCommands TestMigrationSkillCoversItsSpecifiedProtocol TestMigrationSkillMatchesEmbeddedAsset - TestMigrationSkillPastesTheCanonicalRouter" + 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/docs_test.go b/agents/docs_test.go index c095c2b9..0ce7e517 100644 --- a/agents/docs_test.go +++ b/agents/docs_test.go @@ -290,3 +290,24 @@ func TestMigrationSkillPastesTheCanonicalRouter(t *testing.T) { "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/scaffold/assets/skills/migrating-fleet-context/SKILL.md b/agents/internal/scaffold/assets/skills/migrating-fleet-context/SKILL.md index 6b755457..77447642 100644 --- a/agents/internal/scaffold/assets/skills/migrating-fleet-context/SKILL.md +++ b/agents/internal/scaffold/assets/skills/migrating-fleet-context/SKILL.md @@ -221,7 +221,7 @@ scaffolds them non-destructively. Relocate each entry in `misplaced_docs`: ```bash -git mv docs/journal/2026-08-30-something-plan.md docs/plans/ +git mv docs/journal/-plan.md docs/plans/ ``` Then fix relative markdown links inside the moved files. @@ -332,7 +332,18 @@ Asking costs one message. Guessing costs a rule nobody notices is gone. ## Where this comes from -- `docs/design/2026-08-29-two-tier-context-and-llm-migration-architecture.md` §7 — read Amendment 1 first -- `docs/journal/2026-09-01-why-the-migration-skill-shipped-hollow.md` — why the previous version of this skill was wrong -- `docs/qna/why-does-agents-init-never-update-existing-instructions.md` -- `docs/qna/how-does-two-tier-agent-context-prevent-scaffold-drift.md` +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.