Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ called out in a **Breaking** section.

### 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).
- **`abcd intent "<text>"` no longer files a draft from a mistyped subcommand.**
A near-miss for an intent subverb (`intent paln`, `intent lnk itd-5`) is
refused with a did-you-mean and writes nothing, mirroring `abcd capture`'s
Expand Down
4 changes: 3 additions & 1 deletion internal/core/ahoy/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 33 additions & 3 deletions internal/core/ahoy/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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<!-- BEGIN ABCD -->\nx\n<!-- END ABCD -->\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)
Expand Down