dotmd-cli 0.56.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.56.0",
3
+ "version": "0.57.0",
4
4
  "description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, Notion sync, AI summaries.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,220 +1,71 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { green, dim, yellow } from './color.mjs';
5
3
 
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
8
- // Marker is no longer pinned to line 1 — it now lives below the YAML
9
- // frontmatter that Claude Code surfaces as the slash command's description.
10
- // The regex is intentionally non-anchored so getInstalledVersion finds it
11
- // wherever it sits, and the marker string is specific enough that a false
12
- // positive elsewhere in a user-edited file is not a realistic concern.
13
- const VERSION_REGEX = /<!-- dotmd-generated: ([\d.]+) -->/;
14
- function markerFor(version) { return `<!-- dotmd-generated: ${version} -->`; }
15
-
16
- // Trigger sentences surfaced by Claude Code's available-skills system reminder.
17
- // Front-load the "when to reach for it" cue so Claude can route to the right
18
- // slash command without the user having to type the slash. The plans entry
19
- // gets a per-type status vocab appended at generation time so agents arrive
20
- // with the valid `dotmd status` / `dotmd archive` values already in context.
21
- const SLASH_DESCRIPTIONS = {
22
- plans: "dotmd-managed plan briefing for this repo. Use when the user asks what's on the plate, references a plan slug, queues work, or wants to start / close / archive a plan.",
23
- docs: "dotmd-managed docs briefing for this repo. Use when the user asks to list, scaffold, query, validate, archive, or rename non-plan docs (reference docs, ADRs, RFCs, design notes), or asks how the dotmd doc lifecycle works here.",
24
- };
25
-
26
- const VOCAB_TRUNCATE_AT = 12;
27
-
28
- // Per-type valid statuses, rendered as one clause per type. Appended to the
29
- // plans description so it lands in Claude's available-skills listing at
30
- // SessionStart — no discovery command needed before the first `dotmd status`
31
- // / `dotmd archive` call. Types with no declared statuses are skipped (the
32
- // generic global list applies); types with >VOCAB_TRUNCATE_AT statuses are
33
- // truncated with an ellipsis so the description stays bounded.
34
- function statusVocabClause(config) {
35
- if (!config?.typeStatuses) return '';
36
- const parts = [];
37
- for (const [type, statusesSet] of config.typeStatuses.entries()) {
38
- if (!statusesSet || statusesSet.size === 0) continue;
39
- let statuses = [...statusesSet];
40
- if (statuses.length > VOCAB_TRUNCATE_AT) {
41
- statuses = [...statuses.slice(0, VOCAB_TRUNCATE_AT), '…'];
42
- }
43
- parts.push(`Valid ${type} statuses: ${statuses.join(', ')}.`);
44
- }
45
- return parts.join(' ');
46
- }
47
-
48
- function frontmatterFor(name, config) {
49
- let description = SLASH_DESCRIPTIONS[name];
50
- if (name === 'plans') {
51
- const vocab = statusVocabClause(config);
52
- if (vocab) description = `${description} ${vocab}`;
53
- }
54
- return ['---', `description: ${description}`, '---'];
55
- }
56
-
57
- function generatePlansCommand(config, version) {
58
- const lines = [...frontmatterFor('plans', config), markerFor(version), ''];
59
- lines.push('Run `dotmd context` to get the current plans briefing, then use it to orient yourself.');
60
- lines.push('');
61
- lines.push(`Plans are managed by **dotmd** (v${version}). Config at \`dotmd.config.mjs\`. Always use \`dotmd\` directly.`);
62
- lines.push('');
63
- lines.push('Plan-specific commands:');
64
- lines.push('- `dotmd context` — briefing with active/paused/ready plans, age tags, next steps');
65
- lines.push('- `dotmd set <status> <file>` — single status verb. Writes the new status to the plan\'s frontmatter. Use it to transition or close any plan:');
66
- lines.push(' - `dotmd set in-session <file>` — mark a plan in-session (just a frontmatter status; use `dotmd use <file>` to also print the body)');
67
- lines.push(' - `dotmd set archived <file>` — close out (same as `dotmd archive`)');
68
- lines.push('- `dotmd archive <file>` — explicit archive with ref-fixing (equivalent to `set archived`)');
69
- lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
70
- lines.push('- `dotmd new plan <name>` — scaffold with full phase structure');
71
- lines.push('- `dotmd new prompt <name>` — save a resume-prompt to docs/prompts/ (pipe stdin or @path for body)');
72
- lines.push('- `dotmd use` — consume oldest pending prompt (prints body, auto-archives)');
73
- lines.push('- `dotmd use <file>` — open any doc by type: prompt → consume, plan → mark in-session + print card, doc → read');
74
- lines.push('- `dotmd unblocks <file>` — what depends on / is blocked by a plan');
75
- lines.push('- `dotmd actionable` — ready plans with next steps (what to promote)');
76
- lines.push('- `dotmd query --keyword <term>` — find plans by keyword');
77
- lines.push('- `dotmd runlist <hub>` — show ordered children of a runlist hub (→ marks next)');
78
- lines.push('- `dotmd runlist next <hub>` — open the next non-archived child of a runlist hub');
79
-
80
- if (config.raw?.glossary) {
81
- lines.push('- `dotmd glossary <term>` — domain term lookup with related plans');
82
- }
83
-
84
- lines.push('');
85
- lines.push('If the user asks about a specific plan, read its file directly (path is in the briefing or findable via `dotmd query --keyword <term>`).');
86
- lines.push('');
87
- lines.push('If the user asks to change a plan\'s status, use `dotmd set <status> <file>`.');
88
- lines.push('If the user asks to archive a plan, use `dotmd set archived <file>` (or `dotmd archive <file>`).');
89
- lines.push('If the user references a runlist by name — e.g. "what\'s next on <X> runlist", "<X> runlist status", "pick up the next in <X>" — use `dotmd runlist next <X>` (or `dotmd runlist <X>` first to inspect the ordering). Do NOT fall back to `dotmd context` for runlist-scoped questions.');
90
- lines.push('');
91
- lines.push('**Saved prompts (`docs/prompts/*.md`):** if the user references a file under `docs/prompts/` — e.g. "resume via docs/prompts/foo.md", "use this prompt", "load that one" — consume it with `dotmd use <file>` (atomically prints the body and archives the prompt so it cannot be double-consumed). Do NOT `cat` it, read it with the file-reading tool, or copy its body into chat. To pick the oldest pending prompt without naming a file, run `dotmd use` with no arg.');
92
- lines.push('');
93
-
94
- return lines.join('\n');
95
- }
96
-
97
- function generateDocsCommand(config, version) {
98
- const roots = Array.isArray(config.raw?.root) ? config.raw.root : [config.raw?.root ?? 'docs'];
99
- const rootCount = roots.length;
100
-
101
- const lines = [...frontmatterFor('docs', config), markerFor(version), ''];
102
- lines.push(`All documentation in this repo is managed by **dotmd** (v${version}). Docs across ${rootCount} root${rootCount > 1 ? 's' : ''}: ${roots.join(', ')}. Config at \`dotmd.config.mjs\`.`);
103
- lines.push('');
104
-
105
- // Document types from config
106
- const types = config.raw?.types ? Object.keys(config.raw.types) : [];
107
- if (types.length > 0) {
108
- lines.push(`Document types: ${types.map(t => '`' + t + '`').join(', ')}.`);
109
- lines.push('');
110
- }
111
-
112
- lines.push('Commands for working with docs:');
113
- lines.push('- `dotmd context` — LLM-oriented briefing across all types');
114
- lines.push('- `dotmd doctor --apply` — auto-fix everything in one pass (refs, lint, dates, index; bare `dotmd doctor` previews only)');
115
- lines.push('- `dotmd query [filters]` — search by status, keyword, module, surface, type, staleness');
116
- lines.push('- `dotmd health` — plan pipeline, velocity, aging');
117
- lines.push('- `dotmd stats` — doc health dashboard (completeness, checklists, audit coverage)');
118
- lines.push('- `dotmd graph [--dot]` — visualize document relationships');
119
- lines.push('- `dotmd deps [file]` — dependency tree');
120
- lines.push('- `dotmd unblocks <file>` — impact analysis for a doc');
121
- lines.push('- `dotmd diff [file]` — git changes since last updated date');
122
- lines.push('- `dotmd list` — all docs grouped by status');
123
- lines.push('- `dotmd focus <status>` — detailed view for one status group');
124
-
125
- if (config.raw?.glossary) {
126
- lines.push('- `dotmd glossary <term>` — domain term lookup with related docs and plans');
4
+ // dotmd used to scaffold per-repo `.claude/commands/{plans,docs}.md` slash
5
+ // commands — version-stamped, generated from each repo's status vocab, and
6
+ // self-healed by `dotmd hud`. That mechanism is RETIRED. The dotmd Claude Code
7
+ // plugin (plugins/dotmd/skills/dotmd/SKILL.md + bundled hooks) now carries the
8
+ // canonical agent-facing workflow into every repo and every subagent, and
9
+ // `dotmd hud` injects the dynamic per-project status vocab at runtime. A static
10
+ // skill + a runtime hook covers the full picture with no per-repo file to drift.
11
+ //
12
+ // The only job left in this module is teardown: delete the stale generated
13
+ // command files dotmd left behind so retired scaffolding stops shadowing the
14
+ // plugin skill. Removal is banner-gated — files WITHOUT the dotmd marker are
15
+ // hand-authored (e.g. a repo's own module-*.md / domain-*.md briefings) and are
16
+ // NEVER touched. Every dotmd-stamped file is fair game, including legacy ones
17
+ // dotmd no longer generates (e.g. the old baton.md).
18
+
19
+ const GENERATED_MARKER = '<!-- dotmd-generated:';
20
+
21
+ // The marker sits just below the YAML frontmatter Claude Code surfaces as the
22
+ // command description. That description can be long (the retired plans.md baked
23
+ // the full per-type status vocab into it), pushing the banner well past the
24
+ // first kilobyte — so classify against the whole file, not a head slice. These
25
+ // are tiny command files, so reading them in full is cheap.
26
+ function isGeneratedCommandFile(filePath) {
27
+ try {
28
+ return readFileSync(filePath, 'utf8').includes(GENERATED_MARKER);
29
+ } catch {
30
+ return false;
127
31
  }
128
-
129
- lines.push('');
130
- lines.push('Lifecycle:');
131
- lines.push('- `dotmd new plan <name>` — scaffold new plan');
132
- lines.push('- `dotmd new doc <name>` — scaffold reference doc');
133
- lines.push('- `dotmd new prompt <name>` — save a resume-prompt (pipe stdin or @path for body)');
134
- lines.push('- `dotmd use` — consume oldest pending prompt (prints body, auto-archives)');
135
- lines.push('- `dotmd use <file>` — open any doc by type: prompt → consume, plan → start work, doc → read');
136
- lines.push('- `dotmd set <status> [<file>]` — unified transition (archive / status bump; infers path from your active in-session plan)');
137
- lines.push('- `dotmd status <file> <status>` — transition status (legacy; `set` is preferred)');
138
- lines.push('- `dotmd archive <file>` — archive with auto ref-fixing');
139
- lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
140
- lines.push('- `dotmd touch --git` — bulk-sync updated dates from git history');
141
- lines.push('- `dotmd lint --fix` — auto-fix frontmatter issues');
142
- lines.push('- `dotmd fix-refs` — repair broken references and body links');
143
- lines.push('- `dotmd rename <old> <new>` — rename doc + update all references');
144
- lines.push('');
145
- lines.push('**Saved prompts (`docs/prompts/*.md`):** if the user references a file under `docs/prompts/` — e.g. "resume via docs/prompts/foo.md", "use this prompt" — consume it with `dotmd use <file>` (prints the body and archives atomically). Do NOT `cat` it or read it with the file-reading tool. To pick the oldest pending prompt without naming a file, run `dotmd use` with no arg.');
146
- lines.push('');
147
-
148
- return lines.join('\n');
149
32
  }
150
33
 
151
- function getInstalledVersion(filePath) {
152
- if (!existsSync(filePath)) return null;
153
- const content = readFileSync(filePath, 'utf8');
154
- const match = content.match(VERSION_REGEX);
155
- return match ? match[1] : null;
156
- }
157
-
158
- export function scaffoldClaudeCommands(cwd, config, opts = {}) {
159
- const { dryRun = false, version = pkg.version } = opts;
160
- const claudeDir = path.join(cwd, '.claude');
161
- if (!existsSync(claudeDir)) return [];
162
-
163
- const commandsDir = path.join(claudeDir, 'commands');
164
- const results = [];
165
-
166
- const files = [
167
- { name: 'plans.md', generate: () => generatePlansCommand(config, version) },
168
- { name: 'docs.md', generate: () => generateDocsCommand(config, version) },
169
- ];
170
-
171
- for (const { name, generate } of files) {
34
+ // Remove every dotmd-generated slash-command file under .claude/commands.
35
+ // Returns [{ name, action: 'removed' }] for each file cleaned (or that would be
36
+ // cleaned, in dry-run). Never throws — teardown must not break a hook or a
37
+ // command. User-authored command files (no dotmd banner) survive untouched.
38
+ export function removeGeneratedSlashCommands(cwd, opts = {}) {
39
+ const { dryRun = false } = opts;
40
+ const commandsDir = path.join(cwd, '.claude', 'commands');
41
+ if (!existsSync(commandsDir)) return [];
42
+ let entries;
43
+ try { entries = readdirSync(commandsDir); } catch { return []; }
44
+ const removed = [];
45
+ for (const name of entries) {
46
+ if (!name.endsWith('.md')) continue;
172
47
  const filePath = path.join(commandsDir, name);
173
- const installedVersion = getInstalledVersion(filePath);
174
-
175
- if (installedVersion === version) {
176
- results.push({ name, action: 'current' });
177
- } else if (installedVersion) {
178
- // Outdated — regenerate
179
- if (!dryRun) {
180
- mkdirSync(commandsDir, { recursive: true });
181
- writeFileSync(filePath, generate(), 'utf8');
182
- }
183
- results.push({ name, action: 'updated', from: installedVersion, to: version });
184
- } else if (!existsSync(filePath)) {
185
- // New — create
186
- if (!dryRun) {
187
- mkdirSync(commandsDir, { recursive: true });
188
- writeFileSync(filePath, generate(), 'utf8');
189
- }
190
- results.push({ name, action: 'created' });
191
- } else {
192
- // File exists but no version marker — user-managed, don't touch
193
- results.push({ name, action: 'skipped' });
48
+ if (!isGeneratedCommandFile(filePath)) continue;
49
+ if (!dryRun) {
50
+ try { unlinkSync(filePath); } catch { continue; }
194
51
  }
52
+ removed.push({ name, action: 'removed' });
195
53
  }
196
-
197
- return results;
54
+ return removed;
198
55
  }
199
56
 
200
- // Self-heal: regen any slash-command file whose banner is older than pkg.version.
201
- // Designed for runHud to call at SessionStart — closes the gap between "user
202
- // upgraded dotmd" and "slash-command body reflects the new version" without
203
- // requiring a manual `dotmd doctor`. Returns only the entries that actually
204
- // changed so the caller can surface a one-line note; an empty array means the
205
- // hud silent-clean contract is preserved. `skipped` (user-managed, no banner)
206
- // and `current` entries are filtered out — callers don't care about them.
57
+ // Self-heal entrypoint for `dotmd hud` (SessionStart hook). Was: regenerate
58
+ // stale slash commands. Now: delete the retired generated files so the plugin
59
+ // skill is the single source of truth. Returns only the removed entries; an
60
+ // empty array preserves hud's silent-clean contract. Kept under the old name so
61
+ // hud's call site (and its swallow-all-errors wrapper) is unchanged.
207
62
  export function refreshStaleSlashCommands(config) {
208
- const results = scaffoldClaudeCommands(config.repoRoot, config);
209
- return results.filter(r => r.action === 'updated');
63
+ return removeGeneratedSlashCommands(config.repoRoot);
210
64
  }
211
65
 
212
- // Intentionally returns []. Slash-command stamp drift is auto-healed every
213
- // time `dotmd hud` runs (SessionStart hook), and `dotmd doctor` regens them
214
- // on demand. Surfacing a warning at `dotmd check` time was pure noise — it
215
- // fired on every release until the next session, despite the user having no
216
- // action to take (the heal is automatic). Kept the function for API stability
217
- // in case downstream callers import it.
66
+ // Retained as a no-op for API stability. `dotmd check` never warned on slash
67
+ // commands (the old auto-heal made it pure noise), and now there is nothing to
68
+ // generate at all. See git history for the retired scaffolder.
218
69
  export function checkClaudeCommands(_cwd, _opts = {}) {
219
70
  return [];
220
71
  }
package/src/doctor.mjs CHANGED
@@ -7,7 +7,7 @@ import { buildIndex, collectDocFiles } from './index.mjs';
7
7
  import { renderIndexFile, writeIndex } from './index-file.mjs';
8
8
  import { renderCheck, renderManualFixes } from './render.mjs';
9
9
  import { bold, dim, green, yellow } from './color.mjs';
10
- import { checkClaudeCommands, scaffoldClaudeCommands } from './claude-commands.mjs';
10
+ import { checkClaudeCommands, removeGeneratedSlashCommands } from './claude-commands.mjs';
11
11
  import { runMigrateTemplate } from './migrate-template.mjs';
12
12
  import { runMigratePrompts } from './migrate-prompts.mjs';
13
13
  import { runFrontmatterFix } from './frontmatter-fix.mjs';
@@ -98,25 +98,27 @@ export function runDoctor(argv, config, opts = {}) {
98
98
  process.stdout.write('Index updated.\n');
99
99
  }
100
100
 
101
- // Step 5: Refresh Claude Code commands. Always print the heading so the
102
- // numbering stays `1,2,3,4,5,6` — pre-fix it was conditional, so a doctor
103
- // run where everything was already current printed `1,2,3,4,6` with `5.`
104
- // silently missing.
101
+ // Step 5: Clean up retired Claude Code command scaffolding. The per-repo
102
+ // `.claude/commands/{plans,docs}.md` files are superseded by the dotmd plugin
103
+ // skill; doctor sweeps any leftover banner-stamped (dotmd-generated) files.
104
+ // Always print the heading so the numbering stays `1,2,3,4,5,6`.
105
105
  process.stdout.write('\n' + bold('5. Claude Code commands:') + '\n');
106
106
  if (dryRun) {
107
- process.stdout.write('[dry-run] Would refresh .claude/commands/ if outdated.\n');
107
+ const wouldRemove = removeGeneratedSlashCommands(config.repoRoot, { dryRun: true });
108
+ if (wouldRemove.length === 0) {
109
+ process.stdout.write('[dry-run] No retired slash-command files to remove.\n');
110
+ } else {
111
+ for (const r of wouldRemove) {
112
+ process.stdout.write(`[dry-run] Would remove retired .claude/commands/${r.name} (guidance now ships via the dotmd plugin).\n`);
113
+ }
114
+ }
108
115
  } else {
109
- const claudeResults = scaffoldClaudeCommands(config.repoRoot, config);
110
- const changes = claudeResults.filter(r => r.action === 'updated' || r.action === 'created');
111
- if (changes.length === 0) {
112
- process.stdout.write('Nothing to refresh.\n');
116
+ const removed = removeGeneratedSlashCommands(config.repoRoot);
117
+ if (removed.length === 0) {
118
+ process.stdout.write('Nothing to clean up.\n');
113
119
  } else {
114
- for (const r of changes) {
115
- if (r.action === 'updated') {
116
- process.stdout.write(`${green('Updated')} .claude/commands/${r.name} (v${r.from} → v${r.to})\n`);
117
- } else if (r.action === 'created') {
118
- process.stdout.write(`${green('Created')} .claude/commands/${r.name}\n`);
119
- }
120
+ for (const r of removed) {
121
+ process.stdout.write(`${green('Removed')} retired .claude/commands/${r.name} (guidance now ships via the dotmd plugin)\n`);
120
122
  }
121
123
  }
122
124
  }
package/src/hud.mjs CHANGED
@@ -261,11 +261,12 @@ export function runHud(argv, config) {
261
261
 
262
262
  const hud = buildHud(config);
263
263
 
264
- // Self-heal stale slash-command files. Wrapped: a broken scaffolder must
265
- // never kill the SessionStart hook (would block every session). Runs for its
266
- // side effect only — the refresh is no longer announced in stdout (see the
267
- // primer-only contract below). Skipped in --json mode to keep the structured
268
- // shape stable for programmatic callers.
264
+ // Clean up retired generated slash-command files (the plugin skill replaces
265
+ // them). Banner-gated, so hand-authored commands survive. Wrapped: teardown
266
+ // must never kill the SessionStart hook (would block every session). Runs for
267
+ // its side effect only — nothing is announced in stdout (see the primer-only
268
+ // contract below). Skipped in --json mode to keep the structured shape stable
269
+ // for programmatic callers.
269
270
  if (!json) {
270
271
  try { refreshStaleSlashCommands(config); }
271
272
  catch { /* swallow — see comment above */ }
package/src/init.mjs CHANGED
@@ -5,8 +5,7 @@ import path from 'node:path';
5
5
  import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
6
6
  import { green, dim, yellow } from './color.mjs';
7
7
  import { warn } from './util.mjs';
8
- import { scaffoldClaudeCommands } from './claude-commands.mjs';
9
- import { resolveConfig } from './config.mjs';
8
+ import { removeGeneratedSlashCommands } from './claude-commands.mjs';
10
9
 
11
10
  // Subdirectories scaffolded under docsRoot and tracked separately during scans.
12
11
  // Each maps to a builtin type (plan, prompt). New types added here should also
@@ -331,53 +330,32 @@ export async function runInit(cwd, config, opts = {}) {
331
330
  process.stdout.write(`\n ${yellow('hint')} ${n} untagged .md ${noun} found — run \`dotmd bulk-tag --dry-run\` to preview tagging.\n`);
332
331
  }
333
332
 
334
- // Claude Code integration — auto-detect .claude/ directory.
335
- // Re-resolve config so the scaffold sees whatever we (may have) just written.
336
- // Pre-fix: the dispatcher passed `null` to runInit on a fresh repo because
337
- // resolveConfig was called before init wrote the starter, so the `if (config)`
338
- // gate below silently skipped slash-command scaffolding entirely on first init.
339
- // Re-resolving picks up STARTER_CONFIG (or any pre-existing config) in the
340
- // real-run path; in dry-run with no on-disk config, it returns the merged
341
- // DEFAULTS, which is enough for the preview line (`would create…`).
342
- //
343
- // Reports all four scaffold outcomes so the user can't be surprised by
344
- // either a silent regenerate (pre-fix: `updated` was unreported) or by
345
- // dotmd skipping a user-managed file (pre-fix: `skipped` was unreported).
346
- const scaffoldConfig = await resolveConfig(cwd);
347
- if (scaffoldConfig) {
348
- const results = scaffoldClaudeCommands(cwd, scaffoldConfig, { dryRun });
349
- for (const r of results) {
350
- const filename = `.claude/commands/${r.name}`;
351
- if (r.action === 'created') {
352
- process.stdout.write(` ${dryTag}${green('create')} ${filename}\n`);
353
- } else if (r.action === 'updated') {
354
- process.stdout.write(` ${dryTag}${green('update')} ${filename} (v${r.from} → v${r.to})\n`);
355
- } else if (r.action === 'current') {
356
- process.stdout.write(` ${dryTag}${dim('exists')} ${filename}\n`);
357
- } else if (r.action === 'skipped') {
358
- process.stdout.write(` ${dryTag}${yellow('skip')} ${filename} (no version marker — user-managed)\n`);
359
- }
333
+ // Claude Code integration. dotmd no longer scaffolds per-repo
334
+ // `.claude/commands/*.md` slash commands — the dotmd plugin's SKILL.md is the
335
+ // canonical agent-facing workflow now, and `dotmd hud` injects this repo's
336
+ // status vocab at runtime. If a `.claude/` exists, sweep any retired
337
+ // generated command files (banner-gated, so hand-authored ones survive) and
338
+ // point the user at the plugin instead.
339
+ if (existsSync(path.join(cwd, '.claude'))) {
340
+ const removed = removeGeneratedSlashCommands(cwd, { dryRun });
341
+ for (const r of removed) {
342
+ const verb = dryRun ? 'would remove' : 'removed';
343
+ process.stdout.write(` ${dryTag}${yellow('clean')} .claude/commands/${r.name} (retired — ${verb}; guidance ships via the dotmd plugin)\n`);
360
344
  }
361
- }
362
345
 
363
- // SessionStart hook hint — only when .claude/ exists. Print-only; users with
364
- // existing settings.json need to merge by hand because auto-merging hook
365
- // arrays would silently mutate user-managed files.
366
- if (existsSync(path.join(cwd, '.claude'))) {
367
346
  const sessionStart = detectSessionStartHook(cwd);
368
347
  if (sessionStart.wired) {
369
348
  process.stdout.write(` ${dim('exists')} ${sessionStart.file} (SessionStart hook for \`dotmd hud\` already wired)\n`);
370
349
  } else {
371
- process.stdout.write(`\n ${yellow('hint')} wire \`dotmd hud\` to run at SessionStart — add to .claude/settings.json:\n\n`);
372
- process.stdout.write(` {\n`);
373
- process.stdout.write(` "hooks": {\n`);
374
- process.stdout.write(` "SessionStart": [\n`);
375
- process.stdout.write(` { "hooks": [{ "type": "command", "command": "dotmd hud" }] }\n`);
376
- process.stdout.write(` ]\n`);
377
- process.stdout.write(` }\n`);
378
- process.stdout.write(` }\n\n`);
379
- process.stdout.write(` If .claude/settings.json already exists, merge into the existing\n`);
380
- process.stdout.write(` \`hooks.SessionStart\` array rather than replacing the file.\n`);
350
+ process.stdout.write(`\n ${yellow('hint')} install the dotmd Claude Code plugin so its hooks + workflow skill\n`);
351
+ process.stdout.write(` travel to every session and subagent automatically:\n\n`);
352
+ process.stdout.write(` /plugin marketplace add reowens/dotmd\n`);
353
+ process.stdout.write(` /plugin install dotmd@dotmd\n\n`);
354
+ process.stdout.write(` Or, without the plugin, wire \`dotmd hud\` at SessionStart by hand —\n`);
355
+ process.stdout.write(` add to .claude/settings.json (merge into any existing hooks):\n\n`);
356
+ process.stdout.write(` "hooks": { "SessionStart": [\n`);
357
+ process.stdout.write(` { "hooks": [{ "type": "command", "command": "dotmd hud" }] }\n`);
358
+ process.stdout.write(` ] }\n`);
381
359
  }
382
360
  }
383
361
 
package/src/ship.mjs CHANGED
@@ -3,7 +3,6 @@ import { spawnSync } from 'node:child_process';
3
3
  import path from 'node:path';
4
4
  import { die, warn, toRepoPath } from './util.mjs';
5
5
  import { green, dim, yellow } from './color.mjs';
6
- import { scaffoldClaudeCommands } from './claude-commands.mjs';
7
6
 
8
7
  // Files dotmd ship will auto-stage when they're dirty. Anything outside this
9
8
  // allowlist stays in the working tree — user has to `git add` it explicitly,
@@ -77,16 +76,11 @@ export async function runShip(argv, config, opts = {}) {
77
76
 
78
77
  process.stdout.write(`${green('→')} Shipping ${current} → ${target} (${bump})\n`);
79
78
 
80
- // 1. Regen slash commands at the *target* version so the resulting commit
81
- // matches the post-bump state and no dirty tree lingers after release.
82
- const regenResults = scaffoldClaudeCommands(config.repoRoot, config, { version: target, dryRun });
83
- const refreshed = regenResults.filter(r => r.action === 'updated' || r.action === 'created');
84
- if (refreshed.length > 0) {
85
- const verb = dryRun ? 'Would regenerate' : 'Regenerated';
86
- process.stdout.write(`${green('→')} ${verb} slash commands @ ${target}: ${refreshed.map(r => r.name).join(', ')}\n`);
87
- }
79
+ // Per-repo slash-command scaffolding is retired (the dotmd plugin's SKILL.md
80
+ // is canonical now), so there is nothing to regenerate at ship time. Any
81
+ // stale generated files are swept by `dotmd hud` / `dotmd doctor`.
88
82
 
89
- // 2. Identify dirty tracked files. Anything matching the allowlist gets
83
+ // Identify dirty tracked files. Anything matching the allowlist gets
90
84
  // staged; everything else is left dirty so the user can handle it.
91
85
  const dirty = listDirtyFiles(config.repoRoot);
92
86
  const untracked = dirty.filter(d => d.status === '??');