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
12 changes: 7 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
# `test/factory-tier.bats` stubs `gh` and runs the real jq filter — a case per
# clause, each written so that deleting the clause fails it.
#
# `factory-watchdog` is the same argument one layer down, and its silence is
# harder still: it fires only when the foreman is gone, so nobody ever sees it
# work on a good night. `test/factory-watchdog.bats` writes lease files and ages
# logs directly to make every stall shape observable on a runner.
# `factory-watchdog` is the runner, and its silence is the loudest of all: it
# passes every twenty minutes while nobody is watching, decides which red
# branch gets a fixer lane, and revokes the lease when its passes stop landing.
# `test/factory-watchdog.bats` stubs `factory-shift` with a scripted event
# stream, writes lease files and ages logs directly, so every gate and every
# stall shape is observable on a runner in seconds.
#
# `factory-lease` was only ever reached as somebody else's fixture — a grant to
# start the poller above, a lease file written directly, a `chmod -x` to make
# start the runner above, a lease file written directly, a `chmod -x` to make
# `doctor` report it missing — so nothing had handed `grant` a duration nobody
# would type. `test/factory-lease.bats` is the verb's own, and its subject is
# the same shape as the three above: a grant that prints ✓ and is already
Expand Down
24 changes: 16 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ that comes from the user's config, always.
| `libexec/factory-lease` | the standing merge grant |
| `libexec/factory-tier` | one PR's verdict. **The filter is the definition of tier 1** |
| `libexec/factory-shift` | one pass. Deterministic: no judgement lives here |
| `libexec/factory-watchdog` | notices the foreman died |
| `ai/SKILL.md`, `ai/nightshift/SKILL.md` | the agent surface — verbs, and the loop that drives them |
| `libexec/factory-watchdog` | the runner. Passes `factory shift` every `runner.interval` while the lease is live, puts every `ci-red` through the four fixer gates, revokes a lease whose passes stopped landing |
| `ai/SKILL.md` | the agent surface — the verbs. There is no loop skill any more: the loop is the runner |

## Rules

Expand All @@ -43,12 +43,20 @@ that comes from the user's config, always.
variable that raised the line cap would be authority anything in the shift's
environment could grant itself. The three the suites use to make a 45-minute
threshold reachable in seconds — `FACTORY_STALE`, `FACTORY_DEAD`,
`FACTORY_WATCHDOG_INTERVAL` — may only *shorten* the policy's number and are
refused when they would not, because a poller inherits the environment of
whoever ran `lease grant`, and on a night shift that is the foreman.
`FACTORY_NO_WATCHDOG=1` stops `grant` spawning a poller, for a suite that
must not leak one; it hides nothing, since `watchdog once` then reports NO
POLLER at exit 4 and `doctor` carries that line.
`FACTORY_WATCHDOG_INTERVAL`, and `FACTORY_RUNNER_INTERVAL` for the pass
cadence — may only *shorten* the policy's number and are refused when they
would not, because the runner inherits the environment of whoever ran
`lease grant`. `FACTORY_NO_WATCHDOG=1` stops `grant` spawning a runner, for
a suite that must not leak one; it hides nothing, since `watchdog once` then
reports NO RUNNER at exit 4 and `doctor` carries that line.
- **The fixer gates are four, in code, and each refusal is a line.** A `ci-red`
event gets a lane only when `fixer.command` is configured, no shift log holds
a `fixer-spawned` for the same head SHA, today's log holds fewer than
`fixer.cap` for the repo, and the pass's `budget` event said `fixer: true`.
Every other outcome is `fixer-skipped` with the gate named, or
`fixer-failed` with the command's stderr. These were a skill's prose once,
which is a threshold no test can reach; `test/factory-watchdog.bats` has a
case per gate, written so that deleting the gate fails it.
- **Every deny clause needs a case that fails when it is deleted.** A clause
that stops matching has no symptom until a PR someone meant to see merges at
3 a.m. `test/factory-tier.bats` is the shape; the README's floor table and
Expand Down
251 changes: 169 additions & 82 deletions README.md

Large diffs are not rendered by default.

59 changes: 46 additions & 13 deletions ai/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ description: >-
Merge the pull requests that a reviewed filter can vouch for, without waking
the user — and check what a night of that did. Use when the user says "merge
the safe PRs", "what did the factory do last night", "grant/revoke the merge
lease", "is anything red on main", "why didn't PR N merge", "can we afford a
fixer lane", or asks to run one pass of the shift. For running the shift on a
cadence overnight, load the `nightshift` skill instead — that one is the loop,
this one is the verbs.
lease", "keep shipping while I'm asleep", "is anything red on main", "why
didn't PR N merge", "can we afford a fixer lane", or asks to run one pass of
the shift. A live lease runs the shift on its own; this skill is the verbs
around it and how to read what it wrote.
---

# factory — merge what code alone can vouch for
Expand All @@ -20,21 +20,25 @@ decides with a model, never writes PRs, and never merges without a live lease.
**Its failure mode is the status quo**: no lease, an expired one, or a pass that
could not see leaves every PR open, exactly where it is today.

