dotmd-cli 0.49.0 → 0.49.2

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 CHANGED
@@ -188,7 +188,7 @@ dotmd actionable List docs with next steps
188
188
  dotmd index [--print] Generate/update docs.md index block
189
189
  dotmd hud Actionable triage (silent when clean — ideal SessionStart hook)
190
190
  dotmd pickup <file> Pick up a plan (in-session + print body)
191
- dotmd release [<file>] Release in-session lease (alias: unpickup)
191
+ dotmd release [<file>] Release in-session lease (aliases: unpickup, finish)
192
192
  dotmd status <file> <status> Transition document status
193
193
  dotmd archive <file> Archive (status + move + update refs)
194
194
  dotmd bulk archive <files> Archive multiple files at once
@@ -335,8 +335,10 @@ Each invocation appends one JSON line:
335
335
  `{ts, sid, pid, argv, exit, ms, v, err?}`. Writes are atomic via
336
336
  `O_APPEND` (entries are well under `PIPE_BUF`), so concurrent sessions
337
337
  interleave cleanly without locking. Lazy rotation to
338
- `.dotmd/journal.jsonl.1` at >5MB or oldest entry >30 days; one backup
339
- retained.
338
+ `.dotmd/journal.jsonl.1` on version change, at >5MB, or when the oldest
339
+ entry is >30 days; one backup retained and pruned after 30 days.
340
+ Version-change rotation keeps agent-facing journal summaries focused on the
341
+ currently installed dotmd.
340
342
 
341
343
  Read it back with `dotmd journal`:
342
344
 
@@ -583,7 +585,9 @@ dotmd status docs/plans/my-plan.md partial # shipped + tail deferred (referenc
583
585
  dotmd status docs/plans/my-plan.md awaiting # stuck on a human decision
584
586
  ```
585
587
 
586
- `finish` is a legacy command that defaults to `status: done`, which is no longer in the default plan vocabulary as of 0.16. Use `archive` (fully shipped) or `release` + `status` (anything else). If you need it back, add `done` to `types.plan.statuses` in your config.
588
+ `finish` is an alias for `release`, kept for older agent instructions that use
589
+ that verb for closeout. To fully close shipped work, archive it. To keep working
590
+ later, release it back to the prior status or use `dotmd set <status> <file>`.
587
591
 
588
592
  ### Session leases & release
589
593
 
@@ -596,8 +600,8 @@ distinct outcomes when a plan is already `in-session`:
596
600
  re-prints the body. No conflict.
597
601
  - **Cross-session conflict.** If another live session holds the plan,
598
602
  pickup refuses with `Held by <host>/<session> (pid <pid>) since <time>`.
599
- - **Stale lease.** If the holder's pid is dead (or the lease is >24h old),
600
- pickup refuses but suggests `--takeover`.
603
+ - **Reclaimable lease.** If the holder's same-host pid is dead, or the lease is
604
+ older than 4 hours, pickup can reclaim it without `--takeover`.
601
605
 
602
606
  Releasing leases (both names work; `release` is the recommended verb):
603
607
 
@@ -605,13 +609,13 @@ Releasing leases (both names work; `release` is the recommended verb):
605
609
  dotmd release # release every lease owned by current session
606
610
  dotmd release docs/plans/foo.md # release that one (refuses cross-session)
607
611
  dotmd release --to planned # override target status (default: lease.oldStatus)
608
- dotmd release --stale # release leases with dead pid or >24h old
612
+ dotmd release --stale # release leases with dead same-host pid or >4h old
609
613
  dotmd release --all # release every lease (administrative)
610
614
  dotmd release --json # { released: [...], skipped: [...] }
611
615
  ```
612
616
 
613
- `finish`, `archive`, and `rename` auto-release / migrate the lease, so the
614
- common closeout paths are covered without ceremony.
617
+ `finish` is the same as `release`. `archive` and `rename` auto-release or
618
+ migrate the lease, so the common closeout paths are covered without ceremony.
615
619
 
616
620
  **Session id resolution** (in order, first wins):
