Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"name": "work-system",
"source": "./plugins/work-system",
"description": "Generic task and worktree workflow system for Claude Code. Keep tasks as GitHub issues or local markdown files, run them in isolated git worktrees with a choice of worker agent (Claude, codex, grok, kimi, or a PATH-detected cc-harness agent), and track progress across the define/kickoff/continue/status/close lifecycle.",
"version": "1.28.0"
"version": "1.28.1"
},
{
"name": "pr-flow",
Expand Down
55 changes: 43 additions & 12 deletions .claude/knowledge/features/worker-autonomy-mandate.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
title: "Worker Autonomy Mandate (MANDATE.md)"
createdAt: 2026-09-07
updatedAt: 2026-10-20
updatedAt: 2026-10-22
createdFrom: "branch: task/extend-worker-autonomy"
updatedFrom: "branch: task/keep-review-coverage-across-version-rebase"
updatedFrom: "branch: task/harden-mandate-diff-id-and-round-lock"
pluginVersion: 1.9.0
prime: false
---
Expand Down Expand Up @@ -243,6 +243,17 @@ of reviews. The lock is eight lines of `mkdir`. Worth re-reading a standing
rejection whenever the surrounding design moves under it — the argument was sound
when first made and quietly stopped being true.

Its first version had a TOCTOU of its own (CI flake, rc=4, 1.28.1): after a
failed `mkdir` it tested `[ ! -d "$lock" ]` to tell "cannot create" from
contention — but the holder can release the lock in between, so contention
read as a read-only tree. Decide from `mkdir`'s own error (`LC_ALL=C`, "File
exists" = contention) instead of from the lock's state a moment later.
Reproduce such races by running many copies of the concurrent test at once
(16/72 runs failed); pin the fix with a PATH `mkdir` shim that fails once with
"File exists" and no lock left behind. The pid is written aside and renamed
in, so `lock_is_stale` never reads a partial pid; a young lock without one is
never stale, which covers the `mkdir`-to-pid gap.

## Indentation is structure too

The same pass that taught `has-bot` to skip block scalars found the mandate
Expand Down Expand Up @@ -402,12 +413,29 @@ covers. Why it is built this way:
the global git rules fold fixes into earlier commits, so SHAs move. `git
patch-id` was rejected in review: it ignores whitespace and (over `-U0`)
position, so a de-indented `delete_all()` read as covered. The id hashes the
`-U3` diff against the newest merge-base with local/origin main, keeping
context, function context and whitespace and dropping only the `@@` line
numbers and `index` lines (keeping numbers would break coverage on every
rebase over a busy main); an edit on main inside a lane hunk's context
changes it (fail-closed re-review). Accepted residual: one change moved
between two blocks with identical context and enclosing function.
diff against the newest merge-base with local/origin main, keeping
function context and whitespace and dropping the `@@` line numbers and
`index` lines (keeping numbers would break coverage on every rebase over a
busy main). Since 1.28.1 it is a `-U0` diff: 1.28's `-U3` carried three of
MAIN's lines around each hunk, so #89's rebase over neighbouring CLAUDE.md
edits asked to re-review lines the lane never touched. `-U0` alone is enough
(stripping context after `-U3` would keep `-U3`'s hunk merging, which main
insertions between two close lane hunks change); inter-hunk context is
pinned to 0 so a user's config cannot pull main's lines back in. A hunk's
*place* = file, hunk order, and git's function-context line — the only
position left once line numbers go (they move on any main insertion above).
Trap: with no diff driver the function context is the nearest line above
starting with a letter, `_` or `$` — in a top-level shell script that is
often the adjacent line, so a main edit to it still fails closed. Accepted
residuals (named in review round 1): a lane line moved within one header's
region, past no other lane hunk, keeps the id (e.g. above a guard in the
same function; `-U3` caught that); and a main edit that changes what a
lane hunk means — deleting a guard right above it — keeps the id too. The
latter is the point of the change, and such a rebase usually stops on a
conflict a person resolves. Older records (`context` = 1.28's masked `-U3`,
`legacy` = unmasked `-U3`) are matched exactly by `lane_compat_ids`; the
tests recompute both from the old algorithms rather than through
mandate.sh, so a change to its forms cannot move the reference too.
- **Version values are masked, nothing else** (`[mandate] version_paths`). #85
hit the gap: a rebase moved the lane's bump 1.25.0 → 1.26.0, the id changed,
and the only way through was a bare `gh pr merge` around the gate — worse than
Expand All @@ -421,10 +449,13 @@ covers. Why it is built this way:
masked, and every other version value is prefixed with `=` first, so a lane
writing the literal mask token `"*"` cannot pass as a masked renumber.
Accepted residual: a deeper-nested `"version"` in those files is masked too.
Old unmasked records: `covered` also hashes the legacy form and accepts an
exact match (`diff_id_form=legacy`) — it can never cover more than the old id
did; with no manifest touched both forms are byte-identical (a test pins the
pre-masking pipeline's hash).
Old records: `covered` also hashes the older forms and accepts an exact
match — `context` (1.28.0's masked `-U3`) or `legacy` (unmasked `-U3`); it
can never cover more than the old id did. With no manifest touched the two
are the same text, so a 1.23-1.27 record then reports `context` (the label
names the form, not the version). The `-U3` forms keep the old argv,
unpinned: pinning the inter-hunk context there broke records made under a
user's non-default diff config (review round 1).
- **A user ack, never a worker one** (`delta` + `acknowledged`). For a small
real delta, `/merge`/`/land` show `delta` (a diff of the reviewed lane diff
against the head's — so unrelated main edits show nothing) and ask. The ack
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ entries are grouped per plugin, newest first.

## work-system

### 1.28.1 — 2026-10-22
- The merge gate's lane diff id hashes only the lane's own lines: a `-U0` diff, whose place is the file, the hunk order and git's function context. A rebase over main edits next to a lane hunk keeps `covered=yes` (#89 asked to re-review lines the lane never touched). A lane line that changes or moves (under another header, or past another lane hunk) still flips it. A main edit to the hunk's function-context line still asks for a review. Accepted residuals: a lane line moved within one function past no other lane hunk keeps the id, and so does a main edit that changes what a lane hunk means (deleting a guard right above it). Reviews recorded by 1.28.0 (`diff_id_form=context`) or earlier (`legacy`; `context` when no manifest is touched, the same text) keep matching their unchanged lane, also under a non-default diff config, and `delta` still shows context. `delta`'s `diff_form=masked` is now `context-free` (or `context`/`legacy` for an older record).
- `mandate.sh round` (and the other record writers) no longer exit 4 when a concurrent booker releases the lock between a failed `mkdir` and the check: only a `mkdir` error other than "File exists" is a lock that cannot be created. The lock's pid is written atomically, so a partial pid never marks a live lock stale, and a file or symlink at the lock path exits 4 at once with a hint instead of waiting 5 s and blaming another process.

### 1.28.0 — 2026-10-21
- The merge gate's lane diff id masks JSON `"version"` string values on `[mandate] version_paths` (`.work-system.toml` on the main branch; default: every `.claude-plugin/plugin.json` and the root `marketplace.json`; `[]` turns it off), so a rebase that only renumbers a plugin version keeps `covered=yes` (strict SemVer values only — any other value, the literal mask token included, still counts; a renumbered CHANGELOG heading is a delta to acknowledge). Only the policy committed on the main branch counts (origin's ref when there is one, never a working tree or another checked-out branch); a committed value that cannot be read masks nothing. Any other change in those files still needs a review. A review recorded before the masking keeps matching its unchanged lane (`diff_id_form=legacy`). A lane diff holding NUL bytes gets no id (never covered): bash drops NULs, so two contents would share one.
- New `mandate.sh delta [--head <rev>] [--max-lines <n>]` shows what changed since the review on record (a bounded diff of the two lane diffs), and `acknowledged --head <rev> --ack-id <id>` records the user's acknowledgment of that delta as `acked_diff`, `acked_by` and `acked_at`. `delta` shows versions unmasked and prints `ack_id=` (a hash of the shown text) only for a complete, exact, binary-free, readable delta, and the ack is refused once the diff is no longer the one shown. `covered` answers `via=user-ack` for exactly that diff, and a new review clears it. `no-review` stays unackable, and so does a delta measured from an uncommitted review (`approximate-baseline`); a legacy review's delta is shown unmasked. `/land` asks `[a]cknowledge / [s]top` on `changed-since-review`; only the user's own answer counts, and `/continue` never acknowledges.
Expand Down
2 changes: 1 addition & 1 deletion plugins/work-system/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "work-system",
"description": "Generic task and worktree workflow system for Claude Code. Keep tasks as GitHub issues or local markdown files, run them in isolated git worktrees with a choice of worker agent (Claude, codex, grok, kimi, or a PATH-detected cc-harness agent), and track progress across the define/kickoff/continue/status/close lifecycle.",
"version": "1.28.0",
"version": "1.28.1",
"author": {
"name": "gering"
},
Expand Down
10 changes: 7 additions & 3 deletions plugins/work-system/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,9 +427,13 @@ A lane is reviewed locally (`/swarm:review`, `/cycle`), not through GitHub appro
so `/land` and `/pr-flow:merge` gate the merge on that review instead. A *finished*
review records what it saw: `/swarm:review` (the whole lane, committed tree, or
`--pr`) and `/cycle` call `mandate.sh reviewed --head` themselves, storing `reviewed_head` and the lane's diff id — a
hash of the lane's diff against its main branch with hunk line numbers dropped (function context kept), so a
rebase or squash keeps it while any change to the code, whitespace included, does
not. Booking a round (`round`) only spends budget; it records nothing. `mandate.sh
hash of the lane's own changed lines against its main branch (a `-U0` diff; no
line numbers, no context lines from main, function context kept), so a rebase or
squash keeps it, main edits right next to a lane hunk included, while any change
to the lane's code, whitespace included, does not. Moving a lane line under
another function-context header or past another lane hunk changes it too;
moving it within one function, or a main edit that changes what a lane hunk
means (a guard deleted right above it), does not. Booking a round (`round`) only spends budget; it records nothing. `mandate.sh
fixed --after <rev>` records the state after review `<rev>`'s agreed fixes — once per
review, and refused (`reason=other-review`) when `<rev>` is not the review on record.
swarm records only a review of a committed, clean tree, so commit before reviewing.
Expand Down
Loading
Loading