praxis-agent 0.20.1 → 0.20.2

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.
package/README.md CHANGED
@@ -168,9 +168,20 @@ Praxis ──────┘
168
168
  ```
169
169
 
170
170
  Praxis can resume Claude Code sessions, and Claude Code can resume compatible
171
- sessions written by Praxis. The validated read-write target is Claude Code
172
- 2.1.208; unknown versions fail closed for transcript writes while retaining
173
- read-only inspection and export paths.
171
+ sessions written by Praxis. Ordinary Praxis session runtime always emits the
172
+ verified Claude Code 2.1.208 write profile and never derives it from an
173
+ installed Claude version; this fixed profile governs generated append and
174
+ sidechain writes. Native fork creation is a separate restricted lossless copy
175
+ path that preserves each existing source record's producer version, so it can
176
+ copy specific black-box-verified foreign shapes such as the observed Claude
177
+ Code 2.1.233 records; unsupported record shapes and unverified versions still
178
+ fail closed and remain read-only. Maintainers can prove mixed-version Claude JSONL interoperability with
179
+ `npm run test:cross-version-session-compat`, `test:cross-version-fork-compat`,
180
+ `test:cross-version-sidechain-compat`, `test:cross-version-compaction-compat`,
181
+ and `test:cross-version-resume-at-compat`, covering linear resume, native fork,
182
+ foreground sidechain, compaction, and `--resume-session-at` branch projection.
183
+ Each requires `PRAXIS_CLAUDE_BINARY` (Claude Code 2.1.208) and
184
+ `PRAXIS_CLAUDE_CROSS_VERSION_BINARY` (a different Claude Code version).
174
185
 
175
186
  See the
176
187
  [compatibility contract](https://github.com/Forest-Isle/Praxis/blob/main/docs/COMPATIBILITY.md)
@@ -72,9 +72,8 @@ export declare const CLAUDE_2_1_208_COMMAND_INVENTORY: readonly [{
72
72
  readonly visibility: "visible";
73
73
  }, {
74
74
  readonly name: "doctor";
75
- readonly disposition: "required";
75
+ readonly disposition: "included";
76
76
  readonly visibility: "conditional";
77
- readonly reason: "Interactive /doctor is required; the top-level command is not a substitute.";
78
77
  }, {
79
78
  readonly name: "effort";
80
79
  readonly disposition: "included";
@@ -254,9 +253,8 @@ export declare const CLAUDE_2_1_208_COMMAND_INVENTORY: readonly [{
254
253
  readonly reason: "Claude subscription rate-limit purchasing is outside the authentication boundary.";
255
254
  }, {
256
255
  readonly name: "usage";
257
- readonly disposition: "excluded";
256
+ readonly disposition: "included";
258
257
  readonly visibility: "conditional";
259
- readonly reason: "The source command is the Claude subscription plan-usage panel.";
260
258
  }, {
261
259
  readonly name: "insights";
262
260
  readonly disposition: "deferred";
@@ -38,12 +38,7 @@ export const CLAUDE_2_1_208_COMMAND_INVENTORY = [
38
38
  reason: 'The dedicated /cost contract is included; /status is not a substitute.',
39
39
  },
40
40
  { name: 'diff', disposition: 'included', visibility: 'visible' },
41
- {
42
- name: 'doctor',
43
- disposition: 'required',
44
- visibility: 'conditional',
45
- reason: 'Interactive /doctor is required; the top-level command is not a substitute.',
46
- },
41
+ { name: 'doctor', disposition: 'included', visibility: 'conditional' },
47
42
  { name: 'effort', disposition: 'included', visibility: 'visible' },
48
43
  { name: 'exit', disposition: 'included', visibility: 'visible' },
49
44
  {
@@ -173,12 +168,7 @@ export const CLAUDE_2_1_208_COMMAND_INVENTORY = [
173
168
  visibility: 'hidden',
174
169
  reason: 'Claude subscription rate-limit purchasing is outside the authentication boundary.',
175
170
  },
176
- {
177
- name: 'usage',
178
- disposition: 'excluded',
179
- visibility: 'conditional',
180
- reason: 'The source command is the Claude subscription plan-usage panel.',
181
- },
171
+ { name: 'usage', disposition: 'included', visibility: 'conditional' },
182
172
  {
183
173
  name: 'insights',
184
174
  disposition: 'deferred',
@@ -48,6 +48,7 @@ function validateNativeHistory(entries) {
48
48
  }
49
49
  const childParentUuids = new Set();
50
50
  const externalParentUuids = new Set();
51
+ let externalParentReferenceCount = 0;
51
52
  const completedToolCalls = new Set();
52
53
  let ordinaryRootCount = 0;
53
54
  let compactBoundary;
@@ -96,6 +97,7 @@ function validateNativeHistory(entries) {
96
97
  }
97
98
  else {
98
99
  externalParentUuids.add(entry.parentUuid);
100
+ externalParentReferenceCount += 1;
99
101
  }
100
102
  }
101
103
  }
@@ -120,7 +122,15 @@ function validateNativeHistory(entries) {
120
122
  if (compactBoundary) {
121
123
  throw new Error('Claude compact boundary has no adjacent summary');
122
124
  }
123
- if (externalParentUuids.size > 0 || ordinaryRootCount > 1) {
125
+ const omittedSeedUserParent = externalParentUuids.size === 1 ? [...externalParentUuids][0] : undefined;
126
+ const firstEntry = entries[0];
127
+ const isOmittedSeedUserChain = externalParentUuids.size === 1 &&
128
+ ordinaryRootCount === 0 &&
129
+ externalParentReferenceCount === 1 &&
130
+ firstEntry?.type === 'assistant' &&
131
+ firstEntry?.parentUuid === omittedSeedUserParent;
132
+ if ((externalParentUuids.size > 0 && !isOmittedSeedUserChain) ||
133
+ ordinaryRootCount > 1) {
124
134
  throw new Error('Claude fork source has a dangling parentUuid');
125
135
  }
126
136
  const states = new Map();
@@ -69,6 +69,7 @@ const FORKABLE_ATTACHMENT_TYPES = new Set([
69
69
  'read_truncation_notice',
70
70
  'skill_listing',
71
71
  'task_reminder',
72
+ 'total_tokens_reminder',
72
73
  ]);
73
74
  const RAW_CLAUDE_ENTRY = Symbol('raw-claude-entry');
74
75
  function parseEntry(line) {
@@ -925,9 +926,11 @@ function validateForkableEntry(entry) {
925
926
  throw new Error(`Claude transcript entry is missing ${field}`);
926
927
  }
927
928
  }
928
- if (entry.version !== VERIFIED_CLAUDE_SCHEMA_VERSION) {
929
- throw new Error(`Claude transcript fork must target Claude Code ${VERIFIED_CLAUDE_SCHEMA_VERSION}`);
930
- }
929
+ // A native fork is a lossless copy that retains the source producer's
930
+ // version as append-only provenance and changes only fork-specific
931
+ // session-id fields. The strict verified-writer version gate remains on
932
+ // serializeForAppend and serializeForSidechainAppend; the loop above
933
+ // already requires a nonempty producer version here.
931
934
  if (!('parentUuid' in entry) ||
932
935
  (entry.parentUuid !== null && !isNonEmptyString(entry.parentUuid))) {
933
936
  throw new Error('Claude transcript entry has invalid parentUuid');
@@ -975,6 +975,87 @@ async function writeScaffoldComponent(path, content, preserveExisting) {
975
975
  throw error;
976
976
  }
977
977
  }
978
+ function nativeSkillTemplate(name) {
979
+ return `---
980
+ name: ${name}
981
+ description: TODO — describe WHEN Claude should use this. Include trigger phrases users
982
+ might say ("do X", "set up Y", "review Z"). Be specific; this string is what Claude
983
+ matches the user's request against.
984
+ ---
985
+
986
+ # ${name}
987
+
988
+ TODO: what this skill does, and the steps Claude should take.
989
+ `;
990
+ }
991
+ function nativeChannelServerTemplate(name) {
992
+ return `#!/usr/bin/env bun
993
+ /**
994
+ * ${name} channel server — stdio MCP server implementing the channel contract.
995
+ * See https://docs.claude.com/en/docs/claude-code/channels-reference.
996
+ */
997
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
998
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
999
+ import {
1000
+ CallToolRequestSchema,
1001
+ ListToolsRequestSchema,
1002
+ } from '@modelcontextprotocol/sdk/types.js'
1003
+
1004
+ const mcp = new Server(
1005
+ { name: '${name}', version: '0.1.0' },
1006
+ {
1007
+ capabilities: {
1008
+ tools: {},
1009
+ // Required: presence of this key registers the channel notification
1010
+ // listener on Claude's side.
1011
+ experimental: { 'claude/channel': {} },
1012
+ },
1013
+ instructions:
1014
+ "Events from ${name} arrive as <channel source=\\"${name}\\" ...>. Anything " +
1015
+ "you want the sender to see must go through the reply tool — your " +
1016
+ "transcript output never reaches the channel.",
1017
+ },
1018
+ )
1019
+
1020
+ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
1021
+ tools: [
1022
+ {
1023
+ name: 'reply',
1024
+ description: 'Send a message back to the ${name} channel.',
1025
+ inputSchema: {
1026
+ type: 'object',
1027
+ properties: { text: { type: 'string' } },
1028
+ required: ['text'],
1029
+ },
1030
+ },
1031
+ ],
1032
+ }))
1033
+
1034
+ mcp.setRequestHandler(CallToolRequestSchema, async req => {
1035
+ const args = (req.params.arguments ?? {}) as Record<string, unknown>
1036
+ if (req.params.name === 'reply') {
1037
+ // TODO: deliver args.text to the external service.
1038
+ return { content: [{ type: 'text', text: 'sent' }] }
1039
+ }
1040
+ return { content: [{ type: 'text', text: 'unknown tool' }], isError: true }
1041
+ })
1042
+
1043
+ // TODO: when the external service has an inbound event, push it to Claude:
1044
+ //
1045
+ // await mcp.notification({
1046
+ // method: 'notifications/claude/channel',
1047
+ // params: {
1048
+ // content: 'the event body',
1049
+ // meta: { chat_id: '...', sender: '...' },
1050
+ // },
1051
+ // })
1052
+ //
1053
+ // Each meta key becomes an attribute on the <channel> tag. Keys must be
1054
+ // identifiers (letters/digits/underscores) — others are silently dropped.
1055
+
1056
+ await mcp.connect(new StdioServerTransport())
1057
+ `;
1058
+ }
978
1059
  export async function initClaudePlugin(path, name = basename(resolve(path)), options = {}) {
979
1060
  if (!PLUGIN_NAME.test(name))
980
1061
  throw new Error(`Plugin name must match ${PLUGIN_NAME}`);
@@ -1029,42 +1110,68 @@ export async function initClaudePlugin(path, name = basename(resolve(path)), opt
1029
1110
  };
1030
1111
  await writeScaffoldFile(join(manifestDirectory, 'plugin.json'), `${JSON.stringify(manifest, null, 2)}\n`, options.force === true);
1031
1112
  if (nativeLayout) {
1032
- await writeScaffoldComponent(join(root, 'SKILL.md'), `---\nname: ${name}\ndescription: TODO — describe when Claude should use this plugin.\n---\n\n# ${name}\n\nTODO: what this plugin does.\n`, options.force === true);
1113
+ await writeScaffoldComponent(join(root, 'SKILL.md'), nativeSkillTemplate(name), options.force === true);
1033
1114
  }
