runops 2.5.0__tar.gz → 2.6.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 (142) hide show
  1. {runops-2.5.0 → runops-2.6.0}/PKG-INFO +6 -1
  2. {runops-2.5.0 → runops-2.6.0}/README.md +5 -0
  3. {runops-2.5.0 → runops-2.6.0}/SPEC.md +47 -9
  4. {runops-2.5.0 → runops-2.6.0}/docs/guide.md +42 -3
  5. {runops-2.5.0 → runops-2.6.0}/docs/reference.md +15 -3
  6. {runops-2.5.0 → runops-2.6.0}/pyproject.toml +1 -1
  7. {runops-2.5.0 → runops-2.6.0}/src/runops/__init__.py +1 -1
  8. {runops-2.5.0 → runops-2.6.0}/src/runops/cli.py +13 -1
  9. {runops-2.5.0 → runops-2.6.0}/src/runops/harness.py +15 -3
  10. runops-2.6.0/src/runops/ops/analysis.py +113 -0
  11. {runops-2.5.0 → runops-2.6.0}/src/runops/ops/common.py +2 -0
  12. {runops-2.5.0 → runops-2.6.0}/src/runops/ops/create.py +1 -3
  13. {runops-2.5.0 → runops-2.6.0}/src/runops/ops/notes.py +2 -0
  14. {runops-2.5.0 → runops-2.6.0}/src/runops/ops/setup.py +3 -0
  15. {runops-2.5.0 → runops-2.6.0}/src/runops/ops/tidy.py +80 -3
  16. {runops-2.5.0 → runops-2.6.0}/src/runops/project.py +9 -3
  17. runops-2.6.0/src/runops/skills/runops-analyze/SKILL.md +40 -0
  18. {runops-2.5.0 → runops-2.6.0}/src/runops/skills/runops-reference/SKILL.md +4 -2
  19. {runops-2.5.0 → runops-2.6.0}/src/runops/skills/runops-tidy/SKILL.md +4 -0
  20. runops-2.6.0/tests/test_analysis_layout.py +311 -0
  21. {runops-2.5.0 → runops-2.6.0}/tests/test_skills.py +1 -0
  22. {runops-2.5.0 → runops-2.6.0}/uv.lock +1 -1
  23. {runops-2.5.0 → runops-2.6.0}/.agents/skills/check/SKILL.md +0 -0
  24. {runops-2.5.0 → runops-2.6.0}/.agents/skills/improve-harness/SKILL.md +0 -0
  25. {runops-2.5.0 → runops-2.6.0}/.agents/skills/python-package-refactor/SKILL.md +0 -0
  26. {runops-2.5.0 → runops-2.6.0}/.agents/skills/python-package-refactor/agents/openai.yaml +0 -0
  27. {runops-2.5.0 → runops-2.6.0}/.agents/skills/python-package-refactor/examples/refactor-request.md +0 -0
  28. {runops-2.5.0 → runops-2.6.0}/.agents/skills/python-package-refactor/references/anti-patterns.md +0 -0
  29. {runops-2.5.0 → runops-2.6.0}/.agents/skills/python-package-refactor/references/api-compatibility.md +0 -0
  30. {runops-2.5.0 → runops-2.6.0}/.agents/skills/python-package-refactor/references/packaging-modernization.md +0 -0
  31. {runops-2.5.0 → runops-2.6.0}/.agents/skills/python-package-refactor/references/refactor-playbook.md +0 -0
  32. {runops-2.5.0 → runops-2.6.0}/.agents/skills/python-package-refactor/references/verification.md +0 -0
  33. {runops-2.5.0 → runops-2.6.0}/.agents/skills/python-package-refactor/scripts/api_surface_snapshot.py +0 -0
  34. {runops-2.5.0 → runops-2.6.0}/.agents/skills/python-package-refactor/scripts/import_smoke.py +0 -0
  35. {runops-2.5.0 → runops-2.6.0}/.agents/skills/python-package-refactor/scripts/inspect_python_package.py +0 -0
  36. {runops-2.5.0 → runops-2.6.0}/.agents/skills/python-package-refactor/scripts/refactor_quality_gate.py +0 -0
  37. {runops-2.5.0 → runops-2.6.0}/.agents/skills/release/SKILL.md +0 -0
  38. {runops-2.5.0 → runops-2.6.0}/.agents/skills/simulator-adapter/SKILL.md +0 -0
  39. {runops-2.5.0 → runops-2.6.0}/.agents/skills/spec-reviewer/SKILL.md +0 -0
  40. {runops-2.5.0 → runops-2.6.0}/.agents/skills/sync-docs/SKILL.md +0 -0
  41. {runops-2.5.0 → runops-2.6.0}/.claude/agents/spec-reviewer.md +0 -0
  42. {runops-2.5.0 → runops-2.6.0}/.claude/rules/architecture.md +0 -0
  43. {runops-2.5.0 → runops-2.6.0}/.claude/rules/dev-workflow.md +0 -0
  44. {runops-2.5.0 → runops-2.6.0}/.claude/rules/gotchas.md +0 -0
  45. {runops-2.5.0 → runops-2.6.0}/.claude/settings.json +0 -0
  46. {runops-2.5.0 → runops-2.6.0}/.claude/skills/check/SKILL.md +0 -0
  47. {runops-2.5.0 → runops-2.6.0}/.claude/skills/improve-harness/SKILL.md +0 -0
  48. {runops-2.5.0 → runops-2.6.0}/.claude/skills/python-package-refactor/SKILL.md +0 -0
  49. {runops-2.5.0 → runops-2.6.0}/.claude/skills/release/SKILL.md +0 -0
  50. {runops-2.5.0 → runops-2.6.0}/.claude/skills/simulator-adapter/SKILL.md +0 -0
  51. {runops-2.5.0 → runops-2.6.0}/.claude/skills/spec-reviewer/SKILL.md +0 -0
  52. {runops-2.5.0 → runops-2.6.0}/.claude/skills/sync-docs/SKILL.md +0 -0
  53. {runops-2.5.0 → runops-2.6.0}/.codex/README.md +0 -0
  54. {runops-2.5.0 → runops-2.6.0}/.codex/agents/spec-reviewer.toml +0 -0
  55. {runops-2.5.0 → runops-2.6.0}/.codex/config.toml +0 -0
  56. {runops-2.5.0 → runops-2.6.0}/.codex/rules/runops.rules +0 -0
  57. {runops-2.5.0 → runops-2.6.0}/.dockerignore +0 -0
  58. {runops-2.5.0 → runops-2.6.0}/.gitattributes +0 -0
  59. {runops-2.5.0 → runops-2.6.0}/.github/assets/conversation-dark.svg +0 -0
  60. {runops-2.5.0 → runops-2.6.0}/.github/assets/conversation-light.svg +0 -0
  61. {runops-2.5.0 → runops-2.6.0}/.github/assets/hero-dark.svg +0 -0
  62. {runops-2.5.0 → runops-2.6.0}/.github/assets/hero-light.svg +0 -0
  63. {runops-2.5.0 → runops-2.6.0}/.github/assets/make_readme_art.py +0 -0
  64. {runops-2.5.0 → runops-2.6.0}/.github/workflows/ci.yml +0 -0
  65. {runops-2.5.0 → runops-2.6.0}/.github/workflows/publish.yml +0 -0
  66. {runops-2.5.0 → runops-2.6.0}/.gitignore +0 -0
  67. {runops-2.5.0 → runops-2.6.0}/.harness/manifest.toml +0 -0
  68. {runops-2.5.0 → runops-2.6.0}/.pre-commit-config.yaml +0 -0
  69. {runops-2.5.0 → runops-2.6.0}/.vscode/settings.json +0 -0
  70. {runops-2.5.0 → runops-2.6.0}/AGENTS.md +0 -0
  71. {runops-2.5.0 → runops-2.6.0}/CLAUDE.md +0 -0
  72. {runops-2.5.0 → runops-2.6.0}/LICENSE +0 -0
  73. {runops-2.5.0 → runops-2.6.0}/docs/adapter.md +0 -0
  74. {runops-2.5.0 → runops-2.6.0}/src/runops/adapters/__init__.py +0 -0
  75. {runops-2.5.0 → runops-2.6.0}/src/runops/adapters/base.py +0 -0
  76. {runops-2.5.0 → runops-2.6.0}/src/runops/adapters/generic.py +0 -0
  77. {runops-2.5.0 → runops-2.6.0}/src/runops/adapters/registry.py +0 -0
  78. {runops-2.5.0 → runops-2.6.0}/src/runops/case.py +0 -0
  79. {runops-2.5.0 → runops-2.6.0}/src/runops/config.py +0 -0
  80. {runops-2.5.0 → runops-2.6.0}/src/runops/errors.py +0 -0
  81. {runops-2.5.0 → runops-2.6.0}/src/runops/jobgen.py +0 -0
  82. {runops-2.5.0 → runops-2.6.0}/src/runops/launchers.py +0 -0
  83. {runops-2.5.0 → runops-2.6.0}/src/runops/legacy_adapters/__init__.py +0 -0
  84. {runops-2.5.0 → runops-2.6.0}/src/runops/legacy_adapters/beach.py +0 -0
  85. {runops-2.5.0 → runops-2.6.0}/src/runops/legacy_adapters/emses.py +0 -0
  86. {runops-2.5.0 → runops-2.6.0}/src/runops/manifest.py +0 -0
  87. {runops-2.5.0 → runops-2.6.0}/src/runops/ops/__init__.py +0 -0
  88. {runops-2.5.0 → runops-2.6.0}/src/runops/ops/listing.py +0 -0
  89. {runops-2.5.0 → runops-2.6.0}/src/runops/ops/migrate.py +0 -0
  90. {runops-2.5.0 → runops-2.6.0}/src/runops/ops/progress.py +0 -0
  91. {runops-2.5.0 → runops-2.6.0}/src/runops/ops/skills.py +0 -0
  92. {runops-2.5.0 → runops-2.6.0}/src/runops/ops/submit.py +0 -0
  93. {runops-2.5.0 → runops-2.6.0}/src/runops/ops/sync.py +0 -0
  94. {runops-2.5.0 → runops-2.6.0}/src/runops/runtime.py +0 -0
  95. {runops-2.5.0 → runops-2.6.0}/src/runops/skills/runops-check-status/SKILL.md +0 -0
  96. {runops-2.5.0 → runops-2.6.0}/src/runops/skills/runops-create-run/SKILL.md +0 -0
  97. {runops-2.5.0 → runops-2.6.0}/src/runops/skills/runops-maintain/SKILL.md +0 -0
  98. {runops-2.5.0 → runops-2.6.0}/src/runops/slurm.py +0 -0
  99. {runops-2.5.0 → runops-2.6.0}/src/runops/util.py +0 -0
  100. {runops-2.5.0 → runops-2.6.0}/tests/__init__.py +0 -0
  101. {runops-2.5.0 → runops-2.6.0}/tests/conftest.py +0 -0
  102. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/.codex/rules/a.md +0 -0
  103. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/.runops/test-runs/T20260701-0001/receipt.toml +0 -0
  104. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/.runops/work/beach-x-abc/bin/beach +0 -0
  105. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/AGENTS.md +0 -0
  106. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/CLAUDE.md +0 -0
  107. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/cases/beach/plane/beach.toml +0 -0
  108. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/cases/beach/plane/case.toml +0 -0
  109. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/cases/old/beach.toml +0 -0
  110. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/cases/old/case.toml +0 -0
  111. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/launchers.toml +0 -0
  112. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/research/CURRENT.md +0 -0
  113. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/runops.toml +0 -0
  114. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/runs/AGENTS.md +0 -0
  115. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/runs/_archive/plane/R20260601-0001/manifest.toml +0 -0
  116. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/runs/plane/R20260701-0001--a/input/beach.toml +0 -0
  117. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/runs/plane/R20260701-0001--a/manifest.toml +0 -0
  118. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/runs/plane/R20260702-0001/manifest.toml +0 -0
  119. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/runs/scan/R20260701-0002/manifest.toml +0 -0
  120. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/runs/scan/survey.toml +0 -0
  121. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/simulators.toml +0 -0
  122. {runops-2.5.0 → runops-2.6.0}/tests/fixtures/v1-project/site.toml +0 -0
  123. {runops-2.5.0 → runops-2.6.0}/tests/golden/mpi_standard.sh +0 -0
  124. {runops-2.5.0 → runops-2.6.0}/tests/golden/openmp_rsc.sh +0 -0
  125. {runops-2.5.0 → runops-2.6.0}/tests/test_adapters.py +0 -0
  126. {runops-2.5.0 → runops-2.6.0}/tests/test_archive_series.py +0 -0
  127. {runops-2.5.0 → runops-2.6.0}/tests/test_build.py +0 -0
  128. {runops-2.5.0 → runops-2.6.0}/tests/test_dev_harness.py +0 -0
  129. {runops-2.5.0 → runops-2.6.0}/tests/test_jobgen.py +0 -0
  130. {runops-2.5.0 → runops-2.6.0}/tests/test_legacy_adapters.py +0 -0
  131. {runops-2.5.0 → runops-2.6.0}/tests/test_lifecycle.py +0 -0
  132. {runops-2.5.0 → runops-2.6.0}/tests/test_manifest_config.py +0 -0
  133. {runops-2.5.0 → runops-2.6.0}/tests/test_migrate.py +0 -0
  134. {runops-2.5.0 → runops-2.6.0}/tests/test_mv_naming.py +0 -0
  135. {runops-2.5.0 → runops-2.6.0}/tests/test_progress.py +0 -0
  136. {runops-2.5.0 → runops-2.6.0}/tests/test_project_scope.py +0 -0
  137. {runops-2.5.0 → runops-2.6.0}/tests/test_run_job.py +0 -0
  138. {runops-2.5.0 → runops-2.6.0}/tests/test_safety.py +0 -0
  139. {runops-2.5.0 → runops-2.6.0}/tests/test_scratch_tidy.py +0 -0
  140. {runops-2.5.0 → runops-2.6.0}/tests/test_setup_misc.py +0 -0
  141. {runops-2.5.0 → runops-2.6.0}/tests/test_sweep.py +0 -0
  142. {runops-2.5.0 → runops-2.6.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.5.0
3
+ Version: 2.6.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
@@ -76,6 +76,7 @@ runo init
76
76
  | 資源を変えて作り直す | R20261015-0003 を 256 並列・72 時間で作り直して(case はそのまま) |
77
77
  | 片付ける | 古い run を整理して。消すものと archive するものを先に見せて |
78
78
  | 作り直した旧系列を片付ける | 旧系列は新系列で置き換えたので、対応を記録して archive して |
79
+ | 解析する | この系列の速度を比較して。図とスクリプト、調査メモを系列の `_analysis/` にまとめて |
79
80
  | simulator を足す | パッチを当てた ~/BEACH-dev をビルドして、別の simulator として登録して |
80
81
  | 更新する | runops と simulator を最新にして |
81
82
  | v1 から移る | この project を runops v2 に移行して(`runo migrate-v1` を dry-run で確認してから) |
@@ -100,15 +101,19 @@ lunar-sheath/
100
101
  │ │ │ ├── work/ Slurm のログと出力
101
102
  │ │ │ └── analysis/
102
103
  │ │ └── wc-phiz-scan/ sweep はまとめて 1 階層
104
+ │ │ └── _analysis/ 系列・sweep の比較調査(topic ごと)
103
105
  │ ├── _scratch/ smoke / debug(期限付き)
104
106
  │ └── _archive/ archive した run
105
107
  ├── LOG.md run をまたぐ結論と次の判断
106
108
  ├── notes/ 再利用する知見
109
+ ├── analysis/ 複数系列にまたがる調査。共通コードは lib/
107
110
  └── AGENTS.md, .agents/skills/, .claude/skills/
108
111
  ```
109
112
 
110
113
  run は ID が変わらないので、directory を移動してもエージェントは見つけられます。
111
114
  `work/` は Git の対象外で、`manifest.toml`、`input/`、`job.sh` は Git で追えます。
115
+ 解析は依存する run の範囲で置き場所を選び、調査ごとに `README.md`、`scripts/`、`out/`、
116
+ `logs/` をまとめます。判断は `LOG.md` からリンクし、古い解析 job の log は整理候補になります。
112
117
 
113
118
  ## 対応 simulator
114
119
 
@@ -45,6 +45,7 @@ runo init
45
45
  | 資源を変えて作り直す | R20261015-0003 を 256 並列・72 時間で作り直して(case はそのまま) |
46
46
  | 片付ける | 古い run を整理して。消すものと archive するものを先に見せて |
47
47
  | 作り直した旧系列を片付ける | 旧系列は新系列で置き換えたので、対応を記録して archive して |
48
+ | 解析する | この系列の速度を比較して。図とスクリプト、調査メモを系列の `_analysis/` にまとめて |
48
49
  | simulator を足す | パッチを当てた ~/BEACH-dev をビルドして、別の simulator として登録して |
49
50
  | 更新する | runops と simulator を最新にして |
50
51
  | v1 から移る | この project を runops v2 に移行して(`runo migrate-v1` を dry-run で確認してから) |
@@ -69,15 +70,19 @@ lunar-sheath/
69
70
  │ │ │ ├── work/ Slurm のログと出力
70
71
  │ │ │ └── analysis/
71
72
  │ │ └── wc-phiz-scan/ sweep はまとめて 1 階層
73
+ │ │ └── _analysis/ 系列・sweep の比較調査(topic ごと)
72
74
  │ ├── _scratch/ smoke / debug(期限付き)
73
75
  │ └── _archive/ archive した run
74
76
  ├── LOG.md run をまたぐ結論と次の判断
75
77
  ├── notes/ 再利用する知見
78
+ ├── analysis/ 複数系列にまたがる調査。共通コードは lib/
76
79
  └── AGENTS.md, .agents/skills/, .claude/skills/
77
80
  ```
78
81
 
79
82
  run は ID が変わらないので、directory を移動してもエージェントは見つけられます。
80
83
  `work/` は Git の対象外で、`manifest.toml`、`input/`、`job.sh` は Git で追えます。
84
+ 解析は依存する run の範囲で置き場所を選び、調査ごとに `README.md`、`scripts/`、`out/`、
85
+ `logs/` をまとめます。判断は `LOG.md` からリンクし、古い解析 job の log は整理候補になります。
81
86
 
82
87
  ## 対応 simulator
83
88
 
@@ -119,12 +119,14 @@ v1 で運用した 2 つの project(`project_dust_release`: BEACH、run 120、
119
119
  <run_id>--<label>/ # 単発 run
120
120
  <sweep_label>/
121
121
  <run_id>--<label>/ # sweep で生成された run
122
+ _analysis/<topic>/ # この group に依存する調査(§9.1)
122
123
  _scratch/
123
124
  <run_id>--<label>/ # scratch run
124
125
  _archive/
125
126
  <case_name>/... # archive された run(元の相対 path を保つ)
126
127
  adapters/ # 任意。project ローカル adapter `<name>.py`(§8.1)
127
128
  notes/ # 永続的な知見。1 topic 1 file。runops は管理しない(§9)
129
+ analysis/<topic>/ # 複数 group にまたがる調査。共通コードは analysis/lib/(§9.1)
128
130
  LOG.md # project 全体の追記専用ログ(§9)
129
131
  AGENTS.md # agent 向け入口(§10)
130
132
  CLAUDE.md # `@AGENTS.md` のみ
@@ -147,7 +149,8 @@ v1 で運用した 2 つの project(`project_dust_release`: BEACH、run 120、
147
149
  `runs/` 配下の任意の場所に移動してよい。runops は manifest の `run.id` で run を
148
150
  見つける(§7.2)。
149
151
  - 既定の配置先は `runs/<case_name>/`。`--dest` で上書きできる。`_` で始まる
150
- directory(`_scratch`、`_archive`)は runops が予約する。
152
+ directory(`_scratch`、`_archive`、`_analysis`)は runops が予約する。
153
+ `_analysis` は group の調査用で、run の発見・作成先から除く。
151
154
  - run directory の中(自身と子孫)には run を置けない。`--dest` が既存 run の中なら拒否し、
152
155
  `delete` は中に別の run があれば拒否する(§7.2 の走査は manifest のある directory で止まる)。
153
156
  - run id は `R<YYYYMMDD>-<NNNN>`。その日の既存 run と `.runops/last_run_id` の大きい方に 1 を足す。
@@ -160,7 +163,8 @@ v1 で運用した 2 つの project(`project_dust_release`: BEACH、run 120、
160
163
  - `materials/`、`refs/`、`scripts/` などユーザーが置く directory は runops の関知外。
161
164
  runops は `runs/`、`cases/`、`.runops/`、`.agents/skills/`、`.claude/skills/`、
162
165
  `AGENTS.md`、`CLAUDE.md`、`runops.toml` 以外を作成・変更しない(`LOG.md`、`notes/`
163
- は `init` の雛形生成のみ)。
166
+ は `init` の雛形生成のみ)。例外として `tidy --apply --only analysis-logs` は
167
+ §7.5 の解析 log を削除できる。解析のスクリプト・結果・README は agent が書く。
164
168
 
165
169
  ---
166
170
 
@@ -503,7 +507,7 @@ adapter が `progress(run_dir)` を持つとき、`sync` は running の run と
503
507
  - `runs/` 配下(`_archive/` を含む)を再帰的に走査し `manifest.toml` を集める。
504
508
  `.runops/index.toml` に `run_id -> run_dir` の cache を持ち、manifest が cache の
505
509
  path に無ければ再走査して cache を更新する。cache は常に再生成可能で、
506
- 壊れていても動作に影響しない。
510
+ 壊れていても動作に影響しない。`_analysis/` は走査せず、そこにある manifest は run と扱わない。
507
511
  - 走査対象は `runs/` のみ。project 外や `runs/` 外の run は扱わない。
508
512
 
509
513
  ### 7.3 command 一覧
@@ -514,7 +518,7 @@ adapter が `progress(run_dir)` を持つとき、`sync` は running の run と
514
518
  | `runo doctor [--json]` | `runops.toml` の妥当性、adapter の import 可否、executable の存在、`sbatch` の有無、skill の鮮度を検査 |
515
519
  | `runo update [--skills-only] [-y]` | adapter package を upgrade し、skill を再展開する |
516
520
  | `runo build SIM [--force]` | `local_source` simulator を `.runops/build/` に build する(§4.1) |
517
- | `runo context [--json]` | agent 向け現在地。project 名、simulator、active run 数、直近 run 5 件、scratch 期限切れ数、`LOG.md` 末尾 20 行。配置した runops skill が install 済みの runops より古ければ `runo update --skills-only` を促す warning |
521
+ | `runo context [--json]` | agent 向け現在地。project 名、simulator、active run 数、直近 run 5 件、scratch 期限切れ数、`LOG.md` 末尾 20 行、現用 group の解析 topic 名(§9.1)。配置した runops skill が install 済みの runops より古ければ `runo update --skills-only` を促す warning |
518
522
  | `runo case new NAME --simulator SIM [--from CASE]` | case をスキャフォールド。adapter の `case_template()` を展開 |
519
523
  | `runo case list` | case 一覧 |
520
524
  | `runo new CASE [--why TEXT] [--label L] [--set k=v]... [--job k=v]... [--tag T]... [--scratch] [--dest DIR] [--submit] [--created-by WHO] [--json]` | 単発 run を作る |
@@ -552,7 +556,7 @@ adapter が `progress(run_dir)` を持つとき、`sync` は running の run と
552
556
 
553
557
  ### 7.5 `tidy` の規則
554
558
 
555
- `tidy` は候補を 4 種に分けて表示する。`--apply` はすべてを実行し、
559
+ `tidy` は候補を 6 種に分けて表示する。`--apply` はすべてを実行し、
556
560
  `--apply --only KIND` で絞れる。
557
561
 
558
562
  | kind | 条件 | 動作 |
@@ -562,8 +566,14 @@ adapter が `progress(run_dir)` を持つとき、`sync` は running の run と
562
566
  | `old-completed` | `completed` で `defaults.tidy_archive_days`(既定 90)日より古く、`pinned = false` | archive |
563
567
  | `orphan-job` | `submitted`/`running` だが squeue/sacct に job が無い | sync(state 確定) |
564
568
  | `unused-build` | `.runops/build/*` のうち、非 archived run の manifest から参照されないもの | delete |
569
+ | `analysis-logs` | §9.1 の解析 directory 配下の `logs/` にある `*.<jobid>.out/err`。組の最終更新が `tidy_failed_days` より古く、同じ log directory・job 名の最新の組ではない | log file のみ delete |
565
570
 
566
571
  - `--older-than AGE`(例 `14d`)はその回だけ、上の 2 つの日数をどちらもこの値で置き換える。
572
+ - `analysis-logs` は project の `analysis/`、group の `_analysis/`、run の `analysis/`
573
+ (topic の下も含む)を対象にする。stdout/stderr は同じ job id の組として更新時刻を比較し、
574
+ 最新の組は常に残す。同時刻なら job id が大きい組を最新とする。片方だけの log も同じ扱い。
575
+ archived / pinned / submitted / running run の解析と、symlink は対象外。
576
+ 適用直前にも対象範囲・更新時刻・最新組を再確認し、条件が変われば削除せず止まる。
567
577
  - `--purge-work RUN...` は指定 run の `work/` の中身を削除し `work_purged_at` を記録する。
568
578
  adapter が `keep_patterns()` を返す場合、一致するファイルは残す。`completed` 以外は拒否。
569
579
  adapter を読み込めない(残すファイルを判定できない)ときは何も消さずにエラーにする。
@@ -710,7 +720,7 @@ runops は `init` と `update` で次を行う。
710
720
 
711
721
  ### 8.3 runops 自身の skill
712
722
 
713
- runops は次の 5 skill だけを同梱し、simulator skill と同じ仕組みで展開する。
723
+ runops は次の 6 skill だけを同梱し、simulator skill と同じ仕組みで展開する。
714
724
  利用者が覚えるコマンドは `runo init` だけとし、それ以外(simulator の追加、更新、v1 移行を含む)は
715
725
  エージェントへの依頼で行う。
716
726
  各 SKILL.md は 60 行以内に収める。
@@ -720,6 +730,7 @@ runops は次の 5 skill だけを同梱し、simulator skill と同じ仕組み
720
730
  | `runops-create-run` | `new` / `sweep` の使い方。`--why` を必ず書く、scratch の使い分け |
721
731
  | `runops-check-status` | `sync` → `list` → `log` の順。失敗時の `retry` と `tidy --purge-work` |
722
732
  | `runops-tidy` | `tidy` を定期的に実行する判断基準。pin の使い方 |
733
+ | `runops-analyze` | 依存する run の範囲で調査の置き場所を決め、script・結果・log・README をまとめる |
723
734
  | `runops-reference` | command 一覧(§7.3 の表をそのまま載せる) |
724
735
  | `runops-maintain` | simulator の追加、runops と simulator package の更新、`migrate-v1`、`doctor` の読み方 |
725
736
 
@@ -748,7 +759,7 @@ v1 の `emses` / `beach` adapter は次の順で runops 本体から出す。
748
759
  見つかると使われなくなる(解決順 1 が優先)ので、ユーザーが消せばよい。
749
760
  ---
750
761
 
751
- ## 9. ノート
762
+ ## 9. ノートと解析
752
763
 
753
764
  置き場所は 3 つに固定し、runops はどれも「管理」しない(schema、budget、rotation、
754
765
  seal、index を持たない)。
@@ -766,10 +777,33 @@ seal、index を持たない)。
766
777
  - 複数 run にまたがる結論と次の判断は `LOG.md` に日付付きで追記する
767
778
  - 再利用する式・手法・環境の落とし穴は `notes/<topic>.md` に書く。既存 file があれば追記し、
768
779
  似た topic の file を増やさない
769
- - それ以外の場所(`research/`、`analysis/*.md`、`runs/` 直下など)に note file を作らない
780
+ - 調査の詳細は §9.1 の README にまとめ、LOG.md からリンクする
770
781
  - v1 の `research/results/`、`.runops/insights/`、`research/archive/legacy/` は移行時に
771
782
  `notes/legacy/` 配下へ **移動せず** そのまま残し、`notes/README.md` から path を案内する(§11)。
772
783
 
784
+ ### 9.1 解析の置き場所
785
+
786
+ | 結果が依存する範囲 | 置き場所 |
787
+ |---|---|
788
+ | 1 run | `runs/.../<RUN>/analysis/`(必要なら `<topic>/`) |
789
+ | 1 group(同じ directory に並ぶ系列・sweep) | `runs/<group>/_analysis/<topic>/` |
790
+ | 複数 group・case または project 全体 | `analysis/<topic>/` |
791
+ | 共通コード | `analysis/lib/` |
792
+
793
+ - group は directory であり、新しい entity / ID / ledger を持たない。
794
+ - 調査 directory は `README.md`(問い・対象 run id・方法・結果・制約)、`scripts/`
795
+ (Python、sbatch)、`out/`(図・JSON・CSV)、`logs/`(job の stdout/stderr)からなる。
796
+ topic は内容で名付け、同じ問いは README に日付付きで追記する。
797
+ - `init` の `.gitignore` は `**/logs/`、`__pycache__/`、`.runops/analysis-env/` を含む。
798
+ 既存 project のファイルは自動で再配置しない。
799
+ - `context` は現用 group の `_analysis/*/README.md` がある topic 名を表示し、
800
+ `--json` では `group_analysis` に project 相対の group path → topic 名一覧を返す。
801
+ README 本文や `out/`、simulator の `work/` は読まない。
802
+ - `archive DIR` が group 配下の全 run を移す場合、その group と配下の group の
803
+ `_analysis/` も同じ相対 path の `runs/_archive/` に移す。run が残る group の解析は残す。
804
+ 確認表示に解析の移動先を含め、移動前に run と解析の全移動先を検査し、既存なら上書きしない。
805
+ `archive RUN...` / `--older-than` は group の解析を動かさず、`restore RUN` は run のみを戻す。
806
+
773
807
  ---
774
808
 
775
809
  ## 10. harness 生成
@@ -916,7 +950,7 @@ src/runops/
916
950
  | 項目 | 目標 |
917
951
  |---|---|
918
952
  | `src/runops/` の行数(skill の md を除く) | 8,000 行以下 |
919
- | CLI command 数(`case new` / `case list` を含む。同じ command の option 違いは数えない) | 25 以下。§7.3 の表の distinct command は 24 |
953
+ | CLI command 数(`case new` / `case list` を含む。同じ command の option 違いは数えない) | 25 以下。§7.3 の表の distinct command は 25 |
920
954
  | ユーザーが編集する TOML | `runops.toml`、`case.toml` の 2 種 |
921
955
  | runops が書く TOML | `manifest.toml`、`.runops/skills.lock`、`.runops/index.toml` の 3 種 |
922
956
  | 依存 | `typer`、`rich`、`tomli-w`、`tomlkit`、`jinja2`、`packaging`(`mcp` を外す) |
@@ -954,6 +988,9 @@ src/runops/
954
988
  9b. 同名 adapter が entry point と `adapters/X.py` の両方にあるとき entry point が優先される。
955
989
  どちらも無く `generic` でもなければ exit code 2 で案内が出る。
956
990
  10. `manifest.toml` の書き込み途中で例外が起きても元のファイルが壊れない。
991
+ 11. 解析 log の tidy は最新組・新しい組・対象外ファイルを残し、確認後の更新も削除しない。
992
+ group の archive は全 run が対象のときだけ解析も移し、衝突時は何も動かさない。
993
+ context は調査の名前だけを読み、解析内の manifest を run と誤認しない。
957
994
 
958
995
  ### 13.3 ドキュメント
959
996
 
@@ -974,6 +1011,7 @@ v2 の文書は次の 4 ファイルだけにする。
974
1011
 
975
1012
  | 項目 | 決定 | 理由 |
976
1013
  |---|---|---|
1014
+ | 解析の配置 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 は足さない。既存ファイルの整理は移動一覧を提示してから行う |
977
1015
  | job の cwd | run_dir(v1 と同じ)。Slurm log は `work/` | v1 adapter の path 規約と実 project の job.sh をそのまま使える |
978
1016
  | adapter の追加 hook | `configure(sim_config)`、`job_prelude(run_dir)` | generic の `command` 取得と、EMSES の `rm -f *_0000.h5` など v1 の setup command を表す |
979
1017
  | launcher `kind = "none"` | MPI launcher を使わず program を直接実行 | OpenMP のみの BEACH run 用 |
@@ -192,7 +192,7 @@ project の run だけが対象です。同じアカウントの別 project の
192
192
  前の job が終わるのを待って投入したいときは、「R20261016-0005 が成功したら投げて」と頼めば
193
193
  `runo submit RUN --afterok <job_id>` になります。
194
194
 
195
- ## 6. 記録を残す
195
+ ## 6. 解析して記録を残す
196
196
 
197
197
  記録の置き場所は 3 つに決まっています。
198
198
 
@@ -202,10 +202,40 @@ project の run だけが対象です。同じアカウントの別 project の
202
202
  | 複数の run をまたぐ結論と次の判断 | `LOG.md` に日付見出しで追記 | 今日の scan の結論を LOG にまとめて |
203
203
  | 再利用する式、手法、落とし穴 | `notes/<topic>.md`(1 topic 1 file) | Debye 長の見積もり方を notes に残して |
204
204
 
205
- エージェントはこれ以外の場所(`research/`、`analysis/*.md`、`runs/` の下など)にノートを作りません。
206
205
  `notes/` に似た topic の file があれば、新しく作らずに追記します。
207
206
 
208
- `runo context` は `LOG.md` の末尾と `notes/` の file 名を表示します。エージェントは作業の始めに
207
+ 調査の詳細と図・スクリプトは、結果が依存する run の範囲に合わせてまとめます。
208
+
209
+ > この系列の速度を比較して。図とスクリプト、調査メモを系列の `_analysis/` にまとめて。
210
+
211
+ | 依存する範囲 | 置き場所 |
212
+ |---|---|
213
+ | 1 run | `<run_dir>/analysis/`(必要なら topic を切る) |
214
+ | 1 系列・sweep | `runs/<group>/_analysis/<topic>/` |
215
+ | 複数系列・case、project 全体 | `analysis/<topic>/` |
216
+ | 共通コードとそのテスト | `analysis/lib/` |
217
+
218
+ group は run が並ぶ directory です。登録や新しい ID は要りません。
219
+ topic は `speed-by-version` のように問いの内容で名付け、同じ問いをやり直すときは
220
+ 同じ directory の結果を更新し、README に日付付きで追記します。
221
+
222
+ ```text
223
+ <topic>/
224
+ README.md 問い・対象 run id と path・方法・結果・制約
225
+ scripts/ Python と sbatch
226
+ out/ 図、JSON、CSV、HTML
227
+ logs/ 解析 job の stdout/stderr(Git 管理外)
228
+ ```
229
+
230
+ エージェントは `runops-analyze` skill を使い、調査の README と、そこへのリンクを含む
231
+ `LOG.md` の判断を更新します。`notes/` は run に依存しない知見に使います。
232
+ 解析の追加依存は `.runops/analysis-env/` に置き、重い読み込みや作図はサイトの規則に従って
233
+ 計算ノードで実行します。sbatch は調査 directory から投入し、事前に `logs/` を作って
234
+ `#SBATCH -o logs/%x.%j.out` と `#SBATCH -e logs/%x.%j.err` を指定します。
235
+ 既存 project のファイルは自動で移動しません。整理を頼むと、移動前に一覧を提示します。
236
+
237
+ `runo context` は `LOG.md` の末尾、`notes/` の file 名、現用 group の
238
+ `_analysis/*/README.md` がある topic 名を表示します。エージェントは作業の始めに
209
239
  これを読むので、前回の続きから話を始められます。
210
240
 
211
241
  ## 7. 整理する
@@ -221,6 +251,7 @@ run は消さなくても `runo list` が古いものを隠しますが、direct
221
251
  | `expired-scratch` | 期限切れの scratch run(archive したものは除く) | 削除 |
222
252
  | `stale-failed` | 30 日より古い failed / cancelled で、`why` もノートも無い | 削除 |
223
253
  | `old-completed` | 90 日より古い completed | archive |
254
+ | `analysis-logs` | 30 日より古い解析 job の log(各 job 名の最新組は保持) | log のみ削除 |
224
255
  | `orphan-job` | 投入済みなのに Slurm が知らない | `sync` で状態を確定 |
225
256
  | `unused-build` | どの run も使っていない build cache | 削除 |
226
257
 
@@ -239,6 +270,14 @@ run id で引き続き見つかり、`runo restore` で戻せます。
239
270
  記録され、理由はノートに残ります。比較用に派生しただけの run は置き換えとは扱わないので、
240
271
  旧系列が不要かどうかは毎回確かめます。
241
272
 
273
+ directory 指定で group の全 run を archive すると、その `_analysis/` も同じ相対 path で
274
+ 移動します。run が残る group の解析は元の場所に残り、移動先に既存の解析があれば、run を
275
+ 動かす前に停止します。`archive RUN...` と `restore RUN` は run だけを移動します。
276
+
277
+ 解析 log だけの整理は「古い解析 log を整理して」で `runo tidy --only analysis-logs` に
278
+ なります。各 `logs/` の `*.<jobid>.out/err` を組で比較し、job 名ごとに最新の組を残します。
279
+ 片方が新しければ両方とも残し、README、script、結果は削除しません。
280
+
242
281
  論文の図に使う run や基準にする run は pin しておくと、`list` に常に表示され、`tidy` や `delete`
243
282
  の対象になりません。
244
283
 
@@ -6,7 +6,7 @@
6
6
  ## runops.toml
7
7
 
8
8
  project root にあり、`runo` はここを上方向に探して project を特定します。
9
- command はこの project の `runs/` 配下だけを扱い、別 project の run directory や
9
+ run を対象にする command はこの project の `runs/` 配下だけを扱い、別 project の run directory や
10
10
  directory を path で渡すとエラーになります。run id は project ごとなので、同じ id が
11
11
  別 project にあっても cwd の project の run を指します。
12
12
 
@@ -153,7 +153,7 @@ run id は削除後も再利用しません。`--dest` に既存 run の中は
153
153
  | `runo doctor [--json]` | 設定、adapter の解決経路、executable、sbatch、skill の鮮度とリンク切れを点検 |
154
154
  | `runo update [--skills-only] [-y]` | simulator package を更新し skill と AGENTS.md の管理 block を再生成 |
155
155
  | `runo build SIM [--force]` | `local_source` の simulator を build |
156
- | `runo context [--json]` | agent 向けの現在地 |
156
+ | `runo context [--json]` | agent 向けの現在地と現用 group の解析 topic 名(JSON の `group_analysis` は group path → topic 名一覧) |
157
157
  | `runo case new NAME [-s SIM] [--from CASE]` / `runo case list` | case の作成と一覧 |
158
158
  | `runo new CASE [--why] [--label] [--set k=v]... [--job k=v]... [--tag] [--scratch] [--dest DIR] [--submit] [--json]` | run を 1 つ作る。`--job` はこの run の `[job]` の値 |
159
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 対応が必要 |
@@ -173,7 +173,11 @@ run id は削除後も再利用しません。`--dest` に既存 run の中は
173
173
  `list` は既定で active、created、直近 `list_since_days` 日、pinned の run を表示し、
174
174
  scratch と archived は表示しません。
175
175
 
176
- `tidy` の候補は次の 5 種類です。
176
+ `archive DIR` が group 配下の全 run を移すときは、group の `_analysis/` も同じ相対 path で
177
+ 移動します。run が残る group の解析は残し、移動先が存在すれば全 run の移動前に拒否します。
178
+ `archive RUN...` / `--older-than` と `restore RUN` は group の解析を移動しません。
179
+
180
+ `tidy` の候補は次の 6 種類です。
177
181
 
178
182
  | kind | 条件 | 動作 |
179
183
  |---|---|---|
@@ -182,9 +186,17 @@ scratch と archived は表示しません。
182
186
  | `old-completed` | `tidy_archive_days`(既定 90)日より古い completed | archive |
183
187
  | `orphan-job` | Slurm が知らない submitted / running | sync |
184
188
  | `unused-build` | 非 archived run が参照しない `.runops/build/*` | delete |
189
+ | `analysis-logs` | `tidy_failed_days` より古い解析 log。各 log directory・job 名の最新組は保持 | log のみ delete |
185
190
 
186
191
  日数は `runops.toml` の `[defaults]` で変えられます。`--older-than 14d` のように指定すると、
187
192
  その回だけ両方の日数をその値で置き換えます。pinned の run は `tidy` と `delete` の対象になりません。
188
193
  `--purge-work` は adapter を読み込めないと何も消さずにエラーにします。
189
194
 
195
+ `analysis-logs` は project の `analysis/`、group の `_analysis/`、run の `analysis/` 配下の
196
+ `logs/*.<jobid>.out/err`(topic の下も含む)に限定します。組の新しい方の更新時刻で年齢を判定し、
197
+ 最新組を残します。同時刻なら job id が大きい組を最新とします。片方だけの log も同じ扱いです。
198
+ archived / pinned / submitted / running run の解析と symlink は対象外で、適用直前にも条件を
199
+ 確認します。`--json` の解析 log 候補は `files`(削除する project 相対 path)と `cutoff`
200
+ (ISO 日時)も持ちます。確認後に条件が変われば削除せず、再度 tidy を実行するよう案内します。
201
+
190
202
  exit code は 0 が成功、1 が実行時エラー(sbatch 失敗など)、2 が使い方や設定のエラーです。
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "runops"
7
- version = "2.5.0"
7
+ version = "2.6.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.5.0"
3
+ __version__ = "2.6.0"
@@ -263,6 +263,8 @@ def context(as_json: bool = typer.Option(False, "--json")) -> None:
263
263
  )
264
264
  if data["notes_files"]:
265
265
  out.print("notes/: " + ", ".join(data["notes_files"]))
266
+ for group, topics in data["group_analysis"].items():
267
+ out.print(f"{group}/_analysis/: {', '.join(topics)}", markup=False)
266
268
  if data["log_tail"]:
267
269
  out.print("--- LOG.md (tail) ---")
268
270
  out.print(data["log_tail"], markup=False)
@@ -869,12 +871,22 @@ def archive(
869
871
  if m.id in plan.superseded_by:
870
872
  line += f" -> {', '.join(plan.superseded_by[m.id])}"
871
873
  out.print(line)
874
+ for directory in plan.analysis_dirs:
875
+ dest = project.runs_dir / "_archive" / directory.relative_to(project.runs_dir)
876
+ out.print(f" analysis {project.rel(directory)} -> {project.rel(dest)}", markup=False)
872
877
  _confirm(f"Archive {len(plan.targets)} runs?", yes)
873
878
  archived = archive_runs(
874
- project, plan.targets, superseded_by=plan.superseded_by, why=why, by=_actor()
879
+ project,
880
+ plan.targets,
881
+ superseded_by=plan.superseded_by,
882
+ why=why,
883
+ by=_actor(),
884
+ analysis_dirs=plan.analysis_dirs,
875
885
  )
876
886
  for run_id, dst in archived:
877
887
  out.print(f"archived {run_id} -> {project.rel(dst)}")
888
+ for directory in plan.analysis_dirs:
889
+ out.print(f"archived analysis {project.rel(directory)}", markup=False)
878
890
 
879
891
 
880
892
  @app.command("mv")
@@ -12,7 +12,7 @@ END = "<!-- runops:end -->"
12
12
  COMMANDS = """\
13
13
  | command | effect |
14
14
  |---|---|
15
- | `runo context --json` | where the project stands: active runs, recent runs, LOG.md tail |
15
+ | `runo context --json` | active runs, 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]) |
@@ -23,7 +23,7 @@ COMMANDS = """\
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 |
26
- | `runo tidy` / `runo tidy --apply` | list / apply cleanup (expired scratch, stale failures, old runs) |
26
+ | `runo tidy` / `runo tidy --apply` | list / apply cleanup (runs, builds, old analysis logs) |
27
27
  | `runo mv RUN... [--dest DIR] [--label L] --why TEXT` | rename / move runs, keeping ids and inputs |
28
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 |
@@ -78,7 +78,7 @@ the project found from the current directory.
78
78
  stopped. When a new series replaces an old one, ask whether the old one is still
79
79
  needed and archive it with `--superseded-by`.
80
80
 
81
- ## Notes
81
+ ## Notes and analysis
82
82
 
83
83
  Notes live in three places, where `runo context` and the next session find them:
84
84
  - one run: `runo note RUN "..."`;
@@ -86,6 +86,18 @@ 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
+ Place analysis by the runs its results depend on:
90
+
91
+ | Scope | Location |
92
+ |---|---|
93
+ | One run | `runs/.../<RUN>/analysis/` |
94
+ | One group (series or sweep directory) | `runs/<group>/_analysis/<topic>/` |
95
+ | Multiple groups or the project | `analysis/<topic>/`; shared code in `analysis/lib/` |
96
+
97
+ Keep each investigation's question, run ids, method, results and limits in its
98
+ `README.md`, with `scripts/`, `out/` and `logs/` beside it. Link it from `LOG.md`.
99
+ Use `runops-analyze` for this workflow; `runo context` lists group topics with a README.
100
+
89
101
  Analysis tools the simulator package does not bring go in `.runops/analysis-env/`
90
102
  (`uv venv .runops/analysis-env`), so `.venv` stays as `runo update` manages it. Run heavy
91
103
  reading and plotting where the site allows compute jobs.
@@ -0,0 +1,113 @@
1
+ """Discover conventional analysis directories and disposable job logs (SPEC §9.1)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ from collections.abc import Iterator
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+
11
+ from runops.manifest import ACTIVE_STATES, MANIFEST_FILE, Manifest, RunState
12
+ from runops.project import ARCHIVE_DIR, Project
13
+
14
+ GROUP_ANALYSIS = "_analysis"
15
+ _JOB_LOG = re.compile(r"^(.+)\.([0-9]+(?:_[0-9]+)?)\.(out|err)$")
16
+
17
+
18
+ def plain_path(path: Path, root: Path) -> bool:
19
+ """Within *root*, with no symlink components (including broken links)."""
20
+ try:
21
+ relative = path.absolute().relative_to(root.absolute())
22
+ except ValueError:
23
+ return False
24
+ current = root
25
+ for part in relative.parts:
26
+ if part == "..":
27
+ return False
28
+ current /= part
29
+ if current.is_symlink():
30
+ return False
31
+ return True
32
+
33
+
34
+ def group_analysis_dirs(project: Project, under: Path | None = None) -> Iterator[Path]:
35
+ """Walk current groups, stopping at runs and analysis; never visit work/ outputs."""
36
+ base = under or project.runs_dir
37
+ if not plain_path(base, project.root) or ARCHIVE_DIR in base.relative_to(project.root).parts:
38
+ return
39
+ for directory, dirs, files in os.walk(base):
40
+ parent = Path(directory)
41
+ if MANIFEST_FILE in files:
42
+ dirs[:] = []
43
+ continue
44
+ analysis = parent / GROUP_ANALYSIS
45
+ if GROUP_ANALYSIS in dirs and not analysis.is_symlink():
46
+ yield analysis
47
+ dirs[:] = sorted(
48
+ d
49
+ for d in dirs
50
+ if d not in (GROUP_ANALYSIS, ARCHIVE_DIR)
51
+ and not d.startswith(".")
52
+ and not (parent / d).is_symlink()
53
+ )
54
+
55
+
56
+ def group_topics(project: Project) -> dict[str, list[str]]:
57
+ result = {}
58
+ for directory in group_analysis_dirs(project):
59
+ topics = sorted(
60
+ p.name
61
+ for p in directory.iterdir()
62
+ if not p.name.startswith(".")
63
+ and plain_path(p / "README.md", project.root)
64
+ and (p / "README.md").is_file()
65
+ )
66
+ if topics:
67
+ result[project.rel(directory.parent)] = topics
68
+ return result
69
+
70
+
71
+ def log_directories(project: Project, runs: list[Manifest]) -> Iterator[Path]:
72
+ roots = [project.root / "analysis", *group_analysis_dirs(project)]
73
+ roots += [
74
+ m.run_dir / "analysis"
75
+ for m in runs
76
+ if not m.pinned
77
+ and m.status not in (*ACTIVE_STATES, RunState.ARCHIVED)
78
+ and ARCHIVE_DIR not in m.run_dir.relative_to(project.runs_dir).parts
79
+ ]
80
+ for root in roots:
81
+ if not plain_path(root, project.root):
82
+ continue
83
+ for directory, dirs, _ in os.walk(root):
84
+ parent = Path(directory)
85
+ dirs[:] = sorted(
86
+ d for d in dirs if not d.startswith(".") and not (parent / d).is_symlink()
87
+ )
88
+ if parent.name == "logs":
89
+ yield parent
90
+ dirs[:] = []
91
+
92
+
93
+ def old_log_pairs(directory: Path, cutoff: datetime) -> dict[str, list[Path]]:
94
+ """Old job-name/id pairs, retaining the most recently modified pair per job name."""
95
+ jobs: dict[str, dict[str, list[tuple[Path, float]]]] = {}
96
+ for path in sorted(directory.iterdir()):
97
+ match = _JOB_LOG.fullmatch(path.name)
98
+ if match and not path.is_symlink() and path.is_file():
99
+ name, job_id, _ = match.groups()
100
+ jobs.setdefault(name, {}).setdefault(job_id, []).append((path, path.stat().st_mtime))
101
+ result = {}
102
+ for name, pairs in jobs.items():
103
+ newest = max(
104
+ pairs,
105
+ key=lambda job: (
106
+ max(mtime for _, mtime in pairs[job]),
107
+ tuple(int(n) for n in job.split("_")),
108
+ ),
109
+ )
110
+ for job_id, files in pairs.items():
111
+ if job_id != newest and max(mtime for _, mtime in files) < cutoff.timestamp():
112
+ result[f"{name}.{job_id}"] = [path for path, _ in files]
113
+ return result
@@ -49,6 +49,8 @@ def resolve_dest(project: Project, dest: str) -> Path:
49
49
  raise UsageError(f"--dest must be inside {project.runs_dir}")
50
50
  if inside(path, project.runs_dir / ARCHIVE_DIR):
51
51
  raise UsageError("--dest must not be inside runs/_archive")
52
+ if "_analysis" in path.resolve().relative_to(project.runs_dir.resolve()).parts:
53
+ raise UsageError("--dest must not be inside _analysis (reserved for group analysis)")
52
54
  enclosing = project.enclosing_run(path)
53
55
  if enclosing is not None:
54
56
  # Discovery stops at a run directory, so a run nested in it would be lost.
@@ -185,9 +185,7 @@ def _split_values(raw: str) -> list[str]:
185
185
  def _dest_parent(project: Project, case: Case, dest: str, scratch: bool) -> Path:
186
186
  if scratch and not dest:
187
187
  return project.runs_dir / SCRATCH_DIR
188
- if not dest:
189
- return project.runs_dir / case.name
190
- return resolve_dest(project, dest)
188
+ return resolve_dest(project, dest or case.name)
191
189
 
192
190
 
193
191
  def _beside(project: Project, source: Manifest) -> Path | None:
@@ -10,6 +10,7 @@ from runops import __version__
10
10
  from runops.adapters.registry import optional
11
11
  from runops.errors import UsageError
12
12
  from runops.manifest import ACTIVE_STATES, Manifest, RunState
13
+ from runops.ops.analysis import group_topics
13
14
  from runops.ops.common import run_sim_context
14
15
  from runops.ops.skills import stale_runops_skills
15
16
  from runops.project import Project
@@ -91,6 +92,7 @@ def project_context(project: Project, *, log_lines: int = 20) -> dict[str, Any]:
91
92
  "expired_scratch": expired,
92
93
  "recent_runs": [m.to_summary(project.root) for m in recent],
93
94
  "notes_files": note_files,
95
+ "group_analysis": group_topics(project),
94
96
  "log_tail": log_tail,
95
97
  "warnings": [w for w in [stale_runops_skills(project)] if w],
96
98
  }
@@ -82,6 +82,9 @@ def resolve_site(site: str) -> tuple[str, bool]:
82
82
 
83
83
  GITIGNORE = """\
84
84
  .venv/
85
+ .runops/analysis-env/
86
+ **/logs/
87
+ __pycache__/
85
88
  .runops/build/
86
89
  .runops/index.toml
87
90
  .runops/*.lock