runops 2.3.0__tar.gz → 2.4.0__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 (137) hide show
  1. {runops-2.3.0 → runops-2.4.0}/PKG-INFO +2 -1
  2. {runops-2.3.0 → runops-2.4.0}/README.md +1 -0
  3. {runops-2.3.0 → runops-2.4.0}/SPEC.md +22 -7
  4. {runops-2.3.0 → runops-2.4.0}/docs/adapter.md +4 -1
  5. {runops-2.3.0 → runops-2.4.0}/docs/guide.md +8 -5
  6. {runops-2.3.0 → runops-2.4.0}/docs/reference.md +8 -8
  7. {runops-2.3.0 → runops-2.4.0}/pyproject.toml +1 -1
  8. {runops-2.3.0 → runops-2.4.0}/src/runops/__init__.py +1 -1
  9. {runops-2.3.0 → runops-2.4.0}/src/runops/cli.py +47 -18
  10. {runops-2.3.0 → runops-2.4.0}/src/runops/harness.py +10 -6
  11. {runops-2.3.0 → runops-2.4.0}/src/runops/manifest.py +18 -2
  12. {runops-2.3.0 → runops-2.4.0}/src/runops/ops/create.py +110 -10
  13. {runops-2.3.0 → runops-2.4.0}/src/runops/ops/setup.py +4 -0
  14. {runops-2.3.0 → runops-2.4.0}/src/runops/ops/skills.py +42 -0
  15. {runops-2.3.0 → runops-2.4.0}/src/runops/ops/tidy.py +143 -56
  16. {runops-2.3.0 → runops-2.4.0}/src/runops/skills/runops-create-run/SKILL.md +7 -4
  17. {runops-2.3.0 → runops-2.4.0}/src/runops/skills/runops-maintain/SKILL.md +2 -2
  18. {runops-2.3.0 → runops-2.4.0}/src/runops/skills/runops-reference/SKILL.md +5 -5
  19. {runops-2.3.0 → runops-2.4.0}/src/runops/skills/runops-tidy/SKILL.md +5 -4
  20. {runops-2.3.0 → runops-2.4.0}/tests/test_adapters.py +28 -0
  21. {runops-2.3.0 → runops-2.4.0}/tests/test_archive_series.py +56 -1
  22. {runops-2.3.0 → runops-2.4.0}/tests/test_mv_naming.py +40 -0
  23. runops-2.4.0/tests/test_run_job.py +70 -0
  24. {runops-2.3.0 → runops-2.4.0}/tests/test_skills.py +30 -0
  25. {runops-2.3.0 → runops-2.4.0}/uv.lock +1 -1
  26. {runops-2.3.0 → runops-2.4.0}/.agents/skills/check/SKILL.md +0 -0
  27. {runops-2.3.0 → runops-2.4.0}/.agents/skills/improve-harness/SKILL.md +0 -0
  28. {runops-2.3.0 → runops-2.4.0}/.agents/skills/python-package-refactor/SKILL.md +0 -0
  29. {runops-2.3.0 → runops-2.4.0}/.agents/skills/python-package-refactor/agents/openai.yaml +0 -0
  30. {runops-2.3.0 → runops-2.4.0}/.agents/skills/python-package-refactor/examples/refactor-request.md +0 -0
  31. {runops-2.3.0 → runops-2.4.0}/.agents/skills/python-package-refactor/references/anti-patterns.md +0 -0
  32. {runops-2.3.0 → runops-2.4.0}/.agents/skills/python-package-refactor/references/api-compatibility.md +0 -0
  33. {runops-2.3.0 → runops-2.4.0}/.agents/skills/python-package-refactor/references/packaging-modernization.md +0 -0
  34. {runops-2.3.0 → runops-2.4.0}/.agents/skills/python-package-refactor/references/refactor-playbook.md +0 -0
  35. {runops-2.3.0 → runops-2.4.0}/.agents/skills/python-package-refactor/references/verification.md +0 -0
  36. {runops-2.3.0 → runops-2.4.0}/.agents/skills/python-package-refactor/scripts/api_surface_snapshot.py +0 -0
  37. {runops-2.3.0 → runops-2.4.0}/.agents/skills/python-package-refactor/scripts/import_smoke.py +0 -0
  38. {runops-2.3.0 → runops-2.4.0}/.agents/skills/python-package-refactor/scripts/inspect_python_package.py +0 -0
  39. {runops-2.3.0 → runops-2.4.0}/.agents/skills/python-package-refactor/scripts/refactor_quality_gate.py +0 -0
  40. {runops-2.3.0 → runops-2.4.0}/.agents/skills/release/SKILL.md +0 -0
  41. {runops-2.3.0 → runops-2.4.0}/.agents/skills/simulator-adapter/SKILL.md +0 -0
  42. {runops-2.3.0 → runops-2.4.0}/.agents/skills/spec-reviewer/SKILL.md +0 -0
  43. {runops-2.3.0 → runops-2.4.0}/.agents/skills/sync-docs/SKILL.md +0 -0
  44. {runops-2.3.0 → runops-2.4.0}/.claude/agents/spec-reviewer.md +0 -0
  45. {runops-2.3.0 → runops-2.4.0}/.claude/rules/architecture.md +0 -0
  46. {runops-2.3.0 → runops-2.4.0}/.claude/rules/dev-workflow.md +0 -0
  47. {runops-2.3.0 → runops-2.4.0}/.claude/rules/gotchas.md +0 -0
  48. {runops-2.3.0 → runops-2.4.0}/.claude/settings.json +0 -0
  49. {runops-2.3.0 → runops-2.4.0}/.claude/skills/check/SKILL.md +0 -0
  50. {runops-2.3.0 → runops-2.4.0}/.claude/skills/improve-harness/SKILL.md +0 -0
  51. {runops-2.3.0 → runops-2.4.0}/.claude/skills/python-package-refactor/SKILL.md +0 -0
  52. {runops-2.3.0 → runops-2.4.0}/.claude/skills/release/SKILL.md +0 -0
  53. {runops-2.3.0 → runops-2.4.0}/.claude/skills/simulator-adapter/SKILL.md +0 -0
  54. {runops-2.3.0 → runops-2.4.0}/.claude/skills/spec-reviewer/SKILL.md +0 -0
  55. {runops-2.3.0 → runops-2.4.0}/.claude/skills/sync-docs/SKILL.md +0 -0
  56. {runops-2.3.0 → runops-2.4.0}/.codex/README.md +0 -0
  57. {runops-2.3.0 → runops-2.4.0}/.codex/agents/spec-reviewer.toml +0 -0
  58. {runops-2.3.0 → runops-2.4.0}/.codex/config.toml +0 -0
  59. {runops-2.3.0 → runops-2.4.0}/.codex/rules/runops.rules +0 -0
  60. {runops-2.3.0 → runops-2.4.0}/.dockerignore +0 -0
  61. {runops-2.3.0 → runops-2.4.0}/.gitattributes +0 -0
  62. {runops-2.3.0 → runops-2.4.0}/.github/assets/conversation-dark.svg +0 -0
  63. {runops-2.3.0 → runops-2.4.0}/.github/assets/conversation-light.svg +0 -0
  64. {runops-2.3.0 → runops-2.4.0}/.github/assets/hero-dark.svg +0 -0
  65. {runops-2.3.0 → runops-2.4.0}/.github/assets/hero-light.svg +0 -0
  66. {runops-2.3.0 → runops-2.4.0}/.github/assets/make_readme_art.py +0 -0
  67. {runops-2.3.0 → runops-2.4.0}/.github/workflows/ci.yml +0 -0
  68. {runops-2.3.0 → runops-2.4.0}/.github/workflows/publish.yml +0 -0
  69. {runops-2.3.0 → runops-2.4.0}/.gitignore +0 -0
  70. {runops-2.3.0 → runops-2.4.0}/.harness/manifest.toml +0 -0
  71. {runops-2.3.0 → runops-2.4.0}/.pre-commit-config.yaml +0 -0
  72. {runops-2.3.0 → runops-2.4.0}/.vscode/settings.json +0 -0
  73. {runops-2.3.0 → runops-2.4.0}/AGENTS.md +0 -0
  74. {runops-2.3.0 → runops-2.4.0}/CLAUDE.md +0 -0
  75. {runops-2.3.0 → runops-2.4.0}/LICENSE +0 -0
  76. {runops-2.3.0 → runops-2.4.0}/src/runops/adapters/__init__.py +0 -0
  77. {runops-2.3.0 → runops-2.4.0}/src/runops/adapters/base.py +0 -0
  78. {runops-2.3.0 → runops-2.4.0}/src/runops/adapters/generic.py +0 -0
  79. {runops-2.3.0 → runops-2.4.0}/src/runops/adapters/registry.py +0 -0
  80. {runops-2.3.0 → runops-2.4.0}/src/runops/case.py +0 -0
  81. {runops-2.3.0 → runops-2.4.0}/src/runops/config.py +0 -0
  82. {runops-2.3.0 → runops-2.4.0}/src/runops/errors.py +0 -0
  83. {runops-2.3.0 → runops-2.4.0}/src/runops/jobgen.py +0 -0
  84. {runops-2.3.0 → runops-2.4.0}/src/runops/launchers.py +0 -0
  85. {runops-2.3.0 → runops-2.4.0}/src/runops/legacy_adapters/__init__.py +0 -0
  86. {runops-2.3.0 → runops-2.4.0}/src/runops/legacy_adapters/beach.py +0 -0
  87. {runops-2.3.0 → runops-2.4.0}/src/runops/legacy_adapters/emses.py +0 -0
  88. {runops-2.3.0 → runops-2.4.0}/src/runops/ops/__init__.py +0 -0
  89. {runops-2.3.0 → runops-2.4.0}/src/runops/ops/common.py +0 -0
  90. {runops-2.3.0 → runops-2.4.0}/src/runops/ops/listing.py +0 -0
  91. {runops-2.3.0 → runops-2.4.0}/src/runops/ops/migrate.py +0 -0
  92. {runops-2.3.0 → runops-2.4.0}/src/runops/ops/notes.py +0 -0
  93. {runops-2.3.0 → runops-2.4.0}/src/runops/ops/submit.py +0 -0
  94. {runops-2.3.0 → runops-2.4.0}/src/runops/ops/sync.py +0 -0
  95. {runops-2.3.0 → runops-2.4.0}/src/runops/project.py +0 -0
  96. {runops-2.3.0 → runops-2.4.0}/src/runops/runtime.py +0 -0
  97. {runops-2.3.0 → runops-2.4.0}/src/runops/skills/runops-check-status/SKILL.md +0 -0
  98. {runops-2.3.0 → runops-2.4.0}/src/runops/slurm.py +0 -0
  99. {runops-2.3.0 → runops-2.4.0}/src/runops/util.py +0 -0
  100. {runops-2.3.0 → runops-2.4.0}/tests/__init__.py +0 -0
  101. {runops-2.3.0 → runops-2.4.0}/tests/conftest.py +0 -0
  102. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/.codex/rules/a.md +0 -0
  103. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/.runops/test-runs/T20260701-0001/receipt.toml +0 -0
  104. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/.runops/work/beach-x-abc/bin/beach +0 -0
  105. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/AGENTS.md +0 -0
  106. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/CLAUDE.md +0 -0
  107. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/cases/beach/plane/beach.toml +0 -0
  108. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/cases/beach/plane/case.toml +0 -0
  109. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/cases/old/beach.toml +0 -0
  110. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/cases/old/case.toml +0 -0
  111. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/launchers.toml +0 -0
  112. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/research/CURRENT.md +0 -0
  113. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/runops.toml +0 -0
  114. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/runs/AGENTS.md +0 -0
  115. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/runs/_archive/plane/R20260601-0001/manifest.toml +0 -0
  116. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/runs/plane/R20260701-0001--a/input/beach.toml +0 -0
  117. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/runs/plane/R20260701-0001--a/manifest.toml +0 -0
  118. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/runs/plane/R20260702-0001/manifest.toml +0 -0
  119. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/runs/scan/R20260701-0002/manifest.toml +0 -0
  120. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/runs/scan/survey.toml +0 -0
  121. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/simulators.toml +0 -0
  122. {runops-2.3.0 → runops-2.4.0}/tests/fixtures/v1-project/site.toml +0 -0
  123. {runops-2.3.0 → runops-2.4.0}/tests/golden/mpi_standard.sh +0 -0
  124. {runops-2.3.0 → runops-2.4.0}/tests/golden/openmp_rsc.sh +0 -0
  125. {runops-2.3.0 → runops-2.4.0}/tests/test_build.py +0 -0
  126. {runops-2.3.0 → runops-2.4.0}/tests/test_dev_harness.py +0 -0
  127. {runops-2.3.0 → runops-2.4.0}/tests/test_jobgen.py +0 -0
  128. {runops-2.3.0 → runops-2.4.0}/tests/test_legacy_adapters.py +0 -0
  129. {runops-2.3.0 → runops-2.4.0}/tests/test_lifecycle.py +0 -0
  130. {runops-2.3.0 → runops-2.4.0}/tests/test_manifest_config.py +0 -0
  131. {runops-2.3.0 → runops-2.4.0}/tests/test_migrate.py +0 -0
  132. {runops-2.3.0 → runops-2.4.0}/tests/test_project_scope.py +0 -0
  133. {runops-2.3.0 → runops-2.4.0}/tests/test_safety.py +0 -0
  134. {runops-2.3.0 → runops-2.4.0}/tests/test_scratch_tidy.py +0 -0
  135. {runops-2.3.0 → runops-2.4.0}/tests/test_setup_misc.py +0 -0
  136. {runops-2.3.0 → runops-2.4.0}/tests/test_sweep.py +0 -0
  137. {runops-2.3.0 → runops-2.4.0}/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.0
