dotmd-cli 0.49.4 → 0.50.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/README.md +14 -7
- package/bin/dotmd.mjs +17 -12
- package/dotmd.config.example.mjs +1 -0
- package/package.json +1 -1
- package/src/config.mjs +12 -6
- package/src/hud.mjs +17 -69
- package/src/index-file.mjs +13 -3
- package/src/init.mjs +1 -0
- 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
|
|
|
@@ -878,9 +879,15 @@ export const index = {
|
|
|
878
879
|
path: 'docs/docs.md',
|
|
879
880
|
startMarker: '<!-- GENERATED:dotmd:start -->',
|
|
880
881
|
endMarker: '<!-- GENERATED:dotmd:end -->',
|
|
882
|
+
snapshot: 'status', // default; use 'state' to include live current_state text
|
|
881
883
|
};
|
|
882
884
|
```
|
|
883
885
|
|
|
886
|
+
Generated indexes default to status-only rows for live sections so README files
|
|
887
|
+
do not become stale mirrors of volatile `current_state` text. Set
|
|
888
|
+
`snapshot: 'state'` if you want the older `Status Snapshot` table for live
|
|
889
|
+
sections too. Archived highlights still include their historical snapshots.
|
|
890
|
+
|
|
884
891
|
All exports are optional. Additional options: `context`, `display`, `presets`, `templates`, `excludeDirs`, `notion`. See [`dotmd.config.example.mjs`](dotmd.config.example.mjs) for the full reference.
|
|
885
892
|
|
|
886
893
|
Config discovery walks up from cwd looking for `dotmd.config.mjs` or `.dotmd.config.mjs`.
|
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/dotmd.config.example.mjs
CHANGED
|
@@ -144,6 +144,7 @@ export const index = {
|
|
|
144
144
|
path: 'docs/docs.md',
|
|
145
145
|
startMarker: '<!-- GENERATED:dotmd:start -->',
|
|
146
146
|
endMarker: '<!-- GENERATED:dotmd:end -->',
|
|
147
|
+
snapshot: 'status', // default; use 'state' to include live current_state text
|
|
147
148
|
archivedLimit: 8,
|
|
148
149
|
};
|
|
149
150
|
|
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: {
|
|
@@ -315,6 +316,10 @@ function validateConfig(userConfig, config, validStatuses, indexPath) {
|
|
|
315
316
|
warnings.push(`Config: index path does not exist: ${indexPath}`);
|
|
316
317
|
}
|
|
317
318
|
|
|
319
|
+
if (config.index?.snapshot !== undefined && !['status', 'state'].includes(config.index.snapshot)) {
|
|
320
|
+
warnings.push("Config: index.snapshot must be 'status' or 'state'.");
|
|
321
|
+
}
|
|
322
|
+
|
|
318
323
|
// Unknown top-level user config keys
|
|
319
324
|
for (const key of Object.keys(userConfig)) {
|
|
320
325
|
if (!VALID_CONFIG_KEYS.has(key)) {
|
|
@@ -511,6 +516,7 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
511
516
|
indexPath,
|
|
512
517
|
indexStartMarker: config.index?.startMarker ?? '<!-- GENERATED:dotmd:start -->',
|
|
513
518
|
indexEndMarker: config.index?.endMarker ?? '<!-- GENERATED:dotmd:end -->',
|
|
519
|
+
indexSnapshot: config.index?.snapshot ?? 'status',
|
|
514
520
|
archivedHighlightLimit: config.index?.archivedLimit ?? 8,
|
|
515
521
|
|
|
516
522
|
context: config.context,
|
package/src/hud.mjs
CHANGED
|
@@ -4,21 +4,11 @@ import { readLeases, findStaleLeases, currentSessionId, isLeaseStale, STALE_LEAS
|
|
|
4
4
|
import { scrubStaleSilently } from './lease-scrub.mjs';
|
|
5
5
|
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
6
6
|
import { asString, toRepoPath } from './util.mjs';
|
|
7
|
-
import { dim
|
|
7
|
+
import { dim } from './color.mjs';
|
|
8
8
|
import { buildIndex } from './index.mjs';
|
|
9
9
|
import { refreshStaleSlashCommands } from './claude-commands.mjs';
|
|
10
10
|
import { readJournalEntries, journalFilePath } from './journal.mjs';
|
|
11
11
|
|
|
12
|
-
const MAX_PREVIEW = 5;
|
|
13
|
-
|
|
14
|
-
function slug(repoPath) { return path.basename(repoPath, '.md'); }
|
|
15
|
-
|
|
16
|
-
function previewList(items, max = MAX_PREVIEW) {
|
|
17
|
-
const slugs = items.slice(0, max).map(slug);
|
|
18
|
-
const more = items.length > max ? `, +${items.length - max} more` : '';
|
|
19
|
-
return slugs.join(', ') + more;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
12
|
// Statuses that count as "actionable" for a prompt are derived from config:
|
|
23
13
|
// types.prompt.context.expanded (the statuses the user wants prominently shown).
|
|
24
14
|
// Falls back to ['pending'] when no prompt type is configured (defensive default
|
|
@@ -223,11 +213,12 @@ export function runHud(argv, config) {
|
|
|
223
213
|
const hud = buildHud(config);
|
|
224
214
|
|
|
225
215
|
// Self-heal stale slash-command files. Wrapped: a broken scaffolder must
|
|
226
|
-
// never kill the SessionStart hook (would block every session).
|
|
227
|
-
//
|
|
228
|
-
|
|
216
|
+
// never kill the SessionStart hook (would block every session). Runs for its
|
|
217
|
+
// side effect only — the refresh is no longer announced in stdout (see the
|
|
218
|
+
// primer-only contract below). Skipped in --json mode to keep the structured
|
|
219
|
+
// shape stable for programmatic callers.
|
|
229
220
|
if (!json) {
|
|
230
|
-
try {
|
|
221
|
+
try { refreshStaleSlashCommands(config); }
|
|
231
222
|
catch { /* swallow — see comment above */ }
|
|
232
223
|
}
|
|
233
224
|
|
|
@@ -236,58 +227,15 @@ export function runHud(argv, config) {
|
|
|
236
227
|
return;
|
|
237
228
|
}
|
|
238
229
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (hud.owned?.length) state.push(`held: ${hud.owned.length} (${previewList(hud.owned)})`);
|
|
251
|
-
if (hud.prompts?.length) state.push(`prompts: ${hud.prompts.length} (${previewList(hud.prompts)})`);
|
|
252
|
-
if (hud.stale?.length) state.push(`stuck: ${hud.stale.length} (${previewList(hud.stale)})`);
|
|
253
|
-
if (hud.errors > 0) state.push(`errors: ${hud.errors} (run dotmd check)`);
|
|
254
|
-
if (state.length) lines.push(yellow(state.join(' · ')));
|
|
255
|
-
|
|
256
|
-
if (refreshed.length > 0) {
|
|
257
|
-
const from = refreshed[0].from;
|
|
258
|
-
const to = refreshed[0].to;
|
|
259
|
-
const names = refreshed.map(r => r.name).join(', ');
|
|
260
|
-
lines.push(dim(`↻ slash commands refreshed (v${from} → v${to}): ${names}`));
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// F17b: three journal-aware sections. Silent-when-clean: each block emits
|
|
264
|
-
// only when it has entries.
|
|
265
|
-
if (hud.previousSelf?.length) {
|
|
266
|
-
lines.push(dim('— previous self —'));
|
|
267
|
-
for (const e of hud.previousSelf) {
|
|
268
|
-
const cmd = (e.argv ?? []).join(' ');
|
|
269
|
-
const exitTag = e.exit === 0 ? '' : `, exit ${e.exit}`;
|
|
270
|
-
lines.push(dim(` ${cmd} (${e.ago}${exitTag})`));
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (hud.fleet?.length) {
|
|
275
|
-
lines.push(dim('— fleet (last 24h) —'));
|
|
276
|
-
for (const f of hud.fleet) {
|
|
277
|
-
const heldTag = f.holding?.length
|
|
278
|
-
? ` · holding ${f.holding.map(p => path.basename(p, '.md')).join(', ')}`
|
|
279
|
-
: '';
|
|
280
|
-
const staleTag = f.stale ? yellow(' [stale]') : '';
|
|
281
|
-
lines.push(dim(` session ${f.sid} · ${f.cmds} cmds · last ${f.lastAgo}${heldTag}`) + staleTag);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (hud.recentRejections?.length) {
|
|
286
|
-
lines.push(dim('— recent rejections (last 1h) —'));
|
|
287
|
-
for (const r of hud.recentRejections) {
|
|
288
|
-
lines.push(dim(` ${r.count}× "${r.cls}" on \`${r.cmd}\``));
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
process.stdout.write(lines.join('\n') + '\n');
|
|
230
|
+
// SessionStart contract: emit ONLY the command primer — the verb cheat-sheet
|
|
231
|
+
// that tells the agent which dotmd verbs exist. Everything else hud used to
|
|
232
|
+
// print (held/prompts/stuck/errors state, slash-command refresh notices, and
|
|
233
|
+
// the journal-aware previous-self / fleet / recent-rejections sections) is
|
|
234
|
+
// deliberately suppressed here: those signals nudged agents into phantom
|
|
235
|
+
// follow-up work — e.g. "errors: 1 (run dotmd check)" prompting a check run
|
|
236
|
+
// for state that belongs inside its own command. Each of those signals lives
|
|
237
|
+
// in its proper command (`plans`, `prompts`, `check`) and stays available via
|
|
238
|
+
// `dotmd hud --json` for programmatic callers. The hook's job is purely to
|
|
239
|
+
// teach the verbs, never to report status.
|
|
240
|
+
process.stdout.write(dim('dotmd: plans|briefing set <status> [<file>] new <type> <slug> use [<file>] archive <file> (use [no-arg] → oldest pending prompt)') + '\n');
|
|
293
241
|
}
|
package/src/index-file.mjs
CHANGED
|
@@ -21,6 +21,7 @@ export function renderIndexFile(index, config) {
|
|
|
21
21
|
function renderGeneratedBlock(index, config) {
|
|
22
22
|
const lines = [];
|
|
23
23
|
const indexDir = config.indexPath ? path.dirname(path.relative(config.repoRoot, config.indexPath)).split(path.sep).join('/') : '';
|
|
24
|
+
const snapshotMode = config.indexSnapshot ?? 'status';
|
|
24
25
|
|
|
25
26
|
for (const status of config.statusOrder) {
|
|
26
27
|
const docs = index.docs.filter(doc => doc.status === status);
|
|
@@ -34,10 +35,9 @@ function renderGeneratedBlock(index, config) {
|
|
|
34
35
|
|
|
35
36
|
lines.push(`## ${capitalize(status)}`);
|
|
36
37
|
lines.push('');
|
|
37
|
-
lines.push(
|
|
38
|
-
lines.push('|-----|-----------------|');
|
|
38
|
+
lines.push(...snapshotHeader(snapshotMode));
|
|
39
39
|
for (const doc of docs) {
|
|
40
|
-
const snapshot =
|
|
40
|
+
const snapshot = renderIndexSnapshot(doc, config, snapshotMode);
|
|
41
41
|
const linkPath = indexDir ? path.relative(indexDir, doc.path).split(path.sep).join('/') : doc.path;
|
|
42
42
|
lines.push(`| [${escapeTable(doc.title)}](${linkPath}) | ${escapeTable(snapshot)} |`);
|
|
43
43
|
}
|
|
@@ -76,6 +76,16 @@ function renderArchivedSection(docs, config, status) {
|
|
|
76
76
|
return lines;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function snapshotHeader(snapshotMode) {
|
|
80
|
+
if (snapshotMode === 'state') return ['| Doc | Status Snapshot |', '|-----|-----------------|'];
|
|
81
|
+
return ['| Doc | Status |', '|-----|--------|'];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function renderIndexSnapshot(doc, config, snapshotMode) {
|
|
85
|
+
if (snapshotMode === 'state') return formatSnapshot(doc, config);
|
|
86
|
+
return capitalize(doc.status ?? 'unknown');
|
|
87
|
+
}
|
|
88
|
+
|
|
79
89
|
export function writeIndex(content, config) {
|
|
80
90
|
writeFileSync(config.indexPath, content, 'utf8');
|
|
81
91
|
}
|
package/src/init.mjs
CHANGED
|
@@ -189,6 +189,7 @@ function generateDetectedConfig(scan, rootPath) {
|
|
|
189
189
|
lines.push(` path: '${rootPath}/docs.md',`);
|
|
190
190
|
lines.push(` startMarker: '<!-- GENERATED:dotmd:start -->',`);
|
|
191
191
|
lines.push(` endMarker: '<!-- GENERATED:dotmd:end -->',`);
|
|
192
|
+
lines.push(` snapshot: 'status',`);
|
|
192
193
|
lines.push('};');
|
|
193
194
|
lines.push('');
|
|
194
195
|
|
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;
|