mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
v1.91.284.0 feat(absorb-tstack): /modernize skill, per-skill Codex default prompts, gstack-post --attribute, the unattended grant and a CONTRIBUTING porting section (#3497)
* feat(absorb-tstack): /modernize skill, per-skill Codex default prompts, gstack-post --attribute, the unattended grant and a CONTRIBUTING porting section Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(modernize): read the PR text through gstack-issue-guard; re-measure the context ceiling Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes - gstack-post: with --attribute, report scan findings at the body file's own line numbers and a finding inside the callout as PART: attribute (the coordinates were shifted by the three callout lines) - /modernize: route commit messages through gstack-issue-guard with the rest of the PR text, read isCrossRepository and maintainerCanModify, fetch a fork head from pull/N/head, refuse a PR whose head is the base branch, and list moved files NUL-safely - docs/unattended.md: say that /ship never rebases or force-pushes and an unattended /modernize stops at its verdict - CHANGELOG: seven skills carry a codex_default_prompt, not six Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: /modernize fetches a fork head with a forced refspec and stops when the PR moved A rerun after the author force-pushed left refs/modernize/pr-N stale (the non-forced refspec is rejected with exit 0 through the pipe), and the lease pinned to the current remote head would then have let the rebased stale head overwrite the author's newer commits. The fetch is forced, Steps 2 and 4 stop when the head is not the one observed in Step 1, and the head-is-base guard applies only to same-repository PRs (a fork's main into main is a valid head). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: /modernize stops on merge commits in the PR range and pushes a fork head to the fork A plain git rebase drops merge commits and git range-diff compares only the non-merge patches, so a change authored in a PR's merge commit disappeared while the proof printed = for every patch and Step 5 force-pushed the loss. Step 4 now counts merges in $MERGE_BASE..$OLD_HEAD and stops when there are any, switches with -C so a rerun after a refused lease does not fail on the existing branch, and asks for a cross-repository diff to be read as untrusted input before the repository's test command runs it. Step 5 pushes a fork head to the fork's URL with the same pinned lease instead of to origin, where the lease could never match. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: cover the Codex default-prompt render, the openai.yaml ownership proof and the unattended grant Adds the /ship coverage audit's tests: test/host-renders-openai-yaml-proof.test.ts drives bin/gstack-host-renders.sh's weakened proof against a rendered yaml, a hand-written one and a generic-prompt one; test/unattended-grant.test.ts pins the docs/unattended.md grant section and the handoff pointer; test/gen-skill-docs.test.ts adds modernize to the skills whose codex_default_prompt must reach openai.yaml. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: name the static lists a new skill still needs and the Codex metadata file From the ship documentation audit: CONTRIBUTING's new-skill checklist said no static list needs a row, which this PR disproved (lib/doctor-components.ts SKILL_REQUIREMENTS and the catalog rows); the host comparison table and docs/ADDING_A_HOST.md gain the agents/openai.yaml default_prompt source; a blank line before the new docs/unattended.md grant heading. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes CHANGELOG [Unreleased] gate-integrity record: the catalog and context-budget figures re-measured on top of main (1,314 -> 1,359; modernize 9466; alwaysOnTotal 7458 -> 7880), as the merge commit's ratchets state. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: name the optional codex_default_prompt key in the new-skill checklist Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(parity): re-pin the ship union cap at 1.7405 after the main merge GSTA-36 grant 2026-10-27. On the tree merged withf27a7605the ship union is 326,691 / 187,706 = 1.7404 against #3487's 1.7400; the only ship bytes this PR adds are the handoff section's one-line pointer. Cap 1.7400 -> 1.7405, measurement appended to the comment. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): the [Unreleased] gate-integrity bullet names the ship cap re-pin Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes CHANGELOG [Unreleased]: end the Codex default-prompts sentence at "generator input only." so bin/gstack-prose-lint --unreleased --gate (main's new quality-gate step) passes, and restate the gate-integrity bullet's figures for the tree merged with1f246e35: ship cap 1.7524 -> 1.7528 (329,005 / 187,706), modernize 9466 -> 3772 after the preamble hoist, alwaysOnTotal 7769 -> 7880, catalog 1,359 unchanged. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes CHANGELOG [Unreleased] gate-integrity bullet names the current ship cap pin (1.7525 -> 1.7529, measured 329,024 / 187,706 on the392f50d4merge) and keeps the 1.7528 pin in the earlier-merges clause. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
215c76e334
commit
10b77de4f2
46 files changed
+1217
-37
No files matched your search
@@ -71,8 +71,17 @@ import { skillCensus } from './helpers/skill-census';
|
||||
* New-skill ratchet: previous ceiling 1,289 + ceil(99 / 4) = 1,314
|
||||
* token-equivalents (5,256 bytes), leaving 7 bytes. Existing descriptions
|
||||
* are unchanged.
|
||||
* ref modernize addition on base d9b0d600 (2026-10-27), Garry
|
||||
* 2026-10-26 'Do it, send' (plans/absorb-tstack.md item 3)
|
||||
* result pre-addition aggregate 5,249 bytes; modernize adds 178 bytes
|
||||
* (name plus the one-sentence catalog lead naming the three
|
||||
* verdicts), yielding 5,427 bytes = 1,357 token-equivalents
|
||||
* including the root router alias.
|
||||
* New-skill ratchet: previous ceiling 1,314 + ceil(178 / 4) = 1,359
|
||||
* token-equivalents (5,436 bytes), leaving 9 bytes. Existing descriptions
|
||||
* are unchanged.
|
||||
*/
|
||||
const CATALOG_BUDGET_TOKEN_EQUIVALENTS = 1_314;
|
||||
const CATALOG_BUDGET_TOKEN_EQUIVALENTS = 1_359;
|
||||
|
||||
// Largest today: design-consultation at 229 bytes. A description that needs
|
||||
// more than 260 bytes is a body paragraph, not a catalog entry.
|
||||
|
||||
Vendored
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_comment": "Context-budget ratchet ceilings (~tokens). Regenerate: bun test/helpers/capture-context-budget.ts. Headroom: alwaysOnTotal x1.05, eagerPerInvocation x1.1. Graded by test/context-budget-ratchet.test.ts via lib/context-bill.ts checkBudget.",
|
||||
"alwaysOnTotal": 7769,
|
||||
"alwaysOnTotal": 7880,
|
||||
"eagerPerInvocation": {
|
||||
"autoplan": 11289,
|
||||
"benchmark": 8043,
|
||||
@@ -41,6 +41,7 @@
|
||||
"landing-report": 3269,
|
||||
"learn": 2931,
|
||||
"make-pdf": 5204,
|
||||
"modernize": 3772,
|
||||
"office-hours": 19139,
|
||||
"open-gstack-browser": 4872,
|
||||
"openclaw/skills/gstack-openclaw-ceo-review": 2764,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, test, expect, afterAll } from 'bun:test';
|
||||
import { assertSinglePreamble } from '../scripts/gen-skill-docs';
|
||||
import { assertSinglePreamble, extractCodexDefaultPrompt, generateOpenAIYaml } from '../scripts/gen-skill-docs';
|
||||
import { validateSkillFrontmatter } from '../scripts/skill-check';
|
||||
import { externalHostPathLeaks } from './helpers/skill-parser';
|
||||
import { COMMAND_DESCRIPTIONS } from '../browse/src/commands';
|
||||
@@ -2952,6 +2952,57 @@ describe('Codex generation (--host codex)', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// A template's codex_default_prompt: key is the sentence Codex's picker
|
||||
// inserts for the skill; without the key the generic fallback keeps every
|
||||
// entry valid. The key is generator input only: no host render carries it.
|
||||
// Value: protects=the new /modernize skill's picker prompt reaches its Codex render; fails_when=modernize's key is dropped, misquoted or unrendered; why_new=modernize-skill.test reads the template only; seam=none; no_claim=prompt wording quality
|
||||
const CODEX_PROMPT_SKILLS = ['ship', 'review', 'qa', 'autoplan', 'office-hours', 'land-and-deploy', 'modernize'];
|
||||
test('codex_default_prompt: in a template becomes that skill\'s default_prompt', () => {
|
||||
for (const dir of CODEX_PROMPT_SKILLS) {
|
||||
const tmpl = fs.readFileSync(path.join(ROOT, dir, 'SKILL.md.tmpl'), 'utf-8');
|
||||
const prompt = extractCodexDefaultPrompt(tmpl);
|
||||
expect(prompt, `${dir} declares codex_default_prompt`).toBeDefined();
|
||||
expect(prompt!, dir).toContain(`$gstack-${dir}`);
|
||||
expect(prompt!, `${dir}: a prompt tells the picker what the skill is for`).not.toBe(`Use gstack-${dir} for this task.`);
|
||||
const yaml = Bun.YAML.parse(fs.readFileSync(path.join(AGENTS_DIR, `gstack-${dir}`, 'agents', 'openai.yaml'), 'utf-8')) as { interface: { default_prompt: string } };
|
||||
expect(yaml.interface.default_prompt, dir).toBe(prompt!);
|
||||
}
|
||||
});
|
||||
|
||||
test('a template without codex_default_prompt: keeps the generic default_prompt', () => {
|
||||
const tmpl = fs.readFileSync(path.join(ROOT, 'careful', 'SKILL.md.tmpl'), 'utf-8');
|
||||
expect(extractCodexDefaultPrompt(tmpl)).toBeUndefined();
|
||||
const yaml = Bun.YAML.parse(fs.readFileSync(path.join(AGENTS_DIR, 'gstack-careful', 'agents', 'openai.yaml'), 'utf-8')) as { interface: { default_prompt: string } };
|
||||
expect(yaml.interface.default_prompt).toBe('Use gstack-careful for this task.');
|
||||
});
|
||||
|
||||
test('extractCodexDefaultPrompt reads one quoted or plain line and nothing outside the frontmatter', () => {
|
||||
const fm = (line: string) => `---\nname: x\n${line}\ndescription: d\n---\n\ncodex_default_prompt: "body text is not frontmatter"\n`;
|
||||
expect(extractCodexDefaultPrompt(fm('codex_default_prompt: "Use $gstack-x to do \\"it\\"."'))).toBe('Use $gstack-x to do \\"it\\".');
|
||||
expect(extractCodexDefaultPrompt(fm("codex_default_prompt: 'Use $gstack-x: quoted'"))).toBe('Use $gstack-x: quoted');
|
||||
expect(extractCodexDefaultPrompt(fm('codex_default_prompt: Use $gstack-x plain '))).toBe('Use $gstack-x plain');
|
||||
expect(extractCodexDefaultPrompt(fm('codex_default_prompt:'))).toBeUndefined();
|
||||
expect(extractCodexDefaultPrompt(fm('version: 1.0.0'))).toBeUndefined();
|
||||
expect(extractCodexDefaultPrompt('no frontmatter\ncodex_default_prompt: "x"\n')).toBeUndefined();
|
||||
const yaml = Bun.YAML.parse(generateOpenAIYaml('gstack-x', 'short', 'Use $gstack-x: "quoted" prompt')) as { interface: Record<string, string> };
|
||||
expect(yaml.interface.default_prompt).toBe('Use $gstack-x: "quoted" prompt');
|
||||
expect((Bun.YAML.parse(generateOpenAIYaml('gstack-x', 'short')) as { interface: Record<string, string> }).interface.default_prompt).toBe('Use gstack-x for this task.');
|
||||
});
|
||||
|
||||
test('no host render carries codex_default_prompt: (generator input only)', () => {
|
||||
for (const dir of CODEX_PROMPT_SKILLS) {
|
||||
expect(fs.readFileSync(path.join(ROOT, dir, 'SKILL.md'), 'utf-8'), `claude ${dir}`).not.toContain('codex_default_prompt');
|
||||
for (const host of fs.readdirSync(EXTERNAL_OUT)) {
|
||||
const skillsDir = path.join(EXTERNAL_OUT, host, 'skills');
|
||||
if (!fs.existsSync(skillsDir)) continue;
|
||||
for (const name of fs.readdirSync(skillsDir)) {
|
||||
const file = path.join(skillsDir, name, 'SKILL.md');
|
||||
if (fs.existsSync(file)) expect(fs.readFileSync(file, 'utf-8'), `${host}/${name}`).not.toContain('codex_default_prompt');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('no .claude/skills/ in Codex output', () => {
|
||||
for (const skill of CODEX_SKILLS) {
|
||||
const content = readSkillStartup(path.join(AGENTS_DIR, skill.codexName, 'SKILL.md'));
|
||||
@@ -4535,6 +4586,10 @@ describe('voice-triggers processing', () => {
|
||||
const genFm = generated.slice(0, generated.indexOf('\n---', 4));
|
||||
expect(genFm).not.toContain('interactive:');
|
||||
expect(genFm).not.toContain('benefits-from:');
|
||||
const shipTmpl = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md.tmpl'), 'utf-8');
|
||||
expect(shipTmpl.slice(0, shipTmpl.indexOf('\n---', 4))).toContain('codex_default_prompt:');
|
||||
const ship = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
||||
expect(ship.slice(0, ship.indexOf('\n---', 4))).not.toContain('codex_default_prompt:');
|
||||
|
||||
// The runtime-read and host-read keys survive the strip.
|
||||
const investigate = fs.readFileSync(path.join(ROOT, 'investigate', 'SKILL.md'), 'utf-8');
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* the real executable against stub gh/glab on POSIX.
|
||||
*/
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { EXIT, mergeBodySections, runPost, type PostEnv } from "../lib/gstack-post";
|
||||
import { EXIT, attributionCallout, mergeBodySections, runPost, type PostEnv } from "../lib/gstack-post";
|
||||
|
||||
interface Call { cmd: string; args: string[]; input?: string }
|
||||
|
||||
@@ -276,6 +276,74 @@ describe("gstack-post owns the redaction scan", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("gstack-post --attribute labels a comment, opt-in and off by default", () => {
|
||||
const CALLOUT = "> [!NOTE]\n> 🤖 **Claude Opus on behalf of octocat**\n\n";
|
||||
test("without the flag the body is sent exactly as written", () => {
|
||||
const f = fakeEnv({ files: { "/b": "Fixed in abc123.\n" } });
|
||||
expect(run(["pr-comment", "12", "--body-file", "/b"], f)).toBe(EXIT.posted);
|
||||
expect(f.posts()[0]!.input).toBe("Fixed in abc123.\n");
|
||||
expect(f.stdout()).not.toContain("attributed");
|
||||
});
|
||||
|
||||
test("with the flag every comment operation opens with the callout and the body follows unchanged", () => {
|
||||
expect(attributionCallout("Claude Opus on behalf of octocat")).toBe(CALLOUT);
|
||||
const body = "Fixed in abc123.\n";
|
||||
const cases: Array<[string[], string[]]> = [
|
||||
[["pr-comment", "12"], ["pr", "comment", "12", "--body-file=-"]],
|
||||
[["issue-comment", "3"], ["issue", "comment", "3", "--body-file=-"]],
|
||||
[["reply", "4", "--to", "991"], ["api", "--method=POST", "repos/{owner}/{repo}/pulls/4/comments/991/replies", "--field=body=@-"]],
|
||||
];
|
||||
for (const [argv, args] of cases) {
|
||||
const f = fakeEnv({ files: { "/b": body } });
|
||||
expect(run([...argv, "--body-file", "/b", "--attribute", "Claude Opus on behalf of octocat"], f)).toBe(EXIT.posted);
|
||||
expect(f.posts()).toEqual([{ cmd: "gh", args, input: CALLOUT + body }]);
|
||||
expect(f.stdout()).toContain(`POSTED: ${argv[0]} ${argv[1]} (attributed: Claude Opus on behalf of octocat)`);
|
||||
}
|
||||
const gl = fakeEnv({ files: { "/b": body }, remote: "https://gitlab.com/grp/proj.git" });
|
||||
expect(run(["pr-comment", "8", "--body-file", "/b", "--attribute", "Claude Opus on behalf of octocat"], gl)).toBe(EXIT.posted);
|
||||
expect(gl.posts()[0]!.args).toEqual(["mr", "note", "8", `--message=${CALLOUT}${body}`]);
|
||||
});
|
||||
|
||||
test("the scan and the confirmation token cover the callout bytes too", () => {
|
||||
const files = { "/b": "Ping jane.roe@acme-corp.io about it.\n" };
|
||||
const first = fakeEnv({ files, visibility: "public" });
|
||||
expect(run(["pr-comment", "9", "--body-file", "/b", "--attribute", "GPT on behalf of octocat"], first)).toBe(EXIT.confirm);
|
||||
expect(first.stdout()).toContain("RULE: pii.email LINE: 1 PART: body");
|
||||
const t = token(first.stdout())!;
|
||||
const plain = fakeEnv({ files, visibility: "public" });
|
||||
expect(run(["pr-comment", "9", "--body-file", "/b"], plain)).toBe(EXIT.confirm);
|
||||
expect(token(plain.stdout())).not.toBe(t);
|
||||
const other = fakeEnv({ files, visibility: "public" });
|
||||
expect(run(["pr-comment", "9", "--body-file", "/b", "--attribute", "GPT on behalf of someone-else", "--confirm", t], other)).toBe(EXIT.confirm);
|
||||
expect(other.posts()).toEqual([]);
|
||||
const second = fakeEnv({ files, visibility: "public" });
|
||||
expect(run(["pr-comment", "9", "--body-file", "/b", "--attribute", "GPT on behalf of octocat", "--confirm", t], second)).toBe(EXIT.posted);
|
||||
expect(second.posts()[0]!.input).toBe(attributionCallout("GPT on behalf of octocat") + files["/b"]);
|
||||
const secret = fakeEnv({ files: { "/b": "ok\n" } });
|
||||
expect(run(["pr-comment", "9", "--body-file", "/b", "--attribute", `key ${AWS}`], secret)).toBe(EXIT.refused);
|
||||
expect(secret.stdout()).toContain("HIGH RULE: aws.access_key LINE: 1 PART: attribute");
|
||||
expect(secret.posts()).toEqual([]);
|
||||
});
|
||||
|
||||
test("the flag is refused on non-comment operations and for empty or multi-line text", () => {
|
||||
const files = { "/t": "v1.2.3 fix: title", "/b": "body" };
|
||||
for (const argv of [
|
||||
["pr-body", "5", "--body-file", "/b", "--attribute", "x on behalf of y"],
|
||||
["pr-title", "5", "--title-file", "/t", "--attribute", "x on behalf of y"],
|
||||
["pr-create", "--base", "main", "--title-file", "/t", "--body-file", "/b", "--attribute", "x on behalf of y"],
|
||||
["issue-create", "--title-file", "/t", "--body-file", "/b", "--attribute", "x on behalf of y"],
|
||||
["pr-comment", "5", "--body-file", "/b", "--attribute", " "],
|
||||
["pr-comment", "5", "--body-file", "/b", "--attribute", "two\nlines"],
|
||||
["pr-comment", "5", "--body-file", "/b", "--attribute", "x".repeat(201)],
|
||||
["pr-comment", "5", "--body-file", "/b", "--attribute"],
|
||||
]) {
|
||||
const f = fakeEnv({ files });
|
||||
expect(run(argv, f), argv.join(" ")).toBe(EXIT.usage);
|
||||
expect(f.posts()).toEqual([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("gstack-post reports CLI failures and repairs known edit failures", () => {
|
||||
test("a failed gh call exits 3", () => {
|
||||
const f = fakeEnv({ files: { "/b": "hi" }, respond: c => (c.cmd === "gh" && c.args[0] === "pr" ? { status: 1, stderr: "HTTP 404" } : undefined) });
|
||||
|
||||
@@ -168,7 +168,7 @@ export const CARVE_GUARDS: Record<string, CarveGuard> = {
|
||||
// wave's headline capability) grows the union to 1.195x. Deliberate:
|
||||
// the section is on-demand (loads only for Apple store targets), so
|
||||
// per-invocation cost for non-iOS ships is one manifest line.
|
||||
maxSizeRatio: 1.7525, // GSTA-128 flaky pass (#3516, 2026-10-27, GStack Minder's rule 19 grant): the review-army section's small-diff clause names State Machines as item 9's call and a State Machines-only specialist list, +5 union bytes on a main (703b0049, v1.91.280.0) that sat 4 bytes under the 1.7524 cap after #3515 and #3517; measured 328,937 / 187,706 = 1.7524 (before: 328,932 = 1.7524) on the merged tree, pinned one in the last place above. Before that: GSTA-96 hoist merged with v1.91.273.0 (#3501, 2026-10-27): main's 1.7500 plus the hoist's own raise, no trim; measured 328,918 / 187,706 = 1.7523 on the merged tree. Before: Shared advisory identity/dedup + critical-severity validation: 248,065 union bytes / 187,706 baseline = 1.3216 (2026-09-17). + test value bar in the lazy Step 7 section (value cards, weak paths, gate table, base control, machine checks; ~13.6KB): measured 1.396 (2026-09-29). + W1 guarded state-root resolution (`eval gstack-paths; : "${GSTACK_STATE_ROOT:?…}"`) in the Context Recovery preamble, the eureka log and each state-writing bash block; measured 1.401 (2026-09-30). + the shared QA review step's plan-check timing rule (plan checks and their revalidation run on --timeout-ms after smoke expiry); measured 1.4022 (2026-09-30). + the shared TEST_BOOTSTRAP owned-change undo rules (s03 H1-H3, M1) and the value-bar coverage wording (s03 M2/M3); measured 1.4044 (2026-10-02). + severe fix wave ship-review lane: G1 NO_VERSION dispatch, title and CHANGELOG arms (versionless repos ship without inventing a version), F1 parent-side plan binding before the audit dispatch (binding, docs/designs candidates, exact not-run line) and D3's data-not-instructions line; +2,126 union bytes, measured 1.4135 (2026-10-03). + INV-1 consumer lines (outside review `unverified`/`unavailable` is missing coverage in the readiness note and PR body); measured 1.4148 (2026-10-03). + v1.91.19.0 severe fix wave: verdict-form outside-review callers with sandbox preflight and first-use notice (INV-1/B1/Q2), P0/P1 gate and unverified wording, bound-plan discovery (F1), visible LEARNINGS failures (B5), carved external pointers; measured 267,752 / 187,706 = 1.4264 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 267,874 / 187,706 = 1.4271 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051) and quoted-heredoc issue template (#3046); measured 268,199 / 187,706 = 1.4288 (2026-10-06). + Step 10's inline no-PR check before the Greptile dispatch (#3020); measured 268,655 / 187,706 = 1.4313 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 275,422 / 187,706 = 1.4673 (2026-10-06). Combined Oct 6 wave head (B free-text files + D measure loop + A rework): measured 280,363 / 187,706 = 1.4936 (2026-10-06). + gate-once push rule (no second paid gate after a CI push); measured 280,573 / 187,706 = 1.4947 (2026-10-06). + the measurement bar's decision step (exit-code branches for MEETS, MEETS-qualified, needs-classify with the qualifying and fix-round classes, EXTEND once; the measure prose was tightened to offset most of it); measured 281,058 / 187,706 = 1.4973 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 281,438 / 187,706 = 1.4994 (2026-10-07). + Oct 7 wave CEO-28/DX-11: review prompts ask for a severity per finding and design proposals use the proposal gate; measured 281,556 / 187,706 = 1.5000 (2026-10-07). + Oct 7 wave B4: /ship publishes through gstack-post (title prepared in a file in the pr-body section, compose block plus gstack-post exit-code branches replacing the two scans); measured 281,894 / 187,706 = 1.5018 (2026-10-07). + B2 early Greptile PR (lazy Step 6.5 section: detect, public-repo consent, push, provisional title file, open; Step 10's bounded wait; the stop-leaves-the-draft rule); measured 286,626 / 187,706 = 1.5270 (2026-10-07). B3, the classifier, B4 and B2 merged: measured 287,128 / 187,706 = 1.5297 (2026-10-07). + Oct 7 wave guard (CEO-20): conditional `run_in_background` request plus the background-run recovery at each foreground dispatch site; measured 281,370 / 187,706 = 1.4990 (2026-10-07). Merged with the guard area: measured 287,440 / 187,706 = 1.5313 (2026-10-07). + Oct 8 wave: the defensive-security framing and fixture summary mode reach the security specialist and the Red Team (stated once in the dispatch, referenced by the Red Team); measured 288,783 / 187,706 = 1.5385 (2026-10-08). + gate integrity (HONEST_WORK_GATE_INTEGRITY W3): the shared GATE_SCAN_BLOCK in the core checklist (2.5) and after Step 7's child, the four-option gate question and record fields in 9.4, tests_ran states in Step 5, Gate edits and per-lane counts in the PR body with draft-whil
Line truncated
|
||||
maxSizeRatio: 1.7529, // GSTA-128 flaky pass (#3516, 2026-10-27, GStack Minder's rule 19 grant): the review-army section's small-diff clause names State Machines as item 9's call and a State Machines-only specialist list, +5 union bytes on a main (703b0049, v1.91.280.0) that sat 4 bytes under the 1.7524 cap after #3515 and #3517; measured 328,937 / 187,706 = 1.7524 (before: 328,932 = 1.7524) on the merged tree, pinned one in the last place above. Before that: GSTA-96 hoist merged with v1.91.273.0 (#3501, 2026-10-27): main's 1.7500 plus the hoist's own raise, no trim; measured 328,918 / 187,706 = 1.7523 on the merged tree. Before: Shared advisory identity/dedup + critical-severity validation: 248,065 union bytes / 187,706 baseline = 1.3216 (2026-09-17). + test value bar in the lazy Step 7 section (value cards, weak paths, gate table, base control, machine checks; ~13.6KB): measured 1.396 (2026-09-29). + W1 guarded state-root resolution (`eval gstack-paths; : "${GSTACK_STATE_ROOT:?…}"`) in the Context Recovery preamble, the eureka log and each state-writing bash block; measured 1.401 (2026-09-30). + the shared QA review step's plan-check timing rule (plan checks and their revalidation run on --timeout-ms after smoke expiry); measured 1.4022 (2026-09-30). + the shared TEST_BOOTSTRAP owned-change undo rules (s03 H1-H3, M1) and the value-bar coverage wording (s03 M2/M3); measured 1.4044 (2026-10-02). + severe fix wave ship-review lane: G1 NO_VERSION dispatch, title and CHANGELOG arms (versionless repos ship without inventing a version), F1 parent-side plan binding before the audit dispatch (binding, docs/designs candidates, exact not-run line) and D3's data-not-instructions line; +2,126 union bytes, measured 1.4135 (2026-10-03). + INV-1 consumer lines (outside review `unverified`/`unavailable` is missing coverage in the readiness note and PR body); measured 1.4148 (2026-10-03). + v1.91.19.0 severe fix wave: verdict-form outside-review callers with sandbox preflight and first-use notice (INV-1/B1/Q2), P0/P1 gate and unverified wording, bound-plan discovery (F1), visible LEARNINGS failures (B5), carved external pointers; measured 267,752 / 187,706 = 1.4264 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 267,874 / 187,706 = 1.4271 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051) and quoted-heredoc issue template (#3046); measured 268,199 / 187,706 = 1.4288 (2026-10-06). + Step 10's inline no-PR check before the Greptile dispatch (#3020); measured 268,655 / 187,706 = 1.4313 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 275,422 / 187,706 = 1.4673 (2026-10-06). Combined Oct 6 wave head (B free-text files + D measure loop + A rework): measured 280,363 / 187,706 = 1.4936 (2026-10-06). + gate-once push rule (no second paid gate after a CI push); measured 280,573 / 187,706 = 1.4947 (2026-10-06). + the measurement bar's decision step (exit-code branches for MEETS, MEETS-qualified, needs-classify with the qualifying and fix-round classes, EXTEND once; the measure prose was tightened to offset most of it); measured 281,058 / 187,706 = 1.4973 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 281,438 / 187,706 = 1.4994 (2026-10-07). + Oct 7 wave CEO-28/DX-11: review prompts ask for a severity per finding and design proposals use the proposal gate; measured 281,556 / 187,706 = 1.5000 (2026-10-07). + Oct 7 wave B4: /ship publishes through gstack-post (title prepared in a file in the pr-body section, compose block plus gstack-post exit-code branches replacing the two scans); measured 281,894 / 187,706 = 1.5018 (2026-10-07). + B2 early Greptile PR (lazy Step 6.5 section: detect, public-repo consent, push, provisional title file, open; Step 10's bounded wait; the stop-leaves-the-draft rule); measured 286,626 / 187,706 = 1.5270 (2026-10-07). B3, the classifier, B4 and B2 merged: measured 287,128 / 187,706 = 1.5297 (2026-10-07). + Oct 7 wave guard (CEO-20): conditional `run_in_background` request plus the background-run recovery at each foreground dispatch site; measured 281,370 / 187,706 = 1.4990 (2026-10-07). Merged with the guard area: measured 287,440 / 187,706 = 1.5313 (2026-10-07). + Oct 8 wave: the defensive-security framing and fixture summary mode reach the security specialist and the Red Team (stated once in the dispatch, referenced by the Red Team); measured 288,783 / 187,706 = 1.5385 (2026-10-08). + gate integrity (HONEST_WORK_GATE_INTEGRITY W3): the shared GATE_SCAN_BLOCK in the core checklist (2.5) and after Step 7's child, the four-option gate question and record fields in 9.4, tests_ran states in Step 5, Gate edits and per-lane counts in the PR body with draft-whil
Line truncated
|
||||
},
|
||||
'plan-ceo-review': {
|
||||
skill: 'plan-ceo-review',
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* bin/gstack-host-renders.sh: the WEAK proof that an agents/openai.yaml is the
|
||||
* generator's own render, now that a template's codex_default_prompt: makes
|
||||
* the default_prompt line vary per skill (it used to be one fixed sentence).
|
||||
*
|
||||
* Value: protects=_gstack_hr_generated accepts the generator's openai.yaml for any default_prompt and still rejects a foreign or edited one; fails_when=the shape check reverts to the generic-prompt literal, or drops display_name/policy/6-line limits; why_new=host-renders-prune asserts one backed-up line; no test exercises the rejected shapes; seam=none; no_claim=what the prune does after the proof
|
||||
*/
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { generateOpenAIYaml } from '../scripts/gen-skill-docs';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const HELPER = path.join(ROOT, 'bin/gstack-host-renders.sh');
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('_gstack_hr_generated proves an openai.yaml by shape, not by its prompt sentence', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-openai-yaml-proof-'));
|
||||
const tree = path.join(dir, '.agents');
|
||||
const real = path.join(dir, 'checkout');
|
||||
fs.mkdirSync(real, { recursive: true });
|
||||
|
||||
function proven(rel: string, content: string): boolean {
|
||||
const file = path.join(tree, rel);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, content);
|
||||
const r = spawnSync('bash', ['-c', `. "${HELPER}"\n_gstack_hr_generated "${file}" "${tree}" "${real}" && echo proven || echo kept`], {
|
||||
encoding: 'utf8', timeout: 30_000, env: { PATH: process.env.PATH ?? '/usr/bin:/bin', HOME: os.tmpdir() },
|
||||
});
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
return r.stdout.trim() === 'proven';
|
||||
}
|
||||
|
||||
const custom = generateOpenAIYaml('gstack-ship', 'short', 'Use $gstack-ship to take this branch to a pull request: "quoted", colons: too.');
|
||||
const generic = generateOpenAIYaml('gstack-careful', 'short');
|
||||
|
||||
test('the generator\'s render is proven whether the prompt is the skill\'s own sentence or the fallback', () => {
|
||||
expect(custom).not.toContain('Use gstack-ship for this task.');
|
||||
expect(proven('skills/gstack-ship/agents/openai.yaml', custom)).toBe(true);
|
||||
expect(generic).toContain('default_prompt: "Use gstack-careful for this task."');
|
||||
expect(proven('skills/gstack-careful/agents/openai.yaml', generic)).toBe(true);
|
||||
});
|
||||
|
||||
test('a file that is not the generator\'s shape for that skill directory is kept, not backed up', () => {
|
||||
expect(proven('skills/gstack-qa/agents/openai.yaml', custom), 'display_name names another skill').toBe(false);
|
||||
expect(proven('skills/gstack-ship/agents/openai.yaml', custom.replace('policy:\n', 'rules:\n')), 'no policy: block').toBe(false);
|
||||
expect(proven('skills/gstack-ship/agents/openai.yaml', custom.replace(' default_prompt: "', ' default_prompt: \'')), 'prompt not double-quoted').toBe(false);
|
||||
expect(proven('skills/gstack-ship/agents/openai.yaml', custom + ' my_note: hand-written\n'), 'a seventh line').toBe(false);
|
||||
expect(proven('skills/gstack-ship/agents/openai.yaml', 'notes:\n' + custom.slice('interface:\n'.length)), 'first line is not interface:').toBe(false);
|
||||
expect(proven('skills/gstack-ship/openai.yaml', custom), 'not under agents/').toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* /modernize: an aging pull request gets a disposition before any push.
|
||||
*
|
||||
* Pins the three verdicts and what each permits, the hard rules (no push and
|
||||
* no close before the verdict; the skill never closes a PR; a clean rebase is
|
||||
* not currency; age and sunk cost are not evidence), the range-diff proof and
|
||||
* the lease pinned to the observed head, and the catalog rows a new skill owes.
|
||||
*/
|
||||
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 read = (rel: string) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
||||
const tmpl = read('modernize/SKILL.md.tmpl');
|
||||
const skill = read('modernize/SKILL.md');
|
||||
const flat = skill.replace(/\s+/g, ' ');
|
||||
|
||||
describe('/modernize skill', () => {
|
||||
test('frontmatter: tier 2, sensitive, a Codex default prompt and the four triggers', () => {
|
||||
expect(tmpl.startsWith('---\nname: modernize\npreamble-tier: 2\n')).toBe(true);
|
||||
expect(tmpl).toMatch(/^sensitive: true$/m);
|
||||
expect(tmpl).toMatch(/^codex_default_prompt: "Use \$gstack-modernize to /m);
|
||||
for (const t of ['modernize this pr', 'rebase this old pr', 'revive this branch', 'is this pr still worth landing']) {
|
||||
expect(tmpl, t).toContain(' - ' + t + '\n');
|
||||
}
|
||||
expect(skill.slice(0, skill.indexOf('\n---', 4))).not.toContain('codex_default_prompt');
|
||||
});
|
||||
|
||||
test('the disposition table names the three verdicts and comes before any push', () => {
|
||||
for (const v of ['**MODERNIZE**', '**REWORK**', '**CLOSE**']) expect(skill).toContain('| ' + v + ' |');
|
||||
expect(flat).toContain('The disposition comes first. No rebase is pushed, and no PR is closed, until the verdict below is written and shown.');
|
||||
expect(skill.indexOf('## Step 3: The disposition')).toBeLessThan(skill.indexOf('## Step 4 (MODERNIZE only)'));
|
||||
expect(skill.indexOf('## Step 4 (MODERNIZE only)')).toBeLessThan(skill.indexOf('## Step 5 (MODERNIZE only)'));
|
||||
expect(flat).toContain('STOP and ask (AskUserQuestion) before any push or any closing comment');
|
||||
expect(flat).toContain('If the session is unattended, the verdict and its evidence are the handoff and nothing is pushed.');
|
||||
});
|
||||
|
||||
test('never closes a PR on its own', () => {
|
||||
expect(flat).toContain('You never close a PR.');
|
||||
expect(flat).toContain('Without explicit permission in this session, the PR stays open.');
|
||||
expect(flat).toContain('The PR stays open until someone with the authority says so.');
|
||||
expect(skill).not.toMatch(/gh pr close/);
|
||||
});
|
||||
|
||||
test('a clean rebase is not currency and age is not evidence', () => {
|
||||
expect(flat).toContain('Textual cleanliness is not currency.');
|
||||
expect(flat).toContain('Age is not evidence either way.');
|
||||
expect(flat).toContain('Effort already spent on it is not a reason to keep it.');
|
||||
expect(flat).toContain('conceptual: did the base change the thing the PR relies on, even in files the PR never touches?');
|
||||
});
|
||||
|
||||
test('the rebase is proved with git range-diff and pushed with a lease pinned to the observed head', () => {
|
||||
expect(skill).toContain('git range-diff "$MERGE_BASE..$OLD_HEAD" "origin/$BASE..HEAD"');
|
||||
expect(flat).toContain('A hunk you cannot explain is a lost intent: stop and look again.');
|
||||
expect(skill).toContain('OBSERVED=$(gh pr view "$PR" --json headRefOid -q .headRefOid)');
|
||||
expect(skill).toContain('git push "--force-with-lease=$HEAD_BRANCH:$OBSERVED" origin "HEAD:$HEAD_BRANCH"');
|
||||
expect(flat).toContain('`--force` is never used');
|
||||
expect(skill).not.toMatch(/git push (-f|--force)(?!-with-lease)/);
|
||||
expect(flat).toContain("Keep the author's commits as theirs; your fixes are separate commits on top.");
|
||||
});
|
||||
|
||||
test('/careful already rates the lease push MEDIUM, so no new HIGH pattern was added', () => {
|
||||
expect(read('careful/SKILL.md.tmpl')).toContain('`--force-with-lease` is never HIGH');
|
||||
});
|
||||
|
||||
test('is listed where every skill is listed', () => {
|
||||
expect(read('README.md')).toMatch(/^\| `\/modernize` \| /m);
|
||||
expect(read('AGENTS.md')).toMatch(/^\| `\/modernize` \| /m);
|
||||
expect(read('docs/skills.md')).toMatch(/^\| \[`\/modernize`\]\(#modernize\) \| /m);
|
||||
expect(read('docs/skills.md')).toContain('## `/modernize`');
|
||||
expect(read('SKILL.md.tmpl')).toContain('invoke `/modernize`');
|
||||
expect(read('gstack/llms.txt')).toContain('[/modernize](modernize/SKILL.md)');
|
||||
expect(read('docs/PROJECT_STRUCTURE.md')).toMatch(/^├── modernize\//m);
|
||||
expect(read('lib/doctor-components.ts')).toMatch(/^ modernize: CLAUDE_ONLY,$/m);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* docs/unattended.md#what-an-unattended-session-may-do: the grant a parent
|
||||
* gives an unattended run, and the pointer /ship's handoff section carries so a
|
||||
* lane reads it where it reports done.
|
||||
*
|
||||
* Value: protects=the handoff pointer resolves to the grant section, and the grant states merge-never, the pinned-lease push form and the /modernize disposition; fails_when=the heading is renamed or moved, the pointer is dropped from the template or its render, or the grant loses a rule; why_new=no test reads the new section or the handoff pointer; seam=none; no_claim=that a run obeys the grant (eval-side)
|
||||
*/
|
||||
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 read = (rel: string) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
||||
const POINTER = 'docs/unattended.md#what-an-unattended-session-may-do';
|
||||
const slug = (heading: string) => heading.toLowerCase().replace(/[^a-z0-9 -]/g, '').trim().replace(/ /g, '-');
|
||||
|
||||
describe('the unattended grant and the /ship handoff pointer to it', () => {
|
||||
const docs = read('docs/unattended.md');
|
||||
const grantStart = docs.indexOf('\n## What an unattended session may do\n');
|
||||
const grant = docs.slice(grantStart + 1, (i => i === -1 ? docs.length : i)(docs.indexOf('\n## ', grantStart + 1)));
|
||||
|
||||
test('the handoff section points at a heading that exists, in the template and its render', () => {
|
||||
expect(grantStart).toBeGreaterThan(0);
|
||||
const heading = grant.slice(3, grant.indexOf('\n'));
|
||||
expect(`docs/unattended.md#${slug(heading)}`).toBe(POINTER);
|
||||
expect(read('ship/sections/handoff.md.tmpl')).toContain(`What an\nunattended run may do is ${POINTER}.`);
|
||||
expect(read('ship/sections/handoff.md')).toContain(POINTER);
|
||||
expect(grant).toContain('`/ship`\'s handoff section (`ship/sections/handoff.md`) points here');
|
||||
});
|
||||
|
||||
test('the grant names each rule once: merge never, a pinned-lease force-push on its own branch, named test methods, own-eyes proof', () => {
|
||||
const flat = grant.replace(/\s+/g, ' ');
|
||||
expect(flat).toContain('- **Merge: never.**');
|
||||
expect(flat).toContain('- **Force-push: its own branch only, after a rebase the plan named, with a pinned lease.**');
|
||||
expect(flat).toContain('- **Tests: the methods the repository\'s instructions name, without asking first.**');
|
||||
expect(flat).toContain('- **Proof is what the run saw itself.**');
|
||||
expect(flat).toContain('`git push --force-with-lease=<branch>:<sha observed before the rebase> origin <branch>`');
|
||||
expect(flat).toContain('a plain `--force` is never allowed');
|
||||
expect(flat).toContain('`git range-diff <old base>..<old head> <new base>..<new head>` runs before the push');
|
||||
expect(flat).toContain('a `/modernize` disposition names before it happens');
|
||||
expect(flat).toContain('enabling auto-merge, adding the PR to a merge queue, or `gh pr ready` on a PR the run did not open');
|
||||
expect(flat).toContain('appended to `decisions.jsonl` with `kind: "approval"` and `status: "pending"`');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user