DSH: порт claude-harness — спайк (guard, require-plan, bootstrap, /role) #36
@ -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`;
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user