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
250 changes: 180 additions & 70 deletions .claude/skills/gauntlet-loop/SKILL.md

Large diffs are not rendered by default.

34 changes: 31 additions & 3 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,41 @@ FROM mcr.microsoft.com/devcontainers/javascript-node:22
# failed command at a time and falls back to reading whole files — the exact cost
# this methodology exists to remove.
#
# Pinned, and that is not tidiness. `npm install -g <pkg>` with no version is a
# layer whose hash never changes, so Docker serves the first build's answer for as
# long as the cache survives while the registry and this workspace both move on —
# and the rebuild that looks like it fixed that did not. Measured: an image rebuilt
# the same day carried scc v0.23.0 into a workspace whose settings.json registers
# the UserPromptSubmit hook v0.24.0 added, so every prompt of the session answered
# `unknown hook stage "user-prompt"`. Bumping an ARG is what invalidates the layer.
#
# SCC_VERSION is the one that has to be kept in step, because it is the only one
# of the three with a contract to this repository: scc scaffolds the harness hooks
# in the harness's own settings.json and this scc is what runs them, so a binary
# older than the scaffold is a vocabulary the container does not have. Keep it at
# or above the version that scaffolded the workspace. The other two answer to
# nobody here — Claude Code updates itself at runtime — and are pinned only so a
# rebuild is reproducible.
#
# As `node` rather than as root, because this image puts that user's global prefix
# (/usr/local/share/npm-global) on PATH and a root install would land somewhere
# the agent does not look.
ARG SCC_VERSION=0.25.0
ARG CLAUDE_CODE_VERSION=2.1.273
ARG CODEGRAPH_VERSION=1.6.0
RUN su node -c "npm install -g \
@anthropic-ai/claude-code \
@protonspy/scc \
@colbymchenry/codegraph"
@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} \
@protonspy/scc@${SCC_VERSION} \
@colbymchenry/codegraph@${CODEGRAPH_VERSION}"

# Where the credential volume lands, created here rather than left to the mount.
# Docker copies an image path's content *and its ownership* into a fresh named
# volume only when that path already exists; mount onto a path the image never
# created and Docker creates it root-owned, so the agent's first write under it
# fails — measured, as an EACCES on the per-session directory it makes at every
# session start, with the login it could not store then reported back as "not
# logged in".
RUN mkdir -p /home/node/.claude && chown node:node /home/node/.claude

# `gh` — the delivery flow opens the pull request with it, and `scc validate --pr`
# reads the PR body through it.
Expand Down
35 changes: 35 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,3 +370,38 @@ Tests live beside the code and lean on a few package-local helpers rather than a
Conventional Commits, scoped by package or surface, with a descriptive subject written as a claim about behavior — e.g. `feat(cli): return exit 2 when spec validation reports findings`. Changes land through PRs on `main`.

**No attribution, in a commit message or a PR body.** No `Co-Authored-By` for an assistant, no session link, no generated-with footer, no naming of a model, vendor, or harness. This is the rule scc ships in `rules/delivery.md`, and it binds scc's own history first — a tool that told every workspace not to sign its work and then signed its own would be worth ignoring on both counts. Harness defaults append these unless told not to, so a session working here overrides that default rather than following it.

<!-- scc:codegraph-instructions v1 -->
## CodeGraph
Ask the symbol graph before reading files. "Who calls this", "what breaks if I change it",
"where does this concept live" are one command here and a dozen reads otherwise.

- `scc graph explore "<question>"` — the relevant symbols' source plus the call paths between them. Start here.
- `scc graph query <name> [--kind function|class] [--limit N]` — find a symbol by name.
- `scc graph status` — what the graph holds. `--check` exits 2 when there is none.
- `scc graph sync` — re-index after you have written code you then need to search.
- `scc graph build [--force]` — first index, or a full rebuild when the graph has gone wrong.

`scc launch` indexes before the session starts, so the graph is current at turn one.
It goes stale as you edit: sync before searching for something you just wrote.
The graph is CodeGraph's — never edit `.codegraph/`, and never commit it.
<!-- /scc:codegraph-instructions -->

