From 45b25f954eee4fb102ab2e1318b7e0c3a045b438 Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Tue, 30 Jun 2026 18:36:59 +0300 Subject: [PATCH 1/2] feat(linter): warn on social action approval gaps --- README.md | 5 +++-- SPEC.md | 4 ++-- src/linter.ts | 9 +++++++++ tests/linter.test.ts | 6 ++++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 92edea5..e29ccf9 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The agent skill ecosystem is exploding — thousands of skills for OpenClaw, Cla - **SKILL.md Linting** — validates required fields (name, description, location, schema-version), detects placeholder text and broken links - **Schema Validation** — validates frontmatter against OpenClaw, HCS-26, and generic skill schemas - **Structure Checks** — verifies README, LICENSE, SKILL.md, examples/, and scans for leaked secrets -- **Registry Compatibility** — validates against ClaweHub / HCS-26 registry publish requirements +- **Registry Compatibility** — validates against ClawHub / HCS-26 registry publish requirements - **Multi-Ecosystem** — auto-detects OpenClaw, Claude Code, Codex, or Gemini CLI from repo structure - **Configurable Fail Policy** — `errors` / `warnings` / `none` - **PR Comment + Job Summary** — detailed breakdown in GitHub Actions @@ -123,6 +123,7 @@ Overall: PASS | `name-placeholder` | Error | Name contains TODO/FIXME/TBD | | `description-placeholder` | Error | Description contains placeholder text | | `body-placeholder` | Warning | Body contains TODO/FIXME | +| `action-approval-boundary-missing` | Warning | Social or browser action skill lacks explicit approval or no-submit boundary | | `tags-too-many` | Warning | More than 10 tags | ## Structure Checks @@ -136,7 +137,7 @@ Overall: PASS | `secret-detected` | Error | Potential secret in tracked files | | `env-file-present` | Warning | .env file present | -## Registry Checks (ClaweHub / HCS-26) +## Registry Checks (ClawHub / HCS-26) | Check | Level | Description | |-------|-------|-------------| diff --git a/SPEC.md b/SPEC.md index 82a6306..31c238c 100644 --- a/SPEC.md +++ b/SPEC.md @@ -12,7 +12,7 @@ The agent skill ecosystem is exploding (antigravity-awesome-skills: 26k★, awes 3. **Dependency Check** — verify referenced tools, models, and external resources are reachable 4. **Dry-Run Test** — invoke skill in sandbox mode, verify it produces valid output structure 5. **Publish Readiness** — check all required files exist (SKILL.md, README, LICENSE, examples/) -6. **Registry Compatibility** — validate against ClaweHub / HCS-26 registry publish requirements +6. **Registry Compatibility** — validate against ClawHub / HCS-26 registry publish requirements 7. **CI Report** — GitHub Actions job summary + PR comment with lint/test breakdown 8. **Fail Modes** — configurable: fail on errors, warnings, or schema drift @@ -39,7 +39,7 @@ The agent skill ecosystem is exploding (antigravity-awesome-skills: 26k★, awes - examples/ directory with at least one example - No secrets in tracked files (.env, API keys pattern scan) -### Registry Checks (ClaweHub / HCS-26) +### Registry Checks (ClawHub / HCS-26) - skill-id unique format: org/skill-name - category from allowed list - tags: minimum 3, maximum 10 diff --git a/src/linter.ts b/src/linter.ts index c4fe17c..8f71eba 100644 --- a/src/linter.ts +++ b/src/linter.ts @@ -25,6 +25,10 @@ const SEMVER_RE = /^\d+\.\d+(\.\d+)?$/; const NAME_VALID_RE = /^[a-zA-Z0-9][a-zA-Z0-9 _\-.:()]+$/; +const SOCIAL_ACTION_RE = /\b(browser|dm|follow|like|message|post|publish|quote|reply|repost|schedule|send|social|tweet|unfollow|x\.com)\b/i; + +const APPROVAL_BOUNDARY_RE = /\b(approval|approve|confirm|confirmation|draft-only|human review|manual review|never publish|never submit|preview-only|stop before|user review|without publishing|without submitting)\b/i; + export function lintSkillMd(content: string, filePath: string = 'SKILL.md'): LintResult { const issues: LintIssue[] = []; const addIssue = (level: LintIssue['level'], rule: string, message: string) => { @@ -108,6 +112,11 @@ export function lintSkillMd(content: string, filePath: string = 'SKILL.md'): Lin addIssue('warning', 'body-placeholder', 'SKILL.md body appears to contain placeholder text (TODO/FIXME/TBD)'); } + const actionText = [description, body].filter(Boolean).join('\n'); + if (SOCIAL_ACTION_RE.test(actionText) && !APPROVAL_BOUNDARY_RE.test(actionText)) { + addIssue('warning', 'action-approval-boundary-missing', 'Skill appears to drive social or browser actions but does not mention an explicit approval or no-submit boundary'); + } + // Check for broken-looking local links const links = extractBodyLinks(content); for (const link of links) { diff --git a/tests/linter.test.ts b/tests/linter.test.ts index 2250d87..32d7e20 100644 --- a/tests/linter.test.ts +++ b/tests/linter.test.ts @@ -102,6 +102,12 @@ describe('lintSkillMd', () => { expect(result.infos.some(i => i.rule === 'local-link')).toBe(true); }); + it('warns when social action skills do not include an approval boundary', () => { + const content = `---\nname: X Reply Writer\ndescription: Draft and publish X replies from browser context for a social account.\nlocation: ./SKILL.md\nschema-version: 1.0\n---\n# X Reply Writer\n\nUse browser context to write replies on x.com.\n`; + const result = lintSkillMd(content); + expect(result.warnings.some(w => w.rule === 'action-approval-boundary-missing')).toBe(true); + }); + it('returns location in issue', () => { const result = lintSkillMd('# no frontmatter', '/path/SKILL.md'); expect(result.issues[0].location).toBe('/path/SKILL.md'); From e8f86d2ef9386a7a8bb79a1cc44ef10d326844e0 Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Tue, 30 Jun 2026 22:23:14 +0300 Subject: [PATCH 2/2] fix: flag negated social approval boundaries --- dist/linter.js | 8 ++++++++ src/linter.ts | 7 ++++++- tests/linter.test.ts | 6 ++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/dist/linter.js b/dist/linter.js index 9ed5137..d277f38 100644 --- a/dist/linter.js +++ b/dist/linter.js @@ -9,6 +9,9 @@ const PLACEHOLDER_PATTERNS = [ ]; const SEMVER_RE = /^\d+\.\d+(\.\d+)?$/; const NAME_VALID_RE = /^[a-zA-Z0-9][a-zA-Z0-9 _\-.:()]+$/; +const SOCIAL_ACTION_RE = /\b(browser|dm|follow|like|message|post|publish|quote|reply|repost|schedule|send|social|tweet|unfollow|x\.com)\b/i; +const APPROVAL_BOUNDARY_RE = /\b(approval|approve|confirm|confirmation|draft-only|human review|manual review|never publish|never submit|preview-only|stop before|user review|without publishing|without submitting)\b/i; +const NEGATED_APPROVAL_BOUNDARY_RE = /\b(without|no|never|do not|don't|cannot|can't|skip|bypass)\s+(?:user\s+|human\s+|manual\s+|explicit\s+)?(approval|approve|confirmation|confirm|review|consent|permission)\b/i; function lintSkillMd(content, filePath = 'SKILL.md') { const issues = []; const addIssue = (level, rule, message) => { @@ -88,6 +91,11 @@ function lintSkillMd(content, filePath = 'SKILL.md') { if (PLACEHOLDER_PATTERNS.some(p => p.test(body))) { addIssue('warning', 'body-placeholder', 'SKILL.md body appears to contain placeholder text (TODO/FIXME/TBD)'); } + const actionText = [description, body].filter(Boolean).join('\n'); + if (SOCIAL_ACTION_RE.test(actionText) + && (!APPROVAL_BOUNDARY_RE.test(actionText) || NEGATED_APPROVAL_BOUNDARY_RE.test(actionText))) { + addIssue('warning', 'action-approval-boundary-missing', 'Skill appears to drive social or browser actions but does not mention an explicit approval or no-submit boundary'); + } // Check for broken-looking local links const links = (0, detector_1.extractBodyLinks)(content); for (const link of links) { diff --git a/src/linter.ts b/src/linter.ts index 8f71eba..844f126 100644 --- a/src/linter.ts +++ b/src/linter.ts @@ -29,6 +29,8 @@ const SOCIAL_ACTION_RE = /\b(browser|dm|follow|like|message|post|publish|quote|r const APPROVAL_BOUNDARY_RE = /\b(approval|approve|confirm|confirmation|draft-only|human review|manual review|never publish|never submit|preview-only|stop before|user review|without publishing|without submitting)\b/i; +const NEGATED_APPROVAL_BOUNDARY_RE = /\b(without|no|never|do not|don't|cannot|can't|skip|bypass)\s+(?:user\s+|human\s+|manual\s+|explicit\s+)?(approval|approve|confirmation|confirm|review|consent|permission)\b/i; + export function lintSkillMd(content: string, filePath: string = 'SKILL.md'): LintResult { const issues: LintIssue[] = []; const addIssue = (level: LintIssue['level'], rule: string, message: string) => { @@ -113,7 +115,10 @@ export function lintSkillMd(content: string, filePath: string = 'SKILL.md'): Lin } const actionText = [description, body].filter(Boolean).join('\n'); - if (SOCIAL_ACTION_RE.test(actionText) && !APPROVAL_BOUNDARY_RE.test(actionText)) { + if ( + SOCIAL_ACTION_RE.test(actionText) + && (!APPROVAL_BOUNDARY_RE.test(actionText) || NEGATED_APPROVAL_BOUNDARY_RE.test(actionText)) + ) { addIssue('warning', 'action-approval-boundary-missing', 'Skill appears to drive social or browser actions but does not mention an explicit approval or no-submit boundary'); } diff --git a/tests/linter.test.ts b/tests/linter.test.ts index 32d7e20..b7c88c5 100644 --- a/tests/linter.test.ts +++ b/tests/linter.test.ts @@ -108,6 +108,12 @@ describe('lintSkillMd', () => { expect(result.warnings.some(w => w.rule === 'action-approval-boundary-missing')).toBe(true); }); + it('warns when social action approval wording is negated', () => { + const content = `---\nname: X Publisher\ndescription: Publish X posts without approval from the user.\nlocation: ./SKILL.md\nschema-version: 1.0\n---\n# X Publisher\n\nSend social posts without human approval.\n`; + const result = lintSkillMd(content); + expect(result.warnings.some(w => w.rule === 'action-approval-boundary-missing')).toBe(true); + }); + it('returns location in issue', () => { const result = lintSkillMd('# no frontmatter', '/path/SKILL.md'); expect(result.issues[0].location).toBe('/path/SKILL.md');