praxis-agent 0.20.21 → 0.21.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.
@@ -1,6 +1,6 @@
1
1
  import { randomBytes, randomUUID } from 'node:crypto';
2
- import { mkdir, readFile, readdir, stat } from 'node:fs/promises';
3
- import { join } from 'node:path';
2
+ import { lstat, mkdir, readFile, readdir } from 'node:fs/promises';
3
+ import { join, relative } from 'node:path';
4
4
  import { Ajv2020 } from 'ajv/dist/2020.js';
5
5
  import { resolveClaudePaths } from '../compatibility/claude/paths.js';
6
6
  import { workflowAgentFiles } from '../compatibility/claude/workflow.js';
@@ -22,11 +22,24 @@ import { createWorkflowWorktree } from './workflow-worktree.js';
22
22
  const DEFAULT_MAX_DEPTH = 4;
23
23
  const DEFAULT_MAX_CALLS = 16;
24
24
  const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
25
+ const SIDECHAIN_DISCOVERY_MAX_DEPTH = 4;
25
26
  const structuredOnlyTools = {
26
27
  definitions: () => [],
27
28
  prepare: async (call) => call,
28
29
  execute: async () => ({ content: '', isError: false }),
29
30
  };
31
+ async function readSidechainMetadataName(metadataFile) {
32
+ try {
33
+ const value = JSON.parse(await readFile(metadataFile, 'utf8'));
34
+ if (!value || typeof value !== 'object')
35
+ return undefined;
36
+ const name = value.name;
37
+ return typeof name === 'string' ? name : undefined;
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
30
43
  export class StructuredOutputRegistry {
31
44
  base;
32
45
  schema;
@@ -134,7 +147,7 @@ function agentToolRuleName(rule) {
134
147
  const opening = rule.indexOf('(');
135
148
  return (opening < 0 ? rule : rule.slice(0, opening)).trim();
136
149
  }
137
- function enabledAgentToolNames(base, definition, background, additiveTools = new Set()) {
150
+ function enabledAgentToolNames(base, definition, background, permissionMode, additiveTools = new Set()) {
138
151
  const requested = definition?.tools
139
152
  ? new Set(definition.tools.map(agentToolRuleName))
140
153
  : null;
@@ -150,8 +163,10 @@ function enabledAgentToolNames(base, definition, background, additiveTools = new
150
163
  .filter((name) => {
151
164
  if (additiveTools.has(name))
152
165
  return true;
153
- if (AGENT_UNAVAILABLE_TOOLS.has(name))
166
+ if (AGENT_UNAVAILABLE_TOOLS.has(name) &&
167
+ !(name === 'ExitPlanMode' && permissionMode === 'plan')) {
154
168
  return false;
169
+ }
155
170
  if (background &&
156
171
  !name.startsWith('mcp__') &&
157
172
  !BACKGROUND_AGENT_TOOLS.has(name)) {
@@ -968,25 +983,28 @@ export class ClaudeSubagentExecutor {
968
983
  cwd: this.cwd(),
969
984
  sessionId,
970
985
  });
971
- const agentId = await this.resolvePersistedAgentId(paths.projectRoot, sessionId, identifier);
972
- if (!agentId)
986
+ const sidechainPaths = await this.resolvePersistedSidechain(paths.projectRoot, sessionId, identifier);
987
+ if (!sidechainPaths)
973
988
  return;
974
- const sidechainPaths = resolveClaudeSidechainPaths(paths.projectRoot, sessionId, agentId);
989
+ const agentId = sidechainPaths.agentId;
975
990
  const sidechain = new ClaudeSidechainStore(sidechainPaths, join(paths.praxisRoot, 'locks', `${sessionId}-${agentId}.lock`), this.schema);
976
- let metadata;
977
991
  let snapshot;
978
992
  try {
979
- ;
980
- [metadata, snapshot] = await Promise.all([
981
- sidechain.metadata(),
982
- sidechain.loadReadOnly(),
983
- ]);
993
+ snapshot = await sidechain.loadReadOnly();
984
994
  }
985
995
  catch (error) {
986
996
  if (error.code === 'ENOENT')
987
997
  return;
988
998
  throw error;
989
999
  }
1000
+ let metadata = null;
1001
+ try {
1002
+ metadata = await sidechain.metadata();
1003
+ }
1004
+ catch (error) {
1005
+ if (error.code !== 'ENOENT')
1006
+ throw error;
1007
+ }
990
1008
  const root = snapshot.entries[0];
991
1009
  if (!root || root.type !== 'user') {
992
1010
  throw new Error(`Background agent ${agentId} has no sidechain root`);
@@ -1003,15 +1021,20 @@ export class ClaudeSubagentExecutor {
1003
1021
  (lastAssistant.toolCalls?.length ?? 0) > 0) {
1004
1022
  throw new Error(`Background agent ${agentId} is not completed`);
1005
1023
  }
1024
+ const agentType = metadata?.agentType ?? 'general-purpose';
1025
+ const description = metadata?.description ?? 'Recovered Claude sidechain';
1026
+ const toolUseId = metadata?.toolUseId ?? `recovered:${agentId}`;
1027
+ const spawnDepth = metadata?.spawnDepth ?? 1;
1028
+ const name = metadata?.name;
1029
+ const permissionMode = metadata?.permissionMode;
1030
+ const isolation = metadata?.isolation;
1006
1031
  const input = {
1007
- description: metadata.description,
1032
+ description,
1008
1033
  prompt,
1009
- subagentType: metadata.agentType,
1010
- ...(metadata.name ? { name: metadata.name } : {}),
1011
- ...(metadata.permissionMode
1012
- ? { permissionMode: metadata.permissionMode }
1013
- : {}),
1014
- ...(metadata.isolation ? { isolation: metadata.isolation } : {}),
1034
+ subagentType: agentType,
1035
+ ...(name ? { name } : {}),
1036
+ ...(permissionMode ? { permissionMode } : {}),
1037
+ ...(isolation ? { isolation } : {}),
1015
1038
  runInBackground: true,
1016
1039
  };
1017
1040
  const provider = this.options.provider;
@@ -1024,9 +1047,9 @@ export class ClaudeSubagentExecutor {
1024
1047
  input,
1025
1048
  provider,
1026
1049
  agentId,
1027
- spawnDepth: metadata.spawnDepth,
1050
+ spawnDepth,
1028
1051
  promptId: String(root.promptId ?? randomUUID()),
1029
- toolUseId: metadata.toolUseId,
1052
+ toolUseId,
1030
1053
  transcriptPath: sidechainPaths.transcriptFile,
1031
1054
  toolResultDirectory: join(paths.projectRoot, sessionId, 'tool-results'),
1032
1055
  ...(continuation ? { continuationMessage: message } : {}),
@@ -1036,11 +1059,11 @@ export class ClaudeSubagentExecutor {
1036
1059
  });
1037
1060
  this.background.registerCompleted({
1038
1061
  agentId,
1039
- ...(metadata.name ? { name: metadata.name } : {}),
1040
- agentType: metadata.agentType,
1041
- description: metadata.description,
1062
+ ...(name ? { name } : {}),
1063
+ agentType,
1064
+ description,
1042
1065
  prompt,
1043
- toolUseId: metadata.toolUseId,
1066
+ toolUseId,
1044
1067
  outputFile: sidechainPaths.transcriptFile,
1045
1068
  resolvedModel: provider.model ?? 'praxis/provider',
1046
1069
  run,
@@ -1051,37 +1074,110 @@ export class ClaudeSubagentExecutor {
1051
1074
  durationMs: 0,
1052
1075
  });
1053
1076
  }
1054
- async resolvePersistedAgentId(projectRoot, sessionId, identifier) {
1055
- if (/^a[0-9a-f]{16}$/u.test(identifier))
1056
- return identifier;
1057
- if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(identifier))
1077
+ async resolvePersistedSidechain(projectRoot, sessionId, identifier) {
1078
+ const isAgentId = /^a[0-9a-f]{16}$/u.test(identifier);
1079
+ const isName = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(identifier);
1080
+ if (!isAgentId && !isName)
1058
1081
  return null;
1059
- const directory = join(projectRoot, sessionId, 'subagents');
1060
- let names;
1082
+ const candidates = await this.discoverSidechainCandidates(projectRoot, sessionId);
1083
+ const matches = candidates.filter((candidate) => isAgentId
1084
+ ? candidate.agentId === identifier
1085
+ : candidate.name === identifier);
1086
+ matches.sort((left, right) => (right.metadataModifiedAt ?? -Infinity) -
1087
+ (left.metadataModifiedAt ?? -Infinity) ||
1088
+ left.agentId.localeCompare(right.agentId) ||
1089
+ left.paths.directory.localeCompare(right.paths.directory));
1090
+ return matches[0]?.paths ?? null;
1091
+ }
1092
+ async discoverSidechainCandidates(projectRoot, sessionId) {
1093
+ const rootDirectory = join(projectRoot, sessionId, 'subagents');
1094
+ let rootStat;
1061
1095
  try {
1062
- names = await readdir(directory);
1096
+ rootStat = await lstat(rootDirectory);
1063
1097
  }
1064
1098
  catch (error) {
1065
1099
  if (error.code === 'ENOENT')
1066
- return null;
1100
+ return [];
1067
1101
  throw error;
1068
1102
  }
1069
- const matches = await Promise.all(names
1070
- .map((name) => /^agent-(a[0-9a-f]{16})\.meta\.json$/u.exec(name)?.[1])
1071
- .filter((agentId) => agentId !== undefined)
1072
- .map(async (agentId) => {
1073
- const sidechainPaths = resolveClaudeSidechainPaths(projectRoot, sessionId, agentId);
1074
- const sidechain = new ClaudeSidechainStore(sidechainPaths, join(this.options.configRoot, 'praxis', 'locks', `${sessionId}-${agentId}.lock`), this.schema);
1075
- const metadata = await sidechain.metadata();
1076
- if (metadata.name !== identifier)
1077
- return null;
1078
- const file = await stat(sidechainPaths.metadataFile);
1079
- return { agentId, modifiedAt: file.mtimeMs };
1080
- }));
1081
- return (matches
1082
- .filter((match) => match !== null)
1083
- .sort((left, right) => right.modifiedAt - left.modifiedAt ||
1084
- right.agentId.localeCompare(left.agentId))[0]?.agentId ?? null);
1103
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory())
1104
+ return [];
1105
+ const candidates = [];
1106
+ const walk = async (directory, depth) => {
1107
+ if (depth > SIDECHAIN_DISCOVERY_MAX_DEPTH)
1108
+ return;
1109
+ let entries;
1110
+ try {
1111
+ entries = await readdir(directory, { withFileTypes: true });
1112
+ }
1113
+ catch (error) {
1114
+ if (error.code === 'ENOENT')
1115
+ return;
1116
+ throw error;
1117
+ }
1118
+ const byName = new Map(entries.map((entry) => [entry.name, entry]));
1119
+ const subdirectory = relative(rootDirectory, directory).replaceAll('\\', '/');
1120
+ await Promise.all(entries.map(async (entry) => {
1121
+ if (entry.isSymbolicLink())
1122
+ return;
1123
+ const entryPath = join(directory, entry.name);
1124
+ if (entry.isDirectory()) {
1125
+ await walk(entryPath, depth + 1);
1126
+ return;
1127
+ }
1128
+ if (!entry.isFile())
1129
+ return;
1130
+ const metadataMatch = /^agent-(a[0-9a-f]{16})\.meta\.json$/u.exec(entry.name);
1131
+ if (metadataMatch) {
1132
+ const agentId = metadataMatch[1];
1133
+ if (agentId === undefined)
1134
+ return;
1135
+ const transcriptEntry = byName.get(`agent-${agentId}.jsonl`);
1136
+ if (!transcriptEntry ||
1137
+ transcriptEntry.isSymbolicLink() ||
1138
+ !transcriptEntry.isFile()) {
1139
+ return;
1140
+ }
1141
+ const paths = resolveClaudeSidechainPaths(projectRoot, sessionId, agentId, subdirectory === '' ? {} : { subdirectory });
1142
+ let metadataStat;
1143
+ try {
1144
+ metadataStat = await lstat(paths.metadataFile);
1145
+ }
1146
+ catch (error) {
1147
+ if (error.code === 'ENOENT')
1148
+ return;
1149
+ throw error;
1150
+ }
1151
+ if (metadataStat.isSymbolicLink() || !metadataStat.isFile())
1152
+ return;
1153
+ const name = await readSidechainMetadataName(paths.metadataFile);
1154
+ candidates.push({
1155
+ agentId,
1156
+ paths,
1157
+ metadataModifiedAt: metadataStat.mtimeMs,
1158
+ ...(name === undefined ? {} : { name }),
1159
+ });
1160
+ return;
1161
+ }
1162
+ const transcriptMatch = /^agent-(a[0-9a-f]{16})\.jsonl$/u.exec(entry.name);
1163
+ if (!transcriptMatch)
1164
+ return;
1165
+ const agentId = transcriptMatch[1];
1166
+ if (agentId === undefined)
1167
+ return;
1168
+ // A metadata companion (even an invalid one) is owned by the
1169
+ // metadata branch; only a bare transcript without metadata is a
1170
+ // legacy candidate.
1171
+ if (byName.has(`agent-${agentId}.meta.json`))
1172
+ return;
1173
+ candidates.push({
1174
+ agentId,
1175
+ paths: resolveClaudeSidechainPaths(projectRoot, sessionId, agentId, subdirectory === '' ? {} : { subdirectory }),
1176
+ });
1177
+ }));
1178
+ };
1179
+ await walk(rootDirectory, 0);
1180
+ return candidates;
1085
1181
  }
1086
1182
  asyncLaunchResult(options) {
1087
1183
  return [
@@ -1181,7 +1277,7 @@ export class ClaudeSubagentExecutor {
1181
1277
  .definitions()
1182
1278
  .map(({ name }) => name)
1183
1279
  .filter((name) => !inheritedToolNames.has(name)));
1184
- const agentScopedTools = new RestrictedToolRegistry(agentToolBase, enabledAgentToolNames(agentToolBase, customAgent, options.input.runInBackground, additiveAgentToolNames));
1280
+ const agentScopedTools = new RestrictedToolRegistry(agentToolBase, enabledAgentToolNames(agentToolBase, customAgent, options.input.runInBackground, options.input.permissionMode, additiveAgentToolNames));
1185
1281
  const builtInStatusLineAgent = customAgent?.path === BUILTIN_STATUSLINE_AGENT_PATH;
1186
1282
  const scopedTools = builtInStatusLineAgent
1187
1283
  ? new RestrictedToolRegistry(agentScopedTools, ['Read', 'Edit'])
@@ -11,7 +11,7 @@ import { permissionRuleValueToString } from '../permissions/permission-updates.j
11
11
  import { AgentRunCancelledError } from '../core/runtime.js';
12
12
  import { claudePermissionActionKey, claudePermissionRuleMatches, } from '../permissions/claude-permission-resolver.js';
13
13
  import { redactSensitiveText, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
14
- import { CommandPalette, BtwPanel, Composer, DiffDashboard, DialogFrame, ExternalEditorWait, HelpMenu, HookDashboard, ListDashboard, MemoryDashboard, MentionPicker, ModelMenu, PermissionDashboard, SelectionMenu, SessionPicker, ThemePicker, CustomThemeEditor, Transcript, WelcomePanel, useTerminalRows, useTerminalWidth, } from './tui/claude-style.js';
14
+ import { CommandPalette, BtwPanel, Composer, DiffDashboard, DialogFrame, ExternalEditorWait, HelpMenu, HookDashboard, ListDashboard, MemoryDashboard, MentionPicker, ModelMenu, PermissionDashboard, SelectionMenu, SessionPicker, ThemePicker, CustomThemeEditor, SessionIdentity, Transcript, WelcomePanel, useTerminalRows, useTerminalWidth, } from './tui/claude-style.js';
15
15
  import { loadTuiMemoryFiles, openTuiMemoryFolder, } from './tui/memory-files.js';
16
16
  import { loadClaudeReleaseNotes } from './tui/release-notes.js';
17
17
  import { createClaudeStatusLineInput, StatusLine } from './tui/status-line.js';
@@ -417,9 +417,21 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
417
417
  const [history, setHistory] = useState([...initialHistory]);
418
418
  // Startup diagnostics are useful before the first prompt, but they are not
419
419
  // conversation history and must not suppress the new-session welcome panel.
420
- const hasConversationHistory = history.some((item) => item.kind !== 'notice' &&
421
- item.kind !== 'warning' &&
422
- item.kind !== 'local-result');
420
+ // Only real user/assistant transcript entries start a conversation; every
421
+ // other kind (thinking, context, tool, shell, notices, results, and so on)
422
+ // is operational bookkeeping that must not hide the fresh-session welcome.
423
+ const isRealConversation = (item) => item.kind === 'user' || item.kind === 'assistant';
424
+ // The original loaded transcript decides whether the session was resumed,
425
+ // separately from the live history that grows while the session runs.
426
+ const resumedWithTranscript = initialHistory.some(isRealConversation);
427
+ const hasConversationHistory = history.some(isRealConversation);
428
+ // A session is resumed only when it was opened through `resume` and the
429
+ // original transcript already contained real conversation content. Supplying
430
+ // a session ID alone with an empty transcript keeps the session fresh, so the
431
+ // full welcome panel renders and the compact identity stays hidden until real
432
+ // conversation content appears.
433
+ const resumed = resume !== undefined && resumedWithTranscript;
434
+ const freshSession = !resumed && !hasConversationHistory;
423
435
  const sessionLoadRef = useRef(0);
424
436
  const [turnDiffs, setTurnDiffs] = useState([]);
425
437
  const turnNumberRef = useRef(0);
@@ -5635,7 +5647,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
5635
5647
  });
5636
5648
  return (_jsx(TuiThemeProvider, { settings: themeSettings, children: _jsx(Box, { flexDirection: "column", ...(!fixedViewport
5637
5649
  ? {}
5638
- : { height: rows, overflowY: 'hidden' }), children: selectingSession ? (_jsx(SessionPicker, { sessions: filteredPickerChoices, selectedIndex: selectedIndex, screenReader: axScreenReader, query: sessionSearch })) : (_jsxs(_Fragment, { children: [!axScreenReader && !hasConversationHistory && !sessionId ? (_jsx(WelcomePanel, { display: runtimeDisplay, width: width, showTips: runtimeSettings.tips })) : null, sessionId ? (_jsxs(Text, { dimColor: true, children: ["Session ", sessionId.slice(0, 8)] })) : null, _jsx(Transcript, { items: history, activeText: activeText, activeThinking: activeThinking, thinkingExpanded: thinkingExpanded, detailedTranscript: thinkingExpanded || runtimeSettings.verbose, screenReader: axScreenReader }), externalEditorRequest !== null ||
5650
+ : { height: rows, overflowY: 'hidden' }), children: selectingSession ? (_jsx(SessionPicker, { sessions: filteredPickerChoices, selectedIndex: selectedIndex, screenReader: axScreenReader, query: sessionSearch })) : (_jsxs(_Fragment, { children: [!axScreenReader && freshSession ? (_jsx(WelcomePanel, { display: runtimeDisplay, width: width, showTips: runtimeSettings.tips })) : null, sessionId ? (_jsxs(Text, { dimColor: true, children: ["Session ", sessionId.slice(0, 8)] })) : null, !axScreenReader && !resumed && hasConversationHistory ? (_jsx(SessionIdentity, { display: runtimeDisplay, width: width })) : null, _jsx(Transcript, { items: history, activeText: activeText, activeThinking: activeThinking, thinkingExpanded: thinkingExpanded, detailedTranscript: thinkingExpanded || runtimeSettings.verbose, screenReader: axScreenReader }), externalEditorRequest !== null ||
5639
5651
  keybindingsEditing ||
5640
5652
  memoryEditorRequest !== null ? (_jsx(ExternalEditorWait, { screenReader: axScreenReader })) : permission ? (permission.kind === 'tool' && toolPermissionModel ? (_jsx(ToolPermissionDialog, { model: toolPermissionModel, selection: permissionSelection, feedbackMode: permissionFeedbackMode, feedback: input, ruleEditor: permissionRuleEditor, screenReader: axScreenReader })) : (_jsxs(DialogFrame, { title: `Retry interrupted ${permission.call.name}?`, screenReader: axScreenReader, children: [_jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: _jsx(Text, { bold: true, children: describeTool(permission.call, sensitiveValues) }) }), _jsx(Text, { children: "Do you want to proceed?" }), _jsxs(Text, { inverse: !axScreenReader && permissionSelection === 0, children: [selectionPrefix(permissionSelection === 0, axScreenReader), "1. Yes"] }), _jsxs(Text, { inverse: !axScreenReader && permissionSelection === 1, children: [selectionPrefix(permissionSelection === 1, axScreenReader), "2. No"] }), permissionFeedbackMode ? (_jsxs(Text, { children: ["\u203A", ' ', input ||
5641
5653
  (permissionSelection === 0
@@ -77,6 +77,10 @@ export declare function WelcomePanel({ display, width, showTips, }: {
77
77
  width: number;
78
78
  showTips: boolean;
79
79
  }): import("react").JSX.Element | null;
80
+ export declare function SessionIdentity({ display, width, }: {
81
+ display: TuiDisplayMetadata;
82
+ width: number;
83
+ }): import("react").JSX.Element;
80
84
  export declare function MarkdownText({ text }: {
81
85
  text: string;
82
86
  }): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
@@ -125,6 +125,17 @@ export function WelcomePanel({ display, width, showTips, }) {
125
125
  const rightWidth = Math.max(12, panelWidth - leftWidth - 4);
126
126
  return (_jsxs(Box, { flexDirection: "column", width: panelWidth, children: [_jsxs(Text, { color: palette.muted, children: ['╭───', _jsx(Text, { color: palette.brand, bold: true, children: brand }), ` Code v${display.version} `, _jsx(Text, { dimColor: true, children: '─'.repeat(fill) }), '╮'] }), _jsxs(Box, { borderStyle: "round", borderColor: palette.muted, borderTop: false, flexDirection: wide ? 'row' : 'column', width: panelWidth, paddingX: 1, children: [_jsxs(Box, { alignItems: wide ? 'center' : undefined, flexDirection: "column", width: wide ? leftWidth : '100%', children: [_jsx(Text, { children: " " }), _jsxs(Text, { bold: true, children: ["Welcome to ", brand] }), _jsx(Text, { children: " " }), _jsx(Text, { color: palette.brand, bold: true, children: "\u2590\u259B\u2588\u2588\u2588\u259C\u258C" }), _jsx(Text, { color: palette.brand, children: "\u259D\u259C\u2588\u2588\u2588\u2588\u2588\u259B\u2598" }), _jsx(Text, { color: palette.brand, children: " \u2598\u2598 \u259D\u259D" }), _jsx(Text, { children: " " }), _jsxs(Text, { wrap: "truncate-end", children: [model, effort] }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: cwd })] }), _jsxs(Box, { flexDirection: "column", width: wide ? rightWidth : '100%', marginTop: wide ? 0 : 1, children: [_jsx(Text, { bold: true, children: "Get started" }), _jsx(Text, { wrap: "truncate-end", children: "/init to create CLAUDE.md" }), _jsx(Text, { wrap: "truncate-end", children: "/config to open settings" }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { bold: true, children: "Shared with Claude Code" }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: "Sessions \u00B7 memory \u00B7 skills" })] })] })] }));
127
127
  }
128
+ // Compact identity header shown above the transcript once a fresh session has
129
+ // conversation content. Keeps the same product/model/cwd facts as WelcomePanel
130
+ // in a small fixed footprint, truncating every line to the supplied width.
131
+ export function SessionIdentity({ display, width, }) {
132
+ const palette = useTuiPalette();
133
+ const identityWidth = Math.max(1, Math.floor(width));
134
+ const model = display.model ?? 'provider default';
135
+ const effort = display.effort ? ` · ${display.effort} effort` : '';
136
+ const cwd = compactPath(display.cwd);
137
+ return (_jsxs(Box, { flexDirection: "column", width: identityWidth, children: [_jsx(Text, { wrap: "truncate-end", children: _jsx(Text, { color: palette.brand, bold: true, children: `Praxis Code v${display.version}` }) }), _jsxs(Text, { wrap: "truncate-end", children: [model, effort] }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: cwd })] }));
138
+ }
128
139
  const INLINE_SEGMENT_CACHE_MAX = 4096;
129
140
  const inlineSegmentCache = new Map();
130
141
  function cachedInlineSegments(text) {
@@ -720,7 +720,9 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
720
720
  const claudeStatePath = requestedConfigRoot || configuredRoot
721
721
  ? join(configRoot, '.claude.json')
722
722
  : resolve(homedir(), '.claude.json');
723
- const runtimeSettings = controls.safeMode || controls.bare
723
+ const simpleMode = controls.bare ||
724
+ /^(?:1|true|yes|on)$/iu.test((runtimeEnvironment.CLAUDE_CODE_SIMPLE ?? '').trim());
725
+ const runtimeSettings = controls.safeMode || simpleMode
724
726
  ? undefined
725
727
  : await loadRuntimeSettings({ configRoot, statePath: claudeStatePath });
726
728
  const runtimeSettingsPrompt = runtimeSettingsSystemPrompt(runtimeSettings);
@@ -799,6 +801,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
799
801
  eventSink: runtimeEventSink,
800
802
  sessionPersistence: cli.sessionPersistence,
801
803
  costStateStore,
804
+ simpleMode,
802
805
  explicitModel: interactiveModel !== undefined || controls.model !== undefined,
803
806
  explicitSystemPrompt: cli.systemPrompt !== undefined,
804
807
  agentInitialPromptHandledExternally: interactive,
@@ -843,7 +846,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
843
846
  loadClaudeSettings({
844
847
  configRoot,
845
848
  cwd,
846
- ...(cli.bare
849
+ ...(simpleMode
847
850
  ? { settingSources: [] }
848
851
  : cli.settingSources === undefined
849
852
  ? {}
@@ -855,7 +858,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
855
858
  pluginDirectories: cli.pluginDirectories,
856
859
  pluginUrls: cli.pluginUrls,
857
860
  strictPluginDirectories: cli.pluginDirectories.length + cli.pluginUrls.length > 0,
858
- loadInstalled: !cli.safeMode && !cli.bare,
861
+ loadInstalled: !cli.safeMode && !simpleMode,
859
862
  readOnlyHooks: true,
860
863
  environment: runtimeEnvironment,
861
864
  }),
@@ -903,12 +906,12 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
903
906
  complete: (request) => toolProvider.complete(request),
904
907
  }
905
908
  : toolProvider;
906
- const automaticSettingSources = cli.safeMode || cli.bare ? [] : cli.settingSources;
909
+ const automaticSettingSources = cli.safeMode || simpleMode ? [] : cli.settingSources;
907
910
  const settings = [
908
911
  ...(await loadClaudeSettings({
909
912
  configRoot,
910
913
  cwd,
911
- ...(cli.bare
914
+ ...(simpleMode
912
915
  ? { settingSources: [] }
913
916
  : cli.settingSources === undefined
914
917
  ? {}
@@ -930,7 +933,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
930
933
  pluginDirectories: cli.pluginDirectories,
931
934
  pluginUrls: cli.pluginUrls,
932
935
  strictPluginDirectories: cli.pluginDirectories.length + cli.pluginUrls.length > 0,
933
- loadInstalled: !cli.safeMode && !cli.bare,
936
+ loadInstalled: !cli.safeMode && !simpleMode,
934
937
  environment: runtimeEnvironment,
935
938
  });
936
939
  for (const plugin of pluginResources.plugins) {
@@ -972,7 +975,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
972
975
  const extensions = new ClaudeExtensionCatalog(resources, {
973
976
  disableSlashCommands: cli.disableSlashCommands,
974
977
  });
975
- const memoryDirectory = cli.safeMode || cli.bare
978
+ const memoryDirectory = cli.safeMode || simpleMode
976
979
  ? undefined
977
980
  : await resolveClaudeProjectMemoryDirectory({ configRoot, cwd });
978
981
  if (memoryDirectory)
@@ -1056,7 +1059,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1056
1059
  return filterDisabledMcpResources(candidates, await management.disabled());
1057
1060
  };
1058
1061
  const mcpTools = await ClaudeMcpToolRegistry.connect({
1059
- base: cli.bare
1062
+ base: simpleMode
1060
1063
  ? localTools
1061
1064
  : new WebToolRegistry({
1062
1065
  base: localTools,
@@ -1115,7 +1118,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1115
1118
  const extensionTools = new ClaudeExtensionToolRegistry(mcpTools, extensions);
1116
1119
  const lspEnabled = interactive &&
1117
1120
  !cli.safeMode &&
1118
- !cli.bare &&
1121
+ !simpleMode &&
1119
1122
  pluginResources.lsp.length > 0;
1120
1123
  lspTools = lspEnabled
1121
1124
  ? new ClaudeLspToolManager({
@@ -1159,24 +1162,24 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1159
1162
  cli.tools.includes(name)) &&
1160
1163
  !cli.disallowedTools.includes(name));
1161
1164
  const selectedTaskTools = taskToolNames.filter((name) => cli.sessionPersistence &&
1162
- !cli.bare &&
1165
+ !simpleMode &&
1163
1166
  (cli.tools === undefined ||
1164
1167
  cli.tools.includes('default') ||
1165
1168
  cli.tools.includes(name)) &&
1166
1169
  !cli.disallowedTools.includes(name));
1167
- const selectedScheduledTools = scheduledToolNames.filter((name) => !cli.bare &&
1170
+ const selectedScheduledTools = scheduledToolNames.filter((name) => !simpleMode &&
1168
1171
  (cli.tools === undefined ||
1169
1172
  cli.tools.includes('default') ||
1170
1173
  cli.tools.includes(name)) &&
1171
1174
  !cli.disallowedTools.includes(name));
1172
1175
  const selectedWorkflowTools = workflowToolNames.filter((name) => (runtimeSettings?.workflows ?? true) &&
1173
1176
  cli.sessionPersistence &&
1174
- !cli.bare &&
1177
+ !simpleMode &&
1175
1178
  (cli.tools === undefined ||
1176
1179
  cli.tools.includes('default') ||
1177
1180
  cli.tools.includes(name)) &&
1178
1181
  !cli.disallowedTools.includes(name));
1179
- const selectedWorktreeTools = worktreeToolNames.filter((name) => !cli.bare &&
1182
+ const selectedWorktreeTools = worktreeToolNames.filter((name) => !simpleMode &&
1180
1183
  (cli.tools === undefined ||
1181
1184
  cli.tools.includes('default') ||
1182
1185
  cli.tools.includes(name)) &&
@@ -1187,7 +1190,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1187
1190
  cli.tools.includes(name)) &&
1188
1191
  !cli.disallowedTools.includes(name));
1189
1192
  const enableBackgroundBash = cli.sessionPersistence &&
1190
- !cli.bare &&
1193
+ !simpleMode &&
1191
1194
  (cli.tools === undefined ||
1192
1195
  cli.tools.includes('default') ||
1193
1196
  cli.tools.includes('Bash')) &&
@@ -1206,17 +1209,17 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1206
1209
  !workflowToolNames.includes(name) &&
1207
1210
  !worktreeToolNames.includes(name) &&
1208
1211
  !interactiveToolNames.includes(name) &&
1209
- (!cli.bare || (name !== 'WebFetch' && name !== 'WebSearch')));
1212
+ (!simpleMode || (name !== 'WebFetch' && name !== 'WebSearch')));
1210
1213
  const filteredTools = new FilteredToolRegistry(extensionAndLspTools, {
1211
1214
  ...(cli.tools === undefined
1212
- ? cli.bare
1215
+ ? simpleMode
1213
1216
  ? { tools: ['Bash', 'Edit', 'Read'] }
1214
1217
  : {}
1215
1218
  : { tools: selectedBaseTools ?? [] }),
1216
1219
  disallowedTools: cli.disallowedTools,
1217
1220
  });
1218
- const enableSubagents = !cli.bare && selectedAgentTools.length > 0;
1219
- const hooks = cli.safeMode || cli.bare
1221
+ const enableSubagents = !simpleMode && selectedAgentTools.length > 0;
1222
+ const hooks = cli.safeMode || simpleMode
1220
1223
  ? undefined
1221
1224
  : new ClaudeHookRunner({
1222
1225
  settings,
@@ -1243,6 +1246,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1243
1246
  ...(providerForModel ? { providerForModel } : {}),
1244
1247
  ...(providerForMainModel ? { providerForMainModel } : {}),
1245
1248
  tools: filteredTools,
1249
+ toolCapabilityEnvironment: runtimeEnvironment,
1246
1250
  mcp: mcpTools,
1247
1251
  permissions,
1248
1252
  permissionResolverForMode,
@@ -11,6 +11,13 @@ export interface ClaudeSchemaAdapter {
11
11
  serializeForSidechainAppend(entry: ClaudeTranscriptEntry): string;
12
12
  serializeForFork(entry: ClaudeTranscriptEntry): string;
13
13
  }
14
+ /**
15
+ * Historical fixture/default provenance constant for the Claude Code
16
+ * transcript schema. It records the version Praxis fixtures and default
17
+ * translation paths were captured against; it is not an append gate. Any
18
+ * nonempty semver-like Claude Code version whose entry shape is structurally
19
+ * supported is writable through the append/sidechain/fork serializers.
20
+ */
14
21
  export declare const VERIFIED_CLAUDE_SCHEMA_VERSION = "2.1.208";
15
22
  export declare function isClaudeForkableEntryType(type: string): boolean;
16
23
  export declare function copyClaudeEntryWithSessionId(entry: ClaudeTranscriptEntry, sessionId: string): ClaudeTranscriptEntry;
@@ -1,5 +1,16 @@
1
1
  import { isAgentColorValue } from './agent-color.js';
2
+ /**
3
+ * Historical fixture/default provenance constant for the Claude Code
4
+ * transcript schema. It records the version Praxis fixtures and default
5
+ * translation paths were captured against; it is not an append gate. Any
6
+ * nonempty semver-like Claude Code version whose entry shape is structurally
7
+ * supported is writable through the append/sidechain/fork serializers.
8
+ */
2
9
  export const VERIFIED_CLAUDE_SCHEMA_VERSION = '2.1.208';
10
+ const CLAUDE_CODE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
11
+ function isClaudeCodeVersion(version) {
12
+ return CLAUDE_CODE_VERSION_PATTERN.test(version);
13
+ }
3
14
  const APPENDABLE_ENTRY_TYPES = new Set([
4
15
  'agent-color',
5
16
  'agent-name',
@@ -776,9 +787,8 @@ function validateAppendableEntry(entry) {
776
787
  throw new Error(`Claude transcript entry is missing ${field}`);
777
788
  }
778
789
  }
779
- if (entry.version !== VERIFIED_CLAUDE_SCHEMA_VERSION) {
780
- throw new Error(`Claude transcript append must target Claude Code ${VERIFIED_CLAUDE_SCHEMA_VERSION}`);
781
- }
790
+ // version is producer provenance: it must be present and nonempty (enforced
791
+ // above) but is not compared against a single verified writer version.
782
792
  if (!('parentUuid' in entry) ||
783
793
  (entry.parentUuid !== null && !isNonEmptyString(entry.parentUuid))) {
784
794
  throw new Error('Claude transcript entry has invalid parentUuid');
@@ -837,9 +847,8 @@ function validateSidechainEntry(entry) {
837
847
  throw new Error(`Claude sidechain entry is missing ${field}`);
838
848
  }
839
849
  }
840
- if (entry.version !== VERIFIED_CLAUDE_SCHEMA_VERSION) {
841
- throw new Error(`Claude sidechain append must target Claude Code ${VERIFIED_CLAUDE_SCHEMA_VERSION}`);
842
- }
850
+ // version is producer provenance: it must be present and nonempty (enforced
851
+ // above) but is not compared against a single verified writer version.
843
852
  if (!('parentUuid' in entry) ||
844
853
  (entry.parentUuid !== null && !isNonEmptyString(entry.parentUuid))) {
845
854
  throw new Error('Claude sidechain entry has invalid parentUuid');
@@ -971,9 +980,12 @@ function validateForkableEntry(entry) {
971
980
  else
972
981
  validateForkAssistantMessage(entry.message);
973
982
  }
974
- class ClaudeCode21208Adapter {
975
- version = VERIFIED_CLAUDE_SCHEMA_VERSION;
983
+ class ClaudeCodeWritableAdapter {
984
+ version;
976
985
  writeMode = 'read-write';
986
+ constructor(version) {
987
+ this.version = version;
988
+ }
977
989
  parse(line) {
978
990
  return parseEntry(line);
979
991
  }
@@ -1022,8 +1034,8 @@ class ReadOnlyClaudeAdapter {
1022
1034
  }
1023
1035
  }
1024
1036
  export function selectClaudeSchemaAdapter(version) {
1025
- if (version === VERIFIED_CLAUDE_SCHEMA_VERSION) {
1026
- return new ClaudeCode21208Adapter();
1037
+ if (isClaudeCodeVersion(version)) {
1038
+ return new ClaudeCodeWritableAdapter(version);
1027
1039
  }
1028
1040
  return new ReadOnlyClaudeAdapter(version);
1029
1041
  }
@@ -7,6 +7,10 @@ export interface ClaudeSidechainPaths {
7
7
  transcriptFile: string;
8
8
  metadataFile: string;
9
9
  }
10
+ export interface ClaudeSidechainPathOptions {
11
+ /** Bounded relative subdirectory under `<sessionId>/subagents` (e.g. `workflows/<runId>`). */
12
+ subdirectory?: string;
13
+ }
10
14
  export interface ClaudeSidechainMetadata {
11
15
  agentType: string;
12
16
  description: string;
@@ -17,7 +21,7 @@ export interface ClaudeSidechainMetadata {
17
21
  isolation?: 'worktree';
18
22
  }
19
23
  export type ClaudeSidechainPermissionMode = 'acceptEdits' | 'auto' | 'bypassPermissions' | 'default' | 'dontAsk' | 'plan';
20
- export declare function resolveClaudeSidechainPaths(projectRoot: string, sessionId: string, agentId: string): ClaudeSidechainPaths;
24
+ export declare function resolveClaudeSidechainPaths(projectRoot: string, sessionId: string, agentId: string, options?: ClaudeSidechainPathOptions): ClaudeSidechainPaths;
21
25
  export declare function createClaudeSidechainRoot(options: {
22
26
  sessionId: string;
23
27
  promptId: string;