praxis-agent 0.46.4 → 0.47.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.
package/README.md CHANGED
@@ -100,6 +100,10 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
100
100
 
101
101
  ## What Praxis provides
102
102
 
103
+ - **Outcome-driven evaluation** — `praxis eval <target>` runs contained cases
104
+ in isolated workspaces, requires explicit verifier authorization, and writes
105
+ versioned artifacts locally; usage and cost remain explicitly available or
106
+ unknown.
103
107
  - **Local agent runtime** — C+ Quiet Operator responsive TUI with a linear
104
108
  `❯` user / `⏺` assistant conversation, `✻` thinking activity, and `!` shell
105
109
  composer grammar, compact stable tool rows, responsive density,
@@ -22,6 +22,7 @@ import { launchTmuxWorktree } from './platform/tmux-worktree.js';
22
22
  import { type CliControls, type CliRuntimeInfo, type CliElicitationRequest, type CliElicitationResult } from './cli/protocol.js';
23
23
  import { executeProviderAuthCommand } from './cli/provider-auth-command.js';
24
24
  import { type PluginEvalDependencies } from './plugins/claude-plugin-eval.js';
25
+ import { type ProjectEvalDependencies } from './evals/project-eval.js';
25
26
  import { type SelfUpdateResult } from './maintenance/self-update.js';
26
27
  export { parseContextEnvironment, parseProviderEnvironment };
27
28
  export interface CliIO {
@@ -153,6 +154,7 @@ export interface CliDependencies extends InteractiveServiceFactory {
153
154
  statePath?: string;
154
155
  }): Promise<ModelProvider>;
155
156
  pluginEval?: PluginEvalDependencies;
