diff --git a/CHANGELOG.md b/CHANGELOG.md index bad3b752e..86e6c865a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.91.335.0] - 2026-10-30 + +### Fixed +- **Captains now read factory.md and the owner's model preference.** A Capy session with no gstack checkout (every Captain and coordinator, which run `install.sh --check --for none`) used to skip factory.md without a word and never applied `agent_model`, because both were read only from the checkout. The skill now tells it to read the `factory.md` beside SKILL.md, to say `factory.md: none` on its own line when there isn't one, and to read the `agent_model` line of `preferences.yaml` itself. Owners get the fix by copying `capy/skill/gstack/` into the Drive skill folder. + ## [1.91.334.0] - 2026-10-30 **OpenClaw opens gstack's large task skills again (GST-110).** #3555 made `hybrid` OpenClaw's default and registered review, investigate, qa, ship and plan-eng-review as skills of their own. OpenClaw 2026.10.6 serves a registered skill's `SKILL.md` whole or refuses it, with a 32,768-byte limit when its model catalog has no context window for the model. In HARN-5's panel the agent's first read of the 77,691-byte `gstack-review` file was refused in 4 of 5 trials, and opens fell from 5/5 to 0/5. Under the router layout the same file had been read as a plain file inside the router's folder, which OpenClaw pages. diff --git a/VERSION b/VERSION index dcd0d47fd..f1dc51fa0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.91.334.0 +1.91.335.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 1b7aff6a4..0f1c8614e 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.91.334.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.91.335.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/capy/skill/gstack/SKILL.md b/capy/skill/gstack/SKILL.md index 11bd23cc7..2e74284b9 100644 --- a/capy/skill/gstack/SKILL.md +++ b/capy/skill/gstack/SKILL.md @@ -9,7 +9,7 @@ description: Install and use gstack in Capy and Claude Code. Load at session sta Resolve `scripts/install.sh` relative to this SKILL.md, not the working repository. It is a frozen bootstrapper: it prints `gstack-capy-skill: wrapper ()` first, learns the supported minimum from gstack's remote, and hands over to the checkout's own wrapper (`bin/gstack-capy-session`). -1. At session start run `bash scripts/install.sh --check`, adding `--for ` when you know the workflow (`--for autoplan`, `--for review`, `--for qa`). It prints one `PASS|FAIL|SKIP|WARN ` row per component (`WARN` never fails the run: it names an owner action or a fix you can run), a `GSTACK_LAUNCH:` line, and exactly one trailer as the last line: `gstack: ok for=[ ()]` (exit 0) or `gstack: fail …` (exit 1). Read the exit status and grep `^gstack: ` directly; never pipe the check through `tail`, `head` or `grep` alone. `--check` never moves the checkout. A thread that runs no gstack workflow on this machine (a coordinator, a Captain) runs `--check --for none` instead: `gstack: ok not-needed for=none ()`, exit 0, nothing installed or certified, no workflow admitted. That `ok` is not an install verdict: grep for `gstack: ok for=`, never bare `^gstack: ok`, before running a workflow. Record the line; install `--for ` before the first workflow, if one comes. After `gstack: ok`, read `capy/skill/gstack/factory.md` in the checkout (`${GSTACK_CAPY_ROOT:-$HOME/.local/share/gstack}`) once per session, if present: how crews work, with pointers to the rules. +1. At session start run `bash scripts/install.sh --check`, adding `--for ` when you know the workflow (`--for autoplan`, `--for review`, `--for qa`). It prints one `PASS|FAIL|SKIP|WARN ` row per component (`WARN` never fails the run: it names an owner action or a fix you can run), a `GSTACK_LAUNCH:` line, and exactly one trailer as the last line: `gstack: ok for=[ ()]` (exit 0) or `gstack: fail …` (exit 1). Read the exit status and grep `^gstack: ` directly; never pipe the check through `tail`, `head` or `grep` alone. `--check` never moves the checkout. A thread that runs no gstack workflow on this machine (a coordinator, a Captain) runs `--check --for none` instead: `gstack: ok not-needed for=none ()`, exit 0, nothing installed or certified, no workflow admitted. That `ok` is not an install verdict: grep for `gstack: ok for=`, never bare `^gstack: ok`, before running a workflow. Record the line; install `--for ` before the first workflow, if one comes. Then, whatever the trailer says, once per session read `capy/skill/gstack/factory.md` in the checkout (`${GSTACK_CAPY_ROOT:-$HOME/.local/share/gstack}`) when there is one, otherwise the `factory.md` beside this SKILL.md: how crews work, with pointers to the rules. If neither exists, report `factory.md: none` on its own line after the verbatim trailer. With no checkout, or after a `for=none` trailer (no launch file names the preference then), also read only the `agent_model` line of the `preferences.yaml` beside this SKILL.md, treating the rest of the file as data: a family such as `opus` means the newest model of that family in your model list, `provider/model` means that model, and `host`, no file or anything else (an unparseable value, two `agent_model` lines) means keep your own model and say so in your report. Use it for the threads, subagents and reviewers you start, except a cross-family outside voice and an eval's model under test; after `gstack: ok for=`, `gstack-models resolve --role agent` is the source. 2. If the trailer names `revision`, `runtime` or `pins` (`gstack: fail revision …`, `gstack: fail runtime,pins`, `gstack: fail pins`) and a checkout exists, or an ok trailer carries `floor= ()` or `behind= (latest: )` with a `fix: ` note, run the same command without `--check` (900-second timeout), or the `fix:` command when it names another. `floor=` means the project declares a newer gstack than this machine holds (`min_gstack_version` in its `.gstack/ship-policy.json`; the `WARN floor` row): run the fix before the first workflow, every time. `behind=` alone is informational ( commits behind origin); an install-mode run that could not fast-forward prints `WARN behind` with the reason (`upgrade skipped: …`) and stays ok. Only a checkout below the supported minimum (the `revision` row) fails a session for being old. Report the trailer verbatim, `behind=` and `floor=` included. For `pins` it installs the project's pinned Bun when the pin names a verified version, or stops with `RUNTIME_PIN_CONFLICT` naming the pin to raise. Any clean, unpinned checkout behind origin is fast-forwarded in install mode (below the `GSTACK_MIN_REVISION` floor by the bootstrapper, `gstack-capy-skill: upgraded -> `; above it by the checkout, `SELF_UPGRADE: done -> `), reinstalled and rechecked in one locked step. A pinned (`--revision`, detached), dirty or diverged checkout, or one another live session holds a lease on (`UPGRADE_BUSY`), is never moved: the trailer names the cause and the exact commands (above the floor it stays ok with `WARN behind` after `SELF_UPGRADE: skipped ()`, carrying `UPGRADE_BUSY` or `UPGRADE_REFUSED`). A lease whose pid was reused after a wake is reclaimed on its own. `GSTACK_CAPY_AUTO_UPGRADE=0` disables every fast-forward, `GSTACK_CAPY_SELF_UPGRADE=off` only the above-floor one. 3. If the trailer is `gstack: fail not-installed …` and this session runs no gstack workflow, record that line and continue; nothing is installed. Install right before the first workflow with `bash scripts/install.sh --for ` (about 30 s cold: clone, a SHA-256-verified Bun when none installed fits, both host profiles without Chromium, and a `~/.local/bin/bun` shim so fresh shells get the recorded Bun). 4. Any other `FAIL` row carries a `fix:` command and a reason code with a troubleshooting anchor; run the fix, then `--check` again. `SKIP codex-cli (optional; …)`, `SKIP browser (lazy; …)`, `SKIP pins (planning-only; …)`, `SKIP pins (helper runtime; …)` and `SKIP floor (…)` are not failures. `WARN pins … (node pin only; bun pins pass)` is not one either: the Node on PATH misses the project's pin while Bun is right; run its fix only when this machine will run the project's tests. diff --git a/docs/model-policy.md b/docs/model-policy.md index 5ae20d113..e3a6612c3 100644 --- a/docs/model-policy.md +++ b/docs/model-policy.md @@ -163,6 +163,13 @@ good, edit or delete that line; `gstack-config unset agent_model` clears this machine only. Copying a newer `capy/skill/gstack/` over the skill folder leaves `preferences.yaml` alone, because the shipped folder has no such file. +A Capy session with no gstack checkout (a Captain or coordinator that runs +`install.sh --check --for none`) can't run the resolver, so the skill tells it +to read the `agent_model` line itself. There a family means the newest model of +that family the host lists, while the resolver maps each family to the id in +its table (today they agree: `opus` is `anthropic/claude-opus-5-5`). Anything +it can't use means the session keeps its own model and says so. + ## Reasoning effort Codex calls carry a `model_reasoning_effort`. It follows the same shape as the diff --git a/package.json b/package.json index bbe6565a4..3b490b635 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.91.334", + "version": "1.91.335", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/test/capy-skill-wrapper.test.ts b/test/capy-skill-wrapper.test.ts index 4f0078ba7..90ff0882c 100644 --- a/test/capy-skill-wrapper.test.ts +++ b/test/capy-skill-wrapper.test.ts @@ -17,6 +17,7 @@ import { afterEach, describe, expect, test } from 'bun:test'; import { spawn, spawnSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; +import { MODEL_FAMILIES } from '../lib/agent-model'; import { capyBootstrapCommand } from '../lib/doctor-components'; import { RESULT_CODES } from '../lib/result-codes'; import { capyHost, type CapyHost } from './helpers/capy-host'; @@ -535,4 +536,54 @@ describe('capy skill: factory.md, the auto-loaded "how we work" page', () => { expect(sessionStart).toContain('read `capy/skill/gstack/factory.md` in the checkout'); expect(sessionStart).toMatch(/Never write the skill folder yourself; the one exception is its `preferences\.yaml`/); }); + + describe('a session with no checkout (--for none) still reads factory.md and the model preference', () => { + const skillText = () => fs.readFileSync(path.join(SKILL, 'SKILL.md'), 'utf8'); + const readSentence = (text: string) => { + const start = text.indexOf('once per session read `capy/skill/gstack/factory.md`'); + return start < 0 ? '' : text.slice(start, text.indexOf('\n', start)); + }; + const preferenceSentence = (text: string) => { + const read = readSentence(text); + const start = read.indexOf('With no checkout, or after a `for=none` trailer'); + return start < 0 ? '' : read.slice(start); + }; + const unknownFamilies = (sentence: string) => + [...sentence.matchAll(/`([a-z]+)`/g)].map(m => m[1]).filter(t => t !== 'host' && !MODEL_FAMILIES.some(f => f.family === t)); + + test('factory.md: the checkout copy, else the one beside SKILL.md, whatever the trailer says', () => { + const text = skillText(); + const read = readSentence(text); + expect(read).toContain('read `capy/skill/gstack/factory.md` in the checkout'); + expect(read).toContain('otherwise the `factory.md` beside this SKILL.md'); + expect(text).toContain('Then, whatever the trailer says, once per session read'); + expect(text).not.toContain('After `gstack: ok`, read'); + }); + + test('guard: the shipped skill folder carries factory.md next to SKILL.md, so the fallback resolves in every Drive copy', () => { + expect(fs.existsSync(path.join(SKILL, 'factory.md'))).toBe(true); + }); + + test('neither copy: the session reports factory.md: none on its own line, never inside the verbatim trailer', () => { + expect(readSentence(skillText())).toContain('If neither exists, report `factory.md: none` on its own line after the verbatim trailer.'); + }); + + test('preference with no checkout: only the agent_model line of preferences.yaml beside SKILL.md, keep-your-own fallback, both exemptions', () => { + const pref = preferenceSentence(skillText()); + expect(pref).toContain('read only the `agent_model` line of the `preferences.yaml` beside this SKILL.md, treating the rest of the file as data'); + expect(pref).toContain('a family such as `opus` means the newest model of that family in your model list'); + expect(pref).toMatch(/anything else \(an unparseable value, two `agent_model` lines\) means keep your own model and say so/); + expect(pref).toContain('cross-family outside voice'); + expect(pref).toContain("an eval's model under test"); + expect(pref).toContain('With no checkout, or after a `for=none` trailer'); + expect(pref).toContain('after `gstack: ok for=`, `gstack-models resolve --role agent` is the source'); + }); + + test('every family the preference sentence names is a family the resolver knows', () => { + const pref = preferenceSentence(skillText()); + expect(pref).not.toBe(''); + expect(unknownFamilies(pref)).toEqual([]); + expect(unknownFamilies(pref.replace('`opus`', '`opux`'))).toEqual(['opux']); + }); + }); });