Repository navigation
6.0.0-m8
Pre-release6.0.0-m8: a narrower command-gate allowlist, approvals that cannot be forged or reused, the rest of the adversarial review, the first-day field items, both m7 reviews, a faster suite (pre-release)
The Fusebase Flow 6.0 core with items 5 to 10 of the owner's list: the rest of the 6.0.0-m4 adversarial review (the manifest-row forgery, approvals, merges, unicode paths, symlinked overlays, the evidence check reading what the commit takes, the FINDING marker, a post-publish re-check of the release tag); the documentation items; the eleven field items from the two projects' first day under 6.0.0-m6, among them a narrower allowlist in front of the command gate (the command parser 7.10 asked for is not in 6.0.0: the owner's cut of 2026-10-22); the findings of both reviews of 6.0.0-m7; and a test suite that runs in parallel. Both live projects upgrade from 6.0.0-m7 in one step (see "Upgrading a 6.0.0-m7 project to 6.0.0-m8").
Needs git, bash and Python 3.10+ with PyYAML. The hooks and the two approval scripts run the first of python3, python and py -3 that is 3.10+. Get this release; the second line installs PyYAML with the pip of the interpreter Flow will use:
git clone --depth 1 --branch 6.0.0-m8 https://github.com/fusebase-dev/fusebase-flow.git fusebase-flow-6.0.0-m8
<the interpreter Flow will use> -m pip install -r fusebase-flow-6.0.0-m8/hooks/requirements.txt
That is usually python3 on Linux and macOS, where Debian and Ubuntu may refuse a pip install into the system Python: use sudo apt install python3-yaml, or a venv. On Windows with python.org's Python it is python (python -m pip install -r fusebase-flow-6.0.0-m8/hooks/requirements.txt) or py -3. Windows' default App execution aliases make python3 the Microsoft Store shortcut, which fails the 3.10+ check, so Flow goes on to python and py -3; when none of the three is Python 3.10+, the commit is refused until you turn off the python.exe and python3.exe App execution aliases (Settings, Apps, Advanced app settings) or install Python 3.10+. The clone may print warning: refs/tags/6.0.0-m8 ... is not a commit!: an annotated tag in a shallow clone, harmless (the checkout is the tag's commit). The bash lines below run in Git Bash; in PowerShell, the mint line is $env:FF_BOOTSTRAP_TTL_MIN=30; & "$env:ProgramFiles\Git\bin\bash.exe" hooks/local/write-bootstrap-approval.sh (a bare bash there can be WSL's).
Project on 6.0.0-m7, or an earlier 6.0.0 pre-release. Run the install from the project's main worktree. It adds the new files, replaces the Flow files you have not edited, keeps the ones you edited, rewrites its marked blocks, reinstalls the git hooks, records where the push check starts (.git/flow/range-base, below), and touches nothing else:
python fusebase-flow-6.0.0-m8/maintainer/flow.py install --target <your-project> --providers <the providers audit/flow-install.json lists>
If you edited policies/approval-policy.yml before 6.0.0-m3, the re-run keeps your copy, which has no gate_rounds; the gate then allows 2 rounds, the shipped value. An upgrade needs no --wire-hooks (the wiring you have stays) and no --register-reader (a reader task stays as it is). Commit the result as the protected commit the section below shows, under a flow: subject, then check it with python fusebase-flow-6.0.0-m8/maintainer/flow.py health --target <your-project>.
The command gate: 6.0.0-m7's matcher over one allowlist (field item 7.10, not closed; the owner's cut of 2026-10-22). The deny and approval rules (FR-06, FR-12, FR-13, FR-07) match the raw text of a Bash line, as in 6.0.0-m7, after one allowlist takes two kinds of text out, and nothing else: a heredoc body, unless the line that opens it runs bash, sh, zsh, dash, ksh, fish, ssh, docker, podman, kubectl, xargs, psql, mysql or sqlite3; and the quoted arguments of grep, egrep, fgrep, printf, echo and sed, unless that echo or printf is piped into one of those. So grep -n "rm " hooks/local/*.sh, a heredoc into notes.md that mentions fusebase deploy, and echo "To clean up: rm -rf build/" >> NOTES.md are allowed; rm -rf x, fusebase deploy and rm build.log are still denied, and so is a heredoc into bash or ssh host, and echo 'rm -rf x' | sh. A commit message, or any other text outside those two, that quotes a gated command is still refused: reword it, or commit with git commit -F <file>. A PowerShell line is matched as written. The command parser built during 6.0.0-m8 (T353 to T382) left the gate, the tree and the kernel lock (T384). Any error on the way to a decision denies the call (T371), and the hook reads Claude Code's and Codex's JSON as UTF-8 (T385), so a Bash line in Cyrillic, with smart quotes or an emoji is judged, not refused.
Approvals (M6-6). Every protected-path approval is bound to the staged bytes (a digest), lives at most its action's TTL (60 minutes for a protected path; 30 is what the commits use), records the commit HEAD was at when it was minted, and is spent by the first commit that lands on that HEAD: a copy, a hand-written artifact, a reset or an expired one approves nothing. write-bootstrap-approval.sh --category <category> mints for any category the policy declares, and its go-ahead form (--paths, minted before the edit) for any but ci_cd_config; approve-local.sh protected_path_edit names the mint instead of writing a path-and-TTL artifact. A command approval is capped by its action's artifact_ttl_minutes too. An agent's Edit or Write into state/approvals/ or into the approval record in .git/flow/ is denied. Health lists an expired or unreadable artifact left in state/approvals/ (reported, not unhealthy).
flow: commits (M6-7). A flow: commit accepts a path as Flow's only when its staged bytes match the staged audit/flow-install.json row and HEAD's install record already lists it or it lies where the install writes; a row the same commit adds no longer makes project code Flow's. audit/flow-install.json is a protected path, so every Flow install and upgrade commit is a protected commit (it already was).
Merges (M6-8). commit-msg treats a commit as a merge only when MERGE_HEAD names a commit HEAD lacks; a merge that adds a change none of its parents holds (a conflict resolution, staged code) needs that change's task in the subject, as Merge branch 'x' (T17: resolve the conflict in ...).
Unicode and symlinks (M6-9, M6-14, field item 7.6). The install and migration read git's output as UTF-8 and the scripts print UTF-8, so a project folder named café or in Cyrillic installs, and § prints as §. A symlinked AGENTS.md or CLAUDE.md that git checks out as a link (Linux, macOS, Git for Windows with core.symlinks=true) is refused by the install (the block to add by hand is printed) and stops the migration's --apply; on Git for Windows' default core.symlinks=false it is not detected (Limits).
Evidence (r3-83, r1-28, field items 7.2, 7.3, 7.7). The pre-commit's evidence check refuses a staged evidence file, run log or capture whose working copy differs from what is staged, so it reads what the commit takes. A run log's finding counts in any spelling (FINDING:, finding, **Finding**, FINDINGS) and after a blank line inside its table. A test-id row must name a whole test title (a quoted title, or a def or func the name declares), never a word of the file. An indented TAP line (Node's subtests) holds a test-id row. The deploy-order check prints WARNING deploy order not checked: ... when state/deploys/<env>.jsonl holds no finished deploy or the run log's title names no environment.
Tasks, gates and slices (field items 7.1, 7.4, 7.5, 7.8, 7.9, 7.11). A task id is never read inside an item id (O-T276-2 names no T276). A plan:, ship: or flow: commit that names a task is not that task's commit (its time and paths no longer count for it). The item-edit flag gets its own row in state/flags/<slice>.tsv, so the planner can rule it. A verdict carried over from Flow 5.x (carried=5.x, or a note starting "carried") spends no gate round. When the rounds are spent, the gate's carry-over lists each reader's last round only and files an entry by the class it declares (the verify skill's return carries class), never by a keyword. A project's own tool profile goes in policies/agent-tools.local.yml: new variants of builder or planner and new input kinds, read-only tools only (a write or shell tool it lists is dropped, with a line in the slice).
Pushes and merges, m7 review A. The install records the HEAD it found in .git/flow/range-base; the push check judges every commit the remote lacks that this commit does not contain, so a branch cut before the upgrade is judged too. A line that starts ++, and a line an octopus merge adds, are scanned (a diff's file header is told by its position, before the first hunk). Only the pushed remote's tracking refs count as already there. range_check.py --record-commit <sha> needs --go-ahead, the operator's word in chat, and writes an approval_record line to state/audit.log.jsonl. A Write by a Windows spelling of a protected path (\\?\C:\..., \\?\UNC\localhost\C$\..., \\localhost\C$\..., \\.\C:\..., a trailing dot or space, ::$DATA) is judged as the file Windows opens. Only the run of lines a 5.x comment opens goes as 5.x line-ending pins; a project line spelled like a pin elsewhere stays.
Policies, health and wiring, m7 review B. The tool-time gate reads the shipped policies/*.yml as committed at HEAD (the working tree only while HEAD lacks the file), so a script that rewrites policies/command-policy.yml, or a checkout of an older one, loosens nothing; checking out or restoring files in policies/ is denied. A malformed *.local.yml is named in the denial, with how to fix it. The shell-write deny holds for the project's own policies/, state/approvals/ and .git/flow/ (a bare, ./, .. or absolute path), not a nested folder of that name such as apps/api/src/policies/. Health counts a hook manager's hook that runs Flow's (a line that calls hooks/git/<name> or .git/hooks/<name>), and core.hooksPath=hooks/git, as installed. The wiring check reads what Claude Code and Codex run: disableAllHooks, an echo of the handler's path and a matcher that excludes Bash are unenforced; a backslash path, a UTF-8 BOM and .claude/settings.local.json are read.
Printed steps. The kernel's printed next steps name only what a 6.0 tree ships: the git-hook installer ends "Check with: python /maintainer/flow.py health --target ", the pre-commit names the Flow install (not bootstrap-upgrade.sh) to restore a missing runner and asks for FAIL: <why>, the module-size adoption prints a T<n>: subject, and the mint prints no DeprecationWarning. The gate's phase PASS line prints two commands that run.
The suite. python maintainer/tests/test_migrate.py runs the suite's classes across worker processes (FLOW_TEST_WORKERS, default the CPU count up to 4; 1 runs one process), and builds the Flow-installed test project once per process, copying it per test. On this pass's Windows host, loaded by other work, the suite's 245 tests (before the fix pass) ran in 9,310 s with 4 workers; 6.0.0-m7's 188 took 22,486 s in one process there. The heaviest classes now bound the run: the go-ahead tests (test_m6.GoAhead and its 8.3-name twin) took 45 to 51 minutes each.
New project: the same install command with the providers you want, such as --providers claude,codex, at the top level of a git repository's main worktree.
Project on Flow 5.x. First run the dry run. It prints what it would remove, replace and add, what it never touches, and the open branches and worktrees that do not carry Flow 6.0 yet, and changes nothing. The dry run runs the structural preflight; --apply runs the project's lint and typecheck before any change; so a dry run that exits 0 means the migration will go through only once those two also pass by hand. Then run the migration, from the main worktree:
python fusebase-flow-6.0.0-m8/maintainer/flow.py migrate --target <your-project> --providers claude,codex
python fusebase-flow-6.0.0-m8/maintainer/flow.py migrate --target <your-project> --providers claude,codex --apply --go-ahead
--apply first checks everything that could stop it, and changes nothing if a check fails. It copies every file it removes or replaces into .flow-migrate-<UTC time>/. The migration lands as one commit, flow: migrate Fusebase Flow <5.x version> to 6.0.0-m8, through the new git hooks, and writes the Flow .gitignore block. It also removes the 5.x repair backups (*.pre-flow-repair, *.pre-flow-repair.<n>) left under .claude/, .agents/ and .codex/ whose file goes with the migration or is already gone; a folder that holds its own .git (a worktree under .claude/worktrees/, a submodule) is another checkout, and nothing in it is touched. Flow 5.x's line-ending pin lines go from .gitattributes (the dry run lists them). Its report, at the top of docs/flow/ledger.md, says what to do next (carry the open 5.x work into docs/flow/ with the plan skill, one backlog item per evidence row, and keep the gate as your own step, never a builder's task) and the health check to run after that commit, and ends with the undo block below.
Undo. The end of --apply and the migration report in docs/flow/ledger.md print this block with the values filled in. Paste it into Git Bash at the project's top level, as one unit. It checks out the pre-migration commit on a new branch, puts the 5.x git hooks back and restores the untracked files the migration removed or overwrote, from the backup folder. If that folder is gone, the block takes the hooks from the 5.x tree's hooks/git/ and says so, and those untracked files cannot be restored; if that tree has no hooks/git/ copies either, it stops before anything changes. It stops at the first step that fails, and your shell stays open. The branch you migrated keeps the migration commit.
( set -e
if [ -d ".flow-migrate-<UTC time>" ]; then :; elif git cat-file -e "<the pre-migration commit>:hooks/git/pre-commit" 2>/dev/null && git cat-file -e "<the pre-migration commit>:hooks/git/commit-msg" 2>/dev/null && git cat-file -e "<the pre-migration commit>:hooks/git/pre-push" 2>/dev/null; then echo "NOTE: the backup folder .flow-migrate-<UTC time>/ is gone: the git hooks come from the 5.x tree's hooks/git/, and the untracked files the migration removed or overwrote cannot be restored"; else echo "STOPPED before any change: the backup folder .flow-migrate-<UTC time>/ is gone, and the pre-migration commit does not hold all three of hooks/git/pre-commit, commit-msg and pre-push to put the 5.x git hooks back from"; false; fi
git checkout -b flow-5x-<UTC time> <the pre-migration commit>
for h in pre-commit commit-msg pre-push; do if [ -f ".flow-migrate-<UTC time>/.git-hooks/$h" ]; then cp -p -- ".flow-migrate-<UTC time>/.git-hooks/$h" "$(git rev-parse --git-common-dir)/hooks/$h"; elif [ -d ".flow-migrate-<UTC time>" ]; then rm -f -- "$(git rev-parse --git-common-dir)/hooks/$h"; else git cat-file blob "<the pre-migration commit>:hooks/git/$h" > "$(git rev-parse --git-common-dir)/hooks/$h"; chmod +x -- "$(git rev-parse --git-common-dir)/hooks/$h"; fi; done
if [ -d ".flow-migrate-<UTC time>" ]; then while IFS= read -r p; do mkdir -p -- "$(dirname -- "$p")"; cp -p -- ".flow-migrate-<UTC time>/$p" "$p"; done < ".flow-migrate-<UTC time>/.migrate/untracked.txt"; fi
)
Upgrading a 6.0.0-m7 project to 6.0.0-m8
Both live projects take this, from 6.0.0-m7. Run these in Git Bash, in the folder that holds your project's folder; <your-project> is that folder's name. Start from a clean tree: commit your own work first, and close the agent sessions open on the project (a session keeps the rules it loaded). The same lines serve every 6.0.0-m7 install, wired or not. On the install line, give the providers audit/flow-install.json lists under providers (both live projects have claude,codex). Leave --wire-hooks off: the wiring stays as it is.
git clone --depth 1 --branch 6.0.0-m8 https://github.com/fusebase-dev/fusebase-flow.git fusebase-flow-6.0.0-m8
<the interpreter Flow will use> -m pip install -r fusebase-flow-6.0.0-m8/hooks/requirements.txt
python fusebase-flow-6.0.0-m8/maintainer/flow.py install --target <your-project> --providers <the providers audit/flow-install.json lists>
cd <your-project>
git status --short
git add -u
git add -- hooks/local/lib/evidence_rows.py hooks/local/lib/range_record.py hooks/local/lib/wiring.py
FF_BOOTSTRAP_TTL_MIN=30 bash hooks/local/write-bootstrap-approval.sh
git commit -m "flow: upgrade Fusebase Flow 6.0.0-m7 to 6.0.0-m8"
bash hooks/local/write-bootstrap-approval.sh --consume
cd ..
python fusebase-flow-6.0.0-m8/maintainer/flow.py health --target <your-project>
What each step shows, as on the copy these steps were tested on (Onboarding at b176a73d migrated by the published 6.0.0-m4, upgraded by the 6.0.0-m6 and 6.0.0-m7 notes, Claude Code alone wired, a reader task registered and Ready):
- the install lists
added:with the three new Flow files above andreplaced:the Flow files that changed (36 on the copy:VERSION, most files underhooks/,policies/agent-tools.yml,policies/command-policy.yml,policies/protected-paths.yml,templates/ledger.md,workflows/lifecycle.mdand the verify skill with its two mirrors), andmerged: .gitattributes (block updated). Undergit hooks:it showsinstalled pre-commit,current commit-msg,installed pre-pushandcurrent pre-merge-commit, thengit hooks install complete. Check with: python <the Flow core>/maintainer/flow.py health --target <this project>. Its lineprovider hook wiring: not writtenmeans it wrote no wiring this time; the wiring you have stays. Its lineheartbeat reader: registered earlier: <the recorded line>(not scheduledwhen none is recorded) means it registered nothing this time: your task stays as it is. It also writes.git/flow/range-base, the commitHEADwas at; nothing to stage. When other local branches or worktrees have no Flow 6.0 files, a WARNING names each one; git status --shortlistsMlines, the Flow files the install changed (with.gitattributesandaudit/flow-install.json), and??lines for the three new files. If it lists anything else, stop and set your own files aside without touching Flow's, for example withgit stash push -- <your files>, then go on (a plaingit stashwould take Flow's changes too).git add -ustages the changed files, the next line the new ones;- the mint prints one line (no DeprecationWarning); the commit runs the secret scan, the protected paths and the module size, then your lint and typecheck in
commit-msg, which also puts the protected-path changes on record for the push check: about 8 minutes on the copy, on a loaded host, inside the 30-minute mint; - health ends
HEALTH: healthyand exits 0, withversion: 6.0.0-m8 installed, the four git hooks installed,wiring: claude wiredandwiring: codex unenforcedfor an install with Claude Code alone wired, and its reader line still says what the scheduler says:the task is Ready.
In a wired Claude Code session, every line goes through the project's own hooks/handlers/pre_tool_use.py; the agent mints only on the operator's go-ahead in chat. The lines inside the project are allowed. The clone, the pip line and the install run from the folder above the project before the install replaces the handler: 6.0.0-m7's handler cannot find the project's git root there and errors without blocking them. From the install on, the handler judges a line from the folder above (cd <your-project>, health) by the rules of the project it belongs to, and allows these.
Other clones of the project (a teammate's, a second machine): after pulling the upgrade, run bash hooks/local/install-git-hooks.sh in each. Git hooks are per clone; until then the clone keeps 6.0.0-m7's hooks, which check its pushes as 6.0.0-m7 did, and a push it refuses says this first.
After the upgrade. The first push runs the range check from the HEAD the install recorded: commits on an older branch that HEAD does not hold are judged now, and one carrying a protected-path change with no approval on record is refused until the operator records it (python hooks/local/lib/range_check.py --record-commit <sha> --go-ahead). Health may say what it did not before: an approval artifact left in state/approvals/ that has expired (delete it by hand), and installed (<file> calls Flow's) for a hook manager's hook.
What is new
| Piece | What it does |
|---|---|
hooks/shared/command_rules.py, command_policy.py, hooks/handlers/pre_tool_use.py, policies/command-policy.yml (kernel) |
6.0.0-m7's raw matcher over one allowlist: a heredoc body a non-shell reads, and the quoted arguments of grep, printf, echo and sed unless piped into a shell, are not matched; a PowerShell tool's line is matched as written (T384) |
hooks/shared/path_policy.py, approval_artifact.py, write-bootstrap-approval.sh, approve-local.sh (kernel) |
Every protected-path approval digest-bound, capped by its action's TTL, single use (minted_on); --category mints for any declared category, the go-ahead form for any but ci_cd_config; the index read once per process, not once per path (O-31 (j)) |
hooks/local/lib/flow_owned.py, policies/protected-paths.yml (kernel) |
A flow: path is Flow's by HEAD's install record or Flow's install namespace; audit/flow-install.json is protected |
hooks/local/lib/commit_msg.py |
A merge is one whose MERGE_HEAD names commits HEAD lacks; what it adds beyond its parents takes the subject rules |
maintainer/flow.py, health.py, migrate_apply.py, migrate_preflight.py, the scripts under hooks/local/ |
git's output read as UTF-8, output printed as UTF-8; a symlinked AGENTS.md or CLAUDE.md refused, and it stops the migration; the install records .git/flow/range-base; health lists expired approvals and counts a hook manager's hook that runs Flow's |
hooks/local/validate_evidence.py, hooks/local/lib/evidence_rows.py (new), evidence_rules.py |
The staged evidence check refuses a working copy that differs; a finding in any spelling and after a blank line; a test-id row names a whole test title; an indented TAP line; the deploy-order warning |
hooks/local/lib/flow_docs.py, checks.py, hooks/local/lib/rounds.py, gate.py, templates/ledger.md, flow-skills/verify/SKILL.md |
Task ids never inside an item id; plan:, ship: and flow: commits are no task's; the item-edit flag's row; a carried 5.x verdict spends no round; the carry-over lists the last round, classed by what an entry declares; the phase PASS line prints two commands |
hooks/local/slice.py, policies/agent-tools.yml |
Reads the project's policies/agent-tools.local.yml: new variants and input kinds, read-only tools only |
hooks/local/lib/range_check.py, hooks/local/lib/range_record.py (new), hooks/git/pre-push (kernel), hooks/shared/staged_secret_scan.py (kernel) |
The push check starts at the recorded range-base; diff headers told by position, so a ++ line and an octopus merge's line are scanned; only the pushed remote's refs trusted; --record-commit needs --go-ahead and writes an audit line |
hooks/handlers/pre_tool_use.py (kernel) |
Windows spellings of a path (\\?\, \\?\UNC\, \\localhost\C$, \\.\, a trailing dot or space, ::$DATA) judged as the file Windows opens |
hooks/local/lib/eol_pins.py |
Only the run of lines a 5.x comment opens goes as 5.x pins |
hooks/shared/policy_loader.py, policies/command-policy.yml (kernel) |
Shipped policies read from HEAD; checking out or restoring policies/ denied; a malformed local file named; the shell-write deny anchored to the project's own folders |
hooks/local/lib/wiring.py (new), hooks/local/reader.py |
The wiring is what the settings run: disableAllHooks, an echo, a matcher without Bash, a BOM, backslashes, .claude/settings.local.json |
hooks/git/pre-commit, install-git-hooks.sh, check-module-size.sh (kernel) |
The printed steps name what a 6.0 tree ships |
README.md, workflows/lifecycle.md |
The shell the commit route needs (Git Bash, with the PowerShell form), python3 and the Debian PyYAML route, git add -u then the added files, no version |
maintainer/release_check.py (new, not installed), .github/workflows/core-release.yml |
After publishing, the release's tag must still name the commit core-verify verified, or the run fails (M6-11, the owner's approval relayed 2026-10-21) |
maintainer/tests/parallel.py (new, not installed), test_migrate.py, test_m6.py, synthetic.py |
The suite in worker processes; the installed test project built once per process |
hooks/handlers/pre_tool_use.py, post_tool_use.py (kernel; the fix pass) |
Any error on the way to a decision denies, never exit 1 (T371); the hook event's JSON read as UTF-8 bytes, the command hash never raises (T385); a call from outside any git work tree judged by the handler's own project (T380) |
hooks/local/lib/range_check.py, hooks/local/lib/commit_msg.py (the fix pass, review A: T378, T379, T381) |
A pre-push that names no remote (6.0.0-m7's) trusts every remote's refs, and its block names install-git-hooks.sh first (T378); a clean merge whose sides edited lines a few apart passes (T379), and so does one across a rename, a criss-cross history or a merge driver such as merge=union, as git's own merge-tree merges it (T381) |
Corrections to the published notes
The published notes stay as they are; these lines correct them.
- 6.0.0-m5, 6.0.0-m6 and 6.0.0-m7 (M6-26 (10)). "A merge runs neither, as before" (6.0.0-m6 line 34, 6.0.0-m7 line 47): a merge that stopped on a conflict still runs lint and typecheck, in the pre-commit, when you commit the resolution. The migration report ends with the undo block, after the health check ("ends with the health check": 6.0.0-m6 line 47, 6.0.0-m7 line 60). Git hooks are per clone: a teammate's clone keeps the hooks it had until that clone runs the install itself (or
bash hooks/local/install-git-hooks.sh) after pulling the migration. - 6.0.0-m6 (M6-31 (7)). The orphan removal takes a 5.x backup whose file goes or is already gone, not only those "beside files that go"; it leaves alone a folder that holds its own
.git(a worktree under.claude/worktrees/, a submodule), as 6.0.0-m6 shipped it (T309). - 6.0.0-m6 (M6-31 (6)). The gate's phase PASS line printed
(python hooks/local/slice.py P1 --brief skeptic, then verdict), which runs nothing as written; 6.0.0-m8 prints the two commands. - 6.0.0-m7, Limits. "The pre-commit's protected-path check still refuses the commit of such a change" did not hold for a script that rewrote the shipped
policies/command-policy.yml: that edit is never committed, andgit commit --no-verifythen skipped every hook (m7 review B #1). 6.0.0-m8 reads the shipped policies fromHEAD. Its line oncheck-module-size.sh --write-baselineprinting achore(fr25):subject is fixed here too. - 6.0.0-m4 to 6.0.0-m7. Their shallow clone of the tag prints
warning: refs/tags/<tag> ... is not a commit!(r1-54): an annotated tag in a shallow clone, harmless; the notes did not say so.
Line counts, every changed script against its ceiling
| File | 6.0.0-m7 | 6.0.0-m8 | Ceiling |
|---|---|---|---|
maintainer/flow.py |
701 | 730 | 800 (module size) |
maintainer/health.py |
108 | 139 | 800 (module size) |
maintainer/migrate_preflight.py |
333 | 344 | 800 (module size) |
maintainer/release_check.py (new, not installed) |
- | 89 | 800 (module size) |
hooks/local/gate.py |
98 | 72 | 120 |
hooks/local/validate_evidence.py |
213 | 144 | 250 |
hooks/local/lib/evidence_rows.py (new) |
- | 105 | 120 (stated here) |
hooks/local/lib/evidence_rules.py |
86 | 97 | 120 |
hooks/local/lib/rounds.py |
33 | 94 | 120 |
hooks/local/lib/flow_owned.py |
114 | 110 | 120 |
hooks/local/lib/flow_docs.py |
175 | 207 | 250 |
hooks/local/lib/eol_pins.py |
47 | 56 | 120 |
hooks/local/lib/range_check.py |
287 | 273 | 300 |
hooks/local/lib/range_record.py (new) |
- | 66 | 120 (stated here) |
hooks/local/reader.py |
178 | 168 | 200 |
hooks/local/lib/wiring.py (new) |
- | 56 | 120 (stated here) |
hooks/local/lib/commit_msg.py |
155 | 233 | 800 (module size) |
hooks/local/checks.py |
316 | 329 | 800 (module size) |
hooks/local/slice.py |
159 | 184 | 800 (module size) |
hooks/shared/command_rules.py (kernel) |
103 | 296 | 800 (module size) |
hooks/shared/command_policy.py (kernel) |
581 | 600 | 800 (module size) |
hooks/shared/path_policy.py (kernel) |
441 | 530 | 800 (module size) |
hooks/shared/approval_artifact.py (kernel) |
606 | 635 | 800 (module size) |
hooks/shared/policy_loader.py (kernel) |
209 | 240 | 800 (module size) |
hooks/shared/staged_secret_scan.py (kernel) |
184 | 190 | 800 (module size) |
hooks/handlers/pre_tool_use.py (kernel) |
227 | 306 | 800 (module size) |
hooks/handlers/post_tool_use.py (kernel) |
115 | 126 | 800 (module size) |
hooks/local/write-bootstrap-approval.sh (kernel) |
197 | 204 | 800 (module size) |
hooks/local/approve-local.sh (kernel) |
296 | 292 | 800 (module size) |
hooks/local/install-git-hooks.sh (kernel) |
112 | 116 | 800 (module size) |
hooks/local/check-module-size.sh (kernel) |
109 | 112 | 800 (module size) |
hooks/git/pre-commit (kernel) |
912 | 918 | not gated (no extension) |
hooks/git/pre-push (kernel) |
125 | 127 | not gated (no extension) |
No ceiling was raised; the files near theirs were split first, each a pure move in its own commit (flow_owned.py's batch read to flow_docs.py, validate_evidence.py's row checks to evidence_rows.py, gate.py's carry-over to rounds.py, range_check.py's record to range_record.py, reader.py's wiring check to wiring.py). The tests: the new test_m8.py 727 lines, test_m8b.py 279, test_m8c.py 250 and test_m8d.py 110 (the fix pass; the parser's tests left with it), test_m6.py 793 to 789, test_migrate.py 679 to 692, test_scripts.py 792, each under 800; the new parallel.py 123. The builder card and the planner boot: 5,986 of 6,000 bytes and 8,123 of 8,192 (maintainer/measure_boot.py; the planner's grew from 8,094 by the lifecycle page's shell and staging words).
Kernel changes and the 5.x line
Each kernel change landed on 5.x first and was copied here with maintainer/kernel.lock updated in the same commit. The lock holds 38 files, as 6.0.0-m7's did: the parser's modules joined it with T353 and T382 and left it with the cut (T384). The 5.x commits sit on the local branch 5x-kernel, each with 5.x's two manifests restamped: 7f1c3d23 (T332, audit/flow-install.json protected), aaaa1cf5 (T333, approvals), 0bbcf7b5 (T344, the index read once), 2b364963 (T345, the printed steps), 0395c5fc (T353, the parsed command gate, with the corpus as an expectation gate and the owner's lock recorded in docs/backlog/command-gate-shell-evasion/README.md, dated 2026-10-21), 4f8601f9 (T357, the staged scan's headers), 2d145606 (T358, the pre-push names the remote), b429ee99 (T360, Windows spellings), fcdcd093 (T362, policies read at HEAD), 3727d962 (T363, a malformed local file named) and fe71cab3 (T364, the anchored write deny); the fix pass: 81358e39 (T371, any error denies or judges raw), 9e615afd (T372), 62ca1003 (T373), 35a9346b (T374), 77ae0055 (T375), 66308f9c (T376), 7e7e3f86 (T377, with the decision record's fix-pass section), 3449cc51 (T380, a call from outside any git work tree) and f9af30b7 (T382, the parser's split); the cut: ecedcde8 (T384, the parser out, the allowlist in, the decision record's cut section with the owner's words) 4e424e8a (T385, every handler reads its JSON as UTF-8) and d7d3c06b (T387, the allowlist as defined: runners in any spelling, real heredoc openers only). None is public: no GitHub branch carries them, and pushing them to 5.x needs the owner's approval. The 5.x battery passed on fe71cab3, but for test-rule-inventory's two version rows, which fail on the series' base too: its attestation reads v5.11, which the inventory's vX.Y.Z normalizer does not take, and m8 changed neither file. On 4e424e8a, test-command-policy, its denial-message test, test-hook-manifest and run_hook_tests.py ran green, and on d7d3c06b, the head, test-command-policy and its denial-message test; the rest of the battery was not run on them.
Thresholds and their provenance
New in 6.0.0-m8; the others are as the 6.0.0-m7 note lists them.
| Constant | Value | Provenance |
|---|---|---|
A protected-path approval's life (path_policy.py, EDIT_MAX_MINUTES) |
60 min at most, lower when the policy's artifact_ttl_minutes is |
protected_path_edit's shipped TTL; the commits mint 30 |
A command approval's life (command_policy.py) |
its action's artifact_ttl_minutes in the active workflow mode |
policies/approval-policy.yml (a migration 120 min, production_deploy 90 days in direct_to_main) |
The push check's start (range_check.py) |
.git/flow/range-base, the HEAD the install found |
m7 review A #1; without the record, 6.0.0-m7's rule |
| The command gate's matcher residue | 40 should_gate: true corpus rows allowed, 12 should_gate: false rows refused, listed in the corpus (matcher_residue) |
measured on the cut's gate; the corpus test fails when either list changes |
Suite workers (parallel.py, FLOW_TEST_WORKERS) |
the CPU count, at most 4 | a standard GitHub-hosted runner has 2 to 4 vCPUs; more workers than CPUs only add contention |
Decided and deferred
Decided:
- 7.10, the owner's cut (2026-10-22, after the self-check): 6.0.0-m7's matcher plus the narrower allowlist; the parser leaves the gate path and the lock and is not scheduled (6.0.0 ships without it; a parser pass is reconsidered only after two weeks of field refusal counts, with a written grammar spec, the frozen corpus and a stopping rule fixed first). Costs accepted: O3 (a commit message quoting a gated command) stays refused, so 7.10 is not closed in m8; the everyday commands the allowlist leaves refused are reworded, by the standing rule in the projects. Gate round 2 is dropped with the parser.
- M6-11, the owner's approval (relayed 2026-10-21): the post-publish re-check in
core-release.ymlas its own protected CI-file commit; the tag ruleset is the coordinating session's. - M6-7: the install namespace, not the release's own hashes, judges a path
HEAD's record lacks; the protected manifest makes any forged row the operator's go-ahead. - M6-8: a conflict resolution is the merge's own change; its subject names the task.
- r3-83: refuse a staged evidence file whose working copy differs, rather than read the index.
- r1-28: the policy's
finding_markerstays as projects may have tuned it; the case-insensitive word is added beside it. - 7.9: a FAIL verdict's ledger note stays listed in the carry-over, both rounds; only the readers' returns are cut to the last round.
- 7.11: a
.localtool profile may add read-only tools (a profile is the slicer's input check, not a guard; the owner named the file). - A#1: the push check starts at the
HEADthe first 6.0.0-m8 install found; a commit thatHEADholds is not judged again. - A#6: 5.x always wrote its pins under its comment line; pins without one stay, as a project's lines.
- B#1: the pre-commit keeps reading the policies its own way (it judges the staged change to them, a protected path).
- Item 10: the suite runs in parallel by class inside
test_migrate.py;core-verify.ymlis unchanged. - The fix pass (reviews A and B held the tag): each cause is its own commit; a pre-push that names no remote keeps 6.0.0-m7's trust of every remote; T371's fail-closed hook judges a call from outside any git work tree by its own project's rules (T380), so the upgrade's lines from the folder above still run; a clean merge is the one git's own
merge-treewrites (T381), withmerge-filefor a git older than 2.38; an octopus merge keeps T334's rule. - Four statements every note carries, so the suite's note tests read them here: the Store aliases and the 3.10+ check, the
gate_roundsdefault for a policy edited before 6.0.0-m3, the dry run's "only once those two also pass by hand", and the undo block word for word (test_commits.StoreNote, test_core.SmallItems, test_migrate.Preflight, test_migrate_recovery.Undo, which failed at T370 when this note lacked them). - The protected-path stage's O(n^2) git calls (item 10's named cause) landed as T344, ahead of item 10's place in the plan, once its 5.x battery passed: every 5.x battery and core fixture took hours without it.
- The done checks' copy of Onboarding uses its
node_modules, copied once from the fixture's clone into the pass's own folder and junctioned only there (the coordinating session's answer of 2026-10-21).
Deferred:
- A forged install-record row for a path inside Flow's namespace (
hooks/local/x.py) still passesflow_owned.py; the mint the protected manifest now requires is its gate. - The migrate tests' own scratch builds stay per test: cheap, and the migration each runs is the thing under test.
- The m5 review's other M6-26 rows stay as the backlog lists them; its item (8), the scope of T280's exemption, is the owner's question.
- Re-check B's holds b (nested substitutions grow the parser's work) and c (forms past T373 to T376), and gate round 2: dropped with the parser.
- The three reviews of 6.0.0-m8 (A, B, C) and the two re-checks: the items that hold no tag go to the next pre-release (the PowerShell denial's wording, Husky v9 shims, the T354 tool deny-list, T362's
--stagedand-bforms,--consumeper category, stash and single use, the T339 marker and title edges, an agent's Write from a Claude Code worktree into the main checkout'sstate/approvals/, and the others the pass report lists).
Limits
- A commit message, or any text outside the allowlist, that quotes a gated command is refused (
git commit -m "note: fusebase deploy needs the approval first"): reword it, or commit withgit commit -F <file>. Field item 7.10 is not closed in 6.0.0-m8. The corpus (maintainer/tests/command-gate-corpus.json, 188 rows) lists the rows the gate leaves the other way undermatcher_residue: 40 commands it should gate and does not, 12 it should not gate and does. - Text the gate does not see run: text in an allowlisted context that another program runs (sed's
ecommand in a quoted sed script,sed -n '1e rm -rf /tmp/x' file); a heredoc read by a program not on the runner list (make -f - <<EOF, or a script written bycat > s.sh <<EOFand then run); a quote-split word (fusebase de'pl'oy); and a command built at run time (the corpus'saccepted_misses, D2-no). Until a field refusal or incident names one, these stay a documented limit. - On Git for Windows' default
core.symlinks=false, aCLAUDE.mdcommitted as a link toAGENTS.mdchecks out as a plain file holding the textAGENTS.md: the install merges the Flow block into it and the migration's preflight sees a regular file (review B NEXT 1; refusing mode 120000 is next). - A PowerShell line is matched on its raw text, as in 6.0.0-m7: prose in it that quotes a gated command is denied.
- The approval record is a file in
.git/flow/: the tool hooks deny an agent's write into it, but a person, or a process the hooks do not see, can write a row, as with the audit log.--record-commitnow needs--go-aheadand leaves an audit line. .git/flow/range-baseis written by the install: another clone, made before or after the upgrade, has none until the install runs there, and checks pushes as 6.0.0-m7 did (a commit is judged when it or a parent carrieshooks/git/pre-merge-commit, and every remote's tracking refs count as already pushed).- Health counts a hook manager's script that names
hooks/git/<name>(or.git/hooks/<name>) on a line that is no comment, echo or printf as calling Flow's; it does not run the script. - Codex: unproven.
--wire-hookswrites.codex/hooks.json, and Codex asks you to trust each project hook before it runs one; Codex reports edits asapply_patch, which the handlers do not parse, so the tool-time path and secret checks are unenforced there; the git hooks cover both. maintainer/flow.pyandhealth.py, run in a terminal, start their processes withoutCREATE_NO_WINDOW; only the files underhooks/and the git hooks carry it.- Health reads the reader's state in English
schtasksoutput; another language prints that the answer could not be read. - The two CLI checkpoints commit with lint and typecheck red;
FLOW_RULES.mdrule 6 does not name that exception yet. - An edited
policies/evidence-classes.ymlthat an upgrade keeps gets no new keys (O-26). - The Windows (Git Bash) leg's time on the hosted runner is measured on the tag, not here.
hooks/local/run-handler.shstill treats a handler's exit 1 as non-blocking; the handler no longer exits 1 (it prints a decision on every path), and whether the wrapper should block on a handler error is the owner's question.
This is a pre-release. CI gates it on Linux; the Windows (Git Bash) run reports but gates nothing until 6.0.0.