3
+ Version: 2.4.0
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
@@ -73,6 +73,7 @@ runo init
73
73
  | 試してから本計算 | 100 step だけ smoke で試して、通ったら wc を 3 通りに振った候補を見せて |
74
74
  | 様子を見る | 先週投げた run の状況を見て。失敗していたら原因をノートに残して |
75
75
  | 続きを流す | R20261015-0003 の続きを 2 倍の step で |
76
+ | 資源を変えて作り直す | R20261015-0003 を 256 並列・72 時間で作り直して(case はそのまま) |
76
77
  | 片付ける | 古い run を整理して。消すものと archive するものを先に見せて |
77
78
  | 作り直した旧系列を片付ける | 旧系列は新系列で置き換えたので、対応を記録して archive して |
78
79
  | simulator を足す | パッチを当てた ~/BEACH-dev をビルドして、別の simulator として登録して |
@@ -42,6 +42,7 @@ runo init
42
42
  | 試してから本計算 | 100 step だけ smoke で試して、通ったら wc を 3 通りに振った候補を見せて |
43
43
  | 様子を見る | 先週投げた run の状況を見て。失敗していたら原因をノートに残して |
44
44
  | 続きを流す | R20261015-0003 の続きを 2 倍の step で |
45
+ | 資源を変えて作り直す | R20261015-0003 を 256 並列・72 時間で作り直して(case はそのまま) |
45
46
  | 片付ける | 古い run を整理して。消すものと archive するものを先に見せて |
