Skip to content

chore(release): 26.11.3 - #6

Merged
ancongui merged 299 commits into
mainfrom
release/26.11.3
Nov 7, 2026
Merged

ancongui merged 299 commits into
mainfrom
release/26.11.3

Conversation

@ancongui

@ancongui ancongui commented Nov 7, 2026

Copy link
Copy Markdown
Collaborator

What and why

Release 26.11.3 of devkit. It installs devkit with one command, runs its hooks from a pinned copy, adds a live
dashboard, puts the crew under one enforced RACI, holds irreversible actions until the developer grants them, protects
devkit's own state from agents, carries a session's work through compaction, backs up branches that exist only on this
Mac when that starts no CI run, embargoes unpublished fixes, and keeps the audit trail's handoff copy under 45 MB.

Changes

The full list is in CHANGELOG.md under [26.11.3] - 2026-11-06. The last review rounds changed:

  • Embargo: a push from a git folder, a worktree's git folder, env -C, sudo -D, a temporary clone, a sourced
    GIT_DIR or a folder that is no project is held while an embargo exists. A copy of embargoed commits in another
    project's folder is not held yet; that is a known limit, fixed in 26.11.4.
  • Consent: an irreversible action (push, PR, merge, release, deploy) opens only when the developer's answer picks
    the option that performs it; a "no" in any form, another option or free text opens nothing; the question names the
    option that runs the action and its target.
  • Install: dev install no longer stops at the skills step on a fresh clone or a release archive.
  • Tests: the real-state check leaves out only the run's own output file under dev ci --run; scope tests pass in
    worktrees; the sync log handle is closed.
  • Docs: agents use the developer's own gh account and keys (no separate agent identity, by decision); audit
    partitioning is planned for 26.11.5.

How to verify

  • dev ci --run on the pushed tree: 7 of 7 steps passed (compile, 1,821 unit tests on Python 3.15, gitleaks,
    semgrep, osv-scanner, trivy, zizmor); evidence recorded.
  • /usr/bin/python3 -m unittest discover -s tests -t . (Python 3.9.6): 1,823 tests OK, 31 skipped.
  • Every change was reviewed by the crew's Code reviewer; the security changes also by the Security engineer, with
    three red-team rounds on the guard.
  • After the release: curl -fsSL … | DEVKIT_REF=v26.11.3 bash on a clean home, then dev doctor.

Risk and rollback

  • Risk: the guard holds more pushes than before (embargoes, unknown folders); a missing handoff repository makes it
    refuse agents' pushes from folders that are no project, with a message to run dev doctor.
  • Rollback: dev install --version 26.11.2 switches back to the kept pinned copy.

Links

  • CHANGELOG: [26.11.3] - 2026-11-06, including "Known limits".

SubagentStart carries neither the launch's tool_use_id nor a meta file, so the start took the oldest queued launch of its type. Live on 2026-11-06 that put Tank in Kid's lane, the Architect in Sati's, the Keymaker in the Oracle's, and a resumed Tank in Trinity's lane with her step; launches also took their member's first step instead of the one they named (B-fxo6hnp1nbt, B-fxnghfz0rjj).

