dotmd-cli 0.69.0 → 0.70.1
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 +144 -964
- package/bin/dotmd.mjs +251 -202
- package/dotmd.config.example.mjs +5 -8
- package/package.json +6 -10
- package/src/agent-context.mjs +132 -0
- package/src/atomic-mutation.mjs +1505 -0
- package/src/baton.mjs +109 -114
- package/src/bulk-tag.mjs +7 -7
- package/src/check-collapse.mjs +2 -2
- package/src/commands.mjs +326 -12
- package/src/completions.mjs +38 -98
- package/src/config.mjs +18 -3
- package/src/diff.mjs +7 -3
- package/src/doctor.mjs +25 -15
- package/src/export.mjs +154 -25
- package/src/fix-refs.mjs +2 -0
- package/src/frontmatter-fix.mjs +9 -7
- package/src/frontmatter.mjs +3 -2
- package/src/git.mjs +722 -14
- package/src/graph.mjs +53 -25
- package/src/guard.mjs +163 -60
- package/src/hud.mjs +65 -76
- package/src/index-file.mjs +28 -16
- package/src/index.mjs +21 -13
- package/src/init.mjs +1 -1
- package/src/journal.mjs +145 -12
- package/src/lifecycle.mjs +596 -294
- package/src/lint.mjs +117 -56
- package/src/managed-path.mjs +192 -0
- package/src/migrate-prompts.mjs +2 -0
- package/src/migrate-template.mjs +2 -0
- package/src/migrate.mjs +7 -1
- package/src/new.mjs +135 -54
- package/src/output-identity.mjs +106 -0
- package/src/pickup-card.mjs +24 -10
- package/src/pickup.mjs +457 -0
- package/src/prompts.mjs +134 -75
- package/src/query.mjs +22 -10
- package/src/reference-planner.mjs +292 -0
- package/src/rename.mjs +65 -73
- package/src/render.mjs +24 -11
- package/src/runlist.mjs +109 -71
- package/src/section.mjs +2 -1
- package/src/ship.mjs +39 -20
- package/src/stats.mjs +1 -1
- package/src/status-metadata.mjs +87 -0
- package/src/statuses.mjs +11 -26
- package/src/summary.mjs +14 -3
- package/src/update.mjs +38 -10
- package/src/use.mjs +4 -1
- package/src/util.mjs +1 -0
- package/src/validate.mjs +53 -17
- package/src/watch.mjs +6 -1
- package/src/notion.mjs +0 -528
package/bin/dotmd.mjs
CHANGED
|
@@ -5,63 +5,36 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { resolveConfig } from '../src/config.mjs';
|
|
7
7
|
import { die, warn, levenshtein, isArchivedPath, toRepoPath } from '../src/util.mjs';
|
|
8
|
-
import { recordCliInvocation, recordGlobalError } from '../src/journal.mjs';
|
|
8
|
+
import { recordCliInvocation, recordGlobalError, sanitizeTelemetryArgv } from '../src/journal.mjs';
|
|
9
9
|
import { findRepeatFailureHint } from '../src/hints.mjs';
|
|
10
|
+
import {
|
|
11
|
+
KNOWN_COMMANDS,
|
|
12
|
+
canonicalCommand,
|
|
13
|
+
commandOwnsOption,
|
|
14
|
+
commandPolicy,
|
|
15
|
+
commandUsage,
|
|
16
|
+
validateCommandArgs,
|
|
17
|
+
} from '../src/commands.mjs';
|
|
10
18
|
|
|
11
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
20
|
const __dirname = path.dirname(__filename);
|
|
13
21
|
const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
14
22
|
|
|
15
|
-
const QUERY_FLAGS = new Set([
|
|
16
|
-
'--type', '--status', '--keyword', '--body', '--owner', '--surface', '--module',
|
|
17
|
-
'--domain', '--audience', '--execution-mode', '--updated-since', '--limit',
|
|
18
|
-
'--sort', '--group', '--all', '--include-archived', '--exclude-archived',
|
|
19
|
-
'--stale', '--has-next-step', '--has-blockers', '--checklist-open', '--json',
|
|
20
|
-
'--git', '--summarize', '--summarize-limit', '--model',
|
|
21
|
-
]);
|
|
22
23
|
const QUERY_VALUE_FLAGS = new Set([
|
|
23
24
|
'--type', '--status', '--keyword', '--owner', '--surface', '--module',
|
|
24
25
|
'--domain', '--audience', '--execution-mode', '--updated-since', '--limit',
|
|
25
26
|
'--sort', '--group', '--summarize-limit', '--model',
|
|
26
27
|
]);
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
briefing: { flags: new Set(['--json']), values: new Set() },
|
|
36
|
-
context: { flags: new Set(['--json', '--compact', '--summarize', '--model']), values: new Set(['--model']) },
|
|
37
|
-
'agent-context': { flags: new Set(['--json']), values: new Set() },
|
|
38
|
-
hud: { flags: new Set(['--json', '--subagent']), values: new Set() },
|
|
39
|
-
// '-' is the stdin marker (a positional, not a flag) — listed so validation lets it through.
|
|
40
|
-
baton: { flags: new Set(['--status', '--note', '--body', '--message', '--dry-run', '-n', '-']), values: new Set(['--status', '--note', '--body', '--message']) },
|
|
41
|
-
guard: { flags: new Set(), values: new Set() },
|
|
42
|
-
misuse: { flags: new Set(['--json', '--tail', '--by-rule', '--repo']), values: new Set(['--tail', '--repo']) },
|
|
43
|
-
update: { flags: new Set(['--check', '--cli-only', '--plugin-only']), values: new Set() },
|
|
44
|
-
check: { flags: new Set(['--fix', '--errors-only', '--no-collapse', '--json', '--verbose']), values: new Set() },
|
|
45
|
-
doctor: { flags: new Set(['--apply', '--yes', '--dry-run', '-n', '--statuses', '--migrate-template', '--migrate-prompts', '--frontmatter-fix', '--project', '--json', '--include-archived']), values: new Set() },
|
|
46
|
-
runlist: { flags: new Set(['--json', '--full', '--no-index', '--show-files', '--clear-parent', '--before', '--after']), values: new Set(['--before', '--after']), subcommands: new Set(['next', 'add', 'remove', 'reorder']) },
|
|
47
|
-
runlists: { flags: new Set(['--json', '--limit', '--sort']), values: new Set(['--limit', '--sort']) },
|
|
48
|
-
prompts: {
|
|
49
|
-
flags: new Set(['--json', '--status', '--include-archived', '--sort', '--limit', '--all', '--no-index', '--show-files', '--body', '--message', '--title']),
|
|
50
|
-
values: new Set(['--status', '--sort', '--limit', '--body', '--message', '--title']),
|
|
51
|
-
subcommands: new Set(['list', 'next', 'use', 'resume', 'show', 'peek', 'archive', 'new', 'hold', 'unhold', 'shelve', 'unshelve', 'status']),
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
function validateKnownFlags(command, argv, config) {
|
|
56
|
-
const spec = FLAG_SPECS[command] ?? (config?.presets?.[command] ? { flags: QUERY_FLAGS, values: QUERY_VALUE_FLAGS } : null);
|
|
57
|
-
if (!spec) return;
|
|
58
|
-
for (let i = 0; i < argv.length; i++) {
|
|
59
|
-
const arg = argv[i];
|
|
60
|
-
if (spec.subcommands?.has(arg)) continue;
|
|
61
|
-
if (!arg.startsWith('-')) continue;
|
|
62
|
-
if (!spec.flags.has(arg)) die(`Unknown flag for \`dotmd ${command}\`: ${arg}`);
|
|
63
|
-
if (spec.values.has(arg)) i += 1;
|
|
29
|
+
function requireCommandPolicy(command, policy) {
|
|
30
|
+
if (policy) return policy;
|
|
31
|
+
const matches = KNOWN_COMMANDS
|
|
32
|
+
.map(cmd => ({ cmd, dist: levenshtein(command, cmd) }))
|
|
33
|
+
.sort((a, b) => a.dist - b.dist);
|
|
34
|
+
if (matches[0] && matches[0].dist <= 3) {
|
|
35
|
+
die(`Unknown command: ${command}\n\nDid you mean \`dotmd ${matches[0].cmd}\`?`);
|
|
64
36
|
}
|
|
37
|
+
die(`Unknown command: ${command}\n\nRun \`dotmd --help\` for available commands.`);
|
|
65
38
|
}
|
|
66
39
|
|
|
67
40
|
function resolveExistingPath(input, config) {
|
|
@@ -227,7 +200,7 @@ Analyze:
|
|
|
227
200
|
glossary <term> [--list] [--json] Look up domain terms + related docs
|
|
228
201
|
|
|
229
202
|
Validate & Fix:
|
|
230
|
-
doctor [--apply] Auto-fix everything: refs, lint, dates, index (preview by default)
|
|
203
|
+
doctor [--apply] Auto-fix everything: refs, lint, long fields, dates, index (preview by default)
|
|
231
204
|
self-check Project/version skew diagnostic (alias: doctor --project)
|
|
232
205
|
lint [--fix] Check and auto-fix frontmatter issues
|
|
233
206
|
fix-refs [--dry-run] Auto-fix broken reference paths + body links
|
|
@@ -245,7 +218,7 @@ Lifecycle:
|
|
|
245
218
|
ship [patch|minor|major] Regen + commit + bump in one step (default: patch)
|
|
246
219
|
bulk-tag [files...] Tag pre-existing untagged .md files
|
|
247
220
|
touch <file> Bump updated date
|
|
248
|
-
touch --git
|
|
221
|
+
touch --git [<file>...] Sync dates from substantive git history
|
|
249
222
|
rename <old> <new> Rename doc and update all references
|
|
250
223
|
migrate <field> <old> <new> [f...]Batch update a frontmatter field value (optional file filter)
|
|
251
224
|
|
|
@@ -253,7 +226,6 @@ Create & Export:
|
|
|
253
226
|
new <type> <name> [body] Create doc of given type (plan, doc, prompt)
|
|
254
227
|
index [--print] Generate/update docs.md index block
|
|
255
228
|
export [--format md|html|json] Export docs as markdown, HTML, or JSON
|
|
256
|
-
notion import|export|sync [db-id] Notion database integration
|
|
257
229
|
|
|
258
230
|
Setup:
|
|
259
231
|
init Create starter config + docs directory
|
|
@@ -460,8 +432,8 @@ Bundles the release steps into a single command:
|
|
|
460
432
|
1. Auto-stage every dirty file matching the release allowlist
|
|
461
433
|
(src/, test/, bin/, docs/, plugins/, .claude-plugin/,
|
|
462
434
|
.claude/commands/, package*.json, dotmd.config*.mjs, README.md,
|
|
463
|
-
CLAUDE.md, .gitignore).
|
|
464
|
-
dirty
|
|
435
|
+
CLAUDE.md, .gitignore). A real ship refuses while anything outside
|
|
436
|
+
the allowlist is dirty; dry-run reports those files without changing them.
|
|
465
437
|
2. Commit with an auto-generated \`chore: release <version>\` message.
|
|
466
438
|
3. Run \`npm version <bump>\` to bump package.json, tag, push, run
|
|
467
439
|
the publish workflow, and reinstall locally.
|
|
@@ -474,20 +446,25 @@ Options:
|
|
|
474
446
|
|
|
475
447
|
Defaults to patch. Pass \`minor\` or \`major\` to bump those instead.
|
|
476
448
|
|
|
477
|
-
Network failures
|
|
478
|
-
|
|
479
|
-
\`git push origin main --tags\` to recover.`,
|
|
449
|
+
Network failures after the version tag exists are resumed with
|
|
450
|
+
\`npm run release:resume\`. Never push tags or publish manually.`,
|
|
480
451
|
|
|
481
|
-
set: `dotmd set <status> <file-or-slug> — change a document's status
|
|
452
|
+
set: `dotmd set <status> [<file-or-slug>] — change a document's status
|
|
482
453
|
|
|
483
|
-
Writes the new status into the file's frontmatter.
|
|
484
|
-
|
|
454
|
+
Writes the new status into the file's frontmatter. In-session plans carry a
|
|
455
|
+
local, gitignored ownership record under .dotmd/ so one session cannot release
|
|
456
|
+
another session's work.
|
|
485
457
|
- target is an archive status → archive the file (move + ref update)
|
|
486
458
|
- everything else → plain frontmatter status bump
|
|
487
459
|
|
|
488
460
|
<file-or-slug> resolves like \`dotmd use\`/\`archive\`: exact path first, then
|
|
489
461
|
a unique bare slug / basename across the doc roots (\`set paused auth-revamp\`).
|
|
490
462
|
Ambiguous slugs error with the candidate list instead of guessing.
|
|
463
|
+
When the path is omitted, exactly one plan must be owned by this session.
|
|
464
|
+
Claude Code and OpenCode session IDs are recognized automatically. Other hosts
|
|
465
|
+
must set DOTMD_SESSION_ID; anonymous ownership mutations fail closed.
|
|
466
|
+
Pickup hooks use at-least-once delivery with a stable operationId; hook side
|
|
467
|
+
effects must deduplicate that ID.
|
|
491
468
|
|
|
492
469
|
Options:
|
|
493
470
|
--note "<text>" Append the reason to \`## Version History\` in the
|
|
@@ -495,12 +472,14 @@ Options:
|
|
|
495
472
|
the status-change + worklog-edit round-trip.
|
|
496
473
|
--no-index Skip index regen (see \`dotmd archive --help\`).
|
|
497
474
|
--show-files Append \`files: …\` footer.
|
|
475
|
+
--force Recover another session's plan (explicit path required).
|
|
498
476
|
--dry-run, -n Preview without writing.
|
|
499
477
|
|
|
500
478
|
Examples:
|
|
501
479
|
dotmd set in-session docs/plans/x # mark a plan in-session
|
|
502
480
|
dotmd set partial docs/plans/x --note "tail tracked in y.md"
|
|
503
481
|
dotmd set archived docs/plans/x # archive a specific plan
|
|
482
|
+
dotmd set active # release this session's sole owned plan
|
|
504
483
|
|
|
505
484
|
To open a plan (mark in-session AND print its body), use \`dotmd use <file>\`.`,
|
|
506
485
|
|
|
@@ -569,7 +548,8 @@ Options:
|
|
|
569
548
|
--show-files Append a final \`files: a b c …\` line to stderr
|
|
570
549
|
listing every doc/index path the command touched
|
|
571
550
|
(deduped, sorted, repo-relative). Lets agents do
|
|
572
|
-
|
|
551
|
+
\`git add\` with the exact set instead of guessing.
|
|
552
|
+
--force Recover another session's plan (explicit path required).
|
|
573
553
|
--closeout-template Inject a \`## Closeout\` skeleton into the plan body
|
|
574
554
|
before archiving — bullets for outcomes, key
|
|
575
555
|
commits, deferrals. No-op if a \`## Closeout\`
|
|
@@ -722,8 +702,9 @@ the command says so instead of printing an empty list.`,
|
|
|
722
702
|
|
|
723
703
|
doctor: `dotmd doctor — auto-fix everything in one pass
|
|
724
704
|
|
|
725
|
-
Runs in sequence: fix broken references, lint --fix,
|
|
726
|
-
|
|
705
|
+
Runs in sequence: fix broken references, lint --fix, move over-cap
|
|
706
|
+
frontmatter prose into body sections, sync dates from git, regenerate
|
|
707
|
+
the index, then show remaining issues.
|
|
727
708
|
|
|
728
709
|
Modes:
|
|
729
710
|
(default) Auto-fix pass — previews by default since 0.37.0
|
|
@@ -785,11 +766,12 @@ docs. Fixes are applied by rewriting the frontmatter path.
|
|
|
785
766
|
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
786
767
|
|
|
787
768
|
touch: `dotmd touch <file> — bump updated date
|
|
788
|
-
dotmd touch --git —
|
|
769
|
+
dotmd touch --git [<file>...] — sync dates from git history
|
|
789
770
|
|
|
790
771
|
Without --git, updates a single file's frontmatter updated date to today.
|
|
791
|
-
With --git, scans all docs (or
|
|
792
|
-
date to match the last git commit date, fixing date drift warnings.
|
|
772
|
+
With --git, scans all docs (or the specified files) and syncs their updated
|
|
773
|
+
date to match the last substantive git commit date, fixing date drift warnings.
|
|
774
|
+
Commits that only changed the updated line are ignored so the fix converges.
|
|
793
775
|
|
|
794
776
|
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
795
777
|
|
|
@@ -903,19 +885,6 @@ Examples:
|
|
|
903
885
|
dotmd watch check # re-run check on changes
|
|
904
886
|
dotmd watch context # live briefing`,
|
|
905
887
|
|
|
906
|
-
notion: `dotmd notion — Notion database integration
|
|
907
|
-
|
|
908
|
-
Subcommands:
|
|
909
|
-
import <database-id> Pull Notion database → local .md files
|
|
910
|
-
export <database-id> Push local docs → Notion database rows
|
|
911
|
-
sync <database-id> Bidirectional sync (merge by slug)
|
|
912
|
-
|
|
913
|
-
Options:
|
|
914
|
-
--force Overwrite existing files on import
|
|
915
|
-
--dry-run, -n Preview without changes
|
|
916
|
-
|
|
917
|
-
Requires NOTION_TOKEN env var or notion.token in config.`,
|
|
918
|
-
|
|
919
888
|
export: `dotmd export — export docs as markdown, HTML, or JSON
|
|
920
889
|
|
|
921
890
|
Without a file, exports all docs (with optional filters).
|
|
@@ -1078,16 +1047,20 @@ Examples:
|
|
|
1078
1047
|
The "save a resume prompt" verb. Works mid-anything:
|
|
1079
1048
|
|
|
1080
1049
|
Plan mode (a plan is in-session, or you pass one):
|
|
1081
|
-
|
|
1050
|
+
The following publish in one atomic cooperating transaction:
|
|
1051
|
+
1. A resume prompt named resume-<plan-slug> (collision-safe: -2, -3, …),
|
|
1082
1052
|
stamped with a plan: link so consuming it re-claims the plan (see \`dotmd
|
|
1083
1053
|
use\`). The prompt is session-local — the next session's hud surfaces it;
|
|
1084
1054
|
never paste resume text into chat.
|
|
1085
1055
|
2. Releases the plan: one status flip, in-session → active by default
|
|
1086
1056
|
(--status to override, --note to record why in ## Version History).
|
|
1087
|
-
3.
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1057
|
+
3. Defers the shared generated index and prints exact repository-only commit
|
|
1058
|
+
guidance. Prompt and ownership records stay session-local and OUT of the
|
|
1059
|
+
pathspec.
|
|
1060
|
+
Which plan? Pass it explicitly, or baton resolves exactly one plan owned by
|
|
1061
|
+
this authoritative session. Journal entries and global in-session counts never
|
|
1062
|
+
grant ownership. A live pickup-hook delivery lease blocks release and force
|
|
1063
|
+
takeover; hooks are at-least-once and deduplicate the stable operationId.
|
|
1091
1064
|
|
|
1092
1065
|
Slug mode (no plan involved — "save a resume prompt for this"):
|
|
1093
1066
|
dotmd baton <slug> @/tmp/draft.md → saves resume-<slug>, touches NOTHING
|
|
@@ -1101,6 +1074,8 @@ Options:
|
|
|
1101
1074
|
--status <s> Target status for the plan (default: active; plan mode only)
|
|
1102
1075
|
--note "why" Append the reason to ## Version History (plan mode only)
|
|
1103
1076
|
--message / --body Inline body (one-liners; prefer @path or stdin)
|
|
1077
|
+
--force Recover another session's plan (explicit path required)
|
|
1078
|
+
--json Structured repository/session/generated file result
|
|
1104
1079
|
--dry-run, -n Preview without writing
|
|
1105
1080
|
|
|
1106
1081
|
Examples:
|
|
@@ -1215,8 +1190,10 @@ fix is to delete the explicit \`lifecycle\` block so flags take effect.`,
|
|
|
1215
1190
|
|
|
1216
1191
|
bulk: `dotmd bulk archive <f1> <f2> ... — archive multiple files at once
|
|
1217
1192
|
|
|
1218
|
-
Archives each file: sets status to
|
|
1219
|
-
directory, updates references
|
|
1193
|
+
Archives each file in an independent per-item transaction: sets status to
|
|
1194
|
+
archived, moves to archive directory, and updates references. This is explicitly
|
|
1195
|
+
not all-or-none; --json reports archived/failed for every item. The index is
|
|
1196
|
+
regenerated once after all item attempts.
|
|
1220
1197
|
|
|
1221
1198
|
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
1222
1199
|
|
|
@@ -1338,6 +1315,58 @@ Pass file paths as positional args to scope to those files only; otherwise
|
|
|
1338
1315
|
the whole docs tree is scanned.`,
|
|
1339
1316
|
};
|
|
1340
1317
|
|
|
1318
|
+
const GLOBAL_VALUE_OPTIONS = new Set(['--config', '--root', '--type']);
|
|
1319
|
+
const GLOBAL_BOOLEAN_OPTIONS = new Set(['--dry-run', '-n', '--verbose']);
|
|
1320
|
+
|
|
1321
|
+
function splitGlobalArgs(args) {
|
|
1322
|
+
let commandIndex = -1;
|
|
1323
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1324
|
+
const arg = args[i];
|
|
1325
|
+
if (GLOBAL_VALUE_OPTIONS.has(arg)) {
|
|
1326
|
+
if (args[i + 1] === undefined || args[i + 1].startsWith('-')) die(`Missing value for global option \`${arg}\`.`);
|
|
1327
|
+
i += 1;
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
if (GLOBAL_BOOLEAN_OPTIONS.has(arg)) continue;
|
|
1331
|
+
commandIndex = i;
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const command = canonicalCommand(commandIndex === -1 ? 'list' : args[commandIndex]);
|
|
1336
|
+
const rest = [];
|
|
1337
|
+
let explicitConfig = null;
|
|
1338
|
+
let rootArg = null;
|
|
1339
|
+
let typeArg = null;
|
|
1340
|
+
let dryRun = false;
|
|
1341
|
+
let verbose = false;
|
|
1342
|
+
|
|
1343
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1344
|
+
if (i === commandIndex) continue;
|
|
1345
|
+
const arg = args[i];
|
|
1346
|
+
const beforeCommand = commandIndex === -1 || i < commandIndex;
|
|
1347
|
+
if (GLOBAL_VALUE_OPTIONS.has(arg)) {
|
|
1348
|
+
const local = !beforeCommand && commandOwnsOption(command, arg);
|
|
1349
|
+
const next = args[i + 1];
|
|
1350
|
+
if (next === undefined || next.startsWith('-')) die(`Missing value for \`${arg}\`.`);
|
|
1351
|
+
if (local) rest.push(arg, next);
|
|
1352
|
+
else if (arg === '--config') explicitConfig = next;
|
|
1353
|
+
else if (arg === '--root') rootArg = next;
|
|
1354
|
+
else typeArg = next;
|
|
1355
|
+
i += 1;
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
if (arg === '--dry-run' || arg === '-n') { dryRun = true; continue; }
|
|
1359
|
+
if (arg === '--verbose') {
|
|
1360
|
+
if (!beforeCommand && commandOwnsOption(command, arg)) rest.push(arg);
|
|
1361
|
+
else verbose = true;
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
rest.push(arg);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
return { command, rest, explicitConfig, rootArg, typeArg, dryRun, verbose };
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1341
1370
|
async function main() {
|
|
1342
1371
|
const args = process.argv.slice(2);
|
|
1343
1372
|
|
|
@@ -1347,27 +1376,11 @@ async function main() {
|
|
|
1347
1376
|
return;
|
|
1348
1377
|
}
|
|
1349
1378
|
|
|
1350
|
-
//
|
|
1351
|
-
//
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
let explicitConfig = null;
|
|
1356
|
-
let rootArg = null;
|
|
1357
|
-
let typeArg = null;
|
|
1358
|
-
const normalized = [];
|
|
1359
|
-
for (let i = 0; i < args.length; i++) {
|
|
1360
|
-
const a = args[i];
|
|
1361
|
-
if (a === '--config' && args[i + 1]) { explicitConfig = args[++i]; continue; }
|
|
1362
|
-
if (a === '--type' && args[i + 1]) { typeArg = args[++i]; continue; }
|
|
1363
|
-
if (a === '--root' && args[i + 1]) { rootArg = args[++i]; continue; }
|
|
1364
|
-
if (a === '--dry-run' || a === '-n' || a === '--verbose') continue;
|
|
1365
|
-
normalized.push(a);
|
|
1366
|
-
}
|
|
1367
|
-
const dryRun = args.includes('--dry-run') || args.includes('-n');
|
|
1368
|
-
const verbose = args.includes('--verbose');
|
|
1369
|
-
let command = normalized[0] ?? 'list';
|
|
1370
|
-
const restArgs = normalized.slice(1);
|
|
1379
|
+
// Before-command options are global. After the command, a schema-declared
|
|
1380
|
+
// local option wins; otherwise the historical anywhere-global form remains.
|
|
1381
|
+
const parsed = splitGlobalArgs(args);
|
|
1382
|
+
let { command, explicitConfig, rootArg, typeArg, dryRun, verbose } = parsed;
|
|
1383
|
+
let restArgs = parsed.rest;
|
|
1371
1384
|
|
|
1372
1385
|
// Reconstruct the active global flags for proxy commands (e.g. `watch`) that
|
|
1373
1386
|
// re-invoke the CLI in a child process and must propagate them through.
|
|
@@ -1422,28 +1435,64 @@ async function main() {
|
|
|
1422
1435
|
return;
|
|
1423
1436
|
}
|
|
1424
1437
|
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1438
|
+
const dispatchPolicy = commandPolicy(command);
|
|
1439
|
+
_resolvedCommand = command;
|
|
1440
|
+
const doctorSubMode = command === 'doctor' && (
|
|
1441
|
+
args.includes('--statuses') || args.includes('--migrate-template')
|
|
1442
|
+
|| args.includes('--migrate-prompts') || args.includes('--frontmatter-fix')
|
|
1443
|
+
|| args.includes('--project')
|
|
1444
|
+
);
|
|
1445
|
+
const doctorExplicitApply = args.includes('--apply') || args.includes('--yes');
|
|
1446
|
+
const effectiveDryRun = dryRun || (command === 'doctor' && !doctorSubMode && !doctorExplicitApply);
|
|
1447
|
+
const passiveMachineContext = command === 'agent-context'
|
|
1448
|
+
|| (command === 'context' && args.includes('--json') && args.includes('--compact'));
|
|
1449
|
+
// Git/frontmatter drift is validation work, not index construction. Keep the
|
|
1450
|
+
// bounded history scan on commands that report or repair that drift; ordinary
|
|
1451
|
+
// reads still run schema/reference validation without walking 10k commits.
|
|
1452
|
+
const gitStaleness = command === 'check' || command === 'doctor';
|
|
1453
|
+
_suppressObservability = effectiveDryRun || command === 'hud' || passiveMachineContext;
|
|
1432
1454
|
|
|
1433
1455
|
// Per-command help
|
|
1434
1456
|
if (args.includes('--help') || args.includes('-h')) {
|
|
1435
|
-
|
|
1457
|
+
requireCommandPolicy(command, dispatchPolicy);
|
|
1458
|
+
process.stdout.write(`${HELP[command] ?? commandUsage(command)}\n`);
|
|
1436
1459
|
return;
|
|
1437
1460
|
}
|
|
1438
1461
|
|
|
1439
1462
|
if (command === 'completions') {
|
|
1463
|
+
requireCommandPolicy(command, dispatchPolicy);
|
|
1464
|
+
try { restArgs = validateCommandArgs(command, restArgs); } catch (err) { die(err.message); }
|
|
1440
1465
|
const { runCompletions } = await import('../src/completions.mjs');
|
|
1441
1466
|
runCompletions(restArgs);
|
|
1442
1467
|
return;
|
|
1443
1468
|
}
|
|
1444
1469
|
|
|
1445
|
-
|
|
1470
|
+
let config;
|
|
1471
|
+
try {
|
|
1472
|
+
config = await resolveConfig(process.cwd(), explicitConfig);
|
|
1473
|
+
} catch (err) {
|
|
1474
|
+
if (command === 'guard') {
|
|
1475
|
+
process.stdout.write('{}\n');
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
throw err;
|
|
1479
|
+
}
|
|
1446
1480
|
_resolvedConfig = config;
|
|
1481
|
+
const suppressSideEffects = effectiveDryRun || command === 'hud' || passiveMachineContext;
|
|
1482
|
+
Object.defineProperty(config, '_execution', {
|
|
1483
|
+
value: { dryRun, passive: command === 'hud' || passiveMachineContext, suppressSideEffects, gitStaleness },
|
|
1484
|
+
enumerable: false,
|
|
1485
|
+
});
|
|
1486
|
+
// Unknown names may still be user-defined query presets. Every built-in
|
|
1487
|
+
// dispatcher branch, including mutators above the shared index path, must be
|
|
1488
|
+
// present in the centralized command policy registry.
|
|
1489
|
+
if (!config.presets[command]) requireCommandPolicy(command, dispatchPolicy);
|
|
1490
|
+
|
|
1491
|
+
try {
|
|
1492
|
+
restArgs = validateCommandArgs(command, restArgs, { preset: Boolean(config.presets[command]) });
|
|
1493
|
+
} catch (err) {
|
|
1494
|
+
die(err.message);
|
|
1495
|
+
}
|
|
1447
1496
|
|
|
1448
1497
|
// Init — runInit re-resolves the config from disk internally (after any
|
|
1449
1498
|
// starter-config write), so we don't need to pre-pass it.
|
|
@@ -1479,9 +1528,25 @@ async function main() {
|
|
|
1479
1528
|
process.stderr.write(`Repo root: ${config.repoRoot}\n`);
|
|
1480
1529
|
}
|
|
1481
1530
|
|
|
1482
|
-
validateKnownFlags(command, restArgs, config);
|
|
1483
|
-
|
|
1484
1531
|
// Preset aliases (user config can override built-in commands below)
|
|
1532
|
+
if ((command === 'stale' || command === 'actionable') && !config.configuredPresetNames.has(command)) {
|
|
1533
|
+
const { buildIndex } = await import('../src/index.mjs');
|
|
1534
|
+
const { runQuery } = await import('../src/query.mjs');
|
|
1535
|
+
const { statusMetadataFor } = await import('../src/status-metadata.mjs');
|
|
1536
|
+
const index = buildIndex(config);
|
|
1537
|
+
applyIndexFilters(index);
|
|
1538
|
+
const docs = index.docs.filter(doc => {
|
|
1539
|
+
const metadata = statusMetadataFor(config, doc.type, doc.status);
|
|
1540
|
+
if (command === 'stale') return doc.isStale && !metadata?.skipStale;
|
|
1541
|
+
return metadata?.context === 'expanded'
|
|
1542
|
+
&& doc.hasNextStep
|
|
1543
|
+
&& !metadata.terminal
|
|
1544
|
+
&& !metadata.archive
|
|
1545
|
+
&& !isArchivedPath(doc.path, config);
|
|
1546
|
+
});
|
|
1547
|
+
runQuery({ ...index, docs }, ['--sort', 'updated', '--all', ...restArgs], config, { preset: command, type: typeArg, root: rootArg });
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1485
1550
|
if (config.presets[command]) {
|
|
1486
1551
|
const { buildIndex } = await import('../src/index.mjs');
|
|
1487
1552
|
const { runQuery } = await import('../src/query.mjs');
|
|
@@ -1587,16 +1652,15 @@ async function main() {
|
|
|
1587
1652
|
if (command === 'health') { const { runHealth } = await import('../src/health.mjs'); runHealth(restArgs, config); return; }
|
|
1588
1653
|
if (command === 'glossary') { const { runGlossary } = await import('../src/glossary.mjs'); runGlossary(restArgs, config); return; }
|
|
1589
1654
|
if (command === 'export') { const { runExport } = await import('../src/export.mjs'); runExport(restArgs, config, { dryRun, root: rootArg, type: typeArg }); return; }
|
|
1590
|
-
if (command === 'notion') { const { runNotion } = await import('../src/notion.mjs'); await runNotion(restArgs, config, { dryRun }); return; }
|
|
1591
1655
|
|
|
1592
1656
|
// Lifecycle commands
|
|
1593
1657
|
if (command === 'hud') { const { runHud } = await import('../src/hud.mjs'); runHud(restArgs, config); return; }
|
|
1594
|
-
if (command === 'guard') { const { runGuard } = await import('../src/guard.mjs'); await runGuard(restArgs, config); return; }
|
|
1595
|
-
if (command === 'update') { const { runUpdate } = await import('../src/update.mjs'); runUpdate(restArgs, config); return; }
|
|
1658
|
+
if (command === 'guard') { const { runGuard } = await import('../src/guard.mjs'); await runGuard(restArgs, config, { dryRun }); return; }
|
|
1659
|
+
if (command === 'update') { const { runUpdate } = await import('../src/update.mjs'); runUpdate(restArgs, config, { dryRun }); return; }
|
|
1596
1660
|
if (command === 'misuse') { const { runMisuse } = await import('../src/misuse-read.mjs'); runMisuse(restArgs, config); return; }
|
|
1597
1661
|
if (command === 'journal') { const { runJournal } = await import('../src/journal-read.mjs'); runJournal(restArgs, config); return; }
|
|
1598
1662
|
if (command === 'pickup' || command === 'unpickup' || command === 'release' || command === 'finish') {
|
|
1599
|
-
die(`\`dotmd ${command}\` was removed —
|
|
1663
|
+
die(`\`dotmd ${command}\` was removed — use the ownership-aware lifecycle verbs:\n dotmd use <file> # atomically claim + mark in-session + print the plan\n dotmd set <status> <file> # transition and release ownership when leaving in-session\n dotmd archive <file> # close out atomically`);
|
|
1600
1664
|
}
|
|
1601
1665
|
if (command === 'runlist') { const { runRunlist } = await import('../src/runlist.mjs'); await runRunlist(restArgs, config, { dryRun }); return; }
|
|
1602
1666
|
if (command === 'handoff') { die('`dotmd handoff` was removed in 0.31.0. Use `dotmd prompts new <name>` to create a saved prompt instead. The .dotmd/handoffs/ sidecar mechanism no longer exists; see CHANGELOG.'); }
|
|
@@ -1625,10 +1689,7 @@ async function main() {
|
|
|
1625
1689
|
// auto-fix path — sub-modes (--statuses, --migrate-template,
|
|
1626
1690
|
// --migrate-prompts) keep their existing "write unless --dry-run"
|
|
1627
1691
|
// contract because they're explicit one-shots the user opted into.
|
|
1628
|
-
const
|
|
1629
|
-
const explicitApply = args.includes('--apply') || args.includes('--yes');
|
|
1630
|
-
const explicitDryRun = args.includes('--dry-run') || args.includes('-n');
|
|
1631
|
-
const doctorDryRun = subMode ? dryRun : (explicitDryRun || !explicitApply);
|
|
1692
|
+
const doctorDryRun = doctorSubMode ? dryRun : (dryRun || !doctorExplicitApply);
|
|
1632
1693
|
const filtered = restArgs.filter(a => a !== '--apply' && a !== '--yes');
|
|
1633
1694
|
const { runDoctor } = await import('../src/doctor.mjs');
|
|
1634
1695
|
runDoctor(filtered, config, { dryRun: doctorDryRun });
|
|
@@ -1647,7 +1708,10 @@ async function main() {
|
|
|
1647
1708
|
// `query`, `index --print`, ...) stay opt-out so they never mutate disk.
|
|
1648
1709
|
const checkHasPathScope = command === 'check' && restArgs.some(arg => !arg.startsWith('-'));
|
|
1649
1710
|
const AUTO_HEAL_INDEX_COMMANDS = new Set(['check']);
|
|
1650
|
-
const index = buildIndex(config, {
|
|
1711
|
+
const index = buildIndex(config, {
|
|
1712
|
+
autoHealIndex: AUTO_HEAL_INDEX_COMMANDS.has(command) && !checkHasPathScope && !dryRun,
|
|
1713
|
+
invokeHooks: !suppressSideEffects,
|
|
1714
|
+
});
|
|
1651
1715
|
|
|
1652
1716
|
applyIndexFilters(index);
|
|
1653
1717
|
|
|
@@ -1677,6 +1741,31 @@ async function main() {
|
|
|
1677
1741
|
const noCollapse = args.includes('--no-collapse');
|
|
1678
1742
|
const verbose = args.includes('--verbose');
|
|
1679
1743
|
const checkTargets = restArgs.filter(arg => !arg.startsWith('-'));
|
|
1744
|
+
const skippedCheckHooks = config._execution?.suppressSideEffects
|
|
1745
|
+
? ['validate', 'transformDoc', 'formatSnapshot', 'renderCheck']
|
|
1746
|
+
.filter(name => typeof config.hooks?.[name] === 'function')
|
|
1747
|
+
: [];
|
|
1748
|
+
const checkJson = (checkIndex) => {
|
|
1749
|
+
const builtInPassed = checkIndex.errors.length === 0;
|
|
1750
|
+
const complete = skippedCheckHooks.length === 0;
|
|
1751
|
+
return {
|
|
1752
|
+
docsScanned: checkIndex.docs.length,
|
|
1753
|
+
errors: checkIndex.errors,
|
|
1754
|
+
warnings: errorsOnly ? [] : checkIndex.warnings,
|
|
1755
|
+
errorCount: checkIndex.errors.length,
|
|
1756
|
+
warningCount: checkIndex.warnings.length,
|
|
1757
|
+
passed: complete ? builtInPassed : null,
|
|
1758
|
+
...(complete ? {} : {
|
|
1759
|
+
builtInPassed,
|
|
1760
|
+
validationPreview: { status: 'built-in-only', skippedHooks: skippedCheckHooks },
|
|
1761
|
+
}),
|
|
1762
|
+
};
|
|
1763
|
+
};
|
|
1764
|
+
const writeCheckPreviewNote = () => {
|
|
1765
|
+
if (skippedCheckHooks.length > 0) {
|
|
1766
|
+
process.stdout.write(`[preview] Custom ${skippedCheckHooks.join(', ')} hook${skippedCheckHooks.length === 1 ? '' : 's'} skipped; results below cover built-in behavior only.\n`);
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1680
1769
|
|
|
1681
1770
|
if (fix && checkTargets.length > 0) {
|
|
1682
1771
|
die('`dotmd check --fix` does not support path-scoped checks yet. Run `dotmd check <path>` to validate a subset, or `dotmd check --fix` to fix the whole docs tree.');
|
|
@@ -1690,9 +1779,8 @@ async function main() {
|
|
|
1690
1779
|
runLint(['--fix'], config, { dryRun });
|
|
1691
1780
|
if (config.indexPath) {
|
|
1692
1781
|
if (!dryRun) {
|
|
1693
|
-
const {
|
|
1694
|
-
|
|
1695
|
-
wi(rif(freshIndex, config), config);
|
|
1782
|
+
const { writeRenderedIndex } = await import('../src/index-file.mjs');
|
|
1783
|
+
writeRenderedIndex(() => buildIndex(config, { fast: true }), config);
|
|
1696
1784
|
process.stdout.write('Index regenerated.\n');
|
|
1697
1785
|
} else {
|
|
1698
1786
|
process.stdout.write('[dry-run] Would regenerate index.\n');
|
|
@@ -1703,15 +1791,9 @@ async function main() {
|
|
|
1703
1791
|
applyIndexFilters(freshIndex);
|
|
1704
1792
|
applyPathScopeToIndex(freshIndex, config, checkTargets);
|
|
1705
1793
|
if (args.includes('--json')) {
|
|
1706
|
-
process.stdout.write(JSON.stringify(
|
|
1707
|
-
docsScanned: freshIndex.docs.length,
|
|
1708
|
-
errors: freshIndex.errors,
|
|
1709
|
-
warnings: errorsOnly ? [] : freshIndex.warnings,
|
|
1710
|
-
errorCount: freshIndex.errors.length,
|
|
1711
|
-
warningCount: freshIndex.warnings.length,
|
|
1712
|
-
passed: freshIndex.errors.length === 0,
|
|
1713
|
-
}, null, 2) + '\n');
|
|
1794
|
+
process.stdout.write(JSON.stringify(checkJson(freshIndex), null, 2) + '\n');
|
|
1714
1795
|
} else {
|
|
1796
|
+
writeCheckPreviewNote();
|
|
1715
1797
|
process.stdout.write('\n' + renderCheck(freshIndex, config, { errorsOnly, noCollapse, verbose }));
|
|
1716
1798
|
}
|
|
1717
1799
|
if (freshIndex.errors.length > 0) process.exitCode = 1;
|
|
@@ -1721,18 +1803,12 @@ async function main() {
|
|
|
1721
1803
|
applyPathScopeToIndex(index, config, checkTargets);
|
|
1722
1804
|
|
|
1723
1805
|
if (args.includes('--json')) {
|
|
1724
|
-
process.stdout.write(JSON.stringify(
|
|
1725
|
-
docsScanned: index.docs.length,
|
|
1726
|
-
errors: index.errors,
|
|
1727
|
-
warnings: errorsOnly ? [] : index.warnings,
|
|
1728
|
-
errorCount: index.errors.length,
|
|
1729
|
-
warningCount: index.warnings.length,
|
|
1730
|
-
passed: index.errors.length === 0,
|
|
1731
|
-
}, null, 2) + '\n');
|
|
1806
|
+
process.stdout.write(JSON.stringify(checkJson(index), null, 2) + '\n');
|
|
1732
1807
|
if (index.errors.length > 0) process.exitCode = 1;
|
|
1733
1808
|
return;
|
|
1734
1809
|
}
|
|
1735
1810
|
|
|
1811
|
+
writeCheckPreviewNote();
|
|
1736
1812
|
process.stdout.write(renderCheck(index, config, { errorsOnly, noCollapse, verbose }));
|
|
1737
1813
|
if (index.errors.length > 0) process.exitCode = 1;
|
|
1738
1814
|
return;
|
|
@@ -1763,14 +1839,18 @@ async function main() {
|
|
|
1763
1839
|
die('Index generation is not configured. Add an `index` section to your dotmd.config.mjs.');
|
|
1764
1840
|
}
|
|
1765
1841
|
const print = args.includes('--print');
|
|
1766
|
-
const { renderIndexFile,
|
|
1842
|
+
const { renderIndexFile, writeRenderedIndex } = await import('../src/index-file.mjs');
|
|
1843
|
+
if (!print) {
|
|
1844
|
+
const { authorizeRepoGeneratedPath } = await import('../src/managed-path.mjs');
|
|
1845
|
+
authorizeRepoGeneratedPath(config.indexPath, config, { kind: 'Generated index destination' });
|
|
1846
|
+
}
|
|
1767
1847
|
const rendered = renderIndexFile(index, config);
|
|
1768
1848
|
if (print) {
|
|
1769
1849
|
process.stdout.write(rendered);
|
|
1770
1850
|
} else if (dryRun) {
|
|
1771
1851
|
process.stdout.write(`[dry-run] Would update ${config.indexPath}\n`);
|
|
1772
1852
|
} else {
|
|
1773
|
-
|
|
1853
|
+
writeRenderedIndex(() => buildIndex(config, { fast: true }), config);
|
|
1774
1854
|
process.stdout.write(`Updated ${config.indexPath}\n`);
|
|
1775
1855
|
}
|
|
1776
1856
|
return;
|
|
@@ -1817,60 +1897,24 @@ async function main() {
|
|
|
1817
1897
|
return;
|
|
1818
1898
|
}
|
|
1819
1899
|
|
|
1820
|
-
function compactDoc(d) {
|
|
1821
|
-
return {
|
|
1822
|
-
path: d.path,
|
|
1823
|
-
title: d.title,
|
|
1824
|
-
status: d.status,
|
|
1825
|
-
type: d.type,
|
|
1826
|
-
nextStep: d.nextStep ?? null,
|
|
1827
|
-
blockers: d.blockers ?? [],
|
|
1828
|
-
daysSinceUpdate: d.daysSinceUpdate ?? null,
|
|
1829
|
-
};
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
function buildCompactAgentContext(idx) {
|
|
1833
|
-
const activeStatuses = new Set(['in-session', 'active', 'ready', 'planned', 'awaiting', 'blocked']);
|
|
1834
|
-
const active = idx.docs.filter(d => d.type === 'plan' && activeStatuses.has(d.status));
|
|
1835
|
-
const stale = idx.docs.filter(d => d.isStale && !config.lifecycle.skipStaleFor.has(d.status));
|
|
1836
|
-
const awaiting = idx.docs.filter(d => d.status === 'awaiting');
|
|
1837
|
-
const blocked = idx.docs.filter(d => d.status === 'blocked' || d.blockers?.length);
|
|
1838
|
-
const pendingPrompts = idx.docs
|
|
1839
|
-
.filter(d => d.type === 'prompt' && d.status === 'pending' && !isArchivedPath(d.path, config))
|
|
1840
|
-
.sort((a, b) => (a.created ?? '').localeCompare(b.created ?? '') || (a.updated ?? '').localeCompare(b.updated ?? ''));
|
|
1841
|
-
return {
|
|
1842
|
-
generatedAt: new Date().toISOString(),
|
|
1843
|
-
countsByStatus: idx.countsByStatus,
|
|
1844
|
-
countsByType: idx.countsByType,
|
|
1845
|
-
errors: {
|
|
1846
|
-
count: idx.errors.length,
|
|
1847
|
-
items: idx.errors.slice(0, 10).map(e => ({ path: e.path, message: e.message })),
|
|
1848
|
-
},
|
|
1849
|
-
warnings: { count: idx.warnings.length },
|
|
1850
|
-
prompts: {
|
|
1851
|
-
pending: pendingPrompts.length,
|
|
1852
|
-
next: pendingPrompts[0] ? compactDoc(pendingPrompts[0]) : null,
|
|
1853
|
-
},
|
|
1854
|
-
plans: {
|
|
1855
|
-
active: active.slice(0, 12).map(compactDoc),
|
|
1856
|
-
awaiting: awaiting.slice(0, 8).map(compactDoc),
|
|
1857
|
-
blocked: blocked.slice(0, 8).map(compactDoc),
|
|
1858
|
-
stale: stale.slice(0, 12).map(compactDoc),
|
|
1859
|
-
},
|
|
1860
|
-
};
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
1900
|
if (command === 'agent-context') {
|
|
1864
|
-
|
|
1901
|
+
const { buildAgentContext } = await import('../src/agent-context.mjs');
|
|
1902
|
+
const skippedHooks = ['validate', 'transformDoc', 'formatSnapshot'].filter(name => typeof config.hooks?.[name] === 'function');
|
|
1903
|
+
process.stdout.write(JSON.stringify(buildAgentContext(index, config, {
|
|
1904
|
+
roots: rootArg ? [rootArg] : null,
|
|
1905
|
+
types: typeArg ? typeArg.split(',').map(value => value.trim()).filter(Boolean) : null,
|
|
1906
|
+
skippedHooks,
|
|
1907
|
+
}), null, 2) + '\n');
|
|
1865
1908
|
return;
|
|
1866
1909
|
}
|
|
1867
1910
|
|
|
1868
1911
|
if (command === 'briefing') {
|
|
1869
1912
|
if (args.includes('--json')) {
|
|
1913
|
+
const { statusMetadataFor } = await import('../src/status-metadata.mjs');
|
|
1870
1914
|
const plans = index.docs.filter(d => d.type === 'plan');
|
|
1871
1915
|
const docs = index.docs.filter(d => d.type === 'doc');
|
|
1872
1916
|
const research = index.docs.filter(d => d.type === 'research');
|
|
1873
|
-
const stale = index.docs.filter(d => d.isStale && !config.
|
|
1917
|
+
const stale = index.docs.filter(d => d.isStale && !statusMetadataFor(config, d.type, d.status)?.skipStale).length;
|
|
1874
1918
|
// Coordination hubs are runlists, not actionable plans — split them out of
|
|
1875
1919
|
// inSession/active into their own `runlists` array so the JSON mirrors the
|
|
1876
1920
|
// rendered briefing. Empty on repos with no coordination hubs.
|
|
@@ -1880,7 +1924,7 @@ async function main() {
|
|
|
1880
1924
|
const closedStatuses = new Set([...config.lifecycle.archiveStatuses, ...config.lifecycle.terminalStatuses]);
|
|
1881
1925
|
const isLiveHub = (d) => isHub(d) && !closedStatuses.has(d.status) && !isArchivedPath(d.path, config);
|
|
1882
1926
|
process.stdout.write(JSON.stringify({
|
|
1883
|
-
plans: { total: plans.length, inSession: plans.filter(d => d.status === 'in-session' && !isHub(d)).map(d => ({ path: d.path, title: d.title, nextStep: d.nextStep })), active: plans.filter(d => d.status === 'active' && !isHub(d)).map(d => ({ path: d.path, title: d.title, nextStep: d.nextStep })), runlists: plans.filter(isLiveHub).map(d => ({ path: d.path, title: d.title, status: d.status, childCount: coordination.get(d.path)?.childCount ?? 0 })) },
|
|
1927
|
+
plans: { total: plans.length, inSession: plans.filter(d => d.status === 'in-session' && !isHub(d)).map(d => ({ path: d.path, title: d.title, nextStep: d.nextStep })), active: plans.filter(d => d.status === 'active' && !isHub(d)).map(d => ({ path: d.path, title: d.title, nextStep: d.nextStep })), focus: plans.filter(d => statusMetadataFor(config, 'plan', d.status)?.context === 'expanded' && !isHub(d)).map(d => ({ path: d.path, title: d.title, status: d.status, nextStep: d.nextStep })), runlists: plans.filter(isLiveHub).map(d => ({ path: d.path, title: d.title, status: d.status, childCount: coordination.get(d.path)?.childCount ?? 0 })) },
|
|
1884
1928
|
docs: { total: docs.length, active: docs.filter(d => !config.lifecycle.terminalStatuses.has(d.status)).length },
|
|
1885
1929
|
research: { total: research.length, active: research.filter(d => d.status === 'active').length },
|
|
1886
1930
|
stale, errorCount: index.errors.length, warningCount: index.warnings.length,
|
|
@@ -1899,7 +1943,13 @@ async function main() {
|
|
|
1899
1943
|
|
|
1900
1944
|
if (args.includes('--json')) {
|
|
1901
1945
|
if (compact) {
|
|
1902
|
-
|
|
1946
|
+
const { buildAgentContext } = await import('../src/agent-context.mjs');
|
|
1947
|
+
const skippedHooks = ['validate', 'transformDoc', 'formatSnapshot'].filter(name => typeof config.hooks?.[name] === 'function');
|
|
1948
|
+
process.stdout.write(JSON.stringify(buildAgentContext(index, config, {
|
|
1949
|
+
roots: rootArg ? [rootArg] : null,
|
|
1950
|
+
types: typeArg ? typeArg.split(',').map(value => value.trim()).filter(Boolean) : null,
|
|
1951
|
+
skippedHooks,
|
|
1952
|
+
}), null, 2) + '\n');
|
|
1903
1953
|
return;
|
|
1904
1954
|
}
|
|
1905
1955
|
const byStatus = {};
|
|
@@ -1915,7 +1965,7 @@ async function main() {
|
|
|
1915
1965
|
byType[doc.type].push(doc);
|
|
1916
1966
|
}
|
|
1917
1967
|
}
|
|
1918
|
-
if (summarize) {
|
|
1968
|
+
if (summarize && !config._execution?.suppressSideEffects) {
|
|
1919
1969
|
const { summarizeDocBody } = await import('../src/ai.mjs');
|
|
1920
1970
|
const { extractFrontmatter } = await import('../src/frontmatter.mjs');
|
|
1921
1971
|
const { readFileSync } = await import('node:fs');
|
|
@@ -1934,9 +1984,13 @@ async function main() {
|
|
|
1934
1984
|
} catch { /* skip */ }
|
|
1935
1985
|
}
|
|
1936
1986
|
}
|
|
1937
|
-
const
|
|
1987
|
+
const { statusMetadataFor } = await import('../src/status-metadata.mjs');
|
|
1988
|
+
const stale = index.docs.filter(d => d.isStale && !statusMetadataFor(config, d.type, d.status)?.skipStale);
|
|
1938
1989
|
process.stdout.write(JSON.stringify({
|
|
1939
1990
|
generatedAt: new Date().toISOString(),
|
|
1991
|
+
...(summarize && config._execution?.suppressSideEffects
|
|
1992
|
+
? { summaryPreview: { status: 'skipped-preview', reason: 'side-effect-free preview' } }
|
|
1993
|
+
: {}),
|
|
1940
1994
|
docsByType: Object.keys(byType).length > 0 ? byType : undefined,
|
|
1941
1995
|
docsByStatus: byStatus,
|
|
1942
1996
|
countsByStatus: index.countsByStatus,
|
|
@@ -1970,15 +2024,7 @@ async function main() {
|
|
|
1970
2024
|
return;
|
|
1971
2025
|
}
|
|
1972
2026
|
|
|
1973
|
-
|
|
1974
|
-
const { KNOWN_COMMANDS } = await import('../src/commands.mjs');
|
|
1975
|
-
const matches = KNOWN_COMMANDS
|
|
1976
|
-
.map(c => ({ cmd: c, dist: levenshtein(command, c) }))
|
|
1977
|
-
.sort((a, b) => a.dist - b.dist);
|
|
1978
|
-
if (matches[0] && matches[0].dist <= 3) {
|
|
1979
|
-
die(`Unknown command: ${command}\n\nDid you mean \`dotmd ${matches[0].cmd}\`?`);
|
|
1980
|
-
}
|
|
1981
|
-
die(`Unknown command: ${command}\n\nRun \`dotmd --help\` for available commands.`);
|
|
2027
|
+
requireCommandPolicy(command, null);
|
|
1982
2028
|
}
|
|
1983
2029
|
|
|
1984
2030
|
// F17a: opt-in JSONL journal of every CLI invocation. The dispatch tail
|
|
@@ -1987,10 +2033,13 @@ async function main() {
|
|
|
1987
2033
|
// moment it's loaded, so even early dispatcher errors (after config) get
|
|
1988
2034
|
// journaled.
|
|
1989
2035
|
let _resolvedConfig = null;
|
|
2036
|
+
let _resolvedCommand = null;
|
|
2037
|
+
let _suppressObservability = false;
|
|
1990
2038
|
const _startMs = Date.now();
|
|
1991
2039
|
const _invocationArgs = process.argv.slice(2);
|
|
1992
2040
|
|
|
1993
2041
|
function _journalExit(err) {
|
|
2042
|
+
if (_suppressObservability || _resolvedCommand === 'hud' || _invocationArgs.includes('--dry-run') || _invocationArgs.includes('-n')) return;
|
|
1994
2043
|
try {
|
|
1995
2044
|
recordCliInvocation({
|
|
1996
2045
|
config: _resolvedConfig,
|
|
@@ -2021,7 +2070,7 @@ main()
|
|
|
2021
2070
|
// has already failed in this session within the lookup window. Lookup is
|
|
2022
2071
|
// a no-op when the journal is disabled or DOTMD_NO_HINTS=1.
|
|
2023
2072
|
try {
|
|
2024
|
-
const hint = findRepeatFailureHint(_invocationArgs, _resolvedConfig);
|
|
2073
|
+
const hint = findRepeatFailureHint(sanitizeTelemetryArgv(_invocationArgs), _resolvedConfig);
|
|
2025
2074
|
if (hint) out = `${out}\n\nTip: ${hint}`;
|
|
2026
2075
|
} catch { /* hint must never break error reporting */ }
|
|
2027
2076
|
process.stderr.write(`${out}\n`);
|