46
47
  | 作り直した旧系列を片付ける | 旧系列は新系列で置き換えたので、対応を記録して archive して |
47
48
  | simulator を足す | パッチを当てた ~/BEACH-dev をビルドして、別の simulator として登録して |
@@ -283,6 +283,12 @@ ntasks = 800
283
283
  adapter の `render_inputs()` が `[params]` を適用して上書きする。
284
284
  - `runo new` / `runo sweep` の `--set k=v` は `[params]` の上書きである。
285
285
  既存 key でなくても受け付ける(adapter の `validate_params()` が warning を出せる)。
286
+ warning には判定した adapter と package version を付け、executable が package 由来でない
287
+ (`local_source` / `local_executable`)ときは「schema が古いだけかもしれない」と添える。
288
+ - run の `[job]` は、case から作る run では case の `[job]`、`new --from RUN`(`--continue` 含む)
289
+ では **元 run の `[job]`** を引き継ぐ(元 run に無い key は case から)。`--job k=v` で個別に上書きし(`[job]` の key のみ)、
290
+ `--job-from-case` で case の値に戻す。資源の変更に case の編集を要しない。作成時に、元 run
291
+ (または case)に対する params と資源の差分を表示する。
286
292
  - **variation は case ではなく run で表現する。** 「同じ物理設定で 1 つの値だけ変える」
287
293
  ために新しい case を作らない。`new CASE --set`、`new --from RUN --set`、`sweep` を使う。
288
294
  `case new --from CASE` は、base 入力ファイルの構造そのものが変わるときだけ使う。
@@ -388,6 +394,7 @@ cancel_reason = "" # cancel --why の理由。retry はこれが
388
394
 
389
395
  [variation]
390
396
  changed_keys = ["plasma.wc", "plasma.phiz"] # case の params との差分 key
397
+ job_changed_keys = ["walltime"] # case の [job] との差分 key(--job / 元 run から)
391
398
 
392
399
  [identity]
393
400
  input_hash = "sha256:..." # input/ 配下全ファイルの content hash
@@ -419,7 +426,7 @@ created ──submit──▶ submitted ──(slurm)──▶ running ──▶
419
426
  │ └───cancel──────────────┴──────▶ cancelled
420
427
  │
421
428
  └────────────── archive ─────────────────────────▶ archived
