praxis-agent 0.21.6 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +6 -6
  2. package/dist/application/claude-project-purge.d.ts +4 -1
  3. package/dist/application/claude-project-purge.js +85 -18
  4. package/dist/application/session-memory.d.ts +1 -0
  5. package/dist/application/session-memory.js +1 -1
  6. package/dist/application/session-service.d.ts +4 -0
  7. package/dist/application/session-service.js +50 -19
  8. package/dist/application/session-worktree.d.ts +6 -0
  9. package/dist/application/session-worktree.js +11 -5
  10. package/dist/application/subagent-service.d.ts +4 -1
  11. package/dist/application/subagent-service.js +38 -28
  12. package/dist/application/top-level-agent-manager.d.ts +4 -0
  13. package/dist/application/top-level-agent-manager.js +34 -14
  14. package/dist/application/workflow-manager.d.ts +3 -1
  15. package/dist/application/workflow-manager.js +16 -6
  16. package/dist/cli/controls.js +8 -0
  17. package/dist/cli/interactive.d.ts +8 -1
  18. package/dist/cli/interactive.js +79 -46
  19. package/dist/cli/protocol.d.ts +9 -0
  20. package/dist/cli/protocol.js +87 -1
  21. package/dist/cli/tui/claude-style.d.ts +5 -2
  22. package/dist/cli/tui/claude-style.js +6 -4
  23. package/dist/cli/tui/config-settings.js +9 -8
  24. package/dist/cli/tui/custom-themes.js +2 -2
  25. package/dist/cli/tui/mcp-panel.d.ts +3 -1
  26. package/dist/cli/tui/mcp-panel.js +8 -6
  27. package/dist/cli/tui/memory-files.d.ts +3 -1
  28. package/dist/cli/tui/memory-files.js +37 -13
  29. package/dist/cli/tui/permission-settings.d.ts +6 -3
  30. package/dist/cli/tui/permission-settings.js +19 -11
  31. package/dist/cli/tui/sandbox-settings.d.ts +3 -1
  32. package/dist/cli/tui/sandbox-settings.js +7 -3
  33. package/dist/cli/tui/status-line.d.ts +5 -1
  34. package/dist/cli/tui/status-line.js +27 -11
  35. package/dist/cli/tui/terminal-setup.js +9 -3
  36. package/dist/cli/tui/theme-settings.js +2 -2
  37. package/dist/cli/tui/tool-permission.d.ts +2 -1
  38. package/dist/cli/tui/tool-permission.js +23 -17
  39. package/dist/cli-runtime.d.ts +10 -0
  40. package/dist/cli-runtime.js +541 -143
  41. package/dist/compatibility/claude/context.js +7 -1
  42. package/dist/compatibility/claude/fork.js +1 -0
  43. package/dist/extensions/claude-extensions.d.ts +2 -0
  44. package/dist/extensions/claude-extensions.js +27 -7
  45. package/dist/extensions/claude-init-command.d.ts +3 -2
  46. package/dist/extensions/claude-init-command.js +58 -2
  47. package/dist/maintenance/doctor.d.ts +2 -0
  48. package/dist/maintenance/doctor.js +48 -15
  49. package/dist/mcp/claude-mcp-management.d.ts +5 -0
  50. package/dist/mcp/claude-mcp-management.js +27 -9
  51. package/dist/mcp/praxis-mcp-server.js +1 -1
  52. package/dist/permissions/bypass-immune-paths.d.ts +2 -0
  53. package/dist/permissions/bypass-immune-paths.js +25 -3
  54. package/dist/persistence/claude-job-store.d.ts +2 -1
  55. package/dist/persistence/claude-job-store.js +4 -2
  56. package/dist/persistence/claude-migration.d.ts +5 -0
  57. package/dist/persistence/claude-migration.js +225 -0
  58. package/dist/persistence/data-plane.d.ts +34 -0
  59. package/dist/persistence/data-plane.js +66 -0
  60. package/dist/persistence/native-resources.d.ts +9 -0
  61. package/dist/persistence/native-resources.js +102 -0
  62. package/dist/plugins/claude-plugin-eval-discovery.d.ts +1 -0
  63. package/dist/plugins/claude-plugin-eval-discovery.js +4 -0
  64. package/dist/plugins/claude-plugin-eval-runner.d.ts +3 -0
  65. package/dist/plugins/claude-plugin-eval-runner.js +1 -0
  66. package/dist/plugins/claude-plugin-eval.d.ts +11 -3
  67. package/dist/plugins/claude-plugin-eval.js +54 -12
  68. package/dist/plugins/claude-plugin-marketplace.d.ts +15 -14
  69. package/dist/plugins/claude-plugin-marketplace.js +48 -46
  70. package/dist/plugins/claude-plugin-mcpb.d.ts +3 -0
  71. package/dist/plugins/claude-plugin-mcpb.js +4 -2
  72. package/dist/plugins/claude-plugin-runtime.d.ts +2 -0
  73. package/dist/plugins/claude-plugin-runtime.js +8 -6
  74. package/dist/sandbox/claude-sandbox-settings.d.ts +3 -1
  75. package/dist/sandbox/claude-sandbox-settings.js +9 -3
  76. package/dist/tools/claude-scheduled-tools.d.ts +2 -0
  77. package/dist/tools/claude-scheduled-tools.js +89 -74
  78. package/dist/tools/claude-workflow-tools.d.ts +2 -0
  79. package/dist/tools/claude-workflow-tools.js +26 -22
  80. package/dist/tools/claude-worktree-tools.d.ts +2 -0
  81. package/dist/tools/claude-worktree-tools.js +27 -1
  82. package/dist/tools/local-tools.d.ts +2 -0
  83. package/dist/tools/local-tools.js +15 -0
  84. package/dist/tools/web.js +1 -1
  85. package/package.json +1 -1
