diff --git a/docs/superpowers/plans/2026-10-24-dsh-port-spike.md b/docs/superpowers/plans/2026-10-24-dsh-port-spike.md index 2c8f5f7..fd6d423 100644 --- a/docs/superpowers/plans/2026-10-24-dsh-port-spike.md +++ b/docs/superpowers/plans/2026-10-24-dsh-port-spike.md @@ -34,7 +34,7 @@ - [x] Bootstrap `using-superpowers`: резолв версии, marker, dedupe, compaction. - [x] `/role`: парсинг, два слоя, инъекция, followup. - [x] `dsh/install.sh`, `mcp.codegraph.yml`, README/PORT. -- [x] `bash dsh/tests/run.sh` — 25 тестов зелёные. +- [x] `bash dsh/tests/run.sh` — 26 тестов зелёные. - [x] `bash tests/run.sh` — 630 тестов зелёные (регрессий нет). - [x] Живой `guard-main`: `write` и `bash` на main → `BLOCKED`. - [x] Живой `require-plan`: `src/probe.ts` без `.bounded` → `BLOCKED`; diff --git a/dsh/PORT.md b/dsh/PORT.md index fd3493b..ec2dab5 100644 --- a/dsh/PORT.md +++ b/dsh/PORT.md @@ -98,7 +98,7 @@ from the profile, not from the DSH installation, so a DSH-internal import fails at activation with `ERR_MODULE_NOT_FOUND`. `lib/message.js` is a local copy of the tiny `createUserMessage` constructor, and `Config` was replaced by defaults normalized in `apply()`. The pure modules are testable with plain Node; this is -why the port's 25 tests run without a DSH process. +why the port's 26 tests run without a DSH process. ### Guard flow diff --git a/dsh/bundle/lib/guards.js b/dsh/bundle/lib/guards.js index 60561d5..096e6ff 100644 --- a/dsh/bundle/lib/guards.js +++ b/dsh/bundle/lib/guards.js @@ -28,6 +28,13 @@ const PLAN_TOOLS = new Set(['write', 'edit', 'multi_edit', 'str_replace_editor'] const DEFAULT_TIMEOUT_MS = 15_000 +/** Claude skill namespaces do not exist in DSH; rewrite the hook messages. */ +export function forDshReason(reason) { + return String(reason) + .replaceAll('superpowers:using-git-worktrees', 'using-git-worktrees') + .replaceAll('superpowers:brainstorming', 'brainstorming') +} + /** * Run one hook script with the synthesized payload. * @returns the denial text for exit 2, otherwise undefined. @@ -49,7 +56,7 @@ export function runHook({ script, hooksDir, payload, logger, timeoutMs = DEFAULT } catch (error) { if (error?.status === 2) { const reason = String(error.stderr ?? '').trim() - return reason || `BLOCKED by ${script}` + return forDshReason(reason || `BLOCKED by ${script}`) } // Fail open: a broken hook must never stop the agent. logger?.warn?.('[dsh-harness] %s failed open: %s', script, error?.message ?? String(error)) diff --git a/dsh/tests/guard.test.mjs b/dsh/tests/guard.test.mjs index 4c210c3..1c367fc 100644 --- a/dsh/tests/guard.test.mjs +++ b/dsh/tests/guard.test.mjs @@ -2,7 +2,7 @@ import assert from 'node:assert/strict' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import test from 'node:test' -import { checkTool } from '../bundle/lib/guards.js' +import { checkTool, forDshReason } from '../bundle/lib/guards.js' import { cleanup, git, makeRepo, write } from './helpers.mjs' const hooksDir = fileURLToPath(new URL('../bundle/hooks/', import.meta.url)) @@ -20,11 +20,20 @@ test('guard-main blocks a write on main', () => { hooksDir, }) assert.match(reason ?? '', /BLOCKED \[guard-main\]/) + assert.doesNotMatch(reason ?? '', /superpowers:/) + assert.match(reason ?? '', /using-git-worktrees/) } finally { cleanup(repo) } }) +test('forDshReason removes the Claude skill namespace from hook messages', () => { + assert.equal( + forDshReason('Run superpowers:brainstorming; use superpowers:using-git-worktrees.'), + 'Run brainstorming; use using-git-worktrees.', + ) +}) + test('guard-main allows a write on a feature branch', () => { const repo = makeRepo() try { @@ -72,6 +81,8 @@ test('require-plan blocks src edits without a plan or .bounded', () => { hooksDir, }) assert.match(blocked ?? '', /BLOCKED \[require-plan\]/) + assert.doesNotMatch(blocked ?? '', /superpowers:/) + assert.match(blocked ?? '', /brainstorming/) write(join(repo, '.bounded'), '') const bounded = checkTool({