praxis-agent 0.46.5 → 0.48.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,12 @@ 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. Separate runs can be compared with `praxis eval compare`; unknown
107
+ token/cost evidence produces null deltas, while the gate requires no pass-rate
108
+ or safety-rate regression and rejects incomplete safety evidence.
103
109
  - **Local agent runtime** — C+ Quiet Operator responsive TUI with a linear
104
110
  `❯` user / `⏺` assistant conversation, `✻` thinking activity, and `!` shell
105
111
  composer grammar, compact stable tool rows, responsive density,
@@ -295,7 +301,7 @@ normal/low-capability full-frame p95 budgets of `<16.7/<33 ms`.
295
301
  `npm run test:coverage` measures all production code under `src/**` with V8 and
296
302
  enforces global floors of 79% statements, 70% branches, 85% functions, and 81% lines,
297
303
  and rejects any production runtime module with zero covered statements (while allowing
298
- type-only modules). `npm run test:fixtures` executes the 67-behavior native contract; 59 behaviors
304
+ type-only modules). `npm run test:fixtures` executes the 68-behavior native contract; 60 behaviors
299
305
  are qualified and 8 are explicitly excluded. `npm run verify:fixture-contracts`
300
306
  performs the structural check and is part of `npm run check`.
301
307
  `npm run test:core-completion` is retained as a compatibility alias for
@@ -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,8 @@ 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';
74
+ import { PROJECT_EVAL_COMPARE_HELP } from './evals/project-eval-comparison.js';
73
75
  import { CLAUDE_PLUGIN_PRUNE_HELP, CLAUDE_PLUGIN_TAG_HELP, executeClaudePluginPrune, planClaudePluginPrune, tagClaudePlugin, } from './plugins/claude-plugin-maintenance.js';
74
76
  import { formatDoctorReport, runDoctor } from './maintenance/doctor.js';
75
77
  import { runSelfUpdate, } from './maintenance/self-update.js';
@@ -123,6 +125,7 @@ Usage:
123
125
  praxis mcp <list|get|add|add-json|remove|reset-project-choices|login|logout|serve> ...
124
126
  praxis auto-mode <config|defaults|critique>
125
127
  praxis plugin|plugins <details|list|install|uninstall|enable|disable|update|init|prune|tag|validate|marketplace> ...
128
+ praxis eval [options] <target>
126
129
  praxis doctor [--json]
127
130
  praxis auth <status|set-key|login|logout> ...
128
131
  praxis import [options] [source]
@@ -2282,7 +2285,7 @@ const defaultPluginEvalRuntimeFactory = {
2282
2285
  dataPlane: options.dataPlane,
2283
2286
  sessionPersistence: false,
2284
2287
  maxTurns: options.maxTurns,
2285
- pluginDirectories: [...options.pluginDirectories],
2288
+ pluginDirectories: [...(options.pluginDirectories ?? [])],
2286
2289
  addDirectories: [...options.addDirs],
2287
2290
  allowedTools: [...options.allowedTools],
2288
2291
  disallowedTools: [],
@@ -2305,6 +2308,7 @@ const defaultPluginEvalRuntimeFactory = {
2305
2308
  ...(result.costUsd === undefined
2306
2309
  ? {}
2307
2310
  : { costUsd: result.costUsd }),
2311
+ usage: result.usage,
2308
2312
  };
2309
2313
  },
2310
2314
  close: () => service.close?.() ?? Promise.resolve(),
@@ -2523,6 +2527,11 @@ export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta
2523
2527
  runtimeFactory: defaultPluginEvalRuntimeFactory,
2524
2528
  judge: defaultPluginEvalJudge,
2525
2529
  },
2530
+ projectEval: {
2531
+ runtimeFactory: defaultPluginEvalRuntimeFactory,
2532
+ version: VERSION,
2533
+ configRoot: resolveDataPlaneRoot(),
2534
+ },
2526
2535
  cliPath: entrypoint,
