dotmd-cli 0.43.0 → 0.45.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/bin/dotmd.mjs +34 -7
- package/package.json +1 -1
- package/src/claude-commands.mjs +28 -30
- package/src/commands.mjs +1 -1
- package/src/hud.mjs +12 -31
- package/src/lifecycle.mjs +13 -7
- package/src/pickup-card.mjs +2 -2
- package/src/prompts.mjs +2 -2
- package/src/use.mjs +46 -0
- package/src/validate.mjs +2 -2
package/bin/dotmd.mjs
CHANGED
|
@@ -14,6 +14,27 @@ const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'),
|
|
|
14
14
|
const HELP = {
|
|
15
15
|
_main: `dotmd v${pkg.version} — frontmatter markdown document manager
|
|
16
16
|
|
|
17
|
+
Common commands:
|
|
18
|
+
plans Live plans (excludes archived)
|
|
19
|
+
briefing Full briefing with plan counts + next steps
|
|
20
|
+
set <status> [file] Transition status (start work, finish, archive — all via target status)
|
|
21
|
+
new <type> <name> Create plan/doc/prompt (pipe stdin or @path for body)
|
|
22
|
+
use [<file>] Open a doc by type: prompt → consume, plan → start, doc → read
|
|
23
|
+
(no file: consume oldest pending prompt)
|
|
24
|
+
archive <file> Close out a plan (status → archived, move, update refs)
|
|
25
|
+
|
|
26
|
+
More help:
|
|
27
|
+
dotmd help all Full command list
|
|
28
|
+
dotmd help statuses Status vocabulary + transitions
|
|
29
|
+
dotmd <cmd> --help Per-command details
|
|
30
|
+
|
|
31
|
+
Global flags: --config <path> --root <name> --type <t,…> --dry-run/-n --verbose --version`,
|
|
32
|
+
|
|
33
|
+
// Full command list — opt-in via \`dotmd help all\`. Kept exhaustive so the
|
|
34
|
+
// top-level \`--help\` can stay terse without losing discoverability. When you
|
|
35
|
+
// add a new command, add it here too.
|
|
36
|
+
'help:all': `dotmd v${pkg.version} — full command list
|
|
37
|
+
|
|
17
38
|
View & Query:
|
|
18
39
|
hud [--json] Two-line actionable triage (held / prompts / stuck) — silent when clean
|
|
19
40
|
list [--verbose] [--json] List docs grouped by status (default command)
|
|
@@ -22,8 +43,8 @@ View & Query:
|
|
|
22
43
|
focus [status] [--json] Detailed view for one status group
|
|
23
44
|
query [filters] [--json] Filtered search (--status, --keyword, --stale, etc.)
|
|
24
45
|
plans Live plans (excludes archived; --include-archived for all)
|
|
25
|
-
|
|
26
|
-
|
|
46
|
+
use [<file>] Open a doc by type: prompt → consume, plan → start, doc → read
|
|
47
|
+
prompts [list|archive|new|shelve] Prompt admin (list / archive / save / shelve). Use \`dotmd use\` to consume.
|
|
27
48
|
stale Stale docs (preset)
|
|
28
49
|
actionable Docs with next steps (preset)
|
|
29
50
|
|
|
@@ -42,17 +63,14 @@ Analyze:
|
|
|
42
63
|
glossary <term> [--list] [--json] Look up domain terms + related docs
|
|
43
64
|
|
|
44
65
|
Validate & Fix:
|
|
45
|
-
check [--fix] [--errors-only] [--json] Validate frontmatter and references
|
|
46
66
|
doctor [--apply] Auto-fix everything: refs, lint, dates, index (preview by default)
|
|
47
67
|
lint [--fix] Check and auto-fix frontmatter issues
|
|
48
68
|
fix-refs [--dry-run] Auto-fix broken reference paths + body links
|
|
49
69
|
|
|
50
70
|
Lifecycle:
|
|
51
|
-
|
|
52
|
-
release [<file>] [--to <s>] Release in-session lease (alias: unpickup)
|
|
71
|
+
set <status> [<file>] Unified transition: start work, change status, close out, archive — all via target status
|
|
53
72
|
runlist <hub> [next] Show or walk an ordered group of plans (see \`dotmd help runlist\`)
|
|
54
73
|
status <file> <status> Transition document status (deprecated; prefer \`set\`)
|
|
55
|
-
set <status> [<file>] Unified transition: archive/release/transition in one verb
|
|
56
74
|
archive <file> Archive (status + move + update refs)
|
|
57
75
|
bulk archive <f1> <f2> ... Archive multiple files at once
|
|
58
76
|
ship [patch|minor|major] Regen + commit + bump in one step (default: patch)
|
|
@@ -1042,7 +1060,7 @@ async function main() {
|
|
|
1042
1060
|
const key = `help:${topic}`;
|
|
1043
1061
|
if (HELP[key]) { process.stdout.write(`${HELP[key]}\n`); return; }
|
|
1044
1062
|
if (HELP[topic]) { process.stdout.write(`${HELP[topic]}\n`); return; }
|
|
1045
|
-
process.stderr.write(`Unknown help topic: ${topic}\n\nAvailable topics: statuses\nPer-command help: dotmd <cmd> --help\n`);
|
|
1063
|
+
process.stderr.write(`Unknown help topic: ${topic}\n\nAvailable topics: all, statuses\nPer-command help: dotmd <cmd> --help\n`);
|
|
1046
1064
|
process.exit(1);
|
|
1047
1065
|
}
|
|
1048
1066
|
process.stdout.write(`${HELP._main}\n`);
|
|
@@ -1162,6 +1180,15 @@ async function main() {
|
|
|
1162
1180
|
await runPrompts(restArgs, config, { dryRun, verbose });
|
|
1163
1181
|
return;
|
|
1164
1182
|
}
|
|
1183
|
+
// Top-level `dotmd use [file]` — the single "start engaging with this doc"
|
|
1184
|
+
// verb. Dispatches by the target doc's type: prompt → consume + archive,
|
|
1185
|
+
// plan → acquire lease + print, doc → print. With no file: consume oldest
|
|
1186
|
+
// pending prompt. See src/use.mjs for the dispatch table.
|
|
1187
|
+
if (command === 'use') {
|
|
1188
|
+
const { runUse } = await import('../src/use.mjs');
|
|
1189
|
+
await runUse(restArgs, config, { dryRun });
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1165
1192
|
|
|
1166
1193
|
// Commands that handle their own index building
|
|
1167
1194
|
if (command === 'diff') { const { runDiff } = await import('../src/diff.mjs'); runDiff(restArgs, config); return; }
|
package/package.json
CHANGED
package/src/claude-commands.mjs
CHANGED
|
@@ -19,9 +19,9 @@ function markerFor(version) { return `<!-- dotmd-generated: ${version} -->`; }
|
|
|
19
19
|
// gets a per-type status vocab appended at generation time so agents arrive
|
|
20
20
|
// with the valid `dotmd status` / `dotmd archive` values already in context.
|
|
21
21
|
const SLASH_DESCRIPTIONS = {
|
|
22
|
-
plans: "dotmd-managed plan briefing for this repo. Use when the user asks what's on the plate, references a plan slug, queues work, or wants to
|
|
22
|
+
plans: "dotmd-managed plan briefing for this repo. Use when the user asks what's on the plate, references a plan slug, queues work, or wants to start / close / archive a plan.",
|
|
23
23
|
docs: "dotmd-managed docs briefing for this repo. Use when the user asks to list, scaffold, query, validate, archive, or rename non-plan docs (reference docs, ADRs, RFCs, design notes), or asks how the dotmd doc lifecycle works here.",
|
|
24
|
-
baton: "Save a resume prompt for the
|
|
24
|
+
baton: "Save a resume prompt for the active plan and close it out — the minimum handoff. Use when the user says hand off / save a resume / wrap up, or when context is getting tight.",
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
const VOCAB_TRUNCATE_AT = 12;
|
|
@@ -63,19 +63,18 @@ function generatePlansCommand(config, version) {
|
|
|
63
63
|
lines.push('');
|
|
64
64
|
lines.push('Plan-specific commands:');
|
|
65
65
|
lines.push('- `dotmd context` — briefing with active/paused/ready plans, age tags, next steps');
|
|
66
|
-
lines.push('- `dotmd
|
|
67
|
-
lines.push('- `dotmd
|
|
68
|
-
lines.push('- `dotmd
|
|
66
|
+
lines.push('- `dotmd set <status> [<file>]` — single status verb. Use this to start, transition, or close any plan:');
|
|
67
|
+
lines.push(' - `dotmd set in-session <file>` — start work on a plan (marks in-session + prints body)');
|
|
68
|
+
lines.push(' - `dotmd set <status> [<file>]` — transition to any other status; closes out the in-session marker automatically');
|
|
69
|
+
lines.push(' - `dotmd set archived <file>` — close out (same as `dotmd archive`)');
|
|
70
|
+
lines.push('- `dotmd archive <file>` — explicit archive with ref-fixing (equivalent to `set archived`)');
|
|
71
|
+
lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
|
|
72
|
+
lines.push('- `dotmd new plan <name>` — scaffold with full phase structure');
|
|
73
|
+
lines.push('- `dotmd new prompt <name>` — save a resume-prompt to docs/prompts/ (pipe stdin or @path for body)');
|
|
74
|
+
lines.push('- `dotmd use` — consume oldest pending prompt (prints body, auto-archives)');
|
|
75
|
+
lines.push('- `dotmd use <file>` — open any doc by type: prompt → consume, plan → start work, doc → read');
|
|
69
76
|
lines.push('- `dotmd unblocks <file>` — what depends on / is blocked by a plan');
|
|
70
77
|
lines.push('- `dotmd actionable` — ready plans with next steps (what to promote)');
|
|
71
|
-
lines.push('- `dotmd new plan <name>` — scaffold with full phase structure');
|
|
72
|
-
lines.push('- `dotmd prompts new <name> "<body>"` — save a resume-prompt to docs/prompts/');
|
|
73
|
-
lines.push('- `dotmd prompts next` — consume oldest pending prompt (prints body, auto-archives)');
|
|
74
|
-
lines.push('- `dotmd prompts use <file>` — consume a specific prompt (prints body, auto-archives)');
|
|
75
|
-
lines.push('- `dotmd archive <file>` — archive with auto ref-fixing (both directions)');
|
|
76
|
-
lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
|
|
77
|
-
lines.push('- `dotmd set <status> [<file>]` — unified transition (archive / release / status bump in one verb; infers path from held lease)');
|
|
78
|
-
lines.push('- `dotmd status <file> <status>` — transition status (legacy; `set` is preferred)');
|
|
79
78
|
lines.push('- `dotmd query --keyword <term>` — find plans by keyword');
|
|
80
79
|
|
|
81
80
|
if (config.raw?.glossary) {
|
|
@@ -85,10 +84,10 @@ function generatePlansCommand(config, version) {
|
|
|
85
84
|
lines.push('');
|
|
86
85
|
lines.push('If the user asks about a specific plan, read its file directly (path is in the briefing or findable via `dotmd query --keyword <term>`).');
|
|
87
86
|
lines.push('');
|
|
88
|
-
lines.push('If the user asks to change a plan\'s status, use `dotmd
|
|
89
|
-
lines.push('If the user asks to archive a plan, use `dotmd archive <file
|
|
87
|
+
lines.push('If the user asks to change a plan\'s status, use `dotmd set <status> <file>`.');
|
|
88
|
+
lines.push('If the user asks to archive a plan, use `dotmd set archived <file>` (or `dotmd archive <file>`).');
|
|
90
89
|
lines.push('');
|
|
91
|
-
lines.push('**Saved prompts (`docs/prompts/*.md`):** if the user references a file under `docs/prompts/` — e.g. "resume via docs/prompts/foo.md", "use this prompt", "load that one" — consume it with `dotmd
|
|
90
|
+
lines.push('**Saved prompts (`docs/prompts/*.md`):** if the user references a file under `docs/prompts/` — e.g. "resume via docs/prompts/foo.md", "use this prompt", "load that one" — consume it with `dotmd use <file>` (atomically prints the body and archives the prompt so it cannot be double-consumed). Do NOT `cat` it, read it with the file-reading tool, or copy its body into chat. To pick the oldest pending prompt without naming a file, run `dotmd use` with no arg.');
|
|
92
91
|
lines.push('');
|
|
93
92
|
|
|
94
93
|
return lines.join('\n');
|
|
@@ -96,17 +95,17 @@ function generatePlansCommand(config, version) {
|
|
|
96
95
|
|
|
97
96
|
function generateBatonCommand(config, version) {
|
|
98
97
|
const lines = [...frontmatterFor('baton', config), markerFor(version), ''];
|
|
99
|
-
lines.push('Wrap this session.
|
|
100
|
-
lines.push('');
|
|
101
|
-
lines.push('1. **Save the resume prompt.** `dotmd new prompt resume-<plan-slug>` with a 10-20 line body via heredoc: the next concrete decision plus any gotchas. NOT a recap of the plan body. The saved prompt IS the handoff — never print it into chat for copy-paste.');
|
|
98
|
+
lines.push('Wrap this session. Two commands:');
|
|
102
99
|
lines.push('');
|
|
103
|
-
lines.push('
|
|
100
|
+
lines.push('1. **Save the resume prompt.** `dotmd new prompt resume-<plan-slug>` — pipe stdin or pass `@path`. 10-20 line body: the next concrete decision plus any gotchas. NOT a recap of the plan body. The saved prompt IS the handoff — never print it into chat for copy-paste.');
|
|
104
101
|
lines.push('');
|
|
105
|
-
lines.push('
|
|
106
|
-
lines.push('-
|
|
107
|
-
lines.push('-
|
|
102
|
+
lines.push('2. **Close out via `dotmd set <status>`.** Pick the status that matches reality:');
|
|
103
|
+
lines.push(' - `dotmd set active <file>` — work continues, return the plan to the active queue');
|
|
104
|
+
lines.push(' - `dotmd set archived <file>` — fully shipped (also: `dotmd archive <file>`)');
|
|
105
|
+
lines.push(' - `dotmd set paused <file>` / `awaiting <file>` / `partial <file>` / `blocked <file>` — when the status really changed');
|
|
106
|
+
lines.push(' `set` clears the in-session marker automatically when transitioning to any other status.');
|
|
108
107
|
lines.push('');
|
|
109
|
-
lines.push('If you don\'t already know which plan you
|
|
108
|
+
lines.push('If you don\'t already know which plan you have in-session: `dotmd hud --json` and read `.owned`. Do NOT use `dotmd plans --status in-session` — that lists every session\'s in-session plans, not just yours.');
|
|
110
109
|
lines.push('');
|
|
111
110
|
lines.push('The next session\'s `dotmd hud` (SessionStart hook) surfaces the pending prompt automatically.');
|
|
112
111
|
lines.push('');
|
|
@@ -131,7 +130,6 @@ function generateDocsCommand(config, version) {
|
|
|
131
130
|
|
|
132
131
|
lines.push('Commands for working with docs:');
|
|
133
132
|
lines.push('- `dotmd context` — LLM-oriented briefing across all types');
|
|
134
|
-
lines.push('- `dotmd check` — validate frontmatter, refs, body links (target: 0 errors)');
|
|
135
133
|
lines.push('- `dotmd doctor` — auto-fix everything in one pass (refs, lint, dates, index)');
|
|
136
134
|
lines.push('- `dotmd query [filters]` — search by status, keyword, module, surface, type, staleness');
|
|
137
135
|
lines.push('- `dotmd health` — plan pipeline, velocity, aging');
|
|
@@ -151,10 +149,10 @@ function generateDocsCommand(config, version) {
|
|
|
151
149
|
lines.push('Lifecycle:');
|
|
152
150
|
lines.push('- `dotmd new plan <name>` — scaffold new plan');
|
|
153
151
|
lines.push('- `dotmd new doc <name>` — scaffold reference doc');
|
|
154
|
-
lines.push('- `dotmd
|
|
155
|
-
lines.push('- `dotmd
|
|
156
|
-
lines.push('- `dotmd
|
|
157
|
-
lines.push('- `dotmd set <status> [<file>]` — unified transition (archive /
|
|
152
|
+
lines.push('- `dotmd new prompt <name>` — save a resume-prompt (pipe stdin or @path for body)');
|
|
153
|
+
lines.push('- `dotmd use` — consume oldest pending prompt (prints body, auto-archives)');
|
|
154
|
+
lines.push('- `dotmd use <file>` — open any doc by type: prompt → consume, plan → start work, doc → read');
|
|
155
|
+
lines.push('- `dotmd set <status> [<file>]` — unified transition (archive / status bump; infers path from your active in-session plan)');
|
|
158
156
|
lines.push('- `dotmd status <file> <status>` — transition status (legacy; `set` is preferred)');
|
|
159
157
|
lines.push('- `dotmd archive <file>` — archive with auto ref-fixing');
|
|
160
158
|
lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
|
|
@@ -163,7 +161,7 @@ function generateDocsCommand(config, version) {
|
|
|
163
161
|
lines.push('- `dotmd fix-refs` — repair broken references and body links');
|
|
164
162
|
lines.push('- `dotmd rename <old> <new>` — rename doc + update all references');
|
|
165
163
|
lines.push('');
|
|
166
|
-
lines.push('**Saved prompts (`docs/prompts/*.md`):** if the user references a file under `docs/prompts/` — e.g. "resume via docs/prompts/foo.md", "use this prompt" — consume it with `dotmd
|
|
164
|
+
lines.push('**Saved prompts (`docs/prompts/*.md`):** if the user references a file under `docs/prompts/` — e.g. "resume via docs/prompts/foo.md", "use this prompt" — consume it with `dotmd use <file>` (prints the body and archives atomically). Do NOT `cat` it or read it with the file-reading tool. To pick the oldest pending prompt without naming a file, run `dotmd use` with no arg.');
|
|
167
165
|
lines.push('');
|
|
168
166
|
|
|
169
167
|
return lines.join('\n');
|
package/src/commands.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// templates points at a real command.
|
|
5
5
|
export const KNOWN_COMMANDS = [
|
|
6
6
|
'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'briefing', 'context', 'hud',
|
|
7
|
-
'focus', 'query', 'plans', 'prompts', 'stale', 'actionable', 'index', 'pickup', 'release', 'status', 'set', 'archive', 'bulk', 'bulk-tag', 'touch', 'doctor',
|
|
7
|
+
'focus', 'query', 'plans', 'prompts', 'stale', 'actionable', 'index', 'pickup', 'release', 'status', 'set', 'use', 'archive', 'bulk', 'bulk-tag', 'touch', 'doctor',
|
|
8
8
|
'unblocks', 'health', 'glossary', 'modules', 'module',
|
|
9
9
|
'fix-refs', 'lint', 'rename', 'migrate', 'notion', 'export', 'summary',
|
|
10
10
|
'watch', 'diff', 'new', 'init', 'completions', 'statuses', 'journal',
|
package/src/hud.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { readLeases, findStaleLeases, currentSessionId
|
|
3
|
+
import { readLeases, findStaleLeases, currentSessionId } from './lease.mjs';
|
|
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 {
|
|
7
|
+
import { dim } from './color.mjs';
|
|
8
8
|
import { buildIndex } from './index.mjs';
|
|
9
9
|
import { refreshStaleSlashCommands } from './claude-commands.mjs';
|
|
10
10
|
|
|
@@ -115,18 +115,15 @@ export function runHud(argv, config) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
const lines = [];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (hud.errors > 0) {
|
|
128
|
-
lines.push(red(`✗ ${hud.errors} validation error${hud.errors === 1 ? '' : 's'} ${dim('(run: dotmd check)')}`));
|
|
129
|
-
}
|
|
118
|
+
|
|
119
|
+
// Always-on command primer. Replaces the prior plan-state / prompts /
|
|
120
|
+
// stuck-leases / validation-errors lines — those signals belong inside
|
|
121
|
+
// their own commands (`plans`, `prompts`), not in the SessionStart hook.
|
|
122
|
+
// hud's job is purely to remind the agent which verbs exist, since that's
|
|
123
|
+
// the one thing the agent reaches for `--help` to recover. Keep it tight:
|
|
124
|
+
// one line, the minimum verb set.
|
|
125
|
+
lines.push(dim('dotmd: plans|briefing set <status> [<file>] new <type> <slug> use [<file>] archive <file> (use [no-arg] → oldest pending prompt)'));
|
|
126
|
+
|
|
130
127
|
if (refreshed.length > 0) {
|
|
131
128
|
const from = refreshed[0].from;
|
|
132
129
|
const to = refreshed[0].to;
|
|
@@ -134,21 +131,5 @@ export function runHud(argv, config) {
|
|
|
134
131
|
lines.push(dim(`↻ slash commands refreshed (v${from} → v${to}): ${names}`));
|
|
135
132
|
}
|
|
136
133
|
|
|
137
|
-
// Teach-once primer for fresh installs. The first session that runs hud in
|
|
138
|
-
// a dotmd-managed repo gets one line explaining what dotmd is and how to
|
|
139
|
-
// hand off — without this, a clean repo emits nothing and Claude has no
|
|
140
|
-
// signal that dotmd is the prompt/handoff machinery here. Gated on a
|
|
141
|
-
// marker under .dotmd/ (already the per-repo state dir, already gitignored
|
|
142
|
-
// by `dotmd init`) so subsequent sessions stay silent.
|
|
143
|
-
const primerMarker = path.join(config.repoRoot, '.dotmd', 'primer-shown');
|
|
144
|
-
if (!existsSync(primerMarker)) {
|
|
145
|
-
lines.push(dim('dotmd: managing this repo\'s docs. Save in one shot: `cat draft.md | dotmd new <type> <slug>` (or `dotmd new <type> <slug> @path`). Types: plan, doc, prompt. `dotmd plans` shows the queue.'));
|
|
146
|
-
try {
|
|
147
|
-
mkdirSync(path.dirname(primerMarker), { recursive: true });
|
|
148
|
-
writeFileSync(primerMarker, '');
|
|
149
|
-
} catch { /* best-effort — failed marker write just means the primer reprints next session */ }
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (lines.length === 0) return; // silent when clean
|
|
153
134
|
process.stdout.write(lines.join('\n') + '\n');
|
|
154
135
|
}
|
package/src/lifecycle.mjs
CHANGED
|
@@ -115,7 +115,7 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
115
115
|
let newStatus = argv[1];
|
|
116
116
|
|
|
117
117
|
if (!opts.suppressDeprecation) {
|
|
118
|
-
process.stderr.write(dim('`dotmd status <file> <status>` is deprecated; prefer `dotmd set <status> [<file>]` (note: <status> first
|
|
118
|
+
process.stderr.write(dim('`dotmd status <file> <status>` is deprecated; prefer `dotmd set <status> [<file>]` (note: <status> first; <file> optional when a plan is in-session). Removed in a future major.\n'));
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
if (!input) { die('Usage: dotmd status <file> <new-status>'); }
|
|
@@ -303,11 +303,11 @@ export async function runPickup(argv, config, opts = {}) {
|
|
|
303
303
|
const pickupable = new Set(['active', 'planned', 'in-session']);
|
|
304
304
|
if (oldStatus && !pickupable.has(oldStatus)) {
|
|
305
305
|
die(
|
|
306
|
-
`Cannot
|
|
306
|
+
`Cannot start work on a plan with status '${oldStatus}'. Must be active or planned.\n` +
|
|
307
307
|
` ${repoPath}\n` +
|
|
308
308
|
`\n` +
|
|
309
309
|
`Recover with:\n` +
|
|
310
|
-
` dotmd
|
|
310
|
+
` dotmd set active ${repoPath} && dotmd set in-session ${repoPath}`,
|
|
311
311
|
);
|
|
312
312
|
}
|
|
313
313
|
|
|
@@ -326,11 +326,11 @@ export async function runPickup(argv, config, opts = {}) {
|
|
|
326
326
|
leaseOutcome = result.outcome;
|
|
327
327
|
if (result.outcome === 'conflict-alive') {
|
|
328
328
|
const c = result.conflict;
|
|
329
|
-
die(`
|
|
329
|
+
die(`Plan is in-session with ${c.host}/${c.session} (pid ${c.pid}) since ${c.pickedUpAt}.\nUse --takeover to override.\n ${repoPath}`);
|
|
330
330
|
}
|
|
331
331
|
if (result.outcome === 'conflict-stale') {
|
|
332
332
|
const c = result.conflict;
|
|
333
|
-
die(`
|
|
333
|
+
die(`Plan flagged in-session by ${c.host}/${c.session} since ${c.pickedUpAt} (>${STALE_LEASE_AGE_HOURS}h ago, looks abandoned).\nUse --takeover to claim.\n ${repoPath}`);
|
|
334
334
|
}
|
|
335
335
|
if (oldStatus !== 'in-session') {
|
|
336
336
|
updateFrontmatter(filePath, { status: 'in-session', updated: today });
|
|
@@ -370,7 +370,7 @@ export async function runPickup(argv, config, opts = {}) {
|
|
|
370
370
|
process.stderr.write(`${green('▶ Picked up')}: ${repoPath} (${oldStatus ?? 'unset'} → in-session)\n\n`);
|
|
371
371
|
}
|
|
372
372
|
if (fullBody) {
|
|
373
|
-
const header = `[dotmd]
|
|
373
|
+
const header = `[dotmd] in-session: ${repoPath} — close with: dotmd set <status> ${repoPath}\n---\n`;
|
|
374
374
|
process.stdout.write(header);
|
|
375
375
|
const content = (body ?? '').trim();
|
|
376
376
|
if (content) process.stdout.write(content + '\n');
|
|
@@ -665,8 +665,14 @@ export async function runSet(argv, config, opts = {}) {
|
|
|
665
665
|
|
|
666
666
|
if (!newStatus) die('Usage: dotmd set <status> [<path>]');
|
|
667
667
|
|
|
668
|
+
// `set in-session` acquires a lease + prints the plan body (same as the
|
|
669
|
+
// legacy `pickup` verb). Delegated so `set` is the single status verb.
|
|
668
670
|
if (newStatus === 'in-session') {
|
|
669
|
-
|
|
671
|
+
const pickArgs = input ? [input] : [];
|
|
672
|
+
if (argv.includes('--takeover')) pickArgs.push('--takeover');
|
|
673
|
+
if (noIndex) pickArgs.push('--no-index');
|
|
674
|
+
if (showFiles) pickArgs.push('--show-files');
|
|
675
|
+
return runPickup(pickArgs, config, { dryRun });
|
|
670
676
|
}
|
|
671
677
|
|
|
672
678
|
if (!input) {
|
package/src/pickup-card.mjs
CHANGED
|
@@ -194,7 +194,7 @@ export function buildCard(filePath, raw, config) {
|
|
|
194
194
|
// Render the card to human-format string.
|
|
195
195
|
export function renderCard(card) {
|
|
196
196
|
const lines = [];
|
|
197
|
-
lines.push(`[dotmd]
|
|
197
|
+
lines.push(`[dotmd] in-session: ${card.path} — close with: dotmd set <status> ${card.path}`);
|
|
198
198
|
lines.push('---');
|
|
199
199
|
lines.push(`# ${card.title}`);
|
|
200
200
|
const meta = [card.status, card.updated && `updated ${card.updated}`].filter(Boolean).join(' · ');
|
|
@@ -252,7 +252,7 @@ export function renderCard(card) {
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
lines.push('');
|
|
255
|
-
lines.push(dim(`Body: ${formatBytes(card.bodyBytes)}. \`dotmd
|
|
255
|
+
lines.push(dim(`Body: ${formatBytes(card.bodyBytes)}. \`dotmd set in-session ${card.path} --full\` for everything, or Read the file with offset/limit to target a section by line number.`));
|
|
256
256
|
return lines.join('\n') + '\n';
|
|
257
257
|
}
|
|
258
258
|
|
package/src/prompts.mjs
CHANGED
|
@@ -118,7 +118,7 @@ function renderPromptsVerbose(index, config, { hasStatusFlag, includeArchived })
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
function pendingPromptsOldestFirst(config) {
|
|
121
|
+
export function pendingPromptsOldestFirst(config) {
|
|
122
122
|
const index = buildIndex(config);
|
|
123
123
|
const prompts = index.docs.filter(d => d.type === 'prompt' && d.status === 'pending');
|
|
124
124
|
|
|
@@ -192,7 +192,7 @@ function runPromptsUse(argv, config, opts = {}) {
|
|
|
192
192
|
consumePrompt(filePath, config, { ...opts, noIndex, showFiles });
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
function consumePrompt(filePath, config, opts) {
|
|
195
|
+
export function consumePrompt(filePath, config, opts) {
|
|
196
196
|
const { dryRun, noIndex, showFiles } = opts;
|
|
197
197
|
const raw = readFileSync(filePath, 'utf8');
|
|
198
198
|
const { frontmatter, body } = extractFrontmatter(raw);
|
package/src/use.mjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
3
|
+
import { asString, die, resolveDocPath, toRepoPath } from './util.mjs';
|
|
4
|
+
import { consumePrompt, pendingPromptsOldestFirst } from './prompts.mjs';
|
|
5
|
+
import { runPickup } from './lifecycle.mjs';
|
|
6
|
+
|
|
7
|
+
// Top-level `dotmd use [file]` — the single "start engaging with this doc"
|
|
8
|
+
// verb. Dispatches by the target doc's `type:` so agents don't have to know
|
|
9
|
+
// the verb-per-type rule:
|
|
10
|
+
// - prompt → print body + archive (one-shot consume)
|
|
11
|
+
// - plan → acquire lease + print body (same as `set in-session`)
|
|
12
|
+
// - doc → print body (read-only)
|
|
13
|
+
// With no argument, consumes the oldest pending prompt.
|
|
14
|
+
export async function runUse(argv, config, opts = {}) {
|
|
15
|
+
const positional = argv.find(a => !a.startsWith('-'));
|
|
16
|
+
|
|
17
|
+
if (!positional) {
|
|
18
|
+
const queue = pendingPromptsOldestFirst(config);
|
|
19
|
+
if (queue.length === 0) die('No pending prompts. Pass a file to use a plan or doc.');
|
|
20
|
+
const head = queue[0];
|
|
21
|
+
if (!head.abs) die(`Could not resolve path: ${head.doc.path}`);
|
|
22
|
+
return consumePrompt(head.abs, config, opts);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const filePath = resolveDocPath(positional, config);
|
|
26
|
+
if (!filePath) die(`File not found: ${positional}`);
|
|
27
|
+
|
|
28
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
29
|
+
const { frontmatter } = extractFrontmatter(raw);
|
|
30
|
+
const parsed = parseSimpleFrontmatter(frontmatter);
|
|
31
|
+
const type = asString(parsed.type);
|
|
32
|
+
|
|
33
|
+
if (type === 'prompt') {
|
|
34
|
+
return consumePrompt(filePath, config, opts);
|
|
35
|
+
}
|
|
36
|
+
if (type === 'plan') {
|
|
37
|
+
// Delegate to the lease-acquisition path. Same flow as `set in-session`.
|
|
38
|
+
return runPickup([filePath, ...argv.filter(a => a !== positional)], config, opts);
|
|
39
|
+
}
|
|
40
|
+
// Anything else (doc, untyped, custom): print the body. The frontmatter
|
|
41
|
+
// already names everything an agent needs to know about lifecycle for that
|
|
42
|
+
// type, so the verb stays a no-op read for non-actionable types.
|
|
43
|
+
process.stdout.write(raw);
|
|
44
|
+
if (!raw.endsWith('\n')) process.stdout.write('\n');
|
|
45
|
+
process.stderr.write(`${toRepoPath(filePath, config.repoRoot)} (${type ?? 'untyped'})\n`);
|
|
46
|
+
}
|
package/src/validate.mjs
CHANGED
|
@@ -186,14 +186,14 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
|
|
|
186
186
|
doc.warnings.push({
|
|
187
187
|
path: doc.path,
|
|
188
188
|
level: 'warning',
|
|
189
|
-
message: `\`status: in-session\` but no
|
|
189
|
+
message: `\`status: in-session\` but no session is actually working on this (previous session may have crashed). Run \`dotmd set active ${doc.path}\` to clear and re-queue.`,
|
|
190
190
|
});
|
|
191
191
|
} else if (isLeaseStale(lease)) {
|
|
192
192
|
const ageHours = Math.floor((Date.now() - new Date(lease.pickedUpAt).getTime()) / (1000 * 60 * 60));
|
|
193
193
|
doc.warnings.push({
|
|
194
194
|
path: doc.path,
|
|
195
195
|
level: 'warning',
|
|
196
|
-
message: `\`status: in-session\` but
|
|
196
|
+
message: `\`status: in-session\` but last activity was ${ageHours}h ago (>${STALE_LEASE_AGE_HOURS}h, looks abandoned). Run \`dotmd set active ${doc.path}\` to clear and re-queue.`,
|
|
197
197
|
});
|
|
198
198
|
}
|
|
199
199
|
}
|