Shareable workflow template for running an engineering workspace with three cooperating surfaces:
- Claude Code — interactive orchestration, hooks, skill routing, daily/status memory.
- Scaffold / prime — structured clarification → diagnosis → plan → approval → build/review loop.
- Codex Delegate — routine implementation in a sandbox/worktree, accepted only through review-gate.
- Autonomy ladder — bounded turn, goal, time and proactive loops with explicit evidence and stop conditions.
The default install is domain-neutral. Optional packs add reusable domain workflows without secrets or private data.
templates/
claude/ # CLAUDE.md, AGENTS.md, hooks, codex-delegate skill
scaffold/ # context/rules/constraints/done-criteria + skill index cards
demo-workspace/ # tiny project used by smoke-test
packs/
cfs/ # optional CFS/OpenClaw ops pack: cfs-hub + docs-intel workflows
scripts/
apply-kit.sh # copy templates into a target repository
codex-exec.sh # hardened Codex CLI wrapper
smoke-test.sh # create demo workspace and validate structure
scrub-check.sh # denylist check before publishing/sharing
docs/
WORKFLOW.md # operating model
INSTALL.md # install/update instructions
SCRUBBING.md # what must never be shared
REVIEW_GATE.md # accepting delegated work safely
git clone <this-repo-url> agent-workflow-kit
cd agent-workflow-kit
./scripts/smoke-test.sh
# Apply to an existing project:
./scripts/apply-kit.sh /path/to/your/project
# Apply core + CFS pack:
./scripts/apply-kit.sh --pack cfs /path/to/your/projectAfter applying, open the target project in Claude Code or Scaffold and read:
CLAUDE.mdAGENTS.md.scaffold/context.md.scaffold/rules.md
For MCP sync with Codex, install the transport explicitly after applying the kit:
scripts/dev/setup-codex-mcp.sh --scope userThis registers only codex mcp-server; it does not copy credentials or modify sandbox profiles.
Then customize placeholders marked TODO:.
- State lives in git. Keep
dev/status.mdanddev/daily/YYYY-MM-DD.mdcurrent. - No blind implementation. Diagnose first, plan second, implement after approval.
- Delegate routine work, not ownership. Codex can edit; the orchestrator reviews, tests and commits.
- One writer per worktree. Parallel agents get isolated git worktrees.
- Publish only scrubbed artifacts. Run
./scripts/scrub-check.shbefore pushing.
The repository is intentionally public, so its GitHub Actions do not call the
private CFS harness. CI runs the local Node tests, installation smoke test and
scrub check; the Rules job verifies that CLAUDE.md and AGENTS.md remain
identical. Model review is still required by the review-gate
before a production deployment, but it is an accountable human/orchestrator
step rather than a CI job that would silently fail to resolve a private workflow.
Review and planning calls go through .claude/hooks/providers.js. By default every
profile runs on the Codex CLI, where an extra call is covered by the subscription.
A paid HTTP provider (DeepSeek) can serve individual profiles, but it is opt-in and
guarded, because it changes two things at once: calls cost money, and the answer can
come back as prose instead of a verdict.
Read this before enabling a paid provider. Routing a profile to one sends that vendor's API, over the public internet, the raw text it is asked to judge:
- on
UserPromptSubmit— the user prompt, verbatim; - on
PreToolUse— the tool name and the pending shell command, verbatim.
Nothing is redacted, summarised or truncated on the way out, and there is no allowlist of what a prompt may contain: repository paths, hostnames, ticket numbers, stack traces and anything pasted into the turn go with it. The Codex default keeps this traffic inside a subscription you already have; DeepSeek is a third party under its own jurisdiction, retention and training policy. Move a profile onto it only for repositories whose prompts and commands you are willing to disclose to that vendor.
export AGENT_KIT_PROVIDER_PLAN=deepseek # move one profile, not all of them
export DEEPSEEK_API_KEY=... # never committed; read at call time
export AGENT_KIT_ALLOW_EXTERNAL_PROMPTS=1 # explicit acknowledgement of third-party prompt transfer
export AGENT_KIT_BUDGET_USD=5 # required — no ceiling, no metered call
export AGENT_KIT_MODEL_PRICES='{"deepseek-reasoner":{"in":0.55,"out":2.19}}'
export AGENT_KIT_MAX_TOKENS=512 # optional; bounds response length and cost
export AGENT_KIT_LEDGER_STALE_MS=5000 # optional; how long a spend-ledger lock may sitAGENT_KIT_LEDGER_STALE_MS is how long a ledger lock may go untouched before another
hook may reclaim it. The default of 5000 is three orders of magnitude above what a hold
costs (one read plus one append), so it exists to stop a dead process wedging the ledger,
not as a tuning knob. Setting it to 0 makes every lock instantly reclaimable, which
switches the mutual exclusion off — the tests use that deliberately; nothing else should.
AGENT_KIT_PROVIDER_<PROFILE> moves exactly the profile it names. AGENT_KIT_PROVIDER
without a suffix moves every profile, including ones added to the kit later, and a
per-profile setting overrides it. Prefer the per-profile form: the two channels described
above do not carry the same material, and review is the one that sends shell commands.
The example deliberately moves plan rather than review. The review profile serves
the PreToolUse gate, so routing it to a paid vendor is what sends sudo, connection
strings and anything else in a flagged command off the machine; plan sends prompt text
only. Move review only when you mean to.
Routing produces Codex model names (gpt-5.6-terra), which mean nothing to a paid
vendor, so each provider declares its own model per profile: prime, plan and
review go to deepseek-reasoner, build and simple to deepseek-chat. Price the
provider's model names in AGENT_KIT_MODEL_PRICES, never the Codex ones.
AGENT_KIT_MAX_TOKENS is the only bound on how long — and so how expensive — a single
response can be. It defaults to 512, and an unparseable value falls back to that
default rather than removing the bound.
Rules the layer enforces:
- A model with no declared price is unpriced, never free. An unpriced paid model silently recorded at $0 is how a budget goes blind, so the call is refused instead.
- No ceiling, no metered call.
AGENT_KIT_BUDGET_USDis required, and spend accrues across turns so the ceiling bounds the routine rather than one call. - No explicit transfer acknowledgement, no metered call. Set
AGENT_KIT_ALLOW_EXTERNAL_PROMPTS=1only after accepting that the raw prompt and command text described above will go to the third-party provider. - The ceiling is a limit, not a trigger. A call is refused when its worst case —
the prompt as sent, plus
AGENT_KIT_MAX_TOKENSof output — would carry the total past the ceiling. The worst-case amount is reserved under a lock before the request, so concurrent hooks cannot cross the ceiling together. Two things the reservation cannot promise, and does not pretend to:AGENT_KIT_MAX_TOKENSis a request rather than a guarantee, so a vendor answering past it settles above the ceiling — the overshoot is recorded and reported in the note, and the next call is refused. And a call that crashed, timed out or failed in flight keeps its reservation, because we cannot prove the vendor is not billing it; a call that never left the machine, or that the vendor refused at the door (401,403,429and the like — rejected before any model ran), gets it back. Holding those was how a wrong or rotated key walked the ledger to the ceiling one phantom reservation per hook and switched the paid route off, having spent nothing. - A response with no usable
usageblock is a failed call. Unmeasurable spend is unknown, never zero: the answer is discarded and the worst case is charged. A block that is absent, empty, or all zeros is the same claim — no call that reached a model consumed nothing — so no vendor can buy unlimited calls for $0. - A response without an
APPROVE/WARN/BLOCKverdict is a failed call, not a quiet approval, and the prose is never passed on dressed as a verdict. - The heaviest verdict wins.
BLOCKoutranksWARNoutranksAPPROVE, and markdown decoration (**BLOCK**:,- BLOCK —,**Verdict:** BLOCK) is read as a verdict, as is a verdict behind whatever introduced it (Recommendation: BLOCK: …,1. BLOCK: …), because that is how models actually answer. - A denial is read in the words models deny with; an approval is not.
BLOCKED:,Blocked,Verdict: BLOCKED,DENY:andREJECT:all count asBLOCK, whileAPPROVEandWARNstay exact. The asymmetry is the point: a wider denial vocabulary costs a retry, a wider approval one runs the command. - The same asymmetry applies to position. A denial counts on any line, including
behind whatever introduced it. An approval counts only on the first or last line and
never behind a label, because the reviewer is handed the pending command verbatim and
quotes it back: a command carrying its own
APPROVE:line otherwise approved itself, as did prose likeA reviewer may answer:/> Verdict: APPROVE/I refuse this one. - Mentioning a verdict is not stating one. A response that says
BLOCKonly in passing, with no verdict anywhere, is ambiguous rather than decided — so it is a failed call and escalates. Reading the mention as a denial made the gate fire on its own approvals (APPROVE: no BLOCK condition applies), and a gate that fires on correct approvals gets switched off; reading it as an approval runs the command. Ambiguity escalates for a second opinion instead — and if that is inconclusive too, strict mode denies rather than guessing. - A denial outlives the accounting. A paid answer with no usable
usageis discarded as unmeasurable — except for aBLOCKin it, which is kept and still denies. Billing and safety are separate facts, and the fallback is never allowed to soften a denial the first reviewer already made. - Any of the above escalates to the subscription provider, reporting what it escalated from and why. Escalation is never silent, and the decision to deny is taken on the provider's own verdict, never re-read out of the note built for display.
- The breaker counts transports, not opinions, and counts them per provider. Three failed transports in five minutes stop that provider for the rest of the window. A reviewer that answered without a verdict is alive, so it does not count. The count is per provider because a single tally gets it wrong both ways: a successful fallback would erase a dead paid vendor's failures and it would be retried on every hook, while a shared tally would take the working subscription reviewer down with it.
- In strict mode a
BLOCKdenies your tool call outright, and so does an inconclusive review. No verdict, no Codex binary, a timeout, an open breaker or a spent budget all deny the pending command rather than waving it through: a gate that allows whenever its reviewer is unreachable protects nothing at the moment it is needed. This applies only to the commandsshouldReviewToolflags —rm -rf, force-push,sudo,terraform apply,curl | shand friends. UnsetCODEX_COPILOT_MODEto get advisory notes instead. Routing a profile to a paid vendor also gives that vendor a veto, not an opinion; set strict mode with both facts in mind. - The denial is emitted in the shape the host enforces. Hooks that answer
{"decision": "deny"}are silently ignored — the host'sdecisionfield isapprove/block, and output that fails its schema is discarded whole, so the command runs. The hook sendshookSpecificOutput.permissionDecision: "deny"and keeps the legacydecision: "block"alongside it.
Spend is recorded in .claude/cache/provider-spend.jsonl, one appended line per call,
so concurrent hooks cannot lose each other's entries. apply-kit.sh adds
.claude/cache/ to the target's .gitignore; keep it there, since a committed
ledger both leaks usage and resets the ceiling on merge.
Prices in the example above are placeholders — declare the ones you have actually
verified with the vendor. Verify the layer with node --test tests/*.test.js: the
transport is injected, so the suite needs no API key and makes no network calls.
The CFS pack is for OpenClaw/CFS-style operations and includes:
cfs-hub-ops— read-only infra monitoring triage through cfs-hub MCP;cfs-docs-update— docs-first + docs-update workflow around CFS Docs/docs-intel;.mcp.cfs.example.json— placeholder MCP config forcfs-hub,docs-intel-team-dev,docs-intel-legal.
It deliberately excludes personal /eod, CEO private rollups, raw daily/status files, live secrets, raw client data and live infrastructure addresses.
MIT — use, fork and adapt.