Skip to content

chore - fan-out kit: verbs, skill, branch-naming independence, and modus retirement - #178

Merged
dpuglielli merged 38 commits into
mainfrom
chore/fanout-branch-naming
Sep 9, 2026
Merged

dpuglielli merged 38 commits into
mainfrom
chore/fanout-branch-naming

Conversation

@dpuglielli

Copy link
Copy Markdown
Collaborator

Consolidates several .pipeline runs on this branch. 130 files, ~5.3k insertions / ~11.8k deletions — the deletion count is mostly the modus retirement and boundary-artifact cleanup, not removed functionality.

What's here

Fan-out verbs (fanout-comitatus, orch-selfhost runs) — role, fanout, wait-all, state, settled, fan-in, teardown on herd.js. state derives phase from git refs and stores nothing; settled reads completion from commits rather than transient agent status.

Branch-naming independence (fanout-branch-naming run) — the six hardcoded task/${run} literals collapse into one resolved value. --task-branch defaults to task/<run>, --partition-sep to -, and a / separator is refused at parse time with git's own reason (refs are files, so refs/heads/x and refs/heads/x/api cannot coexist). A run id and a branch name are now independent.

comitatus:fan-out skill — extracted from herdr/SKILL.md, which keeps a pointer rather than a second copy of the runbook. The four recipes, topology, model table, and git state table are now in the skill itself rather than in a run artifact no deployed user has.

