diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md
index 70a322881..616cb4370 100644
--- a/packages/coding-agent/CHANGELOG.md
+++ b/packages/coding-agent/CHANGELOG.md
@@ -361,6 +361,10 @@
- Removed `/debug dump-next-request` command
- Removed Wafer Pass from CLI credential help; Wafer Serverless remains available.
+### Fixed
+
+- Fixed advisor and status-line context when the session cwd is a parent of a single child git repo, so nested-repo work is surfaced before missing parent-cwd paths are treated as destroyed. ([#3130](https://github.com/can1357/oh-my-pi/issues/3130))
+
## [16.1.8] - 2026-06-20
### Added
diff --git a/packages/coding-agent/src/advisor/watchdog.ts b/packages/coding-agent/src/advisor/watchdog.ts
index d3595ff81..0d9f06575 100644
--- a/packages/coding-agent/src/advisor/watchdog.ts
+++ b/packages/coding-agent/src/advisor/watchdog.ts
@@ -1,9 +1,23 @@
import * as os from "node:os";
import * as path from "node:path";
-import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
+import { getAgentDir, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
import { expandAtImports } from "../discovery/at-imports";
+import activeRepoWatchdogTemplate from "../prompts/advisor/active-repo-watchdog.md" with { type: "text" };
+import type { ActiveRepoContext } from "../utils/active-repo-context";
import { repo } from "../utils/git";
+function normalizePromptPath(value: string): string {
+ return value.replace(/\\/g, "/");
+}
+
+export function formatActiveRepoWatchdogPrompt(activeRepoContext: ActiveRepoContext): string {
+ return prompt
+ .render(activeRepoWatchdogTemplate, {
+ relativeRepoRoot: normalizePromptPath(activeRepoContext.relativeRepoRoot),
+ })
+ .trim();
+}
+
/**
* Discover and load WATCHDOG.md files walking up from cwd, project .omp folder, and user agent dir.
* Returns formatted watchdog file blocks ready to be appended to the advisor system prompt.
diff --git a/packages/coding-agent/src/modes/components/status-line/component.ts b/packages/coding-agent/src/modes/components/status-line/component.ts
index d4d7b7f4b..e2605b4a9 100644
--- a/packages/coding-agent/src/modes/components/status-line/component.ts
+++ b/packages/coding-agent/src/modes/components/status-line/component.ts
@@ -9,6 +9,7 @@ import { settings } from "../../../config/settings";
import type { AgentSession } from "../../../session/agent-session";
import type { OAuthAccountIdentity } from "../../../session/auth-storage";
import { limitMatchesActiveAccount } from "../../../slash-commands/helpers/active-oauth-account";
+import { type ActiveRepoContext, resolveActiveRepoContextSync } from "../../../utils/active-repo-context";
import * as git from "../../../utils/git";
import { getSessionAccentAnsi, getSessionAccentHex } from "../../../utils/session-color";
import { sanitizeStatusText } from "../../shared";
@@ -155,6 +156,12 @@ interface ContextUsageMemo {
skillsRef: readonly any[] | undefined;
}
+interface ActiveRepoCache {
+ projectDir: string;
+ activeRepo: ActiveRepoContext | null;
+ effectiveGitCwd: string;
+}
+
const EMPTY_MESSAGES: readonly AgentMessage[] = [];
const STATUS_USAGE_START_DELAY_MS = 0;
const STATUS_USAGE_REFRESH_TIMEOUT_MS = 2_000;
@@ -196,17 +203,20 @@ export class StatusLineComponent implements Component {
#goalModeStatus: { enabled: boolean; paused: boolean } | null = null;
#collabStatus: CollabStatus | null = null;
#focusedAgentId: string | undefined;
+ #activeRepoCache: ActiveRepoCache | undefined;
// Git status caching (1s TTL)
#cachedGitStatus: { staged: number; unstaged: number; untracked: number } | null = null;
+ #cachedGitStatusCwd: string | undefined = undefined;
#gitStatusLastFetch = 0;
- #gitStatusInFlight = false;
+ #gitStatusInFlightCwd: string | undefined = undefined;
// PR lookup caching (invalidated on branch/repo context changes)
#cachedPr: { number: number; url: string } | null | undefined = undefined;
#cachedPrContext: PrCacheContext | undefined = undefined;
#prLookupInFlight = false;
#defaultBranch?: string;
+ #defaultBranchCwd: string | undefined = undefined;
#lastTokensPerSecond: number | null = null;
#lastTokensPerSecondTimestamp: number | null = null;
@@ -249,6 +259,18 @@ export class StatusLineComponent implements Component {
);
}
+ #resolveActiveRepoCache(): ActiveRepoCache {
+ const projectDir = getProjectDir();
+ if (this.#activeRepoCache?.projectDir === projectDir) {
+ return this.#activeRepoCache;
+ }
+
+ const activeRepo = resolveActiveRepoContextSync(projectDir);
+ const effectiveGitCwd = activeRepo?.repoRoot ?? projectDir;
+ this.#activeRepoCache = { projectDir, activeRepo, effectiveGitCwd };
+ return this.#activeRepoCache;
+ }
+
/**
* Re-point the status line at another session (focus proxy). Invalidate: model/context/usage all derive
* from it. `focusedAgentId` is the focused subagent id while the view is proxied, undefined for main.
@@ -335,7 +357,8 @@ export class StatusLineComponent implements Component {
return;
}
- const repository = git.repo.resolveSync(getProjectDir());
+ const { effectiveGitCwd } = this.#resolveActiveRepoCache();
+ const repository = git.repo.resolveSync(effectiveGitCwd);
if (!repository) return;
const watchPath = git.repo.isReftableSync(repository)
@@ -390,17 +413,17 @@ export class StatusLineComponent implements Component {
this.#cachedBranchCwd = undefined;
this.#cachedPrContext = undefined;
}
- #getCurrentBranch(): string | null {
+ #getCurrentBranch(effectiveGitCwd?: string): string | null {
if (!this.#gitEnabled()) return null;
- const cwd = getProjectDir();
- if (this.#cachedBranch !== undefined && this.#cachedBranchCwd === cwd) {
+ const gitCwd = effectiveGitCwd ?? this.#resolveActiveRepoCache().effectiveGitCwd;
+ if (this.#cachedBranch !== undefined && this.#cachedBranchCwd === gitCwd) {
return this.#cachedBranch;
}
- const head = git.head.resolveSync(cwd);
+ const head = git.head.resolveSync(gitCwd);
const gitHeadPath = head?.headPath ?? null;
- this.#cachedBranchCwd = cwd;
+ this.#cachedBranchCwd = gitCwd;
this.#cachedBranchRepoId = gitHeadPath;
if (!head) {
this.#cachedBranch = null;
@@ -412,12 +435,18 @@ export class StatusLineComponent implements Component {
return this.#cachedBranch ?? null;
}
- #isDefaultBranch(branch: string): boolean {
+ #isDefaultBranch(branch: string, effectiveGitCwd: string): boolean {
+ if (this.#defaultBranchCwd !== effectiveGitCwd) {
+ this.#defaultBranch = undefined;
+ this.#defaultBranchCwd = effectiveGitCwd;
+ }
+
if (this.#defaultBranch === undefined) {
this.#defaultBranch = "main";
+ const lookupCwd = effectiveGitCwd;
(async () => {
- const resolved = await git.branch.default(getProjectDir());
- if (this.#disposed) return;
+ const resolved = await git.branch.default(lookupCwd);
+ if (this.#disposed || this.#defaultBranchCwd !== lookupCwd) return;
if (resolved) {
this.#defaultBranch = resolved;
if (this.#onBranchChange) {
@@ -429,32 +458,43 @@ export class StatusLineComponent implements Component {
return branch === this.#defaultBranch;
}
- #getGitStatus(): { staged: number; unstaged: number; untracked: number } | null {
+ #getGitStatus(effectiveGitCwd?: string): { staged: number; unstaged: number; untracked: number } | null {
if (!this.#gitEnabled()) return null;
- if (this.#gitStatusInFlight || Date.now() - this.#gitStatusLastFetch < 1000) {
+
+ const gitCwd = effectiveGitCwd ?? this.#resolveActiveRepoCache().effectiveGitCwd;
+ if (this.#gitStatusInFlightCwd !== undefined) {
+ return this.#cachedGitStatusCwd === gitCwd ? this.#cachedGitStatus : null;
+ }
+ if (this.#cachedGitStatusCwd === gitCwd && Date.now() - this.#gitStatusLastFetch < 1000) {
return this.#cachedGitStatus;
}
- this.#gitStatusInFlight = true;
+ this.#gitStatusInFlightCwd = gitCwd;
(async () => {
+ let nextStatus: { staged: number; unstaged: number; untracked: number } | null = null;
try {
- this.#cachedGitStatus = await git.status.summary(getProjectDir());
+ nextStatus = await git.status.summary(gitCwd);
} catch {
- this.#cachedGitStatus = null;
+ nextStatus = null;
} finally {
- this.#gitStatusLastFetch = Date.now();
- this.#gitStatusInFlight = false;
+ if (this.#gitStatusInFlightCwd === gitCwd) {
+ this.#cachedGitStatus = nextStatus;
+ this.#cachedGitStatusCwd = gitCwd;
+ this.#gitStatusLastFetch = Date.now();
+ this.#gitStatusInFlightCwd = undefined;
+ }
}
})();
- return this.#cachedGitStatus;
+ return this.#cachedGitStatusCwd === gitCwd ? this.#cachedGitStatus : null;
}
- #lookupPr(): { number: number; url: string } | null {
+ #lookupPr(effectiveGitCwd?: string): { number: number; url: string } | null {
if (!this.#gitEnabled()) return null;
- const branch = this.#getCurrentBranch();
+ const gitCwd = effectiveGitCwd ?? this.#resolveActiveRepoCache().effectiveGitCwd;
+ const branch = this.#getCurrentBranch(gitCwd);
const currentContext = branch ? createPrCacheContext(branch, this.#cachedBranchRepoId ?? null) : null;
if (canReuseCachedPr(this.#cachedPr, this.#cachedPrContext, currentContext)) {
@@ -463,13 +503,20 @@ export class StatusLineComponent implements Component {
const stalePr = this.#cachedPr;
- // Don't look up if no branch, detached HEAD, default branch, or already in flight
- if (!branch || branch === "detached" || this.#isDefaultBranch(branch) || this.#prLookupInFlight) {
+ if (!branch) {
+ this.#cachedPr = null;
+ this.#cachedPrContext = undefined;
+ return null;
+ }
+
+ // Don't look up if detached, default branch, or already in flight.
+ if (branch === "detached" || this.#isDefaultBranch(branch, gitCwd) || this.#prLookupInFlight) {
return stalePr ?? null;
}
this.#prLookupInFlight = true;
const lookupContext = currentContext;
+ const lookupCwd = gitCwd;
// Fire async lookup, keep stale value visible until resolved
(async () => {
@@ -486,7 +533,7 @@ export class StatusLineComponent implements Component {
};
try {
// Requires `gh repo set-default` to be configured; fails gracefully if not
- const result = await $`gh pr view --json number,url`.quiet().nothrow();
+ const result = await $`gh pr view --json number,url`.cwd(lookupCwd).quiet().nothrow();
if (this.#disposed) return;
if (result.exitCode !== 0) {
setCachedPr(null);
@@ -795,13 +842,14 @@ export class StatusLineComponent implements Component {
contextPercent = collabState.contextUsage.percent ?? contextPercent;
}
- const gitBranch = includeGit || includePr ? this.#getCurrentBranch() : null;
- const gitStatus = includeGit ? this.#getGitStatus() : null;
- const gitPr = includePr ? this.#lookupPr() : null;
-
+ const activeRepoCache = this.#resolveActiveRepoCache();
+ const gitBranch = includeGit || includePr ? this.#getCurrentBranch(activeRepoCache.effectiveGitCwd) : null;
+ const gitStatus = includeGit ? this.#getGitStatus(activeRepoCache.effectiveGitCwd) : null;
+ const gitPr = includePr ? this.#lookupPr(activeRepoCache.effectiveGitCwd) : null;
return {
session: this.session,
focusedAgentId: this.#focusedAgentId,
+ activeRepo: activeRepoCache.activeRepo,
width,
options: segmentOptions ?? {},
planMode: this.#planModeStatus,
diff --git a/packages/coding-agent/src/modes/components/status-line/segments.ts b/packages/coding-agent/src/modes/components/status-line/segments.ts
index 1b6c37375..d73aaeabf 100644
--- a/packages/coding-agent/src/modes/components/status-line/segments.ts
+++ b/packages/coding-agent/src/modes/components/status-line/segments.ts
@@ -202,7 +202,7 @@ const pathSegment: StatusLineSegment = {
render(ctx) {
const opts = ctx.options.path ?? {};
- const projectDir = getProjectDir();
+ const projectDir = ctx.activeRepo?.cwd ?? getProjectDir();
const { scratch, relative } = classifyProjectDir(projectDir);
let pwd = projectDir;
@@ -213,6 +213,9 @@ const pathSegment: StatusLineSegment = {
pwd = stripDisplayRoot(pwd);
}
}
+ if (ctx.activeRepo) {
+ pwd = `${pwd} ↳ ${ctx.activeRepo.relativeRepoRoot}`;
+ }
if (opts.abbreviate !== false) {
pwd = shortenPath(pwd);
}
diff --git a/packages/coding-agent/src/modes/components/status-line/types.ts b/packages/coding-agent/src/modes/components/status-line/types.ts
index 213bfec94..b8528bdd2 100644
--- a/packages/coding-agent/src/modes/components/status-line/types.ts
+++ b/packages/coding-agent/src/modes/components/status-line/types.ts
@@ -1,6 +1,7 @@
import type { CollabSessionState } from "../../../collab/protocol";
import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../../config/settings-schema";
import type { AgentSession } from "../../../session/agent-session";
+import type { ActiveRepoContext } from "../../../utils/active-repo-context";
export type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle };
@@ -47,6 +48,7 @@ export interface SegmentContext {
session: AgentSession;
/** Focused subagent id while the view is proxied at its session, undefined otherwise. */
focusedAgentId?: string | undefined;
+ activeRepo: ActiveRepoContext | null;
width: number;
options: StatusLineSegmentOptions;
planMode: {
diff --git a/packages/coding-agent/src/prompts/advisor/active-repo-watchdog.md b/packages/coding-agent/src/prompts/advisor/active-repo-watchdog.md
new file mode 100644
index 000000000..416074175
--- /dev/null
+++ b/packages/coding-agent/src/prompts/advisor/active-repo-watchdog.md
@@ -0,0 +1,6 @@
+Especially pay attention to:
+
+The session cwd is outside git, and exactly one direct child git repository was detected at `{{relativeRepoRoot}}`.
+
+Paths under `{{relativeRepoRoot}}/` are the active project. Do not claim work is missing, destroyed, or absent at the parent cwd until you have checked under `{{relativeRepoRoot}}/`.
+
diff --git a/packages/coding-agent/src/prompts/system/active-repo-context.md b/packages/coding-agent/src/prompts/system/active-repo-context.md
new file mode 100644
index 000000000..f7d89998b
--- /dev/null
+++ b/packages/coding-agent/src/prompts/system/active-repo-context.md
@@ -0,0 +1,4 @@
+
+The session cwd is outside git. Exactly one direct child git repository was detected at `{{relativeRepoRoot}}`.
+Paths under `{{relativeRepoRoot}}/` are the active project for this session. Parent-cwd misses are inconclusive until checking under `{{relativeRepoRoot}}/`.
+
diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts
index 875a63b4e..d577d2a83 100644
--- a/packages/coding-agent/src/sdk.ts
+++ b/packages/coding-agent/src/sdk.ts
@@ -25,7 +25,7 @@ import { FALLBACK_DIALECT, preferredDialect } from "@oh-my-pi/pi-catalog/identit
import type { Component } from "@oh-my-pi/pi-tui";
import { $env, $flag, getAgentDir, getProjectDir, logger, postmortem, prompt, Snowflake } from "@oh-my-pi/pi-utils";
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
-import { ADVISOR_READONLY_TOOL_NAMES, discoverWatchdogFiles } from "./advisor";
+import { ADVISOR_READONLY_TOOL_NAMES, discoverWatchdogFiles, formatActiveRepoWatchdogPrompt } from "./advisor";
import { type AsyncJob, AsyncJobManager } from "./async";
import { AutoLearnController, buildAutoLearnInstructions } from "./autolearn/controller";
import { loadCapability } from "./capability";
@@ -189,6 +189,7 @@ import { getImageGenTools } from "./tools/image-gen";
import { wrapToolWithMetaNotice } from "./tools/output-meta";
import { queueResolveHandler } from "./tools/resolve";
import { ttsTool } from "./tools/tts";
+import { resolveActiveRepoContext } from "./utils/active-repo-context";
import { EventBus } from "./utils/event-bus";
import { buildNamedToolChoice } from "./utils/tool-choice";
import { buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
@@ -1146,6 +1147,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
? Promise.resolve(options.contextFiles)
: logger.time("discoverContextFiles", discoverContextFiles, cwd, agentDir);
contextFilesPromise.catch(() => {});
+ const activeRepoContextPromise = logger.time("resolveActiveRepoContext", async () => {
+ try {
+ return await resolveActiveRepoContext(cwd);
+ } catch (err) {
+ logger.debug("Failed to resolve active repo context", { err: String(err) });
+ return null;
+ }
+ });
+ activeRepoContextPromise.catch(() => {});
const watchdogFilesPromise = logger.time("discoverWatchdogFiles", () => discoverWatchdogFiles(cwd, agentDir));
watchdogFilesPromise.catch(() => {});
const promptTemplatesPromise = options.promptTemplates
@@ -1393,10 +1403,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
}
return result;
};
- const [contextFiles, resolvedWorkspaceTree, watchdogFiles] = await Promise.all([
+ const [contextFiles, resolvedWorkspaceTree, watchdogFiles, activeRepoContext] = await Promise.all([
contextFilesPromise,
raceWithDeadline("buildWorkspaceTree", workspaceTreePromise),
watchdogFilesPromise,
+ activeRepoContextPromise,
]);
let agent: Agent;
@@ -2268,6 +2279,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
model: settings.get("includeModelInPrompt") ? getActiveModelString() : undefined,
personality: agentKind === "sub" ? "none" : settings.get("personality"),
renderMermaid: settings.get("tui.renderMermaid"),
+ activeRepoContext,
});
if (options.systemPrompt === undefined) {
@@ -2661,10 +2673,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
.filter((tool): tool is Tool => tool != null)
.map(wrapToolWithMetaNotice);
- let advisorWatchdogPrompt: string | undefined;
- if (watchdogFiles && watchdogFiles.length > 0) {
- advisorWatchdogPrompt = watchdogFiles.join("\n\n");
+ const advisorWatchdogPrompts = [...watchdogFiles];
+ if (activeRepoContext) {
+ advisorWatchdogPrompts.push(formatActiveRepoWatchdogPrompt(activeRepoContext));
}
+ const advisorWatchdogPrompt = advisorWatchdogPrompts.length > 0 ? advisorWatchdogPrompts.join("\n\n") : undefined;
// Owned only when this session created the manager; subagents receive a
// parent's manager via `options.mcpManager` and MUST NOT disconnect it.
const ownedMcpManager = options.mcpManager ? undefined : mcpManager;
diff --git a/packages/coding-agent/src/system-prompt.ts b/packages/coding-agent/src/system-prompt.ts
index 7961aa7cd..96efb935f 100644
--- a/packages/coding-agent/src/system-prompt.ts
+++ b/packages/coding-agent/src/system-prompt.ts
@@ -16,6 +16,7 @@ import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile
import { expandAtImports } from "./discovery/at-imports";
import { loadSkills, type Skill } from "./extensibility/skills";
import { hasObsidian } from "./internal-urls/vault-protocol";
+import activeRepoContextTemplate from "./prompts/system/active-repo-context.md" with { type: "text" };
import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
import defaultPersonality from "./prompts/system/personalities/default.md" with { type: "text" };
import friendlyPersonality from "./prompts/system/personalities/friendly.md" with { type: "text" };
@@ -23,6 +24,7 @@ import pragmaticPersonality from "./prompts/system/personalities/pragmatic.md" w
import projectPromptTemplate from "./prompts/system/project-prompt.md" with { type: "text" };
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
import { shortenPath } from "./tools/render-utils";
+import { type ActiveRepoContext, resolveActiveRepoContext } from "./utils/active-repo-context";
import { AGENTS_MD_LIMIT, buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
/** Bundled personality specs, keyed by the `personality` setting value. */
@@ -90,6 +92,19 @@ function firstNonEmpty(...values: (string | undefined | null)[]): string | null
return null;
}
+function normalizePromptPath(value: string): string {
+ return value.replace(/\\/g, "/");
+}
+
+function renderActiveRepoContextPrompt(activeRepoContext: ActiveRepoContext | null): string {
+ if (!activeRepoContext) return "";
+ return prompt
+ .render(activeRepoContextTemplate, {
+ relativeRepoRoot: normalizePromptPath(activeRepoContext.relativeRepoRoot),
+ })
+ .trim();
+}
+
function parseWmicTable(output: string, header: string): string | null {
const lines = output
.split("\n")
@@ -425,6 +440,8 @@ export interface BuildSystemPromptOptions {
includeWorkspaceTree?: boolean;
/** Whether Mermaid fenced blocks render as terminal ASCII diagrams. Default: true */
renderMermaid?: boolean;
+ /** Pre-resolved nested active repo context. Undefined resolves from cwd. */
+ activeRepoContext?: ActiveRepoContext | null;
}
/** Result of building provider-facing system prompt messages. */
@@ -467,6 +484,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
personality = "default",
includeWorkspaceTree = false,
renderMermaid = true,
+ activeRepoContext: providedActiveRepoContext,
} = options;
const inlineToolDescriptors = providedInlineToolDescriptors ?? false;
const resolvedCwd = cwd ?? getProjectDir();
@@ -484,6 +502,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
totalLines: 0,
agentsMdFiles: [],
} satisfies WorkspaceTree,
+ activeRepoContext: null as ActiveRepoContext | null,
};
const deadline = Bun.sleep(SYSTEM_PROMPT_PREP_TIMEOUT_MS).then(() => "__timeout__" as const);
@@ -546,34 +565,42 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
: skillsSettings?.enabled !== false
? loadSkills({ ...skillsSettings, cwd: resolvedCwd }).then(result => result.skills)
: Promise.resolve([]);
+ const activeRepoContextPromise =
+ providedActiveRepoContext !== undefined
+ ? Promise.resolve(providedActiveRepoContext)
+ : logger.time("resolveActiveRepoContext", () => resolveActiveRepoContext(resolvedCwd));
- const [resolvedCustomPrompt, resolvedAppendPrompt, systemPromptCustomization, contextFiles, skills, workspaceTree] =
- await Promise.all([
- withDeadline(
- "customPrompt",
- providedResolvedCustomPrompt !== undefined
- ? Promise.resolve(providedResolvedCustomPrompt)
- : resolvePromptInput(customPrompt, "system prompt"),
- prepDefaults.resolvedCustomPrompt,
- ),
- withDeadline(
- "appendSystemPrompt",
- providedResolvedAppendPrompt !== undefined
- ? Promise.resolve(providedResolvedAppendPrompt)
- : resolvePromptInput(appendSystemPrompt, "append system prompt"),
- prepDefaults.resolvedAppendPrompt,
- ),
- withDeadline(
- "loadSystemPromptFiles",
- systemPromptCustomizationPromise,
- prepDefaults.systemPromptCustomization,
- ),
- withDeadline("loadProjectContextFiles", contextFilesPromise, prepDefaults.contextFiles).then(
- dedupeExactContextFiles,
- ),
- withDeadline("loadSkills", skillsPromise, prepDefaults.skills),
- withDeadline("buildWorkspaceTree", workspaceTreePromise, prepDefaults.workspaceTree),
- ]);
+ const [
+ resolvedCustomPrompt,
+ resolvedAppendPrompt,
+ systemPromptCustomization,
+ contextFiles,
+ skills,
+ workspaceTree,
+ activeRepoContext,
+ ] = await Promise.all([
+ withDeadline(
+ "customPrompt",
+ providedResolvedCustomPrompt !== undefined
+ ? Promise.resolve(providedResolvedCustomPrompt)
+ : resolvePromptInput(customPrompt, "system prompt"),
+ prepDefaults.resolvedCustomPrompt,
+ ),
+ withDeadline(
+ "appendSystemPrompt",
+ providedResolvedAppendPrompt !== undefined
+ ? Promise.resolve(providedResolvedAppendPrompt)
+ : resolvePromptInput(appendSystemPrompt, "append system prompt"),
+ prepDefaults.resolvedAppendPrompt,
+ ),
+ withDeadline("loadSystemPromptFiles", systemPromptCustomizationPromise, prepDefaults.systemPromptCustomization),
+ withDeadline("loadProjectContextFiles", contextFilesPromise, prepDefaults.contextFiles).then(
+ dedupeExactContextFiles,
+ ),
+ withDeadline("loadSkills", skillsPromise, prepDefaults.skills),
+ withDeadline("buildWorkspaceTree", workspaceTreePromise, prepDefaults.workspaceTree),
+ withDeadline("resolveActiveRepoContext", activeRepoContextPromise, prepDefaults.activeRepoContext),
+ ]);
const agentsMdFiles = Array.from(new Set(workspaceTree.agentsMdFiles)).sort().slice(0, AGENTS_MD_LIMIT);
if (timedOut.length > 0) {
@@ -598,7 +625,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
const date = new Date().toISOString().slice(0, 10);
const dateTime = date;
- const promptCwd = shortenPath(resolvedCwd.replace(/\\/g, "/"));
+ const promptCwd = shortenPath(normalizePromptPath(resolvedCwd));
+ const activeRepoContextPrompt = renderActiveRepoContextPrompt(activeRepoContext);
// Build tool metadata for system prompt rendering.
// Priority: explicit list > tools map > conservative SDK fallback.
@@ -697,6 +725,9 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
if (projectPrompt) {
systemPrompt.push(projectPrompt);
}
+ if (activeRepoContextPrompt) {
+ systemPrompt.push(activeRepoContextPrompt);
+ }
return { systemPrompt };
}
diff --git a/packages/coding-agent/src/utils/active-repo-context.ts b/packages/coding-agent/src/utils/active-repo-context.ts
new file mode 100644
index 000000000..41226281b
--- /dev/null
+++ b/packages/coding-agent/src/utils/active-repo-context.ts
@@ -0,0 +1,127 @@
+import * as fs from "node:fs";
+import * as fsPromises from "node:fs/promises";
+import * as path from "node:path";
+
+import { type GitRepository, repo } from "./git";
+
+export interface ActiveRepoContext {
+ cwd: string;
+ repoRoot: string;
+ relativeRepoRoot: string;
+ source: "single-direct-child-repo";
+}
+
+function compareEntryNames(left: fs.Dirent, right: fs.Dirent): number {
+ if (left.name < right.name) return -1;
+ if (left.name > right.name) return 1;
+ return 0;
+}
+
+function buildContext(cwd: string, repoRoot: string): ActiveRepoContext {
+ const resolvedCwd = path.resolve(cwd);
+ const resolvedRepoRoot = path.resolve(repoRoot);
+ return {
+ cwd: resolvedCwd,
+ repoRoot: resolvedRepoRoot,
+ relativeRepoRoot: path.relative(resolvedCwd, resolvedRepoRoot),
+ source: "single-direct-child-repo",
+ };
+}
+
+async function resolveRepository(cwd: string): Promise {
+ try {
+ return await repo.resolve(cwd);
+ } catch {
+ return null;
+ }
+}
+
+function resolveRepositorySync(cwd: string): GitRepository | null {
+ try {
+ return repo.resolveSync(cwd);
+ } catch {
+ return null;
+ }
+}
+
+async function readDirectChildren(cwd: string): Promise {
+ try {
+ const entries = await fsPromises.readdir(cwd, { withFileTypes: true });
+ entries.sort(compareEntryNames);
+ return entries;
+ } catch {
+ return [];
+ }
+}
+
+function readDirectChildrenSync(cwd: string): fs.Dirent[] {
+ try {
+ const entries = fs.readdirSync(cwd, { withFileTypes: true });
+ entries.sort(compareEntryNames);
+ return entries;
+ } catch {
+ return [];
+ }
+}
+
+async function resolveDirectChildDirectory(cwd: string, entry: fs.Dirent): Promise {
+ const childPath = path.join(cwd, entry.name);
+ if (entry.isDirectory()) return childPath;
+ if (!entry.isSymbolicLink()) return null;
+ try {
+ const stat = await fsPromises.stat(childPath);
+ return stat.isDirectory() ? childPath : null;
+ } catch {
+ return null;
+ }
+}
+
+function resolveDirectChildDirectorySync(cwd: string, entry: fs.Dirent): string | null {
+ const childPath = path.join(cwd, entry.name);
+ if (entry.isDirectory()) return childPath;
+ if (!entry.isSymbolicLink()) return null;
+ try {
+ const stat = fs.statSync(childPath);
+ return stat.isDirectory() ? childPath : null;
+ } catch {
+ return null;
+ }
+}
+
+async function findSingleDirectChildRepo(cwd: string): Promise {
+ let context: ActiveRepoContext | null = null;
+ for (const entry of await readDirectChildren(cwd)) {
+ const childPath = await resolveDirectChildDirectory(cwd, entry);
+ if (!childPath) continue;
+ const repository = await resolveRepository(childPath);
+ if (!repository || path.resolve(repository.repoRoot) !== path.resolve(childPath)) continue;
+ if (context) return null;
+ context = buildContext(cwd, repository.repoRoot);
+ }
+ return context;
+}
+
+function findSingleDirectChildRepoSync(cwd: string): ActiveRepoContext | null {
+ let context: ActiveRepoContext | null = null;
+ for (const entry of readDirectChildrenSync(cwd)) {
+ const childPath = resolveDirectChildDirectorySync(cwd, entry);
+ if (!childPath) continue;
+ const repository = resolveRepositorySync(childPath);
+ if (!repository || path.resolve(repository.repoRoot) !== path.resolve(childPath)) continue;
+ if (context) return null;
+ context = buildContext(cwd, repository.repoRoot);
+ }
+ return context;
+}
+
+export async function resolveActiveRepoContext(cwd: string): Promise {
+ const resolvedCwd = path.resolve(cwd);
+ if (await resolveRepository(resolvedCwd)) return null;
+ return findSingleDirectChildRepo(resolvedCwd);
+}
+
+export function resolveActiveRepoContextSync(cwd: string): ActiveRepoContext | null {
+ const resolvedCwd = path.resolve(cwd);
+ if (resolveRepositorySync(resolvedCwd)) return null;
+ return findSingleDirectChildRepoSync(resolvedCwd);
+}
diff --git a/packages/coding-agent/test/advisor-watchdog.test.ts b/packages/coding-agent/test/advisor-watchdog.test.ts
index 5e1e0291b..eb10b35f9 100644
--- a/packages/coding-agent/test/advisor-watchdog.test.ts
+++ b/packages/coding-agent/test/advisor-watchdog.test.ts
@@ -20,6 +20,62 @@ describe("advisor watchdog prompt discovery", () => {
}
});
+ async function withAdvisorHistory(
+ tempDir: TempDir,
+ cwd: string,
+ run: (dump: string) => void | Promise,
+ ): Promise {
+ const authStorage = await AuthStorage.create(tempDir.join("testauth.db"));
+ let session: AgentSession | undefined;
+ try {
+ authStorage.setRuntimeApiKey("openai", "test-key");
+ const modelRegistry = new ModelRegistry(authStorage);
+ const sessionManager = SessionManager.create(cwd, tempDir.join("sessions"));
+ const result = await createAgentSession({
+ cwd,
+ agentDir: tempDir.path(),
+ sessionManager,
+ authStorage,
+ modelRegistry,
+ settings: (() => {
+ const s = Settings.isolated({
+ "async.enabled": false,
+ "advisor.enabled": true,
+ });
+ s.setModelRole("advisor", "openai/gpt-4o-mini");
+ return s;
+ })(),
+ model: getBundledModel("openai", "gpt-4o-mini"),
+ disableExtensionDiscovery: true,
+ skills: [],
+ contextFiles: [],
+ workspaceTree: {
+ rootPath: cwd,
+ rendered: "",
+ truncated: false,
+ totalLines: 0,
+ agentsMdFiles: [],
+ },
+ promptTemplates: [],
+ slashCommands: [],
+ enableMCP: false,
+ enableLsp: false,
+ });
+ session = result.session;
+
+ expect(session.isAdvisorActive()).toBe(true);
+ const dump = session.formatAdvisorHistoryAsText();
+ if (dump === null) throw new Error("Advisor history was not available.");
+ await run(dump);
+ } finally {
+ try {
+ await session?.dispose();
+ } finally {
+ authStorage.close();
+ }
+ }
+ }
+
it("discovers and appends WATCHDOG.md to the advisor prompt", async () => {
const tempDir = TempDir.createSync("@pi-advisor-watchdog-");
tempDirs.push(tempDir);
@@ -77,6 +133,39 @@ describe("advisor watchdog prompt discovery", () => {
}
});
+ it("adds built-in active child repo context to the advisor prompt", async () => {
+ const tempDir = TempDir.createSync("@pi-advisor-watchdog-");
+ tempDirs.push(tempDir);
+ const cwd = tempDir.join("parent-cwd");
+ fs.mkdirSync(path.join(cwd, "active-project", ".git"), { recursive: true });
+ const watchdogContent = "Parent watchdog remains before built-in active repo context.";
+ fs.writeFileSync(path.join(cwd, "WATCHDOG.md"), watchdogContent, "utf8");
+
+ await withAdvisorHistory(tempDir, cwd, dump => {
+ expect(dump).toContain("Especially pay attention to:");
+ expect(dump).toContain("exactly one direct child git repository");
+ expect(dump).toContain("`active-project`");
+ expect(dump).toContain("Do not claim work is missing, destroyed, or absent at the parent cwd");
+ expect(dump).toContain(watchdogContent);
+ expect(dump.indexOf(watchdogContent)).toBeLessThan(
+ dump.indexOf("Do not claim work is missing, destroyed, or absent at the parent cwd"),
+ );
+ });
+ });
+
+ it("omits built-in active child repo context when multiple direct child repos exist", async () => {
+ const tempDir = TempDir.createSync("@pi-advisor-watchdog-");
+ tempDirs.push(tempDir);
+ const cwd = tempDir.join("parent-cwd");
+ fs.mkdirSync(path.join(cwd, "active-project", ".git"), { recursive: true });
+ fs.mkdirSync(path.join(cwd, "second-project", ".git"), { recursive: true });
+
+ await withAdvisorHistory(tempDir, cwd, dump => {
+ expect(dump).not.toContain("exactly one direct child git repository");
+ expect(dump).not.toContain("Do not claim work is missing, destroyed, or absent at the parent cwd");
+ });
+ });
+
it("resolves nested folders and sorts by depth", async () => {
const tempDir = TempDir.createSync("@pi-advisor-watchdog-");
tempDirs.push(tempDir);
diff --git a/packages/coding-agent/test/git-active-context.test.ts b/packages/coding-agent/test/git-active-context.test.ts
new file mode 100644
index 000000000..4a5c9b06f
--- /dev/null
+++ b/packages/coding-agent/test/git-active-context.test.ts
@@ -0,0 +1,119 @@
+import { afterEach, beforeEach, describe, expect, it } from "bun:test";
+import * as fs from "node:fs";
+import * as os from "node:os";
+import * as path from "node:path";
+
+import {
+ type ActiveRepoContext,
+ resolveActiveRepoContext,
+ resolveActiveRepoContextSync,
+} from "@oh-my-pi/pi-coding-agent/utils/active-repo-context";
+
+function createGitDirectory(repoRoot: string): void {
+ const gitDir = path.join(repoRoot, ".git");
+ fs.mkdirSync(gitDir, { recursive: true });
+ fs.writeFileSync(path.join(gitDir, "HEAD"), "ref: refs/heads/main\n", "utf8");
+}
+
+function createLinkedWorktreeGitFile(worktreeRoot: string, gitDir: string, commonDir: string): void {
+ fs.mkdirSync(worktreeRoot, { recursive: true });
+ fs.mkdirSync(gitDir, { recursive: true });
+ fs.mkdirSync(commonDir, { recursive: true });
+ fs.writeFileSync(path.join(gitDir, "HEAD"), "ref: refs/heads/main\n", "utf8");
+ fs.writeFileSync(path.join(gitDir, "commondir"), `${path.relative(gitDir, commonDir)}\n`, "utf8");
+ fs.writeFileSync(path.join(commonDir, "HEAD"), "ref: refs/heads/main\n", "utf8");
+ fs.writeFileSync(path.join(worktreeRoot, ".git"), `gitdir: ${path.relative(worktreeRoot, gitDir)}\n`, "utf8");
+}
+
+async function expectResolvers(cwd: string, expected: ActiveRepoContext | null): Promise {
+ expect(resolveActiveRepoContextSync(cwd)).toEqual(expected);
+ expect(await resolveActiveRepoContext(cwd)).toEqual(expected);
+}
+
+describe("resolveActiveRepoContext", () => {
+ let tempRoot: string;
+
+ beforeEach(() => {
+ tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omp-active-repo-context-"));
+ });
+
+ afterEach(() => {
+ fs.rmSync(tempRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
+ });
+
+ it("returns null when cwd is already inside a repository", async () => {
+ const repoRoot = path.join(tempRoot, "repo");
+ const cwd = path.join(repoRoot, "nested");
+ fs.mkdirSync(cwd, { recursive: true });
+ createGitDirectory(repoRoot);
+
+ await expectResolvers(cwd, null);
+ });
+
+ it("returns null when no direct child repository exists", async () => {
+ const cwd = path.join(tempRoot, "workspace");
+ fs.mkdirSync(path.join(cwd, "not-a-repo"), { recursive: true });
+ fs.writeFileSync(path.join(cwd, "plain-file.txt"), "ignored\n", "utf8");
+
+ await expectResolvers(cwd, null);
+ });
+
+ it("returns the sole direct child repository context", async () => {
+ const cwd = path.join(tempRoot, "workspace");
+ const repoRoot = path.join(cwd, "repo");
+ fs.mkdirSync(path.join(cwd, "not-a-repo"), { recursive: true });
+ fs.writeFileSync(path.join(cwd, "plain-file.txt"), "ignored\n", "utf8");
+ createGitDirectory(repoRoot);
+
+ const expected = {
+ cwd,
+ repoRoot,
+ relativeRepoRoot: "repo",
+ source: "single-direct-child-repo",
+ } satisfies ActiveRepoContext;
+ await expectResolvers(cwd, expected);
+ });
+
+ it("treats a direct child symlink to a repository directory as that child", async () => {
+ const cwd = path.join(tempRoot, "workspace");
+ const targetRoot = path.join(tempRoot, "target-repo");
+ const repoRoot = path.join(cwd, "linked-repo");
+ fs.mkdirSync(cwd, { recursive: true });
+ createGitDirectory(targetRoot);
+ fs.symlinkSync(targetRoot, repoRoot, "junction");
+
+ const expected = {
+ cwd,
+ repoRoot,
+ relativeRepoRoot: "linked-repo",
+ source: "single-direct-child-repo",
+ } satisfies ActiveRepoContext;
+ await expectResolvers(cwd, expected);
+ });
+
+ it("returns null when two direct child repositories exist", async () => {
+ const cwd = path.join(tempRoot, "workspace");
+ fs.mkdirSync(cwd, { recursive: true });
+ createGitDirectory(path.join(cwd, "alpha"));
+ createGitDirectory(path.join(cwd, "beta"));
+
+ await expectResolvers(cwd, null);
+ });
+
+ it("accepts a direct child linked-worktree .git file", async () => {
+ const cwd = path.join(tempRoot, "workspace");
+ const repoRoot = path.join(cwd, "worktree");
+ const gitDir = path.join(tempRoot, "admin", "worktrees", "worktree");
+ const commonDir = path.join(tempRoot, "admin", "common.git");
+ fs.mkdirSync(cwd, { recursive: true });
+ createLinkedWorktreeGitFile(repoRoot, gitDir, commonDir);
+
+ const expected = {
+ cwd,
+ repoRoot,
+ relativeRepoRoot: "worktree",
+ source: "single-direct-child-repo",
+ } satisfies ActiveRepoContext;
+ await expectResolvers(cwd, expected);
+ });
+});
diff --git a/packages/coding-agent/test/issue-953-repro.test.ts b/packages/coding-agent/test/issue-953-repro.test.ts
index d5adae053..3444f4d75 100644
--- a/packages/coding-agent/test/issue-953-repro.test.ts
+++ b/packages/coding-agent/test/issue-953-repro.test.ts
@@ -37,6 +37,7 @@ function createCtx(usage: Partial): SegmentContext
autoCompactEnabled: false,
subagentCount: 0,
sessionStartTime: Date.now(),
+ activeRepo: null,
git: {
branch: null,
status: null,
diff --git a/packages/coding-agent/test/status-line-model.test.ts b/packages/coding-agent/test/status-line-model.test.ts
index 92a7dda85..0bdcc2807 100644
--- a/packages/coding-agent/test/status-line-model.test.ts
+++ b/packages/coding-agent/test/status-line-model.test.ts
@@ -37,6 +37,7 @@ function createModelContext(advisorActive: boolean): SegmentContext {
autoCompactEnabled: false,
subagentCount: 0,
sessionStartTime: Date.now(),
+ activeRepo: null,
git: { branch: null, status: null, pr: null },
usage: null,
};
diff --git a/packages/coding-agent/test/status-line-overflow.test.ts b/packages/coding-agent/test/status-line-overflow.test.ts
index ddfcd7a49..69e806244 100644
--- a/packages/coding-agent/test/status-line-overflow.test.ts
+++ b/packages/coding-agent/test/status-line-overflow.test.ts
@@ -61,6 +61,7 @@ function createCtx(overrides?: { pathMaxLength?: number; branch?: string | null
autoCompactEnabled: false,
subagentCount: 0,
sessionStartTime: Date.now(),
+ activeRepo: null,
git: {
branch: overrides?.branch ?? null,
status: null,
diff --git a/packages/coding-agent/test/status-line-path.test.ts b/packages/coding-agent/test/status-line-path.test.ts
index 11a24456d..932843c4e 100644
--- a/packages/coding-agent/test/status-line-path.test.ts
+++ b/packages/coding-agent/test/status-line-path.test.ts
@@ -47,6 +47,7 @@ function createPathContext(): SegmentContext {
autoCompactEnabled: false,
subagentCount: 0,
sessionStartTime: Date.now(),
+ activeRepo: null,
git: {
branch: null,
status: null,
@@ -60,6 +61,14 @@ afterEach(() => {
setProjectDir(originalProjectDir);
});
+function expectContentToContainPath(content: string, expected: string): void {
+ if (process.platform === "win32") {
+ expect(content.toLowerCase()).toContain(expected.toLowerCase());
+ return;
+ }
+ expect(content).toContain(expected);
+}
+
describe("status line path segment", () => {
it("strips the Projects root for symlink-equivalent aliases", () => {
if (process.platform === "win32") return;
@@ -87,6 +96,7 @@ describe("status line path segment", () => {
expect(rendered.content).not.toContain("home-link");
expect(rendered.content).not.toContain(`${path.sep}Projects${path.sep}`);
} finally {
+ setProjectDir(originalProjectDir);
fs.rmSync(aliasRoot, { recursive: true, force: true });
fs.rmSync(realProjectDir, { recursive: true, force: true });
}
@@ -102,9 +112,10 @@ describe("status line path segment", () => {
expect(rendered.content).toContain(theme.icon.scratchFolder);
expect(rendered.content).not.toContain(theme.icon.folder);
// Display is just the scratch-relative tail — no leading tmpdir, no ancestor segments.
- expect(rendered.content).toContain(path.basename(scratchDir));
+ expectContentToContainPath(rendered.content, path.basename(getProjectDir()));
expect(rendered.content).not.toContain(os.tmpdir());
} finally {
+ setProjectDir(originalProjectDir);
fs.rmSync(scratchDir, { recursive: true, force: true });
}
});
@@ -117,11 +128,12 @@ describe("status line path segment", () => {
setProjectDir(nested);
const rendered = renderSegment("path", createPathContext());
- const tail = `${path.basename(scratchDir)}${path.sep}sub${path.sep}deep`;
+ const tail = `${path.basename(path.dirname(path.dirname(getProjectDir())))}${path.sep}sub${path.sep}deep`;
expect(rendered.content).toContain(theme.icon.scratchFolder);
- expect(rendered.content).toContain(tail);
+ expectContentToContainPath(rendered.content, tail);
expect(rendered.content).not.toContain(os.tmpdir());
} finally {
+ setProjectDir(originalProjectDir);
fs.rmSync(scratchDir, { recursive: true, force: true });
}
});
@@ -138,6 +150,7 @@ describe("status line path segment", () => {
expect(rendered.content).toContain(theme.icon.folder);
expect(rendered.content).not.toContain(theme.icon.scratchFolder);
} finally {
+ setProjectDir(originalProjectDir);
fs.rmSync(scratchDir, { recursive: true, force: true });
}
});
@@ -154,7 +167,33 @@ describe("status line path segment", () => {
expect(rendered.content).toContain(theme.icon.folder);
expect(rendered.content).not.toContain(theme.icon.scratchFolder);
} finally {
+ setProjectDir(originalProjectDir);
fs.rmSync(realProjectDir, { recursive: true, force: true });
}
});
+
+ it("renders the active nested repo suffix after the parent cwd", () => {
+ const parentDir = fs.mkdtempSync(path.join(os.tmpdir(), "omp-status-line-parent-"));
+ const repoDir = path.join(parentDir, "pr-workspace");
+ fs.mkdirSync(repoDir);
+ try {
+ setProjectDir(parentDir);
+ const ctx = createPathContext();
+ ctx.activeRepo = {
+ cwd: parentDir,
+ repoRoot: repoDir,
+ relativeRepoRoot: "pr-workspace",
+ source: "single-direct-child-repo",
+ };
+
+ const rendered = renderSegment("path", ctx);
+ const expected = `${path.basename(getProjectDir())} ↳ pr-workspace`;
+ expect(rendered.visible).toBe(true);
+ expectContentToContainPath(rendered.content, expected);
+ expect(rendered.content).not.toContain(os.tmpdir());
+ } finally {
+ setProjectDir(originalProjectDir);
+ fs.rmSync(parentDir, { recursive: true, force: true });
+ }
+ });
});
diff --git a/packages/coding-agent/test/system-prompt-dedup.test.ts b/packages/coding-agent/test/system-prompt-dedup.test.ts
index 460892b3e..8c3da31bc 100644
--- a/packages/coding-agent/test/system-prompt-dedup.test.ts
+++ b/packages/coding-agent/test/system-prompt-dedup.test.ts
@@ -132,16 +132,47 @@ describe("SYSTEM.md prompt assembly", () => {
});
const promptText = systemPrompt.join("\n\n");
+ const normalizedProjectDir = projectDir.replace(/\\/g, "/");
const appendMatches = promptText.match(new RegExp(escapeRegExp(appendPrompt), "g")) ?? [];
expect(systemPrompt).toHaveLength(2);
expect(promptText).toContain("CLI custom prompt");
expect(promptText).toContain("");
expect(promptText).toContain("");
- expect(promptText).toContain(`current working directory is '${projectDir}'`);
+ expect(promptText).toMatch(
+ new RegExp(
+ `^Today is [^,\\n]+, and the current working directory is '${escapeRegExp(normalizedProjectDir)}'\\.$`,
+ "m",
+ ),
+ );
expect(appendMatches).toHaveLength(1);
expect(promptText).not.toContain("Discovered project SYSTEM prompt");
});
+ it("renders active child repo context in the main system prompt", async () => {
+ const parentDir = path.join(tempDir, "parent-cwd");
+ fs.mkdirSync(path.join(parentDir, "active-project", ".git"), { recursive: true });
+
+ const { systemPrompt } = await buildSystemPrompt({
+ cwd: parentDir,
+ contextFiles: [],
+ skills: [],
+ rules: [],
+ toolNames: [],
+ workspaceTree: {
+ rootPath: parentDir,
+ rendered: "",
+ truncated: false,
+ totalLines: 0,
+ agentsMdFiles: [],
+ },
+ });
+
+ const promptText = systemPrompt.join("\n\n");
+ expect(promptText).toContain("");
+ expect(promptText).toContain("Exactly one direct child git repository was detected at `active-project`.");
+ expect(promptText).toContain("Paths under `active-project/` are the active project");
+ });
+
it("prefers project SYSTEM.md over user SYSTEM.md", async () => {
const projectDir = path.join(tempDir, "project");
fs.mkdirSync(path.join(projectDir, ".omp"), { recursive: true });