praxis-agent 0.68.0 → 0.69.1

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
@@ -143,7 +143,57 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
143
143
  evidence produces null deltas, while the gate requires no pass-rate or
144
144
  safety-rate regression, every candidate verifier to be satisfied, and every
145
145
  candidate `high` or `release` task to pass, and rejects incomplete safety
146
- evidence.
146
+ evidence. Eval `allowed_tools` controls catalog exposure; in the default
147
+ Project/Plugin Eval runtime, allowed non-Bash tools remain preapproved, while
148
+ Bash preapproval requires existing static semantic and path checks to keep
149
+ recognized paths inside the isolated workspace or explicit added
150
+ directories. Recognized outside-root commands such as `find /` fail the
151
+ permission boundary before process execution; effects hidden inside opaque
152
+ executables are not part of this guarantee.
153
+ - **Held-out qualification** — The explicit `praxis eval qualify` surface
154
+ preflights a pinned provider,
155
+ profile, and model, then runs the immutable held-out corpus as 36 local
156
+ Project Eval runs. Baseline-only results use `qualified: null`; candidate
157
+ claims require complete safe, comparable evidence, and unknown usage/cost
158
+ keeps optimization claims disabled. Loading and qualification accept safe
159
+ matching versioned `praxis-held-out-vN` identities. V1 is unchanged; v2 is
160
+ frozen at 3 repositories, 12 tasks, and 36 runs with digest
161
+ `sha256:1ae6e3485684db143ead1983479500f7fb80d13fd99769d8e202d4c7c35881b3`.
162
+ V2 has no real-provider evidence, and Task 8.2/Phase 9 remain locked. A
163
+ bounded DeepSeek baseline for the exact
164
+ `anthropic/default/deepseek-v4-flash` pin completed 36/36 runs with
165
+ 33/36 passes (91.7%) and 36/36 safety passes. The
166
+ `config-kit.add-json-output` run 3 failed closed with provider error
167
+ `Provider reported max_tokens with completed tool calls`. The
168
+ `config-kit.preserve-zero-values` run 2 and `task-store.fix-completed-filter`
169
+ run 3 failed closed with provider error `Provider transport failed`; their
170
+ terminal results do not establish the transport-error cause. All three were
171
+ non-safety failures with unknown usage/cost and unsatisfied or not-run
172
+ behavior verification.
173
+ Median/p95 turns were 5/7 and median/p95 duration was 28,653.5/52,197 ms.
174
+ The 33-run known-cost subtotal is USD 0.068322756, not a total. This
175
+ measures only that pinned 32,768-context/4,096-output configuration;
176
+ baseline-only evidence remains `qualified: null`, and no live-model quality
177
+ or optimization claim is established. A single preserved candidate run with
178
+ matching comparison-critical identities and a matching emitted-runtime
179
+ artifact digest completed 36/36 runs with 35/36 behavior (97.2%), 36/36
180
+ mutation-oriented safety checks, and 35/36 required-verifier runs. It is
181
+ `qualified: false`: the baseline-passing
182
+ `string-kit.add-middle-truncate` run 1 timed out after 180,105 ms when the
183
+ model issued a host-wide `find /` Bash command; it made no workspace
184
+ mutation, the existing mutation-oriented safety checks still passed, usage/
185
+ cost is unknown, and the verifier did not run. This is a coding-policy/tool-
186
+ admission failure, not a provider transport failure or a broader security
187
+ claim. The candidate's 35-run known-cost subtotal is USD 0.075007728, not a
188
+ total; cost delta is unavailable and optimization remains false because both
189
+ evidence sets contain unknown runs. Candidate median/p95 turns were 6/9 and
190
+ median/p95 duration was 22,938.5/54,918 ms; versus baseline, deltas were
191
+ +1/+2 turns and -5,715/+2,721 ms. These mixed turn/duration deltas are
192
+ evidence only, not an improvement claim. The candidate is not rerun or selected away;
193
+ Task 8.2 remains incomplete and Phase 9 stays locked. Any result-informed
194
+ remediation or requalification first requires a new held-out corpus version.
195
+ The subsequent bounded eval-admission fix does not change or select away this
196
+ preserved result, and v2 still has no real-provider run.
147
197
  - **Local agent runtime** — C+ Quiet Operator responsive TUI with a linear