1034
1115
  else {
1035
1116
  await writeScaffoldComponent(join(root, 'commands', 'hello.md'), '# Hello\n\nDescribe the current workspace.\n', options.force === true);
1036
1117
  }
1037
1118
  if (components.has('skills')) {
1038
- await writeScaffoldComponent(join(root, 'skills', 'example', 'SKILL.md'), '---\nname: example\ndescription: Example plugin skill\n---\n\nUse this skill to inspect the workspace.\n', options.force === true);
1119
+ await writeScaffoldComponent(join(root, 'skills', 'example', 'SKILL.md'), nativeSkillTemplate('example'), options.force === true);
1039
1120
  }
1040
1121
  if (components.has('agents')) {
1041
- await writeScaffoldComponent(join(root, 'agents', nativeLayout ? 'example.md' : 'reviewer.md'), '---\nname: example\ndescription: Review changes\n---\n\nReview the current changes.\n', options.force === true);
1122
+ await writeScaffoldComponent(join(root, 'agents', nativeLayout ? 'example.md' : 'reviewer.md'), nativeLayout
1123
+ ? '---\nname: example\ndescription: TODO — when should Claude delegate to this subagent?\ntools:\n - Read\n - Grep\n---\n\nTODO: system prompt for the subagent.\n'
1124
+ : '---\nname: example\ndescription: Review changes\n---\n\nReview the current changes.\n', options.force === true);
1042
1125
  }
