Skip to content
Open
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
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Copy this file to .env and fill in both values.
# Copy this file to .env and fill in the two required values.
# XAI_API_KEY is optional — Claude scripts work without it.
# .env is gitignored. This file is committed so the next person knows what to set.

# From platform.claude.com -> Settings -> API Keys
ANTHROPIC_API_KEY=""

# From weatherapi.com/signup.aspx (free tier, no credit card)
WEATHER_API_KEY=""

# From console.x.ai (optional — only the Grok lesson scripts need this)
XAI_API_KEY=""
5 changes: 2 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ jobs:
strategy:
fail-fast: false
matrix:
# 20.x is the supported floor (--env-file arrived in 20.6);
# 22.x is what the tutorial is written against.
node: ['20.x', '22.x']
# 22 is the supported floor (openai requires it). 24 is current LTS.
node: ['22.x', '24.x']

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ node_modules/
.env
usage.csv
.DS_Store
.grok/worktrees/
.claude/worktrees/
54 changes: 54 additions & 0 deletions .grok/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# `.grok/` — Grok Build project surface

Portable Grok configuration for this repository. **Checked into git** so any
clean clone shares the same contracts after `git pull`.

Claude Code continues to use `.claude/` and `~/.claude/`. The two drivers are
deliberately namespaced; see `skills/xmission/references/dual-driver.md`.

## What is tracked

| Path | Purpose |
|------|---------|
| `skills/xmission/` | **`/xmission`** — Grok mission contract (provision → execute → decommission) |
| `rules/dual-driver.md` | Always-on coexistence with Claude Code |
| `scripts/landed.sh` | Per-file land check used by `/xmission end` (no `~/.claude` dependency) |
| `README.md` | This file |

User-global Grok memory (when enabled): `~/.grok/memory/MEMORY.md` (not in this repo).

## What is not tracked

| Path | Why |
|------|-----|
| `.grok/worktrees/`, `.claude/worktrees/` | Long-lived mission checkouts; local to each machine |
| root `.env*` / `node_modules/` | Secrets and installs (already ignored repo-wide) |

Root `.gitignore` encodes the worktree roots. **Never `git clean -x` on main**
while any worktree exists — ignored mission dirs are still live checkouts
(`git worktree list` first).

## Dual machine

- **Pull the repo** — skills and rules appear under `.grok/` automatically.
- **Worktrees are not synced.** On the other machine: `git fetch`, then either
re-attach with `git worktree add .grok/worktrees/wt-<ref>-<stream> <branch>`
for a pushed branch, or provision fresh with `/xmission`.
- **Branches are shared** via `origin`. Push the mission branch before switching
machines if you intend to continue there.
- No home paths are **depended on** for xmission to run (docs may mention
`~/.claude/…` as do-not-touch references only).

## Invoke

In a Grok Build session rooted at this repo:

```
/xmission <issue# | pr# | description>
/xmission end
```

Natural language (“run an xmission for …”) should load the same skill.

Claude’s `/mission` remains Claude’s (user/global command). Do not treat it as
Grok law unless the operator says so.
22 changes: 22 additions & 0 deletions .grok/rules/dual-driver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Dual-driver rules (Grok + Claude)

Always on for Grok sessions in this repo. Claude sessions get the same table via
root `CLAUDE.md`. Detail: `.grok/skills/xmission/references/dual-driver.md`.

1. **Operator owns the merge.** Never merge a PR or claim ship authority unless the
operator explicitly orders it.
2. **`@claude` on PRs is the operator’s clean review process** when a
mention-triggered workflow exists. Do not post `@claude` unprompted. Do not
overwrite Claude review comments. Fix findings when directed.
3. **Namespaces**

| Driver | Worktrees | Branches | Port range |
|--------|-----------|----------|------------|
| Grok | `.grok/worktrees/` | `x/<type>/…` | **4000–4999** (`4000 + ref%1000`, then free-port bump) |
| Claude | `.claude/worktrees/` | house scheme | **3000–3999** (`3000 + ref%1000`) |

