dotmd-cli 0.39.8 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/dotmd.mjs +33 -1
- package/package.json +1 -1
- package/src/frontmatter-fix.mjs +2 -2
- package/src/hud.mjs +5 -2
- package/src/index.mjs +21 -13
- package/src/new.mjs +101 -6
- package/src/prompts.mjs +5 -1
- package/src/surfaces.mjs +28 -0
- package/src/validate.mjs +6 -3
package/bin/dotmd.mjs
CHANGED
|
@@ -35,6 +35,7 @@ Analyze:
|
|
|
35
35
|
deps [file] [--json] Dependency tree or overview
|
|
36
36
|
modules [--sort cleanup] [--json] Module dashboard (plans grouped by module)
|
|
37
37
|
module <name> [--json] Plans for one module, grouped by status
|
|
38
|
+
surfaces [--json] List configured surface taxonomy
|
|
38
39
|
unblocks <file> [--json] Show what completes when this doc ships
|
|
39
40
|
diff [file] [--summarize] Show changes since last updated date
|
|
40
41
|
summary <file> [--json] AI summary of a document
|
|
@@ -492,6 +493,17 @@ Options:
|
|
|
492
493
|
|
|
493
494
|
Unknown module name suggests close matches (or lists what's available).`,
|
|
494
495
|
|
|
496
|
+
surfaces: `dotmd surfaces — list configured surface taxonomy
|
|
497
|
+
|
|
498
|
+
Prints the values accepted in \`surfaces:\` frontmatter, one per line.
|
|
499
|
+
Source: \`config.taxonomy.surfaces\` in dotmd.config.mjs.
|
|
500
|
+
|
|
501
|
+
Options:
|
|
502
|
+
--json Machine-readable shape: { surfaces: [...] }
|
|
503
|
+
|
|
504
|
+
When the project has no taxonomy configured, any surface value is accepted —
|
|
505
|
+
the command says so instead of printing an empty list.`,
|
|
506
|
+
|
|
495
507
|
doctor: `dotmd doctor — auto-fix everything in one pass
|
|
496
508
|
|
|
497
509
|
Runs in sequence: fix broken references, lint --fix, sync dates from
|
|
@@ -760,12 +772,17 @@ Prompts are documents with \`type: prompt\`, typically saved under
|
|
|
760
772
|
docs/prompts/. They seed future Claude sessions; consuming a prompt
|
|
761
773
|
prints its body to stdout and atomically archives it (one-shot).
|
|
762
774
|
|
|
775
|
+
\`dotmd prompt\` (singular) is an alias for \`dotmd prompts\` — every
|
|
776
|
+
subcommand below works under either spelling.
|
|
777
|
+
|
|
763
778
|
Subcommands:
|
|
764
779
|
list List pending prompts (default)
|
|
765
780
|
next Consume the oldest pending prompt:
|
|
766
781
|
print body to stdout, flip status to archived
|
|
767
782
|
use <file-or-slug> Consume a specific prompt (same as next, but
|
|
768
783
|
targets the named prompt instead of picking oldest)
|
|
784
|
+
resume <file-or-slug> Alias for \`use\` — same behavior, easier name
|
|
785
|
+
when continuing a session
|
|
769
786
|
archive <file-or-slug> Archive a prompt without printing its body
|
|
770
787
|
shelve <file-or-slug> Park a prompt (status → shelved): kept in list,
|
|
771
788
|
hidden from hud/briefing pending surfaces, skipped
|
|
@@ -792,6 +809,8 @@ Examples:
|
|
|
792
809
|
claude "$(dotmd prompts next)" # consume oldest pending + run claude
|
|
793
810
|
claude "$(dotmd prompts use resume-foo)" # by slug
|
|
794
811
|
claude "$(dotmd prompts use docs/prompts/foo.md)" # by path
|
|
812
|
+
claude "$(dotmd prompts resume resume-foo)" # \`resume\` is an alias for \`use\`
|
|
813
|
+
dotmd prompt list # singular alias for \`dotmd prompts list\`
|
|
795
814
|
|
|
796
815
|
dotmd prompts next --dry-run # preview without consuming
|
|
797
816
|
dotmd prompts archive old-thing
|
|
@@ -964,7 +983,7 @@ the whole docs tree is scanned.`,
|
|
|
964
983
|
|
|
965
984
|
async function main() {
|
|
966
985
|
const args = process.argv.slice(2);
|
|
967
|
-
|
|
986
|
+
let command = args[0] ?? 'list';
|
|
968
987
|
|
|
969
988
|
// Pre-config flags
|
|
970
989
|
if (args.includes('--version') || args.includes('-v')) {
|
|
@@ -985,6 +1004,14 @@ async function main() {
|
|
|
985
1004
|
return;
|
|
986
1005
|
}
|
|
987
1006
|
|
|
1007
|
+
// Singular-form alias for the prompts subcommand namespace. Trivial
|
|
1008
|
+
// no-collision collapse — `prompt` was previously "unknown command", now
|
|
1009
|
+
// routes everywhere `prompts` does (incl. per-command --help below, and the
|
|
1010
|
+
// subcommand dispatch at the `prompts` branch in the chain). The other
|
|
1011
|
+
// singular/plural pairs (`plan`/`plans`, `module`/`modules`,
|
|
1012
|
+
// `status`/`statuses`) are deliberately kept distinct — see F20 plan.
|
|
1013
|
+
if (command === 'prompt') command = 'prompts';
|
|
1014
|
+
|
|
988
1015
|
// Per-command help
|
|
989
1016
|
if (args.includes('--help') || args.includes('-h')) {
|
|
990
1017
|
process.stdout.write(`${HELP[command] ?? HELP._main}\n`);
|
|
@@ -1302,6 +1329,11 @@ async function main() {
|
|
|
1302
1329
|
}
|
|
1303
1330
|
return;
|
|
1304
1331
|
}
|
|
1332
|
+
if (command === 'surfaces') {
|
|
1333
|
+
const { runSurfaces } = await import('../src/surfaces.mjs');
|
|
1334
|
+
runSurfaces(restArgs, config);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1305
1337
|
if (command === 'briefing') {
|
|
1306
1338
|
if (args.includes('--json')) {
|
|
1307
1339
|
const plans = index.docs.filter(d => d.type === 'plan');
|
package/package.json
CHANGED
package/src/frontmatter-fix.mjs
CHANGED
|
@@ -9,8 +9,8 @@ import { bold, green, dim } from './color.mjs';
|
|
|
9
9
|
// are deliberately under the cap so a fix-then-edit cycle doesn't reintroduce
|
|
10
10
|
// the warning on the next few-word touch-up.
|
|
11
11
|
const FIELDS = [
|
|
12
|
-
{ name: 'current_state', cap:
|
|
13
|
-
{ name: 'next_step', cap: 300,
|
|
12
|
+
{ name: 'current_state', cap: 1500, target: 1200, heading: '## Current State' },
|
|
13
|
+
{ name: 'next_step', cap: 300, target: 200, heading: '## Next Step' },
|
|
14
14
|
];
|
|
15
15
|
|
|
16
16
|
export function runFrontmatterFix(config, opts = {}) {
|
package/src/hud.mjs
CHANGED
|
@@ -76,10 +76,13 @@ export function buildHud(config) {
|
|
|
76
76
|
// Validation error count — hud's "silent when clean" contract should treat
|
|
77
77
|
// `check` errors as not-clean. Without this, a SessionStart hook firing hud
|
|
78
78
|
// can leave the agent with no visible signal that a check is failing.
|
|
79
|
-
//
|
|
79
|
+
// `errorsOnly: true` skips warning-only cross-doc passes (git staleness,
|
|
80
|
+
// bidirectional refs, claude-commands) that hud never reads — ~6× faster on
|
|
81
|
+
// SessionStart for platform-scale corpora. Per-file validation + checkIndex
|
|
82
|
+
// still run, so the error count matches `dotmd check`'s.
|
|
80
83
|
let errors = 0;
|
|
81
84
|
try {
|
|
82
|
-
const index = buildIndex(config);
|
|
85
|
+
const index = buildIndex(config, { errorsOnly: true });
|
|
83
86
|
errors = index.errors.length;
|
|
84
87
|
} catch { /* swallow — bad config shouldn't break the SessionStart hook */ }
|
|
85
88
|
|
package/src/index.mjs
CHANGED
|
@@ -7,14 +7,22 @@ import { validateDoc, validatePlanShape, validateDocShape, checkBidirectionalRef
|
|
|
7
7
|
import { checkIndex } from './index-file.mjs';
|
|
8
8
|
import { checkClaudeCommands } from './claude-commands.mjs';
|
|
9
9
|
|
|
10
|
-
// `fast: true` skips every pass that
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
10
|
+
// `fast: true` skips every pass that produces warnings/errors — the rendered
|
|
11
|
+
// index file consumes only status/title/snapshot/etc., not the validation
|
|
12
|
+
// output. Use it from `regenIndex` (post-mutation index refresh) where
|
|
13
|
+
// validation has already run elsewhere (or will, next time the user runs
|
|
14
|
+
// `dotmd check`). Saves the full-repo `git log` scan in `checkGitStaleness`
|
|
15
|
+
// plus the bidirectional ref walk + claude-commands check.
|
|
16
|
+
//
|
|
17
|
+
// `errorsOnly: true` runs every error-producing pass (per-file `validateDoc`,
|
|
18
|
+
// `checkIndex`, the `validate` hook) but skips the warning-only cross-doc
|
|
19
|
+
// passes (bidirectional refs, runlist back-pointers, git staleness, claude
|
|
20
|
+
// commands). Use it from `dotmd hud` — the SessionStart hook only renders the
|
|
21
|
+
// error COUNT, so the warning-only passes are pure overhead there. Preserves
|
|
22
|
+
// the invariant that hud's "✗ N validation errors" line matches `dotmd check`.
|
|
16
23
|
export function buildIndex(config, opts = {}) {
|
|
17
|
-
const { fast = false } = opts;
|
|
24
|
+
const { fast = false, errorsOnly = false } = opts;
|
|
25
|
+
const skipWarningOnlyChecks = fast || errorsOnly;
|
|
18
26
|
const docs = collectDocFiles(config).map(f => parseDocFile(f, config, { fast }));
|
|
19
27
|
if (!fast) {
|
|
20
28
|
// Per-file validation (validateDoc) ran during parse without sibling
|
|
@@ -86,13 +94,13 @@ export function buildIndex(config, opts = {}) {
|
|
|
86
94
|
countsByType[type][doc.status] = (countsByType[type][doc.status] ?? 0) + 1;
|
|
87
95
|
}
|
|
88
96
|
|
|
89
|
-
if (!fast) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
97
|
+
if (!fast && config.indexPath) {
|
|
98
|
+
const indexCheck = checkIndex(transformedDocs, config);
|
|
99
|
+
warnings.push(...indexCheck.warnings);
|
|
100
|
+
errors.push(...indexCheck.errors);
|
|
101
|
+
}
|
|
95
102
|
|
|
103
|
+
if (!skipWarningOnlyChecks) {
|
|
96
104
|
const refCheck = checkBidirectionalReferences(transformedDocs, config);
|
|
97
105
|
warnings.push(...refCheck.warnings);
|
|
98
106
|
|
package/src/new.mjs
CHANGED
|
@@ -5,10 +5,23 @@ import { toRepoPath, die, warn, nowIso, emitFilesFooter } from './util.mjs';
|
|
|
5
5
|
import { green, dim, bold } from './color.mjs';
|
|
6
6
|
import { isInteractive, promptText } from './prompt.mjs';
|
|
7
7
|
import { regenIndex } from './lifecycle.mjs';
|
|
8
|
+
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
8
9
|
|
|
9
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
11
12
|
|
|
13
|
+
// Surface-taxonomy hint emitted above the `surfaces:` line in scaffolded docs.
|
|
14
|
+
// Discoverable-by-default: the author sees valid values without leaving the file
|
|
15
|
+
// and without grepping sibling docs (issue #12 trap 1). When the project has no
|
|
16
|
+
// configured taxonomy, fall back to a bare `surfaces:` line.
|
|
17
|
+
function surfacesScaffold(ctx) {
|
|
18
|
+
const valid = ctx?.validSurfaces;
|
|
19
|
+
if (Array.isArray(valid) && valid.length > 0) {
|
|
20
|
+
return `# surfaces — valid: ${valid.join(', ')}\nsurfaces:`;
|
|
21
|
+
}
|
|
22
|
+
return 'surfaces:';
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
const BUILTIN_TEMPLATES = {
|
|
13
26
|
doc: {
|
|
14
27
|
description: 'Reference doc, design note, module overview — build-up shape lite',
|
|
@@ -17,13 +30,15 @@ const BUILTIN_TEMPLATES = {
|
|
|
17
30
|
// it lands in the Overview section. Without it, Overview is left blank
|
|
18
31
|
// and the user fills it in.
|
|
19
32
|
acceptsBody: true,
|
|
20
|
-
frontmatter: (s, d) => [
|
|
33
|
+
frontmatter: (s, d, ctx) => [
|
|
21
34
|
'type: doc',
|
|
22
35
|
`status: ${s}`,
|
|
23
36
|
`created: ${d}`,
|
|
24
37
|
`updated: ${d}`,
|
|
38
|
+
'# modules — real module name(s), or `none` for platform/infra docs',
|
|
25
39
|
'modules:',
|
|
26
|
-
'
|
|
40
|
+
' - none',
|
|
41
|
+
surfacesScaffold(ctx),
|
|
27
42
|
'domain:',
|
|
28
43
|
'audience: internal',
|
|
29
44
|
'related_plans:',
|
|
@@ -54,13 +69,15 @@ ${ctx?.bodyInput?.trim() ?? ''}
|
|
|
54
69
|
// Body input lands in the Problem section. Plans don't have an Overview;
|
|
55
70
|
// Problem is the established opening section in the build-up shape.
|
|
56
71
|
acceptsBody: true,
|
|
57
|
-
frontmatter: (s, d) => [
|
|
72
|
+
frontmatter: (s, d, ctx) => [
|
|
58
73
|
'type: plan',
|
|
59
74
|
`status: ${s}`,
|
|
60
75
|
`created: ${d}`,
|
|
61
76
|
`updated: ${d}`,
|
|
62
|
-
|
|
77
|
+
surfacesScaffold(ctx),
|
|
78
|
+
'# modules — real module name(s), or `none` for tooling/infra plans',
|
|
63
79
|
'modules:',
|
|
80
|
+
' - none',
|
|
64
81
|
'domain:',
|
|
65
82
|
'audience: internal',
|
|
66
83
|
'parent_plan:',
|
|
@@ -164,6 +181,68 @@ Status markers (put in heading text):
|
|
|
164
181
|
},
|
|
165
182
|
};
|
|
166
183
|
|
|
184
|
+
// Body inputs from agents often arrive as a full document (frontmatter + body)
|
|
185
|
+
// written to a tempfile and passed via `@path` or stdin. Without this split,
|
|
186
|
+
// `dotmd new` would prepend its scaffold frontmatter and treat the input's
|
|
187
|
+
// frontmatter as literal body content — resulting in two `---` blocks and a
|
|
188
|
+
// duplicated title. We instead parse the leading block (if any), merge its
|
|
189
|
+
// keys onto the scaffold, and use only what follows as body. See issue #12
|
|
190
|
+
// trap 4. Returns `{ frontmatter: object|null, body: string }`.
|
|
191
|
+
function splitBodyFrontmatter(rawBody) {
|
|
192
|
+
if (!rawBody || typeof rawBody !== 'string') return { frontmatter: null, body: rawBody };
|
|
193
|
+
if (!rawBody.startsWith('---\n')) return { frontmatter: null, body: rawBody };
|
|
194
|
+
const { frontmatter: fmText, body } = extractFrontmatter(rawBody);
|
|
195
|
+
if (!fmText) return { frontmatter: null, body: rawBody };
|
|
196
|
+
const parsed = parseSimpleFrontmatter(fmText);
|
|
197
|
+
return { frontmatter: parsed, body };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Serialize a single frontmatter key/value pair to a YAML block. Mirrors the
|
|
201
|
+
// scaffold's shape so merged output reads naturally next to scaffold defaults.
|
|
202
|
+
function serializeFmEntry(key, value) {
|
|
203
|
+
if (value === null || value === undefined || value === '') return `${key}:`;
|
|
204
|
+
if (Array.isArray(value)) {
|
|
205
|
+
if (value.length === 0) return `${key}:`;
|
|
206
|
+
return `${key}:\n${value.map(v => ` - ${v}`).join('\n')}`;
|
|
207
|
+
}
|
|
208
|
+
if (typeof value === 'string' && value.includes('\n')) {
|
|
209
|
+
const indented = value.split('\n').map(l => ` ${l}`).join('\n');
|
|
210
|
+
return `${key}: |\n${indented}`;
|
|
211
|
+
}
|
|
212
|
+
return `${key}: ${value}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Replace each key in `overrides` within the scaffold-generated frontmatter
|
|
216
|
+
// string. Keys not present in the scaffold are appended. `type:` is never
|
|
217
|
+
// overwritten — the CLI's type arg wins (warning emitted on conflict).
|
|
218
|
+
function mergeBodyFrontmatter(scaffoldFm, overrides, cliType) {
|
|
219
|
+
if (!overrides || Object.keys(overrides).length === 0) return scaffoldFm;
|
|
220
|
+
let fm = scaffoldFm;
|
|
221
|
+
const appended = [];
|
|
222
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
223
|
+
if (key === 'type') {
|
|
224
|
+
if (cliType && value && value !== cliType) {
|
|
225
|
+
warn(`Body frontmatter declares \`type: ${value}\` but CLI arg is \`${cliType}\`; using \`${cliType}\`.`);
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (key === 'created' || key === 'updated') continue; // scaffold owns timestamps
|
|
230
|
+
const serialized = serializeFmEntry(key, value);
|
|
231
|
+
// Match `key:` line + any indented continuation (block-array items or
|
|
232
|
+
// block-scalar bodies). Indented lines start with whitespace; scaffold keys
|
|
233
|
+
// never do, so this consumes only the right slice.
|
|
234
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
235
|
+
const re = new RegExp(`^${escaped}:.*(\\n[ \\t]+.*)*`, 'm');
|
|
236
|
+
if (re.test(fm)) {
|
|
237
|
+
fm = fm.replace(re, serialized);
|
|
238
|
+
} else {
|
|
239
|
+
appended.push(serialized);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (appended.length > 0) fm = fm + '\n' + appended.join('\n');
|
|
243
|
+
return fm;
|
|
244
|
+
}
|
|
245
|
+
|
|
167
246
|
function readBodyInput(source) {
|
|
168
247
|
if (source === '-') {
|
|
169
248
|
try { return readFileSync(0, 'utf8'); } catch (err) { die(`Could not read body from stdin: ${err.message}`); }
|
|
@@ -261,6 +340,20 @@ export async function runNew(argv, config, opts = {}) {
|
|
|
261
340
|
bodyInputSource = bodyArg === '-' ? 'stdin (`-`)' : (bodyArg.startsWith('@') ? `file (\`${bodyArg}\`)` : 'inline body argument');
|
|
262
341
|
}
|
|
263
342
|
|
|
343
|
+
// If the body input has a leading `---…---` frontmatter block, lift its keys
|
|
344
|
+
// out so they override scaffold defaults; only the content after the closing
|
|
345
|
+
// `---` is treated as body. The natural agent pattern is to draft a full doc
|
|
346
|
+
// to a tempfile and pass `@path` — without this, the scaffold ends up with
|
|
347
|
+
// two `---` blocks. See issue #12 trap 4.
|
|
348
|
+
let bodyFrontmatter = null;
|
|
349
|
+
if (bodyInput !== null) {
|
|
350
|
+
const split = splitBodyFrontmatter(bodyInput);
|
|
351
|
+
if (split.frontmatter) {
|
|
352
|
+
bodyFrontmatter = split.frontmatter;
|
|
353
|
+
bodyInput = split.body;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
264
357
|
if (template.requiresBody && (!bodyInput || !bodyInput.trim())) {
|
|
265
358
|
die(`\`${typeName}\` template requires a body. Pass inline, --message "...", - for stdin, or @path for a file.`);
|
|
266
359
|
}
|
|
@@ -359,11 +452,13 @@ export async function runNew(argv, config, opts = {}) {
|
|
|
359
452
|
|
|
360
453
|
// Generate content
|
|
361
454
|
let content;
|
|
362
|
-
const
|
|
455
|
+
const validSurfaces = config.raw?.taxonomy?.surfaces ?? (config.validSurfaces ? [...config.validSurfaces] : null);
|
|
456
|
+
const tmplCtx = { status, title: docTitle, today, bodyInput, validSurfaces };
|
|
363
457
|
if (typeof template === 'function') {
|
|
364
458
|
content = template(name, tmplCtx);
|
|
365
459
|
} else {
|
|
366
|
-
|
|
460
|
+
let fm = template.frontmatter(status, today, tmplCtx);
|
|
461
|
+
if (bodyFrontmatter) fm = mergeBodyFrontmatter(fm, bodyFrontmatter, typeName);
|
|
367
462
|
const body = template.body(docTitle, tmplCtx);
|
|
368
463
|
content = `---\n${fm}\n---\n${body}`;
|
|
369
464
|
}
|
package/src/prompts.mjs
CHANGED
|
@@ -8,7 +8,10 @@ import { runArchive, runStatus } from './lifecycle.mjs';
|
|
|
8
8
|
import { runNew } from './new.mjs';
|
|
9
9
|
import { green, dim } from './color.mjs';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// `resume` is an alias for `use` — agents reach for "resume" when continuing a
|
|
12
|
+
// session; `use` reads as internal mechanics. Both names stay valid; the
|
|
13
|
+
// canonical output ("Consumed: …") is unchanged.
|
|
14
|
+
const SUBCOMMANDS = new Set(['list', 'next', 'use', 'resume', 'archive', 'new', 'shelve', 'unshelve']);
|
|
12
15
|
|
|
13
16
|
export async function runPrompts(argv, config, opts = {}) {
|
|
14
17
|
const sub = argv[0];
|
|
@@ -22,6 +25,7 @@ export async function runPrompts(argv, config, opts = {}) {
|
|
|
22
25
|
case 'list': return runPromptsList(rest, config, opts);
|
|
23
26
|
case 'next': return runPromptsNext(rest, config, opts);
|
|
24
27
|
case 'use': return runPromptsUse(rest, config, opts);
|
|
28
|
+
case 'resume': return runPromptsUse(rest, config, opts);
|
|
25
29
|
case 'archive': return runPromptsArchive(rest, config, opts);
|
|
26
30
|
case 'new': return runPromptsNew(rest, config, opts);
|
|
27
31
|
case 'shelve': return runPromptsShelve(rest, config, opts);
|
package/src/surfaces.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// `dotmd surfaces` — print the configured surface taxonomy.
|
|
2
|
+
//
|
|
3
|
+
// The surface taxonomy (`config.taxonomy.surfaces`) gates which `surfaces:`
|
|
4
|
+
// values the validator accepts. Before this command existed the only way to
|
|
5
|
+
// discover the valid set was to grep sibling plans or open the config file —
|
|
6
|
+
// which sent agents into a retry loop of "guess a surface, run check, get
|
|
7
|
+
// flagged, guess again." See issue #12 trap 1.
|
|
8
|
+
import { dim } from './color.mjs';
|
|
9
|
+
|
|
10
|
+
export function runSurfaces(argv, config) {
|
|
11
|
+
const json = argv.includes('--json');
|
|
12
|
+
// Read from raw user config (preserves declaration order) — `config.taxonomy`
|
|
13
|
+
// isn't exposed on the resolved object; only the derived `validSurfaces` Set is.
|
|
14
|
+
const surfaces = config.raw?.taxonomy?.surfaces ?? null;
|
|
15
|
+
|
|
16
|
+
if (json) {
|
|
17
|
+
process.stdout.write(JSON.stringify({ surfaces: surfaces ?? [] }, null, 2) + '\n');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!surfaces || surfaces.length === 0) {
|
|
22
|
+
process.stdout.write(dim('No surface taxonomy configured. Any surface value is accepted.\n'));
|
|
23
|
+
process.stdout.write(dim('To restrict, set `taxonomy.surfaces` in dotmd.config.mjs.\n'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const s of surfaces) process.stdout.write(s + '\n');
|
|
28
|
+
}
|
package/src/validate.mjs
CHANGED
|
@@ -444,13 +444,16 @@ export function validatePlanShape(doc, body, frontmatter, config) {
|
|
|
444
444
|
});
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
-
// 2. current_state length cap (
|
|
447
|
+
// 2. current_state length cap (1500 chars). Was 500; raised because agents
|
|
448
|
+
// legitimately need ~150-250 words of resume-context (prior incidents, what
|
|
449
|
+
// shipped, what's verified, where to look) and the prior cap forced a
|
|
450
|
+
// truncate-and-move-to-body retry loop on non-trivial plans.
|
|
448
451
|
const currentState = typeof frontmatter.current_state === 'string' ? frontmatter.current_state : '';
|
|
449
|
-
if (currentState.length >
|
|
452
|
+
if (currentState.length > 1500) {
|
|
450
453
|
doc.warnings.push({
|
|
451
454
|
path: doc.path,
|
|
452
455
|
level: 'warning',
|
|
453
|
-
message: `\`current_state\` is ${currentState.length} chars (cap:
|
|
456
|
+
message: `\`current_state\` is ${currentState.length} chars (cap: 1500). Long prose belongs in the body.`,
|
|
454
457
|
});
|
|
455
458
|
}
|
|
456
459
|
|