Deployed-install fixes (this session's work, 4 commits)

  • The packaged roles were unreachable: provisionStable copied only skills/herdr, so the orientation had no path to hand --roles-dir. Fan-out roles now provision beside the helper and the orientation prints the absolute path.
  • Handles come from the call-sign pool (read from reference/names.md, not copied into JS) instead of impl1/impl2. The part moves to the tab label as <handle>-<role> <glyph>. --handle-prefix is removed with an error naming its replacement.
  • architect.md never mentioned orchestrator.md, so nothing told the one agent that takes both roles to read the second — the run stalled at the red gate. The launcher now verifies the gate and sends the second role.
  • Effort levels set to vendor guidance: xhigh for the opus architect/orchestrator, medium for the codex implementer, high for the prober. Mechanical tier moved off Haiku to sonnet at low.

modus retirement — plugin dropped from the npm workspaces array; modus/package.json and its lockfile entry removed accordingly. Bumped 0.4.0 → 0.5.0.

Validation

  • npm test — all suites green (82 / 48 / 23 / 48 / 65 / 338)
  • npm run validate:plugins — passed
  • claude plugin validate . --strict and claude plugin validate comitatus --strict — both passed (the fan-out review manifest had recorded these as unrun due to ENOTFOUND; they run clean now)
  • quick_validate.py comitatus/skills/fan-out — valid
  • Versions bumped consistently across package, both plugin manifests, marketplace, and lockfile: comitatus 0.9.0 → 0.14.0, modus 0.4.0 → 0.5.0

Reviewer notes

  • comitatus 0.14.0 contains a breaking change (--handle-prefix throws). Pre-1.0, so it ships as minor rather than declaring 1.0.0 — flagging rather than deciding.
  • modus 0.5.0 likewise — removing the workspace and ~10k lines of fixtures is breaking for anything that consumed them.
  • fan-in --wait-handle still defaults to arch, which is never a pool call-sign under the new convention. Every recipe passes it explicitly; the stale default is left for a follow-up.
  • The architect's tab reads sly-arch for the whole run, including the orchestration phase. Either rename at the handoff or define arch as "the pane that owns the task branch" — undecided.
  • herdr/SKILL.md still addresses the reader as being launched under the main-checkout workspace, which is false for every agent fanout places in a partition worktree. Not touched here.

🤖 Generated with Claude Code

dpuglielli and others added 30 commits September 5, 2026 06:46
Removing modus/package.json, scripts/, tests/, prompts/ and evals/ left the
repository in a state where the root test chain did not fail — it recursed.
`test:modus` ran `cd modus && npm test`, npm walked up to the root manifest with
no modus package.json to stop at, and re-ran the root `test` script. CI would
have hung to the job timeout rather than going red.

Root package.json drops the modus workspace and the four modus test scripts.
package-lock.json drops the stale extraneous entry npm would not prune.

validate-plugins now treats package.json as optional: a skills-only plugin has
no dependencies to declare and no test script to run, so the file carried
nothing the host manifests do not already state. Where one exists it is still
checked for name and version agreement. bump-version reads the current version
from the Claude manifest when there is no package.json to read it from.

modus goes to 0.5.0 rather than 1.0.0. It is a breaking change, but the plugin
is pre-1.0 and the replacement guidance has not been compared against a plain
agent with no skill loaded, which is not a thing to signal stability about.

Prose follows the code. The README, both host manifests and the marketplace
entry described a plugin that derives and freezes a definition of done; they now
describe one that judges when requested work is finished, and the README says
plainly that this is an untested hypothesis and that removing modus remains a
legitimate outcome. human-work-item loses three links to the deleted
boundary-prose.md and a section premised on a refusal mechanism that no longer
exists. plugin-evaluation, refinement-loop and the fetch-depth comment in
pr-check say what was removed instead of pointing at it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Files touched:
- comitatus/skills/herdr/scripts/fanin.js

Reviewer: inspect teardown evidence guards and merge conflict preservation.
Files touched: comitatus/skills/herdr/scripts/fanout.js
Reviewer: check recipient-cwd role validation, all-handle preflight, and timeout row preservation.
Files touched: comitatus/skills/herdr/scripts/fanout.js, comitatus/__tests__/fanout.test.js
Reviewer: verify failed delivery rows retain worktree and agent state alongside the error.
Files touched:
- comitatus/skills/herdr/scripts/fanin.js
- comitatus/__tests__/fanin.test.js

Reviewer: verify zero remains valid while negative and non-finite values fail at parse time.
two partitions off the two defects the first fan-out run surfaced: up's
unconditional git fetch, and completion read off agent status.

21 failing assertions. fanin.js carries typed stubs for parseSettled and
settledCmd so the settle tests fail on an assertion rather than a missing
export.
criterion 14: a verb in fanout.js/fanin.js that requires herd.js back must
work when herd.js is the CLI entry, not only as a library require. the
regression test spawns node herd.js so it exercises the path run 1's
library-require tests could not see.

22 failing assertions now.
Files touched: comitatus/skills/herdr/scripts/up.js
Reviewer: confirm remote detection remains after handle pre-flight and preserves the base passed to worktree create.
Files touched:
- comitatus/skills/herdr/scripts/fanin.js
- comitatus/skills/herdr/scripts/herd.js
- comitatus/scripts/herd-setup.js
- comitatus/skills/herdr/SKILL.md

Reviewer: verify blocked precedence and the CommonJS export ordering.
fan-in's --wait-handle gate was bypassed deliberately. The gate waits for
the architect's handle to reach idle|done, but the orchestrator role is
taken by the same agent that wrote the manifest, so the handle to settle
is the caller's own - and a caller is 'working' for the whole duration of
its own command. Satisfying it with an unrelated settled handle would
record a pass that verified nothing, so the merge is a plain sequential
git merge in manifest order instead. Sole occupancy of the task worktree
was confirmed against herdr agent list: oz only. Filed as
.pipeline/runs/orch-selfhost/DEFECT-fanin-selfwait.md.
applied on the task branch after fan-in, because no partition may touch
them: the four version manifests plus the lockfile entry (minor - settled
is a new verb), and the SKILL.md prose for up's conditional fetch, which
both partitions would otherwise have edited for one sentence each.

also records .pipeline/runs/orch-selfhost/DEFECT-fanin-selfwait.md: fan-in
could not be used for its own run, so the merge was a plain sequential git
merge in manifest order with its settle gate bypassed. sole occupancy of
the task worktree was confirmed against herdr agent list first.

comitatus 294/294; repo 745 tests across 19 suites; validate:plugins passes.
…files

the reviewer's only finding was that package-lock.json is modified in the
diff and named in neither partition's file list nor shared_files. correct:
bump-version.js moves comitatus/package.json, so the lock goes stale the
moment it runs, and the architect role's shared_files definition lists
lockfiles first. the omission was the manifest's, not the edit's.

.gitignore is recorded for the same reason - it gained the runbook's
.pipeline/runs/*/diff.patch line after the diff was generated, so it was
not in what the reviewer read.

no code change: the finding was about an unnamed file, not a wrong one.
first row in the file: the trial had no cumulative log until now. width 2,
38 wall min, 0 conflicts, 0 red-gate fails, 0 scope violations, 0 blocked,
1 reviewer hit, 0 escalations. spend columns are ? because this session
cannot read the vendor dashboards, and a zero there would be a false entry
in the one column the spend decision reads.
… three-valued rule

names where the rule already lives rather than asserting it: probe-skill's
exit 2 'could not run' in AGENTS.md, and send's three-valued delivery where
accepted means unknown. 0 says the model cost nothing, ? says nobody looked.

also records why the single reviewer hit landed on the orchestrator's own
file: an implementer sees only its own file list, so a file owned by no
partition is invisible from inside one.
.pipeline covered everything inside a run and had no slot for anything that
outlived one. runs/<id>/ is retained deliberately, but indexed by run id
rather than by subject, so 'has anyone looked at this' meant reading every
run directory. backlog.md is that index; one file per item beside it.

migrated the three items that had no home: the fan-in self-wait gate, the
stale stub headers, and the branch-naming generalization. the other four
beads run orch-selfhost filed restated task.md, two partition commits, and
a manifest criterion - nothing to migrate.

the override is recorded outside the managed markers in both AGENTS.md and
CLAUDE.md, because codex implementers read the first and claude agents the
second, and it was the managed rule addressing every agent with no role
file saying who files that produced three beads for one task at width 2.
one partition, deliberately: fanin.js and fanout.js are disjoint files, but
both halves must agree on the flag names, the default and the composition,
and six independent copies of that logic IS the defect. two partitions would
each write their own and both go green, reproducing the bug at width 2.

23 failing assertions. a throw names nothing, so the not-yet-supported flag
is wrapped to print what came back: {"threw":"unknown flag: --task-branch"}.

the run's own branch is chore/fanout-branch-naming while its run id is
fanout-branch-naming, so the run exercises the decoupling it builds.
Files touched:
- comitatus/skills/herdr/scripts/branch-naming.js
- comitatus/skills/herdr/scripts/fanin.js
- comitatus/skills/herdr/scripts/fanout.js
- comitatus/skills/herdr/scripts/herd.js

Reviewer focus:
- slash separators are refused before branch creation or teardown
- --run continues to name only the pipeline artifact directory
version bump across the four manifests AND package-lock.json, which is named
in this manifest's shared_files because run orch-selfhost omitted it and the
reviewer caught it.

SKILL.md's fan-out section opened by asserting '<id> is the task id and the
branch name both', which is the assumption this run removed, so the paragraph
is rewritten rather than appended to: --run names the artifact directory,
--task-branch names the branch, and the / separator is refused with git's own
reason. kept out of the partition so usage() and SKILL.md are not two
descriptions of one surface written by two agents.

backlog item marked done.
rvw found --partition-sep read raw via args[++i] while every other flag went
through requiredText, so 'api,--partition-sep,--base' returned '--base' and
composed task/r7--baseapi. reproduced independently.

the generic validator is not the fix, which is why the hole was there:
requiredText rejects /^--/, and '--' is a separator parseSettled must accept.
so the separator gets its own rule - punctuation git allows in a ref, nothing
else - and '/' keeps its specific git-reason message, so that check runs first.

carried to all four parsers, not just fanout: teardown composes the name it
hands to git branch -D, which is the destructive path this manifest's risk
notes already called the highest in the change.

7 failing assertions. tests only - the fix is the implementer's.
Files touched:
- comitatus/skills/herdr/scripts/branch-naming.js
- comitatus/skills/herdr/scripts/fanin.js
- comitatus/skills/herdr/scripts/fanout.js

Reviewer focus:
- explicit separators accept only flat -, ., and _ punctuation
- slash keeps its git-specific error before the punctuation rule
- parsers reject missing and flag-shaped separator values
- Remove the agent-work-item boundary draft
- Remove the gh-158 boundary and implementation sketch
- Remove the gh-173 boundary
width 1, 32 wall min, 0 conflicts, 0 red-gate fails, 0 blocked, 1 reviewer
hit, 0 escalations, 2 scope violations.

both violations were operator-initiated mid-run and neither went through the
manifest: 9fbd064 deletes four boundary/ files that exist on main, after the
review had already run, and the fan-out skill split was uncommitted when this
row was written. the row states what it measures - criteria 1-14 verified at
865e048 - and says plainly that everything after that commit is outside it.

path-scoped commit on purpose: another agent is editing this worktree, and a
git add -A there would sweep this row into its commit.
dpuglielli and others added 8 commits September 9, 2026 08:54
operator confirmed 9fbd064 is deliberate and meant to reach main. the row
still counts it in scope violations, because that column measures what
changed outside the owning partition's file list - a fact about the diff,
not a verdict on the change.
- Add the deployable comitatus fan-out skill with packaged pipeline roles
- Reference fan-out from the herdr skill and register it in the skill catalog
- Bump comitatus to 0.13.0 and record the completed session
A deployed user with no repository-local .pipeline/roles had no path to give
`role --roles-dir`: provisionStable copied only skills/herdr, so the orientation
had nothing to name. The fan-out roles now land beside the helper under
~/.claude/comitatus and the orientation prints the absolute path.

Provisioned under its own try/catch, and only named once the directory exists on
disk. A fan-out copy that fails must not cost herdr its stable helper path, and
an orientation naming a directory nothing wrote would point `role` at a missing
file - the failure that line exists to prevent.

provisionInto/provisionStable take a `name` (default herdr), so existing callers
and the .herdr.tmp staging convention are unchanged.

README: the hook never writes to the Codex home - herdr-orient.js says so and a
test pins it. The bullet claiming a ~/.codex/skills/herdr sync was stale prose
describing behavior the hook does not have.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fanout minted impl1/impl2, which is not the comitatus convention: a handle is a
bare call-sign from the pool, globally unique, carrying neither model nor part,
so a member can be relaunched on another model without renaming the identity
everyone addresses it by. The part belongs on the tab label instead, where a
human scanning the sidebar can see which agent is playing what.

- up.js: `role=` joins model= and effort= as a per-agent selector key. It is
  label-only - it never reaches the agent's CLI args, and a test pins that. The
  tab becomes `<handle>-<role> <glyph>`, keeping the glyph's kind signal.
- fanout.js: handles come from the pool, read out of reference/names.md rather
  than copied into JS. That document is what a human claims handles from; a
  second copy here would drift from it silently. A name in use is skipped; an
  exhausted pool throws before the first worktree, as a collision used to.
- --handle-prefix is removed, not aliased onto --role-tag. It named the handle,
  and quietly repointing it at a tab label would be the wrong meaning. It throws
  a message naming its replacement.
- A bare --selector is a model id, so the role tag is added after normalising to
  key=value form; appending to a bare model would make one unparseable id.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The skill described the verbs but not how to run a task with them: the recipes
lived only in .pipeline/runs/orch-selfhost/fan-out-trial.md, a run artifact no
deployed user has. Carried across inline - the four recipes, the topology table
and its one-writer-per-tree rule, the model table, the git state table, and the
operational gotchas. Left behind the trial scaffolding (log.md columns, the
decision table, "do not build yet"), which is about running a trial rather than
doing a fan-out.

Also:

- Which position you hold. The skill is read by more than one agent in a run, so
  it no longer asserts who the reader is - a reader identifies itself by whether
  it was sent a role file, and the launcher's constraints are third-person so an
  orchestrator cannot pick them up by accident.
- The launcher's boundary is now explicit: it sets the run up, then reads,
  decides and approves, and writes to no task or partition tree. Previously the
  skill said the opposite ("you by hand"), and no role file bounded it.
- Branch names are placeholders. A skill whose subject is --task-branch should
  not write task/<id> fifteen times as though it were law; the default is
  documented as a default.
- architect.md ends at the red gate and says so. It never mentioned the
  orchestrator, so nothing told the one agent that takes both roles to read the
  second file - the run stalled there. The launcher verifies the gate and sends
  orchestrator.md; self-chaining would have the architect grade its own failing
  tests.
- Effort levels follow vendor guidance: xhigh for the opus architect and
  orchestrator (Anthropic's level for coding and agentic work, and Claude Code's
  own default), medium for the codex implementer (OpenAI's level for routine
  development, which names "fixing a failing test"), high for the prober
  (OpenAI escalates for difficult debugging, the debug recipe's premise).
- The mechanical tier moves from Haiku to sonnet at low effort: one fewer model
  in the run, and 1M context where a directory summary could exceed 200K.

.pipeline/roles/ holds copies rather than symlinks, so both are updated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Minor: new hook behavior (fan-out roles provisioned to the stable home, a new
orientation line) and a new `role=` selector. --handle-prefix is removed, which
is breaking, but comitatus is pre-1.0 and this is not a 1.0 declaration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dpuglielli
dpuglielli merged commit 022572a into main Sep 9, 2026
1 check 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