dotmd-cli 0.49.4 → 0.49.5
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/README.md +8 -7
- package/bin/dotmd.mjs +17 -12
- package/package.json +1 -1
- package/src/config.mjs +7 -6
- package/src/lifecycle.mjs +24 -6
- package/src/prompts.mjs +18 -11
- package/src/use.mjs +2 -2
- package/src/validate.mjs +5 -0
package/README.md
CHANGED
|
@@ -128,7 +128,7 @@ Every document can have a `type` field in its frontmatter. Types determine which
|
|
|
128
128
|
|------|---------|----------------|
|
|
129
129
|
| `plan` | Execution plans | `in-session`, `active`, `planned`, `blocked`, `partial`, `paused`, `awaiting`, `queued-after`, `archived` |
|
|
130
130
|
| `doc` | Design docs, specs, ADRs, RFCs, reference material | `draft`, `active`, `review`, `reference`, `deprecated`, `archived` |
|
|
131
|
-
| `prompt` | Saved prompts that seed future Claude sessions | `pending`, `shelved`, `claimed`, `archived` |
|
|
131
|
+
| `prompt` | Saved prompts that seed future Claude sessions | `pending`, `held`, `shelved`, `claimed`, `archived` |
|
|
132
132
|
|
|
133
133
|
Documents without a `type` field use the global `statuses.order` from config.
|
|
134
134
|
|
|
@@ -206,7 +206,7 @@ dotmd glossary <term> Look up domain terms + related docs
|
|
|
206
206
|
dotmd watch [command] Re-run a command on file changes
|
|
207
207
|
dotmd diff [file] Show changes since last updated date
|
|
208
208
|
dotmd new <type> <name> Create a new doc (type: doc, plan, or prompt)
|
|
209
|
-
dotmd prompts [sub] Manage saved prompts (list, next, use,
|
|
209
|
+
dotmd prompts [sub] Manage saved prompts (list, next, use, hold, archive, new)
|
|
210
210
|
dotmd journal [flags] View opt-in command-usage journal (DOTMD_JOURNAL=1)
|
|
211
211
|
dotmd init Create starter config + docs directory
|
|
212
212
|
dotmd completions <shell> Output shell completion script (bash, zsh)
|
|
@@ -303,16 +303,17 @@ dotmd prompts # list pending prompts (default)
|
|
|
303
303
|
dotmd prompts list --all # all statuses
|
|
304
304
|
dotmd prompts next # print body of oldest pending + auto-archive (one-shot)
|
|
305
305
|
dotmd prompts use <file> # print body of a specific prompt + auto-archive
|
|
306
|
-
dotmd prompts
|
|
307
|
-
# hidden from hud/briefing, skipped by `next`
|
|
308
|
-
dotmd prompts
|
|
306
|
+
dotmd prompts hold <file> # park a prompt (status → held) under prompts/held/:
|
|
307
|
+
# kept in list, hidden from hud/briefing, skipped by `next`
|
|
308
|
+
dotmd prompts unhold <file> # move a held prompt back to pending
|
|
309
|
+
dotmd prompts shelve <file> # legacy alias for `hold`
|
|
309
310
|
dotmd prompts archive <file> # archive without printing the body
|
|
310
311
|
dotmd prompts new <name> [body] # alias for `dotmd new prompt`
|
|
311
312
|
```
|
|
312
313
|
|
|
313
|
-
`dotmd hud` surfaces pending prompts on session start (alongside held leases), so a saved prompt acts as a self-addressed reminder: write it now, the next session sees it.
|
|
314
|
+
`dotmd hud` surfaces pending prompts on session start (alongside held leases), so a saved prompt acts as a self-addressed reminder: write it now, the next session sees it. Held prompts are kept out of the SessionStart surface — use them for "saved but not next."
|
|
314
315
|
|
|
315
|
-
Statuses: `pending` (drafted, awaiting a session), `
|
|
316
|
+
Statuses: `pending` (drafted, awaiting a session), `held` (saved but parked under `prompts/held/` — visible in `prompts list`, hidden from `hud`/`briefing`, skipped by `prompts next`), `archived` (consumed or filed away). `shelved` is a legacy spelling accepted for older files; `claimed` is reserved for a future "in-flight" state but is currently a synonym for archived in practice.
|
|
316
317
|
|
|
317
318
|
### Command Journal (opt-in)
|
|
318
319
|
|
package/bin/dotmd.mjs
CHANGED
|
@@ -44,7 +44,7 @@ const FLAG_SPECS = {
|
|
|
44
44
|
prompts: {
|
|
45
45
|
flags: new Set(['--json', '--status', '--include-archived', '--sort', '--limit', '--all', '--no-index', '--show-files', '--body', '--message', '--title']),
|
|
46
46
|
values: new Set(['--status', '--sort', '--limit', '--body', '--message', '--title']),
|
|
47
|
-
subcommands: new Set(['list', 'next', 'use', 'resume', 'archive', 'new', 'shelve', 'unshelve', 'status']),
|
|
47
|
+
subcommands: new Set(['list', 'next', 'use', 'resume', 'archive', 'new', 'hold', 'unhold', 'shelve', 'unshelve', 'status']),
|
|
48
48
|
},
|
|
49
49
|
};
|
|
50
50
|
|
|
@@ -65,12 +65,12 @@ const HELP = {
|
|
|
65
65
|
|
|
66
66
|
Common commands:
|
|
67
67
|
plans Live plans (excludes archived)
|
|
68
|
-
prompts Prompt queue/admin (list, next, archive, new,
|
|
68
|
+
prompts Prompt queue/admin (list, next, archive, new, hold)
|
|
69
69
|
briefing Full briefing with plan counts + next steps
|
|
70
70
|
agent-context Compact bounded JSON context for agents
|
|
71
71
|
set <status> [file] Transition status (start work, finish, archive — all via target status)
|
|
72
72
|
new <type> <name> Create plan/doc/prompt (pipe stdin or @path for body)
|
|
73
|
-
use [<file>]
|
|
73
|
+
use [<file-or-prompt-slug>] Open a doc by type: prompt → consume, plan → start, doc → read
|
|
74
74
|
(no file: consume oldest pending prompt)
|
|
75
75
|
archive <file> Close out a plan (status → archived, move, update refs)
|
|
76
76
|
|
|
@@ -95,8 +95,8 @@ View & Query:
|
|
|
95
95
|
focus [status] [--json] Detailed view for one status group
|
|
96
96
|
query [filters] [--json] Filtered search (--status, --keyword, --stale, etc.)
|
|
97
97
|
plans Live plans (excludes archived; --include-archived for all)
|
|
98
|
-
use [<file>]
|
|
99
|
-
prompts [list|archive|new|
|
|
98
|
+
use [<file-or-prompt-slug>] Open a doc by type: prompt → consume, plan → start, doc → read
|
|
99
|
+
prompts [list|archive|new|hold] Prompt admin (list / archive / save / hold). Use \`dotmd use\` to consume.
|
|
100
100
|
stale Stale docs (preset)
|
|
101
101
|
actionable Docs with next steps (preset)
|
|
102
102
|
|
|
@@ -235,9 +235,13 @@ prompt statuses
|
|
|
235
235
|
\`dotmd prompts use <file>\` prints body + archives atomically.
|
|
236
236
|
\`dotmd prompts next\` does the same for the oldest pending.
|
|
237
237
|
|
|
238
|
-
|
|
238
|
+
held Saved under prompts/held/ and hidden from \`hud\` /
|
|
239
|
+
\`briefing\` / \`prompts next\`.
|
|
239
240
|
Still listed by \`dotmd prompts list\`.
|
|
240
|
-
\`dotmd prompts
|
|
241
|
+
\`dotmd prompts unhold <file>\` → pending.
|
|
242
|
+
|
|
243
|
+
shelved Legacy spelling for held prompts. \`dotmd prompts shelve\`
|
|
244
|
+
now writes \`status: held\`.
|
|
241
245
|
|
|
242
246
|
claimed Legacy intermediate state (atomic use → archived now).
|
|
243
247
|
|
|
@@ -920,10 +924,11 @@ Subcommands:
|
|
|
920
924
|
resume <file-or-slug> Alias for \`use\` — same behavior, easier name
|
|
921
925
|
when continuing a session
|
|
922
926
|
archive <file-or-slug> Archive a prompt without printing its body
|
|
923
|
-
|
|
924
|
-
hidden from hud/briefing pending
|
|
925
|
-
by \`prompts next\`.
|
|
926
|
-
|
|
927
|
+
hold <file-or-slug> Park a prompt (status → held) under prompts/held/:
|
|
928
|
+
kept in list, hidden from hud/briefing pending
|
|
929
|
+
surfaces, skipped by \`prompts next\`.
|
|
930
|
+
unhold <file-or-slug> Move a held prompt back to pending.
|
|
931
|
+
shelve / unshelve Legacy aliases for hold / unhold.
|
|
927
932
|
new <slug> [body] Create a new prompt (alias for
|
|
928
933
|
\`dotmd new prompt <slug> [body]\`)
|
|
929
934
|
|
|
@@ -931,7 +936,7 @@ Subcommands:
|
|
|
931
936
|
slug matching a prompt basename, or a unique substring of a prompt
|
|
932
937
|
path. Ambiguous substrings error with the candidate list.
|
|
933
938
|
|
|
934
|
-
Default prompt statuses: pending, shelved, claimed, archived.
|
|
939
|
+
Default prompt statuses: pending, held, shelved, claimed, archived.
|
|
935
940
|
|
|
936
941
|
Examples:
|
|
937
942
|
dotmd prompts # pending prompts (default)
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -11,6 +11,7 @@ const CONFIG_FILENAMES = ['dotmd.config.mjs', '.dotmd.config.mjs', 'dotmd.config
|
|
|
11
11
|
const REPLACE_KEYS = new Set([
|
|
12
12
|
'statuses.staleDays',
|
|
13
13
|
'statuses.rootStatuses',
|
|
14
|
+
'lifecycle.filedStatuses',
|
|
14
15
|
'presets',
|
|
15
16
|
'context',
|
|
16
17
|
]);
|
|
@@ -36,8 +37,8 @@ const DEFAULTS = {
|
|
|
36
37
|
staleDays: { draft: 30, active: 14, review: 14 },
|
|
37
38
|
},
|
|
38
39
|
prompt: {
|
|
39
|
-
statuses: ['pending', 'shelved', 'claimed', 'archived'],
|
|
40
|
-
context: { expanded: ['pending'], listed: ['shelved'], counted: ['claimed', 'archived'] },
|
|
40
|
+
statuses: ['pending', 'held', 'shelved', 'claimed', 'archived'],
|
|
41
|
+
context: { expanded: ['pending'], listed: ['held', 'shelved'], counted: ['claimed', 'archived'] },
|
|
41
42
|
staleDays: { pending: 30 },
|
|
42
43
|
},
|
|
43
44
|
},
|
|
@@ -58,10 +59,10 @@ const DEFAULTS = {
|
|
|
58
59
|
skipStaleFor: ['archived', 'reference', 'partial', 'queued-after'],
|
|
59
60
|
skipWarningsFor: ['archived', 'partial', 'queued-after'],
|
|
60
61
|
terminalStatuses: ['archived', 'deprecated', 'reference'],
|
|
61
|
-
// F15:
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
filedStatuses: {},
|
|
62
|
+
// F15: per-status filing buckets (status → dirName). Built-in held/paused
|
|
63
|
+
// statuses file under the owning type folder; archive remains a separate
|
|
64
|
+
// primitive untouched.
|
|
65
|
+
filedStatuses: { held: 'held', shelved: 'held', paused: 'held' },
|
|
65
66
|
},
|
|
66
67
|
|
|
67
68
|
taxonomy: {
|
package/src/lifecycle.mjs
CHANGED
|
@@ -26,6 +26,23 @@ function findFileRoot(filePath, config) {
|
|
|
26
26
|
return roots.find(r => filePath.startsWith(r + '/')) ?? config.docsRoot;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function defaultTypeDir(docType, config) {
|
|
30
|
+
if (docType === 'plan') return 'plans';
|
|
31
|
+
if (docType === 'prompt') return 'prompts';
|
|
32
|
+
const templateDir = config.raw?.templates?.[docType]?.dir;
|
|
33
|
+
return typeof templateDir === 'string' && templateDir ? templateDir : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findFilingRoot(filePath, fileRoot, docType, config) {
|
|
37
|
+
const dirName = defaultTypeDir(docType, config);
|
|
38
|
+
if (!dirName) return fileRoot;
|
|
39
|
+
if (path.basename(fileRoot) === dirName) return fileRoot;
|
|
40
|
+
|
|
41
|
+
const relSegments = path.relative(fileRoot, filePath).split(path.sep);
|
|
42
|
+
if (relSegments[0] === dirName) return path.join(fileRoot, dirName);
|
|
43
|
+
return fileRoot;
|
|
44
|
+
}
|
|
45
|
+
|
|
29
46
|
// Best-effort index regen for any doc-set or doc-status mutation. The
|
|
30
47
|
// generated block groups by status and embeds per-doc snapshots, so any
|
|
31
48
|
// change that affects what would render leaves the index stale. Wrapped
|
|
@@ -167,8 +184,9 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
167
184
|
|
|
168
185
|
const today = nowIso();
|
|
169
186
|
const archiveDir = path.join(fileRoot, config.archiveDir);
|
|
170
|
-
const
|
|
171
|
-
const
|
|
187
|
+
const filingRoot = findFilingRoot(filePath, fileRoot, docType, config);
|
|
188
|
+
const relFromFilingRoot = path.relative(filingRoot, filePath);
|
|
189
|
+
const relSegments = relFromFilingRoot.split(path.sep);
|
|
172
190
|
const inArchive = isArchivedPath(toRepoPath(filePath, config.repoRoot), config);
|
|
173
191
|
const isArchiving = config.lifecycle.archiveStatuses.has(newStatus) && !inArchive;
|
|
174
192
|
const isUnarchiving = !config.lifecycle.archiveStatuses.has(newStatus) && inArchive;
|
|
@@ -200,12 +218,12 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
200
218
|
finalPath = targetPath;
|
|
201
219
|
}
|
|
202
220
|
if (isFiling) {
|
|
203
|
-
const targetPath = path.join(
|
|
221
|
+
const targetPath = path.join(filingRoot, newFiledDir, path.basename(filePath));
|
|
204
222
|
process.stdout.write(`${prefix} Would file: ${toRepoPath(filePath, config.repoRoot)} → ${toRepoPath(targetPath, config.repoRoot)}\n`);
|
|
205
223
|
finalPath = targetPath;
|
|
206
224
|
}
|
|
207
225
|
if (isUnfiling) {
|
|
208
|
-
const targetPath = path.join(
|
|
226
|
+
const targetPath = path.join(filingRoot, path.basename(filePath));
|
|
209
227
|
process.stdout.write(`${prefix} Would unfile: ${toRepoPath(filePath, config.repoRoot)} → ${toRepoPath(targetPath, config.repoRoot)}\n`);
|
|
210
228
|
finalPath = targetPath;
|
|
211
229
|
}
|
|
@@ -236,7 +254,7 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
236
254
|
}
|
|
237
255
|
|
|
238
256
|
if (isFiling) {
|
|
239
|
-
const targetDir = path.join(
|
|
257
|
+
const targetDir = path.join(filingRoot, newFiledDir);
|
|
240
258
|
mkdirSync(targetDir, { recursive: true });
|
|
241
259
|
const targetPath = path.join(targetDir, path.basename(filePath));
|
|
242
260
|
if (existsSync(targetPath)) { die(`Target already exists: ${toRepoPath(targetPath, config.repoRoot)}`); }
|
|
@@ -246,7 +264,7 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
246
264
|
}
|
|
247
265
|
|
|
248
266
|
if (isUnfiling) {
|
|
249
|
-
const targetPath = path.join(
|
|
267
|
+
const targetPath = path.join(filingRoot, path.basename(filePath));
|
|
250
268
|
if (existsSync(targetPath)) { die(`Target already exists: ${toRepoPath(targetPath, config.repoRoot)}`); }
|
|
251
269
|
const result = gitMv(filePath, targetPath, config.repoRoot);
|
|
252
270
|
if (result.status !== 0) { die(result.stderr || 'git mv failed.'); }
|
package/src/prompts.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { green, dim } from './color.mjs';
|
|
|
11
11
|
// `resume` is an alias for `use` — agents reach for "resume" when continuing a
|
|
12
12
|
// session; `use` reads as internal mechanics. Both names stay valid; the
|
|
13
13
|
// canonical output ("Consumed: …") is unchanged.
|
|
14
|
-
const SUBCOMMANDS = new Set(['list', 'next', 'use', 'resume', 'archive', 'new', 'shelve', 'unshelve']);
|
|
14
|
+
const SUBCOMMANDS = new Set(['list', 'next', 'use', 'resume', 'archive', 'new', 'hold', 'unhold', 'shelve', 'unshelve']);
|
|
15
15
|
|
|
16
16
|
export async function runPrompts(argv, config, opts = {}) {
|
|
17
17
|
const sub = argv[0];
|
|
@@ -28,8 +28,10 @@ export async function runPrompts(argv, config, opts = {}) {
|
|
|
28
28
|
case 'resume': return runPromptsUse(rest, config, opts);
|
|
29
29
|
case 'archive': return runPromptsArchive(rest, config, opts);
|
|
30
30
|
case 'new': return runPromptsNew(rest, config, opts);
|
|
31
|
-
case '
|
|
32
|
-
case '
|
|
31
|
+
case 'hold': return runPromptsHold(rest, config, opts);
|
|
32
|
+
case 'unhold': return runPromptsUnhold(rest, config, opts);
|
|
33
|
+
case 'shelve': return runPromptsHold(rest, config, opts);
|
|
34
|
+
case 'unshelve': return runPromptsUnhold(rest, config, opts);
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -190,7 +192,8 @@ function runPromptsNext(argv, config, opts = {}) {
|
|
|
190
192
|
// path + '.md', exact basename match across type: prompt docs, substring
|
|
191
193
|
// match across type: prompt docs. Returns the absolute path or dies with a
|
|
192
194
|
// helpful message (no match / ambiguous match).
|
|
193
|
-
function resolvePromptInput(input, config) {
|
|
195
|
+
export function resolvePromptInput(input, config, options = {}) {
|
|
196
|
+
const dieOnMiss = options.dieOnMiss !== false;
|
|
194
197
|
const direct = resolveDocPath(input, config);
|
|
195
198
|
if (direct) return direct;
|
|
196
199
|
|
|
@@ -201,7 +204,10 @@ function resolvePromptInput(input, config) {
|
|
|
201
204
|
|
|
202
205
|
const index = buildIndex(config);
|
|
203
206
|
const prompts = index.docs.filter(d => d.type === 'prompt');
|
|
204
|
-
if (prompts.length === 0)
|
|
207
|
+
if (prompts.length === 0) {
|
|
208
|
+
if (dieOnMiss) die(`No prompts in the index.`);
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
205
211
|
|
|
206
212
|
const slug = input.replace(/\.md$/, '');
|
|
207
213
|
|
|
@@ -219,7 +225,8 @@ function resolvePromptInput(input, config) {
|
|
|
219
225
|
die(`Multiple prompts match "${input}":\n${bySubstring.map(d => ' ' + d.path).join('\n')}`);
|
|
220
226
|
}
|
|
221
227
|
|
|
222
|
-
die(`No prompt found matching: ${input}`);
|
|
228
|
+
if (dieOnMiss) die(`No prompt found matching: ${input}`);
|
|
229
|
+
return null;
|
|
223
230
|
}
|
|
224
231
|
|
|
225
232
|
function runPromptsUse(argv, config, opts = {}) {
|
|
@@ -300,16 +307,16 @@ async function runPromptsNew(argv, config, opts = {}) {
|
|
|
300
307
|
return runNew(['prompt', ...argv], config, opts);
|
|
301
308
|
}
|
|
302
309
|
|
|
303
|
-
async function
|
|
310
|
+
async function runPromptsHold(argv, config, opts = {}) {
|
|
304
311
|
const input = argv.find(a => !a.startsWith('-'));
|
|
305
|
-
if (!input) die('Usage: dotmd prompts
|
|
312
|
+
if (!input) die('Usage: dotmd prompts hold <file-or-slug>');
|
|
306
313
|
const filePath = resolvePromptInput(input, config);
|
|
307
|
-
return runStatus([filePath, '
|
|
314
|
+
return runStatus([filePath, 'held'], config, opts);
|
|
308
315
|
}
|
|
309
316
|
|
|
310
|
-
async function
|
|
317
|
+
async function runPromptsUnhold(argv, config, opts = {}) {
|
|
311
318
|
const input = argv.find(a => !a.startsWith('-'));
|
|
312
|
-
if (!input) die('Usage: dotmd prompts
|
|
319
|
+
if (!input) die('Usage: dotmd prompts unhold <file-or-slug>');
|
|
313
320
|
const filePath = resolvePromptInput(input, config);
|
|
314
321
|
return runStatus([filePath, 'pending'], config, opts);
|
|
315
322
|
}
|
package/src/use.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
3
3
|
import { asString, die, resolveDocPath, toRepoPath } from './util.mjs';
|
|
4
|
-
import { consumePrompt, pendingPromptsOldestFirst } from './prompts.mjs';
|
|
4
|
+
import { consumePrompt, pendingPromptsOldestFirst, resolvePromptInput } from './prompts.mjs';
|
|
5
5
|
import { runPickup } from './lifecycle.mjs';
|
|
6
6
|
|
|
7
7
|
// Top-level `dotmd use [file]` — the single "start engaging with this doc"
|
|
@@ -22,7 +22,7 @@ export async function runUse(argv, config, opts = {}) {
|
|
|
22
22
|
return consumePrompt(head.abs, config, opts);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const filePath = resolveDocPath(positional, config);
|
|
25
|
+
const filePath = resolveDocPath(positional, config) ?? resolvePromptInput(positional, config, { dieOnMiss: false });
|
|
26
26
|
if (!filePath) die(`File not found: ${positional}`);
|
|
27
27
|
|
|
28
28
|
const raw = readFileSync(filePath, 'utf8');
|
package/src/validate.mjs
CHANGED
|
@@ -49,6 +49,11 @@ function liveTypeDirsForRoots(config) {
|
|
|
49
49
|
for (const dirName of filedDirs) {
|
|
50
50
|
if (path.basename(rootRel) === dirName) continue;
|
|
51
51
|
set.add(rootRel ? `${rootRel}/${dirName}` : dirName);
|
|
52
|
+
for (const typeDir of BUILTIN_TYPE_DIR_NAMES) {
|
|
53
|
+
if (path.basename(rootRel) === typeDir) continue;
|
|
54
|
+
const typeRoot = rootRel ? `${rootRel}/${typeDir}` : typeDir;
|
|
55
|
+
set.add(`${typeRoot}/${dirName}`);
|
|
56
|
+
}
|
|
52
57
|
}
|
|
53
58
|
}
|
|
54
59
|
return set;
|