chore: merge origin/main into mintlify/af82bda4 and resolve conflicts
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
# CI/CD and repository automation (`.github/`)
|
||||
|
||||
> **Do not modify any workflow without explicit approval from a maintainer.** Publishing
|
||||
> credentials are bound to workflow filenames, and the gate workflows decide whether
|
||||
> contributions are accepted. Read this file before proposing any change here.
|
||||
|
||||
## CI: one gate, many pipelines
|
||||
|
||||
`ci-gate.yml` (**CI Gate**) is the single entry point. It runs on every PR, detects which packages changed, and calls only the relevant package workflows as reusable workflows (`workflow_call`). Its final `CI Gate` job aggregates the results: skipped pipelines pass, failed or cancelled ones fail. It is the **only CI status check that needs to be required** in branch protection.
|
||||
|
||||
Package workflows keep their own push-to-main and manual triggers. Their `pull_request` triggers live in the gate's path filters instead.
|
||||
|
||||
| Workflow | File | Standalone triggers | Runs |
|
||||
|----------|------|---------------------|------|
|
||||
| CI Gate | `ci-gate.yml` | All PRs | Routes to and aggregates everything below |
|
||||
| Python SDK | `ci.yml` | Push to main | Ruff + pytest on Python 3.10, 3.11, 3.12 |
|
||||
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main (`mem0-ts/`) | Prettier + build + jest on Node 20, 22 |
|
||||
| Python CLI | `cli-python-ci.yml` | Push to main (`cli/python/`), manual | Ruff + pytest + hatch build on Python 3.10, 3.11, 3.12 |
|
||||
| Node CLI | `cli-node-ci.yml` | Push to main (`cli/node/`), manual | Biome + tsc + vitest + tsup on Node 20, 22 |
|
||||
| OpenClaw | `openclaw-checks.yml` | Push to main (`integrations/openclaw/`), manual | tsc + vitest (Codecov) + tsup on Node 20, 22 |
|
||||
| Agent Plugins Python | `agent-plugins-python-checks.yml` | Push to main (shared Python core and native/portable plugin directories), manual | Runtime tests on Python 3.10; full pytest on 3.11, 3.12; ruff + generated-package drift on 3.12 |
|
||||
| Agent Plugins TypeScript | `agent-plugins-typescript-checks.yml` | Push to main (`integrations/agent-plugin-core/typescript/`), manual | tsc + node:test on Node 22 |
|
||||
| OpenCode Plugin | `opencode-plugin-checks.yml` | Push to main (`integrations/opencode-plugin/`), manual | Bun: tsc + build + dist artifact check |
|
||||
| Pi Agent Plugin | `pi-agent-plugin-checks.yml` | Push to main (`integrations/pi-agent-plugin/`), manual | tsc + vitest + tsup on Node 20, 22 |
|
||||
| DeepSeek Harness Plugin | `deepseek-plugin-checks.yml` | Push to main (`integrations/deepseek-plugin/`), manual | tsc + vitest + tsup on Node 20, 22 |
|
||||
| n8n Node | `n8n-nodes-mem0-checks.yml` | Push to main (`integrations/n8n-nodes-mem0/`), manual | ESLint + tsc build on Node 20 |
|
||||
| Zapier App | `zapier-mem0-checks.yml` | Push to main (`integrations/zapier-mem0/`), manual | tsc + `zapier validate` + offline unit tests on Node 22 |
|
||||
| mem0-strands | `mem0-strands-checks.yml` | Push to main (`integrations/mem0-strands/`), manual | Ruff + mypy + pytest + hatch build on Python 3.10, 3.11, 3.12 |
|
||||
| docs llms.txt | `docs-llms-txt-check.yml` | Manual | `docs/llms.txt` coverage |
|
||||
| GitHub Scripts | inline in `ci-gate.yml` | none | `node` over every `.github/scripts/*.test.js` |
|
||||
|
||||
Adding a package CI workflow: give it `workflow_call` plus `push` / `workflow_dispatch` as needed but **no `pull_request` trigger**, then register it in `ci-gate.yml` with a path filter under the `changes` job, a call job, and an entry in the gate job's `needs` list.
|
||||
|
||||
`GitHub Scripts` is the one row that is a plain job inside `ci-gate.yml` rather than a called workflow, because a reusable workflow wrapping two `node` invocations would be more file than test. It runs on the `github_scripts` filter, which covers `.github/scripts/**` plus every file those tests read: `pr-gate.yml`, `vouch-check-pr.yml`, `issue-labeler.yml`, and `VOUCHED.td`. Add a new `.github/scripts/*.test.js` and it is picked up with no wiring; make a test read a new file and that file belongs in the filter.
|
||||
|
||||
## Branch protection on `main`
|
||||
|
||||
A repository ruleset named `Main Branch Rule`, id `11813754`. It enforces squash-only merges, linear history, no deletion, no force-push, and one approving review. Two status checks belong in its `required_status_checks` rule:
|
||||
|
||||
| Context | Posted by | Why |
|
||||
|---------|-----------|-----|
|
||||
| `CI Gate` | `ci-gate.yml` | Aggregates every package pipeline |
|
||||
| `license/cla` | CLA Assistant | Proves the CLA is signed, not merely requested |
|
||||
|
||||
Editing the ruleset requires repo **admin**. `maintain` is not enough, and the API returns 404 rather than 403 in that case. Until `license/cla` is required, the claim in `CONTRIBUTING.md` that unsigned PRs are blocked from merging holds by convention only.
|
||||
|
||||
Requiring `CI Gate` also means fork PRs from first-time contributors cannot merge until a maintainer approves the workflow run. Those sit at `action_required`, which is intended behavior.
|
||||
|
||||
## CD: one router, many publishers
|
||||
|
||||
`release.yml` (**Release Router**) is the only workflow listening to `release: published`. It matches the tag prefix and dispatches the matching package workflow through `workflow_dispatch`, so one release produces exactly one routed run.
|
||||
|
||||
| Workflow | File | Tag prefix | Target |
|
||||
|----------|------|------------|--------|
|
||||
| Release Router | `release.yml` | all releases | dispatches the rows below |
|
||||
| Python SDK | `cd.yml` | `v*` | PyPI (`mem0ai`) |
|
||||
| TypeScript SDK | `ts-sdk-cd.yml` | `ts-v*` | npm (`mem0ai`) |
|
||||
| Python CLI | `cli-python-cd.yml` | `cli-v*` | PyPI (`mem0-cli`) |
|
||||
| Node CLI | `cli-node-cd.yml` | `cli-node-v*` | npm (`@mem0/cli`) |
|
||||
| Vercel AI SDK | `vercel-ai-cd.yml` | `vercel-ai-v*` | npm (`@mem0/vercel-ai-provider`) |
|
||||
| OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) |
|
||||
| OpenCode Plugin | `opencode-plugin-cd.yml` | `opencode-v*` | npm (`@mem0/opencode-plugin`) |
|
||||
| Pi Agent Plugin | `pi-agent-plugin-cd.yml` | `pi-agent-v*` | npm (`@mem0/pi-agent-plugin`) |
|
||||
| DeepSeek Harness Plugin | `deepseek-plugin-cd.yml` | `deepseek-plugin-v*` | npm (`@mem0/deepseek-plugin`) |
|
||||
| n8n Node | `n8n-nodes-mem0-cd.yml` | `n8n-nodes-mem0-v*` | npm (`@mem0/n8n-nodes-mem0`) |
|
||||
| mem0-strands | `mem0-strands-cd.yml` | `mem0-strands-v*` | PyPI (`mem0-strands`) |
|
||||
|
||||
- Package CD workflows are `workflow_dispatch`-only, with `tag` and `prerelease` inputs. They check out and build the given tag.
|
||||
- All publishing uses **OIDC trusted publishing**. No tokens, no secrets.
|
||||
- Registry trusted-publisher settings are pinned to each package's own workflow **filename**. Renaming a CD workflow breaks publishing for that package.
|
||||
- First publish of a new npm package must be done manually. OIDC works from the second version onward.
|
||||
- To re-publish a release, do **not** delete and recreate the GitHub release. Dispatch the workflow directly: `gh workflow run <package>-cd.yml --ref refs/tags/<tag> -f tag=<tag>`.
|
||||
- The Zapier app deploys to Zapier's platform, not npm, so it is not in the router. Deploy with `gh workflow run zapier-mem0-cd.yml --ref main`.
|
||||
- Adding a package: add its CD workflow, then register its tag prefix in the `case` block in `release.yml`, keeping the bare `v*` arm last.
|
||||
|
||||
## Contribution gates
|
||||
|
||||
| Workflow | File | Purpose |
|
||||
|----------|------|---------|
|
||||
| PR Gate | `pr-gate.yml` | Closes PRs that do not link an issue labeled `accepted`, and reopens them when that label arrives. Exempts members, bots, drafts, and docs-only changes. Never checks out PR code. |
|
||||
| Vouch (check PR) | `vouch-check-pr.yml` | Closes PRs from authors denounced in `VOUCHED.td`. Comments once on PRs from authors merely absent from it, and blocks nothing in that case. |
|
||||
| Vouch (manage list) | `vouch-manage-by-issue.yml` | Maintainers edit the trust list by commenting `!vouch @user`, `!denounce @user`, or `!unvouch @user` on any issue. Opens a PR against `VOUCHED.td` through a GitHub App token, for a maintainer to merge. |
|
||||
| Issue Labeler | `issue-labeler.yml` | Labels issues from the `component` field in the issue forms |
|
||||
| PR Labeler | `pr-labeler.yml` | Path-based labels, plus propagating labels from linked issues |
|
||||
| Stale Bot | `stale.yml` | Marks stale issues and PRs |
|
||||
| llms.txt Check | `docs-llms-txt-check.yml` | Blocks PRs touching `docs/**/*.mdx` when `docs/llms.txt` is out of sync |
|
||||
|
||||
`pr-gate.yml` and `vouch-check-pr.yml` use `pull_request_target`, which is required to label and close fork PRs. Neither checks out PR code and neither has a `run:` step, so there is no pwn-request or script-injection surface. Keep it that way: any future `run:` step in these files must never interpolate `github.event.*` text.
|
||||
|
||||
Both workflows exempt maintainers twice, and the second guard is the one that holds. `author_association` is rendered for the viewer, and a webhook payload has no privileged viewer: `MEMBER` needs the author's org membership to be **public**, `COLLABORATOR` needs a **direct** repository invite. An org member with private membership whose `maintain` comes through a team matches neither and arrives as `CONTRIBUTOR`, which is how PR #6948 was closed by its own author's gate. So the guard also skips any PR whose head branch lives in this repository (`head.repo.full_name == github.repository`). Pushing a branch here already requires write access and outside contributors always arrive from a fork, so that test means the same thing without depending on who is looking. Keep both: the `author_association` arm still covers members who work from their own fork.
|
||||
|
||||
`pr-gate.yml` carries two jobs whose `if:` conditions are deliberately disjoint. `gate` closes, and only ever runs on `opened`, `reopened`, and `ready_for_review`. `reopen` reopens, and only ever runs on `edited` or on `issues: labeled` with the `accepted` label. Nothing can both close and reopen on the same event, which is the property to preserve when editing either guard.
|
||||
|
||||
That split exists because the two halves of a gated PR's recovery arrive in either order. A maintainer usually labels the issue `accepted` at triage, before the author has linked it; sometimes the link lands first and the label follows. So `reopen` handles both directions. From `issues: labeled` it walks `closedByPullRequestsReferences` back to the pull requests that link the issue. From `edited` it takes the edited pull request directly. Both paths then apply the same four tests: the author is not denounced in `VOUCHED.td`, the PR is `CLOSED`, it links an issue labeled `accepted`, and it carries the `<!-- pr-gate -->` marker comment. Without the label path, a maintainer's label is inert. Without the `edited` path, an author who links the issue after it was labeled is stuck, since no other event fires.
|
||||
|
||||
The denounce test is what keeps the two gates from cancelling each other out. A denounced author whose PR also lacked an accepted issue was closed by both workflows, so it carries the `<!-- pr-gate -->` marker, and labeling the linked issue would otherwise reopen it. Vouch cannot undo that: reopening runs through `GITHUB_TOKEN`, which raises no events, so `vouch-check-pr.yml` never fires a second time. Reading the list here is the only place the check can live. It fails open like vouch does, warning and treating nobody as denounced if the file cannot be read, and it is the one piece of vouch semantics duplicated outside `vouch-check-pr.yml`, because `pr-gate.yml` never checks out the repository and so cannot import a shared parser. `.github/scripts/vouch-decision.test.js` covers the parsing and asserts `pr-gate.yml` still filters the list the same way.
|
||||
|
||||
`edited` must never reach the `gate` job. It fires on any title or description change, so when `gate` listened for it the gate re-judged pull requests that had been open for days and closed them the moment their author touched the description, which is what closed #6948. Rescuing on `edited` is safe for the same reason closing on it was not: the job can only move a PR from closed to open.
|
||||
|
||||
Reopening runs through `GITHUB_TOKEN`, which by design raises no further workflow events, so `gate` cannot bounce a freshly reopened PR straight back out.
|
||||
|
||||
The concurrency group is keyed on `github.event.action` as well as `github.event_name` and the number, and both keys carry weight. Without the event name, a maintainer applying `bug` right after `accepted` cancels the reopen mid-flight, since `cancel-in-progress` is on for `pull_request_target` and both label events would land in the same group. Without the action, `opened` and `edited` share a group on the same pull request, and an author who ticks a template checkbox in the seconds after opening cancels the run that was about to gate them: `gate` skips `edited` and `reopen` skips an open pull request, so the cancelled run is never replaced and the pull request stays ungated forever, since `opened` fires exactly once. Rapid successive edits still cancel each other, which is the dedup that was wanted.
|
||||
|
||||
The `edited` arm of `reopen` requires `github.event.pull_request.state == 'closed'`, so ordinary description edits on open pull requests do not start a runner.
|
||||
|
||||
Two known gaps, both mild. A PR that the gate closed, that someone reopened, and that a maintainer then closed deliberately still carries the marker, so labeling its issue reopens it again; a maintainer closes it once more. And an author who strips `Closes #<number>` out after passing keeps an open PR, which a reviewer sees anyway.
|
||||
|
||||
`GATE_EFFECTIVE_FROM` in `pr-gate.yml` is a `created_at` cutoff. `reopened` and `ready_for_review` still fire on PRs opened long before the gate existed, so without the cutoff part of the open backlog would be closed by a rule that did not exist when those PRs were filed. Set it to the actual merge date in UTC.
|
||||
|
||||
The gate's docs-only exemption covers `docs/` plus a named allowlist of four root files: `README.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, and `SECURITY.md`. It is an allowlist rather than a rule about top-level markdown because the repository root also holds `AGENTS.md`, `CLAUDE.md`, and `LLM.md`, which are the instructions coding agents read before touching this codebase. Those are functional files that happen to be written in prose, and rewriting them is a change to behaviour, so they stay gated. Markdown nested anywhere else stays gated for the same reason: `skills/**/*.md` and everything under `.github/` are functional too. Adding a genuinely prose root file means adding it to `rootDocs` in `pr-gate.yml`.
|
||||
|
||||
`.github/scripts/pr-gate-docs-exemption.test.js` covers that predicate. It pulls the `rootDocs` and `isDocs` lines out of `pr-gate.yml` and evaluates them, so it exercises the shipped rule rather than a copy that could drift from it, and it pins `AGENTS.md`, `CLAUDE.md`, and `LLM.md` on the gated side along with `skills/**/*.md`, nested `.github/` files, and the empty file list. It only accepts those two declarations in a literal one-line form, so keep `rootDocs` a `Set` of quoted names and `isDocs` a single arrow expression.
|
||||
|
||||
The two contribution gates answer different questions and neither covers for the other. `pr-gate.yml` judges the change, and the `accepted` label is how a maintainer says yes to it. `vouch-check-pr.yml` judges the author, and `VOUCHED.td` is how a maintainer says no to one. A vouched author with no accepted issue is still closed by the gate; a denounced author with an accepted issue is still closed by vouch. Read either one as a backstop for the other and both get weakened.
|
||||
|
||||
Vouch enforces on the denounce axis only, through `require-vouch: false` with `auto-close: true`. That pair is not the obvious reading of either input, so the decision table from v1.5.0 (`vouch/github.nu` at pinned SHA `d66fa29`) is worth stating outright:
|
||||
|
||||
| Author | `status` | Effect |
|
||||
|---|---|---|
|
||||
| ends in `[bot]` | `skipped` | nothing |
|
||||
| collaborator with write or admin | `vouched` | nothing |
|
||||
| listed in `VOUCHED.td` | `vouched` | nothing |
|
||||
| listed as `-handle` | `closed` | action comments and closes |
|
||||
| absent from the file | `allowed` | workflow comments, nothing closed |
|
||||
|
||||
`require-vouch: true` would close every first-time contributor, which is the opposite of what a trust list is for: the funnel has to stay open or nobody ever earns a vouch. `auto-close: false` is the setting that looked safe and did nothing at all, since in v1.5.0 both the unvouched and the denounced branch return before posting anything, leaving only a line in the run log. That is why `!denounce` was decorative until this pair landed.
|
||||
|
||||
Only the `allowed` arm is ours: a `github-script` step posts the soft comment, keyed on a `<!-- vouch-check -->` marker so a reopen does not comment twice. The `closed` arm belongs to the action, message and all. Keeping the two arms disjoint is what stops a denounced author getting two comments, so if that step is ever re-keyed off `allowed`, check the overlap first.
|
||||
|
||||
`.github/scripts/vouch-decision.test.js` holds that table as a `decide()` function and asserts the workflow's `require-vouch`, `auto-close`, and comment-step gating still produce it, comment counts included. Be clear about what that does and does not prove. `decide()` is a **hand transcription** of `gh-check-pr`, read from `vouch/github.nu` at the pinned SHA; the test cannot run the action, so it cannot notice the action changing underneath it. Left alone it would agree with itself forever, which makes bumping the pinned SHA the one edit it would otherwise sail through. So it also asserts `vouch-check-pr.yml` still pins `PINNED_VOUCH_SHA`, and a bump fails it on purpose: re-read `gh-check-pr` at the new revision, correct `decide()` and the table above, then move the constant. CI runs it through the `GitHub Scripts` job on any change to the scripts or the files they read.
|
||||
|
||||
Failure is open by design. If the action cannot read `VOUCHED.td` it falls back to an empty list, every author reads as absent, and nobody is closed by an API hiccup.
|
||||
|
||||
`vouch-manage-by-issue.yml` runs with `merge-immediately: "false"`. The `Main Branch Rule` ruleset requires one approving review and has no bypass actors, so the action's immediate `PUT /pulls/{n}/merge` would return 405 and leave `VOUCHED.td` unchanged on `main`. The bot opens the PR, a maintainer merges it. Setting `pull-request: "false"` is not an alternative: the same ruleset blocks direct pushes.
|
||||
|
||||
That workflow also needs `VOUCH_APP_ID` and `VOUCH_APP_PRIVATE_KEY` repository secrets. Without them it fails at the token step before doing anything. `vouch-check-pr.yml` needs neither.
|
||||
|
||||
## Issue forms and templates
|
||||
|
||||
`ISSUE_TEMPLATE/*.yml` are GitHub issue **forms**, not markdown templates. Only forms support `required: true` and machine-parseable field ids. Blank issues are disabled in `config.yml`.
|
||||
|
||||
`issue-labeler.yml` reads only the `component` field id through `stefanbuck/github-issue-parser` and `redhat-plumbers-in-action/advanced-issue-labeler`, so adding new field ids is safe. Renaming `component` is not.
|
||||
|
||||
Current field ids:
|
||||
|
||||
| Form | Ids |
|
||||
|------|-----|
|
||||
| `bug_report.yml` | `component`, `description`, `verification`, `ai_assistance` |
|
||||
| `feature_request.yml` | `component`, `description`, `ai_assistance` |
|
||||
| `documentation_issue.yml` | `description`, `ai_assistance` |
|
||||
|
||||
## Trust list
|
||||
|
||||
`VOUCHED.td` is one GitHub username per line, `#` for comments. Seeded from every author with at least one merged PR in this repository, then filtered: accounts at or below a 16% merge rate across five or more attempts were dropped, since landing one change out of many is the signature of automated submission rather than contribution.
|
||||
|
||||
Vouch's only built-in exemptions are accounts ending in `[bot]` and repo collaborators with `write` or `admin`. **Organization membership alone is not one of them.** So `vouch-check-pr.yml` carries a job-level `if:` that skips the check for `OWNER`, `MEMBER`, and `COLLABORATOR` authors, the same exemption `pr-gate.yml` already applies. Org members are still listed in the file as a fallback, but the workflow guard is what actually holds.
|
||||
@@ -0,0 +1 @@
|
||||
AGENTS.md
|
||||
@@ -51,3 +51,43 @@ body:
|
||||
- OS:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: verification
|
||||
attributes:
|
||||
label: How You Verified This
|
||||
description: We only take on bugs someone has actually reproduced. Show your work.
|
||||
value: |
|
||||
### What I Ran
|
||||
|
||||
The exact command or script, and where it ran.
|
||||
|
||||
### What I Saw
|
||||
|
||||
The real output, log line, or traceback. Paste it, do not describe it.
|
||||
|
||||
### Why This Is a Bug
|
||||
|
||||
What should have happened instead, and what says so: a docs link, a
|
||||
docstring, a test, or the code itself.
|
||||
|
||||
### What I Ruled Out
|
||||
|
||||
Anything you checked that turned out not to be the cause.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: ai_assistance
|
||||
attributes:
|
||||
label: AI Assistance
|
||||
description: >-
|
||||
This asks how the bug was found and confirmed, not how the text was
|
||||
written. Drafting the write-up with AI is fine. We ask because it tells
|
||||
us how much to trust the reproduction, not because it counts against you.
|
||||
options:
|
||||
- No AI involved
|
||||
- AI helped me find it, and I reproduced it myself afterwards
|
||||
- AI found and wrote this, and I have not reproduced it myself
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
blank_issues_enabled: true
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Discord Community
|
||||
- name: Question or general help
|
||||
url: https://discord.gg/6PzXDgEjG5
|
||||
about: Ask questions and discuss with the community
|
||||
about: Ask on Discord. The issue tracker is for bugs and accepted work only.
|
||||
- name: Documentation
|
||||
url: https://docs.mem0.ai
|
||||
about: Read the official mem0 documentation
|
||||
- name: Report a security vulnerability
|
||||
url: https://github.com/mem0ai/mem0/security/advisories/new
|
||||
about: Report privately through a security advisory. Never open a public issue.
|
||||
|
||||
@@ -21,3 +21,17 @@ body:
|
||||
How should the docs be improved?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: ai_assistance
|
||||
attributes:
|
||||
label: AI Assistance
|
||||
description: >-
|
||||
This asks how the problem was found, not how the text was written.
|
||||
Drafting the write-up with AI is fine.
|
||||
options:
|
||||
- No AI involved, I hit this reading the docs
|
||||
- AI-assisted, but I checked the page myself
|
||||
- AI found this, and I have not opened the page
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -36,3 +36,18 @@ body:
|
||||
Any workarounds you've tried or other approaches considered.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: ai_assistance
|
||||
attributes:
|
||||
label: AI Assistance
|
||||
description: >-
|
||||
This asks where the idea came from, not how the text was written.
|
||||
Drafting the write-up with AI is fine. A request you hit yourself while
|
||||
building something carries more weight than one a model suggested.
|
||||
options:
|
||||
- No AI involved, this is a need I hit myself
|
||||
- AI-assisted, but the need is mine
|
||||
- AI suggested this feature
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -14,6 +14,18 @@ Closes #<!-- issue number -->
|
||||
- [ ] Refactor (no functional changes)
|
||||
- [ ] Documentation update
|
||||
|
||||
## AI Assistance
|
||||
|
||||
<!-- This is about the code, not this description. Writing the description with AI is fine. -->
|
||||
|
||||
- [ ] No AI assistance
|
||||
- [ ] AI-assisted (autocomplete, or I asked a model questions while writing this)
|
||||
- [ ] AI-generated (an agent wrote most or all of this diff)
|
||||
|
||||
<!-- If you ticked either AI box, name the tool and what you checked yourself. -->
|
||||
|
||||
- [ ] **I can explain every line of this diff and how it interacts with the rest of the codebase, without asking an AI tool.**
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
<!-- If this is a breaking change, describe what breaks and the migration path. Delete this section if not applicable. -->
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
# The list of vouched (or denounced) users for this repository.
|
||||
#
|
||||
# A denounced user (prefixed with a minus) is blocked outright: their pull
|
||||
# requests are closed on sight, whatever they link. Being absent from this file
|
||||
# blocks nothing. An unvouched author gets one comment saying so and their pull
|
||||
# request is reviewed like anyone else's, because a first contribution has to
|
||||
# start somewhere. Vouching is how that comment stops.
|
||||
#
|
||||
# This list is about who, and it is the only thing that judges who. Whether a
|
||||
# change is wanted is a separate question, answered by the accepted label and
|
||||
# enforced by pr-gate.yml. Neither gate substitutes for the other: a vouched
|
||||
# author still needs an accepted issue, and a denounced author is turned away
|
||||
# even holding one.
|
||||
#
|
||||
# Vouch automatically allows two kinds of account without consulting this file:
|
||||
# accounts ending in [bot], and repo collaborators with write or admin
|
||||
# permission. Org membership on its own is NOT one of them, so
|
||||
# vouch-check-pr.yml skips the check for OWNER, MEMBER, and COLLABORATOR
|
||||
# authors, and for any branch pushed to this repository.
|
||||
#
|
||||
# Keep every mem0ai member listed below anyway. The author_association arm of
|
||||
# that guard is weaker than it looks: MEMBER needs the member's org membership
|
||||
# to be public and COLLABORATOR needs a direct repo invite, so a member with
|
||||
# private membership and a team-derived role reads as CONTRIBUTOR. Working from
|
||||
# a branch here covers them, working from their own fork leaves this file as
|
||||
# the only thing that does. A missing or miscased entry is a real gap.
|
||||
#
|
||||
# Syntax:
|
||||
# - One handle per line (without @), sorted alphabetically.
|
||||
# - Optionally specify platform: `platform:username` (e.g. `github:mitchellh`).
|
||||
# - To denounce a user, prefix with minus: `-username`.
|
||||
# - Optionally add a note after a space following the handle.
|
||||
#
|
||||
# Maintainers vouch by commenting "!vouch @username" on any issue, and denounce
|
||||
# with "!denounce @username". The bot commits the change back to this file.
|
||||
#
|
||||
# Seeded on 2026-08-12 from every author with at least one merged pull request,
|
||||
# then filtered: accounts with a merge rate at or below 16% across six or more
|
||||
# attempts were dropped, since landing one change out of many is the signature
|
||||
# of automated submission rather than contribution. Removal is not a ban. Any
|
||||
# maintainer can !vouch these accounts back in.
|
||||
1MikeMakuch
|
||||
aaishikdutta
|
||||
Aarkin7
|
||||
abdullahirfann
|
||||
AbdurNawaz
|
||||
abhay-codes07
|
||||
Abhineshhh
|
||||
ac12644
|
||||
acarbonetto
|
||||
adh-wonolo
|
||||
Aditya-Tripuraneni
|
||||
aesher9o1
|
||||
agumpandey
|
||||
ahnedeee
|
||||
ajmalmohad
|
||||
AkisAya
|
||||
akshat1423
|
||||
akshseh
|
||||
alessandropanzieri
|
||||
alohays
|
||||
aloktripathi1
|
||||
amahuli03
|
||||
amanagarwal042
|
||||
Ameysr
|
||||
amjadraza
|
||||
anantoj
|
||||
anchit-nishant
|
||||
andrewghlee
|
||||
andy-k-improving
|
||||
anifort
|
||||
anishesg
|
||||
AnkushMalaker
|
||||
AnnaSuSu
|
||||
anujshandillya
|
||||
ArchishmanSengupta
|
||||
Arsh-mem0
|
||||
ArthurHoward1
|
||||
aryankhanna475
|
||||
Ashu463
|
||||
atahanyild
|
||||
AtharvaJaiswal005
|
||||
atkinsh
|
||||
avp1598
|
||||
axelray-dev
|
||||
ayaangazali
|
||||
aymenkrifa
|
||||
barry166
|
||||
being-abhi
|
||||
berwinjoule
|
||||
BillionClaw
|
||||
bioshazard
|
||||
bisla
|
||||
bkidd1
|
||||
blino
|
||||
bmsvinci1729
|
||||
boss-mao
|
||||
Br1an67
|
||||
brucewkz
|
||||
cachho
|
||||
caifeizhi
|
||||
candidosales
|
||||
cclauss
|
||||
chaithanyak42
|
||||
chinnuabey
|
||||
ChiragArora31
|
||||
ChrisFloofyKitsune
|
||||
chrisqu777
|
||||
clementantonyk
|
||||
codexvn
|
||||
Colsrch
|
||||
CrepuscularIRIS
|
||||
ctxlong
|
||||
danielsiwiec
|
||||
darkhaniop
|
||||
davidatorres
|
||||
deshraj
|
||||
Dev-Khant
|
||||
Devan019
|
||||
deven298
|
||||
devYRPauli
|
||||
DhanushNehru
|
||||
DhilipBinny
|
||||
Dhravya
|
||||
dimigerontaki
|
||||
Diveyam-Mishra
|
||||
divyansh-1009
|
||||
Divyanshu9822
|
||||
dog-last
|
||||
DrJsPBs
|
||||
dtee1
|
||||
DumoeDss
|
||||
e-biswas
|
||||
Echo3ToEcho7
|
||||
eldar702
|
||||
eltociear
|
||||
EnzoFanAccount
|
||||
Esparon1
|
||||
Fahmid-Arman
|
||||
Failfail2603
|
||||
FarukhS52
|
||||
farzad528
|
||||
felipeavilis
|
||||
femto
|
||||
fengjikui
|
||||
fenilfaldu
|
||||
fileames
|
||||
Flyfoxs
|
||||
fmercurio
|
||||
FoliageOwO
|
||||
fran3cc
|
||||
frank-zsy
|
||||
frederikb96
|
||||
Freshield
|
||||
freya0926
|
||||
G26karthik
|
||||
gabe-l-hart
|
||||
gabrielstein-mem0
|
||||
gajazlikovac
|
||||
gasolin
|
||||
gaurav0107
|
||||
gauravagerwala
|
||||
Genarojrsanchez
|
||||
ghdcksgml1
|
||||
GingerMoon
|
||||
gmdorf
|
||||
golemus
|
||||
GongRzhe
|
||||
GopalGB
|
||||
Gyubin
|
||||
haarishmk26
|
||||
hackice20
|
||||
halanm
|
||||
hardik1408
|
||||
Harin329
|
||||
harshgupta-mem0
|
||||
harshpandit007
|
||||
hayescode
|
||||
hcsum
|
||||
he-yufeng
|
||||
heng-ah
|
||||
Hexecu
|
||||
Himanshu-Sangshetti
|
||||
hjlarry
|
||||
HowieG
|
||||
HrushiYadav
|
||||
HScarb
|
||||
huveewomg
|
||||
Hybirdss
|
||||
ianupamsingh
|
||||
IgnazioDS
|
||||
immuhammadfurqan
|
||||
into-the-night
|
||||
invincible04
|
||||
Itz-Antaripa
|
||||
ixchio
|
||||
Jaco-Ren
|
||||
Jai0401
|
||||
JainamShah-22
|
||||
Jainish-S
|
||||
jarediaz
|
||||
jeanibarz
|
||||
Jerry-Terrasse
|
||||
jessai2026
|
||||
jesse-c
|
||||
jfeng18
|
||||
jferrettiboke
|
||||
jjjojoj
|
||||
joaomdmoura
|
||||
JoeSL
|
||||
johnwlockwood
|
||||
jonasiwnl
|
||||
josephchancey
|
||||
juananpe
|
||||
juaneloDev
|
||||
junmo1215
|
||||
Jupiter363
|
||||
KapilM26
|
||||
karthik-indla
|
||||
KarthikeyaKollu
|
||||
kartik-mem0
|
||||
katarinasupe
|
||||
ketangangal
|
||||
kimnamu
|
||||
kindertheo
|
||||
kirex0
|
||||
kirklin
|
||||
kk2211
|
||||
kmitul
|
||||
koi646
|
||||
kratos0718
|
||||
krescent
|
||||
Krishnachaitanyakc
|
||||
kriszlazar
|
||||
KushagraB424
|
||||
l1anch1
|
||||
lamost423
|
||||
lan17
|
||||
LeonieFreisinger
|
||||
lh0x00
|
||||
limboinf
|
||||
liviaellen
|
||||
longway-code
|
||||
lsvishaal
|
||||
LuciAkirami
|
||||
lucifertrj
|
||||
lvpx
|
||||
ly-wang19
|
||||
maamalama
|
||||
maccuryj
|
||||
mae5357
|
||||
mahone3297
|
||||
Malhis
|
||||
maljazaery
|
||||
manganeseheptoxide
|
||||
manthanguptaa
|
||||
mark-watson
|
||||
Mark-Zeng
|
||||
markmbain
|
||||
matanco1
|
||||
mauricioalarcon
|
||||
maxvonhippel
|
||||
me-tusharchandra
|
||||
mezotv
|
||||
MgeeeeK
|
||||
mggger
|
||||
mgoulart
|
||||
microbluey
|
||||
mikejgray
|
||||
Mingxiangyu
|
||||
Mini256
|
||||
misrasaurabh1
|
||||
mjzcng
|
||||
mogith-pn
|
||||
morgoth9808
|
||||
moyueheng
|
||||
mrbusche
|
||||
Mrinank-Bhowmick
|
||||
muhammed-mamun
|
||||
MUZAMMILPERVAIZ
|
||||
mvanhorn
|
||||
naman09
|
||||
NavyaAlapati13
|
||||
neilbhutada
|
||||
NightClover-code
|
||||
nikhilsharma26500
|
||||
NILAY1556
|
||||
niv-hertz
|
||||
NoahStapp
|
||||
norrishuang
|
||||
OfficialAbhinavSingh
|
||||
officialasishkumar
|
||||
OjusWiZard
|
||||
okaditya84
|
||||
omahs
|
||||
OsamaNabih
|
||||
oskarrough
|
||||
p-tirth
|
||||
Padarn
|
||||
paipeline
|
||||
ParseDark
|
||||
parshvadaftari
|
||||
Parteeksachdeva
|
||||
parthshr370
|
||||
parzival418
|
||||
Paulie-Aditya
|
||||
paurushmittal
|
||||
pc9
|
||||
Pecunia201
|
||||
peterj
|
||||
pragnyanramtha
|
||||
PranavPuranik
|
||||
PrashantDixit0
|
||||
prateekchhikara
|
||||
prathameshagrawal
|
||||
pratikgajjar
|
||||
PratikRai0101
|
||||
Prikshit7766
|
||||
Prithvi1994
|
||||
QunBB
|
||||
rafid001
|
||||
raghavtyagii
|
||||
rahulsharmavishwakarma
|
||||
rajib76
|
||||
rakheesingh
|
||||
ranjithkumar8352
|
||||
Rayhanpatel
|
||||
reachAnushaKondam
|
||||
Real5K
|
||||
Rhythm-08
|
||||
richawo
|
||||
Rishiraj2594
|
||||
RitwijParmar
|
||||
RobinALG87
|
||||
rocke2020
|
||||
rodboev
|
||||
rohitgr7
|
||||
ron-42
|
||||
roshan-shaik-ml
|
||||
rst0070
|
||||
rudra717
|
||||
rudrajmehta-mem0
|
||||
rupamoraczen
|
||||
rupeshbansal
|
||||
ryanrozich
|
||||
SaharshPatel24
|
||||
sahilyadav902
|
||||
sahithreddy05
|
||||
SakshiSrivastava2024
|
||||
SamuelDevdas
|
||||
sarkarsaurabh27
|
||||
sdht0
|
||||
seetharam-rajagopal
|
||||
sergio-toro
|
||||
SerSamgy
|
||||
shafdev
|
||||
shashank42
|
||||
ShauryaaSharma
|
||||
Sheharyar570
|
||||
shenxiangzhuang
|
||||
ShivamMenda
|
||||
shlokkhemani
|
||||
shraderdm
|
||||
shrivastavanolo
|
||||
shubhampal123
|
||||
shuoli84
|
||||
sidmohanty11
|
||||
siroa
|
||||
slobodaapl
|
||||
soapun
|
||||
soumil-rathi
|
||||
spike-spiegel-21
|
||||
srishti-git1110
|
||||
SSDWGG
|
||||
sssserrano
|
||||
subhadip001
|
||||
subhajit20
|
||||
SudoAnirudh
|
||||
sukkritsharmaofficial
|
||||
sw8fbar
|
||||
swarnaprakash
|
||||
sxu75374
|
||||
SZemse
|
||||
taranjeet
|
||||
techcontributor
|
||||
tgabi333
|
||||
theagenticguy
|
||||
thomasgtaylor
|
||||
tomasonjo
|
||||
TommyZihao
|
||||
TruptiAgrawal
|
||||
turtletongue
|
||||
Tushar-kalsi
|
||||
Ukong0324
|
||||
umran666
|
||||
utkarsh240799
|
||||
UzairNaeem3
|
||||
V-Silpin
|
||||
vatsalrathod16
|
||||
vedant381
|
||||
veeceey
|
||||
vgvoleg
|
||||
VictorECDSA
|
||||
VikramIyer125
|
||||
Vir-8
|
||||
vsatyamuralikrishna
|
||||
vuonghuuhung
|
||||
WayneCao
|
||||
whysosaket
|
||||
wobushixiaoj
|
||||
xiangpingjiang
|
||||
XiaojuCH
|
||||
xu-xiang
|
||||
xyb
|
||||
yashikabadaya
|
||||
yashs33244
|
||||
ygorth
|
||||
youneshima
|
||||
ytkimirti
|
||||
YuriyTW
|
||||
YusukeJustinNakajima
|
||||
zaiddkhan
|
||||
zegerhoogeboom
|
||||
zinyando
|
||||
Zlo7
|
||||
Zncl2222
|
||||
zzaym
|
||||
@@ -0,0 +1,60 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const gate = fs.readFileSync(path.join(__dirname, '..', 'workflows', 'pr-gate.yml'), 'utf8');
|
||||
|
||||
const rootDocsLine = gate.match(/^\s*(const rootDocs = new Set\(\['[\w.-]+'(?:, '[\w.-]+')*\]\);)\s*$/m);
|
||||
const isDocsLine = gate.match(/^\s*(const isDocs = \(\w+\) => [\w.'"()[\]\/, |&!=><+-]+;)\s*$/m);
|
||||
|
||||
assert.ok(
|
||||
rootDocsLine,
|
||||
'pr-gate.yml no longer declares rootDocs as a single-line Set of quoted filenames. ' +
|
||||
'This test evaluates that line to exercise the shipped predicate rather than a copy of it, ' +
|
||||
'and only accepts a literal shape, so widen the pattern deliberately or keep the declaration literal.',
|
||||
);
|
||||
assert.ok(
|
||||
isDocsLine,
|
||||
'pr-gate.yml no longer declares isDocs as a single-line arrow expression. ' +
|
||||
'This test evaluates that line to exercise the shipped predicate rather than a copy of it, ' +
|
||||
'and refuses anything with a statement body, so keep it an expression.',
|
||||
);
|
||||
|
||||
const isDocs = new Function(`${rootDocsLine[1]}\n${isDocsLine[1]}\nreturn isDocs;`)();
|
||||
|
||||
const exempt = (files) => files.length > 0 && files.every(isDocs);
|
||||
|
||||
const cases = [
|
||||
[['docs/a.mdx'], true],
|
||||
[['docs/platform/quickstart.mdx'], true],
|
||||
[['README.md'], true],
|
||||
[['CONTRIBUTING.md'], true],
|
||||
[['CODE_OF_CONDUCT.md'], true],
|
||||
[['SECURITY.md'], true],
|
||||
[['README.md', 'CONTRIBUTING.md', 'docs/x.mdx'], true],
|
||||
[['AGENTS.md'], false],
|
||||
[['CLAUDE.md'], false],
|
||||
[['LLM.md'], false],
|
||||
[['README.md', 'AGENTS.md'], false],
|
||||
[['README.md', 'mem0/memory/main.py'], false],
|
||||
[['skills/mem0/SKILL.md'], false],
|
||||
[['.github/AGENTS.md'], false],
|
||||
[['.github/workflows/ci.yml'], false],
|
||||
[['docs-site/index.md'], false],
|
||||
[[], false],
|
||||
];
|
||||
|
||||
let failures = 0;
|
||||
for (const [files, expected] of cases) {
|
||||
const actual = exempt(files);
|
||||
const label = files.length ? files.join(', ') : '(no files)';
|
||||
if (actual === expected) {
|
||||
console.log(`ok ${label} -> ${actual ? 'exempt' : 'gated'}`);
|
||||
} else {
|
||||
failures += 1;
|
||||
console.log(`FAIL ${label} -> ${actual ? 'exempt' : 'gated'}, expected ${expected ? 'exempt' : 'gated'}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(failures === 0 ? '\nPASS' : `\nFAIL (${failures} cases)`);
|
||||
process.exit(failures === 0 ? 0 : 1);
|
||||
@@ -0,0 +1,122 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const workflowPath = path.join(__dirname, '..', 'workflows', 'vouch-check-pr.yml');
|
||||
const workflow = fs.readFileSync(workflowPath, 'utf8');
|
||||
|
||||
const PINNED_VOUCH_SHA = 'd66fa29a64600490892131ad87597c30c91fcac4';
|
||||
|
||||
assert.ok(
|
||||
workflow.includes(`mitchellh/vouch/action/check-pr@${PINNED_VOUCH_SHA}`),
|
||||
`decide() below is a hand transcription of gh-check-pr from vouch/github.nu at ${PINNED_VOUCH_SHA} (v1.5.0). ` +
|
||||
'It reads the action, it does not run it, so on its own it agrees with itself whatever the action does. ' +
|
||||
'vouch-check-pr.yml now pins a different revision: re-read gh-check-pr there, update decide() and the ' +
|
||||
'decision table in .github/AGENTS.md to match it, then set PINNED_VOUCH_SHA to the new SHA.',
|
||||
);
|
||||
|
||||
const actionDefaults = { 'require-vouch': true, 'auto-close': false };
|
||||
|
||||
const booleanInput = (name) => {
|
||||
const match = workflow.match(new RegExp(`^\\s+${name}:\\s*"?(true|false)"?\\s*$`, 'm'));
|
||||
return match ? match[1] === 'true' : actionDefaults[name];
|
||||
};
|
||||
|
||||
const requireVouch = booleanInput('require-vouch');
|
||||
const autoClose = booleanInput('auto-close');
|
||||
|
||||
const commentedStatus = (() => {
|
||||
const match = workflow.match(/steps\.vouch\.outputs\.status == '(\w+)'/);
|
||||
assert.ok(match, 'the follow-up comment step is not keyed on a vouch status');
|
||||
return match[1];
|
||||
})();
|
||||
|
||||
const decide = (author) => {
|
||||
if (author === 'bot') return { status: 'skipped', closed: false, actionComments: false };
|
||||
if (author === 'collaborator' || author === 'vouched') {
|
||||
return { status: 'vouched', closed: false, actionComments: false };
|
||||
}
|
||||
if (author === 'denounced') {
|
||||
if (!autoClose) return { status: 'closed', closed: false, actionComments: false };
|
||||
return { status: 'closed', closed: true, actionComments: true };
|
||||
}
|
||||
if (!requireVouch) return { status: 'allowed', closed: false, actionComments: false };
|
||||
if (!autoClose) return { status: 'closed', closed: false, actionComments: false };
|
||||
return { status: 'closed', closed: true, actionComments: true };
|
||||
};
|
||||
|
||||
const outcome = (author) => {
|
||||
const result = decide(author);
|
||||
return { ...result, workflowComments: result.status === commentedStatus };
|
||||
};
|
||||
|
||||
const cases = [
|
||||
{ author: 'bot', closed: false, comments: 0 },
|
||||
{ author: 'collaborator', closed: false, comments: 0 },
|
||||
{ author: 'vouched', closed: false, comments: 0 },
|
||||
{ author: 'unvouched', closed: false, comments: 1 },
|
||||
{ author: 'denounced', closed: true, comments: 1 },
|
||||
];
|
||||
|
||||
let failures = 0;
|
||||
for (const expected of cases) {
|
||||
const actual = outcome(expected.author);
|
||||
const comments = Number(actual.actionComments) + Number(actual.workflowComments);
|
||||
try {
|
||||
assert.strictEqual(actual.closed, expected.closed, `${expected.author}: closed`);
|
||||
assert.strictEqual(comments, expected.comments, `${expected.author}: comment count`);
|
||||
console.log(`ok ${expected.author} -> ${actual.status}, closed=${actual.closed}, comments=${comments}`);
|
||||
} catch (error) {
|
||||
failures += 1;
|
||||
console.log(`FAIL ${expected.author} -> ${actual.status}, closed=${actual.closed}, comments=${comments}`);
|
||||
console.log(` ${error.message}: expected ${JSON.stringify(expected)}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nvouch@${PINNED_VOUCH_SHA.slice(0, 7)} require-vouch=${requireVouch} auto-close=${autoClose} comment-on=${commentedStatus}`);
|
||||
|
||||
const parseDenounced = (contents) => new Set(contents
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.startsWith('-'))
|
||||
.map((line) => line.slice(1).split(/\s+/)[0].split(':').pop().toLowerCase())
|
||||
.filter(Boolean));
|
||||
|
||||
const gate = fs.readFileSync(path.join(__dirname, '..', 'workflows', 'pr-gate.yml'), 'utf8');
|
||||
assert.ok(
|
||||
gate.includes(".filter((line) => line.startsWith('-'))"),
|
||||
'pr-gate.yml no longer parses the denounce list the way this test does',
|
||||
);
|
||||
|
||||
const vouched = fs.readFileSync(path.join(__dirname, '..', 'VOUCHED.td'), 'utf8');
|
||||
const denouncedNow = parseDenounced(vouched);
|
||||
const sample = parseDenounced([
|
||||
'# -notacomment is a comment line',
|
||||
'-SpamBot seeded 2026-08-12',
|
||||
'-github:OtherSpammer',
|
||||
'realcontributor',
|
||||
'',
|
||||
].join('\n'));
|
||||
|
||||
let parseFailures = 0;
|
||||
for (const [label, actual, expected] of [
|
||||
['denounce entry, with note', sample.has('spambot'), true],
|
||||
['denounce entry, platform prefixed', sample.has('otherspammer'), true],
|
||||
['comment line is not an entry', sample.has('notacomment'), false],
|
||||
['vouched entry is not denounced', sample.has('realcontributor'), false],
|
||||
['live file parses without throwing', denouncedNow instanceof Set, true],
|
||||
]) {
|
||||
try {
|
||||
assert.strictEqual(actual, expected, label);
|
||||
console.log(`ok ${label}`);
|
||||
} catch (error) {
|
||||
parseFailures += 1;
|
||||
console.log(`FAIL ${label}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`denounced in VOUCHED.td: ${denouncedNow.size}`);
|
||||
|
||||
const total = failures + parseFailures;
|
||||
console.log(total === 0 ? 'PASS' : `FAIL (${total} assertions)`);
|
||||
process.exit(total === 0 ? 0 : 1);
|
||||
@@ -0,0 +1,97 @@
|
||||
name: Agent Plugins Python Checks
|
||||
|
||||
# Python runtime, adapters, generated bundles, and portable plugin validation.
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'integrations/agent-plugin-core/**'
|
||||
- '!integrations/agent-plugin-core/typescript/**'
|
||||
- 'integrations/mem0-agent-plugin/**'
|
||||
- 'integrations/claude-code-plugin/**'
|
||||
- 'integrations/cursor-plugin/**'
|
||||
- 'integrations/codex-plugin/**'
|
||||
- 'integrations/kimi-plugin/**'
|
||||
- 'integrations/antigravity-plugin/**'
|
||||
- 'marketplace.json'
|
||||
- '.agents/plugins/marketplace.json'
|
||||
- '.claude-plugin/marketplace.json'
|
||||
- '.codex-plugin/marketplace.json'
|
||||
- '.cursor-plugin/marketplace.json'
|
||||
- '.kimi-plugin/marketplace.json'
|
||||
- '.github/workflows/agent-plugins-python-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install runtime test tooling
|
||||
if: matrix.python-version == '3.10'
|
||||
run: pip install pytest
|
||||
|
||||
- name: Install build and test tooling
|
||||
if: matrix.python-version != '3.10'
|
||||
run: pip install pytest ruff -r integrations/agent-plugin-core/requirements-dev.txt
|
||||
|
||||
- name: Check Python runtime compatibility
|
||||
run: >-
|
||||
python3 -m compileall -q
|
||||
integrations/agent-plugin-core/python
|
||||
integrations/claude-code-plugin/adapters
|
||||
integrations/cursor-plugin/hooks
|
||||
integrations/codex-plugin/hooks
|
||||
integrations/kimi-plugin/hooks
|
||||
integrations/antigravity-plugin/hooks
|
||||
|
||||
- name: Lint
|
||||
if: matrix.python-version == '3.12'
|
||||
run: >-
|
||||
python3 -m ruff check
|
||||
integrations/agent-plugin-core
|
||||
integrations/claude-code-plugin
|
||||
integrations/cursor-plugin
|
||||
integrations/codex-plugin
|
||||
integrations/kimi-plugin
|
||||
integrations/antigravity-plugin
|
||||
|
||||
- name: Verify installable plugins are current
|
||||
if: matrix.python-version == '3.12'
|
||||
run: |
|
||||
for host in claude-code cursor codex kimi antigravity; do
|
||||
python3 integrations/agent-plugin-core/build/build.py "$host" --kind native --check
|
||||
done
|
||||
python3 integrations/agent-plugin-core/build/build.py mem0-agent-plugin --kind portable --check
|
||||
|
||||
- name: Run Python 3.10 runtime tests
|
||||
if: matrix.python-version == '3.10'
|
||||
run: >-
|
||||
python3 -m pytest -q
|
||||
integrations/claude-code-plugin/tests/test_memory_core.py
|
||||
integrations/claude-code-plugin/tests/test_telemetry.py
|
||||
|
||||
- name: Run full tests
|
||||
if: matrix.python-version != '3.10'
|
||||
run: >-
|
||||
python3 -m pytest -q
|
||||
integrations/agent-plugin-core/tests
|
||||
integrations/claude-code-plugin/tests
|
||||
integrations/cursor-plugin/tests
|
||||
integrations/codex-plugin/tests
|
||||
integrations/kimi-plugin/tests
|
||||
integrations/antigravity-plugin/tests
|
||||
--ignore=integrations/claude-code-plugin/tests/integration
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Agent Plugins TypeScript Checks
|
||||
|
||||
# Shared TypeScript runtime checks. Each consuming integration keeps its own
|
||||
# build workflow, which is also triggered when this shared core changes.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'integrations/agent-plugin-core/typescript/**'
|
||||
- '.github/workflows/agent-plugins-typescript-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/agent-plugin-core/typescript/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: integrations/agent-plugin-core/typescript
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Type check
|
||||
working-directory: integrations/agent-plugin-core/typescript
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Run tests
|
||||
working-directory: integrations/agent-plugin-core/typescript
|
||||
run: pnpm test
|
||||
@@ -38,12 +38,16 @@ jobs:
|
||||
cli_python: ${{ steps.filter.outputs.cli_python }}
|
||||
cli_node: ${{ steps.filter.outputs.cli_node }}
|
||||
openclaw: ${{ steps.filter.outputs.openclaw }}
|
||||
mem0_plugin: ${{ steps.filter.outputs.mem0_plugin }}
|
||||
agent_plugins_python: ${{ steps.filter.outputs.agent_plugins_python }}
|
||||
agent_plugins_typescript: ${{ steps.filter.outputs.agent_plugins_typescript }}
|
||||
opencode_plugin: ${{ steps.filter.outputs.opencode_plugin }}
|
||||
pi_agent_plugin: ${{ steps.filter.outputs.pi_agent_plugin }}
|
||||
deepseek_plugin: ${{ steps.filter.outputs.deepseek_plugin }}
|
||||
n8n_nodes_mem0: ${{ steps.filter.outputs.n8n_nodes_mem0 }}
|
||||
zapier_mem0: ${{ steps.filter.outputs.zapier_mem0 }}
|
||||
mem0_strands: ${{ steps.filter.outputs.mem0_strands }}
|
||||
docs_llms_txt: ${{ steps.filter.outputs.docs_llms_txt }}
|
||||
github_scripts: ${{ steps.filter.outputs.github_scripts }}
|
||||
steps:
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
@@ -72,21 +76,45 @@ jobs:
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
openclaw:
|
||||
- 'integrations/openclaw/**'
|
||||
- 'integrations/agent-plugin-core/typescript/**'
|
||||
- '.github/workflows/openclaw-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
mem0_plugin:
|
||||
- 'integrations/mem0-plugin/**'
|
||||
- '!integrations/mem0-plugin/.opencode-plugin/**'
|
||||
- '.github/workflows/mem0-plugin-checks.yml'
|
||||
agent_plugins_python:
|
||||
- 'integrations/agent-plugin-core/**'
|
||||
- '!integrations/agent-plugin-core/typescript/**'
|
||||
- 'integrations/mem0-agent-plugin/**'
|
||||
- 'integrations/claude-code-plugin/**'
|
||||
- 'integrations/cursor-plugin/**'
|
||||
- 'integrations/codex-plugin/**'
|
||||
- 'integrations/kimi-plugin/**'
|
||||
- 'integrations/antigravity-plugin/**'
|
||||
- 'marketplace.json'
|
||||
- '.agents/plugins/marketplace.json'
|
||||
- '.claude-plugin/marketplace.json'
|
||||
- '.codex-plugin/marketplace.json'
|
||||
- '.cursor-plugin/marketplace.json'
|
||||
- '.kimi-plugin/marketplace.json'
|
||||
- '.github/workflows/agent-plugins-python-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
agent_plugins_typescript:
|
||||
- 'integrations/agent-plugin-core/typescript/**'
|
||||
- '.github/workflows/agent-plugins-typescript-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
opencode_plugin:
|
||||
- 'integrations/mem0-plugin/.opencode-plugin/**'
|
||||
- 'integrations/opencode-plugin/**'
|
||||
- 'integrations/agent-plugin-core/typescript/**'
|
||||
- '.github/workflows/opencode-plugin-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
pi_agent_plugin:
|
||||
- 'integrations/pi-agent-plugin/**'
|
||||
- 'integrations/agent-plugin-core/typescript/**'
|
||||
- '.github/workflows/pi-agent-plugin-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
deepseek_plugin:
|
||||
- 'integrations/deepseek-plugin/**'
|
||||
- 'integrations/agent-plugin-core/typescript/**'
|
||||
- '.github/workflows/deepseek-plugin-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
n8n_nodes_mem0:
|
||||
- 'integrations/n8n-nodes-mem0/**'
|
||||
- '.github/workflows/n8n-nodes-mem0-checks.yml'
|
||||
@@ -94,6 +122,10 @@ jobs:
|
||||
- 'integrations/zapier-mem0/**'
|
||||
- '.github/workflows/zapier-mem0-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
mem0_strands:
|
||||
- 'integrations/mem0-strands/**'
|
||||
- '.github/workflows/mem0-strands-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
docs_llms_txt:
|
||||
- 'docs/**/*.mdx'
|
||||
- 'docs/llms.txt'
|
||||
@@ -101,6 +133,13 @@ jobs:
|
||||
- 'scripts/llms-txt-ignore.txt'
|
||||
- '.github/workflows/docs-llms-txt-check.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
github_scripts:
|
||||
- '.github/scripts/**'
|
||||
- '.github/VOUCHED.td'
|
||||
- '.github/workflows/pr-gate.yml'
|
||||
- '.github/workflows/vouch-check-pr.yml'
|
||||
- '.github/workflows/issue-labeler.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
|
||||
python-sdk:
|
||||
name: Python SDK
|
||||
@@ -137,11 +176,17 @@ jobs:
|
||||
uses: ./.github/workflows/openclaw-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
mem0-plugin:
|
||||
name: Mem0 Plugin
|
||||
agent-plugins-python:
|
||||
name: Agent Plugins Python
|
||||
needs: changes
|
||||
if: needs.changes.outputs.mem0_plugin == 'true'
|
||||
uses: ./.github/workflows/mem0-plugin-checks.yml
|
||||
if: needs.changes.outputs.agent_plugins_python == 'true'
|
||||
uses: ./.github/workflows/agent-plugins-python-checks.yml
|
||||
|
||||
agent-plugins-typescript:
|
||||
name: Agent Plugins TypeScript
|
||||
needs: changes
|
||||
if: needs.changes.outputs.agent_plugins_typescript == 'true'
|
||||
uses: ./.github/workflows/agent-plugins-typescript-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
opencode-plugin:
|
||||
@@ -158,6 +203,13 @@ jobs:
|
||||
uses: ./.github/workflows/pi-agent-plugin-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
deepseek-plugin:
|
||||
name: DeepSeek Harness Plugin
|
||||
needs: changes
|
||||
if: needs.changes.outputs.deepseek_plugin == 'true'
|
||||
uses: ./.github/workflows/deepseek-plugin-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
n8n-nodes-mem0:
|
||||
name: n8n Node
|
||||
needs: changes
|
||||
@@ -170,6 +222,13 @@ jobs:
|
||||
uses: ./.github/workflows/zapier-mem0-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
mem0-strands:
|
||||
name: mem0-strands
|
||||
needs: changes
|
||||
if: needs.changes.outputs.mem0_strands == 'true'
|
||||
uses: ./.github/workflows/mem0-strands-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
docs-llms-txt:
|
||||
name: docs llms.txt
|
||||
needs: changes
|
||||
@@ -177,6 +236,24 @@ jobs:
|
||||
uses: ./.github/workflows/docs-llms-txt-check.yml
|
||||
secrets: inherit
|
||||
|
||||
github-scripts:
|
||||
name: GitHub Scripts
|
||||
needs: changes
|
||||
if: needs.changes.outputs.github_scripts == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Run .github/scripts tests
|
||||
run: |
|
||||
for test in .github/scripts/*.test.js; do
|
||||
echo "::group::$test"
|
||||
node "$test"
|
||||
echo "::endgroup::"
|
||||
done
|
||||
|
||||
gate:
|
||||
name: CI Gate
|
||||
needs:
|
||||
@@ -186,12 +263,16 @@ jobs:
|
||||
- cli-python
|
||||
- cli-node
|
||||
- openclaw
|
||||
- mem0-plugin
|
||||
- agent-plugins-python
|
||||
- agent-plugins-typescript
|
||||
- opencode-plugin
|
||||
- pi-agent-plugin
|
||||
- deepseek-plugin
|
||||
- n8n-nodes-mem0
|
||||
- zapier-mem0
|
||||
- mem0-strands
|
||||
- docs-llms-txt
|
||||
- github-scripts
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
}
|
||||
|
||||
base_version=$(git show "$BASE_SHA:pyproject.toml" 2>/dev/null | extract_version || echo "")
|
||||
head_version=$(extract_version < pyproject.toml)
|
||||
head_version=$(git show "$HEAD_SHA:pyproject.toml" | extract_version)
|
||||
|
||||
echo "Base version: ${base_version:-<unknown>}"
|
||||
echo "Head version: $head_version"
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Publish @mem0/deepseek-plugin 📦 to npm
|
||||
|
||||
# Dispatched by release.yml (Release Router) when a release tagged
|
||||
# deepseek-plugin-v* is published. Can also be dispatched manually to re-publish
|
||||
# a tag.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. deepseek-plugin-v0.1.1)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Publish under the version preid dist-tag instead of latest'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish @mem0/deepseek-plugin 📦 to npm
|
||||
if: startsWith(inputs.tag, 'deepseek-plugin-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/deepseek-plugin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/deepseek-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ inputs.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
npx npm@latest publish --provenance --access public
|
||||
fi
|
||||
@@ -0,0 +1,89 @@
|
||||
name: deepseek-plugin checks
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'integrations/deepseek-plugin/**'
|
||||
- 'integrations/agent-plugin-core/typescript/**'
|
||||
- '.github/workflows/deepseek-plugin-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/deepseek-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/deepseek-plugin && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Type check
|
||||
run: cd integrations/deepseek-plugin && pnpm exec tsc --noEmit
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20, 22]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/deepseek-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/deepseek-plugin && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
run: cd integrations/deepseek-plugin && pnpm exec vitest run
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/deepseek-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/deepseek-plugin && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: cd integrations/deepseek-plugin && pnpm build
|
||||
|
||||
- name: Verify package artifact
|
||||
run: python3 integrations/agent-plugin-core/conformance/artifacts.py deepseek
|
||||
@@ -1,58 +0,0 @@
|
||||
name: Mem0 Plugin Checks
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
#
|
||||
# Covers the Python plugin (scripts/ + tests/). The nested .opencode-plugin/
|
||||
# is a separate package with its own workflow (opencode-plugin-checks.yml).
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'integrations/mem0-plugin/**'
|
||||
- '!integrations/mem0-plugin/.opencode-plugin/**'
|
||||
- '.github/workflows/mem0-plugin-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: integrations/mem0-plugin
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install pytest
|
||||
|
||||
- name: Verify hook entry points are executable
|
||||
working-directory: integrations/mem0-plugin
|
||||
run: |
|
||||
missing=$(find scripts -name '*.sh' ! -name '_*' ! -perm -u+x -print)
|
||||
if [ -n "$missing" ]; then
|
||||
echo "Hook entry points must be executable:"
|
||||
echo "$missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check hook manifests are valid JSON
|
||||
working-directory: integrations/mem0-plugin
|
||||
run: |
|
||||
for f in plugin.json mcp_config.json hooks.json hooks/*.json; do
|
||||
jq empty "$f" || (echo "Invalid JSON: $f" && exit 1)
|
||||
done
|
||||
|
||||
- name: Run tests
|
||||
working-directory: integrations/mem0-plugin
|
||||
run: pytest -q
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Publish mem0-strands 🐍 distribution 📦 to PyPI
|
||||
|
||||
# Dispatched by release.yml (Release Router) when a release tagged
|
||||
# mem0-strands-v* is published. Can also be dispatched manually to re-publish
|
||||
# a tag. Publishing uses PyPI Trusted Publishing (OIDC), so no API token is
|
||||
# stored; the `mem0-strands` PyPI project must have a trusted publisher
|
||||
# configured for mem0ai/mem0 + this workflow.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. mem0-strands-v0.1.0)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Unused for PyPI (pre-releases are expressed in the version itself); accepted for router uniformity'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish mem0-strands 📦 to PyPI
|
||||
if: startsWith(inputs.tag, 'mem0-strands-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/mem0-strands/python
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Hatch
|
||||
run: pip install hatch
|
||||
|
||||
- name: Build a binary wheel and a source tarball
|
||||
run: hatch build --clean
|
||||
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: integrations/mem0-strands/python/dist/
|
||||
@@ -0,0 +1,82 @@
|
||||
name: mem0-strands CI
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'integrations/mem0-strands/**'
|
||||
- '.github/workflows/mem0-strands-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install dev dependencies
|
||||
working-directory: integrations/mem0-strands/python
|
||||
run: pip install -e ".[dev]"
|
||||
|
||||
- name: Lint with ruff
|
||||
working-directory: integrations/mem0-strands/python
|
||||
run: ruff check .
|
||||
|
||||
- name: Check formatting
|
||||
working-directory: integrations/mem0-strands/python
|
||||
run: ruff format --check .
|
||||
|
||||
- name: Type-check with mypy
|
||||
working-directory: integrations/mem0-strands/python
|
||||
run: mypy src
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dev dependencies
|
||||
working-directory: integrations/mem0-strands/python
|
||||
run: pip install -e ".[dev]"
|
||||
|
||||
- name: Run tests
|
||||
working-directory: integrations/mem0-strands/python
|
||||
run: pytest
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install Hatch
|
||||
run: pip install hatch
|
||||
|
||||
- name: Build
|
||||
working-directory: integrations/mem0-strands/python
|
||||
run: hatch build --clean
|
||||
|
||||
- name: Verify dist output
|
||||
run: |
|
||||
ls integrations/mem0-strands/python/dist/*.whl || (echo "Wheel file missing" && exit 1)
|
||||
ls integrations/mem0-strands/python/dist/*.tar.gz || (echo "Source dist missing" && exit 1)
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'integrations/openclaw/**'
|
||||
- 'integrations/agent-plugin-core/typescript/**'
|
||||
- '.github/workflows/openclaw-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
@@ -93,7 +94,5 @@ jobs:
|
||||
- name: Build
|
||||
run: cd integrations/openclaw && pnpm build
|
||||
|
||||
- name: Verify dist output exists
|
||||
run: |
|
||||
test -f integrations/openclaw/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
test -f integrations/openclaw/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
|
||||
- name: Verify package artifact
|
||||
run: python3 integrations/agent-plugin-core/conformance/artifacts.py openclaw
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/mem0-plugin/.opencode-plugin
|
||||
working-directory: integrations/opencode-plugin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
@@ -7,7 +7,8 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'integrations/mem0-plugin/.opencode-plugin/**'
|
||||
- 'integrations/opencode-plugin/**'
|
||||
- 'integrations/agent-plugin-core/typescript/**'
|
||||
- '.github/workflows/opencode-plugin-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
@@ -16,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/mem0-plugin/.opencode-plugin
|
||||
working-directory: integrations/opencode-plugin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -34,6 +35,6 @@ jobs:
|
||||
- name: Build
|
||||
run: bun run build
|
||||
|
||||
- name: Verify dist output exists
|
||||
run: |
|
||||
test -f dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
- name: Verify package artifact
|
||||
working-directory: .
|
||||
run: python3 integrations/agent-plugin-core/conformance/artifacts.py opencode
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'integrations/pi-agent-plugin/**'
|
||||
- 'integrations/agent-plugin-core/typescript/**'
|
||||
- '.github/workflows/pi-agent-plugin-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
@@ -84,9 +85,5 @@ jobs:
|
||||
- name: Build
|
||||
run: cd integrations/pi-agent-plugin && pnpm build
|
||||
|
||||
- name: Verify dist output exists
|
||||
run: |
|
||||
test -f integrations/pi-agent-plugin/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
test -f integrations/pi-agent-plugin/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
|
||||
test -f integrations/pi-agent-plugin/dist/entry.js || (echo "Build output missing: dist/entry.js" && exit 1)
|
||||
test -f integrations/pi-agent-plugin/dist/entry.d.ts || (echo "Build output missing: dist/entry.d.ts" && exit 1)
|
||||
- name: Verify package artifact
|
||||
run: python3 integrations/agent-plugin-core/conformance/artifacts.py pi-agent
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
name: PR Gate
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, ready_for_review, edited]
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
concurrency:
|
||||
group: pr-gate-${{ github.event_name }}-${{ github.event.action }}-${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request_target' }}
|
||||
|
||||
env:
|
||||
GATE_EFFECTIVE_FROM: '2026-08-12T00:00:00Z'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: read
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
if: >-
|
||||
github.event_name == 'pull_request_target' &&
|
||||
github.event.action != 'edited' &&
|
||||
github.event.pull_request.draft == false &&
|
||||
github.event.pull_request.user.type != 'Bot' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
!contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
const effectiveFrom = process.env.GATE_EFFECTIVE_FROM;
|
||||
if (effectiveFrom && Date.parse(pr.created_at) < Date.parse(effectiveFrom)) {
|
||||
core.info(`Opened ${pr.created_at}, before the gate took effect ${effectiveFrom}. Skipped.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: current } = await github.rest.pulls.get({
|
||||
owner, repo, pull_number: pr.number,
|
||||
});
|
||||
if (current.state !== 'open') {
|
||||
core.info(`#${pr.number} is already ${current.state}. Skipped.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner, repo, pull_number: pr.number, per_page: 100,
|
||||
});
|
||||
const rootDocs = new Set(['README.md', 'CONTRIBUTING.md', 'CODE_OF_CONDUCT.md', 'SECURITY.md']);
|
||||
const isDocs = (filename) => filename.startsWith('docs/') || rootDocs.has(filename);
|
||||
if (files.length > 0 && files.every((file) => isDocs(file.filename))) {
|
||||
core.info('Docs-only PR, gate skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
const { repository } = await github.graphql(
|
||||
`query ($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $number) {
|
||||
closingIssuesReferences(first: 20) {
|
||||
nodes { number labels(first: 50) { nodes { name } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{ owner, repo, number: pr.number },
|
||||
);
|
||||
|
||||
const accepted = repository.pullRequest.closingIssuesReferences.nodes
|
||||
.filter((issue) => issue.labels.nodes.some((label) => label.name === 'accepted'))
|
||||
.map((issue) => issue.number);
|
||||
|
||||
if (accepted.length > 0) {
|
||||
core.info(`Accepted issue linked: #${accepted.join(', #')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const body = [
|
||||
'<!-- pr-gate -->',
|
||||
'Thanks for taking the time to open this.',
|
||||
'',
|
||||
'We only review pull requests that fix an issue we have already agreed to take on, so this one is closed for now.',
|
||||
'**Closed does not mean rejected.** It means it is not in the queue yet, and reopening takes about a minute.',
|
||||
'',
|
||||
'To get it reviewed:',
|
||||
'',
|
||||
'1. Make sure an issue describes the problem, with the version you are on, a runnable reproduction, and the real output or traceback you saw.',
|
||||
'2. Link it from this pull request description with `Closes #<number>`.',
|
||||
'3. Ask a maintainer to label that issue `accepted`. This pull request reopens by itself when they do.',
|
||||
'',
|
||||
'Issue already labeled `accepted`? Just add `Closes #<number>` to the description. That reopens this too.',
|
||||
'',
|
||||
'Documentation-only changes skip this gate entirely.',
|
||||
'',
|
||||
'See [CONTRIBUTING.md](https://github.com/mem0ai/mem0/blob/main/CONTRIBUTING.md) for the full policy.',
|
||||
].join('\n');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: pr.number, body,
|
||||
});
|
||||
await github.rest.pulls.update({
|
||||
owner, repo, pull_number: pr.number, state: 'closed',
|
||||
});
|
||||
core.info(`Closed #${pr.number}: no accepted issue linked`);
|
||||
|
||||
reopen:
|
||||
if: >-
|
||||
(github.event_name == 'issues' && github.event.label.name == 'accepted') ||
|
||||
(github.event.action == 'edited' && github.event.pull_request.state == 'closed')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const marker = '<!-- pr-gate -->';
|
||||
|
||||
const denounced = await (async () => {
|
||||
try {
|
||||
const { data } = await github.rest.repos.getContent({
|
||||
owner, repo, path: '.github/VOUCHED.td',
|
||||
ref: context.payload.repository.default_branch,
|
||||
});
|
||||
return new Set(Buffer.from(data.content, 'base64').toString('utf8')
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.startsWith('-'))
|
||||
.map((line) => line.slice(1).split(/\s+/)[0].split(':').pop().toLowerCase())
|
||||
.filter(Boolean));
|
||||
} catch (error) {
|
||||
core.warning(`Could not read VOUCHED.td, treating nobody as denounced: ${error.message}`);
|
||||
return new Set();
|
||||
}
|
||||
})();
|
||||
|
||||
const isReopenable = async (number) => {
|
||||
const { repository } = await github.graphql(
|
||||
`query ($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $number) {
|
||||
state
|
||||
author { login }
|
||||
closingIssuesReferences(first: 20) {
|
||||
nodes { labels(first: 50) { nodes { name } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{ owner, repo, number },
|
||||
);
|
||||
const pullRequest = repository.pullRequest;
|
||||
if (denounced.has(pullRequest.author?.login?.toLowerCase())) {
|
||||
core.info(`#${number} is from a denounced author. Vouch outranks this gate.`);
|
||||
return false;
|
||||
}
|
||||
return pullRequest.state === 'CLOSED' &&
|
||||
pullRequest.closingIssuesReferences.nodes.some((issue) =>
|
||||
issue.labels.nodes.some((label) => label.name === 'accepted'));
|
||||
};
|
||||
|
||||
let candidates;
|
||||
if (context.eventName === 'issues') {
|
||||
const { repository } = await github.graphql(
|
||||
`query ($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
issue(number: $number) {
|
||||
closedByPullRequestsReferences(first: 20, includeClosedPrs: true) {
|
||||
nodes { number }
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{ owner, repo, number: context.payload.issue.number },
|
||||
);
|
||||
candidates = repository.issue.closedByPullRequestsReferences.nodes.map((pr) => pr.number);
|
||||
} else {
|
||||
candidates = [context.payload.pull_request.number];
|
||||
}
|
||||
|
||||
for (const number of candidates) {
|
||||
if (!(await isReopenable(number))) {
|
||||
core.info(`#${number} is not a closed pull request linking an accepted issue. Skipped.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner, repo, issue_number: number, per_page: 100,
|
||||
});
|
||||
if (!comments.some((comment) => comment.body?.startsWith(marker))) {
|
||||
core.info(`#${number} was not closed by this gate. Left alone.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.pulls.update({
|
||||
owner, repo, pull_number: number, state: 'open',
|
||||
});
|
||||
} catch (error) {
|
||||
core.warning(`Could not reopen #${number}: ${error.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: number,
|
||||
body: 'An `accepted` issue is linked now, so this is open again and ready for review.',
|
||||
});
|
||||
core.info(`Reopened #${number}`);
|
||||
}
|
||||
@@ -45,7 +45,9 @@ jobs:
|
||||
openclaw-v*) workflow="openclaw-cd.yml" ;;
|
||||
opencode-v*) workflow="opencode-plugin-cd.yml" ;;
|
||||
pi-agent-v*) workflow="pi-agent-plugin-cd.yml" ;;
|
||||
deepseek-plugin-v*) workflow="deepseek-plugin-cd.yml" ;;
|
||||
n8n-nodes-mem0-v*) workflow="n8n-nodes-mem0-cd.yml" ;;
|
||||
mem0-strands-v*) workflow="mem0-strands-cd.yml" ;;
|
||||
v*) workflow="cd.yml" ;;
|
||||
*)
|
||||
echo "::error::Release tag '$TAG' does not match any known package prefix — nothing will be published. See the tag prefix table in AGENTS.md."
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
|
||||
base_version=$(git show "$BASE_SHA:mem0-ts/package.json" 2>/dev/null | jq -r .version || echo "")
|
||||
head_version=$(jq -r .version mem0-ts/package.json)
|
||||
head_version=$(git show "$HEAD_SHA:mem0-ts/package.json" | jq -r .version)
|
||||
|
||||
echo "Base version: ${base_version:-<unknown>}"
|
||||
echo "Head version: $head_version"
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Vouch - Check PR
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
concurrency:
|
||||
group: vouch-check-pr-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check:
|
||||
if: >-
|
||||
github.event.pull_request.user.type != 'Bot' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
!contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: mitchellh/vouch/action/check-pr@d66fa29a64600490892131ad87597c30c91fcac4 # v1.5.0
|
||||
id: vouch
|
||||
with:
|
||||
pr-number: ${{ github.event.pull_request.number }}
|
||||
require-vouch: false
|
||||
auto-close: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- if: steps.vouch.outputs.status == 'allowed'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const pr = context.payload.pull_request;
|
||||
const marker = '<!-- vouch-check -->';
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner, repo, issue_number: pr.number, per_page: 100,
|
||||
});
|
||||
if (comments.some((comment) => comment.body?.startsWith(marker))) {
|
||||
core.info('Vouch comment already posted, skipped.');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = [
|
||||
marker,
|
||||
`Hi @${context.payload.pull_request.user.login}, thanks for opening this pull request.`,
|
||||
'',
|
||||
"This is just a soft check: you are not yet in this repo's vouched contributor list (`.github/VOUCHED.td`). Nothing is blocked and there is nothing you need to do.",
|
||||
'',
|
||||
`A maintainer can vouch for you by commenting \`!vouch @${context.payload.pull_request.user.login}\` on any issue.`,
|
||||
].join('\n');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: pr.number, body,
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Vouch - Manage by Issue
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
concurrency:
|
||||
group: vouch-manage
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
manage:
|
||||
if: contains(github.event.comment.body, '!vouch') || contains(github.event.comment.body, '!denounce') || contains(github.event.comment.body, '!unvouch')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v3
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.VOUCH_APP_ID }}
|
||||
private-key: ${{ secrets.VOUCH_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- uses: mitchellh/vouch/action/manage-by-issue@d66fa29a64600490892131ad87597c30c91fcac4 # v1.5.0
|
||||
with:
|
||||
repo: ${{ github.repository }}
|
||||
issue-id: ${{ github.event.issue.number }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
vouch-keyword: "!vouch"
|
||||
denounce-keyword: "!denounce"
|
||||
unvouch-keyword: "!unvouch"
|
||||
pull-request: "true"
|
||||
merge-immediately: "false"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
Reference in New Issue
Block a user