Skip to content

feat: merge freeze that holds Gram's own merges, and a freezeLifted wake - #110

Merged
ANT1OFF merged 2 commits into
mainfrom
feat/merge-freeze
Nov 3, 2026
Merged

ANT1OFF merged 2 commits into
mainfrom
feat/merge-freeze

Conversation

@ANT1OFF

@ANT1OFF ANT1OFF commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Builds on #108 (condition wakes, merged). Changes merge-gate behaviour: held for an independent review.

What

merge_freeze (orchestrator and worker tool): a thread freezes Gram's own merges onto a repo's branch (the default branch unless branch is given) for a reason, for at most 60 minutes (minutes, default 60). Calling it again renews the freeze. lift: true ends it.

  • Gate: while a freeze stands, merge_when_ready waits instead of merging and records why on the PR, so the ledger shows main is frozen by "Release 1.6" for release 1.6 cut until 14:05Z. set_auto_merge refuses, because GitHub's auto-merge, once armed, would merge under a freeze Gram can't hold back.
  • Race: a merge's last check and its gh pr merge launch run under a per-repo fence that freeze also takes. A freeze is either seen by that last check or lands after the launch; it can't slip between them. The check also runs at evaluate and at the start of each attempt.
  • Authority: only the thread that froze a branch can renew it. That thread, or the orchestrator of the workspace it was in, can lift it. Anyone else is refused, with the expiry time. Freezing over another thread's freeze is refused and points at freezeLifted.
  • Always expires: every read compares until with the clock, so neither a timer that never fired nor a restart past the expiry keeps a freeze alive. Timers also end it on time and are re-armed at start. Rows are persisted in a new merge_freezes table (migration 0002_merge_freezes), so a freeze survives a restart until it expires. A closed thread's freezes end with it; an archived or stopped thread's simply run out.
  • Lift/expiry re-checks: any change on MergeFreezes.changes re-checks every armed merge in that repo, so a held merge goes as soon as the freeze ends.
  • Limits: it governs only Gram's merges. A person or GitHub can still merge, and the tool description and docs say so.

