trap-cli 0.0.1__tar.gz → 0.0.2__tar.gz

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.
Files changed (55) hide show
  1. {trap_cli-0.0.1 → trap_cli-0.0.2}/PKG-INFO +1 -1
  2. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/_version.py +2 -2
  3. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/cli/__init__.py +68 -9
  4. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/git_ops/local.py +29 -16
  5. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/models/provenance.py +3 -1
  6. {trap_cli-0.0.1 → trap_cli-0.0.2}/.gitignore +0 -0
  7. {trap_cli-0.0.1 → trap_cli-0.0.2}/LICENSE +0 -0
  8. {trap_cli-0.0.1 → trap_cli-0.0.2}/README.md +0 -0
  9. {trap_cli-0.0.1 → trap_cli-0.0.2}/examples/echo/task/README.md +0 -0
  10. {trap_cli-0.0.1 → trap_cli-0.0.2}/pyproject.toml +0 -0
  11. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/__init__.py +0 -0
  12. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/auth/__init__.py +0 -0
  13. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/auth/client.py +0 -0
  14. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/auth/login.py +0 -0
  15. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/auth/oauth.py +0 -0
  16. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/auth/store.py +0 -0
  17. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/cli/_auth.py +0 -0
  18. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/cost/__init__.py +0 -0
  19. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/cost/calculator.py +0 -0
  20. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/cost/providers.py +0 -0
  21. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/cost/proxy.py +0 -0
  22. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/display/__init__.py +0 -0
  23. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/display/progress.py +0 -0
  24. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/display/submit.py +0 -0
  25. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/environment/__init__.py +0 -0
  26. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/environment/detector.py +0 -0
  27. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/git_ops/__init__.py +0 -0
  28. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/git_ops/base.py +0 -0
  29. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/git_ops/remote.py +0 -0
  30. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/git_ops/rev.py +0 -0
  31. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/git_ops/url.py +0 -0
  32. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/loader/__init__.py +0 -0
  33. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/loader/errors.py +0 -0
  34. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/loader/trap_yaml.py +0 -0
  35. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/loader/traptask_yaml.py +0 -0
  36. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/models/__init__.py +0 -0
  37. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/models/cost.py +0 -0
  38. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/models/environment.py +0 -0
  39. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/models/report.py +0 -0
  40. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/models/results.py +0 -0
  41. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/models/trap_yaml.py +0 -0
  42. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/models/traptask_yaml.py +0 -0
  43. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/report/__init__.py +0 -0
  44. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/report/base.py +0 -0
  45. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/report/handle.py +0 -0
  46. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/report/json.py +0 -0
  47. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/report/rich.py +0 -0
  48. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/runner/__init__.py +0 -0
  49. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/runner/capture.py +0 -0
  50. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/runner/grader.py +0 -0
  51. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/runner/judge.py +0 -0
  52. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/runner/layout.py +0 -0
  53. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/runner/proc.py +0 -0
  54. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/runner/solution.py +0 -0
  55. {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/runner/task.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trap-cli
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: tp — non-invasive CLI testing framework for AI workflows. Submits results to trapstreet.run.
5
5
  Project-URL: Homepage, https://trapstreet.run
6
6
  Project-URL: Repository, https://github.com/trapstreet/trap
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.0.1'
22
- __version_tuple__ = version_tuple = (0, 0, 1)
21
+ __version__ = version = '0.0.2'
22
+ __version_tuple__ = version_tuple = (0, 0, 2)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -23,6 +23,8 @@ from trap.runner import TaskRunner
23
23
  app = typer.Typer(help="AI prompt / agent / workflow / testing framework.")
24
24
  app.add_typer(auth_app, name="auth")
25
25
  console = Console()
26
+ # Warnings go to stderr so `-o json` stdout stays machine-parseable.
27
+ err_console = Console(stderr=True)
26
28
 
27
29
 
28
30
  def _die(msg: object) -> typer.Exit:
@@ -42,7 +44,7 @@ def _confirm_remote(url: str, *, trust: bool) -> None:
42
44
  runs); refuses (rather than running silently) when there's no TTY to confirm at."""
43
45
  if trust:
44
46
  return
