Skip to content

Releases: AleksandarBisevac/claude-plugins

v2.1.1 — the review that belonged before 2.1.0, and two field reports

Choose a tag to compare

@AleksandarBisevac AleksandarBisevac released this 07 Sep 21:04

A patch, and three of its fixes are why it should not wait. The phase review that belonged
before 2.1.0 was run after it, and it found that the sweep could remove a worktree it had not
measured. Two field reports from live runs on other projects arrived in the same day and are
folded in. If you are on 2.1.0, take this one.

Fixed — the sweep could delete work it never examined

  • The sweep acted on a different worktree than the one it checked. git worktree add --force
    legally puts two records on one branch. Provenance and cleanliness were measured per record and
    then thrown away, so a plan could report "keeping /a — not created by this plugin" and remove
    /a in the next line, judged by /b's answers. The plan now carries the record it measured,
    and the branch is not deleted while any worktree still holds it.
  • "Do not delete the directory you are standing in" was blind one directory down. The check was
    path equality where its own docstring said inside, so from <worktree>/src it never fired.
    Reproduced: git status calls the tree clean, git worktree remove exits 0, and the directory
    and its .env are gone. It is containment now, with a separator boundary so repo-P1-old is not
    a child of repo-P1, and the innermost match wins.
  • A precondition that failed OPEN now fails closed. "The caller did not establish where it is
    standing" and "the caller asked, and is outside every worktree" were one value. The panel passed
    the first one literally, so a panel served from inside a phase worktree could remove the
    directory it was being served from.
  • Provenance proved the plugin made a worktree, never this one. The marker records the
    phase and branch it was written for and only createdBy was read, so git switch inside a phase
    worktree handed one phase's open directory to another phase's sign-off verdict. Both fields are
    compared now, and the refusal names both sides.
  • A malformed tasks array read as a fully settled phase. A list of bare ids passed the
    settlement gate with a why that asserted "every task is terminal" about a list it never
    looked at. An entry it cannot read is a refusal now.

Fixed — sign-off

  • meta.merge.auto: false turned a real refusal into exit 0. A parentBranch this clone does
    not have came back as "deliberately unmerged", with the actual reason never printed. A refusal
    outranks the switch.
  • --no-ff was silently dropped when the parent is checked out nowhere — a fetch cannot make a
    merge commit — so the run fast-forwarded and reported success. It refuses and says why. This
    matters twice: --no-ff is the documented remedy for a not-a-fast-forward exit, and in that
    topology the remedy could never have worked.
  • git branch -d refused branches that had landed. It grades from HEAD, and after a merge into
    a parent checked out nowhere HEAD is never the parent — measured on a real repository, exit 1 and
    "not fully merged" about a branch just proven contained, after its worktree was already gone.
    Deletion is guarded on the branch's own sha now, which refuses exactly what a force delete would
    not: a ref that moved.
  • phase.mergedAt is written before the cleanup, not after, and a stamp that fails holds the
    cleanup back. A deletion is a consequence of the merge; the record of the merge must not depend
    on the consequence succeeding.
  • An already-landed phase is stamped. The re-run the docs tell you to make after merging by
    hand removed the worktree, deleted the branch, and wrote no mergedAt — leaving the phase
    unsettleable for ever.
  • ...and the stamp was going to a plan that did not carry the phase. Found only because the
    failure became load-bearing: the write was redirected to the parent's worktree, whose copy had
    never heard of that phase, and the run exited 0 with mergedAt written nowhere.
  • --dry-run previews the cleanup, computed against what the merge is about to make true, and
    says that it is conditional. It used to show the merge and nothing else while the same command
    without the flag removed the worktree and deleted the branch.

