ml-research-workflow — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ml-research-workflow (Agent Skill) and scored it 96/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 1 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
projtool runs ML experiments across a local development machine and remote AutoDL containers. Three asset categories — code, outputs, reports — have different version-control needs and different storage backends. Your job is to keep them coherent without making the user do bookkeeping.
projtool v4 uses a container root. Code lives in code/<worktree>/ inside that container, with the main git worktree at code/main/ and experiment worktrees at code/<experiment_id>/. Container-root data/ and output/ are local runtime directories; each worktree gets a data symlink so user code can keep reading ./data. Outputs are indexed by run_id and never committed.
Reports — markdown analyses of one or more runs — live in an independent docs repo at container-root docs/, not inside any code worktree. Inside the docs repo, reports are organized by code-repo branch namespace — exp/lora reports live at docs/exp/lora/<date>-<slug>.md, cross-branch summaries at docs/summary/<date>-<slug>.md. Reports cite run_ids and inline headline metrics so they remain readable even if outputs are deleted later, plus they record git.code_repo_url and git.commit_url for cross-repo provenance.
Mutagen handles cross-machine syncing of code (forward, one-way-replica, per-worktree) and outputs (reverse, one-way-safe, single global session). The docs repo does NOT participate in mutagen sync — it is local-only plus its own GitHub origin. AutoDL instances are managed via the algorithm in src/projtool/autodl/ — you only need the MCP tools.
| Goal | MCP tool |
|---|---|
| Open / close GPU instance | instance_up, instance_down, refresh_ssh |
| Make / delete experiment branch + worktree | new_experiment, worktree_status |
| Launch / monitor / finalize a run | start_training, get_run_status, finalize_run |
| Listing | list_runs, load_manifest, list_syncs |
| Reports (operate on docs repo) | start_report, commit_report |
Image build (failure path goes to image-debug skill) | image_build_start, image_save, image_save_status, image_build_finalize, image_build_abandon |
| Remote operations during build / debug | remote_exec, read_remote_file, write_remote_file, patch_remote_file |
| Diagnose / repair | diagnose, reconcile_bindings, sync_resume, sync_flush |
| Template upgrade | retrofit_check_upgrade |
Always read the project's .proj-tool/project.toml before invoking any tool that needs a path; never hardcode paths in your tool calls. v4 path fields are dir names (code_dir, data_dir, docs_dir, output_dir) plus remote paths (outputs_remote, data_remote, remote_workspace). Removed v3 fields such as docs_repo_path and outputs_local should not be used for new projects.
new_experiment with a topic slug → creates exp/<topic> branch,code/<topic> worktree, and the docs repo's `<branch_full>/` placeholder directory + commit. If a bound AutoDL instance already exists for this experiment, the mutagen code-sync session is created immediately; otherwise it is deferred to start_training.
start_training's pre-hook will refuse a dirty tree.)instance_up for the experiment.start_training with the chosen config.Use slot when the same experiment branch needs multiple GPU instances. Do not create empty branches only to borrow more machines.
Example: one exp/distill branch with three distillation configs:
instance_up(experiment_id="distill", slot="w2")
start_training(experiment_id="distill", slot="w2", ...)
instance_down(experiment_id="distill", slot="w2", mode="release")Identity rules:
experiment_id is normalized to canonical exp-<slug>.main; its storage key is just exp-<slug>.exp-<slug>#<slot>.(experiment_id, slot) pair through instance_up,start_training, get_run_status, finalize_run, and instance_down.
get_run_status returns:
Don't poll faster than every ~30s; it costs an SSH round-trip.
start_training launches the tmux command and writes the initial manifest, but it does not inject an automatic finalize hook. A training wrapper/script may call finalize_run(run_status="completed") on clean exit; otherwise you must call finalize_run manually after inspecting get_run_status, logs, and metrics. If get_run_status reports run_status="orphaned", tmux is gone while the manifest still says running; choose completed or failed based on the outputs. See references/dirty-worktree-handling.md for edge cases.
exp/<topic> branch. The report file goes to <container>/docs/<branch_full>/<date>-<slug>.md.
branches (e.g., lora vs qformer), or is a periodic milestone spanning the project. Goes to <container>/docs/summary/<date>-<slug>.md.
start_report(experiment_id, run_ids, report_type, scope). The toolloads each run's manifest, picks the docs-repo target directory (mkdir -p so nested branch names like exp/lora create both segments), prefills references and metrics_inlined skeletons, and fills the cross-repo fields (git.branch, git.commit, git.code_repo_url resolved from the code repo's git remote, git.commit_url constructed).
metrics_inlined consistent with cited numbers.commit_report(report_path, slug=..., commit_message=...). The tool:origin (cross-repo invariant). On failure, raises CrossRepoReferenceError with the explicit instruction to cd <code_repo> && git push origin <branch> first.
git add the specific.md and (if externalized) CSV paths, then commits and pushes to the docs repo's current branch (resolved via git symbolic-ref --short HEAD — never hardcoded as main).
See references/report-frontmatter.md for the schema and references/manifest-schema.md for the manifest fields you can cite.
Cross-worktree access: from any code-repo worktree, you can ls <container>/docs/exp/lora/ to read reports written from another branch — the docs repo is outside all worktrees, so the git-worktree isolation barrier doesn't apply. v2's list_my_reports.sh helper is gone.
When the user asks to build a project image:
image_build_start — opens a dedicated builder instance and pushessetup.sh (you draft it from the user's requirements + base image). For AutoDL PyTorch base images, put export PATH="/root/miniconda3/bin:$PATH" immediately after set -euo pipefail; non-interactive SSH shells may not have conda's Python on PATH.
remote_exec "bash setup.sh" — blocks until done; exit_code is inthe result (non-zero means setup.sh failed, NOT a tool error). Full output is in log_path; stderr_tail has the inline error context.
image_save (non-blocking — returns pending_image_uuidimmediately) → poll image_save_status until image_status='ready' → image_build_finalize. The tool writes .proj-tool/project.toml's [image].project_image_uuid (and related fields) on finalize. Future instance_up calls use that project image by default.
diagnostic procedure. Don't free-style; common errors have known patterns in image-debug/references/.
When you need to run a short script on the remote — sanity-check that the warmstart loads, read step-0 metrics, eval an existing checkpoint, etc. — use remote_exec, not start_training. Three reasons:
start_training registers a run in state.json's run_index, writes amanifest stub, and expects the run to converge to completed / failed / killed via finalize_run. A 30-second diagnostic pollutes that audit trail.
check-ssh-train.sh) refuse a dirty worktreeand require the commit to be pushed; you usually don't want either constraint for a one-shot diagnostic.
experiment" and blocks start_training even when you only meant a one-step run; remote_exec is unaffected.
Pattern:
remote_exec(ssh_alias=<exp_alias>, command="cd /workspace && python diag.py")The script should live at a path outside <remote_workspace> (e.g. /tmp/diag.py, pushed with write_remote_file, or composed inline via bash -c '...'). Anything inside <remote_workspace> that doesn't exist on alpha will be deleted by the next mutagen flush — see the <remote_workspace> invariant below.
When the user says "sync seems stuck" / "mutagen 卡住":
diagnose — runs all health checks; returns a structured report.references/mutagen-troubleshooting.md.
sync_resume, refresh_ssh).still exists → the binding is stale. instance_up will repair it (creates a fresh instance under the same experiment_id).
Note: the docs repo is not part of mutagen. If the docs repo seems out of sync between local and GitHub, that's a plain git pull/git push issue in <container>/docs/, not a projtool diagnostic concern.
When an existing project was used before binding normalization, state may contain split keys such as distill-w2 and exp-distill-w2. The automatic state migration adds missing slot fields but intentionally does not rename or merge live bindings.
Use this sequence after upgrading projtool if diagnose reports split, duplicate, or legacy bindings:
diagnose — identify suspicious keys and stale bindings.reconcile_bindings(dry_run=True) — ask projtool for proposed merges.decisions; if both sides look live, choose the winner explicitly.
reconcile_bindings(dry_run=False, decisions=[...]) — apply the reviewedmapping.
diagnose again — confirm the split keys and stale aliases are gone.Do not edit state.json directly unless projtool itself is unusable and the user explicitly accepts the risk.
pip install -U projtoolUser: "projtool 升级了" / "check if templates have updates".
retrofit_check_upgrade — returns per-file diffs.(>>> projtool >>> … <<< projtool <<<) is the safe scope; outside it is user-owned.
the upgrade is a major bump.
[paths] schema changed: reports field removed, docs_repo_path anddocs_repo_url added. Tool will offer to set defaults.
claude/hooks/check-reports-on-exp.sh — tool will offer to delete it.reports/ directory in the code repo is not auto-migrated.Tool will detect it and emit a warning suggesting manual migration to ~/proj-docs/<project>/<branch_full>/.... v0.4+ may add a migrate_reports_to_docs_repo helper.
Mutagen is the sole source of truth for files under /workspace/. Manual writes are unstable in two directions:
whatever you placed on the remote.
scp'd a snippet from adifferent worktree, dropped an ad-hoc script the local repo lacks, etc.), the next flush deletes it as out-of-tree drift — usually within seconds.
For files that must not propagate back to alpha — diagnostic scripts, cross-worktree temp blobs, throwaway data fixtures — use /tmp/ or any other path outside <remote_workspace>. To inspect a synced file, use read_remote_file. To fix a synced file, edit locally and let the next flush carry the change.
the worktree's files but its .git pointer is the alpha-side absolute path (e.g. /home/<user>/.../code/<exp>/.git/worktrees/<exp>), which doesn't exist on the remote. git log / git status / git diff invoked inside /workspace will fail with fatal: not a git repository. To check which commit is on the remote, either run git log locally in the alpha worktree, read the latest run's manifest.yaml (git.commit), or grep file contents for the change you're tracking.
/root/autodl-fs/runs/...). Otherwise outputs get sucked into the code forward sync and you'll see a sync storm.
find yourself about to write a reports/ file inside any code-repo worktree, stop and use start_report.
<container>/docs/)must not appear in .mutagen.yml.
instance_down returninga successful tool result only means the request was handled. Inspect acted: true means AutoDL was actually asked to power off or release; false means no platform action happened. If acted=false, read reason and did_you_mean before telling the user an instance was closed.
repo's origin before commit_report will push the docs repo. The hook check-ssh-train.sh already enforces "training-time push" for the manifest's commit; commit_report enforces this again at report time in case you're writing a report retroactively.
start_training enforces clean +pushed by default; only set allow_dirty=true or require_pushed=false when the user explicitly accepts the provenance warning.
.proj-tool/project.toml. The user already configured this once during retrofit.
Read these when their topic is in play:
references/manifest-schema.md — full manifest.yaml fields, when each isfilled, how to handle missing fields.
references/report-frontmatter.md — report frontmatter schema (withgit.code_repo_url and git.commit_url cross-repo fields), single vs comparison vs ablation, the >8-runs externalization rule, file path conventions in the docs repo.
references/dirty-worktree-handling.md — what to do when finalize_runfailed, when a manifest is stuck on running, when the user really needs to override the cleanliness check.
references/mutagen-troubleshooting.md — diagnose output → fix mapping;agent version mismatches; one-way-safe conflict resolution. (The docs repo isn't relevant here — it's not in mutagen.)
/root/autodl-fs/runs/... so run outputs survive AutoDL instance release; /root/autodl-tmp/... is now described as explicit scratch storage.
short remote scripts route through remote_exec instead of polluting run_index via start_training; rewrote the <remote_workspace> invariant to be bidirectional (mutagen also deletes manual writes that aren't on alpha) with explicit /tmp/ guidance for cross-worktree temp files; added a new invariant noting that git is unusable inside <remote_workspace> because mutagen doesn't replicate the alpha-side .git pointer.
start_training injects a finalize hook; documented get_run_status orphaned state and project-image default for instance_up.
running Python in setup.sh; manifest reference documents git.is_unpushed for start_training(require_pushed=false).
cdcbc02): image_save_statusreturn field renamed status → image_status to avoid colliding with the MCP envelope's reserved status key; updated 2 wording occurrences in the Image build section so the polling instruction matches the new field name.
image_save + image_save_status poll flow; added image_save_status to tool inventory table; clarified that remote_exec returns exit_code (non-zero is a setup.sh failure, not a tool error) with full output at log_path.
report-frontmatter.md to matchbuild_runs_csv_content output (run_id, commit_at_run_time, started_at, status, <metrics>); removed unimplemented --allow-unpushed-code flag reference.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.