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
3 changes: 2 additions & 1 deletion .oss.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@
"latest": true,
"triggers": {
"merged_prs": 10,
"soak_hours": 48
"soak_hours": 48,
"triage_after_release": true
}
}
}
15 changes: 13 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ phase's argument: the incident behind a rule, the measurement, the approach trie
| `skills/manager/SKILL.md` | 41,739 B | 44,800 B |
| `skills/manager/phases/dispatch.md` | 53,938 B | 57,400 B |
| `skills/manager/phases/handback.md` | 17,249 B | 18,000 B |
| `skills/manager/phases/accounting.md` | 22,987 B | 23,000 B |
| `skills/manager/phases/accounting.md` | 23,564 B | 25,900 B |
| `skills/manager/phases/tick-order.md` | 34,266 B | 36,000 B |
| `skills/manager/phases/release.md` | 10,295 B | 10,900 B |
| `skills/manager/phases/review.md` | 11,390 B | 11,400 B |
Expand Down Expand Up @@ -633,6 +633,17 @@ Ceiling unchanged; comfortably under it.
`curate_route_threshold` -- the config change closing #1303, not a prose expansion of its own.
Ceiling unchanged; still under it.

**`accounting.md`'s budget was raised again for #1386**: 22,987 B became 23,564 B, past the 23,000 B
ceiling by 564 B. The Cadence section's own triage paragraph used to say the last-triaged read is
enforced but consumed by nothing -- true at the time, and the gap this issue closes: a sub-manager
cannot act on it itself, since it dies with its own context at the end of its tick and cannot count
ticks or releases across a spawn boundary. `scripts/triage_trigger.py` gives the scheduler -- the one
actor spanning ticks -- a computed verdict (`due` / `not-due` / `could-not-tell`, mirroring
`release_trigger.py`'s own three-state shape) to read at the `RELEASE: released` handback in
`commands/tick.md`, which is where the paragraph now points rather than restating a second copy of
when the trigger fires. Nothing already in the file argued that point, so nothing was cut to make
room; the ceiling moved to 25,900 B, ~10% headroom over the new size.

**`merge.md`'s budget was raised for #1007**, which closes the race a tick's own cleanup
guard was overridden through: 10,012 B measured became 13,198 B. The new bullet states two
things together -- re-read the tree's HEAD immediately before a force-remove and refuse the
Expand Down Expand Up @@ -809,7 +820,7 @@ spendable again without anybody choosing to.

| file | measured (baseline) | budget |
| --- | --- | --- |
| `commands/tick.md` | 20,177 B | 22,200 B |
| `commands/tick.md` | 21,917 B | 22,200 B |

**Raised for #1041's self-review round: 17,899 B became 18,276 B**, past the 17,900 B ceiling by
1 B of prior headroom. A reviewer spawn caught this file still telling the scheduler a releaser
Expand Down
11 changes: 11 additions & 0 deletions changelog.d/1386.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
- `scripts/triage_trigger.py` computes whether a post-release triage sweep is due instead of leaving
the rule in `skills/manager/phases/accounting.md`'s Cadence section unread by anything that could
act on it (#1386): `due` / `not-due` / `could-not-tell`. A release has landed and either no sweep
was ever recorded, or the most recent one predates the most recent tag -- that is `due`, computed
by comparing the tag's own commit date against `scripts/oss_state.py --last-triage`'s existing
three-state answer, never a tick count a sub-manager cannot keep across its own spawns. An
unreadable tag date or state file is `could-not-tell`, never folded into `not-due` -- an absence
this call could not observe is not the same fact as one it looked for and did not find. The
scheduler reads it at the `RELEASE: released` handback in `commands/tick.md` and dispatches
`oss:triager` when it fires; the repository opts in with `release.triggers.triage_after_release`
in `.oss.json`, the same "absent means not wanted" rule `curate_route_threshold` already uses.
29 changes: 29 additions & 0 deletions commands/tick.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,35 @@ Seven answers, not three, and only one of them is the ordinary case:
same two facts a `paused` sub-manager hands back below — wait on the named observable and resume
the same releaser with `SendMessage`, never a fresh spawn: a fresh one re-derives from gate 1
instead of the gate it actually paused at.

**On `RELEASE: released`, check the triage cadence before spawning the next sub-manager (#1386).**
A sub-manager cannot run this check itself — it dies with its own context at the end of its tick
and cannot count ticks across spawns — so it falls to this session, the one actor spanning ticks.
Run:

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/triage_trigger.py" --repo <clone>
```

Three states, and only one of them is silent: `not-due` needs nothing further. `due` means
dispatch `oss:triager` for one sweep before the next sub-manager — the same "fresh spawn, nothing
handed to it beyond the spawn itself" shape as the releaser above, since the triager re-derives the
board from the tracker rather than from anything this session already holds. `could-not-tell` is
not `not-due`: say so, and either fix what the receipt names (an unreadable state file, an
unreadable tag) or arm a short wakeup and re-check next tick — never proceed as though the sweep
were unnecessary because this call could not see it.

**Record the sweep the moment the triager reports back, or `due` never clears (#1386).** The
triager never writes to the state file itself -- it is Bash and TodoWrite only, and its report is
labels applied plus proposed clusters, nothing about cadence. Without this step the next
`RELEASE: released` reads the same stale `--last-triage` and dispatches a triager again, forever,
on every release after this one. This session runs it, once the triager's own final message
confirms the sweep actually ran (not a refusal, not an empty return):

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/oss_state.py" <state_file> --triage-recorded "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
```

- **`blocked`** — the `BLOCKER:` line names exactly what and on what. Act on it, or arm a wakeup that
names it — the same naming step 7 below always asked of a tick that ends blocked.
- **`paused`** — the `WAIT-DISPATCH:` and `WAIT-OBSERVABLE:` lines name what this tick set in motion
Expand Down
20 changes: 11 additions & 9 deletions docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ surface.
Cadence is config, not memory. `.oss.json` carries the thresholds the loop
tests itself against — how many merged pull requests and how many hours of soak
before a release is due, how full `trap.d/` has to get before a curation pass is
routed. Read them from the file; they differ per repo and this page must not
name their values.
routed, whether a triage sweep is due once a release lands. Read them from the
file; they differ per repo and this page must not name their values.

A cadence step with a threshold in config is one the loop reaches by itself. A
cadence step written only in prose is one somebody has to remember.
Expand Down Expand Up @@ -198,13 +198,15 @@ Two consequences worth stating on their own:

## What is not true yet

**The loop does not reach every step it is told to reach.** A release has a
trigger in config and fires by itself. Curation has one. The triage sweep that
the cadence rule says follows every release has none — the rule lives in a phase
file whose only reader is a sub-manager, which is discarded before the next
release and holds no authority over what the scheduler does next, and no file on
the release path mentions triage at all. The recorder was built and the consumer
was not. The first sweep this loop ever recorded was run by hand.
**The loop does not reach every step it is told to reach — the triage sweep is
the fixed instance, not the open one.** A release has a trigger in config and
fires by itself. Curation has one. The triage sweep used to have none — the rule
lived in a phase file whose only reader is a sub-manager, discarded before the
next release and holding no authority over what the scheduler does next, and no
file on the release path mentioned triage at all. The recorder was built and the
consumer was not: the first sweep this loop ever recorded was run by hand, across
31 tagged releases (#1386). `scripts/triage_trigger.py` is that consumer now,
read by the scheduler at the `RELEASE: released` handback in `commands/tick.md`.

That is the shape to watch for, and it is worth more than the instance: **every
individual file was correct and the loop still did not do the thing.** No
Expand Down
2 changes: 1 addition & 1 deletion scripts/command_budgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
# outcomes (refusal, reply, unresolved) explicitly. Nothing already
# in the file argued either point, so nothing was cut to make room;
# the ceiling moves to 22200 B, ~10% headroom over the new size.
"commands/tick.md": (20177, 22200),
"commands/tick.md": (21917, 22200),
}


Expand Down
18 changes: 17 additions & 1 deletion scripts/oss_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@
MERGE_METHODS = {"squash", "merge", "rebase"}
TRIGGER_KEYS = {"merged_prs", "soak_hours"}

#: #1386: the one trigger key in this set that is a boolean, not a count -- the
#: post-release triage sweep has no meaningful threshold to tune (see
#: scripts/triage_trigger.py's own docstring), so it is validated separately
#: below rather than folded into the "expected a number" arm every other key
#: in this set shares.
TRIGGER_BOOLEAN_KEYS = {"triage_after_release"}

# #478: whether this repository has granted the loop authority to tag and publish a
# release without stopping. Per-repository -- CLAUDE.md's governing rule -- because the
# grant used to live only in a per-machine memory file the skill cannot read and a second
Expand Down Expand Up @@ -2405,7 +2412,8 @@ def _validate_release(release):
if not isinstance(triggers, dict):
problems.append("release.triggers: expected an object")
else:
for key in sorted(set(triggers) - TRIGGER_KEYS):
known_trigger_keys = TRIGGER_KEYS | TRIGGER_BOOLEAN_KEYS
for key in sorted(set(triggers) - known_trigger_keys):
problems.extend(
_unknown_key_problems(
key, "release.triggers.{}: unknown key".format(key)
Expand All @@ -2419,6 +2427,14 @@ def _validate_release(release):
key, value
)
)
for key in sorted(TRIGGER_BOOLEAN_KEYS & set(triggers)):
value = triggers[key]
if value is not None and not isinstance(value, bool):
problems.append(
"release.triggers.{}: expected true or false, got {!r}".format(
key, value
)
)

return problems

Expand Down
16 changes: 14 additions & 2 deletions scripts/skill_phases.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,8 +498,20 @@
# Cadence naming the #1155 threshold route now activated for
# curate_route_threshold. Comfortably under the 23000 B ceiling;
# ceiling unchanged.
22987,
23000,
# Raised for #1386: 22987 B became 23564 B, past the 23000 B ceiling
# by 564 B. The Cadence section's own triage paragraph used to state
# that the last-triaged read is enforced but nothing consumes it --
# true when written, and the gap #1386 closes: a sub-manager cannot
# act on the reading itself (it dies with its own context at the end
# of its tick and cannot count ticks or releases across spawns), so
# scripts/triage_trigger.py is read by the scheduler instead, at the
# `RELEASE: released` handback in commands/tick.md. The paragraph now
# points there rather than restating a second copy of when the
# trigger fires. Nothing already in the file argued that point, so
# nothing was cut to make room; ceiling moved to 25900 B, ~10%
# headroom over the new size.
23564,
25900,
"closing a tick: the cohort freeze, the intake ratio, and what a tick costs to carry",
),
# New for #1037: `commands/tick.md` used to inject its own numbered steps 1-6
Expand Down
Loading