mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-12 03:30:28 +01:00
Merge commit '1ccab0d8bccba50e69587672fb82ac0574638a65' into gsta-260/playwright-1.64.1
This commit is contained in:
commit
0f7641432b
133 files changed
+1414
-240
No files matched your search
@@ -1,5 +1,54 @@
|
||||
# Changelog
|
||||
|
||||
## [1.94.24.0] - 2026-11-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The OpenClaw and Hermes start hook no longer breaks a fresh harness.** When the harness has no config yet, gstack runs none of its commands: `openclaw config set` and `hermes plugins enable` would have created a config holding only gstack's plugin setting, with no model, provider or terminal settings. The hook reports `written` with the setup to run first (`openclaw onboard`, `hermes setup`), and the next `./setup` or `gstack-harness refresh` turns it on, changing only the plugin entry. Turning the hook off or unregistering in such a profile also leaves the config alone. If v1.92.23.0 or a later release before this fix already created a config holding only the plugin entry, move it aside, run the harness's own setup, then `gstack-harness refresh`.
|
||||
- **`OPENCLAW_CONFIG_PATH` naming the profile's own `openclaw.json` keeps the start hook on.** gstack compares the path after resolving symlinks, so only a config outside the profile turns the hook off. When `GSTACK_HARNESS_START_HOOK` or `harness_start_hook` asked for the hook and it ends up off, `gstack-harness register` and `./setup --host` now exit 4 with the reason and the fix instead of exiting 0.
|
||||
- **`gstack-harness status` reads the hook from the harness's config when the harness CLI is not on `PATH`**, as inside Hermes's or OpenClaw's own terminal tool. Hermes's `config.yaml` settles it (`active`, `disabled` or `written`); otherwise status says `unknown` instead of a wrong `written`.
|
||||
|
||||
## [1.94.23.0] - 2026-11-03
|
||||
|
||||
**Two `gstack-pregate` preflight false positives fixed (GST-1211).** Both stopped /ship on real work in gbrain-evals.
|
||||
|
||||
### Fixed
|
||||
- **`strays`:** the scratch-name check matched three prefixes the docs never listed (`temp`, `debug-` and `probe`), so real files like `probe.py` and `temporal-graph-shipped.test.ts` failed preflight. It now matches only the documented names: `zz-*`, `tmp*`, `scratch*`, plus editor leftovers (`*.orig`, `*.rej`, `*.swp`, `*.swo`, `*.bak`, `*.tmp`, `*~`, `.DS_Store`, `Thumbs.db`), which docs/pregate.md and docs/troubleshooting.md now list too.
|
||||
- **`literals`:** a named literal the branch only adds, such as `limit: 300` in a new object, was read as an existing `limit: 10` elsewhere in the file changing to 300, and every test that mentioned 10 was flagged. The check now compares every value under a name, so an added literal isn't a change. Editing an existing value still fails, and so do two cases the old last-value check could miss: changing one of two equal values under the same name, and deleting one site while editing another.
|
||||
|
||||
### Tests
|
||||
- `test/pregate-selection.test.ts`: `probe.py` and `temporal-*.test.ts` pass `strays` while `zz-notes.md`, `tmp-x.ts`, `scratch.py` and `util.js.orig` still fail; a new object literal beside `limit: 10` passes `literals` while editing that value still fails; the pure `changedLiterals` case keeps an exported constant change.
|
||||
|
||||
## [1.94.22.0] - 2026-11-03
|
||||
|
||||
**/investigate, /review, /qa and /verify now say when to use them, and /investigate and /review declare what "done" means (GST-1088 lane B).** Untold, Opus 5.5 never opened /investigate in 252 trials (Run D, gstack-evals#140). A when-to-use description raised GPT-6.1 Sol's open rate from 9.5% to 27.0% in the same run. Claude Code's catalog shows only a description's first sentence, so each of the four now opens with a "Use when ..." sentence. The routing hook and the contract check that read the new frontmatter landed in v1.94.6.0 (lane A, off by default; see docs/skill-routing-and-contracts.md), so the "Done means" list now also names `gstack-skill-contract check` and `waive`.
|
||||
|
||||
### Changed
|
||||
- **Descriptions**: the first sentence of each is now a when-clause, for example /investigate's "Use when a test fails, code errors or output is wrong: find the root cause, then fix it." Hosts that show the full description also get the longer list of situations. The catalog stays within its existing budget.
|
||||
- **Triggers**: /investigate, /review and /verify list the prompts people actually type, such as "test fails", "look over my diff" and "make it green". /qa keeps its three triggers because its size cap had 9 bytes of room.
|
||||
|
||||
### Added
|
||||
- **`contract:` frontmatter** (frozen v1 schema) on /investigate (the DEBUG REPORT labels, and a test or repro run after the last edit) and /review (one Agent dispatch per selected specialist, the adversarial pass, the Pre-Landing Review report). `{{CONTRACT_DONE_MEANS}}` (`scripts/resolvers/contract-done-means.ts`) renders each block as a short "Done means" list at the top of the skill, so the list and the block can't drift.
|
||||
|
||||
### Tests
|
||||
- `test/skill-descriptions.test.ts`: each of the four opens with a when-clause within the catalog, lead and host caps; triggers are short and don't overlap; ten sample prompts match exactly the intended skill; every template's contract block passes the v1 schema and names markers the skill text actually prints; the renderer covers each item kind; and malformed blocks fail with the file and the fix.
|
||||
|
||||
## [1.94.21.0] - 2026-11-03
|
||||
|
||||
- **A stacked PR's /ship receipt now carries even when the child merged main before its receipt was written (GST-1214).** After the parent squash-landed and main was merged in again, `gstack-ship-receipt carry` compared the range from the parent's head, which still held every file the child's first main merge brought in, and refused with `RECEIPT_CARRY_PRODUCT_CHANGED` naming those files. The carry now leaves out paths whose content at the reviewed head is main's, unless HEAD changed them. A real edit to one of those files still refuses.
|
||||
|
||||
## [1.94.20.0] - 2026-11-03
|
||||
|
||||
### Changed
|
||||
|
||||
- **The review pins and the harness pins are one file per pin, so PRs that repin different files no longer conflict (GST-1084).** The reviewer-brief pins that lived in one table in `test/review-packets-removed.test.ts` now live under `test/fixtures/review-pins/`: `files/<pinned path>.json` holds a file's sha and the replay that admits it, `replays/<slug>.json` holds one replay's written no-loss justification, and `brief-bodies/<name>.json` holds one brief body's sha. The harness pins in `scripts/harness-version.json` moved to `scripts/harness-pins/<path>.json`; the manifest keeps only the version and the patterns, so `HARNESS_VERSION` and its readers are unchanged. Before, two PRs that repinned adjacent entries conflicted on the shared table (at least five main-merge conflicts on 2026-11-02, two of them on #3852). Every entry is ported unchanged, including the combined GST-508 entries from #3852.
|
||||
|
||||
### For contributors
|
||||
|
||||
- `/ship`'s post-review route still counts a non-behavioral harness re-pin as release metadata (GST-1031): it now reads the pin files under `scripts/harness-pins/` and refuses a manifest that regains a `files` table.
|
||||
- The checks are at least as strict as before. Every pin still needs its sha and a replay whose written justification names a no-loss replay or re-score, and the brief-body check still guards the bodies. A malformed or misnamed pin file, a stray file in a pin directory, or a `files` block back in `scripts/harness-version.json` now fails too. New tests prove that a stale pin still fails, and that two branches repinning different, adjacent pins merge cleanly with a real `git merge-tree`, while two repins of one pin still conflict.
|
||||
- `bun run scripts/bump-harness-version.ts` writes and deletes pin files itself. Repin a review file by editing its `files/` pin and adding the replay file it names.
|
||||
|
||||
## [1.94.19.0] - 2026-11-03
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.94.19.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.94.24.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,
|
||||
|
||||
@@ -74,9 +74,10 @@ _gstack_harness_cut_block() {
|
||||
# plugins.entries.gstack-start; Hermes removes the plugin and its config entries when its
|
||||
# folder holds exactly the files gstack wrote, unedited, and is reached without a symlink
|
||||
# below the profile; otherwise it only disables it so an edit or a kept copy survives. The harness runs with PATH, HOME and the profile only. Without the harness on
|
||||
# PATH it prints the command to run instead.
|
||||
# PATH it prints the command to run instead. A profile with no harness config (openclaw.json, config.yaml) never had
|
||||
# the plugin turned on, and the harness CLI would create a config holding only that change, so it runs no harness command.
|
||||
gstack_harness_hook_release() {
|
||||
local ledger="$1" kind a b target="" on="" profile="" root="" edited=0 to="" sub cmd n=0 safe=1 total
|
||||
local ledger="$1" kind a b target="" on="" profile="" root="" edited=0 to="" sub cmd cfg n=0 safe=1 total
|
||||
[ -f "$ledger" ] || return 0
|
||||
while IFS=$'\t' read -r kind a b; do
|
||||
case "$kind" in
|
||||
@@ -91,6 +92,8 @@ gstack_harness_hook_release() {
|
||||
[ "${total:-0}" -gt "$n" ] && edited=1
|
||||
fi
|
||||
command -v timeout >/dev/null 2>&1 && to="timeout 60"
|
||||
case "$target" in openclaw) cfg=openclaw.json ;; hermes) cfg=config.yaml ;; *) cfg="" ;; esac
|
||||
if [ -z "$cfg" ] || [ -f "$profile/$cfg" ]; then
|
||||
case "$target" in
|
||||
openclaw) cmd="config unset plugins.entries.gstack-start"
|
||||
if command -v openclaw >/dev/null 2>&1 && env -i PATH="$PATH" HOME="$HOME" OPENCLAW_STATE_DIR="$profile" $to openclaw $cmd </dev/null >/dev/null 2>&1; then :
|
||||
@@ -99,6 +102,7 @@ gstack_harness_hook_release() {
|
||||
if command -v hermes >/dev/null 2>&1 && env -i PATH="$PATH" HOME="$HOME" HERMES_HOME="$profile" $to hermes plugins "$sub" gstack-start </dev/null >/dev/null 2>&1; then :
|
||||
else echo " start hook: turn it off in Hermes yourself: HERMES_HOME=\"$profile\" hermes plugins disable gstack-start"; fi ;;
|
||||
esac
|
||||
fi
|
||||
if [ "$safe" = 1 ] && [ -d "$root/__pycache__" ] && _gstack_harness_owned "$root/__pycache__" "$root"; then
|
||||
rm -f "$root"/__pycache__/*.pyc
|
||||
rmdir "$root/__pycache__" 2>/dev/null
|
||||
|
||||
+13
-5
@@ -666,11 +666,19 @@ function cmdCarry(rest: string[], json: boolean): number {
|
||||
// receipt written against the base while stacked reviewed the parent's hunks too, and the squash moved them into the
|
||||
// base (GST-1199). A parent counts only when merging it into the current base changes no product path.
|
||||
const measure = (rangeBase: string) => {
|
||||
const listed = [touchedPaths(repoRoot, rangeBase, from), touchedPaths(repoRoot, headBase, head)];
|
||||
if (!listed[0] || !listed[1]) return `git could not list the paths the branch touched against ${base}; run /ship on ${head.slice(0, 12)}`;
|
||||
const touched = new Set([...listed[0], ...listed[1]]);
|
||||
const spec = productSpec(repoRoot, rules, from, head, { touched, sides: [[rangeBase, from], [headBase, head]], own: branchSides(repoRoot, baseRef, from, '') });
|
||||
if (!spec) return `git diff ${from.slice(0, 12)} ${head.slice(0, 12)} failed`;
|
||||
const listed = [touchedPaths(repoRoot, rangeBase, from), touchedPaths(repoRoot, headBase, head), rangeBase === fromBase ? new Set<string>() : touchedPaths(repoRoot, fromBase, from)];
|
||||
if (!listed[0] || !listed[1] || !listed[2]) return `git could not list the paths the branch touched against ${base}; run /ship on ${head.slice(0, 12)}`;
|
||||
// From a stack parent, a path whose reviewed content is the base's (an earlier base merge brought it in) is base
|
||||
// context, not reviewed product, unless HEAD changed it against the base (GST-1214).
|
||||
// A pathspec also names everything beneath a path, so a path with any compared path under it (a file the base turned
|
||||
// into a directory) stays in.
|
||||
const compared = [...listed[0], ...listed[1]];
|
||||
const baseMerged = rangeBase === fromBase ? [] : [...listed[0]].filter(p => !listed[2]!.has(p) && !listed[1]!.has(p) && !compared.some(q => q.startsWith(`${p}/`)));
|
||||
const touched = new Set([...listed[0], ...listed[1]].filter(p => !baseMerged.includes(p)));
|
||||
const found = productSpec(repoRoot, rules, from, head, { touched, sides: [[rangeBase, from], [headBase, head]], own: branchSides(repoRoot, baseRef, from, '') });
|
||||
if (!found) return `git diff ${from.slice(0, 12)} ${head.slice(0, 12)} failed`;
|
||||
const skip = baseMerged.map(p => `:(exclude,literal)${p}`);
|
||||
const spec = { ...found, product: [...found.product, ...skip], tests: [...found.tests, ...skip] };
|
||||
const ids = [patchId(repoRoot, rangeBase, from, spec.product), patchId(repoRoot, headBase, head, spec.product)];
|
||||
for (const r of ids) if (typeof r !== 'string') return `git could not compute the product patch-id at one of the heads: ${r.error}`;
|
||||
return { rangeBase, touched, spec, productFrom: ids[0] as string, productHead: ids[1] as string };
|
||||
|
||||
@@ -1001,8 +1001,10 @@ paid test file, its fixtures and its skills' prompt files, including the
|
||||
generated SKILL.md) plus `HARNESS_VERSION`, keyed with model, Claude CLI version
|
||||
and policy version (`scripts/eval-trial-series.ts`). Each trial also records the
|
||||
full consumed-input fingerprint as provenance. `scripts/harness-version.json`
|
||||
pins every shared-harness file (`GLOBAL_TOUCHFILES` plus the session runners, the
|
||||
PTY harness, the e2e gate and the LLM judge) by blob hash. When
|
||||
holds the version and the harness patterns, and `scripts/harness-pins/<path>.json`
|
||||
pins each shared-harness file (`GLOBAL_TOUCHFILES` plus the session runners, the
|
||||
PTY harness, the e2e gate and the LLM judge) by blob hash, one file per pin, so
|
||||
two PRs that record decisions for different harness files never conflict. When
|
||||
`test/harness-version.test.ts` fails, run one of:
|
||||
|
||||
```bash
|
||||
@@ -1010,8 +1012,8 @@ bun run scripts/bump-harness-version.ts --non-behavioral "<why no verdict can ch
|
||||
bun run scripts/bump-harness-version.ts --bump "<what behavior changed>" # new series for every case
|
||||
```
|
||||
|
||||
On a merge conflict in the manifest, take the higher version, then rerun the
|
||||
script.
|
||||
On a merge conflict in the manifest or a pin file, take the higher version, then
|
||||
rerun the script.
|
||||
|
||||
## Ubicloud VMs (`bun run test:ubicloud`)
|
||||
|
||||
|
||||
+6
-4
@@ -80,14 +80,16 @@ It is off unless you turn it on, because turning it on changes the harness's own
|
||||
|---|---|---|
|
||||
| Plugin folder | `${OPENCLAW_STATE_DIR:-~/.openclaw}/extensions/gstack-start` (index.js, openclaw.plugin.json, package.json, note.txt) | `${HERMES_HOME:-~/.hermes}/plugins/gstack-start` (plugin.yaml, __init__.py, note.txt) |
|
||||
| Hook | `before_prompt_build`, returning the note as `prependContext` | `pre_llm_call`, returning the note as `context` |
|
||||
| Settings it changes, through the harness's own CLI | `openclaw config set plugins.entries.gstack-start.hooks.allowConversationAccess true` and `...enabled true` in `openclaw.json`. OpenClaw runs `before_prompt_build` from a plugin it did not bundle only with that grant, which also lets the plugin's hooks see conversation content (the plugin reads only its own note.txt) | `hermes plugins enable gstack-start` adds it to `plugins.enabled` in `config.yaml` (and creates `config.yaml` when there is none) |
|
||||
| Settings it changes, through the harness's own CLI | `openclaw config set plugins.entries.gstack-start.hooks.allowConversationAccess true` and `...enabled true` in `openclaw.json`. OpenClaw runs `before_prompt_build` from a plugin it did not bundle only with that grant, which also lets the plugin's hooks see conversation content (the plugin reads only its own note.txt) | `hermes plugins enable gstack-start` adds it to `plugins.enabled` in `config.yaml` |
|
||||
| Check | `openclaw plugins inspect gstack-start --runtime --json` lists `before_prompt_build` under `typedHooks` | `hermes plugins list` shows `gstack-start` enabled |
|
||||
|
||||
**Turn it on.** `gstack-harness register openclaw --start-hook` (or `hermes`), or `gstack-config set harness_start_hook on` followed by `./setup`, or `GSTACK_HARNESS_START_HOOK=1 ./setup --host openclaw`. The first of these that is set decides: the `--start-hook`/`--no-start-hook` flag, then `GSTACK_HARNESS_START_HOOK` (`1`/`on` or `0`/`off`), then `harness_start_hook`, then the choice saved in the registration's ledger. Whatever decides, the choice is saved, so refresh and upgrades keep it. The hook goes only into the harness profile's own plugin folder; a registration with a custom `--skills-dir`, or OpenClaw run with `OPENCLAW_CONFIG_PATH` pointing at another config, gets no hook (`HARNESS_HOOK_UNSUPPORTED` when you ask for one). A hook you already turned on stays on when a later run happens to have `OPENCLAW_CONFIG_PATH` set.
|
||||
**Turn it on.** `gstack-harness register openclaw --start-hook` (or `hermes`), or `gstack-config set harness_start_hook on` followed by `./setup`, or `GSTACK_HARNESS_START_HOOK=1 ./setup --host openclaw`. The first of these that is set decides: the `--start-hook`/`--no-start-hook` flag, then `GSTACK_HARNESS_START_HOOK` (`1`/`on` or `0`/`off`), then `harness_start_hook`, then the choice saved in the registration's ledger. Whatever decides, the choice is saved, so refresh and upgrades keep it. The hook goes only into the harness profile's own plugin folder; a registration with a custom `--skills-dir`, or OpenClaw run with `OPENCLAW_CONFIG_PATH` pointing at another config, gets no hook (`HARNESS_HOOK_UNSUPPORTED` with `--start-hook`; with `GSTACK_HARNESS_START_HOOK` or `harness_start_hook`, `register` and `./setup --host` say why and exit 4). `OPENCLAW_CONFIG_PATH` naming the profile's own `openclaw.json`, by any path that resolves to it, is fine. A hook you already turned on stays on when a later run happens to have `OPENCLAW_CONFIG_PATH` set.
|
||||
|
||||
**Check it.** The receipt and `gstack-harness status` show `start hook: active` (configured for new sessions; a running OpenClaw gateway or Hermes gateway picks it up when it reloads or restarts), `written` (files in place but not active, with the reason and the command that fixes it), `disabled` (you turned it off in the harness), `unchecked` (a run that left a hook already on alone, or `status --no-check`; `gstack-harness status` checks it) or `off`. When the run that turns the hook on (or `--start-hook`) leaves it inactive, `gstack-harness register` and `./setup --host` exit 4 after registering the skills (`HARNESS_HOOK_INACTIVE`). Later refreshes and upgrades try once more each time, without failing, until the hook is active or you disable the plugin in the harness.
|
||||
**Run the harness's own setup first.** gstack turns the hook on only in a harness that already has its config (`openclaw.json` from `openclaw onboard`, `config.yaml` from `hermes setup`). Without one, the harness CLI would create a config holding nothing but gstack's plugin setting, with no model, provider or terminal settings, so gstack runs no harness command: the plugin's files are written, the hook reports `written` with the setup to run, and `register` and `./setup --host` exit 4. Once the harness is set up, the next `./setup` or `gstack-harness refresh` turns the hook on, changing only the `gstack-start` plugin entry.
|
||||
|
||||
**Turn it off.** `gstack-harness register openclaw --no-start-hook` (or `hermes`), or `gstack-config set harness_start_hook off` followed by `./setup`. Either turns the plugin off through the harness's CLI and removes its folder. You can also switch it off in the harness (`openclaw config set plugins.entries.gstack-start.enabled false`, `hermes plugins disable gstack-start`); gstack then leaves it off and never turns it back on unless you pass `--start-hook` again. `gstack-harness unregister` and `gstack-uninstall` remove it with the rest (Hermes only disables it when its folder holds an edited file or a kept `.edited-*` copy, so your edit survives), and print the harness command to run when the harness CLI is not on `PATH`.
|
||||
**Check it.** The receipt and `gstack-harness status` show `start hook: active` (configured for new sessions; a running OpenClaw gateway or Hermes gateway picks it up when it reloads or restarts), `written` (files in place but not active, with the reason and the command that fixes it), `disabled` (you turned it off in the harness), `unknown` (the harness CLI is not on `PATH`, as inside OpenClaw's or Hermes's own terminal tool, and the config file cannot settle it; gstack reads `plugins.enabled` and `plugins.disabled` from Hermes's `config.yaml` and the plugin entry from `openclaw.json` instead), `unchecked` (a run that left a hook already on alone, or `status --no-check`; `gstack-harness status` checks it) or `off`. When the run that turns the hook on (or `--start-hook`) leaves it inactive, `gstack-harness register` and `./setup --host` exit 4 after registering the skills (`HARNESS_HOOK_INACTIVE`). Later refreshes and upgrades try once more each time, without failing, until the hook is active or you disable the plugin in the harness.
|
||||
|
||||
**Turn it off.** `gstack-harness register openclaw --no-start-hook` (or `hermes`), or `gstack-config set harness_start_hook off` followed by `./setup`. Either turns the plugin off through the harness's CLI and removes its folder. You can also switch it off in the harness (`openclaw config set plugins.entries.gstack-start.enabled false`, `hermes plugins disable gstack-start`); gstack then leaves it off and never turns it back on unless you pass `--start-hook` again. `gstack-harness unregister` and `gstack-uninstall` remove it with the rest (Hermes only disables it when its folder holds an edited file or a kept `.edited-*` copy, so your edit survives), and print the harness command to run when the harness CLI is not on `PATH`. In a profile with no harness config yet (no `openclaw.json` or `config.yaml`), the hook was never turned on, so turning it off or unregistering runs no harness command and leaves the config alone.
|
||||
|
||||
**What it costs.** The note is about 50 tokens and lands on every user turn, and each copy stays in the session's history, so a long session pays more (mostly cache reads). HARN-18's panel measures the added cost per trial. OpenClaw also logs that it cannot verify where the plugin came from, because gstack writes it instead of installing it from npm or ClawHub.
|
||||
|
||||
|
||||
+5
-2
@@ -201,9 +201,12 @@ Two stages with separate clocks, both printed:
|
||||
file is read whole from disk. A file over the size limit, or whose full diff
|
||||
does not parse, is scanned on its added lines alone, without that context.
|
||||
- `strays`: a new file at the repository root, a new top-level directory, or a
|
||||
scratch name (`zz-*`, `tmp*`, `scratch*`) anywhere.
|
||||
scratch name (`zz-*`, `tmp*`, `scratch*`) or editor leftover (`*.orig`,
|
||||
`*.rej`, `*.swp`, `*.swo`, `*.bak`, `*.tmp`, `*~`, `.DS_Store`, `Thumbs.db`)
|
||||
anywhere.
|
||||
- `literals`: for every changed exported constant or named timeout/retry
|
||||
literal, the whole test tree (nightly- and Heavy-only files included) is
|
||||
literal (a named literal the branch only adds, such as `limit: 300` in a new
|
||||
object, is not a change), the whole test tree (nightly- and Heavy-only files included) is
|
||||
searched for the old value in files that name the constant or, when the old
|
||||
value is longer than one character, its module (the module's file name not
|
||||
followed by another extension, or a path ending
|
||||
|
||||
+4
-1
@@ -1536,7 +1536,10 @@ the reviewed range (the reviewed head excluded, at most 100 commits) whose
|
||||
product change the current base already holds: `git merge-tree` of that commit
|
||||
into the base changes no product path. A conflicted product path never counts,
|
||||
so a parent that never landed is never treated as one, even when the child's
|
||||
merge dropped its change. The comparison then runs from that commit: an equal
|
||||
merge dropped its change. The comparison then runs from that commit, leaving out
|
||||
every path whose content at the reviewed head is the base's (a base merge the
|
||||
child made before its receipt brought it in) unless HEAD changed it against the
|
||||
base (GST-1214): an equal
|
||||
patch-id carries with reason `stack parent <sha12> landed`, base context next
|
||||
to the child's unchanged hunks is `RECEIPT_CARRY_SCOPED_REVIEW`, and any other
|
||||
difference refuses with `RECEIPT_CARRY_PRODUCT_CHANGED`.
|
||||
|
||||
@@ -50,14 +50,14 @@ per-failure-class breakdown from `test:health`.
|
||||
full consumed-input fingerprint (all touchfiles, globals included) as
|
||||
`series_fingerprint`, plus `harness_version`, as provenance.
|
||||
(`caseSeriesIdentitiesV2` in `scripts/eval-trial-series.ts`.)
|
||||
- **Harness guard.** `scripts/harness-version.json` pins the blob of every
|
||||
shared execution-harness file: `GLOBAL_TOUCHFILES` plus the session runners,
|
||||
the PTY harness, the e2e gate and the LLM judge (36 files). A free test
|
||||
- **Harness guard.** `scripts/harness-pins/<path>.json` pins the blob of every
|
||||
shared execution-harness file (`scripts/harness-version.json` keeps the version and patterns): `GLOBAL_TOUCHFILES` plus the session runners,
|
||||
the PTY harness, the e2e gate and the LLM judge (42 files). A free test
|
||||
(`test/harness-version.test.ts`) fails when one changes without a decision.
|
||||
`bun run scripts/bump-harness-version.ts --bump "<reason>"` raises
|
||||
`HARNESS_VERSION` and starts a new series for every case;
|
||||
`--non-behavioral "<reason>"` records the new blobs as compatible with no
|
||||
reset. Entries are one line per file so concurrent PRs that touch different
|
||||
reset. Each pin is its own file so concurrent PRs that touch different
|
||||
files merge cleanly; on a conflict, take the higher version, then rerun.
|
||||
- **Weekly history (W4b).** Weeks for quarantine expiry and drift are
|
||||
`evals-periodic.yml` runs on `main`: scheduled runs and main dispatches.
|
||||
|
||||
@@ -3789,7 +3789,9 @@ anchored pattern and a reason ([docs/pregate.md](pregate.md#allowlist));
|
||||
### `PREGATE_STRAYS`
|
||||
|
||||
**Meaning.** A file was added at the repository root, in a new top-level
|
||||
directory, or under a scratch name (`zz-*`, `tmp*`, `scratch*`).
|
||||
directory, or under a scratch name (`zz-*`, `tmp*`, `scratch*`) or an editor
|
||||
leftover name (`*.orig`, `*.rej`, `*.swp`, `*.swo`, `*.bak`, `*.tmp`, `*~`,
|
||||
`.DS_Store`, `Thumbs.db`).
|
||||
|
||||
**Fix.** Delete or move it. An intentional root file goes in `strays.allow`.
|
||||
|
||||
@@ -6470,7 +6472,7 @@ There is no ledger for that harness or id under `$GSTACK_STATE_ROOT/harness`. `g
|
||||
<a id="harness-hook-unsupported"></a>
|
||||
### `gstack-harness: <harness> has no start hook` / `the start hook goes into the <harness> profile <dir>, and this registration uses another skills folder` (HARNESS_HOOK_UNSUPPORTED)
|
||||
|
||||
The [start hook](hosts.md#start-hook) exists for OpenClaw and Hermes only, and it goes into the harness profile's own plugin folder, the one the harness scans. A registration with `--skills-dir` somewhere else has no profile gstack can trust, and with `OPENCLAW_CONFIG_PATH` set OpenClaw reads a config outside the profile, where gstack's settings change would not land, so `--start-hook` is refused and nothing is written. Register without `--skills-dir` (set `OPENCLAW_STATE_DIR` or `HERMES_HOME` to pick the profile), or drop `--start-hook`. When the choice comes from `GSTACK_HARNESS_START_HOOK` or `harness_start_hook` instead of the flag, such a registration just gets no hook, and its receipt says so.
|
||||
The [start hook](hosts.md#start-hook) exists for OpenClaw and Hermes only, and it goes into the harness profile's own plugin folder, the one the harness scans. A registration with `--skills-dir` somewhere else has no profile gstack can trust, and with `OPENCLAW_CONFIG_PATH` naming another config OpenClaw reads settings outside the profile, where gstack's settings change would not land, so `--start-hook` is refused and nothing is written. `OPENCLAW_CONFIG_PATH` naming the profile's own `openclaw.json` (compared after resolving symlinks) is not refused. Register without `--skills-dir` (set `OPENCLAW_STATE_DIR` or `HERMES_HOME` to pick the profile), unset `OPENCLAW_CONFIG_PATH` or point it at the profile's own `openclaw.json`, or drop `--start-hook`. When the choice comes from `GSTACK_HARNESS_START_HOOK` or `harness_start_hook` instead of the flag, the registration gets no hook, and `gstack-harness register` and `./setup --host` exit 4 with the reason (`HARNESS_HOOK_INACTIVE`, `start hook is off`); refreshes only print it.
|
||||
|
||||
<a id="harness-hook-choice-invalid"></a>
|
||||
### `gstack-harness: GSTACK_HARNESS_START_HOOK="<value>" is not on or off` (HARNESS_HOOK_CHOICE_INVALID)
|
||||
@@ -6485,7 +6487,9 @@ Each harness profile has one start hook, owned by the gstack registration that w
|
||||
<a id="harness-hook-inactive"></a>
|
||||
### `gstack-harness: <harness> start hook is written: <why>. Turn it on: <command>` (HARNESS_HOOK_INACTIVE, exit 4)
|
||||
|
||||
The skills are registered and the start hook's files are in place, but the hook is not active in the harness. gstack turns the hook on through the harness's own CLI and then reads it back, so this means the CLI was not on `PATH`, one of its commands failed or timed out (the message quotes its last output), or the harness's settings block the plugin: OpenClaw's `plugins.allow` set without `gstack-start`, `gstack-start` in `plugins.deny`, or `hooks.allowPromptInjection: false`. gstack never edits your allow or deny lists. Run the printed command, then `gstack-harness status`; `--start-hook` again retries the whole step. `./setup --host` exits 4 for the same reason. Refreshes and upgrades retry on their own without failing, and stop once you disable the plugin in the harness.
|
||||
The skills are registered and the start hook's files are in place, but the hook is not active in the harness. gstack turns the hook on through the harness's own CLI and then reads it back, so this means the harness has no config yet (`no OpenClaw config at <path> yet` or `no Hermes config at <path> yet`: run `openclaw onboard` or `hermes setup` first, then `gstack-harness refresh`; gstack runs no harness command until then, because the CLI would create a config holding only gstack's plugin setting), the CLI was not on `PATH`, one of its commands failed or timed out (the message quotes its last output), or the harness's settings block the plugin: OpenClaw's `plugins.allow` set without `gstack-start`, `gstack-start` in `plugins.deny`, or `hooks.allowPromptInjection: false`. gstack never edits your allow or deny lists. Run the printed command, then `gstack-harness status`; `--start-hook` again retries the whole step. `./setup --host` exits 4 for the same reason. Refreshes and upgrades retry on their own without failing, and stop once you disable the plugin in the harness.
|
||||
|
||||
The same code with `start hook is off: <why>` means you asked for the hook through `GSTACK_HARNESS_START_HOOK` or `harness_start_hook` but this registration cannot take it (a custom `--skills-dir`, `OPENCLAW_CONFIG_PATH` naming another config, or an unsafe plugin folder); the message names the fix.
|
||||
|
||||
<a id="harness-qm-credentials"></a>
|
||||
### `gstack-harness: no QM admin credential in the environment` / `... needs --yes` (HARNESS_QM_CREDENTIALS)
|
||||
|
||||
+4
-4
@@ -37,7 +37,7 @@ Conventions:
|
||||
- [/gstack-upgrade](gstack-upgrade/SKILL.md): Upgrade gstack to the latest version.
|
||||
- [/guard](guard/SKILL.md): Full safety mode: destructive command warnings + directory-scoped edits.
|
||||
- [/health](health/SKILL.md): Code quality dashboard.
|
||||
- [/investigate](investigate/SKILL.md): Systematic debugging with root cause investigation.
|
||||
- [/investigate](investigate/SKILL.md): Use when a test fails, code errors or output is wrong: find the root cause, then fix it.
|
||||
- [/ios-clean](ios-clean/SKILL.md): Remove the DebugBridge SPM package and all #if DEBUG wiring from an iOS app.
|
||||
- [/ios-design-review](ios-design-review/SKILL.md): Visual design audit for iOS apps on real hardware.
|
||||
- [/ios-fix](ios-fix/SKILL.md): Autonomous iOS bug fixer.
|
||||
@@ -56,10 +56,10 @@ Conventions:
|
||||
- [/plan-devex-review](plan-devex-review/SKILL.md): Interactive developer experience plan review.
|
||||
- [/plan-eng-review](plan-eng-review/SKILL.md): Eng manager-mode plan review.
|
||||
- [/plan-tune](plan-tune/SKILL.md): Self-tuning question sensitivity + developer psychographic for gstack (v1: observational).
|
||||
- [/qa](qa/SKILL.md): Test the site, API or CLI and fix the bugs it finds.
|
||||
- [/qa](qa/SKILL.md): Use when you test the site, API or CLI and fix bugs.
|
||||
- [/qa-only](qa-only/SKILL.md): Report browser/API/CLI/job/worker/webhook bugs.
|
||||
- [/retro](retro/SKILL.md): Weekly engineering retrospective.
|
||||
- [/review](review/SKILL.md): Review this PR's diff.
|
||||
- [/review](review/SKILL.md): Use when you review this PR's diff.
|
||||
- [/scrape](scrape/SKILL.md): Pull data from a web page through the Aside browser — your real, already signed-in sessions.
|
||||
- [/setup-browser-cookies](setup-browser-cookies/SKILL.md): Import cookies from your real Chromium browser into the headless browse session.
|
||||
- [/setup-deploy](setup-deploy/SKILL.md): Configure deployment settings for /land-and-deploy.
|
||||
@@ -71,7 +71,7 @@ Conventions:
|
||||
- [/test-audit](test-audit/SKILL.md): Find low-value or duplicate tests and the test-only code they keep alive.
|
||||
- [/ubicloud](ubicloud/SKILL.md): Run a heavy suite, build or eval on an ephemeral 16-vCPU Ubicloud VM: TTL-named VMs, a reaper, quota preflight and a hang watchdog.
|
||||
- [/unfreeze](unfreeze/SKILL.md): Clear the freeze boundary set by /freeze, allowing edits to all directories again.
|
||||
- [/verify](verify/SKILL.md): Build loop while coding: run the repo's checks and the tests /ship would pick, fix, repeat until green.
|
||||
- [/verify](verify/SKILL.md): Use when coding: run the checks and tests /ship picks until green.
|
||||
- [/write-skill](write-skill/SKILL.md): Write a new agent skill or improve one without bloat: the edit ladder, levels, drift and size checks, full diets.
|
||||
|
||||
## Browse Commands
|
||||
|
||||
+31
-2
@@ -2,7 +2,7 @@
|
||||
name: investigate
|
||||
preamble-tier: 2
|
||||
version: 1.0.0
|
||||
description: Systematic debugging with root cause investigation. (gstack)
|
||||
description: "Use when a test fails, code errors or output is wrong: find the root cause, then fix it. (gstack)"
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
@@ -15,9 +15,29 @@ allowed-tools:
|
||||
triggers:
|
||||
- debug this
|
||||
- fix this bug
|
||||
- test fails
|
||||
- failing test
|
||||
- why is this broken
|
||||
- root cause analysis
|
||||
- this doesn't work
|
||||
- build is broken
|
||||
- stack trace
|
||||
- investigate this error
|
||||
- root cause analysis
|
||||
contract:
|
||||
version: 1
|
||||
items:
|
||||
- id: debug-report
|
||||
kind: report_labels
|
||||
heading: DEBUG REPORT
|
||||
labels:
|
||||
- Symptom
|
||||
- Root cause
|
||||
- Fix
|
||||
- Evidence
|
||||
- Regression test
|
||||
- Status
|
||||
- id: check-after-last-edit
|
||||
kind: check_after_last_edit
|
||||
hooks:
|
||||
PreToolUse:
|
||||
- matcher: "Edit"
|
||||
@@ -61,6 +81,15 @@ gbrain:
|
||||
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
|
||||
<!-- Regenerate: bun run gen:skill-docs -->
|
||||
|
||||
## Done means
|
||||
|
||||
Finish only when each of these exists (contract v1):
|
||||
|
||||
- `debug-report`: a `DEBUG REPORT` with Symptom, Root cause, Fix, Evidence, Regression test and Status filled in (`none: <why>` where the skill's own stop rule leaves one empty).
|
||||
- `check-after-last-edit`: a test or repro command run after your last edit.
|
||||
|
||||
`gstack-skill-contract check` reads them from the transcript (docs/skill-routing-and-contracts.md). For an item that truly does not apply, run `gstack-skill-contract waive <item> --reason "<why>"` (this session only; it counts as a miss).
|
||||
|
||||
## Shared preamble (load first)
|
||||
|
||||
This skill follows gstack shared preamble `4e82fa4c839f` (AskUserQuestion format, voice, writing, completeness and completion rules). If your Read of that file, headed by this id, is still in context, do not read it again; otherwise Read `~/.claude/skills/gstack/investigate/sections/preamble.md` in full before the command below. A section or subsection here with a shared heading replaces the shared one.
|
||||
|
||||
@@ -3,10 +3,10 @@ name: investigate
|
||||
preamble-tier: 2
|
||||
version: 1.0.0
|
||||
description: |
|
||||
Systematic debugging with root cause investigation. Iron Law: no fixes
|
||||
without root cause.
|
||||
Use when asked to "debug this", "fix this bug", "why is this broken",
|
||||
"investigate this error", or "root cause analysis". (gstack)
|
||||
Use when a test fails, code errors or output is wrong: find the root cause, then fix it.
|
||||
Systematic debugging; Iron Law: no fixes without root cause. Fits a failing
|
||||
build, test, script, service or config, an error message or stack trace,
|
||||
wrong or missing output, or broken data, before any edit that fixes it. (gstack)
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
@@ -19,9 +19,29 @@ allowed-tools:
|
||||
triggers:
|
||||
- debug this
|
||||
- fix this bug
|
||||
- test fails
|
||||
- failing test
|
||||
- why is this broken
|
||||
- root cause analysis
|
||||
- this doesn't work
|
||||
- build is broken
|
||||
- stack trace
|
||||
- investigate this error
|
||||
- root cause analysis
|
||||
contract:
|
||||
version: 1
|
||||
items:
|
||||
- id: debug-report
|
||||
kind: report_labels
|
||||
heading: DEBUG REPORT
|
||||
labels:
|
||||
- Symptom
|
||||
- Root cause
|
||||
- Fix
|
||||
- Evidence
|
||||
- Regression test
|
||||
- Status
|
||||
- id: check-after-last-edit
|
||||
kind: check_after_last_edit
|
||||
hooks:
|
||||
PreToolUse:
|
||||
- matcher: "Edit"
|
||||
@@ -63,6 +83,8 @@ gbrain:
|
||||
render_as: "## Recent eureka moments (cross-project)"
|
||||
---
|
||||
|
||||
{{CONTRACT_DONE_MEANS}}
|
||||
|
||||
{{PREAMBLE}}
|
||||
|
||||
# Systematic Debugging
|
||||
|
||||
+11
-9
@@ -22,7 +22,7 @@ import { spawnSync } from 'node:child_process';
|
||||
import { bunIsolationArgs } from './bun-isolation';
|
||||
import { runBashSync } from './script-invocation';
|
||||
import { frontmatterList, routerLine } from './skill-triggers';
|
||||
import { activateHook, assertHookRoot, hookChoice, hookFiles, hookProfile, hookRoot, inspectHook, isHookTarget, profileSkills, type HookResult } from './harness-start-hook';
|
||||
import { activateHook, assertHookRoot, foreignOpenclawConfig, hookChoice, hookConfig, hookFiles, hookProfile, hookRoot, inspectHook, isHookTarget, profileSkills, type HookResult } from './harness-start-hook';
|
||||
|
||||
export type SkillsMode = 'router' | 'hybrid' | 'all';
|
||||
export type TargetName = 'openclaw' | 'hermes' | 'dots' | 'generic' | 'qm' | 'muse' | 'grok';
|
||||
@@ -135,7 +135,7 @@ export const HARNESS_CODES = {
|
||||
HARNESS_RENDER_FAILED: { anchor: 'harness-render-failed', fix: 'run bun install in the gstack checkout, then register again; the render output above names the failing skill' },
|
||||
HARNESS_BUDGET_EXCEEDED: { anchor: 'harness-budget-exceeded', fix: 'this is a gstack bug: report it with the byte count printed above' },
|
||||
HARNESS_NOT_REGISTERED: { anchor: 'harness-not-registered', fix: 'run gstack-harness status to see the registrations on this machine' },
|
||||
HARNESS_HOOK_UNSUPPORTED: { anchor: 'harness-hook-unsupported', fix: 'the start hook is for OpenClaw and Hermes registered in their own profile: drop --skills-dir (set OPENCLAW_STATE_DIR or HERMES_HOME to pick the profile), or drop --start-hook' },
|
||||
HARNESS_HOOK_UNSUPPORTED: { anchor: 'harness-hook-unsupported', fix: 'the start hook is for OpenClaw and Hermes registered in their own profile: drop --skills-dir (set OPENCLAW_STATE_DIR or HERMES_HOME to pick the profile), unset OPENCLAW_CONFIG_PATH or point it at the profile\'s own openclaw.json, or drop --start-hook' },
|
||||
HARNESS_HOOK_CHOICE_INVALID: { anchor: 'harness-hook-choice-invalid', fix: 'set it to on or off (GSTACK_HARNESS_START_HOOK also takes 1 or 0), or unset it' },
|
||||
HARNESS_HOOK_OWNED_ELSEWHERE: { anchor: 'harness-hook-owned-elsewhere', fix: 'if another gstack install wrote it, run that install\'s gstack-harness register <harness> --no-start-hook; otherwise move the folder aside, then register again' },
|
||||
HARNESS_HOOK_INACTIVE: { anchor: 'harness-hook-inactive', fix: 'run the command printed above, then gstack-harness status; the skills themselves are registered' },
|
||||
@@ -525,7 +525,7 @@ function shellRegistry(opts: RegisterOptions, fn: string, args: string[]): boole
|
||||
return sourcedBash(opts.checkout, 'gstack-install-registry.sh', fn, args, opts.stateRoot, opts.env).status === 0;
|
||||
}
|
||||
|
||||
interface HookPlan { on: boolean; prevOn: boolean; explicit: boolean; retry: boolean; profile: string; root: string; files: Map<string, Buffer>; note?: string }
|
||||
interface HookPlan { on: boolean; prevOn: boolean; explicit: boolean; retry: boolean; profile: string; root: string; files: Map<string, Buffer>; note?: string; fix?: string; requested?: boolean }
|
||||
|
||||
/** The start hook for this run: whether it is on, where it goes, and its files (null for targets without a hook). */
|
||||
function hookPlan(opts: RegisterOptions, dest: string, prev: Ledger | null): HookPlan | null {
|
||||
@@ -544,17 +544,19 @@ function hookPlan(opts: RegisterOptions, dest: string, prev: Ledger | null): Hoo
|
||||
const name = t.name;
|
||||
const off = (p: HookPlan) => { if (saved) assertHookRoot(name, saved.profile, saved.root, saved.root, (code, msg) => { throw new HarnessError(code, msg); }); return p; };
|
||||
if (!choice.on) return off(plan);
|
||||
const why = dest !== profileSkills(t.name, profile) ? 'this registration uses another skills folder (' + dest + ')'
|
||||
: t.name === 'openclaw' && opts.env.OPENCLAW_CONFIG_PATH ? 'OPENCLAW_CONFIG_PATH points OpenClaw at a config outside that profile' : '';
|
||||
const requested = !opts.inherit;
|
||||
const foreign = t.name === 'openclaw' ? foreignOpenclawConfig(opts.env, profile) : '';
|
||||
const [why, fix] = dest !== profileSkills(t.name, profile) ? ['this registration uses another skills folder (' + dest + ')', 'register without --skills-dir to get the hook, or pass --no-start-hook to keep this registration without it']
|
||||
: foreign ? ['OPENCLAW_CONFIG_PATH points OpenClaw at ' + foreign + ', outside that profile', 'unset OPENCLAW_CONFIG_PATH or set it to ' + hookConfig('openclaw', profile) + ', then register again'] : ['', ''];
|
||||
if (why && (plan.explicit || !prevOn)) {
|
||||
if (plan.explicit) throw new HarnessError('HARNESS_HOOK_UNSUPPORTED', 'the start hook goes into the ' + t.displayName + ' profile ' + profile + ', and ' + why);
|
||||
return off({ ...plan, on: false, note: 'off: ' + why + ' (' + choice.source + ' asked for the hook)' });
|
||||
return off({ ...plan, on: false, requested, fix, note: 'off: ' + why + ' (' + choice.source + ' asked for the hook)' });
|
||||
}
|
||||
try {
|
||||
assertHookRoot(t.name, profile, root, prevOn ? prev!.meta.hook_root : undefined, (code, msg) => { throw new HarnessError(code, msg); });
|
||||
} catch (e) {
|
||||
if (plan.explicit || prevOn || !(e instanceof HarnessError)) throw e;
|
||||
return off({ ...plan, on: false, note: 'off: ' + e.message + ' (' + choice.source + ' asked for the hook; ' + HARNESS_CODES[e.code].fix + ')' });
|
||||
return off({ ...plan, on: false, requested, fix: HARNESS_CODES[e.code].fix, note: 'off: ' + e.message + ' (' + choice.source + ' asked for the hook)' });
|
||||
}
|
||||
return { ...plan, files: hookFiles(t.name, root) };
|
||||
}
|
||||
@@ -562,7 +564,7 @@ function hookPlan(opts: RegisterOptions, dest: string, prev: Ledger | null): Hoo
|
||||
/** Turns the hook on after its files are written (when this run turns it on, was asked to, or the last try did not end active), and reads it back. */
|
||||
function finishHook(opts: RegisterOptions, hook: HookPlan | null, released: string): RegisterResult['hook'] {
|
||||
if (!hook) return undefined;
|
||||
if (!hook.on) return { state: 'off', ...(hook.note ? { detail: hook.note } : {}), ...(released ? { fix: released } : {}) };
|
||||
if (!hook.on) return { state: 'off', ...(hook.note ? { detail: hook.note } : {}), ...(released || hook.fix ? { fix: released || hook.fix } : {}), ...(hook.requested ? { required: true } : {}) };
|
||||
const t = opts.target.name as 'openclaw' | 'hermes';
|
||||
const required = !hook.prevOn || hook.explicit;
|
||||
if (!required && !hook.retry) return { state: 'unchecked', profile: hook.profile, root: hook.root, fix: 'gstack-harness status shows whether it is active' };
|
||||
@@ -678,7 +680,7 @@ export function register(opts: RegisterOptions): RegisterResult {
|
||||
if (opts.registry && t.render === 'host') shellRegistry(opts, 'gstack_install_registry_upsert', [t.name, 'global', '-', dest, opts.checkout, opts.checkout, opts.version, '-', '-']);
|
||||
if (same && hook?.on && hook.explicit && prev.meta.hook_active !== 'no') writeLedger(lfile, { ...prev, meta: { ...prev.meta, hook_active: 'no' } });
|
||||
const hookResult = finishHook(opts, hook, released);
|
||||
const active = hookResult?.required || hookResult?.retried ? ({ active: 'yes', disabled: 'disabled' } as Record<string, string>)[hookResult.state] ?? 'no' : '';
|
||||
const active = hook?.on && (hookResult?.required || hookResult?.retried) ? ({ active: 'yes', disabled: 'disabled' } as Record<string, string>)[hookResult!.state] ?? 'no' : '';
|
||||
const saved = active ? readLedger(lfile) : null;
|
||||
if (saved && saved.meta.hook_active !== active) writeLedger(lfile, { ...saved, meta: { ...saved.meta, hook_active: active } });
|
||||
return { id, target: t.name, dest, mode, status: !prev ? 'installed' : same ? 'unchanged' : 'updated', written, skills, router, ledger: lfile, kept, ...(hookResult ? { hook: hookResult } : {}) };
|
||||
|
||||
@@ -22,14 +22,14 @@ export const START_NOTE_MAX = 300;
|
||||
export const START_NOTE = 'gstack: if you use a gstack skill this turn, first run the gstack-skill-start command in its Preamble section exactly as written, with no pipe, head, || true or ; echo. Read its output, then follow the skill.';
|
||||
|
||||
export type HookTarget = 'openclaw' | 'hermes';
|
||||
export type HookState = 'active' | 'written' | 'disabled' | 'unchecked' | 'off';
|
||||
export type HookState = 'active' | 'written' | 'disabled' | 'unknown' | 'unchecked' | 'off';
|
||||
export interface HookResult { state: HookState; fix?: string; detail?: string; profile?: string; root?: string }
|
||||
type Env = Record<string, string | undefined>;
|
||||
|
||||
const MANAGED = "Managed by gstack-harness: it rewrites this file. Turn the hook off: gstack-harness register %s --no-start-hook";
|
||||
const SPECS = {
|
||||
openclaw: {
|
||||
profileVar: 'OPENCLAW_STATE_DIR', home: '.openclaw', sub: 'extensions', skills: ['skills'],
|
||||
profileVar: 'OPENCLAW_STATE_DIR', home: '.openclaw', sub: 'extensions', skills: ['skills'], name: 'OpenClaw', config: 'openclaw.json', setup: 'openclaw onboard',
|
||||
activate: [['config', 'set', 'plugins.entries.' + HOOK_ID + '.hooks.allowConversationAccess', 'true'], ['config', 'set', 'plugins.entries.' + HOOK_ID + '.enabled', 'true']],
|
||||
files: (): Record<string, string> => ({
|
||||
'index.js': [
|
||||
@@ -55,7 +55,7 @@ const SPECS = {
|
||||
}),
|
||||
},
|
||||
hermes: {
|
||||
profileVar: 'HERMES_HOME', home: '.hermes', sub: 'plugins', skills: ['skills', 'gstack'],
|
||||
profileVar: 'HERMES_HOME', home: '.hermes', sub: 'plugins', skills: ['skills', 'gstack'], name: 'Hermes', config: 'config.yaml', setup: 'hermes setup',
|
||||
activate: [['plugins', 'enable', HOOK_ID]],
|
||||
files: (): Record<string, string> => ({
|
||||
'plugin.yaml': ['# ' + MANAGED.replace('%s', 'hermes'), 'name: ' + HOOK_ID, 'version: 1.0.0', "description: Puts gstack's start-command note on each user turn (managed by gstack-harness).", ''].join('\n'),
|
||||
@@ -91,6 +91,51 @@ export const hookRoot = (t: HookTarget, profile: string) => path.join(profile, S
|
||||
/** The skills folder a registration must use for the hook to go into that profile. */
|
||||
export const profileSkills = (t: HookTarget, profile: string) => path.join(profile, ...SPECS[t].skills);
|
||||
const activateCommands = (t: HookTarget, profile: string) => SPECS[t].activate.map(a => SPECS[t].profileVar + '=' + JSON.stringify(profile) + ' ' + t + ' ' + a.join(' ')).join(' && ');
|
||||
/** The harness's own config file in a profile: the file activation edits through the harness CLI. */
|
||||
export const hookConfig = (t: HookTarget, profile: string) => path.join(profile, SPECS[t].config);
|
||||
|
||||
function canonical(p: string): string {
|
||||
try { return fs.realpathSync(p); } catch {}
|
||||
const parent = path.dirname(p);
|
||||
return parent === p ? p : path.join(canonical(parent), path.basename(p));
|
||||
}
|
||||
/** The config OPENCLAW_CONFIG_PATH names when it is not the profile's own openclaw.json (compared after realpath), else ''. */
|
||||
export function foreignOpenclawConfig(env: Env, profile: string): string {
|
||||
const v = env.OPENCLAW_CONFIG_PATH;
|
||||
if (!v) return '';
|
||||
const named = path.resolve(v);
|
||||
return canonical(named) === canonical(hookConfig('openclaw', profile)) ? '' : named;
|
||||
}
|
||||
|
||||
/** A profile with no harness config yet: turning the plugin on through the harness CLI would create a config holding only that setting, so gstack waits for the harness's own setup. */
|
||||
function noConfig(t: HookTarget, profile: string): HookResult | null {
|
||||
const cfg = hookConfig(t, profile);
|
||||
if (fs.existsSync(cfg)) return null;
|
||||
const s = SPECS[t];
|
||||
return { state: 'written', detail: 'no ' + s.name + ' config at ' + cfg + ' yet, and turning the plugin on would create one holding only that setting', fix: 'run ' + s.name + "'s own setup first (" + s.profileVar + '=' + JSON.stringify(profile) + ' ' + s.setup + '), then gstack-harness refresh' };
|
||||
}
|
||||
|
||||
/** Reads the hook from the harness's config file when its CLI is not on PATH (a harness's own terminal tool often lacks it). */
|
||||
function inspectConfig(t: HookTarget, profile: string, fix: string): HookResult {
|
||||
const why = t + ' is not on PATH';
|
||||
const written: HookResult = { state: 'written', detail: why + ', so gstack could not turn the plugin on or check it', fix };
|
||||
const cfg = hookConfig(t, profile);
|
||||
const missing = noConfig(t, profile);
|
||||
if (missing) return { ...missing, detail: why + ' and ' + missing.detail };
|
||||
let plugins: any;
|
||||
try { plugins = ((t === 'hermes' ? Bun.YAML.parse(fs.readFileSync(cfg, 'utf8')) : JSON.parse(fs.readFileSync(cfg, 'utf8'))) as any)?.plugins ?? {}; }
|
||||
catch { return { state: 'unknown', detail: why + ' and ' + cfg + ' could not be parsed, so gstack could not check the plugin', fix: 'gstack-harness status from a shell where ' + t + ' is on PATH' }; }
|
||||
if (t === 'hermes') {
|
||||
const listed = (k: string) => Array.isArray(plugins[k]) && plugins[k].includes(HOOK_ID);
|
||||
if (listed('disabled')) return { state: 'disabled', detail: 'turned off in Hermes (plugins.disabled in ' + cfg + '); gstack leaves it off', fix };
|
||||
if (listed('enabled') && fs.existsSync(path.join(hookRoot(t, profile), 'plugin.yaml'))) return { state: 'active', detail: 'read from plugins.enabled in ' + cfg + ' (' + why + ')' };
|
||||
return written;
|
||||
}
|
||||
const entry = plugins.entries?.[HOOK_ID] ?? {};
|
||||
if (entry.enabled === false) return { state: 'disabled', detail: 'turned off in openclaw.json (plugins.entries.' + HOOK_ID + '.enabled false); gstack leaves it off', fix };
|
||||
if (entry.enabled === true && entry.hooks?.allowConversationAccess === true) return { state: 'unknown', detail: cfg + ' turns it on, but ' + why + ', so gstack could not check that OpenClaw loads it', fix: 'gstack-harness status from a shell where openclaw is on PATH' };
|
||||
return written;
|
||||
}
|
||||
|
||||
/** The plugin's files (absolute path -> bytes): fixed code plus note.txt, which the code reads at each turn. */
|
||||
export function hookFiles(t: HookTarget, root: string): Map<string, Buffer> {
|
||||
@@ -124,6 +169,8 @@ const onPath = (bin: string, env: Env) => (env.PATH ?? '').split(path.delimiter)
|
||||
/** Turns the plugin on in the harness. Returns the first failure, or ''. */
|
||||
export function activateHook(t: HookTarget, profile: string, env: Env, home: string): string {
|
||||
if (!onPath(t, env)) return t + ' is not on PATH';
|
||||
const missing = noConfig(t, profile);
|
||||
if (missing) return missing.detail!;
|
||||
for (const a of SPECS[t].activate) {
|
||||
const r = runHarness(t, profile, a, env, home, 60_000);
|
||||
if (!r.ok) return t + ' ' + a.join(' ') + ' failed (' + r.why + ')';
|
||||
@@ -136,12 +183,14 @@ function parseJson(s: string): any {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Reads the hook back from the harness: active (configured for new sessions), disabled by the user, or written (with the fix). */
|
||||
/** Reads the hook back from the harness: active (configured for new sessions), disabled by the user, written (with the fix), or unknown (no CLI and a config that cannot settle it). Runs no harness command while the profile has no harness config. */
|
||||
const READ_TIMEOUT = 15_000;
|
||||
|
||||
export function inspectHook(t: HookTarget, profile: string, env: Env, home: string): HookResult {
|
||||
const fix = activateCommands(t, profile);
|
||||
if (!onPath(t, env)) return { state: 'written', detail: t + ' is not on PATH, so gstack could not turn the plugin on or check it', fix };
|
||||
if (!onPath(t, env)) return inspectConfig(t, profile, fix);
|
||||
const missing = noConfig(t, profile);
|
||||
if (missing) return missing;
|
||||
if (t === 'hermes') {
|
||||
const r = runHarness(t, profile, ['plugins', 'list', '--json'], env, home, READ_TIMEOUT);
|
||||
const row = (parseJson(r.stdout) ?? []).find?.((p: any) => p?.name === HOOK_ID);
|
||||
|
||||
@@ -57,10 +57,10 @@ export function constantsIn(text: string): Map<string, string> {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Named numeric call sites (`timeout: 5000`) keyed by name; the last value per name wins, which is enough to spot a change. */
|
||||
export function callLiteralsIn(text: string): Map<string, string> {
|
||||
const out = new Map<string, string>();
|
||||
for (const m of text.matchAll(CALL_PATTERN)) out.set(m[1]!, m[2]!);
|
||||
/** Named numeric call sites (`timeout: 5000`) keyed by name, every value in source order. */
|
||||
export function callLiteralsIn(text: string): Map<string, string[]> {
|
||||
const out = new Map<string, string[]>();
|
||||
for (const m of text.matchAll(CALL_PATTERN)) out.set(m[1]!, [...(out.get(m[1]!) ?? []), m[2]!]);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,14 @@ export function changedLiterals(file: string, oldText: string, newText: string):
|
||||
const oldC = constantsIn(oldText); const newC = constantsIn(newText);
|
||||
for (const [name, old] of oldC) { const now = newC.get(name); if (now !== undefined && now !== old) out.push({ name, old, new: now, file }); }
|
||||
const oldCall = callLiteralsIn(oldText); const newCall = callLiteralsIn(newText);
|
||||
for (const [name, old] of oldCall) { const now = newCall.get(name); if (now !== undefined && now !== old && !oldC.has(name)) out.push({ name, old, new: now, file }); }
|
||||
for (const [name, olds] of oldCall) {
|
||||
if (oldC.has(name)) continue;
|
||||
const added = [...(newCall.get(name) ?? [])];
|
||||
const removed = olds.filter(v => { const i = added.indexOf(v); if (i >= 0) added.splice(i, 1); return i < 0; });
|
||||
if (!added.length) continue;
|
||||
const seen = new Set<string>();
|
||||
removed.forEach((old, i) => { if (!seen.has(old)) { seen.add(old); out.push({ name, old, new: added[Math.min(i, added.length - 1)]!, file }); } });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { CheckDef, CheckResult, PregateContext } from '../types';
|
||||
|
||||
/** Conventional root files a repo adds without a declaration. */
|
||||
export const ROOT_OK = /^(?:README|LICENSE|LICENCE|NOTICE|CHANGELOG|CONTRIBUTING|CODE_OF_CONDUCT|SECURITY|AUTHORS|VERSION|Makefile|Dockerfile|Procfile|Gemfile|Rakefile|Cargo\.(?:toml|lock)|go\.(?:mod|sum)|package(?:-lock)?\.json|bun\.lockb?|pnpm-lock\.yaml|yarn\.lock|tsconfig[\w.-]*\.json|bunfig\.toml|pyproject\.toml|pubspec\.(?:yaml|lock)|analysis_options\.yaml|dart_test\.yaml|requirements[\w.-]*\.txt|setup\.(?:py|cfg)|\.[\w.-]+|[\w.-]+\.config\.[cm]?[jt]s|[\w.-]+\.md)$/i;
|
||||
export const STRAY_NAME = /^(?:zz-|tmp|temp|scratch|debug-|probe)|\.(?:orig|rej|swp|swo|bak|tmp)$|~$|^\.DS_Store$|^Thumbs\.db$/i;
|
||||
export const STRAY_NAME = /^(?:zz-|tmp|scratch)|\.(?:orig|rej|swp|swo|bak|tmp)$|~$|^\.DS_Store$|^Thumbs\.db$/i;
|
||||
|
||||
function run(ctx: PregateContext): CheckResult {
|
||||
const inputs = baseInputs(ctx);
|
||||
|
||||
+12
-8
@@ -173,16 +173,20 @@ function commitRelease(cwd: string, release: string[], trailer: string | undefin
|
||||
|
||||
/**
|
||||
* A harness pin edit that only records changed harness files as non-behavioral at the same version and patterns (GST-1031): release
|
||||
* metadata, like a version stamp. A bump, a pattern change or an entry without its reason is behavior.
|
||||
* metadata, like a version stamp. The pins live one file per harness file under scripts/harness-pins/ (GST-1084), beside a manifest
|
||||
* holding only version and patterns. A bump, a pattern change or a pin without its reason is behavior.
|
||||
*/
|
||||
const HARNESS_PIN = 'scripts/harness-version.json';
|
||||
const HARNESS_MANIFEST = 'scripts/harness-version.json';
|
||||
const HARNESS_PINS = 'scripts/harness-pins/';
|
||||
function harnessRepin(cwd: string, from: string, to: string, p: string): boolean {
|
||||
if (p !== HARNESS_PIN) return false;
|
||||
const read = (tree: string): any => { const s = git(cwd, ['show', `${tree}:${p}`]); try { return s.status === 0 ? JSON.parse(s.stdout) : null; } catch { return null; } };
|
||||
const a = read(from), b = read(to);
|
||||
if (!a?.files || !b?.files || typeof b.files !== 'object' || a.version !== b.version || JSON.stringify(a.patterns) !== JSON.stringify(b.patterns)) return false;
|
||||
return Object.entries(b.files as Record<string, any>).every(([f, e]) => JSON.stringify(a.files[f]) === JSON.stringify(e)
|
||||
|| (e?.since === b.version && typeof e?.nonBehavioral === 'string' && e.nonBehavioral.trim().length >= 10));
|
||||
if (p !== HARNESS_MANIFEST && !(p.startsWith(HARNESS_PINS) && p.endsWith('.json'))) return false;
|
||||
const read = (tree: string, file: string): any => { const s = git(cwd, ['show', `${tree}:${file}`]); if (s.status !== 0) return undefined; try { return JSON.parse(s.stdout); } catch { return null; } };
|
||||
const a = read(from, HARNESS_MANIFEST), b = read(to, HARNESS_MANIFEST);
|
||||
if (!a || !b || Object.keys(b).sort().join() !== 'patterns,version' || a.version !== b.version || JSON.stringify(a.patterns) !== JSON.stringify(b.patterns)) return false;
|
||||
if (p === HARNESS_MANIFEST) return true;
|
||||
const e = read(to, p);
|
||||
return e === undefined || JSON.stringify(read(from, p)) === JSON.stringify(e)
|
||||
|| (e?.since === b.version && typeof e?.nonBehavioral === 'string' && e.nonBehavioral.trim().length >= 10);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.94.19",
|
||||
"version": "1.94.24",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
name: qa
|
||||
preamble-tier: 4
|
||||
version: 2.0.0
|
||||
description: Test the site, API or CLI and fix the bugs it finds. (gstack)
|
||||
description: Use when you test the site, API or CLI and fix bugs. (gstack)
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
|
||||
+2
-2
@@ -3,9 +3,9 @@ name: qa
|
||||
preamble-tier: 4
|
||||
version: 2.0.0
|
||||
description: |
|
||||
Test the site, API or CLI and fix the bugs it finds.
|
||||
Use when you test the site, API or CLI and fix bugs.
|
||||
Covers browser/API/CLI/job/worker/webhook bugs and commits verified fixes atomically.
|
||||
Use when asked to "qa", "QA", "qa test this", "test this site", "find bugs", "find bugs on site",
|
||||
Fits "qa", "QA", "qa test this", "test this site", "find bugs", "find bugs on site",
|
||||
"test and fix", or "fix what's broken".
|
||||
Proactively suggest when the user says a feature is ready for testing
|
||||
or asks "does this work?". Three tiers: Quick (critical/high only),
|
||||
|
||||
+27
-1
@@ -2,7 +2,7 @@
|
||||
name: review
|
||||
preamble-tier: 4
|
||||
version: 1.0.0
|
||||
description: Review this PR's diff. (gstack)
|
||||
description: Use when you review this PR's diff. (gstack)
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
@@ -15,13 +15,39 @@ allowed-tools:
|
||||
- WebSearch
|
||||
triggers:
|
||||
- review this pr
|
||||
- look over my diff
|
||||
- code review
|
||||
- check my diff
|
||||
- review my changes
|
||||
- pre-landing review
|
||||
contract:
|
||||
version: 1
|
||||
items:
|
||||
- id: specialists-dispatched
|
||||
kind: selected_specialists_dispatched
|
||||
marker: "Dispatching N specialists:"
|
||||
- id: adversarial-pass
|
||||
kind: agent_dispatch
|
||||
match: Think like an attacker and a chaos engineer
|
||||
- id: review-report
|
||||
kind: report_labels
|
||||
heading: Pre-Landing Review
|
||||
labels:
|
||||
- Gate edits
|
||||
---
|
||||
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
|
||||
<!-- Regenerate: bun run gen:skill-docs -->
|
||||
|
||||
## Done means
|
||||
|
||||
Finish only when each of these exists (contract v1):
|
||||
|
||||
- `specialists-dispatched`: the specialist selection printed (`Dispatching N specialists:` ...), then one Agent dispatch per specialist it names (none when a small diff skips them).
|
||||
- `adversarial-pass`: an Agent dispatch for the adversarial pass, its prompt including "Think like an attacker and a chaos engineer".
|
||||
- `review-report`: a `Pre-Landing Review` with Gate edits filled in (`none: <why>` where the skill's own stop rule leaves one empty).
|
||||
|
||||
`gstack-skill-contract check` reads them from the transcript (docs/skill-routing-and-contracts.md). For an item that truly does not apply, run `gstack-skill-contract waive <item> --reason "<why>"` (this session only; it counts as a miss).
|
||||
|
||||
## Shared preamble (load first)
|
||||
|
||||
This skill follows gstack shared preamble `4e82fa4c839f` (AskUserQuestion format, voice, writing, completeness and completion rules). If your Read of that file, headed by this id, is still in context, do not read it again; otherwise Read `~/.claude/skills/gstack/review/sections/preamble.md` in full before the command below. A section or subsection here with a shared heading replaces the shared one.
|
||||
|
||||
+23
-4
@@ -3,10 +3,11 @@ name: review
|
||||
preamble-tier: 4
|
||||
version: 1.0.0
|
||||
description: |
|
||||
Review this PR's diff. Checks it against the base branch for SQL safety, LLM trust
|
||||
boundary violations, conditional side effects, and other structural issues. Use when
|
||||
asked to "review this PR", "code review", "pre-landing review", or "check my diff".
|
||||
Proactively suggest when the user is about to merge or land code changes. (gstack)
|
||||
Use when you review this PR's diff.
|
||||
Checks it for SQL safety, LLM trust boundaries, conditional side effects and
|
||||
bugs that pass CI and break production. Fits "look over my diff", "review my
|
||||
changes", "code review", "check my diff" or "pre-landing review".
|
||||
Proactively suggest before a merge. (gstack)
|
||||
codex_default_prompt: "Use $gstack-review to review this diff against its base before it lands: find the bugs that pass CI and break in production, fix the clear ones, and list what remains."
|
||||
allowed-tools:
|
||||
- Bash
|
||||
@@ -20,11 +21,29 @@ allowed-tools:
|
||||
- WebSearch
|
||||
triggers:
|
||||
- review this pr
|
||||
- look over my diff
|
||||
- code review
|
||||
- check my diff
|
||||
- review my changes
|
||||
- pre-landing review
|
||||
contract:
|
||||
version: 1
|
||||
items:
|
||||
- id: specialists-dispatched
|
||||
kind: selected_specialists_dispatched
|
||||
marker: "Dispatching N specialists:"
|
||||
- id: adversarial-pass
|
||||
kind: agent_dispatch
|
||||
match: Think like an attacker and a chaos engineer
|
||||
- id: review-report
|
||||
kind: report_labels
|
||||
heading: Pre-Landing Review
|
||||
labels:
|
||||
- Gate edits
|
||||
---
|
||||
|
||||
{{CONTRACT_DONE_MEANS}}
|
||||
|
||||
{{PREAMBLE}}
|
||||
|
||||
{{BASE_BRANCH_DETECT}}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
|
||||
/**
|
||||
* Keep HARNESS_VERSION honest (CEO-27/ENG-3/DX-6). scripts/harness-version.json
|
||||
* pins the blob of every shared-harness file (GLOBAL_TOUCHFILES plus its
|
||||
* `patterns`). test/harness-version.test.ts fails when a harness file changes
|
||||
* holds the version and the harness `patterns`; scripts/harness-pins/<path>.json
|
||||
* pins the blob of every shared-harness file (GLOBAL_TOUCHFILES plus those
|
||||
* patterns). test/harness-version.test.ts fails when a harness file changes
|
||||
* without a decision, and this script records the decision:
|
||||
*
|
||||
* --bump "<reason>" the change alters how cases behave: version + 1,
|
||||
@@ -12,9 +13,10 @@
|
||||
* compatible and no series resets
|
||||
* --check print drift and the fix; exit 1 on drift
|
||||
*
|
||||
* Entries are one line per file, so two PRs touching different harness files
|
||||
* merge cleanly. When both bump, or both touch one file, git conflicts on the
|
||||
* manifest: take the higher version, then rerun this script.
|
||||
* Each pin is its own file (GST-1084), so two PRs touching different harness
|
||||
* files never edit the same file and merge cleanly. When both bump, or both
|
||||
* touch one file, git conflicts on the manifest or that pin: take the higher
|
||||
* version, then rerun this script.
|
||||
*/
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
@@ -23,14 +25,35 @@ import { harnessFiles, treeEntries, type TreeEntry } from './eval-trial-series';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
export const MANIFEST = 'scripts/harness-version.json';
|
||||
export const PINS_DIR = 'scripts/harness-pins';
|
||||
export const DOC_ANCHOR = 'docs/TESTING_INTERNALS.md#harness-version';
|
||||
|
||||
export interface ManifestEntry { blob: string; since: number; nonBehavioral?: string }
|
||||
export interface HarnessManifest { version: number; patterns: string[]; files: Record<string, ManifestEntry> }
|
||||
export interface Drift { changed: string[]; added: string[]; removed: string[] }
|
||||
|
||||
const pinPath = (root: string, file: string) => path.join(root, PINS_DIR, `${file}.json`);
|
||||
|
||||
/** The pin files under root's PINS_DIR, keyed by pinned path; any file that is not <path>.json is an error. */
|
||||
function readPins(root: string): Record<string, ManifestEntry> {
|
||||
const dir = path.join(root, PINS_DIR);
|
||||
const files: Record<string, ManifestEntry> = {};
|
||||
for (const rel of (fs.readdirSync(dir, { recursive: true }) as string[]).map(f => f.split(path.sep).join('/')).sort()) {
|
||||
if (fs.statSync(path.join(dir, rel)).isDirectory()) continue;
|
||||
if (!rel.endsWith('.json')) throw new Error(`${PINS_DIR}/${rel}: every harness pin is <pinned path>.json`);
|
||||
try {
|
||||
files[rel.slice(0, -'.json'.length)] = JSON.parse(fs.readFileSync(path.join(dir, rel), 'utf8'));
|
||||
} catch (e) {
|
||||
throw new Error(`${PINS_DIR}/${rel}: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
export function readManifest(root = ROOT): HarnessManifest {
|
||||
return JSON.parse(fs.readFileSync(path.join(root, MANIFEST), 'utf8'));
|
||||
const head = JSON.parse(fs.readFileSync(path.join(root, MANIFEST), 'utf8'));
|
||||
if (Object.keys(head).sort().join() !== 'patterns,version') throw new Error(`${MANIFEST} holds only version and patterns; pins live in ${PINS_DIR}/<path>.json`);
|
||||
return { version: head.version, patterns: head.patterns, files: readPins(root) };
|
||||
}
|
||||
|
||||
/** The tracked harness files with blobs of their working-tree bytes (an unstaged edit counts; a deleted file drops out). */
|
||||
@@ -43,11 +66,11 @@ export function workingHarnessFiles(root: string, patterns: readonly string[]):
|
||||
return tracked.map((entry, index) => ({ file: entry.file, blob: blobs[index]! }));
|
||||
}
|
||||
|
||||
/** Harness files whose blob differs from, is missing from, or no longer matches an entry in the manifest. */
|
||||
/** Harness files whose blob differs from, is missing from, or no longer matches an entry in the manifest; a pin ahead of the manifest version (a bump that failed partway) counts as changed. */
|
||||
export function harnessDrift(manifest: HarnessManifest, files: TreeEntry[]): Drift {
|
||||
const current = new Map(files.map(entry => [entry.file, entry.blob]));
|
||||
return {
|
||||
changed: files.filter(entry => manifest.files[entry.file] && manifest.files[entry.file]!.blob !== entry.blob).map(entry => entry.file).sort(),
|
||||
changed: files.filter(entry => manifest.files[entry.file] && (manifest.files[entry.file]!.blob !== entry.blob || manifest.files[entry.file]!.since > manifest.version)).map(entry => entry.file).sort(),
|
||||
added: files.filter(entry => !manifest.files[entry.file]).map(entry => entry.file).sort(),
|
||||
removed: Object.keys(manifest.files).filter(file => !current.has(file)).sort(),
|
||||
};
|
||||
@@ -64,7 +87,7 @@ export function formatDrift(drift: Drift, version: number): string {
|
||||
'Fix (pick one):',
|
||||
' bun run scripts/bump-harness-version.ts --non-behavioral "<why no verdict can change>" # refactor/comment/logging: no series reset',
|
||||
' bun run scripts/bump-harness-version.ts --bump "<what behavior changed>" # starts a new pass-rate series for every case',
|
||||
`Merge conflict in ${MANIFEST}: take the higher version, then rerun the script. See ${DOC_ANCHOR}.`,
|
||||
`Merge conflict in ${MANIFEST} or ${PINS_DIR}/: take the higher version, then rerun the script. See ${DOC_ANCHOR}.`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@@ -82,11 +105,16 @@ export function decide(manifest: HarnessManifest, files: TreeEntry[], decision:
|
||||
: manifest.files[entry.file]!])) };
|
||||
}
|
||||
|
||||
/** Stable, one-entry-per-line JSON so concurrent edits to different files merge cleanly. */
|
||||
export function serializeManifest(manifest: HarnessManifest): string {
|
||||
const files = Object.keys(manifest.files).sort().map(file => ` ${JSON.stringify(file)}: ${JSON.stringify(manifest.files[file])}`);
|
||||
return `{\n "version": ${manifest.version},\n "patterns": [\n${manifest.patterns.map(pattern => ` ${JSON.stringify(pattern)}`).join(',\n')}\n ],\n`
|
||||
+ ` "files": {\n${files.join(',\n')}\n }\n}\n`;
|
||||
/** Write one pin file per harness file (rewriting only pins whose bytes change, deleting dropped ones), then the manifest head, so a failed pin write never moves the version. */
|
||||
export function writeManifest(root: string, manifest: HarnessManifest): void {
|
||||
for (const file of Object.keys(readPins(root))) if (!manifest.files[file]) fs.rmSync(pinPath(root, file));
|
||||
for (const [file, entry] of Object.entries(manifest.files)) {
|
||||
const target = pinPath(root, file), text = `${JSON.stringify(entry)}\n`;
|
||||
if (fs.existsSync(target) && fs.readFileSync(target, 'utf8') === text) continue;
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.writeFileSync(target, text);
|
||||
}
|
||||
fs.writeFileSync(path.join(root, MANIFEST), `${JSON.stringify({ version: manifest.version, patterns: manifest.patterns }, null, 2)}\n`);
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
@@ -106,7 +134,7 @@ if (import.meta.main) {
|
||||
}
|
||||
if (mode === '--non-behavioral' && !hasDrift(drift)) { console.log('no harness drift: nothing to record'); process.exit(0); }
|
||||
const next = decide(manifest, files, mode === '--bump' ? 'bump' : 'non-behavioral', argv[1] ?? '');
|
||||
fs.writeFileSync(path.join(ROOT, MANIFEST), serializeManifest(next));
|
||||
writeManifest(ROOT, next);
|
||||
console.log(mode === '--bump'
|
||||
? `HARNESS_VERSION ${manifest.version} -> ${next.version}: every case starts a new pass-rate series (reason: ${argv[1]})`
|
||||
: `recorded ${drift.changed.length + drift.added.length} file(s) as non-behavioral at HARNESS_VERSION ${next.version}${drift.removed.length ? `, dropped ${drift.removed.length} removed file(s)` : ''}`);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"f862cf5241580c7593b9ee62eacf5ef046248d26","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"e8ce3cb98474dded38c5128a631d283b0ca35b63","since":6,"nonBehavioral":"F1 rule-16 flaky rerun: report aggregation, rerun planning and the gate slice exit code; no case's execution, trial outcome, trial record or panel verdict changes, so no pass-rate series resets"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"e0d18da4ea02ac443835a68992abeea2b47f22fc","since":6,"nonBehavioral":"the headline adds only streamed session cost; a billed session's cost is already in its collector total; cache writes without a 5m key fall back to the total minus 1h; cost fields only"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"3f2e4d616b5dd0204035710c66b64d49055c5714","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"dc714d7d897aed185c0ec3d2c92ad22d954039e5","since":6,"nonBehavioral":"createShardSandbox drops the Capy launch markers (GSTACK_SESSION_KIND, GSTACK_CAPY, GSTACK_EPHEMERAL, GSTACK_INSTALL_VERDICT) from shard environments; CI never sets them, so no case's input or verdict changes there, and on a Capy machine fixtures now see the session kind they set themselves instead of the installer's"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"5439d0a8f6e1569fa7c924af20d67c30c81d5d30","since":6,"nonBehavioral":"credential rejections are classed infra instead of assertion: no case's pass or fail changes; only failed trials whose provider rejected the run's key move class, so no rerun spends on a rejected key"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"b552b8825d693493decef76b05b976858cab087e","since":6,"nonBehavioral":"test-pr-profile.ts lists .github/workflows/windows-scoped-tests.yml as free-lane only; the workflow runs no paid case, so no case selection or verdict can change"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"01185d20dc9761b04b9b2f751ec98664fe0eaf93","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"d66dac458b8c13250973e550edf8087d4044cd0b","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets. Also: runAgentSdkTest now validates systemPrompt before starting; every live caller already passed the claude_code preset, so the prompt the model sees is unchanged and no verdict can change"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"4881260ef793668e4963a062db6d06a5e1f7a40f","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"ce7a5eaa66e502a052ea9c53348dd2ab25db6148","since":6,"nonBehavioral":"Removes the plan-design 0D focus picker from the shared harness; floor.ts gains an optional pickSetupChoice hook that no other caller passes, so no other case's input or verdict can change. plan-design-finding-floor, the only case whose seed raises that menu, passes the picker from its owned fixture test/fixtures/design-floor-focus-picker.ts, so its owned bytes start its own new series."}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"bff225017e235f0be6ece29f04971ac04b95b544","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"c040ebbfa815b8cce19ef80c6133bd5a5474149a","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"efb7e197dd5f66c3ad0e34074b14e640754f7a45","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"91996ff2bc303ee29f4e6f1705d1468b2536b32e","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"667d0e09b20fe5d7366f57b77b5fb9b04baa37ef","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"4085ad18cd99698f675deadad2a76a796427b693","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"cc072895461dc52ea317d1beff72121342420d30","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"ff32d03f32e4af76c6c7314a79cd1b35263ed0dd","since":6,"nonBehavioral":"llm-judge.ts adds a pass-through byte counter (bytesReceived, lastByteMs) to the aborted-judge diagnostic; the request, response bytes, deadline and scoring are unchanged. GSTA-224 B2 (#3122): skill-fixture.ts follows review's renamed Step 2.5 heading so the review E2E cases extract the same section (now the skill's own reworded bytes); llm-judge.ts changes one bullet of the cross-skill consistency judge's intended architecture to name review-bot-history.md beside the older greptile-history.md name, so its calibration corpus (greptile-history inputs) stays valid. No other case's input or verdict can change; the cross-skill judge's judged files changed in this PR, so its owned inputs start its own series"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"ff9bfe43c43ec2bdd11114f7b8ec7c5736116f75","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"4f77569f9ce11f9cee9f347216e468cd7c956820","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"16e5ea2bb5b9f28f44808a1a09024dbd3e6f28c4","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"404d7515076d5c00804f89e23ea69296312c00e1","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"c09b69611fb22ddf093adb2baa83434494e5f8fa","since":6,"nonBehavioral":"Widens the two plan-eng-review boundary predicates' qid regex to also accept the optional ;signal=<key> hint suffix (#3442). Only a question carrying that suffix can change verdict, and only the changed question-tuning resolver asks for it; every case that runs a skill with that resolver owns its generated SKILL.md, which this change also edits, so those cases already start their own series. No other case's input or verdict can change."}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"3f2daa12ff6484f96b8d28d910a50231c4bb3ae8","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"554e064ef259b8444280eddfe9b0db93c66a8202","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"81c5bb4c48ab928aca2f186a1ade9df7169d7fbe","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"4f0731ef39bc3a6a7c5c592ce98690a24cb1ed7f","since":6,"nonBehavioral":"PTY transcript pricing finds the session by the caller's --session-id or the one new transcript for the launch cwd; the launcher adds no argument; native AUQ capture opts out; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"ddba9cb68c7c0da935cff7c1cea258dee2dff740","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"8c5c05e54abf961259d34d9c3d77db67138d7794","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"a9938cb98f285474ebc4860cd61012504a36466e","since":6,"nonBehavioral":"The PTY floor and observation runners end a plan-mode turn at an idle prompt (the plan-mode footer without its running 'esc to interrupt' segment, after a 10 s settle) instead of waiting out the deadline. The floor runner exits as api_error only after the idle screen with its error panel held still for 10 s and every tick's question and assessment steps found nothing to credit (the transcript is re-read each tick), so a native or prose question already on screen is credited as before, even when its transcript line lands after the screen draws done (test/helpers/claude-pty-runner.runners.unit.test.ts, the prose-question-before-panel case). With no qualifying question an idle turn stays idle, so a trial that failed at the deadline as observer_timeout now fails earlier as api_error. An observation returns what the deadline would have returned. A running footer, extra footer text and a cut footer still wait for the deadline. No pass becomes a fail or a fail a pass. The session ledger may now record failure_cause api_error or refusal instead of observer_timeout or session_timeout for a plan-mode session whose last idle turn shows a panel; failure_class, the gate red class and pass rates are unchanged, so no pass-rate series resets"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"1c2b75fbb984708725abaa8939125f7cab969f9f","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"1384f765e48e511d5f843df2660b2ec70bbcb6b2","since":6,"nonBehavioral":"The PTY floor and observation runners end a plan-mode turn at an idle prompt (the plan-mode footer without its running 'esc to interrupt' segment, after a 10 s settle) instead of waiting out the deadline. The floor runner exits as api_error only after the idle screen with its error panel held still for 10 s and every tick's question and assessment steps found nothing to credit (the transcript is re-read each tick), so a native or prose question already on screen is credited as before, even when its transcript line lands after the screen draws done (test/helpers/claude-pty-runner.runners.unit.test.ts, the prose-question-before-panel case). With no qualifying question an idle turn stays idle, so a trial that failed at the deadline as observer_timeout now fails earlier as api_error. An observation returns what the deadline would have returned. A running footer, extra footer text and a cut footer still wait for the deadline. No pass becomes a fail or a fail a pass. The session ledger may now record failure_cause api_error or refusal instead of observer_timeout or session_timeout for a plan-mode session whose last idle turn shows a panel; failure_class, the gate red class and pass rates are unchanged, so no pass-rate series resets"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"8fd1749a62f24dc37ae186c8a21c2165f7f7645e","since":6,"nonBehavioral":"PTY sessions price their ledger row (streamed_cost_usd) from the session transcript found by the caller's --session-id or the one new transcript for the launch cwd; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"08f7ccbdbd9d71a5dc86edd2f8fced423a78ad9a","since":6,"nonBehavioral":"PTY transcript pricing finds the session by the caller's --session-id or the one new transcript for the launch cwd; the launcher adds no argument; native AUQ capture opts out; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"6211239dd0485e6495a700b824a7e0e61d1bcd97","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"c99baaaa0a77a5a72384493fd8a85a4253ecb05d","since":6,"nonBehavioral":"PTY sessions price their ledger row (streamed_cost_usd) from the session transcript found by the caller's --session-id or the one new transcript for the launch cwd; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"c73b2596d9eb5377530a6bf1d284e0b6ea1556a0","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"ce7609eacc7a8265a4a211358d1b4fb49ea85bc5","since":6,"nonBehavioral":"GSTA-224 B2 (#3122): skill-fixture.ts follows review's renamed Step 2.5 heading ('Check for review bot comments') so the review E2E cases extract the same section, now the skill's own reworded bytes; llm-judge.ts updates only the cross-skill consistency judge's intended architecture to the renamed review-bot-history.md and its record helper. No other case's input or verdict can change; the cross-skill judge's judged files changed in this PR, so its owned inputs start its own new series"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"a8d21e3817c3f35a1d06af325c92fde7d420e377","since":6,"nonBehavioral":"readSkillEffective also appends a variant's added ### subsections (office-hours' Open-ended questions); readSkillStartup, readSharedSections and readSkillEffective return bytes identical to the pinned helper for every tier 2+ render without added subsections (735 across all hosts) and for office-hours' SKILL.md before this PR, so office-hours' fixture input changes only because its prompt changed in this PR, not because the helper did"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"e1b54ad5e3d3eadd8027de1b44e57554d6d99a41","since":6,"nonBehavioral":"selection only: fragment exemption narrowed to .md files directly in the fragments dir; changed-file lists include a rename's old path; no case prompt, fixture, judge or verdict logic changes (GST-400)"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"15f369056d41b9dc42dcd86f81f60c878074704c","since":6}
|
||||
@@ -9,49 +9,5 @@
|
||||
"test/helpers/pty-screen.ts",
|
||||
"test/helpers/e2e-gate.ts",
|
||||
"test/helpers/llm-judge.ts"
|
||||
],
|
||||
"files": {
|
||||
"scripts/lib/paid-cases.ts": {"blob":"f862cf5241580c7593b9ee62eacf5ef046248d26","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"},
|
||||
"scripts/lib/paid-plan.ts": {"blob":"e8ce3cb98474dded38c5128a631d283b0ca35b63","since":6,"nonBehavioral":"F1 rule-16 flaky rerun: report aggregation, rerun planning and the gate slice exit code; no case's execution, trial outcome, trial record or panel verdict changes, so no pass-rate series resets"},
|
||||
"scripts/lib/paid-report.ts": {"blob":"e0d18da4ea02ac443835a68992abeea2b47f22fc","since":6,"nonBehavioral":"the headline adds only streamed session cost; a billed session's cost is already in its collector total; cache writes without a 5m key fall back to the total minus 1h; cost fields only"},
|
||||
"scripts/lib/published-text.ts": {"blob":"3f2e4d616b5dd0204035710c66b64d49055c5714","since":6},
|
||||
"scripts/lib/shard-engine.ts": {"blob":"dc714d7d897aed185c0ec3d2c92ad22d954039e5","since":6,"nonBehavioral":"createShardSandbox drops the Capy launch markers (GSTACK_SESSION_KIND, GSTACK_CAPY, GSTACK_EPHEMERAL, GSTACK_INSTALL_VERDICT) from shard environments; CI never sets them, so no case's input or verdict changes there, and on a Capy machine fixtures now see the session kind they set themselves instead of the installer's"},
|
||||
"scripts/test-paid-shards.ts": {"blob":"5439d0a8f6e1569fa7c924af20d67c30c81d5d30","since":6,"nonBehavioral":"credential rejections are classed infra instead of assertion: no case's pass or fail changes; only failed trials whose provider rejected the run's key move class, so no rerun spends on a rejected key"},
|
||||
"scripts/test-pr-profile.ts": {"blob":"b552b8825d693493decef76b05b976858cab087e","since":6,"nonBehavioral":"test-pr-profile.ts lists .github/workflows/windows-scoped-tests.yml as free-lane only; the workflow runs no paid case, so no case selection or verdict can change"},
|
||||
"scripts/test-strict-output.ts": {"blob":"01185d20dc9761b04b9b2f751ec98664fe0eaf93","since":6},
|
||||
"test/helpers/agent-sdk-runner.ts": {"blob":"d66dac458b8c13250973e550edf8087d4044cd0b","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets. Also: runAgentSdkTest now validates systemPrompt before starting; every live caller already passed the claude_code preset, so the prompt the model sees is unchanged and no verdict can change"},
|
||||
"test/helpers/auq-substance-panel.ts": {"blob":"4881260ef793668e4963a062db6d06a5e1f7a40f","since":6},
|
||||
"test/helpers/claude-pty-runner.ts": {"blob":"ce7a5eaa66e502a052ea9c53348dd2ab25db6148","since":6,"nonBehavioral":"Removes the plan-design 0D focus picker from the shared harness; floor.ts gains an optional pickSetupChoice hook that no other caller passes, so no other case's input or verdict can change. plan-design-finding-floor, the only case whose seed raises that menu, passes the picker from its owned fixture test/fixtures/design-floor-focus-picker.ts, so its owned bytes start its own new series."},
|
||||
"test/helpers/codex-host-runner.ts": {"blob":"bff225017e235f0be6ece29f04971ac04b95b544","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"},
|
||||
"test/helpers/codex-session-runner.ts": {"blob":"c040ebbfa815b8cce19ef80c6133bd5a5474149a","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"},
|
||||
"test/helpers/e2e-gate.ts": {"blob":"efb7e197dd5f66c3ad0e34074b14e640754f7a45","since":6},
|
||||
"test/helpers/e2e-helpers.ts": {"blob":"91996ff2bc303ee29f4e6f1705d1468b2536b32e","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"},
|
||||
"test/helpers/eval-budgets.ts": {"blob":"667d0e09b20fe5d7366f57b77b5fb9b04baa37ef","since":6},
|
||||
"test/helpers/eval-store.ts": {"blob":"4085ad18cd99698f675deadad2a76a796427b693","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"},
|
||||
"test/helpers/hermetic-env.ts": {"blob":"cc072895461dc52ea317d1beff72121342420d30","since":6},
|
||||
"test/helpers/llm-judge.ts": {"blob":"ff32d03f32e4af76c6c7314a79cd1b35263ed0dd","since":6,"nonBehavioral":"llm-judge.ts adds a pass-through byte counter (bytesReceived, lastByteMs) to the aborted-judge diagnostic; the request, response bytes, deadline and scoring are unchanged. GSTA-224 B2 (#3122): skill-fixture.ts follows review's renamed Step 2.5 heading so the review E2E cases extract the same section (now the skill's own reworded bytes); llm-judge.ts changes one bullet of the cross-skill consistency judge's intended architecture to name review-bot-history.md beside the older greptile-history.md name, so its calibration corpus (greptile-history inputs) stays valid. No other case's input or verdict can change; the cross-skill judge's judged files changed in this PR, so its owned inputs start its own series"},
|
||||
"test/helpers/paid-test-set.ts": {"blob":"ff9bfe43c43ec2bdd11114f7b8ec7c5736116f75","since":6},
|
||||
"test/helpers/pty-screen.ts": {"blob":"4f77569f9ce11f9cee9f347216e468cd7c956820","since":6},
|
||||
"test/helpers/pty/auq.ts": {"blob":"16e5ea2bb5b9f28f44808a1a09024dbd3e6f28c4","since":6},
|
||||
"test/helpers/pty/binary.ts": {"blob":"404d7515076d5c00804f89e23ea69296312c00e1","since":6},
|
||||
"test/helpers/pty/boundaries.ts": {"blob":"c09b69611fb22ddf093adb2baa83434494e5f8fa","since":6,"nonBehavioral":"Widens the two plan-eng-review boundary predicates' qid regex to also accept the optional ;signal=<key> hint suffix (#3442). Only a question carrying that suffix can change verdict, and only the changed question-tuning resolver asks for it; every case that runs a skill with that resolver owns its generated SKILL.md, which this change also edits, so those cases already start their own series. No other case's input or verdict can change."},
|
||||
"test/helpers/pty/classify.ts": {"blob":"3f2daa12ff6484f96b8d28d910a50231c4bb3ae8","since":6},
|
||||
"test/helpers/pty/fake-session.ts": {"blob":"554e064ef259b8444280eddfe9b0db93c66a8202","since":6},
|
||||
"test/helpers/pty/judge.ts": {"blob":"81c5bb4c48ab928aca2f186a1ade9df7169d7fbe","since":6},
|
||||
"test/helpers/pty/launch.ts": {"blob":"4f0731ef39bc3a6a7c5c592ce98690a24cb1ed7f","since":6,"nonBehavioral":"PTY transcript pricing finds the session by the caller's --session-id or the one new transcript for the launch cwd; the launcher adds no argument; native AUQ capture opts out; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"},
|
||||
"test/helpers/pty/plan-native.ts": {"blob":"ddba9cb68c7c0da935cff7c1cea258dee2dff740","since":6},
|
||||
"test/helpers/pty/runners/counting.ts": {"blob":"8c5c05e54abf961259d34d9c3d77db67138d7794","since":6},
|
||||
"test/helpers/pty/runners/floor.ts": {"blob":"a9938cb98f285474ebc4860cd61012504a36466e","since":6,"nonBehavioral":"The PTY floor and observation runners end a plan-mode turn at an idle prompt (the plan-mode footer without its running 'esc to interrupt' segment, after a 10 s settle) instead of waiting out the deadline. The floor runner exits as api_error only after the idle screen with its error panel held still for 10 s and every tick's question and assessment steps found nothing to credit (the transcript is re-read each tick), so a native or prose question already on screen is credited as before, even when its transcript line lands after the screen draws done (test/helpers/claude-pty-runner.runners.unit.test.ts, the prose-question-before-panel case). With no qualifying question an idle turn stays idle, so a trial that failed at the deadline as observer_timeout now fails earlier as api_error. An observation returns what the deadline would have returned. A running footer, extra footer text and a cut footer still wait for the deadline. No pass becomes a fail or a fail a pass. The session ledger may now record failure_cause api_error or refusal instead of observer_timeout or session_timeout for a plan-mode session whose last idle turn shows a panel; failure_class, the gate red class and pass rates are unchanged, so no pass-rate series resets"},
|
||||
"test/helpers/pty/runners/observation.ts": {"blob":"1c2b75fbb984708725abaa8939125f7cab969f9f","since":6},
|
||||
"test/helpers/pty/screen.ts": {"blob":"1384f765e48e511d5f843df2660b2ec70bbcb6b2","since":6,"nonBehavioral":"The PTY floor and observation runners end a plan-mode turn at an idle prompt (the plan-mode footer without its running 'esc to interrupt' segment, after a 10 s settle) instead of waiting out the deadline. The floor runner exits as api_error only after the idle screen with its error panel held still for 10 s and every tick's question and assessment steps found nothing to credit (the transcript is re-read each tick), so a native or prose question already on screen is credited as before, even when its transcript line lands after the screen draws done (test/helpers/claude-pty-runner.runners.unit.test.ts, the prose-question-before-panel case). With no qualifying question an idle turn stays idle, so a trial that failed at the deadline as observer_timeout now fails earlier as api_error. An observation returns what the deadline would have returned. A running footer, extra footer text and a cut footer still wait for the deadline. No pass becomes a fail or a fail a pass. The session ledger may now record failure_cause api_error or refusal instead of observer_timeout or session_timeout for a plan-mode session whose last idle turn shows a panel; failure_class, the gate red class and pass rates are unchanged, so no pass-rate series resets"},
|
||||
"test/helpers/pty/session.ts": {"blob":"8fd1749a62f24dc37ae186c8a21c2165f7f7645e","since":6,"nonBehavioral":"PTY sessions price their ledger row (streamed_cost_usd) from the session transcript found by the caller's --session-id or the one new transcript for the launch cwd; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"},
|
||||
"test/helpers/pty/transcript-cost.ts": {"blob":"08f7ccbdbd9d71a5dc86edd2f8fced423a78ad9a","since":6,"nonBehavioral":"PTY transcript pricing finds the session by the caller's --session-id or the one new transcript for the launch cwd; the launcher adds no argument; native AUQ capture opts out; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"},
|
||||
"test/helpers/session-drain-policy.ts": {"blob":"6211239dd0485e6495a700b824a7e0e61d1bcd97","since":6},
|
||||
"test/helpers/session-ledger.ts": {"blob":"c99baaaa0a77a5a72384493fd8a85a4253ecb05d","since":6,"nonBehavioral":"PTY sessions price their ledger row (streamed_cost_usd) from the session transcript found by the caller's --session-id or the one new transcript for the launch cwd; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"},
|
||||
"test/helpers/session-runner.ts": {"blob":"c73b2596d9eb5377530a6bf1d284e0b6ea1556a0","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"},
|
||||
"test/helpers/skill-fixture.ts": {"blob":"ce7609eacc7a8265a4a211358d1b4fb49ea85bc5","since":6,"nonBehavioral":"GSTA-224 B2 (#3122): skill-fixture.ts follows review's renamed Step 2.5 heading ('Check for review bot comments') so the review E2E cases extract the same section, now the skill's own reworded bytes; llm-judge.ts updates only the cross-skill consistency judge's intended architecture to the renamed review-bot-history.md and its record helper. No other case's input or verdict can change; the cross-skill judge's judged files changed in this PR, so its owned inputs start its own new series"},
|
||||
"test/helpers/skill-startup.ts": {"blob":"a8d21e3817c3f35a1d06af325c92fde7d420e377","since":6,"nonBehavioral":"readSkillEffective also appends a variant's added ### subsections (office-hours' Open-ended questions); readSkillStartup, readSharedSections and readSkillEffective return bytes identical to the pinned helper for every tier 2+ render without added subsections (735 across all hosts) and for office-hours' SKILL.md before this PR, so office-hours' fixture input changes only because its prompt changed in this PR, not because the helper did"},
|
||||
"test/helpers/test-selection.ts": {"blob":"e1b54ad5e3d3eadd8027de1b44e57554d6d99a41","since":6,"nonBehavioral":"selection only: fragment exemption narrowed to .md files directly in the fragments dir; changed-file lists include a rename's old path; no case prompt, fixture, judge or verdict logic changes (GST-400)"},
|
||||
"test/helpers/touchfiles.ts": {"blob":"15f369056d41b9dc42dcd86f81f60c878074704c","since":6}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* {{CONTRACT_DONE_MEANS}}: a skill's `contract:` frontmatter block (GST-1088, frozen v1
|
||||
* schema) rendered as a short "Done means" list at the top of the body, so the
|
||||
* outputs the model reads as required and the block the Stop check reads cannot
|
||||
* drift. parseSkillContract is the schema; test/skill-descriptions.test.ts runs it
|
||||
* over every template, so a malformed block fails CI instead of failing open.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { TemplateContext } from './types';
|
||||
import { getHostConfig } from '../../hosts/index';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..', '..');
|
||||
const code = (s: string) => '\u0060' + s + '\u0060';
|
||||
|
||||
export type ContractItem =
|
||||
| { id: string; kind: 'report_labels'; heading: string; labels: string[] }
|
||||
| { id: string; kind: 'check_after_last_edit' }
|
||||
| { id: string; kind: 'selected_specialists_dispatched'; marker: string }
|
||||
| { id: string; kind: 'agent_dispatch'; match: string };
|
||||
|
||||
export interface SkillContract {
|
||||
version: 1;
|
||||
items: ContractItem[];
|
||||
}
|
||||
|
||||
const KIND_FIELDS: Record<ContractItem['kind'], string[]> = {
|
||||
report_labels: ['heading', 'labels'],
|
||||
check_after_last_edit: [],
|
||||
selected_specialists_dispatched: ['marker'],
|
||||
agent_dispatch: ['match'],
|
||||
};
|
||||
|
||||
// The resolver and section that render the step each kind checks today. A host that suppresses the resolver, or a loadout that excludes the section, never shows that step, so its items are left out of the list there.
|
||||
const BACKED_BY: Partial<Record<ContractItem['kind'], { resolver: string; section: string }>> = {
|
||||
selected_specialists_dispatched: { resolver: 'REVIEW_ARMY', section: 'review-army' },
|
||||
agent_dispatch: { resolver: 'ADVERSARIAL_STEP', section: 'adversarial' },
|
||||
};
|
||||
|
||||
const PLACEHOLDER = /^\[.*\]$|^<.*>$|^\.\.\.$|^(todo|tbd|n\/a)$/i;
|
||||
|
||||
function fail(where: string, problem: string): never {
|
||||
throw new Error('CONTRACT_INVALID ' + where + ': ' + problem + '. Fix: edit ' + where + ', then bun run gen:skill-docs');
|
||||
}
|
||||
|
||||
const isMap = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
||||
const isText = (v: unknown): v is string => typeof v === 'string' && v.trim() !== '';
|
||||
|
||||
/** Validates a parsed `contract:` value against the frozen v1 schema; where names the template in errors. */
|
||||
export function parseSkillContract(value: unknown, where: string): SkillContract {
|
||||
if (!isMap(value)) fail(where, 'contract must be a mapping { version: 1, items: [...] }');
|
||||
for (const key of Object.keys(value)) {
|
||||
if (key !== 'version' && key !== 'items') fail(where, 'contract has unknown key "' + key + '"');
|
||||
}
|
||||
if (value.version !== 1) fail(where, 'contract version must be 1, got ' + JSON.stringify(value.version));
|
||||
if (!Array.isArray(value.items) || value.items.length === 0) fail(where, 'contract items must be a non-empty list');
|
||||
const seen = new Set<string>();
|
||||
const items = value.items.map((raw: unknown, i: number) => {
|
||||
const at = 'items[' + i + ']';
|
||||
if (!isMap(raw)) fail(where, at + ' must be a mapping');
|
||||
const kind = raw.kind as ContractItem['kind'];
|
||||
if (typeof raw.kind !== 'string' || !Object.hasOwn(KIND_FIELDS, kind)) {
|
||||
fail(where, at + ' kind ' + JSON.stringify(raw.kind) + ' is not one of ' + Object.keys(KIND_FIELDS).join(', '));
|
||||
}
|
||||
if (typeof raw.id !== 'string' || !/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(raw.id)) {
|
||||
fail(where, at + ' id must be lowercase kebab-case, got ' + JSON.stringify(raw.id));
|
||||
}
|
||||
if (seen.has(raw.id)) fail(where, at + ' duplicate id "' + raw.id + '"');
|
||||
seen.add(raw.id);
|
||||
for (const key of Object.keys(raw)) {
|
||||
if (key !== 'id' && key !== 'kind' && !KIND_FIELDS[kind].includes(key)) fail(where, at + ' (' + kind + ') has unknown key "' + key + '"');
|
||||
}
|
||||
for (const field of KIND_FIELDS[kind]) {
|
||||
if (field !== 'labels' && !isText(raw[field])) fail(where, at + ' ' + field + ' must be a non-empty string');
|
||||
}
|
||||
if (kind === 'report_labels') {
|
||||
if (!Array.isArray(raw.labels) || raw.labels.length === 0) fail(where, at + ' labels must be a non-empty list');
|
||||
for (const label of raw.labels as unknown[]) {
|
||||
if (!isText(label)) fail(where, at + ' labels must be non-empty strings');
|
||||
if (PLACEHOLDER.test(label.trim())) fail(where, at + ' label ' + JSON.stringify(label) + ' is a placeholder');
|
||||
}
|
||||
}
|
||||
return raw as ContractItem;
|
||||
});
|
||||
return { version: 1, items };
|
||||
}
|
||||
|
||||
/** The template's contract, or null without one; required makes a missing block an error. */
|
||||
export function contractFromTemplate(tmplContent: string, where: string, opts: { required?: boolean } = {}): SkillContract | null {
|
||||
const block = tmplContent.replace(/\r\n/g, '\n').match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
||||
if (!block || !/^contract:/m.test(block[1])) {
|
||||
if (opts.required) fail(where, '{{CONTRACT_DONE_MEANS}} needs a contract block in the frontmatter');
|
||||
return null;
|
||||
}
|
||||
let frontmatter: unknown;
|
||||
try {
|
||||
frontmatter = Bun.YAML.parse(block[1]);
|
||||
} catch (error) {
|
||||
fail(where, 'frontmatter is not valid YAML (' + (error instanceof Error ? error.message : String(error)) + ')');
|
||||
}
|
||||
return parseSkillContract(isMap(frontmatter) ? frontmatter.contract : undefined, where);
|
||||
}
|
||||
|
||||
const list = (xs: string[]) => xs.length < 2 ? xs.join('') : xs.slice(0, -1).join(', ') + ' and ' + xs[xs.length - 1];
|
||||
|
||||
function plainWords(item: ContractItem): string {
|
||||
switch (item.kind) {
|
||||
case 'report_labels': return 'a ' + code(item.heading) + ' with ' + list(item.labels) + ' filled in (' + code('none: <why>') + ' where the skill\'s own stop rule leaves one empty)';
|
||||
case 'check_after_last_edit': return 'a test or repro command run after your last edit';
|
||||
case 'selected_specialists_dispatched': return 'the specialist selection printed (' + code(item.marker) + ' ...), then one Agent dispatch per specialist it names (none when a small diff skips them)';
|
||||
case 'agent_dispatch': return 'an Agent dispatch for the ' + item.id.replace(/-/g, ' ') + ', its prompt including "' + item.match + '"';
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether bin/gstack-skill-contract ships. The PR that adds it flips this, regenerates and re-pins; test/skill-descriptions.test.ts fails while the two disagree. */
|
||||
export const CONTRACT_CHECKER_SHIPPED = true;
|
||||
|
||||
/** The list; checker adds the check and waive commands, which only a tree that ships the checker may mention. */
|
||||
export function renderDoneMeans(contract: SkillContract, checker = CONTRACT_CHECKER_SHIPPED): string {
|
||||
if (contract.items.length === 0) return '';
|
||||
return [
|
||||
'## Done means',
|
||||
'',
|
||||
'Finish only when each of these exists (contract v1):',
|
||||
'',
|
||||
...contract.items.map(item => '- ' + code(item.id) + ': ' + plainWords(item) + '.'),
|
||||
...(checker ? ['', code('gstack-skill-contract check') + ' reads them from the transcript (docs/skill-routing-and-contracts.md). ' +
|
||||
'For an item that truly does not apply, run ' + code('gstack-skill-contract waive <item> --reason "<why>"') + ' (this session only; it counts as a miss).'] : []),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function generateContractDoneMeans(ctx: TemplateContext): string {
|
||||
const where = path.relative(ROOT, ctx.tmplPath).split(path.sep).join('/');
|
||||
const contract = contractFromTemplate(fs.readFileSync(ctx.tmplPath, 'utf-8'), where, { required: true })!;
|
||||
const suppressed = new Set(getHostConfig(ctx.host).suppressedResolvers ?? []);
|
||||
const shown = (item: ContractItem) => {
|
||||
const backing = BACKED_BY[item.kind];
|
||||
return !backing || (!suppressed.has(backing.resolver) && !ctx.modules?.excluded.has('section:' + ctx.skillName + '/' + backing.section));
|
||||
};
|
||||
return renderDoneMeans({ ...contract, items: contract.items.filter(shown) });
|
||||
}
|
||||
@@ -58,6 +58,7 @@ import { generateShipDriverStops } from './ship-driver';
|
||||
import { generateQAScope, generateQAExploratory, generateQAFunctional, generateQAResource, generateQAReview, generateQAReviewPreflight, generateQAMethodReads } from './qa';
|
||||
import { generateVoiceTriggerRouting } from './voice-triggers';
|
||||
import { generateTranscriptDiscovery, generateTranscriptSources } from './transcripts';
|
||||
import { generateContractDoneMeans } from './contract-done-means';
|
||||
|
||||
export const RESOLVERS: Record<string, ResolverFn> = {
|
||||
AUTOPLAN_PUBLICATION_HOOK: generateAutoplanPublicationHook,
|
||||
@@ -197,4 +198,5 @@ export const RESOLVERS: Record<string, ResolverFn> = {
|
||||
SECTION_INDEX,
|
||||
TRANSCRIPT_SOURCES: generateTranscriptSources,
|
||||
TRANSCRIPT_DISCOVERY: generateTranscriptDiscovery,
|
||||
CONTRACT_DONE_MEANS: generateContractDoneMeans,
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"fbe52f3a9e0f4442eb755f1747a95944b373b439bd05c61f3eb68345d1061a5b"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"aad667b5ea4c6d23d930dafffa38178c04be388cd4364fb73759fc92af17bb63"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"a7cc08fb31b6ca65932a316d589f0b09677426d193d63f88ec74fee6d93ce053"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"bd96406da28b03742384560425a333b6d6d7ed2be95bd2026c90e0039632cee2"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"6dd670ea040535ca6cceab81c29a395bb67ce5a2ab8fa2b81e51bc4711e4aada"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"1ebe47ddb3291a9e4fe90b5a260eb57e328144959549bc95c119b68d23652cb5"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"25050a7a61f81df649d1df0d0237dbf961671356395317c6a527741f5bb56877","replay":"GST-940"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"08464b78a723ee463ca3a28d0abbc4eb8f4f512192061c703743707b66b5a311","replay":"GST-942"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"a32963ef38d9d3613c58c805e8517cd2e593c63769b30dd7d40e2592fa48fc78","replay":"GSTA-224 H1"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"b9205f88313cc64902c0b13e9685722ab8138d3dcc58916154f47274abda7d83","replay":"#3638"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"f570db344c9a01df633bcaa281d6017f4f12420fd3e0aa3078c1ce06e0df8332","replay":"#3638"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"e6962fa25b78f362654f2954f79683e524d2ebd452d1494f8a2b099888e8e255","replay":"G1"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"78329b64640260cd8831ca81a9189f3f8dcade9c406431631d3f2cd798cd3967","replay":"#3638"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"263d23ac119dd601d315c191dbdbd503d47c00264c9c0ca81959559ca11d4e95","replay":"#3638"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"b6fd9eb229002ea598f8fe9ff53b1cd8821e3bd37a7aa7b07b5526556c71ebca","replay":"#3638"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"f8015d549e70297f782f0aabf6fe3a9fe31ec5ed61c311ab854e4a858282ed9b","replay":"#3638"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"545c294ae53638b4c8524e8cde08246a4ce3b5c287ec7c44e27f5556a3b0e8cc","replay":"#3638"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"9ea05149f5b13d6a19ecec26285142d09d259cf80a515af828abdd1f43320427","replay":"#3638"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"17bfe813eb3de22835af048ec359770720f1d7f371a26460169f7e32b1bcac9f","replay":"#3638"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"f33846b1dcf2fdeef82d0744ab2ef48fb4a33b34a737dafc33dd0e891756689f","replay":"#3638"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"597215e4438d85d9cc71b48097c61eef7cba4f4b09dc14c92b4b94893b3d3ea2","replay":"#3638"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"7f92a0d0a8319c3c0e076fb3b1a4689c45120b2be2c14dfc7d4cb03aafa9d6a5","replay":"GST-257"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"d6cc2bef7be562387d0c0a22fefe646dff9c295f1d34d8453a6cb496584150e4","replay":"#3638"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"712bf5b0c439e96bac32f19cbe25394dabb4c94aa66600100bfde9b100e15a08","replay":"#3638"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"0142d6bc3f2a5bceb9f8b2adef1d3a206e5836c99a0667fe995568da2e80a5a6","replay":"GST-508+GST-464+GST-967"}
|
||||
@@ -0,0 +1 @@
|
||||
{"sha":"de3e0d35f2af56492c42fd287b6868fbe19bce96b86321e74a4947ba909b74eb","replay":"GST-508+GST-464+GST-967"}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"replay":"#3638","justification":"no-loss round 3: main restored the pre-#3621 reviewers (v1.91.331.0)"}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"replay":"G1","justification":"GSTA-167 variant G Arm G1: live no-loss replay of both prose voices with part 1's prompts, 2 passes, normalizer v2, paired rule against pooled C' (parts 1 and 3; specialists at medium as in C and C2)"}
|
||||
@@ -0,0 +1 @@
|
||||
{"replay":"GST-257","justification":"GST-257 PR 1 dispatch-superset no-loss check (test/review-targeted.test.ts): on every fixture diff the branch dispatches main's reviewers at 8be7f21 unchanged plus only the targeted reader, and over the last 100 non-release commits on main it lost none; adds the targeted role and brief to the G1 and L bytes, whose existing reviewer text is unchanged"}
|
||||
@@ -0,0 +1 @@
|
||||
{"replay":"GST-464","justification":"GST-464 brief-identity no-loss check (the brief-body test below): every reviewer brief lib/review-brief.ts cuts from ship/sections/review-army.md and adversarial.md is byte-identical to the bodies at the L and GST-257 pins; only lead instructions outside the cut spans changed"}
|
||||
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user