1043
1126
  if (components.has('hooks')) {
1044
- await writeScaffoldComponent(join(root, 'hooks', 'hooks.json'), '{\n "hooks": {\n "SessionStart": []\n }\n}\n', options.force === true);
1127
+ if (nativeLayout) {
1128
+ await writeScaffoldComponent(join(root, 'hooks-handlers', 'on-session-start.ts'), '#!/usr/bin/env bun\n// SessionStart hook handler. Reads the event from stdin, writes a JSON result\n// to stdout. Swap "bun" for "node" or "python3" in hooks/hooks.json if your\n// users\' environment lacks bun.\nconst input = await new Response(Bun.stdin.stream()).text()\nconst event = JSON.parse(input)\nprocess.stdout.write(JSON.stringify({}))\n', options.force === true);
1129
+ }
1130
+ await writeScaffoldComponent(join(root, 'hooks', 'hooks.json'), nativeLayout
1131
+ ? '{\n "hooks": {\n "SessionStart": [\n {\n "hooks": [\n {\n "type": "command",\n "command": "bun ${CLAUDE_PLUGIN_ROOT}/hooks-handlers/on-session-start.ts"\n }\n ]\n }\n ]\n }\n}\n'
1132
+ : '{\n "hooks": {\n "SessionStart": []\n }\n}\n', options.force === true);
1045
1133
  }
