bugyi-chops is Bryan Bugyi's community SASE plugin for
scheduled Axe jobs that watch repository health, submit guarded release PRs, and
propose maintenance work. It supplies two console scripts:
| Script | Responsibility |
|---|---|
bugyi_chop_ci_watch |
Notify on CI failures and merge explicitly enabled, guarded release-please PRs |
bugyi_chop_toobig_split |
One sequential condition-gated %auto #split_file:<path> agent per oversized Python file, routed through @medium |
The scripts never launch agents themselves. bugyi_chop_toobig_split scans and
assembles prompts, then uses the public sase.chops SDK to atomically write a
validated result document; Axe owns guard and trigger evaluation, keyed-proposal
deduplication, workspace allocation, proposal launches, and the final action lifecycle
for those proposals.
bugyi_chop_ci_watch is deliberately narrower. It never creates gates, launch
requests, repair prompts, or Axe proposals. Its only mutation outside its own state and
report files is a guarded gh pr merge --merge|--squash|--rebase --match-head-commit
(the flag follows the repository's resolved vars.merge_method, default merge) for
an eligible release-please PR when vars.merge_enabled is true and
SASE_CHOP_DRY_RUN=0 is explicitly present. Missing or true dry-run context suppresses
merging and notifications.
Install the published package into the same managed environment as SASE:
sase plugin install bugyi-chops-g/--git forces a built VCS snapshot install from this repository instead of the
published index distribution; it is not a development install. For local development
against this repository, see Development and releases
below.
All scripts require Python 3.12 or newer and sase>=0.17.0,<0.18. SASE 0.17.0 is the
first compatible release series for repository-scoped release gates. The package
also installs the toobig scanner used by bugyi_chop_toobig_split.
Axe invokes a configured script as <script> --context <context.json> and supplies
SASE_CHOP_RESULT_FILE. A successful script writes schema-versioned JSON like:
{
"schema_version": 1,
"status": "ok",
"summary": "toobig_split: files=1 proposals=1 skipped=0",
"counters": {"files": 1, "proposals": 1, "skipped": 0},
"proposed_launches": [
{
"id": "split_file-src-large_py",
"prompt": "%if::\n```bash\npath=src/large.py\n...\n```\n%auto #split_file:src/large.py",
"workspace": "gh:sase-org/sase",
"model": "@medium"
}
]
}status is ok, no_op, or check_error. Axe validates the entire result before it
launches anything, injects the workspace/name/tribe scaffold, honors wait_on
dependencies, filters duplicate proposals, and tracks linked agents through
action_succeeded or action_failed. The prompts here may use inline xprompts such
as #pr and #split_file; they never use forbidden standalone #!workflow
references. This proposal contract governs bugyi_chop_toobig_split.
bugyi_chop_ci_watch always returns an empty proposed_launches list.
bugyi_chop_ci_watch sweeps the configured repository allowlist through actstat,
reconciles each settled commit with the current default branch through bounded GitHub
queries, and publishes a combined CI WATCH report. Red observations keep typed job
evidence: workflow name, job name, conclusion, direct job/run URL, failing steps,
failing SHA, and current-head SHA when a stale settled failure is still actionable
while newer HEAD checks are unsettled.
Failure notifications are incident-combination based. The state file
ci_watch_state.json tracks per-repository failing-job fingerprints as the delta
source, plus one active incident covering the announced failing-repo set. Concurrent
failures produce a single sase notify create row (-k ci-failure/<repos> -p …);
material fingerprint churn, recoveries, and re-failures within that set add a quiet
sase notify +1 note; a repo the current notification does not cover going red
creates a new row and supersedes the previous one (-S). All-green clears the
incident with a resolution +1 so a later recurrence is announced again. Failed
deliveries remain unsent and are retried on the next live tick. If the installed
sase CLI does not yet understand -k / notify +1, the chop falls back to the
legacy per-repository sase notify create flow for that tick.
Release handling is release-please only. Configure vars.release_repositories as a
list of repositories from vars.repos; generator mappings are rejected and other
release branch families are ignored. A release PR is mergeable only when all guards
pass: exactly one release-please candidate for the repository, configured default base,
non-draft, mergeable and clean, non-empty fully green check rollup, a green current
default branch, idle release-please/publish workflow, deterministic dependency order,
max_merges_per_tick, explicit live mode, a final PR/default-branch reread, repository
metadata allowing the chosen merge strategy, and --match-head-commit race protection.
When GitHub reports the configured strategy disabled for that repository, the chop
records merge_method_not_allowed and does not call gh pr merge.
By default "a green current default branch" means the actstat sweep classified it green.
Configuring vars.gating_workflows (a list of workflow names, empty by default) switches
that guard to explicit HEAD evidence instead: every named workflow must have a completed,
green run at the exact current default-branch HEAD, distinguishing a workflow that never
ran (gating_workflow_missing), is still running (gating_workflow_in_flight), and ran
red (gating_workflow_red). This makes release eligibility independent of the broader
actstat classification, so unrelated flaky or red workflows outside the allowlist no
longer block a release. Configuring vars.heavy_workflows (also empty by default) adds a
freshness requirement on top: every named heavy workflow's newest completed run on the
default branch must be green and no older than vars.heavy_max_age_hours (default 6),
reported as heavy_lane_not_green or heavy_lane_stale otherwise. Both allowlists are
re-checked, from a freshly fetched HEAD, during the final pre-merge reread. Leaving both
lists empty preserves the pre-existing actstat-only behavior. vars.merge_method
(merge, squash, or rebase; default merge) selects the gh pr merge flag.
vars.merge_method, vars.gating_workflows, vars.heavy_workflows, and
vars.heavy_max_age_hours each accept either the flat form shown above or a mapping.
Mapping keys are release repository names in owner/repo form plus the reserved
default key. Resolution is explicit repository entry, then mapping default, then the
built-in default (merge, empty workflow lists, and 6 hours). Unknown repositories,
non-string keys, malformed values, and invalid merge methods fail closed before the
chop starts work.
Successful merge submissions are written durably with notification_sent: false before
any SASE notification is attempted. The release notification is marked delivered only
after sase notify create succeeds, so a later tick retries an unsent release notice.
Failure notifications use sase notify create with a dedup key (and sase notify +1
for later corroboration) on a current SASE, with the same retry rule.
If report publication fails, inline notifications still include the full evidence but
omit the ViewReport action and the chop returns check_error so the failure is
visible.
Minimal configuration:
vars:
actstat_bin: /home/bryan/.cargo/bin/actstat
gh_bin: gh
sase_bin: sase
repos: [sase-org/sase, sase-org/sase-core, sase-org/sase-telegram]
release_repositories: [sase-org/sase-telegram, sase-org/sase]
merge_order: [sase-telegram, sase]
max_merges_per_tick: 1
merge_enabled: trueConfiguration adding an explicit merge method and release gates:
vars:
# ... same as above ...
merge_method: squash
gating_workflows: [CI]
heavy_workflows: [E2E]
heavy_max_age_hours: 6Configuration with repository-specific release settings:
vars:
# ... same as above ...
merge_method:
default: squash
sase-org/sase: merge
gating_workflows:
default: []
sase-org/sase: ["Master Gate"]
heavy_workflows:
default: []
sase-org/sase: ["Full CI"]
heavy_max_age_hours:
default: 6bugyi_chop_toobig_split runs toobig --files-only for each configured tree,
normalizes and de-duplicates its paths, and emits one proposal per file.
The scanner contract is fail-closed around two healthy outcomes. Exit 0 is a
completed scan whose stdout listing, if any, contains only informational or
warning-level paths. Exit 1 with one or more listed paths is also a completed
scan: toobig uses that exit code for a hard-limit hit and still writes the
matching paths to stdout, which this chop consumes as actionable findings.
Malformed invocations, missing trees, and other filesystem errors reuse exit
1 but produce no path payload; those empty exit-1 results, and every other
nonzero status, remain a typed check_error.
Each proposal has:
- the shared
toobig-@clan template, with a keyed basename member template (<basename>.{@<path-digest>}); - the same Rich
clan_summarymetadata, describing the mission, file and scan-root counts, configured limits, and sequential queue; - structured
model: "@medium", which Axe renders as one%model:@mediumdirective alongside%auto #split_file:<path>; - one
%if::Bash fence followed by%auto #split_file:<path>as its prompt; - no proposal
dedupe_key; every scheduled scan may reconsider files that remain oversized; wait_onpointing to the prior file, preserving sequential workspace allocation.
The %if fence runs after the proposal's sequential wait and immediately before typed
admission. SASE briefly claims a temporary numbered workspace to evaluate it — never a
runner, agent identity, or model request — and releases that claim once the verdict is
settled, whether the proposal is eligible or skipped. This is why a queued condition
still sees a file an earlier agent already split even when the chop's own checkout is
stale: the leased checkout synchronizes from the configured upstream before %if runs.
The proposal skips when the target file is gone or has dropped below the configured
floor (min(limits), 700 lines in the default configuration) in that freshly leased
checkout. A read/count failure for an existing file is a visible condition error. This
requires SASE's typed_launch_units flag and a compatible SASE 0.17.x runtime; older
SASE 0.13.x runtimes reject these directives before model dispatch.
The structured @medium field is the only model source. It selects SASE's
configurable, load-balanced alias pool rather than pinning a concrete
provider/model in the prompt body, so operators retune @medium centrally
without a per-chop model variable.
The repeated summary metadata is deliberate: after conditional admission or any other proposal filtering, any surviving proposal can safely become the clan declarer. Axe allocates the concrete clan template once per actionable scan and emits the summary exactly once on that declaration; clan joiners do not redeclare it. In ACE the default scan reads as:
◆ TOOBIG SPLIT · 3 FILES
MISSION
Decompose oversized Python modules into focused, reviewable units
without changing behavior.
2 scan roots · limits 1,000 / 850 / 700 lines · sequential queue
Concrete agent names look like toobig-<token>.<basename>.<token>, for example
toobig-3j.test_query_profile.0. Two files that share a basename keep the same
readable stem and allocate distinct member tokens (.0, .1) inside that clan.
All proposals from that scan belong to the same clan generation, while later scans
can allocate a new one. The plugin authors only the toobig-@ clan template, a
keyed basename member template, and proposal metadata; it never inspects live
agents or chooses concrete tokens. Axe alone allocates the clan and member
tokens, injects declaration/join directives, and launches agents.
The script deliberately has no flock, no sase agent list, and no sase run. Those
responsibilities now belong to Axe:
axe:
lumberjacks:
maintenance:
description: |-
Propose split-file maintenance for oversized Python files once a minute
Runs every 60 seconds so the chop can notice when its hourly `run_every` window opens. Use this lane for
low-cost maintenance proposals guarded by AXE, not for commit-threshold audits.
The `toobig-` clan inhibit guard prevents a new split-file scan while an earlier split swarm is still active.
interval: 60
chops:
toobig_split:
script: bugyi_chop_toobig_split
description: |-
Split oversized Python files in sase
Runs `bugyi_chop_toobig_split` once per hour for the `sase` target and scans the configured `src` and
`tests` trees with the `1000`, `850`, and `700` line limits.
Each actionable scan emits sequential `toobig-` clan proposals. Every member rechecks the configured
700-line floor through `%if` after its wait and before admission, so stale files skip without allocating an
agent while still-oversized files launch normally. The chop sets the structured proposal model to `@medium`;
Axe emits `%model:@medium` and SASE consumes the alias pool at each real invocation.
run_every: 60m
inhibit_if:
agent_clan: {name_prefix: toobig-}
for_each:
source: projects
names: [sase]
vars:
trees: [src, tests]
limits: [1000, 850, 700]The projects target source supplies both target.workspace and
target.workspace_dir. For a literal target, provide the same fields. Compatibility
environment variables are also accepted: SASE_TOOBIG_SPLIT_PROJECT,
SASE_TOOBIG_SPLIT_REPO_ROOT, SASE_TOOBIG_SPLIT_LAUNCH_REF,
SASE_TOOBIG_SPLIT_TREES, SASE_TOOBIG_SPLIT_LIMITS, and
SASE_TOOBIG_SPLIT_TOOBIG.
Preview configured chops without side effects:
sase axe chop run 'ci_watch' -L ci_watch --dry-run --chop-verbose
sase axe chop run 'toobig_split[sase]' -L maintenance --dry-run --chop-verbose
# Short flags:
sase axe chop run 'ci_watch' -L ci_watch -n -V
sase axe chop run 'toobig_split[sase]' -L maintenance -n -V-V sets SASE_CHOP_VERBOSE and prints scanner commands and target diagnostics.
Every actual invocation also emits a compact summary line with bounded integer
counters and an explicit reason for no-op/error outcomes.
For lower-level diagnosis, reuse a context JSON written by Axe and invoke a script
directly. ci_watch requires SASE_CHOP_DRY_RUN=0 before it can notify or submit a
merge; all other direct invocations render decisions only:
SASE_CHOP_RESULT_FILE=/tmp/ci-watch-result.json \
bugyi_chop_ci_watch --context /path/to/context.json --verbose
jq . /tmp/ci-watch-result.json
SASE_CHOP_RESULT_FILE=/tmp/toobig-result.json \
bugyi_chop_toobig_split --context /path/to/context.json --verbose
jq . /tmp/toobig-result.jsonjust install
just checkWhen developing before the matching SASE release is available on PyPI, first run
just install in a current SASE source checkout (with its linked sase-core), then
reuse that environment:
BUGYI_CHOPS_VENV_BIN=/path/to/sase/.venv/bin just install
BUGYI_CHOPS_VENV_BIN=/path/to/sase/.venv/bin just checkjust check runs formatting/lint/type checks, pytest with branch coverage, builds the
wheel and source distribution, and validates both artifacts with Twine. Pull requests
and pushes to master run the same checks on Python 3.12 and 3.13.
Releases are tag-driven. Set the package version, push the matching v<version> tag,
and the publish workflow rebuilds and tests the package before uploading to PyPI via
GitHub trusted publishing. No long-lived PyPI token is stored in the repository.