Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,32 @@ and this project adheres to

## [Unreleased]

### Added

- Two skills that run as forked subagents on a cheaper model, so the
trivial end of a task does not replay the main conversation at the main
model's price. `land` (`model: sonnet`) commits the finished work with a
short Conventional Commit, pushes, opens the pull request and watches CI;
`job-watch` (`model: haiku`) waits for Slurm jobs and reports how they
ended — state, exit code, elapsed, peak memory against the request, the
log tail. The briefing and the MCP server's instructions tell the agent
to hand waits, status checks and landing off to them.

### Changed

- `git-workflow` is shorter and asks for short commit messages: most
commits are the subject line alone, and a body is two or three lines on
a why the diff cannot say, never a narrative. Releases tag the merge
commit on `main` after the pull request lands.
- `hpc-compute` is half its length. The sinteractive CLI walkthrough it
carried — `list`, `session ensure`, the state file, `peek`/`send` — is
what the MCP server now exposes as tools, so the skill keeps the rules
and a five-line reference; the `/tmp` and controller sections point at
`hpc-storage` and `slurm-batch` instead of repeating them. The
per-cluster storage files tell the `/tmp` story once, and every skill's
description, which sits in the system prompt of every session, is
tightened.

## [1.2.0] - 2026-09-05

### Changed
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,8 +370,9 @@ sinteractive claude install # from any installed copy
make claude-install # equivalent, from a checkout
```

This installs the six skills (`hpc-compute`, `slurm-discovery`, `hpc-storage`,
`hpc-software`, `slurm-batch`, `git-workflow`) into `~/.claude/skills/`, then
This installs the eight skills (`hpc-compute`, `slurm-discovery`, `hpc-storage`,
`hpc-software`, `slurm-batch`, `git-workflow`, and the forked `land` and
`job-watch`, which run on a cheaper model) into `~/.claude/skills/`, then
registers in your `settings.json` the four hooks (`sinteractive claude hook
session-start` briefs the agent on the session it is in; `sinteractive claude hook
prompt` warns when walltime is short; `sinteractive claude hook
Expand Down
12 changes: 12 additions & 0 deletions crates/sint/src/commands/agent_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
//! and the briefing names the cluster's scratch for it. And a workflow
//! controller (snakemake, nextflow) is submitted as a job of its own, so it
//! outlives the session instead of dying with it.
//!
//! And one about cost: the wait for a job, a queue or quota check, and
//! landing a branch are trivial, so the briefing sends them to a cheaper
//! model (the forked `job-watch` and `land` skills, or a haiku subagent)
//! rather than letting them replay the whole conversation at full price.

use std::path::{Path, PathBuf};

Expand Down Expand Up @@ -143,6 +148,13 @@ slurm executor, nextflow's slurm executor). Run in this session, or in an
srun held open from it, it dies with the session and the rest of the pipeline
with it; as a job it is bounded by nothing but its own -t.

Not every step needs the model you are running. Waiting on a job, checking
the queue or the quota, and landing a finished branch are trivial: fork them
onto a cheaper model — `/job-watch JOBID` waits and reports how a job ended,
`/land "why"` commits, pushes and opens the pull request — or delegate to a
subagent with model haiku. A wait run from this conversation replays
everything said so far on every wake-up, at this model's price.

Re-check this session with `sinteractive status --json` before long work;
the number above was read when this briefing was generated, and a walltime can
be changed underneath you.
Expand Down
6 changes: 4 additions & 2 deletions crates/sint/src/commands/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ nodes. A session is an orchestration shell, not a compute target: it is a small
with the shell the user is typing in, so editing, git and scheduler queries belong there and \
anything heavier gets its own srun/salloc allocation. Call session_status before long work to \
read the remaining walltime, and wait_for_event (which blocks until something happens) instead \
of polling. peek reads a session's screen; send types into the user's live shell, so only do that \
when asked.";
of polling. Waiting and status checks are trivial work: run them from a subagent on a cheaper \
model (the job-watch skill, or an Agent call with model haiku) rather than the main \
conversation, which replays everything said so far on every wake-up. peek reads a session's \
screen; send types into the user's live shell, so only do that when asked.";

