DSH: установка без зависимости от ~/.claude (#49) #64
48
dsh/PORT.md
48
dsh/PORT.md
@ -81,6 +81,13 @@ file has to be installed as `$DSH_HOME/AGENTS.md` because DSH does not read
|
||||
- **Full installer**: `install.sh` prepares global instructions and skills; the
|
||||
bundle itself is installed by `plugin_manager` because the Desktop profile is
|
||||
Electron-managed.
|
||||
- **Vendored superpowers**: the install-time source is explicit
|
||||
(`--superpowers-from`, `DSH_SUPERPOWERS_SOURCE`, or a clone in
|
||||
`~/.dsh/superpowers`); a copy inside this repository is rejected because
|
||||
upstream moves independently (change `dsh-install-source`, D-1). Removing the
|
||||
legacy `~/.claude` fallbacks is deferred until every machine has rerun
|
||||
`--skills --roles`; until then both runtime and installer warn when they use
|
||||
one.
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
@ -162,10 +169,10 @@ for the repository layout:
|
||||
roleCommand: true
|
||||
bootstrapSubagents: false
|
||||
hooksDir: '' # empty -> bundle/hooks -> ../../hooks
|
||||
rolesDir: '' # empty -> ~/.dsh/roles, ~/.claude/roles, repo roles/
|
||||
rolesDir: '' # empty -> ~/.dsh/roles, legacy ~/.claude/roles, repo roles/
|
||||
projectRolesDir: '' # empty -> <cwd>/.claude/roles
|
||||
roleNames: [dev, admin, analyst, qa]
|
||||
bootstrapSkillPath: '' # empty -> ~/.dsh/skills, then Claude cache
|
||||
bootstrapSkillPath: '' # empty -> ~/.dsh/skills, then legacy Claude Code paths
|
||||
toolMappingPath: '' # empty -> bundle/dsh-tools.md
|
||||
bootstrapMaxBytes: 65536
|
||||
```
|
||||
@ -200,7 +207,14 @@ with "requires ... in the Host scope".
|
||||
`~/.dsh/skills`, `~/.agents/skills`, the newest
|
||||
`~/.claude/plugins/cache/claude-plugins-official/superpowers/*/`, then
|
||||
`~/.claude/skills`. The DSH copy wins because `dsh/install.sh --skills` copies
|
||||
real files there; the Claude cache is only a fallback.
|
||||
real files there; the Claude Code locations are a legacy fallback and the
|
||||
bundle logs one warning per process when it resolves one of them.
|
||||
|
||||
Install-time source resolution is separate and stricter (change
|
||||
`dsh-install-source`): `--superpowers-from` → `$DSH_SUPERPOWERS_SOURCE` →
|
||||
`$DSH_HOME/superpowers/skills` → the legacy cache → `~/.claude/skills`. An
|
||||
invalid explicit source is a hard `FAIL`; no source at all is a `FAIL` on a
|
||||
fresh install and a `WARN` when `~/.dsh/skills` already holds the skills.
|
||||
|
||||
### OpenSpec skills
|
||||
|
||||
@ -230,18 +244,21 @@ openspec archive <id> -y
|
||||
## 6. Install and verify
|
||||
|
||||
Prerequisites: the `claude-harness` checkout, a superpowers source, DSH
|
||||
0.2.1-rc.3, `python3`, `git`, Node. Claude Code itself is optional after
|
||||
`dsh/install.sh --skills --roles` has copied the skills and roles into
|
||||
`~/.dsh`; the hooks come from this checkout.
|
||||
0.2.1-rc.3, `python3`, `git`, Node. Claude Code is not required at all: get the
|
||||
superpowers source once with
|
||||
`git clone --depth 1 https://github.com/obra/superpowers ~/.dsh/superpowers`,
|
||||
then `--skills --roles` copy everything into `~/.dsh`; the hooks come from this
|
||||
checkout.
|
||||
|
||||
```bash
|
||||
# 1. Static checks
|
||||
bash dsh/install.sh --check
|
||||
|
||||
# 2. Global instructions, skills, and roles (run from a terminal; writes
|
||||
# outside the session workspace)
|
||||
# outside the session workspace). The superpowers source comes from
|
||||
# --superpowers-from, DSH_SUPERPOWERS_SOURCE, or ~/.dsh/superpowers.
|
||||
bash dsh/install.sh --global
|
||||
bash dsh/install.sh --skills
|
||||
bash dsh/install.sh --skills # or: --skills --superpowers-from DIR
|
||||
bash dsh/install.sh --roles
|
||||
|
||||
# or all three at once:
|
||||
@ -344,10 +361,13 @@ profile was removed; the real `desktop` profile was not touched.
|
||||
- **Model tiers.** `sonnet`/`haiku`/`opus` are not DSH models. The port uses two
|
||||
live routes (`deepseek-flash`, `deepseek-v4-pro`) from the opt-in overlay in
|
||||
`dsh/subagent-models.yml`; without it every dispatch runs on the session model.
|
||||
- **`~/.claude` dependence.** `dsh/install.sh --skills --roles` copies skills
|
||||
and roles into `~/.dsh`; the bootstrap and `/role` prefer those copies, and
|
||||
the Claude cache/roles are fallbacks. An existing installation must rerun
|
||||
`--skills --roles` once to become independent of a Claude Code rollback.
|
||||
- **`~/.claude` dependence.** No longer needed for install, bootstrap, or
|
||||
roles: the install-time source is explicit, and `--skills --roles` copy real
|
||||
files into `~/.dsh`. The Claude Code locations remain a **legacy migration
|
||||
fallback**; the installer, `--check`, and the runtime warn when they are used
|
||||
(runtime — once per process). An existing installation must rerun
|
||||
`--skills --roles` once to stop relying on them; removal of the fallbacks is
|
||||
a separate task.
|
||||
- **Superpowers porting rules.** The upstream guide requires a session-start
|
||||
bootstrap with no per-session opt-in; `agent/pre-step` satisfies that shape,
|
||||
but a real acceptance transcript is still required before claiming a port.
|
||||
@ -374,3 +394,7 @@ profile was removed; the real `desktop` profile was not touched.
|
||||
gets its own release cadence.
|
||||
8. OpenSpec — **done** (change `dsh-openspec`): проектные скиллы, `--openspec`,
|
||||
`--check`; приёмка — в дизайне change.
|
||||
9. Install without `~/.claude` — **done** (change `dsh-install-source`):
|
||||
`--superpowers-from`/`DSH_SUPERPOWERS_SOURCE`/`~/.dsh/superpowers`, hard
|
||||
`FAIL` instead of a silent empty bootstrap, DSH-owned role paths in the
|
||||
installed `AGENTS.md`, legacy warnings at install and runtime.
|
||||
|
||||
@ -140,10 +140,10 @@ repository layout:
|
||||
roleCommand: true
|
||||
bootstrapSubagents: false
|
||||
hooksDir: '' # empty -> bundle/hooks -> ../../hooks
|
||||
rolesDir: '' # empty -> ~/.dsh/roles, ~/.claude/roles, repo roles/
|
||||
rolesDir: '' # empty -> ~/.dsh/roles, legacy ~/.claude/roles, repo roles/
|
||||
projectRolesDir: '' # empty -> <cwd>/.claude/roles
|
||||
roleNames: [dev, admin, analyst, qa]
|
||||
bootstrapSkillPath: '' # empty -> ~/.dsh/skills, then Claude cache
|
||||
bootstrapSkillPath: '' # empty -> ~/.dsh/skills, then legacy Claude Code paths
|
||||
toolMappingPath: '' # empty -> bundle/dsh-tools.md
|
||||
bootstrapMaxBytes: 65536
|
||||
```
|
||||
@ -155,10 +155,14 @@ repository layout:
|
||||
- `bundle/hooks` is a symlink for local-directory installation. A published
|
||||
bundle needs a real copy of `hooks/` (already listed in `package.json`
|
||||
`files`).
|
||||
- `using-superpowers` is read from `~/.dsh/skills` after `--skills`; the Claude
|
||||
plugin cache is only a fallback, so a partial Claude Code rollback does not
|
||||
break the bootstrap. Set `bootstrapSkillPath` or `DSH_SUPERPOWERS_SKILL` for
|
||||
another layout.
|
||||
- `using-superpowers` is read from `~/.dsh/skills` after `--skills`. The
|
||||
install-time source is `--superpowers-from`, `DSH_SUPERPOWERS_SOURCE`, or
|
||||
`~/.dsh/superpowers`; Claude Code locations are a **legacy migration
|
||||
fallback** and log one warning per process when used, so a partial Claude
|
||||
Code rollback does not break the bootstrap. Set `bootstrapSkillPath` or
|
||||
`DSH_SUPERPOWERS_SKILL` for another layout.
|
||||
- Roles resolve `~/.dsh/roles` first; `~/.claude/roles` and the repository
|
||||
`roles/` are fallbacks. After `--roles` the runtime never needs `~/.claude`.
|
||||
- Role loading is model-driven through the `role` skill; there is no automatic
|
||||
`agent/pre-step` injection. If the model skips the skill, use the `/role`
|
||||
command or ask it to load the role explicitly.
|
||||
|
||||
@ -24,6 +24,7 @@ import { fileURLToPath } from 'node:url'
|
||||
import { registerRoleCommand } from './command-role.js'
|
||||
import { createGuard } from './lib/guards.js'
|
||||
import { isBootstrapVisible, prepareBootstrap } from './lib/bootstrap.js'
|
||||
import { isLegacyClaudePath, warnLegacyOnce } from './lib/legacy.js'
|
||||
import { createUserMessage } from './lib/message.js'
|
||||
import { DEFAULT_ROLE_NAMES } from './lib/role.js'
|
||||
|
||||
@ -93,6 +94,9 @@ export function apply(ctx, rawConfig = {}) {
|
||||
const bundleDir = dirname(fileURLToPath(import.meta.url))
|
||||
const hooksDir = resolveHooksDir(bundleDir, config.hooksDir)
|
||||
const rolesDir = resolveRolesDir(bundleDir, config.rolesDir)
|
||||
if (isLegacyClaudePath(rolesDir)) {
|
||||
warnLegacyOnce(ctx.logger, 'roles', rolesDir, 'roles')
|
||||
}
|
||||
const guard = createGuard({ hooksDir, logger: ctx.logger })
|
||||
|
||||
if (config.enabled && config.guards) {
|
||||
|
||||
4
dsh/bundle/lib/bootstrap.js
vendored
4
dsh/bundle/lib/bootstrap.js
vendored
@ -12,6 +12,7 @@ import { createHash } from 'node:crypto'
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { isLegacyClaudePath, warnLegacyOnce } from './legacy.js'
|
||||
|
||||
const DEFAULT_MAPPING = [
|
||||
'Use the tools reported by the harness. In particular:',
|
||||
@ -141,6 +142,9 @@ export function prepareBootstrap({
|
||||
logger?.warn?.('[dsh-harness] superpowers bootstrap skill not found; skipping injection')
|
||||
return undefined
|
||||
}
|
||||
if (isLegacyClaudePath(resolved, home)) {
|
||||
warnLegacyOnce(logger, 'bootstrap', resolved, 'skills')
|
||||
}
|
||||
|
||||
const mappingFile = mappingPath && existsSync(mappingPath) ? mappingPath : ''
|
||||
const key = `${resolved}|${mappingFile}|${maxBytes}`
|
||||
|
||||
35
dsh/bundle/lib/legacy.js
Normal file
35
dsh/bundle/lib/legacy.js
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Legacy Claude Code source detection.
|
||||
*
|
||||
* After #37/#38 the DSH runtime prefers `~/.dsh` for skills and roles. The
|
||||
* `~/.claude` locations stay as a migration fallback: a machine that has not
|
||||
* rerun `dsh/install.sh --skills --roles` keeps working, but the agent should
|
||||
* say so once instead of serving legacy paths silently.
|
||||
*/
|
||||
|
||||
import { homedir } from 'node:os'
|
||||
import { join, sep } from 'node:path'
|
||||
|
||||
const warned = new Set()
|
||||
|
||||
/** Whether `path` lives under `<home>/.claude` (the legacy source root). */
|
||||
export function isLegacyClaudePath(path, home = homedir()) {
|
||||
if (typeof path !== 'string' || path === '') return false
|
||||
return path.startsWith(join(home, '.claude') + sep)
|
||||
}
|
||||
|
||||
/**
|
||||
* Warn once per process and per kind+path: `agent/pre-step` runs on every
|
||||
* step, so an unconditional warning would flood the log.
|
||||
*/
|
||||
export function warnLegacyOnce(logger, kind, path, flag = kind) {
|
||||
const key = `${kind}|${path}`
|
||||
if (warned.has(key)) return
|
||||
warned.add(key)
|
||||
logger?.warn?.(
|
||||
'[dsh-harness] legacy Claude Code source in use for %s: %s; run dsh/install.sh --%s',
|
||||
kind,
|
||||
path,
|
||||
flag,
|
||||
)
|
||||
}
|
||||
@ -112,3 +112,36 @@ test('resolveBootstrapSkill prefers the DSH copy over the Claude cache', () => {
|
||||
cleanup(home)
|
||||
}
|
||||
})
|
||||
|
||||
test('prepareBootstrap warns once when the legacy ~/.claude skill is used', () => {
|
||||
const home = tempDir()
|
||||
try {
|
||||
const legacy = join(home, '.claude', 'skills', 'using-superpowers', 'SKILL.md')
|
||||
write(legacy, '---\nname: using-superpowers\n---\nlegacy body\n')
|
||||
const warnings = []
|
||||
const logger = { warn: (...args) => warnings.push(args) }
|
||||
|
||||
const first = prepareBootstrap({ home, env: {}, logger })
|
||||
const second = prepareBootstrap({ home, env: {}, logger })
|
||||
|
||||
assert.equal(first.path, legacy)
|
||||
assert.ok(second)
|
||||
assert.equal(warnings.length, 1, 'one warning per process, not per step')
|
||||
assert.match(String(warnings[0][0]), /legacy Claude Code source/)
|
||||
assert.deepEqual(warnings[0].slice(-3), ['bootstrap', legacy, 'skills'])
|
||||
} finally {
|
||||
cleanup(home)
|
||||
}
|
||||
})
|
||||
|
||||
test('prepareBootstrap stays silent for the DSH-owned copy', () => {
|
||||
const home = tempDir()
|
||||
try {
|
||||
write(join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md'), '---\nname: x\n---\nbody\n')
|
||||
const warnings = []
|
||||
prepareBootstrap({ home, env: {}, logger: { warn: (...args) => warnings.push(args) } })
|
||||
assert.equal(warnings.length, 0)
|
||||
} finally {
|
||||
cleanup(home)
|
||||
}
|
||||
})
|
||||
|
||||
@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
|
||||
import { join } from 'node:path'
|
||||
import test from 'node:test'
|
||||
import { resolveRolesDir } from '../bundle/index.js'
|
||||
import { isLegacyClaudePath } from '../bundle/lib/legacy.js'
|
||||
import { buildRoleText, listRoles, parseRole } from '../bundle/lib/role.js'
|
||||
import { cleanup, tempDir, write } from './helpers.mjs'
|
||||
|
||||
@ -108,3 +109,15 @@ test('listRoles reports both layers', () => {
|
||||
cleanup(root)
|
||||
}
|
||||
})
|
||||
|
||||
test('isLegacyClaudePath detects ~/.claude paths only', () => {
|
||||
const home = tempDir()
|
||||
try {
|
||||
assert.equal(isLegacyClaudePath(join(home, '.claude', 'roles', 'dev.md'), home), true)
|
||||
assert.equal(isLegacyClaudePath(join(home, '.dsh', 'roles', 'dev.md'), home), false)
|
||||
assert.equal(isLegacyClaudePath(join(home, '.claude-other', 'x'), home), false)
|
||||
assert.equal(isLegacyClaudePath('', home), false)
|
||||
} finally {
|
||||
cleanup(home)
|
||||
}
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user