45
- console.print(
47
+ err_console.print(
46
48
  "[yellow]⚠ about to download and RUN code from a remote source:[/yellow]\n"
47
49
  f" [bold]{url}[/bold]\n"
48
50
  " trap will execute its setup command, the solution, and any judge/grader\n"
@@ -57,6 +59,39 @@ def _confirm_remote(url: str, *, trust: bool) -> None:
57
59
  raise typer.Exit(code=1)
58
60
 
59
61
 
62
+ def _confirm_unanchored(provenance: Provenance, *, allow: bool) -> None:
63
+ """Gate run/submit when a side has no anchored git provenance.
64
+
65
+ trapstreet accepts such uploads but its leaderboard hides runs it cannot pin to
66
+ a commit — make the user acknowledge that up front instead of discovering it on
67
+ the site. The reason travels in the provenance's `issue` field, so `tp submit`
68
+ (which reads the saved report rather than re-probing the checkouts) names it
69
+ too. `allow` (--allow-unanchored / TRAP_ALLOW_UNANCHORED, for CI) keeps the
70
+ warning but skips the prompt; with no TTY and no `allow` it refuses.
71
+ """
72
+ sides = {"solution": provenance.solution, "task": provenance.task}
73
+ missing = {name: side.issue for name, side in sides.items() if not side.repo}
74
+ if not missing:
75
+ return
76
+ for name, reason in missing.items():
77
+ suffix = f" ({reason})" if reason else ""
78
+ err_console.print(f"[yellow]⚠ {name} has no git provenance{suffix}[/yellow]")
79
+ err_console.print(
80
+ "[yellow] trapstreet accepts the submission, but the leaderboard hides runs that "
81
+ "aren't anchored to a commit on a remote — run from a clean, committed checkout "
82
+ "with an origin remote to make it rankable.[/yellow]"
83
+ )
84
+ if allow:
85
+ return
86
+ if not sys.stdin.isatty():
87
+ raise _die(
88
+ "unanchored provenance needs confirmation; pass --allow-unanchored "
89
+ "(or set TRAP_ALLOW_UNANCHORED=1) to proceed non-interactively"
90
+ )
91
+ if not typer.confirm("Continue anyway?", default=False):
92
+ raise typer.Exit(code=1)
93
+
94
+
60
95
  @app.command()
61
96
  def run(
62
97
  task: Annotated[str | None, typer.Argument()] = None,
@@ -76,6 +111,14 @@ def run(
76
111
  "(git+ URL) solution/task. Also settable via TRAP_TRUST_REMOTE=1.",
77
112
  ),
78
113
  ] = False,
114
+ allow_unanchored: Annotated[
115
+ bool,
116
+ typer.Option(
117
+ "--allow-unanchored",
118
+ help="Skip the confirmation when the run has no git provenance (the "
119
+ "leaderboard hides such runs). Also settable via TRAP_ALLOW_UNANCHORED=1.",
120
+ ),
121
+ ] = False,
79
122
  tags: Annotated[list[str] | None, typer.Option("--tag", "-t")] = None,
80
123
  output: Annotated[OutputFormat, typer.Option("--output", "-o")] = OutputFormat.rich,
81
124
  fail_fast: Annotated[bool, typer.Option("--fail-fast")] = False,
@@ -138,6 +181,16 @@ def run(
138
181
  except (GitOpsError, ConfigError, subprocess.CalledProcessError) as e:
139
182
  raise _die(e) from None
140
183
 
184
+ # Record git provenance (repo + commit) of both checkouts — solution and task —
185
+ # so the run is reproducible; an unanchored side carries an `issue` naming why.
186
+ # Probed once, before any case runs: an unanchored checkout produces a report the
187
+ # leaderboard will hide, so the user must acknowledge (or pre-authorise) that.
188
+ provenance = Provenance(
189
+ solution=LocalRepo.provenance_of(trap_yaml_loader.trap_dir),
190
+ task=LocalRepo.provenance_of(traptask_yaml_loader.traptask_dir),
191
+ )
192
+ _confirm_unanchored(provenance, allow=allow_unanchored or _env_truthy("TRAP_ALLOW_UNANCHORED"))
193
+
141
194
  active_cases = traptask_yaml_loader.cases_with_tags(tags or [])
142
195
 
143
196
  started_at_local = datetime.now()
@@ -162,13 +215,6 @@ def run(
162
215
  )
163
216
  finished_at_utc = datetime.now(UTC)
164
217
 
165
- # Record git provenance (repo + commit) of both checkouts — solution and task —
166
- # so the run is reproducible. Each side is empty for a dirty/remote-less/local tree.
167
- provenance = Provenance(
168
- solution=LocalRepo.provenance_of(trap_yaml_loader.trap_dir),
169
- task=LocalRepo.provenance_of(traptask_yaml_loader.traptask_dir),
170
- )
171
-
172
218
  # Capture the host machine environment (CPU/RAM/OS/Python) unless disabled.
173
219
  # Detection is best-effort and must never abort a completed run.
174
220
  environment_info = None
@@ -232,6 +278,14 @@ def submit(
232
278
  ] = None,
233
279
  workspace: Annotated[Path, typer.Option("--workspace", "-w")] = Path(".trap"),
234
280
  run: Annotated[str, typer.Option("--run", "-r", help="Which run to upload.")] = "latest",
281
+ allow_unanchored: Annotated[
282
+ bool,
283
+ typer.Option(
284
+ "--allow-unanchored",
285
+ help="Skip the confirmation when the run has no git provenance (the "
286
+ "leaderboard hides such runs). Also settable via TRAP_ALLOW_UNANCHORED=1.",
287
+ ),
288
+ ] = False,
235
289
  ) -> None:
236
290
  """Upload a report.json to trapstreet.
237
291
 
@@ -254,9 +308,14 @@ def submit(
254
308
  raise _die(e) from None
255
309
  report_handle = ReportHandle(workspace.resolve(), task_alias, run)
256
310
  try:
257
- report_handle.assert_exists()
311
+ report_data = report_handle.load()
258
312
  except FileNotFoundError:
259
313
  raise _die(f"no report at {report_handle.report_json_path}. Run [bold]tp run[/bold] first.") from None
314
+ # Repeat the unanchored-provenance gate at upload time — the report records
315
+ # what `tp run` saw, so the checkouts aren't re-probed here.
316
+ _confirm_unanchored(
317
+ report_data.provenance, allow=allow_unanchored or _env_truthy("TRAP_ALLOW_UNANCHORED")
318
+ )
260
319
  report_path = report_handle.report_json_path
261
320
 
262
321
  client = ApiClient(resolved_server, resolved_key)
@@ -17,14 +17,17 @@ class LocalRepo:
17
17
  here, so the "open repo + read origin/commit/dirty" logic lives in one place.
18
18
  """
19
19
 
20
- def __init__(self, repo: git.Repo) -> None:
20
+ def __init__(self, repo: git.Repo, path: Path) -> None:
21
21
  self.repo = repo
22
+ # The path this checkout was opened at (may sit below the repo root when
23
+ # opened with search_parent).
24
+ self.path = path
22
25
 
23
26
  @classmethod
24
27
  def open(cls, path: Path, *, search_parent: bool = False) -> LocalRepo | None:
25
28
  """Open the git checkout at `path`, or None if it isn't a git repo."""
26
29
  try:
27
- return cls(git.Repo(path, search_parent_directories=search_parent))
30
+ return cls(git.Repo(path, search_parent_directories=search_parent), path)
28
31
  except (git.InvalidGitRepositoryError, git.NoSuchPathError):
29
32
  return None
30
33
 
@@ -36,25 +39,35 @@ class LocalRepo:
36
39
  except AttributeError:
37
40
  return None
38
41
 
39
- def provenance(self) -> GitProvenance:
40
- """{repo, commit} for a clean checkout with an origin, else empty.
42
+ def provenance_issue(self) -> str | None:
43
+ """Why this checkout cannot be anchored to {repo, commit}, or None if it can.
41
44
 
42
- Empty for a dirty tree (tracked-file changes), a remote-less repo, or any
43
- git error the run isn't reproducible from remote+commit alone, so we
44
- claim nothing. Best-effort: a probe failure degrades to empty rather than
45
- aborting an otherwise-complete run. Untracked files (run outputs under
46
- .trap/, .venv, …) don't count as dirty.
45
+ The run isn't reproducible from remote+commit alone when the repo has no
46
+ origin, no commit, or uncommitted tracked-file changes. Best-effort: a probe
47
+ failure degrades to a reason rather than raising. Untracked files (run
48
+ outputs under .trap/, .venv, …) don't count as dirty.
47
49
  """
48
50
  try:
49
- url = self.origin_normalised_url
50
- if url is None or not self.repo.head.is_valid() or self.repo.is_dirty():
51
- return GitProvenance()
52
- return GitProvenance(repo=url, commit=self.repo.head.commit.hexsha)
51
+ if self.origin_normalised_url is None:
52
+ return "no origin remote"
53
+ if not self.repo.head.is_valid():
54
+ return "no commit to anchor to"
55
+ if self.repo.is_dirty():
56
+ return "uncommitted changes"
57
+ return None
53
58
  except Exception:
54
- return GitProvenance()
59
+ return "git probe failed"
60
+
61
+ def provenance(self) -> GitProvenance:
62
+ """{repo, commit} for a clean checkout with an origin, else empty with `issue`
63
+ naming why — we claim nothing about a run that isn't reproducible."""
64
+ issue = self.provenance_issue()
65
+ if issue is not None:
66
+ return GitProvenance(issue=issue)
67
+ return GitProvenance(repo=self.origin_normalised_url, commit=self.repo.head.commit.hexsha)
55
68
 
56
69
  @classmethod
57
70
  def provenance_of(cls, path: Path) -> GitProvenance:
58
- """Provenance ({repo, commit}) of the checkout at `path`, empty if not a git repo."""
71
+ """Provenance ({repo, commit}) of the checkout at `path`."""
59
72
  local_repo = cls.open(path, search_parent=True)
60
- return local_repo.provenance() if local_repo else GitProvenance()
73
+ return local_repo.provenance() if local_repo else GitProvenance(issue="not a git repo")
@@ -5,10 +5,12 @@ from pydantic import BaseModel, Field
5
5
 
6
6
  class GitProvenance(BaseModel):
7
7
  """Git origin of one checkout: {repo, commit}. Both None when the tree isn't a
8
- clean, remote-backed git repo (see LocalRepo.provenance)."""
8
+ clean, remote-backed git repo — `issue` then names why (see
9
+ LocalRepo.provenance_issue). Anchored checkouts carry no issue."""
9
10
 
10
11
  repo: str | None = None
11
12
  commit: str | None = None
13
+ issue: str | None = None
12
14
 
13
15
 
14
16
  class Provenance(BaseModel):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes