Skip to content

feat: release PAACM 3.5.0 with finishing through the runtime, worktrees and shared machines - #9

Merged
Hyper-AI-Lab merged 32 commits into
mainfrom
feat/kit-3.5
Oct 28, 2026
Merged

Hyper-AI-Lab merged 32 commits into
mainfrom
feat/kit-3.5

Conversation

@Hyper-AI-Lab

@Hyper-AI-Lab Hyper-AI-Lab commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

What this release does

Running PAACM 3.4.1 on a team project — several people on one server, linked worktrees, headless agent runs — showed where the runtime enforced less than the method asks. This release closes those gaps. Every fix has tests that failed on 3.4.1 and pass now. The design is recorded in ADR 0021, and ALGORITHM.md follows it.

Area Before (3.4.1) Now (3.5.0)
Finishing a task Agents stopped with the task still triaged, without REPORT.md or task done. The stop gate let any task stop once the gate was green. task done needs the route's last phase, approved tests, a gate at the route's tier and REPORT.md with OUTCOME: done. The stop gate nudges once: never on a continuation, never twice for the same stop. task start, task phase and a passing gate name the reference to read next.
Worktrees A sibling worktree's edits were not checked. Every edit in .claude/worktrees/x/ needed approval as agent configuration. The CLI could climb into the outer checkout. One resolver that never leaves a worktree. Hooks follow cwd into worktrees of the same repository. The protect hook judges each file by its own worktree's configuration and task. In a session folder that is not a git repository, each file is judged by its own checkout wherever cwd is, and the stop gate also judges the checkouts directly inside that the session edited. A nested .agent/paacm.json is not a second set of rules.
Shared machines Baselines in a shared /tmp/paacm-sessions: the second account's session start failed with EACCES and lost all context. Baselines in the worktree's git directory (0700/0600 on Linux and macOS, pruned after 30 days). A write failure is a warning.
Frozen tests approve-tests could take a new baseline over a test changed through the shell. Refused until reopen-tests (human-confirmed). While a blocked task has such a change, no task starts over it in any worktree whose commit has it, and other worktrees cannot approve tests over it. Approving moves the task from testing to implementing.
Review input Untracked files had no content. A huge diff became empty. Generated files flooded the input. ``` inside the diff broke the fence. One line of evidence. Diff from a temporary index, so new files are included. Generated and large diffs go to review-diffs/. The fence is always long enough. A too-large diff is an explicit error. Evidence lists gate.log, configuration warnings and files saved for the reviewer.
Gate output Agents piped the output, which allow rules refuse. Exit 127 looked like failing code. --quiet plus gate.log. A missing tool is named as an environment problem (trigger 2).
Audit A merge's changes were listed as the task's own. Every configuration change is a WARN the report accounts for; one that arrived with commits a remote-tracking branch has is marked through a merge. Violations stay strict.
Shell guard and defaults --no-verify, core.hooksPath, HUSKY=0, git update-ref, docker build --push and terraform -chdir=… apply were allowed. All of them ask, quoted or not. .github/actions/** and git-hook manager files are protected by default.
Work items — task start --ref, shown in the status, the session context, the review input and the report.
doctor — Warns about check programs that are not installed, and about a review extension without ## Reviewer checklist.
Instructions The core block covered only code, tests and configuration. context.md recommended a compound command. Every file change goes through PAACM. A new always-on rule says to follow the phases to the end and run one runtime command per call. The tests-first phase now runs the fixers and fixes exception lists before approval.

Risk

  • Behaviour changes users will notice:
    • task done refuses tasks that skipped phases, tests or the report.
    • The stop gate adds at most one extra turn when a task is left open.
    • Re-approving tests after a frozen-file change needs reopen-tests.
    • A blocked task with a changed frozen file refuses task start in every worktree whose commit has the change, and approve-tests in the other worktrees.
    • Edited paacm.json files must add the new protected defaults by hand (see Upgrading in the CHANGELOG).
  • Hooks: the protect hook spawns git only to identify the repository of a file outside the project's own folder or in another worktree. Edits inside the project take the fast path, with only file-system calls.
  • Stop gate output: nudges are sent as additionalContext, which Claude Code documents for Stop hooks that give guidance. A Claude Code version that ignored it would simply let the agent stop, which is the 3.4.1 behaviour for these cases. Failing gates still use decision: "block".
  • Known gaps, documented here and not closed by this release:
    • In one working tree, the blocked-task refusal checks the files when the task starts. Restoring a frozen file from an older commit, starting a task, then restoring the edit, gets around it. The shell guard does not ask for git checkout <commit> -- <file>. A second clone is outside what the runtime can see.
    • Below a non-git session folder, the stop gate judges only checkouts directly inside the folder, and only those the session edited through edit tools. A checkout changed only through the shell is protected by the audit and its own gate, not by this session's stop. Judging every checkout at every stop was rejected (ADR 0021): one agent's stop would run, and be held by, other agents' gates.
  • Known limitation, not new: when Cursor runs its own hooks and the imported Claude Code hooks in parallel, both may run the gate or nudge once each. Sequential duplicates are handled.
  • Not run live: Cursor (there is no Cursor CLI here), macOS and Windows. These are covered by the CI matrix and the Cursor-shaped hook tests. The Windows-only part of doctor's program lookup is reasoned, not run.

Evidence

  • Checks: npm test 160/160; validate; build --check (61 files up to date); markdownlint 0 issues; claude plugin validate plugins/paacm --strict and plugin validate . both passed; node bin/paacm.mjs check --tier quick exit 0.
  • New and changed tests: each fix starts with a test that failed on 3.4.1:
    • new: finishing, worktrees, sessions, review, gate and audit;
    • extended: hardening, tools, runtime and content.
    • The test helper now really passes CLAUDE_PROJECT_DIR to hook processes; before, it dropped it.
  • CI fixture script: run locally, unchanged: exit 0.
  • Upgrade from 3.4.1:
    • an edited paacm.json, an extension and the team's AGENTS.md text are kept;
    • an untouched paacm.json receives the new defaults;
    • the lock reads 3.5.0, and doctor reports all installed files intact.
  • Two Linux accounts, one shared temporary folder (run as two unprivileged uids):
    • on 3.4.1, the second account's session start exits 1 with EACCES and emits no context;
    • on 3.5.0, both accounts get their context, and each baseline folder is 0700 and owned by its account.
  • Headless Claude Code 2.1.292 on a fixture project with a real bug ($1.46 in total):
    • a FAST code-and-docs fix ran the whole loop: task, tests approved, gate --quiet, phase 7, REPORT, task done;
    • a STANDARD task told to stop once the gate passed was nudged once, as additionalContext with no hook error shown, then let through;
    • every compound command an agent tried was refused by the IDE, and the agent re-ran it on its own, as the new rule says.
  • Independent fresh-context review of the branch: it reported 12 findings (11 reproduced) and 4 plausible ones. Every confirmed finding and three of the plausible ones are fixed, each with a test; the fourth is the known limitation above. They are listed in docs/project/requirements.md under Release 3.5.0.
  • The maintainer's independent audit of df0d83e: 7 confirmed findings and 1 plausible one. Each confirmed one is fixed with a test that failed first, and the plausible one (file modes on Windows) is documented. The claims it found stronger than the code are corrected in the CHANGELOG, ADR 0021, ALGORITHM.md, the guides and the requirements. The audit's reproduction script, run against this head, reports none of its findings as confirmed. The one stop-gate case it builds (a sibling checkout the session never edited) is now judged by design only once the session edits there.

After merge: tag v3.5.0 and publish the release (on the maintainer's go-ahead).

… sessions

- lib/project.mjs: one project-root resolver for the CLI and the hooks. It never
  climbs out of a worktree, passes through submodules, and compares repositories
  by their common git directory.
- Hooks anchor the project on the IDE's project directory and follow `cwd` into
  worktrees of the same repository (Claude Code keeps CLAUDE_PROJECT_DIR at the
  original root in a worktree session).
- protect and post-edit use the edited file's own worktree, configuration and
  task: sibling worktrees are protected, nested ones are no longer treated as the
  main checkout's .claude/ folder, and a session started in a subdirectory still
  protects approved tests.
- Session start prints the worktree's own runtime command.
- Test helper: CLAUDE_PROJECT_DIR is now actually passed to hook processes.
Baselines lived in a shared os.tmpdir()/paacm-sessions. On a machine several
accounts use, the second account's session start failed with EACCES and the
agent lost the whole session context; other accounts could also read or forge
the baselines. They now live in <git dir>/paacm-sessions (per worktree, 0700/0600,
pruned after 30 days). A baseline that cannot be written is a warning in the
session context, never a failed hook.
…lobal options

The shell guard allowed git commit --no-verify, -n, git -c core.hooksPath=,
git config core.hooksPath and HUSKY=0/SKIP=/LEFTHOOK=0, which skip the project's
pre-commit and pre-push checks. A new ask rule, skip-git-hooks, covers them.
publish-deploy now also matches terraform -chdir=… apply, kubectl --context …
apply, helm and docker with global options, and docker image push.
…y default

.github/actions/** runs in CI like the workflows, and .husky/**, .pre-commit-config.yaml
and the lefthook files decide what runs on commit. They join protect.config, so an
agent cannot weaken them without the human's approval.
…n files

approve-tests could be run again after an approved test (or the check manifest,
or .agent/paacm.json) was changed through the shell and committed, which moved
the baseline past the change without the human. It now refuses while the audit
reports such a change and names reopen-tests, which the human confirms. It also
moves a task from testing to implementing, so fixes made after reopen-tests are
gated again. Protect, audit and approve-tests share one case-aware matcher.
… read next

Agents stopped with the task still triaged, without REPORT.md and without
`paacm task done`, because the stop gate let an open task stop whenever the gate
was green and nothing pointed to the later phases.

- `task done` also requires the last phase (review for STANDARD and DEEP,
  verify for FAST), approved tests, a passing gate at the route's tier, and a
  REPORT.md whose OUTCOME is done; each refusal names the reference to open.
- The stop gate nudges once (never on a continuation, and not twice for the same
  stop) when the gate is green but the task is open, and when a STANDARD or DEEP
  task changed files while still triaged. A gate below the route's tier no
  longer counts.
- `task start`, `task phase` and a passing `gate` print the reference to read
  next, found next to the runtime in project and plugin installs.
…, and missing tools named

Agents piped or redirected the gate's output to keep it short, and IDE allow
rules refuse such compound commands; a check whose tool was missing (exit 127)
was reported like a code failure and counted toward the retry cap.

- `paacm gate --quiet` and `paacm check --quiet` print only the RUN/PASS/FAIL
  lines, the summary, the last 30 lines of a failing check and the exit line.
- With a task, every gate run saves its whole output to
  .agent/tasks/<task>/gate.log (capped at 8 MiB) and names it.
- A check that exits 127 (9009 on Windows) gets a hint: this is the environment,
  not the code; the stop gate says to ask for the tool or block with trigger 2.
…to the evidence

- The diff is built in a temporary index, so new untracked files are reviewed
  with their content; the real index is never touched.
- Diffs of files marked linguist-generated, of files whose diff exceeds 64 KiB,
  and then of the largest files until the rest fits 512 KiB, go to
  review-diffs/ and are listed with their stats; a diff above git's buffer is an
  explicit error, never an empty diff.
- The fence is longer than any backtick run in the diff.
- Evidence lists the gate log, the protected-configuration warnings and the
  files the writer saved in the task folder for reviewers (who have no shell).
- A "## " line inside a fenced code block no longer ends the reviewer checklist.
- paacm-review writes its input into a new private temporary folder.
…he task's own changes

After a task merged its base branch, the audit listed every protected file the
merge changed as "changed in this task", so a report could claim approvals for
changes the task never made. Each warning now has an origin: "task" (own
first-parent commits, a merge's conflict resolutions, uncommitted edits) or
"merge" (arrived only from the merged branch), printed as WARN or NOTE in the
audit, the gate and the review input. Violations stay strict: a merge cannot
bring in a change to an approved test or to the gate configuration.
`--ref` takes an issue number (123, #123), a tracker key (PROJ-123), a GitHub
reference (owner/repo#123) or an https URL, at most 200 printable ASCII
characters. It is kept in state.json and shown by task status, the session
context and the review input; the report template has a WORK ITEM line.
…missing reviewer checklist

- For each check in .agent/checks.conf, doctor reads the programs the command
  starts (the first word of each command after assignments and wrappers,
  following a plain cd for script paths) and warns about one that is not on
  PATH or a script that does not exist: the gate would exit 127 there.
- When .agent/extensions/08-review.md has no "## Reviewer checklist" section,
  doctor says the reviewer gets no project checklist; otherwise it names the
  other sections, which stay with the writer.
… missing tools and --ref

- The core block and the skill description cover any change to the repository's
  files, documentation included; a new always-on rule: record each phase, read
  the reference it names, finish with REPORT.md and `paacm task done` or
  BLOCKED:, and run each runtime command on its own.
- context.md no longer recommends a compound command; the gate's --quiet and
  gate.log replace piping, and other redirects are noted as needing approval.
- 05-tests-first: run fixers on the tests and update the lists the change makes
  wrong before they freeze; re-approval needs reopen-tests.
- 07-verify, 08-review, 09-deliver, 01-intake-triage, runtime.md, paacm-verify
  and the REPORT template describe the new behaviour.
…hangelog

- ADR 0021 records why tasks finish through the runtime and why state and
  protection follow the worktree; ALGORITHM.md section 4, phases 5, 7, 8 and 9
  and the IDE mapping follow it.
- Setup guide: running the checks in CI, worktrees and per-person state on a
  shared machine. Troubleshooting: task done refusals, the stop gate's nudges,
  exit 127, the baseline warning, worktrees; the reopen-tests path for an
  approved test change. Extending and user guides updated.
- Requirements: release 3.5.0 with its evidence. Version 3.5.0.
…-tests names the next step

A headless Claude Code run showed "Stop hook error occurred" for the finish
nudge, because it was sent as decision "block". Claude Code documents
hookSpecificOutput.additionalContext for a Stop hook that gives guidance: it
continues the turn under the same stop_hook_active protection and is shown as
stop hook feedback. The nudges (no task, still triaged, finish, an unconfigured
tier on the first stop) now use it; a failing gate still blocks.

approve-tests now prints that the task moved to implementing and the reference
to read, so the agent does not record the phase again.
Review finding: after running the gate, the stop hook's nudge wrote back the task
state it had read before the gate, so the new lastGate and the reset failure
count were lost and task done then refused a passing gate. The nudge now reads
the state again before recording itself.
…outs below a non-git session folder are protected

Review findings:
- The hooks followed the agent's cwd into any sub-folder with its own
  .agent/paacm.json, and into a worktree without a task, so the stop gate stopped
  checking the session's failing task. cwd now moves the project only into
  another worktree of the same repository, and the stop gate judges both that
  worktree and the session's own project.
- With the session folder outside git (a folder holding several checkouts), no
  edit below it was protected. Each file is now judged by its own checkout, and
  cwd picks the checkout for the other hooks.
- The phases nudge counted the human's uncommitted work from before the session;
  with a session baseline it now counts only the session's changes.
…LOCKED trigger 2

Review findings:
- A FAST task stays triaged after approve-tests, so "Next: phase 9 … task done"
  led to a refusal. A passing gate and the finish nudge now say to record phase 7
  first; 05-tests-first says only a task in testing moves to implementing.
- The missing-tool message offered "ask the human to install the tool", which an
  agent cannot do by stopping while the gate fails. The runner hint, the stop
  gate, 07-verify, paacm-verify and troubleshooting now say: list the check under
  CHECKS NOT RUN and stop with BLOCKED trigger 2.
Review finding: any change off the first-parent line counted as "merge", so the
writer could branch, commit protected configuration, merge it and have it
reported as a NOTE instead of a WARN. A change now counts as merged only when it
came from commits a remote-tracking branch already has (the base branch);
everything else, including a locally merged branch, is the task's own.
…ged frozen files

Review finding: after approve-tests refused a changed approved test, the writer
could block the task and start a new one, whose first approve-tests accepted the
weakened test. task start now refuses while the latest task is blocked and files
frozen by its approved tests changed since; the change is settled in that task
(resume, reopen-tests with the human) or undone.
Review findings:
- `git add -A` stopped on an untracked nested repository without commits and the
  whole review input failed. It now runs with --ignore-errors; the paths git could
  not add are listed in the review input.
- The temporary index was built with read-tree, so every tracked file was hashed
  again (clean filters and LFS ran on each) and a skip-worktree file's local
  content (for example a secret) entered the review. It now starts from a copy of
  the real index: with a settled index no clean filter runs (0 against 50 in a
  50-file check), and skip-worktree files stay out, as in git status.
- A file whose type changed has two diff sections and a submodule shown as a log
  has none, which disabled the per-file split or paired sections with the wrong
  files. Sections are now matched to files by the path in their header (fixed
  prefixes, unescaped names, quoted names decoded), and submodules are shown as
  commit lines.
- The longest backtick run is found with a loop; spreading 150 000 runs into
  Math.max overflowed the stack.
Review finding: git accepts --no-veri for --no-verify; 'export HUSKY=0;' before
git, GIT_CONFIG_COUNT/KEY/PARAMETERS and a global option before 'commit -n'
were allowed. They now ask. 'git log --grep=commit -n 3' no longer asks: the -n
rule needs commit to be git's subcommand.
Review finding: '(cd a && ./run.sh)' kept the closing parenthesis, wrapper
options with a value ('timeout -s KILL 60', 'nice -n 10') were read as the
program, and a script after a quoted cd was checked at the project root. On
Windows, a program written with its extension or found in the project folder,
as cmd.exe finds it, was reported missing.
…e gate log in memory

Review findings (reasoned, then checked): lefthook also reads .lefthook.yaml,
toml, json and jsonc files, also under .config/, and lefthook-local overrides in
every format (lefthook.dev/configuration); the defaults now cover them with four
globs. The gate kept every check's whole output in memory before cutting the log
to its last 8 Mi characters; it now keeps only that much while running.
User audit findings: 'git commit -n&&…', '-n;', '-n>out', '-nm"msg"', a quoted
'-c core.hooksPath=…' or config key, and HUSKY='0' / LEFTHOOK='0' passed the
skip-git-hooks rule although git honours them; 'docker build --push' passed
publish-deploy. All ask now. 'git update-ref' joins git-destructive: it moves a
ref directly (and could forge a remote-tracking ref).
…evidence list

User audit finding: the review input listed the files saved for reviewers with
the kit-content walker, which throws on a symbolic link, so one link replaced
the whole list with an error line. The task folder now has its own walker,
which skips links and unreadable folders.
…the report accounts for

User audit finding: the merge origin came from remote-tracking refs, which the
writer can create itself (update-ref, a fetch refspec, a local remote), so its
own protected-configuration edit could be reported as a note 'reviewed there'.
The origin is now context, never an exemption: every configuration change is a
WARN, marked 'through a merge' where it applies, and the report says who
approved it or where a merged one was reviewed. The review input no longer
claims a merged change was reviewed elsewhere.
…ct rules

User audit finding: the protect hook judged a file by the nearest
.agent/paacm.json, so writing sub/.agent/paacm.json with empty patterns made the
approved tests under sub/ editable (the root gate still caught it later).
Projects are now found by worktree: in the session's worktree a file is judged
by the session project; in another worktree of the repository by the project at
the same place. A .agent/paacm.json or .agent/checks.conf at any depth is
protected configuration by default.
User audit finding: with `cwd` in one checkout, the protect hook judged every
file by that checkout, so a sibling checkout's approved tests could be edited,
and the stop hook never judged the sibling, so its failing gate did not hold
the stop.
- protect and post-edit pass the session folder to editLocation, so a file is
  judged by the checkout it is in.
- post-edit marks the edited checkout (in its git directory) when the session
  folder is not a git repository; the stop hook judges each checkout directly
  inside that folder that the session marked. Checkouts the session never
  edited are not judged, so other agents' tasks there do not hold its stop.
…ee that has them

User audit finding: the blocked-task refusal read only the .agent/tasks folder
of the worktree `task start` ran in, so `git worktree add` from the same commit
started a task, approved the weakened test and passed the gate.
- `task start` checks the latest task of every worktree of the repository; for
  another worktree, only when this HEAD contains that task's approved test
  commit, so an unrelated branch is not held.
- `approve-tests` checks the other worktrees too, which covers a task started
  there before the change was merged.
Non-git session folders (protect at any depth, stop for the checkouts directly
inside that the session edited), the blocked-task refusal across worktrees,
git update-ref and docker build --push in the shell guard, private file modes
on Linux and macOS only, and the audit's findings in the release evidence.
Windows CI: `cmd /c` exits 1, not 9009, for a command it cannot find, so the
gate reported a missing tool as failing code. A check that exits 1 with cmd's
"is not recognized as an internal or external command" message is now a missing
tool; 9009 (what a batch file sees) still is. The docs no longer promise 9009.
@Hyper-AI-Lab
Hyper-AI-Lab merged commit a3a7fa9 into main Oct 28, 2026
10 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