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 with f27a7605 the 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 with 1f246e35: 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 the 392f50d4 merge) 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:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-10-27 22:23:20 +00:00
1 parent 215c76e334
commit 10b77de4f2
46 files changed
+1217 -37

No files matched your search

+69 -1
View File
@@ -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) });