dotmd-cli 0.68.0 → 0.70.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.
Files changed (53) hide show
  1. package/README.md +144 -964
  2. package/bin/dotmd.mjs +241 -197
  3. package/dotmd.config.example.mjs +5 -8
  4. package/package.json +6 -10
  5. package/src/agent-context.mjs +132 -0
  6. package/src/atomic-mutation.mjs +1505 -0
  7. package/src/baton.mjs +109 -87
  8. package/src/bulk-tag.mjs +7 -7
  9. package/src/commands.mjs +326 -12
  10. package/src/completions.mjs +38 -98
  11. package/src/config.mjs +18 -3
  12. package/src/diff.mjs +7 -3
  13. package/src/doctor.mjs +12 -5
  14. package/src/export.mjs +154 -25
  15. package/src/fix-refs.mjs +2 -0
  16. package/src/frontmatter-fix.mjs +2 -0
  17. package/src/frontmatter.mjs +3 -2
  18. package/src/git.mjs +531 -14
  19. package/src/graph.mjs +53 -25
  20. package/src/guard.mjs +163 -60
  21. package/src/hud.mjs +65 -76
  22. package/src/index-file.mjs +28 -16
  23. package/src/index.mjs +17 -12
  24. package/src/init.mjs +1 -1
  25. package/src/journal.mjs +145 -12
  26. package/src/lifecycle.mjs +554 -282
  27. package/src/lint.mjs +57 -9
  28. package/src/managed-path.mjs +192 -0
  29. package/src/migrate-prompts.mjs +2 -0
  30. package/src/migrate-template.mjs +2 -0
  31. package/src/migrate.mjs +7 -1
  32. package/src/new.mjs +135 -54
  33. package/src/output-identity.mjs +106 -0
  34. package/src/pickup-card.mjs +24 -10
  35. package/src/pickup.mjs +457 -0
  36. package/src/prompts.mjs +138 -32
  37. package/src/query.mjs +22 -10
  38. package/src/reference-planner.mjs +292 -0
  39. package/src/rename.mjs +65 -73
  40. package/src/render.mjs +17 -8
  41. package/src/runlist.mjs +109 -71
  42. package/src/section.mjs +2 -1
  43. package/src/ship.mjs +39 -20
  44. package/src/stats.mjs +1 -1
  45. package/src/status-metadata.mjs +87 -0
  46. package/src/statuses.mjs +11 -26
  47. package/src/summary.mjs +14 -3
  48. package/src/update.mjs +38 -10
  49. package/src/use.mjs +4 -1
  50. package/src/util.mjs +1 -0
  51. package/src/validate.mjs +14 -6
  52. package/src/watch.mjs +6 -1
  53. package/src/notion.mjs +0 -528