Fixed — reported from live runs on other projects

  • run-test-gate.py now applies meta.nodePreamble itself. The documentation named it four
    times; the script had no handling at all and spawns its own shell. Two gate runs recorded exit
    127 — a PATH problem — as evidence, so a committed ledger carries false failures for ever.
  • The plan-gate refusal speaks to the right audience. A subagent was told to "add a task
    covering this file to the manifest"
    — something it may not do and has no channel to ask about.
    One executor stopped to ask its operator; in another run the same refusal produced three
    different resolutions, four of them the agent editing the manifest itself. A subagent is now told
    to stop and report, is told it will not be re-spawned, and the manifest exemption no longer
    extends to subagents
    .
  • NO OVERLAP WITH THIS WORK distinguishes two situations it used to render identically. A
    runner printing suite paths while the task declares sources now matches tests/foo.spec.ts to
    src/foo.ts — one report saw this warning on 9 of 12 tasks for that reason alone. It still fires
    where it was earned: the other report called it the best thing in the plugin because eslint and
    tsc named real files and none was the Markdown that task owned. The match widens only onto
    test-shaped paths and only onto the exact stem they carry.
  • tests.add is unioned into task.files. A tdd task creates the file it names, so a scope
    that excluded it failed the task's own commit; one operator hand-fixed 13 tasks.
  • tdd with no case named is warned about, at creation and in the validator, for unfinished
    tasks only — a case named after the task is done is not a red-first case.
  • A task commit can satisfy manifest-revalidated. task.files lives in the shard a task
    commit stages and fileIndex lives in the index it may not, so a scope corrected mid-run
    breached on every later commit — 39 times across 10 of 12 tasks in one run, 93 in a phase of
    another. The pairing is deferred per commit and asked of the manifest as it stands, so it is
    a moment that cannot be true rather than a rule that is waived.
  • set-priority.py no longer refuses a lock you are holding yourself.
  • A write made by a session's first Bash call is journalled. The Bash lane had no
    pre-pass, so the first shell write of a session was seeded over and lost — audit-journal verify
    then reported "an edit the journal never saw", correctly. The offered cause (heredocs) was
    reproduced and refuted before anything moved.
  • /audit:phase 2 resolves. One resolver for every script: exact, case-folded, then a bare
    integer onto P<n> when that phase exists — and the refusal names the ids that do.
  • Rule #1 tells reading a secret from writing about one. Naming a secret filename in a comment,
    a docstring or a printed sentence inside an interpreter body was refused as reading it. It grades
    on the paths a read call names now, keeping the shell-read matcher for a read hidden in a
    subprocess list. The refusal also names the form you actually typed rather than always saying
    python -c.
  • The handbook can no longer advertise a flag nothing accepts. A flag removed properly — parser,
    tests, and a comment at each site explaining the removal — stayed "carried" because the detector
    was a text scan that found its own obituary.

Changed

  • /audit:doctor, the panel and every /audit:worktree verb keep working on git 2.34
    (Ubuntu 22.04 LTS): worktree list --porcelain -z arrived in 2.36 and there is a fallback now.
  • The orchestrator reference says a widened scope continues the running executor rather than
    replacing it, and the executor prompt says what a gate failure in a file it does not own is.

v2.1.0 — the worktree and merge half stops being prose

Choose a tag to compare

@AleksandarBisevac AleksandarBisevac released this 07 Sep 06:33

The worktree and merge half of the pipeline was prose, and prose had three
things wrong.
/audit:worktree composed a path and recorded it nowhere, so
nothing could enumerate what it had created and nothing ever cleaned up; sign-off
steps 5c–5e were git commands the model typed. Measured on the repository that
dogfoods this plugin: 29 linked worktrees, 27 of them holding branches that had
already reached their parent.

Added

  • /audit:worktree grew list, remove and sweep beside add, and every
    verb is a script call rather than a paragraph. sweep is read-only until you
    pass --apply
    plus a verb — --remove-worktrees, --delete-branches,
    --prune. Four conditions, and the first two are about permission rather than
    safety:
    the plugin must have created the worktree (add records that in the
    worktree's own admin directory, where it dies with its subject), the phase must be
    settled — signed off, no task still open, mergedAt recorded — and only then
    does "branch contained in its parent" and "tree clean" decide. A worktree you
    opened yourself, or a colleague's, is indistinguishable from the plugin's by branch
    name and merge state, so nothing but an explicit record can tell them apart.
    remove --path <dir> is how a worktree the plugin did not create comes down: one
    directory, named by the person who wants it gone.
  • meta.merge — three switches deciding what sign-off does once a phase's
    tasks are done: auto, removeWorktree, deleteBranch. All three are on when
    absent, so every existing plan behaves exactly as it did. auto: false is the
    human-in-the-loop switch
    : the phase is still reviewed, gated and committed, and
    the run stops before the merge and prints the command. It exits 0 — that is a
    choice, not a failure — and stamps nothing.
  • The panel edits where a phase lands, and shows what is left over. The Branch
    card gained the merge target (meta.developmentBranch, reachable only by hand
    before), the three switches with the key that decided each, and a live worktree
    table with a Sweep button. That button is the panel's first git write and goes
    through the same path as a config save: the loopback guard, the manifest lock, a
    dry run whose rows fill the existing confirm dialog, and a journal row.
  • /audit:doctor reports what earlier runs left behind — worktrees whose branch
    already reached its parent, records pointing at deleted directories, and branches
    it could not compare. It reports and never reaps: the remedy it prints is the
    read-only command.

Fixed

  • Sign-off could not complete inside a worktree. git switch <parent> fails
    there with fatal: '<parent>' is already used by worktree at … — so the
    documented sign-off was unavailable on exactly the runs /audit:worktree
    recommends. The merge now happens in the worktree that already holds the parent,
    or as a fast-forward with no checkout at all. Nothing moves your HEAD.
  • phase.mergedAt was written into the directory about to be deleted. A phase
    that ran in a worktree stamped its own copy of the plan, and the surviving copy
    still read null. The stamp now follows the merge.
  • git branch -d was trusted to know whether a phase had landed. It grades
    reachability from HEAD, not from the phase's declared parent — measured, it
    deleted a branch whose parentBranch was develop while the work had only
    reached main, exit 0. Deletion is now gated on
    git merge-base --is-ancestor <branch> <parent>.
  • /audit:doctor turned a question git refused into an accusation.
    check_branch_naming read merge-base --is-ancestor's exit code as a boolean, so
    exit 128 — a parentBranch this clone does not have — printed as a definite
    "is NOT yet merged". It now answers contained / not-contained / could not
    say
    .
  • phase.mergedAt's schema description said "the development branch" while
    sign-off has merged into the resolved parent for as long as parentBranch has
    existed.
  • The shell-write notice went quiet for the rest of a session once agents ran.
    guard-bash-writes reports a Bash write no tool edit and no in_progress task
    accounts for, and it bounds "recently" by when the guard last looked. Agents in one
    session share a state file, so a peer's look moved everyone's window forward and a
    write could land inside it unreported. Each writer now carries its own look, taken
    from the state file's own mtime — the same clock the sibling-session comparison
    reads — and a writer with none falls back to the session's floor, which is what an
    older copy's state file produces. A look that could not be established is not
    recorded at all: an unbounded window silences the guard, which is the failure this
    replaces.

Documentation

  • The handbook's /audit:worktree row named a flag that does not exist
    (--include-strangers, built and then removed once it was clear that adopting
    worktrees on a guessed parent is exactly what a sweep must never do), and its sweep
    card still stated the old two-condition rule without the provenance and settlement
    gates. Both are corrected, /audit:doctor's card now names the residue it reports,
    and the panel section documents Sweep as its first git write and how it is fenced.
  • SECURITY.md gains the route. POST /api/worktrees/sweep takes no path, no
    branch and no force parameter, executes only what the shared planner returned, and
    its four conditions all fail closed — removing one of them is a major.

v2.0.1 — the switchboard opens on what a clone would carry

Choose a tag to compare

@AleksandarBisevac AleksandarBisevac released this 01 Sep 08:19

v2.0.0 taught four
surfaces to say whether a capability would survive a clone — and missed the fifth. The Policy
tab
is the one place a reader goes to ask what governs this project, and it was still listing
everything on the machine.

Reported from a real repository: a project using a handful of committed skills listed a hundred
and twenty capabilities, every row reading policy.skills.default is allow.

What changed

Every row now carries the verdict and the basis behind it. One that stays on this machine is
badged and dimmed. Under portability: "strict" the table opens narrowed — with a control that
says how many are hidden and brings them back in one click.

What it will never hide is a row somebody wrote a rule about. The narrowing needs two things
to be true at once: the capability would not survive a clone and the policy says nothing about
it. A denied capability, an allow-listed one, and one audit itself requires all stay on screen
wherever they live — this table previews what the guard will do, and a refusal nobody can see is
a lie about that.

Both halves of that condition are proven by mutation, and the browser gate drives the real panel:
it asserts the denied row survives the narrowing, because that is the failure that would look
perfectly fine in a screenshot.

The empty state gained a third answer for the same reason. "Everything discovered here stays on
this machine"
is not "nothing matched your filter" — and the Clear-filters button would not
have brought those rows back.

Also

The portability tier was being resolved in three places after 2.0.0.
_config_rules.portability_mode owns the fallback now, and the doctor, the panel's write refusal
and the switchboard all call it.

Nothing about the manifest, the config vocabulary or the gate changed — this is a patch.

Full detail in CHANGELOG.md.

v2.0.0 — a plan may not name a skill only its author has

Choose a tag to compare

@AleksandarBisevac AleksandarBisevac released this 31 Aug 20:47

An audit plan names its skills by string, and until now nothing asked whether anyone
else could load them. On the machine that wrote the plan every name resolves — so
/audit:doctor printed a green row while a teammate's clone loaded none of it, and
every task ran without the conventions it was written to follow.

Measured on a real repository: six named skills, five of them from ~/.claude/skills
(in no repository and no marketplace), and four live tasks in the phase due to run next.

What is new

Every discovered skill, subagent and MCP server now carries a verdict — would a clone
of this repository load this?
— and the basis behind it:

  • a capability under .claude/ travels;
  • one in a home directory never does;
  • one from a plugin travels only when the committed .claude/settings.json declares
    it in both extraKnownMarketplaces and enabledPlugins. When exactly one is
    present, the message names the one that is missing — committing only enabledPlugins
    is a documented trap that keeps working for whoever added it.

Four surfaces read that one verdict:

surface what it does
/audit:doctor a plan portability row — a warning at most, naming the skill, where the plan names it, and why
/audit:status --gate --fail-on stranded-skills the CI half, deliberately out of the default gate
the control panel lists what stays here, keeps it out of the pickers, predicts the refusal inline, and refuses the write
the report marks a stranded name from the repository alone, so a shared artifact reads the same on every machine

Read this before upgrading

portability ships "strict", and that changes what an untouched repository accepts.
The panel will refuse a save it took the day before, whenever a name in the patch resolves
on your machine and would not survive a clone. This is the one clause COMPATIBILITY.md
promises against — broken on purpose, recorded there with the reason, because the defect it
prevents is silent on exactly the machine that causes it.

Two ways out, each one row in the Settings tab or in .claude/audit.config.json:

{ "portability": "warn" }   // diagnose everything, refuse nothing
{ "portability": "off" }    // restore the previous behaviour exactly

Other things worth knowing:

  • a name discovery has never seen is still accepted, at every tier — an inventory is not
    a whitelist;
  • stranded-skills fails when the scan could not run, rather than passing quietly;
  • _panel_discovery._mcp_names is now _mcp_entries and returns rows, not strings;
  • check_plan_skills takes the config pair, mirroring check_policy.

Also fixed

  • Two docstrings asserted a layer their module had already left — now a lint
    (layer_doc_drift), proved red in both directions.
  • The policy tab rendered an MCP server's name in the source column.
  • A regression case that could not fail: it compared a level to "finding" while the
    collector writes "FINDING".

Full detail in CHANGELOG.md.

v1.9.0 — test-execution evidence, recorded rather than described

Choose a tag to compare

@AleksandarBisevac AleksandarBisevac released this 28 Aug 23:51

A minor by this repository's own table: the manifest gains keys the plugin reads, the config
gains one, /audit:status gains gate conditions, and none of it changes what a plan that does not
opt in already does. Adding is free; that is what makes this a minor rather than a major.

Added

A plan could say what should be tested and had nowhere to record what was. task.tests.gate
and phase.testGate are declarations; the only homes for a result were outcome.technical, a free
string the schema itself describes as holding "test counts", and verifiedBy[], which is rendered
nowhere. This repository's own dogfooded plan carried "sweep 2173/40 green" in that field — a
model-written number with no basis, in a repository whose build lint fails a file for exactly that
shape.

The measurement mostly existed. run-test-gate.py already bracketed a gate with a tree snapshot,
counted what ran, related the run to the files the work declares, and was carefully three-valued
wherever it could not know something. It performed no disk I/O at all, and its --json flag had
no caller anywhere in the tree. What was missing was memory.

Test-execution evidence, recorded rather than described. A gate run through --record appends
one line to a committed NDJSON ledger beside the manifest, and points a three-key testEvidence
block — runId, status, at — at it from the task or phase. The ledger is the source of truth;
the pointer is a cache, deletable at any moment and re-derivable by reading the ledger. Absent
means no run has been recorded — never "failed"
, which is what makes the key free to add.

The row is assembled from named fields rather than copied, so "no program output is stored here" is
a property of the writer instead of a promise about it. A gate command is stored verbatim only where
the manifest already publishes it; anything else falls back to a digest, a byte length and a program
name. Every path passes through the structural redactor that already existed. A gate's stdout is
not stored — not truncated, not redacted: not stored.

Status is kept apart from observations, because a gate can fail and rewrite the tree, and a
single verdict loses one of those two facts every time both are true. treeMutated, coverage and
ranTotal are three-valued and read by explicit comparison: null is not knowable, 0 is
positively zero, [] is clean, and only [] may render as clean.

run-test-gate.py gained the lifecycle it was missing. It now tears down the whole child process
tree on timeout, SIGINT and SIGTERM rather than the shell alone; distinguishes a timeout from a
command that never started; times each step; resolves a task's own gate when asked; and records an
interrupted run instead of exiting on a traceback. An interrupted run reports treeMutated as not
knowable
rather than clean, because a killed child may still have been writing.

testedState identifies the work a run was asked to judge — a digest over the declared files
taken before the first command, so a fix-in-place runner cannot make the evidence describe what it
produced; a digest of which paths were dirty; and head, which is repository HEAD at execution time
and carries a basis saying it does not identify the tested state, because a task gate runs before
the task commit.

An evidence boundary, so the gate can ask whether a run was possible before asking for one.
--fail-on no-test-evidence used to fail on every finished task in a plan adopted mid-flight, with
no setting that helped. The boundary is the earliest moment there is any evidence that recording
existed — the earlier of a key the recorder writes once and the ledger's own first row — so
deleting either source leaves the other answering. Work finished before it is excused, and the
excuse carries the sentence that justifies it.

Where it surfaces. The report gains a column and a third group in the existing drawer; the panel
gains the same on its overview; /audit:status gains a column and the conditions failing-tests
and no-test-evidence, both opt-in. Sentences that mean different things stay different sentences:
No gate configured, No evidence, Before recording, Pointer without evidence and Completion
undated
have five different causes and five different repairs.

Durability for the runs nothing else would commit. A failing run leaves no task commit behind it,
so a narrow audit-state commit exists for the points after which no commit is coming, and stages
audit state, the trail and the evidence — never the task's own files. Two invariants grade it, and
/audit:doctor correlates pointers against the ledger in both directions.

Tamper evidence by anchoring each run into the existing hash chain rather than building a
second one, split so that a row is never written before the thing it claims exists.

evidence.dir joins the config surface, published on every side that publishes a config key.

Fixed

Two fields shipped that nothing produced. attempt was copied by the row writer, rendered by
both surfaces, and set only by the demo generator — so the shipped example advertised a capability
the product did not have, and that drawer row was blank for every real user. cancelled was in the
schema enum, rendered everywhere, and named by the orchestrator document among the rows that get
written, while a Ctrl-C escaped as a traceback and recorded nothing.

A schema-valid manifest could blank the whole report at load. A phase or task id that collides
with a name on Object.prototype reached the prototype chain through a bare bracket read. Neither
phase.id nor task.id carries a pattern in the schema. Reads now go through one shared helper, and
maps built from outside keys are created prototype-free — a read guard alone does not fix the second
shape.

An open filter panel had no height bound. Two added chip rows took it from a tenth of the screen
to well over a third; the width had been written against the viewport all along and the height never
was.

The tree bracket could not see inside an untracked directory. run-test-gate was the only
porcelain reader in the tree without -uall, so with the subject directory wholly untracked a
fix-in-place runner that created files inside it left the one value that means known clean.

guard-bash-writes stopped blaming the wrong writer, twice. A command that moves the shell
withdraws the authorship half of the finding, because the payload's directory is the session's and
only the command moved. And agents of one session no longer inherit each other's writes: they share
a session id, so the writers are told apart by the discriminator the payload actually carries. Both
are withdrawals — the finding stays reported and the guard names nobody it cannot prove.

Checks that could not see what they exist to catch. The artifact gate compared the working tree,
so a render left unstaged read as clean while the commit carried the old bytes. The published
handbook was read by no gate at all, and had rotted accordingly. The case holding the whole
measurement boundary asserted nothing, because its fixture never committed and porcelain collapsed
the untracked tree to one line. Two browser-gate selectors pinned an arrangement where they meant a
property. And the meta-gate that keeps four descriptions of the gate set honest was itself proven by
nothing.

/audit:status --phase is documented. It was accepted by the parser and named by neither the
command's argument hint nor the README row, and the lint that pairs those two cannot see a flag
missing from both. Its limit is stated with it: it scopes the human render, totals stay whole-plan,
and it scopes neither --gate nor --json.

/audit:doctor reports the plugin that ran the hooks, not only the one on disk. CLAUDE_PLUGIN_ROOT
is fixed when a session starts, so a long-lived session keeps the copy it began with — and a guard
several releases behind was silently in force while the command asked to report it answered about the
installation. Agreement, disagreement and not established are three answers, and the third is not
the first.

A GitHub Release is part of the release procedure. The Releases page had drifted until it
presented a long-superseded version as Latest while the README pinned a far newer tag. Both surfaces
are published and they disagreed in public.

Consequences worth knowing before you upgrade

  • A gate that writes inside a wholly untracked directory now refuses where it used to pass. That
    is the repair, not a regression: it was signing off on work the gate itself had written.
  • testedState.dirtyDigest moves once for a repository that holds an untracked directory at gate
    time, because it digests those porcelain lines and they are now per-file. The retry discriminator
    reads "the tree changed" across the upgrade boundary and self-heals from the next run.
  • Nothing changes for a plan that does not opt in. Both new gate conditions are off by default,
    for the reason the code states beside them: a repository that has never recorded a run carries no
    pointers, so a default holding either would fail every build on upgrade day.

audit 1.8.0

Choose a tag to compare

@AleksandarBisevac AleksandarBisevac released this 27 Aug 06:16

A minor rather than a patch, by this repo's own table: the manifest gains a key the plugin
reads, which is new behaviour and not only fixes. Adding a key is always free — a plan that never
sets phases[].adoTracked resolves exactly as it did before — so nothing moves for anybody who
does not opt in.

Added

/audit:sync status could not tell a deliberate exclusion from drift, so an internal phase
reported as unlinked forever.
A phase that has no business on a shared board — an internal
refactor, a spike, work that never leaves this repository — had nowhere to say so. The only ADO
field a phase carried was phase.ado, the link sync writes when it creates a work item, and an
absent link means not created yet. The drift lens therefore grew a row no push would ever clear,
and a backlog that cannot reach zero is a lens people stop reading — which costs the rows that
were real. phases[].adoTracked is the missing declaration: false keeps the phase, and every
task that inherits from it, out of the push plan; true or absent is tracked, so a plan that never
writes the key produces the plan it produced before.

The obvious spelling was phases[].ado: false, and it is the wrong one. phase.ado is an
adoLink written only by sync. Overloading it would make one field hold an object when the board
has the phase and a boolean when it never will, so every reader — the validator, the panel's ADO
card, the orchestrator echo, the drift table — would have to test the type before it could read the
value, and the first one that forgot would treat false as a link. A declaration the operator makes
and a link the tool writes are different facts with different owners; they are different keys.

One door answers it, and every answer carries its basis. The rule lives in
resolve-ado-tracked.py and the module behind it, the same split as the parent resolver and for
the same reason: prose cannot be tested, and a second copy of a precedence is a second answer. Its
report never returns a bare boolean — it says whether the phase declared it, whether a task
inherited it and from which phase, or that nothing declared anything and the default is tracked.
Bugs are outside it entirely and are told so rather than answered tracked: a bug is not owned
by a phase, and bug.ado is usually written by a pull, off the board. "Nothing here is tracked" is
an answer and exits 0; there is no exit code for a phase being off the board, because that is a
normal state and not a violation.

What a run skips is counted where the operator can see it. The push plan gives untracked items
their own line, printed even at zero, instead of quietly lowering the create count — nothing to
create here
and deliberately skipped this phase want opposite reactions, and a single number
folds them into one. status's inventory reports untracked as a class beside linked and unlinked
rather than as a shortfall in the second. Absence keeping its meaning is a promise, not a
convenience: COMPATIBILITY.md now carries the key, so it keeps being read and adoTracked
absent keeps meaning tracked for the rest of the major line.

Fixed

The panel's phase-row menus clipped their own labels, and every check passed while they did. A
closed <select> renders one line and clips it — no wrap, no ellipsis — so a label longer than the
control is a phrase cut off mid-word. The parent picker painted use the fallback — out of a label
three times that, which reads as a whole option meaning nothing. No substring assertion could see
it: the full literal was in the page, and only the paint was wrong. It took a screenshot.

The fix is one rule rather than shorter strings. fillOptions takes an optional character bound and
moves the full text to the option's title, so nothing is lost and the ellipsis makes the
truncation visible. The bound is a parameter and not a constant, because it is a property of the
control — the panel's other selects are wider, and a global bound would truncate labels that already
fit. And the fallback option was reordered to lead with the work item id: a label whose first words
are prose spends the whole budget before reaching the thing the option is about, so truncating it
loses exactly the wrong half.

guard-bash-writes blamed a reader for somebody else's write, twice in one session, and the
first cause was one missing word.
cd was not on the read-only allowlist. Every segment of a
shell command is judged on its own, so a cd in front of a read left the whole command unproven —
cd <dir> && grep -n foo FILE, the ordinary shape in any session that works across two
repositories, came back watched and then inherited the blame for dirt it had nothing to do with.
cd, pushd and popd move the shell and cannot touch a file. Adding them removes an
attribution and never a refusal: cd x && rm -rf f is still watched, because rm is still not on
the list, and that direction is a case rather than a claim.

And a background job of your own session had nobody to blame but the next command. The guard
already asks whether a PEER session wrote the file — and cannot ask it here, because
_other_sessions() skips mine on purpose, so a detached job launched by this session has no
sibling state file to claim its writes. PostToolUse fires when such a job is launched, minutes
before it writes, so the dirt lands inside a later pass's window. The launch is now recorded —
program name only, never the command text (CWE-532, the rule _journal_io already holds) — and
while any launch is unaccounted for, the verdict drops its authorship claim and names them with
their age, the same shape the guard already uses for a peer session. It is not silenced: an
unplanned source write is worth a line whoever made it.

Not bounded by timeout, which was measured rather than assumed: a background prove-gates.py
launched with timeout: 600000 ran 627 seconds and completed normally. No hook fires when a
background job ends, so no honest window exists — which is why the launches are reported with an
age instead of being silently expired.

audit 1.7.0

Choose a tag to compare

@AleksandarBisevac AleksandarBisevac released this 27 Aug 06:16

A minor rather than a patch, by this repo's own table: the test-gate runner gains a flag and a new
line of output, which is new behaviour and not only fixes.

A gate can run, pass, and cover nothing that changed. run-test-gate.py already caught a gate
that did too much (it rewrote files the task did not own and reported Passed because it had) and a
gate that did nothing (every hook skipped, exit 0, zero checks). This is the third shape and the exit
code separated it from neither: a UI suite ran, passed, and reported a real non-zero count — against a
diff that was a one-value edit to a JSON manifest. The count exists so that a zero cannot pass for
green; a non-zero count overlapping the change nowhere is the same false verdict with better cover.
The runner's output is now related to the files the work under test declares, and the answer is
stated: no overlap, real overlap with the files named, or the question could not be asked. Asked of
the phase by default — that is where the script is invoked from — and --task <taskId> narrows it.

It reports and does not refuse, which was a deliberate decision rather than a shortcut. Refusing
would decide what a gate is allowed to be, which this script declines to do for itself elsewhere, and
it would refuse on a heuristic: the overlap comes from paths a runner happens to print, and a guard
that refuses on a guess teaches people to route around it. Where the runner prints no paths the line
says the question is not knowable from its output — never that nothing overlapped.

The secret guard convicted a whole Bash block on evidence from two different commands inside it.
guard-secrets-read split a command into clauses on ;, | and & — and not on a newline. So a
multi-line block was judged as one clause: an inline-eval marker on one line paired with a write to a
repo path on another, neither of which is a violation alone. Reduced to two lines where each half is
allowed by itself, the pair is refused, and the same pair joined with ; was allowed all along —
which is what named the missing separator. Newlines are separators now. A line continuation still is
not one, and neither is a newline inside quotes, so the remaining shape — an interpreter invocation
and a repo path inside one quoted argument handed to another program — is still refused and is
written down as a limit rather than left to be found again.

Two of that fix's own new cases were asserting nothing, and only mutation found it: one credited a
code branch that, when deleted, changed no verdict, and the other asserted a fail-safe whose result
was produced by a different mechanism entirely. Both now assert on the splitter directly. A third
thing turned up the same way — a backslash in a docstring opened an invalid escape sequence, whose
SyntaxWarning pulled warnings, linecache and tokenize into a hook that must import fast. The
hook import budget caught it; the cached bytecode then hid it from the re-measure.

/audit:phase's hint now names what its verbs accept. The same defect 1.6.0 fixed for
/audit:task, in the document that fix did not touch: add advertised two flags against a script
accepting eight, so an operator reading the hint could not discover the rest. The check that holds a
hint and its script to the same set reads both command documents now, and it found something on
its first run — the writer table it shares with another case had no row for the function that writes
those fields, so the check had been quiet over the gap rather than reporting one.

audit 1.6.0

Choose a tag to compare

@AleksandarBisevac AleksandarBisevac released this 27 Aug 06:16

A minor rather than a patch, by this repo's own table: /audit:phase add gains a flag it did not
have, which is new behaviour and not only a fix.

A flag three verbs accepted and ignored. /audit:phase add --gate-clear was taken by the shared
parser, never read by the code that decides the new phase's gate, and the phase inherited
meta.buildCommands while the caller was told the call had worked. That is the third verb of the
exact same shape, after task scope and task add in 1.5.0. The flag is now read, the contradictory
pair --gate X --gate-clear is refused rather than silently resolved, and the argument hint, the
script's usage block and the README signature all carry it.

The check that should have caught it was asserting nothing. It skipped any verb absent from
commands/task.md, so a commands/phase.md verb sat outside the very check that exists because of
the first two instances — and it then searched the writer's SOURCE TEXT for the flag's attribute,
which the comment explaining the repair satisfies. With the read deleted outright, the case stayed
green on prose about it. It now reads both command documents and walks the AST, so a docstring, a
%r inside a message and a commented-out line no longer read as a use. getattr(args, "…") counts
as a read too: without that a writer reading defensively would be convicted, and how to spell a read
is a style question that belongs to the style lint.

A verb that could not run on the task it was written for. task scope created the tests object
whether or not anything was going to be put in it, so --files on its own left an empty one behind —
and an absent tests is legal while one present without a mode is the single shape the schema
refuses. Nothing was ever corrupted, because the write rolled back; the verb simply exited non-zero.
The task this bites is exactly the one scope was added for: an imported sprint task whose own
description says scope files/tests before running carries no tests key at all. It is now created
only when something populates it, and a tests flag on a task without the object is refused before
the write, naming the flag that resolves it — rather than inventing a grading nobody chose on a task
being scoped precisely because its testing was never decided.

/audit:doctor counted its evidence and then cut it. A finding named some of the tasks it had
just counted, and the reader had no way to learn which name was dropped — the count was true the
whole time, which is why no existing check could see it and why reviewing the count is not a
regression test. The same run carried it twice more, in the no-SHA and ledger-coverage arms of one
function. The sweep behind this release found it across the tree, with hand-picked caps that differ
per site because their elements differ in WIDTH — which is what a character budget expresses and an
element count cannot. One shared renderer replaces all of them: a realistic set is named in full, a
pathological one still truncates with the exact remainder stated, and the first element is always
shown, because a budget that can drop everything turns a finding into a bare number. Count the sites
it now covers with:

python3 -c "import sys;sys.path.insert(0,'plugins/audit/scripts');import _output;print(len(_output.truncated_evidence_violations()))"

A test was defending that bug. One case asserted the truncated form as correct — four dead, three
named — which is how the shape survived review. Rewritten to assert every name is present and that
the dropped one specifically is reachable.

New lint. Inside one % format: len(X) interpolated, a bounded prefix slice of X interpolated,
and nothing in that format stating the remainder. The remainder is read structurally rather than by
grepping for the words and N more, because the sites that were already correct spell their tail
differently and one of them is a hook that can never import the shared renderer — a wording rule
would have convicted a compliant site. Its blind spots are documented in the function and every one
of them under-counts.

audit 1.5.0

Choose a tag to compare

@AleksandarBisevac AleksandarBisevac released this 27 Aug 06:16

A minor, not a patch, by this repo's own table: it carries new behaviour, not only fixes. And the
entry that matters most was found by the plugin running against a real project, not by reading it.

The journal recorded nothing when a session wrote through Bash. journal-writes.py derived
task.complete, task.commit and phase.signoff from a pre-image cached by a hook registered only
on the edit tools, so a session using shell writes lost those rows — and the chain still verified,
which is the worst pairing: tamper-evidence intact over a history missing the events it exists to
record. Measured on a live plan, most of its finished tasks carried no completion row at all. The
pre-image is now kept by the Post pass rather than owned by the Pre pass, so it is refreshed after
every recorded row and any writer's next change can be diffed against it whatever tool made it.
Deriving the rows from the chain instead — the obvious idea — was checked and rejected: stateHash
is a one-way digest, so it proves a file changed and cannot yield the content a diff needs. Where no
baseline exists the pass seeds and says nothing, because a row claiming a write it cannot
establish would be fabricated evidence in the one file whose job is to be trustworthy.

A test gate rewrote five files that the task did not own. pre-commit run --all-files as a docs
task's gate: isort and black are fix-in-place and reported Passed because they had rewritten
the tree. Then the same gate, narrowed to that task's markdown files, skipped every hook on a
Python-only config — exit 0, nothing verified. One design, both failure modes, and the exit code
separated neither from a real verdict. New run-test-gate.py brackets every gate with a working-tree
snapshot and refuses the commit step on any difference regardless of the gate's exit code, and reads
how many checks actually ran. /audit:init now prefers a read-only spelling and flags a candidate
that may write.

A plan can be corrected, and correcting it no longer lies about what changed. /audit:task scope
reaches files, tests, description, risk, blockedBy and dependsOn; /audit:phase retarget
reaches a phase's gate, area, outcome and description; --gate-clear reaches the EMPTY gate that an
import had made unreachable, on add as well as the other verbs. The journal rows those writes leave
now carry the true prior value — two fields wrote a literal null, so the trail attested a state that
was never there, and a third journalled a row for a change that had not happened.

doctor grades the skills a plan names. It already said "runner not on PATH here: that gate
cannot run on this machine" and said nothing about a review skill that does not resolve — the same
shape of machine-local dependency, one warned and one silent. A scan that fails, or an inventory that
comes back empty, is reported as unknown rather than as clean.

The operator's own words reach the journal unchanged, with the rule in one place and a lint that
fails the build if a command asking a human for such text does not say so. status explains a
total that cannot be reached
cancelled was computed, printed by the HTML report and withheld by
the terminal, so a phase read 0/5 over four runnable tasks with nothing saying why; all three
surfaces that print such a total now carry it.

Also: the manifest validator accepts area.skills: null, which the schema has always documented
and the resolver has always honoured — the validator was the only reader out of step, and CI ran both
of them. /audit:task's argument-hint advertises the flags its script actually takes, and a new
check asserts that every flag a verb advertises is read by that verb's own writer — the parser is
global, so argparse accepts a flag a verb ignores.

audit 1.4.1

Choose a tag to compare

@AleksandarBisevac AleksandarBisevac released this 27 Aug 06:16

A patch release, and the two entries a user feels are both about a guard that was quietly not
guarding.
Everything here was found the same way 1.4.0 was — by running the plugin, not by
reading it.

The index lock leaked on every proposal write, and a refused one wrote anyway.
_locks.acquire returns an integer on every path; two callers named it handle and tested
isinstance(handle, dict), which is never true of an int. One misreading, two defects, and the
try/finally around it made the first look handled: the release never ran, so every materialize,
drop and revive left the lock on disk — and the status was never read, so an acquire that was
REFUSED fell straight into the write and changed the manifest with no lock held, which is the
one case the lock exists for. Downstream, that stranded lock made a later command report a live
holder and print a takeover warning on a machine running one session; the liveness probe was right
and the operator was still told to wait for a run that had finished. repair-commits had the same
bug. The reading now lives with the contract as _locks.held(), a refusal carries _locks' own
sentence rather than its terminal lines (those name the host), and a project with no lock scheme is
told apart from a lock that could not be taken.

An imported phase ran with the plan gate inert. /audit:sync pull sprint writes tasks with
files: [] and told the reader to scope them before running — and no verb could: add creates,
cancel closes, move relocates, and the panel reaches skills and model but not files. Since
files is what fileIndex is built from and fileIndex is what the plan gate matches an edit
against, the gate had nothing to match and so failed nothing. New /audit:task scope, with the
index re-derived rather than appended to, so files a task no longer claims are released.

A plan can be corrected now, not only created. /audit:phase retarget reaches a phase's gate,
area, desired outcome and description. --gate-clear is the load-bearing half: --gate appends, so
after an import there was no spelling at all for the EMPTY gate — a designed state (sign-off on
review alone) that /audit:phase add --gate could already reach for a new phase. An imported phase
given a guessed gate could not pass its own sign-off, and every route out was outside the plugin.
init and pull now prefer the empty gate to a guess and print the basis: a guessed gate is worse
than no gate, because no gate says so.

The operator's own words go into the journal unchanged. The trail is tamper-evident and works on
whatever sentence it is given, so a paraphrased --reason makes the chain guarantee something its
subject never wrote. The rule lives once, in reference/manifest-conventions.md, and a lint fails
the build if a command that asks a human for such text does not say so.

status explains a total that cannot be reached. cancelled was computed, printed by the HTML
report, and withheld by the terminal — so a phase read 0/5 over four runnable tasks with nothing
saying why. All three surfaces carry it now, non-zero only.

Also: the panel's ADO card gained the two settings that had no control at all (parentWorkItem
and the conventions block, including a tag-vocabulary editor), and a browser check now walks the
rendered card against the connector's own key vocabulary so a precondition cannot ship with no path
but a hand edit. tagVocabulary gained an OPEN AXIS spelling (["*"]) for values nobody can
enumerate ahead of time, such as release:2026-08. A phase can be put in an area after creation.
isDark() was extracted to a shared part, one path spelling reaches every reader, and a hierarchy
warning now names which end of a link has no rank instead of always naming the child.