pi-python-helper 0.4.1 → 0.4.3

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/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ does not guarantee a stable public tool schema.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.3] - 2026-09-21
11
+
12
+ ### Fixed
13
+
14
+ - A project named through `path` now runs `uv` and `pytest` in the resolved project root instead of `ctx.cwd`. `py_sync`, `py_test`, `py_test_select`, `py_validation_bundle`, and `py_tdd_checkpoint` previously generated and executed their commands in the session directory, so a project reached through `path` failed with "No `pyproject.toml` found in current directory" or ran the wrong tests.
15
+
16
+ ### Added
17
+
18
+ - `py_tdd_checkpoint` accepts an optional `path` for git discovery.
19
+
20
+ ## [0.4.2] - 2026-09-21
21
+
22
+ ### Removed
23
+
24
+ - `src/core/safety.ts` is gone. The tools gate state changes with an explicit `execute: true`, so the Python risk classifier had no production caller and was maintained dead code. The expected classification of Python commands is kept as an executable spec in `test/core.test.ts`, exercised against `pi-helper-core`'s classifier.
25
+
10
26
  ## [0.4.1] - 2026-09-21
11
27
 
12
28
  ### Changed
package/docs/tools.md CHANGED
@@ -415,6 +415,7 @@ Check whether production Python changes have related test changes before impleme
415
415
  | 파라미터 | 타입 | 필수 | 설명 |
416
416
  |---|---|---|---|
417
417
  | `changedPaths` | `array<string>` | 아니오 | maxItems 500 |
418
+ | `path` | `string` | 아니오 | — |
418
419
  | `testChangedPaths` | `array<string>` | 아니오 | maxItems 500 |
419
420
 
420
421
  **프롬프트 가이드라인**