1046
1134
  if (components.has('mcp') || components.has('channel')) {
1047
- await writeScaffoldComponent(join(root, '.mcp.json'), `${JSON.stringify({
1048
- mcpServers: {
1049
- [components.has('channel') ? name : 'example']: {
1135
+ const mcpServers = components.has('channel')
1136
+ ? {
1137
+ [name]: {
1138
+ command: 'bun',
1139
+ args: [
1140
+ 'run',
1141
+ '--cwd',
1142
+ '${CLAUDE_PLUGIN_ROOT}',
1143
+ '--shell=bun',
1144
+ '--silent',
1145
+ 'start',
1146
+ ],
1147
+ },
1148
+ }
1149
+ : {
1150
+ 'example-remote': {
1151
+ type: 'http',
1152
+ url: 'https://example.com/mcp',
1153
+ },
1154
+ 'example-local': {
1050
1155
  command: 'npx',
1051
1156
  args: ['<your-mcp-server-package>'],
1052
1157
  },
1053
- },
1054
- }, null, 2)}\n`, options.force === true);
1158
+ };
1159
+ await writeScaffoldComponent(join(root, '.mcp.json'), `${JSON.stringify({ mcpServers }, null, 2)}\n`, options.force === true);
1055
1160
  }
1056
1161
  if (components.has('lsp')) {
1057
- await writeScaffoldComponent(join(root, '.lsp.json'), '{\n "example": {\n "command": "example-language-server",\n "args": ["--stdio"]\n }\n}\n', options.force === true);
1162
+ await writeScaffoldComponent(join(root, '.lsp.json'), '{\n "example": {\n "command": "example-language-server",\n "args": [\n "--stdio"\n ],\n "extensionToLanguage": {\n ".example": "example"\n }\n }\n}\n', options.force === true);
1058
1163
  }