Launch records are now keyed by tool_use_id (spec C2) with their caller. A lane binds exactly when the launch's PostToolUse names its agentId, or at the agent's first hook through the meta file's toolUseId, before the guard decides. A failed launch (PostToolUseFailure, now registered for Agent and Task) leaves its record. An unbound lane is provisional: no step, no scope. A launch binds once. A bound lane takes the ready step its description names, never just the member's first one. A resumed agent keeps its own lane and takes nothing else. The parallel limit counts launches and bound lanes, so helpers take no slot, and messages to a member reach their working lane.
Kid's crew gates (204c776) hold a member to its own procedure: proofs run, skills loaded, no stubs, tests that can fail, lessons after refusals. Nothing called them yet. SubagentStop now runs them after the report canary for any member that was launched, sends the member back once with every missing item, and keeps the gates' mark on the fresh session record. The canary's own test runs with the gates recording only.
dev status drew a saved status block with the crew folded into Done and long values running past the terminal edge (the developer's screenshot). Crew now lists one member per line, You has its own row, and values wrap under the value column.
The developer asked for tutorials on every platform that non-developers can follow. Each tutorial (Claude Code in Terminal and the IDEs, Claude Desktop's Code tab, Codex CLI, Codex in the ChatGPT app) takes the reader from opening a project to a first task with the crew, and shows what they see: the status block with its Crew and You lines, progress updates, approvals, the crew graph and the dashboard. A short page explains what Claude Desktop chat and Cowork are for and why code changes go to the Code tab. docs/surfaces.md compares what devkit does on each surface and says plainly that Codex is not verified live until its quota resets on 2026-11-09.
The dashboard became a local live server with a new view order, Background work, the crew tabs, one drawer for every detail, live states and honest states, and new screenshots; the page still described a static file with quick-look modals. It now covers dev dashboard with --no-open, --status, --stop, --export and --path, the idle stop, what each view and drawer shows, the live indicator, the routes and keys, and the server's security, with alt text that matches the regenerated screenshots.
dev install now pins a read-only, checksummed copy of devkit for the hooks and the guard, and the guide still described hooks that run the checkout. It now says what the pinned copy is, what it pins (a release, or a commit whose bin/dev ci --run passed), --unverified --why, --version for rollback, that dev update pins the new release, the desktop install step, and the files to remove on uninstall. The first install names the Stacks you work in question (Python and Node preselected) and points to the tutorials.
K1 follow-ups (D1 = B). An agent could swap ~/.local/bin/dev or put another dev earlier on PATH, or run devkit with HOME, DEVKIT_CONFIG, DEV_ROOT or DEVKIT_SESSION pointing at a state tree it wrote itself, and so forge approvals. The guard now protects every dev a shell finds (the link itself, not its target), refuses those variables for dev, python -m devkit, programs importing devkit and nested claude or codex sessions (the documented one-command hatches stay), refuses launchctl setenv of them and DEVKIT_*/DEV_ROOT in shell startup files, and leaves dev install, dev update and install.sh to the developer. devkit itself reads a non-standard DEVKIT_CONFIG or DEV_ROOT only from a terminal with no agent above it. core.ensure_roots makes the three roots 0700 at every start; rm -r and dev rm name the checkout, the running copy and the pin root; the policy diff compares the gitignore with the pinned copy.
An agent that writes a DEVKIT_* variable into ~/.claude/settings.json through a shell passes the guard's file-tool rule, and the ConfigChange hook checked only devkit-managed keys, so the variable reached the next session (spec C11, K1). ConfigChange now compares the env block's DEVKIT_* variables with what devkit manages, refuses any set, change or removal with exit 2, audits it, and puts them back in the file.
With the pinned install, dev install --only re-applies steps from the installed copy instead of reinstalling the plugin from the checkout, a branch install needs bin/dev ci --run first, and agents may no longer run dev install or dev update. Troubleshooting now explains each pin refusal with its fix, the doctor checks of the pinned copy, and rollback with --version; Developing devkit explains how to try a change in your own sessions, updates release step 3 (test, then pin), and lists the modules added since 26.11.0; Getting started says that the developer runs dev update and dev install.
Completion offered 35 of the CLI's commands and none of the crew, progress or task verbs. It now lists every user command, the verbs of dev task, progress, crew and agents with their main options (reopen, graph, rules, raci, set, history, assign and done --why, agents rules --section), the dashboard server's options, and dev install's steps (desktop included), --version over the pinned copies, and --unverified --why. Project ids are fetched only for the commands that take one. Checked with zsh against 23 completion positions using stubbed helpers.
The Keymaker's K1 work (a76e6e2, f54206a) made the guard refuse agent writes to devkit's state, configuration and install, a forged HOME, DEVKIT_CONFIG, DEV_ROOT or dev link, and dev install, dev update or install.sh from an agent; devkit also ignores those variables unless the developer sets them in a terminal. Guard rules gets a section with each decision, and Configuration says who may point devkit elsewhere.
New readers, including people who are not developers, need to find the tutorial for the app they use. The docs index gets a Tutorials section and the surfaces page; the README's install section points to the step-by-step install and the tutorials, mentions the pinned copy, and its documentation table and dashboard alt text match the current pages and screenshots.
The reference lacked the options added in 26.11.3: dev install --version and --unverified --why with the pinned copy, the dashboard server's --no-open, --status, --stop and --export, the task verb reopen, the crew subcommands graph, raci, rules, set and history, dev agents rules, and --why on progress assign and progress done. Each is now documented from its --help and code, and the shell integration lists what completion covers.
Developers write in any language, so a step's kind of work now rests on anchors that do not depend on words: an explicit activity id (--activity on assign, [id] in a plan spec), then the assigned member's Responsible activity, then the files in its scope. The English signals are only the last guess (spec C13); the English refusal stays until progress stores declared activities. Adds the seven kinds of work nobody covered (i18n, dependency updates, accessibility, single sign-on, design systems, codebase questions, backups and recovery), and marks enforced the rules that now have code: a forged HOME, config or dev link (f54206a), resumed subagents in the parallel limit (a4732a8) and the five crew gates (a47f93e). The grouped help lists graph, rules and raci. The generated agents follow with the crew sync.
Kid's a83ff47 adds 7 kinds of work to the RACI (i18n, dependencies, accessibility, sso, design-system, codebase, backup); each agent's RACI section is regenerated with dev crew sync.
Seraph's K1 review found that an interpreter started in ~/.local/state/devkit or ~/.config/devkit could write there with a relative path (cd …/runs && python3 -c "open('x.json','w')"), while a redirect, sed -i or an editor in the same folder was already refused. The interpreter rule now counts a protected working folder as a hit, and a folder it cannot resolve (cd $D) is refused when the command names devkit's state.
Subagents can launch agents of their own (verified at F0), and such a helper had no parent, scope or tool level, and the parallel limit skipped its launch. A launch record now carries its parent and depth, and its lane keeps them. lane_tools and lane_scope compute a lane's limits from its parent chain, so a helper of a scoped or review-only member is held to the member's files and tools, and it takes no plan step. The parallel limit applies at every depth, no launch goes deeper than progress.max_depth (2), and the limit drops to 1 while the recorded 5-hour usage is above progress.quota_percent (90). Spec F3, gaps G3 and G17.
The developer ruled out word lists in any language (spec C13): devkit must never decide what a developer meant by matching their words. Lessons now carry anchors (files, commands, kinds of work, error signatures, stack), set with new flags on lesson add and with a lesson anchor command for older ones; a step recalls them by its kind of work and files, a tool call by its command and files, tool output by its error signature. Skill rules lose their 15 prompt regexes and trigger on files, commands and declared kinds of work. A turn counts as development work from what it did: repository tools, development commands, changed files, or the status block the model wrote. A catalog of kinds of work, skills and lesson anchors is ready for SessionStart, and briefs declare their kind of work. Background jobs may ask a small model headless (opt-in, a daily budget, asked once by the setup wizard) to merge lessons written in different languages; without it they compare anchors. The correction regex on prompts is gone. Briefs also get the stack skills of their files again (the file rules were silently skipped).
Developers write steps in any language, so the kind of work cannot rest on English words. assign takes --activity and stores it on the step as explicit, and the RACI check uses it, then the member's Responsible activity, then the files in scope (raci.anchor, raci.check_assignment). A plan spec declares it with [id], stripped before the after split so it never lands in the dependencies; unknown ids are refused. dispatch.route matches hyphenated keywords only as whole phrases (app-store no longer gives Switch the word app), and planning work routes to Morpheus first. Kid's M41 and M45.
Seraph's review of the crew gates (M67, item 3): a proof run as python3 bin/dev test was not recognized, because the program word was python3, and the stub and test gates scanned every changed line of a file, so another member's uncommitted lines in a shared file were blamed on this one. An interpreter's script is now the program, and the two gates keep only what the member wrote itself: a file it wrote whole, or lines and tests found in its own edits.
Tank's F3 (26e257d) holds nested agents to their parent's tool level, scope and depth, and lowers the parallel limit to one while the 5-hour usage is above the quota. Every member's instructions still said it could not launch agents, and the crew's ready view showed progress.max_parallel as the slot total even when the limit was one. The body now says when a helper is allowed and what it gets, and the view shows dispatch.ready's limit.
Kid's eaafde3 adds the narrow-helper sentence to every member's body and shows the real slot limit; dev crew sync regenerates plugin/agents.
C12 needs the real shapes before the continuity module is built. Fixtures are payloads Claude Code 2.1.319 sent to a capture-only hook in a throwaway session (manual /compact, a blocked /compact, --resume, /clear) and the hook schemas embedded in codex 0.166.0, labeled unverified live until 2026-11-09. The tests pin the fields and ordering devkit will read: shared prompt_id across PreCompact, SessionStart(compact) and PostCompact, the summarizer's nameless SubagentStop, resume context_tokens equal to postTokens, the new session id after /clear, and Codex compaction hooks without summary or context.
Crew members were refused legitimate work: read-only crew commands (graph, show, raci) even run alone, commit messages and heredocs that only mention lane commands, and the bulk-rewrite note fired on quoted text (L-fxom7edle07, L-fxpeihqfwzb, B-fxn17sez8cj). guard.shell_calls now lists what a command runs: its command words, nested shells, the substitutions the shell expands and the program an interpreter runs when it can start a process. crew_shell_verdict, bulk_rewrite and member_write_exempt build on it for the hooks to call (Tank wires them in hooks.py). The shared walker also gets three parsing fixes that closed ways past the protected-state rule: shell keywords (then, do, {) no longer hide a program, python3 - reads its program from the heredoc, and a quoted or commented <<EOF no longer hides the next line. From Seraph's K1 review and Morpheus: a repository's own script is its code like -m, a path inside a progress, crew or lesson message is text, config set audit.* is refused, and the refusal of a program that edits a file naming devkit state now points to the Edit or Write tool.
Members shared the session scratchpad's top level, and a cleanup by one member deleted another member's files twice (L-fxs9q5evz83). Each member now writes and deletes only in scratchpad/<member id>/: scratch_refusal checks a path, crew_shell_verdict walks the command's write and delete targets (rm, mv, redirects, find -delete, nested shells) for the member, and member_write_exempt grants the file tools only the member's own folder there. A temp path inside a git checkout is never exempt, so a worktree or a temp repository keeps its scope, and the exemption reads core.is_temp as the hooks' scope check does.
Seraph's review of F1 and F2 (M69, M78). A crew agent whose launch record was dropped (state busy at the Agent call) was treated as a helper: no lane, so no tool level, scope or slot limits. Its meta file still names the launch, so it now gets a restricted lane (binding unbound): read-only, no step, no scope, no slot, audited as crew.unbound, until Morpheus assigns it a step, which confirms it. The progress step command checked for a run with an unlocked read and started a new run over a half-written one; start now reads the run inside the lock, so a record that does not parse is never written over. The ConfigChange hook now covers the project and local settings (spec C11): a DEVKIT_* variable in their env block is refused and removed, and nothing else there is judged as devkit-managed. New tests also kill two mutants that survived: a pending launch that holds its slot past 15 minutes, and one temporary file name for every writer.
The Keymaker's wiring patch (M59, M95), applied as he wrote it. The lane check in hooks.py read raw text, so a heredoc or a quoted commit message that only mentioned a lane command was refused, and read-only crew commands such as raci were refused too (spec K8). It now asks guard.crew_shell_verdict, which judges what the shell runs. The bulk-rewrite note uses guard.bulk_rewrite the same way, and a member's file writes keep to its own scratch folder and to the plans its step's scope names (guard.scratch_refusal, guard.member_write_exempt). Tests by The Keymaker in tests/test_guard_crew.py; each fails on the parent commit.
Seraph's re-review of 3cb4150 found that a program started in a parent folder of devkit's state (cd ~/.local/state, cd ~/.local) still wrote there with a relative path such as devkit/runs/x.json. A working folder that holds devkit's state or configuration now counts as inside it, except HOME, which holds everything.
Lesson L-fxs9q5evz83 told members to keep scratch work in scratchpad/<member>-<task>/ (tank-f3, tank-f4), so the scratch-folder rule from 479970c refused members in their own task folders. A folder named after the member, or the member followed by a hyphen, is the member's; keymakerx/ or tank-f3/ for another member stays refused.
The PostToolUse.Agent.background fixture still held the capturing machine's temp root (/private/tmp/claude-501/), with its user id. The other fixtures redact machine paths (home becomes /Users/dev), so this one now uses /tmp/claude-dev/. No test reads outputFile; test_hook_capture passes, and the three test_lane_binding SlotTests failures also fail on a clean export of HEAD.
B2b of the backup security review, the tech lead's decision: an embargo was promised to keep a branch off the remote, but it only held dev backup and dev sync; an agent could still git push it, dev push it or open a pull request from it.

A git push, dev push, dev git push, dev pr create or gh pr create that sends an embargoed branch, or commits its embargo holds (another name, a commit id, a branch made from it, a tag, --all), adds the developer-only action embargo/<branch>: refused in every setting without the developer's typed grant embargo <branch>, and never run by a crew member. A note devkit cannot read refuses every push, and a push the guard cannot parse is refused in a project with an embargo. The developer still pushes in a terminal.

Verified: new red-team tests in tests/test_red_team_2.py fail on the previous code and pass; the 35 test modules that use the guard or the branch map pass (981 tests); the full suite ran once (1792 tests, one failure fixed since)
The code reviewer's follow-up to B2a: a handoff note saved with a byte-order mark read as no embargo and no backup policy, so its embargoed branches could be backed up and the guard saw no key to keep.

branches.note_text strips a BOM and turns CRLF into LF wherever devkit reads or rewrites a note (the embargo list, the backup policy, the record edits), and the guard compares a note's keys through the same reader.

Verified: new red-team test NotesInAnotherEncoding fails on the previous code (BOM) and passes; the 35 test modules that use the guard, the branch map or the backup pass on Python 3.15 and 3.9 (983 tests)
The code reviewer's follow-up: a workflow that runs on every push (on: push, no branches filter) and has a job with an environment read as not deploying from a feature branch, so dev backup --accept-ci pushed it and could start a deploy.

backup._push_reaches reads the push trigger as GitHub does: no filter runs for every branch; only a branches filter that leaves the branch out, a branches-ignore filter that names it, or a tag filter alone rules it out. A paths filter still counts, to stay on the safe side.

Verified: new red-team test DeployingWorkflowsWithoutABranchFilter fails on the previous code (the backup pushed) and passes; the 35 test modules that use the guard, the branch map or the backup pass on Python 3.15 and 3.9 (983 tests)
B2a and B2b changed what the guard refuses: the developer was told an embargo keeps a branch off the remote, and the guard and delivery pages must say exactly what that covers.

guard.md lists the handoff note rules (the embargo list and backup policy, shell writes, git in the handoff repository, crew/ and audit/) and adds the embargoed push to the developer's own decisions with its grant line. delivery.md says agents never push an embargoed branch without grant embargo <branch>, that an unreadable note refuses every agent push, that the developer still pushes in a terminal, and that an embargo gone from the note without its lift keeps holding.

Verified: dev docs check docs/guard.md docs/delivery.md: 0 structure issues, all links resolve, 0 Vale findings
dev setup commit-template took an agent's --yes or a piped answer as consent, so an agent could set commit.template in every repository of the workspace.

setup_commit_template now asks core.set_by_developer(), as dev setup prompt does: in an agent's call it writes nothing, says to run it in a terminal and exits 2. --remove still needs no consent.

Verified: python3 -m unittest tests.test_commit_template tests.test_wizard: 74 tests OK on Python 3.15.2 and 3.9.6; dev test --affected: 1753 passed; dev test prove --since HEAD: the new test fails without the change (0 != 2)
The security engineer's finding during B2a, High: an open() whose path is built by two nested calls hid its write mode from the state rule, so a program could write devkit's state, the grant ledger included.

The interpreter gate that guards devkit's state now also takes _ANY_WRITE (a write mode closing an open() call, mode=, a write method). Crew lanes inherit the same gate.

Verified: new red-team test test_a_state_path_built_by_nested_calls_is_still_a_write_to_state fails on the previous guard and passes; the 35 test modules that use the guard, the branch map or the backup pass on Python 3.15 and 3.9 (984 tests)
When devkit could not read the crew roster, a member's lane kept the tool level it was given, while docs/guard.md says the lane check fails closed. A broken or unreadable crew.json silently widened a lane.

The lane now works at the stricter of its level and review, and the guard records why as guard / crew-readonly. The old allowlist test that asserted the fail-open is renamed and inverted; a red-team test covers the same path.

Verified: python3 and /usr/bin/python3 -m unittest tests.test_shell_allowlist tests.test_red_team_2 tests.test_guard_refusals: 77 tests OK on both; both new tests fail with the guard change reverted.
A push or PR whose folder the guard cannot resolve (git -C with a command substitution, cd to a variable, then git push, dev push, dev pr create or gh pr create) passed no folder to the embargo check, which read it as no embargo: an agent could push an embargoed branch.

The embargo check now judges such a push by the session's own folder: an embargo there, a handoff note devkit cannot read, or a folder that is no project refuses it as the developer-only action with target ?, which no grant covers; the refusal tells the agent to write the folder out. The session folder travels in a context variable set by irreversible_actions. CHANGELOG: an agent's push or PR of an embargoed branch needs grant embargo <branch>.

Verified: new red-team test fails 9 ways on the old guard and passes now; test_red_team_2 and test_guard_refusals pass on python3 3.15 and /usr/bin/python3 3.9 (57 tests); dev test --affected 1754 passed; dev sec scan clean
A push from a folder known only at run time was judged by the session's own project, so cd into another project with an embargo got through; --git-dir, --work-tree, GIT_DIR and GIT_WORK_TREE were never read, so a push could send from another repository unseen.

An unknown folder now refuses the push or PR as the developer-only action ? while any project's handoff note or audit trail holds an embargo, or the embargo lists cannot be read. git's -C, --git-dir and --work-tree, and GIT_DIR and GIT_WORK_TREE from the command or an export, name the folder: a literal path is judged as that repository, a run-time value as an unknown folder. The CHANGELOG line says so.

Verified: dev test prove: both new red-team tests fail on the old guard and pass now; test_red_team_2 and test_guard_refusals pass on python3 3.15 and /usr/bin/python3 3.9 (59 tests); dev test --affected 1756 passed; dev sec scan clean
trust_hooks trusted every untrusted or modified hook in ~/.codex/hooks.json at each install, session-start repair and 6-hour job. Agents can write that file, so a hook an agent added there was trusted and then ran outside the guard in every Codex session (security review G3, High).

A hook is devkit's only when Codex lists it declared exactly as config/codex/hooks.json renders it: event, matcher, command, timeout, async, status message and context limit, the fields Codex hashes for trust (codex 0.166 HookMetadata). trust_hooks now takes devkit's hooks object and trusts only those; any other hook in the file stays untrusted, so Codex does not run it until the developer reviews it with /hooks. dev install names the ones it left untrusted, and dev doctor gets a row 'hooks devkit did not write' that lists each with its source and fails while one waits for review; 'devkit hooks trusted' now checks devkit's own hooks only, so a foreign hook no longer sends the developer to dev install. The two fake app-servers in test_pinned_install and test_agent_settings now report the hook fields real Codex returns.

Verified: tests/test_codexrpc.py (3 new): dev test prove, fail without the change (the planted hook was trusted), pass 5 of 5 with it; dev test run --affected --since HEAD: 82 files, 1759 tests passed; touched tests on /usr/bin/python3 3.9.6: 35 passed; dev test audit and dev stubs clean.
The two last 26.11.3 fixes (b3f45a6 and the senior engineer's audit publish stopgap) change what users see: untrusted foreign hooks listed in dev doctor, and a held handoff copy with a warning.

Verified: dev docs check CHANGELOG.md: no errors
The audit job copies the whole local month file into the handoff repository. At about 5 MB per heavy day it passes GitHub's 100 MB file limit in about 20 days, and then GitHub refuses every handoff push. Stopgap for 26.11.3 until 26.11.4 publishes daily gzipped segments (spec P-fyjswfxin8k).

publish() leaves a month over 45 MB unpublished (its handoff copy stays as it was) and records audit.publish, blocked, with warning severity and the size. Other months, prune and the handoff commit are unchanged. dev audit stats (text and --json 'held') and a new dev doctor row name the held month and what to do; docs/audit.md states the rule.

Verified: 4 new tests in tests/test_audit.py (PublishLimitTests) pass 5/5 and fail without the change (dev test prove --since HEAD); 84 affected test files, 1767 tests pass on Python 3.15; test_audit, test_jobs_audit, test_doctor, test_hermetic and test_zz_real_state_untouched pass on Python 3.9; dev docs check docs/audit.md clean.
The Code reviewer found that publish() read a held month file (over 45 MiB) into memory on every daily run just to measure it. It now checks the file size first and skips the read.

Verified: python3 and /usr/bin/python3 -m unittest tests.test_audit: 69 tests OK on each
A push from a folder git can use but devkit maps to no project (inside <repo>/.git, a worktree's git folder, a temp clone, a folder made in the same command, one a sourced file picks, env -C or sudo -D to another folder) skipped the embargo check, so an agent could push an embargoed branch (code review of 605626d and cc259b7).

A push from inside a git folder is judged as its repository's main checkout (git's --git-common-dir), so its embargo holds and grant embargo <branch> in that project opens it. A known folder that is no project gets the same hold as an unknown one (developer-only ?, no grant), the handoff repository's own pushes excepted; an unparsed push there is held in the audit week too. env -C/--chdir and sudo -D/--chdir change the folder of the command they run, and a short option cluster that ends in a value flag (env -iC dir, sudo -iu root) consumes its value. After source or ., the folder and GIT_DIR are unknown. The red-team test now uses the real current_project (only is_temp patched) instead of a stand-in that hid the bypass, and RedTeam2 runs with an empty handoff of its own.

Verified: dev test prove: both new probe tests fail on HEAD and pass 5 of 5 runs; 34 guard test modules pass on python3 3.15 and /usr/bin/python3 3.9 (905 tests each); dev sec scan clean
The guard doc did not say that a push from a folder known only at run time, or from a folder that is no project, is refused while any embargo exists, that no grant opens it, or that git-folder and env -C pushes are judged as their project.

Adds a row to the developer-only table and a paragraph on git folders, env -C, sudo -D and the handoff repository.

Verified: dev docs check docs/guard.md: 0 issues, all links resolve
Since ee90f8c a push from a temp repo (no project) is held while no handoff repository exists, and the hermetic home has one only when another module made it first, so test_gate_tiers failed 34 subtests when run alone.

GateCase.setUp points core.HANDOFF and core.NOTES at an empty temp handoff, as RedTeam2 does: no embargo anywhere.

Verified: python3 3.15 and /usr/bin/python3 3.9: test_gate_tiers alone OK; 34 guard modules with test_gate_tiers first OK on 3.15 (905 tests)
A Mac without a handoff repository refuses agents' pushes from folders that are no project (the tech lead's decision: fail closed, only a broken setup has none), and the refusal did not say how to get out of it.

The reason now says dev setup creates the handoff repository and to run dev doctor. The release and handoff push test also runs with no embargo anywhere, from the project folder with a handoff repository present.

Verified: dev test prove: the new assertion fails on HEAD and passes 5 of 5; 34 guard modules with test_gate_tiers first OK on python3 3.15 and /usr/bin/python3 3.9 (905 tests each); dev sec scan clean
Since ee90f8c a push from a temp repo (no project) is held while no handoff repository exists, and the hermetic home has one only when another module made it first, so test_hooks_wiring failed when run alone.

WiringCase.setUp points core.HANDOFF and core.NOTES at an empty temp handoff, as GateCase and RedTeam2 do.

Verified: test_hooks_wiring alone OK on python3 3.15 and /usr/bin/python3 3.9
Git keeps no empty folder, so a fresh clone or a release archive has no agents/skills and the skills step crashed on iterdir(), stopping dev install for every new install. local_skills() returns no local skills when the folder is missing.

Verified: python3 and /usr/bin/python3 -m unittest tests.test_local_skills: 2 tests OK on each
26.11.3 is released today. Audit partitioning moved to 26.11.5, and the developer dropped credential separation: agents use the developer's gh account and keys, so the docs no longer promise agent credentials.

Verified: bin/dev docs check: 0 issues, all links resolve, 0 prose errors
A verified answer to an irreversible decision opened its action whatever the developer chose, so approve D1: no let a release through (the Senior engineer's probe).

A decision now opens its action only when the developer's own choice, kept from their evidence, is the option that performs it: --performs, else the one option that reads yes or approve, else the recommended one. Without options only approve or yes opens it. dev crew ask refuses an irreversible decision no option clearly performs. use_grant, auto-mode pre-approvals and step grants share the rule, and a refused decision is audited as crew.grant-refused with its id and option, never free text.

Verified: python3 3.15.2 and /usr/bin/python3 3.9.6 -m unittest of 13 affected modules: 493 tests OK on each; bin/dev test prove --since HEAD tests/test_evidence.py: 7 of 7 fail without the change, pass 5 of 5 runs; bin/dev test audit --changed: 0 findings; bin/dev sec scan: clean
An explicit --performs was never checked: with --options "A: release | B: no" --performs B, the developer's "approve D1: no" opened the release. The developer also never saw which choice runs the action; only the tech lead's hint said it.

crew_decision refuses a --performs whose option reads as not doing it, with the same guidance as the default path. ask_text puts the performing option, the action and its target on the question's one line, for example "D1 · Release now? (choosing “A: release” runs release v26.11.4)"; without options, "(answering “yes” runs …)". The refusal audit stays on the gate's check: the gate peeks and uses only a grant it found, so a refused action is never used, and writing only on use would audit nothing. A test through the real gate pins one crew.grant-refused per refused release.

Verified: python3 (3.15.2) and /usr/bin/python3 (3.9.6) -m unittest of test_evidence, test_progress, test_crew, test_audit, test_gate_tiers and 5 more, alone and together: 401 tests OK on both; dev test prove: the 2 consent tests fail without the change
The docs said an embargo keeps a branch off every remote, but a push is judged by the embargo list of the project it runs from, so a copy of the commits in another project's folder is not held yet (fixed in 26.11.4). Daily audit segments moved to 26.11.5, and dev doctor still named 26.11.4.

guard.md and the CHANGELOG say where the embargo holds and add the gap under 26.11.3 Known limits. audit.py's HELD_FIX text (shown by dev doctor), its comment, docs/audit.md and a test comment now name 26.11.5.

Verified: dev docs check: markdownlint 0 issues, all links resolve; python3 and /usr/bin/python3 -m unittest tests.test_audit: 69 tests OK on both
Three pages still said an embargo keeps a branch off every remote; a copy of its commits in another project's folder is not held until 26.11.4, as docs/guard.md now says.

Verified: bin/dev docs check: 0 issues, all links resolve, 0 problems
The Code reviewer found that _refuses split on a space only, so 'B: No.', 'B: no, wait', 'B: (no)' and 'B: Wait.' were not read as refusals: an explicit --performs or a recommended default could make them run the release. It now takes the first word with a regex, and nope and nah count too. The commands page links straight to the guard section.

Verified: python3 and /usr/bin/python3 -m unittest tests.test_evidence: 43 OK each; dev test prove --since HEAD tests/test_evidence.py: the refusal test fails without the change; dev docs check: 0 issues
The Code reviewer found that 'no' or curly-quoted no kept the apostrophe in the first word, so a quoted refusal could still run the release. An apostrophe now counts only inside a word, curly apostrophes and U+02BC map to it, NFKC folds fullwidth letters, and pause, defer, delay, halt, block, refuse, veto, nay, none, negative, abstain and a bare n read as refusals.

Verified: python3 and /usr/bin/python3 -m unittest tests.test_evidence tests.test_progress tests.test_crew tests.test_approvals: 168 OK each; the reviewer's probe: every quoted refusal refuses; dev test prove: the test fails without the change
request_sync never closed its copy of sync.log after starting the background sync, which left a ResourceWarning in every run that requested a sync; the warning text named the test run's temporary home.

Verified: python3 -W error::ResourceWarning and /usr/bin/python3 -m unittest tests.test_work tests.test_hermetic tests.test_shell_allowlist: 56 OK each (QA engineer's fix)
dev ci --run streams the unit-test step into devkit's state, so the real-state check flagged the run's own log. The harness now leaves out only the files its stdout and stderr write to, by device and inode. The two ScopeBaseTests no longer assume the checkout sits at <dev root>/<owner>/<repo>, so they pass in worktrees.

Verified: python3 and /usr/bin/python3 -m unittest tests.test_hermetic tests.test_shell_allowlist tests.test_work: 56 OK each; the new leak test fails when the exclusion is removed (QA engineer's prove)
The 26.11.3 pull request's CI run was cancelled after 15 minutes: on the macOS runner, Google Chrome reached for the login keychain, which the suite's temporary HOME lacks, and each of the 29 browser tests hung until its 120 s timeout. They never ran locally, because no Chrome was found under that HOME.

Chrome now runs with --use-mock-keychain. find_chrome also looks for Playwright's browsers in the account's own home and prefers a full Chrome to the headless shell, as CI has. Below Chrome's 500 px window minimum, browse() loads the page in an iframe of the real width, so the phone tests measure a phone. After one Chrome timeout, later calls fail at once instead of each waiting 120 s.

Verified: CI=true python3 -m unittest tests.test_dashboard < /dev/null: 92 tests OK in 30 s (a browser test took 120 s before); /usr/bin/python3 -m unittest discover -s tests -t .: 1825 OK; dev ci --run: 7 of 7 steps passed

Refs: #6
@ancongui
ancongui merged commit 73086f9 into main Nov 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant