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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/clawsweeper-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ jobs:
COMMENT_BODY: ${{ github.event.comment.body }}
run: |
set -euo pipefail
if [[ "$COMMENT_BODY" == *"<!-- clawsweeper-maintainer-opt-in:"* ]]; then
echo "is_command=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if grep -Eiq '(^|[[:space:]])@(clawsweeper|openclaw-clawsweeper)\b(\[bot\])?|(^|[[:space:]])/(clawsweeper|review|autoclose|auto([[:space:]]+|-)?merge)\b' <<< "$COMMENT_BODY"; then
echo "is_command=true" >> "$GITHUB_OUTPUT"
else
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Ignore maintainer opt-in reminders in the comment prefilter so quoted commands do not queue another dispatch. Thanks @SebTardif (#4).

## 1.0.1 — 2026-10-16

**Highlights:** Keep comment commands running through metadata edits and ignored bot labels, and update the dispatch action with its security-patched HTTP dependency.
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ node --test --test-concurrency=2 tests/*.test.mjs
Comment commands have separate concurrency groups per comment, so issue or pull
request edits cannot cancel them. Edits to the same comment still supersede its
earlier dispatch; metadata edits continue to supersede earlier metadata runs.
Ignored bot label events also use a separate group.
Ignored bot label events also use a separate group. Marked maintainer opt-in
reminders are ignored even when they quote a command; ordinary comment commands
continue to use the shared router's existing command set.

## Deploy

Expand Down
85 changes: 85 additions & 0 deletions tests/comment-prefilter.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';

const workflow = readFileSync(new URL('../.github/workflows/clawsweeper-dispatch.yml', import.meta.url), 'utf8');

function prefilterScript(source) {
const match = source.match(/- name: Pre-filter ClawSweeper comment[\s\S]*?\n {8}run: \|\n([\s\S]*?)\n {6}- name:/);
assert.ok(match, 'prefilter step must exist');
return match[1].replace(/^ {10}/gm, '');
}

function classify(body) {
const root = mkdtempSync(join(tmpdir(), 'comment-prefilter-'));
const output = join(root, 'github-output');
try {
writeFileSync(output, '');
const result = spawnSync('bash', ['-e', '-c', prefilterScript(workflow)], {
encoding: 'utf8',
env: {
PATH: process.env.PATH,
COMMENT_BODY: body,
GITHUB_OUTPUT: output,
},
});
assert.equal(result.status, 0, result.stderr);
return readFileSync(output, 'utf8').trim();
} finally {
rmSync(root, { recursive: true, force: true });
}
}

const accepted = [
'@clawsweeper',
'@openclaw-clawsweeper[bot]',
'/clawsweeper',
'/clawsweeper re-review',
'/review',
'/automerge',
'/auto merge',
'/auto-merge',
'/autoclose',
];

const rejected = [
'/re-review',
'/rerun review',
'/stop',
'/explain',
'/fix',
'/status',
'/approve',
'/build',
'/implement',
];

for (const command of accepted) {
test(`${command} is admitted by the prefilter`, () => {
assert.equal(classify(`please ${command}\n`), 'is_command=true');
});
}

for (const command of rejected) {
test(`${command} is not queued`, () => {
assert.equal(classify(`please ${command}\n`), 'is_command=false');
});
}

test('an opt-in reminder that also contains a command does not route', () => {
const body = [
'<!-- clawsweeper-maintainer-opt-in:opened item=42 -->',
'ClawSweeper reviews are opt-in for maintainers. If a review is required, finish the work and validation first, then comment `@clawsweeper review`.',
'/review',
].join('\n');
assert.equal(classify(body), 'is_command=false');
});

test('ordinary comments and lookalike words do not route', () => {
assert.equal(classify('thanks for the patch\n'), 'is_command=false');
assert.equal(classify('please /preview the page\n'), 'is_command=false');
assert.equal(classify('please /stopwatch the build\n'), 'is_command=false');
});
Loading