diff --git a/.github/workflows/specgit-accept.yml b/.github/workflows/specgit-accept.yml index c280272a4..68a23216a 100644 --- a/.github/workflows/specgit-accept.yml +++ b/.github/workflows/specgit-accept.yml @@ -2,11 +2,14 @@ name: SpecGit Acceptance on: pull_request: - # Delivery PRs target dev (fast-integration layer); the acceptance - # verdict runs only on the dev→main promotion PR, where protect-main's - # checks apply. Keep the trigger main-only (d6ce53a83): running it on - # dev PRs duplicated the verdict against the lighter dev gate. branches: [main] + # Local specialization (repo-only deviation from the specgit template): + # no workflow_dispatch trigger. Dispatch is the privileged context that + # made CodeQL's cache-poisoning taint rule fire on the head_ref checkout; + # it also evaluated the wrong tree here (head_ref is empty on dispatch, + # so the verdict would run against the default branch). This repo's + # delivery flow always goes through a PR, so dispatch has no use. + # Re-apply this deletion after every `specgit init --force`. permissions: contents: read @@ -14,11 +17,12 @@ permissions: jobs: specgit-acceptance: name: SpecGit Acceptance + # Portable gate for any adopting repository: the published CLI is + # installed at the exact version `specgit init` pinned. The adopting + # project's own toolchain (package manager, lockfile, build, layout) + # is never assumed and never invoked. 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 @@ -33,15 +37,13 @@ jobs: - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: '22' + node-version: '20.19' - # 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: Install pinned SpecGit CLI + # Exact version on purpose (no ^): the gate must evaluate with the + # same CLI generation that wrote the binding; upgrades are a + # deliberate re-init. --no-save keeps the adopting tree clean. + run: npm install --no-save --no-audit --no-fund specgit@1.0.1 - name: Wait for sibling checks # The verdict must see the OTHER required checks in a terminal @@ -49,51 +51,84 @@ jobs: # their check-runs yet, so an empty poll is not "done": wait until # every name in spec_git/policy.yaml is present with a terminal # conclusion. This job is not in the policy, so no self-deadlock. + # All GitHub access goes through the authenticated gh CLI. env: GH_TOKEN: ${{ github.token }} WAIT_REPO: ${{ github.repository }} - WAIT_SHA: ${{ github.event.pull_request.head.sha }} + WAIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }} 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()); - const headers = { - authorization: 'Bearer ' + process.env.GH_TOKEN, - accept: 'application/vnd.github+json', - }; - const url = 'https://api.github.com/repos/' + process.env.WAIT_REPO - + '/commits/' + process.env.WAIT_SHA + '/check-runs?per_page=100'; + import { execFileSync } from 'node:child_process'; + import { parse } from 'yaml'; + const policy = parse(readFileSync('spec_git/policy.yaml', 'utf8')); + const required = policy.required_checks ?? []; + const listChecks = () => + JSON.parse( + execFileSync( + 'gh', + [ + 'api', + 'repos/' + process.env.WAIT_REPO + '/commits/' + process.env.WAIT_SHA + '/check-runs?per_page=100', + ], + // gh.cmd needs a shell on Windows; POSIX execs the binary + // directly (shell stays off where it is not needed). + { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], shell: process.platform === 'win32' } + ) + ); const terminal = new Set(['completed']); const terminalHas = (byName, name) => { if (byName.has(name)) return terminal.has(byName.get(name)); 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; + // Transient API failures (5xx, 429, network) retry with bounded + // exponential backoff — a platform blip must not fail the gate. + const MAX_ATTEMPTS = 5; + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + const listChecksWithRetry = async () => { + for (let attempt = 1; ; attempt += 1) { + try { + return listChecks(); + } catch (error) { + const text = String(error) + ' ' + String(error && error.stderr ? error.stderr : ''); + const transient = /HTTP 5\d\d|HTTP 429|ETIMEDOUT|ECONNRESET|ENOTFOUND|timed out/i.test(text); + if (attempt >= MAX_ATTEMPTS || !transient) throw error; + const backoff = Math.min(30000, 2000 * 2 ** (attempt - 1)); + console.log('Transient failure; retry ' + attempt + '/' + MAX_ATTEMPTS + ' in ' + backoff + 'ms'); + await sleep(backoff); + } + } + }; + 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); - const payload = await res.json(); - const byName = new Map(payload.check_runs.map((r) => [r.name, r.status])); + const payload = await listChecksWithRetry(); + // #119: re-runs keep every same-name run; terminality is + // decided on the truth run — latest started_at, ties broken + // by the higher check-run id (docs/reference.md) — never on + // response position. + const truth = new Map(); + for (const r of payload.check_runs) { + const cur = truth.get(r.name); + const later = cur === undefined + || (r.started_at || '') > (cur.started_at || '') + || ((r.started_at || '') === (cur.started_at || '') && (r.id || 0) > (cur.id || 0)); + if (later) truth.set(r.name, r); + } + const byName = new Map([...truth].map(([name, r]) => [name, r.status])); const missing = required.filter((n) => !terminalHas(byName, n)); if (missing.length === 0) { console.log('All required checks are in a terminal state.'); process.exit(0); } console.log('Waiting for: ' + missing.join(', ')); - await new Promise((r) => setTimeout(r, 10000)); + await sleep(10000); } console.error('Timed out waiting for sibling checks.'); process.exit(1); EOF - name: specgit finish - run: specgit finish --json + run: npx --no-install specgit finish --json env: GH_TOKEN: ${{ github.token }} diff --git a/.opencode/hooks/specgit-merge-guard.sh b/.opencode/hooks/specgit-merge-guard.sh index 50a8a0d5d..ad407724b 100755 --- a/.opencode/hooks/specgit-merge-guard.sh +++ b/.opencode/hooks/specgit-merge-guard.sh @@ -1,16 +1,156 @@ #!/bin/sh # SpecGit merge guard (managed by specgit init). Exit 2 = block with reason. -command=$(printf '%s' "$1" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{const j=JSON.parse(s);process.stdout.write((j.tool_input&&j.tool_input.command)||'')}catch{process.stdout.write('')}})") +GUARD_DIR=$(cd "$(dirname "$0")" && pwd) +export GUARD_DIR +# Hook payloads arrive as the first argument or on stdin; accept both. +if [ -n "$1" ]; then + payload=$1 +else + payload=$(cat) +fi +command=$(printf '%s' "$payload" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{const j=JSON.parse(s);process.stdout.write((j.tool_input&&j.tool_input.command)||'')}catch{process.stdout.write('')}})") case "$command" in gh\ pr\ merge*) - # Real-time verdict: re-evaluate the delivery before letting a merge - # through. Verdicts are never persisted, so compute one now. - if specgit finish >/dev/null 2>&1; then - exit 0 - fi - echo "specgit: merge blocked - 'specgit finish' does not exit 0 right now. Fix what the failures name; never weaken spec_git/policy.yaml to pass." >&2 - exit 2 + exec node -e ' + const { spawn } = require("child_process"); + const fs = require("fs"); + const path = require("path"); + const ghMsRaw = parseInt(process.env.SPECGIT_GH_TIMEOUT_MS || "", 10); + const ghMs = Number.isFinite(ghMsRaw) && ghMsRaw > 0 ? ghMsRaw : 15000; + const ghS = Math.max(1, Math.floor(ghMs / 1000)); + let budgetS = Math.max(60, ghS * 8); + const overrideRaw = parseInt(process.env.SPECGIT_GUARD_BUDGET_S || "", 10); + if (Number.isFinite(overrideRaw) && overrideRaw > 0) { + budgetS = Math.max(overrideRaw, ghS); + } + // The hook runner kills long hooks; surface the mismatch instead of + // being cut off mid-verdict. + try { + const hooks = JSON.parse( + fs.readFileSync(path.join(process.env.GUARD_DIR || ".", "..", "hooks.json"), "utf8") + ); + const runner = (hooks.PreToolUse || []) + .flatMap((entry) => entry.hooks || []) + .map((hook) => hook.timeout) + .find((timeout) => typeof timeout === "number"); + if (runner !== undefined && runner - 10 < budgetS) { + console.error( + "specgit: guard budget " + budgetS + "s exceeds the hook runner timeout " + + runner + "s in .opencode/hooks.json - raise the runner timeout or lower SPECGIT_GUARD_BUDGET_S." + ); + } + } catch {} + const cp = require("child_process"); + const isWin = process.platform === "win32"; + // Windows: cmd.exe cannot exec an extensionless sh shim, so prefer + // git-bash sh when present; only then fall back to shell mode. + let child; + if (isWin) { + const probe = cp.spawnSync("sh", ["-c", "exit 0"]); + if (probe.status === 0) { + child = spawn("sh", ["-c", "specgit finish --json"], { + stdio: ["ignore", "pipe", "pipe"], + }); + } + } + if (!child) { + child = spawn("specgit", ["finish", "--json"], { + shell: isWin, + stdio: ["ignore", "pipe", "pipe"], + }); + } + let out = ""; + let err = ""; + let expired = false; + child.stdout.on("data", (chunk) => (out += chunk)); + child.stderr.on("data", (chunk) => (err += chunk)); + const timer = setTimeout(() => { + expired = true; + // Bound the wait strictly: descendants may inherit the pipes, so + // destroy them and exit now — never lag behind orphaned children. + child.stdout.destroy(); + child.stderr.destroy(); + child.kill("SIGKILL"); + console.error( + "specgit: merge blocked - guard budget " + budgetS + "s exhausted before a verdict. This says nothing about the delivery; run specgit finish directly for the full verdict." + ); + process.exit(2); + }, budgetS * 1000); + child.on("error", (error) => { + clearTimeout(timer); + console.error( + "specgit: merge blocked - the verdict could not run (" + error.message + "). Install specgit on PATH, then retry the merge." + ); + process.exit(2); + }); + child.on("close", (code) => { + clearTimeout(timer); + if (expired) { + process.exit(2); + } + if (code === 0) { + process.exit(0); + } + let envelope = null; + try { + envelope = JSON.parse(out); + } catch {} + const verdict = envelope && envelope.verdict; + const gates = (envelope && (envelope.gates || (verdict && verdict.gates))) || []; + const failures = []; + for (const gate of gates) { + for (const failure of (gate && gate.failures) || []) failures.push(failure); + } + const label = (failure, suffix) => { + const detail = failure.detail || {}; + const name = detail.name || failure.code; + const state = suffix || detail.status || detail.conclusion || ""; + return name + (state ? " [" + state + "]" : ""); + }; + const pending = failures.filter((f) => f.code === "checks_pending"); + const failed = failures.filter((f) => f.code === "checks_failed"); + const other = failures.filter( + (f) => f.code !== "checks_pending" && f.code !== "checks_failed" + ); + const lines = []; + if (code === 1) { + lines.push( + "specgit: merge blocked - verdict rejected (exit 1). Fix what the failures name; never weaken spec_git/policy.yaml to pass." + ); + } else { + lines.push( + "specgit: merge blocked - no verdict possible (evidence incomplete, exit " + code + "). This is not a rejection: fix evidence gathering (network, gh auth), then retry." + ); + } + if (pending.length > 0) { + lines.push( + " pending (transient - wait, then re-run): " + pending.map((f) => label(f)).join(", ") + ); + } + if (failed.length > 0) { + lines.push( + " failed (repair required): " + + failed + .map((f) => + label( + f, + f.detail && f.detail.conclusion === "action_required" + ? "action_required - run awaits maintainer approval" + : undefined + ) + ) + .join(", ") + ); + } + if (other.length > 0) { + lines.push(" other failures: " + other.map((f) => label(f)).join(", ")); + } + lines.push("Full verdict: specgit finish"); + console.error(lines.join("\n")); + process.exit(2); + }); + ' ;; git\ push\ origin\ main*|git\ push\ origin\ +main*|git\ push\ origin\ HEAD:main*) echo "specgit: direct push to main is not the delivery path. Deliveries go: specgit issue -> PR -> CI -> specgit finish (exit 0) -> merge." >&2 diff --git a/AGENTS.md b/AGENTS.md index ee8e1066f..c08d5319d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,9 +13,9 @@ feat/**, fix/** ──PR(Typecheck + Unit Tests 门禁)──▶ dev ──push | Branch | 直推 | PR 门禁 | CI 触发 | Purpose | |--------|------|---------|---------|---------| -| `{type}/**` | ✅ 允许 | — | ❌ 不跑 | 开发分支,频繁变更 | +| `{type}/**` | ✅ 允许 | — | push 不触发;PR 触发目标分支门禁 | 开发分支,频繁变更 | | `dev` | ❌ 禁止 | PR 必须通过 **Typecheck + Unit Tests (linux)** | ✅ push 触发 Typecheck + 全量测试 | 快速集成层 | -| `main` | ❌ 禁止 | PR 必须通过 **Typecheck + Unit Tests + E2E (linux + windows)** | ✅ push 触发全量 | 正式质量门禁 + 发版 | +| `main` | ❌ 禁止 | PR 必须通过 **Typecheck + Unit Tests + E2E (linux + windows) + SpecGit Acceptance** | ✅ push 触发全量 | 正式质量门禁 + 发版 | **流程**: 1. 从 `main` 切出 `feat/**` 或 `fix/**` 分支开发 @@ -26,14 +26,26 @@ feat/**, fix/** ──PR(Typecheck + Unit Tests 门禁)──▶ dev ──push 6. 合并到 `main` 后手动 `release-fork` → 产出**正式版** **Rulesets(GitHub Settings → Rules → Rulesets)**: -- `protect-main`:禁止直推/删除/force-push;PR 需通过 4 项检查(Typecheck、Unit Tests (linux)、E2E Tests (linux)、E2E Tests (windows)) +- `protect-main`:禁止直推/删除/force-push;PR 需通过 5 项检查(Typecheck、Unit Tests (linux)、E2E Tests (linux)、E2E Tests (windows)、SpecGit Acceptance) - `protect-dev`:禁止直推/删除/force-push;PR 需通过 Typecheck - `branch-naming`:只允许创建 `feat/**`、`fix/**`、`chore/**`、`docs/**`、`refactor/**`、`test/**`、`release/**`、`hotfix/**` 前缀的新分支 **CI 配置**: -- `ci-typecheck.yml`:push 到 `main`/`dev` + PR → `main`/`dev` 时触发(快速门禁) -- `ci-test.yml`:push 到 `main`/`dev` + PR → `main` 时触发全量测试(`cancel-in-progress: false` 保证跑完);Linux unit-tests job 额外校验生成物新鲜度(`packages/client` 与 `packages/sdk/js` 的 `check:generated`)并跑 HttpAPI 契约门禁 -- `release-fork.yml`:手动触发;从 `dev` 发布自动标记 `--prerelease`,从 `main` 发布正式版 +- `ci-typecheck.yml`:push 到 `main`/`dev` + PR → `main`/`dev` 时触发;除 lint + typecheck 外还跑 `test:dag-core` DAG 核心行为/覆盖率门禁(10min 超时) +- `ci-test.yml`:push 到 `main`/`dev` + PR → `main`/`dev` 时触发全量测试(`cancel-in-progress: false` 保证跑完);Linux unit-tests job 额外校验生成物新鲜度(`packages/client` 与 `packages/sdk/js` 的 `check:generated`)并跑 HttpAPI 契约门禁(`test:httpapi:ci`) +- `specgit-accept.yml`:仅 PR → `main` 时触发;安装 pinned `specgit@1.0.1`,等 `spec_git/policy.yaml` `required_checks` 全部到终态后运行 `specgit finish --json` 产出 SpecGit Acceptance 裁决 +- `release-fork.yml`:手动 `workflow_dispatch` 是唯一真实构建路径(push 到 `main`/`dev` 仅注册不构建);从 `dev` 发布自动产出 `X.Y.Z-dev.N` prerelease,从 `main` 发布 `X.Y.Z` 并标 Latest + +## Standard Delivery Workflow (标准交付流程) + +新功能开发、Debug 等一切交付范畴恒定走此循环;后续所有工作必须遵守该方案,不得另起流程: + +1. **确立条目**:明确条目的内容、范围、类型(`feat`/`fix`/…)。一个 issue = 一个可独立验证的 WHY,无法独立验证的先拆分再立项。 +2. **SpecGit 立项**:`specgit issue ` 创建/复用 issues 批次,确立交付分支与草稿 PR 脚手架(`.specgit.yaml` 绑定);立项前先查重,避免同一 WHY 双开。 +3. **超流执行**:安排 DAG workflow(超流)承载实现——并行开发 + 多角度 Review + 复合(synthesize),其产出作为交付证据基线。 +4. **PR 过门禁**:SpecGit 发起/推进 PR,过 TDD 与 CI 门禁(Typecheck、Unit Tests、DAG gate;`specgit finish` exit 0 是唯一 "done")。 +5. **修复门禁问题**:门禁失败在交付分支修代码/测试,永远不削弱门禁本身。 +6. **合并收尾**:完成 PR 合并(目标分支遵循 Git Workflow,dev 为集成层),PR 正文 `Closes #n` 自动关闭绑定 issues;版本确立与发布按 release train 既有节奏推进。 ## Branch Names @@ -174,11 +186,13 @@ const table = sqliteTable("session", { - Avoid mocks as much as possible, you shouldn't be using globalThis.\* at all unless it's the only option. - Test actual implementation, do not duplicate logic into tests - Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`. +- `bun run test:dag-core`(在 `packages/opencode`):DAG 核心行为与覆盖率门禁,随 ci-typecheck 对每个 PR 强制执行;改状态机/持久化先本地跑它。 ## Type Checking -- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly. +- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly. Root `bun run typecheck` (turbo) covers all packages. - `bun run build` does not typecheck — esbuild transpiles only. A green build can still ship a missing import or a non-existent API, so it is not proof the code is sound. `bun typecheck` (`tsgo --noEmit`) is the commit gate. +- `bun run lint`(仓库根)= `oxlint --max-warnings=4850` 警告数棘轮:任何新增 oxlint 警告都会撑破预算、炸掉 CI Typecheck job——修警告,永远不要抬上限。 ## Extending the Codebase (二次开发) @@ -199,7 +213,7 @@ Invariants for extending the SolidJS/opentui TUI. The DAG inspector (`src/featur - Server-driven shared state lives in `src/context/sync.tsx`: one store slice + one event reducer case per domain, plus an initial fetch during bootstrap as the safety net for events missed before the event stream subscribes. `SyncProvider` requires `ExitProvider` (plus Args/KV/SDK/Project providers); any test harness mounting it must wrap with all of them — see `test/cli/cmd/tui/sync-fixture.tsx`. - Every event type the TUI consumes must be defined with `define()` in `packages/schema` and included in `EventManifest.Definitions`, or the generated SDK event union won't contain it and the reducer case can't typecheck. Ephemeral push events (e.g. `dag.workflow.summary.updated`) stay OUT of the durable-event manifest: emit them via `GlobalBus`, never persist them, and design consumers to tolerate missed events (re-fetch on bootstrap). - Types shared between server and TUI come from the generated SDK (`@opencode-ai/sdk/v2`). Do not hand-duplicate response/summary interfaces in `packages/plugin/src/tui.ts` or TUI code — re-export the SDK type (`export type TuiSidebarDagItem = DagWorkflowSummary`), so a server schema change surfaces as a typecheck error instead of silent drift. -- Prefer server-side aggregation for display data. The TUI renders `DagStore.getWorkflowSummaries` output verbatim; it never aggregates raw `dag.*` events client-side. Derived-view publishers (`src/dag/runtime/summary-publisher.ts`) must stay stateless: recompute from the store on every emission, no module-level caches. +- Prefer server-side aggregation for display data. The TUI renders `DagStore.getWorkflowSummaries` output verbatim; it never aggregates raw `dag.*` events client-side. Derived-view publishers (server-side `packages/opencode/src/dag/runtime/summary-publisher.ts`) must stay stateless: recompute from the store on every emission, no module-level caches. - Extract non-trivial pure logic (topology layout, tree building) into a sibling `*-utils.ts` with unit tests, mirroring `diff-viewer-file-tree-utils.ts` / `dag-inspector-utils.ts`. Component files stay declarative. - Async fetches inside components must guard against stale responses (check the selection still matches before `setState`) and clean up event subscriptions with `onCleanup`. @@ -217,16 +231,23 @@ This repository owns the DAG schema, compiler, validator, runtime, and release i - Built-in commands ship compiled into the binary: `/dag-auto` (requirement → workflow routing: classify, match a saved DAG route, retarget, validate, start). Platform delivery (issues, PRs, CI, merge, release) is specgit's job — never part of `/dag-*`. User command files shadow built-ins by name; register new built-ins through `packages/core/src/plugin/command.ts` + `packages/opencode/src/command/index.ts` (`Default` registry). - Templates come from `opencode-dag-config`: 7 domains × `full`/`lite` plus cross-domain routes (`ultra-flow-route`, `release-route`). Precedence: project `.opencode/workflows/` > global config dir > builtin snapshot (the release pipeline compiles the config repo into the binary via `DAG_TEMPLATES_DIR`). -- `dag.jsonc` supplies DAG node model tiers: `advanced` for `required: true` and review nodes, `standard` otherwise. Never pin `model` inside saved workflow specs. +- `~/.config/opencode/dag.jsonc`(全局用户配置,非仓库文件)supplies DAG node model tiers: `advanced` for `required: true` and review nodes, `standard` otherwise. Never pin `model` inside saved workflow specs. ## Project memory - Memory is fail-closed inert until the project is initialized: running `/init` stamps `project.time_initialized`, which `/memory on` and `memory_search` require. `/memory on` silently answering "Memory remains off" means the project never ran `/init` (or has no real git identity). +- Model 与节奏配置在 `~/.config/opencode/memory.jsonc`(enabled、model、turn_interval、注入上限)。openai-compatible 供应商会把 JSON schema 渲染进 system prompt——schema-blind 模型也能产出合法 topic。写入验证看盘:`~/.local/share/opencode/memory/projects//generations/*/topic-*.yaml`。 ## Release notes Releases follow `.github/RELEASE_NOTES_TEMPLATE.md`: keep section order and emoji headers, omit empty sections, fill the test summary from the CI gates, and end with the `previous_tag...current_tag` changelog link. +Mechanics(fail-closed,graphagent-v1.0.29 验证过): + +- 版本由 `packages/opencode/script/release-version.ts` 机械推导:只认 `graphagent-v*` 标签,下一个 stable 恒为 patch+1(`graphagent-v1.0.28` → `1.0.29`),dev 通道为 `X.Y.Z-dev.N`;opencode 包版本号被忽略。 +- 系列文件 `.github/releases/v<推导版本>.md` 的文件名必须等于推导版本(命名错 = release job fail-closed 炸掉);正文用 `{VERSION}`、`{Prerelease/Stable}`、`{branch}`、`{previous_tag}`、`{current_tag}` 占位符,由 `packages/opencode/script/release-notes.ts` 渲染并校验不变量。 +- 本地演练渲染:`bun run ./packages/opencode/script/release-notes.ts --notes-dir .github/releases --version --channel main --branch main --tag graphagent-v --previous-tag graphagent-v

--repo LeXwDeX/OpenCode-GraphAgent --out /tmp/notes.md` + ## Agent skills ### Issue tracker @@ -241,18 +262,31 @@ Triage uses the five canonical labels `needs-triage`, `needs-info`, `ready-for-a This repository uses a multi-context domain-document layout rooted at `CONTEXT-MAP.md`. See `docs/agents/domain.md`. +### SpecGit harness local specializations + +Kept OUTSIDE the managed block so `specgit init`/`--force` never rewrites them; re-apply each deviation after every re-init: + +- `specgit-accept.yml` drops the template's `workflow_dispatch` trigger. Dispatch is the privileged context that fires CodeQL's cache-poisoning taint rule on the `head_ref` checkout (false positive: no cache use, read-only token, `persist-credentials: false`), and on dispatch events `head_ref` is empty so the verdict would evaluate the default branch — the wrong tree. Delivery here always goes through a PR. The head-ref checkout itself must NOT be replaced with a SHA: `specgit finish` requires HEAD on the delivery branch (detached_head otherwise). +- `spec_git/policy.yaml` `required_checks` uses the template's canonical check IDs (`unit-tests`, `e2e-tests`), not display names. + ## SpecGit delivery harness -Managed by `specgit init`. Everything between the markers is rewritten on -re-init; keep manual guidance outside them. +Managed by `specgit init`. Everything between the markers is regenerated +whenever init writes the harness (a fresh init, or `--force` when a policy +already exists); keep manual guidance outside them. ### The delivery story - Start with `specgit issue ...`: it creates or reuses - the issues, branches, opens the draft pull request that closes every - bound issue, and writes `.specgit.yaml`. Re-running resumes; it is - idempotent. + the issues, branches, opens the draft pull request pre-filled with a + deterministic scaffold (the `Closes #n` line for every bound issue, + then Why / What changed / Evidence / Checklist sections), and writes + `.specgit.yaml`. Re-running resumes; it is idempotent. +- Fill in the scaffold sections as you deliver. Its placeholders are + advisory — the closing references are the only body gate. The PR body + is written once at creation; no SpecGit command edits an existing PR + body, and the repository's own pull-request template is never read. - Finish with `specgit finish`: the verdict, derived from real git, PR, and CI evidence. Exit code 0 is the only "done". @@ -265,6 +299,15 @@ re-init; keep manual guidance outside them. origin. `specgit doctor` probes git, repository, origin, gh, and policy. +### The command surface + +- Ten commands: `specgit init`, `specgit setup`, `specgit issue`, + `specgit pr`, `specgit finish`, `specgit bind`, `specgit unbind`, + `specgit status`, `specgit accept`, `specgit doctor`. +- `specgit setup` installs the agent entry points (commands for opencode, + portable skills for other tools); `specgit bind`, `specgit unbind`, + and `specgit accept` are automation aliases for scripts and CI. + ### Before creating an issue, check for duplicates - Before running `specgit issue` with a new title, search the tracker for diff --git a/CLAUDE.md b/CLAUDE.md index 27e995774..93e9a7e68 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -179,15 +179,21 @@ existing issue (`Fixes #N`). Curated DAG configs are owned by the `opencode-dag- ## SpecGit delivery harness -Managed by `specgit init`. Everything between the markers is rewritten on -re-init; keep manual guidance outside them. +Managed by `specgit init`. Everything between the markers is regenerated +whenever init writes the harness (a fresh init, or `--force` when a policy +already exists); keep manual guidance outside them. ### The delivery story - Start with `specgit issue ...`: it creates or reuses - the issues, branches, opens the draft pull request that closes every - bound issue, and writes `.specgit.yaml`. Re-running resumes; it is - idempotent. + the issues, branches, opens the draft pull request pre-filled with a + deterministic scaffold (the `Closes #n` line for every bound issue, + then Why / What changed / Evidence / Checklist sections), and writes + `.specgit.yaml`. Re-running resumes; it is idempotent. +- Fill in the scaffold sections as you deliver. Its placeholders are + advisory — the closing references are the only body gate. The PR body + is written once at creation; no SpecGit command edits an existing PR + body, and the repository's own pull-request template is never read. - Finish with `specgit finish`: the verdict, derived from real git, PR, and CI evidence. Exit code 0 is the only "done". @@ -200,6 +206,15 @@ re-init; keep manual guidance outside them. origin. `specgit doctor` probes git, repository, origin, gh, and policy. +### The command surface + +- Ten commands: `specgit init`, `specgit setup`, `specgit issue`, + `specgit pr`, `specgit finish`, `specgit bind`, `specgit unbind`, + `specgit status`, `specgit accept`, `specgit doctor`. +- `specgit setup` installs the agent entry points (commands for opencode, + portable skills for other tools); `specgit bind`, `specgit unbind`, + and `specgit accept` are automation aliases for scripts and CI. + ### Before creating an issue, check for duplicates - Before running `specgit issue` with a new title, search the tracker for diff --git a/spec_git/policy.yaml b/spec_git/policy.yaml index fe3768c27..56329f963 100644 --- a/spec_git/policy.yaml +++ b/spec_git/policy.yaml @@ -1,4 +1,5 @@ version: 1 required_checks: + - unit-tests + - e2e-tests - Typecheck - - Unit Tests (linux)