Tom Boucher 1794acb255 chore(#2331): trigger PR-policy workflows on pull_request_target so fork PRs get the verdict (#2333)
* chore(#2331): trigger PR-policy workflows on pull_request_target

Three PR-policy workflows (pr-title-validator, pr-target-validator,
require-issue-link) triggered on plain `pull_request`, so a fork PR's
GITHUB_TOKEN was downgraded to read-only regardless of the declared
`permissions:`. Each one comments on the PR and THEN emits its verdict, so the
createComment 403 killed the github-script step before core.setFailed ran: the
contributor saw an API stack trace instead of the instructions the comment
exists to deliver. Confirmed on PR #2084 (job 86573823878), whose title has
been non-compliant since 2026-07-08 while the explanatory comment 403'd on
every run.

Switches all three to pull_request_target (base-repo context, write-capable
token), matching the three siblings that already do this correctly
(pr-template-format, close-draft-prs, auto-close-unsolicited-prs). Safe: the
only checkouts are BASE-branch with persist-credentials: false, and every
PR-controlled input is read as data — no head code executes. Also wraps each
comment in try/catch so a comment failure can never again suppress the verdict.

Extends tests/workflow-maintainer-skip.test.cjs with the trigger lock already
applied to close-draft-prs.yml (:32-42) for this same defect class.

Closes #2331

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#2331): strip backticks before echoing untrusted text into bot comments

Found by the orthogonal security review of this change.

pr-title-validator and pr-target-validator echo attacker-controlled text (the PR
title; the fork's branch name) into an inline-code span in a comment posted by
github-actions[bot]. A single backtick closes the span early and the remainder
renders as live Markdown — GFM autolinks a bare URL — so a fork author could
make our own bot post an arbitrary clickable link into a PR thread, borrowing
the bot's credibility for phishing.

This interpolation is unchanged from next, but it was NOT previously reachable
from forks: the createComment call 403'd and the comment was never posted. The
trigger switch in the parent commit is what makes it reachable by untrusted
authors for the first time, using the write token it grants — so it is in scope
here and fixed here rather than deferred.

A PR title has no charset restriction, so that vector is fully exploitable. The
branch-name vector is weaker (check-ref-format forbids space, ':', '[' and '*',
so no bare URL, link or emphasis is expressible) but is the same class and is
stripped identically rather than left to the charset to police. Stripping the
backtick is complete: it is the only character that can break out of an
inline-code span. Only the rendered body needs this — core.warning/setFailed go
to the job log, where @actions/core already escapes workflow commands.

Also strengthens the try/catch test to assert core.setFailed sits AFTER the
catch block rather than merely existing, so moving the verdict inside the try
(the exact inversion #2331 fixes) fails the test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#2331): assert verdict ordering on code, not on comment prose

The first cut of the verdict-ordering guard failed against correct code. It used
indexOf('core.setFailed') on raw source, and these workflows name core.setFailed
in their own comments while explaining the bug — at lines 29/118/147, 16 and 6,
all BEFORE the catch block. So the assertion compared a comment to the call and
reported the inversion it was written to catch. gsd-test caught it: 4 unique
failures across linux-node22/24.

The code was right; the test was measuring the wrong text. Fixes:

- readWorkflowCode() strips whole-line YAML/JS comments so positional
  assertions see only executable text.
- The ordering check is extracted to verdictSurvivesCommentFailure() and
  exercised against BOTH a good and an inverted sample, so the guard is proven
  non-vacuous rather than merely passing.
- A test pins the trap itself: raw source really does mention core.setFailed
  before the catch, while the stripped view puts the real call after it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#2331): drop the unnecessary permission widening; the trigger was the whole bug

Both orthogonal review passes flagged the permissions block, from opposite
directions — one said issues:write was dead surface on require-issue-link, the
other said it was the load-bearing scope the two validators lacked. Neither is
right, and the repo's own history settles it:

- pr-title-validator declares pull-requests:write ONLY, and its sticky comment
  has posted 26 times.
- pr-target-validator declares pull-requests:write ONLY — posted 8 times.
- require-issue-link declares issues:write ONLY — posted on same-repo PRs
  #106, #164, #232, #259.

So GitHub accepts EITHER scope for issues.createComment when the target is a
PR, and all three files already declared a sufficient one. The 403 was purely
the fork token downgrade. My added scopes fixed nothing and widened privilege
on precisely the workflows now running as pull_request_target — the context
where surplus scope matters most. Reverted: permissions are byte-identical to
next, and the diff is now trigger + try/catch + sanitizer only.

The permission test previously used an (issues|pull-requests) alternation, so
it passed on the pre-fix tree and would not have caught removal of the scope
that matters. It now asserts each file's SPECIFIC scope and, more usefully,
asserts the absence of the other — locking the least-privilege property against
a future 'add it to be safe' regression. It is a forward lock, not a #2331
fails-first test; the trigger assertion is the fails-first one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 13:55:15 -04:00

GSD Core

Git. Ship. Done.

English · Português · 简体中文 · 日本語 · 한국어

A light-weight meta-prompting, context engineering, and spec-driven development system for Claude Code, OpenCode, Antigravity CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and more.

npm version npm downloads Tests Discord GitHub stars License


What is GSD Core

GSD Core is a context-engineering and spec-driven development framework that drives AI coding agents (Claude Code, Codex, Antigravity CLI, Kimi CLI, Copilot, Cursor, and more) through a disciplined phase loop. It solves context rot — the quality degradation that accumulates as an AI fills its context window — by running all heavy research, planning, and execution work in fresh-context subagents while keeping your main session lean.


How it works

Each milestone repeats the same five-step loop, one phase at a time:

  1. Discuss — capture implementation decisions before anything is planned
  2. Plan — research, decompose, and verify the plan fits a fresh context window
  3. Execute — run plans in parallel waves; each executor starts with a clean 200k-token context
  4. Verify — walk through what was built; diagnose and fix before declaring done
  5. Ship — create the PR, archive the phase, repeat for the next one

Quickstart

npx @opengsd/gsd-core@latest

The installer prompts for your runtime (Claude Code, OpenCode, Antigravity CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and more) and whether to install globally or locally. The installer is required for cross-runtime compatibility — do not copy files from agents/ or commands/ directly.

On another runtime or without Node.js? See Install on your runtime.

Once installed, start a new project or onboard an existing repo:

/gsd-new-project   # greenfield project
/gsd-onboard       # existing codebase

New here? Follow Your first project for a guided walkthrough from install to first shipped phase, or Onboarding an existing codebase for brownfield setup.


Documentation

What's new in 1.7.0 → docs/whats-new-1.7.0.md

Tutorials — learning by doing:

How-to guides — task-focused recipes:

Reference — authoritative facts:

Explanation — concepts and design decisions:

Full index: docs/README.md. Other languages: 日本語 · 한국어 · Português · 简体中文.


Why it works

Most AI-coding setups fail at scale because context bloat silently degrades output quality, there is no shared memory between sessions, and nothing verifies that code actually works. GSD Core solves all three: heavy work runs in fresh subagents, structured artifacts like STATE.md and CONTEXT.md survive session boundaries, and the verify step walks through what was built and generates fix plans before a phase is declared done. See docs/explanation/context-engineering.md for the full reasoning.

Troubleshooting? See docs/how-to/recover-and-troubleshoot.md.


Community

Project Platform
gsd-opencode Original OpenCode port
Discord Community support

Star History

Star History Chart

License

MIT License. See LICENSE for details.


Claude Code is powerful. GSD Core makes it reliable.

Description
No description provided
Readme MIT 77 MiB
Languages
JavaScript 82.3%
TypeScript 17.4%
Shell 0.3%