runops 2.3.0__tar.gz → 2.3.1__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.
- {runops-2.3.0 → runops-2.3.1}/PKG-INFO +1 -1
- {runops-2.3.0 → runops-2.3.1}/SPEC.md +4 -0
- {runops-2.3.0 → runops-2.3.1}/docs/reference.md +1 -1
- {runops-2.3.0 → runops-2.3.1}/pyproject.toml +1 -1
- {runops-2.3.0 → runops-2.3.1}/src/runops/__init__.py +1 -1
- {runops-2.3.0 → runops-2.3.1}/src/runops/cli.py +11 -13
- {runops-2.3.0 → runops-2.3.1}/src/runops/ops/tidy.py +124 -50
- {runops-2.3.0 → runops-2.3.1}/tests/test_archive_series.py +19 -0
- {runops-2.3.0 → runops-2.3.1}/tests/test_mv_naming.py +40 -0
- {runops-2.3.0 → runops-2.3.1}/uv.lock +1 -1
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/check/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/improve-harness/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/agents/openai.yaml +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/examples/refactor-request.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/references/anti-patterns.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/references/api-compatibility.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/references/packaging-modernization.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/references/refactor-playbook.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/references/verification.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/scripts/api_surface_snapshot.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/scripts/import_smoke.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/scripts/inspect_python_package.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/scripts/refactor_quality_gate.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/release/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/simulator-adapter/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/spec-reviewer/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.agents/skills/sync-docs/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.claude/agents/spec-reviewer.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.claude/rules/architecture.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.claude/rules/dev-workflow.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.claude/rules/gotchas.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.claude/settings.json +0 -0
- {runops-2.3.0 → runops-2.3.1}/.claude/skills/check/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.claude/skills/improve-harness/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.claude/skills/python-package-refactor/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.claude/skills/release/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.claude/skills/simulator-adapter/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.claude/skills/spec-reviewer/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.claude/skills/sync-docs/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.codex/README.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/.codex/agents/spec-reviewer.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/.codex/config.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/.codex/rules/runops.rules +0 -0
- {runops-2.3.0 → runops-2.3.1}/.dockerignore +0 -0
- {runops-2.3.0 → runops-2.3.1}/.gitattributes +0 -0
- {runops-2.3.0 → runops-2.3.1}/.github/assets/conversation-dark.svg +0 -0
- {runops-2.3.0 → runops-2.3.1}/.github/assets/conversation-light.svg +0 -0
- {runops-2.3.0 → runops-2.3.1}/.github/assets/hero-dark.svg +0 -0
- {runops-2.3.0 → runops-2.3.1}/.github/assets/hero-light.svg +0 -0
- {runops-2.3.0 → runops-2.3.1}/.github/assets/make_readme_art.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/.github/workflows/ci.yml +0 -0
- {runops-2.3.0 → runops-2.3.1}/.github/workflows/publish.yml +0 -0
- {runops-2.3.0 → runops-2.3.1}/.gitignore +0 -0
- {runops-2.3.0 → runops-2.3.1}/.harness/manifest.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/.pre-commit-config.yaml +0 -0
- {runops-2.3.0 → runops-2.3.1}/.vscode/settings.json +0 -0
- {runops-2.3.0 → runops-2.3.1}/AGENTS.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/CLAUDE.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/LICENSE +0 -0
- {runops-2.3.0 → runops-2.3.1}/README.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/docs/adapter.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/docs/guide.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/adapters/__init__.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/adapters/base.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/adapters/generic.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/adapters/registry.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/case.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/config.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/errors.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/harness.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/jobgen.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/launchers.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/legacy_adapters/__init__.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/legacy_adapters/beach.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/legacy_adapters/emses.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/manifest.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/ops/__init__.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/ops/common.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/ops/create.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/ops/listing.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/ops/migrate.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/ops/notes.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/ops/setup.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/ops/skills.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/ops/submit.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/ops/sync.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/project.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/runtime.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/skills/runops-check-status/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/skills/runops-create-run/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/skills/runops-maintain/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/skills/runops-reference/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/skills/runops-tidy/SKILL.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/slurm.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/src/runops/util.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/__init__.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/conftest.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/.codex/rules/a.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/.runops/test-runs/T20260701-0001/receipt.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/.runops/work/beach-x-abc/bin/beach +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/AGENTS.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/CLAUDE.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/cases/beach/plane/beach.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/cases/beach/plane/case.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/cases/old/beach.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/cases/old/case.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/launchers.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/research/CURRENT.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/runops.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/runs/AGENTS.md +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/runs/_archive/plane/R20260601-0001/manifest.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/runs/plane/R20260701-0001--a/input/beach.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/runs/plane/R20260701-0001--a/manifest.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/runs/plane/R20260702-0001/manifest.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/runs/scan/R20260701-0002/manifest.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/runs/scan/survey.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/simulators.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/site.toml +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/golden/mpi_standard.sh +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/golden/openmp_rsc.sh +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/test_adapters.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/test_build.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/test_dev_harness.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/test_jobgen.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/test_legacy_adapters.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/test_lifecycle.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/test_manifest_config.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/test_migrate.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/test_project_scope.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/test_safety.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/test_scratch_tidy.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/test_setup_misc.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/test_skills.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/test_sweep.py +0 -0
- {runops-2.3.0 → runops-2.3.1}/tests/test_sync_evidence.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: runops
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.1
|
|
4
4
|
Summary: Run ledger and Slurm job submission for HPC simulation projects
|
|
5
5
|
Project-URL: Homepage, https://github.com/Nkzono99/runops
|
|
6
6
|
Project-URL: Documentation, https://github.com/Nkzono99/runops/blob/main/docs/reference.md
|
|
@@ -573,6 +573,9 @@ created ──submit──▶ submitted ──(slurm)──▶ running ──▶
|
|
|
573
573
|
- 対象は `created`、`completed`、`failed`、`cancelled`。`submitted` / `running` は拒否する
|
|
574
574
|
(Slurm が run directory の絶対 path を持つ)。`archived` は `restore` を使う。
|
|
575
575
|
- 既存 path は上書きしない(`move_noreplace`)。`created` の run は job.sh を作り直す。
|
|
576
|
+
- 複数 run は `run.lock` を 1 回取り、全対象の状態と移動先を検査してから移す。1 件でも拒否されれば
|
|
577
|
+
どれも動かさない。移動中に filesystem の error が起きたら止まり、移し終えた run id を error に示す
|
|
578
|
+
(exit code 1)。`archive RUN...` も同じ手順で移す。
|
|
576
579
|
- 手で `mv` しても run は id で見つかるが、`run.label` と `[path].run_dir` が古いまま残る。
|
|
577
580
|
|
|
578
581
|
---
|
|
@@ -967,4 +970,5 @@ v2 の文書は次の 4 ファイルだけにする。
|
|
|
967
970
|
| 操作範囲は起動 project(2026-10-21) | AGENTS.md block と skill に「対象は起動した project の run と job id だけ。アカウント全体の squeue / sacct は診断に見るだけ。自分で別 directory に移らず、生の `sbatch` / `scancel` を使わない」を書く。CLI は別 project の path を拒否する | 範囲を言わない「再投入して」で、エージェントが別 project や runops 管理外のジョブまで再投入した。cwd で project を決める既存の model に、path 引数の抜け道(`submit --all /other/runs` など)があった。他 session が修正のために止めた run を見分けられるよう、`cancel --why` を note に残し、`retry` はその理由を示して確認を取る(#112) |
|
|
968
971
|
| 置き換えた系列の整理(2026-10-21) | `archive` が DIR(系列)を受け、`--superseded-by` で置き換え先を manifest に記録する。新しい command と tidy の種別は足さない。skill は新系列を作ったら旧系列の要否を確かめ、不要なら archive して現用系列・archive 先・件数を報告する | 修正版を作った後に旧系列が作業領域に残り、毎回ユーザーが整理を頼んでいた。`parent_run` だけでは比較用の派生と置き換えを区別できないので、置き換えは明示操作で記録する。旧 `archive` は 1 件ずつ検査していて、途中の running run で一部だけ移動していた(#113) |
|
|
969
972
|
| 生成ハーネスの書き方(2026-10-21) | AGENTS.md の Rules を「作業の進め方」5 項目に絞り、各項目を「何をするか」と短い理由で書く。skill は手順に専念し、AGENTS.md の方針(manifest を手で編集しない等)を繰り返さない | #112・#113 で規則が増え、禁止形(never / do not)が並んでいた。現行の Claude の prompting 指針は、しないことより何をするかを、理由と共に書くことを勧め、強い禁止語は過剰反応を招くとしている |
|
|
973
|
+
| 複数 run の mv / archive(2026-10-22) | 1 回の `run.lock` の下で全対象を読み直して検査し、通ってから移す。途中の filesystem error では移し終えた run id を示して止まる | `mv A B C --dest X` は 1 件ずつ検査して移していたので、途中の submitted run で一部だけ移動していた。archive は表示の後で lock を取り直していて、その間に再投入された run で同じことが起き得た |
|
|
970
974
|
| 使い方ガイド | `docs/guide.md` を追加し、文書を 4 ファイルにする(§13.3) | README の依頼例の表と command 一覧の間を埋める、場面ごとの流れ(case と run の分け方、smoke から本計算、状態の決まり方、整理)の置き場所が無かった |
|
|
@@ -162,7 +162,7 @@ run id は削除後も再利用しません。`--dest` に既存 run の中は
|
|
|
162
162
|
| `runo list [PATH] [--all] [--since 30d] [--case] [--sweep] [--status] [--tag] [--scratch] [--archived] [--json]` | run 一覧 |
|
|
163
163
|
| `runo note RUN [TEXT]` / `runo pin RUN [--off] [--reason]` | ノート追記・表示 / pin |
|
|
164
164
|
| `runo summarize RUN... [--force]` | adapter の要約を `analysis/summary.json` に書く |
|
|
165
|
-
| `runo mv RUN... [--dest DIR] [--label L] [--why TEXT]` | `runs/` 配下で改名・移動。id、入力、sweep、ノートは保つ。submitted / running と archived
|
|
165
|
+
| `runo mv RUN... [--dest DIR] [--label L] [--why TEXT]` | `runs/` 配下で改名・移動。id、入力、sweep、ノートは保つ。submitted / running と archived は不可。全対象を検査してから移し、1 件でも不可なら何も動かさない |
|
|
166
166
|
| `runo archive RUN\|DIR... [--superseded-by RUN\|DIR --why TEXT]` / `runo archive --older-than 90d` / `runo restore RUN` | `runs/_archive/` へ移動 / 戻す。移動前に全対象を検査し、指定した RUN が終了していなければ何も動かさない。DIR は配下の run(系列)で、created / submitted / running と pinned を除いて表示する。`--superseded-by` は置き換え先(その run から `--from` で派生した run、無ければ指定した全 run)を `superseded_by` に記録し、`--why` をノートに残す |
|
|
167
167
|
| `runo delete RUN... [-y]` | created・failed・cancelled・scratch の run を削除 |
|
|
168
168
|
| `runo tidy [--apply] [--only KIND] [--older-than AGE] [--purge-work RUN...]` | 整理候補の表示と実行 |
|
|
@@ -796,7 +796,7 @@ def archive(
|
|
|
796
796
|
"""Move terminal runs to runs/_archive/ (same relative path)."""
|
|
797
797
|
from datetime import datetime
|
|
798
798
|
|
|
799
|
-
from runops.ops.tidy import ArchivePlan,
|
|
799
|
+
from runops.ops.tidy import ArchivePlan, archive_runs, plan_archive
|
|
800
800
|
|
|
801
801
|
project = _project()
|
|
802
802
|
if superseded_by and not why:
|
|
@@ -827,11 +827,11 @@ def archive(
|
|
|
827
827
|
line += f" -> {', '.join(plan.superseded_by[m.id])}"
|
|
828
828
|
out.print(line)
|
|
829
829
|
_confirm(f"Archive {len(plan.targets)} runs?", yes)
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
out.print(f"archived {
|
|
830
|
+
archived = archive_runs(
|
|
831
|
+
project, plan.targets, superseded_by=plan.superseded_by, why=why, by=_actor()
|
|
832
|
+
)
|
|
833
|
+
for run_id, dst in archived:
|
|
834
|
+
out.print(f"archived {run_id} -> {project.rel(dst)}")
|
|
835
835
|
|
|
836
836
|
|
|
837
837
|
@app.command("mv")
|
|
@@ -844,7 +844,7 @@ def mv_cmd(
|
|
|
844
844
|
why: str = typer.Option("", "--why", help="Reason, recorded in the run's notes."),
|
|
845
845
|
) -> None:
|
|
846
846
|
"""Rename or move runs under runs/, keeping their ids, inputs and history."""
|
|
847
|
-
from runops.ops.tidy import
|
|
847
|
+
from runops.ops.tidy import move_runs
|
|
848
848
|
|
|
849
849
|
if not dest and label is None:
|
|
850
850
|
raise UsageError("give --dest and/or --label")
|
|
@@ -852,12 +852,10 @@ def mv_cmd(
|
|
|
852
852
|
targets = _resolve(project, runs)
|
|
853
853
|
if label is not None and len(targets) > 1:
|
|
854
854
|
raise UsageError("--label applies to one RUN; use --dest to move several")
|
|
855
|
-
|
|
856
|
-
for
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
out.print(f"moved {m.id}: {project.rel(result.src)} -> {project.rel(result.dst)}")
|
|
860
|
-
_warn(warnings)
|
|
855
|
+
results = move_runs(project, targets, dest=dest, label=label, why=why, by=_actor())
|
|
856
|
+
for r in results:
|
|
857
|
+
out.print(f"moved {r.run_id}: {project.rel(r.src)} -> {project.rel(r.dst)}")
|
|
858
|
+
_warn([w for r in results for w in r.warnings])
|
|
861
859
|
|
|
862
860
|
|
|
863
861
|
@app.command()
|
|
@@ -10,7 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
|
|
11
11
|
from runops.adapters.registry import optional
|
|
12
12
|
from runops.case import slugify
|
|
13
|
-
from runops.errors import RunopsError, UsageError
|
|
13
|
+
from runops.errors import RunError, RunopsError, UsageError
|
|
14
14
|
from runops.manifest import ACTIVE_STATES, MANIFEST_FILE, Manifest, RunState
|
|
15
15
|
from runops.ops.common import inside, resolve_dest, run_sim_context
|
|
16
16
|
from runops.ops.submit import write_job_for
|
|
@@ -206,12 +206,37 @@ def archive_run(
|
|
|
206
206
|
"""Move a terminal run to ``runs/_archive/``; record replacements and *why* if given."""
|
|
207
207
|
with project.run_lock():
|
|
208
208
|
manifest.reload()
|
|
209
|
-
|
|
209
|
+
dst = _archive_dst(project, manifest)
|
|
210
|
+
return _archive_locked(project, manifest, dst, superseded_by or [], why, by)
|
|
210
211
|
|
|
211
212
|
|
|
212
|
-
def
|
|
213
|
-
project: Project,
|
|
214
|
-
|
|
213
|
+
def archive_runs(
|
|
214
|
+
project: Project,
|
|
215
|
+
manifests: list[Manifest],
|
|
216
|
+
*,
|
|
217
|
+
superseded_by: dict[str, list[str]] | None = None,
|
|
218
|
+
why: str = "",
|
|
219
|
+
by: str = "",
|
|
220
|
+
) -> list[tuple[str, Path]]:
|
|
221
|
+
"""Archive several runs: all are checked under one ``run.lock`` before any moves."""
|
|
222
|
+
replacements = superseded_by or {}
|
|
223
|
+
done: list[tuple[str, Path]] = []
|
|
224
|
+
with project.run_lock():
|
|
225
|
+
for m in manifests:
|
|
226
|
+
m.reload()
|
|
227
|
+
dsts = [_archive_dst(project, m) for m in manifests]
|
|
228
|
+
for m, dst in zip(manifests, dsts, strict=True):
|
|
229
|
+
try:
|
|
230
|
+
_archive_locked(project, m, dst, replacements.get(m.id, []), why, by)
|
|
231
|
+
except (RunopsError, OSError) as exc:
|
|
232
|
+
moved = ", ".join(run_id for run_id, _ in done)
|
|
233
|
+
tail = f" (already archived: {moved})" if moved else ""
|
|
234
|
+
raise RunError(f"{m.id}: {exc}{tail}") from exc
|
|
235
|
+
done.append((m.id, dst))
|
|
236
|
+
return done
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _archive_dst(project: Project, manifest: Manifest) -> Path:
|
|
215
240
|
status = manifest.status
|
|
216
241
|
if status not in _TERMINAL:
|
|
217
242
|
raise UsageError(f"{manifest.id} is {status.value}; only terminal runs can be archived")
|
|
@@ -227,6 +252,19 @@ def _archive_locked(
|
|
|
227
252
|
dst = archive_root / rel
|
|
228
253
|
if dst.exists():
|
|
229
254
|
raise UsageError(f"{dst} already exists")
|
|
255
|
+
return dst
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _archive_locked(
|
|
259
|
+
project: Project,
|
|
260
|
+
manifest: Manifest,
|
|
261
|
+
dst: Path,
|
|
262
|
+
superseded_by: list[str],
|
|
263
|
+
why: str,
|
|
264
|
+
by: str,
|
|
265
|
+
) -> Path:
|
|
266
|
+
status = manifest.status
|
|
267
|
+
src = manifest.run_dir.resolve()
|
|
230
268
|
move_noreplace(src, dst)
|
|
231
269
|
moved = Manifest.load(dst / MANIFEST_FILE)
|
|
232
270
|
moved.run["status_before_archive"] = status.value
|
|
@@ -284,60 +322,96 @@ class MoveResult:
|
|
|
284
322
|
_MOVABLE = (RunState.CREATED, RunState.COMPLETED, RunState.FAILED, RunState.CANCELLED)
|
|
285
323
|
|
|
286
324
|
|
|
287
|
-
|
|
325
|
+
@dataclass
|
|
326
|
+
class _MovePlan:
|
|
327
|
+
manifest: Manifest
|
|
328
|
+
status: RunState
|
|
329
|
+
src: Path
|
|
330
|
+
dst: Path
|
|
331
|
+
label: str
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def move_runs(
|
|
288
335
|
project: Project,
|
|
289
|
-
|
|
336
|
+
manifests: list[Manifest],
|
|
290
337
|
*,
|
|
291
338
|
dest: str = "",
|
|
292
339
|
label: str | None = None,
|
|
293
340
|
why: str = "",
|
|
294
341
|
by: str = "",
|
|
295
|
-
) -> MoveResult:
|
|
296
|
-
"""Rename and/or move
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
342
|
+
) -> list[MoveResult]:
|
|
343
|
+
"""Rename and/or move runs under ``runs/``, keeping their ids, inputs and history.
|
|
344
|
+
|
|
345
|
+
Every run is checked under one ``run.lock`` before any directory moves, so a
|
|
346
|
+
refused run leaves all of them where they were. Submitted/running runs are refused
|
|
347
|
+
(Slurm holds absolute paths into the run directory); archived runs move with
|
|
348
|
+
``restore``. The old path and *why* are appended to each run's notes; a created run
|
|
349
|
+
gets a fresh ``job.sh``.
|
|
301
350
|
"""
|
|
351
|
+
results: list[MoveResult] = []
|
|
302
352
|
with project.run_lock():
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
new_label = (
|
|
313
|
-
manifest.label
|
|
314
|
-
if label is None
|
|
315
|
-
else slugify(label, project.config.defaults.label_max_length)
|
|
316
|
-
)
|
|
317
|
-
if label and not new_label:
|
|
318
|
-
raise UsageError(f"--label {label!r} has no usable characters")
|
|
319
|
-
name = f"{manifest.id}--{new_label}" if new_label else manifest.id
|
|
320
|
-
dst = parent / name
|
|
321
|
-
if dst.resolve() == src:
|
|
322
|
-
raise UsageError(f"{manifest.id} is already at {project.rel(src)}")
|
|
323
|
-
if dst.exists():
|
|
324
|
-
raise UsageError(f"{project.rel(dst)} already exists")
|
|
325
|
-
parent.mkdir(parents=True, exist_ok=True)
|
|
326
|
-
move_noreplace(src, dst)
|
|
327
|
-
moved = Manifest.load(dst / MANIFEST_FILE)
|
|
328
|
-
moved.run["label"] = new_label
|
|
329
|
-
moved.section("path")["run_dir"] = project.rel(dst)
|
|
330
|
-
text = f"moved from {project.rel(src)}"
|
|
331
|
-
moved.add_note(f"{text}: {why}" if why else text, by or "runops")
|
|
332
|
-
moved.save()
|
|
333
|
-
project.update_index(moved.id, dst)
|
|
334
|
-
result = MoveResult(moved.id, src, dst)
|
|
335
|
-
if status == RunState.CREATED:
|
|
353
|
+
plans = [_plan_move(project, m, dest, label) for m in manifests]
|
|
354
|
+
taken: dict[Path, str] = {}
|
|
355
|
+
for plan in plans:
|
|
356
|
+
other = taken.setdefault(plan.dst.resolve(), plan.manifest.id)
|
|
357
|
+
if other != plan.manifest.id:
|
|
358
|
+
raise UsageError(
|
|
359
|
+
f"{other} and {plan.manifest.id} would both move to {project.rel(plan.dst)}"
|
|
360
|
+
)
|
|
361
|
+
for plan in plans:
|
|
336
362
|
try:
|
|
337
|
-
|
|
338
|
-
except RunopsError as exc:
|
|
339
|
-
|
|
340
|
-
|
|
363
|
+
results.append(_apply_move(project, plan, why, by))
|
|
364
|
+
except (RunopsError, OSError) as exc:
|
|
365
|
+
done = ", ".join(r.run_id for r in results)
|
|
366
|
+
moved = f" (already moved: {done})" if done else ""
|
|
367
|
+
raise RunError(f"{plan.manifest.id}: {exc}{moved}") from exc
|
|
368
|
+
finally:
|
|
369
|
+
_prune_empty(plan.src.parent, project.runs_dir)
|
|
370
|
+
return results
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _plan_move(project: Project, manifest: Manifest, dest: str, label: str | None) -> _MovePlan:
|
|
374
|
+
manifest.reload()
|
|
375
|
+
status = manifest.status
|
|
376
|
+
if status not in _MOVABLE:
|
|
377
|
+
hint = "restore it first" if status == RunState.ARCHIVED else "wait or cancel it"
|
|
378
|
+
raise UsageError(f"{manifest.id} is {status.value}; {hint}")
|
|
379
|
+
src = manifest.run_dir.resolve()
|
|
380
|
+
parent = resolve_dest(project, dest) if dest else src.parent
|
|
381
|
+
if inside(parent, src):
|
|
382
|
+
raise UsageError(f"--dest must not be inside {manifest.id} itself")
|
|
383
|
+
new_label = (
|
|
384
|
+
manifest.label
|
|
385
|
+
if label is None
|
|
386
|
+
else slugify(label, project.config.defaults.label_max_length)
|
|
387
|
+
)
|
|
388
|
+
if label and not new_label:
|
|
389
|
+
raise UsageError(f"--label {label!r} has no usable characters")
|
|
390
|
+
name = f"{manifest.id}--{new_label}" if new_label else manifest.id
|
|
391
|
+
dst = parent / name
|
|
392
|
+
if dst.resolve() == src:
|
|
393
|
+
raise UsageError(f"{manifest.id} is already at {project.rel(src)}")
|
|
394
|
+
if dst.exists():
|
|
395
|
+
raise UsageError(f"{project.rel(dst)} already exists")
|
|
396
|
+
return _MovePlan(manifest, status, src, dst, new_label)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _apply_move(project: Project, plan: _MovePlan, why: str, by: str) -> MoveResult:
|
|
400
|
+
plan.dst.parent.mkdir(parents=True, exist_ok=True)
|
|
401
|
+
move_noreplace(plan.src, plan.dst)
|
|
402
|
+
moved = Manifest.load(plan.dst / MANIFEST_FILE)
|
|
403
|
+
moved.run["label"] = plan.label
|
|
404
|
+
moved.section("path")["run_dir"] = project.rel(plan.dst)
|
|
405
|
+
text = f"moved from {project.rel(plan.src)}"
|
|
406
|
+
moved.add_note(f"{text}: {why}" if why else text, by or "runops")
|
|
407
|
+
moved.save()
|
|
408
|
+
project.update_index(moved.id, plan.dst)
|
|
409
|
+
result = MoveResult(moved.id, plan.src, plan.dst)
|
|
410
|
+
if plan.status == RunState.CREATED:
|
|
411
|
+
try:
|
|
412
|
+
write_job_for(project, moved)
|
|
413
|
+
except RunopsError as exc: # submit regenerates job.sh anyway
|
|
414
|
+
result.warnings.append(f"{moved.id}: job.sh not regenerated ({exc})")
|
|
341
415
|
return result
|
|
342
416
|
|
|
343
417
|
|
|
@@ -5,7 +5,11 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from runops.errors import UsageError
|
|
8
11
|
from runops.manifest import Manifest
|
|
12
|
+
from runops.ops.tidy import archive_runs
|
|
9
13
|
from runops.project import Project
|
|
10
14
|
|
|
11
15
|
|
|
@@ -96,3 +100,18 @@ def test_archive_series_skips_active_and_pinned(project_dir: Path, cli, fake_slu
|
|
|
96
100
|
assert cli("archive", b, "-y").code == 0
|
|
97
101
|
res = cli("archive", "runs/demo/mix", "-y")
|
|
98
102
|
assert res.code == 0 and "nothing to archive" in res.out
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_archive_runs_rechecks_under_the_lock(project_dir: Path, cli, fake_slurm) -> None: # type: ignore[no-untyped-def]
|
|
106
|
+
a, b = cli(
|
|
107
|
+
"sweep", "demo", "--set", "x=1,2", "--label", "s", "--apply", "--submit", "-y"
|
|
108
|
+
).run_ids()
|
|
109
|
+
assert cli("cancel", b, "-y").code == 0
|
|
110
|
+
_finish_all(cli, fake_slurm)
|
|
111
|
+
project = Project.open(project_dir)
|
|
112
|
+
stale = [project.resolve_run(a), project.resolve_run(b)]
|
|
113
|
+
# Between the preview and the move, the cancelled b is retried and resubmitted.
|
|
114
|
+
assert cli("retry", b, "--submit", "-y").code == 0
|
|
115
|
+
with pytest.raises(UsageError, match=f"{b} is submitted"):
|
|
116
|
+
archive_runs(project, stale)
|
|
117
|
+
assert _manifest(project_dir, a).status.value == "completed"
|
|
@@ -5,8 +5,11 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
8
10
|
from runops.manifest import Manifest
|
|
9
11
|
from runops.project import Project
|
|
12
|
+
from runops.util import move_noreplace
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
def _manifest(project_dir: Path, run_id: str) -> Manifest:
|
|
@@ -120,3 +123,40 @@ def test_elapsed_from_exit_record(project_dir: Path, cli, fake_slurm) -> None:
|
|
|
120
123
|
)
|
|
121
124
|
cli("sync")
|
|
122
125
|
assert _manifest(project_dir, run_id).job["attempts"][-1]["elapsed"] == "1-01:30:05"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_mv_checks_every_run_before_moving(project_dir: Path, cli, fake_slurm) -> None: # type: ignore[no-untyped-def]
|
|
129
|
+
a, b, c = cli("sweep", "demo", "--set", "x=1,2,3", "--label", "s", "--apply").run_ids()
|
|
130
|
+
before = {r: _manifest(project_dir, r).run_dir for r in (a, b, c)}
|
|
131
|
+
assert cli("submit", b).code == 0
|
|
132
|
+
res = cli("mv", a, b, c, "--dest", "regrouped")
|
|
133
|
+
assert res.code == 2 and f"{b} is submitted" in res.err
|
|
134
|
+
assert {r: _manifest(project_dir, r).run_dir for r in (a, b, c)} == before
|
|
135
|
+
assert not (project_dir / "runs/regrouped").exists()
|
|
136
|
+
|
|
137
|
+
(project_dir / "runs/regrouped" / before[c].name).mkdir(parents=True)
|
|
138
|
+
res = cli("mv", a, c, "--dest", "regrouped")
|
|
139
|
+
assert res.code == 2 and "already exists" in res.err
|
|
140
|
+
assert _manifest(project_dir, a).run_dir == before[a]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_mv_reports_runs_moved_before_a_failure(
|
|
144
|
+
project_dir: Path,
|
|
145
|
+
cli, # type: ignore[no-untyped-def]
|
|
146
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
147
|
+
) -> None:
|
|
148
|
+
a, b = cli("sweep", "demo", "--set", "x=1,2", "--label", "s", "--apply").run_ids()
|
|
149
|
+
calls: list[Path] = []
|
|
150
|
+
|
|
151
|
+
def flaky(src: Path, dst: Path) -> None:
|
|
152
|
+
calls.append(src)
|
|
153
|
+
if len(calls) == 2:
|
|
154
|
+
raise OSError("disk quota exceeded")
|
|
155
|
+
move_noreplace(src, dst)
|
|
156
|
+
|
|
157
|
+
monkeypatch.setattr("runops.ops.tidy.move_noreplace", flaky)
|
|
158
|
+
res = cli("mv", a, b, "--dest", "regrouped")
|
|
159
|
+
assert res.code == 1
|
|
160
|
+
assert f"{b}: disk quota exceeded (already moved: {a})" in res.err
|
|
161
|
+
assert _manifest(project_dir, a).run_dir.parent == project_dir / "runs/regrouped"
|
|
162
|
+
assert _manifest(project_dir, b).run_dir.parent == project_dir / "runs/demo/s"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/examples/refactor-request.md
RENAMED
|
File without changes
|
{runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/references/anti-patterns.md
RENAMED
|
File without changes
|
{runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/references/api-compatibility.md
RENAMED
|
File without changes
|
|
File without changes
|
{runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/references/refactor-playbook.md
RENAMED
|
File without changes
|
{runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/references/verification.md
RENAMED
|
File without changes
|
{runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/scripts/api_surface_snapshot.py
RENAMED
|
File without changes
|
{runops-2.3.0 → runops-2.3.1}/.agents/skills/python-package-refactor/scripts/import_smoke.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/runs/plane/R20260701-0001--a/manifest.toml
RENAMED
|
File without changes
|
{runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/runs/plane/R20260702-0001/manifest.toml
RENAMED
|
File without changes
|
{runops-2.3.0 → runops-2.3.1}/tests/fixtures/v1-project/runs/scan/R20260701-0002/manifest.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|