package/README.md CHANGED
@@ -161,16 +161,16 @@ not in this entry-point README.
161
161
 
162
162
  ## Claude Code interoperability
163
163
 
164
- Praxis and Claude Code use one local data plane by default:
164
+ Praxis uses an independent local data plane by default:
165
165
 
166
166
  ```text
167
- Claude Code ─┐
168
- ├── ~/.claude (or CLAUDE_CONFIG_DIR)
169
- Praxis ──────┘
167
+ Praxis ─── ~/.praxis (or PRAXIS_HOME)
170
168
  ```
171
169
 
172
- Praxis can resume Claude Code sessions, and Claude Code can resume compatible
173
- sessions written by Praxis. Every semver-like Claude Code producer version is
170
+ Use `praxis --data-plane claude` when you need the legacy shared Claude Code
171
+ layout (`~/.claude` or `CLAUDE_CONFIG_DIR`). Praxis can resume Claude Code
172
+ sessions in that mode, and Claude Code can resume compatible sessions written
173
+ there. Every semver-like Claude Code producer version is
174
174
  structurally validated and is read/write compatible when its entry shape is
175
175
  supported; schema adapters are selected from transcript entry structure rather
176
176
  than an installed or fixed producer version, and malformed or unsupported
@@ -1,4 +1,5 @@
1
- export type ClaudeProjectPurgeItemKind = 'project' | 'tasks' | 'debug' | 'file-history' | 'prompt-history' | 'config-key';
1
+ import type { DataPlane } from '../persistence/data-plane.js';
2
+ export type ClaudeProjectPurgeItemKind = 'project' | 'memory' | 'scheduled' | 'tasks' | 'debug' | 'file-history' | 'prompt-history' | 'config-key';
2
3
  type ClaudeProjectPurgeOperation = 'remove-path' | 'filter-history' | 'remove-project-config' | 'clear-project-config';
3
4
  export interface ClaudeProjectPurgeItem {
4
5
  id: string;
@@ -14,6 +15,7 @@ export interface ClaudeProjectPurgePlan {
14
15
  mode: 'project' | 'all';
15
16
  configRoot: string;
16
17
  statePath: string;
18
+ allowStateOutsideConfigRoot?: boolean;
17
19
  targetPath?: string;
18
20
  projectIdentity?: string;
19
21
  projectRootPaths: string[];
@@ -21,6 +23,7 @@ export interface ClaudeProjectPurgePlan {
21
23
  items: ClaudeProjectPurgeItem[];
22
24
  }
23
25
  export interface PlanClaudeProjectPurgeOptions {
26
+ dataPlane?: DataPlane;
24
27
  cwd: string;
25
28
  path?: string;
26
29
  all?: boolean;
@@ -1,6 +1,6 @@
1
1
  import { lstat, readFile, readdir, realpath, rm } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
- import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from 'node:path';
4
4
  import { isClaudeSessionId, sanitizeClaudeProjectPath, } from '../compatibility/claude/paths.js';
5
5
  import { resolveClaudeProjectIdentity } from '../compatibility/claude/shared-resources.js';
6
6
  import { writeFileAtomically } from '../platform/atomic-write.js';
@@ -74,7 +74,8 @@ async function assertSafePurgePath(configRoot, candidate) {
74
74
  const canonical = await realpath(absolute);
75
75
  assertLexicallyContained(configRoot, canonical);
76
76
  }
77
- async function assertMutableRegularFile(path) {
77
+ async function assertMutableRegularFile(path, boundary) {
78
+ const canonicalBoundary = await canonicalPath(boundary);
78
79
  let current = resolve(path);
79
80
  while (true) {
80
81
  try {
@@ -87,6 +88,14 @@ async function assertMutableRegularFile(path) {
87
88
  if (errorCode(error) !== 'ENOENT')
88
89
  throw error;
89
90
  }
91
+ try {
92
+ if ((await realpath(current)) === canonicalBoundary)
93
+ break;
94
+ }
95
+ catch (error) {
96
+ if (errorCode(error) !== 'ENOENT')
97
+ throw error;
98
+ }
90
99
  const parent = dirname(current);
91
100
  if (parent === current)
92
101
  break;
@@ -182,8 +191,8 @@ async function discoverSessionIds(projectRoot) {
182
191
  }
183
192
  return [...sessionIds].sort();
184
193
  }
185
- async function discoverProjectRoots(configRoot, targetPaths) {
186
- const projectsRoot = join(configRoot, 'projects');
194
+ async function discoverProjectRoots(configRoot, targetPaths, dataPlane) {
195
+ const projectsRoot = join(configRoot, dataPlane === 'native' ? 'sessions' : 'projects');
187
196
  if (!(await pathExists(projectsRoot)))
188
197
  return [];
189
198
  await assertSafePurgePath(configRoot, projectsRoot);
@@ -198,9 +207,10 @@ async function discoverProjectRoots(configRoot, targetPaths) {
198
207
  throw error;
199
208
  }
200
209
  const targetKeys = targetPaths.map(sanitizeClaudeProjectPath);
210
+ const worktreeSegment = dataPlane === 'native' ? '--praxis-worktrees-' : '--claude-worktrees-';
201
211
  return entries
202
212
  .filter((entry) => targetKeys.some((targetKey) => entry.name === targetKey ||
203
- entry.name.startsWith(`${targetKey}--worktrees-`)))
213
+ entry.name.startsWith(`${targetKey}${worktreeSegment}`)))
204
214
  .map((entry) => join(projectsRoot, entry.name))
205
215
  .sort();
206
216
  }
@@ -227,11 +237,26 @@ export async function planClaudeProjectPurge(options) {
227
237
  const configuredRoot = options.configRoot ??
228
238
  (process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude'));
229
239
  const configRoot = await canonicalPath(configuredRoot);
230
- const defaultStatePath = options.configRoot !== undefined || Boolean(process.env.CLAUDE_CONFIG_DIR)
231
- ? join(configRoot, '.claude.json')
232
- : resolve(homedir(), '.claude.json');
233
- const statePath = await canonicalPath(options.statePath ?? defaultStatePath);
234
- await assertMutableRegularFile(statePath);
240
+ const dataPlane = options.dataPlane ?? 'claude';
241
+ const homeDirectory = options.homeDirectory ?? homedir();
242
+ const legacyClaudeStatePath = resolve(homeDirectory, '.claude.json');
243
+ const legacyClaudeConfigRoot = await canonicalPath(resolve(homeDirectory, '.claude'));
244
+ const defaultStatePath = dataPlane === 'native'
245
+ ? join(configRoot, 'state.json')
246
+ : options.configRoot !== undefined ||
247
+ Boolean(process.env.CLAUDE_CONFIG_DIR)
248
+ ? join(configRoot, '.claude.json')
249
+ : legacyClaudeStatePath;
250
+ const statePath = resolve(options.statePath ?? defaultStatePath);
251
+ const usesLegacyDefaultClaudeState = dataPlane === 'claude' &&
252
+ !process.env.CLAUDE_CONFIG_DIR &&
253
+ configRoot === legacyClaudeConfigRoot &&
254
+ statePath === legacyClaudeStatePath;
255
+ const canonicalStateParent = await canonicalPath(dirname(statePath));
256
+ if (!usesLegacyDefaultClaudeState) {
257
+ assertLexicallyContained(configRoot, join(canonicalStateParent, basename(statePath)));
258
+ }
259
+ await assertMutableRegularFile(statePath, usesLegacyDefaultClaudeState ? canonicalStateParent : configRoot);
235
260
  const state = parseState(await readOptionalFile(statePath), statePath);
236
261
  const projects = stateProjects(state);
237
262
  const items = [];
@@ -239,9 +264,25 @@ export async function planClaudeProjectPurge(options) {
239
264
  const roots = [
240
265
  {
241
266
  kind: 'project',
242
- name: 'projects',
243
- description: 'all project transcripts and memory',
267
+ name: dataPlane === 'native' ? 'sessions' : 'projects',
268
+ description: dataPlane === 'native'
269
+ ? 'all project transcripts'
270
+ : 'all project transcripts and memory',
244
271
  },
272
+ ...(dataPlane === 'native'
273
+ ? [
274
+ {
275
+ kind: 'memory',
276
+ name: 'memory',
277
+ description: 'all project memory',
278
+ },
279
+ {
280
+ kind: 'scheduled',
281
+ name: 'scheduled',
282
+ description: 'all project scheduled prompts',
283
+ },
284
+ ]
285
+ : []),
245
286
  {
246
287
  kind: 'tasks',
247
288
  name: 'tasks',
@@ -282,6 +323,9 @@ export async function planClaudeProjectPurge(options) {
282
323
  mode: 'all',
283
324
  configRoot,
284
325
  statePath,
326
+ ...(usesLegacyDefaultClaudeState
327
+ ? { allowStateOutsideConfigRoot: true }
328
+ : {}),
285
329
  projectRootPaths: [],
286
330
  sessionIds: [],
287
331
  items,
@@ -294,7 +338,7 @@ export async function planClaudeProjectPurge(options) {
294
338
  cwd: targetPath,
295
339
  homeDirectory: options.homeDirectory ?? homedir(),
296
340
  });
297
- const projectRootPaths = await discoverProjectRoots(configRoot, targetPaths);
341
+ const projectRootPaths = await discoverProjectRoots(configRoot, targetPaths, dataPlane);
298
342
  const sessionIdSet = new Set();
299
343
  for (const projectRoot of projectRootPaths) {
300
344
  await assertSafePurgePath(configRoot, projectRoot);
@@ -308,8 +352,20 @@ export async function planClaudeProjectPurge(options) {
308
352
  await addExistingPath(items, configRoot, removePathItem('debug', join(configRoot, 'debug', `${sessionId}.txt`), `debug log for session ${sessionId}`));
309
353
  await addExistingPath(items, configRoot, removePathItem('file-history', join(configRoot, 'file-history', sessionId), `file edit history for session ${sessionId}`));
310
354
  }
355
+ if (dataPlane === 'native') {
356
+ const projectKeys = new Set([
357
+ ...targetPaths.map(sanitizeClaudeProjectPath),
358
+ ...projectRootPaths.map((path) => basename(path)),
359
+ ]);
360
+ for (const projectKey of projectKeys) {
361
+ await addExistingPath(items, configRoot, removePathItem('memory', join(configRoot, 'memory', projectKey), `memory for project ${projectKey}`));
362
+ await addExistingPath(items, configRoot, removePathItem('scheduled', join(configRoot, 'scheduled', `${projectKey}.json`), `scheduled prompts for project ${projectKey}`));
363
+ }
364
+ }
311
365
  for (const projectRoot of projectRootPaths) {
312
- await addExistingPath(items, configRoot, removePathItem('project', projectRoot, 'project transcripts and memory'));
366
+ await addExistingPath(items, configRoot, removePathItem('project', projectRoot, dataPlane === 'native'
367
+ ? 'project transcripts'
368
+ : 'project transcripts and memory'));
313
369
  }
314
370
  const historyPath = join(configRoot, 'history.jsonl');
315
371
  const historyExists = await pathExists(historyPath);
@@ -351,6 +407,9 @@ export async function planClaudeProjectPurge(options) {
351
407
  mode: 'project',
352
408
  configRoot,
353
409
  statePath,
410
+ ...(usesLegacyDefaultClaudeState
411
+ ? { allowStateOutsideConfigRoot: true }
412
+ : {}),
354
413
  targetPath,
355
414
  projectIdentity,
356
415
  projectRootPaths,
@@ -358,8 +417,8 @@ export async function planClaudeProjectPurge(options) {
358
417
  items,
359
418
  };
360
419
  }
361
- async function atomicallyUpdate(path, update) {
362
- await assertMutableRegularFile(path);
420
+ async function atomicallyUpdate(path, update, boundary) {
421
+ await assertMutableRegularFile(path, boundary);
363
422
  for (let attempt = 0; attempt < MAX_ATOMIC_UPDATE_RETRIES; attempt += 1) {
364
423
  const source = await readOptionalFile(path);
365
424
  const content = update(source);
@@ -389,9 +448,17 @@ async function executeItem(plan, item) {
389
448
  if (source === null)
390
449
  return null;
391
450
  return filterProjectHistory(source, projectPaths).content;
392
- });
451
+ }, plan.configRoot);
393
452
  return;
394
453
  }
454
+ const stateParent = await canonicalPath(dirname(item.path));
455
+ let stateBoundary;
456
+ if (plan.allowStateOutsideConfigRoot)
457
+ stateBoundary = stateParent;
458
+ else {
459
+ assertLexicallyContained(plan.configRoot, join(stateParent, basename(item.path)));
460
+ stateBoundary = plan.configRoot;
461
+ }
395
462
  await atomicallyUpdate(item.path, (source) => {
396
463
  if (source === null)
397
464
  return null;
@@ -412,7 +479,7 @@ async function executeItem(plan, item) {
412
479
  }
413
480
  state.projects = projects;
414
481
  return `${JSON.stringify(state, null, 2)}\n`;
415
- });
482
+ }, stateBoundary);
416
483
  }
417
484
  export async function executeClaudeProjectPurge(plan, options = {}) {
418
485
  if (options.dryRun) {
@@ -22,6 +22,7 @@ export declare function createFreshSessionMemoryState(): SessionMemoryState;
22
22
  export interface SessionMemoryStoreOptions {
23
23
  configRoot: string;
24
24
  sessionId: string;
25
+ sidecarRoot?: string;
25
26
  }
26
27
  /**
27
28
  * Durable sidecar for one session's extracted memory under
@@ -94,7 +94,7 @@ export class SessionMemoryStore {
94
94
  if (!isClaudeSessionId(options.sessionId)) {
95
95
  throw new SessionMemoryStateError(`Invalid session memory session ID: ${options.sessionId}`);
96
96
  }
97
- this.directory = resolve(options.configRoot, 'praxis', 'session-memory', options.sessionId);
97
+ this.directory = resolve(options.sidecarRoot ?? resolve(options.configRoot, 'praxis'), 'session-memory', options.sessionId);
98
98
  this.stateFile = join(this.directory, 'state.json');
99
99
  this.summaryFile = join(this.directory, 'summary.md');
100
100
  }
@@ -1,5 +1,6 @@
1
1
  import { type AgentColorName, type AgentColorSelection } from '../compatibility/claude/agent-color.js';
2
2
  import type { ClaudeConditionalRuleResolver } from '../compatibility/claude/context.js';
3
+ import { type DataPlane } from '../persistence/data-plane.js';
3
4
  import { type ClaudeFileResource, type ClaudeFileResourceConfig } from '../compatibility/claude/file-resources.js';
4
5
  import { type ClaudeDisplayTranscriptItem } from '../compatibility/claude/projection.js';
5
6
  import { type ModelDocument, type ModelImage, type ModelToolCall, type ModelProvider, type ModelUsage, type PermissionApproval, type PermissionDecision, type PermissionResolver, type PermissionUpdate, type RuntimeEventSink, type ToolRegistry } from '../core/runtime.js';
@@ -23,6 +24,8 @@ import type { ClaudePermissionMode } from '../permissions/claude-permission-reso
23
24
  import type { ClaudeMcpRuntime, ClaudeMcpServerStatus, ClaudeMcpToolInspection } from '../mcp/claude-mcp-tools.js';
24
25
  export interface ClaudeSessionServiceOptions {
25
26
  configRoot: string;
27
+ /** `claude` preserves the legacy shared layout; the CLI selects `native` by default. */
28
+ dataPlane?: DataPlane;
26
29
  cwd: string;
27
30
  claudeVersion: string;
28
31
  provider?: ModelProvider;
@@ -246,6 +249,7 @@ export declare class ClaudeSessionService {
246
249
  private restoreWorktree;
247
250
  private paths;
248
251
  private discoverProjectRoot;
252
+ private pathsForCwd;
249
253
  private appendCdCommand;
250
254
  private appendAgentColorUsage;
251
255
  private appendSystemLocalCommand;
@@ -5,6 +5,7 @@ import { basename, extname, isAbsolute, join, relative, resolve, } from 'node:pa
5
5
  import { AGENT_COLOR_DEFAULT, agentColorMessage, getClaudeEffectiveAgentColor, } from '../compatibility/claude/agent-color.js';
6
6
  import { createClaudeCompactEntries, formatClaudeCompactSummary, getCumulativeDroppedTokens, } from '../compatibility/claude/compaction.js';
7
7
  import { discoverClaudeProjectRoot, isClaudeSessionId, resolveClaudePaths, resolveClaudeScheduledTaskFile, } from '../compatibility/claude/paths.js';
8
+ import { resolveDataPlanePaths, resolveScheduledTaskFile, } from '../persistence/data-plane.js';
8
9
  import { downloadClaudeFileResources, } from '../compatibility/claude/file-resources.js';
9
10
  import { createClaudeNativeFork } from '../compatibility/claude/fork.js';
10
11
  import { selectClaudeActiveTranscript, selectClaudeTranscriptAtMessage, } from '../compatibility/claude/history.js';
@@ -347,15 +348,28 @@ export class ClaudeSessionService {
347
348
  this.scheduledPrompts =
348
349
  options.tools && (options.scheduledToolNames?.length ?? 0) > 0
349
350
  ? new ScheduledPromptManager({
350
- filePath: resolveClaudeScheduledTaskFile(options.cwd),
351
- lockFile: join(options.configRoot, 'praxis', 'locks', 'scheduled-tasks.lock'),
351
+ filePath: options.dataPlane === 'native'
352
+ ? resolveScheduledTaskFile({
353
+ dataPlane: 'native',
354
+ cwd: options.cwd,
355
+ root: options.configRoot,
356
+ })
357
+ : resolveClaudeScheduledTaskFile(options.cwd),
358
+ lockFile: join(options.dataPlane === 'native'
359
+ ? resolveDataPlanePaths({
360
+ dataPlane: 'native',
361
+ root: options.configRoot,
362
+ cwd: options.cwd,
363
+ sessionId: '00000000-0000-4000-8000-000000000000',
364
+ }).praxisRoot
365
+ : resolve(options.configRoot, 'praxis'), 'locks', 'scheduled-tasks.lock'),
352
366
  ...(options.enableDynamicWakeups === undefined
353
367
  ? {}
354
368
  : { dynamicWakeupsEnabled: options.enableDynamicWakeups }),
355
369
  })
356
370
  : null;
357
371
  this.workflowManager = options.enableWorkflows
358
- ? new WorkflowManager(options.configRoot, options.cwd, () => this.activeCwd())
372
+ ? new WorkflowManager(options.configRoot, options.cwd, () => this.activeCwd(), options.dataPlane)
359
373
  : null;
360
374
  this.backgroundTasks = new BackgroundTaskRuntime(this.workflowManager);
361
375
  this.worktreeManager =
@@ -363,6 +377,7 @@ export class ClaudeSessionService {
363
377
  ? new SessionWorktreeManager({
364
378
  workspace: options.workspace,
365
379
  sessionId: '',
380
+ dataPlane: options.dataPlane ?? 'claude',
366
381
  ...(options.worktreeBaseRef
367
382
  ? { baseRef: options.worktreeBaseRef }
368
383
  : {}),
@@ -509,6 +524,7 @@ export class ClaudeSessionService {
509
524
  manager: this.scheduledPrompts,
510
525
  sessionId,
511
526
  enabledTools: scheduledToolNames,
527
+ dataPlane: this.options.dataPlane ?? 'claude',
512
528
  })
513
529
  : null;
514
530
  const wrappedBase = scheduledTools ?? taskTools ?? baseTools;
@@ -516,6 +532,7 @@ export class ClaudeSessionService {
516
532
  this.options.permissions
517
533
  ? new ClaudeSubagentExecutor({
518
534
  configRoot: this.options.configRoot,
535
+ dataPlane: this.options.dataPlane ?? 'claude',
519
536
  cwd: this.activeCwd(),
520
537
  cwdProvider: () => this.activeCwd(),
521
538
  claudeVersion: this.options.claudeVersion,
@@ -592,6 +609,7 @@ export class ClaudeSessionService {
592
609
  defaultModel: this.options.provider?.model ?? 'praxis/provider',
593
610
  tokenBudget: null,
594
611
  enabled: capabilities.has('Workflow'),
612
+ dataPlane: this.options.dataPlane ?? 'claude',
595
613
  })
596
614
  : agentTools;
597
615
  if (this.worktreeManager)
@@ -601,6 +619,7 @@ export class ClaudeSessionService {
601
619
  base: workflowTools,
602
620
  manager: this.worktreeManager,
603
621
  workspace: this.options.workspace,
622
+ dataPlane: this.options.dataPlane ?? 'claude',
604
623
  ...(this.options.worktreeToolNames
605
624
  ? { enabledTools: this.options.worktreeToolNames }
606
625
  : {}),
@@ -1051,16 +1070,8 @@ export class ClaudeSessionService {
1051
1070
  throw new Error(`Not a directory: ${requestedCwd}`);
1052
1071
  }
1053
1072
  if (sessionId && this.options.sessionPersistence !== false) {
1054
- const sourcePaths = resolveClaudePaths({
1055
- configDir: this.options.configRoot,
1056
- cwd: this.sessionCwds.get(sessionId) ?? previousCwd,
1057
- sessionId,
1058
- });
1059
- const targetPaths = resolveClaudePaths({
1060
- configDir: this.options.configRoot,
1061
- cwd,
1062
- sessionId,
1063
- });
1073
+ const sourcePaths = this.pathsForCwd(sessionId, this.sessionCwds.get(sessionId) ?? previousCwd);
1074
+ const targetPaths = this.pathsForCwd(sessionId, cwd);
1064
1075
  if (sourcePaths.sessionFile !== targetPaths.sessionFile) {
1065
1076
  try {
1066
1077
  await lstat(targetPaths.sessionFile);
@@ -1909,6 +1920,7 @@ export class ClaudeSessionService {
1909
1920
  manager: this.scheduledPrompts,
1910
1921
  sessionId,
1911
1922
  enabledTools: scheduledToolNames,
1923
+ dataPlane: this.options.dataPlane ?? 'claude',
1912
1924
  })
1913
1925
  : null;
1914
1926
  const baseTools = scheduledTools ?? taskTools ?? this.options.tools;
@@ -1919,6 +1931,7 @@ export class ClaudeSessionService {
1919
1931
  turnPermissions
1920
1932
  ? new ClaudeSubagentExecutor({
1921
1933
  configRoot: this.options.configRoot,
1934
+ dataPlane: this.options.dataPlane ?? 'claude',
1922
1935
  cwd: this.activeCwd(),
1923
1936
  cwdProvider: () => this.activeCwd(),
1924
1937
  claudeVersion: this.options.claudeVersion,
@@ -1996,6 +2009,7 @@ export class ClaudeSessionService {
1996
2009
  defaultModel: provider.model ?? 'praxis/provider',
1997
2010
  tokenBudget: workflowTokenTarget(effectivePrompt),
1998
2011
  enabled: capabilities.has('Workflow'),
2012
+ dataPlane: this.options.dataPlane ?? 'claude',
1999
2013
  })
2000
2014
  : agentTools;
2001
2015
  const workspaceTools = this.worktreeManager &&
@@ -2007,6 +2021,7 @@ export class ClaudeSessionService {
2007
2021
  manager: this.worktreeManager,
2008
2022
  workspace: this.options.workspace,
2009
2023
  enabledTools: this.options.worktreeToolNames ?? [],
2024
+ dataPlane: this.options.dataPlane ?? 'claude',
2010
2025
  })
2011
2026
  : turnTools;
2012
2027
  const messageTools = this.options.brief && workspaceTools
@@ -3277,11 +3292,9 @@ export class ClaudeSessionService {
3277
3292
  this.worktreeManager.restore(state);
3278
3293
  }
3279
3294
  paths(sessionId) {
3280
- const exact = resolveClaudePaths({
3281
- configDir: this.options.configRoot,
3282
- cwd: this.sessionCwds.get(sessionId) ?? this.activeCwd(),
3283
- sessionId,
3284
- });
3295
+ const exact = this.pathsForCwd(sessionId, this.sessionCwds.get(sessionId) ?? this.activeCwd());
3296
+ if (this.options.dataPlane === 'native')
3297
+ return exact;
3285
3298
  const discovered = this.discoveredProjectRoots.get(sessionId);
3286
3299
  if (discovered === undefined)
3287
3300
  return exact;
@@ -3292,6 +3305,8 @@ export class ClaudeSessionService {
3292
3305
  };
3293
3306
  }
3294
3307
  async discoverProjectRoot(sessionId) {
3308
+ if (this.options.dataPlane === 'native')
3309
+ return;
3295
3310
  if (this.discoveredProjectRoots.has(sessionId))
3296
3311
  return;
3297
3312
  const discovered = await discoverClaudeProjectRoot({
@@ -3303,6 +3318,21 @@ export class ClaudeSessionService {
3303
3318
  this.discoveredProjectRoots.set(sessionId, discovered);
3304
3319
  }
3305
3320
  }
3321
+ pathsForCwd(sessionId, cwd) {
3322
+ if (this.options.dataPlane === 'native') {
3323
+ return resolveDataPlanePaths({
3324
+ dataPlane: 'native',
3325
+ root: this.options.configRoot,
3326
+ cwd,
3327
+ sessionId,
3328
+ });
3329
+ }
3330
+ return resolveClaudePaths({
3331
+ configDir: this.options.configRoot,
3332
+ cwd,
3333
+ sessionId,
3334
+ });
3335
+ }
3306
3336
  async appendCdCommand(sessionId, cwd) {
3307
3337
  const result = await this.store(sessionId).withLease(async (lease) => {
3308
3338
  const snapshot = await lease.load();
@@ -3724,7 +3754,7 @@ export class ClaudeSessionService {
3724
3754
  !this.options.agentSystemPromptOverridesExplicit)) {
3725
3755
  return null;
3726
3756
  }
3727
- const memory = await agentMemoryPrompt(this.options.configRoot, this.activeCwd(), agent);
3757
+ const memory = await agentMemoryPrompt(this.options.configRoot, this.activeCwd(), agent, this.options.dataPlane ?? 'claude');
3728
3758
  const system = memory ? `${agent.body}\n\n${memory}` : agent.body;
3729
3759
  return system.trim() ? system : null;
3730
3760
  }
@@ -3766,6 +3796,7 @@ export class ClaudeSessionService {
3766
3796
  store: new SessionMemoryStore({
3767
3797
  configRoot: this.options.configRoot,
3768
3798
  sessionId,
3799
+ sidecarRoot: this.paths(sessionId).praxisRoot,
3769
3800
  }),
3770
3801
  extractor: (input) => this.extractSessionMemory(sessionId, input),
3771
3802
  });
@@ -1,5 +1,8 @@
1
+ import type { DataPlane } from '../persistence/data-plane.js';
1
2
  export interface WorktreeSessionState {
2
3
  originalCwd: string;
4
+ /** Repository root used to validate the persisted worktree path on restore. */
5
+ repositoryRoot?: string;
3
6
  preEnterOriginalCwd: string;
4
7
  worktreePath: string;
5
8
  worktreeName: string;
@@ -29,7 +32,10 @@ export declare class SessionWorktreeManager {
29
32
  workspace: WorkspaceContext;
30
33
  sessionId: string;
31
34
  baseRef?: 'fresh' | 'head';
35
+ /** Claude compatibility retains its established project worktree path. */
36
+ dataPlane?: DataPlane;
32
37
  });
38
+ private worktreeRoot;
33
39
  bindSession(sessionId: string): void;
34
40
  current(): WorktreeSessionState | null;
35
41
  ensureInitial(name?: string): Promise<void>;
@@ -64,6 +64,9 @@ export class SessionWorktreeManager {
64
64
  constructor(options) {
65
65
  this.options = options;
66
66
  }
67
+ worktreeRoot(repositoryRoot) {
68
+ return join(repositoryRoot, this.options.dataPlane === 'native' ? '.praxis' : '.claude', 'worktrees');
69
+ }
67
70
  bindSession(sessionId) {
68
71
  if (this.active && this.active.sessionId !== sessionId) {
69
72
  throw new Error(`Cannot switch to session ${sessionId} while worktree session ${this.active.sessionId} is active`);
@@ -86,7 +89,7 @@ export class SessionWorktreeManager {
86
89
  if (state.sessionId !== this.options.sessionId)
87
90
  return;
88
91
  const worktreePath = resolve(state.worktreePath);
89
- const allowedRoot = resolve(state.originalCwd, '.claude', 'worktrees');
92
+ const allowedRoot = this.worktreeRoot(state.repositoryRoot ?? state.originalCwd);
90
93
  if (!isWithin(allowedRoot, worktreePath))
91
94
  return;
92
95
  // A crashed session can leave the directory behind after Git has
@@ -141,13 +144,14 @@ export class SessionWorktreeManager {
141
144
  if (invalidName(name)) {
142
145
  throw new Error('Worktree name must use slash-separated letters, digits, dots, underscores, or dashes and be at most 64 characters');
143
146
  }
144
- const worktreePath = join(root, '.claude', 'worktrees', name);
147
+ const worktreeRoot = this.worktreeRoot(root);
148
+ const worktreePath = join(worktreeRoot, name);
145
149
  const branch = `worktree-${nameFromPath(name)}`;
146
150
  const baseCommit = this.options.baseRef === 'head'
147
151
  ? originalHeadCommit
148
152
  : ((await gitOptional(root, ['merge-base', 'HEAD', '@{upstream}'])) ??
149
153
  originalHeadCommit);
150
- await mkdir(join(root, '.claude', 'worktrees'), { recursive: true });
154
+ await mkdir(worktreeRoot, { recursive: true });
151
155
  try {
152
156
  await git(root, [
153
157
  'worktree',
@@ -174,6 +178,7 @@ export class SessionWorktreeManager {
174
178
  }
175
179
  const state = {
176
180
  originalCwd,
181
+ repositoryRoot: root,
177
182
  preEnterOriginalCwd: originalCwd,
178
183
  worktreePath,
179
184
  worktreeName: name,
@@ -283,9 +288,9 @@ export class SessionWorktreeManager {
283
288
  }
284
289
  async enterExisting(root, originalCwd, originalBranch, originalHeadCommit, requestedPath) {
285
290
  const worktreePath = await realpath(resolve(originalCwd, requestedPath));
286
- const allowedRoot = resolve(root, '.claude', 'worktrees');
291
+ const allowedRoot = this.worktreeRoot(root);
287
292
  if (!isWithin(allowedRoot, worktreePath)) {
288
- throw new Error('Worktree path must be inside .claude/worktrees');
293
+ throw new Error(`Worktree path must be inside ${this.options.dataPlane === 'native' ? '.praxis' : '.claude'}/worktrees`);
289
294
  }
290
295
  const worktrees = parseWorktrees(await git(root, ['worktree', 'list', '--porcelain']));
291
296
  const entry = worktrees.find((candidate) => candidate.path === worktreePath);
@@ -296,6 +301,7 @@ export class SessionWorktreeManager {
296
301
  throw new Error('Worktree path has an invalid name');
297
302
  return {
298
303
  originalCwd,
304
+ repositoryRoot: root,
299
305
  preEnterOriginalCwd: originalCwd,
300
306
  worktreePath,
301
307
  worktreeName: name,
@@ -4,6 +4,7 @@ import { type ModelProvider, type ModelToolCall, type ModelToolDefinition, type
4
4
  import { type ClaudeAgentRuntimeDefinition, type ClaudeExtensionCatalog } from '../extensions/claude-extensions.js';
5
5
  import type { ClaudeHookRunner } from '../hooks/claude-hooks.js';
6
6
  import type { ClaudeMcpRuntime } from '../mcp/claude-mcp-tools.js';
7
+ import { type DataPlane } from '../persistence/data-plane.js';
7
8
  import { type BackgroundAgentSnapshot } from './background-agent-manager.js';
8
9
  export interface WorkflowAgentRunOptions {
9
10
  sessionId: string;
@@ -47,9 +48,10 @@ export declare class StructuredOutputRegistry implements ToolRegistry {
47
48
  prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
48
49
  execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
49
50
  }
50
- export declare function agentMemoryPrompt(configRoot: string, cwd: string, definition: ClaudeAgentRuntimeDefinition | null): Promise<string | null>;
51
+ export declare function agentMemoryPrompt(configRoot: string, cwd: string, definition: ClaudeAgentRuntimeDefinition | null, dataPlane: DataPlane): Promise<string | null>;
51
52
  export interface ClaudeSubagentExecutorOptions {
52
53
  configRoot: string;
54
+ dataPlane: DataPlane;
53
55
  cwd: string;
54
56
  cwdProvider?: () => string;
55
57
  claudeVersion: string;
@@ -99,6 +101,7 @@ export declare class ClaudeSubagentExecutor {
99
101
  prepare(call: ModelToolCall, depth: number): ModelToolCall;
100
102
  execute(call: ModelToolCall, sessionId: string, depth: number, promptId: string, context: ToolExecutionContext): Promise<ToolExecutionResult>;
101
103
  private sidechainStore;
104
+ private sessionPaths;
102
105
  private createAgentWorktree;
103
106
  private createBackgroundAgentRun;
104
107
  runWorkflowAgent(options: WorkflowAgentRunOptions): Promise<WorkflowAgentRunResult>;