617
621
 
@@ -666,7 +670,7 @@ either is silent.
666
670
 
667
671
  `dotmd check` also catches the symmetric failure mode: a plan whose
668
672
  frontmatter claims `status: in-session` but whose lease either doesn't
669
- exist (last session crashed before releasing) or is stale (>24h since
673
+ exist (last session crashed before releasing) or is stale (>4h since
670
674
  pickup). Each warning names the exact unstuck command
671
675
  (`dotmd release <plan>` or `dotmd status <plan> active`), so plans
672
676
  don't sit stuck in-session indefinitely. Always-on — legit concurrent
package/bin/dotmd.mjs CHANGED
@@ -283,8 +283,10 @@ Reader options:
283
283
  --json Emit selected entries as a JSON array
284
284
 
285
285
  Storage:
286
- Rotates to .dotmd/journal.jsonl.1 at >5MB or oldest entry >30 days.
287
- Single backup retained; older history is dropped on rotation.
286
+ Rotates to .dotmd/journal.jsonl.1 on dotmd version change, at >5MB,
287
+ or when the oldest entry is >30 days.
288
+ Single backup retained for up to 30 days; older history is dropped on
289
+ rotation or pruned after the retention window.
288
290
 
289
291
  Examples:
290
292
  DOTMD_JOURNAL=1 dotmd plans
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.49.0",
3
+ "version": "0.49.2",
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",
package/src/journal.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, appendFileSync, statSync, renameSync, readFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, appendFileSync, statSync, renameSync, readFileSync, unlinkSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  import { currentSessionId } from './lease.mjs';
@@ -8,6 +8,7 @@ const JOURNAL_FILE = 'journal.jsonl';
8
8
  const JOURNAL_BACKUP = 'journal.jsonl.1';
9
9
  const ROTATE_SIZE_BYTES = 5 * 1024 * 1024;
10
10
  const ROTATE_AGE_MS = 30 * 24 * 60 * 60 * 1000;
11
+ const BACKUP_RETENTION_MS = ROTATE_AGE_MS;
11
12
 
12
13
  const ERROR_LOG_FILE = 'dotmd-errors.log';
13
14
  const ERROR_LOG_BACKUP = 'dotmd-errors.log.1';
@@ -26,10 +27,31 @@ export function journalBackupPath(config) {
26
27
  return path.join(config.repoRoot, JOURNAL_DIR, JOURNAL_BACKUP);
27
28
  }
28
29
 
29
- function maybeRotate(file, backup) {
30
+ function firstEntryVersion(file) {
31
+ try {
32
+ const sample = readFileSync(file, 'utf8');
33
+ const nl = sample.indexOf('\n');
34
+ const first = nl >= 0 ? sample.slice(0, nl) : sample;
35
+ if (!first.trim()) return null;
36
+ const obj = JSON.parse(first);
37
+ return obj?.v == null ? null : String(obj.v);
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ function maybeRotate(file, backup, nextEntry = null) {
44
+ pruneStaleBackup(backup);
30
45
  if (!existsSync(file)) return;
31
46
  let st;
32
47
  try { st = statSync(file); } catch { return; }
48
+ if (nextEntry?.v) {
49
+ const existingVersion = firstEntryVersion(file);
50
+ if (existingVersion !== String(nextEntry.v)) {
51
+ try { renameSync(file, backup); } catch {}
52
+ return;
53
+ }
54
+ }
33
55
  if (st.size > ROTATE_SIZE_BYTES) {
34
56
  try { renameSync(file, backup); } catch {}
35
57
  return;
@@ -50,6 +72,16 @@ function maybeRotate(file, backup) {
50
72
  } catch {}
51
73
  }
52
74
 
75
+ function pruneStaleBackup(backup) {
76
+ if (!existsSync(backup)) return;
77
+ try {
78
+ const st = statSync(backup);
79
+ if ((Date.now() - st.mtimeMs) > BACKUP_RETENTION_MS) {
80
+ try { unlinkSync(backup); } catch {}
81
+ }
82
+ } catch {}
83
+ }
84
+
53
85
  export function appendJournalEntry(config, entry) {
54
86
  if (!isJournalEnabled(config)) return;
55
87
  if (!config?.repoRoot) return;
@@ -57,7 +89,7 @@ export function appendJournalEntry(config, entry) {
57
89
  const dir = path.join(config.repoRoot, JOURNAL_DIR);
58
90
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
59
91
  const file = journalFilePath(config);
60
- maybeRotate(file, journalBackupPath(config));
92
+ maybeRotate(file, journalBackupPath(config), entry);
61
93
  // O_APPEND is atomic for writes under PIPE_BUF (4KB on Linux, 512B on
62
94
  // macOS). Entries are well under either threshold, so concurrent CLI
63
95
  // invocations interleave cleanly without locking.
@@ -143,7 +175,7 @@ export function recordGlobalError({ config, startMs, args, err, version }) {
143
175
  const dir = globalErrorLogDir();
144
176
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
145
177
  const file = globalErrorLogPath();
146
- maybeRotate(file, globalErrorLogBackupPath());
178
+ maybeRotate(file, globalErrorLogBackupPath(), entry);
147
179
  appendFileSync(file, JSON.stringify(entry) + '\n', { flag: 'a' });
148
180
  } catch {
149
181
  // Logging must never break exit.
package/src/new.mjs CHANGED
@@ -271,6 +271,16 @@ function readBodyInput(source) {
271
271
  export async function runNew(argv, config, opts = {}) {
272
272
  const { dryRun } = opts;
273
273
 
274
+ const knownTypes = new Set(Object.keys(BUILTIN_TEMPLATES));
275
+ // Also include any custom templates from config
276
+ for (const k of Object.keys(config.raw?.templates ?? {})) knownTypes.add(k);
277
+
278
+ const hasNameForBody = args => {
279
+ if (args.length >= 2 && knownTypes.has(args[0])) return true;
280
+ if (args.length >= 1 && !knownTypes.has(args[0])) return true;
281
+ return false;
282
+ };
283
+
274
284
  // Parse args. Pull out flags first.
275
285
  const positional = [];
276
286
  let status = null;
@@ -296,17 +306,16 @@ export async function runNew(argv, config, opts = {}) {
296
306
  return;
297
307
  }
298
308
  // Treat `-` alone (stdin marker) as a positional, not a flag.
299
- if (!argv[i].startsWith('-') || argv[i] === '-') positional.push(argv[i]);
309
+ // Once the type/name have been collected, a positional body may itself
310
+ // start with `---` frontmatter. Preserve it instead of dropping it as an
311
+ // unknown flag; the leading frontmatter merge below will handle it.
312
+ if (!argv[i].startsWith('-') || argv[i] === '-' || hasNameForBody(positional)) positional.push(argv[i]);
300
313
  }
301
314
 
302
315
  // Resolve type vs name:
303
316
  // `dotmd new plan auth-revamp` → type=plan, name=auth-revamp
304
317
  // `dotmd new auth-revamp` → type=doc (default), name=auth-revamp
305
318
  // `dotmd new prompt foo "body"` → type=prompt, name=foo, bodyArg="body"
306
- const knownTypes = new Set(Object.keys(BUILTIN_TEMPLATES));
307
- // Also include any custom templates from config
308
- for (const k of Object.keys(config.raw?.templates ?? {})) knownTypes.add(k);
309
-
310
319
  let typeName, name, bodyArg = null;
311
320
  if (positional.length >= 1 && knownTypes.has(positional[0])) {
312
321
  typeName = positional[0];
package/src/ship.mjs CHANGED
@@ -20,6 +20,7 @@ const ALLOWLIST_PATTERNS = [
20
20
  /^package(?:-lock)?\.json$/,
21
21
  /^README\.md$/,
22
22
  /^CLAUDE\.md$/,
23
+ /^CHANGELOG\.md$/,
23
24
  /^\.gitignore$/,
24
25
  ];
25
26