<!-- rtk-instructions v2 -->
## RTK
Prefix EVERY command with `rtk`, including each link in a `&&` chain (`rtk git add . && rtk git commit -m "x"`).
No dedicated filter means it passes through unchanged — always safe.

Covered:
- cargo build/check/clippy/test, go test, tsc, lint, prettier, next build
- jest, vitest, playwright, pytest, rspec, rake test, test `<cmd>`
- git (all subcommands)
- gh pr view/checks, gh run list, gh issue list, gh api
- pnpm, npm run, npx, prisma, uv run
- ls, read, grep, find
- err, log, json, deps, env, summary, diff
- docker, kubectl, curl, wget

Meta: `rtk gain [--history]`, `rtk discover`, `rtk proxy <cmd>` (no filtering), `rtk init [--global]`
Caveat: `rtk grep` with `-c -l -L -o -Z` runs raw.
<!-- /rtk-instructions -->
15 changes: 14 additions & 1 deletion internal/assets/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,20 @@ import (
// and clear this artifact's findings". That moved into the skill bodies, where it is
// paid by the run that invokes it instead of by every session; `argument-hint` moved
// with it, since SKILL.md takes the same key.
const Version = "32"
//
// 33: the dev container stops handing the agent a stale binary and a home it cannot
// write. Two failures measured in one session, both of them Docker's defaults rather
// than anything scc decided. An unversioned `npm install -g` is a layer whose hash
// never changes, so an image rebuilt the same day still carried scc v0.23.0 while
// this workspace's settings.json registered the UserPromptSubmit hook v0.24.0 added
// — `unknown hook stage "user-prompt"`, on every prompt of the session, from a
// rebuild that looked like it had fixed it. The three tools are pinned to ARGs now,
// and SCC_VERSION is the one with a contract to the workspace it runs in. And a
// named volume mounted onto a path the image never created is created root-owned, so
// `/home/node/.claude` is created and chowned in the image: without it the first
// write of every session was `EACCES ... mkdir '/home/node/.claude/session-env'` and
// the login it could not store read back as "not logged in".
const Version = "33"

// The embedded tree. "all:" so nothing is silently dropped for having a name the
// default embed pattern skips.
Expand Down
2 changes: 1 addition & 1 deletion internal/assets/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1075,7 +1075,7 @@ func (e errNoSuchRule) Error() string {
// uses wherever a change is cheap to make and expensive to make by accident.
func TestTheTemplateVersionMovesWithTheTemplates(t *testing.T) {
// Bump Version, then replace this with the digest the failure prints.
const fingerprint = "220a7b9ef576f9f83fcfd3f7268952e34cbdc66542ee8be26e4cf755180ca373"
const fingerprint = "5f4a989994ef836fd039f1f188f1027ee4924164a0ae7a6ebd30ce345f6f6353"

sum := sha256.New()
// Version goes into the hash, and without it this test does not do the job its
Expand Down
34 changes: 31 additions & 3 deletions internal/assets/templates/devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,41 @@ FROM mcr.microsoft.com/devcontainers/javascript-node:22
# failed command at a time and falls back to reading whole files — the exact cost
# this methodology exists to remove.
#
# Pinned, and that is not tidiness. `npm install -g <pkg>` with no version is a
# layer whose hash never changes, so Docker serves the first build's answer for as
# long as the cache survives while the registry and this workspace both move on —
# and the rebuild that looks like it fixed that did not. Measured: an image rebuilt
# the same day carried scc v0.23.0 into a workspace whose settings.json registers
# the UserPromptSubmit hook v0.24.0 added, so every prompt of the session answered
# `unknown hook stage "user-prompt"`. Bumping an ARG is what invalidates the layer.
#
# SCC_VERSION is the one that has to be kept in step, because it is the only one
# of the three with a contract to this repository: scc scaffolds the harness hooks
# in the harness's own settings.json and this scc is what runs them, so a binary
# older than the scaffold is a vocabulary the container does not have. Keep it at
# or above the version that scaffolded the workspace. The other two answer to
# nobody here — Claude Code updates itself at runtime — and are pinned only so a
# rebuild is reproducible.
#
# As `node` rather than as root, because this image puts that user's global prefix
# (/usr/local/share/npm-global) on PATH and a root install would land somewhere
# the agent does not look.
ARG SCC_VERSION=0.25.0
ARG CLAUDE_CODE_VERSION=2.1.273
ARG CODEGRAPH_VERSION=1.6.0
RUN su node -c "npm install -g \
@anthropic-ai/claude-code \
@protonspy/scc \
@colbymchenry/codegraph"
@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} \
@protonspy/scc@${SCC_VERSION} \
@colbymchenry/codegraph@${CODEGRAPH_VERSION}"

# Where the credential volume lands, created here rather than left to the mount.
# Docker copies an image path's content *and its ownership* into a fresh named
# volume only when that path already exists; mount onto a path the image never
# created and Docker creates it root-owned, so the agent's first write under it
# fails — measured, as an EACCES on the per-session directory it makes at every
# session start, with the login it could not store then reported back as "not
# logged in".
RUN mkdir -p /home/node/.claude && chown node:node /home/node/.claude

# `gh` — the delivery flow opens the pull request with it, and `scc validate --pr`
# reads the PR body through it.
Expand Down
57 changes: 57 additions & 0 deletions internal/cli/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,63 @@ func TestInitSeedsTheDevContainer(t *testing.T) {
}
}

// The image pins the one tool whose vocabulary the workspace depends on.
//
// `npm install -g <pkg>` with no version is a layer whose hash never changes, so
// Docker keeps serving the first build's answer while the registry moves on — and
// the rebuild that looks like it fixed that did not. Measured: an image rebuilt the
// same day carried scc v0.23.0 into a workspace whose settings.json registered the
// UserPromptSubmit hook a later version added, and every prompt of the session
// answered `unknown hook stage "user-prompt"`. scc is the one of the three with a
// contract to the files around it, so it is the one this test holds; Claude Code
// updates itself at runtime and codegraph answers to nobody here.
func TestTheSeededImagePinsTheToolItMustMatch(t *testing.T) {
root := initWorkspace(t)
docker, err := os.ReadFile(filepath.Join(root, paths.DevcontainerSeg, "Dockerfile"))
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
// The install has to name a version, and the version has to be a build arg
// rather than a literal in the RUN line: bumping the arg is what invalidates
// the layer, and a literal buried in a shell string is not a knob.
if !strings.Contains(string(docker), "ARG SCC_VERSION=") {
t.Errorf("the image does not pin scc to a build arg:\n%s", docker)
}
if !strings.Contains(string(docker), "@protonspy/scc@${SCC_VERSION}") {
t.Errorf("the image installs scc unversioned, so Docker's layer cache picks the version:\n%s", docker)
}
}

// And it creates the directory the credential volume lands on.
//
// Docker copies an image path's content *and its ownership* into a fresh named
// volume only when that path already exists. Mount onto a path the image never
// created and Docker creates it root-owned, so the agent — which runs as node —
// cannot write its own state: measured as `EACCES: permission denied, mkdir
// '/home/node/.claude/session-env'` at every session start, with the login it could
// not store then reported back as "not logged in".
//
// Asserted across both files, because neither states it alone: the Dockerfile
// creates the path and devcontainer.json is what mounts a volume over it.
func TestTheSeededImageOwnsTheCredentialDirectory(t *testing.T) {
root := initWorkspace(t)
docker, err := os.ReadFile(filepath.Join(root, paths.DevcontainerSeg, "Dockerfile"))
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
cfg, err := os.ReadFile(filepath.Join(root, paths.DevcontainerSeg, "devcontainer.json"))
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
const home = "/home/node/.claude"
if !strings.Contains(string(cfg), "target="+home+",type=volume") {
t.Fatalf("the seeded config does not mount a volume at %s, so this test is checking the wrong path:\n%s", home, cfg)
}
if !strings.Contains(string(docker), "mkdir -p "+home) || !strings.Contains(string(docker), "chown node:node "+home) {
t.Errorf("the image does not create %s as node, so the volume lands root-owned:\n%s", home, docker)
}
}

// The two seeded files describe one container between them, and the one thing
// neither can state alone is the user. devcontainer.json names it as remoteUser
// and mounts the agent's credentials into that home; the Dockerfile drops to it
Expand Down
Loading