diff --git a/.github/ISSUE_TEMPLATE/pr_tracker.yml b/.github/ISSUE_TEMPLATE/pr_tracker.yml new file mode 100644 index 00000000..1dbe050a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/pr_tracker.yml @@ -0,0 +1,109 @@ +name: PR tracker (mirror) +description: Mirror an in-flight pull request as a tracking issue for Linear and Slack sync / 镜像一个进行中的 PR 作为追踪 issue +title: "[PR Track] #: " +labels: ["pr-mirror", "tracking"] +body: + - type: markdown + attributes: + value: | + Use this when you want a long-lived, auditable record of an upstream PR. + Linear and Slack subscribe to issues with the `pr-mirror` label. + 镜像一个 PR 用于 Linear / Slack 长期可审计追踪。带 `pr-mirror` 标签的 issue 会被订阅。 + - type: input + id: pr_number + attributes: + label: PR number + placeholder: "e.g. 196" + validations: + required: true + - type: input + id: pr_url + attributes: + label: PR URL + placeholder: https://github.com/EverMind-AI/EverOS/pull/ + validations: + required: true + - type: input + id: author + attributes: + label: Author handle + placeholder: "@github-login" + validations: + required: true + - type: dropdown + id: area + attributes: + label: Area + options: + - methods/EverCore + - methods/HyperMem + - benchmarks/EverMemBench + - benchmarks/EvoAgentBench + - use-cases + - documentation + - CI / build / release + - other + validations: + required: true + - type: dropdown + id: lane + attributes: + label: Review lane + description: How this PR should be triaged. / 该 PR 的优先级处理通道。 + options: + - hotfix (block release until merged) + - normal (standard review) + - docs-only (light review) + - exploratory (no merge intent) + validations: + required: true + - type: textarea + id: scope + attributes: + label: Scope summary + description: One paragraph. What does the PR change, and what is intentionally left out? + placeholder: | + Changes: + - ... + Out of scope: + - ... + validations: + required: true + - type: textarea + id: evidence + attributes: + label: Evidence snapshot + description: | + Required before this mirror can be closed. Paste the CI summary, test command output, + or the link to the run. "No mirror closes without evidence." + 关闭镜像 issue 前必填。粘贴 CI 摘要、测试命令输出或 run 链接。 + render: shell + validations: + required: true + - type: textarea + id: decisions + attributes: + label: Decision log + description: Notable review decisions (approvals, requested changes, deferrals). + placeholder: | + - 2026-05-13 @reviewer: requested change on tests/test_x.py + - 2026-05-13 @author: scoped follow-up to PR #... + - type: input + id: linear_issue + attributes: + label: Linear issue (optional) + placeholder: "EVE-123" + - type: input + id: slack_thread + attributes: + label: Slack thread (optional) + placeholder: "https://everminddash.slack.com/archives/.../p..." + - type: checkboxes + id: closure + attributes: + label: Closure criteria + description: Check all that apply before closing this mirror. + options: + - label: PR is merged, closed, or marked won't-fix upstream. + - label: Evidence snapshot above reflects the final state. + - label: Linear and Slack records have been updated (if linked). diff --git a/.github/ISSUE_TEMPLATE/security_tracker.yml b/.github/ISSUE_TEMPLATE/security_tracker.yml new file mode 100644 index 00000000..2f2c96c8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security_tracker.yml @@ -0,0 +1,116 @@ +name: Security tracker (mirror) +description: Mirror a security PR or disclosure for Linear and Slack escalation / 镜像一个安全 PR 或披露 +title: "[Security Track] CWE-: " +labels: ["security", "pr-mirror", "tracking", "urgent"] +body: + - type: markdown + attributes: + value: | + Use this for any PR or disclosure that affects credentials, authn/authz, data exposure, + supply chain, or sandbox escape. The `urgent` label routes this to high-priority + notifications in Slack and Linear. + 用于凭证、认证授权、数据暴露、供应链、沙箱逃逸等安全 PR / 披露。`urgent` 标签会触发高优先级通知。 + Do NOT include exploit details that are not already public in the upstream PR. + 请勿写入未在 upstream PR 公开的利用细节。 + - type: input + id: cwe + attributes: + label: CWE id + placeholder: "CWE-798" + validations: + required: true + - type: input + id: pr_url + attributes: + label: Upstream PR or advisory URL + placeholder: https://github.com/EverMind-AI/EverOS/pull/ + validations: + required: true + - type: dropdown + id: severity + attributes: + label: Severity + options: + - Critical (full auth bypass / unauthenticated RCE / mass data loss) + - High (privileged data access / credential exposure / persistent compromise) + - Medium (limited data access / requires user interaction) + - Low (defense-in-depth / hardening) + validations: + required: true + - type: dropdown + id: exposure + attributes: + label: Reachability + description: How reachable is this in the documented quickstart / default config? + options: + - Default config (reproducible from a clean clone) + - Default config + network position + - Non-default config but documented + - Hypothetical / not yet reproducible + validations: + required: true + - type: textarea + id: affected + attributes: + label: Affected components + description: File paths, services, or versions impacted. + placeholder: | + - methods/EverCore/docker-compose.yaml (memsys-milvus-minio block) + - methods/EverCore/env.template + validations: + required: true + - type: textarea + id: fix_summary + attributes: + label: Proposed fix summary + description: One paragraph. What does the PR change? Cite the contract that makes it fail-closed. + validations: + required: true + - type: textarea + id: evidence + attributes: + label: Verification evidence + description: | + Required before closure. Show the commands and output that prove the fix works AND + that the unpatched state was exploitable. "No security mirror closes without evidence." + 关闭前必填。展示证明 fix 生效以及未修复状态可利用的命令与输出。 + render: shell + validations: + required: true + - type: textarea + id: residual + attributes: + label: Residual risk / follow-ups + description: Anything intentionally out of scope, plus follow-up issues that should be filed. + placeholder: | + - docs/installation/ still references the old default in examples; follow-up sweep needed. + - Consider adding a CI lint to catch hardcoded secrets in docker-compose files. + - type: input + id: linear_issue + attributes: + label: Linear issue (optional) + placeholder: "EVE-123" + - type: input + id: slack_thread + attributes: + label: Slack thread (optional) + placeholder: "https://everminddash.slack.com/archives/.../p..." + - type: checkboxes + id: disclosure + attributes: + label: Disclosure hygiene + description: Confirm before submitting. + options: + - label: This mirror contains no exploit details beyond what is already public in the upstream PR. + required: true + - label: The upstream PR or advisory link is correct and reachable. + required: true + - label: A maintainer has been pinged in Slack #p-evermind-dash or via Linear EVE if Severity is Critical or High. + - type: checkboxes + id: closure + attributes: + label: Closure criteria + options: + - label: Upstream PR merged, advisory published, or risk formally accepted. + - label: Verification evidence above reflects the merged state. + - label: Residual-risk follow-ups have issues filed (or explicitly waived). diff --git a/.github/MUW_REVIEW_LANE.md b/.github/MUW_REVIEW_LANE.md new file mode 100644 index 00000000..77f01618 --- /dev/null +++ b/.github/MUW_REVIEW_LANE.md @@ -0,0 +1,58 @@ +# MUW Review Lane + +Use this lane when GitHub's native Codex review is useful but its fixed review +wrapper is too loose for MUW closeout work. + +The lane has three steps: + +1. Collect PR evidence. +2. Ask Codex to produce an exact MUW verdict from the generated prompt. +3. Post the verdict back to the PR with an idempotency marker. + +## Collect Evidence + +```bash +node .github/scripts/muw-review-lane.mjs collect --pr 24 --repo Fearvox/EverOS +``` + +The command prints paths like: + +```text +context=/tmp/muw-review-pr-24/pr-24-context.md +prompt=/tmp/muw-review-pr-24/pr-24-prompt.md +metadata=/tmp/muw-review-pr-24/pr-24-metadata.json +``` + +Give the prompt file to Codex. The context bundle includes PR metadata, changed +files, status checks, recent comments, existing reviews, and a redacted patch. + +## Post Verdict + +Save the Codex verdict to a file, then post it: + +```bash +node .github/scripts/muw-review-lane.mjs post \ + --pr 24 \ + --repo Fearvox/EverOS \ + --body-file /tmp/muw-review-pr-24/verdict.md +``` + +`post` refuses bodies that do not contain: + +```text +VERDICT: +VERDICT_SUMMARY: +EVIDENCE: +``` + +It also adds a hidden marker containing the PR head SHA. Re-running `post` for +the same head is a no-op unless `--force` is provided. + +## Why Not Native Review + +- GitHub's `@codex review` endpoint is useful, but it wraps responses in the + native Codex review shell. +- GitHub Agent tasks are mutation-oriented and may create draft PRs even for a + review-only prompt. +- This lane keeps review evidence gathering and comment publishing mechanical, + while leaving the verdict judgment to Codex. diff --git a/.github/MUW_REVIEW_REPLY.md b/.github/MUW_REVIEW_REPLY.md new file mode 100644 index 00000000..fd7af4d0 --- /dev/null +++ b/.github/MUW_REVIEW_REPLY.md @@ -0,0 +1,24 @@ +VERDICT: FLAG +VERDICT_SUMMARY: The PR adds a comprehensive MUW review lane, tracker templates, and automation wiring, but one default target points to the wrong repository and makes the core script unsafe by default. Workflow-level verification evidence is not attached in this branch, so rollout should be held until that default is corrected and one end-to-end dry run is captured. +EVIDENCE: + +1) Severity: High +- File/path: `.github/scripts/muw-review-lane.mjs` +- Evidence: `DEFAULT_REPO` is set to `Fearvox/EverOS` even though this repository remote is `EverMind-AI/EverOS`. +- Why it matters: Running the script without `--repo` can collect/post to the wrong project, creating data leakage risk and invalid review artifacts. +- Fix guidance: Change default to `EverMind-AI/EverOS` (or require explicit `--repo`) and add a guard that confirms current git remote matches the target repo before posting. + +2) Severity: Medium +- File/path: `.github/workflows/overnight-watch.yml`, `.github/workflows/linear-sync.yml`, `.github/workflows/sync-upstream.yml` +- Evidence: New automation workflows are introduced, but this branch does not provide a successful run artifact, dry-run log, or fixture-based script test proving safe behavior. +- Why it matters: These workflows can post comments/sync state automatically; missing proof increases risk of noisy or incorrect cross-system updates. +- Fix guidance: Attach one successful dry run per workflow (or script-level unit test evidence) in PR checks/comments before merge. + +3) Severity: Low +- File/path: `.github/ISSUE_TEMPLATE/pr_tracker.yml`, `.github/ISSUE_TEMPLATE/security_tracker.yml` +- Evidence: Templates are detailed and useful, but they introduce mandatory operational fields without a short onboarding note in CONTRIBUTING/docs. +- Why it matters: Contributors may submit incomplete triage data, reducing template effectiveness. +- Fix guidance: Add a short “how to use tracker templates” section in contributor docs with one minimal example. + +Residual verification gap: +- Confirm no credentials appear in generated context bundles after redaction by running the script against a test PR and scanning artifacts. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..182b6691 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,42 @@ +# Copilot and Codex Review Instructions + +When reviewing pull requests in this repository, use the MUW review contract. +Start every review with this block: + +```text +VERDICT: PASS / FLAG / BLOCK +VERDICT_SUMMARY: three lines or fewer; what passed, what is risky, and the next action +EVIDENCE: +``` + +Use the verdicts this way: + +- `PASS`: the pull request objective is met and the evidence is sufficient. +- `FLAG`: useful progress, but a non-blocking issue, missing evidence, or follow-up remains. +- `BLOCK`: the objective is unmet, unsafe, unverifiable, or materially wrong. + +Report findings first, ordered by severity. For each actionable finding, include: + +- Severity +- File/path +- Evidence from the actual diff, status check, command output, or linked issue +- Why it matters +- Fix guidance or the next verification required + +Review method: + +1. Identify the promised objective from the PR title, body, linked issue, and changed files. +2. Inspect the real diff and available checks before making a success claim. +3. Compare evidence against the objective; do not accept `done` from a summary alone. +4. Verify the smallest real path that proves the claim. +5. Keep evidence concise, reproducible, and repository-relative. + +EverOS-specific checks: + +- For `methods/EverCore/`, preserve async I/O, tenant scoping, and existing module boundaries. +- For prompts, keep EN/ZH variants aligned when both exist. +- For docs and community files, preserve the README reader journey and keep root uncluttered. +- For `.github/workflows/docs.yml`, keep the workflow lightweight and dependency-free unless the PR explicitly changes that contract. +- Do not expose secrets, credential paths, raw tokens, private host values, or operator-only commands in review text. + +For clean reviews, still return the MUW block with the evidence checked and any residual test gap. Keep the final review concise; prefer one clear judgment over a long menu of weak suggestions. diff --git a/.github/scripts/muw-review-lane.mjs b/.github/scripts/muw-review-lane.mjs new file mode 100644 index 00000000..98d57ff3 --- /dev/null +++ b/.github/scripts/muw-review-lane.mjs @@ -0,0 +1,325 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { tmpdir } from "node:os"; + +const DEFAULT_REPO = process.env.GH_REPO || "Fearvox/EverOS"; +const DEFAULT_PATCH_BYTES = 120_000; +const MARKER = "muw-review-lane:v1"; + +function usage(exitCode = 0) { + const text = ` +Usage: + node .github/scripts/muw-review-lane.mjs collect --pr [--repo owner/name] [--out ] + node .github/scripts/muw-review-lane.mjs post --pr --body-file [--repo owner/name] [--force] + +Collect creates a redacted PR evidence bundle and a Codex prompt. +Post publishes a MUW-formatted issue comment with an idempotency marker. +`; + process.stderr.write(text.trimStart()); + process.exit(exitCode); +} + +function parseArgs(argv) { + const [command, ...rest] = argv; + if (!command || command === "--help" || command === "-h") usage(0); + + const options = { command, repo: DEFAULT_REPO, force: false }; + for (let index = 0; index < rest.length; index += 1) { + const arg = rest[index]; + if (arg === "--repo") options.repo = rest[++index]; + else if (arg === "--pr") options.pr = Number(rest[++index]); + else if (arg === "--out") options.out = rest[++index]; + else if (arg === "--body-file") options.bodyFile = rest[++index]; + else if (arg === "--patch-bytes") options.patchBytes = Number(rest[++index]); + else if (arg === "--force") options.force = true; + else throw new Error(`Unknown argument: ${arg}`); + } + + if (!["collect", "post"].includes(command)) usage(1); + if (!Number.isInteger(options.pr) || options.pr <= 0) { + throw new Error("--pr must be a positive pull request number"); + } + return options; +} + +function run(command, args, options = {}) { + return execFileSync(command, args, { + encoding: "utf8", + input: options.input, + env: process.env, + stdio: ["pipe", "pipe", options.inheritStderr ? "inherit" : "pipe"], + maxBuffer: 20 * 1024 * 1024, + }).trim(); +} + +function gh(args, options = {}) { + return run("gh", args, options); +} + +function ghJson(args) { + return JSON.parse(gh(args)); +} + +function redact(value) { + return String(value ?? "") + .replace(/github_pat_[A-Za-z0-9_]+/g, "[REDACTED_GITHUB_TOKEN]") + .replace(/gh[pousr]_[A-Za-z0-9_]+/g, "[REDACTED_GITHUB_TOKEN]") + .replace(/sk-[A-Za-z0-9_-]{20,}/g, "[REDACTED_API_KEY]") + .replace(/xox[baprs]-[A-Za-z0-9-]+/g, "[REDACTED_SLACK_TOKEN]") + .replace(/(Authorization:\s*Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[REDACTED_TOKEN]") + .replace(/(token=)[A-Za-z0-9._~+/=-]+/gi, "$1[REDACTED_TOKEN]"); +} + +function truncate(value, maxBytes) { + const text = redact(value); + const bytes = Buffer.byteLength(text, "utf8"); + if (bytes <= maxBytes) return text; + return `${text.slice(0, maxBytes)}\n\n[TRUNCATED: ${bytes - maxBytes} bytes omitted]`; +} + +function summarizeChecks(items = []) { + if (!items.length) return "- No status checks reported."; + return items + .map((item) => { + const name = item.name || item.context || item.workflowName || item.__typename; + const state = item.conclusion || item.state || item.status || "unknown"; + const url = item.detailsUrl || item.targetUrl || ""; + return `- ${name}: ${state}${url ? ` (${url})` : ""}`; + }) + .join("\n"); +} + +function summarizeFiles(files = []) { + if (!files.length) return "- No changed files reported."; + return files + .map((file) => `- ${file.path} (+${file.additions}/-${file.deletions})`) + .join("\n"); +} + +function summarizeReviews(reviews = []) { + if (!reviews.length) return "- No reviews yet."; + return reviews + .map((review) => { + const author = review.author?.login || "unknown"; + const body = truncate(review.body || "", 700).replace(/\n/g, " "); + return `- ${review.submittedAt || "unknown"} ${author} ${review.state}: ${body}`; + }) + .join("\n"); +} + +function summarizeComments(comments = []) { + if (!comments.length) return "- No issue comments yet."; + return comments + .slice(-20) + .map((comment) => { + const author = comment.author?.login || "unknown"; + const body = truncate(comment.body || "", 700).replace(/\n/g, " "); + return `- ${comment.createdAt || "unknown"} ${author}: ${body}`; + }) + .join("\n"); +} + +function collect(options) { + const outDir = resolve( + options.out || join(tmpdir(), `muw-review-pr-${options.pr}`), + ); + mkdirSync(outDir, { recursive: true }); + + const fields = [ + "number", + "title", + "url", + "author", + "baseRefName", + "headRefName", + "headRefOid", + "isDraft", + "body", + "labels", + "mergeStateStatus", + "statusCheckRollup", + "files", + "commits", + "comments", + "reviews", + ].join(","); + + const pr = ghJson([ + "pr", + "view", + String(options.pr), + "--repo", + options.repo, + "--json", + fields, + ]); + const patch = gh([ + "pr", + "diff", + String(options.pr), + "--repo", + options.repo, + "--patch", + ]); + + const patchBytes = options.patchBytes || DEFAULT_PATCH_BYTES; + const contextPath = join(outDir, `pr-${options.pr}-context.md`); + const promptPath = join(outDir, `pr-${options.pr}-prompt.md`); + const metadataPath = join(outDir, `pr-${options.pr}-metadata.json`); + + const context = [ + `# MUW Review Context: ${options.repo}#${pr.number}`, + "", + `- Title: ${pr.title}`, + `- URL: ${pr.url}`, + `- Author: ${pr.author?.login || "unknown"}`, + `- Draft: ${pr.isDraft}`, + `- Base: ${pr.baseRefName}`, + `- Head: ${pr.headRefName}`, + `- Head SHA: ${pr.headRefOid}`, + `- Merge state: ${pr.mergeStateStatus || "unknown"}`, + `- Labels: ${(pr.labels || []).map((label) => label.name).join(", ") || "none"}`, + "", + "## PR Body", + "", + truncate(pr.body || "_No body._", 12_000), + "", + "## Files", + "", + summarizeFiles(pr.files), + "", + "## Checks", + "", + summarizeChecks(pr.statusCheckRollup), + "", + "## Existing Reviews", + "", + summarizeReviews(pr.reviews), + "", + "## Recent Comments", + "", + summarizeComments(pr.comments), + "", + "## Patch", + "", + "```diff", + truncate(patch, patchBytes), + "```", + "", + ].join("\n"); + + const prompt = [ + "You are reviewing a GitHub pull request using the MUW review contract.", + "", + "Read the context file listed below. Return only a GitHub-ready comment body.", + "Do not include Markdown fences around the whole answer.", + "", + "Required shape:", + "", + "VERDICT: PASS / FLAG / BLOCK", + "VERDICT_SUMMARY: three lines or fewer; what passed, what is risky, and the next action", + "EVIDENCE:", + "", + "Rules:", + "- Do not mark PASS from author summary alone.", + "- Ground claims in files, checks, comments, or patch evidence.", + "- Findings first, ordered by severity.", + "- For each finding include severity, file/path, evidence, why it matters, and fix guidance.", + "- If clean, include evidence checked and residual risk/test gap.", + "- Keep the answer concise and public-safe.", + "", + `Context file: ${contextPath}`, + "", + ].join("\n"); + + writeFileSync(contextPath, context); + writeFileSync(promptPath, prompt); + writeFileSync( + metadataPath, + `${JSON.stringify( + { + repo: options.repo, + pr: pr.number, + headRefOid: pr.headRefOid, + contextPath, + promptPath, + }, + null, + 2, + )}\n`, + ); + + process.stdout.write( + [ + `context=${contextPath}`, + `prompt=${promptPath}`, + `metadata=${metadataPath}`, + `head=${pr.headRefOid}`, + ].join("\n") + "\n", + ); +} + +function markerFor(options, headRefOid) { + return ``; +} + +function validateMuwBody(body) { + const missing = ["VERDICT:", "VERDICT_SUMMARY:", "EVIDENCE:"].filter( + (needle) => !body.includes(needle), + ); + if (missing.length) { + throw new Error(`Body is missing required MUW field(s): ${missing.join(", ")}`); + } +} + +function existingMarkerComment(options, marker) { + const [owner, repo] = options.repo.split("/"); + const comments = ghJson([ + "api", + `/repos/${owner}/${repo}/issues/${options.pr}/comments?per_page=100`, + ]); + return comments.find((comment) => String(comment.body || "").includes(marker)); +} + +function post(options) { + if (!options.bodyFile) throw new Error("--body-file is required for post"); + const body = redact(readFileSync(resolve(options.bodyFile), "utf8")).trim(); + validateMuwBody(body); + + const pr = ghJson([ + "pr", + "view", + String(options.pr), + "--repo", + options.repo, + "--json", + "headRefOid,url", + ]); + const marker = markerFor(options, pr.headRefOid); + const existing = existingMarkerComment(options, marker); + if (existing && !options.force) { + process.stdout.write( + `skip=existing-comment\nurl=${existing.html_url}\nhead=${pr.headRefOid}\n`, + ); + return; + } + + const comment = `${marker}\n${body}\n`; + const url = gh( + ["pr", "comment", String(options.pr), "--repo", options.repo, "--body", comment], + { inheritStderr: true }, + ); + process.stdout.write(`posted=${url}\nhead=${pr.headRefOid}\n`); +} + +try { + const options = parseArgs(process.argv.slice(2)); + if (options.command === "collect") collect(options); + else if (options.command === "post") post(options); +} catch (error) { + process.stderr.write(`muw-review-lane: ${error.message}\n`); + process.exit(1); +} diff --git a/.github/scripts/overnight-watch.mjs b/.github/scripts/overnight-watch.mjs new file mode 100755 index 00000000..5fc2b1c4 --- /dev/null +++ b/.github/scripts/overnight-watch.mjs @@ -0,0 +1,371 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const config = { + repoOwner: process.env.REPO_OWNER || "Fearvox", + repoName: process.env.REPO_NAME || "EverOS", + upstreamRepo: process.env.UPSTREAM_REPO || "EverMind-AI/EverOS", + watchBranch: + process.env.WATCH_BRANCH || "codex-watch-overnight-2026-05-13", + ownerTimezone: process.env.OWNER_TIMEZONE || "America/Los_Angeles", + linearTeamId: + process.env.LINEAR_TEAM_ID || "233391d6-ec9e-4aa8-b534-16a221b8119a", + linearProjectId: + process.env.LINEAR_PROJECT_ID || "39aa3865-345c-4313-9dc0-ab3b509c5d21", + createTrackingIssue: + (process.env.CREATE_TRACKING_ISSUE || "false").toLowerCase() === "true", + githubToken: process.env.GITHUB_TOKEN || process.env.GH_TOKEN || "", + linearApiKey: process.env.LINEAR_API_KEY || "", +}; + +const repoSlug = `${config.repoOwner}/${config.repoName}`; +const recentWindowHours = Number(process.env.WATCH_WINDOW_HOURS || 24); +const since = new Date(Date.now() - recentWindowHours * 60 * 60 * 1000); +const now = new Date(); + +function run(command, args, options = {}) { + const result = execFileSync(command, args, { + encoding: "utf8", + stdio: ["ignore", "pipe", options.inheritStderr ? "inherit" : "pipe"], + env: { + ...process.env, + GH_TOKEN: config.githubToken || process.env.GH_TOKEN, + }, + }); + return result.trim(); +} + +function tryRun(command, args, fallback = "") { + try { + return run(command, args); + } catch (error) { + return fallback; + } +} + +function ghJson(endpoint) { + const output = run("gh", [ + "api", + "-H", + "Accept: application/vnd.github+json", + endpoint, + ]); + return JSON.parse(output); +} + +function ensureRemote(name, url) { + const remotes = tryRun("git", ["remote"]).split("\n").filter(Boolean); + if (!remotes.includes(name)) { + run("git", ["remote", "add", name, url]); + } +} + +function fetchGitState() { + ensureRemote("upstream", `https://github.com/${config.upstreamRepo}.git`); + tryRun("git", ["fetch", "origin", "--prune"], ""); + tryRun("git", ["fetch", "upstream", "main", "--prune"], ""); + + const forkHead = tryRun("git", ["rev-parse", "--short", "origin/main"], "unknown"); + const upstreamHead = tryRun( + "git", + ["rev-parse", "--short", "upstream/main"], + "unknown", + ); + const counts = tryRun( + "git", + ["rev-list", "--left-right", "--count", "origin/main...upstream/main"], + "0\t0", + ) + .split(/\s+/) + .map((value) => Number(value)); + + const watchBranchSha = tryRun( + "git", + ["ls-remote", "--heads", "origin", config.watchBranch], + "", + ) + .split(/\s+/)[0] + ?.slice(0, 12); + + return { + forkHead, + upstreamHead, + forkAhead: counts[0] || 0, + forkBehind: counts[1] || 0, + watchBranchSha: watchBranchSha || "", + }; +} + +function fetchGitHubState() { + const runs = ghJson( + `/repos/${repoSlug}/actions/runs?per_page=50&status=completed`, + ).workflow_runs || []; + const failedRuns = runs + .filter((runInfo) => new Date(runInfo.created_at) >= since) + .filter((runInfo) => + ["failure", "cancelled", "timed_out"].includes(runInfo.conclusion), + ) + .slice(0, 10); + + const upstreamPulls = ghJson( + `/repos/${config.upstreamRepo}/pulls?state=open&sort=updated&direction=desc&per_page=30`, + ) + .filter((pull) => new Date(pull.updated_at) >= since) + .slice(0, 10); + + const forkPulls = ghJson( + `/repos/${repoSlug}/pulls?state=open&sort=updated&direction=desc&per_page=30`, + ) + .filter((pull) => new Date(pull.updated_at) >= since) + .slice(0, 10); + + return { failedRuns, upstreamPulls, forkPulls }; +} + +function lineForPull(pull) { + return `- #${pull.number} ${pull.title} (${pull.user.login}, updated ${pull.updated_at})`; +} + +function lineForRun(runInfo) { + return `- ${runInfo.name}: ${runInfo.conclusion} (${runInfo.html_url})`; +} + +function renderReport(gitState, githubState) { + const findings = []; + + if (!gitState.watchBranchSha) { + findings.push(`Watch branch is not on origin: ${config.watchBranch}`); + } + if (gitState.forkBehind > 0) { + findings.push(`Fork main is behind upstream/main by ${gitState.forkBehind} commit(s).`); + } + if (githubState.failedRuns.length > 0) { + findings.push( + `${githubState.failedRuns.length} completed workflow run(s) failed in the last ${recentWindowHours}h.`, + ); + } + + const verdict = findings.length > 0 ? "FLAG" : "PASS"; + const localNow = now.toLocaleString("en-US", { + timeZone: config.ownerTimezone, + hour12: false, + }); + + return { + verdict, + body: [ + `# Overnight Fork Watch: ${verdict}`, + "", + `Generated: ${now.toISOString()} (${config.ownerTimezone}: ${localNow})`, + `Repository: ${repoSlug}`, + `Upstream: ${config.upstreamRepo}`, + `Watch branch: \`${config.watchBranch}\``, + "", + "## Drift", + "", + `- origin/main: \`${gitState.forkHead}\``, + `- upstream/main: \`${gitState.upstreamHead}\``, + `- fork ahead: ${gitState.forkAhead}`, + `- fork behind: ${gitState.forkBehind}`, + `- watch branch on origin: ${ + gitState.watchBranchSha ? `yes (\`${gitState.watchBranchSha}\`)` : "no" + }`, + "", + "## Findings", + "", + findings.length ? findings.map((item) => `- ${item}`).join("\n") : "- None.", + "", + `## Fork Workflow Failures (${recentWindowHours}h)`, + "", + githubState.failedRuns.length + ? githubState.failedRuns.map(lineForRun).join("\n") + : "- None.", + "", + `## Upstream PRs Updated (${recentWindowHours}h)`, + "", + githubState.upstreamPulls.length + ? githubState.upstreamPulls.map(lineForPull).join("\n") + : "- None.", + "", + `## Fork PRs Updated (${recentWindowHours}h)`, + "", + githubState.forkPulls.length + ? githubState.forkPulls.map(lineForPull).join("\n") + : "- None.", + "", + "## Operator Notes", + "", + "- This issue is safe for public tracking: no local paths, host/IP values, or secrets are included.", + "- A GitHub issue created by `GITHUB_TOKEN` does not trigger secondary workflows, so this watch mirrors to Linear directly when `LINEAR_API_KEY` is available.", + ].join("\n"), + }; +} + +function ensureLabel(name, color, description) { + tryRun("gh", [ + "label", + "create", + name, + "--repo", + repoSlug, + "--color", + color, + "--description", + description, + ]); +} + +function issueHasLinearMarker(issueNumber) { + const comments = ghJson(`/repos/${repoSlug}/issues/${issueNumber}/comments?per_page=100`); + return comments.some((comment) => comment.body.includes("Linear:")); +} + +async function mirrorToLinear(issueNumber, title, body) { + if (!config.linearApiKey || issueHasLinearMarker(issueNumber)) { + return; + } + + const mutation = ` + mutation IssueCreate($input: IssueCreateInput!) { + issueCreate(input: $input) { + success + issue { id identifier url } + } + } + `; + + const response = await fetch("https://api.linear.app/graphql", { + method: "POST", + headers: { + Authorization: config.linearApiKey, + "Content-Type": "application/json", + "x-apollo-operation-name": "IssueCreate", + }, + body: JSON.stringify({ + query: mutation, + variables: { + input: { + title, + description: [ + `**Source**: https://github.com/${repoSlug}/issues/${issueNumber}`, + "", + "---", + "", + body, + ].join("\n"), + teamId: config.linearTeamId, + projectId: config.linearProjectId, + priority: 3, + }, + }, + }), + }); + + const data = await response.json(); + if (!response.ok || data.errors || !data?.data?.issueCreate?.success) { + ensureLabel("sync-failed", "D93F0B", "Linear sync workflow failed for this issue"); + tryRun("gh", [ + "issue", + "edit", + String(issueNumber), + "--repo", + repoSlug, + "--add-label", + "sync-failed", + ]); + throw new Error(`Linear API error: ${JSON.stringify(data)}`); + } + + const linearIssue = data.data.issueCreate.issue; + const marker = `Linear: [${linearIssue.identifier}](${linearIssue.url})\n\n_Auto-created by overnight-watch._`; + run("gh", ["issue", "comment", String(issueNumber), "--repo", repoSlug, "--body", marker]); +} + +function findExistingIssue(title) { + const issues = JSON.parse( + run("gh", [ + "issue", + "list", + "--repo", + repoSlug, + "--state", + "open", + "--label", + "overnight-watch", + "--json", + "number,title", + "--limit", + "20", + ]), + ); + return issues.find((issue) => issue.title === title); +} + +async function createOrUpdateTrackingIssue(report) { + if (!config.createTrackingIssue || report.verdict === "PASS") { + return; + } + + ensureLabel("overnight-watch", "1D76DB", "Automated overnight fork watch"); + ensureLabel("tracking", "5319E7", "Long-lived tracking item"); + ensureLabel("pr-mirror", "0E8A16", "Mirrored into Linear or Slack tracking"); + + const date = now.toISOString().slice(0, 10); + const title = `[watch] Overnight fork patrol: ${date}`; + const existing = findExistingIssue(title); + const tempDir = mkdtempSync(join(tmpdir(), "everos-watch-")); + const bodyFile = join(tempDir, "body.md"); + writeFileSync(bodyFile, report.body); + + if (existing) { + run("gh", [ + "issue", + "comment", + String(existing.number), + "--repo", + repoSlug, + "--body-file", + bodyFile, + ]); + await mirrorToLinear(existing.number, title, report.body); + return; + } + + const created = run("gh", [ + "issue", + "create", + "--repo", + repoSlug, + "--title", + title, + "--body-file", + bodyFile, + "--label", + "overnight-watch", + "--label", + "tracking", + "--label", + "pr-mirror", + ]); + const match = created.match(/\/issues\/(\d+)/); + if (match) { + await mirrorToLinear(Number(match[1]), title, report.body); + } +} + +async function main() { + const gitState = fetchGitState(); + const githubState = fetchGitHubState(); + const report = renderReport(gitState, githubState); + console.log(report.body); + await createOrUpdateTrackingIssue(report); +} + +main().catch((error) => { + console.error(error.message); + process.exit(1); +}); diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a8eec180..aa74ce5f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -103,6 +103,9 @@ jobs: banner_match = re.search(r"\[!\[[^\]]*\]\([^)]+\)\]\(([^)]+)\)", cell) primary_match = re.search(r"^\[(?:Code|Plugin|Live Demo)\]\(([^)]+)\)", cell, flags=re.M) + if "Coming soon" in cell and not primary_match: + continue + if not banner_match: failures.append(f"{path}: {title}: missing linked banner") elif not primary_match: diff --git a/.github/workflows/linear-sync.yml b/.github/workflows/linear-sync.yml new file mode 100644 index 00000000..c2af07de --- /dev/null +++ b/.github/workflows/linear-sync.yml @@ -0,0 +1,124 @@ +name: Linear sync for tracking mirrors + +on: + issues: + types: [opened, labeled] + +permissions: + issues: write + contents: read + +concurrency: + group: linear-sync-issue-${{ github.event.issue.number }} + cancel-in-progress: false + +jobs: + sync: + if: | + contains(github.event.issue.labels.*.name, 'pr-mirror') && + (github.event.action == 'opened' || github.event.label.name == 'pr-mirror') + runs-on: ubuntu-latest + steps: + - name: Mirror GitHub issue to Linear + uses: actions/github-script@v7 + env: + LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} + LINEAR_TEAM_ID: ${{ vars.LINEAR_TEAM_ID }} + LINEAR_PROJECT_ID: ${{ vars.LINEAR_PROJECT_ID }} + with: + script: | + const issue = context.payload.issue; + const repo = `${context.repo.owner}/${context.repo.repo}`; + + // Idempotency: skip if Linear marker comment already exists + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + per_page: 100 + }); + if (comments.some(c => c.body.includes('🔗 Linear:'))) { + core.info('Already synced (marker comment present). Skipping.'); + return; + } + + const isUrgent = issue.labels.some(l => l.name === 'urgent'); + const priority = isUrgent ? 1 : 3; // 1=Urgent, 3=Medium + + const description = [ + `**Source**: [${repo}#${issue.number}](${issue.html_url})`, + '', + '---', + '', + issue.body || '_(no body provided)_', + '', + '---', + '', + `_Auto-synced from GitHub by [linear-sync workflow](https://github.com/${repo}/actions)._` + ].join('\n'); + + const mutation = ` + mutation IssueCreate($input: IssueCreateInput!) { + issueCreate(input: $input) { + success + issue { id identifier url } + } + } + `; + + const response = await fetch('https://api.linear.app/graphql', { + method: 'POST', + headers: { + 'Authorization': process.env.LINEAR_API_KEY, + 'Content-Type': 'application/json', + 'x-apollo-operation-name': 'IssueCreate' + }, + body: JSON.stringify({ + query: mutation, + variables: { + input: { + title: issue.title, + description: description, + teamId: process.env.LINEAR_TEAM_ID, + projectId: process.env.LINEAR_PROJECT_ID, + priority: priority + } + } + }) + }); + + const data = await response.json(); + + if (!response.ok || data.errors || !data?.data?.issueCreate?.success) { + const errMsg = 'Linear API error: ' + JSON.stringify(data, null, 2); + core.error(errMsg); + + try { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'sync-failed', + color: 'D93F0B', + description: 'Linear sync workflow failed for this issue' + }); + } catch (e) { /* exists already */ } + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: ['sync-failed'] + }); + + throw new Error(errMsg); + } + + const linearIssue = data.data.issueCreate.issue; + core.info(`Created ${linearIssue.identifier} -> ${linearIssue.url}`); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `🔗 Linear: [${linearIssue.identifier}](${linearIssue.url})\n\n_Auto-created by linear-sync workflow._` + }); diff --git a/.github/workflows/overnight-watch.yml b/.github/workflows/overnight-watch.yml new file mode 100644 index 00000000..3a0e8489 --- /dev/null +++ b/.github/workflows/overnight-watch.yml @@ -0,0 +1,56 @@ +name: Fork overnight watch + +on: + schedule: + # 02:13 America/Los_Angeles during daylight time. GitHub cron is UTC. + - cron: "13 9 * * *" + workflow_dispatch: + inputs: + create_tracking_issue: + description: "Open/update a tracking issue when the watch verdict is FLAG" + required: false + type: boolean + default: true + watch_branch: + description: "Fork playground branch to verify on origin" + required: false + type: string + default: "codex-watch-overnight-2026-05-13" + +permissions: + actions: read + contents: read + issues: write + pull-requests: read + +concurrency: + group: overnight-watch-${{ github.ref }} + cancel-in-progress: false + +jobs: + watch: + if: github.repository == 'Fearvox/EverOS' + runs-on: ubuntu-latest + env: + REPO_OWNER: ${{ vars.REPO_OWNER || 'Fearvox' }} + REPO_NAME: ${{ vars.REPO_NAME || 'EverOS' }} + UPSTREAM_REPO: EverMind-AI/EverOS + WATCH_BRANCH: ${{ inputs.watch_branch || vars.WATCH_BRANCH || 'codex-watch-overnight-2026-05-13' }} + OWNER_TIMEZONE: ${{ vars.OWNER_TIMEZONE || 'America/Los_Angeles' }} + LINEAR_TEAM_ID: ${{ vars.LINEAR_TEAM_ID || '233391d6-ec9e-4aa8-b534-16a221b8119a' }} + LINEAR_PROJECT_ID: ${{ vars.LINEAR_PROJECT_ID || '39aa3865-345c-4313-9dc0-ab3b509c5d21' }} + LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} + GH_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ github.token }} + CREATE_TRACKING_ISSUE: ${{ inputs.create_tracking_issue || github.event_name == 'schedule' }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Run overnight watch + run: node .github/scripts/overnight-watch.mjs diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 00000000..bfd8d884 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,61 @@ +name: Sync fork from upstream + +on: + schedule: + - cron: '17 */6 * * *' # 每 6 小时跑一次,分钟 17 错开高峰 + workflow_dispatch: # 也支持手动触发 + +permissions: + contents: write + issues: write + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Add upstream remote + run: git remote add upstream https://github.com/EverMind-AI/EverOS.git + + - name: Fetch upstream + run: git fetch upstream + + - name: Rebase fork main onto upstream/main + id: rebase + run: | + git checkout main + set +e + git rebase upstream/main + rc=$? + if [ $rc -ne 0 ]; then + git rebase --abort + echo "conflict=true" >> "$GITHUB_OUTPUT" + exit $rc + fi + echo "conflict=false" >> "$GITHUB_OUTPUT" + + - name: Push to fork main + if: steps.rebase.outputs.conflict == 'false' + run: git push origin main --force-with-lease + + - name: Open issue on conflict + if: failure() && steps.rebase.outputs.conflict == 'true' + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `[sync] Rebase conflict syncing fork from upstream (${new Date().toISOString().slice(0,10)})`, + body: `Auto-sync from \`upstream/main\` failed due to rebase conflicts.\n\nRun id: ${context.runId}\nWorkflow: ${context.workflow}\n\nResolve manually:\n\n\`\`\`\ncd ~/EverOS && git fetch upstream && git rebase upstream/main\n# resolve conflicts, then:\ngit push origin main --force-with-lease\n\`\`\``, + labels: ['tracking'] + }); diff --git a/AGENTS.md b/AGENTS.md index 2707448d..d5b6b160 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,3 +71,14 @@ uv run pyright # Type check, if pyright is installed variants. - Prefer existing repo patterns and component boundaries before adding new abstractions. + +## Review guidelines + +- GitHub Copilot, Codex, and other review agents should follow + `.github/copilot-instructions.md`. +- Start PR reviews with the MUW block: + `VERDICT: PASS / FLAG / BLOCK`, `VERDICT_SUMMARY:`, and `EVIDENCE:`. +- Do not mark a PR `PASS` from author summary alone; inspect the actual diff, + linked issue, and available checks first. +- Report actionable findings first, ordered by severity, with file/path, + evidence, impact, and fix guidance. diff --git a/docs/fork-playground/overnight-watch.md b/docs/fork-playground/overnight-watch.md new file mode 100644 index 00000000..379fd21b --- /dev/null +++ b/docs/fork-playground/overnight-watch.md @@ -0,0 +1,54 @@ +# Overnight Fork Watch + +This fork can move fast, but the upstream feed should stay boring and auditable. +The overnight watch is a small GitHub Actions patrol for `Fearvox/EverOS`. + +## What It Checks + +- `origin/main` drift against `EverMind-AI/EverOS` `upstream/main`. +- Whether the active playground branch exists on the fork: + `codex-watch-overnight-2026-05-13`. +- Failed, cancelled, or timed-out fork workflow runs in the last 24 hours. +- Upstream and fork pull requests updated in the last 24 hours. + +## Tracking Behavior + +The workflow prints a public-safe report on every run. If the verdict is `FLAG`, +it opens or updates a GitHub issue labeled: + +- `overnight-watch` +- `tracking` +- `pr-mirror` + +Issues created by `GITHUB_TOKEN` do not trigger secondary workflows. Because of +that, the watch mirrors the tracking issue to Linear directly when +`LINEAR_API_KEY` is available. The target Linear team/project are: + +- Team: `233391d6-ec9e-4aa8-b534-16a221b8119a` +- Project: `39aa3865-345c-4313-9dc0-ab3b509c5d21` + +A `FLAG` verdict does not fail the watch workflow by itself. Runtime errors +still fail the workflow, but expected drift or downstream failures are reported +through the tracking issue so the watch does not poison its own next run. + +## Manual Run + +```bash +REPO_OWNER=Fearvox \ +REPO_NAME=EverOS \ +WATCH_BRANCH=codex-watch-overnight-2026-05-13 \ +OWNER_TIMEZONE=America/Los_Angeles \ +LINEAR_TEAM_ID=233391d6-ec9e-4aa8-b534-16a221b8119a \ +LINEAR_PROJECT_ID=39aa3865-345c-4313-9dc0-ab3b509c5d21 \ +CREATE_TRACKING_ISSUE=false \ +node .github/scripts/overnight-watch.mjs +``` + +Set `CREATE_TRACKING_ISSUE=true` only when you want the local run to mutate +GitHub issues. + +## Public-Surface Hygiene + +Reports intentionally avoid local absolute paths, host/IP values, token names +beyond the required GitHub secret names, and operator-only commands. They should +be safe to show in Discord or a screen share.