Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/RELEASE_NOTES_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@ typecheck: N/N packages green

---

**Full changelog:** `{previous_tag}...{current_tag}`
**Full changelog:** [`{previous_tag}`...`{current_tag}`](https://github.com/LeXwDeX/OpenCode-GraphAgent/compare/{previous_tag}...{current_tag})
59 changes: 59 additions & 0 deletions .github/releases/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Release Notes Series Files

One markdown file per release series — `.github/releases/vX.Y.Z.md` — is the
source of truth for the GitHub Release body. The `dev` prereleases
(`X.Y.Z-dev.1 … dev.N`) and the `main` stable promotion (`X.Y.Z`) of a series
all render the **same** file; only the channel word differs.

The release job (`.github/workflows/release-fork.yml`) renders and validates
the file **before** `gh release create` and fails closed on any violation — a
release can never ship with placeholder notes.

## Lifecycle

1. **Series starts** — when `X.Y.Z` becomes the next version, the release job
looks for `.github/releases/vX.Y.Z.md`. Until that file is committed, every
release attempt of the series fails; the validator error names the exact
expected path. This is intentional.
2. **Dev prereleases** — each `X.Y.Z-dev.N` build re-renders the current file
content. Update the file as the series evolves.
3. **Stable promotion** — the `main` release of `X.Y.Z` renders the same file;
`{Prerelease/Stable}` becomes `Stable`. The compare range always spans from
the last stable tag, not from the previous `-dev.N`.
4. **Series closes** — after the stable release ships, the file remains as the
historical record. The next series needs its own new `vX.Y.(Z+1).md`.

## Placeholders

Five tokens are machine-substituted at render time:

| Token | Replaced with |
| -------------------- | --------------------------------------------------------- |
| `{VERSION}` | bare semver, e.g. `1.0.10` (no `v` prefix) |
| `{Prerelease/Stable}` | `Prerelease` on `dev`, `Stable` on `main` |
| `{branch}` | releasing branch name (`dev` or `main`) |
| `{previous_tag}` | latest existing stable tag, e.g. `graphagent-v1.0.9` |
| `{current_tag}` | the tag being released, e.g. `graphagent-v1.0.10` |

The template also contains authoring-guidance braces (`{Feature name}`,
`{module}`, `{One-sentence summary …}`). These are **not** substituted —
replace every one of them with real content. The validator fails on any
residual `{` or `}` in the rendered notes.

## Authoring rules (enforced fail-closed)

- Start from `.github/RELEASE_NOTES_TEMPLATE.md` and keep the exact `### `
emoji headings, their canonical order, and the `---` separators between
sections. Omit sections that have no content — do not leave empty headers.
- Copy the emoji headings verbatim from the template; never retype them. The
🏗️ (Architecture / Refactor) and ⚙️ (CI / Engineering) headings end with an
invisible U+FE0F variation selector that editors and copy-paste can strip.
- Prose must be ASCII everywhere except the emoji headings themselves.
- `### 🧪 Test Summary` and `### 🔍 Verification` are mandatory in every
release; the Test Summary body needs at least one fenced code block.
- The final line is the full-changelog compare link with the repository slug
written out literally (`https://github.com/LeXwDeX/OpenCode-GraphAgent/compare/{previous_tag}...{current_tag}`).
A repository rename fails validation on purpose — update the series file.

The grammar is implemented in `packages/opencode/script/release-notes.ts`
(rule errors are prefixed `[release-notes]`).
27 changes: 26 additions & 1 deletion .github/workflows/release-fork.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ jobs:
tag: ${{ steps.release-version.outputs.tag }}
prerelease: ${{ steps.release-version.outputs.prerelease }}
latest: ${{ steps.release-version.outputs.latest }}
previous_tag: ${{ steps.release-version.outputs.previous_tag }}

steps:
- name: Checkout Repository
uses: actions/checkout@v4
Expand Down Expand Up @@ -270,6 +272,29 @@ jobs:
echo "--- SHA256SUMS ---"
cat SHA256SUMS

- name: Setup Bun
uses: ./.github/actions/setup-bun
with:
save-cache: false

# Render + validate the per-series notes file (.github/releases/vX.Y.Z.md)
# BEFORE creating the release. Fail closed: a missing or invalid series
# file stops the job here, so a release can never ship with placeholder
# notes. The script derives the series filename from --version; the
# workflow passes only primitives. Rendered notes go to RUNNER_TEMP and
# are never attached as a release asset.
- name: Render Release Notes (fail closed)
run: |
bun run ./packages/opencode/script/release-notes.ts \
--notes-dir ".github/releases" \
--version "${{ needs.version.outputs.version }}" \
--channel "${{ needs.version.outputs.channel }}" \
--branch "${{ github.ref_name }}" \
--tag "${{ needs.version.outputs.tag }}" \
--previous-tag "${{ needs.version.outputs.previous_tag }}" \
--repo "${{ github.repository }}" \
--out "$RUNNER_TEMP/RELEASE_NOTES.md"

- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
Expand All @@ -284,7 +309,7 @@ jobs:
fi
gh release create "${{ needs.version.outputs.tag }}" \
--title "OpenCode GraphAgent v${{ needs.version.outputs.version }}" \
--notes "GraphAgent release from branch ${{ github.ref_name }}" \
--notes-file "$RUNNER_TEMP/RELEASE_NOTES.md" \
--target "${{ github.sha }}" \
"${EXTRA_FLAGS[@]}" \
release-assets/*
Expand Down
44 changes: 17 additions & 27 deletions .github/workflows/specgit-accept.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ name: SpecGit Acceptance

on:
pull_request:
# Delivery PRs target dev (fast-integration layer) and are promoted to
# main via the release PR — main's legacy branch protection also requires
# the SpecGit Acceptance check, so the verdict must run on both targets.
# dev→main promotion stays governed by the protect-main Ruleset's four
# required checks.
branches: [dev, main]
branches: [main]

permissions:
contents: read
Expand All @@ -16,10 +11,7 @@ jobs:
specgit-acceptance:
name: SpecGit Acceptance
runs-on: ubuntu-latest
# Must exceed the slowest required sibling (Unit Tests (linux) runs
# ~28min on PRs): the verdict waits for every policy check to reach a
# terminal state before evaluating.
timeout-minutes: 45
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -31,18 +23,20 @@ jobs:
fetch-depth: 0
persist-credentials: false

- name: Setup pnpm
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
node-version: '20.19.0'
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

# This repo is a bun workspace and does not vendor the SpecGit CLI;
# install the published CLI instead of building from source. Pinned
# with a caret floor (#366): the CLI releases multiple times a day and
# an unpinned install would let an unnoticed upstream change flip CI
# acceptance verdicts repo-wide.
- name: Install specgit CLI
run: npm install -g specgit@^0.5.0
- name: Build CLI
run: pnpm run build

- name: Wait for sibling checks
# The verdict must see the OTHER required checks in a terminal
Expand All @@ -57,11 +51,9 @@ jobs:
run: |
node --input-type=module <<'EOF'
import { readFileSync } from 'node:fs';
// Minimal parse of policy.yaml's required_checks block list —
// avoids a yaml dependency in this bun-based repo.
const policy = readFileSync('spec_git/policy.yaml', 'utf8');
const section = policy.slice(policy.indexOf('required_checks:'));
const required = [...section.matchAll(/^\s*-\s*(.+)$/gm)].map((m) => m[1].trim());
import { parse } from 'yaml';
const policy = parse(readFileSync('spec_git/policy.yaml', 'utf8'));
const required = policy.required_checks ?? [];
const headers = {
authorization: 'Bearer ' + process.env.GH_TOKEN,
accept: 'application/vnd.github+json',
Expand All @@ -74,9 +66,7 @@ jobs:
const retried = [...byName.keys()].find((k) => k.startsWith(name + ' ('));
return retried !== undefined && terminal.has(byName.get(retried));
};
// Must outlast the slowest required sibling (Unit Tests (linux)
// runs ~28min on PRs); the job timeout above bounds this too.
const deadline = Date.now() + 40 * 60 * 1000;
const deadline = Date.now() + 15 * 60 * 1000;
while (Date.now() < deadline) {
const res = await fetch(url, { headers });
if (!res.ok) throw new Error('check-runs API ' + res.status);
Expand All @@ -95,6 +85,6 @@ jobs:
EOF

- name: specgit finish
run: specgit finish --json
run: node bin/specgit.js finish --json
env:
GH_TOKEN: ${{ github.token }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ tsconfig.tsbuildinfo
.opencode/commands/
.opencode/skills
.qoder
.opencode/workflow-reports/
8 changes: 4 additions & 4 deletions .specgit.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
version: 1
delivery: reduce-dag-auto
delivery: issue389
context:
kind: branch
branch: refactor/392-reduce-dag-auto
branch: feat/todo-step-reminders
issues:
- 392
pr: 393
- 389
pr: 394
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,19 @@ re-init; keep manual guidance outside them.
origin. `specgit doctor` probes git, repository, origin, gh, and
policy.

### Before creating an issue, check for duplicates

- Before running `specgit issue` with a new title, search the tracker for
similar open work: `gh issue list` with keywords from the title
(state, labels, and search terms via `gh search issues`).
- Open and read every plausible candidate (`gh issue view <n>`) — compare
the WHY, not just the wording.
- If a candidate covers the same WHY, continue that issue instead of
creating a new one; if it is close but different, say how they differ.
- When unsure, ask the requester to decide between continuing the existing
issue and creating a duplicate. The team ships one line of work per WHY,
never two.

### Issue granularity

One issue = one independently verifiable WHY. If a deliverable cannot be
Expand Down
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,19 @@ re-init; keep manual guidance outside them.
origin. `specgit doctor` probes git, repository, origin, gh, and
policy.

### Before creating an issue, check for duplicates

- Before running `specgit issue` with a new title, search the tracker for
similar open work: `gh issue list` with keywords from the title
(state, labels, and search terms via `gh search issues`).
- Open and read every plausible candidate (`gh issue view <n>`) — compare
the WHY, not just the wording.
- If a candidate covers the same WHY, continue that issue instead of
creating a new one; if it is close but different, say how they differ.
- When unsure, ask the requester to decide between continuing the existing
issue and creating a duplicate. The team ships one line of work per WHY,
never two.

### Issue granularity

One issue = one independently verifiable WHY. If a deliverable cannot be
Expand Down
Loading
Loading