import { type DurableProjectRoot } from "@sapiom/agent-map/project-roots"; export { resolveProjectRootForPath, type DurableProjectRoot } from "@sapiom/agent-map/project-roots"; /** * Canonical project-root derivation shared by Studio's browser and server. * * UI grouping, trusted project scope, and session capability composition must * resolve the same roots from the same inputs. This module is deliberately * pure and host-neutral: callers supply settings, sessions, pending creation * roots, and registered agent paths; no filesystem or browser API is used. */ import type { SessionStatus } from "./types.js"; /** * Row order within a container. "name" is A-Z; "recent" is * newest-activity-first. It reaches this module because project ORDER is part * of the derivation's output, and the rail renders that order verbatim. */ export type RailSort = "recent" | "name"; /** Everything the rail knows about which folders are projects. */ export interface ProjectRootSources { /** Upstream's workspace list — most-recently-used project directories, * newest first, already deduped and pruned of dead paths at every boot. */ recentDirs: readonly string[]; /** Session cwds widen the candidate set for folders `recentDirs` has not yet * recorded, and carry the recency signal for them. `status` separates the * two very different claims a cwd can make: see rule 2. */ sessions: readonly { cwd: string; createdAt: string; status?: SessionStatus; }[]; /** Folders whose agent is mid-creation: known before any session or agent * exists under them. */ pendingCwds: readonly string[]; /** Existing durable Studio roots. A root already carrying project identity * must not be reclassified when later discovery learns that the directory is * itself an agent; that would strand persisted sessions in a new project. */ pinnedRoots?: readonly string[]; /** * Every registered agent's OWN directory. * * Required, not optional. The rule below cannot be stated without it, and a * caller that forgets it would silently get the old accumulating behaviour * back with every test still green. */ agentPaths: readonly string[]; sort: RailSort; } /** * Choose one deterministic launch root for a multi-root project. * * The outermost active binding wins so a project-wide session starts with the * broadest trusted context. Canonical lexical order breaks equal-depth ties; * callers retain the winning root's original host spelling. */ export declare function preferredProjectRoot(roots: readonly string[]): string | null; /** * Resolve a neutral project session back to its trusted durable root. * * Session cwd may be any descendant used for ordinary coding work. Both the * server scope catalog and the browser rail must contribute the durable root, * rather than independently promoting that descendant into a second project. */ export declare function projectSessionRoot(session: { cwd: string; projectId: string; }, roots: readonly DurableProjectRoot[]): string | null; /** * THE FOLDER THAT HOLDS AN AGENT, or null when nothing better than the agent's * own directory exists. * * ONE ANSWER, because there are two callers and they must not disagree. The * rail's derivation asks it to decide which row to draw; `openProject` asks it * to decide what the picker actually opens when you point it at an agent. * * `projects` must be the list `projectRoots` produces. The guard below is only * as good as the definition of "project" it is handed, and a caller that builds * its own will decline hops the rail would have made, which restores the silent * no-op this exists to remove. `openProject` therefore passes * `projectRoots(...)` verbatim rather than assembling anything. * * Null means REFUSE, and refusing is safe: the agent's own folder stays the * root and renders as a project with that agent inside, which is what opening * an agent's folder honestly means. * * Two reasons to refuse: * * 1. **A filesystem root.** `paths.parentOf` answers `/` (and `C:\`) rather * than null there, deliberately, so that every result stays a listable * path. Taken literally it turns an agent at `/solo` into a project called * `/` holding the entire disk, and the swallow guard below cannot catch it * because at that point there is no other project to swallow yet. * 2. **It would contain another project.** Without this, the clean demo * fixture, whose roots are agent folders sitting beside an ordinary project * under one home directory, promoted them all to `/Users/demo` and produced * a single project holding every other project, with every agent inside it * rendered twice. That is the duplicate-agent rendering this rule exists to * remove, re-created by the repair. * * KNOWN LIMIT, stated rather than papered over: a directory holding nothing but * agent folders and no other project DOES become the project. That is right * everywhere except a home directory, and a home directory in practice always * holds another project, which is what makes the guard fire. A depth floor was * considered and rejected, because every threshold that saves `/Users/demo` * also breaks a legitimate two-segment root. */ /** * WHAT OPENING A FOLDER ACTUALLY OPENS. * * You cannot open a single agent as a project, so pointing the picker at an * agent's own folder opens the folder that holds it. Without this the press is * a silent no-op: `projectRoots` declines to draw a row for an agent-rooted * entry, so the picker says "This is an agent project", the user presses Open, * and nothing changes. * * THE ELIGIBLE PROJECTS ARE `projectRoots`' OWN OUTPUT, not a list assembled * here to resemble it. That is the whole design of this function, and it is the * only version of it that has held: the guard inside `holdingProjectFor` asks * "would this promotion swallow a project", and the answer is only as good as * the definition of "project" it is handed. Four separate attempts to * reconstruct that definition locally were each wrong in a different way, and * every one of them failed in the same direction, by counting something the * rail does not keep and so refusing a hop the rail would have made, which puts * the silent no-op back. * * `projectRoots` is the one place that decides what a project is: chosen * folders, folders with a live session, and session-only folders that hold an * agent no other root already shows, with agent directories excluded and * promotions guarded. Calling it costs one derivation on a user gesture and * removes the entire class of drift, because there is no second definition left * to disagree with. */ export declare function projectToOpen(requested: string, sources: ProjectRootSources): string; export declare function holdingProjectFor(agentDir: string, { agentPaths, projects, }: { agentPaths: readonly string[]; projects: readonly string[]; }): string | null; /** * THE ORDERED LIST OF PROJECT ROOTS. * * One sentence governs this whole function: * * A PROJECT IS A DIRECTORY YOU CHOSE THAT HOLDS AGENTS. * * Both clauses keep session working directories from accumulating as * duplicate project rows. * * RULE 1, "you chose": an agent's OWN directory is not a project. The project * is the directory that HOLDS agents; the agent is the thing inside it. A root * that is itself a registered agent is a category error, and it is the single * cause of both symptoms a real install shows. Its dependency graph has exactly * one node, because nothing else is inside it. And it renders the agent TWICE * whenever some other open project also contains it, once correctly nested and * once again at top level under a different label, because `buildProjectTree` * deliberately files an agent under EVERY root that contains it. An agent-rooted * entry whose agent another project already shows is dropped, and one nothing shows * is replaced by its nearest non-agent ancestor. * * This is not a new rule. `project-membership.agentNeedsOwnProject` has * enforced it on every NEW registration since the accumulation was diagnosed: * "an agent an open project already contains needs nothing remembered". It was * simply never applied to the entries already in the list, so the guard stopped * the bleeding and left the wound. Applying one rule in one direction only is * why SAP-2927 looked complete while the rail still looked broken. * * RULE 2, "that holds agents": a folder known ONLY because a session ran there * earns a row only if it holds an agent no other project already shows, OR a * session is LIVE in it. "A session ran here once and exited" and "something is * running here right now" are different claims, and collapsing them cost a real * case immediately: a bare scaffold session, a live session in a folder with no * agent yet, is exactly how you start an agent in an empty folder, and dropping * its row makes a running session unreachable from the rail. * `recentDirs` is chosen and capped at 8; session cwds are neither, which is * why the second list has to earn its rows and the first does not. Two failures * collapse into that one clause. A visited folder with no agent is not a * project, while an empty project you OPENED keeps its row, because opening a * folder in order to build the first agent in it is the whole point of that * row. And a visited folder INSIDE a project you already opened is not a second * context: `~/polsia` and `~/polsia/services/workers` are two useful views of * one agent when you opened both, and the same agent printed twice when the * inner row is merely where a session happened to start. * * NOTHING IS DELETED. Both rules are derivational: `recentDirs` on disk is * untouched and any folder is one "Add a project" away from coming back. That * is what makes this safe to apply to an install nobody audited, and why it * needs no migration, no first-run flow and no undo. The design's original "no * migration, every entry becomes a project" rule is kept in spirit and dropped * in letter: nothing a user had disappears, but residue of a fixed bug stops * being rendered as a choice they made. */ export declare function projectRoots({ recentDirs, sessions, pendingCwds, pinnedRoots, agentPaths, sort, }: ProjectRootSources): string[]; //# sourceMappingURL=project-roots.d.ts.map