diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..bb61fd0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,49 @@ +name: Bug report +description: Report a workflow, provider, harness, integration, or user-config bug. +title: "[Bug]: " +labels: + - bug +body: + - type: dropdown + id: cause_area + attributes: + label: Where does the issue appear to come from? + options: + - Workflow/process + - Provider behavior + - Harness or integration + - User configuration + - Unsure + validations: + required: true + - type: textarea + id: summary + attributes: + label: Summary + description: What happened, and what did you expect? + validations: + required: true + - type: textarea + id: steps + attributes: + label: Reproduction steps + description: Include commands, target provider, and relevant file paths. + placeholder: | + 1. Run ... + 2. Observe ... + validations: + required: true + - type: textarea + id: evidence + attributes: + label: Evidence + description: Paste logs, transcripts, screenshots, or minimal examples. Remove private data first. + validations: + required: true + - type: checkboxes + id: safety + attributes: + label: Safety check + options: + - label: I removed credentials, private paths, chat identifiers, and private project data. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..8a29019 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Security report + url: https://github.com/damienen/basd-coding-dispatch/security/advisories/new + about: Report sensitive vulnerabilities privately. diff --git a/.github/ISSUE_TEMPLATE/docs_install_issue.yml b/.github/ISSUE_TEMPLATE/docs_install_issue.yml new file mode 100644 index 0000000..bd3cb50 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/docs_install_issue.yml @@ -0,0 +1,51 @@ +name: Docs or install issue +description: Report unclear docs, scaffold problems, or install confusion. +title: "[Docs/install]: " +labels: + - documentation +body: + - type: dropdown + id: target + attributes: + label: Target + options: + - Hermes + - OpenClaw + - Codex CLI + - Claude Code + - OpenCode + - Cursor + - Generic agent + - Repository docs + validations: + required: true + - type: textarea + id: confusion + attributes: + label: What was confusing or broken? + validations: + required: true + - type: textarea + id: attempted + attributes: + label: What did you try? + description: Include commands and outputs if relevant. + validations: + required: true + - type: input + id: install_command + attributes: + label: Install command + description: Example: npx basd-coding-dispatch init --target openclaw --dir ~/openclaw-workspace + - type: input + id: install_target_path + attributes: + label: Target/path + description: Include the target and destination path, with private home/project names redacted if needed. + - type: checkboxes + id: safety + attributes: + label: Safety check + options: + - label: I removed credentials, private paths, chat identifiers, and private project data. + required: true diff --git a/.github/ISSUE_TEMPLATE/provider_adapter_request.yml b/.github/ISSUE_TEMPLATE/provider_adapter_request.yml new file mode 100644 index 0000000..285e01c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/provider_adapter_request.yml @@ -0,0 +1,51 @@ +name: Provider adapter request +description: Request or document support for a provider, harness, plugin, or extension mechanism. +title: "[Provider adapter]: " +labels: + - provider-adapter +body: + - type: input + id: target_tool + attributes: + label: Target tool + description: Name and version of the provider, harness, editor, or agent tool. + validations: + required: true + - type: textarea + id: mechanism + attributes: + label: Plugin or extension mechanism + description: Describe the native mechanism, manifest format, install directory, or manual instruction path. + validations: + required: true + - type: textarea + id: manual_attempt + attributes: + label: Manual install attempt + description: What did you try with the current file-based scaffold? + validations: + required: true + - type: textarea + id: validation_evidence + attributes: + label: Validation evidence + description: Include commands, transcripts, screenshots, or a minimal fixture showing the adapter works. + validations: + required: true + - type: dropdown + id: requested_status + attributes: + label: Requested status label + options: + - experimental + - tested + - planned + validations: + required: true + - type: checkboxes + id: safety + attributes: + label: Safety check + options: + - label: I removed credentials, private paths, chat identifiers, and private project data. + required: true diff --git a/.github/ISSUE_TEMPLATE/workflow_improvement.yml b/.github/ISSUE_TEMPLATE/workflow_improvement.yml new file mode 100644 index 0000000..00e0a74 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/workflow_improvement.yml @@ -0,0 +1,40 @@ +name: Workflow improvement +description: Suggest a gate, review, verification, or dispatch-process improvement. +title: "[Workflow]: " +labels: + - workflow +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What failure mode does this improvement prevent? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposal + description: Describe the process change and where it belongs. + validations: + required: true + - type: dropdown + id: location + attributes: + label: Best location + options: + - Core skill + - References + - Integration docs + - Examples + - CLI or validation + - Unsure + validations: + required: true + - type: textarea + id: evidence + attributes: + label: Evidence or example + description: Add a short transcript, example, or failure case. + validations: + required: false diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..89716ed --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,41 @@ +## Problem Solved + +Describe the failure mode, user need, or workflow gap this PR solves. + +## Scope Fit + +- [ ] This belongs in core rather than an integration. +- [ ] This belongs in an integration rather than core. +- [ ] This is docs/examples/tooling only. + +Explain the choice: + +## Provider or Harness Tested + +Name the provider, harness, or generic workflow tested: + +## Evidence + +Add transcript, command output, screenshot, or example: + +## Docs Updated + +- [ ] README updated if public behavior changed. +- [ ] Integration docs updated if provider behavior changed. +- [ ] Examples updated if workflow behavior changed. +- [ ] Status labels remain honest. + +## Verification + +- [ ] `npm run validate` +- [ ] `npm run smoke-test` +- [ ] `npm pack --dry-run` + +## Human Review + +- [ ] I want human review before merge. + +## Safety + +- [ ] No credentials, private paths, chat identifiers, or private project data. +- [ ] No unvalidated native marketplace/plugin metadata. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a881c27 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + + - name: Install + run: npm ci + + - name: Validate + run: npm run validate + + - name: Smoke test + run: npm run smoke-test + + - name: Pack dry run + run: npm pack --dry-run diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3f6f67b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,52 @@ +name: Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + registry-url: https://registry.npmjs.org + + - name: Install + run: npm ci + + - name: Validate + run: npm run validate + + - name: Smoke test + run: npm run smoke-test + + - name: Pack dry run + run: npm pack --dry-run + + - name: Check tag matches package version + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + run: | + node -e "const pkg=require('./package.json'); const tag=process.env.GITHUB_REF_NAME; if (tag !== 'v' + pkg.version) { console.error(`Tag ${tag} does not match package version ${pkg.version}`); process.exit(1); }" + + - name: Publish to npm + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --access public --provenance + + - name: Skip publish for manual run + if: github.event_name == 'workflow_dispatch' + run: echo "workflow_dispatch runs validation, smoke test, and pack dry run only; skipping npm publish." diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..344bf1f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +*.log +.DS_Store +.env +.env.* +coverage/ +npm-debug.log* diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c865d92 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,36 @@ +# AGENTS.md + +## Anti-Slop Governance + +This repository is for plan-first AI coding. Do not treat a strong model as a substitute for process. + +## Required Conduct + +1. Classify the request before implementation. +2. Choose the provider or provider split explicitly. +3. Use a spec gate when the request is ambiguous, user-facing, broad, risky, or cross-module. +4. Use an implementation-plan gate before feature work. +5. Keep code changes scoped to the approved plan. +6. Use independent review for meaningful implementation work. +7. Verify before reporting completion. +8. Report exact commands, evidence, skipped checks, and remaining risk. + +## Hard Stops + +- Do not invent marketplace/plugin support that has not been validated. +- Do not add hidden provider metadata unless the install path is tested. +- Do not commit private paths, credentials, private chat identifiers, or private client/project data. +- Do not push, publish, release, or open a PR unless the maintainer explicitly asks. +- Do not mark work complete without verification evidence. + +## Repo Checks + +Run these before completion: + +```bash +npm run validate +npm run smoke-test +npm pack --dry-run +``` + +Use `npm ci` when a lockfile is present and dependency installation is part of the task. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..46f4ef1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## 0.1.0 + +- Initial public repo skeleton. +- Added npm scaffolder with native Hermes/OpenClaw skill installs and target-specific manual adapter files. +- Added Hermes/OpenClaw-first public skill and self-contained skill-local process references. +- Added examples, governance docs, GitHub templates, CI, and safe release workflow skeleton. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..62cf0b9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,24 @@ +# CLAUDE.md + +## Anti-Slop Governance + +This repo expects Claude Code sessions to follow the same dispatch gates as every other provider. A fast answer is not enough; the work must be classified, planned, reviewed, and verified. + +## Workflow + +1. Classify the user request. +2. Decide whether Claude should implement, review, or work in a split-provider flow. +3. Ask for a spec gate when requirements are unclear. +4. Ask for implementation-plan approval before feature code. +5. Implement only the approved scope. +6. Request independent review for meaningful code changes. +7. Run verification before final response. +8. Report what changed, what ran, what failed or was skipped, and what remains risky. + +## Guardrails + +- Keep provider-specific details in `integrations/` or `references/`. +- Keep `skills/basd-coding-dispatch/SKILL.md` lean and portable. +- Do not add credentials, private paths, private identifiers, or private project data. +- Do not claim native plugin support until it has been validated. +- Do not push, publish, release, or open a PR without explicit maintainer instruction. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..c264e82 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,15 @@ +# Code of Conduct + +## Our Standard + +Be direct, respectful, and specific. Technical disagreement is welcome when it is grounded in evidence and focused on improving the project. + +## Unacceptable Behavior + +- Harassment, threats, or discriminatory language. +- Publishing private information without permission. +- Deliberately introducing unsafe instructions, fake claims, credentials, or private data. + +## Enforcement + +Maintainers may edit, close, or remove contributions that violate this code. Serious or repeated violations may result in a ban from project spaces. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7c0894f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contributing + +Contributions should improve the workflow, docs, examples, validation, or installer without turning v0.1 into a marketplace implementation. + +## Before Opening a PR + +1. Confirm the change belongs in core, references, integrations, examples, or tooling. +2. Keep provider-specific mechanics out of `skills/basd-coding-dispatch/SKILL.md`. +3. Do not add private paths, credentials, chat identifiers, private client data, or unvalidated native plugin metadata. +4. Run: + +```bash +npm ci +npm run validate +npm run smoke-test +npm pack --dry-run +``` + +## Provider Adapters + +Provider adapters should start as documented manual install steps unless the native plugin or extension path has been validated. Include evidence in the PR: commands, transcript, screenshots, or a minimal reproducible fixture. + +## Documentation Style + +- Be precise about tested versus experimental behavior. +- Prefer neutral examples. +- Explain operational gates in references and examples instead of expanding the portable skill into a large manual. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8c7c3ad --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 basd-coding-dispatch contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..8a970f0 --- /dev/null +++ b/NOTICE @@ -0,0 +1,8 @@ +basd-coding-dispatch + +This repository packages a Hermes/OpenClaw-first coding dispatch workflow, +examples, reference documentation, and a small npm scaffolder. + +The workflow is inspired by rigorous agent-process practices, including +plan-first gates and independent review, without implying affiliation with any +provider, harness, or workflow project. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d039927 --- /dev/null +++ b/README.md @@ -0,0 +1,137 @@ +`basd-coding-dispatch` is a native Hermes/OpenClaw coding-dispatch skill for approval-gated coding from Telegram and mobile chat. + +[![CI](https://github.com/damienen/basd-coding-dispatch/actions/workflows/ci.yml/badge.svg)](https://github.com/damienen/basd-coding-dispatch/actions/workflows/ci.yml) +[![npm](https://img.shields.io/npm/v/basd-coding-dispatch.svg)](https://www.npmjs.com/package/basd-coding-dispatch) +[![license](https://img.shields.io/npm/l/basd-coding-dispatch.svg)](LICENSE) +[![release](https://img.shields.io/github/v/release/damienen/basd-coding-dispatch?include_prereleases&label=release)](https://github.com/damienen/basd-coding-dispatch/releases) + +Run serious coding work from your phone without letting the agent skip process. A Telegram message can enter your Hermes gateway, load this skill, classify the task, ask for the right approvals, route implementation to Codex or Claude worker backends, enforce quality gates, force independent review when it matters, and report only after verification evidence exists. + +Hermes is the maintained home for v0.1. OpenClaw users can use the same workflow as a workspace skill. Provider neutrality remains real, but it is a backend worker-routing mechanism rather than the headline: the dispatcher owns the gates, and Codex, Claude, or another validated backend does the work. + +## Workflow + +```text +Telegram / mobile request + -> Hermes gateway or OpenClaw workspace skill + -> classify task + -> choose worker-routing: Codex / Claude / split + -> spec gate + -> implementation-plan gate + -> implementation + -> subagent review + -> verification + -> report +``` + +![basd-coding-dispatch workflow](assets/flow-diagram.svg) + +## Prerequisites + +- Hermes or OpenClaw is already installed and authenticated for the runtime you plan to use. +- Codex CLI and Claude Code are already installed and authenticated if you want Hermes/OpenClaw to route worker tasks to those backends. +- Node.js 22 or newer is available for `npx basd-coding-dispatch`. + +## Quickstart + +Install the full Hermes skill directory (default target): + +```bash +npx basd-coding-dispatch init +``` + +Install into a specific Hermes home: + +```bash +npx basd-coding-dispatch init --dir ~/.hermes +``` + +Install for OpenClaw workspace skills: + +```bash +npx basd-coding-dispatch init --target openclaw --dir ~/openclaw-workspace +``` + +Skip default companion skills: + +```bash +npx basd-coding-dispatch init --skip-companion-skills +npx basd-coding-dispatch init --target openclaw --no-companion-skills +``` + +Create the manual project scaffold for a generic agent: + +```bash +npx basd-coding-dispatch init --target generic-agent --dir ./project +``` + +Inspect local install status: + +```bash +npx basd-coding-dispatch doctor +npx basd-coding-dispatch validate +``` + +## What gets installed + +Hermes and OpenClaw targets install `skills/basd-coding-dispatch/` as a self-contained skill, including `SKILL.md` and skill-local `references/`. By default, they also install pinned companion skills from `integrations/companion-skills.json` when those skills are missing: Hermes Codex/Claude Code worker guides plus the Superpowers process skills used for planning, review, TDD, debugging, and verification. Default companion installs fetch from `raw.githubusercontent.com`, so they require network access to GitHub raw content. + +Existing companion skills are skipped unless `--force` is provided. Existing core `basd-coding-dispatch` files still block the install unless `--force` is provided. Use `--skip-companion-skills` or `--no-companion-skills` for the offline/core-only path. + +Manual adapter targets write process docs and skill/reference files into a project. They do not fetch or install companion skills. + +## What this does not install/configure + +This package does not install Hermes, OpenClaw, Codex CLI, Claude Code, auth tokens, API keys, model credentials, Telegram gateways, or native marketplace/plugin metadata. It assumes those runtimes and credentials already exist. + +For OpenClaw, `--dir ` writes to that workspace root under `/skills/`. Runtime discovery still depends on the workspace that your OpenClaw process is actually using; verify with `openclaw skills list` and `openclaw skills info basd-coding-dispatch`. + +## Adaptation Status + +Hermes is the flagship install path for v0.1. OpenClaw support is based on the verified local workspace-skill layout: `openclaw skills info aeo-geo` reports source `openclaw-workspace` and a path under `~/openclaw-workspace/skills/...`. Other harnesses remain manual or experimental adapters until their native install paths are validated. + +| Target | Status | Install notes | +| --- | --- | --- | +| Hermes | tested | Flagship full-skill install under `$HERMES_HOME/skills/` or `~/.hermes/skills/`. See `integrations/hermes/install.md`. | +| OpenClaw | tested | Workspace-skill install under `$OPENCLAW_WORKSPACE/skills/` or `~/openclaw-workspace/skills/`. See `integrations/openclaw/install.md`. | +| Generic agents | tested | Manual file scaffold behavior is covered by the smoke test. See `integrations/generic-agent/install.md`. | +| Codex CLI | experimental | Supported worker backend; native harness adapter is manual. See `integrations/codex/install.md`. | +| Claude Code | experimental | Supported worker backend; native harness adapter is manual. See `integrations/claude-code/install.md`. | +| OpenCode | experimental | Future/manual adapter only. See `integrations/opencode/install.md`. | +| Cursor | experimental | Future/manual adapter only. See `integrations/cursor/install.md`. | + +## Quality Gates + +- Classify the request before choosing a worker-routing shape. +- Produce a spec gate for ambiguous work. +- Produce an implementation-plan gate before feature code. +- Keep worker-routing mechanics in `references/` and `integrations/`, not in the portable skill body. +- Use independent review for meaningful implementation work. +- Verify with concrete commands, transcripts, examples, or screenshots before reporting done. +- Keep public repo files free of private paths, credentials, private client data, and unvalidated provider metadata. + +See `skills/basd-coding-dispatch/references/quality-gates.md` for the installed skill reference and `references/quality-gates.md` for repo browsing. + +## Examples + +- `examples/small-fix.md`: small bugfix flow from classification through verification. +- `examples/feature-build.md`: feature work with separate spec and implementation-plan gates. +- `examples/dual-provider-review.md`: one worker implements, another reviews, and the dispatcher reconciles findings. +- `examples/telegram-agent-workflow.md`: flagship phone-to-Hermes flow from Telegram through approval, worker routing, review, and verification. + +## Included Files + +- `skills/basd-coding-dispatch/SKILL.md`: lean public skill for Hermes, OpenClaw, and agents that support skill-style workflows. +- `skills/basd-coding-dispatch/references/`: reference files included with native skill installs. +- `references/`: repo-level copies of the process references for browsing. +- `integrations/`: install/adaptation notes for supported targets. +- `AGENTS.md` and `CLAUDE.md`: strict anti-slop governance files. +- `.github/`: issue templates, PR template, CI workflow, and safe release workflow skeleton. + +## Inspiration + +This project is inspired by the rigor of Superpowers-style agent workflows: explicit gates, review before trust, and verification before completion. It is not affiliated with Superpowers or any provider. + +## Launch Writing + +Launch writing happens after the repository is ready. This repo intentionally does not include launch article drafts in v0.1. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..740a936 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,18 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| --- | --- | +| 0.1.x | Yes | + +## Reporting a Vulnerability + +Open a security advisory or contact the maintainers through the repository security channel. Do not post exploit details in a public issue if the report includes a working exploit or sensitive material. + +## Repository Safety Rules + +- Never commit credentials, private paths, private chat identifiers, or private client/project data. +- Keep examples generic. +- Do not add native plugin metadata until the install path is validated. +- The npm scaffolder must refuse to overwrite files unless `--force` is used. diff --git a/assets/flow-diagram.svg b/assets/flow-diagram.svg new file mode 100644 index 0000000..e847cba --- /dev/null +++ b/assets/flow-diagram.svg @@ -0,0 +1,45 @@ + + basd-coding-dispatch flow diagram + Telegram or mobile request moves through Hermes or OpenClaw, worker routing, gates, implementation, review, verification, and report. + + + + + + + + + + + + Telegram + mobile + Hermes + OpenClaw + route + worker + spec + implementation + plan + build + report + + + + + + + + + + + + + + + + + Codex / Claude / validated backends + review + verification + + diff --git a/assets/logo.svg b/assets/logo.svg new file mode 100644 index 0000000..3b5c3aa --- /dev/null +++ b/assets/logo.svg @@ -0,0 +1,11 @@ + + basd-coding-dispatch logo + A simple dispatch mark with connected plan gates. + + + + + + + + diff --git a/bin/basd-coding-dispatch.mjs b/bin/basd-coding-dispatch.mjs new file mode 100755 index 0000000..454752d --- /dev/null +++ b/bin/basd-coding-dispatch.mjs @@ -0,0 +1,755 @@ +#!/usr/bin/env node + +import { existsSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { runValidation } from "../scripts/validate.mjs"; + +const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const skillDirectory = "skills/basd-coding-dispatch"; +const skillReferenceFiles = [ + "quality-gates.md", + "provider-command-recipes.md", + "session-topology.md", + "review-orchestration.md", + "subagent-skill-bundles.md" +]; +const skillFiles = [ + `${skillDirectory}/SKILL.md`, + `${skillDirectory}/references/README.md`, + ...skillReferenceFiles.map((file) => `${skillDirectory}/references/${file}`) +]; +const rootReferenceFiles = skillReferenceFiles.map((file) => `references/${file}`); +const companionManifestFile = "integrations/companion-skills.json"; + +const TARGETS = [ + "hermes", + "openclaw", + "codex", + "claude-code", + "opencode", + "cursor", + "generic-agent" +]; +const DEFAULT_TARGET = "hermes"; +const NATIVE_SKILL_TARGETS = new Set(["hermes", "openclaw"]); + +const REQUIRED_SOURCE_FILES = [ + "README.md", + "LICENSE", + "NOTICE", + "CHANGELOG.md", + "CONTRIBUTING.md", + "SECURITY.md", + "CODE_OF_CONDUCT.md", + "AGENTS.md", + "CLAUDE.md", + "llms.txt", + "package.json", + "bin/basd-coding-dispatch.mjs", + "scripts/validate.mjs", + "scripts/smoke-test.mjs", + companionManifestFile, + ...skillFiles, + ...rootReferenceFiles, + "integrations/hermes/install.md", + "integrations/openclaw/install.md", + "integrations/codex/install.md", + "integrations/claude-code/install.md", + "integrations/opencode/install.md", + "integrations/cursor/install.md", + "integrations/generic-agent/install.md", + "examples/small-fix.md", + "examples/feature-build.md", + "examples/dual-provider-review.md", + "examples/telegram-agent-workflow.md", + "assets/logo.svg", + "assets/flow-diagram.svg", + ".github/workflows/ci.yml", + ".github/workflows/release.yml", + ".github/ISSUE_TEMPLATE/bug_report.yml", + ".github/ISSUE_TEMPLATE/provider_adapter_request.yml", + ".github/ISSUE_TEMPLATE/workflow_improvement.yml", + ".github/ISSUE_TEMPLATE/docs_install_issue.yml", + ".github/ISSUE_TEMPLATE/config.yml", + ".github/pull_request_template.md" +]; + +const BASE_SCAFFOLD_FILES = [ + "llms.txt", + ...skillFiles, + ...rootReferenceFiles +]; + +const TARGET_FILES = { + codex: [ + "AGENTS.md", + ...BASE_SCAFFOLD_FILES, + "integrations/codex/install.md" + ], + "claude-code": [ + "CLAUDE.md", + ...BASE_SCAFFOLD_FILES, + "integrations/claude-code/install.md" + ], + opencode: [ + "AGENTS.md", + ...BASE_SCAFFOLD_FILES, + "integrations/opencode/install.md" + ], + cursor: [ + "AGENTS.md", + ...BASE_SCAFFOLD_FILES, + "integrations/cursor/install.md" + ], + "generic-agent": [ + "AGENTS.md", + "CLAUDE.md", + ...BASE_SCAFFOLD_FILES, + "integrations/generic-agent/install.md" + ] +}; + +const ALL_SCAFFOLD_FILES = [...new Set(Object.values(TARGET_FILES).flat())].sort(); + +function printHelp(stream = process.stdout) { + stream.write(`basd-coding-dispatch + +Usage: + basd-coding-dispatch init [--target ] [--dir ] [--force] [--skip-companion-skills] + basd-coding-dispatch doctor + basd-coding-dispatch validate + basd-coding-dispatch help + +Targets: + hermes Native Hermes skill install under $HERMES_HOME/skills or ~/.hermes/skills (default) + openclaw OpenClaw workspace skill install under ~/openclaw-workspace/skills + generic-agent Manual project scaffold + codex Experimental manual adapter scaffold + claude-code Experimental manual adapter scaffold + opencode Experimental manual adapter scaffold + cursor Experimental manual adapter scaffold + +Examples: + basd-coding-dispatch init + basd-coding-dispatch init --skip-companion-skills + basd-coding-dispatch init --dir ~/.hermes + basd-coding-dispatch init --target openclaw --dir ~/openclaw-workspace + basd-coding-dispatch init --target generic-agent --dir ./my-project + basd-coding-dispatch doctor + +Options: + --skip-companion-skills Do not fetch default companion skills for native targets + --no-companion-skills Alias for --skip-companion-skills +`); +} + +function parseInitArgs(args) { + const options = { + target: DEFAULT_TARGET, + dir: undefined, + dirProvided: false, + force: false, + installCompanionSkills: true + }; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + + if (arg === "--target") { + const value = args[index + 1]; + if (!value) { + throw new Error("--target requires a value"); + } + options.target = value; + index += 1; + } else if (arg === "--dir") { + const value = args[index + 1]; + if (!value) { + throw new Error("--dir requires a value"); + } + options.dir = value; + options.dirProvided = true; + index += 1; + } else if (arg === "--force") { + options.force = true; + } else if (arg === "--skip-companion-skills" || arg === "--no-companion-skills") { + options.installCompanionSkills = false; + } else if (arg === "--help" || arg === "-h") { + options.help = true; + } else { + throw new Error(`Unknown init option: ${arg}`); + } + } + + if (!TARGETS.includes(options.target)) { + throw new Error(`Unknown target: ${options.target}`); + } + + return options; +} + +function printFileList(label, files) { + process.stdout.write(`${label}:\n`); + if (files.length === 0) { + process.stdout.write(" none\n"); + return; + } + for (const file of files) { + process.stdout.write(` ${file}\n`); + } +} + +async function copyScaffoldFile(relativeFile, destinationRoot) { + const sourcePath = path.join(packageRoot, relativeFile); + const destinationPath = path.join(destinationRoot, relativeFile); + const content = await readFile(sourcePath, "utf8"); + await mkdir(path.dirname(destinationPath), { recursive: true }); + await writeFile(destinationPath, content, "utf8"); +} + +async function listSourceFiles(relativeDirectory) { + const fullDirectory = path.join(packageRoot, relativeDirectory); + const entries = await readdir(fullDirectory, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + const relativePath = path.join(relativeDirectory, entry.name); + if (entry.isDirectory()) { + files.push(...await listSourceFiles(relativePath)); + } else if (entry.isFile()) { + files.push(relativePath); + } + } + + return files.sort(); +} + +async function loadCompanionSkillManifest() { + const content = await readFile(path.join(packageRoot, companionManifestFile), "utf8"); + return JSON.parse(content); +} + +function defaultCompanionSkills(manifest) { + return (manifest.skills ?? []).filter((skill) => skill.installByDefault === true); +} + +function formatFile(file) { + return file.split(path.sep).join("/"); +} + +function expandHomePath(value) { + if (value === "~") { + return os.homedir(); + } + + if (value.startsWith("~/") || value.startsWith("~\\")) { + return path.join(os.homedir(), value.slice(2)); + } + + return value; +} + +function resolveUserPath(value) { + return path.resolve(process.cwd(), expandHomePath(value)); +} + +function assertSafeRelativePath(value, label) { + if (typeof value !== "string" || value.length === 0) { + throw new Error(`${label} must be a non-empty relative path`); + } + + if (path.isAbsolute(value) || value.split(/[\\/]+/).includes("..")) { + throw new Error(`${label} must stay inside the install root: ${value}`); + } +} + +function resolveNativeRoot(target, options = {}) { + if (options.dirProvided) { + return resolveUserPath(options.dir); + } + + if (target === "hermes") { + return resolveUserPath(process.env.HERMES_HOME || path.join(os.homedir(), ".hermes")); + } + + if (target === "openclaw") { + return resolveUserPath( + process.env.OPENCLAW_WORKSPACE || path.join(os.homedir(), "openclaw-workspace") + ); + } + + throw new Error(`Unsupported native target: ${target}`); +} + +function printNativeNextStep(target) { + if (target === "hermes") { + process.stdout.write("next: hermes skills list\n"); + process.stdout.write("next: /skill basd-coding-dispatch\n"); + return; + } + + process.stdout.write("next: openclaw skills list\n"); + process.stdout.write("next: /skill basd-coding-dispatch\n"); +} + +function companionOutputFile(skill, file) { + assertSafeRelativePath(skill.name, "companion skill name"); + assertSafeRelativePath(file.destinationPath, "companion destinationPath"); + return formatFile(path.join("skills", skill.name, file.destinationPath)); +} + +function companionFixturePath(skill, file) { + assertSafeRelativePath(file.sourcePath, "companion sourcePath"); + const fixtureRoot = resolveUserPath(process.env.BASD_COMPANION_SKILLS_FIXTURE_DIR); + return path.join(fixtureRoot, skill.sourceRepo, file.sourcePath); +} + +async function fetchCompanionFile(skill, file) { + if (process.env.BASD_COMPANION_SKILLS_FIXTURE_DIR) { + return readFile(companionFixturePath(skill, file), "utf8"); + } + + if (typeof fetch !== "function") { + throw new Error("global fetch is unavailable; Node 22 or newer is required"); + } + + const response = await fetch(file.rawUrl); + if (!response.ok) { + throw new Error(`HTTP ${response.status} ${response.statusText}`.trim()); + } + + return response.text(); +} + +async function prepareCompanionSkillInstall(root, options) { + const manifest = await loadCompanionSkillManifest(); + const skills = defaultCompanionSkills(manifest); + const result = { + created: [], + completed: [], + skipped: [], + overwritten: [], + writePlans: [] + }; + + for (const skill of skills) { + const plannedFiles = skill.files.map((file) => { + const outputFile = companionOutputFile(skill, file); + + return { + sourcePath: file.sourcePath, + outputFile, + destinationPath: path.join(root, outputFile), + rawUrl: file.rawUrl + }; + }); + const existingFiles = plannedFiles.filter((file) => existsSync(file.destinationPath)); + const missingFiles = plannedFiles.filter((file) => !existsSync(file.destinationPath)); + + if (!options.force && missingFiles.length === 0) { + result.skipped.push(skill.name); + continue; + } + + const filesToInstall = options.force ? plannedFiles : missingFiles; + const filesWithContent = []; + for (const file of filesToInstall) { + try { + const content = await fetchCompanionFile(skill, file); + filesWithContent.push({ ...file, content }); + } catch (error) { + throw new Error( + `failed to fetch companion skill ${skill.name} file ${file.sourcePath} from ${file.rawUrl}: ${error.message}` + ); + } + } + + result.writePlans.push({ + skillName: skill.name, + files: filesWithContent + }); + + if (options.force && existingFiles.length > 0) { + result.overwritten.push(skill.name); + } else if (!options.force && existingFiles.length > 0) { + result.completed.push(skill.name); + } else { + result.created.push(skill.name); + } + } + + return result; +} + +async function writeCompanionSkillInstall(plan) { + for (const skillPlan of plan.writePlans) { + for (const file of skillPlan.files) { + await mkdir(path.dirname(file.destinationPath), { recursive: true }); + await writeFile(file.destinationPath, file.content, "utf8"); + } + } +} + +async function initNativeSkill(options) { + const root = resolveNativeRoot(options.target, options); + const installDir = path.join(root, "skills", "basd-coding-dispatch"); + const sourceFiles = await listSourceFiles(skillDirectory); + const plannedFiles = sourceFiles.map((sourceFile) => { + const skillRelativeFile = path.relative(skillDirectory, sourceFile); + const outputFile = path.join("skills", "basd-coding-dispatch", skillRelativeFile); + + return { + sourceFile, + outputFile: formatFile(outputFile), + destinationPath: path.join(root, outputFile) + }; + }); + + const refusedFiles = plannedFiles + .filter((file) => existsSync(file.destinationPath) && !options.force) + .map((file) => file.outputFile); + + if (refusedFiles.length > 0) { + process.stdout.write(`target: ${options.target}\n`); + process.stdout.write(`root: ${root}\n`); + process.stdout.write(`dir: ${installDir}\n`); + printFileList("created", []); + printFileList("skipped", []); + printFileList("refused", refusedFiles); + printNativeNextStep(options.target); + process.stderr.write("Refusing to overwrite existing skill files without --force.\n"); + return 1; + } + + let companionPlan; + if (options.installCompanionSkills) { + try { + companionPlan = await prepareCompanionSkillInstall(root, options); + } catch (error) { + process.stdout.write(`target: ${options.target}\n`); + process.stdout.write(`root: ${root}\n`); + process.stdout.write(`dir: ${installDir}\n`); + printFileList("created", []); + printFileList("skipped", []); + printFileList("refused", []); + printFileList("companion created", []); + printFileList("companion completed", []); + printFileList("companion skipped", []); + printFileList("companion overwritten", []); + process.stderr.write(`${error.message}\n`); + return 1; + } + } + + const createdFiles = []; + for (const file of plannedFiles) { + const content = await readFile(path.join(packageRoot, file.sourceFile), "utf8"); + await mkdir(path.dirname(file.destinationPath), { recursive: true }); + await writeFile(file.destinationPath, content, "utf8"); + createdFiles.push(file.outputFile); + } + + if (companionPlan) { + await writeCompanionSkillInstall(companionPlan); + } + + process.stdout.write(`target: ${options.target}\n`); + process.stdout.write(`root: ${root}\n`); + process.stdout.write(`dir: ${installDir}\n`); + printFileList("created", createdFiles); + printFileList("skipped", []); + printFileList("refused", []); + if (options.installCompanionSkills) { + printFileList("companion created", companionPlan.created); + printFileList("companion completed", companionPlan.completed); + printFileList("companion skipped", companionPlan.skipped); + printFileList("companion overwritten", companionPlan.overwritten); + } else { + process.stdout.write("companion skills: skipped by flag\n"); + } + printNativeNextStep(options.target); + + return 0; +} + +async function init(args) { + let options; + try { + options = parseInitArgs(args); + } catch (error) { + process.stderr.write(`${error.message}\n\n`); + printHelp(process.stderr); + return 1; + } + + if (options.help) { + printHelp(); + return 0; + } + + if (NATIVE_SKILL_TARGETS.has(options.target)) { + return initNativeSkill(options); + } + + const selectedFiles = TARGET_FILES[options.target]; + const selectedSet = new Set(selectedFiles); + const skippedFiles = ALL_SCAFFOLD_FILES.filter((file) => !selectedSet.has(file)); + const destinationRoot = resolveUserPath(options.dir ?? "."); + const refusedFiles = selectedFiles.filter((file) => { + return existsSync(path.join(destinationRoot, file)) && !options.force; + }); + + if (refusedFiles.length > 0) { + process.stdout.write(`target: ${options.target}\n`); + process.stdout.write(`dir: ${destinationRoot}\n`); + printFileList("created", []); + printFileList("skipped", skippedFiles); + printFileList("refused", refusedFiles); + process.stderr.write("Refusing to overwrite existing files without --force.\n"); + return 1; + } + + const createdFiles = []; + for (const file of selectedFiles) { + await copyScaffoldFile(file, destinationRoot); + createdFiles.push(file); + } + + process.stdout.write(`target: ${options.target}\n`); + process.stdout.write(`dir: ${destinationRoot}\n`); + printFileList("created", createdFiles); + printFileList("skipped", skippedFiles); + printFileList("refused", []); + + return 0; +} + +function commandCandidates(command) { + if (path.isAbsolute(command) || command.includes("/") || command.includes("\\")) { + return [command]; + } + + const directories = (process.env.PATH || "") + .split(path.delimiter) + .filter(Boolean); + const extensions = process.platform === "win32" + ? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM") + .split(";") + .filter(Boolean) + : [""]; + + return directories.flatMap((directory) => { + return extensions.map((extension) => path.join(directory, `${command}${extension}`)); + }); +} + +function findCommand(command) { + return commandCandidates(command).find((candidate) => existsSync(candidate)); +} + +function firstOutputLine(value) { + return (value || "").trim().split(/\r?\n/)[0]?.trim() || ""; +} + +function nodeFallbackOutputLines(executable, args) { + const fallback = spawnSync(process.execPath, [executable, ...args], { + encoding: "utf8", + timeout: 5000 + }); + + return { + succeeded: fallback.status === 0 && !fallback.error, + stdoutLine: firstOutputLine(fallback.stdout), + stderrLine: firstOutputLine(fallback.stderr) + }; +} + +function commandStatus(command, args, options = {}) { + const executable = findCommand(command); + + if (!executable) { + return "missing"; + } + + const result = spawnSync(executable, args, { + encoding: "utf8", + timeout: 5000 + }); + + if (result.error?.code === "ENOENT") { + return "missing"; + } + + if (result.error) { + if (options.nodeFallback) { + const fallbackLines = nodeFallbackOutputLines(executable, args); + + if (fallbackLines.succeeded && fallbackLines.stdoutLine) { + return `present (${fallbackLines.stdoutLine})`; + } + + if (fallbackLines.succeeded && fallbackLines.stderrLine) { + return `present (${fallbackLines.stderrLine})`; + } + } + + return `error (${result.error.code || result.error.message || "unknown spawn error"})`; + } + + const stdoutLine = firstOutputLine(result.stdout); + + if (stdoutLine) { + return `present (${stdoutLine})`; + } + + let fallbackStderrLine = ""; + if (options.nodeFallback) { + const fallbackLines = nodeFallbackOutputLines(executable, args); + + if (fallbackLines.succeeded && fallbackLines.stdoutLine) { + return `present (${fallbackLines.stdoutLine})`; + } + + if (fallbackLines.succeeded) { + fallbackStderrLine = fallbackLines.stderrLine; + } + } + + const stderrLine = firstOutputLine(result.stderr); + + if (stderrLine) { + return `present (${stderrLine})`; + } + + if (fallbackStderrLine) { + return `present (${fallbackStderrLine})`; + } + + return `present (${executable})`; +} + +function printInstallStatus(label, root, skillPath, rootLabel = "root") { + process.stdout.write(`${label} ${rootLabel}: ${root}\n`); + process.stdout.write( + `${label} skill: ${existsSync(skillPath) ? "installed" : "missing"} at ${skillPath}\n` + ); +} + +function printCompanionInstallStatus(label, root, skills) { + process.stdout.write(`${label} companion skills:\n`); + for (const skill of skills) { + const skillPath = path.join(root, "skills", skill.name, "SKILL.md"); + process.stdout.write( + ` ${skill.name}: ${existsSync(skillPath) ? "installed" : "missing"} at ${skillPath}\n` + ); + } +} + +async function doctor() { + const validationErrors = []; + const validationStatus = await runValidation({ + rootDir: packageRoot, + stdout: { write: () => true }, + stderr: { + write: (chunk) => { + validationErrors.push(String(chunk)); + return true; + } + } + }); + const targetErrors = TARGETS.flatMap((target) => { + if (NATIVE_SKILL_TARGETS.has(target)) { + return []; + } + + const files = TARGET_FILES[target] ?? []; + if (files.length === 0) { + return [`target ${target} has no scaffold files`]; + } + return files + .filter((file) => !REQUIRED_SOURCE_FILES.includes(file)) + .map((file) => `target ${target} references unknown source file ${file}`); + }); + + const hasSourceErrors = validationStatus !== 0 || targetErrors.length > 0; + + process.stdout.write(`source: ${hasSourceErrors ? "failed" : "ok"}\n`); + + if (hasSourceErrors) { + process.stderr.write(validationErrors.join("")); + for (const error of targetErrors) { + process.stderr.write(`${error}\n`); + } + return 1; + } + + const companionManifest = await loadCompanionSkillManifest(); + const companionSkills = defaultCompanionSkills(companionManifest); + const hermesRoot = resolveNativeRoot("hermes", { dirProvided: false }); + const openclawRoot = resolveNativeRoot("openclaw", { dirProvided: false }); + + process.stdout.write( + `companion manifest: ok (${companionSkills.length} default skills at ${companionManifestFile})\n` + ); + process.stdout.write(`hermes cli: ${commandStatus("hermes", ["--version"])}\n`); + printInstallStatus( + "hermes", + hermesRoot, + path.join(hermesRoot, "skills", "basd-coding-dispatch", "SKILL.md"), + "home" + ); + printCompanionInstallStatus("hermes", hermesRoot, companionSkills); + process.stdout.write(`openclaw cli: ${commandStatus("openclaw", ["--version"], { nodeFallback: true })}\n`); + process.stdout.write(`openclaw workspace: ${openclawRoot}\n`); + process.stdout.write( + `openclaw skill: ${ + existsSync(path.join(openclawRoot, "skills", "basd-coding-dispatch", "SKILL.md")) + ? "installed" + : "missing" + } at ${path.join(openclawRoot, "skills", "basd-coding-dispatch", "SKILL.md")}\n` + ); + printCompanionInstallStatus("openclaw", openclawRoot, companionSkills); + process.stdout.write("Doctor passed\n"); + return 0; +} + +function validate() { + return runValidation({ rootDir: packageRoot }); +} + +export async function main(argv = process.argv.slice(2)) { + const [command = "help", ...args] = argv; + + if (command === "help" || command === "--help" || command === "-h") { + printHelp(); + return 0; + } + + if (command === "init") { + return init(args); + } + + if (command === "doctor") { + return doctor(); + } + + if (command === "validate") { + return validate(); + } + + process.stderr.write(`Unknown command: ${command}\n\n`); + printHelp(process.stderr); + return 1; +} + +const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : ""; +const currentPath = path.resolve(fileURLToPath(import.meta.url)); + +if (invokedPath === currentPath) { + const exitCode = await main(); + process.exitCode = exitCode; +} diff --git a/examples/dual-provider-review.md b/examples/dual-provider-review.md new file mode 100644 index 0000000..38245a7 --- /dev/null +++ b/examples/dual-provider-review.md @@ -0,0 +1,42 @@ +# Dual Worker Review Example + +Reference files: `references/review-orchestration.md`, `references/session-topology.md` + +## Request + +Implement a release workflow skeleton and make sure it cannot publish unless the package token is configured. + +## Classification + +Release and packaging task. Higher risk because incorrect claims or unsafe publish behavior can affect public packages. + +## Worker Routing + +Worker A implements the workflow. Worker B reviews the diff and checks whether the workflow is safe, conventional, and honest. + +## Implementer Brief + +```text +Create a tag-based release workflow. +Run CI-like checks. +Publish only when the npm package token is configured. +Do not publish during implementation. +Do not create a release. +``` + +## Reviewer Brief + +```text +Review the workflow for unsafe publish behavior, fake release claims, missing checks, and credential exposure. +Findings first. Include file and line references when possible. +``` + +## Dispatcher Reconciliation + +The dispatcher accepts concrete findings, rejects out-of-scope suggestions, applies fixes, and reruns: + +```bash +npm run validate +npm run smoke-test +npm pack --dry-run +``` diff --git a/examples/feature-build.md b/examples/feature-build.md new file mode 100644 index 0000000..e113a1e --- /dev/null +++ b/examples/feature-build.md @@ -0,0 +1,51 @@ +# Feature Build Example + +Reference files: `references/quality-gates.md`, `references/session-topology.md` + +## Request + +Add a new worker adapter target for a coding harness. + +## Classification + +Feature build. It changes installer behavior, docs, validation, examples, and public claims. + +## Spec Gate + +```text +Problem: users need a scaffold target for the harness. +Non-goals: no native marketplace package, no hidden metadata, no credential handling. +Behavior: init supports --target , copies only validated public files, refuses overwrite without --force. +Docs: integration page includes status label and tested scope. +Acceptance: validate, smoke-test, and pack dry-run pass. +``` + +No code is written before the spec gate is accepted. + +## Implementation-Plan Gate + +```text +Files: +- bin/basd-coding-dispatch.mjs +- scripts/validate.mjs +- scripts/smoke-test.mjs +- integrations//install.md +- README.md + +Steps: +1. Add failing smoke-test coverage for the new target. +2. Add the target to CLI target maps. +3. Add validation coverage for the integration doc. +4. Add install docs with an honest status label. +5. Run npm run validate, npm run smoke-test, npm pack --dry-run. +``` + +No feature code is written before the implementation plan is accepted. + +## Verification + +```bash +npm run validate +npm run smoke-test +npm pack --dry-run +``` diff --git a/examples/small-fix.md b/examples/small-fix.md new file mode 100644 index 0000000..e05d193 --- /dev/null +++ b/examples/small-fix.md @@ -0,0 +1,35 @@ +# Small Fix Example + +Reference files: `references/quality-gates.md`, `references/provider-command-recipes.md` + +## Request + +The CLI exits zero for an unknown command. Make unknown commands print help and exit nonzero. + +## Classification + +Small fix. User-visible CLI behavior. No spec gate needed beyond the stated behavior. + +## Worker Routing + +Codex implements. Independent review is lightweight because the change is small but touches CLI behavior. + +## Implementation Plan + +1. Add a smoke-test assertion for unknown commands. +2. Confirm the assertion fails. +3. Update the command dispatcher to print help and return a nonzero status. +4. Run the smoke test and validation. + +## Review + +Reviewer checks that known commands still work, unknown commands fail, and help text remains accurate. + +## Verification + +```bash +npm run smoke-test +npm run validate +``` + +Report both exit statuses before marking the fix complete. diff --git a/examples/telegram-agent-workflow.md b/examples/telegram-agent-workflow.md new file mode 100644 index 0000000..584b96f --- /dev/null +++ b/examples/telegram-agent-workflow.md @@ -0,0 +1,54 @@ +# Telegram To Hermes Workflow Example + +Reference files: `skills/basd-coding-dispatch/references/quality-gates.md`, `integrations/hermes/install.md` + +This is the flagship v0.1 loop: the human stays on a phone, Telegram sends the request to a Hermes gateway, Hermes loads `basd-coding-dispatch`, and worker backends do the coding only after the gates are satisfied. + +No private chat identifiers, bot tokens, or project paths belong in the public workflow. + +## Phone Request + +```text +From Telegram: +Patch the install flow so Hermes is the default skill path and OpenClaw users get a workspace-skill install. Keep Codex and Claude as worker backends. +``` + +## Hermes Dispatcher Response + +```text +Classification: install and public docs feature; public repo risk. +Worker routing: Codex implements CLI/docs changes; independent reviewer checks claims and tests. +Spec gate: native Hermes install must copy the full skill directory under the Hermes home. OpenClaw install may be documented only as workspace-skill compatible. Other harness adapters stay experimental/manual unless validated. +Implementation-plan gate: add failing smoke coverage, update CLI install target resolution, copy skill-local references, rewrite README/integration docs, then run package validation. +``` + +## Phone Approval + +```text +Approved. Do not push, publish, or claim native marketplace support. +``` + +## Agent Work + +Hermes routes the approved work to a coding backend. The worker edits inside the repository, avoids real `~/.hermes` mutation by using temp dirs in tests, and reports evidence back through Hermes: + +```text +Changed: +- bin/basd-coding-dispatch.mjs +- skills/basd-coding-dispatch/** +- integrations/hermes/install.md +- integrations/openclaw/install.md +- README.md + +Verification: +- npm run validate: passed +- npm run smoke-test: passed +- npm pack --dry-run: passed + +Remaining risk: +- Cursor, OpenCode, and native Codex/Claude harness adapters remain experimental/manual until their install paths are validated. +``` + +## Why This Works From A Phone + +The phone is only the approval and control surface. Hermes keeps the durable skill, the coding backend keeps filesystem access, and the workflow requires concrete checkpoints before expensive or risky steps continue. diff --git a/integrations/claude-code/install.md b/integrations/claude-code/install.md new file mode 100644 index 0000000..b674b5a --- /dev/null +++ b/integrations/claude-code/install.md @@ -0,0 +1,25 @@ +# Claude Code Install + +Status: experimental + +Claude Code is a supported worker backend for implementation and review. This target is an experimental manual adapter scaffold, not a native Claude Code marketplace or plugin install. + +## Install + +```bash +npx basd-coding-dispatch init --target claude-code --dir +``` + +## Expected Files + +- `CLAUDE.md` +- `llms.txt` +- `skills/basd-coding-dispatch/SKILL.md` +- `skills/basd-coding-dispatch/references/README.md` +- `skills/basd-coding-dispatch/references/*.md` +- `references/*.md` +- `integrations/claude-code/install.md` + +## Usage + +Keep `CLAUDE.md` strict and short. Load the public skill for workflow and the references only when the task needs worker routing, review orchestration, or quality gates. diff --git a/integrations/codex/install.md b/integrations/codex/install.md new file mode 100644 index 0000000..9819b86 --- /dev/null +++ b/integrations/codex/install.md @@ -0,0 +1,25 @@ +# Codex CLI Install + +Status: experimental + +Codex is a supported worker backend for implementation and review. This target is an experimental manual adapter scaffold, not a native Codex plugin or marketplace install. + +## Install + +```bash +npx basd-coding-dispatch init --target codex --dir +``` + +## Expected Files + +- `AGENTS.md` +- `llms.txt` +- `skills/basd-coding-dispatch/SKILL.md` +- `skills/basd-coding-dispatch/references/README.md` +- `skills/basd-coding-dispatch/references/*.md` +- `references/*.md` +- `integrations/codex/install.md` + +## Usage + +Ask Codex to use the copied skill and references for plan-first dispatch, or let Hermes/OpenClaw route approved worker tasks to Codex. Keep local project instructions in `AGENTS.md` and avoid changing shared references unless the improvement belongs upstream. diff --git a/integrations/companion-skills.json b/integrations/companion-skills.json new file mode 100644 index 0000000..93b318b --- /dev/null +++ b/integrations/companion-skills.json @@ -0,0 +1,191 @@ +{ + "schemaVersion": 1, + "description": "Pinned upstream companion skills installed by default for native Hermes and OpenClaw targets.", + "skills": [ + { + "name": "codex", + "sourceRepo": "NousResearch/hermes-agent", + "ref": "faa13e49f81480771ceeb55991bb0c27edf1a5fb", + "sourceDirectory": "skills/autonomous-ai-agents/codex", + "installByDefault": true, + "license": "MIT", + "licenseNotes": "Upstream repository and skill frontmatter declare MIT.", + "updateNotes": "Pinned to a verified main commit. Revalidate raw URLs before changing this ref.", + "files": [ + { + "sourcePath": "skills/autonomous-ai-agents/codex/SKILL.md", + "destinationPath": "SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/NousResearch/hermes-agent/faa13e49f81480771ceeb55991bb0c27edf1a5fb/skills/autonomous-ai-agents/codex/SKILL.md" + } + ] + }, + { + "name": "claude-code", + "sourceRepo": "NousResearch/hermes-agent", + "ref": "faa13e49f81480771ceeb55991bb0c27edf1a5fb", + "sourceDirectory": "skills/autonomous-ai-agents/claude-code", + "installByDefault": true, + "license": "MIT", + "licenseNotes": "Upstream repository and skill frontmatter declare MIT.", + "updateNotes": "Pinned to a verified main commit. Revalidate raw URLs before changing this ref.", + "files": [ + { + "sourcePath": "skills/autonomous-ai-agents/claude-code/SKILL.md", + "destinationPath": "SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/NousResearch/hermes-agent/faa13e49f81480771ceeb55991bb0c27edf1a5fb/skills/autonomous-ai-agents/claude-code/SKILL.md" + } + ] + }, + { + "name": "using-superpowers", + "sourceRepo": "obra/superpowers", + "ref": "f2cbfbefebbfef77321e4c9abc9e949826bea9d7", + "sourceDirectory": "skills/using-superpowers", + "installByDefault": true, + "license": "MIT", + "licenseNotes": "Upstream repository declares MIT.", + "updateNotes": "Pinned to a verified main commit. Revalidate raw URLs before changing this ref.", + "files": [ + { + "sourcePath": "skills/using-superpowers/SKILL.md", + "destinationPath": "SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/f2cbfbefebbfef77321e4c9abc9e949826bea9d7/skills/using-superpowers/SKILL.md" + } + ] + }, + { + "name": "brainstorming", + "sourceRepo": "obra/superpowers", + "ref": "f2cbfbefebbfef77321e4c9abc9e949826bea9d7", + "sourceDirectory": "skills/brainstorming", + "installByDefault": true, + "license": "MIT", + "licenseNotes": "Upstream repository declares MIT.", + "updateNotes": "Pinned to a verified main commit. Revalidate raw URLs before changing this ref.", + "files": [ + { + "sourcePath": "skills/brainstorming/SKILL.md", + "destinationPath": "SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/f2cbfbefebbfef77321e4c9abc9e949826bea9d7/skills/brainstorming/SKILL.md" + } + ] + }, + { + "name": "writing-plans", + "sourceRepo": "obra/superpowers", + "ref": "f2cbfbefebbfef77321e4c9abc9e949826bea9d7", + "sourceDirectory": "skills/writing-plans", + "installByDefault": true, + "license": "MIT", + "licenseNotes": "Upstream repository declares MIT.", + "updateNotes": "Pinned to a verified main commit. Revalidate raw URLs before changing this ref.", + "files": [ + { + "sourcePath": "skills/writing-plans/SKILL.md", + "destinationPath": "SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/f2cbfbefebbfef77321e4c9abc9e949826bea9d7/skills/writing-plans/SKILL.md" + } + ] + }, + { + "name": "subagent-driven-development", + "sourceRepo": "obra/superpowers", + "ref": "f2cbfbefebbfef77321e4c9abc9e949826bea9d7", + "sourceDirectory": "skills/subagent-driven-development", + "installByDefault": true, + "license": "MIT", + "licenseNotes": "Upstream repository declares MIT.", + "updateNotes": "Pinned to a verified main commit. Revalidate raw URLs before changing this ref.", + "files": [ + { + "sourcePath": "skills/subagent-driven-development/SKILL.md", + "destinationPath": "SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/f2cbfbefebbfef77321e4c9abc9e949826bea9d7/skills/subagent-driven-development/SKILL.md" + }, + { + "sourcePath": "skills/subagent-driven-development/implementer-prompt.md", + "destinationPath": "implementer-prompt.md", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/f2cbfbefebbfef77321e4c9abc9e949826bea9d7/skills/subagent-driven-development/implementer-prompt.md" + }, + { + "sourcePath": "skills/subagent-driven-development/spec-reviewer-prompt.md", + "destinationPath": "spec-reviewer-prompt.md", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/f2cbfbefebbfef77321e4c9abc9e949826bea9d7/skills/subagent-driven-development/spec-reviewer-prompt.md" + }, + { + "sourcePath": "skills/subagent-driven-development/code-quality-reviewer-prompt.md", + "destinationPath": "code-quality-reviewer-prompt.md", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/f2cbfbefebbfef77321e4c9abc9e949826bea9d7/skills/subagent-driven-development/code-quality-reviewer-prompt.md" + } + ] + }, + { + "name": "requesting-code-review", + "sourceRepo": "obra/superpowers", + "ref": "f2cbfbefebbfef77321e4c9abc9e949826bea9d7", + "sourceDirectory": "skills/requesting-code-review", + "installByDefault": true, + "license": "MIT", + "licenseNotes": "Upstream repository declares MIT.", + "updateNotes": "Pinned to a verified main commit. Revalidate raw URLs before changing this ref.", + "files": [ + { + "sourcePath": "skills/requesting-code-review/SKILL.md", + "destinationPath": "SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/f2cbfbefebbfef77321e4c9abc9e949826bea9d7/skills/requesting-code-review/SKILL.md" + } + ] + }, + { + "name": "verification-before-completion", + "sourceRepo": "obra/superpowers", + "ref": "f2cbfbefebbfef77321e4c9abc9e949826bea9d7", + "sourceDirectory": "skills/verification-before-completion", + "installByDefault": true, + "license": "MIT", + "licenseNotes": "Upstream repository declares MIT.", + "updateNotes": "Pinned to a verified main commit. Revalidate raw URLs before changing this ref.", + "files": [ + { + "sourcePath": "skills/verification-before-completion/SKILL.md", + "destinationPath": "SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/f2cbfbefebbfef77321e4c9abc9e949826bea9d7/skills/verification-before-completion/SKILL.md" + } + ] + }, + { + "name": "test-driven-development", + "sourceRepo": "obra/superpowers", + "ref": "f2cbfbefebbfef77321e4c9abc9e949826bea9d7", + "sourceDirectory": "skills/test-driven-development", + "installByDefault": true, + "license": "MIT", + "licenseNotes": "Upstream repository declares MIT.", + "updateNotes": "Pinned to a verified main commit. Revalidate raw URLs before changing this ref.", + "files": [ + { + "sourcePath": "skills/test-driven-development/SKILL.md", + "destinationPath": "SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/f2cbfbefebbfef77321e4c9abc9e949826bea9d7/skills/test-driven-development/SKILL.md" + } + ] + }, + { + "name": "systematic-debugging", + "sourceRepo": "obra/superpowers", + "ref": "f2cbfbefebbfef77321e4c9abc9e949826bea9d7", + "sourceDirectory": "skills/systematic-debugging", + "installByDefault": true, + "license": "MIT", + "licenseNotes": "Upstream repository declares MIT.", + "updateNotes": "Pinned to a verified main commit. Revalidate raw URLs before changing this ref.", + "files": [ + { + "sourcePath": "skills/systematic-debugging/SKILL.md", + "destinationPath": "SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/f2cbfbefebbfef77321e4c9abc9e949826bea9d7/skills/systematic-debugging/SKILL.md" + } + ] + } + ] +} diff --git a/integrations/cursor/install.md b/integrations/cursor/install.md new file mode 100644 index 0000000..eab677d --- /dev/null +++ b/integrations/cursor/install.md @@ -0,0 +1,25 @@ +# Cursor Install + +Status: experimental + +The Cursor target is a future/manual adapter scaffold. Cursor-specific rule or extension metadata is not included in v0.1 because this repo has not validated a native install path. + +## Install + +```bash +npx basd-coding-dispatch init --target cursor --dir +``` + +## Expected Files + +- `AGENTS.md` +- `llms.txt` +- `skills/basd-coding-dispatch/SKILL.md` +- `skills/basd-coding-dispatch/references/README.md` +- `skills/basd-coding-dispatch/references/*.md` +- `references/*.md` +- `integrations/cursor/install.md` + +## Usage + +Reference `AGENTS.md` and the skill in Cursor chat or project instructions. Keep any editor-specific behavior separate from the Hermes/OpenClaw dispatch process until a native adapter is validated. diff --git a/integrations/generic-agent/install.md b/integrations/generic-agent/install.md new file mode 100644 index 0000000..e412aaf --- /dev/null +++ b/integrations/generic-agent/install.md @@ -0,0 +1,26 @@ +# Generic Agent Install + +Status: tested + +The generic target is a manual file-based scaffold for agents that can read project instructions and Markdown skills. It is useful outside Hermes/OpenClaw, or when a project wants the governance files checked into its own repository. The package smoke test verifies that the files are created and overwrite protection works. + +## Install + +```bash +npx basd-coding-dispatch init --target generic-agent --dir +``` + +## Expected Files + +- `AGENTS.md` +- `CLAUDE.md` +- `llms.txt` +- `skills/basd-coding-dispatch/SKILL.md` +- `skills/basd-coding-dispatch/references/README.md` +- `skills/basd-coding-dispatch/references/*.md` +- `references/*.md` +- `integrations/generic-agent/install.md` + +## Usage + +Tell the agent to read `AGENTS.md`, then use `skills/basd-coding-dispatch/SKILL.md` for the dispatch loop. Load skill-local references first when running from the installed skill directory, and use root-level `references/` for repo browsing. diff --git a/integrations/hermes/install.md b/integrations/hermes/install.md new file mode 100644 index 0000000..211470f --- /dev/null +++ b/integrations/hermes/install.md @@ -0,0 +1,56 @@ +# Hermes Install + +Status: tested + +Hermes is the maintained home for v0.1. This target installs the complete `basd-coding-dispatch` skill directory into the Hermes skills directory, including `SKILL.md` and skill-local `references/` files. + +By default, the installer also installs pinned companion skills from `integrations/companion-skills.json` when they are missing. These include Hermes Codex/Claude Code worker guides and Superpowers process skills for planning, review, TDD, debugging, and verification. Default companion installs fetch from `raw.githubusercontent.com`, so they require network access to GitHub raw content. + +## Install + +```bash +npx basd-coding-dispatch init +``` + +To install only `basd-coding-dispatch` and skip companion skills for the offline/core-only path: + +```bash +npx basd-coding-dispatch init --skip-companion-skills +npx basd-coding-dispatch init --no-companion-skills +``` + +Destination resolution: + +- `--dir ` treats `` as the Hermes home root and installs under `/skills/basd-coding-dispatch/`. +- Without `--dir`, `$HERMES_HOME` is used when set. +- Without `--dir` or `$HERMES_HOME`, the default is `~/.hermes/skills/basd-coding-dispatch/`. + +## Expected Skill Files + +- `skills/basd-coding-dispatch/SKILL.md` +- `skills/basd-coding-dispatch/references/README.md` +- `skills/basd-coding-dispatch/references/quality-gates.md` +- `skills/basd-coding-dispatch/references/provider-command-recipes.md` +- `skills/basd-coding-dispatch/references/session-topology.md` +- `skills/basd-coding-dispatch/references/review-orchestration.md` +- `skills/basd-coding-dispatch/references/subagent-skill-bundles.md` + +Default companion skills install under sibling directories such as `skills/codex/`, `skills/claude-code/`, and `skills/using-superpowers/`. Existing companion skills are skipped unless `--force` is provided. + +The installer refuses to overwrite existing core `basd-coding-dispatch` skill files without `--force`. + +## Use From Hermes + +After install, check discovery with: + +```bash +hermes skills list +``` + +Then start a session with the skill, for example: + +```text +/skill basd-coding-dispatch +``` + +Use Telegram or another mobile chat surface as the control plane, then let Hermes route approved implementation work to Codex, Claude, or another validated worker backend. diff --git a/integrations/openclaw/install.md b/integrations/openclaw/install.md new file mode 100644 index 0000000..6183d92 --- /dev/null +++ b/integrations/openclaw/install.md @@ -0,0 +1,59 @@ +# OpenClaw Install + +Status: tested + +OpenClaw users can run the same dispatch workflow as a workspace skill. Local evidence for v0.1 verifies OpenClaw 2026.5.2 against the workspace-skill layout under `~/openclaw-workspace/skills/...`. + +Hermes remains the maintained home. OpenClaw support is documented as workspace-skill compatible, not as a marketplace, plugin, or ClawHub package. After installing, verify discovery on your local OpenClaw runtime with `openclaw skills list` and `openclaw skills info basd-coding-dispatch`. + +By default, the installer also installs pinned companion skills from `integrations/companion-skills.json` when they are missing. These include Hermes Codex/Claude Code worker guides and Superpowers process skills for planning, review, TDD, debugging, and verification. Default companion installs fetch from `raw.githubusercontent.com`, so they require network access to GitHub raw content. + +## Install + +```bash +npx basd-coding-dispatch init --target openclaw +``` + +To install only `basd-coding-dispatch` and skip companion skills for the offline/core-only path: + +```bash +npx basd-coding-dispatch init --target openclaw --skip-companion-skills +npx basd-coding-dispatch init --target openclaw --no-companion-skills +``` + +Destination resolution: + +- `--dir ` treats `` as the OpenClaw workspace root and writes under `/skills/`. +- Without `--dir`, `$OPENCLAW_WORKSPACE` is used when set. +- Without `--dir` or `$OPENCLAW_WORKSPACE`, the default is `~/openclaw-workspace/skills/basd-coding-dispatch/`. + +Runtime discovery depends on the workspace that OpenClaw is actually using. If `openclaw skills list` does not show the skill, check the active OpenClaw workspace path before reinstalling. + +## Expected Skill Files + +- `skills/basd-coding-dispatch/SKILL.md` +- `skills/basd-coding-dispatch/references/README.md` +- `skills/basd-coding-dispatch/references/quality-gates.md` +- `skills/basd-coding-dispatch/references/provider-command-recipes.md` +- `skills/basd-coding-dispatch/references/session-topology.md` +- `skills/basd-coding-dispatch/references/review-orchestration.md` +- `skills/basd-coding-dispatch/references/subagent-skill-bundles.md` + +Default companion skills install under sibling directories such as `skills/codex/`, `skills/claude-code/`, and `skills/using-superpowers/`. Existing companion skills are skipped unless `--force` is provided. + +The installer refuses to overwrite existing core `basd-coding-dispatch` skill files without `--force`. + +## Use From OpenClaw + +Check skill discovery with: + +```bash +openclaw skills list +openclaw skills info basd-coding-dispatch +``` + +Then start a session with the skill, for example: + +```text +/skill basd-coding-dispatch +``` diff --git a/integrations/opencode/install.md b/integrations/opencode/install.md new file mode 100644 index 0000000..7b8319b --- /dev/null +++ b/integrations/opencode/install.md @@ -0,0 +1,25 @@ +# OpenCode Install + +Status: experimental + +The OpenCode target is a future/manual adapter scaffold. Native extension metadata is planned only after the install path can be validated locally. + +## Install + +```bash +npx basd-coding-dispatch init --target opencode --dir +``` + +## Expected Files + +- `AGENTS.md` +- `llms.txt` +- `skills/basd-coding-dispatch/SKILL.md` +- `skills/basd-coding-dispatch/references/README.md` +- `skills/basd-coding-dispatch/references/*.md` +- `references/*.md` +- `integrations/opencode/install.md` + +## Usage + +Use the copied files as explicit session instructions. Record any OpenCode-specific command behavior in project docs rather than the portable skill. diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..e983461 --- /dev/null +++ b/llms.txt @@ -0,0 +1,23 @@ +# basd-coding-dispatch + +Purpose: Hermes/OpenClaw-first coding-dispatch skill for approval-gated mobile and Telegram coding workflows. + +Primary files: +- README.md +- skills/basd-coding-dispatch/SKILL.md +- references/quality-gates.md +- references/provider-command-recipes.md +- references/session-topology.md +- references/review-orchestration.md +- references/subagent-skill-bundles.md +- integrations/hermes/install.md +- integrations/openclaw/install.md +- integrations/*/install.md +- examples/*.md + +Rules for agents: +- Classify the task before worker routing. +- Use spec and implementation-plan gates for non-trivial work. +- Keep provider mechanics outside the portable skill. +- Verify before completion. +- Do not include private data, credentials, or unvalidated native metadata. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2bb320d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,19 @@ +{ + "name": "basd-coding-dispatch", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "basd-coding-dispatch", + "version": "0.1.0", + "license": "MIT", + "bin": { + "basd-coding-dispatch": "bin/basd-coding-dispatch.mjs" + }, + "engines": { + "node": ">=22" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b317f43 --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "name": "basd-coding-dispatch", + "version": "0.1.0", + "description": "Hermes/OpenClaw provider-agnostic coding dispatch skill for approval-gated AI coding from any channel.", + "repository": { + "type": "git", + "url": "git+https://github.com/damienen/basd-coding-dispatch.git" + }, + "homepage": "https://github.com/damienen/basd-coding-dispatch#readme", + "bugs": { + "url": "https://github.com/damienen/basd-coding-dispatch/issues" + }, + "type": "module", + "bin": { + "basd-coding-dispatch": "./bin/basd-coding-dispatch.mjs" + }, + "scripts": { + "validate": "node scripts/validate.mjs", + "smoke-test": "node scripts/smoke-test.mjs" + }, + "files": [ + ".github", + "AGENTS.md", + "CLAUDE.md", + "CHANGELOG.md", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + "LICENSE", + "NOTICE", + "README.md", + "SECURITY.md", + "assets", + "bin", + "examples", + "integrations", + "llms.txt", + "references", + "scripts", + "skills" + ], + "keywords": [ + "ai-coding", + "agents", + "hermes", + "openclaw", + "telegram", + "mobile", + "codex", + "claude", + "workflow", + "subagents", + "worker-routing" + ], + "author": "Basd", + "license": "MIT", + "engines": { + "node": ">=22" + } +} diff --git a/references/provider-command-recipes.md b/references/provider-command-recipes.md new file mode 100644 index 0000000..6e37623 --- /dev/null +++ b/references/provider-command-recipes.md @@ -0,0 +1,52 @@ +# Provider Command Recipes + +These recipes keep Hermes/OpenClaw dispatch portable while worker backends vary by project. + +## Install Native Skill Targets + +```bash +npx basd-coding-dispatch init +npx basd-coding-dispatch init --dir "$HERMES_HOME" +npx basd-coding-dispatch init --target openclaw --dir ~/openclaw-workspace +``` + +Use `--force` only when you have reviewed the installed skill files and intentionally want to replace them. + +## Initialize Manual Workflow Files + +```bash +npx basd-coding-dispatch init --target generic-agent --dir ./project +npx basd-coding-dispatch init --target codex --dir ./project +npx basd-coding-dispatch init --target claude-code --dir ./project +``` + +Codex and Claude are supported worker backends. Their native harness adapters remain manual or experimental unless the install path has been validated. + +## Validate the Package + +```bash +npm ci +npm run validate +npm run smoke-test +npm pack --dry-run +``` + +## Dispatch Prompt Shape + +```text +Classify this request. +Choose the worker-routing shape. +Produce the spec gate if needed. +Produce the implementation-plan gate before code. +Implement only after the gate is accepted. +Use independent review for meaningful code changes. +Verify before reporting completion. +``` + +## Split-Worker Shape + +```text +Worker A: implement the approved plan. +Worker B: review the diff for correctness, tests, scope, docs, and leakage. +Dispatcher: reconcile findings, apply accepted fixes, run verification, and report. +``` diff --git a/references/quality-gates.md b/references/quality-gates.md new file mode 100644 index 0000000..4bc601f --- /dev/null +++ b/references/quality-gates.md @@ -0,0 +1,75 @@ +# Quality Gates + +Quality gates keep AI coding work explicit enough for a human or another agent to evaluate. + +## 1. Classification Gate + +Every request should be classified before work starts: + +- Question or explanation. +- Small fix. +- Feature build. +- Refactor. +- Debugging or incident investigation. +- Code review. +- Release or packaging task. +- Provider integration task. + +The classification determines how much ceremony is needed. + +## 2. Worker-Routing Gate + +Choose the worker shape: + +- Hermes/OpenClaw dispatcher controls the gates. +- Codex implements. +- Claude implements. +- One worker implements and another reviews. +- Dispatcher coordinates multiple subagents. +- Generic agent follows the portable skill. + +Record the reason when the task is risky or split. + +## 3. Spec Gate + +Use a spec gate when a task is broad, ambiguous, user-facing, public, security-sensitive, or likely to change architecture. + +A useful spec includes: + +- Problem statement. +- Non-goals. +- User-visible behavior. +- Constraints. +- Acceptance criteria. + +## 4. Implementation-Plan Gate + +Use an implementation-plan gate before feature code. A useful plan names: + +- Files to create or modify. +- Behavioral changes. +- Tests and validation commands. +- Review strategy. +- Rollback or recovery concerns. + +## 5. Review Gate + +Meaningful implementation work needs independent review. The reviewer should look for: + +- Behavioral regressions. +- Missing tests. +- Unsafe worker or provider assumptions. +- Docs that overclaim tested behavior. +- Private data or credential leakage. + +## 6. Verification Gate + +Completion requires concrete evidence: + +- Commands and exit statuses. +- Test output. +- Manual transcript or example. +- Screenshots only when visual verification matters. +- Clear note for skipped checks. + +Do not claim done when verification is missing. diff --git a/references/review-orchestration.md b/references/review-orchestration.md new file mode 100644 index 0000000..0f77311 --- /dev/null +++ b/references/review-orchestration.md @@ -0,0 +1,36 @@ +# Review Orchestration + +Independent review is useful only when the reviewer has a clear job and enough evidence. + +## Reviewer Brief + +Give the reviewer: + +- The approved spec. +- The approved implementation plan. +- The diff or changed file list. +- Expected verification commands. +- Known constraints and non-goals. + +Do not give the reviewer a desired conclusion. + +## Review Checklist + +Ask for findings first: + +- Correctness bugs. +- Regressions. +- Missing tests or insufficient verification. +- Worker-specific assumptions that should be documented. +- Public docs that overclaim tested behavior. +- Private data or credential leakage. + +## Dispatcher Reconciliation + +The dispatcher should: + +1. Accept findings that are concrete and reproducible. +2. Reject findings that contradict the approved scope, with a short reason. +3. Apply fixes. +4. Re-run verification. +5. Report remaining risk. diff --git a/references/session-topology.md b/references/session-topology.md new file mode 100644 index 0000000..193df6f --- /dev/null +++ b/references/session-topology.md @@ -0,0 +1,50 @@ +# Session Topology + +## Hermes/OpenClaw Dispatcher + +Use Hermes or an OpenClaw workspace skill as the maintained dispatch layer: + +- A mobile or Telegram request reaches the dispatcher. +- The dispatcher classifies the task and chooses the worker-routing shape. +- Codex, Claude, or another validated worker backend executes the approved work. +- Review and verification evidence returns to the human before completion is claimed. + +## Single Worker + +Use one worker for narrow tasks: + +- Small bugfix. +- Documentation correction. +- Focused validation improvement. +- Single-file cleanup. + +The worker still runs classification, planning, verification, and reporting. + +## Implementer Plus Reviewer + +Use one implementer and one independent reviewer when: + +- The change affects user-facing behavior. +- The change touches release, install, or security paths. +- The code is easy to overfit to one provider. +- The plan has multiple files or unclear risk. + +## Dispatcher With Subagents + +Use a dispatcher when the work has parallel tracks: + +- Documentation and CLI can be implemented independently. +- One agent can inspect provider docs while another implements generic logic. +- A reviewer can run while implementation continues on a disjoint area. + +The dispatcher owns final integration and verification. + +## Mobile Control + +Mobile or chat-based control can work when the gates are preserved: + +- Short command from the human. +- Dispatcher asks only the necessary clarifying questions. +- Spec and plan are posted back for approval. +- Implementation happens in the coding environment. +- Verification output is summarized back to the mobile channel. diff --git a/references/subagent-skill-bundles.md b/references/subagent-skill-bundles.md new file mode 100644 index 0000000..e1a8acf --- /dev/null +++ b/references/subagent-skill-bundles.md @@ -0,0 +1,35 @@ +# Subagent Skill Bundles + +Use focused bundles so subagents do not duplicate work. + +## Implementation Worker + +Use for bounded file ownership: + +- Inputs: spec, implementation plan, owned files, verification command. +- Output: changed files, verification result, risk. +- Rule: do not edit files outside ownership without dispatcher approval. + +## Review Worker + +Use for independent review: + +- Inputs: spec, plan, diff, verification commands. +- Output: ordered findings with file and line references where possible. +- Rule: findings first, summary second. + +## Docs Worker + +Use for public docs: + +- Inputs: audience, files, tested status, banned claims. +- Output: concise docs that match verified behavior. +- Rule: never imply native marketplace support before validation. + +## Integration Worker + +Use for provider adaptation: + +- Inputs: target tool, install mechanism, manual install attempt, validation evidence. +- Output: install notes and status label. +- Rule: mark uncertain native support as experimental or planned. diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs new file mode 100755 index 0000000..9e42763 --- /dev/null +++ b/scripts/smoke-test.mjs @@ -0,0 +1,701 @@ +#!/usr/bin/env node + +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { main } from "../bin/basd-coding-dispatch.mjs"; + +const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const cliPath = path.join(packageRoot, "bin", "basd-coding-dispatch.mjs"); +const companionManifestPath = path.join(packageRoot, "integrations", "companion-skills.json"); + +const manualAdapterTargets = [ + "codex", + "claude-code", + "opencode", + "cursor", + "generic-agent" +]; + +const skillReferenceFiles = [ + "quality-gates.md", + "provider-command-recipes.md", + "session-topology.md", + "review-orchestration.md", + "subagent-skill-bundles.md" +]; + +const companionSkillNames = [ + "codex", + "claude-code", + "using-superpowers", + "brainstorming", + "writing-plans", + "subagent-driven-development", + "requesting-code-review", + "verification-before-completion", + "test-driven-development", + "systematic-debugging" +]; + +const subagentDrivenDevelopmentPromptFiles = [ + "implementer-prompt.md", + "spec-reviewer-prompt.md", + "code-quality-reviewer-prompt.md" +]; + +const fixtureFiles = [ + { + repo: "NousResearch/hermes-agent", + path: "skills/autonomous-ai-agents/codex/SKILL.md", + content: "---\nname: codex\n---\n# Codex fixture\n" + }, + { + repo: "NousResearch/hermes-agent", + path: "skills/autonomous-ai-agents/claude-code/SKILL.md", + content: "---\nname: claude-code\n---\n# Claude Code fixture\n" + }, + ...[ + "using-superpowers", + "brainstorming", + "writing-plans", + "subagent-driven-development", + "requesting-code-review", + "verification-before-completion", + "test-driven-development", + "systematic-debugging" + ].map((name) => ({ + repo: "obra/superpowers", + path: `skills/${name}/SKILL.md`, + content: `---\nname: ${name}\n---\n# ${name} fixture\n` + })), + ...subagentDrivenDevelopmentPromptFiles.map((file) => ({ + repo: "obra/superpowers", + path: `skills/subagent-driven-development/${file}`, + content: `# ${file} fixture\n` + })) +]; + +function captureWrite(chunks) { + return (chunk, encoding, callback) => { + chunks.push(String(chunk)); + + if (typeof encoding === "function") { + encoding(); + } + if (typeof callback === "function") { + callback(); + } + + return true; + }; +} + +async function run(args) { + const stdoutChunks = []; + const stderrChunks = []; + const originalStdoutWrite = process.stdout.write; + const originalStderrWrite = process.stderr.write; + + process.stdout.write = captureWrite(stdoutChunks); + process.stderr.write = captureWrite(stderrChunks); + + try { + const status = await main(args); + const stdout = stdoutChunks.join(""); + const stderr = stderrChunks.join(""); + + return { + status, + stdout, + stderr, + combined: `${stdout}${stderr}` + }; + } finally { + process.stdout.write = originalStdoutWrite; + process.stderr.write = originalStderrWrite; + } +} + +function assertSuccess(result, label) { + assert.equal( + result.status, + 0, + `${label} failed with status ${result.status}\n${result.combined}` + ); +} + +function assertFailure(result, label) { + assert.notEqual(result.status, 0, `${label} unexpectedly succeeded`); +} + +function assertInstalledSkill(root, label) { + const skillRoot = path.join(root, "skills", "basd-coding-dispatch"); + + assert.ok( + existsSync(path.join(skillRoot, "SKILL.md")), + `skill missing for ${label}` + ); + + for (const reference of skillReferenceFiles) { + assert.ok( + existsSync(path.join(skillRoot, "references", reference)), + `skill reference ${reference} missing for ${label}` + ); + } +} + +function assertInstalledCompanionSkills(root, label) { + for (const skillName of companionSkillNames) { + assert.ok( + existsSync(path.join(root, "skills", skillName, "SKILL.md")), + `companion skill ${skillName} missing for ${label}` + ); + } + + for (const file of subagentDrivenDevelopmentPromptFiles) { + assert.ok( + existsSync(path.join(root, "skills", "subagent-driven-development", file)), + `subagent-driven-development prompt ${file} missing for ${label}` + ); + } +} + +function assertMissingCompanionSkills(root, label) { + for (const skillName of companionSkillNames) { + assert.ok( + !existsSync(path.join(root, "skills", skillName, "SKILL.md")), + `companion skill ${skillName} should be absent for ${label}` + ); + } + + for (const file of subagentDrivenDevelopmentPromptFiles) { + assert.ok( + !existsSync(path.join(root, "skills", "subagent-driven-development", file)), + `subagent-driven-development prompt ${file} should be absent for ${label}` + ); + } +} + +async function writeFixtureFiles(root) { + for (const file of fixtureFiles) { + const fixturePath = path.join(root, file.repo, file.path); + await mkdir(path.dirname(fixturePath), { recursive: true }); + await writeFile(fixturePath, file.content, "utf8"); + } +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function runSubprocess(args, options = {}) { + const result = spawnSync(process.execPath, [cliPath, ...args], { + cwd: options.cwd ?? packageRoot, + env: options.env ?? process.env, + encoding: "utf8", + timeout: 10000 + }); + + return { + status: result.status, + stdout: result.stdout, + stderr: result.stderr, + combined: `${result.stdout}${result.stderr}` + }; +} + +function canRunNodeSubprocess() { + const result = spawnSync(process.execPath, ["-e", "process.stdout.write('ok')"], { + encoding: "utf8", + timeout: 10000 + }); + + return result.status === 0 && result.stdout === "ok"; +} + +const scratch = await mkdtemp(path.join(os.tmpdir(), "basd-smoke-")); +const originalCompanionFixtureRoot = process.env.BASD_COMPANION_SKILLS_FIXTURE_DIR; + +try { + assert.ok(existsSync(companionManifestPath), "companion skill manifest is missing"); + + const companionFixtureRoot = path.join(scratch, "companion-fixtures"); + await writeFixtureFiles(companionFixtureRoot); + process.env.BASD_COMPANION_SKILLS_FIXTURE_DIR = companionFixtureRoot; + + const help = await run(["help"]); + assertSuccess(help, "help"); + assert.match(help.stdout, /basd-coding-dispatch init/); + assert.match(help.stdout, /hermes\s+Native Hermes skill install.*\(default\)/); + assert.match(help.stdout, /openclaw/); + assert.match(help.stdout, /--skip-companion-skills/); + assert.match(help.stdout, /--no-companion-skills/); + + const unknown = await run(["missing-command"]); + assertFailure(unknown, "unknown command"); + assert.match(unknown.combined, /Unknown command/); + assert.match(unknown.combined, /basd-coding-dispatch help/); + + const doctor = await run(["doctor"]); + assertSuccess(doctor, "doctor"); + assert.match(doctor.stdout, /source: ok/); + assert.match(doctor.stdout, /companion manifest: ok/); + assert.match(doctor.stdout, /hermes cli:/); + assert.match(doctor.stdout, /hermes home:/); + assert.doesNotMatch(doctor.stdout, /hermes root:/); + assert.match(doctor.stdout, /hermes companion skills:/); + assert.match(doctor.stdout, /openclaw cli:/); + assert.match(doctor.stdout, /openclaw workspace:/); + assert.match(doctor.stdout, /openclaw companion skills:/); + assert.match(doctor.stdout, /Doctor passed/); + + const originalPathForDoctor = process.env.PATH; + try { + const fakeBin = path.join(scratch, "fake-bin"); + await mkdir(path.join(fakeBin, "hermes"), { recursive: true }); + await mkdir(path.join(fakeBin, "openclaw"), { recursive: true }); + process.env.PATH = fakeBin; + + const weirdDoctor = await run(["doctor"]); + assertSuccess(weirdDoctor, "doctor handles non-ENOENT spawn errors"); + assert.match(weirdDoctor.stdout, /hermes cli: error \(/); + assert.doesNotMatch(weirdDoctor.stdout, /hermes cli: present \(/); + assert.match(weirdDoctor.stdout, /openclaw cli: error \(/); + assert.doesNotMatch(weirdDoctor.stdout, /openclaw cli: present \(/); + } finally { + if (originalPathForDoctor === undefined) { + delete process.env.PATH; + } else { + process.env.PATH = originalPathForDoctor; + } + } + + const validate = await run(["validate"]); + assertSuccess(validate, "validate"); + assert.match(validate.stdout, /Validation passed/); + + const cliSource = await readFile(cliPath, "utf8"); + assert.doesNotMatch(cliSource, /function shellQuote/); + assert.doesNotMatch(cliSource, /shell:\s*true/); + assert.doesNotMatch(cliSource, /command -v/); + + const originalCwd = process.cwd(); + const originalHome = process.env.HOME; + const originalHermesHomeForTilde = process.env.HERMES_HOME; + const originalOpenClawWorkspaceForTilde = process.env.OPENCLAW_WORKSPACE; + const fakeHome = path.join(scratch, "fake-home"); + + try { + process.chdir(scratch); + process.env.HOME = fakeHome; + process.env.HERMES_HOME = "~/.hermes-from-env"; + process.env.OPENCLAW_WORKSPACE = "~/openclaw-from-env"; + + const hermesTildeEnv = await run(["init"]); + assertSuccess(hermesTildeEnv, "bare init expands HERMES_HOME tilde"); + assert.match( + hermesTildeEnv.stdout, + new RegExp(`root: ${escapeRegExp(path.join(fakeHome, ".hermes-from-env"))}`) + ); + assertInstalledSkill(path.join(fakeHome, ".hermes-from-env"), "hermes tilde env"); + assertInstalledCompanionSkills( + path.join(fakeHome, ".hermes-from-env"), + "hermes tilde env" + ); + + const openclawTildeEnv = await run(["init", "--target", "openclaw"]); + assertSuccess(openclawTildeEnv, "openclaw init expands OPENCLAW_WORKSPACE tilde"); + assert.match( + openclawTildeEnv.stdout, + new RegExp(`root: ${escapeRegExp(path.join(fakeHome, "openclaw-from-env"))}`) + ); + assertInstalledSkill(path.join(fakeHome, "openclaw-from-env"), "openclaw tilde env"); + assertInstalledCompanionSkills( + path.join(fakeHome, "openclaw-from-env"), + "openclaw tilde env" + ); + + const openclawTildeDir = await run([ + "init", + "--target", + "openclaw", + "--dir", + "~/openclaw-from-dir" + ]); + assertSuccess(openclawTildeDir, "openclaw init expands --dir tilde"); + assert.match( + openclawTildeDir.stdout, + new RegExp(`root: ${escapeRegExp(path.join(fakeHome, "openclaw-from-dir"))}`) + ); + assertInstalledSkill(path.join(fakeHome, "openclaw-from-dir"), "openclaw tilde dir"); + assertInstalledCompanionSkills( + path.join(fakeHome, "openclaw-from-dir"), + "openclaw tilde dir" + ); + + const manualTildeDir = await run([ + "init", + "--target", + "generic-agent", + "--dir", + "~/.generic-agent-target" + ]); + assertSuccess(manualTildeDir, "generic-agent init expands --dir tilde"); + assert.match( + manualTildeDir.stdout, + new RegExp(`dir: ${escapeRegExp(path.join(fakeHome, ".generic-agent-target"))}`) + ); + assertInstalledSkill(path.join(fakeHome, ".generic-agent-target"), "generic-agent tilde dir"); + assertMissingCompanionSkills( + path.join(fakeHome, ".generic-agent-target"), + "generic-agent tilde dir" + ); + assert.ok( + !existsSync(path.join(scratch, "~")), + "generic-agent tilde dir should not create a literal ./~ directory" + ); + } finally { + process.chdir(originalCwd); + + if (originalHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = originalHome; + } + + if (originalHermesHomeForTilde === undefined) { + delete process.env.HERMES_HOME; + } else { + process.env.HERMES_HOME = originalHermesHomeForTilde; + } + + if (originalOpenClawWorkspaceForTilde === undefined) { + delete process.env.OPENCLAW_WORKSPACE; + } else { + process.env.OPENCLAW_WORKSPACE = originalOpenClawWorkspaceForTilde; + } + } + + if (canRunNodeSubprocess()) { + const subprocessHome = path.join(scratch, "fake-home-subprocess"); + const subprocessEnv = { + ...process.env, + HOME: subprocessHome, + HERMES_HOME: "~/.hermes-from-env-subprocess", + OPENCLAW_WORKSPACE: "~/openclaw-from-env-subprocess" + }; + + const hermesTildeEnv = runSubprocess(["init"], { env: subprocessEnv, cwd: scratch }); + assertSuccess(hermesTildeEnv, "subprocess bare init expands HERMES_HOME tilde"); + assert.match( + hermesTildeEnv.stdout, + new RegExp(`root: ${escapeRegExp(path.join(subprocessHome, ".hermes-from-env-subprocess"))}`) + ); + assertInstalledSkill( + path.join(subprocessHome, ".hermes-from-env-subprocess"), + "subprocess hermes tilde env" + ); + assertInstalledCompanionSkills( + path.join(subprocessHome, ".hermes-from-env-subprocess"), + "subprocess hermes tilde env" + ); + + const openclawTildeEnv = runSubprocess(["init", "--target", "openclaw"], { + env: subprocessEnv, + cwd: scratch + }); + assertSuccess( + openclawTildeEnv, + "subprocess openclaw init expands OPENCLAW_WORKSPACE tilde" + ); + assert.match( + openclawTildeEnv.stdout, + new RegExp(`root: ${escapeRegExp(path.join(subprocessHome, "openclaw-from-env-subprocess"))}`) + ); + assertInstalledSkill( + path.join(subprocessHome, "openclaw-from-env-subprocess"), + "subprocess openclaw tilde env" + ); + assertInstalledCompanionSkills( + path.join(subprocessHome, "openclaw-from-env-subprocess"), + "subprocess openclaw tilde env" + ); + + const openclawTildeDir = runSubprocess( + ["init", "--target", "openclaw", "--dir", "~/openclaw-from-dir-subprocess"], + { env: { ...process.env, HOME: subprocessHome }, cwd: scratch } + ); + assertSuccess(openclawTildeDir, "subprocess openclaw init expands --dir tilde"); + assert.match( + openclawTildeDir.stdout, + new RegExp(`root: ${escapeRegExp(path.join(subprocessHome, "openclaw-from-dir-subprocess"))}`) + ); + assertInstalledSkill( + path.join(subprocessHome, "openclaw-from-dir-subprocess"), + "subprocess openclaw tilde dir" + ); + assertInstalledCompanionSkills( + path.join(subprocessHome, "openclaw-from-dir-subprocess"), + "subprocess openclaw tilde dir" + ); + + const manualTildeDir = runSubprocess( + ["init", "--target", "generic-agent", "--dir", "~/.generic-agent-target-subprocess"], + { env: { ...process.env, HOME: subprocessHome }, cwd: scratch } + ); + assertSuccess(manualTildeDir, "subprocess generic-agent init expands --dir tilde"); + assert.match( + manualTildeDir.stdout, + new RegExp(`dir: ${escapeRegExp(path.join(subprocessHome, ".generic-agent-target-subprocess"))}`) + ); + assertInstalledSkill( + path.join(subprocessHome, ".generic-agent-target-subprocess"), + "subprocess generic-agent tilde dir" + ); + assertMissingCompanionSkills( + path.join(subprocessHome, ".generic-agent-target-subprocess"), + "subprocess generic-agent tilde dir" + ); + assert.ok( + !existsSync(path.join(scratch, "~")), + "subprocess generic-agent tilde dir should not create a literal ./~ directory" + ); + } + + const hermesHome = path.join(scratch, "hermes-home"); + const hermesInit = await run(["init", "--target", "hermes", "--dir", hermesHome]); + assertSuccess(hermesInit, "init hermes"); + assert.match(hermesInit.stdout, /target: hermes/); + assert.match(hermesInit.stdout, /root:/); + assert.match(hermesInit.stdout, /next: hermes skills list/); + assertInstalledSkill(hermesHome, "hermes"); + assertInstalledCompanionSkills(hermesHome, "hermes"); + assert.ok( + !existsSync(path.join(hermesHome, "AGENTS.md")), + "hermes target should install only the native skill directory" + ); + + const hermesSkipHome = path.join(scratch, "hermes-skip-home"); + const hermesSkipInit = await run([ + "init", + "--target", + "hermes", + "--dir", + hermesSkipHome, + "--skip-companion-skills" + ]); + assertSuccess(hermesSkipInit, "init hermes skips companion skills"); + assert.match(hermesSkipInit.stdout, /companion skills: skipped by flag/); + assertInstalledSkill(hermesSkipHome, "hermes skip"); + assertMissingCompanionSkills(hermesSkipHome, "hermes skip"); + + const openclawNoCompanionWorkspace = path.join(scratch, "openclaw-no-companion"); + const openclawNoCompanionInit = await run([ + "init", + "--target", + "openclaw", + "--dir", + openclawNoCompanionWorkspace, + "--no-companion-skills" + ]); + assertSuccess(openclawNoCompanionInit, "init openclaw skips companion skills alias"); + assert.match(openclawNoCompanionInit.stdout, /companion skills: skipped by flag/); + assertInstalledSkill(openclawNoCompanionWorkspace, "openclaw no companion"); + assertMissingCompanionSkills(openclawNoCompanionWorkspace, "openclaw no companion"); + + const openclawWorkspace = path.join(scratch, "openclaw-workspace"); + const openclawInit = await run(["init", "--target", "openclaw", "--dir", openclawWorkspace]); + assertSuccess(openclawInit, "init openclaw"); + assert.match(openclawInit.stdout, /target: openclaw/); + assert.match(openclawInit.stdout, /next: openclaw skills list/); + assertInstalledSkill(openclawWorkspace, "openclaw"); + assertInstalledCompanionSkills(openclawWorkspace, "openclaw"); + + const existingCompanionRoot = path.join(scratch, "existing-companion"); + const existingCodexPath = path.join(existingCompanionRoot, "skills", "codex", "SKILL.md"); + await mkdir(path.dirname(existingCodexPath), { recursive: true }); + await writeFile(existingCodexPath, "keep me\n", "utf8"); + + const existingCompanionInit = await run([ + "init", + "--target", + "hermes", + "--dir", + existingCompanionRoot + ]); + assertSuccess(existingCompanionInit, "init skips existing companion skill"); + assert.match(existingCompanionInit.stdout, /companion skipped:/); + assert.match(existingCompanionInit.stdout, /codex/); + assert.equal(await readFile(existingCodexPath, "utf8"), "keep me\n"); + assertInstalledSkill(existingCompanionRoot, "existing companion root"); + for (const skillName of companionSkillNames.filter((name) => name !== "codex")) { + assert.ok( + existsSync(path.join(existingCompanionRoot, "skills", skillName, "SKILL.md")), + `companion skill ${skillName} missing after existing-companion install` + ); + } + + const partialCompanionRoot = path.join(scratch, "partial-companion"); + const partialPromptPath = path.join( + partialCompanionRoot, + "skills", + "subagent-driven-development", + "spec-reviewer-prompt.md" + ); + await mkdir(path.dirname(partialPromptPath), { recursive: true }); + await writeFile(partialPromptPath, "keep partial prompt\n", "utf8"); + + const partialCompanionInit = await run([ + "init", + "--target", + "hermes", + "--dir", + partialCompanionRoot + ]); + assertSuccess(partialCompanionInit, "init completes partial companion skill"); + assert.match(partialCompanionInit.stdout, /companion completed:/); + assert.match(partialCompanionInit.stdout, /subagent-driven-development/); + assert.equal(await readFile(partialPromptPath, "utf8"), "keep partial prompt\n"); + assertInstalledSkill(partialCompanionRoot, "partial companion root"); + assertInstalledCompanionSkills(partialCompanionRoot, "partial companion root"); + + const forceCompanionRoot = path.join(scratch, "force-existing-companion"); + const forceCodexPath = path.join(forceCompanionRoot, "skills", "codex", "SKILL.md"); + await mkdir(path.dirname(forceCodexPath), { recursive: true }); + await writeFile(forceCodexPath, "replace me\n", "utf8"); + + const forceCompanionInit = await run([ + "init", + "--target", + "hermes", + "--dir", + forceCompanionRoot, + "--force" + ]); + assertSuccess(forceCompanionInit, "force init overwrites existing companion skill"); + assert.match(forceCompanionInit.stdout, /companion overwritten:/); + assert.match(forceCompanionInit.stdout, /codex/); + assert.notEqual(await readFile(forceCodexPath, "utf8"), "replace me\n"); + assertInstalledSkill(forceCompanionRoot, "force existing companion root"); + assertInstalledCompanionSkills(forceCompanionRoot, "force existing companion root"); + + const originalHermesHome = process.env.HERMES_HOME; + const originalOpenClawWorkspace = process.env.OPENCLAW_WORKSPACE; + try { + const hermesEnvHome = path.join(scratch, "hermes-env-home"); + process.env.HERMES_HOME = hermesEnvHome; + const hermesEnvInit = await run(["init"]); + assertSuccess(hermesEnvInit, "bare init hermes from HERMES_HOME"); + assert.match(hermesEnvInit.stdout, /target: hermes/); + assert.match(hermesEnvInit.stdout, new RegExp(`root: ${escapeRegExp(hermesEnvHome)}`)); + assertInstalledSkill(hermesEnvHome, "hermes env"); + assertInstalledCompanionSkills(hermesEnvHome, "hermes env"); + + const openclawEnvWorkspace = path.join(scratch, "openclaw-env-workspace"); + process.env.OPENCLAW_WORKSPACE = openclawEnvWorkspace; + const openclawEnvInit = await run(["init", "--target", "openclaw"]); + assertSuccess(openclawEnvInit, "init openclaw from OPENCLAW_WORKSPACE"); + assertInstalledSkill(openclawEnvWorkspace, "openclaw env"); + assertInstalledCompanionSkills(openclawEnvWorkspace, "openclaw env"); + } finally { + if (originalHermesHome === undefined) { + delete process.env.HERMES_HOME; + } else { + process.env.HERMES_HOME = originalHermesHome; + } + + if (originalOpenClawWorkspace === undefined) { + delete process.env.OPENCLAW_WORKSPACE; + } else { + process.env.OPENCLAW_WORKSPACE = originalOpenClawWorkspace; + } + } + + for (const target of manualAdapterTargets) { + const destination = path.join(scratch, target); + const init = await run(["init", "--target", target, "--dir", destination]); + assertSuccess(init, `init ${target}`); + assert.match(init.stdout, /created:/); + assert.match(init.stdout, /skipped:/); + assertInstalledSkill(destination, target); + assertMissingCompanionSkills(destination, target); + } + + const nativeConflictRoot = path.join(scratch, "native-conflict"); + const firstNative = await run(["init", "--target", "hermes", "--dir", nativeConflictRoot]); + assertSuccess(firstNative, "initial hermes install"); + + const secondNative = await run(["init", "--target", "hermes", "--dir", nativeConflictRoot]); + assertFailure(secondNative, "conflicting hermes install"); + assert.match(secondNative.stdout, /refused:/); + + const forcedNative = await run(["init", "--target", "hermes", "--dir", nativeConflictRoot, "--force"]); + assertSuccess(forcedNative, "forced hermes install"); + assert.match(forcedNative.stdout, /created:/); + + const openclawConflictRoot = path.join(scratch, "openclaw-native-conflict"); + const firstOpenClawNative = await run([ + "init", + "--target", + "openclaw", + "--dir", + openclawConflictRoot + ]); + assertSuccess(firstOpenClawNative, "initial openclaw install"); + + const secondOpenClawNative = await run([ + "init", + "--target", + "openclaw", + "--dir", + openclawConflictRoot + ]); + assertFailure(secondOpenClawNative, "conflicting openclaw install"); + assert.match(secondOpenClawNative.stdout, /refused:/); + + const forcedOpenClawNative = await run([ + "init", + "--target", + "openclaw", + "--dir", + openclawConflictRoot, + "--force" + ]); + assertSuccess(forcedOpenClawNative, "forced openclaw install"); + assert.match(forcedOpenClawNative.stdout, /created:/); + + const conflictDir = path.join(scratch, "conflict"); + const first = await run(["init", "--target", "generic-agent", "--dir", conflictDir]); + assertSuccess(first, "initial generic scaffold"); + + const second = await run(["init", "--target", "generic-agent", "--dir", conflictDir]); + assertFailure(second, "conflicting generic scaffold"); + assert.match(second.stdout, /refused:/); + + const forced = await run(["init", "--target", "generic-agent", "--dir", conflictDir, "--force"]); + assertSuccess(forced, "forced generic scaffold"); + assert.match(forced.stdout, /created:/); + + const agentsPath = path.join(conflictDir, "AGENTS.md"); + const agents = await readFile(agentsPath, "utf8"); + assert.match(agents, /Anti-Slop Governance/); + + const customFile = path.join(conflictDir, "custom.txt"); + await writeFile(customFile, "keep me\n", "utf8"); + const afterForce = await readFile(customFile, "utf8"); + assert.equal(afterForce, "keep me\n"); + + console.log("Smoke test passed"); +} finally { + if (originalCompanionFixtureRoot === undefined) { + delete process.env.BASD_COMPANION_SKILLS_FIXTURE_DIR; + } else { + process.env.BASD_COMPANION_SKILLS_FIXTURE_DIR = originalCompanionFixtureRoot; + } + await rm(scratch, { recursive: true, force: true }); +} diff --git a/scripts/validate.mjs b/scripts/validate.mjs new file mode 100755 index 0000000..2bf4633 --- /dev/null +++ b/scripts/validate.mjs @@ -0,0 +1,683 @@ +#!/usr/bin/env node + +import { readdir, readFile, stat } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const defaultRepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +let repoRoot = defaultRepoRoot; +let errors = []; +const companionManifestFile = "integrations/companion-skills.json"; + +const requiredFiles = [ + "README.md", + "LICENSE", + "NOTICE", + "CHANGELOG.md", + "CONTRIBUTING.md", + "SECURITY.md", + "CODE_OF_CONDUCT.md", + "AGENTS.md", + "CLAUDE.md", + "llms.txt", + "package.json", + "bin/basd-coding-dispatch.mjs", + "scripts/validate.mjs", + "scripts/smoke-test.mjs", + companionManifestFile, + "skills/basd-coding-dispatch/SKILL.md", + "skills/basd-coding-dispatch/references/README.md", + "skills/basd-coding-dispatch/references/quality-gates.md", + "skills/basd-coding-dispatch/references/provider-command-recipes.md", + "skills/basd-coding-dispatch/references/session-topology.md", + "skills/basd-coding-dispatch/references/review-orchestration.md", + "skills/basd-coding-dispatch/references/subagent-skill-bundles.md", + "references/quality-gates.md", + "references/provider-command-recipes.md", + "references/session-topology.md", + "references/review-orchestration.md", + "references/subagent-skill-bundles.md", + "integrations/hermes/install.md", + "integrations/openclaw/install.md", + "integrations/codex/install.md", + "integrations/claude-code/install.md", + "integrations/opencode/install.md", + "integrations/cursor/install.md", + "integrations/generic-agent/install.md", + "examples/small-fix.md", + "examples/feature-build.md", + "examples/dual-provider-review.md", + "examples/telegram-agent-workflow.md", + "assets/logo.svg", + "assets/flow-diagram.svg", + ".github/workflows/ci.yml", + ".github/workflows/release.yml", + ".github/ISSUE_TEMPLATE/bug_report.yml", + ".github/ISSUE_TEMPLATE/provider_adapter_request.yml", + ".github/ISSUE_TEMPLATE/workflow_improvement.yml", + ".github/ISSUE_TEMPLATE/docs_install_issue.yml", + ".github/ISSUE_TEMPLATE/config.yml", + ".github/pull_request_template.md" +]; + +const forbiddenPublicFiles = [ + ".claude-plugin", + ".codex-plugin", + ".cursor-plugin", + ".opencode", + "gemini-extension.json" +]; + +const leakageTerms = [ + ["/root", "/openclaw-workspace"].join(""), + ["/root/.", "hermes"].join(""), + ["Da", "mian"].join(""), + ["oc", "_basd"].join(""), + ["fast", ".xyz"].join(""), + ["SL", "ACK_"].join(""), + ["TELE", "GRAM_"].join(""), + ["GITHUB", "_TOKEN"].join(""), + ["OPENAI", "_API_KEY"].join(""), + ["ANTHROPIC", "_API_KEY"].join("") +]; + +const scannedExtensions = new Set([ + ".md", + ".mjs", + ".js", + ".yml", + ".yaml", + ".json", + ".txt", + ".svg" +]); + +const ignoredDirectories = new Set([".git", "node_modules"]); + +const exampleReferences = { + "examples/small-fix.md": [ + "references/quality-gates.md", + "references/provider-command-recipes.md" + ], + "examples/feature-build.md": [ + "references/quality-gates.md", + "references/session-topology.md" + ], + "examples/dual-provider-review.md": [ + "references/review-orchestration.md", + "references/session-topology.md" + ], + "examples/telegram-agent-workflow.md": [ + "skills/basd-coding-dispatch/references/quality-gates.md", + "integrations/hermes/install.md" + ] +}; + +const integrationDocs = [ + "integrations/hermes/install.md", + "integrations/openclaw/install.md", + "integrations/codex/install.md", + "integrations/claude-code/install.md", + "integrations/opencode/install.md", + "integrations/cursor/install.md", + "integrations/generic-agent/install.md" +]; + +const skillReferenceFiles = [ + "quality-gates.md", + "provider-command-recipes.md", + "session-topology.md", + "review-orchestration.md", + "subagent-skill-bundles.md" +]; + +const expectedCompanionSkills = [ + "codex", + "claude-code", + "using-superpowers", + "brainstorming", + "writing-plans", + "subagent-driven-development", + "requesting-code-review", + "verification-before-completion", + "test-driven-development", + "systematic-debugging" +]; + +const verifiedCompanionRefs = { + "NousResearch/hermes-agent": "faa13e49f81480771ceeb55991bb0c27edf1a5fb", + "obra/superpowers": "f2cbfbefebbfef77321e4c9abc9e949826bea9d7" +}; + +function addError(message) { + errors.push(message); +} + +async function readJson(relativePath) { + const content = await readFile(path.join(repoRoot, relativePath), "utf8"); + return JSON.parse(content); +} + +function isPlainObject(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function isSafeRelativePath(value) { + return ( + typeof value === "string" && + value.length > 0 && + !path.isAbsolute(value) && + !value.split(/[\\/]+/).includes("..") + ); +} + +async function listFiles(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + if (entry.isDirectory() && ignoredDirectories.has(entry.name)) { + continue; + } + + const fullPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...await listFiles(fullPath)); + } else if (entry.isFile()) { + files.push(fullPath); + } + } + + return files; +} + +async function validateRequiredFiles() { + for (const file of requiredFiles) { + const fullPath = path.join(repoRoot, file); + if (!existsSync(fullPath)) { + addError(`Required file is missing: ${file}`); + continue; + } + + const info = await stat(fullPath); + if (!info.isFile()) { + addError(`Required path is not a file: ${file}`); + } + } +} + +async function validatePackageJson() { + let packageJson; + try { + packageJson = await readJson("package.json"); + } catch (error) { + addError(`package.json could not be parsed: ${error.message}`); + return; + } + + const expectations = { + name: "basd-coding-dispatch", + version: "0.1.0", + type: "module", + license: "MIT" + }; + + for (const [key, value] of Object.entries(expectations)) { + if (packageJson[key] !== value) { + addError(`package.json ${key} must be ${value}`); + } + } + + if (packageJson.bin?.["basd-coding-dispatch"] !== "./bin/basd-coding-dispatch.mjs") { + addError("package.json bin must map basd-coding-dispatch to ./bin/basd-coding-dispatch.mjs"); + } + + if (packageJson.scripts?.validate !== "node scripts/validate.mjs") { + addError("package.json scripts.validate must run node scripts/validate.mjs"); + } + + if (packageJson.scripts?.["smoke-test"] !== "node scripts/smoke-test.mjs") { + addError("package.json scripts.smoke-test must run node scripts/smoke-test.mjs"); + } + + if (!packageJson.description?.includes("Hermes/OpenClaw")) { + addError("package.json description must lead with Hermes/OpenClaw positioning"); + } + + for (const keyword of ["hermes", "openclaw", "telegram", "mobile", "codex", "claude"]) { + if (!packageJson.keywords?.includes(keyword)) { + addError(`package.json keywords must include ${keyword}`); + } + } +} + +async function validateSkillFrontmatter() { + const skillPath = "skills/basd-coding-dispatch/SKILL.md"; + const content = await readFile(path.join(repoRoot, skillPath), "utf8"); + const frontmatter = content.match(/^---\n([\s\S]*?)\n---\n/); + + if (!frontmatter) { + addError(`${skillPath} must start with YAML frontmatter`); + return; + } + + const fields = Object.fromEntries( + frontmatter[1] + .split("\n") + .map((line) => line.match(/^([a-zA-Z0-9_-]+):\s*(.+)$/)) + .filter(Boolean) + .map((match) => [match[1], match[2].trim()]) + ); + + if (!fields.name) { + addError(`${skillPath} frontmatter is missing name`); + } + + if (!fields.description) { + addError(`${skillPath} frontmatter is missing description`); + } +} + +async function validateSkillReferences() { + const skillPath = "skills/basd-coding-dispatch/SKILL.md"; + const content = await readFile(path.join(repoRoot, skillPath), "utf8"); + + for (const file of skillReferenceFiles) { + const skillReference = `references/${file}`; + const skillLocalPath = `skills/basd-coding-dispatch/references/${file}`; + + if (!existsSync(path.join(repoRoot, skillLocalPath))) { + addError(`Installed skill reference is missing: ${skillLocalPath}`); + } + + if (!content.includes(skillReference)) { + addError(`${skillPath} should mention ${skillReference}`); + } + } + + const referenceReadme = await readFile( + path.join(repoRoot, "skills/basd-coding-dispatch/references/README.md"), + "utf8" + ); + + for (const file of skillReferenceFiles) { + if (!referenceReadme.includes(file)) { + addError(`skills/basd-coding-dispatch/references/README.md should mention ${file}`); + } + } +} + +async function validateCompanionManifest() { + let manifest; + try { + manifest = await readJson(companionManifestFile); + } catch (error) { + addError(`${companionManifestFile} could not be parsed: ${error.message}`); + return; + } + + if (!isPlainObject(manifest)) { + addError(`${companionManifestFile} must contain a JSON object`); + return; + } + + if (manifest.schemaVersion !== 1) { + addError(`${companionManifestFile} schemaVersion must be 1`); + } + + if (!Array.isArray(manifest.skills) || manifest.skills.length === 0) { + addError(`${companionManifestFile} must include a non-empty skills array`); + return; + } + + const seenNames = new Set(); + const defaultSkills = []; + + for (const [index, skill] of manifest.skills.entries()) { + const label = `${companionManifestFile} skills[${index}]`; + + if (!isPlainObject(skill)) { + addError(`${label} must be an object`); + continue; + } + + for (const field of [ + "name", + "sourceRepo", + "ref", + "sourceDirectory", + "license", + "licenseNotes", + "updateNotes" + ]) { + if (typeof skill[field] !== "string" || skill[field].length === 0) { + addError(`${label}.${field} must be a non-empty string`); + } + } + + if (typeof skill.installByDefault !== "boolean") { + addError(`${label}.installByDefault must be a boolean`); + } + + if (typeof skill.name === "string") { + if (seenNames.has(skill.name)) { + addError(`${label}.name duplicates ${skill.name}`); + } + seenNames.add(skill.name); + + if (skill.installByDefault === true) { + defaultSkills.push(skill.name); + } + } + + if (!isSafeRelativePath(skill.sourceDirectory)) { + addError(`${label}.sourceDirectory must be a safe relative path`); + } + + if (!/^[a-f0-9]{40}$/i.test(skill.ref ?? "")) { + addError(`${label}.ref must be a 40-character commit hash`); + } + + if (!Object.hasOwn(verifiedCompanionRefs, skill.sourceRepo)) { + addError(`${label}.sourceRepo is not one of the approved upstream repositories`); + } else if (verifiedCompanionRefs[skill.sourceRepo] !== skill.ref) { + addError(`${label}.ref must match the verified ref for ${skill.sourceRepo}`); + } + + if (!Array.isArray(skill.files) || skill.files.length === 0) { + addError(`${label}.files must be a non-empty array`); + continue; + } + + let hasSkillFile = false; + for (const [fileIndex, file] of skill.files.entries()) { + const fileLabel = `${label}.files[${fileIndex}]`; + + if (!isPlainObject(file)) { + addError(`${fileLabel} must be an object`); + continue; + } + + for (const field of ["sourcePath", "destinationPath", "rawUrl"]) { + if (typeof file[field] !== "string" || file[field].length === 0) { + addError(`${fileLabel}.${field} must be a non-empty string`); + } + } + + if (!isSafeRelativePath(file.sourcePath)) { + addError(`${fileLabel}.sourcePath must be a safe relative path`); + } + + if (!isSafeRelativePath(file.destinationPath)) { + addError(`${fileLabel}.destinationPath must be a safe relative path`); + } + + if ( + typeof file.sourcePath === "string" && + typeof skill.sourceDirectory === "string" && + !file.sourcePath.startsWith(`${skill.sourceDirectory}/`) + ) { + addError(`${fileLabel}.sourcePath must live under ${skill.sourceDirectory}`); + } + + const expectedRawUrl = + `https://raw.githubusercontent.com/${skill.sourceRepo}/${skill.ref}/${file.sourcePath}`; + if (file.rawUrl !== expectedRawUrl) { + addError(`${fileLabel}.rawUrl must equal ${expectedRawUrl}`); + } + + if (file.destinationPath === "SKILL.md") { + hasSkillFile = true; + } + } + + if (!hasSkillFile) { + addError(`${label}.files must install SKILL.md`); + } + } + + for (const skillName of expectedCompanionSkills) { + if (!seenNames.has(skillName)) { + addError(`${companionManifestFile} is missing companion skill ${skillName}`); + } + + if (!defaultSkills.includes(skillName)) { + addError(`${companionManifestFile} must install ${skillName} by default`); + } + } +} + +async function validateLeakageScan() { + const files = await listFiles(repoRoot); + + for (const file of files) { + const relativeFile = path.relative(repoRoot, file); + const extension = path.extname(file); + + if (!scannedExtensions.has(extension)) { + continue; + } + + const content = await readFile(file, "utf8"); + for (const term of leakageTerms) { + if (content.includes(term)) { + addError(`Private leakage pattern found in ${relativeFile}: ${term}`); + } + } + } +} + +async function validateExampleReferences() { + for (const [exampleFile, references] of Object.entries(exampleReferences)) { + const content = await readFile(path.join(repoRoot, exampleFile), "utf8"); + for (const reference of references) { + if (!existsSync(path.join(repoRoot, reference))) { + addError(`${exampleFile} references missing file ${reference}`); + } + if (!content.includes(reference)) { + addError(`${exampleFile} should mention ${reference}`); + } + } + } +} + +async function validateIntegrationStatuses() { + for (const file of integrationDocs) { + if (!existsSync(path.join(repoRoot, file))) { + continue; + } + + const content = await readFile(path.join(repoRoot, file), "utf8"); + if (!/^Status:\s*(tested|experimental|planned)$/m.test(content)) { + addError(`${file} must include a status label of tested, experimental, or planned`); + } + + if ( + ["integrations/hermes/install.md", "integrations/openclaw/install.md"].includes(file) && + ( + !content.includes("companion skills") || + !content.includes("raw.githubusercontent.com") || + !content.includes("--skip-companion-skills") || + !content.includes("--no-companion-skills") + ) + ) { + addError( + `${file} must document companion skills, raw.githubusercontent.com network access, and skip flags` + ); + } + } +} + +function validateForbiddenPublicFiles() { + for (const file of forbiddenPublicFiles) { + if (existsSync(path.join(repoRoot, file))) { + addError(`Do not include unvalidated native metadata: ${file}`); + } + } +} + +async function validateReadme() { + const content = await readFile(path.join(repoRoot, "README.md"), "utf8"); + const firstLine = content.split("\n")[0]; + const requiredFirstSentence = "`basd-coding-dispatch` is a native Hermes/OpenClaw coding-dispatch skill for approval-gated coding from Telegram and mobile chat."; + + if (firstLine !== requiredFirstSentence) { + addError("README.md first sentence does not match the required text"); + } + + for (const requiredText of [ + "npx basd-coding-dispatch init", + "Prerequisites", + "What gets installed", + "What this does not install/configure", + "companion skills", + "raw.githubusercontent.com", + "--skip-companion-skills", + "--no-companion-skills", + "Telegram", + "mobile", + "worker-routing", + "Codex", + "Claude", + "OpenClaw", + "Hermes is the maintained home", + "quality gates" + ]) { + if (!content.includes(requiredText)) { + addError(`README.md must include ${requiredText}`); + } + } +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function extractWorkflowStep(content, stepName) { + const lines = content.split("\n"); + const stepStartPattern = new RegExp(`^(\\s*)-\\s+name:\\s+${escapeRegExp(stepName)}\\s*$`); + const startIndex = lines.findIndex((line) => stepStartPattern.test(line)); + + if (startIndex === -1) { + return ""; + } + + const stepIndent = lines[startIndex].match(stepStartPattern)?.[1] ?? ""; + let endIndex = lines.length; + + for (let index = startIndex + 1; index < lines.length; index += 1) { + if (lines[index].startsWith(`${stepIndent}- name:`)) { + endIndex = index; + break; + } + } + + return lines.slice(startIndex, endIndex).join("\n"); +} + +async function validateReleaseWorkflow() { + const content = await readFile(path.join(repoRoot, ".github/workflows/release.yml"), "utf8"); + + for (const requiredText of [ + "workflow_dispatch:", + "id-token: write", + "Check tag matches package version", + "github.event_name == 'push'", + "GITHUB_REF_NAME", + "workflow_dispatch runs validation, smoke test, and pack dry run only", + "npm publish --access public --provenance" + ]) { + if (!content.includes(requiredText)) { + addError(`.github/workflows/release.yml must include ${requiredText}`); + } + } + + if (/^\s*NPM_TOKEN\s*:/m.test(content)) { + addError(".github/workflows/release.yml must not define NPM_TOKEN as a workflow environment key"); + } + + if (content.includes("env.NPM_TOKEN")) { + addError(".github/workflows/release.yml must not gate publish behavior on env.NPM_TOKEN"); + } + + const publishStep = extractWorkflowStep(content, "Publish to npm"); + if (!publishStep) { + addError(".github/workflows/release.yml must include a Publish to npm step"); + } else { + for (const requiredText of [ + "if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')", + "NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}", + "npm publish --access public --provenance" + ]) { + if (!publishStep.includes(requiredText)) { + addError(`.github/workflows/release.yml Publish to npm step must include ${requiredText}`); + } + } + + const workflowOutsidePublishStep = content.replace(publishStep, ""); + if (/NODE_AUTH_TOKEN|secrets\.NPM_TOKEN/.test(workflowOutsidePublishStep)) { + addError( + ".github/workflows/release.yml must expose the npm secret only as NODE_AUTH_TOKEN in the Publish to npm step" + ); + } + } + + for (const stepName of ["Install", "Validate", "Smoke test", "Pack dry run"]) { + const step = extractWorkflowStep(content, stepName); + if (!step) { + addError(`.github/workflows/release.yml must include a ${stepName} step`); + } else if (/^\s+if:\s*/m.test(step)) { + addError(`.github/workflows/release.yml ${stepName} step must run for workflow_dispatch`); + } + } + + const manualSkipStep = extractWorkflowStep(content, "Skip publish for manual run"); + if (!manualSkipStep) { + addError(".github/workflows/release.yml must include a Skip publish for manual run step"); + } else if ( + !manualSkipStep.includes("if: github.event_name == 'workflow_dispatch'") || + !manualSkipStep.includes( + "workflow_dispatch runs validation, smoke test, and pack dry run only; skipping npm publish." + ) + ) { + addError(".github/workflows/release.yml manual skip step must clearly skip npm publish"); + } +} + +export async function runValidation(options = {}) { + repoRoot = path.resolve(options.rootDir ?? defaultRepoRoot); + errors = []; + + await validateRequiredFiles(); + await validatePackageJson(); + await validateSkillFrontmatter(); + await validateSkillReferences(); + await validateCompanionManifest(); + await validateLeakageScan(); + await validateExampleReferences(); + await validateIntegrationStatuses(); + validateForbiddenPublicFiles(); + await validateReadme(); + await validateReleaseWorkflow(); + + const stdout = options.stdout ?? process.stdout; + const stderr = options.stderr ?? process.stderr; + + if (errors.length > 0) { + for (const error of errors) { + stderr.write(`- ${error}\n`); + } + return 1; + } + + stdout.write("Validation passed\n"); + return 0; +} + +const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : ""; +const currentPath = path.resolve(fileURLToPath(import.meta.url)); + +if (invokedPath === currentPath) { + process.exitCode = await runValidation(); +} diff --git a/skills/basd-coding-dispatch/SKILL.md b/skills/basd-coding-dispatch/SKILL.md new file mode 100644 index 0000000..4b8ec2e --- /dev/null +++ b/skills/basd-coding-dispatch/SKILL.md @@ -0,0 +1,52 @@ +--- +name: basd-coding-dispatch +description: Hermes/OpenClaw-first dispatch workflow for approval-gated AI coding from Telegram and mobile chat. Use when planning, implementing, reviewing, or coordinating Codex, Claude, split-worker, subagent, or generic agent workflows. +--- + +# basd-coding-dispatch + +Use this skill to keep mobile-controlled coding work inside a disciplined Hermes/OpenClaw workflow. + +The dispatcher owns the gates. Codex, Claude, and other validated tools are worker backends selected by routing policy, not the center of the workflow. + +## Core Loop + +1. Classify the request. + - Identify whether it is a question, small fix, feature, refactor, debugging task, review, release task, or integration task. + - Identify risk: user-facing behavior, security, data loss, public docs, dependency changes, or workflow changes. + +2. Select the worker-routing shape. + - Choose one implementer when the task is narrow. + - Split implementation and review when the task is risky or broad. + - Keep provider mechanics in `references/` and integration docs. + +3. Run the spec gate. + - Produce a concise spec before code when requirements are ambiguous, user-facing, or broad. + - Wait for approval when the user asked for gates or when the scope is not clear. + +4. Run the implementation-plan gate. + - Name files, behavior, tests, verification commands, and rollback considerations. + - Do not write feature code before the plan is approved when plan approval is required. + +5. Implement. + - Keep changes scoped to the approved plan. + - Preserve unrelated user changes. + - Avoid worker lock-in unless the integration requires it. + +6. Review independently. + - Use a separate reviewer or subagent when available for meaningful changes. + - Reconcile findings before final verification. + +7. Verify before completion. + - Run the smallest commands that prove the work, then broader checks when risk justifies them. + - Report commands, status, skipped checks, and residual risk. + +## References + +Load only the reference needed for the task: + +- `references/quality-gates.md` for gate criteria. +- `references/provider-command-recipes.md` for Hermes/OpenClaw install and worker command patterns. +- `references/session-topology.md` for single-worker and split-worker shapes. +- `references/review-orchestration.md` for independent review. +- `references/subagent-skill-bundles.md` for assigning focused subagent work. diff --git a/skills/basd-coding-dispatch/references/README.md b/skills/basd-coding-dispatch/references/README.md new file mode 100644 index 0000000..b21402c --- /dev/null +++ b/skills/basd-coding-dispatch/references/README.md @@ -0,0 +1,12 @@ +# Skill References + +Hermes and OpenClaw install the full `skills/basd-coding-dispatch/` directory. These reference files are skill-local so an installed skill can work without reaching back into the repository root. + +Suggested loading order: + +1. Start with `skills/basd-coding-dispatch/SKILL.md`. +2. Load `references/quality-gates.md` for classification, spec, plan, review, and verification gates. +3. Load `references/provider-command-recipes.md` for Hermes/OpenClaw installs and worker command shapes. +4. Load `references/session-topology.md` for mobile dispatch, single-worker, split-worker, and subagent sessions. +5. Load `references/review-orchestration.md` when meaningful implementation work needs independent review. +6. Load `references/subagent-skill-bundles.md` when assigning bounded worker tasks. diff --git a/skills/basd-coding-dispatch/references/provider-command-recipes.md b/skills/basd-coding-dispatch/references/provider-command-recipes.md new file mode 100644 index 0000000..6e37623 --- /dev/null +++ b/skills/basd-coding-dispatch/references/provider-command-recipes.md @@ -0,0 +1,52 @@ +# Provider Command Recipes + +These recipes keep Hermes/OpenClaw dispatch portable while worker backends vary by project. + +## Install Native Skill Targets + +```bash +npx basd-coding-dispatch init +npx basd-coding-dispatch init --dir "$HERMES_HOME" +npx basd-coding-dispatch init --target openclaw --dir ~/openclaw-workspace +``` + +Use `--force` only when you have reviewed the installed skill files and intentionally want to replace them. + +## Initialize Manual Workflow Files + +```bash +npx basd-coding-dispatch init --target generic-agent --dir ./project +npx basd-coding-dispatch init --target codex --dir ./project +npx basd-coding-dispatch init --target claude-code --dir ./project +``` + +Codex and Claude are supported worker backends. Their native harness adapters remain manual or experimental unless the install path has been validated. + +## Validate the Package + +```bash +npm ci +npm run validate +npm run smoke-test +npm pack --dry-run +``` + +## Dispatch Prompt Shape + +```text +Classify this request. +Choose the worker-routing shape. +Produce the spec gate if needed. +Produce the implementation-plan gate before code. +Implement only after the gate is accepted. +Use independent review for meaningful code changes. +Verify before reporting completion. +``` + +## Split-Worker Shape + +```text +Worker A: implement the approved plan. +Worker B: review the diff for correctness, tests, scope, docs, and leakage. +Dispatcher: reconcile findings, apply accepted fixes, run verification, and report. +``` diff --git a/skills/basd-coding-dispatch/references/quality-gates.md b/skills/basd-coding-dispatch/references/quality-gates.md new file mode 100644 index 0000000..4bc601f --- /dev/null +++ b/skills/basd-coding-dispatch/references/quality-gates.md @@ -0,0 +1,75 @@ +# Quality Gates + +Quality gates keep AI coding work explicit enough for a human or another agent to evaluate. + +## 1. Classification Gate + +Every request should be classified before work starts: + +- Question or explanation. +- Small fix. +- Feature build. +- Refactor. +- Debugging or incident investigation. +- Code review. +- Release or packaging task. +- Provider integration task. + +The classification determines how much ceremony is needed. + +## 2. Worker-Routing Gate + +Choose the worker shape: + +- Hermes/OpenClaw dispatcher controls the gates. +- Codex implements. +- Claude implements. +- One worker implements and another reviews. +- Dispatcher coordinates multiple subagents. +- Generic agent follows the portable skill. + +Record the reason when the task is risky or split. + +## 3. Spec Gate + +Use a spec gate when a task is broad, ambiguous, user-facing, public, security-sensitive, or likely to change architecture. + +A useful spec includes: + +- Problem statement. +- Non-goals. +- User-visible behavior. +- Constraints. +- Acceptance criteria. + +## 4. Implementation-Plan Gate + +Use an implementation-plan gate before feature code. A useful plan names: + +- Files to create or modify. +- Behavioral changes. +- Tests and validation commands. +- Review strategy. +- Rollback or recovery concerns. + +## 5. Review Gate + +Meaningful implementation work needs independent review. The reviewer should look for: + +- Behavioral regressions. +- Missing tests. +- Unsafe worker or provider assumptions. +- Docs that overclaim tested behavior. +- Private data or credential leakage. + +## 6. Verification Gate + +Completion requires concrete evidence: + +- Commands and exit statuses. +- Test output. +- Manual transcript or example. +- Screenshots only when visual verification matters. +- Clear note for skipped checks. + +Do not claim done when verification is missing. diff --git a/skills/basd-coding-dispatch/references/review-orchestration.md b/skills/basd-coding-dispatch/references/review-orchestration.md new file mode 100644 index 0000000..0f77311 --- /dev/null +++ b/skills/basd-coding-dispatch/references/review-orchestration.md @@ -0,0 +1,36 @@ +# Review Orchestration + +Independent review is useful only when the reviewer has a clear job and enough evidence. + +## Reviewer Brief + +Give the reviewer: + +- The approved spec. +- The approved implementation plan. +- The diff or changed file list. +- Expected verification commands. +- Known constraints and non-goals. + +Do not give the reviewer a desired conclusion. + +## Review Checklist + +Ask for findings first: + +- Correctness bugs. +- Regressions. +- Missing tests or insufficient verification. +- Worker-specific assumptions that should be documented. +- Public docs that overclaim tested behavior. +- Private data or credential leakage. + +## Dispatcher Reconciliation + +The dispatcher should: + +1. Accept findings that are concrete and reproducible. +2. Reject findings that contradict the approved scope, with a short reason. +3. Apply fixes. +4. Re-run verification. +5. Report remaining risk. diff --git a/skills/basd-coding-dispatch/references/session-topology.md b/skills/basd-coding-dispatch/references/session-topology.md new file mode 100644 index 0000000..193df6f --- /dev/null +++ b/skills/basd-coding-dispatch/references/session-topology.md @@ -0,0 +1,50 @@ +# Session Topology + +## Hermes/OpenClaw Dispatcher + +Use Hermes or an OpenClaw workspace skill as the maintained dispatch layer: + +- A mobile or Telegram request reaches the dispatcher. +- The dispatcher classifies the task and chooses the worker-routing shape. +- Codex, Claude, or another validated worker backend executes the approved work. +- Review and verification evidence returns to the human before completion is claimed. + +## Single Worker + +Use one worker for narrow tasks: + +- Small bugfix. +- Documentation correction. +- Focused validation improvement. +- Single-file cleanup. + +The worker still runs classification, planning, verification, and reporting. + +## Implementer Plus Reviewer + +Use one implementer and one independent reviewer when: + +- The change affects user-facing behavior. +- The change touches release, install, or security paths. +- The code is easy to overfit to one provider. +- The plan has multiple files or unclear risk. + +## Dispatcher With Subagents + +Use a dispatcher when the work has parallel tracks: + +- Documentation and CLI can be implemented independently. +- One agent can inspect provider docs while another implements generic logic. +- A reviewer can run while implementation continues on a disjoint area. + +The dispatcher owns final integration and verification. + +## Mobile Control + +Mobile or chat-based control can work when the gates are preserved: + +- Short command from the human. +- Dispatcher asks only the necessary clarifying questions. +- Spec and plan are posted back for approval. +- Implementation happens in the coding environment. +- Verification output is summarized back to the mobile channel. diff --git a/skills/basd-coding-dispatch/references/subagent-skill-bundles.md b/skills/basd-coding-dispatch/references/subagent-skill-bundles.md new file mode 100644 index 0000000..e1a8acf --- /dev/null +++ b/skills/basd-coding-dispatch/references/subagent-skill-bundles.md @@ -0,0 +1,35 @@ +# Subagent Skill Bundles + +Use focused bundles so subagents do not duplicate work. + +## Implementation Worker + +Use for bounded file ownership: + +- Inputs: spec, implementation plan, owned files, verification command. +- Output: changed files, verification result, risk. +- Rule: do not edit files outside ownership without dispatcher approval. + +## Review Worker + +Use for independent review: + +- Inputs: spec, plan, diff, verification commands. +- Output: ordered findings with file and line references where possible. +- Rule: findings first, summary second. + +## Docs Worker + +Use for public docs: + +- Inputs: audience, files, tested status, banned claims. +- Output: concise docs that match verified behavior. +- Rule: never imply native marketplace support before validation. + +## Integration Worker + +Use for provider adaptation: + +- Inputs: target tool, install mechanism, manual install attempt, validation evidence. +- Output: install notes and status label. +- Rule: mark uncertain native support as experimental or planned.