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
62 changes: 32 additions & 30 deletions .claude/skills/crypter-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ Carry a requirement from a sentence to a draft pull request whose checks pass.
You own the whole run. Building and reviewing happen in the container; you hold the plan, the
findings and every CI attempt, which is why the judgement calls are yours.

Run from the root of the main checkout. The container's mounts resolve against it, so a run
started from a worktree writes its plan where the container cannot read it.
Run from the root of a checkout — a worktree does as well as a main one. `/plans` and `/runs`
resolve against it, and the container is named after it, so the one you reach is always the one
reading the plan you wrote.

There is one gate: the user approves the plan. Everything after it runs to a green draft pull
request, or to a written account of why CI would not take it.
Expand Down Expand Up @@ -40,43 +41,44 @@ The container needs both mounts, the run directory has to be writable from insid
image has to carry the current tooling. Confirm before starting:

```bash
docker exec crypter-pipeline test -d /plans/{run-id} && \
docker exec crypter-pipeline test -w /runs/{run-id}/findings && \
docker exec crypter-pipeline test -x /usr/local/bin/crypter-workspace
.devcontainer/pipeline.sh exec -- test -d /plans/{run-id} && \
.devcontainer/pipeline.sh exec -- test -w /runs/{run-id}/findings && \
.devcontainer/pipeline.sh exec -- test -x /usr/local/bin/crypter-workspace
```

The mount checks also settle which checkout the container belongs to: one created against a
different one reaches neither directory. The last check is separate because an older image
passes the first two and then fails at workspace creation with nothing but a missing executable
to go on. All three are `test` because `docker exec` runs a binary and not a shell, so a builtin
like `command -v` exits 127 whether or not the thing it was looking for is there.
`pipeline.sh` resolves the container from the checkout it sits in, so which container you get is
settled by where you are rather than by anything you check. The mount checks prove `/plans` and
`/runs` are both there and that uid 1001 can write the run directory. The last check is separate
because an older image passes the first two and then fails at workspace creation with nothing but
a missing executable to go on. All three are `test` because `docker exec` runs a binary and not a
shell, so a builtin like `command -v` exits 127 whether or not the thing it was looking for is
there.

**Do not `docker start` an exited container to fix any of this.** Mounts and image are fixed
when a container is created, so starting one built from another checkout, or from an older
image, brings back the same wrong container. Bring it up from here instead:
**Do not `docker start` an exited container to fix any of this.** The image is fixed when a
container is created, so starting an old one brings back the old tooling. Bring it up from here
instead:

```bash
docker compose -f .devcontainer/docker-compose.yml up -d --build
.devcontainer/pipeline.sh up
```

That rebuilds the image and recreates the container against this checkout's mounts. It replaces
any container of the same name, so **ask the user before running it** — theirs may belong to
another checkout and hold work you cannot see.
That rebuilds the image and recreates this checkout's container. It cannot touch another
checkout's. What it does destroy is every workspace under `/work` in *this* container, because
`/work` is the container's own filesystem and not a volume — so **ask the user before running it
if another run may be live here.**

Then make the workspace the container builds in. It is a clone of your repository, taken from
the read-only `/host-git` mount, and it lasts exactly as long as this run:
Then make the workspace the container builds in. It is a clone of the repository taken from
GitHub, and it lasts exactly as long as this run:

```bash
git fetch origin
docker exec crypter-pipeline crypter-workspace create {run-id}
.devcontainer/pipeline.sh exec -- crypter-workspace create {run-id}
```

Fetch first — the workspace takes its `origin/stable` from yours, so a stale remote-tracking ref
puts the whole run on an old base. A change of your own targets `stable`, which is what `create`
uses when no `--base` is given. **If either fails, stop and say so.**
A change of your own targets `stable`, which is what `create` uses when no `--base` is given.
**If it fails, stop and say so.**

The workspace holds only committed history. Uncommitted work in your checkout is not visible to
the container and never reaches the branch.
The workspace takes `stable` as the repository holds it, so nothing about your checkout — what
it is on, how stale it is, what is uncommitted in it — reaches the branch.

## 1. Plan

Expand All @@ -88,7 +90,7 @@ It settles the plan with the user itself. **Do not continue until they have appr
## 2. Build

```bash
docker exec -w /work/{run-id} crypter-pipeline \
.devcontainer/pipeline.sh exec -w /work/{run-id} -- \
claude --permission-mode auto -p "/crypter-devcontainer-implement {run-id} {branch}"
```

