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
10 changes: 10 additions & 0 deletions .claude/agents/test-author.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
name: test-author
description: Writes failing tests from a behavior contract without reading the implementation. Use for the red stage of a tdd work order when no second harness is available to author the tests.
---

Adopt the role card at `ops/process/roles/test-author.md` and follow it
exactly. Read the card first. You are the in-harness fallback for this role —
a second harness (Codex or Grok, per `ops/process/cross-review.md`) is
preferred when available, and the caller must say in the PR when the
fallback was used.
10 changes: 10 additions & 0 deletions .claude/agents/test-skeptic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
name: test-skeptic
description: Adversarially reviews tests for shapes that pass over broken code. Use before recording green on a tdd work order when no third harness is available to judge the tests.
---

Adopt the role card at `ops/process/roles/test-skeptic.md` and follow it
exactly. Read the card first. You are the in-harness fallback for this role —
a harness that wrote neither the tests nor the implementation is preferred
(`ops/process/cross-review.md`), and the caller must say in the PR when the
fallback was used.
8 changes: 8 additions & 0 deletions .claude/skills/bdd/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
name: bdd
description: Given/When/Then behavior scenarios before tests or code, kept traceable to tests. Use at the spec stage of a work order, or when behavior needs agreeing on before implementation.
---

Read and follow `ops/process/bdd.md` — the canonical process document,
shared by every harness that works this repo. Scenarios land under
`.dev/design/features/`; when they are done, hand off per `ops/process/tdd.md`.
10 changes: 10 additions & 0 deletions .claude/skills/cross-review/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
name: cross-review
description: Have the other two harnesses (Codex, Grok — or Claude when another harness leads) review work against a detached snapshot. Use before merging test-bearing or evidence-bearing changes.
---

Read and follow `ops/process/cross-review.md` — the canonical process
document. The two rules that must survive any summary: reviewers get a
detached snapshot, never the live worktree; and only the owner triggers
`@codex review` on GitHub — the local `codex exec` reviewer is a different
thing and is yours to run.
38 changes: 38 additions & 0 deletions .claude/skills/dev-lane/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
name: dev-lane
description: The dev-lane pipeline — plan (Fable), tests (Grok), check the tests (Codex), code (Opus), review the code (Grok AND Codex, both). NO PRODUCER OWNS TWO CONSECUTIVE ARTIFACTS: whoever writes an implementation does not write or approve its tests. Scope is settled in session with the owner, then handed off. Load before starting any change under .dev/, and before dispatching any harness.
---

Read and follow `ops/process/pipeline.md` — the canonical sequence.
`ops/process/cross-review.md` covers review only; reading it alone gives
three roles where there are six.

Three rules that must survive any summary:

- **No producer owns two consecutive artifacts.** The specific models
matter less than that constraint. Measured twice on this repo: nine
defects past a green suite whose tests and code shared an author, and
eleven of fourteen review findings against two checkers being missing
test cases rather than coding errors.

- **The firewall is proved, not intended.** Withholding is invisible — a
snapshot that leaked the wrong file looks exactly like one that did
not. Prove both directions before dispatching: nothing matching a
withheld pattern present, *and* something matching every given pattern
present. An empty snapshot satisfies the first perfectly.

- **Contracts are extracted from an app's contract document, never from a
plan.** That is what keeps a plan disposable; the plan it was learned
from could not be retired because 1,236 citations under
`.dev/app/workflow/contracts/` point at it.

Scope is settled in session with the owner; the plan is not. State
*what* and the boundaries, and leave *how* to the planner. Review goes
to two harnesses, not one, so the rule holds whoever is driving — and a
CHANGES verdict is ruled on by a harness that produced neither the
artifact nor the finding, never by the producer, before any of it is
worked.

Reach: Claude loads this natively and Grok through claude-compat. **Codex
does not see repo skills** — its copy of these rules is `AGENTS.md`, which
every harness reads.
11 changes: 11 additions & 0 deletions .claude/skills/tdd/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
name: tdd
description: The wf-governed red→green loop — honest reds, sealed frozen sets, independent tests. Use when implementing anything under a tdd gate kind, or starting test-first work.
---

