runops 2.4.0__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.
- {runops-2.4.0 → runops-2.5.0}/PKG-INFO +1 -1
- {runops-2.4.0 → runops-2.5.0}/SPEC.md +22 -3
- {runops-2.4.0 → runops-2.5.0}/docs/adapter.md +4 -2
- {runops-2.4.0 → runops-2.5.0}/docs/guide.md +6 -0
- {runops-2.4.0 → runops-2.5.0}/docs/reference.md +5 -3
- {runops-2.4.0 → runops-2.5.0}/pyproject.toml +1 -1
- {runops-2.4.0 → runops-2.5.0}/src/runops/__init__.py +1 -1
- {runops-2.4.0 → runops-2.5.0}/src/runops/cli.py +13 -1
- {runops-2.4.0 → runops-2.5.0}/src/runops/manifest.py +8 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/ops/create.py +17 -1
- runops-2.5.0/src/runops/ops/progress.py +197 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/ops/skills.py +25 -14
- {runops-2.4.0 → runops-2.5.0}/src/runops/ops/sync.py +8 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/skills/runops-check-status/SKILL.md +5 -2
- {runops-2.4.0 → runops-2.5.0}/src/runops/skills/runops-create-run/SKILL.md +1 -1
- {runops-2.4.0 → runops-2.5.0}/src/runops/skills/runops-reference/SKILL.md +1 -1
- runops-2.5.0/tests/test_progress.py +120 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_run_job.py +12 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_skills.py +9 -1
- {runops-2.4.0 → runops-2.5.0}/uv.lock +1 -1
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/check/SKILL.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/improve-harness/SKILL.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/python-package-refactor/SKILL.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/python-package-refactor/agents/openai.yaml +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/python-package-refactor/examples/refactor-request.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/python-package-refactor/references/anti-patterns.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/python-package-refactor/references/api-compatibility.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/python-package-refactor/references/packaging-modernization.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/python-package-refactor/references/refactor-playbook.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/python-package-refactor/references/verification.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/python-package-refactor/scripts/api_surface_snapshot.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/python-package-refactor/scripts/import_smoke.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/python-package-refactor/scripts/inspect_python_package.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/python-package-refactor/scripts/refactor_quality_gate.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/release/SKILL.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/simulator-adapter/SKILL.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/spec-reviewer/SKILL.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.agents/skills/sync-docs/SKILL.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.claude/agents/spec-reviewer.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.claude/rules/architecture.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.claude/rules/dev-workflow.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.claude/rules/gotchas.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.claude/settings.json +0 -0
- {runops-2.4.0 → runops-2.5.0}/.claude/skills/check/SKILL.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.claude/skills/improve-harness/SKILL.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.claude/skills/python-package-refactor/SKILL.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.claude/skills/release/SKILL.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.claude/skills/simulator-adapter/SKILL.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.claude/skills/spec-reviewer/SKILL.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.claude/skills/sync-docs/SKILL.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.codex/README.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/.codex/agents/spec-reviewer.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/.codex/config.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/.codex/rules/runops.rules +0 -0
- {runops-2.4.0 → runops-2.5.0}/.dockerignore +0 -0
- {runops-2.4.0 → runops-2.5.0}/.gitattributes +0 -0
- {runops-2.4.0 → runops-2.5.0}/.github/assets/conversation-dark.svg +0 -0
- {runops-2.4.0 → runops-2.5.0}/.github/assets/conversation-light.svg +0 -0
- {runops-2.4.0 → runops-2.5.0}/.github/assets/hero-dark.svg +0 -0
- {runops-2.4.0 → runops-2.5.0}/.github/assets/hero-light.svg +0 -0
- {runops-2.4.0 → runops-2.5.0}/.github/assets/make_readme_art.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/.github/workflows/ci.yml +0 -0
- {runops-2.4.0 → runops-2.5.0}/.github/workflows/publish.yml +0 -0
- {runops-2.4.0 → runops-2.5.0}/.gitignore +0 -0
- {runops-2.4.0 → runops-2.5.0}/.harness/manifest.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/.pre-commit-config.yaml +0 -0
- {runops-2.4.0 → runops-2.5.0}/.vscode/settings.json +0 -0
- {runops-2.4.0 → runops-2.5.0}/AGENTS.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/CLAUDE.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/LICENSE +0 -0
- {runops-2.4.0 → runops-2.5.0}/README.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/adapters/__init__.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/adapters/base.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/adapters/generic.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/adapters/registry.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/case.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/config.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/errors.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/harness.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/jobgen.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/launchers.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/legacy_adapters/__init__.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/legacy_adapters/beach.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/legacy_adapters/emses.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/ops/__init__.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/ops/common.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/ops/listing.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/ops/migrate.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/ops/notes.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/ops/setup.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/ops/submit.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/ops/tidy.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/project.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/runtime.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/skills/runops-maintain/SKILL.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/skills/runops-tidy/SKILL.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/slurm.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/src/runops/util.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/__init__.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/conftest.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/.codex/rules/a.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/.runops/test-runs/T20260701-0001/receipt.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/.runops/work/beach-x-abc/bin/beach +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/AGENTS.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/CLAUDE.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/cases/beach/plane/beach.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/cases/beach/plane/case.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/cases/old/beach.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/cases/old/case.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/launchers.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/research/CURRENT.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/runops.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/runs/AGENTS.md +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/runs/_archive/plane/R20260601-0001/manifest.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/runs/plane/R20260701-0001--a/input/beach.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/runs/plane/R20260701-0001--a/manifest.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/runs/plane/R20260702-0001/manifest.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/runs/scan/R20260701-0002/manifest.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/runs/scan/survey.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/simulators.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/fixtures/v1-project/site.toml +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/golden/mpi_standard.sh +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/golden/openmp_rsc.sh +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_adapters.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_archive_series.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_build.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_dev_harness.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_jobgen.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_legacy_adapters.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_lifecycle.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_manifest_config.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_migrate.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_mv_naming.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_project_scope.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_safety.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_scratch_tidy.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_setup_misc.py +0 -0
- {runops-2.4.0 → runops-2.5.0}/tests/test_sweep.py +0 -0
- {runops-2.4.0 → 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
|
+
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
|
|
@@ -460,6 +460,22 @@ created ──submit──▶ submitted ──(slurm)──▶ running ──▶
|
|
|
460
460
|
|
|
461
461
|
---
|
|
462
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
|
+
|
|
463
479
|
## 7. CLI
|
|
464
480
|
|
|
465
481
|
### 7.1 共通規則
|
|
@@ -502,7 +518,7 @@ created ──submit──▶ submitted ──(slurm)──▶ running ──▶
|
|
|
502
518
|
| `runo case new NAME --simulator SIM [--from CASE]` | case をスキャフォールド。adapter の `case_template()` を展開 |
|
|
503
519
|
| `runo case list` | case 一覧 |
|
|
504
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 を作る |
|
|
505
|
-
| `runo new --from RUN [--why] [--set k=v]... [--job k=v]... [--job-from-case] [--continue] [--submit]` | 既存 run の params と `[job]` から派生 run を作る(§5
|
|
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) |
|
|
506
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` と同時のみ有効 |
|
|
507
523
|
| `runo submit RUN... [--partition P] [--qos Q] [--afterok JOBID] [-y]` | job.sh を生成して sbatch。`created` 以外は拒否 |
|
|
508
524
|
| `runo submit --all [DIR] [-y]` | DIR 配下の `created` run をすべて投入。件数と概算 core-hours を表示して確認 |
|
|
@@ -621,6 +637,7 @@ class SimulatorAdapter(ABC):
|
|
|
621
637
|
def agent_context_dir(self) -> Path | None: return None # §8.2
|
|
622
638
|
def configure(self, sim_config: dict) -> None: ... # runops.toml の [simulators.X] を受け取る
|
|
623
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}
|
|
624
641
|
```
|
|
625
642
|
|
|
626
643
|
- job.sh は run_dir へ `cd` してから実行する(v1 と同じ。v1 adapter の path 規約
|
|
@@ -675,8 +692,9 @@ runops は `init` と `update` で次を行う。
|
|
|
675
692
|
(例: `mpiemses3d-input-review`)。`runops-` で始まる名前は予約されており展開しない。
|
|
676
693
|
2. `references/` を `.agents/skills/<adapter>-references/` にコピーし、
|
|
677
694
|
`SKILL.md` が無ければ reference 一覧を列挙した `SKILL.md` を生成する。
|
|
678
|
-
|
|
679
|
-
`../<adapter>-references
|
|
695
|
+
`docs/` があれば同じく `.agents/skills/<adapter>-docs/` に置く。展開した skill の
|
|
696
|
+
`../../references/`、`../../docs/`(package 内の配置での参照)は `../<adapter>-references/`、
|
|
697
|
+
`../<adapter>-docs/` に書き換える。
|
|
680
698
|
3. 展開先には `.managed-by-runops` marker を置く。marker のある directory は
|
|
681
699
|
`update` 時に **無条件で上書き** する。marker の無い同名 directory は触らず warning。
|
|
682
700
|
4. `.runops/skills.lock` に adapter 名、package version、展開した directory、content hash を記録する。
|
|
@@ -981,5 +999,6 @@ v2 の文書は次の 4 ファイルだけにする。
|
|
|
981
999
|
| 置き換えた系列の整理(2026-10-21) | `archive` が DIR(系列)を受け、`--superseded-by` で置き換え先を manifest に記録する。新しい command と tidy の種別は足さない。skill は新系列を作ったら旧系列の要否を確かめ、不要なら archive して現用系列・archive 先・件数を報告する | 修正版を作った後に旧系列が作業領域に残り、毎回ユーザーが整理を頼んでいた。`parent_run` だけでは比較用の派生と置き換えを区別できないので、置き換えは明示操作で記録する。旧 `archive` は 1 件ずつ検査していて、途中の running run で一部だけ移動していた(#113) |
|
|
982
1000
|
| 生成ハーネスの書き方(2026-10-21) | AGENTS.md の Rules を「作業の進め方」5 項目に絞り、各項目を「何をするか」と短い理由で書く。skill は手順に専念し、AGENTS.md の方針(manifest を手で編集しない等)を繰り返さない | #112・#113 で規則が増え、禁止形(never / do not)が並んでいた。現行の Claude の prompting 指針は、しないことより何をするかを、理由と共に書くことを勧め、強い禁止語は過剰反応を招くとしている |
|
|
983
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 は保存と表示だけにする |
|
|
984
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 数は増やさない |
|
|
985
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,14 +77,15 @@ 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
86
|
`runops-` で始まる名前は予約されています。skill から reference へは
|
|
85
|
-
`../<adapter>-references/X.md
|
|
86
|
-
`../../references/X.md` は展開時にこの形へ書き換えます。`runo doctor` は展開済み skill の
|
|
87
|
+
`../<adapter>-references/X.md`(`docs/` なら `../<adapter>-docs/X.md`)と書いてください。
|
|
88
|
+
package 内の配置どおりの `../../references/X.md`、`../../docs/X.md` は展開時にこの形へ書き換えます。`runo doctor` は展開済み skill の
|
|
87
89
|
相対リンク切れを報告します。展開した directory には
|
|
88
90
|
`.managed-by-runops` が置かれ、更新時に上書きされます。利用者が作った同名 directory は
|
|
89
91
|
上書きしません。
|
|
@@ -131,6 +131,11 @@ walltime は、同じ case・同じ資源で終わった run の実時間を元
|
|
|
131
131
|
`runo sync` は、active な run が何件あっても `squeue` と `sacct` を 1 回ずつしか呼びません。
|
|
132
132
|
エージェントには `squeue` を直接叩かせず、`runo sync` を使わせてください。
|
|
133
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
|
+
|
|
134
139
|
### 状態の決まり方
|
|
135
140
|
|
|
136
141
|
run の状態は `created → submitted → running → completed | failed | cancelled` と進みます。
|
|
@@ -180,6 +185,7 @@ project の run だけが対象です。同じアカウントの別 project の
|
|
|
180
185
|
> R20261015-0003 の続きを 2 倍の step で。
|
|
181
186
|
|
|
182
187
|
`runo new --from RUN --continue` で、終わった run の出力から再開する run を作ります。
|
|
188
|
+
`--from` で作る run は元 run と同じ directory に置かれるので、同じ計算の継続は 1 つの directory に並びます。
|
|
183
189
|
再開の方法は simulator ごとに違い、adapter が対応している場合だけ使えます
|
|
184
190
|
(現在は MPIEMSES3D が対応し、`SNAPSHOT1` から再開します)。新しい run は元の run を `parent_run` として記録します。
|
|
185
191
|
|
|
@@ -110,7 +110,7 @@ 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
115
|
| `[variation]` | `changed_keys`、`job_changed_keys`(case の `[job]` と違う key) |
|
|
116
116
|
| `[identity]` | `input_hash` |
|
|
@@ -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`
|
|
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、
|
|
@@ -154,7 +156,7 @@ run id は削除後も再利用しません。`--dest` に既存 run の中は
|
|
|
154
156
|
| `runo context [--json]` | agent 向けの現在地 |
|
|
155
157
|
| `runo case new NAME [-s SIM] [--from CASE]` / `runo case list` | case の作成と一覧 |
|
|
156
158
|
| `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
|
|
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 対応が必要 |
|
|
158
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 は確認を取る) |
|
|
@@ -592,9 +592,20 @@ def retry(
|
|
|
592
592
|
|
|
593
593
|
def _status_table(project: Project, manifests: list[Manifest]) -> Table:
|
|
594
594
|
from runops.ops.listing import relative_age
|
|
595
|
+
from runops.ops.progress import format_progress
|
|
595
596
|
|
|
596
597
|
table = Table(
|
|
597
|
-
"id",
|
|
598
|
+
"id",
|
|
599
|
+
"status",
|
|
600
|
+
"label",
|
|
601
|
+
"case",
|
|
602
|
+
"sweep",
|
|
603
|
+
"created",
|
|
604
|
+
"elapsed",
|
|
605
|
+
"progress",
|
|
606
|
+
"note",
|
|
607
|
+
"why",
|
|
608
|
+
box=None,
|
|
598
609
|
)
|
|
599
610
|
for m in manifests:
|
|
600
611
|
s = m.to_summary(project.root)
|
|
@@ -625,6 +636,7 @@ def _status_table(project: Project, manifests: list[Manifest]) -> Table:
|
|
|
625
636
|
s["sweep"],
|
|
626
637
|
relative_age(s["created_at"]),
|
|
627
638
|
elapsed,
|
|
639
|
+
format_progress(s["progress"]),
|
|
628
640
|
s["last_note"][:60],
|
|
629
641
|
s["why"][:60],
|
|
630
642
|
)
|
|
@@ -220,8 +220,16 @@ class Manifest:
|
|
|
220
220
|
"input_hash": str(self.section("identity").get("input_hash", "")),
|
|
221
221
|
# Walltime evidence for the next run: elapsed of the latest attempt.
|
|
222
222
|
**self.timing(),
|
|
223
|
+
"progress": self.progress,
|
|
223
224
|
}
|
|
224
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
|
+
|
|
225
233
|
def timing(self) -> dict[str, Any]:
|
|
226
234
|
"""Walltime, latest attempt elapsed, the fraction used, and the last Slurm state."""
|
|
227
235
|
walltime = str(self.job.get("walltime", ""))
|
|
@@ -26,7 +26,7 @@ from runops.errors import RunError, UsageError
|
|
|
26
26
|
from runops.manifest import MANIFEST_FILE, SCHEMA_VERSION, Manifest
|
|
27
27
|
from runops.ops.common import SimContext, resolve_dest, sim_context
|
|
28
28
|
from runops.ops.submit import write_job_for
|
|
29
|
-
from runops.project import SCRATCH_DIR, Project
|
|
29
|
+
from runops.project import ARCHIVE_DIR, SCRATCH_DIR, Project
|
|
30
30
|
from runops.runtime import resolve_runtime
|
|
31
31
|
from runops.util import hash_tree, move_noreplace, now_iso, parse_value
|
|
32
32
|
|
|
@@ -190,6 +190,18 @@ def _dest_parent(project: Project, case: Case, dest: str, scratch: bool) -> Path
|
|
|
190
190
|
return resolve_dest(project, dest)
|
|
191
191
|
|
|
192
192
|
|
|
193
|
+
def _beside(project: Project, source: Manifest) -> Path | None:
|
|
194
|
+
"""The source run's directory under ``runs/``, unless it is archived or scratch."""
|
|
195
|
+
parent = source.run_dir.resolve().parent
|
|
196
|
+
runs = project.runs_dir.resolve()
|
|
197
|
+
if parent != runs and runs not in parent.parents:
|
|
198
|
+
return None
|
|
199
|
+
first = parent.relative_to(runs).parts[:1]
|
|
200
|
+
if first and first[0] in (ARCHIVE_DIR, SCRATCH_DIR):
|
|
201
|
+
return None
|
|
202
|
+
return parent
|
|
203
|
+
|
|
204
|
+
|
|
193
205
|
def create_run(project: Project, request: CreateRequest) -> CreateResult:
|
|
194
206
|
"""Create one run from a case, or derived from another run (``--from``)."""
|
|
195
207
|
source: Manifest | None = None
|
|
@@ -226,6 +238,10 @@ def create_run(project: Project, request: CreateRequest) -> CreateResult:
|
|
|
226
238
|
job = dict(case.job)
|
|
227
239
|
job.update(request.job)
|
|
228
240
|
parent = _dest_parent(project, case, request.dest, request.scratch)
|
|
241
|
+
if source is not None and not request.dest and not request.scratch:
|
|
242
|
+
# A derived or continued run sits beside its source, so one series (e.g. the
|
|
243
|
+
# restarts of one calculation) stays in one directory.
|
|
244
|
+
parent = _beside(project, source) or parent
|
|
229
245
|
spec = RunSpec(
|
|
230
246
|
params=params,
|
|
231
247
|
changed=changed,
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Progress samples from the adapter's optional ``progress()`` hook (SPEC §6.4, §8.1).
|
|
2
|
+
|
|
3
|
+
``runo sync`` asks the adapter how far each active run is (``done`` of ``total`` in some
|
|
4
|
+
``unit``) and appends a timestamped sample to ``submit/progress.<job_id>.jsonl``. From the
|
|
5
|
+
samples it derives, per attempt, the recent rate, the rate since the program started
|
|
6
|
+
(counting from the adapter's ``start``, e.g. the batch a resumed run began at) and the
|
|
7
|
+
remaining time, and keeps that summary in ``[[job.attempts]].progress`` so ``status`` and
|
|
8
|
+
``list`` show it without reading files.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from datetime import datetime, timedelta
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from runops.adapters.registry import optional
|
|
19
|
+
from runops.errors import RunopsError
|
|
20
|
+
from runops.manifest import Manifest
|
|
21
|
+
from runops.ops.common import run_sim_context
|
|
22
|
+
from runops.project import Project
|
|
23
|
+
from runops.util import parse_iso
|
|
24
|
+
|
|
25
|
+
SAMPLES_KEPT = 1000 # per attempt; older samples are dropped when the file doubles
|
|
26
|
+
RECENT_WINDOW = timedelta(minutes=10)
|
|
27
|
+
_START_MARK = "runops: start "
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def samples_path(run_dir: Path, job_id: str) -> Path:
|
|
31
|
+
return run_dir / "submit" / f"progress.{job_id}.jsonl"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def read_progress(project: Project, manifest: Manifest) -> dict[str, Any] | None:
|
|
35
|
+
"""The adapter's ``progress(run_dir)`` as ``{done, total, unit, start}``, or ``None``."""
|
|
36
|
+
try:
|
|
37
|
+
ctx = run_sim_context(project, manifest)
|
|
38
|
+
raw = optional(ctx.adapter, "progress", None, manifest.run_dir)
|
|
39
|
+
except (RunopsError, OSError, ValueError):
|
|
40
|
+
return None
|
|
41
|
+
if not isinstance(raw, dict) or not isinstance(raw.get("done"), (int, float)):
|
|
42
|
+
return None
|
|
43
|
+
total = raw.get("total")
|
|
44
|
+
start = raw.get("start")
|
|
45
|
+
return {
|
|
46
|
+
"done": raw["done"],
|
|
47
|
+
"total": total if isinstance(total, (int, float)) and total > 0 else None,
|
|
48
|
+
"unit": str(raw.get("unit") or "step"),
|
|
49
|
+
"start": start if isinstance(start, (int, float)) else None,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def record_progress(
|
|
54
|
+
project: Project, manifest: Manifest, *, at: datetime, final: bool = False
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Append one sample for the latest attempt and refresh its summary in *manifest*.
|
|
57
|
+
|
|
58
|
+
Call under ``run.lock``; the caller saves the manifest. *final* marks the sample
|
|
59
|
+
taken when the run left the queue; *at* is then its finish time, so the rate does
|
|
60
|
+
not include the time after the job ended.
|
|
61
|
+
"""
|
|
62
|
+
attempts = manifest.job.get("attempts") or []
|
|
63
|
+
if not attempts or not manifest.job_id:
|
|
64
|
+
return
|
|
65
|
+
current = read_progress(project, manifest)
|
|
66
|
+
if current is None:
|
|
67
|
+
return
|
|
68
|
+
path = samples_path(manifest.run_dir, manifest.job_id)
|
|
69
|
+
samples = _read_samples(path)
|
|
70
|
+
samples.append({"at": at.isoformat(timespec="seconds"), "done": current["done"]})
|
|
71
|
+
_write_samples(path, samples)
|
|
72
|
+
attempt = attempts[-1]
|
|
73
|
+
if not attempt.get("started_at"):
|
|
74
|
+
started = program_start(manifest)
|
|
75
|
+
if started:
|
|
76
|
+
attempt["started_at"] = started
|
|
77
|
+
attempt["progress"] = summarize(
|
|
78
|
+
samples,
|
|
79
|
+
total=current["total"],
|
|
80
|
+
unit=current["unit"],
|
|
81
|
+
start=current["start"],
|
|
82
|
+
started_at=parse_iso(str(attempt.get("started_at", ""))),
|
|
83
|
+
final=final,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def summarize(
|
|
88
|
+
samples: list[dict[str, Any]],
|
|
89
|
+
*,
|
|
90
|
+
total: float | None,
|
|
91
|
+
unit: str,
|
|
92
|
+
start: float | None,
|
|
93
|
+
started_at: datetime | None,
|
|
94
|
+
final: bool = False,
|
|
95
|
+
) -> dict[str, Any]:
|
|
96
|
+
points = [(t, float(s["done"])) for s in samples if (t := parse_iso(str(s.get("at", ""))))]
|
|
97
|
+
last_at, done = points[-1]
|
|
98
|
+
raw_done = samples[-1]["done"]
|
|
99
|
+
# Origin for the attempt's average: the program start and the adapter's start value
|
|
100
|
+
# (a resumed run starts at its checkpoint, not at 0); else the first sample.
|
|
101
|
+
if started_at is not None and start is not None and started_at <= last_at:
|
|
102
|
+
origin = (started_at, float(start))
|
|
103
|
+
else:
|
|
104
|
+
origin = points[0]
|
|
105
|
+
earlier = [p for p in points[:-1] if p[0] <= last_at - RECENT_WINDOW]
|
|
106
|
+
reference = earlier[-1] if earlier else (points[0] if len(points) > 1 else origin)
|
|
107
|
+
rate_attempt = _rate(origin, points[-1])
|
|
108
|
+
rate_recent = _rate(reference, points[-1]) or rate_attempt
|
|
109
|
+
summary: dict[str, Any] = {
|
|
110
|
+
"done": raw_done,
|
|
111
|
+
"total": total,
|
|
112
|
+
"unit": unit,
|
|
113
|
+
"at": last_at.isoformat(timespec="seconds"),
|
|
114
|
+
"fraction": round(done / total, 4) if total else None,
|
|
115
|
+
"rate_recent_per_hour": _round(rate_recent),
|
|
116
|
+
"rate_attempt_per_hour": _round(rate_attempt),
|
|
117
|
+
"eta_hours": None,
|
|
118
|
+
}
|
|
119
|
+
if start is not None:
|
|
120
|
+
summary["start"] = start
|
|
121
|
+
if not final and total and rate_recent:
|
|
122
|
+
summary["eta_hours"] = round(max(total - done, 0.0) / rate_recent, 2)
|
|
123
|
+
# TOML has no null: leave unknown values out.
|
|
124
|
+
return {k: v for k, v in summary.items() if v is not None}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def program_start(manifest: Manifest) -> str:
|
|
128
|
+
"""When the program of the latest job started: job.sh prints it in stdout."""
|
|
129
|
+
from runops.ops.sync import log_paths
|
|
130
|
+
|
|
131
|
+
stdout, _ = log_paths(manifest)
|
|
132
|
+
if stdout is None:
|
|
133
|
+
return ""
|
|
134
|
+
try:
|
|
135
|
+
with stdout.open("rb") as handle:
|
|
136
|
+
head = handle.read(8192).decode("utf-8", errors="replace")
|
|
137
|
+
except OSError:
|
|
138
|
+
return ""
|
|
139
|
+
for line in head.splitlines():
|
|
140
|
+
if line.startswith(_START_MARK):
|
|
141
|
+
stamp = line[len(_START_MARK) :].split()[0]
|
|
142
|
+
return stamp if parse_iso(stamp) else ""
|
|
143
|
+
return ""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def format_progress(progress: dict[str, Any] | None) -> str:
|
|
147
|
+
"""Short text for tables: ``62% 0.198 s/batch eta 3.1h``."""
|
|
148
|
+
if not progress:
|
|
149
|
+
return ""
|
|
150
|
+
parts = []
|
|
151
|
+
if progress.get("fraction") is not None:
|
|
152
|
+
parts.append(f"{progress['fraction']:.0%}")
|
|
153
|
+
else:
|
|
154
|
+
parts.append(f"{progress['done']} {progress['unit']}")
|
|
155
|
+
rate = progress.get("rate_recent_per_hour")
|
|
156
|
+
if rate:
|
|
157
|
+
parts.append(f"{3600 / rate:.3g} s/{progress['unit']}")
|
|
158
|
+
if progress.get("eta_hours") is not None:
|
|
159
|
+
parts.append(f"eta {progress['eta_hours']:.1f}h")
|
|
160
|
+
return " ".join(parts)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _rate(a: tuple[datetime, float], b: tuple[datetime, float]) -> float | None:
|
|
164
|
+
hours = (b[0] - a[0]).total_seconds() / 3600
|
|
165
|
+
if hours <= 0 or b[1] < a[1]:
|
|
166
|
+
return None
|
|
167
|
+
return (b[1] - a[1]) / hours
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _round(value: float | None) -> float | None:
|
|
171
|
+
return float(f"{value:.4g}") if value else None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _read_samples(path: Path) -> list[dict[str, Any]]:
|
|
175
|
+
try:
|
|
176
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
177
|
+
except OSError:
|
|
178
|
+
return []
|
|
179
|
+
out = []
|
|
180
|
+
for line in lines:
|
|
181
|
+
try:
|
|
182
|
+
item = json.loads(line)
|
|
183
|
+
except json.JSONDecodeError:
|
|
184
|
+
continue
|
|
185
|
+
if isinstance(item, dict) and "at" in item and "done" in item:
|
|
186
|
+
out.append(item)
|
|
187
|
+
return out
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _write_samples(path: Path, samples: list[dict[str, Any]]) -> None:
|
|
191
|
+
if len(samples) > 2 * SAMPLES_KEPT:
|
|
192
|
+
samples = samples[-SAMPLES_KEPT:]
|
|
193
|
+
path.write_text("".join(json.dumps(s) + "\n" for s in samples), encoding="utf-8")
|
|
194
|
+
return
|
|
195
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
197
|
+
handle.write(json.dumps(samples[-1]) + "\n")
|
|
@@ -69,8 +69,9 @@ def adapter_sources(project: Project, warnings: list[str]) -> list[SkillSource]:
|
|
|
69
69
|
)
|
|
70
70
|
continue
|
|
71
71
|
skills[path.name] = path # the package owns its skill namespace
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
for folder in _DOC_FOLDERS:
|
|
73
|
+
if (directory / folder).is_dir():
|
|
74
|
+
skills[f"{sim.adapter}-{folder}"] = directory / folder
|
|
74
75
|
version = ctx.resolved.version or "local"
|
|
75
76
|
sources.append(SkillSource(sim.adapter, version, skills))
|
|
76
77
|
return sources
|
|
@@ -104,8 +105,8 @@ def deploy(project: Project, sources: list[SkillSource]) -> DeployReport:
|
|
|
104
105
|
shutil.copytree(src, target)
|
|
105
106
|
if not (target / "SKILL.md").is_file():
|
|
106
107
|
_write_reference_index(target, source.source)
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
else:
|
|
109
|
+
_point_at_deployed_documents(target, source)
|
|
109
110
|
(target / MARKER).write_text(
|
|
110
111
|
f'source = "{source.source}"\nversion = "{source.version}"\n',
|
|
111
112
|
encoding="utf-8",
|
|
@@ -160,18 +161,27 @@ def _marker_source(marker: Path) -> str:
|
|
|
160
161
|
return ""
|
|
161
162
|
|
|
162
163
|
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
|
|
164
|
+
# Document folders an agent context may hold beside skills/. Each is deployed as the
|
|
165
|
+
# skill directory <adapter>-<folder>/ (with a generated SKILL.md index).
|
|
166
|
+
_DOC_FOLDERS = ("references", "docs")
|
|
166
167
|
|
|
167
168
|
|
|
168
|
-
def
|
|
169
|
+
def _point_at_deployed_documents(target: Path, source: SkillSource) -> None:
|
|
170
|
+
"""Rewrite ../../<folder>/ (the package layout) to ../<adapter>-<folder>/ (deployed)."""
|
|
171
|
+
moves = {
|
|
172
|
+
f"../../{folder}/": f"../{source.source}-{folder}/"
|
|
173
|
+
for folder in _DOC_FOLDERS
|
|
174
|
+
if f"{source.source}-{folder}" in source.skills
|
|
175
|
+
}
|
|
176
|
+
if not moves:
|
|
177
|
+
return
|
|
169
178
|
for md in target.rglob("*.md"):
|
|
170
179
|
text = md.read_text(encoding="utf-8")
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
180
|
+
new = text
|
|
181
|
+
for old, replacement in moves.items():
|
|
182
|
+
new = new.replace(old, replacement)
|
|
183
|
+
if new != text:
|
|
184
|
+
md.write_text(new, encoding="utf-8")
|
|
175
185
|
|
|
176
186
|
|
|
177
187
|
_LINK = re.compile(r"\]\(([^)\s]+)\)|`(\.\.?/[^`\s]+)`")
|
|
@@ -201,14 +211,15 @@ def broken_skill_links(project: Project) -> dict[str, list[str]]:
|
|
|
201
211
|
|
|
202
212
|
def _write_reference_index(target: Path, source: str) -> None:
|
|
203
213
|
files = sorted(p.relative_to(target).as_posix() for p in target.rglob("*") if p.is_file())
|
|
214
|
+
kind = target.name.removeprefix(f"{source}-") # references | docs
|
|
204
215
|
lines = [
|
|
205
216
|
"---",
|
|
206
217
|
f"name: {target.name}",
|
|
207
|
-
f"description:
|
|
218
|
+
f"description: {kind.capitalize()} documents for the {source} simulator. "
|
|
208
219
|
"Read the file that matches the question.",
|
|
209
220
|
"---",
|
|
210
221
|
"",
|
|
211
|
-
f"# {source}
|
|
222
|
+
f"# {source} {kind}",
|
|
212
223
|
"",
|
|
213
224
|
*[f"- [{name}]({name})" for name in files],
|
|
214
225
|
"",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
@@ -10,6 +11,7 @@ from runops.errors import RunopsError, UsageError
|
|
|
10
11
|
from runops.jobgen import exit_record_path
|
|
11
12
|
from runops.manifest import ACTIVE_STATES, Manifest, RunState
|
|
12
13
|
from runops.ops.common import run_sim_context
|
|
14
|
+
from runops.ops.progress import record_progress
|
|
13
15
|
from runops.project import Project
|
|
14
16
|
from runops.slurm import CommandRunner, JobState, default_runner, query, query_many
|
|
15
17
|
from runops.util import format_duration, load_toml, now_iso, parse_duration, parse_iso, tomllib
|
|
@@ -112,6 +114,12 @@ def _settle(project: Project, manifest: Manifest, job: JobState | None) -> SyncR
|
|
|
112
114
|
elapsed = _elapsed(job, record)
|
|
113
115
|
if elapsed:
|
|
114
116
|
attempts[-1]["elapsed"] = elapsed
|
|
117
|
+
# The last sample is dated at the finish, so the rate ends where the job did.
|
|
118
|
+
record_progress(
|
|
119
|
+
project, manifest, at=parse_iso(now) or datetime.now().astimezone(), final=True
|
|
120
|
+
)
|
|
121
|
+
elif new_state == RunState.RUNNING:
|
|
122
|
+
record_progress(project, manifest, at=datetime.now().astimezone())
|
|
115
123
|
manifest.set_status(new_state)
|
|
116
124
|
manifest.save()
|
|
117
125
|
return SyncResult(manifest.id, before.value, new_state.value, slurm_state, message)
|
|
@@ -9,12 +9,15 @@ description: Check the progress or result of runops runs (sync, list, log, retry
|
|
|
9
9
|
2. `runo list` shows active and recent runs. `--all`, `--case C`, `--status failed`
|
|
10
10
|
and `--json` narrow or widen the view. Scratch and archived runs are hidden unless
|
|
11
11
|
`--scratch` / `--archived` is given.
|
|
12
|
-
3. `runo status RUN` shows one run
|
|
12
|
+
3. `runo status RUN` shows one run, with progress, recent speed and time left when the
|
|
13
|
+
adapter reports progress (`--json` → `progress`; counted from the resume point, ending
|
|
14
|
+
at the finish). Use it for speed comparisons instead of timing logs by hand.
|
|
15
|
+
`runo log RUN` and `runo log RUN --err` show the
|
|
13
16
|
tail of the latest job's stdout / stderr (`-n 200` for more, `-f` to follow).
|
|
14
17
|
4. For a failed run, read the stderr log and the adapter summary
|
|
15
18
|
(`runo summarize RUN`). Record the cause with `runo note RUN "..."`.
|
|
16
19
|
`failed (TIMEOUT)` means the walltime ran out: compare `elapsed` with `walltime`
|
|
17
|
-
and
|
|
20
|
+
and give the next run more with `--job walltime=...`.
|
|
18
21
|
5. To rerun the same inputs: `runo retry RUN` then `runo submit RUN` after confirming.
|
|
19
22
|
To change inputs, create a new run with `runo new --from RUN --set ...`.
|
|
20
23
|
6. Recovery ("jobs vanished, resubmit them"): `runo sync`, then
|
|
@@ -9,7 +9,7 @@ description: Create simulation runs with runops (runo new / new --from / sweep).
|
|
|
9
9
|
2. Pick the case. Express the variation with `--set`, not with a new case:
|
|
10
10
|
- one run: `runo new CASE --why "..." --set section.key=value`
|
|
11
11
|
- from an existing run: `runo new --from RUN --why "..." --set key=value`
|
|
12
|
-
(keeps the source's params and `[job]
|
|
12
|
+
(keeps the source's params and `[job]`, and sits beside it; the output lists what differs)
|
|
13
13
|
- continue from a finished run (if the adapter supports it): `runo new --from RUN --continue --why "..."`
|
|
14
14
|
- a product of values: `runo sweep CASE --set a=1,2 --set b=x,y --why "..."`
|
|
15
15
|
shows the points and core-hours without creating anything; add `--apply` to create.
|
|
@@ -46,6 +46,6 @@ from the cwd; paths into another project's `runs/` are refused.
|
|
|
46
46
|
|
|
47
47
|
`[run]` id, label, status, why, scratch, expires_at, pinned, tags, superseded_by, `[[notes]]`;
|
|
48
48
|
`[origin]` case, sweep, parent_run; `[simulator]` executable and provenance;
|
|
49
|
-
`[job]` resources, job_id, `[[job.attempts]]` (job_id, exit_code, elapsed, cancel_reason); `[params]` final values;
|
|
49
|
+
`[job]` resources, job_id, `[[job.attempts]]` (job_id, exit_code, elapsed, cancel_reason, progress); `[params]` final values;
|
|
50
50
|
`[variation].changed_keys`, `job_changed_keys`. Status: created -> submitted -> running ->
|
|
51
51
|
completed | failed | cancelled; terminal runs can be archived.
|