Merge remote-tracking branch 'origin/main' into capy/flaky-free-2026-10-22

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
garrytan committed 2026-10-22 19:58:42 +00:00
commit 9832a8919b
9 files changed
+82 -6

No files matched your search

@@ -64,6 +64,7 @@ jobs:
--name cso-qualified-runtime-statements --dir qualification-evidence
- name: Generate a fail-closed catalog candidate
env:
GH_TOKEN: ${{ github.token }}
CSO_QUALIFICATION_RUN: ${{ inputs.qualification_run_id }}
run: |
set -euo pipefail
+19
View File
@@ -10,6 +10,25 @@
- **`bin/gstack-docs-candidate`**: snapshot lists `ls-files` once instead of twice and hashes with `Bun.CryptoHasher`. A snapshot spawn went from 68 ms to 46 ms, and the record it writes is byte-identical.
- **`test/docsync-fault-interface.test.ts`**: the nested callback run is split into three outer tests by test-name pattern (10, 12 and 14 cases). Each part asserts its exact pass count, its exact filtered-out count and `0 fail`, so all 36 cases still run and the patterns are disjoint.
## [1.91.178.0] - 2026-10-22
**A /qa run no longer gets a hint that another report root could change its verdict.** Train gate 39646086155 failed qa-functional-webhook-fix once, and gate 39166935531 failed qa-functional-cli-fix the same way on 2026-10-18. Both times the agent ran `gstack-qa-evidence materialize`, read a fail verdict that ended "this verdict is final for this report root", and its very next call wrote a stray placeholder file in the fixture's parent directory (`qa-reports-placeholder-unused`, `qa-Q3vLqZ-placeholder`) before writing `report.md`. Each agent then reported the stray file itself. The out-of-fixture write detector was right both times and is unchanged.
### Fixed
- **`lib/qa-evidence.ts`**: materialize's `next` instruction now ends "this verdict is final. Next, write the Markdown report beside evidence.json." It names the next write and where it goes, instead of qualifying the verdict by report root.
- **`test/helpers/qa-functional-eval.ts`**: the functional fixture's declared interface says that every Write/Edit targets an owned path inside the working directory, and that a write anywhere else, including the parent directory, fails acceptance. That was already the acceptance rule; the prompt now states it.
### Gate integrity
- No detector, threshold, trial count or case kind changed. `test/qa-evidence.test.ts` pins the new `next` wording and checks that it no longer mentions a report root.
## [1.91.177.0] - 2026-10-22
**The /cso runtime promotion workflow can finish.** Promote run 39691485727 failed after its `cso-runtime-release` approval. Its "Generate a fail-closed catalog candidate" step calls `gh api …/compare` to prove the staged commit is an ancestor of the qualification head, but the step had no `GH_TOKEN`. On a hosted runner `gh` then exits 4 ("set the GH_TOKEN environment variable"). The run stopped before it signed, pushed or opened anything.
### Fixed
- **`.github/workflows/cso-runtime-promote.yml`**: the candidate step gets `GH_TOKEN: ${{ github.token }}`, like every other step there that calls `gh`.
- **`test/workflow-gh-token.test.ts`** (new): fails when any step in any workflow invokes `gh` with no `GH_TOKEN` or `GITHUB_TOKEN` in its step, job or workflow env. actionlint does not check this. This step was the only one in the repository missing a token.
## [1.91.176.0] - 2026-10-21
**A qualified /cso runtime set can be released again after the S1 shebang change.** The release contract fingerprints every source byte compiled into a runtime image (`preparationSha256`). S1 (#3312) changed only line 1 of the two compiled helpers, `lib/cso/verifier.ts` and `lib/cso/preparation-container.ts`, from `#!/usr/bin/env bun` to the isolated shebang. That line is embedded in the compiled binary as an unused string and never runs in the image, but it moved the fingerprint. Qualify run 39535948607 therefore refused the run-5 statements, staged at 133cb36, with `STALE_RUNTIME_QUALIFICATION: preparationSha256`. Garry chose this narrow rule change over a re-stage and a fresh paid qualification (2026-10-21, option 2).
+1 -1
View File
@@ -1 +1 @@
1.91.176.0
1.91.178.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.91.176.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.91.178.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,
+1 -1
View File
@@ -385,7 +385,7 @@ function materialize(root: string, source: string) {
const sha256 = publish(root, 'evidence.json', { ...annotations, evidence, learning, verdict });
return { action: 'materialize', status: 'complete', sha256, annotationsSha256: hash(bytes), exitCode: 0, verdict,
reportLinks: notes.map(note => `[checkpoint ${note.name.slice(12, 15)}](${note.name})`),
next: `Include every reportLinks entry in the Markdown report, and report the overall status as ${verdict.status}${verdict.open.length ? ` (open: ${verdict.open.join('; ')})` : ''}; this verdict is final for this report root.` };
next: `Include every reportLinks entry in the Markdown report, and report the overall status as ${verdict.status}${verdict.open.length ? ` (open: ${verdict.open.join('; ')})` : ''}; this verdict is final. Next, write the Markdown report beside evidence.json.` };
}
const QA_EVIDENCE_USAGE = 'capture ROOT ID [--public] --deadline FILE|--timeout-ms MS [--after PREVIOUS_CAPTURE --hypothesis TEXT] -- COMMAND ARGS (--after publishes checkpoint ID linking PREVIOUS_CAPTURE to this probe; required after the first complete capture unless a checkpoint was published) | checkpoint ROOT ID CAPTURE OBSERVATION_COMMAND HYPOTHESIS NEXT_COMMAND | checkpoint ROOT ID INTENT_FILE | materialize ROOT ANNOTATIONS (annotations: {evidence: [{capture, command, contract, expected, classification: pass|superseded|product-defect|fail|setup-blocked|blocked|inconclusive}], limits: [..]}; revision, runtime, cwd and learning are filled in)';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.91.176",
"version": "1.91.178",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+1 -1
View File
@@ -51,7 +51,7 @@ Use one causal sentence per checkpoint hypothesis (English, more than 20 charact
Fixture execution boundary:
- No prior plans, global learnings, remote or cross-session artifact store. Branch/base are main, with a clean successful seed commit. Do not run global setup, telemetry, plan discovery, base-detection scripts or learning writes.
- Reports belong only in existing qa-reports. Retain fixture state; the owner cleans it after preserving evidence. Authorized source/test edits use Write/Edit. Read/Glob/Grep support arbitrary read-only discovery, including directory/path inventory.
- Reports belong only in existing qa-reports. Retain fixture state; the owner cleans it after preserving evidence. Authorized source/test edits use Write/Edit. Every Write/Edit targets an owned path inside this working directory; a write anywhere else, including its parent directory, fails acceptance. Read/Glob/Grep support arbitrary read-only discovery, including directory/path inventory.
- Bash accepts separate literal commands only: no shell composition, scripts or added path operands. Read-only forms are pwd, ls, ls -la, git status --short, git status --porcelain, git branch --show-current, git diff, git diff --stat, git rev-parse HEAD, bun --version, and exactly date -u +%Y-%m-%dT%H:%M:%SZ. Native tests use bun test with optional named test/*.test.ts selectors.
- These are complete command forms, not general shell examples. For inventory inside a named directory, use Read/Glob/Grep; the listed ls forms inspect only the working directory. Do not add operands or flags beyond the declared forms, even for read-only discovery.
- The installed production helper is bin/gstack-qa-evidence (absolute owned path also accepted); \`bun bin/gstack-qa-evidence --help\` prints its usage, and the helper source is not part of the task. Probe outputs are declared public/synthetic, so capture with: bun bin/gstack-qa-evidence capture qa-reports NNN --public --timeout-ms 10000 -- NATIVE_PROBE. The child must be one of the observation forms below. Each execution/replay gets a fresh three-digit ID. The helper does not authorize another command, interpreter, path, pipeline or redirect.
+2 -1
View File
@@ -495,7 +495,8 @@ test('every capture after an input change names the commands still to rerun, unt
const materialized = f.run('materialize', f.root, 'annotations.json');
expect(materialized.status, materialized.stderr).toBe(0);
expect(receipt(materialized.stdout).verdict).toEqual({ status: 'pass', open: [] });
expect(receipt(materialized.stdout).next).toContain('this verdict is final for this report root');
expect(receipt(materialized.stdout).next).toContain('this verdict is final. Next, write the Markdown report beside evidence.json.');
expect(receipt(materialized.stdout).next).not.toContain('report root');
});
test('a descriptive classification is rejected before publication, so the corrected label can still materialize', () => {
+55
View File
@@ -0,0 +1,55 @@
/**
* Every workflow step that runs the GitHub CLI gets a token (free).
*
* On a GitHub-hosted runner `gh` reads no ambient credential: a step without
* GH_TOKEN (or GITHUB_TOKEN) in its step, job or workflow env fails at its
* first gh call with "To use GitHub CLI in a GitHub Actions workflow, set the
* GH_TOKEN environment variable" (exit 4). That is how promote run 39691485727
* died after a 40-minute protected-environment approval: its candidate step
* called `gh api …/compare` with no token. actionlint does not check this.
*/
import { describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
const ROOT = path.resolve(import.meta.dir, '..');
const WORKFLOWS = path.join(ROOT, '.github', 'workflows');
/** A gh invocation at a command position, never ghcr.io, --gh or a word ending in gh. */
const GH_CALL = /(?:^|[\s;&|(`]|\$\()gh\s+[a-z]/;
/** `<file>:<job>:<step>` for every run step that calls gh with no token in scope. */
export function tokenlessGhSteps(file: string, document: any): string[] {
const found: string[] = [];
const hasToken = (env: any) => Boolean(env?.GH_TOKEN || env?.GITHUB_TOKEN);
for (const [jobName, job] of Object.entries<any>(document?.jobs ?? {})) {
for (const [index, step] of (Array.isArray(job?.steps) ? job.steps : []).entries()) {
if (typeof step?.run !== 'string') continue;
const calls = step.run.split('\n').some((line: string) => !line.trim().startsWith('#') && GH_CALL.test(line));
if (calls && !hasToken(step.env) && !hasToken(job.env) && !hasToken(document.env)) found.push(`${file}:${jobName}:${step.name ?? index}`);
}
}
return found;
}
describe('workflow gh token', () => {
test('no workflow step runs gh without GH_TOKEN or GITHUB_TOKEN in scope', () => {
const files = fs.readdirSync(WORKFLOWS).filter(name => /\.ya?ml$/.test(name));
expect(files).toContain('cso-runtime-promote.yml');
const missing = files.flatMap(file => tokenlessGhSteps(file, Bun.YAML.parse(fs.readFileSync(path.join(WORKFLOWS, file), 'utf8'))));
expect(missing).toEqual([]);
});
test('the scanner flags the promote run 39691485727 shape and accepts every scope that supplies a token', () => {
const step = (env?: Record<string, string>) => ({ name: 'candidate', env, run: 'set -e\nx="$(jq -r .a f)"\ngh api "repos/$R/compare/$a...$b" > out.json' });
expect(tokenlessGhSteps('w.yml', { jobs: { propose: { steps: [step({ CSO_QUALIFICATION_RUN: '1' })] } } })).toEqual(['w.yml:propose:candidate']);
expect(tokenlessGhSteps('w.yml', { jobs: { propose: { steps: [step({ GH_TOKEN: '${{ github.token }}' })] } } })).toEqual([]);
expect(tokenlessGhSteps('w.yml', { jobs: { propose: { env: { GITHUB_TOKEN: 't' }, steps: [step()] } } })).toEqual([]);
expect(tokenlessGhSteps('w.yml', { env: { GH_TOKEN: 't' }, jobs: { propose: { steps: [step()] } } })).toEqual([]);
for (const run of ['v="$(gh run list --json id)"', 'a && gh pr create --fill', 'gh attestation verify "oci://$i"']) {
expect(tokenlessGhSteps('w.yml', { jobs: { j: { steps: [{ run }] } } })).toEqual(['w.yml:j:0']);
}
for (const run of ['docker pull ghcr.io/o/r@sha256:x', 'tool --gh api', '# gh api is called by the next step', 'echo high']) {
expect(tokenlessGhSteps('w.yml', { jobs: { j: { steps: [{ run }] } } })).toEqual([]);
}
});
});