Read and follow `ops/process/tdd.md` — it is the canonical process document,
shared by every harness that works this repo (Claude, Codex, Grok), so it is
not duplicated here. When a step calls for the test-author or test-skeptic
role, prefer a different harness via `ops/process/cross-review.md`; the
`test-author` and `test-skeptic` subagents are the fallback when you must
fill a role in-harness.
10 changes: 7 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: shivammathur/setup-php@v2
- name: term wall
uses: minspec/.github/.github/actions/term-wall@6175b67bd0df4710dd0bb8b79df413b452265c6c # minspec/.github#8
env:
TERM_WALL: ${{ vars.TERM_WALL }}
- uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: '8.4'
- run: composer validate --strict
- uses: actions/cache@v4
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.cache/composer/files
key: composer-${{ hashFiles('composer.lock') }}
Expand Down
3 changes: 3 additions & 0 deletions ops/bin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Owner-owned, machine-specific — copy from dispatch.conf.example.
dispatch.conf
term-wall.conf
21 changes: 21 additions & 0 deletions ops/bin/dispatch.conf.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copy to dispatch.conf. The real dispatch.conf is gitignored: it is
# owner-owned, machine-specific config sourced by dispatch.sh, never a
# public artifact. A missing conf refuses loudly — it is not inferred.
#
# LAUNCHER_PIN is optional. Left empty, dispatch.sh finds the launcher next
# to itself (../devlane/dispatch/launch.py) — portable across worktrees and
# machines. Set it to an ABSOLUTE launch.py path only if you want the hard
# supply-chain lock (dispatch then runs exactly that launcher and no other).
#
# Owner allow rule (adding it is the owner's act):
# "Bash(bash /ABSOLUTE/PATH/TO/minspec/workbench/ops/bin/dispatch.sh:*)"
# Values may be overridden per call by an environment variable of the same
# name, then by the corresponding flag.

LAUNCHER_PIN=''
DEFAULT_TIMEOUT='300'
WF_AGENT_DEFAULT='GPT-5.6 Sol <noreply@openai.com>'
MODEL_GROK='grok-4.6'
MODEL_CODEX='gpt-5.6-sol'
MODEL_CLAUDE_READ='claude-opus-5'
MODEL_CLAUDE_PLAN='claude-fable-5'
144 changes: 144 additions & 0 deletions ops/bin/dispatch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env bash
set -uo pipefail

refuse() {
printf 'dispatch: refusal: %s\n' "$*" >&2
exit 3
}

script_dir=$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
conf_path=${DISPATCH_CONF:-"$script_dir/dispatch.conf"}
[[ -f $conf_path ]] || refuse "expected a sourced conf; found no file at $conf_path; set DISPATCH_CONF to dispatch.conf"

# LAUNCHER_PIN is resolved after sourcing, not required here: by default the
# launcher is found relative to this script (portable across worktrees and
# machines, and no home path leaks into a public conf). An operator who wants
# the hard supply-chain lock may still set an absolute LAUNCHER_PIN in the conf.
keys=(DEFAULT_TIMEOUT WF_AGENT_DEFAULT MODEL_GROK MODEL_CODEX MODEL_CLAUDE_READ MODEL_CLAUDE_PLAN)
for key in "${keys[@]}"; do
if [[ -v $key ]]; then
printf -v "saved_$key" '%s' "${!key}"
printf -v "had_$key" '%s' 1
else
printf -v "had_$key" '%s' 0
fi
done
# shellcheck source=/dev/null
source "$conf_path" || refuse "expected a sourceable conf; found an error in $conf_path; fix its shell assignments"
for key in "${keys[@]}"; do
had="had_$key"
saved="saved_$key"
if [[ ${!had} == 1 ]]; then
printf -v "$key" '%s' "${!saved}"
fi
[[ -v $key ]] || refuse "expected conf key $key; found it unset; define $key in $conf_path"
done

# Resolve the launcher. An absolute LAUNCHER_PIN from the conf or environment
# wins (the operator's hard lock); otherwise default to this script's sibling
# launcher, so a fresh checkout or worktree runs without editing any path.
if [[ -z ${LAUNCHER_PIN:-} ]]; then
LAUNCHER_PIN="$script_dir/../devlane/dispatch/launch.py"
fi
LAUNCHER_PIN=$(realpath -e -- "$LAUNCHER_PIN" 2>/dev/null) \
|| refuse "expected LAUNCHER_PIN to name a launcher; found none at the resolved path; place launch.py or set LAUNCHER_PIN"

branch=''
worktree=''
job=''
harness=''
model=''
unit=''
stage=''
scope_file=''
input=''
follows=''
timeout=$DEFAULT_TIMEOUT
agent=$WF_AGENT_DEFAULT
dry_run=0

