Coding agent is attempting to work in process.cwd() rather than project_root which is kind of a recipe disaster. In theory, we should be able to point the agent at this ticket, for it to execute the changes. But it would still have to target a working copy of its own repo (ie project_root is a clone of https://github.com/ForestMars/Coda2) rather than doing surgery on the live running instance of itself, obviously.
As part of this ticket we also need to move the system prompt out of code and into its own file, as we do for support-agent.
Here's my proposed fix:
- Add PROJECT_ROOT resolver utility
Create packages/lib/src/project-root.ts:
typescriptimport { resolve, dirname } from "path";
import { existsSync } from "fs";
/**
- Resolves the working project directory for the agent.
- Priority order:
-
- CODA_PROJECT_DIR env var (explicit override)
-
- --project CLI arg
-
- Walk up from process.cwd() until we find a .git or package.json
-
that is NOT Coda2's own root
*/
export function resolveProjectRoot(): string {
// 1. Explicit env override
if (process.env.CODA_PROJECT_DIR) {
const dir = resolve(process.env.CODA_PROJECT_DIR);
if (!existsSync(dir)) {
throw new Error(CODA_PROJECT_DIR="${dir}" does not exist);
}
return dir;
}
// 2. CLI arg: --project /some/path
const projectArgIdx = process.argv.indexOf("--project");
if (projectArgIdx !== -1 && process.argv[projectArgIdx + 1]) {
return resolve(process.argv[projectArgIdx + 1]);
}
// 3. Walk up from CWD, skip our own repo root
const codaRoot = resolve(import.meta.dir, "../../.."); // packages/lib/src -> repo root
let dir = process.cwd();
while (dir !== dirname(dir)) {
if (dir !== codaRoot && existsSync(${dir}/.git)) {
return dir;
}
dir = dirname(dir);
}
// Fallback: use process.cwd() and warn loudly
console.warn(
[Coda] WARNING: Could not determine project root. +
Defaulting to process.cwd()="${process.cwd()}". +
Set CODA_PROJECT_DIR or pass --project <path>.
);
return process.cwd();
}
// Singleton — resolved once at startup
export const PROJECT_ROOT = resolveProjectRoot();
2. Fix your tools to use it
Wherever your tools execute shell commands or resolve file paths, swap process.cwd() for PROJECT_ROOT. In packages/tools, the pattern to hunt for is:
typescript// ❌ Before — anchors to wherever bun was launched
const result = await $${cmd}.cwd(process.cwd());
const absPath = path.resolve(filePath); // same problem
// ✅ After
import { PROJECT_ROOT } from "@coda/lib/project-root";
const result = await $${cmd}.cwd(PROJECT_ROOT);
const absPath = path.resolve(PROJECT_ROOT, filePath);
For child_process.spawn / execa style calls:
typescriptspawn(cmd, args, { cwd: PROJECT_ROOT }) // not process.cwd()
3. Inject it into the system prompt — prominently
This is the part that makes the LLM safe. In whatever builds your system prompt (likely in packages/agents), add this at the very top of the system content, before any other instructions:
typescriptimport { PROJECT_ROOT } from "@coda/lib/project-root";
const systemPrompt = [
// === PRIMACY POSITION — agent reads this first ===
{
type: "text",
text: `## Working Directory
Your current working project directory is:
${PROJECT_ROOT}
CRITICAL RULES:
- ALL file paths you reference or create MUST be absolute paths under ${PROJECT_ROOT}
- NEVER use paths outside of ${PROJECT_ROOT} unless explicitly instructed
- NEVER assume the working directory is / or any other system root
- When in doubt about a path, prefix it with ${PROJECT_ROOT}/
- You are NOT working in the Coda agent's own source directory`,
},
// ...
];
This follows the same "Context Primacy" pattern you already use for your Knowledge Graph in the SupportBot — inject the most important grounding fact first.
- Add it to .env for convenience
bash# .env
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://otel-collector:4318/v1/logs
OTEL_SERVICE_NAME=sup-agent
LOG_LEVEL=info
LOKI_HOST=http://localhost:3100
Set this to lock the agent to a specific project directory
CODA_PROJECT_DIR=/path/to/your/target/project
And update the npm script to make it easy to pass:
json"chat": "bun --env-file=.auth apps/cli/chat.ts",
"chat:project": "CODA_PROJECT_DIR=$PROJECT bun --env-file=.auth apps/cli/chat.ts",
So users can run: PROJECT=/my/app bun chat:project
Coding agent is attempting to work in process.cwd() rather than project_root which is kind of a recipe disaster. In theory, we should be able to point the agent at this ticket, for it to execute the changes. But it would still have to target a working copy of its own repo (ie project_root is a clone of https://github.com/ForestMars/Coda2) rather than doing surgery on the live running instance of itself, obviously.
As part of this ticket we also need to move the system prompt out of code and into its own file, as we do for support-agent.
Here's my proposed fix:
Create packages/lib/src/project-root.ts:
typescriptimport { resolve, dirname } from "path";
import { existsSync } from "fs";
/**
*/
export function resolveProjectRoot(): string {
// 1. Explicit env override
if (process.env.CODA_PROJECT_DIR) {
const dir = resolve(process.env.CODA_PROJECT_DIR);
if (!existsSync(dir)) {
throw new Error(
CODA_PROJECT_DIR="${dir}" does not exist);}
return dir;
}
// 2. CLI arg: --project /some/path
const projectArgIdx = process.argv.indexOf("--project");
if (projectArgIdx !== -1 && process.argv[projectArgIdx + 1]) {
return resolve(process.argv[projectArgIdx + 1]);
}
// 3. Walk up from CWD, skip our own repo root
const codaRoot = resolve(import.meta.dir, "../../.."); // packages/lib/src -> repo root
let dir = process.cwd();
while (dir !== dirname(dir)) {
if (dir !== codaRoot && existsSync(
${dir}/.git)) {return dir;
}
dir = dirname(dir);
}
// Fallback: use process.cwd() and warn loudly
console.warn(
[Coda] WARNING: Could not determine project root.+Defaulting to process.cwd()="${process.cwd()}".+Set CODA_PROJECT_DIR or pass --project <path>.);
return process.cwd();
}
// Singleton — resolved once at startup
export const PROJECT_ROOT = resolveProjectRoot();
2. Fix your tools to use it
Wherever your tools execute shell commands or resolve file paths, swap process.cwd() for PROJECT_ROOT. In packages/tools, the pattern to hunt for is:
typescript// ❌ Before — anchors to wherever
bunwas launchedconst result = await $
${cmd}.cwd(process.cwd());const absPath = path.resolve(filePath); // same problem
// ✅ After
import { PROJECT_ROOT } from "@coda/lib/project-root";
const result = await $
${cmd}.cwd(PROJECT_ROOT);const absPath = path.resolve(PROJECT_ROOT, filePath);
For child_process.spawn / execa style calls:
typescriptspawn(cmd, args, { cwd: PROJECT_ROOT }) // not process.cwd()
3. Inject it into the system prompt — prominently
This is the part that makes the LLM safe. In whatever builds your system prompt (likely in packages/agents), add this at the very top of the system content, before any other instructions:
typescriptimport { PROJECT_ROOT } from "@coda/lib/project-root";
const systemPrompt = [
// === PRIMACY POSITION — agent reads this first ===
{
type: "text",
text: `## Working Directory
Your current working project directory is:
${PROJECT_ROOT}
CRITICAL RULES:
},
// ...
];
This follows the same "Context Primacy" pattern you already use for your Knowledge Graph in the SupportBot — inject the most important grounding fact first.
bash# .env
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://otel-collector:4318/v1/logs
OTEL_SERVICE_NAME=sup-agent
LOG_LEVEL=info
LOKI_HOST=http://localhost:3100
Set this to lock the agent to a specific project directory
CODA_PROJECT_DIR=/path/to/your/target/project
And update the npm script to make it easy to pass:
json"chat": "bun --env-file=.auth apps/cli/chat.ts",
"chat:project": "CODA_PROJECT_DIR=$PROJECT bun --env-file=.auth apps/cli/chat.ts",
So users can run: PROJECT=/my/app bun chat:project