422
- (completed / failed / cancelled から)
429
+ (created / completed / failed / cancelled から)
423
430
  ```
424
431
 
425
432
  - `retry` は `failed` / `cancelled` を `created` に戻し `job.attempt` を +1 する。
@@ -494,9 +501,9 @@ created ──submit──▶ submitted ──(slurm)──▶ running ──▶
494
501
  | `runo context [--json]` | agent 向け現在地。project 名、simulator、active run 数、直近 run 5 件、scratch 期限切れ数、`LOG.md` 末尾 20 行。配置した runops skill が install 済みの runops より古ければ `runo update --skills-only` を促す warning |
495
502
  | `runo case new NAME --simulator SIM [--from CASE]` | case をスキャフォールド。adapter の `case_template()` を展開 |
496
503
  | `runo case list` | case 一覧 |
497
- | `runo new CASE [--why TEXT] [--label L] [--set k=v]... [--tag T]... [--scratch] [--dest DIR] [--submit] [--created-by WHO] [--json]` | 単発 run を作る |
498
- | `runo new --from RUN [--why] [--set k=v]... [--continue] [--submit]` | 既存 run の params から派生 run を作る。`--continue` は adapter が `extend_inputs()` を実装する場合のみ有効(§8.1) |
499
- | `runo sweep CASE --set k=v1,v2,... [--set ...] [--names KEY=n1,n2,...]... [--label L] [--dest DIR] [--why TEXT] [--apply] [--submit] [--limit N] [--json]` | 直積の候補を表示する。`--apply` で生成。`--submit` は `--apply` と同時のみ有効 |
504
+ | `runo new CASE [--why TEXT] [--label L] [--set k=v]... [--job k=v]... [--tag T]... [--scratch] [--dest DIR] [--submit] [--created-by WHO] [--json]` | 単発 run を作る |
505
+ | `runo new --from RUN [--why] [--set k=v]... [--job k=v]... [--job-from-case] [--continue] [--submit]` | 既存 run の params と `[job]` から派生 run を作る(§5)。`--continue` は adapter が `extend_inputs()` を実装する場合のみ有効(§8.1) |
506
+ | `runo sweep CASE --set k=v1,v2,... [--set ...] [--names KEY=n1,n2,...]... [--job k=v]... [--label L] [--dest DIR] [--why TEXT] [--apply] [--submit] [--limit N] [--json]` | 直積の候補を表示する。`--apply` で生成。`--submit` は `--apply` と同時のみ有効 |
500
507
  | `runo submit RUN... [--partition P] [--qos Q] [--afterok JOBID] [-y]` | job.sh を生成して sbatch。`created` 以外は拒否 |
501
508
  | `runo submit --all [DIR] [-y]` | DIR 配下の `created` run をすべて投入。件数と概算 core-hours を表示して確認 |
502
509
  | `runo cancel RUN... [--why TEXT] [-y]` | scancel して sync。`--why` は `cancelled: ...` の note として残す |
@@ -509,7 +516,7 @@ created ──submit──▶ submitted ──(slurm)──▶ running ──▶
509
516
  | `runo pin RUN [--off] [--reason TEXT]` | `run.pinned` を切り替える。pinned は `list` に常に出て、`tidy` / `delete` の対象外 |
510
517
  | `runo summarize RUN... [--force]` | adapter の `summarize()` を呼び `analysis/summary.json` を書く。既存があれば `--force` なしではスキップ |
511
518
  | `runo mv RUN... [--dest DIR] [--label L] [--why TEXT]` | run を `runs/` 配下で改名・移動する。id、入力、履歴は変えない(§7.7) |
512
- | `runo archive RUN\|DIR... [--superseded-by RUN\|DIR --why TEXT] [-y]` / `runo archive --older-than 90d [-y]` | `runs/_archive/<元の相対 path>` に移動して `archived` にする。移動前に全対象を検査し、指定した RUN が終端でなければ何も動かさない。DIR は配下の run(系列)を指し、終端でない run と pinned を除いて表示する。`--superseded-by` は各 run に置き換え先(`parent_run` がその run の置き換え先 run、無ければ指定した全 run)を `run.superseded_by` に記録し、`--why` を note に残す |
519
+ | `runo archive RUN\|DIR... [--superseded-by RUN\|DIR --why TEXT] [-y]` / `runo archive --older-than 90d [-y]` | `runs/_archive/<元の相対 path>` に移動して `archived` にする。対象は created と終端の run(created は job を持たないため)。移動前に全対象を検査し、指定した RUN が対象外なら何も動かさない。DIR は配下の run(系列)を指し、submitted / running と pinned を除いて表示する。created は `--superseded-by` があるときだけ含める。`--superseded-by` は各 run に置き換え先(`parent_run` がその run の置き換え先 run、無ければ指定した全 run)を `run.superseded_by` に記録し、`--why` を note に残す |
513
520
  | `runo restore RUN` | archive 前の場所に戻す。既に何かあれば拒否 |
514
521
  | `runo delete RUN... [-y]` | directory を削除。`created`/`failed`/`cancelled`/scratch のみ。`pinned` は拒否 |
515
522
  | `runo tidy [--apply] [--only KIND] [--older-than AGE] [--purge-work RUN...] [--json]` | 整理候補を表示する(§7.5)。`--apply` で実行 |
@@ -534,7 +541,7 @@ created ──submit──▶ submitted ──(slurm)──▶ running ──▶
534
541
 
535
542
  | kind | 条件 | 動作 |
536
543
  |---|---|---|
537
- | `expired-scratch` | `scratch = true` かつ `expires_at` を過ぎ、`pinned = false` | delete |
544
+ | `expired-scratch` | `scratch = true` かつ `expires_at` を過ぎ、`pinned = false`、archived でない | delete |
538
545
  | `stale-failed` | `failed`/`cancelled` で `defaults.tidy_failed_days`(既定 30)日より古く、note も `why` も空 | delete |
539
546
  | `old-completed` | `completed` で `defaults.tidy_archive_days`(既定 90)日より古く、`pinned = false` | archive |
540
547
  | `orphan-job` | `submitted`/`running` だが squeue/sacct に job が無い | sync(state 確定) |
@@ -573,6 +580,9 @@ created ──submit──▶ submitted ──(slurm)──▶ running ──▶
573
580
  - 対象は `created`、`completed`、`failed`、`cancelled`。`submitted` / `running` は拒否する
574
581
  (Slurm が run directory の絶対 path を持つ)。`archived` は `restore` を使う。
575
582
  - 既存 path は上書きしない(`move_noreplace`)。`created` の run は job.sh を作り直す。
583
+ - 複数 run は `run.lock` を 1 回取り、全対象の状態と移動先を検査してから移す。1 件でも拒否されれば
584
+ どれも動かさない。移動中に filesystem の error が起きたら止まり、移し終えた run id を error に示す
585
+ (exit code 1)。`archive RUN...` も同じ手順で移す。
576
586
  - 手で `mv` しても run は id で見つかるが、`run.label` と `[path].run_dir` が古いまま残る。
577
587
 
578
588
  ---
@@ -665,12 +675,15 @@ runops は `init` と `update` で次を行う。
665
675
  (例: `mpiemses3d-input-review`)。`runops-` で始まる名前は予約されており展開しない。
666
676
  2. `references/` を `.agents/skills/<adapter>-references/` にコピーし、
667
677
  `SKILL.md` が無ければ reference 一覧を列挙した `SKILL.md` を生成する。
678
+ 展開した skill の `../../references/`(package 内の配置での参照)は
679
+ `../<adapter>-references/` に書き換える。
668
680
  3. 展開先には `.managed-by-runops` marker を置く。marker のある directory は
669
681
  `update` 時に **無条件で上書き** する。marker の無い同名 directory は触らず warning。
670
682
  4. `.runops/skills.lock` に adapter 名、package version、展開した directory、content hash を記録する。
671
683
 
672
684
  `doctor` は lock の package version と現在 install されている version を比較し、
673
- 差があれば `runo update --skills-only` を提案する。
685
+ 差があれば `runo update --skills-only` を提案する。展開済み skill の相対リンクで
686
+ 存在しない file を指すものも報告する。
674
687
 
675
688
  既存の simulator repo(MPIEMSES3D、emout、BEACH)は `plugins/<name>-context/` に
676
689
  `skills/` と `references/` を既に持つ。各 package 側で必要な変更は
@@ -967,4 +980,6 @@ v2 の文書は次の 4 ファイルだけにする。
967
980
  | 操作範囲は起動 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
981
  | 置き換えた系列の整理(2026-10-21) | `archive` が DIR(系列)を受け、`--superseded-by` で置き換え先を manifest に記録する。新しい command と tidy の種別は足さない。skill は新系列を作ったら旧系列の要否を確かめ、不要なら archive して現用系列・archive 先・件数を報告する | 修正版を作った後に旧系列が作業領域に残り、毎回ユーザーが整理を頼んでいた。`parent_run` だけでは比較用の派生と置き換えを区別できないので、置き換えは明示操作で記録する。旧 `archive` は 1 件ずつ検査していて、途中の running run で一部だけ移動していた(#113) |
969
982
  | 生成ハーネスの書き方(2026-10-21) | AGENTS.md の Rules を「作業の進め方」5 項目に絞り、各項目を「何をするか」と短い理由で書く。skill は手順に専念し、AGENTS.md の方針(manifest を手で編集しない等)を繰り返さない | #112・#113 で規則が増え、禁止形(never / do not)が並んでいた。現行の Claude の prompting 指針は、しないことより何をするかを、理由と共に書くことを勧め、強い禁止語は過剰反応を招くとしている |
983
+ | 複数 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 で同じことが起き得た |
984
+ | FEEDBACK 棚卸し 2.4.0(2026-10-23) | `--job k=v` と `new --from` の `[job]` 継承、created の archive / restore、archived scratch を期限削除しない、展開 skill の references 参照の書き換えと `doctor` のリンク検査、adapter warning への版の併記、生成 AGENTS.md に解析環境の置き場所 | hub の FEEDBACK で、資源変更のための case 編集(3 回)、置換済み draft を archive できず helper で迂回(2 回)、保管目的の archive が期限削除される、skill から reference に辿れない、新版の正規キーが誤記に見える、解析依存の置き場所が project ごとに違う、が記録された。いずれも既存 command の拡張で、command 数は増やさない |
970
985
  | 使い方ガイド | `docs/guide.md` を追加し、文書を 4 ファイルにする(§13.3) | README の依頼例の表と command 一覧の間を埋める、場面ごとの流れ(case と run の分け方、smoke から本計算、状態の決まり方、整理)の置き場所が無かった |
@@ -81,7 +81,10 @@ project の `adapters/mysim.py` に `Adapter` という名前で置けば使え
81
81
  `runo init` と `runo update` は `skills/*` を project の `.agents/skills/` と
82
82
  `.claude/skills/` に同名でコピーし、`references/` を `<adapter>-references` として置きます。
83
83
  skill 名は package 側で名前空間を付けてください(例: `mysim-input-review`)。
84
- `runops-` で始まる名前は予約されています。展開した directory には
84
+ `runops-` で始まる名前は予約されています。skill から reference へは
85
+ `../<adapter>-references/X.md` と書いてください。package 内の配置どおりの
86
+ `../../references/X.md` は展開時にこの形へ書き換えます。`runo doctor` は展開済み skill の
87
+ 相対リンク切れを報告します。展開した directory には
85
88
  `.managed-by-runops` が置かれ、更新時に上書きされます。利用者が作った同名 directory は
86
89
  上書きしません。
87
90
 
@@ -105,8 +105,11 @@ run id、入力、sweep の帰属、ノートはそのままで、移動元は
105
105
  投入、削除、整理、移行の前には、エージェントが対象の run、partition、資源量、core-hours を示して
106
106
  確認を取ります。`runo` 自体も、複数 run の投入や削除の前に確認を求めます。
107
107
 
108
- 資源量(partition、walltime、並列数)は case の `[job]` に書きます。1 回だけ partition を
109
- 変えるなら「今回は gr20001b で投げて」と頼めば `runo submit RUN --partition gr20001b` になります。
108
+ 資源量(partition、walltime、並列数)は case の `[job]` に書きます。run ごとに変えるときは
109
+ 「R… を 256 並列・72 時間で作り直して」と頼めば `runo new --from RUN --job processes=256 --job walltime=72:00:00`
110
+ になり、case は変えません。`--from` で作る run は元 run の資源を引き継ぎ、作成時に元 run との
111
+ params と資源の差分を表示します。1 回だけ partition を変えるなら「今回は gr20001b で投げて」と頼めば
112
+ `runo submit RUN --partition gr20001b` になります。
110
113
 
111
114
  walltime は、同じ case・同じ資源で終わった run の実時間を元に決めます。`runo list --json` と
112
115
  `runo status` は、各 run の `elapsed`(最新の実行の実時間)、`walltime`、使った割合を示します。
@@ -209,7 +212,7 @@ run は消さなくても `runo list` が古いものを隠しますが、direct
209
212
 
210
213
  | 種類 | 条件 | 処理 |
211
214
  |---|---|---|
212
- | `expired-scratch` | 期限切れの scratch run | 削除 |
215
+ | `expired-scratch` | 期限切れの scratch run(archive したものは除く) | 削除 |
213
216
  | `stale-failed` | 30 日より古い failed / cancelled で、`why` もノートも無い | 削除 |
214
217
  | `old-completed` | 90 日より古い completed | archive |
215
218
  | `orphan-job` | 投入済みなのに Slurm が知らない | `sync` で状態を確定 |
@@ -225,8 +228,8 @@ run id で引き続き見つかり、`runo restore` で戻せます。
225
228
  > 旧系列は不要。新系列で置き換えたと記録して archive して。
226
229
 
227
230
  で `runo archive runs/<case>/<旧系列> --superseded-by runs/<case>/<新系列> --why "..."` になります。
228
- 系列の directory を渡すと、対象の run を先に表示し、投入中・実行中・作成のみの run と pin した run は
229
- 除きます。各旧 run の `superseded_by` には、その run から `--from` で作った新 run(無ければ新系列の全 run)が
231
+ 系列の directory を渡すと、対象の run を先に表示し、投入中・実行中の run と pin した run は
232
+ 除きます。投入前の run(created)も `--superseded-by` を付けたときは archive でき、`runo restore` で戻せます。各旧 run の `superseded_by` には、その run から `--from` で作った新 run(無ければ新系列の全 run)が
230
233
  記録され、理由はノートに残ります。比較用に派生しただけの run は置き換えとは扱わないので、
231
234
  旧系列が不要かどうかは毎回確かめます。
232
235
 
@@ -112,7 +112,7 @@ runops だけが書きます。手で編集しないでください。主な fie
112
112
  | `[simulator]` | `name`、`adapter`、`adapter_source`、`adapter_version`、`executable`、`resolver_mode`、`package_version`、`exe_hash`、`git_commit`、`git_dirty`、`build_dir` |
113
113
  | `[job]` | case の `[job]` に加え `job_id`、`attempt`、`submitted_at`、`finished_at`、`last_slurm_state`、`exit_code`、`[[job.attempts]]`(attempt ごとの `job_id`、`started_at`、`exit_code`、`exe_hash`、`elapsed`、`cancel --why` の `cancel_reason`) |
114
114
  | `[params]` | case の params に `--set` を適用した最終値 |
115
- | `[variation]` | `changed_keys` |
115
+ | `[variation]` | `changed_keys`、`job_changed_keys`(case の `[job]` と違う key) |
116
116
  | `[identity]` | `input_hash` |
117
117
  | `[storage]` | `work_purged_at` |
118
118
  | `[[notes]]` | `at`、`by`、`text` |
@@ -148,22 +148,22 @@ run id は削除後も再利用しません。`--dest` に既存 run の中は
148
148
  | command | 動作 |
149
149
  |---|---|
150
150
  | `runo init [SIM...] [--site kudpc\|standard] [--no-install] [-y]` | project を作る。`SIM` は `emses` や `name=package-spec`。端末から引数なしで実行すると site と simulator を尋ねる。`--site` の既定はホスト名から推定(KUDPC のログインノードなら `kudpc`、`camphor` も同義) |
151
- | `runo doctor [--json]` | 設定、adapter の解決経路、executable、sbatch、skill の鮮度を点検 |
151
+ | `runo doctor [--json]` | 設定、adapter の解決経路、executable、sbatch、skill の鮮度とリンク切れを点検 |
152
152
  | `runo update [--skills-only] [-y]` | simulator package を更新し skill と AGENTS.md の管理 block を再生成 |
153
153
  | `runo build SIM [--force]` | `local_source` の simulator を build |
154
154
  | `runo context [--json]` | agent 向けの現在地 |
155
155
  | `runo case new NAME [-s SIM] [--from CASE]` / `runo case list` | case の作成と一覧 |
156
- | `runo new CASE [--why] [--label] [--set k=v]... [--tag] [--scratch] [--dest DIR] [--submit] [--json]` | run を 1 つ作る |
157
- | `runo new --from RUN [--set k=v]... [--continue]` | 既存 run の params から派生。`--continue` は adapter の restart 対応が必要 |
158
- | `runo sweep CASE --set k=v1,v2 ... [--names k=n1,n2...] [--label] [--dest DIR] [--limit N] [--apply] [--submit] [--json]` | 直積の候補表示。`--apply` で作成。`--names` は各値の短い label、`--dest` は sweep directory の親 |
156
+ | `runo new CASE [--why] [--label] [--set k=v]... [--job k=v]... [--tag] [--scratch] [--dest DIR] [--submit] [--json]` | run を 1 つ作る。`--job` はこの run の `[job]` の値 |
157
+ | `runo new --from RUN [--set k=v]... [--job k=v]... [--job-from-case] [--continue]` | 既存 run の params と `[job]` から派生(`--job-from-case` で `[job]` は case から)。`--continue` は adapter の restart 対応が必要 |
158
+ | `runo sweep CASE --set k=v1,v2 ... [--names k=n1,n2...] [--job k=v]... [--label] [--dest DIR] [--limit N] [--apply] [--submit] [--json]` | 直積の候補表示。`--apply` で作成。`--names` は各値の短い label、`--dest` は sweep directory の親 |
159
159
  | `runo submit RUN... [-p P] [--qos Q] [--afterok JOB]` / `runo submit --all [DIR] [-y]` | job.sh を生成して sbatch |
160
160
  | `runo cancel RUN... [--why TEXT] [-y]` / `runo retry RUN [--submit] [-y]` | scancel(`--why` は note に残る)/ failed・cancelled を created に戻す(理由付きで cancel された run は確認を取る) |
161
161
  | `runo status [RUN...]` / `runo sync [RUN...]` / `runo log RUN [--err] [-n N] [-f]` | 状態表示 / Slurm から更新 / log 表示 |
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 は不可 |
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` をノートに残す |
165
+ | `runo mv RUN... [--dest DIR] [--label L] [--why TEXT]` | `runs/` 配下で改名・移動。id、入力、sweep、ノートは保つ。submitted / running と archived は不可。全対象を検査してから移し、1 件でも不可なら何も動かさない |
166
+ | `runo archive RUN\|DIR... [--superseded-by RUN\|DIR --why TEXT]` / `runo archive --older-than 90d` / `runo restore RUN` | `runs/_archive/` へ移動 / 戻す。対象は created と終了した run。移動前に全対象を検査し、対象外の RUN があれば何も動かさない。DIR は配下の run(系列)で、submitted / running と pinned を除き、created は `--superseded-by` のときだけ含めて表示する。`--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...]` | 整理候補の表示と実行 |
169
169
  | `runo migrate-v1 [--dry-run] [--apply-delete-test-runs] [-y]` | v1 project の変換 |
@@ -175,7 +175,7 @@ scratch と archived は表示しません。
175
175
 
176
176
  | kind | 条件 | 動作 |
177
177
  |---|---|---|
178
- | `expired-scratch` | 期限切れの scratch run | delete |
178
+ | `expired-scratch` | 期限切れの scratch run(archived は除く) | delete |
179
179
  | `stale-failed` | `tidy_failed_days`(既定 30)日より古い failed / cancelled で、why も note も無い | delete |
180
180
  | `old-completed` | `tidy_archive_days`(既定 90)日より古い completed | archive |
181
181
  | `orphan-job` | Slurm が知らない submitted / running | sync |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "runops"
7
- version = "2.3.0"
7
+ version = "2.4.0"
8
8
  description = "Run ledger and Slurm job submission for HPC simulation projects"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -1,3 +1,3 @@
1
1
  """runops: run ledger and Slurm job submission for HPC simulation projects."""
2
2
 
3
- __version__ = "2.3.0"
3
+ __version__ = "2.4.0"
@@ -312,9 +312,13 @@ def case_list(as_json: bool = typer.Option(False, "--json")) -> None:
312
312
  # ---------------------------------------------------------------------------
313
313
 
314
314
 
315
- def _print_created(project: Project, manifests: list[Manifest]) -> None:
315
+ def _print_created(
316
+ project: Project, manifests: list[Manifest], changes: list[str] | None = None
317
+ ) -> None:
316
318
  for m in manifests:
317
319
  out.print(f"created {m.id} {project.rel(m.run_dir)}")
320
+ for line in changes or []:
321
+ out.print(f" {line}", markup=False)
318
322
 
319
323
 
320
324
  def _submit_each(project: Project, manifests: list[Manifest], *, quiet: bool = False) -> None:
@@ -341,16 +345,24 @@ def new(
341
345
  continue_run: bool = typer.Option(
342
346
  False, "--continue", help="Continue from --from RUN (adapter support needed)."
343
347
  ),
348
+ job: Optional[list[str]] = typer.Option(
349
+ None, "--job", help="key=value for this run's [job] (walltime, processes, ...)."
350
+ ),
351
+ job_from_case: bool = typer.Option(
352
+ False, "--job-from-case", help="With --from: take [job] from the case, not the run."
353
+ ),
344
354
  submit: bool = typer.Option(False, "--submit"),
345
355
  created_by: str = typer.Option("", "--created-by"),
346
356
  as_json: bool = typer.Option(False, "--json", help="Print the created run as JSON."),
347
357
  ) -> None:
348
358
  """Create one run."""
349
- from runops.ops.create import CreateRequest, create_run, parse_sets
359
+ from runops.ops.create import CreateRequest, create_run, parse_job, parse_sets
350
360
 
351
361
  project = _project()
352
362
  if case and from_run:
353
363
  raise UsageError("give either CASE or --from RUN, not both")
364
+ if job_from_case and not from_run:
365
+ raise UsageError("--job-from-case needs --from RUN")
354
366
  request = CreateRequest(
355
367
  case=case or "",
356
368
  sets=parse_sets(sets or []),
@@ -362,13 +374,15 @@ def new(
362
374
  created_by=created_by or _actor(),
363
375
  from_run=from_run,
364
376
  continue_run=continue_run,
377
+ job=parse_job(job or []),
378
+ job_from_case=job_from_case,
365
379
  )
366
380
  result = create_run(project, request)
367
381
  _warn(result.warnings)
368
382
  if not why:
369
383
  _warn(["no --why given; consider `runo note RUN` to record the purpose"])
370
384
  if not as_json:
371
- _print_created(project, result.manifests)
385
+ _print_created(project, result.manifests, result.changes)
372
386
  if submit:
373
387
  _submit_each(project, result.manifests, quiet=as_json)
374
388
  if as_json:
@@ -388,6 +402,9 @@ def sweep(
388
402
  dest: str = typer.Option(
389
403
  "", "--dest", help="Parent of the sweep directory (default runs/<case>)."
390
404
  ),
405
+ job: Optional[list[str]] = typer.Option(
406
+ None, "--job", help="key=value for the [job] of every point (walltime, processes, ...)."
407
+ ),
391
408
  why: str = typer.Option("", "--why"),
392
409
  apply: bool = typer.Option(False, "--apply", help="Create the runs."),
393
410
  submit: bool = typer.Option(False, "--submit", help="Submit after --apply."),
@@ -401,6 +418,7 @@ def sweep(
401
418
  from runops.ops.create import (
402
419
  CreateRequest,
403
420
  apply_sweep,
421
+ parse_job,
404
422
  parse_sweep_names,
405
423
  parse_sweep_sets,
406
424
  plan_sweep,
@@ -418,6 +436,7 @@ def sweep(
418
436
  limit=limit,
419
437
  dest=dest,
420
438
  names=parse_sweep_names(names or [], axes),
439
+ job=parse_job(job or []),
421
440
  )
422
441
  if as_json and not apply:
423
442
  _print_json(
@@ -429,6 +448,7 @@ def sweep(
429
448
  {"ref": p.ref, "label": p.label, "values": p.values} for p in plan.points
430
449
  ],
431
450
  "core_hours": plan.core_hours,
451
+ "job": plan.job,
432
452
  }
433
453
  )
434
454
  return
@@ -446,6 +466,15 @@ def sweep(
446
466
  f"{len(plan.points)} runs -> {project.rel(plan.parent)}/ "
447
467
  f"(~{plan.core_hours:.1f} core-hours)"
448
468
  )
469
+ overridden = {k: v for k, v in plan.job.items() if plan.case.job.get(k) != v}
470
+ if overridden:
471
+ out.print(
472
+ " job vs case: "
473
+ + ", ".join(
474
+ f"{k} {plan.case.job.get(k, '-')} -> {v}" for k, v in overridden.items()
475
+ ),
476
+ markup=False,
477
+ )
449
478
  if not apply:
450
479
  out.print("preview only; add --apply to create")
451
480
  return
@@ -458,7 +487,7 @@ def sweep(
458
487
  result = apply_sweep(project, plan, request)
459
488
  _warn(result.warnings)
460
489
  if not as_json:
461
- _print_created(project, result.manifests)
490
+ _print_created(project, result.manifests, result.changes)
462
491
  if submit:
463
492
  _submit_each(project, result.manifests, quiet=as_json)
464
493
  if as_json:
@@ -586,6 +615,8 @@ def _status_table(project: Project, manifests: list[Manifest]) -> Table:
586
615
  status += " [dim](scratch)[/]"
587
616
  if s["pinned"]:
588
617
  status += " [dim](pinned)[/]"
618
+ if s["superseded_by"]:
619
+ status += f" [dim](-> {', '.join(s['superseded_by'])})[/]"
589
620
  table.add_row(
590
621
  s["id"],
591
622
  status,
@@ -793,10 +824,10 @@ def archive(
793
824
  why: str = typer.Option("", "--why", help="Reason, recorded in each run's notes."),
794
825
  yes: bool = typer.Option(False, "-y", "--yes"),
795
826
  ) -> None:
796
- """Move terminal runs to runs/_archive/ (same relative path)."""
827
+ """Move created or finished runs to runs/_archive/ (same relative path)."""
797
828
  from datetime import datetime
798
829
 
799
- from runops.ops.tidy import ArchivePlan, archive_run, plan_archive
830
+ from runops.ops.tidy import ArchivePlan, archive_runs, plan_archive
800
831
 
801
832
  project = _project()
802
833
  if superseded_by and not why:
@@ -827,11 +858,11 @@ def archive(
827
858
  line += f" -> {', '.join(plan.superseded_by[m.id])}"
828
859
  out.print(line)
829
860
  _confirm(f"Archive {len(plan.targets)} runs?", yes)
830
- for m in plan.targets:
831
- dst = archive_run(
832
- project, m, superseded_by=plan.superseded_by.get(m.id), why=why, by=_actor()
833
- )
834
- out.print(f"archived {m.id} -> {project.rel(dst)}")
861
+ archived = archive_runs(
862
+ project, plan.targets, superseded_by=plan.superseded_by, why=why, by=_actor()
863
+ )
864
+ for run_id, dst in archived:
865
+ out.print(f"archived {run_id} -> {project.rel(dst)}")
835
866
 
836
867
 
837
868
  @app.command("mv")
@@ -844,7 +875,7 @@ def mv_cmd(
844
875
  why: str = typer.Option("", "--why", help="Reason, recorded in the run's notes."),
845
876
  ) -> None:
846
877
  """Rename or move runs under runs/, keeping their ids, inputs and history."""
847
- from runops.ops.tidy import move_run
878
+ from runops.ops.tidy import move_runs
848
879
 
849
880
  if not dest and label is None:
850
881
  raise UsageError("give --dest and/or --label")
@@ -852,12 +883,10 @@ def mv_cmd(
852
883
  targets = _resolve(project, runs)
853
884
  if label is not None and len(targets) > 1:
854
885
  raise UsageError("--label applies to one RUN; use --dest to move several")
855
- warnings: list[str] = []
856
- for m in targets:
857
- result = move_run(project, m, dest=dest, label=label, why=why, by=_actor())
858
- warnings += result.warnings
859
- out.print(f"moved {m.id}: {project.rel(result.src)} -> {project.rel(result.dst)}")
860
- _warn(warnings)
886
+ results = move_runs(project, targets, dest=dest, label=label, why=why, by=_actor())
887
+ for r in results:
888
+ out.print(f"moved {r.run_id}: {project.rel(r.src)} -> {project.rel(r.dst)}")
889
+ _warn([w for r in results for w in r.warnings])
861
890
 
862
891
 
863
892
  @app.command()
@@ -14,8 +14,8 @@ COMMANDS = """\
14
14
  |---|---|
15
15
  | `runo context --json` | where the project stands: active runs, recent runs, LOG.md tail |
16
16
  | `runo case list` / `runo case new NAME --simulator SIM [--from CASE]` | list / scaffold cases |
17
- | `runo new CASE --why TEXT [--set k=v]... [--scratch] [--submit]` | create one run |
18
- | `runo new --from RUN --why TEXT [--set k=v]... [--continue]` | derive a run from another |
17
+ | `runo new CASE --why TEXT [--set k=v]... [--job k=v]... [--scratch] [--submit]` | create one run |
18
+ | `runo new --from RUN --why TEXT [--set k=v]... [--job k=v]... [--continue]` | derive a run (keeps its params and [job]) |
19
19
  | `runo sweep CASE --set k=v1,v2 [--names k=n1,n2] [--apply] [--submit]` | preview / create a parameter product |
20
20
  | `runo submit RUN... [--partition P]` / `runo submit --all DIR` | sbatch created runs |
21
21
  | `runo sync` / `runo status RUN` / `runo log RUN [--err]` | refresh state from Slurm / show / read logs |
@@ -25,7 +25,7 @@ COMMANDS = """\
25
25
  | `runo summarize RUN` | adapter summary into analysis/summary.json |
26
26
  | `runo tidy` / `runo tidy --apply` | list / apply cleanup (expired scratch, stale failures, old runs) |
27
27
  | `runo mv RUN... [--dest DIR] [--label L] --why TEXT` | rename / move runs, keeping ids and inputs |
28
- | `runo archive RUN\\|DIR... [--superseded-by RUN\\|DIR --why T]` | move finished runs (or a series) to runs/_archive |
28
+ | `runo archive RUN\\|DIR... [--superseded-by RUN\\|DIR --why T]` | move created or finished runs (or a series) to runs/_archive |
29
29
  | `runo restore RUN` / `runo delete RUN` | move back / remove |
30
30
  | `runo build SIM` | build a local_source simulator into .runops/build |
31
31
  | `runo doctor` / `runo update` | check environment / upgrade simulator packages and skills |"""
@@ -70,9 +70,9 @@ the project found from the current directory.
70
70
  sessions, so act on them only when the user names them. If nothing here matches, say so.
71
71
  - **Change runs through `runo`.** `runo` writes each run's manifest and `input/`, so
72
72
  they stay an exact record of what ran.
73
- Express a change as a new run (`runo new --from RUN --set k=v`, `runo sweep`) and
74
- rename or regroup with `runo mv`. Variations are runs; a new case is for a different
75
- structure of the base input.
73
+ Express a change as a new run (`runo new --from RUN --set k=v`, `--job walltime=...`
74
+ for resources, `runo sweep`) and rename or regroup with `runo mv`. Variations are
75
+ runs; a new case is for a different structure of the base input.
76
76
  - **Leave the reason behind.** Give each new run `--why`. When you cancel runs to fix or
77
77
  replace them, add `runo cancel --why`, so later sessions and `runo retry` leave them
78
78
  stopped. When a new series replaces an old one, ask whether the old one is still
@@ -86,6 +86,10 @@ Notes live in three places, where `runo context` and the next session find them:
86
86
  - reusable knowledge (formulas, methods, pitfalls): `notes/<topic>.md`, extending the
87
87
  existing file for a topic.
88
88
 
89
+ Analysis tools the simulator package does not bring go in `.runops/analysis-env/`
90
+ (`uv venv .runops/analysis-env`), so `.venv` stays as `runo update` manages it. Run heavy
91
+ reading and plotting where the site allows compute jobs.
92
+
89
93
  ## Skills
90
94
 
91
95
  {skill_lines}
@@ -27,7 +27,7 @@ ACTIVE_STATES = frozenset({RunState.SUBMITTED, RunState.RUNNING})
27
27
  TERMINAL_STATES = frozenset({RunState.COMPLETED, RunState.FAILED, RunState.CANCELLED})
28
28
 
29
29
  _TRANSITIONS: dict[RunState, frozenset[RunState]] = {
30
- RunState.CREATED: frozenset({RunState.SUBMITTED}),
30
+ RunState.CREATED: frozenset({RunState.SUBMITTED, RunState.ARCHIVED}),
31
31
  RunState.SUBMITTED: frozenset(
32
32
  {RunState.RUNNING, RunState.COMPLETED, RunState.FAILED, RunState.CANCELLED}
33
33
  ),
@@ -37,10 +37,24 @@ _TRANSITIONS: dict[RunState, frozenset[RunState]] = {
37
37
  RunState.COMPLETED: frozenset({RunState.ARCHIVED}),
38
38
  RunState.FAILED: frozenset({RunState.CREATED, RunState.ARCHIVED}),
39
39
  RunState.CANCELLED: frozenset({RunState.CREATED, RunState.ARCHIVED}),
40
- RunState.ARCHIVED: frozenset(TERMINAL_STATES),
40
+ RunState.ARCHIVED: frozenset({*TERMINAL_STATES, RunState.CREATED}),
41
41
  }
42
42
 
43
43
 
44
+ # Shown with each run so resource differences are visible next to parameter differences.
45
+ _RESOURCE_KEYS = (
46
+ "partition",
47
+ "qos",
48
+ "walltime",
49
+ "nodes",
50
+ "ntasks",
51
+ "cpus_per_task",
52
+ "processes",
53
+ "threads",
54
+ "cores",
55
+ )
56
+
57
+
44
58
  class Manifest:
45
59
  """A run manifest backed by a plain dict so unknown fields survive."""
46
60
 
@@ -198,6 +212,8 @@ class Manifest:
198
212
  # Enough for an agent to compare runs without another command.
199
213
  "params": self.params,
200
214
  "changed_keys": list(self.section("variation").get("changed_keys", [])),
215
+ "job_changed_keys": list(self.section("variation").get("job_changed_keys", [])),
216
+ "resources": {k: self.job[k] for k in _RESOURCE_KEYS if k in self.job},
201
217
  "parent_run": self.parent_run,
202
218
  "superseded_by": self.superseded_by,
203
219
  "exe_hash": str(self.section("simulator").get("exe_hash", "")),