@@ -89,7 +89,7 @@ export function registerTestingTools(pi: Pi): void {
89
89
  let changed = params.changedPaths ?? [];
90
90
  let changedSource = 'argument';
91
91
  if (!changed.length) {
92
- const discovered = await changedPaths(ctx.cwd, signal);
92
+ const discovered = await changedPaths(root, signal);
93
93
  changed = discovered.paths;
94
94
  changedSource = discovered.source === 'git' ? 'git' : (discovered.error ?? 'none');
95
95
  }
@@ -109,7 +109,7 @@ export function registerTestingTools(pi: Pi): void {
109
109
  (entry) => isRunnableTestFile(entry.path) || basename(entry.path) === 'conftest.py',
110
110
  )
111
111
  .map((entry) => entry.path);
112
- const command = pytestCommand(ctx.cwd, {
112
+ const command = pytestCommand(root, {
113
113
  targets: selection.fellBackToAll ? [] : targets,
114
114
  });
115
115
 
@@ -232,7 +232,7 @@ export function registerTestingTools(pi: Pi): void {
232
232
  const started = Date.now();
233
233
  try {
234
234
  const root = (await resolveProjectRoot(ctx.cwd, params.path)) ?? ctx.cwd;
235
- const command = pytestCommand(ctx.cwd, {
235
+ const command = pytestCommand(root, {
236
236
  targets: params.targets,
237
237
  lastFailed: params.lastFailed,
238
238
  keyword: params.keyword,
@@ -261,7 +261,7 @@ export function registerTestingTools(pi: Pi): void {
261
261
  }
262
262
 
263
263
  const run = await runCommand(command.executable, command.args, {
264
- cwd: ctx.cwd,
264
+ cwd: root,
265
265
  signal,
266
266
  timeoutMs: (params.timeoutSeconds ?? 900) * 1000,
267
267
  maxBytes: 512 * 1024,
@@ -73,7 +73,7 @@ export function registerValidationTools(pi: Pi): void {
73
73
  const root = (await resolveProjectRoot(ctx.cwd, params.path)) ?? ctx.cwd;
74
74
  const mode = params.mode ?? 'check';
75
75
  const extras = params.extras ?? 'all';
76
- const command = mode === 'check' ? uvLockCheck(ctx.cwd) : uvSyncFrozen(ctx.cwd, { extras });
76
+ const command = mode === 'check' ? uvLockCheck(root) : uvSyncFrozen(root, { extras });
77
77
  const lockPresent = await isFile(join(root, 'uv.lock'));
78
78
 
79
79
  if (!params.execute) {
@@ -109,7 +109,7 @@ export function registerValidationTools(pi: Pi): void {
109
109
  }
110
110
 
111
111
  const run = await runCommand(command.executable, command.args, {
112
- cwd: ctx.cwd,
112
+ cwd: root,
113
113
  signal,
114
114
  timeoutMs: (params.timeoutSeconds ?? 600) * 1000,
115
115
  maxBytes: 256 * 1024,
@@ -296,9 +296,9 @@ export function registerValidationTools(pi: Pi): void {
296
296
  try {
297
297
  const root = (await resolveProjectRoot(ctx.cwd, params.path)) ?? ctx.cwd;
298
298
  const lockPresent = await isFile(join(root, 'uv.lock'));
299
- const lock = uvLockCheck(ctx.cwd);
300
- const sync = uvSyncFrozen(ctx.cwd);
301
- const test = pytestCommand(ctx.cwd, { targets: params.targets });
299
+ const lock = uvLockCheck(root);
300
+ const sync = uvSyncFrozen(root);
301
+ const test = pytestCommand(root, { targets: params.targets });
302
302
  const timeoutMs = (params.timeoutSeconds ?? 1800) * 1000;
303
303
  const runQuality = params.quality ?? true;
304
304
 
@@ -317,7 +317,7 @@ export function registerValidationTools(pi: Pi): void {
317
317
  toolConfiguration: manifestScan.payload?.manifest?.toolConfiguration,
318
318
  })
319
319
  : [];
320
- const quality = qualityCommands(ctx.cwd, runners);
320
+ const quality = qualityCommands(root, runners);
321
321
  const commands = [lock, sync, test, ...quality];
322
322
 
323
323
  if (!params.execute) {
@@ -369,14 +369,14 @@ export function registerValidationTools(pi: Pi): void {
369
369
 
370
370
  const lockRun = lockPresent
371
371
  ? await runCommand(lock.executable, lock.args, {
372
- cwd: ctx.cwd,
372
+ cwd: root,
373
373
  signal,
374
374
  timeoutMs,
375
375
  maxBytes: 256 * 1024,
376
376
  })
377
377
  : undefined;
378
378
  const syncRun = await runCommand(sync.executable, sync.args, {
379
- cwd: ctx.cwd,
379
+ cwd: root,
380
380
  signal,
381
381
  timeoutMs,
382
382
  maxBytes: 256 * 1024,
@@ -400,7 +400,7 @@ export function registerValidationTools(pi: Pi): void {
400
400
 
401
401
  const testRun = syncOk
402
402
  ? await runCommand(test.executable, test.args, {
403
- cwd: ctx.cwd,
403
+ cwd: root,
404
404
  signal,
405
405
  timeoutMs,
406
406
  maxBytes: 512 * 1024,
@@ -466,7 +466,7 @@ export function registerValidationTools(pi: Pi): void {
466
466
  }
467
467
  const preview = quality[index];
468
468
  const run = await runCommand(preview.executable, preview.args, {
469
- cwd: ctx.cwd,
469
+ cwd: root,
470
470
  signal,
471
471
  timeoutMs,
472
472
  maxBytes: 256 * 1024,
@@ -615,14 +615,16 @@ export function registerValidationTools(pi: Pi): void {
615
615
  parameters: Type.Object({
616
616
  changedPaths: Type.Optional(Type.Array(Type.String(), { maxItems: 500 })),
617
617
  testChangedPaths: Type.Optional(Type.Array(Type.String(), { maxItems: 500 })),
618
+ path: Type.Optional(Type.String()),
618
619
  }),
619
620
  async execute(_id, params, signal, _update, ctx) {
620
621
  const started = Date.now();
622
+ const root = (await resolveProjectRoot(ctx.cwd, params.path)) ?? ctx.cwd;
621
623
  let changedPaths = params.changedPaths ?? [];
622
624
  let source = 'argument';
623
625
  if (!changedPaths.length) {
624
626
  const diff = await runCommand('git', ['diff', '--name-only', 'HEAD'], {
625
- cwd: ctx.cwd,
627
+ cwd: root,
626
628
  signal,
627
629
  timeoutMs: 10000,
628
630
  maxBytes: 100_000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-python-helper",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Python (uv) development tools for the pi coding agent",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -1,109 +0,0 @@
1
- /**
2
- * Python's risk rules for the shared classifier.
3
- *
4
- * Python has no equivalent of a `cmd_vel` topic that deterministically signals
5
- * actuation, so risk is read from the command text. Segment splitting, safe
6
- * override precedence, and the compound-command merge live in
7
- * `pi-helper-core`; this module supplies only the package-manager, environment,
8
- * and migration rules that are specific to Python. The universal rules (git
9
- * history, file deletion, destructive SQL, containers, pipe-to-shell) are
10
- * applied by the core and must not be repeated here.
11
- */
12
- import {
13
- classifyCommand as coreClassifyCommand,
14
- isMutatingCommand as coreIsMutatingCommand,
15
- riskOf as coreRiskOf,
16
- type CommandRisk,
17
- type RiskRule,
18
- type SafetyRules,
19
- } from 'pi-helper-core';
20
-
21
- export { splitCommandSegments } from 'pi-helper-core';
22
- export type { CommandClassification, CommandRisk } from 'pi-helper-core';
23
-
24
- /**
25
- * Safe overrides are matched before risk patterns so a read-only form does not
26
- * inherit the risk of the command it qualifies. Only the Python-specific
27
- * read-only forms live here; `--check`, `--dry-run`, and `--collect-only` are
28
- * already universal overrides in the core.
29
- */
30
- const PYTHON_SAFE_OVERRIDES: RegExp[] = [
31
- /\buv\s+(?:tree|export|version|help)\b/,
32
- /\buv\s+pip\s+(?:list|freeze|check)\b/,
33
- /\bruff\s+(?:check|format)\b[^&|;]*(?:--diff|--check|--no-cache)\b/,
34
- /\bmypy\b[^&|;]*--no-incremental\b/,
35
- ];
36
-
37
- const PYTHON_RISK_PATTERNS: RiskRule[] = [
38
- // Irreversible: cannot be undone by a local revert.
39
- {
40
- risk: 'irreversible',
41
- pattern: /\b(?:uv|poetry|flit|hatch)\s+publish\b|\btwine\s+upload\b/,
42
- reason: 'Publishing to a package index is public and cannot be retracted.',
43
- },
44
- {
45
- risk: 'irreversible',
46
- pattern: /\b(?:conda|mamba)\s+env\s+remove\b|\bconda\s+remove\b[^&|;]*--all\b/,
47
- reason: 'Removing an environment destroys installed state.',
48
- },
49
- {
50
- risk: 'irreversible',
51
- pattern: /\b(?:alembic|manage\.py)\b[^&|;]*(?:downgrade|\bzero\b)/,
52
- reason: 'Reversing a database migration can drop data.',
53
- },
54
-
55
- // Mutating: changes project, environment, or remote state but is recoverable.
56
- {
57
- risk: 'mutating',
58
- pattern: /\b(?:uv|pdm|poetry)\s+(?:add|remove|sync|lock|update|venv)\b/,
59
- reason: 'Modifies the environment or the lockfile.',
60
- },
61
- {
62
- risk: 'mutating',
63
- pattern: /\buv\s+pip\s+(?:install|uninstall|sync)\b/,
64
- reason: 'Changes installed packages in the active environment.',
65
- },
66
- {
67
- risk: 'mutating',
68
- pattern: /\b(?:pip|pip3)\s+(?:install|uninstall)\b/,
69
- reason: 'Changes installed packages in the active environment.',
70
- },
71
- {
72
- risk: 'mutating',
73
- pattern: /\b(?:conda|mamba)\s+(?:install|create|update|remove)\b/,
74
- reason: 'Changes conda environment state.',
75
- },
76
- {
77
- risk: 'mutating',
78
- pattern: /\b(?:alembic|manage\.py)\b[^&|;]*\b(?:upgrade|migrate|makemigrations)\b/,
79
- reason: 'Applies a schema change to a database.',
80
- },
81
- {
82
- risk: 'mutating',
83
- pattern: /\bpre-commit\s+(?:install|autoupdate|run|clean)\b/,
84
- reason: 'Rewrites hook configuration or working tree files.',
85
- },
86
- ];
87
-
88
- const PYTHON_RULES: Partial<SafetyRules> = {
89
- safeOverrides: PYTHON_SAFE_OVERRIDES,
90
- patterns: PYTHON_RISK_PATTERNS,
91
- };
92
-
93
- /**
94
- * Classify a shell command by the highest risk of its segments. Used to warn
95
- * before a tool runs something that cannot be undone, and to gate this
96
- * package's own environment-modifying commands behind explicit opt-in.
97
- */
98
- export function classifyCommand(command: string): import('pi-helper-core').CommandClassification {
99
- return coreClassifyCommand(command, PYTHON_RULES);
100
- }
101
-
102
- export function isMutatingCommand(command: string): boolean {
103
- return coreIsMutatingCommand(command, PYTHON_RULES);
104
- }
105
-
106
- /** Commands that this package runs itself always carry a known risk class. */
107
- export function riskOf(args: string[]): CommandRisk {
108
- return coreRiskOf(args, PYTHON_RULES);
109
- }