Nothing runs on a schedule. One `factory shift` is one pass; something has to
call it — usually a person, or an agent driving the `nightshift` loop.
**The lease is the on/off switch.** `factory lease grant 12h` starts a runner
(`factory watchdog run`) that passes `factory shift` every 20 minutes until the
lease ends, spawns a fixer lane on a red default branch when four gates in
code allow it, and revokes the lease if its passes stop landing. No agent
session drives it. The shift log is the handover.

## Verbs

| do this | run this |
|---|---|
| take merge authority for a while | `factory lease grant 12h` |
| take it until told otherwise | `factory lease grant indefinitely` |
| check / drop that authority | `factory lease status` · `factory lease revoke` |
| sense everything, merge nothing | `factory shift --dry-run` |
| one real pass | `factory shift` |
| one real pass, by hand | `factory shift` |
| ask why one PR is not mergeable | `factory tier <owner/repo> <number>` |
| read the effective policy | `factory config print` |
| is this machine able to run a shift | `factory doctor` |
| is the foreman alive | `factory watchdog once` |
| are passes landing under the lease | `factory watchdog once` |
| last night's report | `cat ~/.cache/factory/shift-$(date +%Y%m%d).log` |

Every read verb takes `--json` — `doctor` included, where it returns one
Expand All @@ -47,13 +51,36 @@ run always means the run failed, never that the verb had no JSON to give.

- "merge the docs PRs" / "clear the safe ones" → `factory shift --dry-run`
first, then `factory shift` under a lease
- "keep shipping while I'm away" → `factory doctor`, then
`factory lease grant <duration>`. That is the whole start: the runner does
the rest. Confirm with `factory watchdog once` (exit 0), and tell the user
in one line what authority stands and until when
- "why is #212 still open?" → `factory tier <repo> 212` — the refusal names its
own reason
- "what happened last night?" → read today's (or yesterday's) shift log
- "stop it merging things" → `factory lease revoke`
- "can it merge X too?" → that is a policy edit at `factory config path`, and it
is the user's call, never yours

## Reading the log

- `merged` / `queued` / `would-merge` — verdicts. `queued` waits for a person by
design.
- `CI-RED <repo> <url>` — the default branch is red. The lines right after it
say what the runner did about it: `fixer-spawned: <repo> <sha>`, or
`fixer-skipped: <repo> — <gate>` naming which of the four gates refused
(no `fixer.command`, same head SHA already had a lane, `fixer.cap` reached
today, or budget), or `fixer-failed` with the command's stderr.
- `pass-retry: <event> — one more pass at the next tick` — the runner saw an
unknown or an abort and is running once more. Two of the same unknown in a
row is a story for the user, not something to fix.
- `shift-stalled` / `shift-resumed` / `shift-dead` — passes stopped landing
under a live lease, resumed, or stopped long enough that the runner revoked
the lease. `machine-slept` is a gap that was the machine's. `shift-over` is
the lease ending the ordinary way.
- `pass-failed` — `factory shift` exited before it could write anything. The
line quotes why; it is usually the config.

## When NOT to

- **Never merge a PR the shift queued.** A `queued:` line is a verdict: the PR
Expand All @@ -64,6 +91,12 @@ run always means the run failed, never that the verb had no JSON to give.
it against the new head.
- **Never widen `tier1` to get something through.** The filter is the whole
definition of what may merge unattended.
- **Never loop `factory shift` yourself while a lease is live.** The runner is
already doing it, on the cadence the policy names. A pass by hand is fine;
a second loop is two things merging under one grant.
- **Never spawn a fixer lane yourself off a `CI-RED` line.** The four gates are
in code and their verdict is the line after it. A lane the gates refused is
a lane the budget or the cap said no to.
- Opening PRs, reviewing code, releasing — none of that is here.

## Traps
Expand All @@ -74,18 +107,18 @@ run always means the run failed, never that the verb had no JSON to give.
judged and refused; `tier-unknown` means nothing judged it. The second one is
a PR nobody has looked at.
- **`ci-unknown` is not a green branch**, and `prs-unknown` is not a repo with
no PRs. Both mean the pass was blind there. Run it again once; if it repeats,
say so.
no PRs. Both mean the pass was blind there. The runner retries once on its
own; if the line repeats, say so.
- **A `pass ABORTED` exits non-zero and merged nothing.** Nothing was sensed —
do not report it as a quiet night.
- **The policy file is machine-local** (`factory config path`), deliberately: a
copy inside a repo would be a file a PR could edit to widen the filter judging
it. Do not add one to a repo.
- **The lease is the user's grant.** Never grant one to get past a refusal, and
never re-grant one the watchdog revoked — that revocation is the watchdog
reporting the shift stopped being run.
never re-grant one the runner revoked — `shift-dead` is the runner reporting
its passes stopped landing, and the reason is in the lines above it.
- Exit codes: `0` ok/tier 1 · `1` no lease, or an aborted pass · `2` usage or
bad config · `3` refused / foreman stalled · `4` live lease, no poller.
bad config · `3` refused / shift stalled · `4` live lease, no runner.
**`doctor` is the exception**: `0` ready with nothing to note, `1` ready
**with notes**, `2` blocking. A ready machine usually exits 1, so read
`.ready` rather than the code.
Expand Down
134 changes: 0 additions & 134 deletions ai/nightshift/SKILL.md

This file was deleted.

Loading
Loading