148
198
  `❯` user / `⏺` assistant conversation, `✻` thinking activity, and `!` shell
149
199
  composer grammar, compact stable tool rows, responsive density,
@@ -407,7 +457,7 @@ normal/low-capability full-frame p95 budgets of `<16.7/<33 ms`.
407
457
  `npm run test:coverage` measures all production code under `src/**` with V8 and
408
458
  enforces global floors of 79% statements, 70% branches, 85% functions, and 81% lines,
409
459
  and rejects any production runtime module with zero covered statements (while allowing
410
- type-only modules). `npm run test:fixtures` executes the 75-behavior native contract; 67 behaviors
460
+ type-only modules). `npm run test:fixtures` executes the 76-behavior native contract; 68 behaviors
411
461
  are qualified and 8 are explicitly excluded. Schema-v2 risk tiers and executable evidence dimensions
412
462
  are enforced fail-closed. `npm run verify:fixture-contracts` performs the structural check and is part
413
463
  of `npm run check`.
@@ -1 +1 @@
1
- {"schema_version":"1.0","source_revision":"git:ef170d59a802ce59a82fd5cae23c0adaff912d8a","source_dirty":false,"artifact_sha256":"sha256:60ff6f473f030f3a5cd5c2db430f866898607b9f1c205f55ed68abcc2eff1b0d"}
1
+ {"schema_version":"1.0","source_revision":"git:12b1ee6385af5b44620dacf57a70067018a1d5c6","source_dirty":false,"artifact_sha256":"sha256:7667434c7ce48becd3905fd7ba20b81c8b62e9d49dec911e122cbadf672fb5c1"}
@@ -72,6 +72,7 @@ import { describeClaudePlugin, initClaudePlugin, installClaudePlugin, loadClaude
72
72
  import { addClaudeMarketplace, disableAllNativePlugins, installClaudeMarketplacePlugin, listClaudeMarketplaceAvailablePlugins, listNativePluginRecords, readClaudeKnownMarketplaces, removeClaudeMarketplace, setNativePluginEnabled, saveClaudePluginConfig, uninstallNativePlugin, updateClaudeMarketplace, updateNativePlugin, validateClaudeMarketplace, } from './plugins/claude-plugin-marketplace.js';
73
73
  import { executeClaudePluginEvalCommand, PLUGIN_EVAL_HELP, } from './plugins/claude-plugin-eval.js';
74
74
  import { executeProjectEvalCommand, PROJECT_EVAL_HELP, } from './evals/project-eval.js';
75
+ import { isEvalToolCallPreapproved } from './evals/eval-tool-admission.js';
75
76
  import { PROJECT_EVAL_COMPARE_HELP } from './evals/project-eval-comparison.js';
76
77
  import { loadPraxisBuildIdentity } from './platform/praxis-build-identity.js';
77
78
  import { CLAUDE_PLUGIN_PRUNE_HELP, CLAUDE_PLUGIN_TAG_HELP, executeClaudePluginPrune, planClaudePluginPrune, tagClaudePlugin, } from './plugins/claude-plugin-maintenance.js';
@@ -2323,7 +2324,12 @@ const defaultPluginEvalRuntimeFactory = {
2323
2324
  USERPROFILE: options.home,
2324
2325
  },
2325
2326
  providerEnvironment: process.env,
2326
- isSessionActionApproved: (call) => options.allowedTools.includes(call.name),
2327
+ isSessionActionApproved: (call) => isEvalToolCallPreapproved(call, {
2328
+ cwd: options.cwd,
2329
+ homeDirectory: options.home,
2330
+ allowedTools: options.allowedTools,
2331
+ additionalDirectories: options.addDirs,
2332
+ }),
2327
2333
  controls: {
2328
2334
  ...DEFAULT_CLI_CONTROLS,
2329
2335
  dataPlane: options.dataPlane,
@@ -2336,6 +2342,10 @@ const defaultPluginEvalRuntimeFactory = {
2336
2342
  tools: [...options.allowedTools],
2337
2343
  permissionMode: 'dontAsk',
2338
2344
  ...(options.model ? { model: options.model } : {}),
2345
+ ...(options.provider ? { provider: options.provider } : {}),
2346
+ ...(options.providerProfile
2347
+ ? { providerProfile: options.providerProfile }
2348
+ : {}),
2339
2349
  ...(options.appendSystemPrompt
2340
2350
  ? { appendSystemPrompt: options.appendSystemPrompt }
2341
2351
  : {}),
@@ -2368,6 +2378,10 @@ const defaultProjectEvalRuntimeFactory = {
2368
2378
  cwd: options.cwd,
2369
2379
  environment,
2370
2380
  ...(options.model === undefined ? {} : { model: options.model }),
2381
+ ...(options.provider === undefined ? {} : { provider: options.provider }),
2382
+ ...(options.providerProfile === undefined
2383
+ ? {}
2384
+ : { profile: options.providerProfile }),
2371
2385
  includeSettings: true,
2372
2386
  includeProjectSettings: false,
2373
2387
  });
@@ -5116,6 +5130,14 @@ async function execute(argv, io, dependencies, signal) {
5116
5130
  io.stdout(PROJECT_EVAL_COMPARE_HELP);
5117
5131
  return 0;
5118
5132
  }
5133
+ if (special.args[1] === 'qualify' &&
5134
+ special.args
5135
+ .slice(2)
5136
+ .some((value) => value === '-h' || value === '--help')) {
5137
+ const { HELD_OUT_QUALIFICATION_HELP } = await import('./evals/held-out-qualification.js');
5138
+ io.stdout(HELD_OUT_QUALIFICATION_HELP);
5139
+ return 0;
5140
+ }
5119
5141
  if (special.args
5120
5142
  .slice(1)
5121
5143
  .some((value) => value === '-h' || value === '--help')) {
@@ -5124,12 +5146,25 @@ async function execute(argv, io, dependencies, signal) {
5124
5146
  }
5125
5147
  if (!dependencies.projectEval)
5126
5148
  throw new Error('Project eval unavailable');
5127
- return executeProjectEvalCommand([
5149
+ const prefixFlags = [
5128
5150
  ...(specialPrefix?.model === undefined
5129
5151
  ? []
5130
5152
  : ['--model', specialPrefix.model]),
5131
- ...special.args.slice(1),
5132
- ], io, dependencies.projectEval, process.cwd(), signal);
5153
+ ...(specialPrefix?.provider === undefined
5154
+ ? []
5155
+ : ['--provider', specialPrefix.provider]),
5156
+ ...(specialPrefix?.providerProfile === undefined
5157
+ ? []
5158
+ : ['--profile', specialPrefix.providerProfile]),
5159
+ ];
5160
+ if (special.args[1] === 'compare' && prefixFlags.length > 0)
5161
+ throw new Error('eval compare does not accept global provider, profile, or model options');
5162
+ const evalArgs = special.args[1] === 'qualify'
5163
+ ? ['qualify', ...prefixFlags, ...special.args.slice(2)]
5164
+ : special.args[1] === 'compare'
5165
+ ? ['compare', ...special.args.slice(2)]
5166
+ : [...prefixFlags, ...special.args.slice(1)];
5167
+ return executeProjectEvalCommand(evalArgs, io, dependencies.projectEval, process.cwd(), signal);
5133
5168
  }
5134
5169
  if ((special.args[0] === 'plugin' || special.args[0] === 'plugins') &&
5135
5170
  special.args[1] === 'eval') {
@@ -14,6 +14,8 @@ export interface EvalRuntimeFactoryOptions {
14
14
  cwd: string;
15
15
  configRoot: string;
16
16
  home: string;
17
+ provider?: string;
18
+ providerProfile?: string;
17
19
  model?: string;
18
20
  maxTurns: number;
19
21
  pluginDirectories?: readonly string[];
@@ -0,0 +1,9 @@
1
+ import type { ModelToolCall } from '../core/runtime.js';
2
+ export interface EvalToolAdmissionOptions {
3
+ cwd: string;
4
+ homeDirectory: string;
5
+ allowedTools: readonly string[];
6
+ additionalDirectories: readonly string[];
7
+ }
8
+ export declare function isEvalToolCallPreapproved(call: ModelToolCall, options: EvalToolAdmissionOptions): boolean;
9
+ //# sourceMappingURL=eval-tool-admission.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { resolve } from 'node:path';
2
+ import { validateBashSemantics } from '../permissions/bash-ast.js';
3
+ import { validateBashPathSafety } from '../permissions/bash-path-safety.js';
4
+ function commandInput(input) {
5
+ if (!input || typeof input !== 'object' || Array.isArray(input))
6
+ return;
7
+ const command = input.command;
8
+ return typeof command === 'string' ? command : undefined;
9
+ }
10
+ export function isEvalToolCallPreapproved(call, options) {
11
+ if (!options.allowedTools.includes(call.name))
12
+ return false;
13
+ if (call.name !== 'Bash')
14
+ return true;
15
+ const command = commandInput(call.input);
16
+ if (command === undefined)
17
+ return false;
18
+ if (!validateBashSemantics(command).safe)
19
+ return false;
20
+ const cwd = resolve(options.cwd);
21
+ const roots = [
22
+ cwd,
23
+ ...options.additionalDirectories.map((directory) => resolve(cwd, directory)),
24
+ ];
25
+ return validateBashPathSafety(command, {
26
+ cwd,
27
+ homeDirectory: options.homeDirectory,
28
+ readRoots: roots,
29
+ writeRoots: roots,
30
+ permissionMode: 'acceptEdits',
31
+ }).safe;
32
+ }
33
+ //# sourceMappingURL=eval-tool-admission.js.map
@@ -14,8 +14,8 @@ export interface HeldOutCorpusRepository {
14
14
  export interface HeldOutCorpus {
15
15
  readonly root: string;
16
16
  readonly schemaVersion: '1.0';
17
- readonly id: 'praxis-held-out-v1';
18
- readonly version: 1;
17
+ readonly id: `praxis-held-out-v${number}`;
18
+ readonly version: number;
19
19
  readonly split: 'held-out';
20
20
  readonly repetitions: 3;
21
21
  readonly policy: HeldOutCorpusPolicy;
@@ -8,7 +8,6 @@ const MAX_MANIFEST_BYTES = 1024 * 1024;
8
8
  const MAX_FILES = 4096;
9
9
  const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
10
10
  const MAX_ENTRIES = 16_384;
11
- const REQUIRED_TAGS = ['held-out', 'praxis-held-out-v1'];
12
11
  const FORBIDDEN_TAGS = new Set([
13
12
  'tuning',
14
13
  'calibration',
@@ -22,6 +21,20 @@ const MUTATION_GLOB_OPTIONS = {
22
21
  nocomment: true,
23
22
  nonegate: true,
24
23
  };
24
+ function validateCorpusIdentity(id, version) {
25
+ if (typeof id !== 'string' || !/^praxis-held-out-v[1-9]\d*$/u.test(id))
26
+ throw new Error('Unsupported corpus id');
27
+ if (typeof version !== 'number' ||
28
+ !Number.isSafeInteger(version) ||
29
+ version <= 0)
30
+ throw new Error('Unsupported corpus version');
31
+ if (id === 'praxis-held-out-v1' && version !== 1)
32
+ throw new Error('Unsupported corpus version');
33
+ const suffix = Number(id.slice('praxis-held-out-v'.length));
34
+ if (!Number.isSafeInteger(suffix) || suffix !== version)
35
+ throw new Error('Corpus id and version do not match');
36
+ return { id: id, version };
37
+ }
25
38
  function object(value, label) {
26
39
  if (!value || typeof value !== 'object' || Array.isArray(value))
27
40
  throw new Error(`${label} must be an object`);
@@ -175,10 +188,7 @@ export async function loadHeldOutCorpus(root) {
175
188
  ], 'corpus');
176
189
  if (manifest.schema_version !== '1.0')
177
190
  throw new Error('Unsupported corpus schema_version');
178
- if (manifest.id !== 'praxis-held-out-v1')
179
- throw new Error('Unsupported corpus id');
180
- if (manifest.version !== 1)
181
- throw new Error('Unsupported corpus version');
191
+ const identity = validateCorpusIdentity(manifest.id, manifest.version);
182
192
  if (manifest.split !== 'held-out')
183
193
  throw new Error('corpus split must be held-out');
184
194
  if (manifest.repetitions !== 3)
@@ -270,7 +280,8 @@ export async function loadHeldOutCorpus(root) {
270
280
  const forbidden = item.expect.forbiddenChangedPaths.map((pattern) => new Minimatch(pattern, MUTATION_GLOB_OPTIONS));
271
281
  if (mutationPaths.some((path) => forbidden.some((matcher) => matcher.match(path))))
272
282
  throw new Error(`${item.name} mutation paths overlap`);
273
- if (!REQUIRED_TAGS.every((tag) => item.tags.includes(tag)) ||
283
+ if (!item.tags.includes('held-out') ||
284
+ !item.tags.includes(identity.id) ||
274
285
  !item.tags.includes(repository.id))
275
286
  throw new Error(`${item.name} is missing required held-out tags`);
276
287
  if (item.tags.some((tag) => FORBIDDEN_TAGS.has(tag)))
@@ -285,8 +296,8 @@ export async function loadHeldOutCorpus(root) {
285
296
  return {
286
297
  root: corpusRoot,
287
298
  schemaVersion: '1.0',
288
- id: 'praxis-held-out-v1',
289
- version: 1,
299
+ id: identity.id,
300
+ version: identity.version,
290
301
  split: 'held-out',
291
302
  repetitions: 3,
292
303
  policy: {
@@ -0,0 +1,140 @@
1
+ import { type PraxisBuildIdentity } from '../platform/praxis-build-identity.js';
2
+ import { type ProjectEvalIdentity } from './project-eval-identity.js';
3
+ import { type ProjectEvalComparisonMetric } from './project-eval-comparison.js';
4
+ import type { ProjectEvalDependencies, ProjectEvalRunSummary } from './project-eval.js';
5
+ /**
6
+ * Qualification orchestration for the immutable held-out corpus.
7
+ *
8
+ * This module owns the qualification contract and evidence envelope; actual
9
+ * case execution remains in the Project Eval runner.
10
+ */
11
+ export declare const HELD_OUT_QUALIFICATION_HELP = "Usage: praxis eval qualify [options] <corpus>\n\nQualify an explicitly pinned provider/model against the immutable held-out corpus.\n\nOptions:\n\nRequired:\n --provider <id> Provider identifier\n --profile <id> Provider profile identifier\n --model <id> Model identifier\n --confirm-held-out <id@sha256> Confirm the exact held-out corpus digest\n --run-verification Run the declared case verifiers\n --output-dir <dir> Write qualification artifacts here\n --allow-tools <rules> Grant gated tools; comma-separated and repeatable\n\nOptional:\n --baseline <qualification-result.json> Compare against a completed result\n --keep-temp Preserve temporary workspaces\n --json Print exactly one qualification JSON value\n --verbose Print run progress to stderr\n -h, --help Display help";
12
+ export interface HeldOutQualificationOptions {
13
+ corpus?: string;
14
+ provider?: string;
15
+ profile?: string;
16
+ model?: string;
17
+ confirmHeldOut?: string;
18
+ allowTools: string[];
19
+ runVerification: boolean;
20
+ outputDir?: string;
21
+ baseline?: string;
22
+ keepTemp: boolean;
23
+ json: boolean;
24
+ verbose: boolean;
25
+ help?: true;
26
+ }
27
+ /** Parse the strict, side-effect-free qualification command line. */
28
+ export declare function parseHeldOutQualificationOptions(argv: readonly string[]): HeldOutQualificationOptions;
29
+ export interface HeldOutQualificationAggregateReference {
30
+ repository: string;
31
+ path: string;
32
+ sha256: `sha256:${string}`;
33
+ identity_sha256: `sha256:${string}`;
34
+ }
35
+ export interface HeldOutQualificationRun {
36
+ repository: string;
37
+ case: string;
38
+ run: number;
39
+ passed: boolean;
40
+ safety_passed: boolean;
41
+ verifier_satisfied: boolean;
42
+ turns: number;
43
+ duration_ms: number;
44
+ usage_known: boolean;
45
+ cost_known: boolean;
46
+ usage: ProjectEvalRunSummary['usage'];
47
+ cost_usd: number | null;
48
+ identity: ProjectEvalIdentity;
49
+ }
50
+ export interface HeldOutQualificationCaseSummary {
51
+ repository: string;
52
+ case: string;
53
+ repetitions: number;
54
+ passed: number;
55
+ failed: number;
56
+ safety_passed: number;
57
+ safety_failed: number;
58
+ verifier_satisfied_runs: number;
59
+ verifier_unsatisfied_runs: number;
60
+ passed_all: boolean;
61
+ safety_passed_all: boolean;
62
+ verifier_satisfied: boolean;
63
+ }
64
+ export interface HeldOutQualificationResult {
65
+ schema_version: '1.0';
66
+ corpus: {
67
+ id: string;
68
+ version: number;
69
+ content_sha256: `sha256:${string}`;
70
+ repository_count: number;
71
+ task_count: number;
72
+ repetitions: number;
73
+ planned_run_count: number;
74
+ };
75
+ provider: string;
76
+ profile: string;
77
+ protocol: string;
78
+ model: string;
79
+ endpoint_sha256: `sha256:${string}`;
80
+ plan_sha256: `sha256:${string}`;
81
+ praxis_version: string;
82
+ build: PraxisBuildIdentity;
83
+ node_version: string;
84
+ platform: string;
85
+ architecture: string;
86
+ start: string;
87
+ duration_ms: number;
88
+ aggregates: readonly HeldOutQualificationAggregateReference[];
89
+ planned_run_count: number;
90
+ completed_run_count: number;
91
+ passed: number;
92
+ failed: number;
93
+ safety_passed: number;
94
+ safety_failed: number;
95
+ verifier_satisfied_runs: number;
96
+ verifier_unsatisfied_runs: number;
97
+ usage_known_runs: number;
98
+ usage_unknown_runs: number;
99
+ cost_known_runs: number;
100
+ cost_unknown_runs: number;
101
+ usage_totals: {
102
+ input_tokens: number;
103
+ output_tokens: number;
104
+ cache_read_input_tokens: number;
105
+ cache_creation_input_tokens: number;
106
+ web_search_requests: number;
107
+ } | null;
108
+ known_cost_total_usd: number | null;
109
+ median_turns: number;
110
+ p95_turns: number;
111
+ median_duration_ms: number;
112
+ p95_duration_ms: number;
113
+ cases: readonly HeldOutQualificationCaseSummary[];
114
+ runs: readonly HeldOutQualificationRun[];
115
+ baseline?: {
116
+ source_sha256: `sha256:${string}`;
117
+ summary: {
118
+ passed: number;
119
+ failed: number;
120
+ pass_rate: number;
121
+ safety_pass_rate: number;
122
+ planned_run_count: number;
123
+ };
124
+ };
125
+ regressions: readonly {
126
+ case: string;
127
+ run: number;
128
+ baseline_passed: boolean;
129
+ candidate_passed: boolean;
130
+ }[];
131
+ metric_deltas: Readonly<Record<string, ProjectEvalComparisonMetric<number | null>>>;
132
+ qualified: boolean | null;
133
+ optimization_claim_allowed: boolean;
134
+ }
135
+ export type HeldOutQualificationDependencies = ProjectEvalDependencies;
136
+ export declare function executeHeldOutQualificationCommand(argv: readonly string[], io: {
137
+ stdout(message: string): void;
138
+ stderr(message: string): void;
139
+ }, dependencies: HeldOutQualificationDependencies, callerCwd?: string, signal?: AbortSignal): Promise<number>;
140
+ //# sourceMappingURL=held-out-qualification.d.ts.map