need_value() {
(($# >= 2)) || refuse "expected a value after $1; found end of arguments; provide $1 VALUE"
[[ $2 != --* ]] || refuse "expected a value after $1; found $2; provide $1 VALUE"
}

while (($#)); do
case $1 in
--branch) need_value "$@"; branch=$2; shift 2 ;;
--worktree) need_value "$@"; worktree=$2; shift 2 ;;
--job) need_value "$@"; job=$2; shift 2 ;;
--harness) need_value "$@"; harness=$2; shift 2 ;;
--model) need_value "$@"; model=$2; shift 2 ;;
--unit) need_value "$@"; unit=$2; shift 2 ;;
--stage) need_value "$@"; stage=$2; shift 2 ;;
--scope-file) need_value "$@"; scope_file=$2; shift 2 ;;
--input) need_value "$@"; input=$2; shift 2 ;;
--follows) need_value "$@"; follows=$2; shift 2 ;;
--timeout) need_value "$@"; timeout=$2; shift 2 ;;
--agent) need_value "$@"; agent=$2; shift 2 ;;
--dry-run) dry_run=1; shift ;;
*) refuse "expected a documented flag; found unknown flag $1; remove it" ;;
esac
done

[[ -n $job ]] || refuse "expected required --job; found it missing; pass --job JOB"
[[ -n $harness ]] || refuse "expected required --harness; found it missing; pass --harness grok, codex, or claude"
[[ -n $scope_file ]] || refuse "expected required --scope-file; found it missing; pass --scope-file FILE"
[[ -n $branch || -n $worktree ]] || refuse "expected one of --branch or --worktree; found neither; select a lineage worktree"
[[ -z $branch || -z $worktree ]] || refuse "expected one of --branch or --worktree; found both; pass exactly one"
case $harness in grok|codex|claude) ;; *) refuse "expected --harness grok, codex, or claude; found $harness; choose a supported harness" ;; esac
[[ -f $scope_file ]] || refuse "expected --scope-file to name a file; found $scope_file; create the file or correct the path"

scope_bytes=$(wc -c < "$scope_file") || refuse "expected a readable --scope-file; found unreadable $scope_file; correct its permissions"
scope_bytes=${scope_bytes//[[:space:]]/}
((scope_bytes <= 1024)) || refuse "expected --scope-file at most 1024 bytes; found $scope_bytes bytes; shorten it"
scope=$(cat -- "$scope_file"; printf x) || refuse "expected a readable --scope-file; found unreadable $scope_file; correct its permissions"
scope=${scope%x}

if [[ -n $branch ]]; then
found_path=
candidate=
while IFS= read -r line; do
case $line in
'worktree '*) candidate=${line#worktree } ;;
"branch refs/heads/$branch") found_path=$candidate; break ;;
esac
done < <(git worktree list --porcelain)
[[ -n $found_path ]] || refuse "expected branch $branch in a worktree; found none; run git worktree add <path> $branch"
worktree=$found_path
fi
[[ -d $worktree ]] || refuse "expected a worktree directory; found $worktree; correct --worktree"

ref=$(git -C "$worktree" rev-parse HEAD 2>/dev/null) || refuse "expected a git worktree; found $worktree without HEAD; correct the target"
lineage=$(git -C "$worktree" symbolic-ref --quiet --short HEAD 2>/dev/null || true)

if [[ -z $model ]]; then
case $harness in
grok) model=$MODEL_GROK ;;
codex) model=$MODEL_CODEX ;;
claude) if [[ $stage == plan ]]; then model=$MODEL_CLAUDE_PLAN; else model=$MODEL_CLAUDE_READ; fi ;;
esac
fi

argv=("$LAUNCHER_PIN" "$job" --harness "$harness" --model "$model" --ref "$ref")
[[ -z $lineage ]] || argv+=(--lineage "$lineage")
[[ -z $unit ]] || argv+=(--unit "$unit")
[[ -z $stage ]] || argv+=(--stage "$stage")
argv+=(--scope "$scope")
[[ -z $input ]] || argv+=(--input "$input")
[[ -z $follows ]] || argv+=(--follows "$follows")

print_launch() {
DEFAULT_TIMEOUT=$timeout WF_AGENT=$agent python3 -c 'import json, os, sys; print(json.dumps({"argv": sys.argv[1:], "env": {"DEFAULT_TIMEOUT": os.environ["DEFAULT_TIMEOUT"], "WF_AGENT": os.environ["WF_AGENT"]}}, ensure_ascii=False))' "${argv[@]}"
}

print_launch
((dry_run)) && exit 0