/// Default and cap for `wait_for_event`'s timeout, in seconds.
const WAIT_DEFAULT_SECS: u64 = 300;
Expand Down
1 change: 1 addition & 0 deletions crates/sint/tests/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ fn handshake_tools_and_the_documented_contracts() {
"{instructions}"
);
assert!(instructions.contains("wait_for_event"), "{instructions}");
assert!(instructions.contains("cheaper model"), "{instructions}");

let tools = mcp.request("tools/list", json!({}));
let mut names: Vec<&str> = tools["tools"]
Expand Down
3 changes: 3 additions & 0 deletions crates/sint/tests/reporting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,9 @@ fn agent_context_briefing() {
"be changed underneath you.\n\nStorage quota, while exceeded,",
"/tmp is this node's own disk",
"is itself submitted with sbatch",
"Not every step needs the model you are running",
"`/job-watch JOBID`",
"`/land \"why\"`",
] {
assert!(out.contains(needle), "missing {needle:?} in:\n{out}");
}
Expand Down
21 changes: 15 additions & 6 deletions docs/scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@ are removed when their `hpc-*` successor is installed. Exit codes: 0 done,
1 no assets found, 2 a settings file was refused (everything else was still
installed).

**Six [skills](https://code.claude.com/docs/en/skills)** teach agents how work
**Eight [skills](https://code.claude.com/docs/en/skills)** teach agents how work
is done here. Skills load on demand from their descriptions, so an agent picks
up the one the task calls for rather than carrying all six. The three `hpc-*`
up the one the task calls for rather than carrying all eight. The three `hpc-*`
skills go one step further: their SKILL.md holds the rules shared by both
clusters this tool runs on and delegates the rest to an `alpine.md` or
`bodhi.md` beside it, so the agent reads the system it is actually on and is
Expand All @@ -196,8 +196,9 @@ never fed the other one's partitions, paths, and quotas.
sinteractive session is a compute target, real work goes into an allocation
sized for it, nothing on a node's `/tmp` crosses into that allocation, a
workflow controller is submitted as a job of its own so it outlives the
session, reuse sessions rather than piling them up, check the time budget
before long jobs, observe a session with `peek`/`send`, and clean up.
session, the wait for a job is handed to a cheaper model, reuse sessions
rather than piling them up, check the time budget before long jobs, and
clean up.

`slurm-discovery` covers finding out what the cluster offers rather than
assuming it: what the partitions are and how big, which accounts and QOS you
Expand Down Expand Up @@ -233,10 +234,18 @@ last one actually used — noting that `MaxRSS` lives on the step rows, where

`git-workflow` covers the git conventions, and is about the repository open in
the session rather than the cluster: semantic versioning with annotated
`vX.Y.Z` tags, Conventional Commit messages, one worktree per branch under
`.claude/worktrees/`, landing work through a pull request rather than
`vX.Y.Z` tags, Conventional Commit messages kept to a subject line, one
worktree per branch, landing work through a pull request rather than
committing to `main`, and running the repo's own CI gates before pushing.

`land` and `job-watch` are the trivial ends of the two workflows above, and
run as forked subagents on a cheaper model — `context: fork` with
`model: sonnet` and `model: haiku` in their frontmatter — so that committing,
pushing and watching CI, or waiting for a Slurm job and reporting how it
ended, does not replay the main conversation at the main model's price. The
briefing and the MCP server's instructions tell the agent to hand those
steps off.

**`sinteractive claude context`** prints a briefing on the current session —
job, node, partition, allocation size, walltime remaining, and the rules
above. It exits 1 outside a session. Run it by hand to see exactly what an
Expand Down
154 changes: 48 additions & 106 deletions skills/git-workflow/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,140 +1,82 @@
---
name: git-workflow
description: Git conventions for this user's repositories — semantic versioning, Conventional Commit messages, one worktree per branch, and landing work through pull requests. Use whenever a task involves branching, committing, opening or reviewing a pull request, cutting a release, tagging a version, or writing a changelog entry.
description: Git conventions for this user's repositories — a worktree per branch, short Conventional Commit messages, semver with annotated tags, and landing through a pull request. Use when branching, committing, opening a pull request, cutting a release, or writing a changelog entry.
---

# Git workflow

These are the user's standing preferences. They apply to whatever repository
is open, not to any one project. Where a repository documents something
stricter of its own — a `CONTRIBUTING.md`, a release checklist — that wins.
Standing preferences for every repository. Where a repository documents
something stricter of its own — a `CONTRIBUTING.md`, a release checklist —
that wins.

## Branch in a worktree, never on `main`

One worktree per line of work, made with the `EnterWorktree` tool; prefer it
over `git worktree add` so the session's working directory follows the
worktree instead of being left behind in the main checkout. Where the
worktree lands is the tool's business, not yours: with sinteractive's
`worktree-create` hook registered (`sinteractive claude install`) it is on
the cluster's scratch filesystem — `/scratch/alpine/$USER/worktrees/<repo>/
<name>` on Alpine — because a worktree is a throwaway build tree and
`/projects` is the small, backed-up tier; elsewhere it is Claude Code's
stock `<repo>/.claude/worktrees/<name>`. `git worktree list` says where.
Never symlink `.claude/worktrees` somewhere else: Claude Code refuses to
create a worktree through a symlink.

The base commit comes from the `worktree.baseRef` setting: `fresh` (the
default) branches from `origin/<default-branch>`, so the work starts from what
is actually on the remote rather than from whatever the local checkout has
drifted to; `head` branches from local `HEAD`, for work that genuinely builds
on uncommitted local history.

`.claude/worktrees/` is a byproduct of the workflow, not source. If the
repository does not already ignore it, add it to `.gitignore` — or to
`.git/info/exclude` when the ignore file is shared and the convention is not.
A worktree on scratch needs no ignoring; it is not inside the checkout.

Leave with `ExitWorktree`: `keep` while the branch is still in flight,
`remove` once the pull request has merged. A worktree outliving its branch is
a checkout of something that no longer exists.

Branch names are short kebab-case topics describing the change, reading much
the way the subject line does: `fix-stale-time-budget`, `ci-validate-shell`,
`install-claude-from-anywhere`.
One worktree per line of work, made with `EnterWorktree`. Where it lands is
the tool's business (`git worktree list` says; with sinteractive's hooks
registered it is on the cluster's scratch on Alpine). Never symlink
`.claude/worktrees` elsewhere — Claude Code refuses to create a worktree
through a symlink — and make sure the repository ignores it. Leave with
`ExitWorktree`: `keep` while the branch is in flight, `remove` once merged.

## Land through a pull request

```bash
git push -u origin HEAD
gh pr create --fill # then edit the body to say why
gh pr checks --watch # let CI go green before merging
gh pr merge
```
Branch names are short kebab-case topics: `fix-stale-time-budget`,
`ci-validate-shell`.

Nothing goes onto `main` directly — not a typo fix, not a version bump, not a
one-line revert. The pull request is where CI runs and where the reasoning is
recorded; a change that skips it has neither, and the gap only surfaces later,
when somebody asks why a line is the way it is.

Never force-push a branch someone else may have checked out, and never rewrite
history that is already on the remote.

## Conventional Commits
## Commit messages are short

```
type(scope): subject

Body explaining why, wrapped at 72.

BREAKING CHANGE: what callers must now do differently.
```

Types: `feat`, `fix`, `docs`, `chore`, `refactor`, `test`, `ci`, `perf`,
`build`, `revert`. The scope is optional and names the area touched
(`fix(nodes):`, `feat(claude):`); leave it off when the change is repo-wide.
`build`, `revert`. The scope is optional and names the area touched. The
subject is imperative, lowercase, no trailing period, under 72 characters,
and completes "this commit will …".

The subject is imperative, lowercase, no trailing period, and completes the
sentence "this commit will …". The body explains *why* — the failure mode
being fixed, the alternative that was rejected and what was wrong with it —
because the diff already says what changed and nothing else records the
reasoning. A mechanical change needs no body; a judgment call always does.
**Most commits are the subject line alone.** Add a body only when the diff
cannot say why — a non-obvious cause, a constraint that forced the shape —
and keep it to two or three lines. No narrative of what changed, what else
was tried, or how the problem was found. Mark a major bump with `!` before
the colon or a `BREAKING CHANGE:` footer.

Mark anything forcing a major version bump with either a `BREAKING CHANGE:`
footer or a `!` before the colon (`feat(api)!:`).
Match a repository whose history plainly follows another convention, and
never rewrite history that is on the remote.

Where a repository's recent history plainly follows a different convention,
match the repository rather than switching styles mid-log — and never rewrite
existing commits to conform.

## Semantic versioning

`MAJOR.MINOR.PATCH`: MAJOR when existing usage breaks, MINOR for
backwards-compatible additions, PATCH for fixes that change no interface.
Before 1.0.0 the guarantee shifts down a place — MINOR is where breaking
changes go, and users should expect them there.
## Land through a pull request

Tags are `v`-prefixed and **annotated**:
Nothing goes onto `main` directly — not a typo fix, not a version bump.
Before pushing, run what CI runs (`.github/workflows/*.yml`: usually a
formatter, a linter, tests); when those are real compute, give them their
own allocation (`hpc-compute`).

```bash
git tag -a v1.4.0 -m 'Release v1.4.0'
git push -u origin HEAD
gh pr create --fill # body: one to three sentences on why
gh pr checks --watch
gh pr merge --squash # once green
```

A lightweight tag is a bare pointer with no tagger, date, or message, so a
release cut that way leaves no record of when it was made or by whom.
Annotate every one.
Committing, pushing, opening the PR and watching CI is mechanical: the
`land` skill does it in a forked subagent on a cheaper model. Invoke it with
a one-line why once the work and its checks are done, rather than spending
the main conversation on it.

## Releasing
## Versions and releases

**Find every place the version is written before changing any of them.** It is
routinely more than one: a `VERSION=` in a script, `pyproject.toml`,
`package.json`, a `DESCRIPTION`, the `.TH` line of a man page, a docs config.
Grep for the current version string across the repository and bump the whole
set in one commit — a stale copy is invisible until a user reports that
`--version` disagrees with the tag.
`MAJOR.MINOR.PATCH`: MAJOR when existing usage breaks, MINOR for additions,
PATCH for fixes. Before 1.0.0, breaking changes go in MINOR.

Then, for a repository keeping a changelog in Keep a Changelog form: rename
`## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD`, open a fresh empty
`[Unreleased]` above it, and update the comparison links at the foot of the
file — `[Unreleased]` moves to `compare/vX.Y.Z...HEAD`, and a new `[X.Y.Z]`
link points at `compare/vPREV...vX.Y.Z`.
To release: grep the current version string across the repository and bump
every copy in one commit (a `VERSION=`, `Cargo.toml`, `pyproject.toml`, the
man page's `.TH` line, a docs config). In a Keep a Changelog file,
`[Unreleased]` becomes `[X.Y.Z] - YYYY-MM-DD` with a fresh empty
`[Unreleased]` above it, and the comparison links at the foot move. Commit
as `chore(release): vX.Y.Z` and land it through a pull request like
anything else. Then tag the merge commit on `main` — annotated, never
lightweight, so the tag records who cut it and when:

```bash
git commit -m 'chore(release): v1.4.0'
git checkout main && git pull
git tag -a v1.4.0 -m 'Release v1.4.0'
git push --follow-tags
gh release create v1.4.0 --generate-notes
```

A release is still a pull request. Tag the merge commit on `main`, not the
branch.

## Run the repository's own checks before pushing

Read `.github/workflows/*.yml` and run what CI runs, locally, first. The gates
are usually a linter, a formatter check, and a test suite, and they take
seconds by hand; discovering them from a red pull request costs a round trip
and leaves a failed run in the history for nothing.

When those checks are heavy enough to be real compute — a full test suite, a
build — they belong in their own Slurm allocation rather than in the session
shell. See the `hpc-compute` skill.
Loading
Loading