157
+ projectEval?: ProjectEvalDependencies;
156
158
  runInteractive?(options: {
157
159
  agent?: string;
158
160
  controls?: CliControls;
@@ -70,6 +70,7 @@ import { executeProviderAuthCommand } from './cli/provider-auth-command.js';
70
70
  import { describeClaudePlugin, initClaudePlugin, installClaudePlugin, loadClaudePlugins, readPluginRegistry, setClaudePluginEnabled, uninstallClaudePlugin, updateClaudePlugin, validateClaudePlugin, } from './plugins/claude-plugin-runtime.js';
71
71
  import { addClaudeMarketplace, disableAllNativePlugins, installClaudeMarketplacePlugin, listClaudeMarketplaceAvailablePlugins, listNativePluginRecords, readClaudeKnownMarketplaces, removeClaudeMarketplace, setNativePluginEnabled, saveClaudePluginConfig, uninstallNativePlugin, updateClaudeMarketplace, updateNativePlugin, validateClaudeMarketplace, } from './plugins/claude-plugin-marketplace.js';
72
72
  import { executeClaudePluginEvalCommand, PLUGIN_EVAL_HELP, } from './plugins/claude-plugin-eval.js';
73
+ import { executeProjectEvalCommand, PROJECT_EVAL_HELP, } from './evals/project-eval.js';
73
74
  import { CLAUDE_PLUGIN_PRUNE_HELP, CLAUDE_PLUGIN_TAG_HELP, executeClaudePluginPrune, planClaudePluginPrune, tagClaudePlugin, } from './plugins/claude-plugin-maintenance.js';
74
75
  import { formatDoctorReport, runDoctor } from './maintenance/doctor.js';
75
76
  import { runSelfUpdate, } from './maintenance/self-update.js';
@@ -123,6 +124,7 @@ Usage:
123
124
  praxis mcp <list|get|add|add-json|remove|reset-project-choices|login|logout|serve> ...
124
125
  praxis auto-mode <config|defaults|critique>
125
126
  praxis plugin|plugins <details|list|install|uninstall|enable|disable|update|init|prune|tag|validate|marketplace> ...
127
+ praxis eval [options] <target>
126
128
  praxis doctor [--json]
127
129
  praxis auth <status|set-key|login|logout> ...
128
130
  praxis import [options] [source]
@@ -2282,7 +2284,7 @@ const defaultPluginEvalRuntimeFactory = {
2282
2284
  dataPlane: options.dataPlane,
2283
2285
  sessionPersistence: false,
2284
2286
  maxTurns: options.maxTurns,
2285
- pluginDirectories: [...options.pluginDirectories],
2287
+ pluginDirectories: [...(options.pluginDirectories ?? [])],
2286
2288
  addDirectories: [...options.addDirs],
2287
2289
  allowedTools: [...options.allowedTools],
2288
2290
  disallowedTools: [],
@@ -2305,6 +2307,7 @@ const defaultPluginEvalRuntimeFactory = {
2305
2307
  ...(result.costUsd === undefined
2306
2308
  ? {}
2307
2309
  : { costUsd: result.costUsd }),
2310
+ usage: result.usage,
2308
2311
  };
2309
2312
  },
2310
2313
  close: () => service.close?.() ?? Promise.resolve(),
@@ -2523,6 +2526,11 @@ export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta
2523
2526
  runtimeFactory: defaultPluginEvalRuntimeFactory,
2524
2527
  judge: defaultPluginEvalJudge,
2525
2528
  },
2529
+ projectEval: {
2530
+ runtimeFactory: defaultPluginEvalRuntimeFactory,
2531
+ version: VERSION,
2532
+ configRoot: resolveDataPlaneRoot(),
2533
+ },
2526
2534
  cliPath: entrypoint,
2527
2535
  runInteractive: async ({ agent, controls, initialPrompt, resume, signal, }) => {
2528
2536
  const { runInteractive } = await import('./cli/interactive.js');
@@ -4899,6 +4907,7 @@ async function executeTeamCommand(args, io, dependencies, signal) {
4899
4907
  }
4900
4908
  function specialCommandIndex(argv) {
4901
4909
  const commands = new Set([
4910
+ 'eval',
4902
4911
  'team',
4903
4912
  'plugin',
4904
4913
  'plugins',
@@ -4963,7 +4972,9 @@ async function execute(argv, io, dependencies, signal) {
4963
4972
  return executeTeamCommand(argv.slice(commandIndex), io, dependencies, signal);
4964
4973
  }
4965
4974
  const specialPrefix = commandIndex > 0 &&
4966
- (argv[commandIndex] === 'plugin' || argv[commandIndex] === 'plugins')
4975
+ (argv[commandIndex] === 'plugin' ||
4976
+ argv[commandIndex] === 'plugins' ||
4977
+ argv[commandIndex] === 'eval')
4967
4978
  ? parseCliInvocation([...argv.slice(0, commandIndex), '__probe__'])
4968
4979
  : undefined;
4969
4980
  const special = commandIndex >= 0
@@ -4971,6 +4982,22 @@ async function execute(argv, io, dependencies, signal) {
4971
4982
  args: argv.slice(commandIndex),
4972
4983
  }
4973
4984
  : { args: [...argv] };
4985
+ if (special.args[0] === 'eval') {
4986
+ if (special.args
4987
+ .slice(1)
4988
+ .some((value) => value === '-h' || value === '--help')) {
4989
+ io.stdout(PROJECT_EVAL_HELP);
4990
+ return 0;
4991
+ }
4992
+ if (!dependencies.projectEval)
4993
+ throw new Error('Project eval unavailable');
4994
+ return executeProjectEvalCommand([
4995
+ ...(specialPrefix?.model === undefined
4996
+ ? []
4997
+ : ['--model', specialPrefix.model]),
4998
+ ...special.args.slice(1),
4999
+ ], io, dependencies.projectEval, process.cwd(), signal);
5000
+ }
4974
5001
  if ((special.args[0] === 'plugin' || special.args[0] === 'plugins') &&
4975
5002
  special.args[1] === 'eval') {
4976
5003
  if (!dependencies.pluginEval)
@@ -0,0 +1,102 @@
1
+ import type { ModelUsage, RuntimeEvent } from '../core/runtime.js';
2
+ import type { DataPlane } from '../persistence/data-plane.js';
3
+ export interface EvalRuntime {
4
+ run(prompt: string, signal: AbortSignal): Promise<{
5
+ text: string;
6
+ turns: number;
7
+ costUsd?: number;
8
+ usage?: ModelUsage;
9
+ }>;
10
+ close?(): Promise<void>;
11
+ }
12
+ export interface EvalRuntimeFactoryOptions {
13
+ dataPlane: DataPlane;
14
+ cwd: string;
15
+ configRoot: string;
16
+ home: string;
17
+ model?: string;
18
+ maxTurns: number;
19
+ pluginDirectories?: readonly string[];
20
+ allowedTools: readonly string[];
21
+ appendSystemPrompt?: string;
22
+ historyFile?: string;
23
+ addDirs: readonly string[];
24
+ env: Readonly<Record<string, string>>;
25
+ eventSink(event: RuntimeEvent): void;
26
+ }
27
+ export interface EvalRuntimeFactory {
28
+ create(options: EvalRuntimeFactoryOptions): Promise<EvalRuntime>;
29
+ }
30
+ export interface EvalTraceEvent {
31
+ type: string;
32
+ tool?: string;
33
+ input?: Record<string, unknown>;
34
+ [key: string]: unknown;
35
+ }
36
+ export type EvalFocus = 'last_message' | 'trace' | 'files' | {
37
+ source: 'file';
38
+ path: string;
39
+ };
40
+ export type EvalArm = 'with-only' | 'both';
41
+ export type EvalToolMatch = string | {
42
+ tool: string;
43
+ input_match?: Record<string, unknown>;
44
+ };
45
+ export interface EvalGrader {
46
+ name: string;
47
+ weight: number;
48
+ arm?: EvalArm;
49
+ }
50
+ export type EvalGraderBase = EvalGrader;
51
+ export type EvalDeterministicGrader = (EvalGrader & {
52
+ type: 'regex';
53
+ target: EvalFocus;
54
+ pattern: string;
55
+ flags: string;
56
+ match: 'contains' | 'not_contains' | `count:${number}`;
57
+ }) | (EvalGrader & {
58
+ type: 'tool_order';
59
+ before: EvalToolMatch;
60
+ after: EvalToolMatch;
61
+ }) | (EvalGrader & {
62
+ type: 'tool_used';
63
+ tool: string;
64
+ input_match?: Record<string, unknown>;
65
+ min: number;
66
+ max: number;
67
+ }) | (EvalGrader & {
68
+ type: 'file_exists';
69
+ path: string;
70
+ exists: boolean;
71
+ });
72
+ export interface EvalRunArtifacts {
73
+ lastMessage: string;
74
+ trace: readonly EvalTraceEvent[];
75
+ cwd: string;
76
+ }
77
+ export interface EvalGraderResult {
78
+ name: string;
79
+ passed: boolean;
80
+ weight: number;
81
+ explanation: string;
82
+ judge_votes?: readonly boolean[];
83
+ evidence?: string;
84
+ with_only?: boolean;
85
+ }
86
+ export interface EvalJudge {
87
+ vote(request: {
88
+ criteria: string;
89
+ focus: string;
90
+ baseline?: string;
91
+ model: string;
92
+ signal?: AbortSignal;
93
+ }): Promise<{
94
+ passed: boolean;
95
+ explanation?: string;
96
+ costUsd: number;
97
+ }>;
98
+ }
99
+ export declare const DEFAULT_EVAL_ALLOWED_TOOLS: readonly ["Read", "Glob", "Grep", "Skill"];
100
+ export declare function resolveEvalAllowedTools(requested: readonly string[], operatorGrants: readonly string[]): string[];
101
+ export declare function normalizeEvalTraceEvent(event: RuntimeEvent): EvalTraceEvent;
102
+ //# sourceMappingURL=eval-contract.d.ts.map
@@ -0,0 +1,57 @@
1
+ export const DEFAULT_EVAL_ALLOWED_TOOLS = [
2
+ 'Read',
3
+ 'Glob',
4
+ 'Grep',
5
+ 'Skill',
6
+ ];
7
+ function toolName(rule) {
8
+ const i = rule.indexOf('(');
9
+ return i < 0 ? rule : rule.slice(0, i);
10
+ }
11
+ function gatedTool(rule) {
12
+ const name = toolName(rule);
13
+ return (['Bash', 'Write', 'Edit', 'NotebookEdit', 'WebFetch', 'WebSearch'].includes(name) || name.startsWith('mcp__'));
14
+ }
15
+ function grants(requested, allowed) {
16
+ const name = toolName(requested);
17
+ const wildcard = (pattern) => {
18
+ const parts = pattern.split('*');
19
+ if (parts.length === 1 || !requested.startsWith(parts[0] ?? ''))
20
+ return false;
21
+ let cursor = parts[0]?.length ?? 0;
22
+ for (const part of parts.slice(1, -1)) {
23
+ const index = requested.indexOf(part, cursor);
24
+ if (index < 0)
25
+ return false;
26
+ cursor = index + part.length;
27
+ }
28
+ const last = parts.at(-1) ?? '';
29
+ return last.length === 0 || requested.slice(cursor).endsWith(last);
30
+ };
31
+ return allowed.some((grant) => grant === requested || grant === name || wildcard(grant));
32
+ }
33
+ export function resolveEvalAllowedTools(requested, operatorGrants) {
34
+ const selected = requested.length ? requested : DEFAULT_EVAL_ALLOWED_TOOLS;
35
+ for (const rule of selected)
36
+ if (gatedTool(rule) && !grants(rule, operatorGrants))
37
+ throw new Error(`Eval case requests gated tool ${rule}; grant it with --allow-tools`);
38
+ return [...new Set(selected)];
39
+ }
40
+ export function normalizeEvalTraceEvent(event) {
41
+ if (event.type === 'tool-call')
42
+ return {
43
+ type: 'tool-call',
44
+ tool: event.call.name,
45
+ input: event.call.input,
46
+ id: event.call.id,
47
+ };
48
+ if (event.type === 'tool-result')
49
+ return {
50
+ type: 'tool-result',
51
+ callId: event.callId,
52
+ content: event.content,
53
+ isError: event.isError,
54
+ };
55
+ return event;
56
+ }
57
+ //# sourceMappingURL=eval-contract.js.map
@@ -0,0 +1,6 @@
1
+ import { type EvalDeterministicGrader, type EvalGraderResult, type EvalRunArtifacts } from './eval-contract.js';
2
+ export declare function gradeDeterministicEvalRun(options: {
3
+ graders: readonly EvalDeterministicGrader[];
4
+ artifacts: EvalRunArtifacts;
5
+ }): Promise<EvalGraderResult[]>;
6
+ //# sourceMappingURL=eval-graders.d.ts.map
@@ -0,0 +1,94 @@
1
+ import { glob, readFile, realpath } from 'node:fs/promises';
2
+ import { resolve, sep } from 'node:path';
3
+ import { minimatch } from 'minimatch';
4
+ function subset(actual, expected) {
5
+ if (expected && typeof expected === 'object' && !Array.isArray(expected)) {
6
+ if (!actual || typeof actual !== 'object' || Array.isArray(actual))
7
+ return false;
8
+ return Object.entries(expected).every(([key, value]) => subset(actual[key], value));
9
+ }
10
+ return Object.is(actual, expected);
11
+ }
12
+ function matches(event, match) {
13
+ const selected = typeof match === 'string' ? { tool: match } : match;
14
+ return (event.type === 'tool-call' &&
15
+ event.tool === selected.tool &&
16
+ (!selected.input_match || subset(event.input, selected.input_match)));
17
+ }
18
+ async function containedPath(root, candidate) {
19
+ const rootPath = await realpath(resolve(root));
20
+ const path = await realpath(resolve(rootPath, candidate));
21
+ if (path !== rootPath && !path.startsWith(`${rootPath}${sep}`))
22
+ throw new Error(`Grader path escapes workspace: ${candidate}`);
23
+ return path;
24
+ }
25
+ async function focusText(focus, artifacts) {
26
+ if (focus === 'last_message')
27
+ return artifacts.lastMessage;
28
+ if (focus === 'trace')
29
+ return artifacts.trace.map((item) => JSON.stringify(item)).join('\n');
30
+ if (focus === 'files') {
31
+ const names = [];
32
+ for await (const path of glob('**/*', {
33
+ cwd: artifacts.cwd,
34
+ exclude: ['node_modules/**', '.git/**'],
35
+ }))
36
+ names.push(path);
37
+ return names.sort().join('\n');
38
+ }
39
+ return readFile(await containedPath(artifacts.cwd, focus.path), 'utf8');
40
+ }
41
+ export async function gradeDeterministicEvalRun(options) {
42
+ const results = [];
43
+ for (const grader of options.graders) {
44
+ let passed = false;
45
+ let evidence = '';
46
+ if (grader.type === 'regex') {
47
+ const target = await focusText(grader.target, options.artifacts);
48
+ const expression = new RegExp(grader.pattern, grader.flags.includes('g') ? grader.flags : `${grader.flags}g`);
49
+ const count = [...target.matchAll(expression)].length;
50
+ passed =
51
+ grader.match === 'contains'
52
+ ? count > 0
53
+ : grader.match === 'not_contains'
54
+ ? count === 0
55
+ : count === Number(grader.match.slice(6));
56
+ evidence = `matches=${count}`;
57
+ }
58
+ else if (grader.type === 'tool_used') {
59
+ const count = options.artifacts.trace.filter((event) => matches(event, {
60
+ tool: grader.tool,
61
+ ...(grader.input_match ? { input_match: grader.input_match } : {}),
62
+ })).length;
63
+ passed = count >= grader.min && count <= grader.max;
64
+ evidence = `uses=${count}`;
65
+ }
66
+ else if (grader.type === 'tool_order') {
67
+ const before = options.artifacts.trace.findIndex((event) => matches(event, grader.before));
68
+ const after = options.artifacts.trace.findIndex((event, index) => index > before && matches(event, grader.after));
69
+ passed = before >= 0 && after > before;
70
+ evidence = `before=${before},after=${after}`;
71
+ }
72
+ else {
73
+ let count = 0;
74
+ for await (const path of glob('**/*', { cwd: options.artifacts.cwd })) {
75
+ if (!minimatch(path, grader.path))
76
+ continue;
77
+ await containedPath(options.artifacts.cwd, path);
78
+ count += 1;
79
+ }
80
+ passed = grader.exists ? count > 0 : count === 0;
81
+ evidence = `files=${count}`;
82
+ }
83
+ results.push({
84
+ name: grader.name,
85
+ passed,
86
+ weight: grader.weight,
87
+ explanation: passed ? 'passed' : 'failed',
88
+ evidence,
89
+ ...(grader.arm === 'with-only' ? { with_only: true } : {}),
90
+ });
91
+ }
92
+ return results;
93
+ }
94
+ //# sourceMappingURL=eval-graders.js.map
@@ -0,0 +1,59 @@
1
+ import type { ModelUsage } from '../core/runtime.js';
2
+ import { type EvalGraderResult, type EvalRuntimeFactory } from './eval-contract.js';
3
+ import type { ProjectEvalCase } from './project-eval-schema.js';
4
+ export type ProjectEvalTermination = 'timeout' | 'interrupted' | null;
5
+ export interface ProjectEvalVerificationResult {
6
+ schema_version: '1.0';
7
+ name: string;
8
+ command: string;
9
+ argv: readonly string[];
10
+ exit_code: number | null;
11
+ timed_out: boolean;
12
+ stdout: string;
13
+ stderr: string;
14
+ truncated: boolean;
15
+ duration_ms: number;
16
+ passed: boolean;
17
+ error: string | null;
18
+ }
19
+ export interface ProjectEvalRunResult {
20
+ schema_version: '1.0';
21
+ case: string;
22
+ run: number;
23
+ version: string;
24
+ model: string | null;
25
+ passed: boolean;
26
+ score: 0 | 1;
27
+ turns: number;
28
+ usage: ModelUsage | null;
29
+ cost_usd: number | null;
30
+ cost_known: boolean;
31
+ duration_ms: number;
32
+ termination: ProjectEvalTermination;
33
+ graders: readonly EvalGraderResult[];
34
+ checks: readonly EvalGraderResult[];
35
+ artifacts: {
36
+ trace: 'trace.jsonl';
37
+ workspace_diff: 'workspace-diff.json';
38
+ verification: 'verification.json';
39
+ result: 'result.json';
40
+ };
41
+ error: string | null;
42
+ cleanup_errors: readonly string[];
43
+ temp_root: string | null;
44
+ }
45
+ interface ProjectEvalRunOptions {
46
+ case: ProjectEvalCase;
47
+ factory: EvalRuntimeFactory;
48
+ run: number;
49
+ allowTools?: readonly string[];
50
+ model?: string;
51
+ keepTemp?: boolean;
52
+ runVerification?: boolean;
53
+ outputDir: string;
54
+ version: string;
55
+ signal?: AbortSignal;
56
+ }
57
+ export declare function runProjectEvalCase(options: ProjectEvalRunOptions): Promise<ProjectEvalRunResult>;
58
+ export {};
59
+ //# sourceMappingURL=project-eval-runner.d.ts.map