From 3719ca9e5fad94fa5af70cbc727b24b09a8ab5f0 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:44:07 +0100 Subject: [PATCH] fix: stray .abcd/ no longer classifies as managed-repo (iss-88) ahoy folder classification computed the strong (managed) signal as `registered || abcdDir || markerFired`, so the mere presence of an `.abcd/` directory promoted a folder to managed-repo even with no index registration and no marker block. A repo carrying a stray `.abcd/` thus overclaimed managed-repo. Drop `.abcd/` from the strong set: only index registration or a marker block promotes to managed. A stray `.abcd/` now reports unmanaged-repo (git repo) or unmanaged-folder (no git). No new FolderKind is introduced. Detector-first: TestClassifyStrayAbcdDirIsNotManaged asserts a bare `.abcd/` is not managed. Two existing tests encoded the old bug: TestClassifyManagedRepoByAbcdDir asserted `.git`+`.abcd` => managed (repurposed to assert unmanaged-repo), and TestDetectHookManifestGap- OnBrokenPlugin relied on a bare `.abcd/` to reach the gap-detection path (now planted a marker block to make the folder genuinely managed). Assisted-by: Claude:claude-opus-4-8 --- .../iss-88-managed-repo-label-overclaims.md | 1 + CHANGELOG.md | 9 +++++ internal/core/ahoy/detect.go | 4 ++- internal/core/ahoy/detect_test.go | 36 +++++++++++++++++-- 4 files changed, 46 insertions(+), 4 deletions(-) rename .abcd/work/issues/{open => resolved}/iss-88-managed-repo-label-overclaims.md (80%) diff --git a/.abcd/work/issues/open/iss-88-managed-repo-label-overclaims.md b/.abcd/work/issues/resolved/iss-88-managed-repo-label-overclaims.md similarity index 80% rename from .abcd/work/issues/open/iss-88-managed-repo-label-overclaims.md rename to .abcd/work/issues/resolved/iss-88-managed-repo-label-overclaims.md index a17d774..047d3eb 100644 --- a/.abcd/work/issues/open/iss-88-managed-repo-label-overclaims.md +++ b/.abcd/work/issues/resolved/iss-88-managed-repo-label-overclaims.md @@ -7,6 +7,7 @@ category: "observation" source: "agent-finding" found_during: "2026-07-13 B1 dogfood: prepare-this-repo audit of Manuscripts" found_at: "internal/core/ahoy/detect.go" +resolution: "Dropped .abcd/ from the ahoy strong-signal set: only index registration or a marker block promotes to managed-repo; a stray .abcd/ now reports unmanaged. Detector-first in internal/core/ahoy/detect_test.go." --- abcd ahoy classifies a repo with a partial or stray .abcd/ as folder_kind managed-repo even when adopted is null and index_registered is false -- managed reads as abcd manages this when it only means abcd could. No folder_kind value distinguishes a stray .abcd/ built by another workflow from an abcd-adopted repo. Observed on Manuscripts, whose .abcd/development/ was hand-built with zero abcd involvement yet is reported managed. Related: iss-62 managed-repo-identity-gate. Detector: a folder_kind vocabulary that separates adopted from merely-abcd-shaped; acceptance is ahoy --json on a hand-built .abcd/ not reporting managed. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 73ccfa7..74ad9e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,15 @@ called out in a **Breaking** section. questions, and accepts, edits, or strikes every acceptance criterion before `abcd intent plan` is run as their sign-off act. +### Fixed + +- **`abcd ahoy` no longer overclaims `managed-repo` for a stray `.abcd/` + directory (iss-88).** Folder classification treated the mere presence of an + `.abcd/` directory as a strong managed signal, so a repo with an unregistered, + markerless `.abcd/` reported `managed-repo`. Only index registration or a + marker block now promotes a folder to managed; a stray `.abcd/` reports + `unmanaged-repo` (or `unmanaged-folder` outside a git repo). + ## [0.3.0] - 2026-07-18 ### Security diff --git a/internal/core/ahoy/detect.go b/internal/core/ahoy/detect.go index 14a1456..18f22e5 100644 --- a/internal/core/ahoy/detect.go +++ b/internal/core/ahoy/detect.go @@ -100,7 +100,9 @@ func classify(cwd string, id RepoIdentity, idx *historyIndex) (FolderKind, map[s gitRepo := isDir(filepath.Join(cwd, ".git")) signals["git_repo"] = gitRepo - strong := registered || abcdDir || markerFired + // A bare .abcd/ directory is not a managed signal on its own (iss-88): only + // index registration or a marker block promotes a folder to managed-repo. + strong := registered || markerFired switch { case strong: return ManagedRepo, signals diff --git a/internal/core/ahoy/detect_test.go b/internal/core/ahoy/detect_test.go index e1f4fc6..fdb63c1 100644 --- a/internal/core/ahoy/detect_test.go +++ b/internal/core/ahoy/detect_test.go @@ -65,7 +65,10 @@ func TestClassifyUnmanagedRepo(t *testing.T) { } } -func TestClassifyManagedRepoByAbcdDir(t *testing.T) { +// TestClassifyAbcdDirInRepoIsNotManaged pins iss-88: a git repo carrying a stray +// .abcd/ directory but no index registration and no marker block is an +// unmanaged-repo, not a managed-repo — the .abcd/ dir alone never promotes. +func TestClassifyAbcdDirInRepoIsNotManaged(t *testing.T) { setupHermetic(t) dir := t.TempDir() if err := os.Mkdir(filepath.Join(dir, ".git"), 0o755); err != nil { @@ -78,8 +81,29 @@ func TestClassifyManagedRepoByAbcdDir(t *testing.T) { if err != nil { t.Fatal(err) } - if det.FolderKind != ManagedRepo { - t.Errorf("kind = %q, want %q", det.FolderKind, ManagedRepo) + if det.FolderKind != UnmanagedRepo { + t.Errorf("kind = %q, want %q", det.FolderKind, UnmanagedRepo) + } +} + +// TestClassifyStrayAbcdDirIsNotManaged pins iss-88: a bare .abcd/ directory with +// no index registration and no marker block must NOT overclaim managed-repo. With +// no .git present it is an unmanaged folder. +func TestClassifyStrayAbcdDirIsNotManaged(t *testing.T) { + setupHermetic(t) + dir := t.TempDir() + if err := os.Mkdir(filepath.Join(dir, ".abcd"), 0o755); err != nil { + t.Fatal(err) + } + det, err := Detect(dir) + if err != nil { + t.Fatal(err) + } + if det.FolderKind == ManagedRepo { + t.Errorf("stray .abcd/ overclaims managed: kind = %q, want not %q", det.FolderKind, ManagedRepo) + } + if det.FolderKind != UnmanagedFolder { + t.Errorf("kind = %q, want %q", det.FolderKind, UnmanagedFolder) } } @@ -151,6 +175,12 @@ func TestDetectHookManifestGapOnBrokenPlugin(t *testing.T) { if err := os.Mkdir(filepath.Join(dir, ".abcd"), 0o755); err != nil { t.Fatal(err) } + // A marker block makes the folder genuinely managed (post iss-88 the .abcd/ + // dir alone no longer promotes), so the deeper gap checks run. + body := "# Project\n\n\nx\n\n" + if err := os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte(body), 0o644); err != nil { + t.Fatal(err) + } det, err := Detect(dir) if err != nil { t.Fatal(err)