Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 32 additions & 19 deletions docs/chatgpt-coding-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,31 @@ ChatGPT may support automatic checkout recovery through optional host
conversation metadata. This is an OpenAI-host adapter detail, not a standard MCP
conversation field. When that optional context is available, opening the same
checkout project again in the same conversation can continue in the existing
workspace, and the context already provided for that reused checkout is not
repeated. The portable workflow remains the same: keep using the `workspaceId`
returned by `open_workspace` for later operations. Hosts without supported
conversation context receive a normal new workspace and continue with that
explicit `workspaceId` workflow.
workspace. DevSpace also remembers which global and project context snapshots
have already been delivered in that conversation, so unchanged instructions,
skills, agent profiles, and provider information are not repeated across
checkout/worktree switches or project changes. The portable workflow remains
the same: keep using the `workspaceId` returned by `open_workspace` for later
operations. Hosts without supported conversation context receive a normal new
workspace and complete context on each open.
The model receives actionable workspace instructions; automatic-reuse
bookkeeping is not a model-facing choice.

Worktree mode is deliberately different: every call creates a new managed
worktree and a new workspace session with complete context, even for the same
path and base ref.

The first successful open of a checkout provides complete instructions and
coding context. A repeated open that reuses the same checkout workspace does
not repeat the model-visible context, but the workspace UI continues to show the
complete details. Every new worktree establishes and returns its own complete
context, even when the same project was already opened in checkout or another
worktree. Opening checkout after a worktree therefore provides the checkout's
own context.
Worktree mode still creates a new managed worktree and workspace session on
every call. Model context is independent from that workspace lifecycle: if a
global or project scope is unchanged from an earlier open in the conversation,
the new worktree result can omit it. If a branch or filesystem change alters a
scope, `open_workspace` returns the complete replacement snapshot for that scope.

Treat returned `instructions`, `skills`, and `agents` scopes, plus instructions
later read from nested instruction files or `SKILL.md` files, as durable
operating context. Preserve them when summarizing or compacting conversation
state. A later `open_workspace` result that omits a scope means the previously
returned snapshot is still current; a scope that appears again replaces the
retained snapshot for that scope. When project instructions reappear, reread
relevant nested instruction files before relying on an older read. When a
skills scope reappears, reread a matching `SKILL.md` before relying on its
previously read instructions.

Do not call `open_workspace` again for the same checkout folder unless:

Expand Down Expand Up @@ -98,8 +104,12 @@ When a workspace opens, DevSpace loads root-level instruction files:
- `CLAUDE.md`
- `CLAUDE.MD`

Nested instruction files are returned as `availableAgentsFiles`. The model
should read the relevant nested file before working under that directory.
`open_workspace` groups instructions by scope. Global loaded instructions are
returned under `instructions.global`; project root instructions are under
`instructions.project.loaded`; nested project instruction files are advertised
under `instructions.project.available`. Project instruction paths are relative
to the current workspace root. The model should read the relevant nested file
before working under that directory.

This keeps instructions explicit and inspectable instead of silently injecting
new context during later tool calls.
Expand Down Expand Up @@ -135,7 +145,10 @@ Legacy project paths such as `.pi/skills` can be added to `skills.paths` when ne
When `open_workspace` returns matching skills, the model should read the
advertised `SKILL.md` before following that skill.

Skill paths may be outside the workspace. DevSpace only permits reading:
Project skill paths are workspace-relative, so the same retained project skill
catalog remains valid when moving between a checkout and managed worktree.
Global skill paths use home-shortened or absolute paths and may be outside the
workspace. DevSpace only permits reading:

- advertised `SKILL.md` files
- files under a skill directory after that skill's `SKILL.md` has been read
Expand Down
27 changes: 18 additions & 9 deletions docs/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,20 +133,25 @@ project.
Workspace session metadata is persisted. ChatGPT may provide optional
conversation metadata that lets DevSpace resume the same checkout workspace for
the same project in that conversation; repeated opens reuse the `workspaceId`
and do not repeat context already provided for that reused checkout. Worktree
mode always creates a new isolated workspace with its own complete context.
Hosts without supported conversation metadata receive a normal new workspace.
In all cases, continue passing the `workspaceId` returned by `open_workspace` to
later tools. Other MCP hosts use this explicit workspace workflow as well.
and DevSpace avoids repeating unchanged global/project context already delivered
in that conversation. Worktree mode creates a new managed worktree and workspace session, but
unchanged instructions, skills, agent profiles, and provider state can be
omitted from its model-visible result. A scope that appears again is the complete
replacement snapshot for that scope. Hosts without supported conversation
metadata receive a normal new workspace and complete context on each open. In
all cases, continue passing the `workspaceId` returned by `open_workspace` to
later tools and retain previously supplied context scopes when later results omit
them.

