dotmd-cli 0.40.2 → 0.41.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/bin/dotmd.mjs +26 -0
- package/package.json +1 -1
- package/src/claude-commands.mjs +42 -12
- package/src/commands.mjs +1 -1
- package/src/lifecycle.mjs +76 -0
package/bin/dotmd.mjs
CHANGED
|
@@ -53,6 +53,7 @@ Lifecycle:
|
|
|
53
53
|
runlist <hub> [next] Show or walk an ordered group of plans (see \`dotmd help runlist\`)
|
|
54
54
|
finish <file> [done|active] Finish a plan (set done or active)
|
|
55
55
|
status <file> <status> Transition document status
|
|
56
|
+
set <status> [<file>] Unified transition: archive/release/transition in one verb
|
|
56
57
|
archive <file> Archive (status + move + update refs)
|
|
57
58
|
bulk archive <f1> <f2> ... Archive multiple files at once
|
|
58
59
|
bulk-tag [files...] Tag pre-existing untagged .md files
|
|
@@ -306,6 +307,30 @@ Options:
|
|
|
306
307
|
|
|
307
308
|
If no file is given, prompts with a list of in-session plans.`,
|
|
308
309
|
|
|
310
|
+
set: `dotmd set <status> [<file>] — unified status-transition verb
|
|
311
|
+
|
|
312
|
+
Routes to the right plumbing based on the target status:
|
|
313
|
+
- target is an archive status → archive the file (move + ref update)
|
|
314
|
+
- source is in-session → also releases the held lease
|
|
315
|
+
- everything else → plain frontmatter status bump
|
|
316
|
+
|
|
317
|
+
When <file> is omitted, dotmd infers it from the calling session's held
|
|
318
|
+
lease. With zero or multiple leases, you must pass <file> explicitly.
|
|
319
|
+
|
|
320
|
+
To acquire an in-session lease, use \`dotmd pickup <file>\` instead —
|
|
321
|
+
\`dotmd set in-session\` is refused so the asymmetric lease-acquisition
|
|
322
|
+
path is never skipped silently.
|
|
323
|
+
|
|
324
|
+
Options:
|
|
325
|
+
--no-index Skip index regen (see \`dotmd archive --help\`).
|
|
326
|
+
--show-files Append \`files: …\` footer.
|
|
327
|
+
--dry-run, -n Preview without writing.
|
|
328
|
+
|
|
329
|
+
Examples:
|
|
330
|
+
dotmd set partial # release current lease, mark partial
|
|
331
|
+
dotmd set archived docs/plans/x # archive a specific plan
|
|
332
|
+
dotmd set active # finish a held in-session plan`,
|
|
333
|
+
|
|
309
334
|
status: `dotmd status <file> <new-status> — transition document status
|
|
310
335
|
|
|
311
336
|
Moves the document to the new status. If transitioning to an archive
|
|
@@ -1137,6 +1162,7 @@ async function main() {
|
|
|
1137
1162
|
if (command === 'handoff') { die('`dotmd handoff` was removed in 0.31.0. Use `dotmd prompts new <name>` to create a saved prompt instead. The .dotmd/handoffs/ sidecar mechanism no longer exists; see CHANGELOG.'); }
|
|
1138
1163
|
if (command === 'finish') { const { runFinish } = await import('../src/lifecycle.mjs'); await runFinish(restArgs, config, { dryRun }); return; }
|
|
1139
1164
|
if (command === 'status') { const { runStatus } = await import('../src/lifecycle.mjs'); await runStatus(restArgs, config, { dryRun }); return; }
|
|
1165
|
+
if (command === 'set') { const { runSet } = await import('../src/lifecycle.mjs'); await runSet(restArgs, config, { dryRun }); return; }
|
|
1140
1166
|
if (command === 'archive') { const { runArchive } = await import('../src/lifecycle.mjs'); runArchive(restArgs, config, { dryRun }); return; }
|
|
1141
1167
|
if (command === 'bulk' && restArgs[0] === 'archive') { const { runBulkArchive } = await import('../src/lifecycle.mjs'); runBulkArchive(restArgs.slice(1), config, { dryRun }); return; }
|
|
1142
1168
|
if (command === 'bulk' && restArgs[0] === 'tag') { const { runBulkTag } = await import('../src/bulk-tag.mjs'); runBulkTag(restArgs.slice(1), config, { dryRun }); return; }
|
package/package.json
CHANGED
package/src/claude-commands.mjs
CHANGED
|
@@ -14,21 +14,49 @@ const VERSION_MARKER = `<!-- dotmd-generated: ${pkg.version} -->`;
|
|
|
14
14
|
const VERSION_REGEX = /<!-- dotmd-generated: ([\d.]+) -->/;
|
|
15
15
|
|
|
16
16
|
// Trigger sentences surfaced by Claude Code's available-skills system reminder.
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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.
|
|
20
21
|
const SLASH_DESCRIPTIONS = {
|
|
21
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 pick up / release / archive a plan.",
|
|
22
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.",
|
|
23
24
|
baton: "Save a resume prompt for the held plan and release the lease — the minimum handoff. Use when the user says hand off / save a resume / wrap up, or when context is getting tight.",
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
const VOCAB_TRUNCATE_AT = 12;
|
|
28
|
+
|
|
29
|
+
// Per-type valid statuses, rendered as one clause per type. Appended to the
|
|
30
|
+
// plans description so it lands in Claude's available-skills listing at
|
|
31
|
+
// SessionStart — no discovery command needed before the first `dotmd status`
|
|
32
|
+
// / `dotmd archive` call. Types with no declared statuses are skipped (the
|
|
33
|
+
// generic global list applies); types with >VOCAB_TRUNCATE_AT statuses are
|
|
34
|
+
// truncated with an ellipsis so the description stays bounded.
|
|
35
|
+
function statusVocabClause(config) {
|
|
36
|
+
if (!config?.typeStatuses) return '';
|
|
37
|
+
const parts = [];
|
|
38
|
+
for (const [type, statusesSet] of config.typeStatuses.entries()) {
|
|
39
|
+
if (!statusesSet || statusesSet.size === 0) continue;
|
|
40
|
+
let statuses = [...statusesSet];
|
|
41
|
+
if (statuses.length > VOCAB_TRUNCATE_AT) {
|
|
42
|
+
statuses = [...statuses.slice(0, VOCAB_TRUNCATE_AT), '…'];
|
|
43
|
+
}
|
|
44
|
+
parts.push(`Valid ${type} statuses: ${statuses.join(', ')}.`);
|
|
45
|
+
}
|
|
46
|
+
return parts.join(' ');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function frontmatterFor(name, config) {
|
|
50
|
+
let description = SLASH_DESCRIPTIONS[name];
|
|
51
|
+
if (name === 'plans') {
|
|
52
|
+
const vocab = statusVocabClause(config);
|
|
53
|
+
if (vocab) description = `${description} ${vocab}`;
|
|
54
|
+
}
|
|
55
|
+
return ['---', `description: ${description}`, '---'];
|
|
28
56
|
}
|
|
29
57
|
|
|
30
58
|
function generatePlansCommand(config) {
|
|
31
|
-
const lines = [...frontmatterFor('plans'), VERSION_MARKER, ''];
|
|
59
|
+
const lines = [...frontmatterFor('plans', config), VERSION_MARKER, ''];
|
|
32
60
|
lines.push('Run `dotmd context` to get the current plans briefing, then use it to orient yourself.');
|
|
33
61
|
lines.push('');
|
|
34
62
|
lines.push(`Plans are managed by **dotmd** (v${pkg.version}). Config at \`dotmd.config.mjs\`. Always use \`dotmd\` directly.`);
|
|
@@ -46,7 +74,8 @@ function generatePlansCommand(config) {
|
|
|
46
74
|
lines.push('- `dotmd prompts use <file>` — consume a specific prompt (prints body, auto-archives)');
|
|
47
75
|
lines.push('- `dotmd archive <file>` — archive with auto ref-fixing (both directions)');
|
|
48
76
|
lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
|
|
49
|
-
lines.push('- `dotmd status <file>
|
|
77
|
+
lines.push('- `dotmd set <status> [<file>]` — unified transition (archive / release / status bump in one verb; infers path from held lease)');
|
|
78
|
+
lines.push('- `dotmd status <file> <status>` — transition status (legacy; `set` is preferred)');
|
|
50
79
|
lines.push('- `dotmd query --keyword <term>` — find plans by keyword');
|
|
51
80
|
|
|
52
81
|
if (config.raw?.glossary) {
|
|
@@ -65,8 +94,8 @@ function generatePlansCommand(config) {
|
|
|
65
94
|
return lines.join('\n');
|
|
66
95
|
}
|
|
67
96
|
|
|
68
|
-
function generateBatonCommand() {
|
|
69
|
-
const lines = [...frontmatterFor('baton'), VERSION_MARKER, ''];
|
|
97
|
+
function generateBatonCommand(config) {
|
|
98
|
+
const lines = [...frontmatterFor('baton', config), VERSION_MARKER, ''];
|
|
70
99
|
lines.push('Wrap this session. Minimum required (two commands):');
|
|
71
100
|
lines.push('');
|
|
72
101
|
lines.push('1. **Save the resume prompt.** `dotmd new prompt resume-<plan-slug>` with a 10-20 line body via heredoc: the next concrete decision plus any gotchas. NOT a recap of the plan body. The saved prompt IS the handoff — never print it into chat for copy-paste.');
|
|
@@ -89,7 +118,7 @@ function generateDocsCommand(config) {
|
|
|
89
118
|
const roots = Array.isArray(config.raw?.root) ? config.raw.root : [config.raw?.root ?? 'docs'];
|
|
90
119
|
const rootCount = roots.length;
|
|
91
120
|
|
|
92
|
-
const lines = [...frontmatterFor('docs'), VERSION_MARKER, ''];
|
|
121
|
+
const lines = [...frontmatterFor('docs', config), VERSION_MARKER, ''];
|
|
93
122
|
lines.push(`All documentation in this repo is managed by **dotmd** (v${pkg.version}). Docs across ${rootCount} root${rootCount > 1 ? 's' : ''}: ${roots.join(', ')}. Config at \`dotmd.config.mjs\`.`);
|
|
94
123
|
lines.push('');
|
|
95
124
|
|
|
@@ -125,7 +154,8 @@ function generateDocsCommand(config) {
|
|
|
125
154
|
lines.push('- `dotmd prompts new <name> "<body>"` — save a resume-prompt');
|
|
126
155
|
lines.push('- `dotmd prompts next` — consume oldest pending prompt (prints body, auto-archives)');
|
|
127
156
|
lines.push('- `dotmd prompts use <file>` — consume a specific prompt (prints body, auto-archives)');
|
|
128
|
-
lines.push('- `dotmd status <file>
|
|
157
|
+
lines.push('- `dotmd set <status> [<file>]` — unified transition (archive / release / status bump; infers path from held lease)');
|
|
158
|
+
lines.push('- `dotmd status <file> <status>` — transition status (legacy; `set` is preferred)');
|
|
129
159
|
lines.push('- `dotmd archive <file>` — archive with auto ref-fixing');
|
|
130
160
|
lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
|
|
131
161
|
lines.push('- `dotmd touch --git` — bulk-sync updated dates from git history');
|
|
@@ -157,7 +187,7 @@ export function scaffoldClaudeCommands(cwd, config, opts = {}) {
|
|
|
157
187
|
const files = [
|
|
158
188
|
{ name: 'plans.md', generate: () => generatePlansCommand(config) },
|
|
159
189
|
{ name: 'docs.md', generate: () => generateDocsCommand(config) },
|
|
160
|
-
{ name: 'baton.md', generate: () => generateBatonCommand() },
|
|
190
|
+
{ name: 'baton.md', generate: () => generateBatonCommand(config) },
|
|
161
191
|
];
|
|
162
192
|
|
|
163
193
|
for (const { name, generate } of files) {
|
package/src/commands.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// templates points at a real command.
|
|
5
5
|
export const KNOWN_COMMANDS = [
|
|
6
6
|
'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'briefing', 'context', 'hud',
|
|
7
|
-
'focus', 'query', 'plans', 'prompts', 'stale', 'actionable', 'index', 'pickup', 'release', 'finish', 'status', 'archive', 'bulk', 'bulk-tag', 'touch', 'doctor',
|
|
7
|
+
'focus', 'query', 'plans', 'prompts', 'stale', 'actionable', 'index', 'pickup', 'release', 'finish', 'status', 'set', 'archive', 'bulk', 'bulk-tag', 'touch', 'doctor',
|
|
8
8
|
'unblocks', 'health', 'glossary', 'modules', 'module',
|
|
9
9
|
'fix-refs', 'lint', 'rename', 'migrate', 'notion', 'export', 'summary',
|
|
10
10
|
'watch', 'diff', 'new', 'init', 'completions', 'statuses', 'journal',
|
package/src/lifecycle.mjs
CHANGED
|
@@ -680,6 +680,82 @@ export function runArchive(argv, config, opts = {}) {
|
|
|
680
680
|
return { touched };
|
|
681
681
|
}
|
|
682
682
|
|
|
683
|
+
// Unified status-transition verb. Collapses status/archive/release into one
|
|
684
|
+
// signature — `dotmd set <status> [<path>]` — and dispatches to the right
|
|
685
|
+
// plumbing based on the *target* status:
|
|
686
|
+
// - target in archiveStatuses (and file not already archived) → runArchive
|
|
687
|
+
// (gets us ref-fixing + auto lease release + closeout-template offer)
|
|
688
|
+
// - source = in-session, target != in-session → runStatus +
|
|
689
|
+
// auto-release of the held lease (so users don't have to chain `release`)
|
|
690
|
+
// - everything else (incl. unarchive, plain transitions) → runStatus
|
|
691
|
+
//
|
|
692
|
+
// Path is inferred from the calling session's held lease when omitted. With
|
|
693
|
+
// zero leases or >1 leases, we refuse and ask for explicit `<path>` instead
|
|
694
|
+
// of guessing.
|
|
695
|
+
//
|
|
696
|
+
// `dotmd set in-session <path>` is refused — acquiring a lease is asymmetric
|
|
697
|
+
// enough to deserve its own verb (`dotmd pickup`), and silently routing here
|
|
698
|
+
// would skip the lease-acquisition path entirely.
|
|
699
|
+
export async function runSet(argv, config, opts = {}) {
|
|
700
|
+
const { dryRun } = opts;
|
|
701
|
+
const noIndex = argv.includes('--no-index');
|
|
702
|
+
const showFiles = argv.includes('--show-files');
|
|
703
|
+
argv = argv.filter(a => a !== '--no-index' && a !== '--show-files');
|
|
704
|
+
|
|
705
|
+
const newStatus = argv[0];
|
|
706
|
+
let input = argv[1];
|
|
707
|
+
|
|
708
|
+
if (!newStatus) die('Usage: dotmd set <status> [<path>]');
|
|
709
|
+
|
|
710
|
+
if (newStatus === 'in-session') {
|
|
711
|
+
die('To acquire an in-session lease, use `dotmd pickup <file>`. `dotmd set` is for releasing/transitioning a held lease.');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (!input) {
|
|
715
|
+
const leases = readLeases(config);
|
|
716
|
+
const sid = currentSessionId();
|
|
717
|
+
const owned = Object.entries(leases).filter(([_, l]) => l.session === sid);
|
|
718
|
+
if (owned.length === 0) {
|
|
719
|
+
die('No <path> given and no held lease to infer from.\nUsage: dotmd set <status> <path>');
|
|
720
|
+
}
|
|
721
|
+
if (owned.length > 1) {
|
|
722
|
+
const paths = owned.map(([p]) => ` - ${p}`).join('\n');
|
|
723
|
+
die(`No <path> given; you hold ${owned.length} leases:\n${paths}\nSpecify <path> explicitly.`);
|
|
724
|
+
}
|
|
725
|
+
input = owned[0][0];
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const filePath = resolveDocPath(input, config);
|
|
729
|
+
if (!filePath) die(`File not found: ${input}\nSearched: ${toRepoPath(config.repoRoot, config.repoRoot) || '.'}, ${toRepoPath(config.docsRoot, config.repoRoot)}`);
|
|
730
|
+
|
|
731
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
732
|
+
const { frontmatter: fmRaw } = extractFrontmatter(raw);
|
|
733
|
+
const parsedFm = parseSimpleFrontmatter(fmRaw);
|
|
734
|
+
const oldStatus = asString(parsedFm.status);
|
|
735
|
+
|
|
736
|
+
const fileRoot = findFileRoot(filePath, config);
|
|
737
|
+
const relFromRoot = path.relative(fileRoot, filePath);
|
|
738
|
+
const inArchive = relFromRoot.startsWith(config.archiveDir + '/') || relFromRoot.startsWith(config.archiveDir + path.sep);
|
|
739
|
+
|
|
740
|
+
if (config.lifecycle.archiveStatuses.has(newStatus) && !inArchive) {
|
|
741
|
+
const archiveArgs = [filePath];
|
|
742
|
+
if (noIndex) archiveArgs.push('--no-index');
|
|
743
|
+
if (showFiles) archiveArgs.push('--show-files');
|
|
744
|
+
return runArchive(archiveArgs, config, { dryRun });
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const statusArgs = [filePath, newStatus];
|
|
748
|
+
if (noIndex) statusArgs.push('--no-index');
|
|
749
|
+
if (showFiles) statusArgs.push('--show-files');
|
|
750
|
+
await runStatus(statusArgs, config, { dryRun });
|
|
751
|
+
|
|
752
|
+
if (oldStatus === 'in-session' && newStatus !== 'in-session' && !dryRun) {
|
|
753
|
+
const repoPath = toRepoPath(filePath, config.repoRoot);
|
|
754
|
+
try { releaseLease(config, repoPath, { force: false }); }
|
|
755
|
+
catch (err) { warn(`Could not release lease for ${repoPath}: ${err.message}`); }
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
683
759
|
export function runBulkArchive(argv, config, opts = {}) {
|
|
684
760
|
const { dryRun } = opts;
|
|
685
761
|
const noIndex = argv.includes('--no-index') || opts.noIndex;
|