1059
1164
  if (components.has('output-style')) {
1060
- await writeScaffoldComponent(join(root, 'output-styles', `${name}.md`), `---\nname: ${name}\ndescription: TODO — output style description\nforce-for-plugin: true\n---\n\nTODO: style instructions.\n`, options.force === true);
1165
+ await writeScaffoldComponent(join(root, 'output-styles', `${name}.md`), `---\nname: ${name}\ndescription: TODO — one line shown in the Output style picker in /config\nforce-for-plugin: true\nkeep-coding-instructions: true\n---\n\nTODO: the style prompt. This is appended to Claude's system prompt while the\nstyle is active. With force-for-plugin: true, the style applies automatically\nwhen this plugin is enabled.\n`, options.force === true);
1061
1166
  }
1062
1167
  if (components.has('channel')) {
1063
- await writeScaffoldComponent(join(root, 'server.ts'), '// TODO: implement channel server.\n', options.force === true);
1168
+ await writeScaffoldComponent(join(root, 'server.ts'), nativeChannelServerTemplate(name), options.force === true);
1064
1169
  await writeScaffoldComponent(join(root, 'package.json'), `${JSON.stringify({
1065
1170
  name: `claude-channel-${name}`,
1066
1171
  version: '0.1.0',
1067
1172
  type: 'module',
1173
+ scripts: { start: 'bun install --no-summary && bun server.ts' },
1174
+ dependencies: { '@modelcontextprotocol/sdk': '^1.0.0' },
1068
1175
  }, null, 2)}\n`, options.force === true);
1069
1176
  }
1070
1177
  return validateClaudePlugin(root);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.20.1",
3
+ "version": "0.20.2",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",
@@ -62,6 +62,11 @@
62
62
  "test:mcp-resource-compat": "npm run build && node scripts/verify-mcp-resource-compatibility.mjs",
63
63
  "test:notebook-compat": "npm run build && node scripts/verify-notebook-compatibility.mjs",
64
64
  "test:context-compat": "npm run build && node scripts/verify-context-runtime.mjs",
65
+ "test:cross-version-session-compat": "npm run build && node scripts/verify-cross-version-session-compatibility.mjs",
66
+ "test:cross-version-resume-at-compat": "npm run build && node scripts/verify-cross-version-resume-at-compatibility.mjs",
67
+ "test:cross-version-fork-compat": "npm run build && node scripts/verify-cross-version-fork-compatibility.mjs",
68
+ "test:cross-version-sidechain-compat": "npm run build && node scripts/verify-cross-version-sidechain-compatibility.mjs",
69
+ "test:cross-version-compaction-compat": "npm run build && node scripts/verify-cross-version-compaction-compatibility.mjs",
65
70
  "test:dynamic-system-compat": "npm run build && node scripts/verify-dynamic-system-prompt.mjs",
66
71
  "test:permission-compat": "npm run build && node scripts/verify-claude-permissions.mjs",
67
72
  "test:permission-prompt-compat": "npm run build && node scripts/verify-permission-prompt-tool.mjs",