import assert from 'node:assert/strict' import { join } from 'node:path' import test from 'node:test' import { apply } from '../bundle/index.js' import { buildHarnessCheckText, buildRoleHintText, detectAdoption, detectRole, isHarnessMessage, makePrestepMarker, } from '../bundle/lib/prestep.js' import { cleanup, tempDir, write } from './helpers.mjs' const SECTION = '## OpenSpec и superpowers\n\nСекция подключения.\n' /** * A throwaway directory tree that looks like a harness-adopted repository: * `.git/`, `openspec/`, `.codegraph/`, and the CLAUDE.md section. */ function adoptedRepo() { const repo = tempDir('dsh-harness-adopted-') write(join(repo, '.git', 'HEAD'), 'ref: refs/heads/main\n') write(join(repo, 'openspec', 'specs', '.keep'), '') write(join(repo, '.codegraph', '.keep'), '') write(join(repo, 'CLAUDE.md'), `# Проект\n\n${SECTION}`) return repo } // --------------------------------------------------------------------------- // detectAdoption // --------------------------------------------------------------------------- test('detectAdoption: adopted repo reports adopted with no missing signals', () => { const repo = adoptedRepo() try { assert.deepEqual(detectAdoption(repo), { adopted: true, root: repo, missing: [] }) } finally { cleanup(repo) } }) test('detectAdoption: repo without openspec/ and the CLAUDE.md section is not adopted', () => { const repo = tempDir('dsh-harness-partial-') try { write(join(repo, '.git', 'HEAD'), 'ref: refs/heads/main\n') write(join(repo, '.codegraph', '.keep'), '') write(join(repo, 'CLAUDE.md'), '# Проект без секции\n') assert.deepEqual(detectAdoption(repo), { adopted: false, root: repo, missing: ['openspec/', 'секция «OpenSpec и superpowers» в CLAUDE.md'], }) } finally { cleanup(repo) } }) test('detectAdoption: cwd in a subdirectory walks up to the repo root', () => { const repo = adoptedRepo() try { const subdir = join(repo, 'dsh', 'bundle', 'lib') write(join(subdir, 'module.js'), 'export {}\n') assert.deepEqual(detectAdoption(subdir), { adopted: true, root: repo, missing: [] }) } finally { cleanup(repo) } }) test('detectAdoption: cwd outside any git repo stays silent', () => { const outside = tempDir('dsh-harness-outside-') try { assert.deepEqual(detectAdoption(outside), { adopted: true, root: null, missing: [] }) } finally { cleanup(outside) } }) test('detectAdoption: a .git file (worktree checkout) is a repo root too', () => { const repo = tempDir('dsh-harness-worktree-') try { write(join(repo, '.git'), `gitdir: ${join(repo, '..', 'main', '.git', 'worktrees', 'x')}\n`) write(join(repo, 'openspec', 'specs', '.keep'), '') write(join(repo, '.codegraph', '.keep'), '') write(join(repo, 'CLAUDE.md'), SECTION) assert.deepEqual(detectAdoption(join(repo, 'openspec')), { adopted: true, root: repo, missing: [], }) } finally { cleanup(repo) } }) // --------------------------------------------------------------------------- // buildHarnessCheckText // --------------------------------------------------------------------------- test('buildHarnessCheckText names the root, missing signals, and next steps', () => { const text = buildHarnessCheckText('/tmp/proj', [ 'openspec/', 'секция «OpenSpec и superpowers» в CLAUDE.md', ]) assert.match( text, /^\[dsh-harness\] Проект \/tmp\/proj не подключён к обвязке: нет: openspec\/; секция «OpenSpec и superpowers» в CLAUDE\.md\. /, ) assert.match(text, /openspec init --tools agents/) assert.match(text, /codegraph init/) assert.match(text, /Предложи владельцу настройку/) }) // --------------------------------------------------------------------------- // detectRole // --------------------------------------------------------------------------- test('detectRole: admin keywords (jira, гант, спринт)', () => { assert.equal(detectRole('Разбери jira-задачу'), 'admin') assert.equal(detectRole('Покажи гант проекта'), 'admin') assert.equal(detectRole('Новый спринт начинается'), 'admin') assert.equal(detectRole('JIRA-тикет'), 'admin') }) test('detectRole: qa keywords (стенд, прогон, скриншот, макет)', () => { assert.equal(detectRole('Подними стенд'), 'qa') assert.equal(detectRole('Нужен прогон сценария'), 'qa') assert.equal(detectRole('Сделай скриншот'), 'qa') assert.equal(detectRole('Сверь с макетом'), 'qa') }) test('detectRole: analyst keywords (тз, oq, спека, методика)', () => { assert.equal(detectRole('Оформи ТЗ'), 'analyst') assert.equal(detectRole('Закрой OQ'), 'analyst') assert.equal(detectRole('Спека готова'), 'analyst') assert.equal(detectRole('Нужна методика'), 'analyst') }) test('detectRole: dev keywords (срез, issue, pr, тест, миграция)', () => { assert.equal(detectRole('Сними срез метрик'), 'dev') assert.equal(detectRole('Открой issue'), 'dev') assert.equal(detectRole('Проверь тест'), 'dev') assert.equal(detectRole('Нужна миграция'), 'dev') }) test('detectRole: pr matches only as a whole word', () => { assert.equal(detectRole('сделай PR'), 'dev') assert.equal(detectRole('просто посмотри'), null) assert.equal(detectRole('preview the branch'), null) }) test('detectRole: priority is admin before qa, analyst, and dev', () => { assert.equal(detectRole('спринт и тест'), 'admin') assert.equal(detectRole('прогон и issue'), 'qa') assert.equal(detectRole('спека и тест'), 'analyst') }) test('detectRole: no keyword or empty input yields null', () => { assert.equal(detectRole('Привет, как дела?'), null) assert.equal(detectRole(''), null) }) // --------------------------------------------------------------------------- // buildRoleHintText // --------------------------------------------------------------------------- test('buildRoleHintText: known role names it and asks to load the skill', () => { const text = buildRoleHintText('qa') assert.ok(text.startsWith('[dsh-harness]')) assert.match(text, /Роль сессии: qa\./) assert.match(text, /Вызови skill role\./) }) test('buildRoleHintText: null asks one question with all four roles', () => { const text = buildRoleHintText(null) assert.ok(text.startsWith('[dsh-harness]')) assert.match(text, /Роль сессии неясна\./) for (const role of ['dev', 'admin', 'analyst', 'qa']) { assert.ok(text.includes(`${role} —`), `${role} variant is listed`) } assert.match(text, /четырьмя вариантами/) }) // --------------------------------------------------------------------------- // isHarnessMessage // --------------------------------------------------------------------------- test('isHarnessMessage: bootstrap, DSH, and legacy claude-harness texts are harness messages', () => { assert.equal(isHarnessMessage('\nYou have superpowers.'), true) assert.equal(isHarnessMessage('[dsh-harness] Роль сессии: dev.'), true) assert.equal(isHarnessMessage('[claude-harness] Роль сессии: dev'), true) }) test('isHarnessMessage: plain user text is not a harness message', () => { assert.equal(isHarnessMessage('Привет! Разбери задачу.'), false) assert.equal(isHarnessMessage(''), false) }) // --------------------------------------------------------------------------- // makePrestepMarker // --------------------------------------------------------------------------- test('makePrestepMarker: marker format, body prefix, determinism, uniqueness', () => { const { marker, body } = makePrestepMarker('Привет') assert.match(marker, /^$/) assert.ok(body.startsWith(marker)) assert.equal(body, `${marker}\nПривет`) assert.equal(makePrestepMarker('Привет').marker, marker, 'same text, same marker') assert.notEqual(makePrestepMarker('Пока').marker, marker, 'different text, different marker') assert.notEqual(makePrestepMarker('привет').marker, marker, 'case matters') }) // --------------------------------------------------------------------------- // apply(): wiring of harness-check and role-hint into agent/pre-step // --------------------------------------------------------------------------- /** Text of the first text block of a message. */ function messageText(message) { return message.content[0].text } /** * apply() against a fake ctx: the `agent/pre-step` listener is captured and * can be run with a canned decision. The session cwd is served by the fake * workingDirectory; bootstrap is off in most cases so its big text does not * pollute the message lists. */ function prestepHarness(config = {}, cwd) { const listeners = {} const ctx = { tools: { guard() {} }, on(event, handler) { listeners[event] = handler }, inject() {}, workingDirectory: { get() { return cwd } }, logger: { info() {}, warn() {}, error() {} }, } apply(ctx, config) const handler = listeners['agent/pre-step'] assert.ok(handler, 'agent/pre-step listener is registered') return { run({ agent = {}, messages = [] } = {}) { return handler({ agent, signal: {} }, async () => ({ kind: 'continue', messages })) }, } } /** A tree with `.git` but none of the adoption signals. */ function nonAdoptedRepo() { const repo = tempDir('dsh-harness-noadopt-') write(join(repo, '.git', 'HEAD'), 'ref: refs/heads/main\n') write(join(repo, 'CLAUDE.md'), '# Проект без секции\n') return repo } function userMessage(text) { return { content: [{ type: 'text', text }], source: { kind: 'user' } } } test('apply: harness-check injects when adoption signals are missing', async () => { const repo = nonAdoptedRepo() try { const harness = prestepHarness({ bootstrap: false }, repo) const decision = await harness.run({ agent: { session: 's1' }, messages: [] }) assert.equal(decision.messages.length, 1) const text = messageText(decision.messages[0]) assert.match(text, /^/) assert.match(text, /\[dsh-harness\] Проект .* не подключён к обвязке/) assert.match(text, /openspec init --tools agents/) assert.equal(decision.messages[0].source.kind, 'user') } finally { cleanup(repo) } }) test('apply: harness-check stays silent for an adopted repo and outside git', async () => { const repo = adoptedRepo() try { const harness = prestepHarness({ bootstrap: false }, repo) const decision = await harness.run({ agent: { session: 's1' }, messages: [] }) assert.deepEqual(decision.messages, []) } finally { cleanup(repo) } const outside = tempDir('dsh-harness-outside-') try { const harness = prestepHarness({ bootstrap: false }, outside) const decision = await harness.run({ agent: { session: 's1' }, messages: [] }) assert.deepEqual(decision.messages, []) } finally { cleanup(outside) } }) test('apply: role-hint injects Роль сессии: dev for a keyword user message', async () => { const outside = tempDir('dsh-harness-outside-') try { const harness = prestepHarness({ bootstrap: false }, outside) const user = userMessage('Напиши тест для модуля') const decision = await harness.run({ agent: { session: 's1' }, messages: [user] }) assert.equal(decision.messages.length, 2) const text = messageText(decision.messages[1]) assert.match(text, /^/) assert.match(text, /\[dsh-harness\] Роль сессии: dev\./) } finally { cleanup(outside) } }) test('apply: role-hint asks the four-variant question when no keyword matches', async () => { const outside = tempDir('dsh-harness-outside-') try { const harness = prestepHarness({ bootstrap: false }, outside) const user = userMessage('Привет, как дела?') const decision = await harness.run({ agent: { session: 's1' }, messages: [user] }) assert.equal(decision.messages.length, 2) const text = messageText(decision.messages[1]) assert.match(text, /^/) assert.match(text, /\[dsh-harness\] Роль сессии неясна\./) assert.match(text, /четырьмя вариантами/) } finally { cleanup(outside) } }) test('apply: role-hint stays silent when the step has no new user message', async () => { const outside = tempDir('dsh-harness-outside-') try { const harness = prestepHarness({ bootstrap: false }, outside) const decision = await harness.run({ agent: { session: 's1' }, messages: [] }) assert.deepEqual(decision.messages, []) } finally { cleanup(outside) } }) test('apply: a user message that equals the role-hint question does not re-trigger role-hint', async () => { const outside = tempDir('dsh-harness-outside-') try { const harness = prestepHarness({ bootstrap: false }, outside) const user = userMessage(buildRoleHintText(null)) const decision = await harness.run({ agent: { session: 's1' }, messages: [user] }) assert.deepEqual(decision.messages, [user], 'isHarnessMessage filters before detectRole') } finally { cleanup(outside) } }) test('apply: a [claude-harness]-prefixed user message is skipped by role-hint', async () => { const outside = tempDir('dsh-harness-outside-') try { const harness = prestepHarness({ bootstrap: false }, outside) const user = userMessage('[claude-harness] Роль сессии: dev. Вызови skill role.') const decision = await harness.run({ agent: { session: 's1' }, messages: [user] }) assert.deepEqual(decision.messages, [user], 'legacy prefix is filtered too') } finally { cleanup(outside) } }) test('apply: bootstrap:false leaves harness-check wired (the gate is ctx.on, not bootstrapOk)', async () => { const repo = nonAdoptedRepo() try { const harness = prestepHarness({ bootstrap: false, roleHint: false }, repo) const decision = await harness.run({ agent: { session: 's1' }, messages: [] }) assert.equal(decision.messages.length, 1) assert.match(messageText(decision.messages[0]), /не подключён к обвязке/) } finally { cleanup(repo) } }) // --------------------------------------------------------------------------- // Subagent skip (T2.1): real runtime delegation signals // --------------------------------------------------------------------------- /** * Agent shapes that the runtime uses for a delegated step. The old * parent/parentAgent/parentSessionId/depth checks were dead; these three are * the live ones. */ const SUBAGENT_AGENTS = [ { session: { header: { delegationDepth: 1 } } }, { session: { header: { parentSession: 's0' } } }, { options: { subagentDepth: 1 } }, ] test('apply: a delegated pre-step injects nothing (bootstrap, harness-check, role-hint)', async () => { const repo = nonAdoptedRepo() const skill = tempDir('dsh-harness-skill-') try { write(join(skill, 'SKILL.md'), '---\nname: using-superpowers\n---\nbody\n') const harness = prestepHarness({ bootstrapSkillPath: join(skill, 'SKILL.md') }, repo) for (const agent of SUBAGENT_AGENTS) { const decision = await harness.run({ agent, messages: [userMessage('Напиши тест')] }) assert.equal(decision.messages.length, 1, `no injection for ${JSON.stringify(agent)}`) assert.equal(messageText(decision.messages[0]), 'Напиши тест') } } finally { cleanup(repo) cleanup(skill) } }) test('apply: a plain pre-step still injects bootstrap, harness-check, and role-hint', async () => { const repo = nonAdoptedRepo() const skill = tempDir('dsh-harness-skill-') try { write(join(skill, 'SKILL.md'), '---\nname: using-superpowers\n---\nbody\n') const harness = prestepHarness({ bootstrapSkillPath: join(skill, 'SKILL.md') }, repo) const decision = await harness.run({ agent: { session: 's1' }, messages: [userMessage('Напиши тест')] }) assert.equal(decision.messages.length, 4) assert.equal(messageText(decision.messages[0]), 'Напиши тест') const texts = decision.messages.map(messageText) assert.ok(texts.some((text) => text.includes('DSH-HARNESS-BOOTSTRAP')), 'bootstrap injected') assert.ok(texts.some((text) => text.includes('не подключён к обвязке')), 'harness-check injected') assert.ok(texts.some((text) => text.includes('Роль сессии: dev')), 'role-hint injected') } finally { cleanup(repo) cleanup(skill) } })