Expand All @@ -97,7 +99,7 @@ Keep the title and description it reports; `crypter-step-open-pull-request` need
## 3. Examine

```bash
docker exec -w /work/{run-id} crypter-pipeline \
.devcontainer/pipeline.sh exec -w /work/{run-id} -- \
claude --permission-mode auto -p "/crypter-devcontainer-examine {run-id} {branch} origin/stable /plans/{run-id}/plan.md"
```

Expand All @@ -123,7 +125,7 @@ them.
Where anything was accepted:

```bash
docker exec -w /work/{run-id} crypter-pipeline \
.devcontainer/pipeline.sh exec -w /work/{run-id} -- \
claude --permission-mode auto -p "/crypter-devcontainer-remediate {run-id} {branch} /runs/{run-id}/triage.md"
```

Expand Down Expand Up @@ -159,7 +161,7 @@ The branch is on the fork and the artifacts are on your disk, so the workspace h
to hold:

```bash
docker exec crypter-pipeline crypter-workspace remove {run-id}
.devcontainer/pipeline.sh exec -- crypter-workspace remove {run-id}
```

Remove it on every exit path, including the ones where you stopped early. Nothing under `/runs`
Expand Down
63 changes: 32 additions & 31 deletions .claude/skills/crypter-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ your own inside it is a claim made in someone else's name.

You are given a pull request number: `/crypter-review {pr-number}`.

Run from the root of the main checkout. The container's mounts resolve against it.
Run from the root of a checkout. `/plans` and `/runs` resolve against it, and the container is
named after it, so the one you reach is always the one whose artifacts you are reading.

Use `pr-{number}` as the run id.

Expand All @@ -40,35 +41,35 @@ The container's `agent` is uid 1001 and your files are uid 1000, so the agents w
directories this side creates and grants. Creating them here also keeps you able to delete what
they wrote.

Then confirm the running container is the one this checkout describes, before anything depends
on it:
Then confirm the container is up and current, before anything depends on it:

```bash
docker exec crypter-pipeline test -w /runs/pr-{number}/findings && \
docker exec crypter-pipeline test -w /runs/pr-{number}/verification && \
docker exec crypter-pipeline test -x /usr/local/bin/crypter-workspace
.devcontainer/pipeline.sh exec -- test -w /runs/pr-{number}/findings && \
.devcontainer/pipeline.sh exec -- test -w /runs/pr-{number}/verification && \
.devcontainer/pipeline.sh exec -- test -x /usr/local/bin/crypter-workspace
```

The first proves the `/runs` mount reaches the directory you just made, which a container
created against a different checkout will not. The second proves the image carries the current
tooling. A container that fails either is not this checkout's, and every later step fails
against it in a way that reads like something else — a missing executable, findings written
somewhere you never look.
`pipeline.sh` resolves the container from the checkout it sits in, so which container you get is
settled by where you are rather than by anything you check. What the probes are for is the rest:
the first two prove `/runs` is mounted and that uid 1001 can write the directories you just
made, and the third proves the image carries the current tooling. Without them a later step
fails in a way that reads like something else — a missing executable, findings written somewhere
you never look.

Both are `test` because `docker exec` runs a binary and not a shell, so a builtin like
All three are `test` because `docker exec` runs a binary and not a shell, so a builtin like
`command -v` exits 127 whether or not the thing it was looking for is there.

**Do not `docker start` an exited container to fix this.** Mounts and image are fixed when a
container is created, so starting one built from another checkout, or from an older image,
brings back the same wrong container. Bring it up from here instead:
**Do not `docker start` an exited container to fix this.** The image is fixed when a container is
created, so starting an old one brings back the old tooling. Bring it up from here instead:

```bash
docker compose -f .devcontainer/docker-compose.yml up -d --build
.devcontainer/pipeline.sh up
```

That rebuilds the image and recreates the container against this checkout's mounts. It replaces
any container of the same name, so **ask the user before running it** — theirs may belong to
another checkout and hold work you cannot see.
That rebuilds the image and recreates this checkout's container. It cannot touch another
checkout's. What it does destroy is every workspace under `/work` in *this* container, because
`/work` is the container's own filesystem and not a volume — so **ask the user before running it
if another run may be live here.**

## 1. Read the pull request

Expand All @@ -87,28 +88,28 @@ against the branch the pull request actually names.

## 2. Fetch it into a workspace

