mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-15 12:57:22 +01:00
Merge remote-tracking branch 'origin/main' into gsta-99-parked-label
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> # Conflicts: # CHANGELOG.md
This commit is contained in:
commit
3d76e2e31d
166 files changed
+9041
-470
No files matched your search
+24
-9
@@ -120,15 +120,30 @@ lease …`). Stale needs proof: two well-formed identities from the same backend
|
||||
that differ (`ps` is read under `LC_ALL=C TZ=UTC0`). An identity that cannot be
|
||||
read, a malformed record or a backend change keeps the lease. `touch` (publish
|
||||
by rename, prune) and reclaim (reread, then delete only the record judged
|
||||
stale) run under one lock in the lease directory, a symlink naming its holder's
|
||||
pid and start identity, so a renewal is never deleted and a live lease never
|
||||
disappears from a concurrent scan. The lock is broken only when its holder is
|
||||
dead or its pid was reused, never by age, and breakers serialize on a second
|
||||
symlink and reread the lock before removing it. One killed gstack-lease process
|
||||
is always safe; two killed in a row, one holding the lock and the next while
|
||||
breaking it, can still let a reclaim delete a renewal. While a live process holds it,
|
||||
`touch` exits 3 without writing (`gstack-skill-start` reports that the
|
||||
session holds no lease) and a reclaim keeps the lease. `bin/gstack-lease list` shows `alive|dead|stale`, and
|
||||
stale) run under one lock in the lease directory, a `.lock` directory whose
|
||||
`owner` file names its holder's pid and start identity, so a renewal is never
|
||||
deleted and a live lease never disappears from a concurrent scan. The lock is a
|
||||
directory because `mkdir` is atomic on every host, while Git Bash on Windows
|
||||
cannot `ln -s` to a target that does not exist (#3330). A taker publishes a
|
||||
claim before its `mkdir` and renames it in as the owner after, so an ownerless
|
||||
lock is broken only once no claimant lives and the lock is at least 2 s old
|
||||
and unchanged while checked, which keeps a taker between its two steps safe.
|
||||
A claim that records no start identity is abandoned after 2 minutes, so a
|
||||
reused pid cannot keep an orphan forever; an owner that names no valid pid
|
||||
counts as a dead holder, and an owner file that cannot be read keeps the
|
||||
lock. A live holder that never lets go bounds the wait at 300 tries or 30 s. A lock that names a holder is broken only when that holder is dead or
|
||||
its pid was reused, never by age, and breakers serialize on a second lock and
|
||||
reread the lock before removing it. A symlink `.lock` left by an older gstack
|
||||
is honored the same way. Three known limits remain. An older gstack's `ln -s`
|
||||
onto a directory lock links inside it and succeeds, so a process that started
|
||||
before an upgrade of this checkout can share the lock with a newer one. And a
|
||||
breaker killed while holding the second lock, followed by two live breakers
|
||||
clearing it at once, can let a reclaim delete a renewal. On a host without start
|
||||
identities, a taker stalled over 2 minutes between its `mkdir` and its owner
|
||||
rename can be broken and then publish into its successor's lock. While a live process
|
||||
holds the lock, or it cannot be taken at all, `touch` exits 3 without writing
|
||||
(`gstack-skill-start` reports that the session holds no lease) and a reclaim
|
||||
keeps the lease. `bin/gstack-lease list` shows `alive|dead|stale`, and
|
||||
`GSTACK_UPGRADE_FORCE=1` overrides.
|
||||
|
||||
## What the installer does
|
||||
|
||||
@@ -87,6 +87,8 @@ After weighing Codex's argument, we chose to roll back CEO EXPANSION and ship an
|
||||
"inferred": {
|
||||
"values": {"scope_appetite": 0.72, "risk_tolerance": 0.58, "...": "..."},
|
||||
"sample_size": 47,
|
||||
"signal_events": 19,
|
||||
"contributing_events": {"scope_appetite": 9, "risk_tolerance": 6, "...": "..."},
|
||||
"diversity": {
|
||||
"skills_covered": 5,
|
||||
"question_ids_covered": 14,
|
||||
@@ -103,6 +105,8 @@ After weighing Codex's argument, we chose to roll back CEO EXPANSION and ship an
|
||||
}
|
||||
```
|
||||
|
||||
`signal_events` counts the logged answers that moved any dimension, and `contributing_events` counts, per dimension, the logged answers that moved it (an answer that writes two deltas to one dimension counts once), so a dimension with no evidence (0) is visible beside one whose evidence balances out; both print 0.5 (#2623). Each value is the sigmoid of its accumulated deltas, and each UTC day's sum per dimension (by the row's `ts`) is clamped to the largest single delta in the signal map (currently ±0.06), so one burst of answers can't pin a dimension; `--trace <dim>` lists every contributing answer, each clamped day and the total. The bound keys on the day, not on `session_id`: the PostToolUse hook and the agent log different ids for the same review (the host's conversation id and the preamble's run id), and an agent can leave the `SESSION_ID` placeholder unsubstituted, so no logged id reliably marks one review. The cost is that several reviews on one day count as one. `contributing_events` still counts an answer logged by both writers twice. Only an ISO 8601 date or date-time `ts` counts as dated (a time without a zone is read as UTC), every other row shares one undated bucket, and `diversity.days_span` counts the same UTC days. Nothing displays `contributing_events` yet (#3436).
|
||||
|
||||
**Diversity check** (Codex #13): `inferred` is considered "enough data" only when `sample_size >= 20 AND skills_covered >= 3 AND question_ids_covered >= 8 AND days_span >= 7`. Below this, `/plan-tune profile` shows "not enough observed data yet" instead of a potentially-misleading inferred value.
|
||||
|
||||
## Data flow (v1)
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
# Releases and update channels
|
||||
|
||||
gstack stamps a version at every merge to main, about a dozen a day. Since
|
||||
late October 2026 there is also a weekly tagged release, and team-mode
|
||||
installs follow it instead of every merge.
|
||||
|
||||
## What a release is
|
||||
|
||||
Every Monday at 07:00 Pacific (06:00 in winter) the **Weekly release**
|
||||
workflow (`.github/workflows/weekly-release.yml`, which runs
|
||||
`scripts/release-cut.ts`) takes the newest green commit on main and:
|
||||
|
||||
1. tags it `v<VERSION>` (the VERSION file at that commit, an annotated tag);
|
||||
2. publishes a GitHub Release whose notes are one line per CHANGELOG version
|
||||
since the previous release, with a compare link and a link to the full
|
||||
CHANGELOG at that tag;
|
||||
3. fast-forwards the `stable` branch to it.
|
||||
|
||||
A tag and its release never change. `stable` is a moving pointer: always a
|
||||
commit on main's first-parent history, moved only by this workflow, only
|
||||
forward.
|
||||
|
||||
**Green** means, on that exact commit: the `free-tests` check run succeeded
|
||||
(the status main requires), the `main-backstop` check run succeeded, every
|
||||
check run on it has completed and none failed, and `refs/notes/gstack-trains`
|
||||
holds no active freeze. The Main backstop gate workflow publishes
|
||||
`main-backstop` on each push to main from a job of its own: success when a
|
||||
green train record covers the tree or the backstop gated it green, neutral
|
||||
when main moved past the push before it was gated (superseded) or a freeze's
|
||||
own backstop run owns it (deferred), failure when it froze the queue or could
|
||||
not finish. Neutral is never green: a commit whose own tree was never gated is
|
||||
not released. A push that gets no backstop run at all (a dependabot push, a
|
||||
policy without a train block) has no `main-backstop` check run and is not
|
||||
green either; a cancelled check run counts as red. A commit with a check run still going is skipped and the next
|
||||
older one is considered, back through 7 days or 200 first-parent commits. The
|
||||
workflow runs no tests and no paid evals of its own; it only reads what the
|
||||
trains and the backstop already recorded. With nothing green in the window it
|
||||
fails and lists why each commit was skipped.
|
||||
|
||||
A cut that stopped half-way (tag and `stable` pushed, release not created) is
|
||||
finished by the next run, by cron or by hand: `stable` itself stays a
|
||||
candidate until its release exists, and its notes compare against the release
|
||||
tag before it.
|
||||
|
||||
## Channels
|
||||
|
||||
| Channel | Follows | Who |
|
||||
|---|---|---|
|
||||
| `stable` | `refs/heads/stable`: the weekly release, plus any hotfix cut off-cycle | team mode (`./setup --team`) by default |
|
||||
| `main` | every merge to main | plain `./setup`, contributors, Capy crew machines |
|
||||
|
||||
The config key is `update_channel` (`bin/gstack-config`):
|
||||
|
||||
```bash
|
||||
gstack-config get update_channel # auto (default) | stable | main
|
||||
gstack-config set update_channel main # a team install that wants every merge
|
||||
gstack-config set update_channel stable # a solo install that wants weekly releases
|
||||
gstack-config unset update_channel # back to auto
|
||||
```
|
||||
|
||||
`auto` resolves to `stable` when `team_mode` is true and to `main` otherwise,
|
||||
so existing team installs moved to weekly releases with nothing to migrate and
|
||||
contributor installs did not move at all. `GSTACK_UPDATE_CHANNEL=stable|main`
|
||||
overrides the key for one command. The resolution lives in one helper,
|
||||
`bin/gstack-update-channel.sh`, used by:
|
||||
|
||||
- `bin/gstack-session-update`, the team-mode session-start hook: it fetches
|
||||
the channel ref and fast-forwards to it. Its log
|
||||
(`<state root>/analytics/session-update.log`) carries `CHANNEL stable target=<sha>`
|
||||
when it follows a release.
|
||||
- `bin/gstack-update-check`, the "update available" line in every skill
|
||||
preamble: it reads `<channel>/VERSION` and compares with `refs/heads/<channel>`,
|
||||
so a stable install is not told every hour that main moved.
|
||||
- `bin/gstack-upgrade` and `/gstack-upgrade`: they fast-forward to the channel
|
||||
ref (`UPSTREAM_REF=origin/stable` in the skill's output) and say which channel
|
||||
they followed.
|
||||
|
||||
Two edge cases are handled on purpose:
|
||||
|
||||
- **Before the first release exists** origin has no `stable`. Every caller then
|
||||
follows main and says so (`CHANNEL_FALLBACK stable->main` in the hook log,
|
||||
`channel stable has no release on origin yet` from `gstack-upgrade`).
|
||||
- **An install already past the release** (it followed main until now, or a
|
||||
hotfix was cut below it) stays where it is until stable passes it. The
|
||||
updater never moves a checkout backwards. A depth-1 clone cannot see that an
|
||||
older release is its ancestor, so the VERSIONs decide: main stamps a higher
|
||||
version at every merge and stable is always a main commit.
|
||||
|
||||
The local branch stays `main` either way. A stable install is a `main`
|
||||
checkout parked at the last released commit, which is why fast-forward-only
|
||||
still holds and nothing about version stamping changed.
|
||||
|
||||
## Cutting a release by hand (hotfixes)
|
||||
|
||||
A hotfix is an ordinary PR: it merges to main through a train and gets its
|
||||
version like any other change. To put it on stable before Monday, run the
|
||||
workflow by hand with the merge commit's SHA:
|
||||
|
||||
```bash
|
||||
gh workflow run weekly-release.yml -f ref=<full sha on main> -f dry_run=false
|
||||
```
|
||||
|
||||
or Actions → **Weekly release** → Run workflow, on main (the cut job runs
|
||||
only from `refs/heads/main`, with main's tooling; `ref` must be the full
|
||||
40-hex SHA). The same green checks apply. The commit is tagged with its VERSION, the release notes say "hotfix release"
|
||||
and list every version since the previous release (everything merged in
|
||||
between ships with it), and stable fast-forwards. Team installs pick it up at
|
||||
their next hourly check.
|
||||
|
||||
There is no release branch and no cherry-picking: stable only ever names main
|
||||
commits. If main carries a change that must not ship yet, revert it on main
|
||||
first, then promote.
|
||||
|
||||
## Dry runs
|
||||
|
||||
Run the workflow with `dry_run=true` (the default for a manual run) to see the
|
||||
plan without changing anything: the candidate, its VERSION and tag, the stable
|
||||
move, how many version lines the notes would carry, every skipped commit with
|
||||
its reason, and the exact commands a real cut would run. The scheduled Monday
|
||||
run is never a dry run.
|
||||
|
||||
```bash
|
||||
gh workflow run weekly-release.yml -f dry_run=true
|
||||
gh run watch
|
||||
```
|
||||
|
||||
The step summary carries the same plan plus the full release notes.
|
||||
|
||||
A pull request that touches `.github/workflows/weekly-release.yml` or
|
||||
`scripts/release-cut.ts` runs the same dry run against main as its own job
|
||||
(`Dry run against main`, read-only token), so a change to the release tooling
|
||||
is exercised against real check runs before it merges.
|
||||
|
||||
## Outcomes
|
||||
|
||||
| Line | Meaning |
|
||||
|---|---|
|
||||
| `CANDIDATE: <sha> (<date>)` | the commit that will be (or was) released |
|
||||
| `NOTHING_NEW: origin/main <sha> is already released as v<X>` | nothing merged since the last release; exit 0 |
|
||||
| `NO_GREEN_COMMIT: ...` with `SKIPPED:` lines | nothing in the window passed the gate; exit 1 (a dry run exits 0: the skips are its answer) |
|
||||
| `REFUSED: non-fast-forward: ...` | the candidate is behind stable; stable only moves forward |
|
||||
| `REFUSED: queue frozen since train ...` | the main backstop froze the queue; clear it first (docs/ship-policy.md) |
|
||||
| `REFUSED: tag v<X> already names <other sha>` | a version was stamped twice; needs a look before anything is cut |
|
||||
| `TAG:`, `STABLE:`, `RELEASE:` | what the cut did; a re-run reports each step as already done and finishes a step an earlier run did not reach |
|
||||
+2
-2
@@ -462,7 +462,7 @@ You know the feeling. You have a feature, a page, a landing screen... and you're
|
||||
3. It generates 3 distinct design variants as PNGs
|
||||
4. A comparison board opens in your browser with all 3 side-by-side
|
||||
5. You click "Approve" on the one you like, or give feedback for another round
|
||||
6. The approved variant saves to `~/.gstack/projects/$SLUG/designs/` with an `approved.json`
|
||||
6. The approved variant saves to `~/.gstack/projects/$SLUG/designs/` with an `approved.json`; for a landing, pricing or campaign screen, it also records the page archetype you picked (`classic-hero`, `long-form-story`, `minimal-conversion` or `comparison`) and its section outline
|
||||
|
||||
That `approved.json` is one way to feed `/design-html`. The design pipeline chains: shotgun picks the direction, design-html renders it as working code. But `/design-html` also works with CEO plans, design reviews, or just a description.
|
||||
|
||||
@@ -512,7 +512,7 @@ Not every page needs the full Pretext engine. The skill reads the design and pic
|
||||
|
||||
### The refinement loop
|
||||
|
||||
1. Reads the approved mockup from `approved.json`
|
||||
1. Reads the approved mockup from `approved.json`, plus any page archetype and section outline you picked on the board, which it builds as given instead of asking again
|
||||
2. Uses GPT-4o vision to extract implementation spec (colors, typography, layout)
|
||||
3. Generates self-contained HTML with Pretext inlined (15KB, zero network dependency)
|
||||
4. Spins up a live-reload server so you see changes instantly
|
||||
|
||||
Reference in new issue
Block a user