To review work, call `show_changes` once after the final related file change. It
shows the combined changes and advances the review point automatically.

## Data Retention

DevSpace does not currently prune workspace sessions, conversation bindings,
or review refs. A future product retention policy will define safe cleanup for
these records; no automatic deletion is performed today.
conversation context fingerprints, or review refs. A future product retention
policy will define safe cleanup for these records; no automatic deletion is
performed today.

## MCP Workspace Path Rejected

Expand Down Expand Up @@ -241,6 +246,8 @@ Legacy project paths such as `.pi/skills` can be added to `skills.paths` when ne

If a skill appears in `open_workspace`, the model must read that skill's
`SKILL.md` before reading other files inside the skill directory.
Project skill paths are relative to the active workspace root; global skill paths
may be home-shortened or absolute.

## Review Card Does Not Appear

Expand All @@ -254,5 +261,7 @@ in `~/.devspace/config.jsonc` and reconnect the MCP server.

Historical `show_changes` cards use the `reviewRef` in their structured result
to recover the exact Git-backed review when a host reloads the app without its
original result metadata. `open_workspace` can rebuild its card directly from
its structured result.
original result metadata. `open_workspace` can rebuild its card from the scoped
workspace context present in its structured result; hidden result metadata is
used when available to preserve the complete card for selectively suppressed
scopes.
18 changes: 18 additions & 0 deletions src/db/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ const migrations: Migration[] = [
name: "local-agent-effort-rename",
up: migrateLocalAgentEffortRename,
},
{
version: 7,
name: "workspace-conversation-contexts",
up: migrateWorkspaceConversationContexts,
},
];

