Skip to content

6.0.0-m7

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 21 Oct 11:08
· 1042 commits to main since this release

6.0.0-m7: the tool-time path check, the 5.x line-ending pins, merges and pushes checked, true health, local overrides that only tighten (pre-release)

The Fusebase Flow 6.0 core with the first five items of the owner's m7 list: the tool-time protected-path check judges the path git names, whatever spelling the tool sends (an 8.3 short name, .., ./, a symlink, NotebookEdit's notebook path); the 5.x folder-wide text eol=lf lines that damaged binaries committed under them are removed by the migration and by the install; a new pre-merge-commit hook and a pre-push check of every commit the remote lacks stop a merge, cherry-pick, rebase or revert from carrying a secret or an unapproved protected-path change; health reads the folder git really runs hooks from, parses the wiring and lists the Flow files an install kept; and a gitignored *.local.yml override only ever tightens, while a shell write into policies/ or state/approvals/ is denied. Both live projects upgrade from 6.0.0-m6 in one step (see "Upgrading a 6.0.0-m6 project to 6.0.0-m7").

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+, so a Windows host with python.org's Python and no python3 works too. PyYAML must be installed for that one, with its own pip: <the interpreter Flow will use> -m pip install -r fusebase-flow-6.0.0-m7/hooks/requirements.txt, not a bare pip, which can belong to another Python. Get this release; the second line installs PyYAML:

git clone --depth 1 --branch 6.0.0-m7 https://github.com/fusebase-dev/fusebase-flow.git fusebase-flow-6.0.0-m7
<the interpreter Flow will use> -m pip install -r fusebase-flow-6.0.0-m7/hooks/requirements.txt

That is usually python3 on Linux and macOS; on Windows with python.org's Python it is python (python -m pip install -r fusebase-flow-6.0.0-m7/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+.

Project on 6.0.0-m6, or an earlier 6.0.0 pre-release. Run the install from the project's main worktree (in a linked worktree, made by git worktree add, it refuses and names the main one). It adds the new files, replaces the Flow files you have not edited, keeps the ones you edited, rewrites its marked blocks in .gitattributes and .gitignore and audit/flow-install.json, removes Flow 5.x's line-ending pins from .gitattributes, reinstalls the git hooks, and touches nothing else:

python fusebase-flow-6.0.0-m7/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. Add --register-reader to schedule the heartbeat reader every 5 minutes (schtasks on Windows, crontab elsewhere): it writes your crontab only after crontab -l exits 0 or says there is no crontab yet, adds no second line for the same project, and refuses a schtasks command over 261 characters; when it refuses or fails it prints the line to add by hand, and the install still exits 0. --wire-hooks merges Flow's two tool handlers into .claude/settings.json and .codex/hooks.json beside the CLI's entries, which it never changes; a plain install writes neither file. An upgrade needs no --wire-hooks: the wiring you have stays as it is, and a reader task stays as it is. Commit the result as the protected commit workflows/lifecycle.md describes: stage it by name, mint with FF_BOOTSTRAP_TTL_MIN=30 bash hooks/local/write-bootstrap-approval.sh (the default 15 minutes can run out during a slow pre-commit), commit with a flow: subject, such as flow: upgrade Fusebase Flow 6.0.0-m6 to 6.0.0-m7, then run the same script with --consume. A flow: commit needs no task, so it also goes through in a project that already has a plan; it passes only when every staged path is a Flow file with the bytes audit/flow-install.json records, one of the four documents, or a change inside Flow's marked block in AGENTS.md, CLAUDE.md, .gitattributes or .gitignore (in .gitattributes, also the removal of Flow 5.x's pins). Then check it, read-only: python fusebase-flow-6.0.0-m7/maintainer/flow.py health --target <your-project> exits 0 when the install is healthy.

The tool-time path check. With Claude Code's tool hooks wired, an Edit, Write or NotebookEdit of a protected file is judged by the path git names: the handler resolves the edited path (from the session's folder when it is relative) and the project's folder before comparing them, so an 8.3 short name (C:\Users\RUNNER~1\...), another case, .., ./, a junction or a symlink inside the project reaches the protected check. Before, any of those spellings made the comparison fail and the edit was allowed (the commit-time check still refused it).

