fix(workspace): align clawd + bootstrap
This commit is contained in:
@@ -26,6 +26,8 @@
|
|||||||
- Onboarding: prompt immediately for OpenAI Codex redirect URL on remote/headless logins.
|
- Onboarding: prompt immediately for OpenAI Codex redirect URL on remote/headless logins.
|
||||||
- Configure: add OpenAI Codex (ChatGPT OAuth) auth choice (align with onboarding).
|
- Configure: add OpenAI Codex (ChatGPT OAuth) auth choice (align with onboarding).
|
||||||
- Doctor: suggest adding the workspace memory system when missing (opt-out via `--no-workspace-suggestions`).
|
- Doctor: suggest adding the workspace memory system when missing (opt-out via `--no-workspace-suggestions`).
|
||||||
|
- Doctor: normalize default workspace path to `~/clawd` (avoid `~/clawdbot`).
|
||||||
|
- Workspace: only create `BOOTSTRAP.md` for brand-new workspaces (don’t recreate after deletion).
|
||||||
- Build: fix duplicate protocol export, align Codex OAuth options, and add proper-lockfile typings.
|
- Build: fix duplicate protocol export, align Codex OAuth options, and add proper-lockfile typings.
|
||||||
- Build: install Bun in the Dockerfile so `pnpm build` can run Bun scripts. Thanks @loukotal for PR #284.
|
- Build: install Bun in the Dockerfile so `pnpm build` can run Bun scripts. Thanks @loukotal for PR #284.
|
||||||
- Typing indicators: stop typing once the reply dispatcher drains to prevent stuck typing across Discord/Telegram/WhatsApp.
|
- Typing indicators: stop typing once the reply dispatcher drains to prevent stuck typing across Discord/Telegram/WhatsApp.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ CLAWDBOT runs a single embedded agent runtime derived from **p-mono** (internal
|
|||||||
|
|
||||||
## Workspace (required)
|
## Workspace (required)
|
||||||
|
|
||||||
You must set an agent home directory via `agent.workspace`. CLAWDBOT uses this as the agent’s **only** working directory (`cwd`) for tools and context.
|
CLAWDBOT uses a single agent workspace directory (`agent.workspace`) as the agent’s **only** working directory (`cwd`) for tools and context.
|
||||||
|
|
||||||
Recommended: use `clawdbot setup` to create `~/.clawdbot/clawdbot.json` if missing and initialize the workspace files.
|
Recommended: use `clawdbot setup` to create `~/.clawdbot/clawdbot.json` if missing and initialize the workspace files.
|
||||||
|
|
||||||
@@ -31,6 +31,14 @@ On the first turn of a new session, CLAWDBOT injects the contents of these files
|
|||||||
|
|
||||||
If a file is missing, CLAWDBOT injects a single “missing file” marker line (and `clawdbot setup` will create a safe default template).
|
If a file is missing, CLAWDBOT injects a single “missing file” marker line (and `clawdbot setup` will create a safe default template).
|
||||||
|
|
||||||
|
`BOOTSTRAP.md` is only created for a **brand new workspace** (no other bootstrap files present). If you delete it after completing the ritual, it should not be recreated on later restarts.
|
||||||
|
|
||||||
|
To disable bootstrap file creation entirely (for pre-seeded workspaces), set:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{ agent: { skipBootstrap: true } }
|
||||||
|
```
|
||||||
|
|
||||||
## Built-in tools (internal)
|
## Built-in tools (internal)
|
||||||
|
|
||||||
p’s embedded core tools (read/bash/edit/write and related internals) are defined in code and always available. `TOOLS.md` does **not** control which tools exist; it’s guidance for how *you* want them used.
|
p’s embedded core tools (read/bash/edit/write and related internals) are defined in code and always available. `TOOLS.md` does **not** control which tools exist; it’s guidance for how *you* want them used.
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ Now message the assistant number from your allowlisted phone.
|
|||||||
|
|
||||||
Clawd reads operating instructions and “memory” from its workspace directory.
|
Clawd reads operating instructions and “memory” from its workspace directory.
|
||||||
|
|
||||||
By default, Clawdbot uses `~/clawd` as the agent workspace, and will create it (plus starter `AGENTS.md`, `SOUL.md`, `TOOLS.md`) automatically on setup/first agent run.
|
By default, Clawdbot uses `~/clawd` as the agent workspace, and will create it (plus starter `AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`) automatically on setup/first agent run. `BOOTSTRAP.md` is only created when the workspace is brand new (it should not come back after you delete it).
|
||||||
|
|
||||||
Tip: treat this folder like Clawd’s “memory” and make it a git repo (ideally private) so your `AGENTS.md` + memory files are backed up.
|
Tip: treat this folder like Clawd’s “memory” and make it a git repo (ideally private) so your `AGENTS.md` + memory files are backed up.
|
||||||
|
|
||||||
@@ -103,6 +103,16 @@ Optional: choose a different workspace with `agent.workspace` (supports `~`).
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you already ship your own workspace files from a repo, you can disable bootstrap file creation entirely:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
agent: {
|
||||||
|
skipBootstrap: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## The config that turns it into “an assistant”
|
## The config that turns it into “an assistant”
|
||||||
|
|
||||||
CLAWDBOT defaults to a good assistant setup, but you’ll usually want to tune:
|
CLAWDBOT defaults to a good assistant setup, but you’ll usually want to tune:
|
||||||
|
|||||||
@@ -616,6 +616,18 @@ Default: `~/clawd`.
|
|||||||
If `agent.sandbox` is enabled, non-main sessions can override this with their
|
If `agent.sandbox` is enabled, non-main sessions can override this with their
|
||||||
own per-session workspaces under `agent.sandbox.workspaceRoot`.
|
own per-session workspaces under `agent.sandbox.workspaceRoot`.
|
||||||
|
|
||||||
|
### `agent.skipBootstrap`
|
||||||
|
|
||||||
|
Disables automatic creation of the workspace bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, and `BOOTSTRAP.md`).
|
||||||
|
|
||||||
|
Use this for pre-seeded deployments where your workspace files come from a repo.
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
agent: { skipBootstrap: true }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### `agent.userTimezone`
|
### `agent.userTimezone`
|
||||||
|
|
||||||
Sets the user’s timezone for **system prompt context** (not for timestamps in
|
Sets the user’s timezone for **system prompt context** (not for timestamps in
|
||||||
|
|||||||
@@ -545,10 +545,10 @@ pkill -f "clawdbot"
|
|||||||
# Remove data
|
# Remove data
|
||||||
trash ~/.clawdbot
|
trash ~/.clawdbot
|
||||||
|
|
||||||
# Remove repo and re-clone
|
# Remove repo and re-clone (adjust path if you cloned elsewhere)
|
||||||
trash ~/clawdbot
|
trash ~/Projects/clawdbot
|
||||||
git clone https://github.com/clawdbot/clawdbot.git
|
git clone https://github.com/clawdbot/clawdbot.git ~/Projects/clawdbot
|
||||||
cd clawdbot && pnpm install && pnpm build
|
cd ~/Projects/clawdbot && pnpm install && pnpm build
|
||||||
pnpm clawdbot onboard
|
pnpm clawdbot onboard
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -36,4 +36,17 @@ describe("ensureAgentWorkspace", () => {
|
|||||||
await ensureAgentWorkspace({ dir, ensureBootstrapFiles: true });
|
await ensureAgentWorkspace({ dir, ensureBootstrapFiles: true });
|
||||||
expect(await fs.readFile(agentsPath, "utf-8")).toBe("custom");
|
expect(await fs.readFile(agentsPath, "utf-8")).toBe("custom");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not recreate BOOTSTRAP.md once workspace exists", async () => {
|
||||||
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-ws-"));
|
||||||
|
const agentsPath = path.join(dir, "AGENTS.md");
|
||||||
|
const bootstrapPath = path.join(dir, "BOOTSTRAP.md");
|
||||||
|
|
||||||
|
await fs.writeFile(agentsPath, "custom", "utf-8");
|
||||||
|
await fs.rm(bootstrapPath, { force: true });
|
||||||
|
|
||||||
|
await ensureAgentWorkspace({ dir, ensureBootstrapFiles: true });
|
||||||
|
|
||||||
|
await expect(fs.stat(bootstrapPath)).rejects.toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -222,6 +222,21 @@ export async function ensureAgentWorkspace(params?: {
|
|||||||
const userPath = path.join(dir, DEFAULT_USER_FILENAME);
|
const userPath = path.join(dir, DEFAULT_USER_FILENAME);
|
||||||
const bootstrapPath = path.join(dir, DEFAULT_BOOTSTRAP_FILENAME);
|
const bootstrapPath = path.join(dir, DEFAULT_BOOTSTRAP_FILENAME);
|
||||||
|
|
||||||
|
const isBrandNewWorkspace = await (async () => {
|
||||||
|
const paths = [agentsPath, soulPath, toolsPath, identityPath, userPath];
|
||||||
|
const existing = await Promise.all(
|
||||||
|
paths.map(async (p) => {
|
||||||
|
try {
|
||||||
|
await fs.access(p);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return existing.every((v) => !v);
|
||||||
|
})();
|
||||||
|
|
||||||
const agentsTemplate = await loadTemplate(
|
const agentsTemplate = await loadTemplate(
|
||||||
DEFAULT_AGENTS_FILENAME,
|
DEFAULT_AGENTS_FILENAME,
|
||||||
DEFAULT_AGENTS_TEMPLATE,
|
DEFAULT_AGENTS_TEMPLATE,
|
||||||
@@ -252,7 +267,9 @@ export async function ensureAgentWorkspace(params?: {
|
|||||||
await writeFileIfMissing(toolsPath, toolsTemplate);
|
await writeFileIfMissing(toolsPath, toolsTemplate);
|
||||||
await writeFileIfMissing(identityPath, identityTemplate);
|
await writeFileIfMissing(identityPath, identityTemplate);
|
||||||
await writeFileIfMissing(userPath, userTemplate);
|
await writeFileIfMissing(userPath, userTemplate);
|
||||||
await writeFileIfMissing(bootstrapPath, bootstrapTemplate);
|
if (isBrandNewWorkspace) {
|
||||||
|
await writeFileIfMissing(bootstrapPath, bootstrapTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dir,
|
dir,
|
||||||
|
|||||||
@@ -268,9 +268,9 @@ describe("doctor", () => {
|
|||||||
parsed: {
|
parsed: {
|
||||||
gateway: { mode: "local", bind: "loopback" },
|
gateway: { mode: "local", bind: "loopback" },
|
||||||
agent: {
|
agent: {
|
||||||
workspace: "/Users/steipete/clawdbot",
|
workspace: "/Users/steipete/clawd",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
workspaceRoot: "/Users/steipete/clawdbot/sandboxes",
|
workspaceRoot: "/Users/steipete/clawd/sandboxes",
|
||||||
docker: {
|
docker: {
|
||||||
image: "clawdbot-sandbox",
|
image: "clawdbot-sandbox",
|
||||||
containerPrefix: "clawdbot-sbx",
|
containerPrefix: "clawdbot-sbx",
|
||||||
@@ -282,9 +282,9 @@ describe("doctor", () => {
|
|||||||
config: {
|
config: {
|
||||||
gateway: { mode: "local", bind: "loopback" },
|
gateway: { mode: "local", bind: "loopback" },
|
||||||
agent: {
|
agent: {
|
||||||
workspace: "/Users/steipete/clawdbot",
|
workspace: "/Users/steipete/clawd",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
workspaceRoot: "/Users/steipete/clawdbot/sandboxes",
|
workspaceRoot: "/Users/steipete/clawd/sandboxes",
|
||||||
docker: {
|
docker: {
|
||||||
image: "clawdbot-sandbox",
|
image: "clawdbot-sandbox",
|
||||||
containerPrefix: "clawdbot-sbx",
|
containerPrefix: "clawdbot-sbx",
|
||||||
@@ -365,8 +365,8 @@ describe("doctor", () => {
|
|||||||
const sandbox = agent.sandbox as Record<string, unknown>;
|
const sandbox = agent.sandbox as Record<string, unknown>;
|
||||||
const docker = sandbox.docker as Record<string, unknown>;
|
const docker = sandbox.docker as Record<string, unknown>;
|
||||||
|
|
||||||
expect(agent.workspace).toBe("/Users/steipete/clawdbot");
|
expect(agent.workspace).toBe("/Users/steipete/clawd");
|
||||||
expect(sandbox.workspaceRoot).toBe("/Users/steipete/clawdbot/sandboxes");
|
expect(sandbox.workspaceRoot).toBe("/Users/steipete/clawd/sandboxes");
|
||||||
expect(docker.image).toBe("clawdbot-sandbox");
|
expect(docker.image).toBe("clawdbot-sandbox");
|
||||||
expect(docker.containerPrefix).toBe("clawdbot-sbx");
|
expect(docker.containerPrefix).toBe("clawdbot-sbx");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -247,15 +247,28 @@ async function noteSecurityWarnings(cfg: ClawdbotConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function replacePathSegment(
|
function normalizeDefaultWorkspacePath(
|
||||||
value: string | undefined,
|
value: string | undefined,
|
||||||
from: string,
|
|
||||||
to: string,
|
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
if (!value) return value;
|
if (!value) return value;
|
||||||
const pattern = new RegExp(`(^|[\\/])${from}([\\/]|$)`, "g");
|
|
||||||
if (!pattern.test(value)) return value;
|
const resolved = resolveUserPath(value);
|
||||||
return value.replace(pattern, `$1${to}$2`);
|
const home = os.homedir();
|
||||||
|
|
||||||
|
const next = [
|
||||||
|
["clawdis", "clawd"],
|
||||||
|
["clawdbot", "clawd"],
|
||||||
|
].reduce((acc, [from, to]) => {
|
||||||
|
const fromPrefix = path.join(home, from);
|
||||||
|
if (acc === fromPrefix) return path.join(home, to);
|
||||||
|
const withSep = `${fromPrefix}${path.sep}`;
|
||||||
|
if (acc.startsWith(withSep)) {
|
||||||
|
return path.join(home, to).concat(acc.slice(fromPrefix.length));
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, resolved);
|
||||||
|
|
||||||
|
return next === resolved ? value : next;
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceLegacyName(value: string | undefined): string | undefined {
|
function replaceLegacyName(value: string | undefined): string | undefined {
|
||||||
@@ -556,11 +569,7 @@ function normalizeLegacyConfigValues(cfg: ClawdbotConfig): {
|
|||||||
let next: ClawdbotConfig = cfg;
|
let next: ClawdbotConfig = cfg;
|
||||||
|
|
||||||
const workspace = cfg.agent?.workspace;
|
const workspace = cfg.agent?.workspace;
|
||||||
const updatedWorkspace = replacePathSegment(
|
const updatedWorkspace = normalizeDefaultWorkspacePath(workspace);
|
||||||
replacePathSegment(workspace, "clawdis", "clawdbot"),
|
|
||||||
"clawd",
|
|
||||||
"clawdbot",
|
|
||||||
);
|
|
||||||
if (updatedWorkspace && updatedWorkspace !== workspace) {
|
if (updatedWorkspace && updatedWorkspace !== workspace) {
|
||||||
next = {
|
next = {
|
||||||
...next,
|
...next,
|
||||||
@@ -573,11 +582,7 @@ function normalizeLegacyConfigValues(cfg: ClawdbotConfig): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const workspaceRoot = cfg.agent?.sandbox?.workspaceRoot;
|
const workspaceRoot = cfg.agent?.sandbox?.workspaceRoot;
|
||||||
const updatedWorkspaceRoot = replacePathSegment(
|
const updatedWorkspaceRoot = normalizeDefaultWorkspacePath(workspaceRoot);
|
||||||
replacePathSegment(workspaceRoot, "clawdis", "clawdbot"),
|
|
||||||
"clawd",
|
|
||||||
"clawdbot",
|
|
||||||
);
|
|
||||||
if (updatedWorkspaceRoot && updatedWorkspaceRoot !== workspaceRoot) {
|
if (updatedWorkspaceRoot && updatedWorkspaceRoot !== workspaceRoot) {
|
||||||
next = {
|
next = {
|
||||||
...next,
|
...next,
|
||||||
|
|||||||
@@ -223,10 +223,11 @@ export async function openUrl(url: string): Promise<boolean> {
|
|||||||
export async function ensureWorkspaceAndSessions(
|
export async function ensureWorkspaceAndSessions(
|
||||||
workspaceDir: string,
|
workspaceDir: string,
|
||||||
runtime: RuntimeEnv,
|
runtime: RuntimeEnv,
|
||||||
|
options?: { skipBootstrap?: boolean },
|
||||||
) {
|
) {
|
||||||
const ws = await ensureAgentWorkspace({
|
const ws = await ensureAgentWorkspace({
|
||||||
dir: workspaceDir,
|
dir: workspaceDir,
|
||||||
ensureBootstrapFiles: true,
|
ensureBootstrapFiles: !options?.skipBootstrap,
|
||||||
});
|
});
|
||||||
runtime.log(`Workspace OK: ${ws.dir}`);
|
runtime.log(`Workspace OK: ${ws.dir}`);
|
||||||
const sessionsDir = resolveSessionTranscriptsDir();
|
const sessionsDir = resolveSessionTranscriptsDir();
|
||||||
|
|||||||
@@ -219,7 +219,9 @@ export async function runNonInteractiveOnboarding(
|
|||||||
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
|
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
|
||||||
await writeConfigFile(nextConfig);
|
await writeConfigFile(nextConfig);
|
||||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||||
await ensureWorkspaceAndSessions(workspaceDir, runtime);
|
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
|
||||||
|
skipBootstrap: Boolean(nextConfig.agent?.skipBootstrap),
|
||||||
|
});
|
||||||
|
|
||||||
if (opts.installDaemon) {
|
if (opts.installDaemon) {
|
||||||
const service = resolveGatewayService();
|
const service = resolveGatewayService();
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export async function setupCommand(
|
|||||||
|
|
||||||
const ws = await ensureAgentWorkspace({
|
const ws = await ensureAgentWorkspace({
|
||||||
dir: workspace,
|
dir: workspace,
|
||||||
ensureBootstrapFiles: true,
|
ensureBootstrapFiles: !next.agent?.skipBootstrap,
|
||||||
});
|
});
|
||||||
runtime.log(`Workspace OK: ${ws.dir}`);
|
runtime.log(`Workspace OK: ${ws.dir}`);
|
||||||
|
|
||||||
|
|||||||
@@ -604,7 +604,9 @@ export async function runOnboardingWizard(
|
|||||||
|
|
||||||
await writeConfigFile(nextConfig);
|
await writeConfigFile(nextConfig);
|
||||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||||
await ensureWorkspaceAndSessions(workspaceDir, runtime);
|
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
|
||||||
|
skipBootstrap: Boolean(nextConfig.agent?.skipBootstrap),
|
||||||
|
});
|
||||||
|
|
||||||
nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter);
|
nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter);
|
||||||
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
|
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
|
||||||
|
|||||||
Reference in New Issue
Block a user