π οΈ foundry β Development Notes¶
Technical detail for contributors, maintainers and agents. User-facing material lives in README.md. Content here was moved out of the README unchanged in substance; long paragraphs are split into bullets at sentence and semicolon boundaries.
Contents¶
- Approval and guard internals
- Review loop and workflow internals
- Rule loading internals
- Profile and context-composition internals
- Hook inventory
- Bin helper inventory
- Test layout
- Rule table (full descriptions)
- Contribution and review expectations (full text)
- Setup notes
Approval and guard internals¶
Local Git grant (full text)¶
- Local Git operations that need authorization (commit, staging owned changes, a local merge and its merge commit, cherry-pick, branch/worktree prep) ask one question: Approve (this operation only), Approve always (this operation, plus a standing grant for this project) or Deny (skip it and leave changes as they are).
- The
approval-guard.jshook records an Approve always answer inclaude-git-approval.jsoninside git's own common directory (git rev-parse --git-common-dir) β once per project - it applies to every worktree, and git never tracks or checks out anything there.
- While a grant passes the rule's check (
node <foundry>/hooks/commit-guard.js --check-grant: a regular, non-linked file with the expected version, scope and answer, in the session project's own common directory β resolved with Git's discovery environment removed and bare repositories refused, so a tracked bare-repository fixture, a planted.gitfile or a steeredGIT_DIRsupplies nothing), later sessions skip the question for task-scoped non-destructive local operations, and committing the agent's own completed task is the default on completion - the reply that commits states
grant .git/claude-git-approval.json@<created_at>orno grant. - Only the hook writes the grant, from your actual answer to a question whose Approve always option tells you it persists, what it covers and how to revoke it.
- The hook blocks an agent tool call that names the grant directly
- it sees names, not contents, so a patch, script or archive that writes the file is not blocked and stays forbidden by the rule alone.
rm "$(git rev-parse --git-common-dir)/claude-git-approval.json"revokes it.- A grant never covers push (asked every time), force operations, history rewriting or destructive recovery
- a checkout under review gains nothing from it
- your narrower instructions (no commit, leave unstaged, read-only, wait) still win, and spawned subagents, teammates and bridge children never use it.
- Without a grant or an active skill workflow, the question is asked.
Push token (full text)¶
- Every push asks its own question,
git-push: Approve (this push, once) or Deny. - The question shows the branch, the target and the command the agent will run.
- Your Approve makes the
approval-guard.jshook record a single-use token,claude-push-approval.jsonin the same common directory. - The token holds the current branch, HEAD and the id of the question you answered, and expires after 15 minutes
- a later Approve replaces it and a later Deny removes it.
commit-guard.jslets one push of that branch at that HEAD to a configured remote run βgit push,git push origin,-u,origin <branch>,origin HEAD:<branch>,"$(git branch --show-current)",git lfs push origin <branch>β after checking that your Approve to that very question is in the session transcript, and consumes the token as it does- of parallel push calls only one runs, and a spawned agent never spends it.
- The push must run from the checkout itself, be the only push in its command, and have no commit, reset or checkout beside it.
- A second push, a new commit or another branch needs a new Approve.
- Anything else β deleting a remote branch,
--prune,--all,--tags, another branch or ref, a URL, path or variable destination,cd,send-pack,http-push,subtree pushβ no Approve can cover: the agent gives you the command to run yourself. - A dry run (
--dry-run) needs no approval. - Force pushes stay forbidden even with a token.
- A new session or
/cleardeletes a pending token β per project, so a second session in the same project clears the first one's. - The former
! touch /tmp/claude-push-auth-β¦sentinel no longer authorizes anything.
gh write token (full text)¶
- Foundry alone ships the git/gh guards, approval records and one-time tokens.
- GitHub reads through
ghrun without a question. - Every GitHub write through
gh, on any repository, asks its own question,gh-write: Approve (this exact command, once) or Deny. - The question names the command once in an inline code span
- your Approve makes the
approval-guard.jshook record a single-use token,claude-gh-write-approval.jsonin the same common directory, holding that command for 15 minutes. gh-write-guard.jsthen lets exactly that text run once as the whole Bash command and spends the token- other text, a second run or a spawned agent's call stays blocked.
- A later Approve replaces the token, a later Deny removes it, and a new session or
/cleardeletes it. - Outside a git repository nothing is recorded, so you run the command yourself.
- Claude Code still applies a settings deny rule after a hook allows a call, so the plugins no longer ship gh write deny entries
- re-run
/foundry:setuponce to remove the ones an earlier setup merged.
Token reservation lock¶
- Single-use push and GitHub-write approvals use an exclusive reservation in the git common directory while validating, spending, replacing or revoking a token.
- Concurrent calls wait at most one second, then deny if it remains busy
- copies of the same tool call share its proven claim.
- The reservation is removed on normal completion.
- If a hook crashes, stop active hooks before manually deleting
<token-file>.spent-lockfrom the git common directory - Foundry also removes these reservations at SessionStart and
/clear.
Review loop and workflow internals¶
Challenge-loop presentation¶
- Review loops fix feasible authorized findings, investigate repeated signatures, and continue whenever the weighted score decreases
- structural labels alone do not stop remediation.
- Completion prompts ask only for a missing decision or authorization, regardless of report length, and already-authorized next steps continue.
- Concrete gates for fresh scope, paid execution, sensitive or destructive actions, and remote changes remain in force.
- Independent challenge loops show the cumulative old/new table once after each newly validated challenge, before fixes or another review.
- Pending reviews and unrelated status updates do not repeat it
- final handoffs retain their canonical Results table.
- Each open finding needs a recorded fix, escalation, or justified deferral
- chat table timing has no machine-readable delivery receipt.
Report templates¶
- The audit summary, configuration-health, investigation, and session report templates use short descriptive fields
- writing requirements sit in separate notes and are omitted from completed reports.
- Other audit modes and calibrate/brainstorm/distill templates keep their earlier placeholder style.
Workflow audits¶
- Workflow audits and adversarial reviews trace producer guarantees through downstream consumers and the next ordinary user action.
- They challenge identity, ownership, destination, lifetime, and resume assumptions with concrete counterexamples and report untested handoffs.
- The shared review/fix loop inventories first-pass coverage, checks each fix against its invariant and a sibling route, and distinguishes carried defects from later discoveries.
- Out-of-scope declines preserve mandated deliverable files and caller reply formats
unsupportedenvelopes surface a routing miss withreasonandowner, never findings or a timeout.
Loop round contract¶
- Each round challenges and collects findings, reports the cumulative old/new table, resolves feasible authorized findings, escalates unfixable
security/critical/highfindings, then repeats until clean or plateau/non-convergence, subject to authority, evidence, recurrence and stricter caller budgets. - The table is
Iteration | Security | Critical | High | Medium | Low | Nits | Weighted score - open and fixed-pending-verification signatures split into old and new, while verified-fixed and rejected findings stay excluded.
- Security and critical have separate columns
- score weights remain
20/10/6/4/2/1. - Empty rounds print no progress table.
- A 1,500-output-token reviewer target is hard only when the runtime supports it
- findings are never dropped to meet it.
- Before the baseline review of a diff the parent or its resolver wrote, the author records an attestation (invariant, sibling routes, consumer, failing-before/passing-after check) and fixes the gaps it exposes
- the reviewer never sees it and it never counts toward scoring.
- Each round's cost lands in an advisory
loop-metrics.jsonl.
Codemap prompt reuse¶
- Optional Codemap guidance is shipped locally with foundry
- loading it does not read another plugin's private shared directory.
- Codemap queries still require the
codemap-pyplugin, with file-read fallback when the CLI or local contract is unavailable. - Specialist Codemap prompts reuse only matching, successful, fresh answers, inspecting each batch child's metadata and preserving valid empty results.
- Supplied answers avoid duplicate pre-flight calls
- source and independent-review reads remain allowed.
- QA distinguishes static test/mock relationships from measured line coverage and treats missing measurements as unknown.
Shared quality stack¶
skills/_shared/quality-stack.mdis the canonical copy of the quality-gate stackdevelop's feature/fix/refactor skills run at close, propagated byte-identical intodevelop's own_shared/viapropagate_shared.py.- Its flaky-test retry now re-runs only the failing node-ids instead of the whole directory twice, doctests fold into the main pytest run when a collection probe confirms it's safe (report status
not-merged/pass/fail, replacing the old binary pass/fail line), and apytest-xdistcapability probe runs the wide pass in parallel when available β a failure that reproduces only under that parallel pass is reportedPARALLEL_FAIL_SERIAL_PASS(likely a test-isolation bug) rather than marked flaky. ! BREAKING: the doctest report line's format changed to the 3-state form above.
Planning and delegation¶
- Small implementation uses a concise plan, brief parent self-challenge (not independent), acceptance within existing authority, useful bounded execution ownership or indivisible parent execution, and consolidation with independent result challenge and relevant verification.
- Medium/large work retains independent plan challenge before acceptance.
- Approval prompts only for actual missing authority or protected decisions
- reuse accepted parent plans and establish core acceptance before optional expansion.
- Validate a coherent bounded outcome or split it into smaller outcomes, then complete the active branch depth-first using a compact task map and context limited to the active piece, dependencies, and interfaces.
- Complete required leaf challenge and verification before dependent work
- dependency-ready siblings with agreed stable interfaces and disjoint owners may proceed.
- Unknown causes or design choices first become bounded investigation or decision prerequisites
- parent consolidation retains required gates.
- This replaces broadly open workstreams and far-ahead detail to reduce context, coordination, and rework
- that benefit is expected, not measured.
Skill internals moved from the README¶
- Codemap integration metadata uses the
codemap-py.integration.v2managed-block body while retaining thev1sentinel format - use
codemap-py integrate auditfor read-only integration evidence. - Full-sweep quality audit of the project's
.claude/config by default, or of allplugins/*/agent and skill files with--local(plugin source checkout). - Installed plugins in
~/.claude/plugins/cache/are not audited - a default run that finds no
.claude/agents or skills reports them asβ UNSCANNEDinstead of a clean sweep. - Catches broken cross-references, inventory drift, model-tier mismatches, description overlap, doc staleness.
- Reports findings by severity; fix level chosen from always-fire follow-up gate after report.
- Adversarial mode challenges every claim via
foundry:challenger+ Codex. /foundry:audit's fix dispatch (audit/modes/fix.md,audit/templates/audit-fix-prompt.md) overrides per-call tohaikuforPARALLEL_SAFE_CATEGORIESfindings (pure transcription of an already-known replacement) regardless of the spawned agent's own frontmatter tier β judgment-bearing and cross-file fixes keep the agent's default.- Confidence describes the correctness of the delivered result and how it was produced and checked within scope.
- Report completion separately; a high score does not pass an unmet gate.
Calibration, audit and helper notes¶
propagate_shared.pyandcheck_orphaned_bin.pyuse ASCII status labels so success, no-op propagation, and orphan warnings work with legacy console encodings such as Windows cp1252.-
Plugin manifests accept numeric
X.Y.Zversions: unchanged or one patch, minor, or major bump from committed HEAD. - Python dictionary literals bind to an enclosing class/function and a direct named assignment when present, then compare by version field name.
- A newly inserted named assignment does not shift another producer's baseline.
- Changed duplicate inline fields without stable assignment names fail closed because the checker cannot identify which producer changed.
- Computed keys, dynamic values, and reassignment through a dictionary variable are outside this static check.
-
The shared-file checker also keeps the short Codex question guide and conditional details identical across Codex Rig, Codemap and Bridge.
- Copies ship independently; installed plugins do not depend on one another.
Rule loading internals¶
~/.claude/rules/is one flat directory shared by every installed plugin, and four of this marketplace's plugins ship arules/quality-gates.md, so source basenames would silently overwrite one another.- Each plugin therefore namespaces its own rules with its plugin name:
quality-gates.mdinstalls asfoundry-quality-gates.md, alongsidedevelop-quality-gates.md,oss-quality-gates.md, andresearch-quality-gates.mdfrom the sibling plugins' own setup skills. - The prefix is inert β verified against Claude Code 2.1.220 that it changes neither unconditional rule loading nor
paths:frontmatter matching. -
Stub + on-demand split (token diet):
debugging.md,external-data.md,artifact-lifecycle.md,quality-gates.md,communication.md,claude-config.md,public-github.md,task-lifecycle.md,notebooks.md(full body:notebook-style.md) = thin always-loaded stubs with hard constraints only - full procedural bodies live in
rules/_full/(resolved from plugin cache, NOT symlinked or injected), Read on demand at the trigger point named in each stub β drafting a commit, multi-file fix, multi-page fetch, defining new output dirs, writing a report, building anAskUserQuestioncall, setting a Bash timeout, a GitHub write, a subagent spawn, writing a notebook cell. - Sections that matter only while touching one kind of file sit behind a narrower
pathsglob instead (markdown.mdfor*.md, including the full link-verification wording and the instruction-file compression gate,python-code.mdfor pinned lint/format hooks and complexity limits), each leaving a one-line eager stub where the obligation must hold outside the trigger. - Rules that matter only during one activity are injected rather than loaded:
hooks/rule-inject.jsaddsagent-spawn.md(spawn slots, end-turn-after-spawn, parallel spawn ceilings) on the firstAgent()call andgit-commit.mdon the firstgit add/commit/pushof each session and subagent, again after compaction or/clear - the always-loaded files keep compact stubs carrying the hard limits (
task-lifecycle.mdspawn stubs,claude-config.mdspawn ceilings and git hard bans). tests/rules/test_rule_context_budget.pypins the always-loaded byte total.-
Zero constraint loss: every named rule survives in the stub or its
_full/body, verified by heading-set diff. -
Measured (2026-08-07): the always-injected preamble is ~20.5K tokens per agent and is re-sent every turn β a 56-turn agent re-sends ~1.15M tokens of it.
- The splits above cut ~2.5K of that, plus ~1.3K from
plugins/CLAUDE.mdβplugins/AUTHORING.md. - Rule savings are realized only after
make sync-claudepropagates to the plugin cache β~/.claude/rules/*.mdare symlinks into a version-pinned cache directory, so edits to this repo'srules/do not affect running agents until installed.
Profile and context-composition internals¶
cost_analyzer.py --compositionwrites a## Context compositionMarkdown section with per-window usage, injection bytes (including instructions), estimated tool-result tokens and duplicate bytes, and likely large-input triggers.- It accepts
--projects-root,--session-id(full id or prefix),--since, and--output - main and subagent contexts are reported independently.
- Explicit compaction records define windows when present
- otherwise a post-call context drop below 70% starts a window.
--runtime claudeis the default and also reports repeated Read content flags, hook counts and sizes per prompt, large cache-write triggers, and the session's 5-minute/1-hour cache-write split.- Claude sessions also show a timestamp-ordered Logged compaction events table from
~/.claude/logs/compactions.jsonl(override with--compactions-log), including hook metadata and gate estimates. - Each gate is paired with the next logged precompact event and compared with the following main-transcript window: estimated versus realised calls, and the current gate's 120,000-token floor versus realised first-call context (prompt + output).
- Missing timing or a matching window is reported as unavailable
- gates without a following precompact event say no compaction followed.
- Missing logs add a no logged compaction events note
- malformed rows warn and are skipped, and unknown events are ignored.
--runtime codexreports tokens only, without prices, from$CODEX_HOME/sessions(default~/.codex/sessions)--projects-rootoverrides discovery.- Codex deduplicates native
response_idusage records or legacy sequential cumulative token-count events, reports subagent shares across linked rollout threads, flags attempted code edits, and estimates each thread's AGENTS.md bytes Γ calls. - Native response records take precedence for an entire thread
- historical event-only usage in mixed-format threads is unavailable.
- Injection kinds include AGENTS.md, skills, model switches, multi-agent instructions and tagged hook developer messages.
- This analyzer ships in the Claude foundry plugin.
- Reads JSONL logs foundry
task-log.jshook already writes for the clock side (bin/timing_analyzer.py) and Claude Code transcripts for the cost side (bin/cost_analyzer.py) β answers "where did wall clock go in/oss:resolve?" and "what did this session cost?" alike. - Agents run in the background, so a spawn-only row carries a false-zero
duration_ms - the real span is recovered by joining the matching
startedβcompletedpair ininvocations.jsonl. - The cost side is best-effort: if no transcripts fall in the window, the
## Tokens & costsection is omitted and the clock report still ships.
Hook inventory¶
Every hook under hooks/, as originally documented. hooks.json registers them with ${CLAUDE_PLUGIN_ROOT} paths.
hooks.json¶
- hook registrations (${CLAUDE_PLUGIN_ROOT} paths), including PreCompact and PostCompact
- the two SessionEnd entries (task-log.js, audit-close.js) carry an explicit 15 s timeout β the cap every plugin's SessionEnd audit-close.js registration shares
task-log.js¶
- SubagentStart/Stop tracking to /tmp/claude-state-
/ - PreCompact/PostCompact append metadata to ~/.claude/logs/compactions.jsonl (no instruction or summary text)
- PreCompact writes the session's edited-file list from
timings.jsonl(Write/Edit/MultiEdit/NotebookEdit, whole session) into session-context.md and saves skill-contract.md verbatim with verified/unverified status from the writer's session + raw-byte SHA-256 owner stamp, skipping matching foreign-session stamps - SessionEnd sweeps idle, clean, registered agent-/oss- worktrees (2 h) and keeps dev-*, dirty or output-holding trees
statusline.js¶
- status bar agent counts
teammate-quality.js¶
- TaskCompleted/TeammateIdle teammate output quality gate
lint-on-save.js¶
- runs pre-commit after every Write/Edit
- async + cross-session lock; 15s timeout
- skips .temp/
artifact-guard.js¶
- PostToolUse Write/Edit on .reports//*.md, .temp//*.md
- soft 10K-token cap + .temp/ ultra-caveman article-density heuristic
- feedback only, never blocks
rtk-rewrite.js¶
- transparently rewrites CLI calls for token compression
agent-router.js¶
- PreToolUse Agent hook; 3-tier routing fallback (worktree β cache β local)
commit-guard.js¶
- PreToolUse Bash guard; git commit is prompt-discipline only, not hook-gated
--check-grant(the rule's local Git grant check, reads no stdin) printsgrant .git/claude-git-approval.json@<created_at>for a recordlib/approval-grants.jsaccepts, elseno grant: <reason>β Foundry only, along with its grant writer- force push (any
--force*,-fcluster,+refspec,--mirror, any abbreviation git accepts such as--mir, and an unquoted glob argument that a planted file could turn into one) blocked unconditionally on any branch forpush,send-pack,http-push,subtree β¦ pushandlfs β¦ push(program name matched lower-cased) - every
remote-<transport>helper and every subcommand under an enabling inlinehelp.autocorrectcount as force - a dry run (
--dry-run,-n) passes without a token - regular push runs only by spending the single-use push token
claude-push-approval.json(scopepush-once, 15 min) throughlib/approval-grants.jsspendTokenβ an exclusive reservation and a rename claim pertool_use_id, so of parallel calls exactly one runs, after proving the approvinggit-pushquestion and the user's Approve in the session transcript - a spawned agent's call or one without
tool_use_idnever spends it - by the user's decision it covers one push of the approved branch at the approved HEAD to a configured remote name (
git push [-u] [<remote>] [HEAD|<branch>][:<branch>], a current-branch substitution,lfs push), while a delete (:ref,--delete,-d),--prune,--all/--branches,--tags,--follow-tags, any other option or abbreviation, another ref, a URL, path, unknown or variable destination,cd/pushd/popd, a-C/--git-dir/--work-tree/GIT_DIRswitch, inline config redirecting the push,send-pack,http-pushandsubtree pushare never covered (blocked, token kept, the user runs them by hand) - a second push, a loop, function or repeating runner, HEAD-moving git beside the push (commit, reset, checkout, update-ref, a config or remote write), a new commit or another branch is blocked (no auto-arm)
- any exception on the push path exits 2
- SessionStart and
/cleardelete both tokens with their claims (per project, not per session), SessionStart also this repository's leftoverclaude-push-auth-*sentinels, which no longer authorize anything - invocations found through grouping, prefixes, quoting, brace expansion, any-case git names, inline
-c/--config-envaliases, here-strings and heredocs fed to a shell, nested shell source andpython -c/perl -e/ruby -e/node -ecode vialib/shell-git.js - fails closed when the command cannot be parsed or the library is missing
- accepted limits (third-party
git-<name>programs, GitHub API force updates left togh-write-guard.js) listed in its header and the lib header
gh-write-guard.js¶
- PreToolUse Bash; blocks (exit 2) every GitHub write made through
ghthe user did not approve, before any allow rule applies: known write subcommands (pr create/merge/comment,issue close,release/gistwrites,repo edit/delete/sync/rename,run rerun/cancel,workflow run/enable/disable,label/secret/variablewrites and the rest of gh 2.102's write set, every documented group and verb alias included) wherever flags sit or however quoted ($'β¦'escapes bash and zsh read alike decoded, quoting they read differently blocked, as is any$form,{name}>descriptor prefix or shell option switched at run time that the lexer does not model; a variable or substitution where a write verb could stand blocks), andgh apifailing closed on any-X/--methodbut a literal GET (run-time values included),--input, a method-override or run-time header name, REST-f/-Ffields without an explicit GET, a GraphQLmutation, a query file or a query the command text does not show, and unknown options, flags placed before theapiword included - reads and unknown subcommands pass to normal permission handling
- invocations found via
lib/shell-git.js, which drops redirections while quoting is known - fails closed without the library or an exact argv (quoting the lexer cannot follow, gh named in interpreter code)
- inactive without Node.js (launcher exits 0, SessionStart diagnostic warns)
- an exact-argv write whose whole command text, trimmed, equals a valid
gh-write-oncetoken'scommandruns once:lib/approval-grants.jsspendTokenreserves the token before renaming it to a claim keyed bytool_use_id, so every loaded Foundry copy allow that call and no other, and the hook prints a PreToolUseallow - other text, an invalid, expired or foreign token, a spawned agent, no
tool_use_id, nolib/approval-grants.jsor a write found without an exact argv stays blocked and keeps the token - ships in Foundry only, along with both libs
allow-dispatch.js¶
- PreToolUse Bash; the only registered auto-allow hook β calls the two required decision modules plus Foundry's optional gh-read module below as libraries in rank order (provenance, shape, gh read), writes the first allow's payload, and appends one audit record naming the effective verdict plus every module's own verdict
- a module that throws contributes "none" and never suppresses the others
- exits 0 on every path, because an allow-only hook that crashes grants nothing and falls back to normal permission handling
- propagated to every plugin except codemap-py
sentinel-read-allow.js¶
- decision module (shape lane), no longer registered as a hook of its own
- auto-allows blueprint idioms ($(cat "${TMPDIR:-/tmp}/β¦") sentinel reads, $(date -u +FMT) stamps, substitution-free
IFS= read -r VAR < sentinelform) when every segment is read-only β kills "Contains expansion" prompts for pre-canned skill code - whole-line
#comments are skipped in segment validation (a separator after one still validates as live, and a quoted#stays data), and..rejects only as a real path component so.../v1.2..v1.3are not read as traversal - an argument may expand only
$NAME,${NAME}or${NAME}with a:-/%/#read of literal text or a nested name β a zsh sigil (${~X},$=X),$[β¦], an assigning operator (:=,::=), a subscript, or a{NAME}brace group against a redirection (which assigns NAME) falls through - verdicts are invariant under runtime value substitution, which is what this module covers that
blueprint-allow.jsstructurally cannot - keeps its standalone entry point, which
bin/audit_hook_coverage.pystill subprocesses - everything else falls through to normal permission checks
- propagated to every plugin except codemap-py
blueprint-allow.js¶
- decision module (provenance lane, rank 1), no longer registered as a hook of its own
- normalizes the command, hashes it, and exact-matches it against the plugin's committed
blueprint-manifest.json(generated bybin/build_blueprint_manifest.pyfrom every verbatim bash block/command shipped in skills/agents/rules) - a hit plus an independent danger re-check allows without a prompt, covering
$(python β¦)/$(git β¦)capture-substitutions the shape-based module cannot reach - any deviation from the blueprint text misses and falls through to a normal prompt
- propagated to every plugin except codemap-py
github-read-allow.js¶
- decision module (gh-read lane, rank 3), not registered as a hook of its own
- auto-allows a Bash command made only of allowlisted gh reads (read verbs, help and local config reads,
gh apiGETs incl.?query, explicit-X GETfields and double-quoted endpoint variables, GraphQL queries the text shows), each optionally piped into a closed filter set (jq,head,tail,wc,sort,uniq,grep,cut,tr,column) with no file, program or environment access, in every session and spawned agent, with no grant or record - never allows a gh write; ships in Foundry only
- the shared dispatcher skips this optional lane where its module is absent
audit-close.js¶
- PostToolUse/PostToolUseFailure (Bash) + SessionStart/SessionEnd
- appends one record observing that a Bash call completed, and one per session boundary
- writes nothing to stdout on any event (SessionStart stdout is injected into the model's context)
- every installed plugin writes its own row with nothing coordinating them, so four rows for one completion are corroboration rather than duplicates
- propagated to every plugin except codemap-py
lib/audit-log.js¶
- shared record writer for the two hooks above: canonical serialization (a cross-language contract with
bin/verify_blueprint_audit.py), self-hash, and a single append into~/.claude/logs/audit/s-<key>.jsonl - append-only by design β it holds no lock, marker or state directory and has no code path that deletes or rewrites a file
- never throws, so a logging failure cannot change a permission decision
RIG_AUDIT=0disables writing only- propagated to every plugin except codemap-py
stale-plugin-check.js¶
- SessionStart (every source); when the plugin root this session loaded carries
.orphaned_ator is not the installed path, prints one JSON object: asystemMessagethe user sees (plugin, loaded vs installed version,exit, then claude --resume <session_id>) andadditionalContexttelling the model not to run that plugin's workflows on the stale copy until the user chooses, through AskUserQuestion, to restart or continue β asked once per session and plugin version: a compact/clear warning after that ask keeps the user notice but drops the ask, while a first detection at compact/clear (plugin replaced mid-session) still asks - each copy checks only its own plugin, because plugins update independently
- silent when current or outside the plugin cache
- never blocks; propagated to every plugin except codemap-py
rules-check.js¶
- SessionStart (every source); when setup's
foundry-<name>.mdrule links are missing (setup never ran or failed), names each rule file to read and injects the commit rule so commits keep their format and co-author trailers
rule-inject.js¶
- PostToolUse + PostToolUseFailure
Agent|Bash, SessionStartcompact|clear, PreCompact - injects
rules/agent-spawn.mdasadditionalContextafter the firstAgent()call andrules/git-commit.mdafter the first Bash call running a commit-creating git command (vialib/shell-git.js), once per session and per subagent (agent_id) - a call blocked by a PreToolUse hook never spends the slot
- the context arrives beside the call's result, so commit-format bullets stay always-loaded in
claude-config.md - reads the plugin's own
rules/at run time - SessionStart
compactrestores the skill contract as verified or advisory via the writer's owner stamp (matching session + raw-byte SHA-256), skips matching foreign-session stamps, and uses a Read path above the injection cap clearonly resets sentinels
approval-guard.js¶
- PostToolUse AskUserQuestion + PreToolUse Bash/Write/Edit/MultiEdit/NotebookEdit
- runs the writer and the self-grant guard of
lib/approval-grants.js. - Writer: the user's
Approve alwaysanswer to the exactgit-approvequestion (optionsApprove/Approve always/Deny) writesclaude-git-approval.json(version, scope, created_at, question, answer) in the git common dir β one grant per project, shared by every worktree git-pushansweredApprovewrites the single-use push tokenclaude-push-approval.json(scopepush-once, adding branch, head, expires_at) for one push of the current branch at the current HEAD (a detached HEAD records nothing), andDenyremoves a pending tokengh-writeansweredApprovewrites the single-use tokenclaude-gh-write-approval.json(scopegh-write-once, adding command and expires_at) for the one gh command the question names in an inline code span (none, two, a fenced block or a span across lines records nothing), andDenyremoves it- an
Approve alwaysdescription not naming that file, an answer the model pre-filled in its own call (read from the session transcript'stool_useblock named bytool_use_id, tail-first, bounded to 16 MiB β never from PostToolUsetool_input, which the harness may fill with the user's pick), a spawned-agent question, a missing transcript, id or call, a rejected location, or a record path that is a symbolic link or not a regular file records nothing and says why - written through an exclusive temp file and a rename, and an identical record (times aside) is left unchanged by a silent copy, so the loaded Foundry copies leave one record and report it once.
- Guard: exit 2 on file-tool writes to any approval-record name or to a path named
.git(other paths inside.gitpass), and on Bash commands naming a record as a write target (a redirection or output option onto it, or an operand of anything but an exact read, delete, print or mention-only git subcommand; a glob counts only in the common dir; scope ids never count), the common dir as a whole where a command writes it (copy, move, sync, extract or clone destination, anylnoperand, a redirect onto it, an operand of a command not known to only read), a brace or glob word that could expand to a record or the git dir, a claim or temp record name through a file tool, or that could run the writer β names matched as the shell and filesystem resolve them (quotes, escapes and$'β¦'removed, NFKC + case folded, Windows trailing dot/space/::$DATA/8.3 names, globs and braces,.git//and.git/./collapsed) - exclusion operands (
--exclude,--ignore, find's-path/-namewithout a writing action,tree -I,zip -x) are not targets - it sees names, never contents, so a patch,
git apply, archive, script or forged payload stays prompt discipline - ships in Foundry only
lib/approval-grants.js¶
- approval-record scope table (
local-git-non-destructive,push-onceandgh-write-once, each written), record store, branch and HEAD reader (gitHead, shared by the writer and the spend), single-use spend shared by every loaded Foundry copy (spendToken,removeRecord) and guard logic behindapproval-guard.js, read bycommit-guard.js(--check-grant, push token spend, token wipe) andgh-write-guard.js(gh write token spend) - one
approval-recordfamily at version 1, one file per scope in the git common dir, resolved withGIT_DIR/GIT_COMMON_DIR/GIT_WORK_TREE/GIT_CEILING_DIRECTORIES/GIT_DISCOVERY_ACROSS_FILESYSTEM/GIT_INDEX_FILEremoved and-c safe.bareRepository=explicit, refusing a bare repository, a missing work tree, a common dir not named.gitor not linked back from its work tree, and one other thanCLAUDE_PROJECT_DIR's - a reader rejects an unknown record version or scope, so copies of different releases fail closed
- mirrored as prose in codex-rig's
native-skill-contract.md - ships in Foundry only
lib/shell-git.js¶
- program-agnostic quote-aware shell lexer shared by
commit-guard.js,rule-inject.js(git readings,gitSubcommandArgs) andgh-write-guard.js(programInvocations), shipped only in Foundry: finds every invocation in a Bash command through{ }/if/time/sudo/xargs, quoted args, brace expansion, substitutions, heredocs, here-strings andbash -c/eval/env -Ssource - unquoted redirections are dropped with their target and fd number while quoting is known (a quoted
>stays an argument) - the program name ignores case and
.exe - inline
-c/--config-envaliases expanded (run-time values read as a force push) - an enabling inline
help.autocorrectreads every subcommand as a force push - an unquoted glob argument adds a reading as
--force python -c/perl -e/ruby -e/node -ecode naming git and push reads as a push, and code naming any other program makesprogramInvocationsanswer null (no exact argv, the caller fails closed)- out-of-scope spellings listed as accepted limits in its header
- for git a coarse operator split is unioned so detection only grows
- fails closed on unparsable quoting or oversized brace expansion
session-restore.js¶
- SessionStart matcher
clear; reads<cwd>/.claude/state/session/LATESTfrom the hook payload's owncwd, and injects that handover doc as raw stdout (the documented SessionStart context channel) when it is unconsumed and under 30 min old β then marks it consumed and unlinks the pointer so a second/clearnever re-injects - over ~8000 chars only
## Goal+ files table +## Next step+ a/foundry:session recallpointer go in - every other path is a silent exit 0, so it can never block a session start
write-guard.js¶
- PreToolUse Edit/Write/NotebookEdit
- guard-only inverse of the two allow hooks above β grants nothing, only forces a confirmation (
permissionDecision: "ask") on writes to a small repository-independent protected set:CLAUDE.md/AGENTS.md,.claude/settings*.json,.pre-commit-config.yaml,pyproject.toml - a protected file git already tracks never asks, because every write to it is diffable and revertible
- untracked, new and gitignored protected files ask, and a failed git probe counts as untracked
- a protected file that does not exist yet asks once and is recorded in a per-session ledger (
write-guard-created-<session id>in the temp dir), after which writes to it by that session pass, while a file that already existed untracked keeps asking - source, tests,
.github/**,CHANGELOG.mdand dependency lockfiles are deliberately unprotected, so pairing withacceptEditsremoves routine friction while untracked protected files stay gated - patterns are anchored on a path-separator boundary (
docs/pyproject_notes.mdpasses through), matched after POSIX normalization so Windows backslashes cannot disable it, and case-insensitive because macOS/Windows filesystems case-fold (claude.mdwrites the realCLAUDE.md) - everything else is silent passthrough
- propagated to every plugin except codemap-py
md-compress.js¶
- normalizes markdown whitespace/table-padding in place on Edit
batch-nudge.js¶
- PostToolBatch/SubagentStart/SubagentStop/UserPromptSubmit
- counts, per agent (
agent_id, so subagents sharing a session never sum), consecutive single-call batches of a read-only tool (Read/Grep/Glob; Bash only for single read-only commands:ls/grep/cat/head/tail/wc, read-onlygitsubcommands,findwithout write actions, no redirects or chaining, only plain characters, no quotes, escapes, braces, parentheses,$or globs; strict by design) - a parallel or non-batchable batch resets the streak
- SubagentStart/SubagentStop delete that agent's streak file
- at streak 4, injects a reminder as
additionalContext(exit 0, never blocks)
enforce-audit-header.js¶
- Blocks only audit's fix follow-up until aggregate.md and valid summary.jsonl exist and the follow-up call's first question text is a short plain summary (at most 1,500 characters) naming the
summary.jsonlpath and the Audit Report headline - option descriptions and previews never supply it, and a reply-text copy before the call does not count.
- A valid empty summary permits a zero-finding audit.
- Recovery questions remain available.
- On Stop: keeps a turn going once per aggregate when its final reply (the text after the turn's last tool call) lacks the full Audit Report
- a shown follow-up does not satisfy it.
enforce-profile-header.js¶
- Blocks only the owning workflow follow-up until its report exists and the follow-up call's first question text is a short plain summary (report path,
Title,Outcome/Verdict; at most 1,500 characters, no table) - option descriptions and previews never supply it, and a reply-text table before the call does not count.
- Diagnostic/recovery questions remain available
- unreadable delivery evidence is not success.
- On Stop: keeps a turn going once per report when its final reply (the text after the turn's last tool call) lacks the full header table
- a shown follow-up does not satisfy it.
report-header-table.js¶
- Shared local module: matches workflow-specific questions and current report headers
- at question time checks only the call's first question text for the report path and the
TitleandOutcome/Verdictvalues within 1,500 characters - at Stop only the final reply after the turn's last tool call counts for the full header table.
Bin helper inventory¶
These Python helpers are installed workflow support and maintainer surfaces, not additional slash-command skills. They resolve paths, validate plugin documents, prepare reports, or perform bounded deterministic transforms; invoke them through the owning skill or with the installed plugin root.
propagate_shared.py and check_orphaned_bin.py use ASCII status labels so success, no-op propagation, and orphan warnings work with legacy console encodings such as Windows cp1252.
Audit, consistency, and document checks¶
| Helper | Purpose |
|---|---|
audit_churn.py |
Emit git-history recurrence signals for /foundry:audit. |
audit_preflight.py |
Parse /foundry:audit arguments and prepare its run state. |
audit_static.py |
Run and aggregate the deterministic Layer-1 audit checks. |
check_agent_waits.py |
Flag skill/agent text that polls for agents or sends task bookkeeping as its own turn. |
check_bash_persistence.py |
Detect shell variables referenced across separate Bash tool calls. |
check_bin_test_coverage.py |
Require every bin/ script to have a real test file (audit Check R4). |
check_cli_flag_drift.py |
Detect drift between documented flags and argparse options. |
check_codemap_guard.py |
Detect unmanaged codemap index-guard copies. |
check_agent_plugin_root.py |
Flag agent Markdown that writes ${CLAUDE_PLUGIN_ROOT:-β¦} (audit Layer-1 agent-plugin-root; exit 1 on a finding, plugin-root-check: allow exempts a line). |
check_bridge.py |
Detect whether the bridge plugin is installed and enabled (marketplace allowlist). |
check_fence_symmetry.py |
Validate Markdown code-fence pairing and nesting. |
check_gitignored_refs.py |
Reject tracked-file references to this repo's gitignored documents. |
check_install_state.py |
Validate post-install state under ~/.claude for /foundry:audit, including native Windows symlink targets. |
check_mode_dispatch.py |
Detect dangling mode-dispatch references in skill files. |
check_orphaned_bin.py |
Find bin scripts not referenced by plugin Markdown. |
check_plugin_layout.py |
Validate a plugin's on-disk layout for /foundry:audit Check 8. |
check_plugin_module_docs.py |
Require a module docstring on every shipped plugin module. |
check_plugin_version_sync.py |
Require matching dual-host manifests, retain tracked host manifests in shipped plugins, and check changed shipped JSON version, schema, and protocol fields plus matching Python dictionary literals and module-level *_VERSION, _SCHEMA, and PROTOCOL integers against committed HEAD. |
check_readme_drift.py |
Detect README version and bin-reference drift from disk. |
check_routing_links.py |
Validate computed paths in skills and agent files. |
check_rtk_alignment.py |
Compare the RTK rewrite hook's prefix list against the installed RTK. |
check_skill_contracts.py |
Run structural contract checks over skill Markdown for /foundry:audit. |
check_spawn_prompt_vars.py |
Detect unresolved variables in Markdown spawn prompts. |
check_tag_symmetry.py |
Check XML-tag symmetry in agent and skill Markdown. |
classify_resolver_sites.py |
Classify resolver call sites as inline-required or cat-only. |
consolidate_inventory.py |
For /foundry:audit --consolidate: inventory instruction sections with load group and merge-blocking dependencies, select the groups to examine, vet proposed merge clusters, and settle judge decisions and reviews. |
scan_efficiency_signals.py |
Emit deterministic cost signals for /foundry:audit --efficiency. |
efficiency_measured.py |
Measure model-switch rewrites, cold start, pre-gate cost, subagent growth and idle-gap cache losses from transcripts; keeps a baseline ledger for /foundry:audit --efficiency. |
size_importance.py |
Judge instruction blocks spine or complement and flag only complement text that outweighs its load class, for /foundry:audit --efficiency (E11). |
Plugin manifests accept numeric
X.Y.Zversions: unchanged or one patch, minor, or major bump from committed HEAD.Python dictionary literals bind to an enclosing class/function and a direct named assignment when present, then compare by version field name.
A newly inserted named assignment does not shift another producer's baseline.
Changed duplicate inline fields without stable assignment names fail closed because the checker cannot identify which producer changed.
Computed keys, dynamic values, and reassignment through a dictionary variable are outside this static check.
Session, install, and state helpers¶
| Helper | Purpose |
|---|---|
find-polluter.py |
Binary-search a test suite for test-isolation contamination. |
agent_watch.py |
Report each spawned agent's deliverable and deadline in one call β no polling. |
append_ledger.py |
Append staged .rec records (or --from files) to a growing ledger without rewriting it. |
adversarial_loop.py |
Record and validate review β fix loop rounds: append a round (and, with --append-metrics, its advisory cost record), print the progress table, require root cause on a repeated finding and stop at its third occurrence. |
compact_gate.py |
Decide if boundary compaction pays: continue, non-blocking advise, or opt-in stop. |
compaction_boundary.py |
Print a skill's compaction Boundary step, then run the optional foundry compact_gate.py; continue when the provider is missing. Propagated to oss and develop. |
get_plugin_install_path.py |
Resolve the active plugin path from Claude's registry. |
heal_git_artifacts.py |
Reclaim stale skill locks and orphaned git worktrees; locks --pattern must be a bare file-name glob, and a worktree whose status cannot be read is never removable; unregistered dirs are removed only with --include-orphans after a fresh re-check, symlinks are never followed, and --managed-prefix rejects *. |
jq_write.py |
Apply an atomic jq JSON edit (--arg, --slurpfile); cwd, TMPDIR, ~/.claude/*.json; an unlisted flag exits 3. |
load_mode.py |
Resolve a skill mode/template directory and emit one file. |
load_shared_doc.py |
Resolve the plugin shared-doc directory and emit one file; --optional turns a missing plugin, directory or file into one β WARNING line with exit 0 (warn-and-skip, used by the Codex pre-pass and audit cross-validation loads). |
make_run_dir.py |
Create a UTC-timestamped run directory. |
merge_settings_permissions.py |
Merge shipped permissions-{allow,deny}.json into ~/.claude/settings.json atomically. |
settings_json.py |
Python-only setup JSON probes and atomic writes for hooks, statusLine, enabledPlugins, env defaults and advisorModel. |
plugin_layout.py |
Print each plugin's manifest-declared skills and agents directories (PLUGIN_LAYOUT: lines) so --local audit globs honour renamed directories. |
purge_plugin_cache.py |
Remove orphaned versions from the plugin cache. |
remove_hook_from_registry.py |
Remove matching hook commands from a JSON registry. |
resolve_agent_file.py |
Locate the source file for a calibration target. |
resolve_memory_dir.py |
Resolve the project-scoped Claude memory directory. |
resolve_plugin_root.py |
Resolve and validate the current installed plugin root. |
resolve_shared_path.py |
Resolve a plugin's shared directory portably (own, or the active codemap-py install); the cwd source-tree fallback is used only inside that plugin's own source repository, elsewhere it exits 1. |
resolve_skill_subdir.py |
Resolve a skill modes/ or templates/ subdirectory; the project-local .claude/skills tier applies only with --local. |
state.py |
Persist small shell values across Bash tool-call boundaries. |
symlink_with_guard.py |
Scan and safely manage foundry-init symlink conflicts. |
write_skill_contract.py |
Write contract + owner stamp; leading --keep-sentinel <name> (flat session sentinel) or --keep-file <path> (file in a per-session state dir) appends user-keep; optional -- calls_ahead=N drop=... interactive=false tail. |
Reporting, synchronization, and transforms¶
The shared-file checker also keeps the short Codex question guide and conditional details identical across Codex Rig, Codemap and Bridge. Copies ship independently; installed plugins do not depend on one another.
| Helper | Purpose |
|---|---|
audit_hook_coverage.py |
Measure how often the auto-allow hooks fire across session transcripts. |
build_blueprint_manifest.py |
Hash shipped bash blueprints into the per-plugin allow manifest. |
codemap_changed_modules.py |
List the codemap module names behind the Python files changed since the branch point (--base REF, --cap N); feeds the agents' per-module query loops. |
cost_analyzer.py |
Bucket Claude session token usage and estimated cost. |
extract-keep-flag.py |
Parse --keep "<items>" and clear a stale compaction contract (--inlined; sentinel writes atomic, a contract owned by another session is left alone). |
extract_code_blocks.py |
Extract and classify fenced Markdown code blocks. |
list_audit_files.py |
Inventory the config files /foundry:audit covers, with block counts; --entrypoints --paths-only -0 emits NUL-terminated agent and SKILL.md paths for xargs -0 in either tree. |
measure_config_size.py |
Size the config Claude loads, for /foundry:audit. |
parse-skill-flags.py |
Parse boolean and value skill flags into shell assignments; --keep-state <prefix> also writes per-session sentinels and clears a stale contract, replacing extract-keep-flag.py + eval. |
propagate_shared.py |
Keep byte-identical cross-plugin shared files synchronized. |
resolve_centrality.py |
Convert codemap centrality output into a resolver map. |
timing_analyzer.py |
Bucket Claude session clock time into a Markdown report. |
verify_blueprint_audit.py |
Verify and prune the auto-allow audit log in ~/.claude/logs/audit/. |
verify_perm.py |
Verify permission presence across settings and the permissions guide. |
Test layout¶
One runner (pytest) covers both Python bin/ scripts and JS hooks. Current test count: grep -rc 'def test_' plugins/cc_foundry/tests/*.py | awk -F: '{s+=$2} END{print s}'.
Apply pytest markers and conditional skips directly to test functions or existing test classes, as described in the bin authoring guide.
Run locally (requires Python β₯ 3.10 and Node β₯ 18 β run from repo root):
JS hook tests only:
JS hook tests (subprocess, black-box)¶
Each test spawns node <hook>.js with JSON payload on stdin, asserts filesystem side-effects under /tmp/claude-state-<session_id>/ + exit codes.
| File | Hook | Tests | Covered |
|---|---|---|---|
test_task_log_js.py |
task-log.js |
35 | Agent lifecycle (Pre/PostToolUse, SubagentStart/Stop), tool_use_idβagent_id re-key on SubagentStart (direct + agent_type-fallback match, including named agents matched by name) so SubagentStop's unlink actually lands, background agents kept past dispatch-time PostToolUse, pending/ race condition, worktree last_active refresh on tool activity, codex skill tracking, per-tool counters (last stamp per call, inflight opened at PreToolUse and released on PostToolUse/Failure, floored at 0 on duplicate fire), SessionEnd stale-worktree sweep keeps dev-*, uncommitted and fresh trees |
test_stale_plugin_check_js.py |
stale-plugin-check.js |
77 | Runs every shipped copy (foundry, oss, develop, research) under its own plugin name: silent for the installed root, a source-tree root, a missing registry, or another event; for an orphaned, replaced or uninstalled root prints exactly one JSON object whose systemMessage names the plugin, both versions and the claude --resume <session_id> step (a plain restart when the payload has no session id) and whose additionalContext requires an AskUserQuestion before that plugin's workflows run β dropped on a compact/clear after this session already got it, kept on first detection at compact/clear, on resume, and without a session id; each plugin registers its own copy once on SessionStart, and every copy is in the propagation manifest |
test_rules_check_js.py |
rules-check.js |
4 | Blank config names every rule and injects the commit rule without frontmatter; silent when all rules are linked or on another event; a partial install names only the missing rule |
test_rule_inject_js.py |
rule-inject.js |
80 | Agent injects the rule body once (exact PostToolUse/PostToolUseFailure additionalContext shape, no frontmatter), second Agent silent, each agent_id and each session injects once; git add/commit/push in any segment or spelling inject (x && git commit, git -C dir push, env VAR=1 git push, heredoc message), git status/echo git commit-ish/git commit-tree/git stash push silent; compact/clear reset sentinels; compact restores contracts, clear silent; startup leaves them; a rule over 9,500 chars injects summary + rule and _full paths; malformed/empty/foreign payloads, missing rule file and unwritable temp dir exit 0 silently; both shipped rules inject in full under the cap; grouped/wrapped/nested, GIT, inline-alias, bash <<<, inline help.autocorrect and redirection-before-subcommand (git 2>/dev/null commit) forms and merge/revert/cherry-pick/am/rebase/annotated tag inject; PreToolUse claims nothing; a commit-guard-blocked push leaves the slot; subagent PreCompact re-arms only that subagent; missing lib fails open; registration matchers |
test_statusline_js.py |
statusline.js |
23 | π€ segment: empty/active/stale agents, non-worktree agent stays visible past 10 min (30-min backstop) and drops past 30 min, long-running agent kept visible by fresh last_active, codex agent type label (via agents/ and the codex/ dir), missing-agents/-dir tolerance. π οΈ segment: freshness from last (a call 5 s into a 40 s-old window stays shown), in-flight call visible past the 30 s window, finished call and leaked in-flight slot dropped, legacy since-only record still renders |
test_commit_guard_js.py |
commit-guard.js |
403 | Push guard: every remote write needs a single-use push-once token covering one push of the approved branch at the approved HEAD to a configured remote, remote and branch written out (delete, prune, tags, other ref, URL, variable target, cd, inline push redirects blocked); spent once by rename-claim with transcript proof, never by a spawned agent; a dry run spends nothing. Force and mirror pushes blocked even with a token. Shell readings: heredoc data vs code, substitutions, zsh-only syntax failing closed with the syntax named. Local Git (staging, merge, cherry-pick, branch/tag delete, reset, stash, worktree) passes. --check-grant, SessionStart and /clear token wipe, fail closed on internal errors. |
test_approval_guard_js.py |
approval-guard.js |
411 | Writer: records only from the user's own answer to the exact question (git-approve Approve always with the record file disclosed; git-push and gh-write Approve with question id, branch and HEAD or the exact command and file digests); refuses spawned agents, prefilled answers, drifted option sets, hidden characters and compound gh spans; Deny revokes pending tokens; only the writing copy reports. Two and four Foundry installs in temporary roots race on the same answer and tool-call id, leaving one record and spending one token. Self-grant guard: blocks Bash and file-tool writes of record, claim and temp names (brace, glob and symlink spellings), writes onto the git dir as a whole, running the writer, and named writes to a session transcript; reads, revoke, memory files and local Git reads of .git pass. |
test_gh_write_guard_js.py |
gh-write-guard.js |
447 | Blocks every gh write without a gh-write-once token for the exact command; the token runs once across all loaded Foundry copies (rename-claim), only with its transcript proof, unchanged file operands and a single gh invocation. Force updates (repo sync --force, pr update-branch --rebase, force fields including nested GraphQL keys, --input to refs or GraphQL) blocked even with a token. Reads pass; wrappers, substitutions, heredocs and zsh-only syntax (named) are covered. |
test_agent_router_js.py |
agent-router.js |
12 | Builtin/plugin-agent passthrough (tier 1), unknown-agent reroute to general-purpose (tier 3), SessionStart index build, tier-2B LLM request (resolved model ID, thinking disabled) and response parse (typed text block; error/non-2xx/no-text raise, never a silent "none"), runtime Haiku resolution (env override, 24h cache, Models API pick by line, logged fallback, session-scoped cache path) |
test_batch_nudge_js.py |
batch-nudge.js |
66 | never blocks (exit 0, no decision/continue key) at any streak; no nudge below threshold; nudge (additionalContext, cites Β§Turn Batching) fires exactly on the batch crossing threshold and the streak restarts after it; parallel or mixed batch resets streak; non-batchable tool (Edit) resets streak; UserPromptSubmit resets the main agent's streak, not a subagent's; interleaved agents keep separate streaks and one agent's parallel batch never resets another's; read-only Bash (git log/status/show, find -name, ls/grep/tail/wc, the plain characters =:@%,+) counted, mutating git subcommands, --output, redirects, newline/substitution/compound commands and find -delete/-exec are not; Pre/PostToolUse events ignored; malformed input exits 0 silently; SubagentStart/SubagentStop reset and delete only that agent's file, never main's; commands with quotes, escapes, braces, parentheses, $IFS, globs or zsh =(...) not counted; subagent events with no, empty or main agent_id keep main's streak; failed state write leaves no temp file; hooks.json registers the hook on PostToolBatch, SubagentStart, SubagentStop and UserPromptSubmit only; cited rule heading exists |
test_sentinel_read_allow_js.py |
sentinel-read-allow.js |
146 | Blueprint sentinel reads + date stamps + substitution-free IFS= read -r form allowed (observed prompt commands, unquoted variant, defaults, pipes into whitelisted tokens); passthrough on non-whitelisted tokens, non-TMPDIR paths, nested/second substitutions, backticks, process subst, heredocs, write redirects, quote smuggling, Codex bypass PoCs, any $ form outside the admit set (zsh sigils, $[β¦], assigning operators, subscripts) and {NAME} redirection prefixes; whole-line comments skipped without letting a separator hide a live command, and a quoted # stays data; .. matched as a path component so .../v1.2..v1.3/file..txt allow while leading/mid/trailing/bare/${V:-..} traversal rejects; verdict invariant across runtime value substitution (sentinel basename, var name, CSID form, target filename, subdir path) but flips on injection through the varied slot; no updatedInput emitted; non-Bash + malformed JSON hygiene; requiring the module attaches no stdin listener and does not exit, and evaluate labels allow / passthrough(shape-mismatch) / none(not-applicable) without changing the payload decide produces |
test_blueprint_allow_js.py |
blueprint-allow.js |
119 | Verbatim block/command/trailing-comment/trailing-whitespace/CRLF variants allowed with src provenance, no updatedInput; one-char deviations and recombined halves of two manifest entries passthrough; missing manifest, malformed JSON, wrong-typed entries, non-Bash tool, malformed stdin fail open; independent danger re-check refuses even a tampered danger-token entry the generator would never emit; normalization vectors shared with the generator via blueprint_normalization_vectors.json (digests agree across JS/Python); evaluate labels each decline by the check that made it (danger, manifest-unavailable, bailout, no-match) and carries a digest exactly when the command was normalized |
test_write_guard_js.py |
write-guard.js |
47 | Every protected class asks with its labelled reason (agent instructions, permission config, lint gate, package metadata); in a git repository a tracked protected file (relative or absolute path), permission config and the commit gate included, passes through while untracked, new and gitignored ones ask; a missing protected file asks on creation and passes afterwards in the same session (a denied creation, a pre-existing untracked file, another session and a dangling symlink still ask); .github/** scripts and workflows, CHANGELOG.md and lockfiles never ask, tracked or not; absolute paths, Windows backslash separators and case variants (Claude.md, PYPROJECT.TOML β case-folding filesystems) match; Write and NotebookEdit (via notebook_path) guarded alongside Edit; routine source/test edits and anchored near-misses (src/my_changelog_helper.py, src/github/client.py) pass through; unguarded tools, missing path and malformed stdin fail open; the only permissionDecision in the source is ask, and no updatedInput is emitted |
test_enforce_audit_header_js.py |
enforce-audit-header.js |
β | Current aggregate summary in the first question's text (summary path and Audit Report headline; option fields and reply text alone denied), 1,500-character cap, a shown follow-up never settling final-reply delivery, zero findings, missing/unreadable evidence, recovery questions, and sentinel lifecycle. |
test_enforce_profile_header_js.py |
enforce-profile-header.js |
β | Current report summary in the first question's text (report path, Title, Outcome; option fields and reply text alone denied), 1,500-character cap, a shown follow-up never settling final-reply delivery, recovery boundary, state-file parsing, and sentinel lifecycle. |
test_report_header_table_js.py |
report-header-table.js |
β | Question-time summary (report path, Title, Outcome or Verdict in the first question's text; later questions, option descriptions, previews, tables and HTML denied; 1,500-character cap; optional previews held to the host preview cap), Stop judged on the final reply only and never settled by a shown follow-up, once-per-report continuation, inline-markdown value cells and workflow recognition. |
run_hook and state_dir = pytest fixtures in conftest.py; test methods receive as parameters β no from conftest import needed.
Why Python tests for JS hooks? Black-box contract tests: each spawns real node process, sends JSON payload on stdin, asserts filesystem side-effects + exit codes. Python = orchestrator only β JS runs for real. One runner (pytest) covers all plugin tests (Python bin/ scripts + JS hooks), no package.json or node_modules required. Tradeoff: covers hook interface contract, not individual JS function internals. Jest = right addition if internal logic ever warrants unit testing.
Python bin/ script tests¶
| File | Script | Tests | Covered |
|---|---|---|---|
test_check_routing_links.py |
check_routing_links.py |
42 | Computed path resolution, orphan-risk detection (R2), bin-ref integrity (R3), security path guard |
test_check_codemap_guard.py |
check_codemap_guard.py |
44 | MANIFEST-managed (read with ast.literal_eval) vs registry-declared vs provider-CLI classification, registry rot detection, git-root anchoring and raw-project-name invariants |
test_build_blueprint_manifest.py |
build_blueprint_manifest.py |
37 | Normalization (CRLF, quote-aware comment strip, blank-run collapse, spacing variants never collide), logical-command splitting (never on ;/&&, continuation-safe, escaped-backslash-aware), heredoc/quote-span bail-out to whole-block-only, danger-token detection (deferred + block-level, dropped count reported), block+command dual hashing with src at the opening fence, cross-OS path posix-ification (no backslash ever in emitted JSON), --update/--check drift + determinism, byte-identical two-run reproducibility |
test_symlink_with_guard.py |
symlink_with_guard.py |
66 | Create/update/remove symlinks, guard against stale links, unconditional purge of ~/.claude/skills/ links |
test_extract_code_blocks.py |
extract_code_blocks.py |
30 | Fence parsing, lang normalisation, heuristic code/prose classification, token filtering |
test_check_agent_waits.py |
check_agent_waits.py |
42 | Waiting-tool, fixed-poll and bookkeeping-turn detection vs prohibitions; repo-wide zero-finding gate over every plugin; foundry spawn sites use the shared deadline protocol; quality-stack nested index freshness; task-only blocks flagged when they set in_progress or never say they ride with real work (reflow-safe); task-driving skills must state the ride rule |
test_check_bash_persistence.py |
check_bash_persistence.py |
75 | Cross-block variable reference detection, env-var filtering, multi-block files, modes/ scan, explicit --scan-dir with no match |
test_find_polluter.py |
find_polluter.py |
64 | Safe/unsafe node-id validation, isolation test runner, bisect loop (timeout exits 1), path-traversal + report guidance |
test_check_cli_flag_drift.py |
check_cli_flag_drift.py |
35 | AST flag extraction, invocation-scoped matching, docstring Usage:-block scan vs own argparse surface, REMAINDER passthrough, no-exec guarantee |
test_audit_log_js.py |
hooks/lib/audit-log.js |
62 | Canonical serialization and record hash matched against committed cross-language vectors (key order, explicit null versus absence, non-ASCII and above-BMP characters, integers only, ill-formed strings rejected); filesystem keys hashed so a/b and a?b cannot merge and lone-surrogate ids are rejected before hashing; directory 0700 and file 0600; append never rewrites earlier bytes; disabled, unwritable, symlinked and unserializable cases return _unwritten rather than throwing; _no-session soft cutoff refuses and leaves the file byte-identical; identity read from plugin.json, not the version-named directory; the export surface contains no file-removing API |
test_shell_git_js.py |
hooks/lib/shell-git.js |
259 | Shell lexer shared by the guards: simple commands across quoting, substitutions ($(β¦), backticks, process substitution), heredocs (quoted bodies as data to data commands, bodies fed to a shell as code, a heredoc inside $( that bash 3.2 could close early failing closed), wrappers (env, xargs, sudo, find -exec), zsh-only and bash/zsh-divergent syntax marked unreadable with a named reason, and the coarse fallback pass. |
test_hook_stdout_captures.py |
both decision modules + allow-dispatch.js |
37 | Replays fixtures/hook_stdout_captures.json, the raw stdout bytes both modules wrote before the refactor, across the whole command corpus and six manifest scenarios: each module's standalone driver is byte-unchanged, and the dispatcher writes `blueprint |
test_allow_dispatch_js.py |
allow-dispatch.js |
30 | One row per call carrying the effective verdict plus every lane's verdict; blueprint outranks shape when both allow; a gh read is allowed at rank 3 with no record, and blueprint outranks it; real dispatcher rows verify with three lanes, or two when the optional gh-read module is absent; whitespace splits the lanes (passthrough versus none); both-none writes no row; a module that throws or fails to load leaves the other deciding and is recorded module-error; a stalled shape lane does not delay an already-written blueprint allow; RIG_AUDIT=0 never loads the library, and a broken library leaves stdout and exit code untouched; raw command text never reaches the log and nothing outside the audit directory is created; integration-marked concurrent writers all verify their own hashes |
test_github_read_allow_js.py |
github-read-allow.js |
429 | Allowlisted gh reads (read verbs with any option, help and local config reads, gh api GETs incl. ?query, explicit -X GET fields and double-quoted endpoint variables after a literal /, GraphQL queries the text shows), each optionally piped into the closed filter set, are allowed in a repository, outside one and in a spawned agent with no record, and gh-write-guard passes every one; filters reading or writing a file, running a program or reading the environment, run-time text outside the endpoint, GET fields that may reach GraphQL, config get oauth_token, extension help, gh writes and unreadable quoting are not; without the lexer nothing is allowed |
test_heredoc_in_substitution_js.py |
lib/shell-git.js |
9 | A heredoc inside $(β¦) whose body closes the substitution early in bash 3.2 is unreadable syntax (fail closed); the git commit -m "$(cat <<'EOF' β¦ EOF )" idiom with balanced parentheses or an apostrophe stays readable. |
test_audit_close_js.py |
audit-close.js |
32 | Success and failure rows with the same status words timings.jsonl uses; both events write both rows; authority is never inferred from execution; non-Bash and unhandled events write nothing; lifecycle rows are skipped without a usable session id while completion rows still land in the shared stream; four simulated plugins produce four retained rows naming their own versions; stdout empty on every event; nothing created outside the log |
test_verify_blueprint_audit.py |
bin/verify_blueprint_audit.py |
107 | Table-driven against fixtures/audit/ β a corpus and its expectations built before the verifier existed. Exit 1 only on a failed record hash; torn framing and every classification stay warnings. Covers the classifications that were wrong in earlier designs: a healthy four-plugin file is clean, four closers are corroboration while a missing one is incomplete-closure, a four-plugin shape allow is clean apart from multi-allow, abstention requires a real passthrough, and none/module-error are counted apart from it. Plus the reader's own robustness: a stray directory or dead symlink in the log dir must not raise, since an uncaught raise exits 1 and that code means corrupt-record. Plus acceptance of two or three distinct known verdict lanes and rejection of missing, extra, duplicate or unknown lanes and values this system never writes, both limits, the JSON output shape and totals, and prune's dry run, age cutoff, --keep-session and refusal to prune _no-session.jsonl |
test_audit_registration.py |
hooks.json + propagation + docs | 38 | Every plugin registers the dispatcher, registers neither decision module directly, and registers the closer on all four events with the right matcher; the four new files are in the propagation manifest with all three copies and the check-only gate passes; the verifier is named in all four READMEs (a gap check_orphaned_bin.py does not cover) and no document still claims a registration order; the shape module still runs as a subprocess for audit_hook_coverage.py; a claude-state-<session> teardown cannot touch the audit log |
test_audit_hook_coverage.py |
audit_hook_coverage.py |
22 | Manifest union across plugins (single-plugin probe under-reports), newest-version selection by numeric key at any nesting depth, mechanism attribution with blueprint winning over shape, memoization, shape-hook argv/payload dispatch, transcript parsing (Bash-only, malformed-line and vanished-file tolerance), --since/--project/--skills-only filters |
test_verify_perm.py |
verify_perm.py |
21 | Settings allow-entry detection, missing/malformed JSON, CLI exit codes |
test_check_orphaned_bin.py |
check_orphaned_bin.py |
21 | Orphaned bin/ script detection, consumer-reference parsing, multi-plugin scan |
test_cost_analyzer.py |
cost_analyzer.py |
30 | Message-id dedupe (3x-inflation guard), per-model pricing (future id of a known family at that family's newest price with a stderr warning, no-family id and retired id without its own row at per-field max, cache write/read ratio per model, 1h vs 5m write split, Haiku 5.5 prompt-size band), main/sidechain bucketing with per-call cost, subagent-transcript merge, session discovery no-double-count, CLI session-id + window modes |
test_efficiency_measured.py |
efficiency_measured.py |
113 | Content-block row merge (tool names survive, Bash reduced to first real word), per-skill command spans, model-switch and idle-gap thresholds (growth and compaction excluded), literal-CLI prefix, exact pre-gate cost, pinned baseline and delta, label normalisation, project scoping, unreadable-transcript skipping, CLI exit codes |
test_jq_write.py |
jq_write.py |
50 | Arg parsing (unlisted flag exits 3), JSON path writes, merge semantics, a private temp file per call (a foreign <target>.tmp untouched), file mode kept, symlinked settings.json written through, $HOME before Path.home() |
test_resolve_shared_path.py |
resolve_shared_path.py |
37 | Plugin/subdir validation, path traversal rejection, tier-1/2/3 resolution cascade, registry record beating a newer cache dir, un-prefixed provider source fallback, tier 3 refused outside the plugin's own source repository |
test_heal_git_artifacts.py |
heal_git_artifacts.py |
83 | PID liveness on both platform branches (Windows simulated on every host, POSIX probe patched so signal 0 is asserted rather than sent), lock verdicts, worktree tiering, the --include-orphans opt-in and pre-removal orphan re-check, symlink reporting, the rejected * prefix, CLI exit codes, and a guard that no doctest calls the POSIX probe β signal 0 is CTRL_C_EVENT on Windows, so one did abort every Windows CI leg with a KeyboardInterrupt |
test_check_fence_symmetry.py |
check_fence_symmetry.py |
17 | Unclosed fences, nested/interleaved fences, multi-file scan |
test_check_gitignored_refs.py |
check_gitignored_refs.py |
24 | Token extraction, existence+check-ignore classification in a real tmp checkout, waiver marker, placeholder templates, CLI exit codes |
test_check_bridge.py |
check_bridge.py |
17 | nested registry parsing, installPath requirement, stale-cache rejection, settings opt-out, marketplace allowlist |
test_make_run_dir.py |
make_run_dir.py |
17 | Portability invariants (no /tmp literals, no utcnow), timestamp format, links into instruction sources |
test_check_spawn_prompt_vars.py |
check_spawn_prompt_vars.py |
16 | $VAR in markdown code blocks, caller-substituted-var whitelist, multi-file scan |
test_check_tag_symmetry.py |
check_tag_symmetry.py |
14 | Empty/whitespace XML blocks, unbalanced open/close tags, multi-file scan |
test_resolve_skill_subdir.py |
resolve_skill_subdir.py |
16 | Tier-1/2/3 subdir resolution cascade, --local-only project tier (.claude/skills), fallback ordering |
test_purge_plugin_cache.py |
purge_plugin_cache.py |
15 | Report-vs-apply contract, seven deletion guards, --expect-count abort, argument rejection |
test_audit_static.py |
audit_static.py |
14 | Layer-1 checker aggregation: seeded-defect detection, clean-scope pass, option-shaped file names passed after --, JSONL output |
test_state.py |
state.py |
28 | Set/load round trip, unsafe and reserved key rejection, line-break value rejection, session-scoped namespaces, clear |
test_resolve_memory_dir.py |
resolve_memory_dir.py |
30 | Claude Code directory-name slugs (case kept, one hyphen per code unit, Windows/Unicode, 200-char hash cut), git fallback, missing-git fallback |
test_resolve_plugin_root.py |
resolve_plugin_root.py |
9 | Registry lookup, cache-scan fallback, orphaned-version skip, security gates (cache-dir + manifest-name) |
test_get_plugin_install_path.py |
get_plugin_install_path.py |
18 | Registry lookup, multiple-entry tie-breaking, missing plugin exit code, stale loaded-root warning (orphaned or not the installed path) |
test_classify_resolver_sites.py |
classify_resolver_sites.py |
5 | Directory scan for extractable/non-extractable resolver sites, CLI report + --list-extractable β maintainer-only, ad hoc invocation, not wired into any skill |
test_c33_dir_resolution.py |
(C33 dir-resolution logic) | 4 | Latest-version selection, older-version exclusion, no-cache fallback |
test_consolidate_inventory.py |
consolidate_inventory.py |
671 | Section splitting (nested fences, frontmatter), paths: parsing, load classes incl. match-everything globs, stub/_full pairing, install-alternative and CLAUDE.src.md groups, shared-file, policy-sibling and frontmatter blocking, inbound citations, merge-scope groups (cross-plugin, per-skill lazy, agents), cluster guard (single-file, degenerate, duplicate, overlapping, unsafe id), group selection, settle rules and digest-bound review verdicts, cited deep headings, shared-file digest, strict paths: reading, one CommonMark block reader (containers, lazy lines, agreement with markdown-it) and edit simulation, inline-markup titles, flow-list comments, stale inventory and re-derived reports, malformed inventory and report files, read-only guarantee, real-tree controls |
test_audit_preflight.py |
audit_preflight.py |
52 | Bare-scope/challenge-alias parsing, --keep flag extraction, tool-presence marker caching (fresh/expired/corrupt), session-scoped state files, mutually-exclusive-flag and missing-dir exits, template resolution, links at state children |
test_check_bin_test_coverage.py |
check_bin_test_coverage.py |
17 | Real-function counting vs stub bodies (pass/ellipsis), async and nested-class tests counted, non-test functions ignored, syntax-error files treated as covered, missing/empty/stub-only test files flagged, private scripts skipped, --local clean/dirty tree gating |
test_check_install_state.py |
check_install_state.py |
20 | Nested/flat registry shape resolution, missing registry/foundry/cache-dir severity, settings merge completeness, statusline and allow-list checks, bridge enablement, stale-hook and rule-link staleness, native Windows and POSIX symlink-target checks, --check dispatch |
test_check_plugin_layout.py |
check_plugin_layout.py |
26 | Manifest presence/validity/required-fields/name checks, real dir/file existence severities, reference resolution, hooks.json presence and validity, hooks-registry sync across plugin/settings, setup-skill keyword coverage, absent-CLI and claude plugin validate timeout degrading to a skip so 8f/8g still run, absent-plugin-dir and broken-plugin runs both reporting on stdout at exit 0, plugin-root reference extraction |
test_check_rtk_alignment.py |
check_rtk_alignment.py |
17 | Single/double-quoted prefix-array parsing, missing/unreadable file fallback, indented-command extraction, aligned-list pass, unknown/unlisted/meta-command classification, grep -w word-boundary semantics (prose-only mention valid, hyphen a boundary, bare substring still invalid), RTK/hook-absent skip, unparsable list skip, rtk --help timeout skip, misaligned prefixes reported on stdout |
test_check_skill_contracts.py |
check_skill_contracts.py |
27 | Bare-comment and shell-timeout exemption detection, single/multiline Bash-call timeout presence, add_argument exemption that a docstring mention does not satisfy, config-file discovery across both roots (installed .claude/agents/*.md, source plugins/*/agents/*.md, nested rules/_full/, unrelated Markdown excluded), mode-file shadow and overlap detection, --check dispatch, non-local 32f skip |
test_list_audit_files.py |
list_audit_files.py |
19 | Local sweep finds skill entrypoints/modes/templates/shared, excludes nested agent sidecars and rules/_full, installed sweep narrower, entrypoints-only sweep, NUL/newline bare-path output and -0 argument guard, dedupe+sort, block counting per file, header/relative-path labelling, local vs installed root selection |
test_measure_config_size.py |
measure_config_size.py |
17 | Empty-dir header-only output, agent/skill/rules size thresholds (warn/fail), skill path labelling, tighter rules budget, global CLAUDE.md counted once, delivered ~/.claude/rules/ counted and per-file flagged, symlinked rule counted once, --mode required and inventory/overhead dispatch |
test_resolve_agent_file.py |
resolve_agent_file.py |
12 | Bare/plugin-prefixed agent and skill resolution, leading-slash skill selection, cc-prefixed vs bare plugin directory precedence, single/ambiguous/no-match glob resolution, resolved vs unresolved CLI output, --local failure labelling |
test_scan_efficiency_signals.py |
scan_efficiency_signals.py |
24 | Frontmatter block extraction, Agent-in-loop detection (for/while) with batch-guard/head-limit exemption, declared-model presence, file-type inclusion (SKILL.md/agent files, modes excluded), report section ordering, boilerplate file counting, missing-model reporting, CLI exit code |
test_size_importance.py |
size_importance.py |
129 | Block splitting (fences kept whole), the obligation proxy, spine on any doubt (high-stakes words beat the gate, directive-first, structural words, imperatives and ordered items under a rationale label), exact gate shapes with paragraph-level proof for a gated block, move and trim by load class, the own-gate exemption, copy collapsing, the pre-trim versus split-out real-case shape, and the command line. |
test_append_ledger.py |
append_ledger.py |
9 | Staged record appended after existing lines, staged file consumed, missing trailing newline supplied, parallel <ledger>.<writer>.rec swept in name order, absent ledger created, empty flush no-op, --from sources appended and kept, missing source skipped, --from leaves staged records alone |
test_write_skill_contract.py |
write_skill_contract.py |
93 | Five-field contract write, optional seven-field form appending a labelled ledger list (empty items omit the block, blank lines dropped), keep-file first line only (cut at bare CR and other line separators, newline-less file adds nothing), parent-dir creation, overwrite of previous contract, cwd-relative path, verbatim placeholder survival, wrong-arity/empty-skill exit codes, --help usage, no file written on usage error |
test_compaction_boundary.py |
compaction_boundary.py |
19 | Boundary-step excerpt cases and byte-equality with the replaced sed range, contract lookup (caller dir, own plugin, none), gate pass-through, resolver failure, gate failure, missing resolver and contract, -h |
test_plugin_layout.py |
plugin_layout.py |
7 | Manifest-declared skills/agents directory per host manifest, defaults when a manifest is invalid or declares neither, PLUGIN_LAYOUT: lines, missing plugins directory tolerated |
test_check_agent_plugin_root.py |
check_agent_plugin_root.py |
9 | Only the ${CLAUDE_PLUGIN_ROOT:-β¦} form is flagged, per-line findings over --scan-dir, clean-scan pass line, non-Markdown file ignored, repo-wide gate that no foundry agent defaults the plugin root |
test_extract_keep_flag.py |
extract-keep-flag.py |
23 | Option-value reading, keep value written and printed, stale contract cleared only when owned and not inlined, missing slug / session id exit 2, --out confined to allowed roots, symlink at the sentinel replaced not followed, venue persisted |
test_merge_settings_permissions.py |
merge_settings_permissions.py |
43 | Output matches the replaced jq merge, user rule order kept, write through a symlinked settings.json, idempotent second run, file mode kept, $HOME before Path.home() (and the fallback), paths outside the write root refused, bad input leaves settings unchanged |
test_settings_json.py |
settings_json.py, setup skill |
43 | Every subcommand, exact probes, atomic replacement and failure cleanup, preserved env values, idempotent shipped setup blocks with jq forced to fail. |
test_bin_input_hardening.py |
several foundry bin/ scripts |
34 | Trailing-newline token validators, purge registry guard, resolve_agent_file path components, wildcard session id, literal ledger names, sentinel-name separators, symlink-guard marker and source checks, bounded Git calls in the version-sync check |
test_codemap_changed_modules.py |
codemap_changed_modules.py |
21 | Default-branch detection, merge-base vs HEAD~1 comparison point, NUL-separated changed .py files, index-name resolution and cap, absent CLI / unusable payload / unchanged tree as silent no-ops, --base/--cap parsing |
CI runs the full suite on every push to main and on PRs touching plugins/ (see .github/workflows/ci-tests.yml).
Rule table (full descriptions)¶
| Rule file | Applies to | Governs |
|---|---|---|
communication.md |
all | Re: anchor format, progress narration, tone, output routing, breaking-findings format, terminal colors, confidence display, Markdown no-hard-wrap stub (structure selection in markdown.md) |
quality-gates.md |
all | Confidence block format, independent adversarial review/fix loop, link verification stub (full wording in markdown.md), Python complexity-limit stub (rule in python-code.md), output routing (long output to file) |
git-commit.md |
injected by rule-inject.js after the first commit-creating git call (add/commit/push/merge/revert/cherry-pick/am/rebase, annotated tag) runs; **/.git/COMMIT_EDITMSG, **/.gitmessage |
Commit message format, diff-gathering before writing, co-author trailers, branch + push safety (hard bans stay always-loaded in claude-config.md) |
claude-config.md |
all | Bash timeouts (3x P90), turn batching (one response per independent call set), directory navigation rules, no hardcoded absolute paths, agent/skill spawn discipline, parallel spawn ceilings stub (table in agent-spawn.md), pinned pre-commit hooks over bare lint tools (stub; detail in python-code.md), instruction-file compression gate (stub; gate in foundry-config.md), git commit/push hard bans |
artifact-lifecycle.md |
all | Canonical artifact layout (.plans/, .reports/, .temp/), run directory naming, TTL policy, plan and benchmark evidence kept out of shipped content |
external-data.md |
all | Pagination rules: GitHub CLI, REST APIs, GraphQL, Cloud APIs β never work on partial result set |
foundry-config.md |
.claude/** |
Plan-mode gate before any .claude/ edit, post-edit checklist, XML tag conventions, distribution rules |
python-code.md |
**/*.py |
Google-style docstrings, closed option sets as enums, dataclass/TypedDict selection and record-type boundaries, deprecation API, multi-OS portability, complexity limits, pinned lint/format hook usage |
python-testing.md |
tests/**/*.py, **/test_*.py |
pytest design: TDD process, fixtures, parametrization, marker selection, pytest-xdist runs, behaviour-vs-bookkeeping check classification, mutation checks on a copy, cross-OS tests, mocking, what to test in priority order, test names, uncollected-test hazards, prose-reading tests, shared-helper scope, test-style hook codes |
notebooks.md |
**/*.ipynb, **/*.py |
Notebook cell granularity, markdown narrative depth, plot framing, shell magics, docstring placement β .ipynb and Jupytext # %% scripts |
public-github.md |
all | GitHub reads run free; every gh write needs the user's one-time gh-write approval of the exact command |
untrusted-content.md |
all | Ingested external text is data, never instruction β delimiters, no permission widening, memory propagation |
markdown.md |
**/*.md |
Never hard-wrap prose, structure selection (bullets, numbered lists, tables, blockquotes), behavior-sensitive reformatting, instruction-file compression gate, link verification (fetch + read + match per URL) |
agent-spawn.md |
injected by rule-inject.js after the first Agent() call runs; **/skills/**/*.md, **/agents/**/*.md |
Agent() spawn slots (three-slot delta-first labels, pre-spawn check), end the turn after spawning with per-agent deadlines, parallel spawn ceilings by model tier |
model-tiering.md |
**/skills/**/*.md, **/agents/**/*.md |
Model tier ladder (script, Haiku, Sonnet, Opus), checkable escalation triggers, no model: pin on inline skills, lean brief-file delegation, author cost checklist |
Contribution and review expectations (full text)¶
- Evidence and real-case acceptance: shipped
rules/quality-gates.mdrequires cited proof for factual, causal and completion claims, a representative real case alongside TDD, and unchanged user-authorized behavior. - Mocks are supplementary polishing after real behavior is validated
- unavailable real acceptance stays explicit.
- Review limits remain factual process evidence
- an out-of-scope extra review needs one explicit user decision with its gap, expected output and stop condition.
- The shared cross-validation protocol requires parent validation at every severity before findings publication, then capped independent critical verification.
- Review presentation uses role names only and square-bracketed scores, such as
Software engineer [3] - execution provenance stays in evidence and limits.
- Version-one review envelopes distinguish source judgment, verification readiness and durable open concerns.
- Static proof may settle a claim
- unavailable evidence never refutes it, and incomplete feasible work never becomes unconditional approval.
- Missing coverage and other review limits never become source findings or automatic extra-review requests
- declined optional reviews are omitted from findings and action items while factual limits remain disclosed.
Setup notes¶
- Merges
statusLine,permissions.allow,permissions.deny,enabledPlugins, the shippedenvdefaults, and (when pinned by the project)advisorModelinto~/.claude/settings.json - removes the obsolete
Bash(gh api repos/*:*)allow entry, the broadBash(claude:*),Bash(find:*),Bash(node:*),Bash(sed:*)andBash(eval:*)allow entries (permissions-allow-retired.json), and the retired deny entries (gh writes, local branch/tag deletion;permissions-deny-retired.json) while preserving every other existing allow and deny entry. - It also symlinks all rule files into
~/.claude/rules/asfoundry-<name>.mdandTEAM_PROTOCOL.mdinto~/.claude/. - Run it from the project repository root.
- The setup summary counts removals from the complete retired allow set.
- Idempotent β safe to re-run.