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 +14 -10
- package/bin/dotmd.mjs +4 -2
- package/package.json +1 -1
- package/src/journal.mjs +36 -4
- package/src/new.mjs +14 -5
- package/src/ship.mjs +1 -0
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 (
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
600
|
-
pickup
|
|
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 >
|
|
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
|
|
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 (>
|
|
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
|
|
287
|
-
|
|
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
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
|
|
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
|
-
|
|
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];
|