mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +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
@@ -185,6 +185,7 @@ export const SKILL_REQUIREMENTS: Readonly<Record<string, SkillRequirement>> = {
|
||||
'ios-sync': CLAUDE_ONLY,
|
||||
'landing-report': CLAUDE_ONLY,
|
||||
learn: CLAUDE_ONLY,
|
||||
modernize: CLAUDE_ONLY,
|
||||
retro: CLAUDE_ONLY,
|
||||
'setup-deploy': CLAUDE_ONLY,
|
||||
'setup-gbrain': CLAUDE_ONLY,
|
||||
|
||||
+35
-7
@@ -44,9 +44,9 @@ export const EXIT = { posted: 0, refused: 1, confirm: 2, cliFailed: 3, usage: 64
|
||||
|
||||
export const USAGE = `usage: gstack-post <op> [<target>] [options]
|
||||
|
||||
gstack-post pr-comment <pr> --body-file F
|
||||
gstack-post issue-comment <issue> --body-file F
|
||||
gstack-post reply <pr> --to <review-comment-id> --body-file F (GitHub only)
|
||||
gstack-post pr-comment <pr> --body-file F [--attribute "<model> on behalf of <login>"]
|
||||
gstack-post issue-comment <issue> --body-file F [--attribute ...]
|
||||
gstack-post reply <pr> --to <review-comment-id> --body-file F [--attribute ...] (GitHub only)
|
||||
gstack-post pr-title <pr> --title-file F
|
||||
gstack-post pr-body <pr> --body-file F [--replace]
|
||||
gstack-post pr-create --base <branch> [--head <branch>] [--draft] --title-file F --body-file F
|
||||
@@ -59,6 +59,8 @@ export const USAGE = `usage: gstack-post <op> [<target>] [options]
|
||||
--replace pr-body: overwrite the whole body instead of merging it by
|
||||
## section into the current one (the default keeps every
|
||||
section the file does not rewrite, the ship receipt included)
|
||||
--attribute <text> comments only; opens the comment with a "> [!NOTE]"
|
||||
callout naming who wrote it (one line, off by default)
|
||||
|
||||
Write the text files with your file-write tool; never put the text in a
|
||||
shell command. The helper scans the exact bytes it sends.
|
||||
@@ -80,11 +82,26 @@ interface Request {
|
||||
host?: HostKind;
|
||||
visibility?: RepoVisibility;
|
||||
confirm?: string;
|
||||
attribute?: string;
|
||||
}
|
||||
|
||||
/** Operations that post a comment, the only ones an attribution callout applies to. */
|
||||
const COMMENT_OPS: ReadonlySet<Op> = new Set<Op>(["pr-comment", "issue-comment", "reply"]);
|
||||
|
||||
/**
|
||||
* The opt-in attribution callout (Garry, 2026-10-26: yes, opt-in, off by
|
||||
* default): a GitHub alert block that opens an agent-posted comment with who
|
||||
* wrote it, so a maintainer reading the thread sees the author before the text.
|
||||
* GitLab renders it as a plain blockquote. Applied to comments and replies only,
|
||||
* never to a PR body, a title or an issue body.
|
||||
*/
|
||||
export function attributionCallout(attribute: string): string {
|
||||
return `> [!NOTE]\n> 🤖 **${attribute}**\n\n`;
|
||||
}
|
||||
|
||||
class UsageError extends Error {}
|
||||
|
||||
const VALUE_FLAGS = new Set(["--to", "--base", "--head", "--title-file", "--body-file", "--host", "--repo-visibility", "--confirm"]);
|
||||
const VALUE_FLAGS = new Set(["--to", "--base", "--head", "--title-file", "--body-file", "--host", "--repo-visibility", "--confirm", "--attribute"]);
|
||||
|
||||
function parseArgs(argv: string[]): Request {
|
||||
const [op, ...rest] = argv;
|
||||
@@ -118,7 +135,13 @@ function parseArgs(argv: string[]): Request {
|
||||
target: positional[0], to: flags.get("--to"), base: flags.get("--base"), head: flags.get("--head"),
|
||||
titleFile: flags.get("--title-file"), bodyFile: flags.get("--body-file"),
|
||||
host: host as HostKind | undefined, visibility: vis as RepoVisibility | undefined, confirm: flags.get("--confirm"),
|
||||
attribute: flags.get("--attribute")?.trim(),
|
||||
};
|
||||
if (req.attribute !== undefined) {
|
||||
if (!COMMENT_OPS.has(req.op)) throw new UsageError("--attribute applies to pr-comment, issue-comment and reply only");
|
||||
if (!req.attribute || /[\r\n\u0000-\u001f]/.test(req.attribute)) throw new UsageError("--attribute needs one line of text, like \"<model> on behalf of <login>\"");
|
||||
if (req.attribute.length > 200) throw new UsageError("--attribute must be 200 characters or fewer");
|
||||
}
|
||||
const creates = req.op === "pr-create" || req.op === "issue-create";
|
||||
if (positional.length > (creates ? 0 : 1)) throw new UsageError(`unexpected argument "${positional[creates ? 0 : 1]}"`);
|
||||
if (!creates && !req.target) throw new UsageError(`${req.op} needs a target (a number or a URL)`);
|
||||
@@ -455,7 +478,7 @@ function send(env: PostEnv, kind: HostKind, req: Request, n: string | undefined,
|
||||
}
|
||||
}
|
||||
if (r.stdout.trim()) env.out(r.stdout.endsWith("\n") ? r.stdout : r.stdout + "\n");
|
||||
env.out(`POSTED: ${req.op}${n ? ` ${n}` : ""}\n`);
|
||||
env.out(`POSTED: ${req.op}${n ? ` ${n}` : ""}${req.attribute ? ` (attributed: ${req.attribute})` : ""}\n`);
|
||||
return EXIT.posted;
|
||||
}
|
||||
|
||||
@@ -482,6 +505,7 @@ export function runPost(argv: string[], env: PostEnv, configBin = "gstack-config
|
||||
const list = (names: string[]) => (names.length ? names.join(", ") : "none");
|
||||
env.err(`gstack-post: pr-body merged by section: replaced ${list(m.replaced)}; kept ${list(m.kept)}; added ${list(m.added)}\n`);
|
||||
}
|
||||
if (body !== undefined && req.attribute) body = attributionCallout(req.attribute) + body;
|
||||
const parts: Array<[Part, string]> = [];
|
||||
if (title !== undefined) parts.push(["title", title]);
|
||||
if (body !== undefined) parts.push(["body", body]);
|
||||
@@ -491,7 +515,11 @@ export function runPost(argv: string[], env: PostEnv, configBin = "gstack-config
|
||||
const hasQuad = parts.some(([, text]) => /\b\d{1,3}(?:\.\d{1,3}){3}\b/.test(text));
|
||||
const { found, oversize } = scanParts(parts, vis, email.status === 0 ? email.stdout.trim() || undefined : undefined, hasQuad ? repoReleaseVersions(env) : []);
|
||||
env.out(`REPO_VISIBILITY: ${vis}\n`);
|
||||
const line = (m: Located) => `RULE: ${m.finding.id} LINE: ${m.finding.line} PART: ${m.part}\n`;
|
||||
const calloutLines = req.attribute ? attributionCallout(req.attribute).split("\n").length - 1 : 0;
|
||||
const line = (m: Located) => {
|
||||
if (m.part === "body" && m.finding.line <= calloutLines) return `RULE: ${m.finding.id} LINE: 1 PART: attribute\n`;
|
||||
return `RULE: ${m.finding.id} LINE: ${m.part === "body" ? m.finding.line - calloutLines : m.finding.line} PART: ${m.part}\n`;
|
||||
};
|
||||
const high = found.filter(m => m.finding.severity === "HIGH");
|
||||
if (oversize || high.length) {
|
||||
for (const m of high) env.out(`HIGH ${line(m)}`);
|
||||
@@ -504,7 +532,7 @@ export function runPost(argv: string[], env: PostEnv, configBin = "gstack-config
|
||||
const medium = found.filter(m => m.finding.severity === "MEDIUM");
|
||||
if (medium.length) {
|
||||
const token = confirmationToken(
|
||||
{ op: req.op, host: remote.kind, repo: [remote.host, ...remote.segments].join("/"), target: n ?? null, to: req.to ?? null, base: req.base ?? null, head: req.head ?? null, draft: req.draft },
|
||||
{ op: req.op, host: remote.kind, repo: [remote.host, ...remote.segments].join("/"), target: n ?? null, to: req.to ?? null, base: req.base ?? null, head: req.head ?? null, draft: req.draft, attribute: req.attribute ?? null },
|
||||
parts, medium);
|
||||
if (req.confirm !== token) {
|
||||
for (const m of medium) env.out(line(m));
|
||||
|
||||
Reference in new issue
Block a user