export function migrateDatabase(sqlite: Database.Database): void {
Expand Down Expand Up @@ -208,6 +213,19 @@ function migrateWorkspaceConversationBindings(sqlite: Database.Database): void {
`);
}

function migrateWorkspaceConversationContexts(sqlite: Database.Database): void {
sqlite.exec(`
create table if not exists workspace_conversation_contexts (
conversation_scope_id text not null,
context_key text not null,
fingerprint text not null,
created_at text not null,
last_used_at text not null,
primary key (conversation_scope_id, context_key)
);
`);
}

function migrateLocalAgentStructuredErrors(sqlite: Database.Database): void {
addColumnIfMissing(sqlite, "local_agent_sessions", "error_code", "text");
addColumnIfMissing(sqlite, "local_agent_sessions", "error_retryable", "text");
Expand Down
16 changes: 16 additions & 0 deletions src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ export const workspaceConversationBindings = sqliteTable(
],
);

export const workspaceConversationContexts = sqliteTable(
"workspace_conversation_contexts",
{
conversationScopeId: text("conversation_scope_id").notNull(),
contextKey: text("context_key").notNull(),
fingerprint: text("fingerprint").notNull(),
createdAt: text("created_at").notNull(),
lastUsedAt: text("last_used_at").notNull(),
},
(table) => [
primaryKey({ columns: [table.conversationScopeId, table.contextKey] }),
],
);

export const oauthClients = sqliteTable(
"oauth_clients",
{
Expand Down Expand Up @@ -122,5 +136,7 @@ export type LoadedAgentFileRow = typeof loadedAgentFiles.$inferSelect;
export type NewLoadedAgentFileRow = typeof loadedAgentFiles.$inferInsert;
export type WorkspaceConversationBindingRow = typeof workspaceConversationBindings.$inferSelect;
export type NewWorkspaceConversationBindingRow = typeof workspaceConversationBindings.$inferInsert;
export type WorkspaceConversationContextRow = typeof workspaceConversationContexts.$inferSelect;
export type NewWorkspaceConversationContextRow = typeof workspaceConversationContexts.$inferInsert;
export type LocalAgentSessionRow = typeof localAgentSessions.$inferSelect;
export type NewLocalAgentSessionRow = typeof localAgentSessions.$inferInsert;
3 changes: 3 additions & 0 deletions src/local-agent-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const profiles: LocalAgentProfile[] = [
name: "reviewer",
description: "Review changes.",
provider: "codex",
scope: "project",
filePath: "/project/reviewer.md",
body: "Review only.",
disabled: false,
Expand All @@ -37,6 +38,7 @@ const profiles: LocalAgentProfile[] = [
name: "custom",
description: "Use a custom model.",
provider: "codex",
scope: "project",
model: "gpt-custom",
filePath: "/project/custom.md",
body: "Inspect.",
Expand All @@ -46,6 +48,7 @@ const profiles: LocalAgentProfile[] = [
name: "claude-reviewer",
description: "Unavailable profile.",
provider: "claude",
scope: "project",
filePath: "/project/claude.md",
body: "Review.",
disabled: false,
Expand Down
1 change: 1 addition & 0 deletions src/local-agent-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const profile: LocalAgentProfile = {
name: "reviewer",
description: "Test reviewer",
provider: "codex",
scope: "project",
filePath: join(root, "reviewer.md"),
body: "Review only.",
disabled: false,
Expand Down
1 change: 1 addition & 0 deletions src/local-agent-profiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ try {
assert.equal(profiles[0]?.name, "reviewer");
assert.equal(profiles[0]?.description, "Project reviewer #1.");
assert.equal(profiles[0]?.provider, "claude");
assert.equal(profiles[0]?.scope, "project");
assert.equal(profiles[0]?.model, "sonnet");
assert.equal(profiles[0]?.effort, "high");
assert.equal(profiles[0]?.body, "Project body.");
Expand Down
10 changes: 6 additions & 4 deletions src/local-agent-profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface LocalAgentProfile {
name: string;
description: string;
provider: LocalAgentProvider;
scope: "global" | "project";
model?: string;
effort?: string;
filePath: string;
Expand Down Expand Up @@ -51,14 +52,14 @@ export async function loadLocalAgentProfiles(
if (!config.subagents.enabled) return [];

const profileDirs = [
config.devspaceAgentsDir,
join(workspaceRoot, ".devspace", "agents"),
{ path: config.devspaceAgentsDir, scope: "global" as const },
{ path: join(workspaceRoot, ".devspace", "agents"), scope: "project" as const },
];
const profilesByName = new Map<string, LocalAgentProfile>();

for (const directory of profileDirs) {
for (const profile of await loadProfilesFromDirectory(directory)) {
profilesByName.set(profile.name, profile);
for (const profile of await loadProfilesFromDirectory(directory.path)) {
profilesByName.set(profile.name, { ...profile, scope: directory.scope });
}
}

Expand Down Expand Up @@ -146,6 +147,7 @@ function profileFromFrontmatter(
name,
description,
provider,
scope: "global",
model: readString(frontmatter, "model"),
effort: readString(frontmatter, "effort"),
filePath,
Expand Down
2 changes: 2 additions & 0 deletions src/local-agent-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const profiles: LocalAgentProfile[] = [
name: "reviewer",
description: "Review changes.",
provider: "codex",
scope: "project",
model: "gpt-5-codex",
effort: "high",
filePath: "/workspace/.devspace/agents/reviewer.md",
Expand All @@ -20,6 +21,7 @@ const profiles: LocalAgentProfile[] = [
name: "claude",
description: "A profile that shadows the raw provider.",
provider: "opencode",
scope: "project",
model: "qwen/custom",
filePath: "/workspace/.devspace/agents/claude.md",
body: "Use OpenCode.",
Expand Down
1 change: 1 addition & 0 deletions src/oauth-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ async function testDatabaseConfiguration(stateDir: string): Promise<void> {
{ version: 4, name: "workspace-conversation-bindings" },
{ version: 5, name: "local-agent-structured-errors" },
{ version: 6, name: "local-agent-effort-rename" },
{ version: 7, name: "workspace-conversation-contexts" },
]);
} finally {
database.close();
Expand Down
Loading
Loading