2527
2536
  runInteractive: async ({ agent, controls, initialPrompt, resume, signal, }) => {
2528
2537
  const { runInteractive } = await import('./cli/interactive.js');
@@ -4899,6 +4908,7 @@ async function executeTeamCommand(args, io, dependencies, signal) {
4899
4908
  }
4900
4909
  function specialCommandIndex(argv) {
4901
4910
  const commands = new Set([
4911
+ 'eval',
4902
4912
  'team',
4903
4913
  'plugin',
4904
4914
  'plugins',
@@ -4963,7 +4973,9 @@ async function execute(argv, io, dependencies, signal) {
4963
4973
  return executeTeamCommand(argv.slice(commandIndex), io, dependencies, signal);
4964
4974
  }
4965
4975
  const specialPrefix = commandIndex > 0 &&
4966
- (argv[commandIndex] === 'plugin' || argv[commandIndex] === 'plugins')
4976
+ (argv[commandIndex] === 'plugin' ||
4977
+ argv[commandIndex] === 'plugins' ||
4978
+ argv[commandIndex] === 'eval')
4967
4979
  ? parseCliInvocation([...argv.slice(0, commandIndex), '__probe__'])
4968
4980
  : undefined;
4969
4981
  const special = commandIndex >= 0
@@ -4971,6 +4983,29 @@ async function execute(argv, io, dependencies, signal) {
4971
4983
  args: argv.slice(commandIndex),
4972
4984
  }
4973
4985
  : { args: [...argv] };
4986
+ if (special.args[0] === 'eval') {
4987
+ if (special.args[1] === 'compare' &&
4988
+ special.args
4989
+ .slice(2)
4990
+ .some((value) => value === '-h' || value === '--help')) {
4991
+ io.stdout(PROJECT_EVAL_COMPARE_HELP);
4992
+ return 0;
4993
+ }
4994
+ if (special.args
4995
+ .slice(1)
4996
+ .some((value) => value === '-h' || value === '--help')) {
4997
+ io.stdout(PROJECT_EVAL_HELP);
4998
+ return 0;
4999
+ }
5000
+ if (!dependencies.projectEval)
5001
+ throw new Error('Project eval unavailable');
5002
+ return executeProjectEvalCommand([
5003
+ ...(specialPrefix?.model === undefined
5004
+ ? []
5005
+ : ['--model', specialPrefix.model]),
5006
+ ...special.args.slice(1),
5007
+ ], io, dependencies.projectEval, process.cwd(), signal);
5008
+ }
4974
5009
  if ((special.args[0] === 'plugin' || special.args[0] === 'plugins') &&
4975
5010
  special.args[1] === 'eval') {
4976
5011
  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,76 @@
1
+ import type { ProjectEvalAggregate } from './project-eval.js';
2
+ export declare const PROJECT_EVAL_COMPARE_HELP = "Usage: praxis eval compare [options]\n\nCompare two completed project evaluation aggregate artifacts.\n\nOptions:\n --baseline <aggregate-result.json> Baseline aggregate artifact\n --baseline-name <name> Name shown for the baseline\n --candidate <aggregate-result.json> Candidate aggregate artifact\n --candidate-name <name> Name shown for the candidate\n --output-dir <dir> Write comparison-result.json here\n --json Print exactly one comparison JSON value\n -h, --help Display help";
3
+ export interface ProjectEvalCompareOptions {
4
+ baseline?: string;
5
+ baselineName?: string;
6
+ candidate?: string;
7
+ candidateName?: string;
8
+ outputDir?: string;
9
+ json: boolean;
10
+ help?: true;
11
+ }
12
+ interface LoadedAggregate {
13
+ aggregate: ProjectEvalAggregate;
14
+ sourcePath: string;
15
+ safetyKnown: boolean;
16
+ }
17
+ export interface ProjectEvalComparisonMetric<T = number | null> {
18
+ baseline: T;
19
+ candidate: T;
20
+ delta: T;
21
+ }
22
+ export interface ProjectEvalComparisonResult {
23
+ schema_version: '1.0';
24
+ baseline: {
25
+ name: string;
26
+ source_path: string;
27
+ version: string;
28
+ model: string | null;
29
+ };
30
+ candidate: {
31
+ name: string;
32
+ source_path: string;
33
+ version: string;
34
+ model: string | null;
35
+ };
36
+ comparable_run_count: number;
37
+ passed: boolean;
38
+ regressions: readonly {
39
+ case: string;
40
+ run: number;
41
+ baseline_passed: boolean;
42
+ candidate_passed: boolean;
43
+ }[];
44
+ metrics: {
45
+ pass_rate: ProjectEvalComparisonMetric<number>;
46
+ safety_pass_rate: ProjectEvalComparisonMetric<number | null>;
47
+ average_turns: ProjectEvalComparisonMetric<number>;
48
+ input_tokens: ProjectEvalComparisonMetric<number | null>;
49
+ output_tokens: ProjectEvalComparisonMetric<number | null>;
50
+ cache_read_input_tokens: ProjectEvalComparisonMetric<number | null>;
51
+ cache_creation_input_tokens: ProjectEvalComparisonMetric<number | null>;
52
+ known_cost_total_usd: ProjectEvalComparisonMetric<number | null>;
53
+ average_duration_ms: ProjectEvalComparisonMetric<number>;
54
+ permission_decisions: {
55
+ allow: ProjectEvalComparisonMetric<number | null>;
56
+ ask: ProjectEvalComparisonMetric<number | null>;
57
+ deny: ProjectEvalComparisonMetric<number | null>;
58
+ };
59
+ tool_errors: ProjectEvalComparisonMetric<number | null>;
60
+ retries: ProjectEvalComparisonMetric<number | null>;
61
+ terminations: {
62
+ completed: ProjectEvalComparisonMetric<number>;
63
+ timeout: ProjectEvalComparisonMetric<number>;
64
+ interrupted: ProjectEvalComparisonMetric<number>;
65
+ };
66
+ };
67
+ }
68
+ export declare function parseProjectEvalCompareOptions(argv: readonly string[]): ProjectEvalCompareOptions;
69
+ export declare function loadProjectEvalAggregate(inputPath: string, callerCwd?: string): Promise<LoadedAggregate>;
70
+ export declare function compareProjectEvalAggregates(baseline: LoadedAggregate, candidate: LoadedAggregate, baselineName: string, candidateName: string): ProjectEvalComparisonResult;
71
+ export declare function executeProjectEvalCompareCommand(argv: readonly string[], io: {
72
+ stdout(message: string): void;
73
+ stderr(message: string): void;
74
+ }, callerCwd?: string, signal?: AbortSignal): Promise<number>;
75
+ export {};
76
+ //# sourceMappingURL=project-eval-comparison.d.ts.map