dotmd-cli 0.69.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 +238 -195
- 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/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 +3 -3
- 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 +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,16 +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
|
-
|
|
1048
|
+
The following publish in one atomic cooperating transaction:
|
|
1049
|
+
1. A resume prompt named resume-<plan-slug> (collision-safe: -2, -3, …),
|
|
1082
1050
|
stamped with a plan: link so consuming it re-claims the plan (see \`dotmd
|
|
1083
1051
|
use\`). The prompt is session-local — the next session's hud surfaces it;
|
|
1084
1052
|
never paste resume text into chat.
|
|
1085
1053
|
2. Releases the plan: one status flip, in-session → active by default
|
|
1086
1054
|
(--status to override, --note to record why in ## Version History).
|
|
1087
|
-
3.
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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.
|
|
1091
1062
|
|
|
1092
1063
|
Slug mode (no plan involved — "save a resume prompt for this"):
|
|
1093
1064
|
dotmd baton <slug> @/tmp/draft.md → saves resume-<slug>, touches NOTHING
|
|
@@ -1101,6 +1072,8 @@ Options:
|
|
|
1101
1072
|
--status <s> Target status for the plan (default: active; plan mode only)
|
|
1102
1073
|
--note "why" Append the reason to ## Version History (plan mode only)
|
|
1103
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
|
|
1104
1077
|
--dry-run, -n Preview without writing
|
|
1105
1078
|
|
|
1106
1079
|
Examples:
|
|
@@ -1215,8 +1188,10 @@ fix is to delete the explicit \`lifecycle\` block so flags take effect.`,
|
|
|
1215
1188
|
|
|
1216
1189
|
bulk: `dotmd bulk archive <f1> <f2> ... — archive multiple files at once
|
|
1217
1190
|
|
|
1218
|
-
Archives each file: sets status to
|
|
1219
|
-
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.
|
|
1220
1195
|
|
|
1221
1196
|
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
1222
1197
|
|
|
@@ -1338,6 +1313,58 @@ Pass file paths as positional args to scope to those files only; otherwise
|
|
|
1338
1313
|
the whole docs tree is scanned.`,
|
|
1339
1314
|
};
|
|
1340
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
|
+
|
|
1341
1368
|
async function main() {
|
|
1342
1369
|
const args = process.argv.slice(2);
|
|
1343
1370
|
|
|
@@ -1347,27 +1374,11 @@ async function main() {
|
|
|
1347
1374
|
return;
|
|
1348
1375
|
}
|
|
1349
1376
|
|
|
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);
|
|
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;
|
|
1371
1382
|
|
|
1372
1383
|
// Reconstruct the active global flags for proxy commands (e.g. `watch`) that
|
|
1373
1384
|
// re-invoke the CLI in a child process and must propagate them through.
|
|
@@ -1422,28 +1433,60 @@ async function main() {
|
|
|
1422
1433
|
return;
|
|
1423
1434
|
}
|
|
1424
1435
|
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
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;
|
|
1432
1448
|
|
|
1433
1449
|
// Per-command help
|
|
1434
1450
|
if (args.includes('--help') || args.includes('-h')) {
|
|
1435
|
-
|
|
1451
|
+
requireCommandPolicy(command, dispatchPolicy);
|
|
1452
|
+
process.stdout.write(`${HELP[command] ?? commandUsage(command)}\n`);
|
|
1436
1453
|
return;
|
|
1437
1454
|
}
|
|
1438
1455
|
|
|
1439
1456
|
if (command === 'completions') {
|
|
1457
|
+
requireCommandPolicy(command, dispatchPolicy);
|
|
1458
|
+
try { restArgs = validateCommandArgs(command, restArgs); } catch (err) { die(err.message); }
|
|
1440
1459
|
const { runCompletions } = await import('../src/completions.mjs');
|
|
1441
1460
|
runCompletions(restArgs);
|
|
1442
1461
|
return;
|
|
1443
1462
|
}
|
|
1444
1463
|
|
|
1445
|
-
|
|
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
|
+
}
|
|
1446
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
|
+
}
|
|
1447
1490
|
|
|
1448
1491
|
// Init — runInit re-resolves the config from disk internally (after any
|
|
1449
1492
|
// starter-config write), so we don't need to pre-pass it.
|
|
@@ -1479,9 +1522,25 @@ async function main() {
|
|
|
1479
1522
|
process.stderr.write(`Repo root: ${config.repoRoot}\n`);
|
|
1480
1523
|
}
|
|
1481
1524
|
|
|
1482
|
-
validateKnownFlags(command, restArgs, config);
|
|
1483
|
-
|
|
1484
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
|
+
}
|
|
1485
1544
|
if (config.presets[command]) {
|
|
1486
1545
|
const { buildIndex } = await import('../src/index.mjs');
|
|
1487
1546
|
const { runQuery } = await import('../src/query.mjs');
|
|
@@ -1587,16 +1646,15 @@ async function main() {
|
|
|
1587
1646
|
if (command === 'health') { const { runHealth } = await import('../src/health.mjs'); runHealth(restArgs, config); return; }
|
|
1588
1647
|
if (command === 'glossary') { const { runGlossary } = await import('../src/glossary.mjs'); runGlossary(restArgs, config); return; }
|
|
1589
1648
|
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
1649
|
|
|
1592
1650
|
// Lifecycle commands
|
|
1593
1651
|
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; }
|
|
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; }
|
|
1596
1654
|
if (command === 'misuse') { const { runMisuse } = await import('../src/misuse-read.mjs'); runMisuse(restArgs, config); return; }
|
|
1597
1655
|
if (command === 'journal') { const { runJournal } = await import('../src/journal-read.mjs'); runJournal(restArgs, config); return; }
|
|
1598
1656
|
if (command === 'pickup' || command === 'unpickup' || command === 'release' || command === 'finish') {
|
|
1599
|
-
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`);
|
|
1600
1658
|
}
|
|
1601
1659
|
if (command === 'runlist') { const { runRunlist } = await import('../src/runlist.mjs'); await runRunlist(restArgs, config, { dryRun }); return; }
|
|
1602
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.'); }
|
|
@@ -1625,10 +1683,7 @@ async function main() {
|
|
|
1625
1683
|
// auto-fix path — sub-modes (--statuses, --migrate-template,
|
|
1626
1684
|
// --migrate-prompts) keep their existing "write unless --dry-run"
|
|
1627
1685
|
// 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);
|
|
1686
|
+
const doctorDryRun = doctorSubMode ? dryRun : (dryRun || !doctorExplicitApply);
|
|
1632
1687
|
const filtered = restArgs.filter(a => a !== '--apply' && a !== '--yes');
|
|
1633
1688
|
const { runDoctor } = await import('../src/doctor.mjs');
|
|
1634
1689
|
runDoctor(filtered, config, { dryRun: doctorDryRun });
|
|
@@ -1647,7 +1702,10 @@ async function main() {
|
|
|
1647
1702
|
// `query`, `index --print`, ...) stay opt-out so they never mutate disk.
|
|
1648
1703
|
const checkHasPathScope = command === 'check' && restArgs.some(arg => !arg.startsWith('-'));
|
|
1649
1704
|
const AUTO_HEAL_INDEX_COMMANDS = new Set(['check']);
|
|
1650
|
-
const index = buildIndex(config, {
|
|
1705
|
+
const index = buildIndex(config, {
|
|
1706
|
+
autoHealIndex: AUTO_HEAL_INDEX_COMMANDS.has(command) && !checkHasPathScope && !dryRun,
|
|
1707
|
+
invokeHooks: !suppressSideEffects,
|
|
1708
|
+
});
|
|
1651
1709
|
|
|
1652
1710
|
applyIndexFilters(index);
|
|
1653
1711
|
|
|
@@ -1677,6 +1735,31 @@ async function main() {
|
|
|
1677
1735
|
const noCollapse = args.includes('--no-collapse');
|
|
1678
1736
|
const verbose = args.includes('--verbose');
|
|
1679
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
|
+
};
|
|
1680
1763
|
|
|
1681
1764
|
if (fix && checkTargets.length > 0) {
|
|
1682
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.');
|
|
@@ -1690,9 +1773,8 @@ async function main() {
|
|
|
1690
1773
|
runLint(['--fix'], config, { dryRun });
|
|
1691
1774
|
if (config.indexPath) {
|
|
1692
1775
|
if (!dryRun) {
|
|
1693
|
-
const {
|
|
1694
|
-
|
|
1695
|
-
wi(rif(freshIndex, config), config);
|
|
1776
|
+
const { writeRenderedIndex } = await import('../src/index-file.mjs');
|
|
1777
|
+
writeRenderedIndex(() => buildIndex(config, { fast: true }), config);
|
|
1696
1778
|
process.stdout.write('Index regenerated.\n');
|
|
1697
1779
|
} else {
|
|
1698
1780
|
process.stdout.write('[dry-run] Would regenerate index.\n');
|
|
@@ -1703,15 +1785,9 @@ async function main() {
|
|
|
1703
1785
|
applyIndexFilters(freshIndex);
|
|
1704
1786
|
applyPathScopeToIndex(freshIndex, config, checkTargets);
|
|
1705
1787
|
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');
|
|
1788
|
+
process.stdout.write(JSON.stringify(checkJson(freshIndex), null, 2) + '\n');
|
|
1714
1789
|
} else {
|
|
1790
|
+
writeCheckPreviewNote();
|
|
1715
1791
|
process.stdout.write('\n' + renderCheck(freshIndex, config, { errorsOnly, noCollapse, verbose }));
|
|
1716
1792
|
}
|
|
1717
1793
|
if (freshIndex.errors.length > 0) process.exitCode = 1;
|
|
@@ -1721,18 +1797,12 @@ async function main() {
|
|
|
1721
1797
|
applyPathScopeToIndex(index, config, checkTargets);
|
|
1722
1798
|
|
|
1723
1799
|
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');
|
|
1800
|
+
process.stdout.write(JSON.stringify(checkJson(index), null, 2) + '\n');
|
|
1732
1801
|
if (index.errors.length > 0) process.exitCode = 1;
|
|
1733
1802
|
return;
|
|
1734
1803
|
}
|
|
1735
1804
|
|
|
1805
|
+
writeCheckPreviewNote();
|
|
1736
1806
|
process.stdout.write(renderCheck(index, config, { errorsOnly, noCollapse, verbose }));
|
|
1737
1807
|
if (index.errors.length > 0) process.exitCode = 1;
|
|
1738
1808
|
return;
|
|
@@ -1763,14 +1833,18 @@ async function main() {
|
|
|
1763
1833
|
die('Index generation is not configured. Add an `index` section to your dotmd.config.mjs.');
|
|
1764
1834
|
}
|
|
1765
1835
|
const print = args.includes('--print');
|
|
1766
|
-
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
|
+
}
|
|
1767
1841
|
const rendered = renderIndexFile(index, config);
|
|
1768
1842
|
if (print) {
|
|
1769
1843
|
process.stdout.write(rendered);
|
|
1770
1844
|
} else if (dryRun) {
|
|
1771
1845
|
process.stdout.write(`[dry-run] Would update ${config.indexPath}\n`);
|
|
1772
1846
|
} else {
|
|
1773
|
-
|
|
1847
|
+
writeRenderedIndex(() => buildIndex(config, { fast: true }), config);
|
|
1774
1848
|
process.stdout.write(`Updated ${config.indexPath}\n`);
|
|
1775
1849
|
}
|
|
1776
1850
|
return;
|
|
@@ -1817,60 +1891,24 @@ async function main() {
|
|
|
1817
1891
|
return;
|
|
1818
1892
|
}
|
|
1819
1893
|
|
|
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
1894
|
if (command === 'agent-context') {
|
|
1864
|
-
|
|
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');
|
|
1865
1902
|
return;
|
|
1866
1903
|
}
|
|
1867
1904
|
|
|
1868
1905
|
if (command === 'briefing') {
|
|
1869
1906
|
if (args.includes('--json')) {
|
|
1907
|
+
const { statusMetadataFor } = await import('../src/status-metadata.mjs');
|
|
1870
1908
|
const plans = index.docs.filter(d => d.type === 'plan');
|
|
1871
1909
|
const docs = index.docs.filter(d => d.type === 'doc');
|
|
1872
1910
|
const research = index.docs.filter(d => d.type === 'research');
|
|
1873
|
-
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;
|
|
1874
1912
|
// Coordination hubs are runlists, not actionable plans — split them out of
|
|
1875
1913
|
// inSession/active into their own `runlists` array so the JSON mirrors the
|
|
1876
1914
|
// rendered briefing. Empty on repos with no coordination hubs.
|
|
@@ -1880,7 +1918,7 @@ async function main() {
|
|
|
1880
1918
|
const closedStatuses = new Set([...config.lifecycle.archiveStatuses, ...config.lifecycle.terminalStatuses]);
|
|
1881
1919
|
const isLiveHub = (d) => isHub(d) && !closedStatuses.has(d.status) && !isArchivedPath(d.path, config);
|
|
1882
1920
|
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 })) },
|
|
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 })) },
|
|
1884
1922
|
docs: { total: docs.length, active: docs.filter(d => !config.lifecycle.terminalStatuses.has(d.status)).length },
|
|
1885
1923
|
research: { total: research.length, active: research.filter(d => d.status === 'active').length },
|
|
1886
1924
|
stale, errorCount: index.errors.length, warningCount: index.warnings.length,
|
|
@@ -1899,7 +1937,13 @@ async function main() {
|
|
|
1899
1937
|
|
|
1900
1938
|
if (args.includes('--json')) {
|
|
1901
1939
|
if (compact) {
|
|
1902
|
-
|
|
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');
|
|
1903
1947
|
return;
|
|
1904
1948
|
}
|
|
1905
1949
|
const byStatus = {};
|
|
@@ -1915,7 +1959,7 @@ async function main() {
|
|
|
1915
1959
|
byType[doc.type].push(doc);
|
|
1916
1960
|
}
|
|
1917
1961
|
}
|
|
1918
|
-
if (summarize) {
|
|
1962
|
+
if (summarize && !config._execution?.suppressSideEffects) {
|
|
1919
1963
|
const { summarizeDocBody } = await import('../src/ai.mjs');
|
|
1920
1964
|
const { extractFrontmatter } = await import('../src/frontmatter.mjs');
|
|
1921
1965
|
const { readFileSync } = await import('node:fs');
|
|
@@ -1934,9 +1978,13 @@ async function main() {
|
|
|
1934
1978
|
} catch { /* skip */ }
|
|
1935
1979
|
}
|
|
1936
1980
|
}
|
|
1937
|
-
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);
|
|
1938
1983
|
process.stdout.write(JSON.stringify({
|
|
1939
1984
|
generatedAt: new Date().toISOString(),
|
|
1985
|
+
...(summarize && config._execution?.suppressSideEffects
|
|
1986
|
+
? { summaryPreview: { status: 'skipped-preview', reason: 'side-effect-free preview' } }
|
|
1987
|
+
: {}),
|
|
1940
1988
|
docsByType: Object.keys(byType).length > 0 ? byType : undefined,
|
|
1941
1989
|
docsByStatus: byStatus,
|
|
1942
1990
|
countsByStatus: index.countsByStatus,
|
|
@@ -1970,15 +2018,7 @@ async function main() {
|
|
|
1970
2018
|
return;
|
|
1971
2019
|
}
|
|
1972
2020
|
|
|
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.`);
|
|
2021
|
+
requireCommandPolicy(command, null);
|
|
1982
2022
|
}
|
|
1983
2023
|
|
|
1984
2024
|
// F17a: opt-in JSONL journal of every CLI invocation. The dispatch tail
|
|
@@ -1987,10 +2027,13 @@ async function main() {
|
|
|
1987
2027
|
// moment it's loaded, so even early dispatcher errors (after config) get
|
|
1988
2028
|
// journaled.
|
|
1989
2029
|
let _resolvedConfig = null;
|
|
2030
|
+
let _resolvedCommand = null;
|
|
2031
|
+
let _suppressObservability = false;
|
|
1990
2032
|
const _startMs = Date.now();
|
|
1991
2033
|
const _invocationArgs = process.argv.slice(2);
|
|
1992
2034
|
|
|
1993
2035
|
function _journalExit(err) {
|
|
2036
|
+
if (_suppressObservability || _resolvedCommand === 'hud' || _invocationArgs.includes('--dry-run') || _invocationArgs.includes('-n')) return;
|
|
1994
2037
|
try {
|
|
1995
2038
|
recordCliInvocation({
|
|
1996
2039
|
config: _resolvedConfig,
|
|
@@ -2021,7 +2064,7 @@ main()
|
|
|
2021
2064
|
// has already failed in this session within the lookup window. Lookup is
|
|
2022
2065
|
// a no-op when the journal is disabled or DOTMD_NO_HINTS=1.
|
|
2023
2066
|
try {
|
|
2024
|
-
const hint = findRepeatFailureHint(_invocationArgs, _resolvedConfig);
|
|
2067
|
+
const hint = findRepeatFailureHint(sanitizeTelemetryArgv(_invocationArgs), _resolvedConfig);
|
|
2025
2068
|
if (hint) out = `${out}\n\nTip: ${hint}`;
|
|
2026
2069
|
} catch { /* hint must never break error reporting */ }
|
|
2027
2070
|
process.stderr.write(`${out}\n`);
|