starforge-cli 0.1.6__tar.gz → 0.1.7__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 (55) hide show
  1. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/PKG-INFO +2 -2
  2. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/pyproject.toml +2 -2
  3. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/__init__.py +1 -1
  4. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/cli.py +2 -2
  5. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/commands/common.py +16 -20
  6. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/commands/exp.py +10 -13
  7. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/commands/init.py +9 -9
  8. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/commands/jobs.py +12 -4
  9. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/commands/plugin.py +0 -1
  10. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/commands/recipe.py +1 -1
  11. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/commands/sweep.py +1 -1
  12. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/new_experiment.py +5 -5
  13. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/recipe_lock.py +1 -1
  14. starforge_cli-0.1.6/starforge_cli/project.py → starforge_cli-0.1.7/starforge_cli/repo.py +28 -24
  15. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/experiment-template/README.md +1 -1
  16. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/spec_builder.py +8 -11
  17. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/.gitignore +0 -0
  18. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/README.md +0 -0
  19. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/api_client.py +0 -0
  20. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/auth.py +0 -0
  21. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/catalog.py +0 -0
  22. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/cli_ui.py +0 -0
  23. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/client_device.py +0 -0
  24. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/commands/__init__.py +0 -0
  25. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/commands/admin.py +0 -0
  26. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/commands/bench.py +0 -0
  27. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/commands/dataset.py +0 -0
  28. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/commands/login.py +0 -0
  29. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/commands/serve.py +0 -0
  30. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/commands/submit.py +0 -0
  31. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/config_resolve.py +0 -0
  32. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/data_prep.py +0 -0
  33. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/packing.py +0 -0
  34. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/plugins_lock.py +0 -0
  35. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/agent-run.py.tmpl +0 -0
  36. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/custom-framework/train.sh +0 -0
  37. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/experiment-template/.gitkeep +0 -0
  38. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/experiment-template/config.yaml +0 -0
  39. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/common/README.md +0 -0
  40. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/common/__init__.py +0 -0
  41. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/configs/README.md +0 -0
  42. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/configs/base/README.md +0 -0
  43. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/configs/base/distillation_math.yaml +0 -0
  44. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/configs/base/grpo_lora.yaml +0 -0
  45. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/configs/base/grpo_math_1B.yaml +0 -0
  46. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/configs/base/grpo_megatron.yaml +0 -0
  47. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/configs/base/grpo_noncolocated.yaml +0 -0
  48. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/configs/base/grpo_sliding_puzzle.yaml +0 -0
  49. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/configs/base/ppo_math_1B.yaml +0 -0
  50. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/configs/base/rm.yaml +0 -0
  51. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/configs/base/sft.yaml +0 -0
  52. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/configs/models/README.md +0 -0
  53. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/configs/models/qwen3.5-4b.yaml +0 -0
  54. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/configs/models/qwen3.5-9b.yaml +0 -0
  55. {starforge_cli-0.1.6 → starforge_cli-0.1.7}/starforge_cli/scaffold/project/gitignore +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: starforge-cli
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: StarForge(星锻)命令行客户端:pip 安装后用 sf init 创建微调项目,再提交训练。
5
5
  Project-URL: Homepage, https://github.com/wccdev/starforge
6
6
  Project-URL: Documentation, https://github.com/wccdev/starforge
@@ -18,7 +18,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
18
  Requires-Python: >=3.10
19
19
  Requires-Dist: datasets>=2.18
20
20
  Requires-Dist: pyyaml>=6
21
- Requires-Dist: starforge-core==0.1.6
21
+ Requires-Dist: starforge-core==0.2.0
22
22
  Requires-Dist: typer>=0.12
23
23
  Provides-Extra: dev
24
24
  Requires-Dist: fastapi>=0.110; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "starforge-cli"
3
- version = "0.1.6"
3
+ version = "0.1.7"
4
4
  description = "StarForge(星锻)命令行客户端:pip 安装后用 sf init 创建微调项目,再提交训练。"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -17,7 +17,7 @@ classifiers = [
17
17
  "Topic :: Scientific/Engineering :: Artificial Intelligence",
18
18
  ]