freezeLifted condition kind (on #108's interface): { kind: "freezeLifted", repo, branch? } fires when the freeze ends, lifted or expired. Without branch it takes the freeze standing on the repo; it is refused if nothing is frozen. It is driven by MergeFreezes.changes, with a 1-minute poll fallback and a 5-second debounce.

Release flow: docs/RELEASING.md § Holding main during the cut. The cutting thread freezes main just before merging the release PR, renews while needed, and lifts once release:rc has recorded the Linux dispatch, instead of asking the orchestrator to hold merges. The release-worker brief itself lives in orchestrator memory, outside the repo, so it should point at this section.

Review fixes (c259b65, findings 1–9 from the independent review of 204a03c)

  1. Release flow no longer blocks itself: allow: [PR numbers] (at most 4) lets the named PRs merge under the freeze. I chose this over exempting the owner's bound PR because it names exact PRs. RELEASING.md now freezes with allow: [<release PR>]. Test: the allowed PR merges while the freeze stands.
  2. GitHub auto-merge armed before the freeze: on freeze, Gram turns off GitHub auto-merge on open PRs into the branch that belong to Gram threads (except allowed ones) and records them on the freeze with their method. When the freeze ends, by lift, expiry or after a restart, it arms them again, or hands them to a freeze standing on the branch by then. The result lists autoMergeOff and any autoMergeStillArmed. Not covered, and said so in the tool and docs: a person merging, auto-merge on a PR no Gram thread has, and merge-queue entries. Tests: taken down and re-armed on lift; re-armed on expiry after a restart; a PR no Gram thread has is left alone.
  3. Workspace scope: a freeze is refused unless the repo is the origin of one of the caller's workspace projects. Refusal test.
  4. set_auto_merge: the check and the launch run under the freeze fence, and when a freeze stands on the repo it reads the PR's base afresh (gh pr view --json baseRefName). Test: a stale badge base plus a fresh main is refused; an allowed PR merges.
  5. Branch validation: branches are checked in the tool schema and on the resolved default branch, and the row is written before the in-memory map. Test: release+1 and a default branch named trunk+1 are refused, and nothing is left standing.
    6–7. Expiry: one watcher per freeze re-reads the freeze and the clock at least every 60 s and ends it once expired. lift and the thread-closed path no longer touch timers, and ending can't be interrupted partway. Every read still compares against the clock. Test: lift, then a new freeze by the owner still ends on time and its end lets the held merge go. A macOS sleep that suspends timers can't be faithfully simulated under TestClock, so 7 rests on the 60-second re-read by design.
  6. Total cap: a freeze stands at most 4 hours from when it was first set (since). Past that, renewal is refused and a fresh freeze is needed after it ends. Test included.
  7. Reason text: collapsed to a single line, with control characters removed. Test included.
  • Note: the docs say a freeze can wait behind an in-flight merge's fresh gh read, since the fence covers it.

Re-review fixes (8225a52, findings A and B from the re-review of c259b65; rebased on main after #111)

  • A. Re-arm no longer overrides a deliberate choice: a PR leaves the freeze's re-arm list the moment someone turns its auto-merge off (set_auto_merge enabled:false), holds its merge (merge_when_ready hold), unbinds it (pr_unbind), or Gram sees it closed or merged. The change is saved at once, so a restart can't bring it back. At the end, re-arm also checks each PR fresh: it skips one that no Gram thread binds any more, whose thread is archived, babysat or in a paused workspace, or whose merge is held. Tests cover turn-off and hold (both survive a restart), unbind and close, and archived and paused. With the fix disabled, both turn-off tests fail.
  • B. A quit partway through no longer loses the record: each PR goes on the freeze's record before its auto-merge is turned off, under the branch's lock, and comes off again if GitHub refuses. A quit partway can at worst leave a PR recorded that wasn't turned off, and arming that again is harmless. Test: GitHub hangs on the second PR and the app quits there; after the restart and expiry, both are armed again.
  • Rebase: the branch is rebased onto feat(orchestration): wake on stuck CI and on machine load or low disk #111 (stuck-CI and machine kinds) and squashed. freezeLifted now sits beside those kinds in the union, the sources and schedule_wake's description, and wakeConditionsLayer takes all the GitHub and machine readers as one layer.
  • Prompt budget: with feat(orchestration): wake on stuck CI and on machine load or low disk #111's kinds and merge_freeze both in, I tightened the tool's text and then raised orchestrator from 62,400 to 63,000 and worker from 35,700 to 36,600 (measured 62,424 and 36,318). Plain is unchanged at 24,642 of 24,900.
  • Verification: 22 related test files pass (348 tests), including feat(orchestration): wake on stuck CI and on machine load or low disk #111's wake-conditions, service-condition-wakes-ci-machine and ci-stuck. Typecheck, lint, docs and scaffold checks are clean.

Files

  • packages/contracts/src/wake-conditions.ts: FreezeLiftedCondition, MergeFreeze, mergeFreezeText, MERGE_FREEZE_MAX_MINUTES = 60, MERGE_FREEZE_REASON_MAX = 200
  • packages/server/src/git/merge-freezes.ts: the MergeFreezes service (freeze, lift, active, list, fence, changes), plus MergeFreezesNone for test layers that don't exercise freezes
  • packages/server/src/git/merge-when-ready.ts: the freeze check at evaluate, at attempt start and inside the launch fence; the re-check on freeze changes
  • packages/server/src/app-tools/pr-tools.ts: the merge_freeze tool; set_auto_merge refusal
  • packages/server/src/wakes/condition-sources.ts: the freezeLifted source; wakeConditionsLayer
  • packages/server/src/persistence/migrations.ts: 0002_merge_freezes
  • docs: AGENTS_AND_FEATURES.md (merge freezes, freezeLifted row), RELEASING.md

Verification

  • service-merge-freeze.test.ts (real harness, full-autonomy workspace, PR armed with merge_when_ready):
    • Gate and authority: freezing the default branch with a reason; minutes: 61 refused; the ready PR waits with the exact line main is frozen by "Release 1.6" for the 1.6 release cut until HH:MMZ and no gh pr merge; set_auto_merge refused naming the freeze. A non-owning worker can't freeze over it or lift it ("Only "Release 1.6" or its orchestrator…"). The owner renews. The orchestrator lifts, and the merge then launches pinned with --match-head-commit.
    • Restart and expiry: the freeze survives a restart and the ready PR still waits. The owning agent is interrupted and never returns, yet the freeze expires at its time (active is undefined) and the merge goes. A plain thread's freezeLifted wake fires once after its debounce; freezeLifted on an unfrozen branch is refused.
  • feat: condition wakes for schedule_wake (branch moved, thread finished) #108's service-condition-wakes.test.ts still passes (6 tests).
  • merge-after-local-check, merge-base-lock, merge-moved-base, merge-ci-launch, pr-tools, pr-tools-merge, pr-tools-merge-launch, migrations, held-briefs, service-wakes, tools, tool-ergonomics, http/server, registry: pass (16 files, 258 tests, with the 3 migrations cases updated for 0002; re-run green).
  • prompt-budget.test.ts: the worker limit is raised on purpose from 34,700 to 35,600. merge_freeze plus feat: condition wakes for schedule_wake (branch moved, thread finished) #108's condition text bring the worker to 35,137 characters. The orchestrator is at 61,243 of 62,400; plain threads don't get the tool.
  • bun run typecheck, bun run lint, bun run fmt, bun run check:docs, bun run check:scaffold: clean. No full local bun run check; CI and the managed check gate it.

Known limits

  • A PR whose badge doesn't name its base is held by a freeze anywhere on its repo (the conservative choice).
  • The fence is per repo, so two Gram merges onto different bases of one repo serialize their final launch. This only matters if they launch at the same moment.
  • The user has no "lift" button; the freeze is at most 60 minutes and the orchestrator can lift it. Adding a UI control would mean a new surface, which the brief rules out.
  • Migration 0002 is the first 1.x migration. A parallel PR adding one must renumber.

ANT1OFF added a commit that referenced this pull request Nov 3, 2026
## What

When a worker or an orchestrator is at rest waiting on something, the
sidebar now says what, at a glance: an hourglass and a short line under
its name, and the whole list on hover. The Board's chips and ⌘J's
Workers list read the same selector, so the three surfaces say the same
words.

Presentation only. Nothing in `packages/` changes: everything comes from
the ledger the web already gets (PRs, wakes, watched runs, limit waits)
and the threads store.

### One "waiting on" model
`apps/web/src/orchestration/waiting-on.ts` turns a calm worker's or
orchestrator's waits into:
- `label`: the row's line, e.g. `Waiting · checks on #69`
- `reasons`: every wait, each with a kind, a short form and a full line
- `hint`: the hover, i.e. the label with `+N` when more apply, then
every wait's line

| Kind | Label | Comes from |
| --- | --- | --- |
| checks | `Waiting · checks on #69`, `Waiting · local check #69` |
required checks pending; Gram's local check running |
| review | `Waiting · review on #69` | the review bot reviewing the head
|
| merge | `Waiting · merge of #69`, `Waiting · #69 held` | an armed
`merge_when_ready`; a hold on an agent's work |
| dependency | `Waiting · #102 to merge`, `Waiting · acme/api#7 to
merge`, `Waiting · #645 to move` | `depends_on`; a wake on another PR |
| run | `Waiting · Release run` | `run_monitor` |
| condition | `Waiting · main to move`, `Waiting · thread to finish`;
any other kind as its chip reads it (`wakeConditionLabel`) | #108's
condition wakes; the hover names the thread and says `wakeConditionWhen`
|
| timer | `Waiting · until 08:10`, `Waiting · until Wed 09:00` | a timed
wake |
| limit | `Paused · limit until 08:10`, `Paused · retry at 07:05` | a
usage-limit or rate-limit wait |
| held | `Held · by you` | the user holds the worker |
| frozen | (reserved) | nothing produces it yet; the merge freeze will |

**Which wait the label names.** What holds everything else comes first
(a limit, a frozen base, your hold). Then what clears soonest by its
nature (CI, the review bot, Gram's merge), then the longer waits
(another PR, a run, a condition). A timed wake comes last, since it's
mostly the fallback beside the others. Within a kind, the soonest wins.

**Waiting is never "needs you".** A wait is only said of a calm tone
(waiting, idle, or finished with a run still watched), never beside
work, an ask or a failure. Nothing here touches the Needs you list or
counts.

### Surfaces
- **Sidebar worker rows**: the hourglass line replaces the card's now
line ("checks running", "you hold it") while the worker waits. The hover
card lists every wait.
- **Orchestrator line**: a second line under the workspace name for its
own pending wakes, or a limit it waits out. The line's hover adds the
list. It's hidden while the workspace is paused or parked.
- **Board**: a chip's line is the same label, and its hover the same
list.
- **⌘J**: a row's now line is the same label, and its hover the same
list.
- The waiting tone's label is now "Waiting" rather than "Waiting on
checks", since it covers more than checks.

### Behaviour change
A worker waiting out a usage limit (`limitWait` in the ledger) now reads
as waiting on automation, with the hollow dot. Before, its activity
ignored the limit, so it showed idle (or attention).

### Labels kept to four words
The Board keeps a chip's line to four words. Every label fits, with the
` · ` separator counted as punctuation; `board-rail.spec.ts` now counts
it that way.

### Condition kinds still to come
`conditionShort` special-cases `branchMoved` and `threadFinished` only.
Any other kind falls back to contracts' `wakeConditionLabel`, and its
hover line to `wakeConditionWhen`. So #110 (`freezeLifted`), #111
(`ciStuck`, machine load and disk) and #112 (`releasePublished`,
`versionChanged`) show up without a change here. Whichever lands last
adds any icon, words or priority of its own. The `frozen` kind is
reserved for #110's frozen-main state.

## Screenshots
From `pr-shots` on synthetic mock data. The "kinds" scene
(`simulateWaitingOn`) adds a timed wake, a watched release run, a usage
limit, an armed merge, a wake on another PR and one on `main` moving,
and the orchestrator resting on two wakes of its own (a thread to
finish, ranked first, and a timed one).


**The sidebar over `casefiles`: workers waiting on CI, the review bot
and a dependency say what they wait on** (dark)

_After only: the base timed out staging it; the light pair below has the
before._

![sidebar-waiting-dark
after](https://github.com/user-attachments/assets/8cc9172d-160d-4f3a-8544-e82574065fc2)

**The sidebar over `casefiles`: workers waiting on CI, the review bot
and a dependency say what they wait on** (light)

| Before | After |
| --- | --- |
| ![sidebar-waiting-light
before](https://github.com/user-attachments/assets/8e4579f5-a2dc-4621-8fe9-01d367de281b)
| ![sidebar-waiting-light
after](https://github.com/user-attachments/assets/29058cf0-2c0c-499a-8b52-56cd887be7b5)
|

**Every kind of wait in the sidebar, and the orchestrator resting on a
wake of its own** (dark)

_After only: the scene's mock scenario is new in this PR._

![sidebar-waiting-kinds-dark
after](https://github.com/user-attachments/assets/0beada9b-6c10-4254-a7d9-98bfe7fe50ea)

**Every kind of wait in the sidebar, and the orchestrator resting on a
wake of its own** (light)

_After only: the scene's mock scenario is new in this PR._

![sidebar-waiting-kinds-light
after](https://github.com/user-attachments/assets/34353279-1bc1-4030-a5f8-35354028e9e0)

**A waiting worker's hover card lists every wait, the one its row names
first** (dark)

_After only: the scene's mock scenario is new in this PR._

![sidebar-waiting-hover-dark
after](https://github.com/user-attachments/assets/605089f6-e232-47e4-933b-d626fd010bf2)

**The same waits on the Board's chips, in the sidebar's words** (dark)

_After only: the scene's mock scenario is new in this PR._

![board-waiting-kinds-dark
after](https://github.com/user-attachments/assets/7f7e95fd-cd77-473d-93cc-8741f318a85f)

**The same waits on the Board's chips, in the sidebar's words** (light)

_After only: the scene's mock scenario is new in this PR._

![board-waiting-kinds-light
after](https://github.com/user-attachments/assets/8325dda3-5043-4bd4-813e-52b88bb33567)

**⌘J's Workers list says the same** (dark)

_After only: the scene's mock scenario is new in this PR._

![workers-list-waiting-dark
after](https://github.com/user-attachments/assets/c91f9952-74e7-4c0d-98b5-2f834c58522c)

**The same `casefiles` workers on the Board: the chips agree with
Quiet's dots** (dark)

| Before | After |
| --- | --- |
| ![quiet-busy-board-dark
before](https://github.com/user-attachments/assets/ba5d04eb-5035-4fbb-8a44-2f65f8deb895)
| ![quiet-busy-board-dark
after](https://github.com/user-attachments/assets/0a43c050-93f4-4562-bb81-e46e2b1297ac)
|

**The same `casefiles` workers on the Board: the chips agree with
Quiet's dots** (light)

| Before | After |
| --- | --- |
| ![quiet-busy-board-light
before](https://github.com/user-attachments/assets/d317ead1-7c3c-4404-8976-a7bda03e991d)
| ![quiet-busy-board-light
after](https://github.com/user-attachments/assets/1200bee4-f0c0-4feb-bb26-b0d73beef83f)
|


## Verification
On `fc8e877`, rebased onto `main` after #108:
- `bun run typecheck`, `bun run lint`, `bun run fmt:check`, `bun run
check:scaffold`: pass
- `vitest run src/orchestration src/views src/sidebar src/thread
src/shell src/api`: 1228 passed (on `767c1af`); `waiting-on.test.ts`: 30
passed on the head
- Affected e2e, `--workers=2` (`overview-sidebar board-parity board-rail
board-virtual feature-sheet orchestration workers-list quiet-view
overview-extras workspace-density reaching-workers worker-questions
one-thread-budgets ask-stack`): 135 passed, 3 failed. Two were tests to
update for this change (the orchestrator's waits read in rank order;
⌘J's four-word cap counting the ` · `), fixed in `fc8e877`. The third,
`overview-sidebar.spec.ts:851` (a Needs you count of 9 for 8), is
untouched by this and passed alone. All three passed alone on the head.
- Specs updated, not deleted: `board-rail` and `workers-list` count
words without the separator; `overview-sidebar` reads a row's name with
its wait, and tells the agent's glyph from the hourglass.
- Not run: the full `bun run check`, left to the merge gate.

## Known limits
- `frozen` has a label and a rank but no source until the merge freeze
lands.
- A thread-finished condition shows "thread to finish" on the row; the
thread's title is in the hover only, to keep the line short.
- Synthetic mock data only; not run against a live provider or real
workspace.
merge_freeze (orchestrator and worker) freezes Gram's merges onto a
branch of a workspace repo, the default branch unless named, for a
reason and at most 60 minutes, renewable up to four hours in all by the
thread that froze it; allow names PRs that may still merge (the release
PR). While it stands, merge_when_ready waits and says so on the PR
("main is frozen by <thread> for <reason> until HH:MMZ"), and
set_auto_merge refuses; both launch under a fence the freeze takes too.
GitHub auto-merge armed on Gram's threads' PRs onto the branch is turned
off for the freeze and armed again after, unless someone let it go of
meanwhile. Only the freezing thread or its workspace's orchestrator
lifts it; it always ends at its time, over a restart or with its thread
gone. It cannot stop a person, a merge-queue entry or another PR's
auto-merge.

freezeLifted is a condition kind for schedule_wake, fired on the end of
a freeze. docs/RELEASING.md has the cut freeze main, allowing the
release PR, until the Linux dispatch is recorded.
@ANT1OFF
ANT1OFF merged commit 5096a48 into main Nov 3, 2026
23 checks passed
@ANT1OFF
ANT1OFF deleted the feat/merge-freeze branch November 3, 2026 08:14
ANT1OFF added a commit that referenced this pull request Nov 3, 2026
…ion change (#112)

## What

Two new condition kinds for `schedule_wake`, built on the engine from
#108. Also a fix to that engine, which the new kinds exposed.

- **`releasePublished`**: a package on npm, or a GitHub repo's releases,
publishes a version newer than `after` that is at least `minAgeDays`
old. Prereleases (and GitHub drafts) don't count unless
`includePrerelease` is set. This is for dependency bumps that wait out a
release-age window.
- **`versionChanged`**: the installed version of Gram, `claude`, `codex`
or `gh` changes from `from`.

## The spec types (`packages/contracts/src/wake-conditions.ts`)

```ts
export const ReleaseVersion = Schema.String.check(Schema.isPattern(/^v?\d+\.\d+\.\d+(-[\w.-]{1,40})?$/));
export const ReleaseName = Schema.String.check(Schema.isPattern(/^@?[\w.~][\w.~-]*(\/[\w.~-]+)?$/)); // never a leading dash
export const RELEASE_MIN_AGE_DAYS = 7; // Gram's own Dependabot cooldown

export const ReleasePublishedCondition = Schema.Struct({
  kind: Schema.Literal("releasePublished"),
  source: Schema.Literals(["npm", "github"]),
  name: ReleaseName,                                   // @scope/pkg, or owner/repo
  after: Schema.optionalKey(ReleaseVersion),           // filled with the latest release when set
  minAgeDays: Schema.optionalKey(Schema.Finite),       // filled with 7; 0 = none; the source refuses one outside whole 0..90
  includePrerelease: Schema.optionalKey(Schema.Boolean),
});

export const VersionTarget = Schema.Literals(["gram", "claude", "codex", "gh"]);
export const VersionChangedCondition = Schema.Struct({
  kind: Schema.Literal("versionChanged"),
  target: VersionTarget,
  from: Schema.optionalKey(ReleaseVersion),            // filled with the installed version when set
});
```

New kinds and optional fields, so `WAKE_CONDITION_VERSION` stays at 1.

**Words:**
- Chip label: `codex newer than 0.166.0, 7 days old`, `cli release` (no
baseline yet), `Gram update`, `Codex update`.
- `when`: `@openai/codex on npm has a release newer than 0.166.0, at
least 7 days old`, `Gram is updated from 1.5.0`, `the installed Codex
changes from 0.166.0`.

**Facts on fire:**
- `releasePublished`: `source`, `name`, `version`, `published` (ISO),
`url`, `after`.
- `versionChanged`: `target`, `version`, `previous`.

**Refusals when set:**
- No such package or repo.
- A GitHub name that isn't `owner/repo` (including `.`/`..`).
- A newer release that is old enough already.
- A `from` that isn't what's installed.
- A CLI the launch checks didn't find.
- A Gram build that doesn't report its version (dev).

## Network calls

Gram's existing policy (`agent-runtime/releases.ts`) is to reach
packages only through the user's own npm, never a registry npm wasn't
configured with. This follows it:

- **npm:** `npm view --json <name> time versions`, from the home folder
through `Proc`. npm applies its own registry, proxy and cache. Gram
never reads `.npmrc`. npm's stderr is read only for `E404`, never kept
or logged. Timeout 2 min, output cap 4 MiB.
- **GitHub:** `gh api repos/<owner>/<repo>/releases?per_page=30
--hostname github.com`, with the user's gh, after
`GhBudget.admit("core", "background")` and under `asBackground`, as
`BranchHeads` does.
- Nothing else is sent: only the name asked about, through the user's
own tools.

`versionChanged` makes **no** network calls and starts **no** process.

## Poll intervals

| Kind | Looks | Network / probes |
| --- | --- | --- |
| `releasePublished` | every 1 h (cheap: over the kept answer); debounce
0 | one read per package, shared by every wake on it, kept 6 h; a read
when a wake is set unless one is under 60 s old; a failed read is never
met and is retried after 12 h, then every 24 h, and is reset by a
success |
| `versionChanged` | every 10 min, 10 s after start, and on each
launch-check run; debounce 0 | none. Gram's version is
`ServerConfig.appVersion`. A CLI's comes from the last
`LaunchChecksCache` run (at start, on Re-check, after a Settings
update). It is read through the new `latest` and `changes`, which never
start a run. Setting a wake may wait for the cache's one first run,
which `app.launchChecks` starts anyway |

## Rebased onto #111

#111 landed the same per-kind `metSince` fix in `wakes/conditions.ts`,
so this PR now keeps main's version of that file. The regression test
here (`another kind's sweep leaves a debounce under way alone`) stays as
extra cover. The union, sources, layers and test fakes now include both
PRs' kinds.

## Rebased onto #113

#113's waiting-on board keeps labels to four words. This PR gives the
new kinds short board words: `Waiting · codex release` and `Waiting ·
Gram update`. The longer chip label stays (`codex newer than 0.166.0, 7
days old`). #113's test for "a kind with no words" used
`releasePublished` as its stand-in future kind, with a made-up shape.
Now that the kind is real, that broke `check tests 1/2` and `macos tests
2/3`. The test now uses a kind that doesn't exist, and a new test covers
the real words. Web `orchestration` tests: 47 files, 463 passed.

## Rebased onto #110

#110 (merge freeze, `freezeLifted`) merged first. The union, the
sources, `wakeConditionsLayer`'s inputs, the server's layers and the
test harness now carry all seven kinds.

## Prompt budget

The `schedule_wake` description lists all seven kinds. My sentence was
cut to `releasePublished` (a release of `name` newer than `after`,
`minAgeDays` old) and `versionChanged` (installed `target` leaves
`from`). The defaults are in the schema, the docs and the tool's answer.
The wording was also tightened elsewhere: no "release-age window"
example, no "the message names the event", and a shorter fallback
clause.

The new kinds' input fields still push two roles over, so worker and
plain are **raised by 500 on purpose**, as #111 and #110 did for theirs.
The reason is in the test header.

| Role | Measured | Limit |
| --- | --- | --- |
| orchestrator | 62,853 | 63,000 (unchanged) |
| worker | 36,747 | 37,100 (was 36,600) |
| plain | 25,071 | 25,400 (was 24,900) |

`minAgeDays` is `Schema.Finite`, as #111's numbers are, to keep the
schema short. The source refuses a fraction or a value outside 0..90
(tested).

## Verification

- After the rebase onto #110, `bun run test` over 62 files (server
wakes, condition, ci-machine, merge-freeze, release, restarts, tools,
budget, launch-checks, http server, contracts, web `orchestration` and
`thread-view`): 659 passed. Typecheck, lint, docs and scaffold: clean.
- After the earlier rebase onto #111, `bun run test` over 23 files (the
list below plus #111's `service-condition-wakes-ci-machine`, and the web
`thread-view`): 698 passed, 1 skipped.
- Before the rebase: `bun run test` over `service-release-wakes` (new,
4), `service-condition-wakes` (7), `prompt-budget`, `service-wakes`,
`service-digests`, `service-restarts`, `rules`, `tools`,
`tool-ergonomics`, `wakes/*` (including the new `upstream-releases`
parser test), `launch-checks/*` (including the new `observeRuns` test),
`agent-support`, `issues/tools`, `http/server`, `thread-store`,
contracts `wake-conditions` (new) and `orchestration`: 20 files, 666
passed, 1 skipped.
- Web `thread-view`: 16 passed.
- What the new tests cover:
  - Each kind fires once.
- Age gating: nothing at 6 days; fires at 7 from the hourly look without
a new read.
  - Prereleases are skipped, and counted when asked for.
  - An already-due release is refused.
- An unreadable registry is set without a baseline, never fires, and is
read at 0, 12 h and 36 h (backoff); the first good read becomes the
baseline, then reads every 6 h.
  - A CLI's change fires on a launch-check run.
- A Gram update fires after a restart through the existing `ThreadWakes`
host.
  - Refusals, and names shaped like flags or paths.
- `bun run typecheck`, `bun run lint`, `bun run fmt`, `bun run
check:docs`, `bun run check:scaffold`: clean.
- Gram's managed full check of the pre-rebase head (06407b6) failed 24
tests. 22 were timeouts, and the other 2 were element lookups in web
panel tests, all in files this PR doesn't touch, with machine load
around 20–50. On the rebased head, `dreams-service-archive`,
`dreams-service-adoption`, `codex-dream-protocol`, web `pr-panel` and
`message-block` were rerun: 94 passed. The release-stable test was not
rerun.

## Known limits

- All npm, GitHub and launch-check behaviour is behind
`UpstreamReleases` and `InstalledVersions`, which the tests fake. No
live-provider qualification was run.
- A CLI update made outside Gram (for example `npm i -g` in a terminal)
is seen only at the next launch-check run: a restart, Re-check, or an
update from Settings. This is deliberate: it reuses the existing probes
and never adds one.
- For `claude` and `codex`, `versionChanged` watches the PATH CLI the
launch checks probe, not a managed runtime.
- A GitHub release's version is the first `x.y.z` in its tag (`v1.2.3`,
`rust-v0.166.0`). Tags naming no version are skipped. One repo
publishing several packages under different tag prefixes isn't told
apart.
- Only github.com is read for `source: "github"`.
- The kept reads and backoff are in memory, so a restart reads again
once.
ANT1OFF added a commit that referenced this pull request Nov 3, 2026
Desktop release metadata proposal only. Merging this PR does not publish
or tag Gram; follow docs/RELEASING.md to release.
---


## [1.6.0](v1.5.0...v1.6.0)
(2026-11-03)


### Features

* condition wakes for schedule_wake (branch moved, thread finished)
([#108](#108))
([550545a](550545a))
* merge freeze that holds Gram's own merges, and a freezeLifted wake
([#110](#110))
([5096a48](5096a48))
* **orchestration:** wake on an upstream release or an installed version
change ([#112](#112))
([efdd8fa](efdd8fa))
* **orchestration:** wake on stuck CI and on machine load or low disk
([#111](#111))
([be9f98f](be9f98f))
* **web:** show what a worker or orchestrator is waiting on
([#113](#113))
([4a2ae69](4a2ae69))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: gram-release-please[bot] <351321620+gram-release-please[bot]@users.noreply.github.com>
Co-authored-by: Johan <32440065+ANT1OFF@users.noreply.github.com>
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