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.
- package/README.md +144 -964
- package/bin/dotmd.mjs +241 -197
- 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 -87
- package/src/bulk-tag.mjs +7 -7
- 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 +12 -5
- package/src/export.mjs +154 -25
- package/src/fix-refs.mjs +2 -0
- package/src/frontmatter-fix.mjs +2 -0
- package/src/frontmatter.mjs +3 -2
- package/src/git.mjs +531 -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 +17 -12
- package/src/init.mjs +1 -1
- package/src/journal.mjs +145 -12
- package/src/lifecycle.mjs +554 -282
- package/src/lint.mjs +57 -9
- 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 +138 -32
- package/src/query.mjs +22 -10
- package/src/reference-planner.mjs +292 -0
- package/src/rename.mjs +65 -73
- package/src/render.mjs +17 -8
- 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 +14 -6
- 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) {
|
|
@@ -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\`
|
|
@@ -903,19 +883,6 @@ Examples:
|
|
|
903
883
|
dotmd watch check # re-run check on changes
|
|
904
884
|
dotmd watch context # live briefing`,
|
|
905
885
|
|
|
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
886
|
export: `dotmd export — export docs as markdown, HTML, or JSON
|
|
920
887
|
|
|
921
888
|
Without a file, exports all docs (with optional filters).
|
|
@@ -1078,15 +1045,20 @@ Examples:
|
|
|
1078
1045
|
The "save a resume prompt" verb. Works mid-anything:
|
|
1079
1046
|
|
|
1080
1047
|
Plan mode (a plan is in-session, or you pass one):
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1048
|
+
The following publish in one atomic cooperating transaction:
|
|
1049
|
+
1. A resume prompt named resume-<plan-slug> (collision-safe: -2, -3, …),
|
|
1050
|
+
stamped with a plan: link so consuming it re-claims the plan (see \`dotmd
|
|
1051
|
+
use\`). The prompt is session-local — the next session's hud surfaces it;
|
|
1052
|
+
never paste resume text into chat.
|
|
1084
1053
|
2. Releases the plan: one status flip, in-session → active by default
|
|
1085
1054
|
(--status to override, --note to record why in ## Version History).
|
|
1086
|
-
3.
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1055
|
+
3. Defers the shared generated index and prints exact repository-only commit
|
|
1056
|
+
guidance. Prompt and ownership records stay session-local and OUT of the
|
|
1057
|
+
pathspec.
|
|
1058
|
+
Which plan? Pass it explicitly, or baton resolves exactly one plan owned by
|
|
1059
|
+
this authoritative session. Journal entries and global in-session counts never
|
|
1060
|
+
grant ownership. A live pickup-hook delivery lease blocks release and force
|
|
1061
|
+
takeover; hooks are at-least-once and deduplicate the stable operationId.
|
|
1090
1062
|
|
|
1091
1063
|
Slug mode (no plan involved — "save a resume prompt for this"):
|
|
1092
1064
|
dotmd baton <slug> @/tmp/draft.md → saves resume-<slug>, touches NOTHING
|
|
@@ -1100,6 +1072,8 @@ Options:
|
|
|
1100
1072
|
--status <s> Target status for the plan (default: active; plan mode only)
|
|
1101
1073
|
--note "why" Append the reason to ## Version History (plan mode only)
|
|
1102
1074
|
--message / --body Inline body (one-liners; prefer @path or stdin)
|
|
1075
|
+
--force Recover another session's plan (explicit path required)
|
|
1076
|
+
--json Structured repository/session/generated file result
|
|
1103
1077
|
--dry-run, -n Preview without writing
|
|
1104
1078
|
|
|
1105
1079
|
Examples:
|
|
@@ -1214,8 +1188,10 @@ fix is to delete the explicit \`lifecycle\` block so flags take effect.`,
|
|
|
1214
1188
|
|
|
1215
1189
|
bulk: `dotmd bulk archive <f1> <f2> ... — archive multiple files at once
|
|
1216
1190
|
|
|
1217
|
-
Archives each file: sets status to
|
|
1218
|
-
directory, updates references
|
|
1191
|
+
Archives each file in an independent per-item transaction: sets status to
|
|
1192
|
+
archived, moves to archive directory, and updates references. This is explicitly
|
|
1193
|
+
not all-or-none; --json reports archived/failed for every item. The index is
|
|
1194
|
+
regenerated once after all item attempts.
|
|
1219
1195
|
|
|
1220
1196
|
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
1221
1197
|
|
|
@@ -1337,6 +1313,58 @@ Pass file paths as positional args to scope to those files only; otherwise
|
|
|
1337
1313
|
the whole docs tree is scanned.`,
|
|
1338
1314
|
};
|
|
1339
1315
|
|
|
1316
|
+
const GLOBAL_VALUE_OPTIONS = new Set(['--config', '--root', '--type']);
|
|
1317
|
+
const GLOBAL_BOOLEAN_OPTIONS = new Set(['--dry-run', '-n', '--verbose']);
|
|
1318
|
+
|
|
1319
|
+
function splitGlobalArgs(args) {
|
|
1320
|
+
let commandIndex = -1;
|
|
1321
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1322
|
+
const arg = args[i];
|
|
1323
|
+
if (GLOBAL_VALUE_OPTIONS.has(arg)) {
|
|
1324
|
+
if (args[i + 1] === undefined || args[i + 1].startsWith('-')) die(`Missing value for global option \`${arg}\`.`);
|
|
1325
|
+
i += 1;
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
if (GLOBAL_BOOLEAN_OPTIONS.has(arg)) continue;
|
|
1329
|
+
commandIndex = i;
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const command = canonicalCommand(commandIndex === -1 ? 'list' : args[commandIndex]);
|
|
1334
|
+
const rest = [];
|
|
1335
|
+
let explicitConfig = null;
|
|
1336
|
+
let rootArg = null;
|
|
1337
|
+
let typeArg = null;
|
|
1338
|
+
let dryRun = false;
|
|
1339
|
+
let verbose = false;
|
|
1340
|
+
|
|
1341
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1342
|
+
if (i === commandIndex) continue;
|
|
1343
|
+
const arg = args[i];
|
|
1344
|
+
const beforeCommand = commandIndex === -1 || i < commandIndex;
|
|
1345
|
+
if (GLOBAL_VALUE_OPTIONS.has(arg)) {
|
|
1346
|
+
const local = !beforeCommand && commandOwnsOption(command, arg);
|
|
1347
|
+
const next = args[i + 1];
|
|
1348
|
+
if (next === undefined || next.startsWith('-')) die(`Missing value for \`${arg}\`.`);
|
|
1349
|
+
if (local) rest.push(arg, next);
|
|
1350
|
+
else if (arg === '--config') explicitConfig = next;
|
|
1351
|
+
else if (arg === '--root') rootArg = next;
|
|
1352
|
+
else typeArg = next;
|
|
1353
|
+
i += 1;
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
if (arg === '--dry-run' || arg === '-n') { dryRun = true; continue; }
|
|
1357
|
+
if (arg === '--verbose') {
|
|
1358
|
+
if (!beforeCommand && commandOwnsOption(command, arg)) rest.push(arg);
|
|
1359
|
+
else verbose = true;
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
rest.push(arg);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
return { command, rest, explicitConfig, rootArg, typeArg, dryRun, verbose };
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1340
1368
|
async function main() {
|
|
1341
1369
|
const args = process.argv.slice(2);
|
|
1342
1370
|
|
|
@@ -1346,27 +1374,11 @@ async function main() {
|
|
|
1346
1374
|
return;
|
|
1347
1375
|
}
|
|
1348
1376
|
|
|
1349
|
-
//
|
|
1350
|
-
//
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
let explicitConfig = null;
|
|
1355
|
-
let rootArg = null;
|
|
1356
|
-
let typeArg = null;
|
|
1357
|
-
const normalized = [];
|
|
1358
|
-
for (let i = 0; i < args.length; i++) {
|
|
1359
|
-
const a = args[i];
|
|
1360
|
-
if (a === '--config' && args[i + 1]) { explicitConfig = args[++i]; continue; }
|
|
1361
|
-
if (a === '--type' && args[i + 1]) { typeArg = args[++i]; continue; }
|
|
1362
|
-
if (a === '--root' && args[i + 1]) { rootArg = args[++i]; continue; }
|
|
1363
|
-
if (a === '--dry-run' || a === '-n' || a === '--verbose') continue;
|
|
1364
|
-
normalized.push(a);
|
|
1365
|
-
}
|
|
1366
|
-
const dryRun = args.includes('--dry-run') || args.includes('-n');
|
|
1367
|
-
const verbose = args.includes('--verbose');
|
|
1368
|
-
let command = normalized[0] ?? 'list';
|
|
1369
|
-
const restArgs = normalized.slice(1);
|
|
1377
|
+
// Before-command options are global. After the command, a schema-declared
|
|
1378
|
+
// local option wins; otherwise the historical anywhere-global form remains.
|
|
1379
|
+
const parsed = splitGlobalArgs(args);
|
|
1380
|
+
let { command, explicitConfig, rootArg, typeArg, dryRun, verbose } = parsed;
|
|
1381
|
+
let restArgs = parsed.rest;
|
|
1370
1382
|
|
|
1371
1383
|
// Reconstruct the active global flags for proxy commands (e.g. `watch`) that
|
|
1372
1384
|
// re-invoke the CLI in a child process and must propagate them through.
|
|
@@ -1421,28 +1433,60 @@ async function main() {
|
|
|
1421
1433
|
return;
|
|
1422
1434
|
}
|
|
1423
1435
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1436
|
+
const dispatchPolicy = commandPolicy(command);
|
|
1437
|
+
_resolvedCommand = command;
|
|
1438
|
+
const doctorSubMode = command === 'doctor' && (
|
|
1439
|
+
args.includes('--statuses') || args.includes('--migrate-template')
|
|
1440
|
+
|| args.includes('--migrate-prompts') || args.includes('--frontmatter-fix')
|
|
1441
|
+
|| args.includes('--project')
|
|
1442
|
+
);
|
|
1443
|
+
const doctorExplicitApply = args.includes('--apply') || args.includes('--yes');
|
|
1444
|
+
const effectiveDryRun = dryRun || (command === 'doctor' && !doctorSubMode && !doctorExplicitApply);
|
|
1445
|
+
const passiveMachineContext = command === 'agent-context'
|
|
1446
|
+
|| (command === 'context' && args.includes('--json') && args.includes('--compact'));
|
|
1447
|
+
_suppressObservability = effectiveDryRun || command === 'hud' || passiveMachineContext;
|
|
1431
1448
|
|
|
1432
1449
|
// Per-command help
|
|
1433
1450
|
if (args.includes('--help') || args.includes('-h')) {
|
|
1434
|
-
|
|
1451
|
+
requireCommandPolicy(command, dispatchPolicy);
|
|
1452
|
+
process.stdout.write(`${HELP[command] ?? commandUsage(command)}\n`);
|
|
1435
1453
|
return;
|
|
1436
1454
|
}
|
|
1437
1455
|
|
|
1438
1456
|
if (command === 'completions') {
|
|
1457
|
+
requireCommandPolicy(command, dispatchPolicy);
|
|
1458
|
+
try { restArgs = validateCommandArgs(command, restArgs); } catch (err) { die(err.message); }
|
|
1439
1459
|
const { runCompletions } = await import('../src/completions.mjs');
|
|
1440
1460
|
runCompletions(restArgs);
|
|
1441
1461
|
return;
|
|
1442
1462
|
}
|
|
1443
1463
|
|
|
1444
|
-
|
|
1464
|
+
let config;
|
|
1465
|
+
try {
|
|
1466
|
+
config = await resolveConfig(process.cwd(), explicitConfig);
|
|
1467
|
+
} catch (err) {
|
|
1468
|
+
if (command === 'guard') {
|
|
1469
|
+
process.stdout.write('{}\n');
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
throw err;
|
|
1473
|
+
}
|
|
1445
1474
|
_resolvedConfig = config;
|
|
1475
|
+
const suppressSideEffects = effectiveDryRun || command === 'hud' || passiveMachineContext;
|
|
1476
|
+
Object.defineProperty(config, '_execution', {
|
|
1477
|
+
value: { dryRun, passive: command === 'hud' || passiveMachineContext, suppressSideEffects },
|
|
1478
|
+
enumerable: false,
|
|
1479
|
+
});
|
|
1480
|
+
// Unknown names may still be user-defined query presets. Every built-in
|
|
1481
|
+
// dispatcher branch, including mutators above the shared index path, must be
|
|
1482
|
+
// present in the centralized command policy registry.
|
|
1483
|
+
if (!config.presets[command]) requireCommandPolicy(command, dispatchPolicy);
|
|
1484
|
+
|
|
1485
|
+
try {
|
|
1486
|
+
restArgs = validateCommandArgs(command, restArgs, { preset: Boolean(config.presets[command]) });
|
|
1487
|
+
} catch (err) {
|
|
1488
|
+
die(err.message);
|
|
1489
|
+
}
|
|
1446
1490
|
|
|
1447
1491
|
// Init — runInit re-resolves the config from disk internally (after any
|
|
1448
1492
|
// starter-config write), so we don't need to pre-pass it.
|
|
@@ -1478,9 +1522,25 @@ async function main() {
|
|
|
1478
1522
|
process.stderr.write(`Repo root: ${config.repoRoot}\n`);
|
|
1479
1523
|
}
|
|
1480
1524
|
|
|
1481
|
-
validateKnownFlags(command, restArgs, config);
|
|
1482
|
-
|
|
1483
1525
|
// Preset aliases (user config can override built-in commands below)
|
|
1526
|
+
if ((command === 'stale' || command === 'actionable') && !config.configuredPresetNames.has(command)) {
|
|
1527
|
+
const { buildIndex } = await import('../src/index.mjs');
|
|
1528
|
+
const { runQuery } = await import('../src/query.mjs');
|
|
1529
|
+
const { statusMetadataFor } = await import('../src/status-metadata.mjs');
|
|
1530
|
+
const index = buildIndex(config);
|
|
1531
|
+
applyIndexFilters(index);
|
|
1532
|
+
const docs = index.docs.filter(doc => {
|
|
1533
|
+
const metadata = statusMetadataFor(config, doc.type, doc.status);
|
|
1534
|
+
if (command === 'stale') return doc.isStale && !metadata?.skipStale;
|
|
1535
|
+
return metadata?.context === 'expanded'
|
|
1536
|
+
&& doc.hasNextStep
|
|
1537
|
+
&& !metadata.terminal
|
|
1538
|
+
&& !metadata.archive
|
|
1539
|
+
&& !isArchivedPath(doc.path, config);
|
|
1540
|
+
});
|
|
1541
|
+
runQuery({ ...index, docs }, ['--sort', 'updated', '--all', ...restArgs], config, { preset: command, type: typeArg, root: rootArg });
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1484
1544
|
if (config.presets[command]) {
|
|
1485
1545
|
const { buildIndex } = await import('../src/index.mjs');
|
|
1486
1546
|
const { runQuery } = await import('../src/query.mjs');
|
|
@@ -1586,16 +1646,15 @@ async function main() {
|
|
|
1586
1646
|
if (command === 'health') { const { runHealth } = await import('../src/health.mjs'); runHealth(restArgs, config); return; }
|
|
1587
1647
|
if (command === 'glossary') { const { runGlossary } = await import('../src/glossary.mjs'); runGlossary(restArgs, config); return; }
|
|
1588
1648
|
if (command === 'export') { const { runExport } = await import('../src/export.mjs'); runExport(restArgs, config, { dryRun, root: rootArg, type: typeArg }); return; }
|
|
1589
|
-
if (command === 'notion') { const { runNotion } = await import('../src/notion.mjs'); await runNotion(restArgs, config, { dryRun }); return; }
|
|
1590
1649
|
|
|
1591
1650
|
// Lifecycle commands
|
|
1592
1651
|
if (command === 'hud') { const { runHud } = await import('../src/hud.mjs'); runHud(restArgs, config); return; }
|
|
1593
|
-
if (command === 'guard') { const { runGuard } = await import('../src/guard.mjs'); await runGuard(restArgs, config); return; }
|
|
1594
|
-
if (command === 'update') { const { runUpdate } = await import('../src/update.mjs'); runUpdate(restArgs, config); return; }
|
|
1652
|
+
if (command === 'guard') { const { runGuard } = await import('../src/guard.mjs'); await runGuard(restArgs, config, { dryRun }); return; }
|
|
1653
|
+
if (command === 'update') { const { runUpdate } = await import('../src/update.mjs'); runUpdate(restArgs, config, { dryRun }); return; }
|
|
1595
1654
|
if (command === 'misuse') { const { runMisuse } = await import('../src/misuse-read.mjs'); runMisuse(restArgs, config); return; }
|
|
1596
1655
|
if (command === 'journal') { const { runJournal } = await import('../src/journal-read.mjs'); runJournal(restArgs, config); return; }
|
|
1597
1656
|
if (command === 'pickup' || command === 'unpickup' || command === 'release' || command === 'finish') {
|
|
1598
|
-
die(`\`dotmd ${command}\` was removed —
|
|
1657
|
+
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`);
|
|
1599
1658
|
}
|
|
1600
1659
|
if (command === 'runlist') { const { runRunlist } = await import('../src/runlist.mjs'); await runRunlist(restArgs, config, { dryRun }); return; }
|
|
1601
1660
|
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.'); }
|
|
@@ -1624,10 +1683,7 @@ async function main() {
|
|
|
1624
1683
|
// auto-fix path — sub-modes (--statuses, --migrate-template,
|
|
1625
1684
|
// --migrate-prompts) keep their existing "write unless --dry-run"
|
|
1626
1685
|
// contract because they're explicit one-shots the user opted into.
|
|
1627
|
-
const
|
|
1628
|
-
const explicitApply = args.includes('--apply') || args.includes('--yes');
|
|
1629
|
-
const explicitDryRun = args.includes('--dry-run') || args.includes('-n');
|
|
1630
|
-
const doctorDryRun = subMode ? dryRun : (explicitDryRun || !explicitApply);
|
|
1686
|
+
const doctorDryRun = doctorSubMode ? dryRun : (dryRun || !doctorExplicitApply);
|
|
1631
1687
|
const filtered = restArgs.filter(a => a !== '--apply' && a !== '--yes');
|
|
1632
1688
|
const { runDoctor } = await import('../src/doctor.mjs');
|
|
1633
1689
|
runDoctor(filtered, config, { dryRun: doctorDryRun });
|
|
@@ -1646,7 +1702,10 @@ async function main() {
|
|
|
1646
1702
|
// `query`, `index --print`, ...) stay opt-out so they never mutate disk.
|
|
1647
1703
|
const checkHasPathScope = command === 'check' && restArgs.some(arg => !arg.startsWith('-'));
|
|
1648
1704
|
const AUTO_HEAL_INDEX_COMMANDS = new Set(['check']);
|
|
1649
|
-
const index = buildIndex(config, {
|
|
1705
|
+
const index = buildIndex(config, {
|
|
1706
|
+
autoHealIndex: AUTO_HEAL_INDEX_COMMANDS.has(command) && !checkHasPathScope && !dryRun,
|
|
1707
|
+
invokeHooks: !suppressSideEffects,
|
|
1708
|
+
});
|
|
1650
1709
|
|
|
1651
1710
|
applyIndexFilters(index);
|
|
1652
1711
|
|
|
@@ -1676,6 +1735,31 @@ async function main() {
|
|
|
1676
1735
|
const noCollapse = args.includes('--no-collapse');
|
|
1677
1736
|
const verbose = args.includes('--verbose');
|
|
1678
1737
|
const checkTargets = restArgs.filter(arg => !arg.startsWith('-'));
|
|
1738
|
+
const skippedCheckHooks = config._execution?.suppressSideEffects
|
|
1739
|
+
? ['validate', 'transformDoc', 'formatSnapshot', 'renderCheck']
|
|
1740
|
+
.filter(name => typeof config.hooks?.[name] === 'function')
|
|
1741
|
+
: [];
|
|
1742
|
+
const checkJson = (checkIndex) => {
|
|
1743
|
+
const builtInPassed = checkIndex.errors.length === 0;
|
|
1744
|
+
const complete = skippedCheckHooks.length === 0;
|
|
1745
|
+
return {
|
|
1746
|
+
docsScanned: checkIndex.docs.length,
|
|
1747
|
+
errors: checkIndex.errors,
|
|
1748
|
+
warnings: errorsOnly ? [] : checkIndex.warnings,
|
|
1749
|
+
errorCount: checkIndex.errors.length,
|
|
1750
|
+
warningCount: checkIndex.warnings.length,
|
|
1751
|
+
passed: complete ? builtInPassed : null,
|
|
1752
|
+
...(complete ? {} : {
|
|
1753
|
+
builtInPassed,
|
|
1754
|
+
validationPreview: { status: 'built-in-only', skippedHooks: skippedCheckHooks },
|
|
1755
|
+
}),
|
|
1756
|
+
};
|
|
1757
|
+
};
|
|
1758
|
+
const writeCheckPreviewNote = () => {
|
|
1759
|
+
if (skippedCheckHooks.length > 0) {
|
|
1760
|
+
process.stdout.write(`[preview] Custom ${skippedCheckHooks.join(', ')} hook${skippedCheckHooks.length === 1 ? '' : 's'} skipped; results below cover built-in behavior only.\n`);
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1679
1763
|
|
|
1680
1764
|
if (fix && checkTargets.length > 0) {
|
|
1681
1765
|
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.');
|
|
@@ -1689,9 +1773,8 @@ async function main() {
|
|
|
1689
1773
|
runLint(['--fix'], config, { dryRun });
|
|
1690
1774
|
if (config.indexPath) {
|
|
1691
1775
|
if (!dryRun) {
|
|
1692
|
-
const {
|
|
1693
|
-
|
|
1694
|
-
wi(rif(freshIndex, config), config);
|
|
1776
|
+
const { writeRenderedIndex } = await import('../src/index-file.mjs');
|
|
1777
|
+
writeRenderedIndex(() => buildIndex(config, { fast: true }), config);
|
|
1695
1778
|
process.stdout.write('Index regenerated.\n');
|
|
1696
1779
|
} else {
|
|
1697
1780
|
process.stdout.write('[dry-run] Would regenerate index.\n');
|
|
@@ -1702,15 +1785,9 @@ async function main() {
|
|
|
1702
1785
|
applyIndexFilters(freshIndex);
|
|
1703
1786
|
applyPathScopeToIndex(freshIndex, config, checkTargets);
|
|
1704
1787
|
if (args.includes('--json')) {
|
|
1705
|
-
process.stdout.write(JSON.stringify(
|
|
1706
|
-
docsScanned: freshIndex.docs.length,
|
|
1707
|
-
errors: freshIndex.errors,
|
|
1708
|
-
warnings: errorsOnly ? [] : freshIndex.warnings,
|
|
1709
|
-
errorCount: freshIndex.errors.length,
|
|
1710
|
-
warningCount: freshIndex.warnings.length,
|
|
1711
|
-
passed: freshIndex.errors.length === 0,
|
|
1712
|
-
}, null, 2) + '\n');
|
|
1788
|
+
process.stdout.write(JSON.stringify(checkJson(freshIndex), null, 2) + '\n');
|
|
1713
1789
|
} else {
|
|
1790
|
+
writeCheckPreviewNote();
|
|
1714
1791
|
process.stdout.write('\n' + renderCheck(freshIndex, config, { errorsOnly, noCollapse, verbose }));
|
|
1715
1792
|
}
|
|
1716
1793
|
if (freshIndex.errors.length > 0) process.exitCode = 1;
|
|
@@ -1720,18 +1797,12 @@ async function main() {
|
|
|
1720
1797
|
applyPathScopeToIndex(index, config, checkTargets);
|
|
1721
1798
|
|
|
1722
1799
|
if (args.includes('--json')) {
|
|
1723
|
-
process.stdout.write(JSON.stringify(
|
|
1724
|
-
docsScanned: index.docs.length,
|
|
1725
|
-
errors: index.errors,
|
|
1726
|
-
warnings: errorsOnly ? [] : index.warnings,
|
|
1727
|
-
errorCount: index.errors.length,
|
|
1728
|
-
warningCount: index.warnings.length,
|
|
1729
|
-
passed: index.errors.length === 0,
|
|
1730
|
-
}, null, 2) + '\n');
|
|
1800
|
+
process.stdout.write(JSON.stringify(checkJson(index), null, 2) + '\n');
|
|
1731
1801
|
if (index.errors.length > 0) process.exitCode = 1;
|
|
1732
1802
|
return;
|
|
1733
1803
|
}
|
|
1734
1804
|
|
|
1805
|
+
writeCheckPreviewNote();
|
|
1735
1806
|
process.stdout.write(renderCheck(index, config, { errorsOnly, noCollapse, verbose }));
|
|
1736
1807
|
if (index.errors.length > 0) process.exitCode = 1;
|
|
1737
1808
|
return;
|
|
@@ -1762,14 +1833,18 @@ async function main() {
|
|
|
1762
1833
|
die('Index generation is not configured. Add an `index` section to your dotmd.config.mjs.');
|
|
1763
1834
|
}
|
|
1764
1835
|
const print = args.includes('--print');
|
|
1765
|
-
const { renderIndexFile,
|
|
1836
|
+
const { renderIndexFile, writeRenderedIndex } = await import('../src/index-file.mjs');
|
|
1837
|
+
if (!print) {
|
|
1838
|
+
const { authorizeRepoGeneratedPath } = await import('../src/managed-path.mjs');
|
|
1839
|
+
authorizeRepoGeneratedPath(config.indexPath, config, { kind: 'Generated index destination' });
|
|
1840
|
+
}
|
|
1766
1841
|
const rendered = renderIndexFile(index, config);
|
|
1767
1842
|
if (print) {
|
|
1768
1843
|
process.stdout.write(rendered);
|
|
1769
1844
|
} else if (dryRun) {
|
|
1770
1845
|
process.stdout.write(`[dry-run] Would update ${config.indexPath}\n`);
|
|
1771
1846
|
} else {
|
|
1772
|
-
|
|
1847
|
+
writeRenderedIndex(() => buildIndex(config, { fast: true }), config);
|
|
1773
1848
|
process.stdout.write(`Updated ${config.indexPath}\n`);
|
|
1774
1849
|
}
|
|
1775
1850
|
return;
|
|
@@ -1816,60 +1891,24 @@ async function main() {
|
|
|
1816
1891
|
return;
|
|
1817
1892
|
}
|
|
1818
1893
|
|
|
1819
|
-
function compactDoc(d) {
|
|
1820
|
-
return {
|
|
1821
|
-
path: d.path,
|
|
1822
|
-
title: d.title,
|
|
1823
|
-
status: d.status,
|
|
1824
|
-
type: d.type,
|
|
1825
|
-
nextStep: d.nextStep ?? null,
|
|
1826
|
-
blockers: d.blockers ?? [],
|
|
1827
|
-
daysSinceUpdate: d.daysSinceUpdate ?? null,
|
|
1828
|
-
};
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
function buildCompactAgentContext(idx) {
|
|
1832
|
-
const activeStatuses = new Set(['in-session', 'active', 'ready', 'planned', 'awaiting', 'blocked']);
|
|
1833
|
-
const active = idx.docs.filter(d => d.type === 'plan' && activeStatuses.has(d.status));
|
|
1834
|
-
const stale = idx.docs.filter(d => d.isStale && !config.lifecycle.skipStaleFor.has(d.status));
|
|
1835
|
-
const awaiting = idx.docs.filter(d => d.status === 'awaiting');
|
|
1836
|
-
const blocked = idx.docs.filter(d => d.status === 'blocked' || d.blockers?.length);
|
|
1837
|
-
const pendingPrompts = idx.docs
|
|
1838
|
-
.filter(d => d.type === 'prompt' && d.status === 'pending' && !isArchivedPath(d.path, config))
|
|
1839
|
-
.sort((a, b) => (a.created ?? '').localeCompare(b.created ?? '') || (a.updated ?? '').localeCompare(b.updated ?? ''));
|
|
1840
|
-
return {
|
|
1841
|
-
generatedAt: new Date().toISOString(),
|
|
1842
|
-
countsByStatus: idx.countsByStatus,
|
|
1843
|
-
countsByType: idx.countsByType,
|
|
1844
|
-
errors: {
|
|
1845
|
-
count: idx.errors.length,
|
|
1846
|
-
items: idx.errors.slice(0, 10).map(e => ({ path: e.path, message: e.message })),
|
|
1847
|
-
},
|
|
1848
|
-
warnings: { count: idx.warnings.length },
|
|
1849
|
-
prompts: {
|
|
1850
|
-
pending: pendingPrompts.length,
|
|
1851
|
-
next: pendingPrompts[0] ? compactDoc(pendingPrompts[0]) : null,
|
|
1852
|
-
},
|
|
1853
|
-
plans: {
|
|
1854
|
-
active: active.slice(0, 12).map(compactDoc),
|
|
1855
|
-
awaiting: awaiting.slice(0, 8).map(compactDoc),
|
|
1856
|
-
blocked: blocked.slice(0, 8).map(compactDoc),
|
|
1857
|
-
stale: stale.slice(0, 12).map(compactDoc),
|
|
1858
|
-
},
|
|
1859
|
-
};
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
1894
|
if (command === 'agent-context') {
|
|
1863
|
-
|
|
1895
|
+
const { buildAgentContext } = await import('../src/agent-context.mjs');
|
|
1896
|
+
const skippedHooks = ['validate', 'transformDoc', 'formatSnapshot'].filter(name => typeof config.hooks?.[name] === 'function');
|
|
1897
|
+
process.stdout.write(JSON.stringify(buildAgentContext(index, config, {
|
|
1898
|
+
roots: rootArg ? [rootArg] : null,
|
|
1899
|
+
types: typeArg ? typeArg.split(',').map(value => value.trim()).filter(Boolean) : null,
|
|
1900
|
+
skippedHooks,
|
|
1901
|
+
}), null, 2) + '\n');
|
|
1864
1902
|
return;
|
|
1865
1903
|
}
|
|
1866
1904
|
|
|
1867
1905
|
if (command === 'briefing') {
|
|
1868
1906
|
if (args.includes('--json')) {
|
|
1907
|
+
const { statusMetadataFor } = await import('../src/status-metadata.mjs');
|
|
1869
1908
|
const plans = index.docs.filter(d => d.type === 'plan');
|
|
1870
1909
|
const docs = index.docs.filter(d => d.type === 'doc');
|
|
1871
1910
|
const research = index.docs.filter(d => d.type === 'research');
|
|
1872
|
-
const stale = index.docs.filter(d => d.isStale && !config.
|
|
1911
|
+
const stale = index.docs.filter(d => d.isStale && !statusMetadataFor(config, d.type, d.status)?.skipStale).length;
|
|
1873
1912
|
// Coordination hubs are runlists, not actionable plans — split them out of
|
|
1874
1913
|
// inSession/active into their own `runlists` array so the JSON mirrors the
|
|
1875
1914
|
// rendered briefing. Empty on repos with no coordination hubs.
|
|
@@ -1879,7 +1918,7 @@ async function main() {
|
|
|
1879
1918
|
const closedStatuses = new Set([...config.lifecycle.archiveStatuses, ...config.lifecycle.terminalStatuses]);
|
|
1880
1919
|
const isLiveHub = (d) => isHub(d) && !closedStatuses.has(d.status) && !isArchivedPath(d.path, config);
|
|
1881
1920
|
process.stdout.write(JSON.stringify({
|
|
1882
|
-
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 })) },
|
|
1921
|
+
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 })) },
|
|
1883
1922
|
docs: { total: docs.length, active: docs.filter(d => !config.lifecycle.terminalStatuses.has(d.status)).length },
|
|
1884
1923
|
research: { total: research.length, active: research.filter(d => d.status === 'active').length },
|
|
1885
1924
|
stale, errorCount: index.errors.length, warningCount: index.warnings.length,
|
|
@@ -1898,7 +1937,13 @@ async function main() {
|
|
|
1898
1937
|
|
|
1899
1938
|
if (args.includes('--json')) {
|
|
1900
1939
|
if (compact) {
|
|
1901
|
-
|
|
1940
|
+
const { buildAgentContext } = await import('../src/agent-context.mjs');
|
|
1941
|
+
const skippedHooks = ['validate', 'transformDoc', 'formatSnapshot'].filter(name => typeof config.hooks?.[name] === 'function');
|
|
1942
|
+
process.stdout.write(JSON.stringify(buildAgentContext(index, config, {
|
|
1943
|
+
roots: rootArg ? [rootArg] : null,
|
|
1944
|
+
types: typeArg ? typeArg.split(',').map(value => value.trim()).filter(Boolean) : null,
|
|
1945
|
+
skippedHooks,
|
|
1946
|
+
}), null, 2) + '\n');
|
|
1902
1947
|
return;
|
|
1903
1948
|
}
|
|
1904
1949
|
const byStatus = {};
|
|
@@ -1914,7 +1959,7 @@ async function main() {
|
|
|
1914
1959
|
byType[doc.type].push(doc);
|
|
1915
1960
|
}
|
|
1916
1961
|
}
|
|
1917
|
-
if (summarize) {
|
|
1962
|
+
if (summarize && !config._execution?.suppressSideEffects) {
|
|
1918
1963
|
const { summarizeDocBody } = await import('../src/ai.mjs');
|
|
1919
1964
|
const { extractFrontmatter } = await import('../src/frontmatter.mjs');
|
|
1920
1965
|
const { readFileSync } = await import('node:fs');
|
|
@@ -1933,9 +1978,13 @@ async function main() {
|
|
|
1933
1978
|
} catch { /* skip */ }
|
|
1934
1979
|
}
|
|
1935
1980
|
}
|
|
1936
|
-
const
|
|
1981
|
+
const { statusMetadataFor } = await import('../src/status-metadata.mjs');
|
|
1982
|
+
const stale = index.docs.filter(d => d.isStale && !statusMetadataFor(config, d.type, d.status)?.skipStale);
|
|
1937
1983
|
process.stdout.write(JSON.stringify({
|
|
1938
1984
|
generatedAt: new Date().toISOString(),
|
|
1985
|
+
...(summarize && config._execution?.suppressSideEffects
|
|
1986
|
+
? { summaryPreview: { status: 'skipped-preview', reason: 'side-effect-free preview' } }
|
|
1987
|
+
: {}),
|
|
1939
1988
|
docsByType: Object.keys(byType).length > 0 ? byType : undefined,
|
|
1940
1989
|
docsByStatus: byStatus,
|
|
1941
1990
|
countsByStatus: index.countsByStatus,
|
|
@@ -1969,15 +2018,7 @@ async function main() {
|
|
|
1969
2018
|
return;
|
|
1970
2019
|
}
|
|
1971
2020
|
|
|
1972
|
-
|
|
1973
|
-
const { KNOWN_COMMANDS } = await import('../src/commands.mjs');
|
|
1974
|
-
const matches = KNOWN_COMMANDS
|
|
1975
|
-
.map(c => ({ cmd: c, dist: levenshtein(command, c) }))
|
|
1976
|
-
.sort((a, b) => a.dist - b.dist);
|
|
1977
|
-
if (matches[0] && matches[0].dist <= 3) {
|
|
1978
|
-
die(`Unknown command: ${command}\n\nDid you mean \`dotmd ${matches[0].cmd}\`?`);
|
|
1979
|
-
}
|
|
1980
|
-
die(`Unknown command: ${command}\n\nRun \`dotmd --help\` for available commands.`);
|
|
2021
|
+
requireCommandPolicy(command, null);
|
|
1981
2022
|
}
|
|
1982
2023
|
|
|
1983
2024
|
// F17a: opt-in JSONL journal of every CLI invocation. The dispatch tail
|
|
@@ -1986,10 +2027,13 @@ async function main() {
|
|
|
1986
2027
|
// moment it's loaded, so even early dispatcher errors (after config) get
|
|
1987
2028
|
// journaled.
|
|
1988
2029
|
let _resolvedConfig = null;
|
|
2030
|
+
let _resolvedCommand = null;
|
|
2031
|
+
let _suppressObservability = false;
|
|
1989
2032
|
const _startMs = Date.now();
|
|
1990
2033
|
const _invocationArgs = process.argv.slice(2);
|
|
1991
2034
|
|
|
1992
2035
|
function _journalExit(err) {
|
|
2036
|
+
if (_suppressObservability || _resolvedCommand === 'hud' || _invocationArgs.includes('--dry-run') || _invocationArgs.includes('-n')) return;
|
|
1993
2037
|
try {
|
|
1994
2038
|
recordCliInvocation({
|
|
1995
2039
|
config: _resolvedConfig,
|
|
@@ -2020,7 +2064,7 @@ main()
|
|
|
2020
2064
|
// has already failed in this session within the lookup window. Lookup is
|
|
2021
2065
|
// a no-op when the journal is disabled or DOTMD_NO_HINTS=1.
|
|
2022
2066
|
try {
|
|
2023
|
-
const hint = findRepeatFailureHint(_invocationArgs, _resolvedConfig);
|
|
2067
|
+
const hint = findRepeatFailureHint(sanitizeTelemetryArgv(_invocationArgs), _resolvedConfig);
|
|
2024
2068
|
if (hint) out = `${out}\n\nTip: ${hint}`;
|
|
2025
2069
|
} catch { /* hint must never break error reporting */ }
|
|
2026
2070
|
process.stderr.write(`${out}\n`);
|