The container has no network remote. It clones from your repository through a read-only mount,
so the pull request head goes into your repository first and travels across from there:
The container clones from the repository itself, so the pull request head comes straight from
GitHub and nothing has to be staged in your checkout first:

```bash
git fetch origin +refs/pull/{number}/head:refs/pr/{number} {base-branch}
docker exec crypter-pipeline crypter-workspace create pr-{number} \
--base {base-branch} '+refs/pr/{number}:refs/heads/pr-{number}'
.devcontainer/pipeline.sh exec -- crypter-workspace create pr-{number} \
--base {base-branch} '+refs/pull/{number}/head:refs/heads/pr-{number}'
```

The refspec is forced, so reviewing a pull request again after its author rebased or amended
picks up the new head instead of being rejected. The base branch is fetched alongside it because
the workspace clones your repository, and a base you have never fetched is not there to diff
against.
picks up the new head instead of being rejected. The base branch needs no fetching of its own —
the clone brings every branch the repository has.

**If either fails, stop and say so.**
Whatever your checkout is on, and however stale it is, does not reach the review.

**If it fails, stop and say so.**

The workspace lasts for this review and no longer.

## 3. Examine

```bash
docker exec -w /work/pr-{number} crypter-pipeline \
.devcontainer/pipeline.sh exec -w /work/pr-{number} -- \
claude --permission-mode auto -p "/crypter-devcontainer-examine pr-{number} pr-{number} origin/{base-branch}"
```

Expand Down Expand Up @@ -140,7 +141,7 @@ finding the author has to answer. So every finding is ruled on against the code
anywhere — by a verifier in the container, one per finding, not by you.

```bash
docker exec -w /work/pr-{number} crypter-pipeline \
.devcontainer/pipeline.sh exec -w /work/pr-{number} -- \
claude --permission-mode auto -p "/crypter-devcontainer-verify pr-{number} pr-{number} /runs/pr-{number}/findings/"
```

Expand Down Expand Up @@ -195,7 +196,7 @@ retry it against a different line.**
## 6. Tear down and report

```bash
docker exec crypter-pipeline crypter-workspace remove pr-{number}
.devcontainer/pipeline.sh exec -- crypter-workspace remove pr-{number}
```

Remove it on every exit path, including the ones where you stopped early. The findings and
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/crypter-step-open-pull-request/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The branch lives in the run's workspace. `git` reaches it over `docker exec`:

```bash
git -c protocol.ext.allow=user fetch \
"ext::docker exec -i crypter-pipeline git upload-pack /work/{run-id}" {branch}:{branch}
"ext::docker exec -i $(.devcontainer/pipeline.sh name) git upload-pack /work/{run-id}" {branch}:{branch}
```

`protocol.ext.allow` is passed per command and stays out of your config. **If this fails, stop
Expand Down
52 changes: 26 additions & 26 deletions .claude/skills/crypter-triage-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ recorded as verified, and the verified ones become commits.
Findings come from anywhere — the reviewer lenses, a person, another tool. They are treated the
same way, because where a finding came from says nothing about whether it is true.

Run from the root of the main checkout. The container's mounts resolve against it.
Run from the root of a checkout. `/plans` and `/runs` resolve against it, and the container is
named after it, so the one you reach is always the one whose artifacts you are reading.

## Setup

Expand All @@ -27,34 +28,34 @@ chmod 777 .claude/runs/pr-{number} .claude/runs/pr-{number}/verification
The container's `agent` is uid 1001 and your files are uid 1000, so the agents write into
directories this side creates and grants.

Then confirm the running container is the one this checkout describes, before anything depends
on it:
Then confirm the container is up and current, before anything depends on it:

```bash
docker exec crypter-pipeline test -w /runs/pr-{number}/verification && \
docker exec crypter-pipeline test -x /usr/local/bin/crypter-workspace
.devcontainer/pipeline.sh exec -- test -w /runs/pr-{number}/verification && \
.devcontainer/pipeline.sh exec -- test -x /usr/local/bin/crypter-workspace
```

The first proves the `/runs` mount reaches the directory you just made, which a container
created against a different checkout will not. The second proves the image carries the current
tooling. A container that fails either is not this checkout's, and every later step fails
against it in a way that reads like something else — a missing executable, verification written
somewhere you never look.
`pipeline.sh` resolves the container from the checkout it sits in, so which container you get is
settled by where you are rather than by anything you check. What the probes are for is the rest:
the first proves `/runs` is mounted and that uid 1001 can write the directory you just made, and
the second proves the image carries the current tooling. Without them a later step fails in a way
that reads like something else — a missing executable, verification written somewhere you never
look.

