114 lines
4.7 KiB
JavaScript
114 lines
4.7 KiB
JavaScript
/**
|
||
* Role loading for the `/role` command.
|
||
*
|
||
* The file format stays the claude-harness format: a global layer
|
||
* (`~/.claude/roles/<role>.md`) and an optional project layer
|
||
* (`<workspace>/.claude/roles/<role>.md`). The project layer wins on conflict.
|
||
* Auto-memory is Claude Code-only; this port explicitly drops that step and
|
||
* leaves the role's "Читать на старте" section to the model.
|
||
*/
|
||
|
||
import { existsSync, readFileSync } from 'node:fs'
|
||
import { homedir } from 'node:os'
|
||
import { join } from 'node:path'
|
||
|
||
export const DEFAULT_ROLE_NAMES = ['dev', 'admin', 'analyst', 'qa']
|
||
|
||
/** Parse `/role [name]` without owning any grammar beyond the first word. */
|
||
export function parseRole(rawInput, roleNames = DEFAULT_ROLE_NAMES) {
|
||
const input = String(rawInput ?? '').trim()
|
||
if (input === '') return { kind: 'list' }
|
||
const role = input.split(/\s+/u)[0]
|
||
if (!roleNames.includes(role)) return { kind: 'invalid', role }
|
||
return { kind: 'role', role }
|
||
}
|
||
|
||
/** Absolute paths of both layers for one role. */
|
||
export function rolePaths({ role, cwd, rolesDir = join(homedir(), '.claude', 'roles'), projectRolesDir = '' }) {
|
||
const globalPath = join(rolesDir, `${role}.md`)
|
||
const projectPath = join(projectRolesDir || join(cwd, '.claude', 'roles'), `${role}.md`)
|
||
return { globalPath, projectPath }
|
||
}
|
||
|
||
function readIfPresent(path) {
|
||
try {
|
||
return existsSync(path) ? readFileSync(path, 'utf8') : undefined
|
||
} catch {
|
||
return undefined
|
||
}
|
||
}
|
||
|
||
/** Render the list shown by a bare `/role`. */
|
||
export function listRoles({ cwd, rolesDir, projectRolesDir, roleNames = DEFAULT_ROLE_NAMES }) {
|
||
const lines = [
|
||
`Роли claude-harness; общий слой: ${rolesDir || join(homedir(), '.claude', 'roles')}`,
|
||
`Workspace: ${cwd}`,
|
||
'',
|
||
]
|
||
for (const role of roleNames) {
|
||
const { globalPath, projectPath } = rolePaths({ role, cwd, rolesDir, projectRolesDir })
|
||
const global = existsSync(globalPath) ? 'есть' : 'нет'
|
||
const project = existsSync(projectPath) ? 'есть' : 'нет'
|
||
lines.push(`- ${role}: общий — ${global}; проектный — ${project}`)
|
||
}
|
||
lines.push('', 'Вызов: /role dev | /role admin | /role analyst | /role qa')
|
||
return lines.join('\n')
|
||
}
|
||
|
||
/**
|
||
* Build the model-visible role instructions.
|
||
* @throws when the global role file is missing.
|
||
*/
|
||
export function buildRoleText({ role, cwd, rolesDir, projectRolesDir, roleNames = DEFAULT_ROLE_NAMES }) {
|
||
if (!roleNames.includes(role)) throw new Error(`Неизвестная роль: ${role}`)
|
||
|
||
const { globalPath, projectPath } = rolePaths({ role, cwd, rolesDir, projectRolesDir })
|
||
const globalText = readIfPresent(globalPath)
|
||
if (globalText === undefined) {
|
||
throw new Error(`Нет общего файла роли: ${globalPath}`)
|
||
}
|
||
const projectText = readIfPresent(projectPath)
|
||
const projectMissing = projectText === undefined
|
||
|
||
const sections = [
|
||
`[dsh-harness] Роль сессии: ${role}. Workspace: ${cwd}`,
|
||
'Новая роль заменяет предыдущую целиком: правила прежней роли больше не действуют.',
|
||
'',
|
||
`## Общий слой: ${globalPath}`,
|
||
globalText.trim(),
|
||
'',
|
||
]
|
||
|
||
if (projectMissing) {
|
||
sections.push(
|
||
`## Проектный слой: ${projectPath}`,
|
||
`Проектного файла роли ${role} нет — работаю по общему слою.`,
|
||
'',
|
||
)
|
||
} else {
|
||
sections.push(`## Проектный слой: ${projectPath}`, projectText.trim(), '')
|
||
}
|
||
|
||
sections.push(
|
||
'## Память',
|
||
'Auto-memory в DeepSeek Harness нет. Раздел «Читать на старте» выполни сам: пути читай через read,',
|
||
'команды запускай через bash и только на чтение. Строки с префиксом `brief:` в этом spike не поддерживаются —',
|
||
'пропусти их и скажи об этом одной строкой.',
|
||
'',
|
||
'## Ответ',
|
||
'Ответь ровно пятью строками:',
|
||
'- Роль: <role> — <описание из первой строки файла роли>.',
|
||
'- Процесс: <одна строка из раздела «Процесс» — что действует по умолчанию>.',
|
||
'- Прочитано: <перечень файлов обоих слоёв и файлов памяти, через запятую>.',
|
||
'- Не делать: <раздел «Не делать» одной строкой>.',
|
||
'- Ждёт решения владельца: <из памяти роли; если нет — «ничего»>.',
|
||
)
|
||
|
||
return {
|
||
text: sections.join('\n'),
|
||
globalPath,
|
||
projectPath,
|
||
projectMissing,
|
||
}
|
||
}
|