The scan that offered to import itself
Greenhouse has a rule I keep coming back to: nothing gets deleted, and nothing gets moved without asking. So when I built the piece that lets you point the app at a folder you already have projects in - instead of a fresh empty one - the engine side was designed the same way. It scans the folder, shows you what it found, and only touches disk once you say yes.
That engine half was actually built weeks ago, in a different session, and marked done. This week I finally wired it up to a real screen. And within about five minutes of testing it for real, it found itself.
The scan that scanned itself
I pointed the import scanner at a freshly initialized vault - the folder structure Greenhouse creates for you automatically, before you’ve added a single project. The scan came back with nine results. Nine folders it wanted me to assign to a pipeline stage. There was exactly one project in that vault: none. Every single result was Greenhouse’s own internal scaffolding - the ideas folder, the vault folder, every stage folder it had just created for itself.
The scanning function itself was fine. It does exactly one job: list subfolders, skip anything hidden. .greenhouse starts with a dot, so that one got skipped correctly. But none of the app’s own working folders start with a dot. They’re not hidden, they’re just internal. The scanner had no way to know that “internal” and “hidden” aren’t the same thing, because nobody had ever asked it to know the difference - the only test it had ever been given was a folder full of ordinary, generic project subfolders. A scan of an already-initialized vault was a scenario that had simply never come up before, because nothing had ever called this code with a real vault as input.
The fix is almost boring: the generic scanning function stays generic, no opinions about what a “vault” is. One layer up, where the actual configuration is already loaded, I built the exclude list - the ideas folder, the vault folder, every configured stage - and filtered there instead. Keep the dumb tool dumb; let the smart layer be smart.
The second version of the same mistake
Once I’d fixed that, I started testing what happens after you actually import something. Assign a folder to a stage, click the button, watch it appear in your project list. Then, out of habit, I went back and re-ran the scan.
The folder I’d just imported was still in the results. Offering itself up to be imported again.
This one had a cleaner explanation once I thought about it: importing a folder never moves it. That’s deliberate - the whole point is that nothing physically rearranges your files without your say-so. But a plain folder listing has no way to know “this folder is now tracked in the database.” It just sees a folder, sitting exactly where it always was, indistinguishable from one that’s never been touched. Detecting a thing and acting on a thing were two completely separate operations that didn’t talk to each other, and I’d only built one side of that conversation.
Same fix shape as the first bug: the layer that already has both the scan results and the database open checks each candidate against what’s already tracked, and quietly drops the ones that are. The generic scanner still doesn’t need to know anything about databases.
What actually proved this worked
Both of these were the kind of bug that a mocked test would sail straight past, because a mock only ever returns what you told it to return - it can’t discover that your assumptions about “empty folder” or “already imported” don’t match reality. So the last step was running the real thing: a real vault folder, seeded with two real subfolders, driven through the actual compiled app with no fakery at any layer. Scan, confirm both folders show up. Import one for real. Check that a real file landed on disk, not just a database row. Scan again, confirm the imported one disappeared and the other one didn’t. Reload the dashboard, confirm the imported project is sitting there as an actual, clickable card.
Every one of those steps was a chance for one of the two bugs to still be lurking. Neither was, by the end - but I wouldn’t have trusted that without watching it happen for real.
Related reading
Two notes nobody ever saw
Greenhouse saved a capture note and a handoff note faithfully, and showed neither: one had no field in the wire type, the other had a working command nobody called.
Adding a native macOS menu to Greenhouse broke Cmd+Q, and my test harness couldn't press Cmd+N either
set_menu replaces the default menu, it doesn't extend it - taking Quit, Hide, and every Edit shortcut with it. Then the WebDriver actions endpoint turned out unable to press a key.
Teaching a project manager to recognize a Logic Pro session
.logicx files aren't files, they're bundles - and the filter hiding .DS_Store junk was hiding every DAW project too. Plus the silent no-op click that fooled a retry loop.