Line endings. Flow 5.x projects carry folder-wide text eol=lf lines in .gitattributes outside Flow's marked block (hooks/**, templates/**, state/**, workflows/**, agents/**, policies/**, .agents/skills/**, .codex/agents/**, .claude/commands/** and others). git treats a binary committed under them as text and rewrites its CRLF bytes: a PNG loses a byte and its signature breaks, in the repository and in every clone. The migration's dry run lists them and --apply removes them (the file is backed up first); the install removes them on an upgrade and names each line it removed. Your own lines stay: extension rules such as *.sql text eol=lf or *.png binary, a pin of your own folder, a 5.x copy of Flow's extension rules. 6.0's own block pins exact paths only. A binary committed before the upgrade under those folders keeps the bytes git stored then; commit it again after the upgrade to store it whole.

Merges and pushes. git runs no pre-commit for a merge it completes on its own, a cherry-pick, a rebase's replays or a revert, so a secret or a protected-path change committed with --no-verify on one branch could reach the remote by any of them. Now:

  • the new pre-merge-commit git hook checks what a merge brings in: the secret patterns, and each protected-path change, which must be on record; files whose merged bytes a remote-tracking branch already holds came from the remote and are not judged. A refused merge stays in progress: git merge --abort drops it.
  • the pre-push checks every commit the remote lacks the same way (hooks/local/lib/range_check.py).
  • "on record": each protected-path change the pre-commit (or pre-merge-commit) allowed is written by commit-msg to .git/flow/approved-changes.tsv, one file for all worktrees. A change passes when recorded steps lead from its old bytes to its new ones, so an amend or a squash of approved commits passes and a revert does not; a flow: commit of Flow's own files passes as it did at commit time.
  • not judged: commits a remote-tracking branch holds, and the history from before this check (commits whose tree, and whose parents' trees, have no hooks/git/pre-merge-commit), so the first push after the upgrade checks only what the upgrade and later commits carry.
  • a refused push names the commit and what it carries. A secret: rotate it and rewrite the branch without it. A protected-path change the operator approves (a cherry-pick of an approved change onto another base, a rebase that merged two changes to the same protected file): the agent runs python hooks/local/lib/range_check.py --record-commit <sha> on the operator's go-ahead in chat, then pushes again. An approved protected change on a branch the remote lacks merges through git merge --no-commit <branch>, the mint, git commit, then --consume.

Health. flow.py health reads the git hooks in the folder git runs them from (git rev-parse --git-path hooks): with core.hooksPath pointing elsewhere (Husky writes it) no Flow hook runs, and health says NOT healthy and names both folders; the install prints a WARNING then and still writes Flow's hooks only to the common git folder's hooks/, never into the hook manager's. Wiring is read by parsing .claude/settings.json and .codex/hooks.json: wired needs a PreToolUse entry whose command names hooks/handlers/pre_tool_use.py; a file that only mentions a handler reads unenforced. A Flow file the install kept because you edited it is recorded as kept and listed kept, not current; a kept file under hooks/ is NOT healthy, as a changed one is.

Local overrides. A gitignored policies/*.local.yml only ever tightens: it may add deny and approval rules, protected paths and secret patterns, lower a TTL or gate_rounds, and turn strict_approvals on; it never removes, replaces or weakens a shipped entry, and anything else it holds is ignored (a local allow, default, whitelist, workflow_mode, exempt_globs). What a project loosens belongs in a committed, protected file: its own over-ceiling files in policies/module-size-baseline.project.txt, a workflow mode or a TTL in the committed policy (an edit of a Flow file, which the install then keeps). A shell write into policies/, state/approvals/ or .git/flow/ (redirection, tee, cp/mv/install/ln/rsync into them, sed -i, a script that opens a file there for writing, PowerShell's Set-Content and its kin) is denied at tool time; reading them stays open, and Flow's own scripts (the two mints, check-module-size.sh --write-baseline) write there as before.

Editing a protected Flow file with the hooks wired. With Claude Code's tool hooks wired, an agent's Edit or Write of a Flow file (policies/validator-free-paths.yml, say) is denied unless an approval covers it, and the approval minted after staging cannot exist before the edit. Now one go-ahead covers both: when the operator says go in chat for the files named, the agent runs FF_BOOTSTRAP_TTL_MIN=30 bash hooks/local/write-bootstrap-approval.sh --paths <file>... before the edit, edits those files, commits exactly them under a task's T<n>, then runs the same script with --consume. The approval names the files and the commit HEAD is at, covers Flow's own files only (never a CI workflow), lasts 30 minutes at most, writes an audit line at each use, and allows no other file and no commit that stages any other path; the commit that lands on that HEAD spends it, and moving HEAD back (git reset --soft, a branch or a detached HEAD there) does not revive it. Minting after staging works as before.

One commit at a time. The planner and the builders share one work tree and one git index. Every commit, the planner's and the builders', goes through python hooks/local/commit.py -m "<subject>" <files>: it waits while another such commit, or a git process holding the index, has the repository, stages exactly the files named and commits only them, so a planner commit during a builder run neither fails on .git/index.lock nor takes a builder's staged files. A file you deleted or renamed with git rm or git mv is named like any other; names are literal (app[id].ts is that one file) and a folder is refused. An .git/index.lock over two minutes old while no git process runs is named for you to remove, rather than waited on.

Open branches and worktrees. Git keeps one set of hooks for a whole repository, so the 6.0 hooks run on every branch and in every worktree as soon as one branch installs or migrates. install and migrate list the other local branches and linked worktrees that do not carry Flow 6.0 yet. On such a branch an ordinary commit is refused with a message that names the migrated branch: merge it in first (git merge <the migrated branch>), then commit. The FuseBase CLI's two update checkpoints still commit there, without lint and typecheck. Do this for each branch and worktree in the list before its next commit; a release or hotfix branch gets the whole migration with that merge. git log --all --oneline --grep='^flow: migrate' finds the migration commit, and git branch -a --contains <that commit> the branches that hold it.

Start a fresh agent session. After migrating or upgrading, close the agent sessions that were open and start new ones: a session keeps the rules it loaded when it started, so an open one goes on following 5.x's (or the previous release's).

Commit subjects. commit-msg takes exactly one T<n> in the subject, a task of docs/flow/phases.md at HEAD once that file has tasks; plan: and ship: only on the files the plan and ship skills name; flow: only on Flow's own files, for Flow's install, migration and upgrade commits; a merge by git's state. The FuseBase CLI's two update checkpoints, chore(update): pre app update (<date>) and chore(update): post app update (<date>), pass whatever they stage, so fusebase update can commit them: exempt_subjects in policies/evidence-classes.yml names them, and only the committed file counts. If you edited that file, the re-run keeps your copy without the key; copy the key from the release's file. A commit outside the trivial tier (policies/evidence-classes.yml, empty by default) needs its task's backlog item.

Lint and typecheck. Git runs the pre-commit before the commit message exists, so Flow 6.0 runs your project's lint and typecheck in commit-msg, after the subject check, whenever the commit-msg git runs is Flow's (with a hook manager such as Husky in core.hooksPath, they stay in the pre-commit): a commit with a task still runs them and is refused when they fail. The two CLI checkpoints skip them and print a warning saying they were not run; run them yourself after fusebase update. A merge runs neither, as before. Flow's own records under state/ (gate reports, flag rulings, run indexes, deploy lines) skip them with no declaration, except files whose extension a lint covers (.json, .mjs and the other code extensions); a glob you declare in policies/validator-free-paths.yml that matches such files prints a warning on each commit.

Evidence rows. A row whose earlier run of the same phase recorded a FAIL (FLAKY) passes only when its note cites an owner answer A<n> that docs/flow/backlog.md has under ## Answers, or states reproduced <k>/<n> and cites each of those n runs by id; "repro" or "not reproduced" alone no longer does. The pre-commit's evidence check shows a row you recorded as FAIL, or BLOCKED without an accepted answer, as written ("holds as written"), never as PASS, and still lets the commit through. A live run starts after the deploy it checks has finished, except a pre-deploy baseline run, named so in its run-log title (# Live run baseline-<run> <id> (<env>)), which may start before the deploy it serves. gate.py counts only ledger records (lines that start with their time), never a ## Next or ## Handoff bullet that says "review"; a criterion that names its own phase's gate is still refused before dispatch, but a slice's gate (P6-S1), another phase's and "before the P6 gate" are not.

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 above, 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-m7/maintainer/flow.py migrate --target <your-project> --providers claude,codex
python fusebase-flow-6.0.0-m7/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-m7, 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, ends with the health check to run after that commit and with 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.

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-m6 project to 6.0.0-m7

Both live projects take this, from 6.0.0-m6. Run these in Git Bash (any terminal elsewhere), in the folder that holds your project's folder; <your-project> is that folder's name. Start from a clean tree: commit or stash 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-m6 install: one with no tool hooks wired, one with Claude Code alone wired, and one with Claude Code and Codex wired. On the install line, give the providers your install already has: audit/flow-install.json lists them under providers (both live projects have claude,codex). Leave --wire-hooks off: the wiring in .claude/settings.json and .codex/hooks.json stays as it is, since Flow's entries did not change from 6.0.0-m6, and --wire-hooks with claude,codex would add a .codex/hooks.json a Claude-only wiring never had.

git clone --depth 1 --branch 6.0.0-m7 https://github.com/fusebase-dev/fusebase-flow.git fusebase-flow-6.0.0-m7
<the interpreter Flow will use> -m pip install -r fusebase-flow-6.0.0-m7/hooks/requirements.txt
python fusebase-flow-6.0.0-m7/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/git/pre-merge-commit hooks/local/lib/eol_pins.py hooks/local/lib/range_check.py
FF_BOOTSTRAP_TTL_MIN=30 bash hooks/local/write-bootstrap-approval.sh
git commit -m "flow: upgrade Fusebase Flow 6.0.0-m6 to 6.0.0-m7"
bash hooks/local/write-bootstrap-approval.sh --consume
cd ..
python fusebase-flow-6.0.0-m7/maintainer/flow.py health --target <your-project>

What each step shows, as on the copy these steps were tested on (Onboarding migrated by 6.0.0-m4, upgraded to 6.0.0-m6 by its note, Claude Code alone wired, a reader task registered and Ready):

  • the install lists added: with the three new Flow files above and replaced: the Flow files that changed (ten on the copy: VERSION, hooks/git/commit-msg, hooks/git/pre-push, hooks/handlers/pre_tool_use.py, hooks/local/install-git-hooks.sh, hooks/local/lib/flow_owned.py, hooks/local/reader.py, hooks/shared/module_size.py, hooks/shared/policy_loader.py, policies/command-policy.yml). A project migrated from Flow 5.x that still carries 5.x's line-ending pins (Onboarding does) also shows merged: .gitattributes (block updated) and [flow] .gitattributes: removed 56 Flow 5.x line-ending pin lines, then each line; a project's own lines are not listed and stay. Under git hooks: it shows current pre-commit, installed commit-msg, installed pre-push and installed pre-merge-commit. Its line provider hook wiring: not written means it wrote no wiring this time; the wiring you have stays. Its line heartbeat reader: registered earlier: <the recorded line> (not scheduled when none is recorded) means it registered nothing this time: your task stays as it is, an enabled one enabled. With core.hooksPath set it prints the WARNING described above. When other local branches or worktrees have no Flow 6.0 files, a WARNING names each one that must merge this branch before its next commit;
  • git status --short lists M lines, the Flow files the install changed (with .gitattributes and audit/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 with git stash push -- <your files>, then go on (a plain git stash would take Flow's changes too). git add -u stages the changed files, the next line the new ones;
  • 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 the commit carries on record for the push check; it takes a few minutes on a large project, which the 30-minute mint covers;
  • health ends HEALTH: healthy and exits 0, now with git hook: pre-merge-commit installed beside the three; for an install with Claude Code alone wired it shows wiring: claude wired and wiring: codex unenforced, and its reader line still says what the scheduler says: the task is Ready.

In a wired Claude Code session, the steps inside the project (git status --short, the two git add lines, the mint, the commit, --consume and cd ..) go through the project's own hooks/handlers/pre_tool_use.py, which allows each of them; the agent mints only on the operator's go-ahead in chat. Outside the project folder that handler stops with an error (it cannot find the project's git root) without blocking the command, so run the clone, the install and health in a terminal.

After the upgrade. Health may now say what it did not before:

  • git hooks: core.hooksPath makes git run the hooks in <folder>, not the Flow hooks install writes to <.git/hooks>: ... and NOT healthy: a hook manager (Husky) owns the hooks; until it calls Flow's, none of them runs. The install prints the same as a WARNING.
  • wiring: claude unenforced or wiring: codex unenforced where it said wired: the settings file mentions a Flow handler but registers no PreToolUse command for hooks/handlers/pre_tool_use.py; re-run the install with --wire-hooks for that provider, or leave it unenforced.
  • kept, not current: <path> (your edit; ...): a Flow file you edited, which the install keeps and does not upgrade; under hooks/ it makes health NOT healthy. Compare it with the release's copy and take the release's, or keep your edit knowingly.
    And the first push after the upgrade runs the new range check over what the upgrade commit and later commits carry; your older history is not checked.

What is new

Piece What it does
hooks/handlers/pre_tool_use.py (kernel) Takes a relative edited path from the session's folder, resolves it and the project's folder, then compares them: an 8.3 short name, another case, .., ./, a junction or a symlink inside the project reaches the protected check; NotebookEdit's notebook_path is read, and its new_source goes through the secret check. A path that resolves outside the project is judged as given
hooks/local/lib/eol_pins.py (new), flow.py, migrate.py The Flow 5.x line-ending pin lines (Onboarding's tree at b176a73d, matched by their words): install removes them from .gitattributes and names them; migrate lists them in the dry run and removes them on --apply, the file backed up
flow_owned.py A flow: commit may carry that removal from .gitattributes; it judges a change between any two trees, so the range check asks it of a commit already made
hooks/git/pre-merge-commit (new), hooks/local/lib/range_check.py (new) A merge git completes on its own is checked for secret patterns and protected-path changes with no approval on record, files a remote-tracking branch already holds left out
hooks/git/pre-push (kernel) On a 6.0 tree, after the push boundary, every commit the remote lacks is checked the same way, merges by what differs from every parent; history from before the check is not judged
hooks/git/commit-msg After the subject check, puts the staged protected-path changes on record in .git/flow/approved-changes.tsv
hooks/local/install-git-hooks.sh (kernel) Installs pre-merge-commit beside the three hooks, where hooks/git/ has it
flow.py health Reads the hooks in the folder git runs them from (core.hooksPath followed), each hook the project's hooks/git/ carries; NOT healthy when git runs another folder's; lists kept Flow files kept, not current (NOT healthy for one under hooks/)
reader.py, wired() Parses the provider's settings: wired needs a PreToolUse command naming hooks/handlers/pre_tool_use.py, which exists; state/status.line reads the same
flow.py install Records a kept file in the manifest ("kept": true); warns when core.hooksPath points git away from Flow's hooks
hooks/shared/policy_loader.py (kernel) A *.local.yml only adds: deny and approval rules, protected paths and categories, secret patterns (a shipped id keeps its shipped entry), lower TTLs and gate_rounds, strict_approvals on; every other key is ignored, whatever local_override_may_relax says
hooks/shared/module_size.py (kernel) A local file adds source_globs only; exempt_globs there is ignored, with a note naming the project's committed baseline file
policies/command-policy.yml (kernel) Seven FR-07 deny rules: a shell write into policies/, state/approvals/ or .git/flow/
.gitattributes (kernel) Pins hooks/git/pre-merge-commit to LF, as the three older hooks are: a Windows clone (core.autocrlf=true) checked it out with CRLF, and the flow: upgrade commit was refused (found by this note's upgrade, rehearsed on a copy)
flow.py install (line endings) Writes and records every Flow file with LF, whatever the release checkout holds, so the recorded sha is the blob git commits

Line counts, every changed script against its ceiling

File 6.0.0-m6 6.0.0-m7 Ceiling
maintainer/flow.py 660 701 800 (module size)
maintainer/migrate.py 660 666 800 (module size)
maintainer/migrate_preflight.py 333 333 800 (module size)
maintainer/health.py 103 108 800 (module size)
hooks/local/lib/flow_owned.py 106 114 120
hooks/local/lib/eol_pins.py (new) - 47 120 (stated here)
hooks/local/lib/range_check.py (new) - 287 300 (stated here)
hooks/local/reader.py 175 178 200
hooks/handlers/pre_tool_use.py (kernel) 219 227 800 (module size)
hooks/shared/policy_loader.py (kernel) 176 209 800 (module size)
hooks/shared/module_size.py (kernel) 390 393 800 (module size)
hooks/git/pre-push (kernel) 115 125 not gated (no extension)
hooks/git/commit-msg 115 120 not gated (no extension)
hooks/git/pre-merge-commit (new) - 43 not gated (no extension)
hooks/local/install-git-hooks.sh (kernel) 112 112 800 (module size)
maintainer/migrate_apply.py 474 474 800 (module size)

Unchanged and under their ceilings: gate.py 98 of 120, validate_evidence.py 213 of 250, evidence_rules.py 86 of 120, rounds.py 33 of 120, flow_docs.py 175 of 250. No ceiling was raised. The tests: the new test_m7.py 528 lines, test_migrate.py 691 to 679 (its release-note test now reads this note's one-step section), each under 800 (test_m6.py stays 793). The builder card and the planner boot did not change: 5,986 of 6,000 bytes and 8,094 of 8,192 (maintainer/measure_boot.py).

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 (now 38 files). The 5.x commits sit on the local branch 5x-kernel, each with 5.x's two manifests restamped. New in 6.0.0-m7: 967b6dfc (the tool-time path check resolves both sides), 16b3ecd0 (the pre-push runs the range check on a 6.0 tree, inert on 5.x; the installer names pre-merge-commit and skips it where hooks/git/ lacks it), e97ba347 (local overrides only tighten; the shell-write deny; four 5.x tests follow: a workflow mode is switched in the committed policy, the policy-state test keeps a tightening override, the denial test has a sample for each new rule) and 2d3977ed (.gitattributes pins hooks/git/pre-merge-commit, a file 5.x does not ship, so the two lines' .gitattributes stay byte-identical). The earlier ones are listed in the 6.0.0-m6 note; apart from 4f6c12bc none is public: no GitHub branch carries them, and pushing them to 5.x needs the owner's approval. Core only, and the lock says why: commit-msg (decision 10(c)), now also putting approved changes on record, and the new hooks/git/pre-merge-commit, in the lock as a git hook git runs, core only because the range check and its record are 6.0's (5.x ships none, and its installer skips it).

Thresholds and their provenance

Constant Value Provenance
Silence alert (reader.py) 30 min consumer watchdog 2026-10-14; the transcript floor is 15 min (design section 14)
Repeated command (reader.py, checks.py) 3 identical command hashes in flight (the heartbeat's line 2, or the audit line) owner seed 2026-10-15; UNCALIBRATED: logged, not an alert, until measured (owner, 2026-10-18)
No-commit span (checks.py) 200 tool calls p99 of 434 between-commit spans, release 1
Orientation (checks.py) 135 tool calls before first work p99 of 156 working agents, release 1
Slice budget (checks.py) 2x the estimate in minutes or files, with no commit owner seed 2026-10-15
Duplicate block (checks.py) more than 20 lines owner seed 2026-10-15; the smallest real row on release 1 was 27
Capture floor (evidence-classes.yml) images 10,240 bytes, other files 32 a blank 1280x720 PNG is 4,313 bytes, a blank 1920x1080 one 8,559 (zlib level 6)
Gate rounds (approval-policy.yml) 2, for the verdicts and for the reviews, each adversarial verdict C2-07; the verify skill: review rounds share the limit with their own counter
schtasks /TR (flow.py) 261 characters the length schtasks /Create refuses beyond
A go-ahead's life (path_policy.py, GO_AHEAD_MAX_MINUTES) 30 min at most the 30-minute mint the protected commits already use (a slow pre-commit)
The commit lock (commit.py) touched every 10 s, taken over after 120 s untouched, waited for up to 1,800 s a holder that died stops touching it; Onboarding's code commits took 72 to 86 s
Lint-covered extensions (validator_tier.py) .js .mjs .cjs .jsx .ts .mts .cts .tsx .json .jsonc .json5 .py .html .htm .css .scss .vue .svelte ESLint lints .json and .mjs under docs/ and state/ in both live projects (owner, m6 list)
Exempt subjects (evidence-classes.yml, exempt_subjects) chore(update): pre app update (<date>), chore(update): post app update (<date>), matched whole the FuseBase CLI's lib/commands/steps/update-git-checkpoint.ts, found by the 6.0.0-m4 review
Checkpoint forms in commit-msg the same two, matched in the hook a branch older than the migration has no 6.0 policy at HEAD; the lint skip reads them too
Branches named in the refusal the first 20 flow: migrate commits of git log --all bounds the lookup on a large history
An aged index.lock (commit.py) over 120 s old while no git process runs: named for removal, not waited on the commit lock's 120 s; a live git refreshes or removes its lock long before
Flow 5.x pin lines (eol_pins.py) 18 patterns, the 34 5.x skill mirrors under .claude/skills/, 4 comment lines Onboarding's .gitattributes at b176a73d (no Flow 5.x script wrote them)
The range check's start (range_check.py) a commit is judged when it or a parent carries hooks/git/pre-merge-commit the first tree with the record; older history has none to be checked against
The range check's cost 6.7 s for a push of 52 commits, the push boundary included measured on this Windows host (test_m7.RangeCheck); one git log --raw and one git log -p for the whole range

Limits

  • The review of this release, for 6.0.0-m8: a branch older than the upgrade is not range-checked; an octopus merge, and lines a combined diff marks other than ++, skip the secret scan; remote-tracking branches of every remote are trusted, not only the push's; --record-commit is the operator's word with no audit line of its own; six more Windows spellings of a path pass the tool-time check; a project line written exactly like a 5.x pin is removed with them.
  • Not in this release, 6.0.0-m8's: the rest of the adversarial review's causes in the owner's order (M6-7 the manifest-row forgery, M6-6 an expiry cap and single use for every approval category, M6-8 the MERGE_HEAD check, M6-9 unicode paths, M6-14 symlinked overlays, r3-83 the validator reading from the index, r1-28 the FINDING marker match, M6-11 the tag ruleset and post-publish re-check), the documentation items, and the eleven field items from the two projects' first day under 6.0.0-m6.
  • The approval record is a file in .git/flow/: a shell write into it is denied at tool time, but a person, or a process the hooks do not see, can write a row, as with the audit log. A flow: commit is accepted by flow_owned.py, so the manifest-row forgery (M6-7, 6.0.0-m8) reaches the push check too.
  • Each clone keeps its own record. A protected change made in another clone reaches yours through the remote, which the check does not judge; one fetched straight from a path or a URL with no remote-tracking branch is judged, and its push needs --record-commit.
  • A rebase that replays an approved protected-path change onto a base where that file changed too, or a merge whose both sides changed the same protected file, makes a change nobody approved as such: the push or the merge is refused until the operator records it (--record-commit, or git merge --no-commit, the mint and git commit).
  • pre-merge-commit sees what a merge brings into the index, not the commits it came from (git writes MERGE_HEAD after the hook runs); the pre-push judges those commits one by one.
  • Health does not tell a hook manager that calls Flow's hooks (.husky/pre-commit running them) from one that does not: with core.hooksPath set it is NOT healthy either way.
  • The tool-time deny of a shell write into policies/ matches the command's text: a write by a script file, a variable or an alias is not seen, and a heredoc or a commit message that only mentions > policies/... is denied. The pre-commit's protected-path check still refuses the commit of such a change.
  • Codex: unproven. --wire-hooks writes .codex/hooks.json, and Codex asks you to trust each project hook before it runs one; no slice has run on Codex yet. Codex reports edits as apply_patch, which the handlers do not parse, so the tool-time path and secret checks are unenforced there; the git hooks cover both. docs/provider-coverage.md lists each element.
  • A go-ahead is spent by any commit that lands after it, a builder's too, and stays spent: the agent then asks again.
  • maintainer/flow.py and health.py, run in a terminal, start their processes without CREATE_NO_WINDOW; only the files under hooks/ and the git hooks carry it.
  • Health reads the reader's state in English schtasks output; another language prints that the answer could not be read.
  • hooks/local/check-module-size.sh --write-baseline still prints chore(fr25): adopt module-size baseline, which commit-msg rejects: commit the adoption under a task's T<n>.
  • The two CLI checkpoints commit with lint and typecheck red; FLOW_RULES.md rule 6 does not name that exception yet.
  • An edited policies/evidence-classes.yml that an upgrade keeps gets no new keys (O-26); it is read at HEAD only and has no local twin, so this release leaves it as it was.

This is a pre-release. CI gates it on Linux; the Windows (Git Bash) run reports but gates nothing until 6.0.0.