Both are `test` because `docker exec` runs a binary and not a shell, so a builtin like
`command -v` exits 127 whether or not the thing it was looking for is there.

**Do not `docker start` an exited container to fix this.** Mounts and image are fixed when a
container is created, so starting one built from another checkout, or from an older image,
brings back the same wrong container. Bring it up from here instead:
**Do not `docker start` an exited container to fix this.** The image is fixed when a container is
created, so starting an old one brings back the old tooling. Bring it up from here instead:

```bash
docker compose -f .devcontainer/docker-compose.yml up -d --build
.devcontainer/pipeline.sh up
```

That rebuilds the image and recreates the container against this checkout's mounts. It replaces
any container of the same name, so **ask the user before running it** — theirs may belong to
another checkout and hold work you cannot see.
That rebuilds the image and recreates this checkout's container. It cannot touch another
checkout's. What it does destroy is every workspace under `/work` in *this* container, because
`/work` is the container's own filesystem and not a volume — so **ask the user before running it
if another run may be live here.**

## 1. Collect the findings

Expand All @@ -77,24 +78,23 @@ about intent. A question is for the author to answer, not for a verifier.

## 2. Fetch the head into a workspace

The container has no network remote. It clones from your repository through a read-only mount,
so the head goes into your repository first and travels across from there:
The container clones from the repository itself, so the head comes straight from GitHub and
nothing has to be staged in your checkout first:

```bash
git fetch origin +refs/pull/{number}/head:refs/pr/{number}
docker exec crypter-pipeline crypter-workspace create pr-{number} \
'+refs/pr/{number}:refs/heads/{head-branch}'
.devcontainer/pipeline.sh exec -- crypter-workspace create pr-{number} \
'+refs/pull/{number}/head:refs/heads/{head-branch}'
```

The branch in the workspace takes the pull request's own branch name, so the commits go back to
the branch they came from.

**If either fails, stop and say so.**
**If it fails, stop and say so.**

## 3. Verify

```bash
docker exec -w /work/pr-{number} crypter-pipeline \
.devcontainer/pipeline.sh exec -w /work/pr-{number} -- \
claude --permission-mode auto -p "/crypter-devcontainer-verify pr-{number} {head-branch} /runs/pr-{number}/review.md"
```

Expand Down Expand Up @@ -126,7 +126,7 @@ thread.
Where `triage.md` has anything, and the head branch is one you can push to:

```bash
docker exec -w /work/pr-{number} crypter-pipeline \
.devcontainer/pipeline.sh exec -w /work/pr-{number} -- \
claude --permission-mode auto -p "/crypter-devcontainer-remediate pr-{number} {head-branch} /runs/pr-{number}/triage.md"
```

Expand All @@ -139,7 +139,7 @@ stands, and the author does the fixing. Say so in the report.
## 6. Tear down and report

```bash
docker exec crypter-pipeline crypter-workspace remove pr-{number}
.devcontainer/pipeline.sh exec -- crypter-workspace remove pr-{number}
```

Remove it on every exit path, including the ones where you stopped early. The verdicts under
Expand Down
3 changes: 3 additions & 0 deletions .devcontainer/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
CRYPTER_GIT_EMAIL=""
CRYPTER_GIT_NAME=""

# The repository workspaces are cloned from. Set it to work against a fork.
#CRYPTER_REPO_URL="https://github.com/Crypter-File-Transfer/Crypter.git"
5 changes: 0 additions & 5 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,6 @@ RUN dotnet tool install dotnet-ef --version '10.0.*' --tool-path "${DOTNET_TOOLS
COPY .devcontainer/workspace.sh /usr/local/bin/crypter-workspace
RUN chmod +x /usr/local/bin/crypter-workspace

# /host-git is the host's repository, owned by the host user. Git refuses to read a repository
# owned by anyone else until it is named as safe, and the whole point of the mount is that it
# belongs to somebody else.
RUN git config --system --add safe.directory /host-git

# The caches and the agent's Claude Code state are named volumes. Docker creates a mount point
# that the image does not already contain as root, so creating these here is what gives the
# volumes the right ownership. /work holds the ephemeral workspaces and is not a volume.
Expand Down
Loading
Loading