From 9db5e0db69320b98ebc331b42d91d0b7638339e4 Mon Sep 17 00:00:00 2001 From: BoldBlackBot <296328274+BoldBlackBot@users.noreply.github.com> Date: Sat, 15 Aug 2026 17:39:33 -0400 Subject: [PATCH 1/5] =?UTF-8?q?test:=20sweep=20golden=20test=20tokens=20bc?= =?UTF-8?q?law=20=E2=86=92=20dispatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The product IS the rename, so the golden test speaks the new token: invariant 1 generates with name=dispatch expecting template/ byte-for-byte, invariants 2/2b expect dispatch→foo, and the residual grep now hunts 'dispatch'. RED against the current generator; the generator and template sweeps follow to turn it green. Co-Authored-By: Enrique Canals <84596+EnriqueCanals@users.noreply.github.com> --- test/golden.test.mjs | 100 +++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/test/golden.test.mjs b/test/golden.test.mjs index 95f31c7..c8e844a 100644 --- a/test/golden.test.mjs +++ b/test/golden.test.mjs @@ -1,19 +1,19 @@ -// Golden test for @boldblackai/create-bclaw. +// Golden test for @boldblackai/create-dispatch. // -// The product IS "rename bclaw→ completely", so this test is the +// The product IS "rename dispatch→ completely", so this test is the // correctness proof (RFC §Verification). Three invariants: -// 1. create-bclaw bclaw == template/ byte-for-byte (rename is a no-op when -// name == bclaw; proves the copy is faithful). -// 2. create-bclaw foo == (create-bclaw bclaw output with bclaw→foo applied +// 1. create-dispatch dispatch == template/ byte-for-byte (rename is a no-op when +// name == dispatch; proves the copy is faithful). +// 2. create-dispatch foo == (create-dispatch dispatch output with dispatch→foo applied // to contents AND path components; proves the rename is complete and is // the ONLY delta). -// 3. grep bclaw on the foo output == empty (the hard "no residual" assertion, +// 3. grep dispatch on the foo output == empty (the hard "no residual" assertion, // enforced independently). // // Region substitution (rfcs/2026-07-15_region-substitution-token.md) adds a // second literal token, `us-east-1`→, so invariants 2/3 generalize: -// 2b. create-bclaw foo --region us-west-2 == bclaw output renamed -// [bclaw→foo, us-east-1→us-west-2]. +// 2b. create-dispatch foo --region us-west-2 == dispatch output renamed +// [dispatch→foo, us-east-1→us-west-2]. // 3b. grep us-east-1 on the foo --region us-west-2 output == empty. // // Plus CLI smoke tests for name validation and the non-empty-target guard. @@ -80,7 +80,7 @@ async function tree(dir) { /** * Apply an ordered list of literal substring replaces to a tree's contents * AND path components. Each pair is `[from, to]`; the generator's two tokens - * are `[["bclaw", name], ["us-east-1", region]]`. + * are `[["dispatch", name], ["us-east-1", region]]`. */ function renameTree(treeObj, pairs) { const out = {}; @@ -130,13 +130,13 @@ function residual(treeObj, lit) { return hits; } -test("invariant 1: `create-bclaw bclaw` output == template/", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-iv1-")); - const res = await run(["bclaw"], tmp); +test("invariant 1: `create-dispatch dispatch` output == template/", async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-iv1-")); + const res = await run(["dispatch"], tmp); assert.equal(res.code, 0, `cli failed: ${res.stderr}`); // Generated output has `.template` suffixes stripped on materialize; the // on-disk template keeps them, so normalize both sides before comparing. - const generated = stripTemplateKeys(await tree(path.join(tmp, "bclaw"))); + const generated = stripTemplateKeys(await tree(path.join(tmp, "dispatch"))); const tmpl = stripTemplateKeys(await tree(TEMPLATE)); assert.deepEqual( Object.keys(generated).toSorted(), @@ -146,44 +146,44 @@ test("invariant 1: `create-bclaw bclaw` output == template/", async () => { assert.deepEqual(generated, tmpl, "contents differ from template/"); }); -test("invariant 2: `create-bclaw foo` == bclaw output with bclaw→foo", async () => { - const tmpBclaw = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-iv2-b-")); - const tmpFoo = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-iv2-f-")); - const rb = await run(["bclaw"], tmpBclaw); +test("invariant 2: `create-dispatch foo` == dispatch output with dispatch→foo", async () => { + const tmpDispatch = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-iv2-b-")); + const tmpFoo = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-iv2-f-")); + const rb = await run(["dispatch"], tmpDispatch); const rf = await run(["foo"], tmpFoo); - assert.equal(rb.code, 0, `bclaw failed: ${rb.stderr}`); + assert.equal(rb.code, 0, `dispatch failed: ${rb.stderr}`); assert.equal(rf.code, 0, `foo failed: ${rf.stderr}`); - const bclawTree = await tree(path.join(tmpBclaw, "bclaw")); + const dispatchTree = await tree(path.join(tmpDispatch, "dispatch")); const fooTree = await tree(path.join(tmpFoo, "foo")); - const expected = renameTree(bclawTree, [["bclaw", "foo"]]); + const expected = renameTree(dispatchTree, [["dispatch", "foo"]]); assert.deepEqual( Object.keys(fooTree).toSorted(), Object.keys(expected).toSorted(), "file sets differ", ); - assert.deepEqual(fooTree, expected, "foo output is not bclaw output renamed"); + assert.deepEqual(fooTree, expected, "foo output is not dispatch output renamed"); }); -test("invariant 3: zero residual `bclaw` in `foo` output", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-iv3-")); +test("invariant 3: zero residual `dispatch` in `foo` output", async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-iv3-")); const res = await run(["foo"], tmp); assert.equal(res.code, 0, `cli failed: ${res.stderr}`); const fooTree = await tree(path.join(tmp, "foo")); - const hits = residual(fooTree, "bclaw"); - assert.equal(hits.length, 0, `residual bclaw found: ${JSON.stringify(hits)}`); + const hits = residual(fooTree, "dispatch"); + assert.equal(hits.length, 0, `residual dispatch found: ${JSON.stringify(hits)}`); }); -test("invariant 2b: `create-bclaw foo --region us-west-2` == bclaw output renamed both tokens", async () => { - const tmpBclaw = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-iv2b-b-")); - const tmpFoo = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-iv2b-f-")); - const rb = await run(["bclaw"], tmpBclaw); +test("invariant 2b: `create-dispatch foo --region us-west-2` == dispatch output renamed both tokens", async () => { + const tmpDispatch = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-iv2b-b-")); + const tmpFoo = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-iv2b-f-")); + const rb = await run(["dispatch"], tmpDispatch); const rf = await run(["foo", "--region", "us-west-2"], tmpFoo); - assert.equal(rb.code, 0, `bclaw failed: ${rb.stderr}`); + assert.equal(rb.code, 0, `dispatch failed: ${rb.stderr}`); assert.equal(rf.code, 0, `foo --region failed: ${rf.stderr}`); - const bclawTree = await tree(path.join(tmpBclaw, "bclaw")); + const dispatchTree = await tree(path.join(tmpDispatch, "dispatch")); const fooTree = await tree(path.join(tmpFoo, "foo")); - const expected = renameTree(bclawTree, [ - ["bclaw", "foo"], + const expected = renameTree(dispatchTree, [ + ["dispatch", "foo"], ["us-east-1", "us-west-2"], ]); assert.deepEqual( @@ -191,11 +191,11 @@ test("invariant 2b: `create-bclaw foo --region us-west-2` == bclaw output rename Object.keys(expected).toSorted(), "file sets differ", ); - assert.deepEqual(fooTree, expected, "foo output is not bclaw output renamed with both tokens"); + assert.deepEqual(fooTree, expected, "foo output is not dispatch output renamed with both tokens"); }); test("invariant 3b: zero residual `us-east-1` in `foo --region us-west-2` output", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-iv3b-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-iv3b-")); const res = await run(["foo", "--region", "us-west-2"], tmp); assert.equal(res.code, 0, `cli failed: ${res.stderr}`); const fooTree = await tree(path.join(tmp, "foo")); @@ -207,7 +207,7 @@ test("CLI: invalid names are rejected", async () => { const cases = ["1starts-with-digit", "under_score", `x${"a".repeat(59)}`, "-leading-hyphen", ""]; for (const bad of cases) { // eslint-disable-next-line no-await-in-loop -- parametrized test cases run sequentially for readable, isolated output; concurrency adds no value here - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-bad-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-bad-")); // eslint-disable-next-line no-await-in-loop -- parametrized test cases run sequentially for readable, isolated output; concurrency adds no value here const res = await run([bad], tmp); assert.notEqual(res.code, 0, `expected rejection for name ${JSON.stringify(bad)}`); @@ -217,14 +217,14 @@ test("CLI: invalid names are rejected", async () => { test("CLI: a 59-char name is accepted, 60 is rejected", async () => { const ok59 = `${"a".repeat(58)}z`; // 59 chars, starts with letter const bad60 = `${"a".repeat(59)}z`; // 60 chars - const t1 = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-59-")); - const t2 = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-60-")); + const t1 = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-59-")); + const t2 = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-60-")); assert.equal((await run([ok59], t1)).code, 0, "59-char name should be accepted"); assert.notEqual((await run([bad60], t2)).code, 0, "60-char name should be rejected"); }); test("CLI: refuses a non-empty target without --force, allows with --force", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-force-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-force-")); const target = path.join(tmp, "foo"); await fs.mkdir(target, { recursive: true }); await fs.writeFile(path.join(target, "preexisting.txt"), "x"); @@ -235,13 +235,13 @@ test("CLI: refuses a non-empty target without --force, allows with --force", asy }); test("CLI: unknown flags are rejected", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-unknown-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-unknown-")); const res = await run(["--bogus", "foo"], tmp); assert.notEqual(res.code, 0, "unknown flag --bogus should be rejected"); }); test("CLI: no name + non-TTY stdin exits non-zero with a hint", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-notty-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-notty-")); // run() spawns with piped stdin → process.stdin.isTTY is undefined in the child, // so the CLI must refuse to fall back to an interactive prompt. const res = await run([], tmp); @@ -250,13 +250,13 @@ test("CLI: no name + non-TTY stdin exits non-zero with a hint", async () => { }); test("CLI: a trailing hyphen in the name is rejected", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-trail-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-trail-")); const res = await run(["foo-"], tmp); assert.notEqual(res.code, 0, "name ending with a hyphen should be rejected"); }); test("CLI: -V prints version; -v is not a version alias", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-ver-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-ver-")); const ok = await run(["-V"], tmp); assert.equal(ok.code, 0, "-V should print version and exit 0"); assert.match(ok.stdout, /\S/, "-V should print the version"); @@ -265,19 +265,19 @@ test("CLI: -V prints version; -v is not a version alias", async () => { }); test("CLI: --region accepts a valid region", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-reg-ok-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-reg-ok-")); const res = await run(["foo", "--region", "eu-central-1"], tmp); assert.equal(res.code, 0, `valid region should be accepted: ${res.stderr}`); }); test("CLI: --region rejects an invalid region", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-reg-bad-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-reg-bad-")); const res = await run(["foo", "--region", "not-a-region"], tmp); assert.notEqual(res.code, 0, "invalid region should be rejected"); }); test("CLI: a name colliding with the region token (us-east-1) is rejected", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-reg-name-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-reg-name-")); const res = await run(["us-east-1"], tmp); assert.notEqual( res.code, @@ -287,20 +287,20 @@ test("CLI: a name colliding with the region token (us-east-1) is rejected", asyn }); test("generate: renames the token inside symlink targets", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-sym-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-sym-")); const src = path.join(tmp, "template"); const out = path.join(tmp, "out"); await fs.mkdir(src, { recursive: true }); - await fs.writeFile(path.join(src, "bclaw-target.txt"), "hi"); + await fs.writeFile(path.join(src, "dispatch-target.txt"), "hi"); // `.template` suffix is stripped on materialize → link becomes `foo-link` - await fs.symlink("bclaw-target.txt", path.join(src, "bclaw-link.template")); + await fs.symlink("dispatch-target.txt", path.join(src, "dispatch-link.template")); await generate({ name: "foo", targetDir: out, templateDir: src, region: "us-east-1" }); const target = await fs.readlink(path.join(out, "foo-link")); assert.equal(target, "foo-target.txt", "symlink target string should be renamed"); }); test("generate: substitutes the region token into contents", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "bclaw-reg-gen-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-reg-gen-")); const out = path.join(tmp, "out"); await generate({ name: "foo", From 9469a8ea7d854e5f77901c79e482efb3e96f1317 Mon Sep 17 00:00:00 2001 From: BoldBlackBot <296328274+BoldBlackBot@users.noreply.github.com> Date: Sat, 15 Aug 2026 17:44:06 -0400 Subject: [PATCH 2/5] =?UTF-8?q?feat(cli,generator):=20rename=20rename-toke?= =?UTF-8?q?n=20and=20CLI=20identity=20bclaw=20=E2=86=92=20dispatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RENAME_FROM is now "dispatch" — the token the generator substitutes for the user's chosen agent name. CLI identity strings follow: banner, help header, default prompt answer, git init author fallback (create-dispatch@local), and the initial commit message. The npm-init shorthand is corrected along the way: npm prepends create- itself, so the equivalent of npx @boldblackai/create-dispatch is npm init @boldblackai/dispatch (the old @boldblackai/bclaw line resolved to @boldblackai/create-bclaw). Golden test: 17/17 green. Co-Authored-By: Enrique Canals <84596+EnriqueCanals@users.noreply.github.com> --- src/cli.ts | 16 ++++++++-------- src/generate.ts | 22 +++++++++++----------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 099dc44..f206ed4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -33,11 +33,11 @@ function pkgVersion(): string { function printHelp(): void { const help = [ - "@boldblackai/create-bclaw — scaffold a bclaw repository", + "@boldblackai/create-dispatch — scaffold a dispatch agent repository", "", "Usage:", - " npx @boldblackai/create-bclaw generate .//", - " npm init @boldblackai/bclaw (equivalent)", + " npx @boldblackai/create-dispatch generate .//", + " npm init @boldblackai/dispatch (equivalent)", "", "Options:", " --force write into a non-empty target (merges; overwrites existing files)", @@ -88,9 +88,9 @@ async function askName(): Promise { try { while (true) { // eslint-disable-next-line no-await-in-loop -- interactive prompt: each iteration awaits the user's answer before re-prompting; cannot be batched - const answer = await rl.question("Claw name? (bclaw) ").catch(() => null); + const answer = await rl.question("Agent name? (dispatch) ").catch(() => null); if (answer === null) cancelled(); - const s = (answer ?? "").trim() || "bclaw"; + const s = (answer ?? "").trim() || "dispatch"; if (validName(s)) return s; process.stderr.write(`${nameRule()} — try again\n`); } @@ -178,7 +178,7 @@ async function main(): Promise { return; } - console.log("@boldblackai/create-bclaw"); + console.log("@boldblackai/create-dispatch"); const unknown = flags.filter((f) => !KNOWN_FLAGS.has(f)); if (unknown.length > 0) { @@ -189,7 +189,7 @@ async function main(): Promise { const nameFromArg = typeof name === "string"; if (!nameFromArg) { if (!process.stdin.isTTY) { - fail("no claw name provided and stdin is not a TTY — pass the name as an argument"); + fail("no agent name provided and stdin is not a TTY — pass the name as an argument"); } name = await askName(); } @@ -220,7 +220,7 @@ async function main(): Promise { } if (!nameFromArg) { - const ok = await askConfirm(`Generate claw "${name}" into ${targetDir}?`); + const ok = await askConfirm(`Generate agent "${name}" into ${targetDir}?`); if (!ok) cancelled(); } diff --git a/src/generate.ts b/src/generate.ts index e43d598..3824e44 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -12,13 +12,13 @@ import * as path from "node:path"; /** * The single rename token. Every occurrence in the source template is - * lowercase and standalone (no `Bclaw`, no glued substrings), so a literal + * lowercase and standalone (no `Dispatch`, no glued substrings), so a literal * substring replace is the whole transform. See RFC §Rename model. */ -const RENAME_FROM = "bclaw"; +const RENAME_FROM = "dispatch"; /** - * The region token. Substituted alongside `bclaw`→`name` so the deployer IAM + * The region token. Substituted alongside `dispatch`→`name` so the deployer IAM * policy's `kms:ViaService` (a static JSON that can't use `${AWS::Region}`) * matches the user's chosen region. See * rfcs/2026-07-15_region-substitution-token.md. @@ -41,7 +41,7 @@ export interface GenerateOptions { /** * Copy `template/` → `targetDir/`, applying BOTH literal token replaces - * (`bclaw`→`name`, `us-east-1`→`region`) to file contents AND path components, + * (`dispatch`→`name`, `us-east-1`→`region`) to file contents AND path components, * then assert no residual token remains and `git init` the result. The copy + * residual scan are synchronous recursive walks (depth-first: a directory must * be listed before its entries are recursed into, so the steps are inherently @@ -65,11 +65,11 @@ export async function generate(opts: GenerateOptions): Promise { mkdirSync(targetDir, { recursive: true }); copyTree(templateDir, targetDir, name, region); - // Hard post-copy assertions: zero residual `bclaw` AND (when the region is + // Hard post-copy assertions: zero residual `dispatch` AND (when the region is // not the no-op default) zero residual `us-east-1` in contents and path // components. Each is skipped when its own target embeds the token — a - // literal grep would flag the legitimate replacement (name == "bclaw" or - // name == "mybclaw" for the name token; region == "us-east-1" for the + // literal grep would flag the legitimate replacement (name == "dispatch" or + // name == "mydispatch" for the name token; region == "us-east-1" for the // region token, which is exactly the no-op case). if (!name.includes(RENAME_FROM)) { assertNoResidual(targetDir, RENAME_FROM); @@ -89,7 +89,7 @@ export async function generate(opts: GenerateOptions): Promise { } /** - * Recursively copy src→dest, renaming BOTH tokens (`bclaw`→name, + * Recursively copy src→dest, renaming BOTH tokens (`dispatch`→name, * `us-east-1`→region) in path components + contents (+ symlink targets). */ function copyTree(src: string, dest: string, name: string, region: string): void { @@ -182,13 +182,13 @@ function assertNoResidual(root: string, token: string): void { async function gitInit(dir: string): Promise { await git(dir, ["init", "--quiet"]); if (!(await gitConfig(dir, "user.email"))) { - await git(dir, ["config", "user.email", "create-bclaw@local"]); + await git(dir, ["config", "user.email", "create-dispatch@local"]); } if (!(await gitConfig(dir, "user.name"))) { - await git(dir, ["config", "user.name", "create-bclaw"]); + await git(dir, ["config", "user.name", "create-dispatch"]); } await git(dir, ["add", "-A"]); - await git(dir, ["commit", "--quiet", "-m", "Initial commit from @boldblackai/create-bclaw"]); + await git(dir, ["commit", "--quiet", "-m", "Initial commit from @boldblackai/create-dispatch"]); } async function gitConfig(dir: string, key: string): Promise { From 985b5bc995a2ddea9d128ffb5d447a34b41cf0bb Mon Sep 17 00:00:00 2001 From: BoldBlackBot <296328274+BoldBlackBot@users.noreply.github.com> Date: Sat, 15 Aug 2026 17:44:13 -0400 Subject: [PATCH 3/5] =?UTF-8?q?feat(template):=20sweep=20rename=20token=20?= =?UTF-8?q?bclaw=20=E2=86=92=20dispatch=20across=20template/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bundled snapshot now speaks the new token everywhere the generator substitutes: SSM namespace /dispatch/, KMS alias alias/dispatch-ssm, IAM identities (dispatch-deployer, dispatch-cfn-exec), policy file names, and the shipped skills — setup-dispatch, manage-dispatch, teardown-dispatch (dirs + SKILL.md frontmatter names + cross-links), moved with git mv so they render as renames. The one capitalized sentence start ("Bclaw uses a two-role model") is normalized to lowercase so no stale token casing survives generation. Region token us-east-1 handling is untouched. Co-Authored-By: Enrique Canals <84596+EnriqueCanals@users.noreply.github.com> --- .../SKILL.md | 34 ++--- .../merge_config.py | 0 .../{setup-bclaw => setup-dispatch}/SKILL.md | 124 +++++++++--------- .../template.yaml | 52 ++++---- .../SKILL.md | 48 +++---- template/.env.example | 2 +- template/AGENTS.md | 14 +- template/README.md | 120 ++++++++--------- template/agent_home/AGENTHOME.md | 4 +- template/agent_home/SOUL.md | 2 +- template/agent_home/config.yaml | 18 +-- ...icy.json => dispatch-cfn-exec-policy.json} | 26 ++-- ...rust.json => dispatch-cfn-exec-trust.json} | 0 ...olicy.json => dispatch-deploy-policy.json} | 28 ++-- template/slack-manifest.json | 6 +- 15 files changed, 239 insertions(+), 239 deletions(-) rename template/.agents/skills/{manage-bclaw => manage-dispatch}/SKILL.md (97%) rename template/.agents/skills/{manage-bclaw => manage-dispatch}/merge_config.py (100%) rename template/.agents/skills/{setup-bclaw => setup-dispatch}/SKILL.md (89%) rename template/.agents/skills/{setup-bclaw => setup-dispatch}/template.yaml (95%) rename template/.agents/skills/{teardown-bclaw => teardown-dispatch}/SKILL.md (90%) rename template/{bclaw-cfn-exec-policy.json => dispatch-cfn-exec-policy.json} (90%) rename template/{bclaw-cfn-exec-trust.json => dispatch-cfn-exec-trust.json} (100%) rename template/{bclaw-deploy-policy.json => dispatch-deploy-policy.json} (87%) diff --git a/template/.agents/skills/manage-bclaw/SKILL.md b/template/.agents/skills/manage-dispatch/SKILL.md similarity index 97% rename from template/.agents/skills/manage-bclaw/SKILL.md rename to template/.agents/skills/manage-dispatch/SKILL.md index 4059016..1e3ebb1 100644 --- a/template/.agents/skills/manage-bclaw/SKILL.md +++ b/template/.agents/skills/manage-dispatch/SKILL.md @@ -1,16 +1,16 @@ --- -name: manage-bclaw +name: manage-dispatch description: > - Manage a running ECS (EC2 launch type) bclaw. Five modes: (1) Overlay — push the - repo's agent_home/ onto the bclaw's ~/.hermes (EBS-backed) to update + Manage a running ECS (EC2 launch type) dispatch. Five modes: (1) Overlay — push the + repo's agent_home/ onto the dispatch's ~/.hermes (EBS-backed) to update skills, memories, system prompt, or personas without a redeploy (via ECS - Exec); (2) Run — execute arbitrary commands on the live bclaw for inspection, + Exec); (2) Run — execute arbitrary commands on the live dispatch for inspection, debugging, or one-off operations (via ECS Exec); (3) Merge-config — key-level merge of agent_home/config.yaml into live config.yaml; (4) Upgrade image — roll - the running bclaw onto a new ghcr.io/boldblackai/harness tag by bumping the + the running dispatch onto a new ghcr.io/boldblackai/harness tag by bumping the HarnessImageTag stack parameter and redeploying (no image rebuild); (5) Host — retrieve a stuck container instance's console output or force a wedged instance to replace itself, scoped to the claw's instances via - aws:ResourceTag/ClawName. Companion to setup-bclaw / teardown-bclaw. + aws:ResourceTag/ClawName. Companion to setup-dispatch / teardown-dispatch. --- # Manage Harness ECS on EC2 @@ -54,13 +54,13 @@ common case); for `config.yaml` changes, default to **Merge-config**. ## Prerequisites 1. **The claw is already set up and RUNNING.** This skill manages a live - claw; it does not create one (use `setup-bclaw` first). + claw; it does not create one (use `setup-dispatch` first). Verify the task is `RUNNING` in the first step of either mode. 2. **ECS Exec permissions on the caller.** `aws ecs execute-command` uses SSM Session Manager. The deployer principal (the key in `.env`) needs `ecs:ExecuteCommand` (on the cluster + task) plus the four `ssmmessages:*` - channel actions. These are already in the `bclaw-deploy` policy (`ECSExec` + channel actions. These are already in the `dispatch-deploy` policy (`ECSExec` + `SSMMessages` statements) — no separate addition needed. If the caller still gets an `AccessDeniedException` naming `ssmmessages` or `ecs:ExecuteCommand`, re-attach the policy in the console (file edits @@ -98,12 +98,12 @@ All `aws` commands in this skill assume this shell state. ## Shared first step: connect to the claw -Both modes start here. Collect the **claw name** (default `bclaw`) and +Both modes start here. Collect the **claw name** (default `dispatch`) and **region** (default `us-east-1`) via `ask_user_question`. Then verify the claw is live and ECS Exec works. ```bash -CLAW_NAME=bclaw +CLAW_NAME=dispatch AWS_REGION=us-east-1 ``` @@ -588,7 +588,7 @@ base64 lines (dropping the `Session Manager` / `Starting session` lines). ```bash uv run --no-project --with 'ruamel.yaml==0.19.1' python3 \ - .agents/skills/manage-bclaw/merge_config.py \ + .agents/skills/manage-dispatch/merge_config.py \ --local /workspace/agent_home/config.yaml \ --remote /tmp/claw_config.current \ --remote-mtime "${REMOTE_MTIME:-0}" \ @@ -767,7 +767,7 @@ Edit the `HarnessImageTag` default so the choice survives the next deploy — a version bump is just this edit plus the redeploy in Step 4, then commit: ``` -# .agents/skills/setup-bclaw/template.yaml +# .agents/skills/setup-dispatch/template.yaml HarnessImageTag: Type: String Default: hermes-1.9.4 # ← was hermes-1.9.3 @@ -782,7 +782,7 @@ HarnessImageTag: ```bash aws cloudformation deploy \ - --template-file .agents/skills/setup-bclaw/template.yaml \ + --template-file .agents/skills/setup-dispatch/template.yaml \ --stack-name "$CLAW_NAME" \ --region "$AWS_REGION" \ --capabilities CAPABILITY_NAMED_IAM \ @@ -847,7 +847,7 @@ to register to ECS, it is wedged but passing health checks, or you need its boot log. Modes 1–4 all assume a RUNNING task to ECS Exec into; Mode 5 is the path when there is no task (or the task is not the issue). -Both actions are scoped by the `bclaw-deploy` policy's `EC2InstanceOps` +Both actions are scoped by the `dispatch-deploy` policy's `EC2InstanceOps` statement to the claw's own instances (`aws:ResourceTag/ClawName`), so they cannot touch co-tenant instances in the same account. @@ -856,7 +856,7 @@ cannot touch co-tenant instances in the same account. - **Shell with mise + AWS creds** (same as the other modes). All `aws` commands below assume this shell state. - **The claw's region** (`AWS_REGION`, default `us-east-1`) and **claw name** - (`CLAW_NAME`, default `bclaw`). The container instance is tagged + (`CLAW_NAME`, default `dispatch`). The container instance is tagged `ClawName=` and `Name=-instance`. Find the claw's running container instance: @@ -990,8 +990,8 @@ aws ec2 terminate-instances --instance-ids "$INSTANCE_ID" --dry-run --region "$A presigned URL carries no credentials, so no task-role S3 permissions are needed. -- **Companion skills.** `setup-bclaw` (create the claw), - `teardown-bclaw` (destroy it). This skill sits between them: +- **Companion skills.** `setup-dispatch` (create the claw), + `teardown-dispatch` (destroy it). This skill sits between them: Modes 1 (Overlay), 2 (Run), and 3 (Merge-config) mutate or inspect the live claw over ECS Exec without touching the CloudFormation stack or task definition; Mode 4 (Upgrade image) performs an in-place stack update that re-renders the task definition and diff --git a/template/.agents/skills/manage-bclaw/merge_config.py b/template/.agents/skills/manage-dispatch/merge_config.py similarity index 100% rename from template/.agents/skills/manage-bclaw/merge_config.py rename to template/.agents/skills/manage-dispatch/merge_config.py diff --git a/template/.agents/skills/setup-bclaw/SKILL.md b/template/.agents/skills/setup-dispatch/SKILL.md similarity index 89% rename from template/.agents/skills/setup-bclaw/SKILL.md rename to template/.agents/skills/setup-dispatch/SKILL.md index a7d6c65..b7f3c1c 100644 --- a/template/.agents/skills/setup-bclaw/SKILL.md +++ b/template/.agents/skills/setup-dispatch/SKILL.md @@ -1,13 +1,13 @@ --- -name: setup-bclaw +name: setup-dispatch description: > - Bootstraps a Hermes Agent bclaw on AWS ECS (EC2 launch type) from scratch to + Bootstraps a Hermes Agent dispatch on AWS ECS (EC2 launch type) from scratch to a running gateway. Follows a gated sequence: probe one ARM64 AZ → deploy CloudFormation (VPC, persistent EBS volume, a single-instance Auto Scaling Group, ECS service at DesiredCount 0 on the first deploy) → write SSM secrets → scale to 1 → overlay agent_home/ + install the aws_ssm plugin + merge its - secrets config → restart → verify. Use when setting up a new bclaw on AWS or re-deploying - after teardown. Companion to teardown-bclaw. + secrets config → restart → verify. Use when setting up a new dispatch on AWS or re-deploying + after teardown. Companion to teardown-dispatch. --- # Setup Harness ECS on EC2 @@ -43,17 +43,17 @@ keeps secrets out of template diffs and lets them survive stack deletes. This skill assumes AWS credentials are already configured. See `README.md` → **Setup** for the one-time IAM onboarding (create the deployer user, attach the -`bclaw-deploy-policy.json` policy, add the access key to `.env`). That must be +`dispatch-deploy-policy.json` policy, add the access key to `.env`). That must be completed before running this skill. Permissions are not pre-checked — if the deployer principal is missing an action, CloudFormation will surface the exact `is not authorized to perform` error at deploy time (Phase 2). The deployer's IAM powers are deliberately narrow: it manages the CloudFormation stack and a single dedicated **service role** -(`bclaw-cfn-exec`) that CloudFormation assumes to perform the actual +(`dispatch-cfn-exec`) that CloudFormation assumes to perform the actual infrastructure creates. That service role is created in **Phase 0** below (it cannot be a stack resource — the stack needs it to exist before it can be -created), so onboarding attaches only `bclaw-deploy-policy.json`; nothing else +created), so onboarding attaches only `dispatch-deploy-policy.json`; nothing else is created up front. Before starting, ensure the shell has `mise` active and AWS credentials @@ -96,12 +96,12 @@ it is created here, before the first deploy, idempotently, and deleted last in teardown. The role's trust policy and inline execution policy ship alongside this skill's -deploy policy as `bclaw-cfn-exec-trust.json` (trusts only -`cloudformation.amazonaws.com`) and `bclaw-cfn-exec-policy.json` (the lifecycle +deploy policy as `dispatch-cfn-exec-trust.json` (trusts only +`cloudformation.amazonaws.com`) and `dispatch-cfn-exec-policy.json` (the lifecycle permissions). Run from the repo root so the `file://` paths resolve: ```bash -CLAW_NAME=bclaw # the claw name (fixed at generation) +CLAW_NAME=dispatch # the claw name (fixed at generation) AWS_REGION= CFN_EXEC="${CLAW_NAME}-cfn-exec" @@ -127,7 +127,7 @@ aws iam get-role --role-name "$CFN_EXEC" --query 'Role.RoleName' --output text If `update-assume-role-policy` runs (the role already existed from a prior setup), `put-role-policy` still re-applies the inline policy — re-running this -phase after editing `bclaw-cfn-exec-policy.json` is the way to update the +phase after editing `dispatch-cfn-exec-policy.json` is the way to update the service role's permissions, and it takes effect on the next `cloudformation deploy`. The role's ARN is passed to the deploy as `--role-arn` in Phase 2. @@ -153,7 +153,7 @@ Use `ask_user_question` to collect: The provider choice determines which provider API-key SSM parameter the user creates in Phase 3. The aws_ssm secret-source plugin resolves it (and the - other `/bclaw/*` secrets mapped in `agent_home/config.yaml`'s `env:`) into + other `/dispatch/*` secrets mapped in `agent_home/config.yaml`'s `env:`) into the env at gateway startup, so there is no per-provider stack parameter — adding or swapping a provider key later is an SSM write (+ an `env:` entry if it's a brand-new param name) + task restart, no template edit or @@ -165,15 +165,15 @@ Use `ask_user_question` to collect: | Provider | SSM parameter | |---|---| - | openrouter | `/bclaw/OPENROUTER_API_KEY` | - | anthropic | `/bclaw/ANTHROPIC_API_KEY` | - | zai | `/bclaw/ZAI_API_KEY` | + | openrouter | `/dispatch/OPENROUTER_API_KEY` | + | anthropic | `/dispatch/ANTHROPIC_API_KEY` | + | zai | `/dispatch/ZAI_API_KEY` | 3. **GitHub authentication** — whether the agent should make authenticated `gh`/HTTPS-git calls. This is OPTIONAL: the claw is a Slack bot and runs fine without it. Use `ask_user_question` with these two choices: - **Yes** — the claw authenticates `gh` automatically on every boot from - `/bclaw/GH_TOKEN_VAL` (the container `Command` runs + `/dispatch/GH_TOKEN_VAL` (the container `Command` runs `gh auth login --with-token`). Requires creating that SSM parameter in Phase 3 and passing `EnableGitHubKey=true` in Phase 2. - **No** (default) — no GitHub credential is injected; `gh`/HTTPS-git @@ -183,7 +183,7 @@ Use `ask_user_question` to collect: Store them as shell variables used in every later command: ```bash -CLAW_NAME=bclaw # the claw name (fixed at generation) +CLAW_NAME=dispatch # the claw name (fixed at generation) AWS_REGION= INFER_PROVIDER= # from step 2 ENABLE_GH= # from step 3 (default false) @@ -236,8 +236,8 @@ is the step that prevents the #1 source of stray stacks: a *previous* run whose deploy failed and rolled back (stack now in `ROLLBACK_COMPLETE`) or whose teardown didn't finish (`DELETE_FAILED`). `cloudformation deploy` refuses to run into a stack in those states — it errors out, and the temptation is then to -deploy under a *different* name, leaving the dead `bclaw` stack orphaned (still -billing its retained EBS volume, still squatting on the `/bclaw/*` secret +deploy under a *different* name, leaving the dead `dispatch` stack orphaned (still +billing its retained EBS volume, still squatting on the `/dispatch/*` secret namespace). Detect it here and fix it instead. ```bash @@ -253,7 +253,7 @@ if there is none. Act on the result: | Result | State | What to do | |---|---|---| | `does not exist` error | Fresh — no prior attempt | Proceed to the deploy below (this is a `CREATE`). | -| `CREATE_COMPLETE` / `UPDATE_COMPLETE` | Already fully deployed | This run is an in-place `UPDATE`, not a fresh deploy. Usually fine (e.g. pushing a `template.yaml` change). But if the user wanted a clean rebuild, run the `teardown-bclaw` skill first. Tell the user it's an update before deploying. | +| `CREATE_COMPLETE` / `UPDATE_COMPLETE` | Already fully deployed | This run is an in-place `UPDATE`, not a fresh deploy. Usually fine (e.g. pushing a `template.yaml` change). But if the user wanted a clean rebuild, run the `teardown-dispatch` skill first. Tell the user it's an update before deploying. | | `ROLLBACK_COMPLETE` / `CREATE_FAILED` / `ROLLBACK_FAILED` | Half-started: a deploy failed and rolled back | **STOP.** The stack exists but is unusable — `deploy` will refuse to touch it. Tear it down (below), then re-run setup. | | `UPDATE_ROLLBACK_COMPLETE` / `UPDATE_FAILED` / `UPDATE_ROLLBACK_FAILED` | Half-started: an update on a good stack failed | **STOP.** Cleanest fix is `delete-stack` + redeploy; alternatively `continue-update-rollback` recovers the prior good state. | | `DELETE_IN_PROGRESS` | A teardown is mid-flight | **STOP.** Wait for it to finish (`stack-delete-complete` waiter), then re-check this step. | @@ -262,7 +262,7 @@ if there is none. Act on the result: | `REVIEW_IN_PROGRESS` | A stack with a pending change set (rare for `deploy`) | **STOP.** `delete-stack` then redeploy. | **If the gate stopped on a half-started stack, never abandon it under the -`bclaw` name.** Run the `teardown-bclaw` skill (it scales to 0 first, deletes +`dispatch` name.** Run the `teardown-dispatch` skill (it scales to 0 first, deletes the stack, and handles the retained-EBS + force-delete gotchas), or for a quick rollback cleanup: @@ -272,7 +272,7 @@ aws cloudformation delete-stack --stack-name "$CLAW_NAME" --region "$AWS_REGION" aws cloudformation wait stack-delete-complete --stack-name "$CLAW_NAME" --region "$AWS_REGION" ``` -Two caveats specific to this stack when cleaning up a stale `bclaw`: +Two caveats specific to this stack when cleaning up a stale `dispatch`: - **`DELETE_FAILED` on the ASG is common.** CloudFormation's resource handler can fail to confirm an Auto Scaling Group or its instance is gone (the @@ -321,7 +321,7 @@ overrides carry no `Enable*Key`: ```bash aws cloudformation deploy \ - --template-file .agents/skills/setup-bclaw/template.yaml \ + --template-file .agents/skills/setup-dispatch/template.yaml \ --stack-name "$CLAW_NAME" \ --region "$AWS_REGION" \ --capabilities CAPABILITY_NAMED_IAM \ @@ -385,7 +385,7 @@ aws cloudformation describe-stacks \ Confirm `ClusterName`, `ServiceName`, `EbsVolumeId`, `AutoScalingGroupName`, `KmsKeyArn`, `KmsKeyAlias` (should be `alias/${CLAW_NAME}-ssm`), and -`SsmParameterPrefix` (should be `/bclaw`) are all present. +`SsmParameterPrefix` (should be `/dispatch`) are all present. > **First-deploy instance boot takes a few minutes.** The ASG launches the > container instance, whose UserData installs the AWS CLI (if missing), finds + @@ -400,12 +400,12 @@ Confirm `ClusterName`, `ServiceName`, `EbsVolumeId`, `AutoScalingGroupName`, **Gate: stack is `CREATE_COMPLETE`.** -The claw needs SSM SecureString parameters under the `/bclaw/` namespace — +The claw needs SSM SecureString parameters under the `/dispatch/` namespace — **4 Slack tokens** (always required), the **inference-provider key** chosen in Phase 1 step 2, and an **optional GitHub key** (only if `ENABLE_GH=true` from Phase 1 step 3). The namespace is hardcoded in the template (not constructed from `ClawName`), which means the deployer's IAM policy can be -scoped to `arn:aws:ssm:*:*:parameter/bclaw/*` instead of `*`. They are **not** created by CloudFormation — the user +scoped to `arn:aws:ssm:*:*:parameter/dispatch/*` instead of `*`. They are **not** created by CloudFormation — the user writes them here so they survive stack updates and deletes. Every one of them is resolved into the container env at gateway startup by the aws_ssm secret-source plugin (installed in Phase 5), using the TaskRole's SSM-read @@ -426,10 +426,10 @@ inputs). | SSM key | What it is | Where to find it | |---|---|---| -| `/bclaw/SLACK_BOT_TOKEN` | Slack bot OAuth token (`xoxb-`) | Slack app → OAuth & Permissions → Bot User OAuth Token | -| `/bclaw/SLACK_APP_TOKEN` | Slack app-level token (`xapp-`, enables socket mode) | Slack app → Basic Information → App-Level Tokens | -| `/bclaw/SLACK_ALLOWED_USERS` | Comma-separated Slack user IDs allowed to use the bot | Slack profile → "Copy member ID" | -| `/bclaw/SLACK_HOME_CHANNEL` | Slack channel ID the bot treats as home | Right-click channel → "Copy link", take the trailing ID | +| `/dispatch/SLACK_BOT_TOKEN` | Slack bot OAuth token (`xoxb-`) | Slack app → OAuth & Permissions → Bot User OAuth Token | +| `/dispatch/SLACK_APP_TOKEN` | Slack app-level token (`xapp-`, enables socket mode) | Slack app → Basic Information → App-Level Tokens | +| `/dispatch/SLACK_ALLOWED_USERS` | Comma-separated Slack user IDs allowed to use the bot | Slack profile → "Copy member ID" | +| `/dispatch/SLACK_HOME_CHANNEL` | Slack channel ID the bot treats as home | Right-click channel → "Copy link", take the trailing ID | **GitHub key (optional, from Phase 1 step 3):** create this only if `ENABLE_GH=true` — it authenticates `gh`/HTTPS-git on every boot. Skip this @@ -437,7 +437,7 @@ entire subsection if the user opted out. | SSM key | What it is | Where to find it | |---|---|---| -| `/bclaw/GH_TOKEN_VAL` | GitHub PAT — used for on-boot `gh auth login` (see Phase 6a). Named `*_VAL`, not `GH_TOKEN`, to dodge `gh`'s reserved env var | https://github.com/settings/tokens (classic PAT or fine-grained; needs the scopes the claw's `gh`/git usage requires) | +| `/dispatch/GH_TOKEN_VAL` | GitHub PAT — used for on-boot `gh auth login` (see Phase 6a). Named `*_VAL`, not `GH_TOKEN`, to dodge `gh`'s reserved env var | https://github.com/settings/tokens (classic PAT or fine-grained; needs the scopes the claw's `gh`/git usage requires) | **Inference-provider key (1, from Phase 1 `$INFER_PROVIDER`):** create the one matching the chosen provider — this is the key the gateway uses as its model @@ -445,9 +445,9 @@ backend. | `$INFER_PROVIDER` | SSM key | Where to find it | |---|---|---| -| openrouter | `/bclaw/OPENROUTER_API_KEY` | https://openrouter.ai/keys | -| anthropic | `/bclaw/ANTHROPIC_API_KEY` | https://console.anthropic.com/settings/keys | -| zai | `/bclaw/ZAI_API_KEY` | https://z.ai/manage-apikey/apikey-list (Zhipu AI / open.bigmodel.cn for mainland China) | +| openrouter | `/dispatch/OPENROUTER_API_KEY` | https://openrouter.ai/keys | +| anthropic | `/dispatch/ANTHROPIC_API_KEY` | https://console.anthropic.com/settings/keys | +| zai | `/dispatch/ZAI_API_KEY` | https://z.ai/manage-apikey/apikey-list (Zhipu AI / open.bigmodel.cn for mainland China) | For each parameter the user creates in the console, the settings are: @@ -455,7 +455,7 @@ For each parameter the user creates in the console, the settings are: - **Type:** `SecureString` - **KMS Key ID:** `alias/${CLAW_NAME}-ssm` — the claw's own CMK, created by the stack in Phase 2. **NOT** the default `alias/aws/ssm`. Type the alias name - (e.g. `alias/bclaw-ssm`) into the console's KMS key picker; it resolves to + (e.g. `alias/dispatch-ssm`) into the console's KMS key picker; it resolves to the key the template just created. - **Value:** the secret itself (masked input). @@ -464,7 +464,7 @@ For each parameter the user creates in the console, the settings are: > command with a leading space so the value stays out of shell history: > > ```bash -> aws ssm put-parameter --name "/bclaw/SLACK_BOT_TOKEN" \ +> aws ssm put-parameter --name "/dispatch/SLACK_BOT_TOKEN" \ > --type SecureString --key-id "alias/${CLAW_NAME}-ssm" \ > --value "" --region "$AWS_REGION" > # repeat for the other 3 Slack secrets + the provider key (+ GH_TOKEN_VAL if @@ -490,7 +490,7 @@ REQUIRED="SLACK_BOT_TOKEN SLACK_APP_TOKEN SLACK_ALLOWED_USERS SLACK_HOME_CHANNEL [ "$ENABLE_GH" = "true" ] && REQUIRED="$REQUIRED GH_TOKEN_VAL" for k in $REQUIRED; do - aws ssm get-parameter --name "/bclaw/$k" \ + aws ssm get-parameter --name "/dispatch/$k" \ --region "$AWS_REGION" --query 'Parameter.Name' --output text 2>&1 done ``` @@ -612,7 +612,7 @@ NO secrets in the env — the aws_ssm plugin isn't installed and its `secrets:` config block isn't present, so Slack isn't connected yet. This phase completes the bootstrapping: overlay the curated `agent_home/`, install the plugin, merge its config block into the live `config.yaml`, then restart. After the -restart the plugin resolves every `/bclaw/*` secret at the first env load and +restart the plugin resolves every `/dispatch/*` secret at the first env load and the Slack bot connects. You need the manage skill's ECS Exec transport for all three steps below. @@ -626,12 +626,12 @@ Setup has already satisfied its entry conditions: first ECS Exec call of this setup, so run it to confirm the plugin works and the caller has exec perms. -#### 5a. Overlay agent_home/ (manage-bclaw Mode 1) +#### 5a. Overlay agent_home/ (manage-dispatch Mode 1) Establish the curated baseline — skills, memories, system prompt, `SOUL.md` persona, and the default `boldblackai/skills` marketplace tap (`agent_home/skills/.hub/taps.json`) — on the claw's `/home/harness/.hermes`. -Run `manage-bclaw` in +Run `manage-dispatch` in **Mode 1 (Overlay)** now; it owns the full procedure (tar+base64 over ECS Exec, chunked transfer, decode/extract/`chown`, merge-with-overwrite semantics, dry-run gate). `config.yaml` is excluded from the overlay on @@ -643,7 +643,7 @@ to overlay — skip this step; the claw keeps its self-seeded defaults. #### 5b. Install the aws_ssm secret-source plugin -Install the plugin that resolves the `/bclaw/*` SSM parameters into env vars +Install the plugin that resolves the `/dispatch/*` SSM parameters into env vars at gateway startup. It writes into `~/.hermes/plugins/` (EBS-backed, persists across restarts), so this is a one-time setup step. Run it as the harness user (uid 1000) from an exec session (which runs as root): @@ -671,13 +671,13 @@ aws ecs execute-command --cluster "$CLAW_NAME" --task "$TASK_ARN" \ > resolves the source; the gateway log then shows > `AWS SSM Parameter Store: applied N secrets`. -#### 5c. Merge the secrets config block (manage-bclaw Mode 3) +#### 5c. Merge the secrets config block (manage-dispatch Mode 3) The plugin's config lives in `agent_home/config.yaml` (a minimal `secrets:` block). The overlay excluded it, so merge it into the live `~/.hermes/config.yaml` at the key level — this preserves the live config's comments/order/env-driven keys while adding the non-env `secrets:` block -(which survives the cloud-mode re-seed on restart). Run `manage-bclaw` in +(which survives the cloud-mode re-seed on restart). Run `manage-dispatch` in **Mode 3 (Merge-config)** now; it fetches the live config (byte-chunked over ECS Exec), runs `merge_config.py` (ruamel.yaml round-trip via `uv`), and pushes the merged result back. Confirm the merge when it asks. @@ -685,7 +685,7 @@ pushes the merged result back. Confirm the merge when it asks. #### Restart to apply (unconditional) Restart so the gateway re-reads its config and loads the plugin. After the -restart the plugin resolves every `/bclaw/*` secret at the first env load and +restart the plugin resolves every `/dispatch/*` secret at the first env load and the Slack bot connects (the service's recreate deployment config makes this a stop-old-then-start-new swap, ~10-20s downtime): @@ -745,7 +745,7 @@ aws ecs execute-command --cluster "$CLAW_NAME" --task "$TASK_ARN" \ `gh`/HTTPS-git authentication is **not** a manual step — when GitHub auth is enabled (`ENABLE_GH=true`), the task definition injects `GH_TOKEN_VAL` from -the `/bclaw/GH_TOKEN_VAL` SSM parameter and the container `Command` runs, as +the `/dispatch/GH_TOKEN_VAL` SSM parameter and the container `Command` runs, as the harness user on every boot: ``` @@ -777,7 +777,7 @@ A healthy boot shows `Logged in to github.com as `. If instead you see rejected (rotate it, see below) or GitHub was briefly unreachable (the next task restart retries automatically). -**Rotating the token.** Update the `/bclaw/GH_TOKEN_VAL` SSM parameter in +**Rotating the token.** Update the `/dispatch/GH_TOKEN_VAL` SSM parameter in the **AWS Console** (Systems Manager → Parameter Store → open the parameter → **Edit** → paste the new PAT → Save), then force a new task so the boot command re-runs the login: @@ -799,12 +799,12 @@ Report to the user: - Claw name, region, inference provider, and the single AZ the instance/task live in (with ARM64 confirmation) - Stack name and key outputs (cluster, EBS volume ID, ASG name, SSM prefix) -- The SSM parameter locations (4 Slack + the provider key, plus `/bclaw/GH_TOKEN_VAL` if GitHub auth was enabled — values never displayed) -- The aws_ssm plugin is installed and its `secrets:` config merged (Phase 5); it resolves every `/bclaw/*` secret at gateway startup. Slack is connected (confirmed in Phase 5). To add or rotate a key, write the SSM param + force a new task (`aws ecs update-service --force-new-deployment`) — no template edit or redeploy. -- GitHub auth (if enabled) is automatic on boot from `/bclaw/GH_TOKEN_VAL` (Phase 6a) — verify with `runuser -u harness -- gh auth status` from an exec session; if disabled, `gh auth status` showing "not logged in" is expected +- The SSM parameter locations (4 Slack + the provider key, plus `/dispatch/GH_TOKEN_VAL` if GitHub auth was enabled — values never displayed) +- The aws_ssm plugin is installed and its `secrets:` config merged (Phase 5); it resolves every `/dispatch/*` secret at gateway startup. Slack is connected (confirmed in Phase 5). To add or rotate a key, write the SSM param + force a new task (`aws ecs update-service --force-new-deployment`) — no template edit or redeploy. +- GitHub auth (if enabled) is automatic on boot from `/dispatch/GH_TOKEN_VAL` (Phase 6a) — verify with `runuser -u harness -- gh auth status` from an exec session; if disabled, `gh auth status` showing "not logged in" is expected - How to tail logs: `aws logs tail "/ecs/${CLAW_NAME}" --follow --region "$AWS_REGION"` - How to shell in: the `aws ecs execute-command` snippet from Phase 6 -- How to tear down: point at the `teardown-bclaw` skill +- How to tear down: point at the `teardown-dispatch` skill --- @@ -816,9 +816,9 @@ Report to the user: `Dockerfile` or `entrypoint.sh` is needed. Do not build a derived image. - **Secrets live in SSM, not Secrets Manager.** Following the piranesi pattern, - secrets are namespaced SecureString parameters (`/bclaw/KEY`) that the user - writes. The `/bclaw/` namespace is hardcoded in the template so the - deployer IAM policy can pin `parameter/bclaw/*`. They are not + secrets are namespaced SecureString parameters (`/dispatch/KEY`) that the user + writes. The `/dispatch/` namespace is hardcoded in the template so the + deployer IAM policy can pin `parameter/dispatch/*`. They are not CloudFormation resources, so stack updates never clobber their values and they survive stack deletes. The teardown skill deletes them explicitly after user confirmation. SecureStrings are encrypted with a customer-managed KMS @@ -850,7 +850,7 @@ Report to the user: `config.yaml`), then `exec`s the wrapper as the harness user (uid 1000). The login is **non-fatal** — a failure (bad token, GitHub outage) is logged to CloudWatch and the gateway still starts. `GH_TOKEN_VAL` is an **optional** - SSM param (`/bclaw/GH_TOKEN_VAL`), gated behind the `EnableGitHubKey` stack + SSM param (`/dispatch/GH_TOKEN_VAL`), gated behind the `EnableGitHubKey` stack parameter (default `false`) and injected via `secrets[]` — it is the ONE secret still injected by CloudFormation, because the on-boot `gh auth login` runs before the aws_ssm plugin loads. (Every other secret — the Slack tokens @@ -867,7 +867,7 @@ Report to the user: terminal/execute_code sandbox scrubs token-like env vars from its environment, so `gh`/git calls the agent makes find no env var — they rely on the stored credential in `~/.config/gh/hosts.yml` (on the EBS volume, - persists across restarts). To rotate: update `/bclaw/GH_TOKEN_VAL` in the + persists across restarts). To rotate: update `/dispatch/GH_TOKEN_VAL` in the AWS console (Parameter Store → Edit, or `put-parameter --overwrite`) then `update-service --force-new-deployment` (the boot command re-runs on every task start). `printf` (not `echo`) is used so a token beginning with `-` @@ -917,7 +917,7 @@ Report to the user: sidecar to re-inject. - **AWS credentials.** See Prerequisites → "AWS credentials — the deployer IAM - user" for creating the deployer principal, the `bclaw-deploy` policy, and the + user" for creating the deployer principal, the `dispatch-deploy` policy, and the `.env` format. `.env` is gitignored; never commit it. - **First-task image pull.** Initial task placement takes 2–3 minutes, most of @@ -946,20 +946,20 @@ Report to the user: them, then verify the live task def matches intent. - **Adding new SSM secrets.** To forward an additional secret into the - gateway's env, just put it in SSM as a SecureString under `/bclaw/` (encrypted + gateway's env, just put it in SSM as a SecureString under `/dispatch/` (encrypted with the claw's CMK, `alias/${CLAW_NAME}-ssm`), then force a new task so the aws_ssm plugin resolves it at startup: ```bash - aws ssm put-parameter --name "/bclaw/MY_API_KEY" \ + aws ssm put-parameter --name "/dispatch/MY_API_KEY" \ --type SecureString --key-id "alias/${CLAW_NAME}-ssm" \ --value "" --region "$AWS_REGION" aws ecs update-service --cluster "$CLAW_NAME" --service "$CLAW_NAME" \ --force-new-deployment --region "$AWS_REGION" ``` - The leaf name becomes the env var (`/bclaw/MY_API_KEY` → `MY_API_KEY`; - sub-paths flatten, e.g. `/bclaw/db/PASSWORD` → `DB_PASSWORD`). No + The leaf name becomes the env var (`/dispatch/MY_API_KEY` → `MY_API_KEY`; + sub-paths flatten, e.g. `/dispatch/db/PASSWORD` → `DB_PASSWORD`). No `template.yaml` edit, no CloudFormation redeploy, no new stack parameter — - the plugin already covers `parameter/bclaw/*`. Rotation is the same flow + the plugin already covers `parameter/dispatch/*`. Rotation is the same flow (`put-parameter --overwrite` + restart). This works for any secret consumed inside Hermes after the plugin loads (the Slack tokens, the provider keys, skill API keys). @@ -982,5 +982,5 @@ Report to the user: PyYAML linter (in `patch`/`write_file`) reports false-positive errors on CloudFormation intrinsic shorthand (`!Equals`, `!Sub`, `!If` — valid CFN, not valid plain YAML). Ignore those; instead validate with cfn-lint: - `uvx cfn-lint .agents/skills/setup-bclaw/template.yaml` (run via + `uvx cfn-lint .agents/skills/setup-dispatch/template.yaml` (run via `mise exec -- uvx cfn-lint ...`). diff --git a/template/.agents/skills/setup-bclaw/template.yaml b/template/.agents/skills/setup-dispatch/template.yaml similarity index 95% rename from template/.agents/skills/setup-bclaw/template.yaml rename to template/.agents/skills/setup-dispatch/template.yaml index a3427df..bb5076c 100644 --- a/template/.agents/skills/setup-bclaw/template.yaml +++ b/template/.agents/skills/setup-dispatch/template.yaml @@ -14,14 +14,14 @@ Description: > UserData finds it by tag, attaches it, and mounts it by label on every boot, so data survives instance replacement. Single-AZ is the cost of zonal EBS. - Managed by the setup-bclaw / teardown-bclaw agent skills. + Managed by the setup-dispatch / teardown-dispatch agent skills. # ── Parameters ────────────────────────────────────────────────────────────── Parameters: ClawName: Type: String - Default: bclaw + Default: dispatch Description: > Name of the claw. Prefixed onto every resource (cluster, log group, IAM roles, EBS volume tags). @@ -107,7 +107,7 @@ Parameters: Default: "false" AllowedValues: ["true", "false"] Description: > - Set to "true" to inject /bclaw/GH_TOKEN_VAL into the container env and + Set to "true" to inject /dispatch/GH_TOKEN_VAL into the container env and run the on-boot `gh auth login --with-token` from the container Command (requires the SSM parameter to exist). Defaults to "false" — GitHub auth is OPTIONAL: the claw is a Slack bot and runs fine without it. Enable it @@ -264,17 +264,17 @@ Resources: # ── Secrets are NOT CloudFormation resources ────────────────────────────── # Following the piranesi pattern, secrets live in SSM Parameter Store as - # namespaced SecureString parameters (e.g. /bclaw/OPENROUTER_API_KEY) that + # namespaced SecureString parameters (e.g. /dispatch/OPENROUTER_API_KEY) that # the USER writes in a skill phase (Phase 2) before the service scales up. # They are not owned by this stack, so they survive stack deletes and stack # updates never clobber their values. # - # Secrets live as SSM SecureString parameters under /bclaw/ (the piranesi + # Secrets live as SSM SecureString parameters under /dispatch/ (the piranesi # pattern) and are NOT CloudFormation resources, so they survive stack # updates/deletes. They reach the container two ways: # # 1. aws_ssm secret-source plugin (hermes-aws-ssm-secret-source) — resolves - # the /bclaw/* params explicitly mapped in agent_home/config.yaml + # the /dispatch/* params explicitly mapped in agent_home/config.yaml # `env:` into env vars at the first env load (after .env, before Hermes # reads credentials), using the TaskRole's read-ssm-params grant. The # plugin is mapped-only: only listed params are fetched and only @@ -293,15 +293,15 @@ Resources: # role's ssm:GetParameters grant (below) resolves it at container start. # # The claw is a Slack socket-mode bot (see README + slack-manifest.json). - # SSM parameters under /bclaw/ (the plugin resolves those mapped in `env:`): - # /bclaw/SLACK_BOT_TOKEN — Slack bot OAuth token (xoxb-) - # /bclaw/SLACK_APP_TOKEN — Slack app-level token (xapp-, socket mode) - # /bclaw/SLACK_ALLOWED_USERS — comma-separated Slack user IDs - # /bclaw/SLACK_HOME_CHANNEL — Slack home channel ID - # /bclaw/OPENROUTER_API_KEY — OpenRouter API key (recommended) - # /bclaw/ANTHROPIC_API_KEY — Anthropic (direct Claude API) - # /bclaw/ZAI_API_KEY — Z.AI / Zhipu (GLM) - # /bclaw/GH_TOKEN_VAL — GitHub PAT for on-boot `gh auth login` + # SSM parameters under /dispatch/ (the plugin resolves those mapped in `env:`): + # /dispatch/SLACK_BOT_TOKEN — Slack bot OAuth token (xoxb-) + # /dispatch/SLACK_APP_TOKEN — Slack app-level token (xapp-, socket mode) + # /dispatch/SLACK_ALLOWED_USERS — comma-separated Slack user IDs + # /dispatch/SLACK_HOME_CHANNEL — Slack home channel ID + # /dispatch/OPENROUTER_API_KEY — OpenRouter API key (recommended) + # /dispatch/ANTHROPIC_API_KEY — Anthropic (direct Claude API) + # /dispatch/ZAI_API_KEY — Z.AI / Zhipu (GLM) + # /dispatch/GH_TOKEN_VAL — GitHub PAT for on-boot `gh auth login` # (see secrets[] + Command below; named # *_VAL, not GH_TOKEN, to avoid colliding # with gh's reserved env var) @@ -330,10 +330,10 @@ Resources: # GetParameters (batch). Scoped to the claw's SSM parameter # prefix; WithDecryption is server-side, gated by kms:Decrypt. Action: ssm:GetParameters - Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/bclaw/*" + Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/dispatch/*" - Effect: Allow # Scoped to the claw's own CMK (SsmKmsKey, above). The - # encryption context is pinned to the /bclaw/ namespace so + # encryption context is pinned to the /dispatch/ namespace so # the key can't decrypt params outside it; CallerAccount + # ViaService bind the key to this account's SSM service. Action: kms:Decrypt @@ -343,7 +343,7 @@ Resources: "kms:ViaService": !Sub "ssm.${AWS::Region}.amazonaws.com" "kms:CallerAccount": !Ref AWS::AccountId StringLike: - "kms:EncryptionContext:PARAMETER_ARN": !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/bclaw/*" + "kms:EncryptionContext:PARAMETER_ARN": !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/dispatch/*" # ── IAM: task role (used by the container; needs ssmmessages for ECS Exec) ── @@ -371,11 +371,11 @@ Resources: Resource: "*" # SSM read access for the aws_ssm secret-source plugin # (hermes-aws-ssm-secret-source). It runs in the container process and - # calls GetParameters at startup to resolve the /bclaw/* SecureString + # calls GetParameters at startup to resolve the /dispatch/* SecureString # params explicitly mapped in agent_home/config.yaml `env:` into env # vars. Mirrors the ExecutionRole's read-ssm-params grant. WithDecryption # is server-side, gated by kms:Decrypt below; the encryption context is - # pinned to the /bclaw/ namespace so the key can't decrypt params + # pinned to the /dispatch/ namespace so the key can't decrypt params # outside it. - PolicyName: read-ssm-params PolicyDocument: @@ -383,7 +383,7 @@ Resources: Statement: - Effect: Allow Action: ssm:GetParameters - Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/bclaw/*" + Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/dispatch/*" - Effect: Allow Action: kms:Decrypt Resource: !GetAtt SsmKmsKey.Arn @@ -392,7 +392,7 @@ Resources: "kms:ViaService": !Sub "ssm.${AWS::Region}.amazonaws.com" "kms:CallerAccount": !Ref AWS::AccountId StringLike: - "kms:EncryptionContext:PARAMETER_ARN": !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/bclaw/*" + "kms:EncryptionContext:PARAMETER_ARN": !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/dispatch/*" # ── IAM: container-instance role + instance profile ─────────────────────── # The EC2 container instance (ASG-managed) needs: @@ -716,13 +716,13 @@ Resources: # BEFORE `exec hermes gateway`, i.e. before the aws_ssm secret-source # plugin (hermes-aws-ssm-secret-source) loads. Every other secret — # the Slack tokens and the inference-provider keys — is resolved from - # the /bclaw/ SSM namespace by the plugin at the first env load + # the /dispatch/ SSM namespace by the plugin at the first env load # (Hermes #64189, in the image since hermes-1.9.3), using the # TaskRole's read-ssm-params grant. See agent_home/config.yaml. Secrets: - !If - GitHubKeyEnabled - - { Name: GH_TOKEN_VAL, ValueFrom: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/bclaw/GH_TOKEN_VAL" } + - { Name: GH_TOKEN_VAL, ValueFrom: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/dispatch/GH_TOKEN_VAL" } - !Ref AWS::NoValue MountPoints: - { SourceVolume: hermes-data, ContainerPath: /home/harness/.hermes, ReadOnly: false } @@ -817,10 +817,10 @@ Outputs: LogGroup: Value: !Ref LogGroup SsmParameterPrefix: - Value: "/bclaw" + Value: "/dispatch" Description: > SSM SecureString parameter namespace for this claw. The aws_ssm - secret-source plugin resolves every /bclaw/* parameter into an env var + secret-source plugin resolves every /dispatch/* parameter into an env var at the first gateway start: the 4 Slack params (SLACK_BOT_TOKEN, SLACK_APP_TOKEN, SLACK_ALLOWED_USERS, SLACK_HOME_CHANNEL), the chosen inference-provider key (OPENROUTER_API_KEY, ANTHROPIC_API_KEY, or diff --git a/template/.agents/skills/teardown-bclaw/SKILL.md b/template/.agents/skills/teardown-dispatch/SKILL.md similarity index 90% rename from template/.agents/skills/teardown-bclaw/SKILL.md rename to template/.agents/skills/teardown-dispatch/SKILL.md index 74d6aa3..abbbbab 100644 --- a/template/.agents/skills/teardown-bclaw/SKILL.md +++ b/template/.agents/skills/teardown-dispatch/SKILL.md @@ -1,12 +1,12 @@ --- -name: teardown-bclaw +name: teardown-dispatch description: > - Tears down a Hermes Agent bclaw on ECS (EC2 launch type) and all associated + Tears down a Hermes Agent dispatch on ECS (EC2 launch type) and all associated AWS resources. Follows a reverse-order sequence: scale to 0 → delete CloudFormation stack (VPC, EBS volume [retained], ASG, ECS, IAM) → delete retained EBS → delete orphaned VPC → delete SSM secrets → delete the - CloudFormation service role (`bclaw-cfn-exec`). Use when asked to destroy, - teardown, or decommission the bclaw. Companion to setup-bclaw. + CloudFormation service role (`dispatch-cfn-exec`). Use when asked to destroy, + teardown, or decommission the dispatch. Companion to setup-dispatch. --- # Teardown Harness ECS on EC2 @@ -18,7 +18,7 @@ service/task/cluster, the stack's IAM roles (exec, task, container instance + instance profile), the log group, and the standalone EBS data volume. The EBS volume is retained by the stack's `DeletionPolicy: Retain` and must be deleted explicitly. SSM secrets are not stack-owned and are deleted separately. The -CloudFormation service role (`bclaw-cfn-exec`) is also not stack-owned — it is +CloudFormation service role (`dispatch-cfn-exec`) is also not stack-owned — it is created out-of-band in setup Phase 0 (the stack cannot create the role it assumes to create itself), so it survives `delete-stack` and is deleted here as the final step, after the stack is gone. @@ -41,7 +41,7 @@ destroy the claw. This is destructive and irreversible — all EBS data (sessions, memories, skills, the SQLite databases, gh credentials) will be lost unless the user backs it up first. -Also collect the **claw name** (default `bclaw`) and **region** (default +Also collect the **claw name** (default `dispatch`) and **region** (default `us-east-1`) via `ask_user_question`. Store as: ```bash @@ -129,7 +129,7 @@ container instance), security group, subnet, route table, internet gateway, and VPC. The standalone EBS volume is left behind (subject to the retain policy — see Phase 3). -Delete the stack, passing `--role-arn bclaw-cfn-exec` so CloudFormation assumes +Delete the stack, passing `--role-arn dispatch-cfn-exec` so CloudFormation assumes the service role (the same role the deploys use) to perform the deletions. The service role is itself deleted in Phase 6, so it must be passed here while it still exists and is still needed: @@ -164,9 +164,9 @@ aws cloudformation wait stack-delete-complete \ If they're empty/gone but the stack is stuck on handler confirmation, re-run `delete-stack --deletion-mode FORCE_DELETE_STACK`. - **IAM roles (`NoSuchEntity`).** The exec/task/container-instance roles - (`bclaw-*`) can be deleted out from under the handler (e.g. a prior partial + (`dispatch-*`) can be deleted out from under the handler (e.g. a prior partial teardown), so the handler 404s confirming a resource that no longer exists. - Verify with `aws iam get-role` for each `bclaw-*` role (returns + Verify with `aws iam get-role` for each `dispatch-*` role (returns `NoSuchEntity` if gone). In both cases the resources are confirmed gone via direct CLI, but the stack @@ -213,7 +213,7 @@ echo "volume state: $STATE" If `in-use`, the instance may still be terminating — wait for it, or force-detach (the deployer policy scopes `ec2:DetachVolume` by -`aws:ResourceTag/Name: bclaw-data`, which this volume carries): +`aws:ResourceTag/Name: dispatch-data`, which this volume carries): ```bash aws ec2 detach-volume --volume-id "$EBS_ID" --region "$AWS_REGION" --force || true @@ -233,8 +233,8 @@ aws ec2 delete-volume \ > can find + reattach it by tag. > **EBS delete permissions are tag-conditioned, not absent.** The deployer -> policy scopes `ec2:DeleteVolume` via `aws:ResourceTag/Name: bclaw-data`, so -> volumes tagged `Name=bclaw-data` (all of them, regardless of which stack +> policy scopes `ec2:DeleteVolume` via `aws:ResourceTag/Name: dispatch-data`, so +> volumes tagged `Name=dispatch-data` (all of them, regardless of which stack > version created them) are within scope. Try the `delete-volume` command > directly. If it fails with `AccessDeniedException`, note the volume IDs in > the final report for manual console cleanup. To delete multiple retained @@ -324,8 +324,8 @@ aws ec2 delete-vpc --vpc-id "$VPC_ID" --region "$AWS_REGION" \ > won't clear, delete it from the console. > **The deployer can delete only claw-tagged networking.** Every resource -> deleted above carries a `Name=bclaw*` tag, which is what the policy's -> `EC2NetworkingManage` statement (`aws:ResourceTag/Name: bclaw*`) keys on. The +> deleted above carries a `Name=dispatch*` tag, which is what the policy's +> `EC2NetworkingManage` statement (`aws:ResourceTag/Name: dispatch*`) keys on. The > read-only `Describe*` calls are unscoped, so finding the VPC always works; > the deletes succeed only against the claw's own resources. @@ -341,16 +341,16 @@ window — that only applies to Secrets Manager). > **The deployer policy grants the deletes directly.** Its `SSMSecrets` > statement allows `ssm:DeleteParameter` on -> `arn:aws:ssm:*:*:parameter/bclaw/*`, so the deletes below succeed without a +> `arn:aws:ssm:*:*:parameter/dispatch/*`, so the deletes below succeed without a > console fallback. Only fall back to console cleanup if a delete fails with > `AccessDeniedException` for a param outside that ARN scope. ```bash -# Delete ALL params under /bclaw/ — covers whichever provider key +# Delete ALL params under /dispatch/ — covers whichever provider key # (OPENROUTER_API_KEY | ANTHROPIC_API_KEY | ZAI_API_KEY) this deploy used. # The namespace is one-claw-per-account, so this is the full secret set. aws ssm describe-parameters \ - --parameter-filters "Key=Name,Option=BeginsWith,Values=/bclaw/" \ + --parameter-filters "Key=Name,Option=BeginsWith,Values=/dispatch/" \ --region "$AWS_REGION" \ --query 'Parameters[].Name' --output text | tr '\t' '\n' | while read -r p; do [ -n "$p" ] || continue @@ -366,7 +366,7 @@ Verify the namespace is empty: ```bash aws ssm describe-parameters \ - --parameter-filters "Key=Name,Option=BeginsWith,Values=/bclaw/" \ + --parameter-filters "Key=Name,Option=BeginsWith,Values=/dispatch/" \ --region "$AWS_REGION" \ --query 'Parameters[].Name' --output table ``` @@ -379,7 +379,7 @@ Expected: an empty list. **Gate: stack is `DELETE_COMPLETE` (Phase 2).** -`bclaw-cfn-exec` is the role CloudFormation assumed during every deploy and the +`dispatch-cfn-exec` is the role CloudFormation assumed during every deploy and the Phase 2 stack delete. It is not stack-owned, so `delete-stack` leaves it behind — it must be removed explicitly, and **last** (it was needed during the stack delete, and the deployer's only IAM-create powers now target this one literal @@ -422,7 +422,7 @@ aws ec2 describe-volumes --region "$AWS_REGION" \ # No remaining SSM params aws ssm describe-parameters \ - --parameter-filters "Key=Name,Option=BeginsWith,Values=/bclaw/" \ + --parameter-filters "Key=Name,Option=BeginsWith,Values=/dispatch/" \ --region "$AWS_REGION" --query 'Parameters[].Name' --output text | grep -q . \ && echo "ssm: STILL EXISTS" || echo "ssm: gone" @@ -469,10 +469,10 @@ Report to the user: irreversible. If the user might redeploy, have them record the values before Phase 5 — they'll need to re-write them during setup. -- **The SSM namespace is hardcoded (`/bclaw/`), not derived from `ClawName`.** Secrets live at - `/bclaw/` so the deployer's IAM policy can be scoped to - `parameter/bclaw/*` (see the setup skill's Phase 0). With one claw per - account, Phase 5 deleting `/bclaw/*` removes the account's entire secret +- **The SSM namespace is hardcoded (`/dispatch/`), not derived from `ClawName`.** Secrets live at + `/dispatch/` so the deployer's IAM policy can be scoped to + `parameter/dispatch/*` (see the setup skill's Phase 0). With one claw per + account, Phase 5 deleting `/dispatch/*` removes the account's entire secret set — correct for a full teardown. - **Order matters.** Always scale to 0 (Phase 1) before deleting the stack diff --git a/template/.env.example b/template/.env.example index ff39b3e..de847b4 100644 --- a/template/.env.example +++ b/template/.env.example @@ -14,4 +14,4 @@ export AWS_REGION=us-east-1 # Alternative to the two keys above: use a named AWS profile instead of static # keys (e.g. an SSO role configured via `aws configure`). Uncomment and set, # and delete the two keys above. -# AWS_PROFILE=bclaw-deployer +# AWS_PROFILE=dispatch-deployer diff --git a/template/AGENTS.md b/template/AGENTS.md index fa21624..0ec3257 100644 --- a/template/AGENTS.md +++ b/template/AGENTS.md @@ -1,7 +1,7 @@ -# bclaw +# dispatch -`bclaw` is a Hermes Agent claw (a long-running gateway) deployed as a Slack +`dispatch` is a Hermes Agent claw (a long-running gateway) deployed as a Slack socket-mode bot. It is outbound-only — no load balancer, no inbound ports. ## Search @@ -28,9 +28,9 @@ You can use web-search-prime to look things up that aren't obvious in the reposi - Deploys to **AWS ECS (EC2 launch type)** — a single container instance in an Auto Scaling Group (`min=max=desired=1`) with a persistent **EBS data volume** — - managed by the `setup-bclaw` / `teardown-bclaw` agent skills in + managed by the `setup-dispatch` / `teardown-dispatch` agent skills in `.agents/skills/`. The CloudFormation template lives alongside the setup - skill at `.agents/skills/setup-bclaw/template.yaml`. + skill at `.agents/skills/setup-dispatch/template.yaml`. - No derived image is built. The signed upstream `ghcr.io/boldblackai/harness` image is deployed as-is — host bind-mounts on the EBS volume support the 4-way mount layout directly, so no custom @@ -39,7 +39,7 @@ You can use web-search-prime to look things up that aren't obvious in the reposi ### AWS infrastructure -- Stack name = claw name (default `bclaw`), region `us-east-1`. +- Stack name = claw name (default `dispatch`), region `us-east-1`. - Dedicated VPC (10.0.0.0/16) with **1 public subnet in a single AZ** (EBS is zonal, so the volume, instance, and task all live in one AZ). The setup skill probes Graviton AZ availability via `describe-instance-type-offerings` and @@ -52,10 +52,10 @@ You can use web-search-prime to look things up that aren't obvious in the reposi and mounts it by filesystem label on every boot, so data survives ASG instance replacement. SQLite's WAL mode needs a real local block device (it is unsafe on NFS), which is the reason state is on EBS. -- Secrets are **SSM SecureString** parameters under the claw's `/bclaw/KEY` +- Secrets are **SSM SecureString** parameters under the claw's `/dispatch/KEY` namespace, written by the user in setup Phase 3 (piranesi pattern). Not stack-owned, so they survive stack updates/deletes. A Hermes secret-source - plugin (`aws_ssm`, installed in setup Phase 5) resolves every `/bclaw/*` + plugin (`aws_ssm`, installed in setup Phase 5) resolves every `/dispatch/*` parameter into the gateway env at startup, using the TaskRole's SSM-read grant — so adding/rotating a key is an SSM write + restart, no redeploy. This carries the Slack tokens (`SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`, diff --git a/template/README.md b/template/README.md index aef37e7..06095a4 100644 --- a/template/README.md +++ b/template/README.md @@ -1,4 +1,4 @@ -# bclaw +# dispatch A Hermes Agent claw — a long-running gateway deployed as a Slack socket-mode bot. It is outbound-only: no load balancer, no inbound ports. The Slack app @@ -31,32 +31,32 @@ run the skill. ### 1. Create the deployer IAM user -IAM → Users → Create user (e.g. `bclaw-deployer`) with programmatic access (an +IAM → Users → Create user (e.g. `dispatch-deployer`) with programmatic access (an access key). -### 2. Attach the `bclaw-deploy` policy and create the service role +### 2. Attach the `dispatch-deploy` policy and create the service role -Bclaw uses a **two-role** model so the deployer's long-lived access key is +dispatch uses a **two-role** model so the deployer's long-lived access key is never root-equivalent if it leaks: -- **`bclaw-deployer`** (the human identity, attached policy - [`bclaw-deploy-policy.json`](./bclaw-deploy-policy.json)) — narrow powers: +- **`dispatch-deployer`** (the human identity, attached policy + [`dispatch-deploy-policy.json`](./dispatch-deploy-policy.json)) — narrow powers: manage the CloudFormation stack, write/read SSM secrets, shell in, scale the service, debug the container instance, recover orphans during teardown, and - manage **one** literal role (`bclaw-cfn-exec`). -- **`bclaw-cfn-exec`** (the CloudFormation service role, inline policy - [`bclaw-cfn-exec-policy.json`](./bclaw-cfn-exec-policy.json), trust - [`bclaw-cfn-exec-trust.json`](./bclaw-cfn-exec-trust.json)) — the broad + manage **one** literal role (`dispatch-cfn-exec`). +- **`dispatch-cfn-exec`** (the CloudFormation service role, inline policy + [`dispatch-cfn-exec-policy.json`](./dispatch-cfn-exec-policy.json), trust + [`dispatch-cfn-exec-trust.json`](./dispatch-cfn-exec-trust.json)) — the broad infrastructure-create lifecycle (EC2/ASG/ECS/IAM/KMS/logs) that CloudFormation assumes during every deploy and the stack delete. It is **not** a stack resource (the stack cannot create the role it assumes to create itself), so the setup skill creates it idempotently in **Phase 0** before the first deploy, and the teardown skill deletes it last. -The deployer identity only ever passes `bclaw-cfn-exec` to CloudFormation +The deployer identity only ever passes `dispatch-cfn-exec` to CloudFormation (`iam:PassRole` conditioned to `cloudformation.amazonaws.com`); it never touches the infrastructure resources directly. Attach -[`bclaw-deploy-policy.json`](./bclaw-deploy-policy.json) to the user during +[`dispatch-deploy-policy.json`](./dispatch-deploy-policy.json) to the user during this onboarding step — the service role is created later, at deploy time. #### Deployer policy — why some resources stay `Resource: "*"` @@ -70,9 +70,9 @@ constraints: | Statement | Why it stays `*` (no condition) | |---|---| -| `ReadOnlyDescribe` | Merged read-only bucket: every `Describe*`/`List*` action across EC2/ASG/Logs/SSM/ECS is List-type with no resource-level support (AWS requires `*`). All read-only; the sensitive create/delete/mutate actions live on `bclaw-cfn-exec`. | -| `ECSRead` | `DescribeTaskDefinition`/`DescribeTasks`/`ListTasks` operate on runtime-assigned IDs and task-definition families with no resource-level support — AWS requires `*`. Read-only (used for probing the live service). The sensitive `RegisterTaskDefinition` write lives on `bclaw-cfn-exec`. | -| `SSMMessages` | Amazon Message Gateway Service (`ssmmessages`) does not support resource-level permissions at all — AWS requires `Resource: "*"` for all four channel actions. Needed for `aws ecs execute-command` (ECS Exec) over the host's internet path. The sensitive `ecs:ExecuteCommand` itself IS scoped to bclaw tasks/cluster (`ECSExec`). | +| `ReadOnlyDescribe` | Merged read-only bucket: every `Describe*`/`List*` action across EC2/ASG/Logs/SSM/ECS is List-type with no resource-level support (AWS requires `*`). All read-only; the sensitive create/delete/mutate actions live on `dispatch-cfn-exec`. | +| `ECSRead` | `DescribeTaskDefinition`/`DescribeTasks`/`ListTasks` operate on runtime-assigned IDs and task-definition families with no resource-level support — AWS requires `*`. Read-only (used for probing the live service). The sensitive `RegisterTaskDefinition` write lives on `dispatch-cfn-exec`. | +| `SSMMessages` | Amazon Message Gateway Service (`ssmmessages`) does not support resource-level permissions at all — AWS requires `Resource: "*"` for all four channel actions. Needed for `aws ecs execute-command` (ECS Exec) over the host's internet path. The sensitive `ecs:ExecuteCommand` itself IS scoped to dispatch tasks/cluster (`ECSExec`). | | `CloudFormationGlobalMeta` | `GetTemplateSummary`/`ValidateTemplate` are account-global metadata calls with no resource ARN — AWS requires `*`. Read-only (the deploy command runs them before the change set). | **Tag-conditioned statements** (ABAC — a tag condition restricts to the claw's @@ -80,19 +80,19 @@ resources; `EC2InstanceOps` is additionally ARN-scoped to `instance/*`): | Statement | Condition | What it protects | |---|---|---| -| `EC2NetworkingManage` | `aws:ResourceTag/Name = bclaw*` | Teardown recovery: can only delete/mutate the claw's own VPC/subnets/route-tables/IGW/SG left orphaned by a `FORCE_DELETE_STACK`. Cannot touch any other networking. (Network **create** lives on `bclaw-cfn-exec`.) | -| `EC2InstanceOps` | `aws:ResourceTag/ClawName = bclaw` (ARN-scoped to `instance/*`) | `manage-bclaw` Mode 4: read the console output of, or terminate, the claw's own container instances. Cannot touch co-tenant instances. | -| `EC2DataVolumeManage` | `aws:ResourceTag/Name = bclaw-data` | Teardown Phase 3: delete the claw's own retained data volume. | -| `KMSUseKey` | `kms:ResourceAliases = alias/bclaw-ssm` (+ `kms:ViaService = ssm`) | Decrypt/Encrypt/ScheduleKeyDeletion only on the claw's own CMK, only via SSM. Cannot use any other key. | +| `EC2NetworkingManage` | `aws:ResourceTag/Name = dispatch*` | Teardown recovery: can only delete/mutate the claw's own VPC/subnets/route-tables/IGW/SG left orphaned by a `FORCE_DELETE_STACK`. Cannot touch any other networking. (Network **create** lives on `dispatch-cfn-exec`.) | +| `EC2InstanceOps` | `aws:ResourceTag/ClawName = dispatch` (ARN-scoped to `instance/*`) | `manage-dispatch` Mode 4: read the console output of, or terminate, the claw's own container instances. Cannot touch co-tenant instances. | +| `EC2DataVolumeManage` | `aws:ResourceTag/Name = dispatch-data` | Teardown Phase 3: delete the claw's own retained data volume. | +| `KMSUseKey` | `kms:ResourceAliases = alias/dispatch-ssm` (+ `kms:ViaService = ssm`) | Decrypt/Encrypt/ScheduleKeyDeletion only on the claw's own CMK, only via SSM. Cannot use any other key. | -ARN-pinned statements (no `*`): `CloudFormation` (`stack/bclaw/*`), -`ManageCfnExecRole` (`role/bclaw-cfn-exec` — create/delete the one service -role), `PassRoleToCfn` (`role/bclaw-cfn-exec` → `cloudformation.amazonaws.com`), -`ECSExec` (`cluster/bclaw`, `task/bclaw/*`), `ECSServiceManage` (`cluster/bclaw`, -`service/bclaw/*`), `LogsRead` (`log-group:/ecs/bclaw*`), `SSMSecrets` -(`parameter/bclaw/*`), and `DenyDirectSSMSession` (`task/bclaw/*`, a Deny). +ARN-pinned statements (no `*`): `CloudFormation` (`stack/dispatch/*`), +`ManageCfnExecRole` (`role/dispatch-cfn-exec` — create/delete the one service +role), `PassRoleToCfn` (`role/dispatch-cfn-exec` → `cloudformation.amazonaws.com`), +`ECSExec` (`cluster/dispatch`, `task/dispatch/*`), `ECSServiceManage` (`cluster/dispatch`, +`service/dispatch/*`), `LogsRead` (`log-group:/ecs/dispatch*`), `SSMSecrets` +(`parameter/dispatch/*`), and `DenyDirectSSMSession` (`task/dispatch/*`, a Deny). -#### Service role (`bclaw-cfn-exec`) — the infrastructure-create lifecycle +#### Service role (`dispatch-cfn-exec`) — the infrastructure-create lifecycle The service role's inline policy carries everything CloudFormation needs to realize `template.yaml` and tear it back down. It is the natural home for the @@ -108,12 +108,12 @@ powers that cannot be safely scoped on a human-held key: service-linked role. - **Auto Scaling** group CRUD + the autoscaling service-linked role (`AutoScalingCreate`/`AutoScalingManage`/`CreateAutoScalingServiceLinkedRole`). -- **IAM** create/delete on `role/bclaw-*` and `instance-profile/bclaw-*` +- **IAM** create/delete on `role/dispatch-*` and `instance-profile/dispatch-*` (`IAMRoles`/`IAMInstanceProfiles`) — the stack's exec/task/instance roles. -- **`iam:PassRole`** on `role/bclaw-*` + `instance-profile/bclaw-*` (`PassRole`, +- **`iam:PassRole`** on `role/dispatch-*` + `instance-profile/dispatch-*` (`PassRole`, resource-scoped, **not** service-conditioned — `iam:PassedToService`-conditioning breaks this stack's Auto Scaling launch-template validation; the resource scope - is the boundary, and cfn-exec can only pass the stack's own `bclaw-*` roles). + is the boundary, and cfn-exec can only pass the stack's own `dispatch-*` roles). - **ECS** cluster/service/task-definition write + describe (`ECSWrite`/`ECSDescribe`). - **KMS** key lifecycle — `CreateKey`/`CreateAlias`/`DeleteAlias`/`PutKeyPolicy`/ `EnableKeyRotation`/`DescribeKey` (`KMSLifecycle`, `Resource: "*"`). The @@ -124,7 +124,7 @@ powers that cannot be safely scoped on a human-held key: - **SSM** public AMI-id resolution (`SSMPublicEcsAmi`) + read-only describes (`ReadOnlyDescribe`). -Because `bclaw-cfn-exec` is assumable **only** by `cloudformation.amazonaws.com` +Because `dispatch-cfn-exec` is assumable **only** by `cloudformation.amazonaws.com` (its trust policy) and the deployer's only `iam:PassRole` for it is conditioned to that same service, none of these broad powers are reachable by the human-held key — closing the privilege-escalation chains a leaked deployer key @@ -133,20 +133,20 @@ otherwise opens. #### Notes - **Shell-in (ECS Exec) permissions are on the deployer.** `ECSExec` grants - `ecs:ExecuteCommand` scoped to the bclaw cluster + tasks; `SSMMessages` + `ecs:ExecuteCommand` scoped to the dispatch cluster + tasks; `SSMMessages` grants the four `ssmmessages:*` channel actions (`ssmmessages` cannot be - resource-scoped). Needed by setup (Phases 6–7), teardown, and `manage-bclaw`. + resource-scoped). Needed by setup (Phases 6–7), teardown, and `manage-dispatch`. AWS additionally recommends **denying** `ssm:StartSession` on ECS tasks (`DenyDirectSSMSession`): sessions via `ecs:ExecuteCommand` are logged; direct SSM sessions bypass ECS Exec logging and consume the session quota. - **The deployer's `iam:PassRole` is `PassedToService`-conditioned** - (`PassRoleToCfn`: `bclaw-cfn-exec` → `cloudformation.amazonaws.com` only). The + (`PassRoleToCfn`: `dispatch-cfn-exec` → `cloudformation.amazonaws.com` only). The service role's `iam:PassRole` (`PassRole`) is **resource-scoped** to - `bclaw-*` roles/instance-profiles but **not** service-conditioned — + `dispatch-*` roles/instance-profiles but **not** service-conditioned — `iam:PassedToService`-conditioning breaks this stack's Auto Scaling launch-template validation (the same constraint that forces cfn-exec's KMS key-management actions to be unconditional). The boundary is the resource - scope: cfn-exec can only pass the stack's own `bclaw-*` roles, and cfn-exec + scope: cfn-exec can only pass the stack's own `dispatch-*` roles, and cfn-exec is itself assumable only by `cloudformation.amazonaws.com`. - **No `iam:SimulatePrincipalPolicy`.** The deployer policy intentionally omits it (a leaked key should not be able to probe its own scope). Permission gaps @@ -188,7 +188,7 @@ aws sts get-caller-identity --query 'Account' --output text The claw runs as a Slack **socket-mode** bot — it makes an outbound WebSocket connection to Slack, so there is no inbound URL to host (the `hermes-agent.local` request URLs in `slack-manifest.json` are placeholders, -ignored under socket mode). The manifest fully defines the app: name (`bclaw`), +ignored under socket mode). The manifest fully defines the app: name (`dispatch`), the slash commands, OAuth scopes, event subscriptions, bot user, and interactivity. Socket mode is on by default in the manifest. @@ -201,7 +201,7 @@ interactivity. Socket mode is on by default in the manifest. **Create**. The app comes up with socket mode on, every slash command registered, the bot -user `bclaw`, OAuth scopes, and event subscriptions already configured — +user `dispatch`, OAuth scopes, and event subscriptions already configured — nothing to toggle by hand. ### App-level token (socket mode auth) @@ -211,28 +211,28 @@ Slack generates separately (it can't live in the manifest): - **Basic Information** → **App-Level Tokens** → **Generate Token and Scope**. - Name it (e.g. `socket`), add the **`connections:write`** scope → **Generate**. -- Copy the `xapp-` token → this becomes `/bclaw/SLACK_APP_TOKEN`. +- Copy the `xapp-` token → this becomes `/dispatch/SLACK_APP_TOKEN`. ### Install to the workspace - **OAuth & Permissions** → **Install to Workspace** → authorize. - Copy the **Bot User OAuth Token** (`xoxb-`) → this becomes - `/bclaw/SLACK_BOT_TOKEN`. + `/dispatch/SLACK_BOT_TOKEN`. ### Channel + user IDs -- **Your user ID** (for `/bclaw/SLACK_ALLOWED_USERS`): in Slack, click your +- **Your user ID** (for `/dispatch/SLACK_ALLOWED_USERS`): in Slack, click your profile → **Copy member ID**. Comma-separate multiple IDs. -- **Home channel ID** (for `/bclaw/SLACK_HOME_CHANNEL`): right-click the +- **Home channel ID** (for `/dispatch/SLACK_HOME_CHANNEL`): right-click the channel → **Copy link**, take the trailing ID. -- If the home channel is **private**, invite the bot with `/invite @bclaw` so +- If the home channel is **private**, invite the bot with `/invite @dispatch` so it can read and post there (public channels are covered by its `channels:*` scopes once installed). ## Secrets you'll need -The claw needs SSM SecureString parameters under the `/bclaw/` namespace. The -scaffolder renames `bclaw` to your claw name everywhere — SSM paths, the IAM +The claw needs SSM SecureString parameters under the `/dispatch/` namespace. The +scaffolder renames `dispatch` to your claw name everywhere — SSM paths, the IAM scope, and the CloudFormation template — so the namespace matches the claw name. It is a literal in the template (not derived from the `ClawName` parameter at deploy time), which is what lets the IAM policy pin it to a fixed ARN prefix. @@ -241,7 +241,7 @@ not CloudFormation resources, so they survive stack updates and deletes. A Hermes secret-source plugin (`aws_ssm`, from [boldblackai/hermes-aws-ssm-secret-source](https://github.com/boldblackai/hermes-aws-ssm-secret-source), -installed during setup) resolves every `/bclaw/*` parameter into the gateway's +installed during setup) resolves every `/dispatch/*` parameter into the gateway's environment at startup, so adding or rotating a key is just an SSM write + task restart — no template edit, no redeploy. The only secret NOT resolved by the plugin is the optional GitHub token (`GH_TOKEN_VAL`): the on-boot `gh auth @@ -252,14 +252,14 @@ stack parameter gates it). Gather the values beforehand: | SSM key | What it is | Where to find it | |---|---|---| -| `/bclaw/SLACK_BOT_TOKEN` | Slack bot OAuth token (`xoxb-`) | Slack app → OAuth & Permissions → Bot User OAuth Token | -| `/bclaw/SLACK_APP_TOKEN` | Slack app-level token (`xapp-`, enables socket mode) | Slack app → Basic Information → App-Level Tokens | -| `/bclaw/SLACK_ALLOWED_USERS` | Comma-separated Slack user IDs allowed to use the bot | Slack profile → "Copy member ID" | -| `/bclaw/SLACK_HOME_CHANNEL` | Slack channel ID the bot treats as home | Right-click channel → "Copy link", take the trailing ID | +| `/dispatch/SLACK_BOT_TOKEN` | Slack bot OAuth token (`xoxb-`) | Slack app → OAuth & Permissions → Bot User OAuth Token | +| `/dispatch/SLACK_APP_TOKEN` | Slack app-level token (`xapp-`, enables socket mode) | Slack app → Basic Information → App-Level Tokens | +| `/dispatch/SLACK_ALLOWED_USERS` | Comma-separated Slack user IDs allowed to use the bot | Slack profile → "Copy member ID" | +| `/dispatch/SLACK_HOME_CHANNEL` | Slack channel ID the bot treats as home | Right-click channel → "Copy link", take the trailing ID | ### Optional: GitHub authentication -`/bclaw/GH_TOKEN_VAL` is the ONE secret still injected via CloudFormation +`/dispatch/GH_TOKEN_VAL` is the ONE secret still injected via CloudFormation (`secrets[]` + the `EnableGitHubKey` stack parameter, Phase 1 step 3), because the on-boot `gh auth login --with-token` runs before Hermes (and the aws_ssm plugin) start. Enable it only if the agent should make authenticated @@ -268,28 +268,28 @@ injected. | SSM key | What it is | Where to find it | |---|---|---| -| `/bclaw/GH_TOKEN_VAL` | GitHub PAT — on-boot `gh auth login` (see setup skill Phase 6a). Named `*_VAL`, not `GH_TOKEN`, to avoid `gh`'s reserved env var | https://github.com/settings/tokens | +| `/dispatch/GH_TOKEN_VAL` | GitHub PAT — on-boot `gh auth login` (see setup skill Phase 6a). Named `*_VAL`, not `GH_TOKEN`, to avoid `gh`'s reserved env var | https://github.com/settings/tokens | ### Inference-provider key (create at least one) | SSM key | What it is | Where to find it | |---|---|---| -| `/bclaw/OPENROUTER_API_KEY` | OpenRouter API key (recommended) | https://openrouter.ai/keys | -| `/bclaw/ANTHROPIC_API_KEY` | Anthropic (direct Claude API) | https://console.anthropic.com/ | -| `/bclaw/ZAI_API_KEY` | Z.AI / Zhipu (GLM) | https://z.ai/manage-apikey/apikey-list | +| `/dispatch/OPENROUTER_API_KEY` | OpenRouter API key (recommended) | https://openrouter.ai/keys | +| `/dispatch/ANTHROPIC_API_KEY` | Anthropic (direct Claude API) | https://console.anthropic.com/ | +| `/dispatch/ZAI_API_KEY` | Z.AI / Zhipu (GLM) | https://z.ai/manage-apikey/apikey-list | The aws_ssm plugin resolves every provider key present in SSM, so you can create more than one if the gateway uses multiple providers. Create at least the one matching the provider you chose in Phase 1. Each parameter is a **SecureString** and must be encrypted with the claw's own -KMS key (alias `alias/bclaw-ssm`), created by the setup skill's CloudFormation +KMS key (alias `alias/dispatch-ssm`), created by the setup skill's CloudFormation stack in Phase 2 — **not** the default `alias/aws/ssm`. The deployer IAM policy -pins `kms:Decrypt`/`kms:Encrypt` to `alias/bclaw-ssm` via +pins `kms:Decrypt`/`kms:Encrypt` to `alias/dispatch-ssm` via `kms:ResourceAliases`, so the claw can only decrypt parameters this key encrypted. A parameter left under the default SSM key fails to decrypt, so the aws_ssm plugin can't resolve it and the gateway runs without it. In the -console's KMS key picker, type `alias/bclaw-ssm` (substituting your claw name) — +console's KMS key picker, type `alias/dispatch-ssm` (substituting your claw name) — it resolves to the key the stack just created. @@ -298,13 +298,13 @@ it resolves to the key the stack just created. ### 4. Run the setup skill It follows a gated sequence: create the CloudFormation service role -(`bclaw-cfn-exec`) → probe one ARM64 AZ → deploy CloudFormation (VPC, EBS +(`dispatch-cfn-exec`) → probe one ARM64 AZ → deploy CloudFormation (VPC, EBS volume, EC2 container instance + Auto Scaling Group, ECS service at DesiredCount 0 on the first deploy) → write SSM secrets → scale to 1 → overlay `agent_home/` + install the aws_ssm plugin + merge its secrets config → restart → verify. -`.agents/skills/setup-bclaw/SKILL.md` +`.agents/skills/setup-dispatch/SKILL.md` Permissions are not pre-checked — if the deployer principal is missing an action, CloudFormation surfaces the exact `is not authorized to perform` error @@ -313,4 +313,4 @@ at deploy time (Phase 2). Fix any gap in the policy from step 2 and re-run. ## Tear down -`.agents/skills/teardown-bclaw/SKILL.md` +`.agents/skills/teardown-dispatch/SKILL.md` diff --git a/template/agent_home/AGENTHOME.md b/template/agent_home/AGENTHOME.md index 1c65744..220dc86 100644 --- a/template/agent_home/AGENTHOME.md +++ b/template/agent_home/AGENTHOME.md @@ -1,7 +1,7 @@ # agent_home/ This directory is the **source of truth for curated claw state**. The -`manage-bclaw` skill overlays its contents 1:1 onto the running +`manage-dispatch` skill overlays its contents 1:1 onto the running claw's `/home/harness/.hermes/` (EFS-backed). ## What goes here @@ -31,6 +31,6 @@ every update: ## How it's applied -See `.agents/skills/manage-bclaw/SKILL.md`. The overlay is a merge +See `.agents/skills/manage-dispatch/SKILL.md`. The overlay is a merge with overwrite: new files are added, changed files are overwritten, and files absent from here are preserved on the claw. diff --git a/template/agent_home/SOUL.md b/template/agent_home/SOUL.md index 4b798fb..2a73e86 100644 --- a/template/agent_home/SOUL.md +++ b/template/agent_home/SOUL.md @@ -1,6 +1,6 @@ # Personality -You are 'bclaw', a helpful senior engineer that helps teams get their work done. +You are 'dispatch', a helpful senior engineer that helps teams get their work done. ## Style - Be direct without being cold diff --git a/template/agent_home/config.yaml b/template/agent_home/config.yaml index b242cce..a272e54 100644 --- a/template/agent_home/config.yaml +++ b/template/agent_home/config.yaml @@ -1,8 +1,8 @@ # aws_ssm secret source for Hermes. Pushed into the live ~/.hermes/config.yaml -# by the manage-bclaw skill's Merge-config mode (run once during setup); the +# by the manage-dispatch skill's Merge-config mode (run once during setup); the # overlay excludes config.yaml on purpose. The aws_ssm plugin # (hermes-aws-ssm-secret-source, installed during setup) resolves the -# explicitly-listed /bclaw/* SecureString params into env vars at the first +# explicitly-listed /dispatch/* SecureString params into env vars at the first # gateway start, using the TaskRole's read-ssm-params grant. The plugin is # mapped-only: only the params listed under `env:` are fetched, and only # SecureString params are accepted (missing or non-SecureString entries warn @@ -16,10 +16,10 @@ secrets: override_existing: true # rotate centrally without a .env edit region: "" # empty = botocore default chain (AWS_REGION / profile / task role) env: - SLACK_BOT_TOKEN: /bclaw/SLACK_BOT_TOKEN - SLACK_APP_TOKEN: /bclaw/SLACK_APP_TOKEN - SLACK_ALLOWED_USERS: /bclaw/SLACK_ALLOWED_USERS - SLACK_HOME_CHANNEL: /bclaw/SLACK_HOME_CHANNEL - OPENROUTER_API_KEY: /bclaw/OPENROUTER_API_KEY - ANTHROPIC_API_KEY: /bclaw/ANTHROPIC_API_KEY - ZAI_API_KEY: /bclaw/ZAI_API_KEY + SLACK_BOT_TOKEN: /dispatch/SLACK_BOT_TOKEN + SLACK_APP_TOKEN: /dispatch/SLACK_APP_TOKEN + SLACK_ALLOWED_USERS: /dispatch/SLACK_ALLOWED_USERS + SLACK_HOME_CHANNEL: /dispatch/SLACK_HOME_CHANNEL + OPENROUTER_API_KEY: /dispatch/OPENROUTER_API_KEY + ANTHROPIC_API_KEY: /dispatch/ANTHROPIC_API_KEY + ZAI_API_KEY: /dispatch/ZAI_API_KEY diff --git a/template/bclaw-cfn-exec-policy.json b/template/dispatch-cfn-exec-policy.json similarity index 90% rename from template/bclaw-cfn-exec-policy.json rename to template/dispatch-cfn-exec-policy.json index 1cfde0a..01f2ae7 100644 --- a/template/bclaw-cfn-exec-policy.json +++ b/template/dispatch-cfn-exec-policy.json @@ -12,7 +12,7 @@ "Resource": "*", "Condition": { "StringLike": { - "aws:RequestTag/Name": "bclaw*" + "aws:RequestTag/Name": "dispatch*" } } }, @@ -50,7 +50,7 @@ "Resource": "*", "Condition": { "StringLike": { - "aws:ResourceTag/Name": "bclaw*" + "aws:ResourceTag/Name": "dispatch*" } } }, @@ -61,7 +61,7 @@ "Resource": "*", "Condition": { "StringEquals": { - "aws:RequestTag/Name": "bclaw-data" + "aws:RequestTag/Name": "dispatch-data" } } }, @@ -85,7 +85,7 @@ "Resource": "*", "Condition": { "StringEquals": { - "aws:RequestTag/ClawName": "bclaw" + "aws:RequestTag/ClawName": "dispatch" } } }, @@ -101,7 +101,7 @@ "Resource": "*", "Condition": { "StringEquals": { - "aws:ResourceTag/ClawName": "bclaw" + "aws:ResourceTag/ClawName": "dispatch" } } }, @@ -127,7 +127,7 @@ "iam:ListAttachedRolePolicies", "iam:UpdateAssumeRolePolicy" ], - "Resource": "arn:aws:iam::*:role/bclaw-*" + "Resource": "arn:aws:iam::*:role/dispatch-*" }, { "Sid": "IAMInstanceProfiles", @@ -139,15 +139,15 @@ "iam:AddRoleToInstanceProfile", "iam:RemoveRoleFromInstanceProfile" ], - "Resource": "arn:aws:iam::*:instance-profile/bclaw-*" + "Resource": "arn:aws:iam::*:instance-profile/dispatch-*" }, { "Sid": "PassRole", "Effect": "Allow", "Action": "iam:PassRole", "Resource": [ - "arn:aws:iam::*:role/bclaw-*", - "arn:aws:iam::*:instance-profile/bclaw-*" + "arn:aws:iam::*:role/dispatch-*", + "arn:aws:iam::*:instance-profile/dispatch-*" ] }, { @@ -162,9 +162,9 @@ "ecs:RegisterTaskDefinition" ], "Resource": [ - "arn:aws:ecs:*:*:cluster/bclaw", - "arn:aws:ecs:*:*:service/bclaw/*", - "arn:aws:ecs:*:*:task-definition/bclaw:*" + "arn:aws:ecs:*:*:cluster/dispatch", + "arn:aws:ecs:*:*:service/dispatch/*", + "arn:aws:ecs:*:*:task-definition/dispatch:*" ] }, { @@ -211,7 +211,7 @@ "logs:ListTagsForResource", "logs:UntagResource" ], - "Resource": "arn:aws:logs:*:*:log-group:/ecs/bclaw*" + "Resource": "arn:aws:logs:*:*:log-group:/ecs/dispatch*" }, { "Sid": "SSMPublicEcsAmi", diff --git a/template/bclaw-cfn-exec-trust.json b/template/dispatch-cfn-exec-trust.json similarity index 100% rename from template/bclaw-cfn-exec-trust.json rename to template/dispatch-cfn-exec-trust.json diff --git a/template/bclaw-deploy-policy.json b/template/dispatch-deploy-policy.json similarity index 87% rename from template/bclaw-deploy-policy.json rename to template/dispatch-deploy-policy.json index c3e2590..8363be2 100644 --- a/template/bclaw-deploy-policy.json +++ b/template/dispatch-deploy-policy.json @@ -18,7 +18,7 @@ "cloudformation:ExecuteChangeSet", "cloudformation:DeleteChangeSet" ], - "Resource": "arn:aws:cloudformation:*:*:stack/bclaw/*" + "Resource": "arn:aws:cloudformation:*:*:stack/dispatch/*" }, { "Sid": "CloudFormationGlobalMeta", @@ -80,7 +80,7 @@ "Resource": "*", "Condition": { "StringLike": { - "aws:ResourceTag/Name": "bclaw*" + "aws:ResourceTag/Name": "dispatch*" } } }, @@ -94,7 +94,7 @@ "Resource": "*", "Condition": { "StringEquals": { - "aws:ResourceTag/Name": "bclaw-data" + "aws:ResourceTag/Name": "dispatch-data" } } }, @@ -108,7 +108,7 @@ "Resource": "arn:aws:ec2:*:*:instance/*", "Condition": { "StringEquals": { - "aws:ResourceTag/ClawName": "bclaw" + "aws:ResourceTag/ClawName": "dispatch" } } }, @@ -124,13 +124,13 @@ "iam:GetRolePolicy", "iam:UpdateAssumeRolePolicy" ], - "Resource": "arn:aws:iam::*:role/bclaw-cfn-exec" + "Resource": "arn:aws:iam::*:role/dispatch-cfn-exec" }, { "Sid": "PassRoleToCfn", "Effect": "Allow", "Action": "iam:PassRole", - "Resource": "arn:aws:iam::*:role/bclaw-cfn-exec", + "Resource": "arn:aws:iam::*:role/dispatch-cfn-exec", "Condition": { "StringEquals": { "iam:PassedToService": "cloudformation.amazonaws.com" @@ -142,8 +142,8 @@ "Effect": "Allow", "Action": "ecs:ExecuteCommand", "Resource": [ - "arn:aws:ecs:*:*:cluster/bclaw", - "arn:aws:ecs:*:*:task/bclaw/*" + "arn:aws:ecs:*:*:cluster/dispatch", + "arn:aws:ecs:*:*:task/dispatch/*" ] }, { @@ -151,8 +151,8 @@ "Effect": "Allow", "Action": "ecs:UpdateService", "Resource": [ - "arn:aws:ecs:*:*:cluster/bclaw", - "arn:aws:ecs:*:*:service/bclaw/*" + "arn:aws:ecs:*:*:cluster/dispatch", + "arn:aws:ecs:*:*:service/dispatch/*" ] }, { @@ -182,7 +182,7 @@ "Sid": "DenyDirectSSMSession", "Effect": "Deny", "Action": "ssm:StartSession", - "Resource": "arn:aws:ecs:*:*:task/bclaw/*" + "Resource": "arn:aws:ecs:*:*:task/dispatch/*" }, { "Sid": "LogsRead", @@ -192,7 +192,7 @@ "logs:FilterLogEvents", "logs:GetLogEvents" ], - "Resource": "arn:aws:logs:*:*:log-group:/ecs/bclaw*" + "Resource": "arn:aws:logs:*:*:log-group:/ecs/dispatch*" }, { "Sid": "SSMSecrets", @@ -203,7 +203,7 @@ "ssm:GetParameters", "ssm:DeleteParameter" ], - "Resource": "arn:aws:ssm:*:*:parameter/bclaw/*" + "Resource": "arn:aws:ssm:*:*:parameter/dispatch/*" }, { "Sid": "KMSUseKey", @@ -216,7 +216,7 @@ "Resource": "*", "Condition": { "ForAnyValue:StringLike": { - "kms:ResourceAliases": "alias/bclaw-ssm" + "kms:ResourceAliases": "alias/dispatch-ssm" }, "StringEquals": { "kms:ViaService": "ssm.us-east-1.amazonaws.com" diff --git a/template/slack-manifest.json b/template/slack-manifest.json index a23edc6..ebc6f40 100644 --- a/template/slack-manifest.json +++ b/template/slack-manifest.json @@ -4,8 +4,8 @@ "minor_version": 1 }, "display_information": { - "name": "bclaw", - "description": "bclaw", + "name": "dispatch", + "description": "dispatch", "background_color": "#1a1a2e" }, "features": { @@ -15,7 +15,7 @@ "messages_tab_read_only_enabled": false }, "bot_user": { - "display_name": "bclaw", + "display_name": "dispatch", "always_online": true }, "assistant_view": { From 634499ebc863f9fead016382bcc76b63ee2eaea0 Mon Sep 17 00:00:00 2001 From: BoldBlackBot <296328274+BoldBlackBot@users.noreply.github.com> Date: Sat, 15 Aug 2026 17:47:42 -0400 Subject: [PATCH 4/5] chore(meta): rename package identity, release plumbing, and repo self-references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit package.json: name @boldblackai/create-dispatch, bin create-dispatch, version 1.1.0, repository URL github.com/boldblackai/create-dispatch, homepage dispatch.boldblack.ai. tag-on-merge.yml idempotence guard now checks @boldblackai/create-dispatch@. AGENTS.md self-references and the corkboard journal namespace (journal:create-dispatch:) follow; the release skill and the 2026-07-14 integration journal's operative template-file references are updated. The dated citation of rfcs/2026-06-27_bclaw-cli-scaffolder.md stays — it is a filename of a historical, immutable RFC. Co-Authored-By: Enrique Canals <84596+EnriqueCanals@users.noreply.github.com> --- .agents/skills/release/SKILL.md | 16 +++++++-------- .github/workflows/tag-on-merge.yml | 2 +- AGENTS.md | 20 +++++++++---------- package.json | 12 +++++------ .../2026-07-14_scope-ecs-cfn-exec-write.md | 4 ++-- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.agents/skills/release/SKILL.md b/.agents/skills/release/SKILL.md index 11777e4..8ea6c8e 100644 --- a/.agents/skills/release/SKILL.md +++ b/.agents/skills/release/SKILL.md @@ -1,9 +1,9 @@ --- name: release -description: Automate releasing the @boldblackai/create-bclaw npm package. Use this skill whenever the user wants to cut a release, publish a new version, bump the version, tag a release, update the CHANGELOG, or run npm publish. Triggers on phrases like "release version X", "cut a release", "publish", "bump to X.X.X", "tag this release", "release the project", or any combination of version bumping + publishing intent. Always use this skill for release work — don't attempt ad-hoc release steps without it. +description: Automate releasing the @boldblackai/create-dispatch npm package. Use this skill whenever the user wants to cut a release, publish a new version, bump the version, tag a release, update the CHANGELOG, or run npm publish. Triggers on phrases like "release version X", "cut a release", "publish", "bump to X.X.X", "tag this release", "release the project", or any combination of version bumping + publishing intent. Always use this skill for release work — don't attempt ad-hoc release steps without it. --- -# Release Skill for `@boldblackai/create-bclaw` +# Release Skill for `@boldblackai/create-dispatch` Automates the full release pipeline: pre-flight checks → version bump → CHANGELOG → verify → build → open release PR → (maintainer merges) → CI auto-tags, publishes to npm (OIDC), creates GitHub release. @@ -11,7 +11,7 @@ Automates the full release pipeline: pre-flight checks → version bump → CHAN > > **Release model:** The trust boundary is "can merge a PR to main" = "can release." The agent has zero upstream write access — it opens the PR from its fork (BoldBlackBot); the maintainer's squash-merge triggers everything. > -> **Prerequisite (one-time, manual on npmjs.com):** Configure the trusted publisher for `@boldblackai/create-bclaw` under Settings → Trusted Publisher → GitHub Actions: org=`boldblackai`, repo=`create-bclaw`, workflow filename=`tag-on-merge.yml`. Then under Settings → Publishing access, select "Require two-factor authentication and disallow tokens" (recommended) — OIDC publishes are unaffected by this setting. +> **Prerequisite (one-time, manual on npmjs.com):** Configure the trusted publisher for `@boldblackai/create-dispatch` under Settings → Trusted Publisher → GitHub Actions: org=`boldblackai`, repo=`create-dispatch`, workflow filename=`tag-on-merge.yml`. Then under Settings → Publishing access, select "Require two-factor authentication and disallow tokens" (recommended) — OIDC publishes are unaffected by this setting. ## Step 1: Pre-flight checks (abort on failure) @@ -122,7 +122,7 @@ git commit -m "release v" Ensure a fork remote exists (for the agent's bot account): ```bash -git remote add fork https://github.com/BoldBlackBot/create-bclaw.git 2>/dev/null || true +git remote add fork https://github.com/BoldBlackBot/create-dispatch.git 2>/dev/null || true ``` Push the release branch: @@ -135,7 +135,7 @@ Open the PR — the squash merge commit message (`release v`) is the se ```bash gh pr create \ - --repo boldblackai/create-bclaw \ + --repo boldblackai/create-dispatch \ --head BoldBlackBot:release/v \ --base main \ --title "release v" \ @@ -166,19 +166,19 @@ After the PR is squash-merged, the merge commit (`release v (#N)`) trig ### 11a: Verify tag-on-merge ran (tag + npm + release) ```bash -gh run list --repo boldblackai/create-bclaw --workflow tag-on-merge.yml --limit 1 +gh run list --repo boldblackai/create-dispatch --workflow tag-on-merge.yml --limit 1 ``` Confirm the workflow succeeded. If it failed, check logs: ```bash -gh run view --repo boldblackai/create-bclaw --log-failed +gh run view --repo boldblackai/create-dispatch --log-failed ``` Once the workflow succeeds, verify the package landed on npm **with provenance attestations**: ```bash -npm view @boldblackai/create-bclaw@ dist --json +npm view @boldblackai/create-dispatch@ dist --json ``` Confirm the output includes an `attestations` field (not just `signatures`). If `attestations` is missing, the publish did not generate provenance — investigate before continuing. diff --git a/.github/workflows/tag-on-merge.yml b/.github/workflows/tag-on-merge.yml index 2b084ff..5becb4e 100644 --- a/.github/workflows/tag-on-merge.yml +++ b/.github/workflows/tag-on-merge.yml @@ -53,7 +53,7 @@ jobs: - name: Publish to npm (OIDC trusted publishing) run: | VERSION="${{ steps.version.outputs.version }}" - if npm view "@boldblackai/create-bclaw@${VERSION}" dist --json >/dev/null 2>&1; then + if npm view "@boldblackai/create-dispatch@${VERSION}" dist --json >/dev/null 2>&1; then echo "Version $VERSION already published, skipping" else npm publish diff --git a/AGENTS.md b/AGENTS.md index 412ec0f..f74b2ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,10 +19,10 @@ Significant changes, architectural decisions, and new features should be propose ## Overview -This repo is `@boldblackai/create-bclaw`, an `npx`-distributed CLI that +This repo is `@boldblackai/create-dispatch`, an `npx`-distributed CLI that generates a renamed skeleton of a Hermes Agent claw repo. Running -`npx @boldblackai/create-bclaw foo` produces a `foo/` directory whose contents -match the bundled `template/` snapshot except every lowercase `bclaw` reference +`npx @boldblackai/create-dispatch foo` produces a `foo/` directory whose contents +match the bundled `template/` snapshot except every lowercase `dispatch` reference — file contents and file/directory names, including the SSM namespace, IAM scopes, and KMS alias — is renamed to `foo`. A second literal token, `us-east-1`, is substituted with the chosen AWS region (`--region`, default `us-east-1`) so @@ -64,10 +64,10 @@ steps and current-state facts only. ## Layout: Generator Repo + Integration Repo Since it doesn't make sense to deploy changes made in /workspace (its just templates + generator), we use an integration -repository instead (`/alt/integration`), which has AWS creds and represents a live, deployed bclaw we can make changes to. +repository instead (`/alt/integration`), which has AWS creds and represents a live, deployed dispatch agent we can make changes to. -- `/workspace` (no aws access): the `create-bclaw` project, it creates project skeletons from `template/` -- `/alt/integration` (aws access via mise): a project created from `create-bclaw`; we edit and iterate on THIS repo, and +- `/workspace` (no aws access): the `create-dispatch` project, it creates project skeletons from `template/` +- `/alt/integration` (aws access via mise): a project created from `create-dispatch`; we edit and iterate on THIS repo, and integrate ("port back") changes back into `/workspace/template/` once we verify they work. ### Workflow conventions @@ -80,11 +80,11 @@ integrate ("port back") changes back into `/workspace/template/` once we verify ### Integration cycles -Working through a change to bclaw templates (skills, policies, CFN, etc), such as implementing a proposed RFC, goes through what +Working through a change to dispatch templates (skills, policies, CFN, etc), such as implementing a proposed RFC, goes through what is known as an "Integration Cycle". We always start a cycle by creating an integration journal and applying/testing our changes into `/alt/integration`. Once I (and only I) confirm the changes work in the `/alt/integration` project (this requires a deploy or a possible regeneration), we can integrate our changes back into -the `create-bclaw` templates under `/workspace` and run the golden test. We can use the integration cycle journal to help us integrate our changes. +the `create-dispatch` templates under `/workspace` and run the golden test. We can use the integration cycle journal to help us integrate our changes. #### Port-back: diff `/alt/integration` against `template/` @@ -112,7 +112,7 @@ is scoped to what a generated cluster inherits from `template/`. #### Integration cycle journal Format -To aid in porting back changes, keep a journal of issues we encountered during an integration cycle. The journal lives in the `journal:create-bclaw:` namespace on the **corkboard** (DokuWiki), as a page like `journal:create-bclaw:YYYY-MM-DD_short_title`, authored in DokuWiki syntax — Markdown renders as literal text, so use wiki markup (see the corkboard skill) — in journal-style append-only format: +To aid in porting back changes, keep a journal of issues we encountered during an integration cycle. The journal lives in the `journal:create-dispatch:` namespace on the **corkboard** (DokuWiki), as a page like `journal:create-dispatch:YYYY-MM-DD_short_title`, authored in DokuWiki syntax — Markdown renders as literal text, so use wiki markup (see the corkboard skill) — in journal-style append-only format: - `====== Title ======` — short descriptive title (page H1) - `**Date:**` — date (ISO format) @@ -121,7 +121,7 @@ To aid in porting back changes, keep a journal of issues we encountered during a ONLY add issues, do not talk about plans or implementation details (the rfc is for that, just link to it). -Once the port-back is complete and the golden test passes, **set the journal's status to `Done` and leave it in place** — it now lives permanently in the `journal:create-bclaw:` namespace as a record of the cycle, so do not delete it. +Once the port-back is complete and the golden test passes, **set the journal's status to `Done` and leave it in place** — it now lives permanently in the `journal:create-dispatch:` namespace as a record of the cycle, so do not delete it. ## Tool Versions diff --git a/package.json b/package.json index 27c5bd7..0ce19a6 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { - "name": "@boldblackai/create-bclaw", - "version": "1.0.2", - "description": "CLI to generate bclaw repositories", + "name": "@boldblackai/create-dispatch", + "version": "1.1.0", + "description": "CLI to generate dispatch agent repositories", "type": "module", "publishConfig": { "access": "public" }, "bin": { - "create-bclaw": "dist/cli.js" + "create-dispatch": "dist/cli.js" }, "packageManager": "pnpm@11.9.0", "files": [ @@ -45,7 +45,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/boldblackai/create-bclaw.git" + "url": "git+https://github.com/boldblackai/create-dispatch.git" }, - "homepage": "https://bclaw.sh" + "homepage": "https://dispatch.boldblack.ai" } diff --git a/references/integrations/2026-07-14_scope-ecs-cfn-exec-write.md b/references/integrations/2026-07-14_scope-ecs-cfn-exec-write.md index bd87910..63fbd07 100644 --- a/references/integrations/2026-07-14_scope-ecs-cfn-exec-write.md +++ b/references/integrations/2026-07-14_scope-ecs-cfn-exec-write.md @@ -7,7 +7,7 @@ Related RFC: none (journal-only cycle). ## Goal -Tighten two areas of `template/bclaw-cfn-exec-policy.json`: +Tighten two areas of `template/dispatch-cfn-exec-policy.json`: 1. **`ECSWrite`** — the `Resource` array ends with `"*"`, which supersedes the three specific ARNs above it, so `ecs:DeleteCluster` (and every other action @@ -15,7 +15,7 @@ Tighten two areas of `template/bclaw-cfn-exec-policy.json`: AWS service-authorization reference shows 6 of the 7 actions support resource-level perms; only `DeregisterTaskDefinition` requires `*`. Fix: split into `ECSWriteScoped` (the 6 actions on the existing - `cluster/bclaw` / `service/bclaw/*` / `task-definition/bclaw:*` ARNs, **no + `cluster/dispatch` / `service/dispatch/*` / `task-definition/dispatch:*` ARNs, **no trailing `*`**) + `ECSWriteGlobal` (`DeregisterTaskDefinition` alone on `*`). 2. **`EC2NetworkingCreate`** — all 6 creates + a broad `CreateTags` on `*` From 81e6b491fbb0206e30348e2fb4ca84e56650725c Mon Sep 17 00:00:00 2001 From: BoldBlackBot <296328274+BoldBlackBot@users.noreply.github.com> Date: Sat, 15 Aug 2026 17:47:49 -0400 Subject: [PATCH 5/5] docs: rewrite README naming and add rename RFC + 1.1.0 changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README: product is a dispatch agent (countable-noun phrasing), install command npx @boldblackai/create-dispatch, repo links point at create-dispatch, npm-init shorthand npm init @boldblackai/dispatch, and the BusinessClaw etymology sentence is removed — the new name is not short for anything. rfcs/2026-08-15_rename-to-dispatch.md records the rationale, replacement map, and migration notes (old npm name stays installable pending deprecation; deployed agents unaffected). CHANGELOG gains the 1.1.0 entry. Co-Authored-By: Enrique Canals <84596+EnriqueCanals@users.noreply.github.com> --- CHANGELOG.md | 13 ++++++ README.md | 30 +++++++------- rfcs/2026-08-15_rename-to-dispatch.md | 60 +++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 rfcs/2026-08-15_rename-to-dispatch.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e0a1362..d6d6235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [1.1.0] - 2026-08-15 + +### Summary + +The product is renamed: the npm package is now `@boldblackai/create-dispatch` (the GitHub repo becomes `boldblackai/create-dispatch`; the old URL redirects). The generator's rename token is now `dispatch` — `npx @boldblackai/create-dispatch ` produces a skeleton with the SSM namespace `/dispatch/`, KMS alias `alias/dispatch-ssm`, and the `setup-dispatch` / `manage-dispatch` / `teardown-dispatch` skills. The advertised npm-init shorthand is corrected to `npm init @boldblackai/dispatch ` (npm prepends `create-` itself; the previous `@boldblackai/bclaw` line resolved to the old package). Already-deployed agents are **not** affected — renames apply to new generations only; existing installs keep working and can migrate at any time by running `npx @boldblackai/create-dispatch `. + +### Changes + +- 634499e chore(meta): rename package identity, release plumbing, and repo self-references +- 985b5bc feat(template): sweep rename token bclaw → dispatch across template/ +- 9469a8e feat(cli,generator): rename rename-token and CLI identity bclaw → dispatch +- 9db5e0d test: sweep golden test tokens bclaw → dispatch + ## [1.0.2] - 2026-08-01 ### Summary diff --git a/README.md b/README.md index 1c1de82..7acc8ec 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,33 @@ -# @boldblackai/create-bclaw +# @boldblackai/create-dispatch -Create a repository for your own bclaw, deployed to your own AWS account. +Create a repository for your own dispatch agent, deployed to your own AWS account. -## What is a bclaw? +## What is a dispatch agent? -bclaw is short for "**B**usinessClaw": an opinionated deployment of [hermes-agent](https://hermes-agent.nousresearch.com/) configured as a +A dispatch agent is an opinionated deployment of [hermes-agent](https://hermes-agent.nousresearch.com/) configured as a long-running ["claw"](https://www.cnet.com/tech/services-and-software/claw-ai-explainer-openclaw-nvidia/) within your Slack workspace. -Create, customize and deploy as many as you'd like. Each generated bclaw repository corresponds to one specific long-running agent and Slack application/user. +Create, customize and deploy as many as you'd like. Each generated repository corresponds to one specific long-running agent and Slack application/user. -For example, you could generate a `@swe-pal` for a "Devin" type experience: code reviews, pull requests, etc. Or, a `@reportclaw` that posts reports at scheduled times to configured channels. +For example, you could generate a `@swe-pal` for a "Devin" type experience: code reviews, pull requests, etc. Or, a `@reporter` that posts reports at scheduled times to configured channels. ## How it works -1. Generate your bclaw repository +1. Generate your dispatch agent repository - npx @boldblackai/create-bclaw swe-pal + npx @boldblackai/create-dispatch swe-pal 2. Follow the instructions in the README to create the IAM user and policy to get the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` for `.env`. This also walks you through creating and installing the Slack app into your workspace to get the `SLACK_APP_TOKEN` and `SLACK_BOT_TOKEN` you'll need later. 3. The generated repository is a set of skills, so open up `swe-pal` in your favorite harness ([Pi](https://boldblackai.github.io/harness/agents/pi/), [Hermes](https://boldblackai.github.io/harness/agents/hermes/), [OpenCode](https://boldblackai.github.io/harness/agents/opencode/)) -4. Run the `/setup-bclaw` skill. This will prompt you for an inference provider, it supports [OpenRouter](https://openrouter.ai/), [ZAI](https://z.ai/subscribe), and [Anthropic](https://www.anthropic.com/) out of the box, but trivial +4. Run the `/setup-dispatch` skill. This will prompt you for an inference provider, it supports [OpenRouter](https://openrouter.ai/), [ZAI](https://z.ai/subscribe), and [Anthropic](https://www.anthropic.com/) out of the box, but trivial to use any that [hermes-agent already supports](https://hermes-agent.nousresearch.com/docs/integrations/providers/). -5. To manage it (update running image version, update skills/SOUL.md, etc) you can use the `/manage-bclaw` skill. +5. To manage it (update running image version, update skills/SOUL.md, etc) you can use the `/manage-dispatch` skill. -6. To uninstall it, run the `/teardown-bclaw` skill. +6. To uninstall it, run the `/teardown-dispatch` skill. ## What you get @@ -40,9 +40,9 @@ to use any that [hermes-agent already supports](https://hermes-agent.nousresearc ## Usage ```bash -npx @boldblackai/create-bclaw +npx @boldblackai/create-dispatch # or equivalently -npm init @boldblackai/bclaw +npm init @boldblackai/dispatch ``` If no name is given (and stdin is a TTY), you'll be prompted for one. @@ -60,7 +60,7 @@ into the generated claw (notably the deployer IAM policy's `kms:ViaService`, which is a static JSON that can't use CloudFormation's `${AWS::Region}`): ```bash -npx @boldblackai/create-bclaw --region us-west-2 +npx @boldblackai/create-dispatch --region us-west-2 ``` `--region` must match `^[a-z]{2}(-gov)?-[a-z]+-[0-9]+$` (any AWS region, @@ -72,7 +72,7 @@ is a TTY you'll be prompted; otherwise the default is used silently. - `--region ` — AWS region to bake into the claw (default `us-east-1`). Substituted into the deployer IAM policy's `kms:ViaService` so the claw works in that region. - `--force` — generate into a non-empty target directory, merging with existing files (default: refuse). - `--version`, `-V` — print the version. -- `--help`, `-h` — show help. +- `--help`, `-h` — show this help. ## Development diff --git a/rfcs/2026-08-15_rename-to-dispatch.md b/rfcs/2026-08-15_rename-to-dispatch.md new file mode 100644 index 0000000..48b6c91 --- /dev/null +++ b/rfcs/2026-08-15_rename-to-dispatch.md @@ -0,0 +1,60 @@ +# Rename bclaw → dispatch + +**Date:** 2026-08-15 +**Status:** Proposed + +## Goal + +Rename the product from **bclaw** to **dispatch** across the generator repo: npm package, GitHub repo, homepage, CLI identity, generator rename token, and the bundled `template/` snapshot. This is Phase 1 of the product rename — the code sweep lands first; the GitHub repo and npm package renames follow after this PR merges (copy leads, services catch up). + +## Motivation + +The name is changing at the product level (site copy precedent: "Name your dispatch agent", `/setup-dispatch`, `/manage-dispatch`, `/teardown-dispatch`). The new name is not short for anything — the "BusinessClaw" etymology is retired. `dispatch` reads as a normal English word and works as a scoped package suffix, a repo suffix, and a token inside generated IAM/SSM/KMS identifiers. + +## Technical Details + +### Replacement map (condensed) + +| Old | New | +|---|---| +| npm package `@boldblackai/create-bclaw` | `@boldblackai/create-dispatch` (stays scoped) | +| npm-init shorthand `npm init @boldblackai/bclaw` | `npm init @boldblackai/dispatch` (npm prepends `create-` itself; the old line resolved to `@boldblackai/create-bclaw`) | +| GitHub repo `boldblackai/create-bclaw` | `boldblackai/create-dispatch` (renamed on GitHub **after** this PR merges; redirect covers old links) | +| homepage `https://bclaw.sh` | `https://dispatch.boldblack.ai` | +| bin `create-bclaw` | `create-dispatch` | +| generator rename token `RENAME_FROM = "bclaw"` | `RENAME_FROM = "dispatch"` | +| template SSM namespace `/bclaw/` | `/dispatch/` | +| template skills `setup-bclaw` / `manage-bclaw` / `teardown-bclaw` | `setup-dispatch` / `manage-dispatch` / `teardown-dispatch` (dirs, `SKILL.md` frontmatter names, cross-references) | +| git init author fallback `create-bclaw@local` | `create-dispatch@local` | + +Region token handling (`us-east-1`) is untouched. + +The template must keep the token **lowercase and standalone** (rename-model constraint — a literal substring replace is the whole transform). The one capitalized sentence start ("Bclaw uses a two-role model") is normalized to lowercase as part of the sweep. `template/` contained no pre-existing English-word `dispatch` occurrences, so the new token cannot collide with prose. + +The golden test is updated first (TDD): it now generates with `name=dispatch` expecting `template/` byte-for-byte, and its residual grep hunts `dispatch`. + +### Verification + +- Golden test (17 tests): `pnpm test` — byte-for-byte template fidelity, both token renames, residual greps. +- Sweep gate: `grep -rniE "bclaw" .` → zero hits outside historical RFCs (dated records keep their titles). + +## Migration Notes + +- **Existing deployed agents are NOT affected.** The rename changes what the generator emits for *new* generations; running deployments keep their generated names, SSM namespaces, and IAM scopes untouched. +- **npm:** the old package `@boldblackai/create-bclaw` remains installable and will be deprecated with a pointer to `@boldblackai/create-dispatch`. Users should switch to `npx @boldblackai/create-dispatch `. Note: the new name's first publish must be manual (trusted publishing cannot do first publishes, npm/cli#8544); trusted-publisher registration follows, then `npm deprecate` of the old name. +- **GitHub:** renaming `boldblackai/create-bclaw` → `boldblackai/create-dispatch` leaves a redirect on the old URL, so existing clones and links keep working (local clones should update their remote URL at their leisure). +- **Homepage:** `https://bclaw.sh` → `https://dispatch.boldblack.ai` (DNS/site cutover follows the repo rename). + +## Implementation Checklist + +- [x] Golden test swept to the new token (RED first, then GREEN) +- [x] `src/generate.ts` — `RENAME_FROM = "dispatch"`; `src/cli.ts` — banner, help, prompt default, git-init identity, commit message; npm-init shorthand corrected +- [x] `template/` — token sweep in contents; `git mv` for policy files and the three skill directories +- [x] `package.json` — name, bin, version 1.1.0, repository URL, homepage; lockfile regenerated +- [x] `tag-on-merge.yml` idempotence guard → `@boldblackai/create-dispatch@${VERSION}` +- [x] `README.md` rewritten (naming, install command, links, etymology removed) +- [x] `CHANGELOG.md` — 1.1.0 entry +- [x] `AGENTS.md` self-references + `journal:create-dispatch:` corkboard namespace +- [ ] (post-merge) GitHub repo renamed to `boldblackai/create-dispatch` +- [ ] (post-merge) manual first publish of `@boldblackai/create-dispatch`, trusted-publisher registration, deprecate `@boldblackai/create-bclaw` +- [ ] (post-merge) homepage DNS cutover to `dispatch.boldblack.ai`