runops 2.6.0__tar.gz → 2.8.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.6.0 → runops-2.8.0}/PKG-INFO +1 -1
- {runops-2.6.0 → runops-2.8.0}/SPEC.md +16 -8
- {runops-2.6.0 → runops-2.8.0}/docs/guide.md +10 -2
- {runops-2.6.0 → runops-2.8.0}/docs/reference.md +9 -6
- {runops-2.6.0 → runops-2.8.0}/pyproject.toml +1 -1
- {runops-2.6.0 → runops-2.8.0}/src/runops/__init__.py +1 -1
- {runops-2.6.0 → runops-2.8.0}/src/runops/cli.py +38 -29
- {runops-2.6.0 → runops-2.8.0}/src/runops/harness.py +2 -2
- {runops-2.6.0 → runops-2.8.0}/src/runops/manifest.py +11 -3
- {runops-2.6.0 → runops-2.8.0}/src/runops/ops/create.py +92 -9
- {runops-2.6.0 → runops-2.8.0}/src/runops/ops/notes.py +6 -6
- {runops-2.6.0 → runops-2.8.0}/src/runops/ops/progress.py +16 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/skills/runops-analyze/SKILL.md +9 -4
- runops-2.8.0/src/runops/skills/runops-analyze/example-report/README.md +97 -0
- runops-2.8.0/src/runops/skills/runops-analyze/example-report/out/fig1_maps.png +0 -0
- runops-2.8.0/src/runops/skills/runops-analyze/example-report/out/fig2_dependence.png +0 -0
- runops-2.8.0/src/runops/skills/runops-analyze/example-report/out/values.csv +6 -0
- runops-2.8.0/src/runops/skills/runops-analyze/example-report/report.html +149 -0
- runops-2.8.0/src/runops/skills/runops-analyze/example-report/scripts/build_html.py +72 -0
- runops-2.8.0/src/runops/skills/runops-analyze/example-report/scripts/make_figures.py +122 -0
- runops-2.8.0/src/runops/skills/runops-analyze/report-guide.md +91 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/skills/runops-check-status/SKILL.md +4 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/skills/runops-reference/SKILL.md +3 -3
- {runops-2.6.0 → runops-2.8.0}/tests/test_progress.py +50 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_safety.py +51 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_skills.py +43 -0
- {runops-2.6.0 → runops-2.8.0}/uv.lock +1 -1
- runops-2.6.0/.harness/manifest.toml +0 -15
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/check/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/improve-harness/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/python-package-refactor/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/python-package-refactor/agents/openai.yaml +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/python-package-refactor/examples/refactor-request.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/python-package-refactor/references/anti-patterns.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/python-package-refactor/references/api-compatibility.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/python-package-refactor/references/packaging-modernization.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/python-package-refactor/references/refactor-playbook.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/python-package-refactor/references/verification.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/python-package-refactor/scripts/api_surface_snapshot.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/python-package-refactor/scripts/import_smoke.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/python-package-refactor/scripts/inspect_python_package.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/python-package-refactor/scripts/refactor_quality_gate.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/release/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/simulator-adapter/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/spec-reviewer/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.agents/skills/sync-docs/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.claude/agents/spec-reviewer.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.claude/rules/architecture.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.claude/rules/dev-workflow.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.claude/rules/gotchas.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.claude/settings.json +0 -0
- {runops-2.6.0 → runops-2.8.0}/.claude/skills/check/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.claude/skills/improve-harness/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.claude/skills/python-package-refactor/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.claude/skills/release/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.claude/skills/simulator-adapter/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.claude/skills/spec-reviewer/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.claude/skills/sync-docs/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.codex/README.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/.codex/agents/spec-reviewer.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/.codex/config.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/.codex/rules/runops.rules +0 -0
- {runops-2.6.0 → runops-2.8.0}/.dockerignore +0 -0
- {runops-2.6.0 → runops-2.8.0}/.gitattributes +0 -0
- {runops-2.6.0 → runops-2.8.0}/.github/assets/conversation-dark.svg +0 -0
- {runops-2.6.0 → runops-2.8.0}/.github/assets/conversation-light.svg +0 -0
- {runops-2.6.0 → runops-2.8.0}/.github/assets/hero-dark.svg +0 -0
- {runops-2.6.0 → runops-2.8.0}/.github/assets/hero-light.svg +0 -0
- {runops-2.6.0 → runops-2.8.0}/.github/assets/make_readme_art.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/.github/workflows/ci.yml +0 -0
- {runops-2.6.0 → runops-2.8.0}/.github/workflows/publish.yml +0 -0
- {runops-2.6.0 → runops-2.8.0}/.gitignore +0 -0
- {runops-2.6.0 → runops-2.8.0}/.pre-commit-config.yaml +0 -0
- {runops-2.6.0 → runops-2.8.0}/.vscode/settings.json +0 -0
- {runops-2.6.0 → runops-2.8.0}/AGENTS.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/CLAUDE.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/LICENSE +0 -0
- {runops-2.6.0 → runops-2.8.0}/README.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/docs/adapter.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/adapters/__init__.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/adapters/base.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/adapters/generic.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/adapters/registry.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/case.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/config.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/errors.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/jobgen.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/launchers.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/legacy_adapters/__init__.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/legacy_adapters/beach.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/legacy_adapters/emses.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/ops/__init__.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/ops/analysis.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/ops/common.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/ops/listing.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/ops/migrate.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/ops/setup.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/ops/skills.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/ops/submit.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/ops/sync.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/ops/tidy.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/project.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/runtime.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/skills/runops-create-run/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/skills/runops-maintain/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/skills/runops-tidy/SKILL.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/slurm.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/src/runops/util.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/__init__.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/conftest.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/.codex/rules/a.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/.runops/test-runs/T20260701-0001/receipt.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/.runops/work/beach-x-abc/bin/beach +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/AGENTS.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/CLAUDE.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/cases/beach/plane/beach.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/cases/beach/plane/case.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/cases/old/beach.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/cases/old/case.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/launchers.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/research/CURRENT.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/runops.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/runs/AGENTS.md +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/runs/_archive/plane/R20260601-0001/manifest.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/runs/plane/R20260701-0001--a/input/beach.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/runs/plane/R20260701-0001--a/manifest.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/runs/plane/R20260702-0001/manifest.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/runs/scan/R20260701-0002/manifest.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/runs/scan/survey.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/simulators.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/fixtures/v1-project/site.toml +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/golden/mpi_standard.sh +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/golden/openmp_rsc.sh +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_adapters.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_analysis_layout.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_archive_series.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_build.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_dev_harness.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_jobgen.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_legacy_adapters.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_lifecycle.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_manifest_config.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_migrate.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_mv_naming.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_project_scope.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_run_job.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_scratch_tidy.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_setup_misc.py +0 -0
- {runops-2.6.0 → runops-2.8.0}/tests/test_sweep.py +0 -0
- {runops-2.6.0 → runops-2.8.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.8.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
|
|
@@ -475,8 +475,12 @@ adapter が `progress(run_dir)` を持つとき、`sync` は running の run と
|
|
|
475
475
|
上限 1000 件程度)。queue を離れたときの観測は終了時刻で記録し、終了後の待ち時間を速度に含めない。
|
|
476
476
|
- 要約を `[[job.attempts]].progress` に置く: `done`、`total`、`unit`、`start`、`fraction`、
|
|
477
477
|
`rate_recent_per_hour`(直近 10 分)、`rate_attempt_per_hour`(program 開始から。起点は
|
|
478
|
-
`start`)、`eta_hours`(running
|
|
478
|
+
`start`)、`eta_hours`(running のみ)、`last_change_at`(`done` が最後に増えた観測時刻)、
|
|
479
|
+
`stalled_hours`(最新観測が `last_change_at` から 1 時間以上進んでいないとき、その長さ。
|
|
480
|
+
終了後の観測には付けない)。program 開始時刻は job.sh が stdout に出す
|
|
479
481
|
`runops: start` 行から取り、`started_at` に入れる。
|
|
482
|
+
- `status` / `list` は `stalled_hours` があれば `stalled 6.7h` と表示する。観測は `sync` のときだけ
|
|
483
|
+
なので、停止の検出は `sync` の間隔より細かくならない。
|
|
480
484
|
- `status` / `list` は要約だけを読む(file を開かない)。read-only command は観測を足さない。
|
|
481
485
|
- 解析は adapter が行い、runops は保存と表示だけを持つ(§1.2 #4)。
|
|
482
486
|
|
|
@@ -497,7 +501,8 @@ adapter が `progress(run_dir)` を持つとき、`sync` は running の run と
|
|
|
497
501
|
`--json` は作成した run の同じ summary を返す。summary には `params`、`changed_keys`、
|
|
498
502
|
`parent_run`、`exe_hash`、`input_hash` を含め、run 同士の比較に別 command を要らなくする。
|
|
499
503
|
walltime の根拠として `walltime`、最新 attempt の `elapsed`、`walltime_used`(elapsed / walltime)、
|
|
500
|
-
`slurm_state`(`TIMEOUT`
|
|
504
|
+
`slurm_state`(`TIMEOUT` など)も含める。`list` / `status` の `--brief` は summary から
|
|
505
|
+
`params`、`why`、`last_note` を外す(多数の run を横断して渡すとき用。表では note / why 列を省く)。
|
|
501
506
|
- 破壊的 command(`cancel`、`delete`、`tidy --apply`、`archive`)は対象を表示して
|
|
502
507
|
確認を取る。`-y/--yes` で省略。
|
|
503
508
|
- exit code: 0 成功、1 対象 run の実行時エラー(例: sbatch 失敗)、2 使用方法・設定エラー。
|
|
@@ -518,7 +523,7 @@ adapter が `progress(run_dir)` を持つとき、`sync` は running の run と
|
|
|
518
523
|
| `runo doctor [--json]` | `runops.toml` の妥当性、adapter の import 可否、executable の存在、`sbatch` の有無、skill の鮮度を検査 |
|
|
519
524
|
| `runo update [--skills-only] [-y]` | adapter package を upgrade し、skill を再展開する |
|
|
520
525
|
| `runo build SIM [--force]` | `local_source` simulator を `.runops/build/` に build する(§4.1) |
|
|
521
|
-
| `runo context [--json]` | agent 向け現在地。project 名、simulator、active run
|
|
526
|
+
| `runo context [--json]` | agent 向け現在地。project 名、simulator、active run 数と全 active run(`--brief` と同じ summary、`active`)、直近 run 5 件、scratch 期限切れ数、`LOG.md` 末尾 20 行、現用 group の解析 topic 名(§9.1)。配置した runops skill が install 済みの runops より古ければ `runo update --skills-only` を促す warning |
|
|
522
527
|
| `runo case new NAME --simulator SIM [--from CASE]` | case をスキャフォールド。adapter の `case_template()` を展開 |
|
|
523
528
|
| `runo case list` | case 一覧 |
|
|
524
529
|
| `runo new CASE [--why TEXT] [--label L] [--set k=v]... [--job k=v]... [--tag T]... [--scratch] [--dest DIR] [--submit] [--created-by WHO] [--json]` | 単発 run を作る |
|
|
@@ -528,10 +533,10 @@ adapter が `progress(run_dir)` を持つとき、`sync` は running の run と
|
|
|
528
533
|
| `runo submit --all [DIR] [-y]` | DIR 配下の `created` run をすべて投入。件数と概算 core-hours を表示して確認 |
|
|
529
534
|
| `runo cancel RUN... [--why TEXT] [-y]` | scancel して sync。`--why` は `cancelled: ...` の note として残す |
|
|
530
535
|
| `runo retry RUN [--submit] [-y]` | `failed`/`cancelled` を `created` に戻す。直近の cancel に `--why` があれば理由を示して確認を取る |
|
|
531
|
-
| `runo status [RUN...] [--json]` | manifest の状態を表示。Slurm
|
|
536
|
+
| `runo status [RUN...] [--brief] [--json]` | manifest の状態を表示。Slurm を呼ばない。`--brief` は §7.1 |
|
|
532
537
|
| `runo sync [RUN...] [--json]` | squeue/sacct で状態を更新。引数なしは `submitted`/`running` 全件 |
|
|
533
538
|
| `runo log RUN [--err] [-n N] [-f]` | 最新 job の stdout/stderr を表示。`-f` は tail -f |
|
|
534
|
-
| `runo list [PATH] [--all] [--since 30d] [--case C] [--sweep L] [--status S]... [--tag T]... [--scratch] [--archived] [--json]` | run 一覧(§7.4) |
|
|
539
|
+
| `runo list [PATH] [--all] [--since 30d] [--case C] [--sweep L] [--status S]... [--tag T]... [--scratch] [--archived] [--brief] [--json]` | run 一覧(§7.4) |
|
|
535
540
|
| `runo note RUN [TEXT] [--by WHO]` | TEXT があれば `[[notes]]` に追記、なければ notes と `notes.md` を表示 |
|
|
536
541
|
| `runo pin RUN [--off] [--reason TEXT]` | `run.pinned` を切り替える。pinned は `list` に常に出て、`tidy` / `delete` の対象外 |
|
|
537
542
|
| `runo summarize RUN... [--force]` | adapter の `summarize()` を呼び `analysis/summary.json` を書く。既存があれば `--force` なしではスキップ |
|
|
@@ -550,7 +555,8 @@ adapter が `progress(run_dir)` を持つとき、`sync` は running の run と
|
|
|
550
555
|
- 既定で表示しない: scratch run、archived run、上記以外の古い terminal run。
|
|
551
556
|
- `--all` は scratch と archived 以外をすべて表示。`--scratch`、`--archived` は
|
|
552
557
|
それぞれを **追加** する。
|
|
553
|
-
- 表示列: id、status、label、case、sweep、created
|
|
558
|
+
- 表示列: id、status、label、case、sweep、created(相対時刻)、elapsed、progress、最新 note の先頭 60 文字、`why`。
|
|
559
|
+
`--brief` は note と `why` の列を省く。
|
|
554
560
|
- sort は `created_at` 降順。
|
|
555
561
|
- `PATH` を与えた場合はその directory 配下に限定する。
|
|
556
562
|
|
|
@@ -723,14 +729,14 @@ runops は `init` と `update` で次を行う。
|
|
|
723
729
|
runops は次の 6 skill だけを同梱し、simulator skill と同じ仕組みで展開する。
|
|
724
730
|
利用者が覚えるコマンドは `runo init` だけとし、それ以外(simulator の追加、更新、v1 移行を含む)は
|
|
725
731
|
エージェントへの依頼で行う。
|
|
726
|
-
各 SKILL.md は 60
|
|
732
|
+
各 SKILL.md は 60 行以内に収める。詳しい指針や例が要る skill は、同じ directory に別 file として置き、SKILL.md から相対リンクで参照する。
|
|
727
733
|
|
|
728
734
|
| skill | 内容 |
|
|
729
735
|
|---|---|
|
|
730
736
|
| `runops-create-run` | `new` / `sweep` の使い方。`--why` を必ず書く、scratch の使い分け |
|
|
731
737
|
| `runops-check-status` | `sync` → `list` → `log` の順。失敗時の `retry` と `tidy --purge-work` |
|
|
732
738
|
| `runops-tidy` | `tidy` を定期的に実行する判断基準。pin の使い方 |
|
|
733
|
-
| `runops-analyze` | 依存する run の範囲で調査の置き場所を決め、script・結果・log・README
|
|
739
|
+
| `runops-analyze` | 依存する run の範囲で調査の置き場所を決め、script・結果・log・README をまとめる。読み手に渡す比較レポートの書き方は同梱の `report-guide.md` と `example-report/` に置く |
|
|
734
740
|
| `runops-reference` | command 一覧(§7.3 の表をそのまま載せる) |
|
|
735
741
|
| `runops-maintain` | simulator の追加、runops と simulator package の更新、`migrate-v1`、`doctor` の読み方 |
|
|
736
742
|
|
|
@@ -1011,6 +1017,7 @@ v2 の文書は次の 4 ファイルだけにする。
|
|
|
1011
1017
|
|
|
1012
1018
|
| 項目 | 決定 | 理由 |
|
|
1013
1019
|
|---|---|---|
|
|
1020
|
+
| 比較レポートの書き方(2026-11-10) | `runops-analyze` に執筆ガイド `report-guide.md` と模擬データの例 `example-report/` を同梱する。README.md を本文の正本とし、例の `scripts/build_html.py` が図を埋め込んだ `report.html` を作る。command やレポート生成機構は足さない | 差分図と条件の列挙が中心のレポートは読み手に伝わらず、元の分布を同じ尺度で並べ、問い・観測・解釈をつなげて書き直すと伝わった(#119)。書き方は判断を伴うので、手順と例として skill に置く |
|
|
1014
1021
|
| 解析の配置 2.6.0(2026-10-23) | 依存する run の範囲に応じて run 内 `analysis/`、group の `_analysis/<topic>/`、project の `analysis/<topic>/` にまとめる。skill、log の tidy、group archive、context を対応させる(§9.1) | 報告・図・script・job log が分散・混在していた。group は既存の directory として扱い、新しい entity や command は足さない。既存ファイルの整理は移動一覧を提示してから行う |
|
|
1015
1022
|
| job の cwd | run_dir(v1 と同じ)。Slurm log は `work/` | v1 adapter の path 規約と実 project の job.sh をそのまま使える |
|
|
1016
1023
|
| adapter の追加 hook | `configure(sim_config)`、`job_prelude(run_dir)` | generic の `command` 取得と、EMSES の `rm -f *_0000.h5` など v1 の setup command を表す |
|
|
@@ -1038,5 +1045,6 @@ v2 の文書は次の 4 ファイルだけにする。
|
|
|
1038
1045
|
| 生成ハーネスの書き方(2026-10-21) | AGENTS.md の Rules を「作業の進め方」5 項目に絞り、各項目を「何をするか」と短い理由で書く。skill は手順に専念し、AGENTS.md の方針(manifest を手で編集しない等)を繰り返さない | #112・#113 で規則が増え、禁止形(never / do not)が並んでいた。現行の Claude の prompting 指針は、しないことより何をするかを、理由と共に書くことを勧め、強い禁止語は過剰反応を招くとしている |
|
|
1039
1046
|
| 複数 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 で同じことが起き得た |
|
|
1040
1047
|
| 進捗と系列配置 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 は保存と表示だけにする |
|
|
1048
|
+
| 横断の状態確認 2.7.0(2026-10-30) | `list` / `status` に `--brief`、`context` に全 active run の `active`、progress 要約に `last_change_at` と `stalled_hours`(§6.4、§7.1、§7.3) | hub で 4 project の active run を集めるのに `recent_runs` 5 件では足りず、全 `params` 付きの `list --json` は shell 引数長を超えた(3 回)。進捗が 6 時間止まった run を 49% と報告した。新しい command は足さず、既存の summary を小さくし、観測から止まりを出す |
|
|
1041
1049
|
| 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 数は増やさない |
|
|
1042
1050
|
| 使い方ガイド | `docs/guide.md` を追加し、文書を 4 ファイルにする(§13.3) | README の依頼例の表と command 一覧の間を埋める、場面ごとの流れ(case と run の分け方、smoke から本計算、状態の決まり方、整理)の置き場所が無かった |
|
|
@@ -60,7 +60,8 @@ runops が扱う名詞は case、run、sweep の 3 つです。
|
|
|
60
60
|
作った run の `manifest.toml` と `input/` は変更しません。入力を直したいときは新しい run を作ります。
|
|
61
61
|
同じ入力の run が既にあると、作成時に警告が出ます。
|
|
62
62
|
`new --from` は元の run の params を引き継ぎますが、入力ファイルは今の case から作ります。
|
|
63
|
-
元の run を作った後に case
|
|
63
|
+
元の run を作った後に case の入力ファイルを直して値が変わると、作成時の警告に変わる key と値が並びます。
|
|
64
|
+
元の値のまま続けたいときは `--set` で指定します。
|
|
64
65
|
|
|
65
66
|
### 名前の付け方
|
|
66
67
|
|
|
@@ -134,7 +135,10 @@ walltime は、同じ case・同じ資源で終わった run の実時間を元
|
|
|
134
135
|
adapter が進捗に対応していれば(BEACH の batch、MPIEMSES3D の step)、`sync` のたびに時刻付きの
|
|
135
136
|
観測を残し、`runo list` / `status` に `62% 0.198 s/batch eta 3.1h` のように進み具合、直近 10 分の
|
|
136
137
|
速度、残り時間を出します。resume した run は再開点から数え、終わった run は終了時刻までで速度を出すので、
|
|
137
|
-
版を切り替えた前後の比較にそのまま使えます(`--json` の `progress
|
|
138
|
+
版を切り替えた前後の比較にそのまま使えます(`--json` の `progress`)。`sync` を重ねても進みが 1 時間以上
|
|
139
|
+
変わらない run には `stalled 6.7h` が付くので、Slurm では RUNNING でも止まっている run を見分けられます。
|
|
140
|
+
全 project を横断して見るときは `runo context --json` の `active`(全 active run)か `runo list --brief --json`
|
|
141
|
+
(`params` や note を省いた小さい JSON)を使います。
|
|
138
142
|
|
|
139
143
|
### 状態の決まり方
|
|
140
144
|
|
|
@@ -229,6 +233,10 @@ topic は `speed-by-version` のように問いの内容で名付け、同じ問
|
|
|
229
233
|
|
|
230
234
|
エージェントは `runops-analyze` skill を使い、調査の README と、そこへのリンクを含む
|
|
231
235
|
`LOG.md` の判断を更新します。`notes/` は run に依存しない知見に使います。
|
|
236
|
+
比較の結果を人に見せるレポートを頼むと(「この 3 条件を比較してレポートにまとめて」)、
|
|
237
|
+
エージェントは skill 同梱の執筆ガイドと例に沿って、問いと結論、条件を同じ尺度で並べた図、
|
|
238
|
+
図の読み方と結果の意味、再現情報の順に書きます。README.md と、図を埋め込んで単体で
|
|
239
|
+
読める `report.html` の両方ができます。
|
|
232
240
|
解析の追加依存は `.runops/analysis-env/` に置き、重い読み込みや作図はサイトの規則に従って
|
|
233
241
|
計算ノードで実行します。sbatch は調査 directory から投入し、事前に `logs/` を作って
|
|
234
242
|
`#SBATCH -o logs/%x.%j.out` と `#SBATCH -e logs/%x.%j.err` を指定します。
|
|
@@ -129,8 +129,10 @@ program を起動せずに終わり、`stage = "setup"` と exit code を記録
|
|
|
129
129
|
|
|
130
130
|
`submit` は executable を hash し直して attempt に記録し、作成時と違えば警告します。
|
|
131
131
|
`new` と `sweep --apply` は同じ入力(`input_hash`)の run が既にあれば警告します。
|
|
132
|
-
`new --from RUN` は、元の run を作った後に case
|
|
133
|
-
|
|
132
|
+
`new --from RUN` は、元の run を作った後に case の入力ファイルが変わり、そのために入力の値が
|
|
133
|
+
元の run と違うとき、違う key と値を警告に並べます(入力は今の case から作るため)。TOML 以外の
|
|
134
|
+
入力は file 名だけを示します。`--set` した key と、adapter が `--continue` のために書いた key は
|
|
135
|
+
並べません。どれも作成や投入は拒否しません。
|
|
134
136
|
|
|
135
137
|
`submit`、`cancel`、`retry`、`sync` は lock の中で manifest を読み直してから更新します。
|
|
136
138
|
同じ run を 2 回指定しても、複数のエージェントが同時に投入しても sbatch は 1 回です。
|
|
@@ -141,7 +143,8 @@ run id は削除後も再利用しません。`--dest` に既存 run の中は
|
|
|
141
143
|
`list --json` と `status --json` は、walltime の根拠として `walltime`、最新 attempt の `elapsed`、
|
|
142
144
|
`walltime_used`(elapsed / walltime)、`slurm_state` も返します。adapter が `progress()` を持てば
|
|
143
145
|
`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`
|
|
146
|
+
`eta_hours`、`last_change_at`、`stalled_hours`)も返し、表の `progress` 列に `62% 0.198 s/batch eta 3.1h` と出します。
|
|
147
|
+
`sync` の観測で `done` が 1 時間以上増えていなければ `stalled 6.7h` が付きます(観測は `sync` のときだけ)。表では `TIMEOUT` などを
|
|
145
148
|
`failed (TIMEOUT)` と示します。
|
|
146
149
|
|
|
147
150
|
全 command は `--help` で option を確認できます。`RUN` には run id、run directory、
|
|
@@ -153,15 +156,15 @@ run id は削除後も再利用しません。`--dest` に既存 run の中は
|
|
|
153
156
|
| `runo doctor [--json]` | 設定、adapter の解決経路、executable、sbatch、skill の鮮度とリンク切れを点検 |
|
|
154
157
|
| `runo update [--skills-only] [-y]` | simulator package を更新し skill と AGENTS.md の管理 block を再生成 |
|
|
155
158
|
| `runo build SIM [--force]` | `local_source` の simulator を build |
|
|
156
|
-
| `runo context [--json]` | agent
|
|
159
|
+
| `runo context [--json]` | agent 向けの現在地。全 active run(`active`、`--brief` と同じ形)、直近 5 run、現用 group の解析 topic 名(JSON の `group_analysis` は group path → topic 名一覧) |
|
|
157
160
|
| `runo case new NAME [-s SIM] [--from CASE]` / `runo case list` | case の作成と一覧 |
|
|
158
161
|
| `runo new CASE [--why] [--label] [--set k=v]... [--job k=v]... [--tag] [--scratch] [--dest DIR] [--submit] [--json]` | run を 1 つ作る。`--job` はこの run の `[job]` の値 |
|
|
159
162
|
| `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
163
|
| `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 の親 |
|
|
161
164
|
| `runo submit RUN... [-p P] [--qos Q] [--afterok JOB]` / `runo submit --all [DIR] [-y]` | job.sh を生成して sbatch |
|
|
162
165
|
| `runo cancel RUN... [--why TEXT] [-y]` / `runo retry RUN [--submit] [-y]` | scancel(`--why` は note に残る)/ failed・cancelled を created に戻す(理由付きで cancel された run は確認を取る) |
|
|
163
|
-
| `runo status [RUN...]` / `runo sync [RUN...]` / `runo log RUN [--err] [-n N] [-f]` | 状態表示 / Slurm から更新 / log 表示 |
|
|
164
|
-
| `runo list [PATH] [--all] [--since 30d] [--case] [--sweep] [--status] [--tag] [--scratch] [--archived] [--json]` | run
|
|
166
|
+
| `runo status [RUN...] [--brief]` / `runo sync [RUN...]` / `runo log RUN [--err] [-n N] [-f]` | 状態表示 / Slurm から更新 / log 表示 |
|
|
167
|
+
| `runo list [PATH] [--all] [--since 30d] [--case] [--sweep] [--status] [--tag] [--scratch] [--archived] [--brief] [--json]` | run 一覧。`--brief` は `params`、`why`、note を省く(表では note / why 列) |
|
|
165
168
|
| `runo note RUN [TEXT]` / `runo pin RUN [--off] [--reason]` | ノート追記・表示 / pin |
|
|
166
169
|
| `runo summarize RUN... [--force]` | adapter の要約を `analysis/summary.json` に書く |
|
|
167
170
|
| `runo mv RUN... [--dest DIR] [--label L] [--why TEXT]` | `runs/` 配下で改名・移動。id、入力、sweep、ノートは保つ。submitted / running と archived は不可。全対象を検査してから移し、1 件でも不可なら何も動かさない |
|
|
@@ -257,6 +257,16 @@ def context(as_json: bool = typer.Option(False, "--json")) -> None:
|
|
|
257
257
|
f"runs: {counts or 'none'}; active: {data['active_runs']}; "
|
|
258
258
|
f"expired scratch: {data['expired_scratch']}"
|
|
259
259
|
)
|
|
260
|
+
if data["active"]:
|
|
261
|
+
from runops.ops.progress import format_progress
|
|
262
|
+
|
|
263
|
+
out.print("active:")
|
|
264
|
+
for run in data["active"]:
|
|
265
|
+
out.print(
|
|
266
|
+
f" {run['id']} {run['status']:9} {run['case']} {run['label']} "
|
|
267
|
+
f"{format_progress(run['progress'])}"
|
|
268
|
+
)
|
|
269
|
+
out.print("recent:")
|
|
260
270
|
for run in data["recent_runs"]:
|
|
261
271
|
out.print(
|
|
262
272
|
f" {run['id']} {run['status']:9} {run['case']} {run['label']} {run['why'][:60]}"
|
|
@@ -592,23 +602,14 @@ def retry(
|
|
|
592
602
|
_submit_each(project, [m])
|
|
593
603
|
|
|
594
604
|
|
|
595
|
-
def _status_table(project: Project, manifests: list[Manifest]) -> Table:
|
|
605
|
+
def _status_table(project: Project, manifests: list[Manifest], *, brief: bool = False) -> Table:
|
|
596
606
|
from runops.ops.listing import relative_age
|
|
597
607
|
from runops.ops.progress import format_progress
|
|
598
608
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
"
|
|
602
|
-
|
|
603
|
-
"case",
|
|
604
|
-
"sweep",
|
|
605
|
-
"created",
|
|
606
|
-
"elapsed",
|
|
607
|
-
"progress",
|
|
608
|
-
"note",
|
|
609
|
-
"why",
|
|
610
|
-
box=None,
|
|
611
|
-
)
|
|
609
|
+
columns = ["id", "status", "label", "case", "sweep", "created", "elapsed", "progress"]
|
|
610
|
+
if not brief:
|
|
611
|
+
columns += ["note", "why"]
|
|
612
|
+
table = Table(*columns, box=None)
|
|
612
613
|
for m in manifests:
|
|
613
614
|
s = m.to_summary(project.root)
|
|
614
615
|
color = {
|
|
@@ -630,7 +631,7 @@ def _status_table(project: Project, manifests: list[Manifest]) -> Table:
|
|
|
630
631
|
status += " [dim](pinned)[/]"
|
|
631
632
|
if s["superseded_by"]:
|
|
632
633
|
status += f" [dim](-> {', '.join(s['superseded_by'])})[/]"
|
|
633
|
-
|
|
634
|
+
row = [
|
|
634
635
|
s["id"],
|
|
635
636
|
status,
|
|
636
637
|
s["label"],
|
|
@@ -639,15 +640,19 @@ def _status_table(project: Project, manifests: list[Manifest]) -> Table:
|
|
|
639
640
|
relative_age(s["created_at"]),
|
|
640
641
|
elapsed,
|
|
641
642
|
format_progress(s["progress"]),
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
643
|
+
]
|
|
644
|
+
if not brief:
|
|
645
|
+
row += [s["last_note"][:60], s["why"][:60]]
|
|
646
|
+
table.add_row(*row)
|
|
645
647
|
return table
|
|
646
648
|
|
|
647
649
|
|
|
648
650
|
@app.command()
|
|
649
651
|
def status(
|
|
650
652
|
runs: Optional[list[str]] = typer.Argument(None),
|
|
653
|
+
brief: bool = typer.Option(
|
|
654
|
+
False, "--brief", help="Leave out params, why, notes and the note/why columns."
|
|
655
|
+
),
|
|
651
656
|
as_json: bool = typer.Option(False, "--json"),
|
|
652
657
|
) -> None:
|
|
653
658
|
"""Show manifest state (no Slurm query). Without RUN: active runs."""
|
|
@@ -662,18 +667,19 @@ def status(
|
|
|
662
667
|
if as_json:
|
|
663
668
|
data = []
|
|
664
669
|
for m in targets:
|
|
665
|
-
s = m.to_summary(project.root)
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
670
|
+
s = m.to_summary(project.root, brief=brief)
|
|
671
|
+
if not brief:
|
|
672
|
+
s.update(
|
|
673
|
+
{
|
|
674
|
+
"params": m.params,
|
|
675
|
+
"job": {k: v for k, v in m.job.items() if k != "attempts"},
|
|
676
|
+
"notes": m.notes,
|
|
677
|
+
}
|
|
678
|
+
)
|
|
673
679
|
data.append(s)
|
|
674
680
|
_print_json(data)
|
|
675
681
|
return
|
|
676
|
-
out.print(_status_table(project, targets))
|
|
682
|
+
out.print(_status_table(project, targets, brief=brief))
|
|
677
683
|
|
|
678
684
|
|
|
679
685
|
@app.command("sync")
|
|
@@ -730,6 +736,9 @@ def list_cmd(
|
|
|
730
736
|
tags: Optional[list[str]] = typer.Option(None, "--tag"),
|
|
731
737
|
scratch: bool = typer.Option(False, "--scratch", help="Also show scratch runs."),
|
|
732
738
|
archived: bool = typer.Option(False, "--archived", help="Also show archived runs."),
|
|
739
|
+
brief: bool = typer.Option(
|
|
740
|
+
False, "--brief", help="Leave out params, why and notes (small JSON, short table)."
|
|
741
|
+
),
|
|
733
742
|
as_json: bool = typer.Option(False, "--json"),
|
|
734
743
|
) -> None:
|
|
735
744
|
"""List active and recent runs."""
|
|
@@ -752,12 +761,12 @@ def list_cmd(
|
|
|
752
761
|
)
|
|
753
762
|
runs = select_runs(project, flt)
|
|
754
763
|
if as_json:
|
|
755
|
-
_print_json([m.to_summary(project.root) for m in runs])
|
|
764
|
+
_print_json([m.to_summary(project.root, brief=brief) for m in runs])
|
|
756
765
|
return
|
|
757
766
|
if not runs:
|
|
758
767
|
out.print("no runs (try --all, --scratch, --archived)")
|
|
759
768
|
return
|
|
760
|
-
out.print(_status_table(project, runs))
|
|
769
|
+
out.print(_status_table(project, runs, brief=brief))
|
|
761
770
|
|
|
762
771
|
|
|
763
772
|
@app.command()
|
|
@@ -12,14 +12,14 @@ END = "<!-- runops:end -->"
|
|
|
12
12
|
COMMANDS = """\
|
|
13
13
|
| command | effect |
|
|
14
14
|
|---|---|
|
|
15
|
-
| `runo context --json` | active runs, recent runs, group analysis topics, LOG.md tail |
|
|
15
|
+
| `runo context --json` | all active runs (with progress), recent runs, group analysis topics, LOG.md tail |
|
|
16
16
|
| `runo case list` / `runo case new NAME --simulator SIM [--from CASE]` | list / scaffold cases |
|
|
17
17
|
| `runo new CASE --why TEXT [--set k=v]... [--job k=v]... [--scratch] [--submit]` | create one run |
|
|
18
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 |
|
|
22
|
-
| `runo list [--all] [--case C] [--status S] [--json]` | recent + active runs (old, scratch, archived hidden) |
|
|
22
|
+
| `runo list [--all] [--case C] [--status S] [--brief] [--json]` | recent + active runs (old, scratch, archived hidden); `--brief` drops params/why/notes |
|
|
23
23
|
| `runo cancel RUN [--why T]` / `runo retry RUN` | stop a job / reset failed or cancelled to created |
|
|
24
24
|
| `runo note RUN "text"` / `runo pin RUN --reason TEXT` | append an observation / protect a key run |
|
|
25
25
|
| `runo summarize RUN` | adapter summary into analysis/summary.json |
|
|
@@ -188,14 +188,18 @@ class Manifest:
|
|
|
188
188
|
notes = self.data.setdefault("notes", [])
|
|
189
189
|
notes.append({"at": now_iso(), "by": by, "text": text})
|
|
190
190
|
|
|
191
|
-
def to_summary(self, root: Path) -> dict[str, Any]:
|
|
192
|
-
"""Flat dict for ``list --json`` / ``status --json``.
|
|
191
|
+
def to_summary(self, root: Path, *, brief: bool = False) -> dict[str, Any]:
|
|
192
|
+
"""Flat dict for ``list --json`` / ``status --json``.
|
|
193
|
+
|
|
194
|
+
*brief* leaves out ``params``, ``why`` and ``last_note``, so a listing of many
|
|
195
|
+
runs stays small enough to pass around (e.g. a cross-project status table).
|
|
196
|
+
"""
|
|
193
197
|
notes = self.notes
|
|
194
198
|
try:
|
|
195
199
|
rel = self.run_dir.resolve().relative_to(root.resolve()).as_posix()
|
|
196
200
|
except ValueError:
|
|
197
201
|
rel = str(self.run_dir)
|
|
198
|
-
|
|
202
|
+
summary = {
|
|
199
203
|
"id": self.id,
|
|
200
204
|
"status": self.status.value,
|
|
201
205
|
"label": self.label,
|
|
@@ -222,6 +226,10 @@ class Manifest:
|
|
|
222
226
|
**self.timing(),
|
|
223
227
|
"progress": self.progress,
|
|
224
228
|
}
|
|
229
|
+
if brief:
|
|
230
|
+
for key in ("params", "why", "last_note"):
|
|
231
|
+
del summary[key]
|
|
232
|
+
return summary
|
|
225
233
|
|
|
226
234
|
@property
|
|
227
235
|
def progress(self) -> dict[str, Any] | None:
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import contextlib
|
|
5
6
|
import itertools
|
|
6
7
|
import shutil
|
|
8
|
+
from collections.abc import Iterable
|
|
7
9
|
from dataclasses import dataclass, field
|
|
8
10
|
from datetime import datetime, timedelta
|
|
9
11
|
from pathlib import Path
|
|
@@ -28,7 +30,17 @@ from runops.ops.common import SimContext, resolve_dest, sim_context
|
|
|
28
30
|
from runops.ops.submit import write_job_for
|
|
29
31
|
from runops.project import ARCHIVE_DIR, SCRATCH_DIR, Project
|
|
30
32
|
from runops.runtime import resolve_runtime
|
|
31
|
-
from runops.util import
|
|
33
|
+
from runops.util import (
|
|
34
|
+
hash_file,
|
|
35
|
+
hash_tree,
|
|
36
|
+
load_toml,
|
|
37
|
+
move_noreplace,
|
|
38
|
+
now_iso,
|
|
39
|
+
parse_value,
|
|
40
|
+
tomllib,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
SHOWN_INPUT_CHANGES = 10
|
|
32
44
|
|
|
33
45
|
|
|
34
46
|
@dataclass
|
|
@@ -40,6 +52,7 @@ class RunSpec:
|
|
|
40
52
|
sweep: str = ""
|
|
41
53
|
job: dict[str, Any] = field(default_factory=dict)
|
|
42
54
|
job_changed: list[str] = field(default_factory=list)
|
|
55
|
+
input_changes: list[str] = field(default_factory=list) # set for a run derived from another
|
|
43
56
|
|
|
44
57
|
|
|
45
58
|
@dataclass
|
|
@@ -118,6 +131,60 @@ def _job_keys(job: dict[str, Any]) -> dict[str, Any]:
|
|
|
118
131
|
return {k: v for k, v in job.items() if k in JOB_KEYS}
|
|
119
132
|
|
|
120
133
|
|
|
134
|
+
def _flat(value: Any, prefix: str = "") -> dict[str, Any]:
|
|
135
|
+
"""The leaves of a TOML document under dotted keys, arrays of tables by index."""
|
|
136
|
+
items: Iterable[tuple[Any, Any]]
|
|
137
|
+
if isinstance(value, dict):
|
|
138
|
+
items = value.items()
|
|
139
|
+
elif isinstance(value, list) and any(isinstance(x, dict) for x in value):
|
|
140
|
+
items = enumerate(value)
|
|
141
|
+
else:
|
|
142
|
+
return {prefix: value}
|
|
143
|
+
flat: dict[str, Any] = {}
|
|
144
|
+
for key, item in items:
|
|
145
|
+
flat.update(_flat(item, f"{prefix}.{key}" if prefix else str(key)))
|
|
146
|
+
return flat
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _input_values(input_dir: Path) -> dict[tuple[str, str], Any]:
|
|
150
|
+
"""What a run's input files say: ``(file, dotted key)`` for TOML, ``(file, "")`` otherwise."""
|
|
151
|
+
values: dict[tuple[str, str], Any] = {}
|
|
152
|
+
for path in sorted(p for p in input_dir.rglob("*") if p.is_file()):
|
|
153
|
+
rel = path.relative_to(input_dir).as_posix()
|
|
154
|
+
document = None
|
|
155
|
+
if path.suffix == ".toml":
|
|
156
|
+
with contextlib.suppress(tomllib.TOMLDecodeError, UnicodeDecodeError):
|
|
157
|
+
document = load_toml(path)
|
|
158
|
+
if document is None:
|
|
159
|
+
values[(rel, "")] = hash_file(path)
|
|
160
|
+
else:
|
|
161
|
+
values.update({(rel, key): value for key, value in _flat(document).items()})
|
|
162
|
+
return values
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _differing(
|
|
166
|
+
before: dict[tuple[str, str], Any], after: dict[tuple[str, str], Any]
|
|
167
|
+
) -> list[tuple[str, str]]:
|
|
168
|
+
return sorted(k for k in before.keys() | after.keys() if before.get(k) != after.get(k))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _input_changes(
|
|
172
|
+
before: dict[tuple[str, str], Any],
|
|
173
|
+
after: dict[tuple[str, str], Any],
|
|
174
|
+
explained: set[str],
|
|
175
|
+
) -> list[str]:
|
|
176
|
+
"""What differs between two runs' inputs, leaving out the keys in *explained*."""
|
|
177
|
+
lines = []
|
|
178
|
+
for key in _differing(before, after):
|
|
179
|
+
rel, name = key
|
|
180
|
+
if not name:
|
|
181
|
+
lines.append(rel)
|
|
182
|
+
elif not any(name == k or name.startswith(f"{k}.") for k in explained):
|
|
183
|
+
old, new = (repr(side[key]) if key in side else "-" for side in (before, after))
|
|
184
|
+
lines.append(f"{name}: {old} -> {new}")
|
|
185
|
+
return lines
|
|
186
|
+
|
|
187
|
+
|
|
121
188
|
def _diff(label: str, before: dict[str, Any], after: dict[str, Any]) -> str:
|
|
122
189
|
keys = [k for k in after if before.get(k) != after[k]]
|
|
123
190
|
keys += [k for k in before if k not in after]
|
|
@@ -263,14 +330,15 @@ def create_run(project: Project, request: CreateRequest) -> CreateResult:
|
|
|
263
330
|
line = _diff("job vs case", dict(case.job), job)
|
|
264
331
|
if line:
|
|
265
332
|
result.changes.append(line)
|
|
266
|
-
if source is not None:
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
333
|
+
if source is not None and spec.input_changes:
|
|
334
|
+
# Inputs come from the case as it is now, not as it was for the source.
|
|
335
|
+
shown = "; ".join(spec.input_changes[:SHOWN_INPUT_CHANGES])
|
|
336
|
+
hidden = len(spec.input_changes) - SHOWN_INPUT_CHANGES
|
|
337
|
+
more = f"; and {hidden} more" if hidden > 0 else ""
|
|
338
|
+
result.warnings.append(
|
|
339
|
+
f"case {case.name!r} input files changed since {source.id} was created; "
|
|
340
|
+
f"{result.manifests[0].id} differs from it beyond the params in: {shown}{more}"
|
|
341
|
+
)
|
|
274
342
|
return result
|
|
275
343
|
|
|
276
344
|
|
|
@@ -483,8 +551,13 @@ def _populate(
|
|
|
483
551
|
dst = input_dir / rel
|
|
484
552
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
485
553
|
shutil.copy2(src, dst)
|
|
554
|
+
recorded = str(source.section("origin").get("case_input_hash", "")) if source else ""
|
|
555
|
+
case_moved = bool(recorded) and recorded != case.input_hash()
|
|
556
|
+
rendered: dict[tuple[str, str], Any] = {}
|
|
486
557
|
try:
|
|
487
558
|
ctx.adapter.render_inputs(dict(spec.params), stage)
|
|
559
|
+
if case_moved:
|
|
560
|
+
rendered = _input_values(input_dir)
|
|
488
561
|
if request.continue_run and source is not None:
|
|
489
562
|
extend = getattr(ctx.adapter, "extend_inputs", None)
|
|
490
563
|
if not callable(extend):
|
|
@@ -497,6 +570,16 @@ def _populate(
|
|
|
497
570
|
) from None
|
|
498
571
|
except (ValueError, KeyError, OSError) as exc:
|
|
499
572
|
raise RunError(f"{run_id}: render_inputs failed: {exc}") from exc
|
|
573
|
+
if case_moved and source is not None:
|
|
574
|
+
values = _input_values(input_dir)
|
|
575
|
+
# Keys the user set, or the adapter wrote to continue, are not case changes.
|
|
576
|
+
explained = {
|
|
577
|
+
k.replace("[", ".").replace("]", "") for k in changed_keys(source.params, spec.params)
|
|
578
|
+
}
|
|
579
|
+
explained |= {name for _, name in _differing(rendered, values) if name}
|
|
580
|
+
spec.input_changes = _input_changes(
|
|
581
|
+
_input_values(source.run_dir / "input"), values, explained
|
|
582
|
+
)
|
|
500
583
|
|
|
501
584
|
now = now_iso()
|
|
502
585
|
run: dict[str, Any] = {
|
|
@@ -64,11 +64,9 @@ def project_context(project: Project, *, log_lines: int = 20) -> dict[str, Any]:
|
|
|
64
64
|
expires = parse_iso(str(m.run.get("expires_at", "")))
|
|
65
65
|
if expires is not None and expires <= now:
|
|
66
66
|
expired += 1
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
reverse=True,
|
|
71
|
-
)[:5]
|
|
67
|
+
by_created = sorted(runs, key=lambda m: m.created_at, reverse=True)
|
|
68
|
+
recent = [m for m in by_created if m.status != RunState.ARCHIVED][:5]
|
|
69
|
+
active = [m for m in by_created if m.status in ACTIVE_STATES]
|
|
72
70
|
log_path = project.root / "LOG.md"
|
|
73
71
|
log_tail = ""
|
|
74
72
|
if log_path.is_file():
|
|
@@ -88,7 +86,9 @@ def project_context(project: Project, *, log_lines: int = 20) -> dict[str, Any]:
|
|
|
88
86
|
for name, sim in project.config.simulators.items()
|
|
89
87
|
},
|
|
90
88
|
"status_counts": counts,
|
|
91
|
-
"active_runs":
|
|
89
|
+
"active_runs": len(active),
|
|
90
|
+
# Every submitted / running run, briefly (no params), so one call shows them all.
|
|
91
|
+
"active": [m.to_summary(project.root, brief=True) for m in active],
|
|
92
92
|
"expired_scratch": expired,
|
|
93
93
|
"recent_runs": [m.to_summary(project.root) for m in recent],
|
|
94
94
|
"notes_files": note_files,
|
|
@@ -12,6 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import json
|
|
14
14
|
from datetime import datetime, timedelta
|
|
15
|
+
from itertools import pairwise
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import Any
|
|
17
18
|
|
|
@@ -24,6 +25,7 @@ from runops.util import parse_iso
|
|
|
24
25
|
|
|
25
26
|
SAMPLES_KEPT = 1000 # per attempt; older samples are dropped when the file doubles
|
|
26
27
|
RECENT_WINDOW = timedelta(minutes=10)
|
|
28
|
+
STALL_WINDOW = timedelta(hours=1) # no change across this much observed time -> stalled
|
|
27
29
|
_START_MARK = "runops: start "
|
|
28
30
|
|
|
29
31
|
|
|
@@ -106,11 +108,23 @@ def summarize(
|
|
|
106
108
|
reference = earlier[-1] if earlier else (points[0] if len(points) > 1 else origin)
|
|
107
109
|
rate_attempt = _rate(origin, points[-1])
|
|
108
110
|
rate_recent = _rate(reference, points[-1]) or rate_attempt
|
|
111
|
+
# When done last moved: the first sample, then every sample that exceeds the one before.
|
|
112
|
+
last_change_at = points[0][0]
|
|
113
|
+
for (_, prev_done), (at, now_done) in pairwise(points):
|
|
114
|
+
if now_done > prev_done:
|
|
115
|
+
last_change_at = at
|
|
116
|
+
stalled = last_at - last_change_at
|
|
109
117
|
summary: dict[str, Any] = {
|
|
110
118
|
"done": raw_done,
|
|
111
119
|
"total": total,
|
|
112
120
|
"unit": unit,
|
|
113
121
|
"at": last_at.isoformat(timespec="seconds"),
|
|
122
|
+
"last_change_at": last_change_at.isoformat(timespec="seconds"),
|
|
123
|
+
"stalled_hours": (
|
|
124
|
+
round(stalled.total_seconds() / 3600, 1)
|
|
125
|
+
if not final and stalled >= STALL_WINDOW
|
|
126
|
+
else None
|
|
127
|
+
),
|
|
114
128
|
"fraction": round(done / total, 4) if total else None,
|
|
115
129
|
"rate_recent_per_hour": _round(rate_recent),
|
|
116
130
|
"rate_attempt_per_hour": _round(rate_attempt),
|
|
@@ -157,6 +171,8 @@ def format_progress(progress: dict[str, Any] | None) -> str:
|
|
|
157
171
|
parts.append(f"{3600 / rate:.3g} s/{progress['unit']}")
|
|
158
172
|
if progress.get("eta_hours") is not None:
|
|
159
173
|
parts.append(f"eta {progress['eta_hours']:.1f}h")
|
|
174
|
+
if progress.get("stalled_hours") is not None:
|
|
175
|
+
parts.append(f"stalled {progress['stalled_hours']:.1f}h")
|
|
160
176
|
return " ".join(parts)
|
|
161
177
|
|
|
162
178
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: runops-analyze
|
|
3
|
-
description: Analyze or compare run results in a runops project. Use when plotting, investigating a series or sweep, writing an analysis report, or choosing where analysis scripts and outputs belong.
|
|
3
|
+
description: Analyze or compare run results in a runops project. Use when plotting, investigating a series or sweep, writing an analysis or comparison report for a reader, or choosing where analysis scripts and outputs belong.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Analyze run results
|
|
@@ -30,11 +30,16 @@ description: Analyze or compare run results in a runops project. Use when plotti
|
|
|
30
30
|
In the script use `#SBATCH -o logs/%x.%j.out` and `#SBATCH -e logs/%x.%j.err`,
|
|
31
31
|
the site's resource directives, and `srun` for the payload where required.
|
|
32
32
|
Keep the working directory at the investigation root so `scripts/` and `out/` resolve.
|
|
33
|
-
5.
|
|
33
|
+
5. When the result is a report someone will read (a comparison of runs, a dependence,
|
|
34
|
+
"what changed and why"), write it by [report-guide.md](report-guide.md): question and
|
|
35
|
+
answer first, original fields side by side on one scale, each figure explained
|
|
36
|
+
before and after, provenance and checks at the end, as `README.md` plus a
|
|
37
|
+
self-contained `report.html`. [example-report/](example-report/README.md) shows it.
|
|
38
|
+
6. Update the investigation README, then append the decision and its README link to
|
|
34
39
|
`LOG.md`. Put reusable formulas, methods and pitfalls in `notes/<topic>.md`.
|
|
35
|
-
|
|
40
|
+
7. `runo tidy --only analysis-logs` previews old job logs, retaining the newest pair per
|
|
36
41
|
job name in each logs directory. Apply with `runo tidy --apply --only analysis-logs`.
|
|
37
42
|
Archiving a whole group directory also moves its `_analysis/`; a partial archive
|
|
38
43
|
leaves shared analysis with the remaining runs. `restore RUN` restores only that run.
|
|
39
|
-
|
|
44
|
+
8. For an existing project's scattered files, show source → destination paths before
|
|
40
45
|
moving anything, then fix affected script paths and document links. Keep results.
|