DEFAULT_TIMEOUT=$timeout WF_AGENT=$agent python3 "${argv[@]}"
rc=$?
printf 'LAUNCH EXIT %s\n' "$rc"
jobs_root=${DISPATCH_JOBS:-"${XDG_STATE_HOME:-$HOME/.local/state}/minspec/dispatch"}
newest=$(find "$jobs_root" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %f\n' 2>/dev/null | sort -nr | head -n 1 | cut -d' ' -f2-)
printf 'record: %s\n' "${newest:-unresolved: no dispatch record found under $jobs_root}"
exit "$rc"
77 changes: 77 additions & 0 deletions ops/bin/go.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# go.sh — the one-line way to hand work to the lane, so delegating is
# easier than grinding it inline. Everything dispatch.sh needs — harness,
# model, scope file, worktree resolution — is defaulted here from a short
# intent word. Read-only jobs stay read-only; the firewall is baked into
# the harness defaults (no producer owns two consecutive artifacts).
#
# go.sh <intent> <branch|.> <scope sentence ...>
#
# intents (default harness in parens):
# sweep (codex) security/defect sweep over disjoint categories
# audit (grok) adversarial review of a change — findings that survive
# review (both) sweep + audit, the review lane in one word
# plan (claude) a plan for the scope, no edits
# tests (claude) author tests from the contract, red-first
# check (codex) check existing tests against the contract
# implement (claude) implement to green
# adjudicate (grok) adjudicate a contested result
#
# Override the harness with HARNESS=grok|codex|claude. Everything prints
# where its verdict landed; nothing is cached.
set -uo pipefail

here=$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
die() { printf 'go: %s\n' "$*" >&2; exit 64; }

(( $# >= 3 )) || die "usage: go.sh <intent> <branch|.> <scope sentence ...>"
intent=$1; where=$2; shift 2
scope="$*"

# intent -> job + default harness. review is the one that fans out.
case $intent in
sweep) job=sweep; def_harness=codex ;;
audit) job=adversarial-review; def_harness=grok ;;
plan) job=plan; def_harness=claude; stage=plan ;;
tests) job=author-tests; def_harness=claude ;;
check) job=check-tests; def_harness=codex ;;
implement) job=implement; def_harness=claude ;;
adjudicate) job=adjudicate; def_harness=grok ;;
review) job=__fanout__; def_harness= ;;
*) die "unknown intent '$intent'; see the header of $here/go.sh" ;;
esac

# where: '.' means this checkout; else a branch name in some worktree.
if [[ $where == . ]]; then
root=$(git -C "$PWD" rev-parse --show-toplevel 2>/dev/null) \
|| die "'.' given but $PWD is not inside a git worktree"
locator=(--worktree "$root")
else
locator=(--branch "$where")
fi

# scope must fit the launcher's 1024-byte scope-file cap.
(( ${#scope} <= 1024 )) || die "scope is ${#scope} bytes; the cap is 1024 — shorten it"
scope_file=$(mktemp -t go-scope.XXXXXX) || die "cannot create scope file"
trap 'rm -f "$scope_file"' EXIT
printf '%s' "$scope" >"$scope_file"

fire() { # fire <job> <harness> [<stage>]
local j=$1 h=${2:-} st=${3:-}
h=${HARNESS:-$h}
[[ -n $h ]] || die "no harness for job '$j'; set HARNESS=grok|codex|claude"
printf '>> go: %s job=%s harness=%s target=%s\n' "$intent" "$j" "$h" "$where" >&2
local args=(--job "$j" --harness "$h" --scope-file "$scope_file" "${locator[@]}")
[[ -n $st ]] && args+=(--stage "$st")
bash "$here/dispatch.sh" "${args[@]}"
}

if [[ $job == __fanout__ ]]; then
# The review lane: security sweep (codex) then adversarial audit (grok).
# Two harnesses, so no single producer owns the whole review.
rc=0
fire sweep codex || rc=$?
fire adversarial-review grok || rc=$?
exit "$rc"
fi
fire "$job" "$def_harness" "${stage:-}"
5 changes: 5 additions & 0 deletions ops/bin/term-wall.conf.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copy this file to the gitignored term-wall.conf beside it, holding the
# real pattern: one line, an extended, case-insensitive regular expression
# — the names this organisation does not use. The pattern is configuration,
# never tree content; the line below is a placeholder, not the pattern.
ex[a]mple-name
8 changes: 8 additions & 0 deletions ops/devlane/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
__pycache__/
*.py[cod]

# Runtime dispatch output — job captures, snapshots, isolated HOMEs.
# The lever SOURCE (levers/*.sh, levers/claude, levers/codex, README)
# is tracked; everything a run WRITES under jobs/ is never committed.
dispatch/levers/jobs/
dispatch/levers/**/jobs/
Loading
Loading