runops 2.3.1__tar.gz → 2.5.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 (139) hide show
  1. {runops-2.3.1 → runops-2.5.0}/PKG-INFO +2 -1
  2. {runops-2.3.1 → runops-2.5.0}/README.md +1 -0
  3. {runops-2.3.1 → runops-2.5.0}/SPEC.md +37 -7
  4. {runops-2.3.1 → runops-2.5.0}/docs/adapter.md +6 -1
  5. {runops-2.3.1 → runops-2.5.0}/docs/guide.md +14 -5
  6. {runops-2.3.1 → runops-2.5.0}/docs/reference.md +11 -9
  7. {runops-2.3.1 → runops-2.5.0}/pyproject.toml +1 -1
  8. {runops-2.3.1 → runops-2.5.0}/src/runops/__init__.py +1 -1
  9. {runops-2.3.1 → runops-2.5.0}/src/runops/cli.py +49 -6
  10. {runops-2.3.1 → runops-2.5.0}/src/runops/harness.py +10 -6
  11. {runops-2.3.1 → runops-2.5.0}/src/runops/manifest.py +26 -2
  12. {runops-2.3.1 → runops-2.5.0}/src/runops/ops/create.py +127 -11
  13. runops-2.5.0/src/runops/ops/progress.py +197 -0
  14. {runops-2.3.1 → runops-2.5.0}/src/runops/ops/setup.py +4 -0
  15. {runops-2.3.1 → runops-2.5.0}/src/runops/ops/skills.py +57 -4
  16. {runops-2.3.1 → runops-2.5.0}/src/runops/ops/sync.py +8 -0
  17. {runops-2.3.1 → runops-2.5.0}/src/runops/ops/tidy.py +19 -6
  18. {runops-2.3.1 → runops-2.5.0}/src/runops/skills/runops-check-status/SKILL.md +5 -2
  19. {runops-2.3.1 → runops-2.5.0}/src/runops/skills/runops-create-run/SKILL.md +7 -4
  20. {runops-2.3.1 → runops-2.5.0}/src/runops/skills/runops-maintain/SKILL.md +2 -2
  21. {runops-2.3.1 → runops-2.5.0}/src/runops/skills/runops-reference/SKILL.md +6 -6
  22. {runops-2.3.1 → runops-2.5.0}/src/runops/skills/runops-tidy/SKILL.md +5 -4
  23. {runops-2.3.1 → runops-2.5.0}/tests/test_adapters.py +28 -0
  24. {runops-2.3.1 → runops-2.5.0}/tests/test_archive_series.py +37 -1
  25. runops-2.5.0/tests/test_progress.py +120 -0
  26. runops-2.5.0/tests/test_run_job.py +82 -0
  27. {runops-2.3.1 → runops-2.5.0}/tests/test_skills.py +38 -0
  28. {runops-2.3.1 → runops-2.5.0}/uv.lock +1 -1
  29. {runops-2.3.1 → runops-2.5.0}/.agents/skills/check/SKILL.md +0 -0
  30. {runops-2.3.1 → runops-2.5.0}/.agents/skills/improve-harness/SKILL.md +0 -0
  31. {runops-2.3.1 → runops-2.5.0}/.agents/skills/python-package-refactor/SKILL.md +0 -0
  32. {runops-2.3.1 → runops-2.5.0}/.agents/skills/python-package-refactor/agents/openai.yaml +0 -0
  33. {runops-2.3.1 → runops-2.5.0}/.agents/skills/python-package-refactor/examples/refactor-request.md +0 -0
  34. {runops-2.3.1 → runops-2.5.0}/.agents/skills/python-package-refactor/references/anti-patterns.md +0 -0
  35. {runops-2.3.1 → runops-2.5.0}/.agents/skills/python-package-refactor/references/api-compatibility.md +0 -0
  36. {runops-2.3.1 → runops-2.5.0}/.agents/skills/python-package-refactor/references/packaging-modernization.md +0 -0
  37. {runops-2.3.1 → runops-2.5.0}/.agents/skills/python-package-refactor/references/refactor-playbook.md +0 -0
  38. {runops-2.3.1 → runops-2.5.0}/.agents/skills/python-package-refactor/references/verification.md +0 -0
  39. {runops-2.3.1 → runops-2.5.0}/.agents/skills/python-package-refactor/scripts/api_surface_snapshot.py +0 -0
  40. {runops-2.3.1 → runops-2.5.0}/.agents/skills/python-package-refactor/scripts/import_smoke.py +0 -0
  41. {runops-2.3.1 → runops-2.5.0}/.agents/skills/python-package-refactor/scripts/inspect_python_package.py +0 -0
  42. {runops-2.3.1 → runops-2.5.0}/.agents/skills/python-package-refactor/scripts/refactor_quality_gate.py +0 -0
  43. {runops-2.3.1 → runops-2.5.0}/.agents/skills/release/SKILL.md +0 -0
  44. {runops-2.3.1 → runops-2.5.0}/.agents/skills/simulator-adapter/SKILL.md +0 -0
  45. {runops-2.3.1 → runops-2.5.0}/.agents/skills/spec-reviewer/SKILL.md +0 -0
  46. {runops-2.3.1 → runops-2.5.0}/.agents/skills/sync-docs/SKILL.md +0 -0
  47. {runops-2.3.1 → runops-2.5.0}/.claude/agents/spec-reviewer.md +0 -0
  48. {runops-2.3.1 → runops-2.5.0}/.claude/rules/architecture.md +0 -0
  49. {runops-2.3.1 → runops-2.5.0}/.claude/rules/dev-workflow.md +0 -0
  50. {runops-2.3.1 → runops-2.5.0}/.claude/rules/gotchas.md +0 -0
  51. {runops-2.3.1 → runops-2.5.0}/.claude/settings.json +0 -0
  52. {runops-2.3.1 → runops-2.5.0}/.claude/skills/check/SKILL.md +0 -0
  53. {runops-2.3.1 → runops-2.5.0}/.claude/skills/improve-harness/SKILL.md +0 -0
  54. {runops-2.3.1 → runops-2.5.0}/.claude/skills/python-package-refactor/SKILL.md +0 -0
  55. {runops-2.3.1 → runops-2.5.0}/.claude/skills/release/SKILL.md +0 -0
  56. {runops-2.3.1 → runops-2.5.0}/.claude/skills/simulator-adapter/SKILL.md +0 -0
  57. {runops-2.3.1 → runops-2.5.0}/.claude/skills/spec-reviewer/SKILL.md +0 -0
  58. {runops-2.3.1 → runops-2.5.0}/.claude/skills/sync-docs/SKILL.md +0 -0
  59. {runops-2.3.1 → runops-2.5.0}/.codex/README.md +0 -0
  60. {runops-2.3.1 → runops-2.5.0}/.codex/agents/spec-reviewer.toml +0 -0
  61. {runops-2.3.1 → runops-2.5.0}/.codex/config.toml +0 -0
  62. {runops-2.3.1 → runops-2.5.0}/.codex/rules/runops.rules +0 -0
  63. {runops-2.3.1 → runops-2.5.0}/.dockerignore +0 -0
  64. {runops-2.3.1 → runops-2.5.0}/.gitattributes +0 -0
  65. {runops-2.3.1 → runops-2.5.0}/.github/assets/conversation-dark.svg +0 -0
  66. {runops-2.3.1 → runops-2.5.0}/.github/assets/conversation-light.svg +0 -0
  67. {runops-2.3.1 → runops-2.5.0}/.github/assets/hero-dark.svg +0 -0
  68. {runops-2.3.1 → runops-2.5.0}/.github/assets/hero-light.svg +0 -0
  69. {runops-2.3.1 → runops-2.5.0}/.github/assets/make_readme_art.py +0 -0
  70. {runops-2.3.1 → runops-2.5.0}/.github/workflows/ci.yml +0 -0
  71. {runops-2.3.1 → runops-2.5.0}/.github/workflows/publish.yml +0 -0
  72. {runops-2.3.1 → runops-2.5.0}/.gitignore +0 -0
  73. {runops-2.3.1 → runops-2.5.0}/.harness/manifest.toml +0 -0
  74. {runops-2.3.1 → runops-2.5.0}/.pre-commit-config.yaml +0 -0
  75. {runops-2.3.1 → runops-2.5.0}/.vscode/settings.json +0 -0
  76. {runops-2.3.1 → runops-2.5.0}/AGENTS.md +0 -0
  77. {runops-2.3.1 → runops-2.5.0}/CLAUDE.md +0 -0
  78. {runops-2.3.1 → runops-2.5.0}/LICENSE +0 -0
  79. {runops-2.3.1 → runops-2.5.0}/src/runops/adapters/__init__.py +0 -0
  80. {runops-2.3.1 → runops-2.5.0}/src/runops/adapters/base.py +0 -0
  81. {runops-2.3.1 → runops-2.5.0}/src/runops/adapters/generic.py +0 -0
  82. {runops-2.3.1 → runops-2.5.0}/src/runops/adapters/registry.py +0 -0
  83. {runops-2.3.1 → runops-2.5.0}/src/runops/case.py +0 -0
  84. {runops-2.3.1 → runops-2.5.0}/src/runops/config.py +0 -0
  85. {runops-2.3.1 → runops-2.5.0}/src/runops/errors.py +0 -0
  86. {runops-2.3.1 → runops-2.5.0}/src/runops/jobgen.py +0 -0
  87. {runops-2.3.1 → runops-2.5.0}/src/runops/launchers.py +0 -0
  88. {runops-2.3.1 → runops-2.5.0}/src/runops/legacy_adapters/__init__.py +0 -0
  89. {runops-2.3.1 → runops-2.5.0}/src/runops/legacy_adapters/beach.py +0 -0
  90. {runops-2.3.1 → runops-2.5.0}/src/runops/legacy_adapters/emses.py +0 -0
  91. {runops-2.3.1 → runops-2.5.0}/src/runops/ops/__init__.py +0 -0
  92. {runops-2.3.1 → runops-2.5.0}/src/runops/ops/common.py +0 -0
  93. {runops-2.3.1 → runops-2.5.0}/src/runops/ops/listing.py +0 -0
  94. {runops-2.3.1 → runops-2.5.0}/src/runops/ops/migrate.py +0 -0
  95. {runops-2.3.1 → runops-2.5.0}/src/runops/ops/notes.py +0 -0
  96. {runops-2.3.1 → runops-2.5.0}/src/runops/ops/submit.py +0 -0
  97. {runops-2.3.1 → runops-2.5.0}/src/runops/project.py +0 -0
  98. {runops-2.3.1 → runops-2.5.0}/src/runops/runtime.py +0 -0
  99. {runops-2.3.1 → runops-2.5.0}/src/runops/slurm.py +0 -0
  100. {runops-2.3.1 → runops-2.5.0}/src/runops/util.py +0 -0
  101. {runops-2.3.1 → runops-2.5.0}/tests/__init__.py +0 -0
  102. {runops-2.3.1 → runops-2.5.0}/tests/conftest.py +0 -0
  103. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/.codex/rules/a.md +0 -0
  104. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/.runops/test-runs/T20260701-0001/receipt.toml +0 -0
  105. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/.runops/work/beach-x-abc/bin/beach +0 -0
  106. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/AGENTS.md +0 -0
  107. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/CLAUDE.md +0 -0
  108. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/cases/beach/plane/beach.toml +0 -0
  109. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/cases/beach/plane/case.toml +0 -0
  110. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/cases/old/beach.toml +0 -0
  111. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/cases/old/case.toml +0 -0
  112. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/launchers.toml +0 -0
  113. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/research/CURRENT.md +0 -0
  114. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/runops.toml +0 -0
  115. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/runs/AGENTS.md +0 -0
  116. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/runs/_archive/plane/R20260601-0001/manifest.toml +0 -0
  117. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/runs/plane/R20260701-0001--a/input/beach.toml +0 -0
  118. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/runs/plane/R20260701-0001--a/manifest.toml +0 -0
  119. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/runs/plane/R20260702-0001/manifest.toml +0 -0
  120. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/runs/scan/R20260701-0002/manifest.toml +0 -0
  121. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/runs/scan/survey.toml +0 -0
  122. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/simulators.toml +0 -0
  123. {runops-2.3.1 → runops-2.5.0}/tests/fixtures/v1-project/site.toml +0 -0
  124. {runops-2.3.1 → runops-2.5.0}/tests/golden/mpi_standard.sh +0 -0
  125. {runops-2.3.1 → runops-2.5.0}/tests/golden/openmp_rsc.sh +0 -0
  126. {runops-2.3.1 → runops-2.5.0}/tests/test_build.py +0 -0
  127. {runops-2.3.1 → runops-2.5.0}/tests/test_dev_harness.py +0 -0
  128. {runops-2.3.1 → runops-2.5.0}/tests/test_jobgen.py +0 -0
  129. {runops-2.3.1 → runops-2.5.0}/tests/test_legacy_adapters.py +0 -0
  130. {runops-2.3.1 → runops-2.5.0}/tests/test_lifecycle.py +0 -0
  131. {runops-2.3.1 → runops-2.5.0}/tests/test_manifest_config.py +0 -0
  132. {runops-2.3.1 → runops-2.5.0}/tests/test_migrate.py +0 -0
  133. {runops-2.3.1 → runops-2.5.0}/tests/test_mv_naming.py +0 -0
  134. {runops-2.3.1 → runops-2.5.0}/tests/test_project_scope.py +0 -0
  135. {runops-2.3.1 → runops-2.5.0}/tests/test_safety.py +0 -0
  136. {runops-2.3.1 → runops-2.5.0}/tests/test_scratch_tidy.py +0 -0
  137. {runops-2.3.1 → runops-2.5.0}/tests/test_setup_misc.py +0 -0
  138. {runops-2.3.1 → runops-2.5.0}/tests/test_sweep.py +0 -0
  139. {runops-2.3.1 → runops-2.5.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.1
3
+ Version: 2.5.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 する。
@@ -453,6 +460,22 @@ created ──submit──▶ submitted ──(slurm)──▶ running ──▶
453
460
 
454
461
  ---
455
462
 
463
+ ### 6.4 進捗
464
+
465
+ adapter が `progress(run_dir)` を持つとき、`sync` は running の run と、queue を離れた
466
+ 直後の run について呼び、`{done, total, unit, start}` を受け取る。`done` は今の到達点、
467
+ `total` は目標(不明なら省略)、`unit` は数える単位(`batch`、`step` など)、`start` は
468
+ この attempt を始めた地点(resume した run の再開点。新規なら 0)。
469
+
470
+ - 観測は `submit/progress.<job_id>.jsonl` に `{at, done}` として追記する(attempt ごと、
471
+ 上限 1000 件程度)。queue を離れたときの観測は終了時刻で記録し、終了後の待ち時間を速度に含めない。
472
+ - 要約を `[[job.attempts]].progress` に置く: `done`、`total`、`unit`、`start`、`fraction`、
473
+ `rate_recent_per_hour`(直近 10 分)、`rate_attempt_per_hour`(program 開始から。起点は
474
+ `start`)、`eta_hours`(running のみ)。program 開始時刻は job.sh が stdout に出す
475
+ `runops: start` 行から取り、`started_at` に入れる。
476
+ - `status` / `list` は要約だけを読む(file を開かない)。read-only command は観測を足さない。
477
+ - 解析は adapter が行い、runops は保存と表示だけを持つ(§1.2 #4)。
478
+
456
479
  ## 7. CLI
457
480
 
458
481
  ### 7.1 共通規則
@@ -494,9 +517,9 @@ created ──submit──▶ submitted ──(slurm)──▶ running ──▶
494
517
  | `runo context [--json]` | agent 向け現在地。project 名、simulator、active run 数、直近 run 5 件、scratch 期限切れ数、`LOG.md` 末尾 20 行。配置した runops skill が install 済みの runops より古ければ `runo update --skills-only` を促す warning |
495
518
  | `runo case new NAME --simulator SIM [--from CASE]` | case をスキャフォールド。adapter の `case_template()` を展開 |
496
519
  | `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` と同時のみ有効 |
520
+ | `runo new CASE [--why TEXT] [--label L] [--set k=v]... [--job k=v]... [--tag T]... [--scratch] [--dest DIR] [--submit] [--created-by WHO] [--json]` | 単発 run を作る |
521
+ | `runo new --from RUN [--why] [--set k=v]... [--job k=v]... [--job-from-case] [--continue] [--submit]` | 既存 run の params と `[job]` から派生 run を作る(§5)。置き場所の既定は元 run と同じ directory(元 run が archive / scratch なら `runs/<case>/`)。`--continue` は adapter が `extend_inputs()` を実装する場合のみ有効(§8.1) |
522
+ | `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
523
  | `runo submit RUN... [--partition P] [--qos Q] [--afterok JOBID] [-y]` | job.sh を生成して sbatch。`created` 以外は拒否 |
501
524
  | `runo submit --all [DIR] [-y]` | DIR 配下の `created` run をすべて投入。件数と概算 core-hours を表示して確認 |
502
525
  | `runo cancel RUN... [--why TEXT] [-y]` | scancel して sync。`--why` は `cancelled: ...` の note として残す |
@@ -509,7 +532,7 @@ created ──submit──▶ submitted ──(slurm)──▶ running ──▶
509
532
  | `runo pin RUN [--off] [--reason TEXT]` | `run.pinned` を切り替える。pinned は `list` に常に出て、`tidy` / `delete` の対象外 |
510
533
  | `runo summarize RUN... [--force]` | adapter の `summarize()` を呼び `analysis/summary.json` を書く。既存があれば `--force` なしではスキップ |
511
534
  | `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 に残す |
535
+ | `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
536
  | `runo restore RUN` | archive 前の場所に戻す。既に何かあれば拒否 |
514
537
  | `runo delete RUN... [-y]` | directory を削除。`created`/`failed`/`cancelled`/scratch のみ。`pinned` は拒否 |
515
538
  | `runo tidy [--apply] [--only KIND] [--older-than AGE] [--purge-work RUN...] [--json]` | 整理候補を表示する(§7.5)。`--apply` で実行 |
@@ -534,7 +557,7 @@ created ──submit──▶ submitted ──(slurm)──▶ running ──▶
534
557
 
535
558
  | kind | 条件 | 動作 |
536
559
  |---|---|---|
537
- | `expired-scratch` | `scratch = true` かつ `expires_at` を過ぎ、`pinned = false` | delete |
560
+ | `expired-scratch` | `scratch = true` かつ `expires_at` を過ぎ、`pinned = false`、archived でない | delete |
538
561
  | `stale-failed` | `failed`/`cancelled` で `defaults.tidy_failed_days`(既定 30)日より古く、note も `why` も空 | delete |
539
562
  | `old-completed` | `completed` で `defaults.tidy_archive_days`(既定 90)日より古く、`pinned = false` | archive |
540
563
  | `orphan-job` | `submitted`/`running` だが squeue/sacct に job が無い | sync(state 確定) |
@@ -614,6 +637,7 @@ class SimulatorAdapter(ABC):
614
637
  def agent_context_dir(self) -> Path | None: return None # §8.2
615
638
  def configure(self, sim_config: dict) -> None: ... # runops.toml の [simulators.X] を受け取る
616
639
  def job_prelude(self, run_dir: Path) -> list[str]: return [] # job.sh で実行直前に置く shell 行
640
+ def progress(self, run_dir: Path) -> dict | None: return None # §6.4。{done, total, unit, start}
617
641
  ```
618
642
 
619
643
  - job.sh は run_dir へ `cd` してから実行する(v1 と同じ。v1 adapter の path 規約
@@ -668,12 +692,16 @@ runops は `init` と `update` で次を行う。
668
692
  (例: `mpiemses3d-input-review`)。`runops-` で始まる名前は予約されており展開しない。
669
693
  2. `references/` を `.agents/skills/<adapter>-references/` にコピーし、
670
694
  `SKILL.md` が無ければ reference 一覧を列挙した `SKILL.md` を生成する。
695
+ `docs/` があれば同じく `.agents/skills/<adapter>-docs/` に置く。展開した skill の
696
+ `../../references/`、`../../docs/`(package 内の配置での参照)は `../<adapter>-references/`、
697
+ `../<adapter>-docs/` に書き換える。
671
698
  3. 展開先には `.managed-by-runops` marker を置く。marker のある directory は
672
699
  `update` 時に **無条件で上書き** する。marker の無い同名 directory は触らず warning。
673
700
  4. `.runops/skills.lock` に adapter 名、package version、展開した directory、content hash を記録する。
674
701
 
675
702
  `doctor` は lock の package version と現在 install されている version を比較し、
676
- 差があれば `runo update --skills-only` を提案する。
703
+ 差があれば `runo update --skills-only` を提案する。展開済み skill の相対リンクで
704
+ 存在しない file を指すものも報告する。
677
705
 
678
706
  既存の simulator repo(MPIEMSES3D、emout、BEACH)は `plugins/<name>-context/` に
679
707
  `skills/` と `references/` を既に持つ。各 package 側で必要な変更は
@@ -971,4 +999,6 @@ v2 の文書は次の 4 ファイルだけにする。
971
999
  | 置き換えた系列の整理(2026-10-21) | `archive` が DIR(系列)を受け、`--superseded-by` で置き換え先を manifest に記録する。新しい command と tidy の種別は足さない。skill は新系列を作ったら旧系列の要否を確かめ、不要なら archive して現用系列・archive 先・件数を報告する | 修正版を作った後に旧系列が作業領域に残り、毎回ユーザーが整理を頼んでいた。`parent_run` だけでは比較用の派生と置き換えを区別できないので、置き換えは明示操作で記録する。旧 `archive` は 1 件ずつ検査していて、途中の running run で一部だけ移動していた(#113) |
972
1000
  | 生成ハーネスの書き方(2026-10-21) | AGENTS.md の Rules を「作業の進め方」5 項目に絞り、各項目を「何をするか」と短い理由で書く。skill は手順に専念し、AGENTS.md の方針(manifest を手で編集しない等)を繰り返さない | #112・#113 で規則が増え、禁止形(never / do not)が並んでいた。現行の Claude の prompting 指針は、しないことより何をするかを、理由と共に書くことを勧め、強い禁止語は過剰反応を招くとしている |
973
1001
  | 複数 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 で同じことが起き得た |
1002
+ | 進捗と系列配置 2.5.0(2026-10-23) | adapter に任意の `progress()`、`sync` が時刻付き観測を保存し速度・残り時間を要約(§6.4)。`new --from` の既定置き場所を元 run と同じ directory に。agent context の `docs/` も展開 | 速度比較のために agent が時刻付き batch 観測を手で保存し、再開点や終了後の待ちを毎回補正していた(4 回)。継続世代が case 直下に並び、同じ計算の系列を見分けにくかった。skill が package の `docs/` を参照していたが展開されていなかった。進捗の解析は simulator 固有なので adapter が持ち、runops は保存と表示だけにする |
1003
+ | 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 数は増やさない |
974
1004
  | 使い方ガイド | `docs/guide.md` を追加し、文書を 4 ファイルにする(§13.3) | README の依頼例の表と command 一覧の間を埋める、場面ごとの流れ(case と run の分け方、smoke から本計算、状態の決まり方、整理)の置き場所が無かった |
@@ -49,6 +49,7 @@ class Adapter:
49
49
  | `keep_patterns() -> list[str]` | `tidy --purge-work` で残す glob |
50
50
  | `extend_inputs(src_run, run_dir, params) -> list[str]` | `runo new --from RUN --continue` の restart 設定 |
51
51
  | `agent_context_dir() -> Path | None` | 同梱 skill と reference の場所 |
52
+ | `progress(run_dir) -> dict | None` | 進捗 `{"done", "total", "unit", "start"}`。`runo sync` が時刻付きで保存し、速度と残り時間を出す |
52
53
 
53
54
  `runtime` は `executable`、`package_version`、`exe_hash`、`git_commit`、`git_dirty`、
54
55
  `env`、`resolver_mode`、`build_dir` を持ちます。
@@ -76,12 +77,16 @@ project の `adapters/mysim.py` に `Adapter` という名前で置けば使え
76
77
  <dir>/
77
78
  skills/<skill-name>/SKILL.md
78
79
  references/*.md
80
+ docs/*.md # 任意。references と同じく展開する
79
81
  ```
80
82
 
81
83
  `runo init` と `runo update` は `skills/*` を project の `.agents/skills/` と
82
84
  `.claude/skills/` に同名でコピーし、`references/` を `<adapter>-references` として置きます。
83
85
  skill 名は package 側で名前空間を付けてください(例: `mysim-input-review`)。
84
- `runops-` で始まる名前は予約されています。展開した directory には
86
+ `runops-` で始まる名前は予約されています。skill から reference へは
87
+ `../<adapter>-references/X.md`(`docs/` なら `../<adapter>-docs/X.md`)と書いてください。
88
+ package 内の配置どおりの `../../references/X.md`、`../../docs/X.md` は展開時にこの形へ書き換えます。`runo doctor` は展開済み skill の
89
+ 相対リンク切れを報告します。展開した directory には
85
90
  `.managed-by-runops` が置かれ、更新時に上書きされます。利用者が作った同名 directory は
86
91
  上書きしません。
87
92
 
@@ -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`、使った割合を示します。
@@ -128,6 +131,11 @@ walltime は、同じ case・同じ資源で終わった run の実時間を元
128
131
  `runo sync` は、active な run が何件あっても `squeue` と `sacct` を 1 回ずつしか呼びません。
129
132
  エージェントには `squeue` を直接叩かせず、`runo sync` を使わせてください。
130
133
 
134
+ adapter が進捗に対応していれば(BEACH の batch、MPIEMSES3D の step)、`sync` のたびに時刻付きの
135
+ 観測を残し、`runo list` / `status` に `62% 0.198 s/batch eta 3.1h` のように進み具合、直近 10 分の
136
+ 速度、残り時間を出します。resume した run は再開点から数え、終わった run は終了時刻までで速度を出すので、
137
+ 版を切り替えた前後の比較にそのまま使えます(`--json` の `progress`)。
138
+
131
139
  ### 状態の決まり方
132
140
 
133
141
  run の状態は `created → submitted → running → completed | failed | cancelled` と進みます。
@@ -177,6 +185,7 @@ project の run だけが対象です。同じアカウントの別 project の
177
185
  > R20261015-0003 の続きを 2 倍の step で。
178
186
 
179
187
  `runo new --from RUN --continue` で、終わった run の出力から再開する run を作ります。
188
+ `--from` で作る run は元 run と同じ directory に置かれるので、同じ計算の継続は 1 つの directory に並びます。
180
189
  再開の方法は simulator ごとに違い、adapter が対応している場合だけ使えます
181
190
  (現在は MPIEMSES3D が対応し、`SNAPSHOT1` から再開します)。新しい run は元の run を `parent_run` として記録します。
182
191
 
@@ -209,7 +218,7 @@ run は消さなくても `runo list` が古いものを隠しますが、direct
209
218
 
210
219
  | 種類 | 条件 | 処理 |
211
220
  |---|---|---|
212
- | `expired-scratch` | 期限切れの scratch run | 削除 |
221
+ | `expired-scratch` | 期限切れの scratch run(archive したものは除く) | 削除 |
213
222
  | `stale-failed` | 30 日より古い failed / cancelled で、`why` もノートも無い | 削除 |
214
223
  | `old-completed` | 90 日より古い completed | archive |
215
224
  | `orphan-job` | 投入済みなのに Slurm が知らない | `sync` で状態を確定 |
@@ -225,8 +234,8 @@ run id で引き続き見つかり、`runo restore` で戻せます。
225
234
  > 旧系列は不要。新系列で置き換えたと記録して archive して。
226
235
 
227
236
  で `runo archive runs/<case>/<旧系列> --superseded-by runs/<case>/<新系列> --why "..."` になります。
228
- 系列の directory を渡すと、対象の run を先に表示し、投入中・実行中・作成のみの run と pin した run は
229
- 除きます。各旧 run の `superseded_by` には、その run から `--from` で作った新 run(無ければ新系列の全 run)が
237
+ 系列の directory を渡すと、対象の run を先に表示し、投入中・実行中の run と pin した run は
238
+ 除きます。投入前の run(created)も `--superseded-by` を付けたときは archive でき、`runo restore` で戻せます。各旧 run の `superseded_by` には、その run から `--from` で作った新 run(無ければ新系列の全 run)が
230
239
  記録され、理由はノートに残ります。比較用に派生しただけの run は置き換えとは扱わないので、
231
240
  旧系列が不要かどうかは毎回確かめます。
232
241
 
@@ -110,9 +110,9 @@ runops だけが書きます。手で編集しないでください。主な fie
110
110
  | `[path]` | `run_dir`(project 相対)、`archived_from`、`archived_at` |
111
111
  | `[origin]` | `case`、`sweep`、`parent_run`、`created_by`、`case_input_hash` |
112
112
  | `[simulator]` | `name`、`adapter`、`adapter_source`、`adapter_version`、`executable`、`resolver_mode`、`package_version`、`exe_hash`、`git_commit`、`git_dirty`、`build_dir` |
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`) |
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`、`runo sync` が付ける `progress`(§進捗)) |
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` |
@@ -139,7 +139,9 @@ run id は削除後も再利用しません。`--dest` に既存 run の中は
139
139
  ## command
140
140
 
141
141
  `list --json` と `status --json` は、walltime の根拠として `walltime`、最新 attempt の `elapsed`、
142
- `walltime_used`(elapsed / walltime)、`slurm_state` も返します。表では `TIMEOUT` などを
142
+ `walltime_used`(elapsed / walltime)、`slurm_state` も返します。adapter が `progress()` を持てば
143
+ `progress`(`done`、`total`、`unit`、`fraction`、`rate_recent_per_hour`、`rate_attempt_per_hour`、
144
+ `eta_hours`)も返し、表の `progress` 列に `62% 0.198 s/batch eta 3.1h` と出します。表では `TIMEOUT` などを
143
145
  `failed (TIMEOUT)` と示します。
144
146
 
145
147
  全 command は `--help` で option を確認できます。`RUN` には run id、run directory、
@@ -148,14 +150,14 @@ run id は削除後も再利用しません。`--dest` に既存 run の中は
148
150
  | command | 動作 |
149
151
  |---|---|
150
152
  | `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 の鮮度を点検 |
153
+ | `runo doctor [--json]` | 設定、adapter の解決経路、executable、sbatch、skill の鮮度とリンク切れを点検 |
152
154
  | `runo update [--skills-only] [-y]` | simulator package を更新し skill と AGENTS.md の管理 block を再生成 |
153
155
  | `runo build SIM [--force]` | `local_source` の simulator を build |
154
156
  | `runo context [--json]` | agent 向けの現在地 |
155
157
  | `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 の親 |
158
+ | `runo new CASE [--why] [--label] [--set k=v]... [--job k=v]... [--tag] [--scratch] [--dest DIR] [--submit] [--json]` | run を 1 つ作る。`--job` はこの run の `[job]` の値 |
159
+ | `runo new --from RUN [--set k=v]... [--job k=v]... [--job-from-case] [--continue]` | 既存 run の params と `[job]` から派生(`--job-from-case` で `[job]` は case から)。置き場所の既定は元 run と同じ directory。`--continue` は adapter の restart 対応が必要 |
160
+ | `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
161
  | `runo submit RUN... [-p P] [--qos Q] [--afterok JOB]` / `runo submit --all [DIR] [-y]` | job.sh を生成して sbatch |
160
162
  | `runo cancel RUN... [--why TEXT] [-y]` / `runo retry RUN [--submit] [-y]` | scancel(`--why` は note に残る)/ failed・cancelled を created に戻す(理由付きで cancel された run は確認を取る) |
161
163
  | `runo status [RUN...]` / `runo sync [RUN...]` / `runo log RUN [--err] [-n N] [-f]` | 状態表示 / Slurm から更新 / log 表示 |
@@ -163,7 +165,7 @@ run id は削除後も再利用しません。`--dest` に既存 run の中は
163
165
  | `runo note RUN [TEXT]` / `runo pin RUN [--off] [--reason]` | ノート追記・表示 / pin |
164
166
  | `runo summarize RUN... [--force]` | adapter の要約を `analysis/summary.json` に書く |
165
167
  | `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/` へ移動 / 戻す。移動前に全対象を検査し、指定した RUN が終了していなければ何も動かさない。DIR は配下の run(系列)で、created / submitted / running と pinned を除いて表示する。`--superseded-by` は置き換え先(その run から `--from` で派生した run、無ければ指定した全 run)を `superseded_by` に記録し、`--why` をノートに残す |
168
+ | `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
169
  | `runo delete RUN... [-y]` | created・failed・cancelled・scratch の run を削除 |
168
170
  | `runo tidy [--apply] [--only KIND] [--older-than AGE] [--purge-work RUN...]` | 整理候補の表示と実行 |
169
171
  | `runo migrate-v1 [--dry-run] [--apply-delete-test-runs] [-y]` | v1 project の変換 |
@@ -175,7 +177,7 @@ scratch と archived は表示しません。
175
177
 
176
178
  | kind | 条件 | 動作 |
177
179
  |---|---|---|
178
- | `expired-scratch` | 期限切れの scratch run | delete |
180
+ | `expired-scratch` | 期限切れの scratch run(archived は除く) | delete |
179
181
  | `stale-failed` | `tidy_failed_days`(既定 30)日より古い failed / cancelled で、why も note も無い | delete |
180
182
  | `old-completed` | `tidy_archive_days`(既定 90)日より古い completed | archive |
181
183
  | `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.1"
7
+ version = "2.5.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.1"
3
+ __version__ = "2.5.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:
@@ -563,9 +592,20 @@ def retry(
563
592
 
564
593
  def _status_table(project: Project, manifests: list[Manifest]) -> Table:
565
594
  from runops.ops.listing import relative_age
595
+ from runops.ops.progress import format_progress
566
596
 
567
597
  table = Table(
568
- "id", "status", "label", "case", "sweep", "created", "elapsed", "note", "why", box=None
598
+ "id",
599
+ "status",
600
+ "label",
601
+ "case",
602
+ "sweep",
603
+ "created",
604
+ "elapsed",
605
+ "progress",
606
+ "note",
607
+ "why",
608
+ box=None,
569
609
  )
570
610
  for m in manifests:
571
611
  s = m.to_summary(project.root)
@@ -586,6 +626,8 @@ def _status_table(project: Project, manifests: list[Manifest]) -> Table:
586
626
  status += " [dim](scratch)[/]"
587
627
  if s["pinned"]:
588
628
  status += " [dim](pinned)[/]"
629
+ if s["superseded_by"]:
630
+ status += f" [dim](-> {', '.join(s['superseded_by'])})[/]"
589
631
  table.add_row(
590
632
  s["id"],
591
633
  status,
@@ -594,6 +636,7 @@ def _status_table(project: Project, manifests: list[Manifest]) -> Table:
594
636
  s["sweep"],
595
637
  relative_age(s["created_at"]),
596
638
  elapsed,
639
+ format_progress(s["progress"]),
597
640
  s["last_note"][:60],
598
641
  s["why"][:60],
599
642
  )
@@ -793,7 +836,7 @@ def archive(
793
836
  why: str = typer.Option("", "--why", help="Reason, recorded in each run's notes."),
794
837
  yes: bool = typer.Option(False, "-y", "--yes"),
795
838
  ) -> None:
796
- """Move terminal runs to runs/_archive/ (same relative path)."""
839
+ """Move created or finished runs to runs/_archive/ (same relative path)."""
797
840
  from datetime import datetime
798
841
 
799
842
  from runops.ops.tidy import ArchivePlan, archive_runs, plan_archive
@@ -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,14 +212,24 @@ 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", "")),
204
220
  "input_hash": str(self.section("identity").get("input_hash", "")),
205
221
  # Walltime evidence for the next run: elapsed of the latest attempt.
206
222
  **self.timing(),
223
+ "progress": self.progress,
207
224
  }
208
225
 
226
+ @property
227
+ def progress(self) -> dict[str, Any] | None:
228
+ """Progress of the latest attempt as `runo sync` last saw it (adapter ``progress()``)."""
229
+ attempts = self.job.get("attempts") or []
230
+ value = attempts[-1].get("progress") if attempts else None
231
+ return dict(value) if isinstance(value, dict) else None
232
+
209
233
  def timing(self) -> dict[str, Any]:
210
234
  """Walltime, latest attempt elapsed, the fraction used, and the last Slurm state."""
211
235
  walltime = str(self.job.get("walltime", ""))