19
19
  dependencies = [
20
- "starforge-core==0.1.6",
20
+ "starforge-core==0.2.0",
21
21
  "typer>=0.12",
22
22
  "datasets>=2.18",
23
23
  "pyyaml>=6",
@@ -1,3 +1,3 @@
1
1
  """starforge-cli:统一 CLI 包。命令入口见 starforge_cli.cli:app(安装后提供 `sf`)。"""
2
2
 
3
- __version__ = "0.1.6"
3
+ __version__ = "0.1.7"
@@ -36,12 +36,12 @@ app = typer.Typer(
36
36
  # ----------------------------- 项目与身份 -----------------------------
37
37
  from starforge_cli.commands import init as init_cmd # noqa: E402
38
38
 
39
- app.command(help="创建 StarForge 微调项目(experiments/ + 官方基底 + common 骨架)")(init_cmd.init)
39
+ app.command(help="创建 StarForge 仓库(experiments/ + 官方基底 + common 骨架)")(init_cmd.init)
40
40
  app.command(help="登录 StarForge")(login.login)
41
41
  app.command(help="登出")(login.logout)
42
42
 
43
43
  # ----------------------------- 实验资产(纯本地)-----------------------------
44
- app.command(help="列出实验 / 项目")(exp.ls)
44
+ app.command(help="列出仓库里的实验目录")(exp.ls)
45
45
  app.command(help="新建实验(--from fork 现成实验;--method 来自 SDK recipe catalog)")(exp.new)
46
46
  app.command(name="methods", help="列出可用的后训练方法与它们的超参")(exp.methods)
47
47
  app.command(help="校验实验 config(提交前本地检查)")(exp.validate)
@@ -2,7 +2,6 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import os
5
- from enum import Enum
6
5
  from pathlib import Path
7
6
  from typing import Optional
8
7
 
@@ -12,24 +11,24 @@ from starforge_cli import cli_ui
12
11
 
13
12
  #: StarForge 项目标记文件:`sf init` 生成,项目发现的唯一依据。
14
13
  #: CLI 已 pip 化分发(starforge-cli),不再假设自己被 clone 在项目仓库里。
15
- PROJECT_MARKER = "starforge.yaml"
14
+ REPO_MARKER = "starforge.yaml"
16
15
 
17
16
 
18
17
  def project_root() -> Path:
19
18
  """从 cwd 向上定位含 starforge.yaml 的项目根(类比 git 的仓库发现)。
20
19
 
21
20
  找不到即失败并给出可执行指引——所有需要项目上下文的命令都经此收口;
22
- login / status 等全局命令不触碰它。SF_PROJECT_ROOT 环境变量可显式覆盖
21
+ login / status 等全局命令不触碰它。SF_REPO_ROOT 环境变量可显式覆盖
23
22
  (CI / 脚本场景免 cd)。
24
23
  """
25
- if env := os.environ.get("SF_PROJECT_ROOT"):
24
+ if env := os.environ.get("SF_REPO_ROOT"):
26
25
  p = Path(env).resolve()
27
- if (p / PROJECT_MARKER).is_file():
26
+ if (p / REPO_MARKER).is_file():
28
27
  return p
29
- cli_ui.fail(f"SF_PROJECT_ROOT={env} 不是 StarForge 项目(缺 {PROJECT_MARKER})")
28
+ cli_ui.fail(f"SF_REPO_ROOT={env} 不是 StarForge 仓库(缺 {REPO_MARKER})")
30
29
  cur = Path.cwd().resolve()
31
30
  for cand in (cur, *cur.parents):
32
- if (cand / PROJECT_MARKER).is_file():
31
+ if (cand / REPO_MARKER).is_file():
33
32
  return cand
34
33
  cli_ui.fail(
35
34
  "当前目录不在 StarForge 项目内",
@@ -56,22 +55,21 @@ def _root() -> Path:
56
55
 
57
56
  def project_name() -> str:
58
57
  """当前项目的 starforge.yaml name。提交到 console 的 project 只走这里。"""
59
- from starforge_cli.project import ProjectError, load_project_name
58
+ from starforge_cli.repo import RepoError, load_project_name
60
59
 
61
60
  try:
62
61
  return load_project_name(_root())
63
- except ProjectError as exc:
62
+ except RepoError as exc:
64
63
  cli_ui.fail(str(exc))
65
64
 
66
65
 
67
- class Kind(str, Enum):
68
- experiments = "experiments"
69
- projects = "projects"
66
+ #: 实验目录的唯一存放位置(历史上还支持 projects/,已与 experiments/ 合并)。
67
+ EXPERIMENTS_DIR = "experiments"
70
68
 
71
69
 
72
70
  def resolve_exp(name: str) -> str:
73
- """把实验名解析为相对仓库根的路径,接受 'experiments/x' / 'projects/x' / 'x'。"""
74
- cands = [name] if "/" in name else [f"experiments/{name}", f"projects/{name}"]
71
+ """把实验名解析为相对仓库根的路径,接受 'experiments/x' / 'x'。"""
72
+ cands = [name] if "/" in name else [f"{EXPERIMENTS_DIR}/{name}"]
75
73
  for c in cands:
76
74
  if (_root() / c).is_dir():
77
75
  return c
@@ -79,12 +77,10 @@ def resolve_exp(name: str) -> str:
79
77
 
80
78
 
81
79
  def list_exps() -> list[str]:
82
- out: list[str] = []
83
- for kind in ("experiments", "projects"):
84
- base = _root() / kind
85
- if base.is_dir():
86
- out += [p.name for p in base.iterdir() if p.is_dir()]
87
- return sorted(set(out))
80
+ base = _root() / EXPERIMENTS_DIR
81
+ if not base.is_dir():
82
+ return []
83
+ return sorted({p.name for p in base.iterdir() if p.is_dir()})
88
84
 
89
85
 
90
86
  def list_profiles() -> list[str]:
@@ -11,15 +11,15 @@ from starforge_cli.new_experiment import NewExperimentError, create_experiment
11
11
 
12
12
 
13
13
  def ls() -> None:
14
- """列出实验 / 项目。"""
15
- for kind in ("experiments", "projects"):
16
- base = common.ROOT / kind
17
- if not base.is_dir():
18
- continue
19
- exps = sorted(p.name for p in base.iterdir() if p.is_dir())
20
- typer.echo(f"\n[{kind}] ({len(exps)})")
21
- for e in exps:
22
- typer.echo(f" - {e}")
14
+ """列出仓库里的实验目录。"""
15
+ base = common.ROOT / common.EXPERIMENTS_DIR
16
+ if not base.is_dir():
17
+ typer.echo(f"[{common.EXPERIMENTS_DIR}] (0)")
18
+ return
19
+ exps = sorted(p.name for p in base.iterdir() if p.is_dir())
20
+ typer.echo(f"\n[{common.EXPERIMENTS_DIR}] ({len(exps)})")
21
+ for e in exps:
22
+ typer.echo(f" - {e}")
23
23
 
24
24
 
25
25
  def new(
@@ -37,9 +37,6 @@ def new(
37
37
  "--framework-version",
38
38
  help="精确框架版本;必须是 recipe catalog 已发布版本",
39
39
  ),
40
- kind: common.Kind = typer.Option(
41
- common.Kind.experiments, "--kind", help="experiments 或 projects"
42
- ),
43
40
  ) -> None:
44
41
  """新建实验(--from fork 现成实验;--method 来自 SDK recipe catalog)。"""
45
42
  if from_exp and method != "nemo-rl/grpo":
@@ -52,7 +49,7 @@ def new(
52
49
  try:
53
50
  create_experiment(
54
51
  common.ROOT,
55
- kind.value,
52
+ common.EXPERIMENTS_DIR,
56
53
  name,
57
54
  src=src,
58
55
  method=method,
@@ -1,4 +1,4 @@
1
- """`sf init`:交互式创建 StarForge 微调项目。"""
1
+ """`sf init`:交互式创建 StarForge 仓库。"""
2
2
  from __future__ import annotations
3
3
 
4
4
  from pathlib import Path
@@ -7,25 +7,25 @@ from typing import Optional
7
7
  import typer
8
8
 
9
9
  from starforge_cli import cli_ui
10
- from starforge_cli.project import InitError, init_project
10
+ from starforge_cli.repo import InitError, init_repo
11
11
 
12
12
 
13
13
  def init(
14
14
  directory: Optional[str] = typer.Argument(
15
- None, help="项目目录(省略则交互询问;. 表示当前目录)"
15
+ None, help="仓库目录(省略则交互询问;. 表示当前目录)"
16
16
  ),
17
- name: Optional[str] = typer.Option(None, "--name", help="项目名(默认目录名)"),
17
+ name: Optional[str] = typer.Option(None, "--name", help="项目名,写入 starforge.yaml(默认目录名)"),
18
18
  no_git: bool = typer.Option(False, "--no-git", help="不初始化 git 仓库"),
19
19
  yes: bool = typer.Option(False, "--yes", "-y", help="非交互:全部取默认值"),
20
20
  ) -> None:
21
- """创建 StarForge 项目:experiments/ + configs/(官方基底)+ common/ 骨架。
21
+ """创建 StarForge 仓库:experiments/ + configs/(官方基底)+ common/ 骨架。
22
22
 
23
- 项目是独立的 git 仓库——实验配置与共享代码归你和团队所有,
23
+ 仓库是独立的 git 仓库——实验配置与共享代码归你和团队所有,
24
24
  与 CLI 工具的版本解耦(升级 CLI 只需 pip install -U starforge-cli)。
25
25
  """
26
26
  target = (directory or "").strip()
27
27
  if not target and not yes:
28
- target = typer.prompt("项目目录", default="my-starforge-lab")
28
+ target = typer.prompt("仓库目录", default="my-starforge-lab")
29
29
  if not target:
30
30
  target = "my-starforge-lab"
31
31
  dest = Path(target).expanduser()
@@ -39,11 +39,11 @@ def init(
39
39
  use_git = typer.confirm("初始化 git 仓库?", default=True)
40
40
 
41
41
  try:
42
- root = init_project(dest if target != "." else Path.cwd(), name=project_name, git=use_git)
42
+ root = init_repo(dest if target != "." else Path.cwd(), name=project_name, git=use_git)
43
43
  except InitError as e:
44
44
  cli_ui.fail(str(e))
45
45
 
46
- typer.secho(f"✓ StarForge 项目已创建:{root}", fg=typer.colors.GREEN)
46
+ typer.secho(f"✓ StarForge 仓库已创建:{root}", fg=typer.colors.GREEN)
47
47
  typer.echo("下一步:")
48
48
  if target not in (".",):
49
49
  typer.echo(f" cd {target}")
@@ -160,8 +160,13 @@ def job_stop(
160
160
  job_id: str = typer.Argument(..., help="作业 ID"),
161
161
  ) -> None:
162
162
  gate()
163
- api_client.job_control_via_server("stop", job_id)
164
- typer.secho("✓ 已停止作业", fg=typer.colors.GREEN)
163
+ res = api_client.job_control_via_server("stop", job_id)
164
+ # 服务端用 stopped 区分「真停了」与「本来就是终态」——一律打印「已停止」会让人
165
+ # 以为卡放出来了。老服务端不返回这个字段,缺失时按真停了处理。
166
+ if res.get("stopped", True):
167
+ typer.secho("✓ 已停止作业", fg=typer.colors.GREEN)
168
+ else:
169
+ typer.secho(f"· 作业已是终态({res.get('status') or '未知'}),无需停止", fg=typer.colors.YELLOW)
165
170
 
166
171
 
167
172
  @job_app.command("pause", help="暂停作业(保留 checkpoint,可继续)")
@@ -187,8 +192,11 @@ def job_delete(
187
192
  job_id: str = typer.Argument(..., help="作业 ID"),
188
193
  ) -> None:
189
194
  gate()
190
- api_client.job_control_via_server("delete", job_id)
191
- typer.secho("✓ 已删除记录", fg=typer.colors.GREEN)
195
+ res = api_client.job_control_via_server("delete", job_id)
196
+ if res.get("removed_db", True):
197
+ typer.secho("✓ 已删除记录", fg=typer.colors.GREEN)
198
+ else:
199
+ typer.secho("· 台账记录未变(可能已被删除)", fg=typer.colors.YELLOW)
192
200
 
193
201
 
194
202
  @job_app.command("cancel-all", help="停止我所有运行中 / 排队中的作业")
@@ -10,7 +10,6 @@
10
10
  from __future__ import annotations
11
11
 
12
12
  import io
13
- import json
14
13
  import shutil
15
14
  import tarfile
16
15
  from pathlib import Path
@@ -54,7 +54,7 @@ def recipe_status(
54
54
  exp: Optional[str] = typer.Argument(
55
55
  None, autocompletion=common.complete_exp, help="实验名或路径;省略则需 --all"
56
56
  ),
57
- all_exps: bool = typer.Option(False, "--all", help="扫描 experiments/projects/smoke"),
57
+ all_exps: bool = typer.Option(False, "--all", help="扫描 experiments/ 与 smoke/"),
58
58
  server: bool = typer.Option(False, "--server", help="再与 Console catalog 握手对照"),
59
59
  ) -> None:
60
60
  manager = RecipeLockManager()
@@ -4,7 +4,7 @@
4
4
  - **不造第二条提交路径**:每个变体就是一次普通 `sf submit`(同一份 spec 构建、
5
5
  校验、打包、配额准入、容量排队),服务端零特殊逻辑。sweep 只是「展开 + 循环 +
6
6
  分组标识」。
7
- - 分组复用现成机制:project 来自 starforge.yaml(web 实验分组页按项目聚合)+
7
+ - 分组复用现成机制:project 来自 starforge.yaml(web 项目页按项目聚合)+
8
8
  client_meta.sweep_id / sweep_params(服务端落 jobs.sweep_id 索引列,
9
9
  一键停止 / 徽标展示用)。
10
10
  - 配额不足的变体自动入队(persist_and_enqueue),不会失败——批量提交天然
@@ -12,12 +12,12 @@ class NewExperimentError(Exception):
12
12
 
13
13
 
14
14
  def _resolve_src_dir(repo_root: Path, src: str) -> Path:
15
- for c in (src, f"experiments/{src}", f"projects/{src}"):
15
+ for c in (src, f"experiments/{src}"):
16
16
  p = repo_root / c
17
17
  if p.is_dir():
18
18
  return p
19
19
  raise NewExperimentError(
20
- f"找不到来源实验: {src}(试过 {src} / experiments/{src} / projects/{src})"
20
+ f"找不到来源实验: {src}(试过 {src} / experiments/{src})"
21
21
  )
22
22
 
23
23
 
@@ -135,7 +135,7 @@ def _create_from_template(
135
135
  # get_recipe 的报错已列出可用值,并对无前缀裸名给出两段式提示。
136
136
  raise NewExperimentError(f"--method 非法: {exc}") from exc
137
137
  # 实验模板随 CLI 包分发(starforge_cli/scaffold/),不依赖项目内容。
138
- from starforge_cli.project import experiment_template
138
+ from starforge_cli.repo import experiment_template
139
139
 
140
140
  template = experiment_template()
141
141
  if not template.is_dir():
@@ -180,8 +180,8 @@ def create_experiment(
180
180
  framework_version: str = "",
181
181
  ) -> None:
182
182
  """新建或 fork 实验;失败时抛 NewExperimentError。"""
183
- if kind not in ("experiments", "projects"):
184
- raise NewExperimentError("第一个参数必须是 experiments 或 projects")
183
+ if kind != "experiments":
184
+ raise NewExperimentError("实验目录只能建在 experiments/ 下")
185
185
 
186
186
  if src:
187
187
  _fork_experiment(repo_root, kind, name, src)
@@ -20,7 +20,7 @@ LOCK_FILE = "recipe.lock.json"
20
20
  LOCK_VERSION = "forge/recipe-lock/v3"
21
21
  _LEGACY_LOCK_VERSION = "forge/recipe-lock/v2"
22
22
  _SUPPORTED_LOCK_VERSIONS = (LOCK_VERSION, _LEGACY_LOCK_VERSION)
23
- _EXPERIMENT_KINDS = ("experiments", "projects", "smoke")
23
+ _EXPERIMENT_KINDS = ("experiments", "smoke")
24
24
 
25
25
 
26
26
  class LockState(str, Enum):
@@ -1,16 +1,20 @@
1
- """StarForge 项目脚手架:`sf init` 的实现。
1
+ """StarForge 仓库脚手架:`sf init` 的实现。
2
2
 
3
3
  CLI 经 pip 分发(starforge-cli),用户不 clone 工具仓库;`sf init` 生成
4
- 自包含的项目目录(自己的 git 仓库),布局与作业包白名单严格对应:
4
+ 自包含的仓库目录(自己的 git 仓库),布局与作业包白名单严格对应:
5
5
 
6
6
  my-lab/
7
- ├── starforge.yaml 项目标记(项目发现的唯一依据)
8
- ├── experiments/ 实验目录(sf new 在此创建)
7
+ ├── starforge.yaml 仓库标记,并声明本仓库的项目名(项目发现的唯一依据)
8
+ ├── experiments/ 实验目录(sf new 在此创建,一个目录 = 一次实验/作业)
9
9
  ├── configs/ 官方基底 + 模型片段(脚手架落地,用户可调可 pin)
10
- ├── common/ 项目共享代码(数据脚本 / 环境 / 奖励)
10
+ ├── common/ 仓库共享代码(数据脚本 / 环境 / 奖励)
11
11
  └── .gitignore
12
12
 
13
- 平台 bootstrap/runner 不落地到项目,也不由 CLI 上传;console 在准入后注入
13
+ 仓库与项目的关系:仓库是磁盘上的容器,项目是 starforge.yaml 声明的名字,提交后
14
+ 在 console 侧按 @<用户名>/<项目名> 聚合出「项目」。项目名不要求全局唯一——用户名
15
+ 编入分组键,不同用户的同名项目互不干扰。
16
+
17
+ 平台 bootstrap/runner 不落地到仓库,也不由 CLI 上传;console 在准入后注入
14
18
  内容寻址 Job Capsule,用户代码不能覆盖平台入口。
15
19
  """
16
20
  from __future__ import annotations
@@ -24,31 +28,31 @@ from pathlib import Path
24
28
 
25
29
  import yaml
26
30
 
27
- PROJECT_MARKER = "starforge.yaml"
31
+ REPO_MARKER = "starforge.yaml"
28
32
  # 与 console 分组键一致:会进 URL / 路径,只允许安全字符。
29
33
  _PROJECT_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$")
30
34
 
31
35
 
32
- class ProjectError(RuntimeError):
36
+ class RepoError(RuntimeError):
33
37
  pass
34
38
 
35
39
 
36
40
  def load_project_name(root: Path) -> str:
37
- """读 starforge.yaml 的 name。提交到 console 的 project 只来自这里。"""
38
- path = root / PROJECT_MARKER
41
+ """读 starforge.yaml 的 name(本仓库的项目名)。提交到 console 的 project 只来自这里。"""
42
+ path = root / REPO_MARKER
39
43
  if not path.is_file():
40
- raise ProjectError(f"{root} 不是 StarForge 项目(缺 {PROJECT_MARKER})")
44
+ raise RepoError(f"{root} 不是 StarForge 仓库(缺 {REPO_MARKER})")
41
45
  try:
42
46
  data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
43
47
  except yaml.YAMLError as exc:
44
- raise ProjectError(f"{path} 无法解析:{exc}") from exc
48
+ raise RepoError(f"{path} 无法解析:{exc}") from exc
45
49
  if not isinstance(data, dict):
46
- raise ProjectError(f"{path} 根节点必须是对象")
50
+ raise RepoError(f"{path} 根节点必须是对象")
47
51
  name = str(data.get("name") or "").strip()
48
52
  if not name:
49
- raise ProjectError(f"{path} 缺少 name")
53
+ raise RepoError(f"{path} 缺少 name")
50
54
  if name in (".", "..") or not _PROJECT_NAME_RE.match(name):
51
- raise ProjectError(f"非法项目名 {name!r}(仅允许字母数字与 . _ -)")
55
+ raise RepoError(f"非法项目名 {name!r}(仅允许字母数字与 . _ -)")
52
56
  return name
53
57
 
54
58
 
@@ -66,14 +70,14 @@ class InitError(RuntimeError):
66
70
  pass
67
71
 
68
72
 
69
- def init_project(dest: Path, *, name: str = "", git: bool = True) -> Path:
70
- """在 dest 生成 StarForge 项目;目录已是项目或非空冲突时报错。"""
73
+ def init_repo(dest: Path, *, name: str = "", git: bool = True) -> Path:
74
+ """在 dest 生成 StarForge 仓库;目录已是仓库或非空冲突时报错。"""
71
75
  dest = dest.resolve()
72
- if (dest / PROJECT_MARKER).is_file():
73
- raise InitError(f"{dest} 已经是 StarForge 项目")
76
+ if (dest / REPO_MARKER).is_file():
77
+ raise InitError(f"{dest} 已经是 StarForge 仓库")
74
78
  if dest.exists() and any(dest.iterdir()):
75
79
  # 允许在非空目录初始化(如已有 README 的空仓库),但拒绝覆盖关键路径
76
- for clash in ("experiments", "configs", "common", PROJECT_MARKER):
80
+ for clash in ("experiments", "configs", "common", REPO_MARKER):
77
81
  if (dest / clash).exists():
78
82
  raise InitError(f"{dest} 下已存在 {clash},拒绝覆盖;换个目录或清理后重试")
79
83
  dest.mkdir(parents=True, exist_ok=True)
@@ -93,8 +97,8 @@ def init_project(dest: Path, *, name: str = "", git: bool = True) -> Path:
93
97
  cli_version = _pkg_version("starforge-cli")
94
98
  except Exception: # noqa: BLE001 — 源码运行(未安装分发元数据)
95
99
  cli_version = "dev"
96
- (dest / PROJECT_MARKER).write_text(
97
- "# StarForge 项目标记:`sf` 命令以此发现项目根,勿删除。\n"
100
+ (dest / REPO_MARKER).write_text(
101
+ "# StarForge 仓库标记:`sf` 命令以此发现仓库根,勿删除。\n"
98
102
  f'apiVersion: forge/project/v1\n'
99
103
  f'name: {project_name}\n'
100
104
  f'created_by: starforge-cli {cli_version}\n',
@@ -104,7 +108,7 @@ def init_project(dest: Path, *, name: str = "", git: bool = True) -> Path:
104
108
  if not readme.exists():
105
109
  readme.write_text(
106
110
  f"# {project_name}\n\n"
107
- "StarForge(星锻)微调项目。常用命令:\n\n"
111
+ "StarForge(星锻)微调仓库。常用命令:\n\n"
108
112
  "```bash\n"
109
113
  "sf new my-exp --method nemo-rl/grpo # 新建实验\n"
110
114
  "sf validate my-exp # 本地校验\n"
@@ -112,7 +116,7 @@ def init_project(dest: Path, *, name: str = "", git: bool = True) -> Path:
112
116
  "sf job logs # 跟随日志\n"
113
117
  "```\n\n"
114
118
  "目录说明:`experiments/` 实验本体;`configs/` 官方基底与模型片段;\n"
115
- "`common/` 项目共享代码(数据脚本 / Agent 环境 / 奖励函数)。\n",
119
+ "`common/` 仓库共享代码(数据脚本 / Agent 环境 / 奖励函数)。\n",
116
120
  encoding="utf-8",
117
121
  )
118
122
 
@@ -25,7 +25,7 @@
25
25
  ## 运行
26
26
 
27
27
  ```bash
28
- uv run sf submit <实验名> --profile h100
28
+ sf submit <实验名> --profile h100
29
29
  ```
30
30
 
31
31
  产物(checkpoint / 日志)落到本目录 `outputs/`(已 .gitignore)。
@@ -157,7 +157,7 @@ def materialize_pools(
157
157
  roles = [f"{role}:{pool_name}"] if role else []
158
158
  return name, pools, roles
159
159
 
160
- missing = [raw for raw, (role, _, _) in zip(exprs, parsed) if not role]
160
+ missing = [raw for raw, (role, _, _) in zip(exprs, parsed, strict=False) if not role]
161
161
  if missing:
162
162
  raise SpecError(
163
163
  f"多个 --profile 时每条都要 role= 前缀(角色到池的映射),缺前缀:{missing[0]!r}"
@@ -231,16 +231,13 @@ def build_spec(
231
231
  raise SpecError(f"方法 {r.id} 使用 platform observability,不允许 --observability-url")
232
232
  if r.framework == "custom" and not selected_image:
233
233
  raise SpecError("custom recipe 要求显式提供 --image")
234
- if r.framework in {"verl", "trl"} and operation == "train":
235
- missing = [
236
- flag
237
- for flag, value in (
238
- ("--model", base_model),
239
- ("--train-data", train_data),
240
- ("--validation-data", validation_data),
241
- )
242
- if not value.strip()
243
- ]
234
+ if r.framework in {"verl", "trl", "openrlhf"} and operation == "train":
235
+ required = [("--model", base_model), ("--train-data", train_data)]
236
+ # openrlhf:RL 的 eval.dataset 依赖 remote reward(上游硬约束),验证集是
237
+ # 可选项,约束由服务端 adapter 在编译期裁决;verl/trl 仍然三项必填。
238
+ if r.framework != "openrlhf":
239
+ required.append(("--validation-data", validation_data))
240
+ missing = [flag for flag, value in required if not value.strip()]
244
241
  if missing:
245
242
  raise SpecError(f"{r.framework} recipe 要求显式提供:{' '.join(missing)}")
246
243
 
File without changes
File without changes