@@ -250,13 +250,6 @@ export const presets = {
250
250
  // // },
251
251
  // };
252
252
 
253
- // ─── Notion ──────────────────────────────────────────────────────────────────
254
- // IMPORTANT: Use environment variables for tokens — never hardcode secrets in config files.
255
- // export const notion = {
256
- // token: process.env.NOTION_TOKEN,
257
- // database: process.env.NOTION_DATABASE_ID,
258
- // };
259
-
260
253
  // ─── Function Hooks ──────────────────────────────────────────────────────────
261
254
  // Hooks are optional. Each receives a default implementation it can wrap or replace.
262
255
 
@@ -290,7 +283,11 @@ export const presets = {
290
283
  // export function onNew({ path, status, title, template }) {}
291
284
  // export function onRename({ oldPath, newPath, referencesUpdated }) {}
292
285
  // export function onLint({ path, fixes }) {}
293
- // export function onPickup({ path, oldStatus, newStatus }) {}
286
+ // Pickup hooks are delivered at least once. A retry reuses operationId, so any
287
+ // external side effect must deduplicate by that stable ID. Delivery uses a
288
+ // persisted process-identity lease; live/unverifiable owners are not reclaimed
289
+ // by age, and an expired lease retries only when its owner is demonstrably dead.
290
+ // export function onPickup({ path, oldStatus, newStatus, operationId }) {}
294
291
 
295
292
  // AI hooks — override summarization (replaces local MLX model).
296
293
  // export function summarizeDoc(body, meta) { return 'Custom summary'; }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.68.0",
4
- "description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, Notion sync, AI summaries.",
3
+ "version": "0.70.0",
4
+ "description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, lifecycle, and AI summaries.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -26,7 +26,6 @@
26
26
  "rfc",
27
27
  "document-management",
28
28
  "yaml",
29
- "notion",
30
29
  "graph",
31
30
  "export",
32
31
  "ai-summary",
@@ -41,15 +40,12 @@
41
40
  "scripts": {
42
41
  "test": "node --test test/*.test.mjs",
43
42
  "postinstall": "node scripts/postinstall.mjs",
44
- "preversion": "npm test",
45
- "version": "node bin/dotmd.mjs hud >/dev/null 2>&1; node scripts/sync-plugin-version.mjs; git add .claude/commands docs/docs.md plugins .claude-plugin 2>/dev/null; true",
46
- "postversion": "bash scripts/postversion.sh"
43
+ "preversion": "node scripts/release-preflight.mjs && npm test",
44
+ "version": "node scripts/prepare-version-commit.mjs",
45
+ "postversion": "bash scripts/postversion.sh",
46
+ "release:resume": "node scripts/recover-local-release.mjs && bash scripts/postversion.sh"
47
47
  },
48
48
  "engines": {
49
49
  "node": ">=20"
50
- },
51
- "dependencies": {
52
- "@notionhq/client": "^5.13.0",
53
- "notion-to-md": "^3.1.9"
54
50
  }
55
51
  }
@@ -0,0 +1,132 @@
1
+ import { isArchivedPath, truncate } from './util.mjs';
2
+ import { isCoordinationHub } from './runlist.mjs';
3
+ import {
4
+ actionablePromptStatuses,
5
+ comparePromptDocs,
6
+ compareStrings,
7
+ resolveStatusMetadata,
8
+ statusMetadataFor,
9
+ } from './status-metadata.mjs';
10
+
11
+ export const AGENT_CONTEXT_SCHEMA = Object.freeze({ name: 'dotmd.agent-context', version: 1 });
12
+
13
+ const LIMITS = Object.freeze({ vocabulary: 32, prompts: 8, plans: 12, hubs: 8, issues: 10 });
14
+
15
+ export function boundedCollection(items, limit) {
16
+ const selected = items.slice(0, limit);
17
+ return {
18
+ total: items.length,
19
+ shown: selected.length,
20
+ truncated: selected.length < items.length,
21
+ items: selected,
22
+ };
23
+ }
24
+
25
+ function compactDoc(doc) {
26
+ const blockers = (doc.blockers ?? []).map(item => truncate(String(item), 160));
27
+ return {
28
+ path: doc.path,
29
+ title: truncate(doc.title ?? '', 160),
30
+ status: doc.status,
31
+ type: doc.type,
32
+ nextStep: doc.nextStep ? truncate(doc.nextStep, 300) : null,
33
+ blockers: boundedCollection(blockers, 3),
34
+ daysSinceUpdate: doc.daysSinceUpdate ?? null,
35
+ };
36
+ }
37
+
38
+ function rankMap(metadata, type) {
39
+ return new Map((metadata.byType[type] ?? []).map(item => [item.name, item.rank]));
40
+ }
41
+
42
+ function compareByStatusAndPath(ranks) {
43
+ return (a, b) => (ranks.get(a.status) ?? Number.MAX_SAFE_INTEGER) - (ranks.get(b.status) ?? Number.MAX_SAFE_INTEGER)
44
+ || compareStrings(a.path, b.path);
45
+ }
46
+
47
+ function issueItem(issue) {
48
+ return { path: issue.path ?? null, message: truncate(issue.message ?? String(issue), 300) };
49
+ }
50
+
51
+ export function buildAgentContext(index, config, options = {}) {
52
+ const metadata = resolveStatusMetadata(config);
53
+ const planRanks = rankMap(metadata, 'plan');
54
+ const closed = (doc) => isArchivedPath(doc.path, config)
55
+ || config.lifecycle.archiveStatuses.has(doc.status)
56
+ || config.lifecycle.terminalStatuses.has(doc.status);
57
+ const livePlans = index.docs.filter(doc => doc.type === 'plan' && !closed(doc));
58
+ const hubs = livePlans.filter(isCoordinationHub).sort((a, b) => compareStrings(a.path, b.path));
59
+ const leaves = livePlans.filter(doc => !isCoordinationHub(doc));
60
+ const focus = leaves
61
+ .filter(doc => statusMetadataFor(config, 'plan', doc.status)?.context === 'expanded')
62
+ .sort(compareByStatusAndPath(planRanks));
63
+ const listed = leaves
64
+ .filter(doc => statusMetadataFor(config, 'plan', doc.status)?.context === 'listed')
65
+ .sort(compareByStatusAndPath(planRanks));
66
+ const stale = leaves
67
+ .filter(doc => doc.isStale && !statusMetadataFor(config, 'plan', doc.status)?.skipStale)
68
+ .sort((a, b) => (b.daysSinceUpdate ?? -1) - (a.daysSinceUpdate ?? -1)
69
+ || compareByStatusAndPath(planRanks)(a, b));
70
+
71
+ const promptStatuses = actionablePromptStatuses(config);
72
+ const prompts = index.docs
73
+ .filter(doc => doc.type === 'prompt' && promptStatuses.has(doc.status) && !closed(doc))
74
+ .sort(comparePromptDocs);
75
+
76
+ const vocabulary = {};
77
+ const vocabularyTypes = options.types?.length
78
+ ? metadata.typeOrder.filter(type => options.types.includes(type))
79
+ : metadata.typeOrder;
80
+ for (const type of vocabularyTypes) {
81
+ vocabulary[type] = boundedCollection(
82
+ (metadata.byType[type] ?? []).map(item => ({
83
+ name: item.name,
84
+ context: item.context,
85
+ staleDays: item.staleDays,
86
+ startable: item.startable,
87
+ terminal: item.terminal,
88
+ archive: item.archive,
89
+ })),
90
+ LIMITS.vocabulary,
91
+ );
92
+ }
93
+
94
+ const issueSort = (a, b) => compareStrings(a.path ?? '', b.path ?? '')
95
+ || compareStrings(a.message ?? '', b.message ?? '');
96
+ const errors = [...index.errors].sort(issueSort).map(issueItem);
97
+ const warnings = [...index.warnings].sort(issueSort).map(issueItem);
98
+
99
+ return {
100
+ schema: AGENT_CONTEXT_SCHEMA,
101
+ generatedAt: options.generatedAt ?? new Date().toISOString(),
102
+ scope: {
103
+ roots: options.roots?.length ? [...options.roots] : null,
104
+ types: options.types?.length ? [...options.types] : null,
105
+ },
106
+ statusVocabulary: vocabulary,
107
+ counts: {
108
+ documents: index.docs.length,
109
+ byStatus: { ...index.countsByStatus },
110
+ byType: Object.fromEntries(Object.entries(index.countsByType).map(([type, counts]) => [type, { ...counts }])),
111
+ errors: index.errors.length,
112
+ warnings: index.warnings.length,
113
+ },
114
+ prompts: {
115
+ actionable: boundedCollection(prompts.map(compactDoc), LIMITS.prompts),
116
+ next: prompts[0] ? compactDoc(prompts[0]) : null,
117
+ },
118
+ plans: {
119
+ focus: boundedCollection(focus.map(compactDoc), LIMITS.plans),
120
+ listed: boundedCollection(listed.map(compactDoc), LIMITS.plans),
121
+ stale: boundedCollection(stale.map(compactDoc), LIMITS.plans),
122
+ hubs: boundedCollection(hubs.map(compactDoc), LIMITS.hubs),
123
+ },
124
+ issues: {
125
+ errors: boundedCollection(errors, LIMITS.issues),
126
+ warnings: boundedCollection(warnings, LIMITS.issues),
127
+ },
128
+ ...(options.skippedHooks?.length ? {
129
+ validationPreview: { status: 'built-in-only', skippedHooks: [...options.skippedHooks] },
130
+ } : {}),
131
+ };
132
+ }