4. **One owner per kickoff.** The other driver may babysit or review read-only.
5. **Main checkout is integration ground** for both drivers. Feature commits only
in a mission worktree. Dirt on main is never yours to clean. Never
`git clean -x{d,f}` while any worktree exists (`git worktree list` first).
6. Full contract: `.grok/skills/xmission/` (`/xmission`).
91 changes: 91 additions & 0 deletions .grok/scripts/landed.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
# Repo copy for Grok /xmission decommission (tracked under .grok/scripts/).
# Portable — no home-dir dependency. Usage:
# .grok/scripts/landed.sh <branch> origin/<default>
#
# Exit 0 = fully landed and safe to decommission. Exit 1 = something is unlanded.
# Exit 2 = bad args / missing refs / internal git failure.
#
# Does NOT fetch. Callers must `git fetch origin` first so origin/<default> is current.
#
# Why a script (not a one-liner): the inline forms were wrong three times —
# 1. Ancestry (`branch --merged`, `git log --not <base>`) false-positives on
# squash-merged branches (squash rewrites the commit).
# 2. Plain two-dot `git diff <base> <branch>` false-positives as soon as the
# base moves; cannot tell "mine never landed" from "someone else's did".
# 3. `git rev-parse "ref:path" 2>/dev/null || echo none` — on a missing path
# rev-parse prints its ARGUMENT, so two ABSENT deletions look unequal forever.
# `--verify --quiet` (or empty ls-tree) is the fix.
# Hardened further (PR #63 review):
# 4. core.quotePath can C-quote non-ASCII paths → rev-parse misses both sides →
# ABSENT==ABSENT false "landed". Use quotePath=false + NUL-delimited names.
# 5. Blob-only compare misses mode-only changes (chmod). Compare ls-tree mode+sha.
# 6. Rename detection collapses rename to destination only — use --no-renames so
# the deletion half of a half-landed rename is still checked.
# 7. Pathspecs are glob-matched by default: a path with [ ] * ? can match a
# sibling file on both refs → false "landed". Use pathspec magic :(literal).
# 8. git diff in a process substitution hides its exit status from pipefail;
# a failed diff leaves touched=0 and would exit 0. Capture first, check status.
#
# Property: for each path the branch touched relative to the merge base, the tree
# entry (mode + blob sha) on BRANCH equals that on BASE; absent is first-class.
set -uo pipefail

BRANCH="${1:-$(git rev-parse --abbrev-ref HEAD)}"
BASE="${2:-origin/main}"

git rev-parse --verify --quiet "$BRANCH" >/dev/null || { echo "no such branch: $BRANCH" >&2; exit 2; }
git rev-parse --verify --quiet "$BASE" >/dev/null || { echo "no such base: $BASE" >&2; exit 2; }

base_commit=$(git merge-base "$BASE" "$BRANCH") || exit 2

# $1=ref $2=path -> "mode sha" or ABSENT
# :(literal) so [ ] * ? in the path are not pathspec wildcards.
entry() {
local out
out=$(git ls-tree "$1" -- ":(literal)$2" 2>/dev/null) || true
if [ -z "$out" ]; then
echo ABSENT
return
fi
# ls-tree line: <mode> <type> <sha>\t<path>
# One path with :(literal) should yield at most one line; take first fields only.
printf '%s\n' "$out" | awk '{ print $1 " " $3; exit }'
}

# Capture NUL-delimited path list first so a failed diff cannot look like "nothing
# changed" (process substitution would hide git's exit status from pipefail).
paths_tmp=
paths_tmp=$(mktemp) || { echo "mktemp failed" >&2; exit 2; }
trap 'rm -f "$paths_tmp"' EXIT

git -c core.quotePath=false diff -z --name-only --no-renames "$base_commit" "$BRANCH" >"$paths_tmp"
diff_status=$?
if [ "$diff_status" -ne 0 ]; then
echo "git diff failed (exit $diff_status) — cannot decide landed state" >&2
exit 2
fi

unlanded=0
touched=0
while IFS= read -r -d '' f; do
[ -n "$f" ] || continue
touched=$((touched + 1))
if [ "$(entry "$BRANCH" "$f")" = "$(entry "$BASE" "$f")" ]; then
echo "landed: $f"
else
echo "UNLANDED: $f"
unlanded=$((unlanded + 1))
fi
done <"$paths_tmp"

if [ "$touched" -eq 0 ]; then
echo "no files differ from the merge base — $BRANCH is contained in $BASE"
fi

if [ "$unlanded" -eq 0 ]; then
echo "OK: $touched file(s) checked, all landed on $BASE"
exit 0
fi
echo "STOP: $unlanded of $touched file(s) not on $BASE — do not decommission"
exit 1
Loading