mixdog 0.9.89 → 0.9.91

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.
Files changed (40) hide show
  1. package/package.json +5 -1
  2. package/src/output-styles/detailed.md +27 -0
  3. package/src/output-styles/extreme-minimal.md +8 -8
  4. package/src/output-styles/minimal.md +7 -10
  5. package/src/output-styles/simple.md +13 -21
  6. package/src/rules/lead/01-general.md +9 -1
  7. package/src/rules/lead/lead-brief.md +15 -14
  8. package/src/rules/shared/01-tool.md +31 -37
  9. package/src/runtime/agent/orchestrator/agent-runtime/commit-message-completion.mjs +67 -0
  10. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +35 -1
  11. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +28 -1
  12. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +20 -2
  13. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +116 -3
  14. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -11
  15. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -10
  16. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +15 -3
  17. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +9 -0
  18. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +16 -2
  19. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +176 -16
  20. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  21. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +30 -1
  22. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +1 -1
  23. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +96 -5
  24. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +72 -1
  25. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +48 -9
  27. package/src/runtime/channels/backends/discord.mjs +21 -1
  28. package/src/runtime/channels/tool-defs.mjs +1 -1
  29. package/src/runtime/memory/lib/trace-store.mjs +25 -3
  30. package/src/runtime/memory/tool-defs.mjs +5 -5
  31. package/src/runtime/search/tool-defs.mjs +2 -18
  32. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  33. package/src/session-runtime/output-styles.mjs +7 -6
  34. package/src/session-runtime/tool-defs.mjs +0 -1
  35. package/src/session-runtime/tool-surface.mjs +9 -0
  36. package/src/session-runtime/workflow.mjs +7 -0
  37. package/src/standalone/explore-tool.mjs +2 -2
  38. package/src/workflows/default/WORKFLOW.md +2 -1
  39. package/src/workflows/solo/WORKFLOW.md +7 -5
  40. package/src/output-styles/default.md +0 -40
@@ -12,7 +12,9 @@ async function ensureDiscord() {
12
12
  import {
13
13
  mkdirSync,
14
14
  statSync,
15
- realpathSync
15
+ realpathSync,
16
+ readdirSync,
17
+ unlinkSync
16
18
  } from "fs";
17
19
  import { join, sep } from "path";
18
20
  import { createHash } from "crypto";
@@ -83,8 +85,26 @@ class DiscordBackend {
83
85
  }
84
86
  // ── Lifecycle ──────────────────────────────────────────────────────
85
87
  async connect() {
88
+ this._sweepInboxOnce();
86
89
  return gateway.connect(this);
87
90
  }
91
+ // Voice/attachment inbox retention: downloaded .ogg/.wav payloads are
92
+ // consumed at transcription time and were never cleaned up (audit: 477
93
+ // files / 55 MB accumulated since July). Best-effort, once per process.
94
+ _sweepInboxOnce() {
95
+ if (this._inboxSweepDone) return;
96
+ this._inboxSweepDone = true;
97
+ try {
98
+ const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
99
+ for (const name of readdirSync(this.inboxDir)) {
100
+ const p = join(this.inboxDir, name);
101
+ try {
102
+ const st = statSync(p);
103
+ if (st.isFile() && st.mtimeMs < cutoff) unlinkSync(p);
104
+ } catch { /* per-file best-effort */ }
105
+ }
106
+ } catch { /* inbox may not exist yet */ }
107
+ }
88
108
  async disconnect() {
89
109
  for (const interval of this.typingIntervals.values()) {
90
110
  clearInterval(interval);
@@ -32,7 +32,7 @@ export const TOOL_DEFS = [
32
32
  name: "fetch",
33
33
  title: "Fetch",
34
34
  annotations: { title: "Fetch", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
35
- description: "Discord-only recent messages. NOT for URLs; use web_fetch.",
35
+ description: "Discord-only: read recent messages from a channel.",
36
36
  inputSchema: {
37
37
  type: "object",
38
38
  properties: {
@@ -465,13 +465,18 @@ async function ensureCurrentAndNextMonthPartitions(client) {
465
465
  // Trace retention: drop named monthly partitions older than the configured
466
466
  // window. Trace is regenerable observability data, so aging out old months is
467
467
  // non-destructive to durable state. Config via MIXDOG_TRACE_RETENTION_MONTHS
468
- // (default 6); set to 0 to disable. Only DROPs partitions strictly older than
468
+ // (default 1); set to 0 to disable. Only DROPs partitions strictly older than
469
469
  // the cutoff month; the default catch-all partition is never dropped.
470
+ // Default rationale: a personal-machine audit found trace partitions at 76%
471
+ // of the 1.5 GB memory DB (July alone 787 MB) while durable entries were
472
+ // 207 MB. Monthly partitions mean retention 1 always keeps the current +
473
+ // previous month (30-60 days of full-resolution diagnostics) while bounding
474
+ // the DB to a rolling ceiling instead of unbounded growth.
470
475
  const TRACE_RETENTION_MONTHS = (() => {
471
476
  const raw = process.env.MIXDOG_TRACE_RETENTION_MONTHS
472
- if (raw == null || raw === '') return 6
477
+ if (raw == null || raw === '') return 1
473
478
  const n = Number(raw)
474
- return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 6
479
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 1
475
480
  })()
476
481
 
477
482
  async function dropAgedTracePartitions(client) {
@@ -499,6 +504,22 @@ async function dropAgedTracePartitions(client) {
499
504
  `)
500
505
  }
501
506
 
507
+ // Row-level ageing for the non-partitioned trace side tables. agent_calls and
508
+ // agent_llm grew unbounded (284 MB observed) because only trace_events had a
509
+ // retention path. Same window, same regenerable-diagnostics rationale; space
510
+ // is reclaimed by autovacuum row reuse rather than immediate disk shrink.
511
+ async function deleteAgedTraceRows(client) {
512
+ if (TRACE_RETENTION_MONTHS <= 0) return
513
+ const months = TRACE_RETENTION_MONTHS
514
+ for (const table of ['agent_calls', 'agent_llm', 'bridge_calls', 'bridge_llm']) {
515
+ try {
516
+ await client.query(`DELETE FROM ${table} WHERE ts < now() - interval '${months} months'`)
517
+ } catch (err) {
518
+ __mixdogMemoryLog(`[trace-store] aged-row delete skipped for ${table}: ${err?.message ?? err}\n`)
519
+ }
520
+ }
521
+ }
522
+
502
523
  async function isBootstrapComplete(client) {
503
524
  try {
504
525
  // Harden the check: require (a) root is partitioned, (b) schema-version
@@ -624,6 +645,7 @@ export async function openTraceDatabase(dataDir) {
624
645
  await c.query(`SET search_path = trace, public`)
625
646
  await ensureCurrentAndNextMonthPartitions(c)
626
647
  await dropAgedTracePartitions(c)
648
+ await deleteAgedTraceRows(c)
627
649
  } catch (err) {
628
650
  __mixdogMemoryLog(`[trace-store] periodic partition ensure failed: ${err?.message ?? err}\n`)
629
651
  } finally {
@@ -3,6 +3,8 @@
3
3
  // tool registrations) but are not advertised via ListTools / tools.json, so
4
4
  // they never reach an external LLM. `aiWrapped: true` routes dispatches
5
5
  // through ai-wrapped-dispatch.mjs instead of the module's handleToolCall.
6
+ // Shared period grammar for recall/search_memories.
7
+ const PERIOD_DESCRIPTION = "last (recent sessions; +query topic-filter; limit=session count), Nm/Nh/Nd, today/yesterday/this_week/last_week, all, YYYY-MM-DD, date~date, HH:MM~HH:MM, or 'YYYY-MM-DD HH:MM~HH:MM'.";
6
8
  export const TOOL_DEFS = [
7
9
  {
8
10
  name: 'memory',
@@ -46,24 +48,22 @@ export const TOOL_DEFS = [
46
48
  name: 'recall',
47
49
  title: 'Recall',
48
50
  annotations: { title: 'Recall', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
49
- description: 'Retrieve stored memory/session history for prior-work context (resumes, earlier decisions); not a reflexive pre-step. Query is topic/semantic, not regex; period selects the time window; id for exact follow-up.',
51
+ description: 'Retrieve stored memory/session history for prior-work context (resumes, earlier decisions). Query is topic/semantic, not regex; period selects the time window; id for exact follow-up.',
50
52
  inputSchema: {
51
53
  type: 'object',
52
54
  properties: {
53
55
  query: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Search text, or array for independent fan-out queries.' },
54
56
  id: { anyOf: [{ type: 'number' }, { type: 'array', items: { type: 'number' }, minItems: 1 }], description: 'Exact #id(s) from recall. Do not invent ids.' },
55
- period: { type: 'string', description: "last (recent sessions; +query topic-filter; limit=session count), Nm/Nh/Nd, today/yesterday/this_week/last_week, all, YYYY-MM-DD, date~date, HH:MM~HH:MM, or 'YYYY-MM-DD HH:MM~HH:MM'." },
57
+ period: { type: 'string', description: PERIOD_DESCRIPTION },
56
58
  limit: { type: 'number', description: 'Max entries.' },
57
59
  offset: { type: 'number', description: 'Skip entries.' },
58
60
  sort: { type: 'string', enum: ['importance', 'date'], description: 'importance or date.' },
59
61
  category: { anyOf: [{ type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'] }, { type: 'array', items: { type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'] }, minItems: 1 }], description: 'Category filter.' },
60
62
  includeArchived: { type: 'boolean', description: 'Include archived entries.' },
61
- sessionId: { type: 'string', description: 'Scoped session id.' },
62
63
  includeMembers: { type: 'boolean', description: 'Include chunk members in output; does not widen the search pool.' },
63
64
  includeRaw: { type: 'boolean', description: 'Include unchunked raw/episode rows.' },
64
65
  sessionOnly: { type: 'boolean', description: 'Search this session only.' },
65
66
  projectScope: { type: 'string', description: 'Project pool selector: inferred from cwd, common, all, or slug.' },
66
- cwd: { type: 'string', description: 'Infer projectScope.' },
67
67
  },
68
68
  additionalProperties: false,
69
69
  },
@@ -78,7 +78,7 @@ export const TOOL_DEFS = [
78
78
  type: 'object',
79
79
  properties: {
80
80
  query: { type: 'string', description: 'Search text.' },
81
- period: { type: 'string', description: "last (recent sessions; +query topic-filter), Nm/Nh/Nd, today/yesterday/this_week/last_week, all, YYYY-MM-DD, date range, HH:MM~HH:MM (today), or 'YYYY-MM-DD HH:MM~HH:MM'." },
81
+ period: { type: 'string', description: PERIOD_DESCRIPTION },
82
82
  sort: { type: 'string', enum: ['date', 'importance'], description: 'date or importance.' },
83
83
  category: { anyOf: [{ type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'] }, { type: 'array', items: { type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'] }, minItems: 1 }], description: 'Category filter.' },
84
84
  limit: { type: 'number', default: 30, description: 'Max entries.' },
@@ -8,7 +8,7 @@ export const TOOL_DEFS = [
8
8
  {
9
9
  name: 'search',
10
10
  title: 'Mixdog Web Search',
11
- description: `Web/docs/current-info search. Not repo-local; use explore/code_graph/grep. ${TOOL_SYNC_EXECUTION_CONTRACT} Cached; query supports array fan-out.`,
11
+ description: `Web/docs/current-info search. ${TOOL_SYNC_EXECUTION_CONTRACT} Cached; query supports array fan-out.`,
12
12
  inputSchema: {
13
13
  type: 'object',
14
14
  properties: {
@@ -16,23 +16,7 @@ export const TOOL_DEFS = [
16
16
  site: { type: 'string', description: 'Site/domain filter.' },
17
17
  type: { type: 'string', enum: ['web', 'news', 'images'], description: 'Default web.' },
18
18
  maxResults: { type: 'number', minimum: 1, maximum: 20, description: 'Result count, 1-20.' },
19
- locale: {
20
- description: 'Optional locale hint.',
21
- anyOf: [
22
- { type: 'string' },
23
- {
24
- type: 'object',
25
- properties: {
26
- country: { type: 'string', description: 'Country.' },
27
- language: { type: 'string', description: 'Language.' },
28
- region: { type: 'string', description: 'Region.' },
29
- city: { type: 'string', description: 'City.' },
30
- timezone: { type: 'string', description: 'Timezone.' },
31
- },
32
- additionalProperties: false,
33
- },
34
- ],
35
- },
19
+ locale: { type: 'string', description: 'Optional locale hint, e.g. ko-KR or a city/region name.' },
36
20
  contextSize: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Default low.' },
37
21
  },
38
22
  required: [],
@@ -2,7 +2,7 @@ export const TOOL_SYNC_EXECUTION_CONTRACT =
2
2
  'Runs synchronously in this tool call.';
3
3
 
4
4
  export const TOOL_ASYNC_EXECUTION_CONTRACT =
5
- 'Runs sync inline; no default auto-background. async forces a background task_id + completion notification. status/read/wait are recovery/blocking only.';
5
+ 'Runs sync inline by default; async returns a background task_id and delivers a completion notification.';
6
6
 
7
7
  export const TOOL_MANUAL_CONTROL_CONTRACT =
8
8
  'wait/read/status/cancel are for explicit blocking or recovery only.';
@@ -5,10 +5,11 @@ import { readFileSync, readdirSync, statSync } from 'node:fs';
5
5
  import { clean } from './session-text.mjs';
6
6
  import { readJsonSafe } from './fs-utils.mjs';
7
7
 
8
- const OUTPUT_STYLE_ORDER = ['default', 'simple', 'minimal', 'extreme-minimal'];
8
+ const OUTPUT_STYLE_ORDER = ['simple', 'detailed', 'minimal', 'extreme-minimal'];
9
9
  const OUTPUT_STYLE_ALIASES = new Map([
10
- ['compact', 'default'],
11
- ['normal', 'default'],
10
+ ['compact', 'simple'],
11
+ ['normal', 'simple'],
12
+ ['detail', 'detailed'],
12
13
  ['extreme', 'extreme-minimal'],
13
14
  ['extremesimple', 'extreme-minimal'],
14
15
  ['extreme-simple', 'extreme-minimal'],
@@ -146,15 +147,15 @@ export function findOutputStyle(value, styles) {
146
147
 
147
148
  function configuredOutputStyleValue(dataDir) {
148
149
  const unified = readJsonSafe(join(dataDir, 'mixdog-config.json')) || {};
149
- return clean(unified.outputStyle || (unified.agent && unified.agent.outputStyle) || 'default') || 'default';
150
+ return clean(unified.outputStyle || (unified.agent && unified.agent.outputStyle) || 'simple') || 'simple';
150
151
  }
151
152
 
152
153
  export function outputStyleStatus(rootDir, dataDir, { fresh = false } = {}) {
153
154
  const styles = listOutputStyleCatalog(rootDir, dataDir, { fresh });
154
155
  const configured = configuredOutputStyleValue(dataDir);
155
156
  const current = findOutputStyle(configured, styles)
156
- || findOutputStyle('default', styles)
157
+ || findOutputStyle('simple', styles)
157
158
  || styles[0]
158
- || { id: 'default', label: 'Default', description: '', aliases: [], source: 'builtin' };
159
+ || { id: 'simple', label: 'Simple', description: '', aliases: [], source: 'builtin' };
159
160
  return { configured, current, styles };
160
161
  }
@@ -18,7 +18,6 @@ export const TOOL_SEARCH_TOOL = {
18
18
  type: 'object',
19
19
  properties: {
20
20
  names: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Exact deferred tool names/aliases to load.' },
21
- select: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Legacy alias for names; accepts exact names/aliases and select:name syntax.' },
22
21
  },
23
22
  additionalProperties: false,
24
23
  },
@@ -26,8 +26,17 @@ export function createToolSurface({
26
26
 
27
27
  // A workflow that delegates to NOBODY must not advertise the agent tool: a
28
28
  // schema-visible tool policy always rejects is a guaranteed error turn.
29
+ // The SESSION's effective workflow wins over the config-active one: headless
30
+ // and per-session overrides (e.g. bench `workflow=solo`) never touch the
31
+ // config default, so keying off config alone left the agent tool visible in
32
+ // Solo sessions (observed across a full TB2.1 run: advertised, never used).
29
33
  function workflowAllowsAgents() {
30
34
  try {
35
+ const sessionWorkflow = getSession()?.workflow;
36
+ if (sessionWorkflow && typeof sessionWorkflow === 'object'
37
+ && sessionWorkflow.agentsConfigured === true) {
38
+ return Array.isArray(sessionWorkflow.agents) && sessionWorkflow.agents.length > 0;
39
+ }
31
40
  const pack = loadWorkflowPack(cfgMod.getPluginData?.() || dataDir, activeWorkflowId(getConfig()));
32
41
  if (!pack || pack.agentsConfigured !== true) return true;
33
42
  return Array.isArray(pack.agents) && pack.agents.length > 0;
@@ -229,6 +229,13 @@ export function createWorkflowHelpers({ rootDir, dataDir, readMarkdownDocument,
229
229
  name: clean(pack?.name) || (id === DEFAULT_WORKFLOW_ID ? 'Default' : id),
230
230
  description: clean(pack?.description),
231
231
  source: clean(pack?.source),
232
+ // Delegation surface fields: the session stores this summary as
233
+ // session.workflow, and the agent-tool gate (tool-surface.mjs
234
+ // workflowAllowsAgents) needs them to filter the agent tool for
235
+ // packs that delegate to nobody — including headless/bench sessions
236
+ // whose workflow never touches the config-active pack.
237
+ agentsConfigured: pack?.agentsConfigured === true,
238
+ agents: Array.isArray(pack?.agents) ? pack.agents.slice() : [],
232
239
  };
233
240
  }
234
241
 
@@ -18,11 +18,11 @@ export const EXPLORE_TOOL = {
18
18
  name: 'explore',
19
19
  title: 'Explore',
20
20
  annotations: { title: 'Explore', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
21
- description: 'Locator for broad/uncertain targets with no known path, repo or machine-wide (dot dirs included). Array = independent targets: query[] fans out facets (max 8), never rephrasings.',
21
+ description: 'Locator for broad/uncertain targets with no known path, repo or machine-wide (dot dirs included). query[] fans out up to 8 independent facets in parallel.',
22
22
  inputSchema: {
23
23
  type: 'object',
24
24
  properties: {
25
- query: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Narrow locator query; array = independent facets of the task (not rephrasings), fanned out in parallel.' },
25
+ query: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Narrow locator query; array = independent facets fanned out in parallel.' },
26
26
  cwd: { type: 'string', description: 'Project/root directory.' },
27
27
  },
28
28
  required: [],
@@ -17,7 +17,8 @@ On approval, fan out at maximum width: one agent per independent scope, all
17
17
  spawned in one turn; only a scope that depends on another's output waits.
18
18
  Split the plan into as many scopes as possible: disjoint file/module sets
19
19
  are independent; merge only on a true output dependency. Prefer parallel
20
- scopes over sequential slices in one agent.
20
+ scopes over sequential slices in one agent. Brief each agent per the Lead
21
+ Brief contract.
21
22
 
22
23
  Route by complexity: simple, well-understood implementation goes to Worker;
23
24
  complex or investigative implementation goes to Heavy Worker; Lead itself
@@ -15,11 +15,13 @@ changed request resets planning; a scope change requires fresh approval.
15
15
  On approval, Lead executes all work itself — never spawn, send, or delegate
16
16
  to agents. Complete in-scope fixes without reapproval.
17
17
 
18
- Verification is single-pass: run the one check that most directly proves the
19
- deliverable exactly once; a pass is final. Iterate only on a failing check,
20
- re-running just that check after each fix, or report the blocker.
18
+ Verification is single-pass and risk-proportional: exactly one check that
19
+ most directly proves the deliverable — a direct result check for trivial or
20
+ read-only work, full test/build runs only for risky or behavior-changing
21
+ work. A pass is final. Iterate only on a failing check, re-running just that
22
+ check after each fix, or report the blocker.
21
23
 
22
- Report the result and its proving check against the approved plan. Build, deploy, commit,
23
- and push happen only on an explicit user request.
24
+ Report the result against the approved plan. Build, deploy, commit, and push
25
+ happen only on an explicit user request.
24
26
 
25
27
  On direction change, pause and re-consult the user.
@@ -1,40 +0,0 @@
1
- ---
2
- name: default
3
- title: Default
4
- description: Concise engineering summaries
5
- keep-coding-instructions: true
6
- ---
7
-
8
- # Output Style
9
-
10
- Mixdog default — the most detailed style, but always summary-form, never
11
- essay-form. Depth comes from picking the right facts, not explaining more.
12
-
13
- Content
14
- - Lead with the outcome in one short sentence, then only the detail that
15
- matters: what changed, key evidence (paths, commands, errors, verification).
16
- - Summarize at the concept level: name the problem/behavior and direction,
17
- not the code path; cite a symbol/path only as an anchor.
18
- State conclusions, not reasoning: no mechanism walkthroughs or chained
19
- qualifiers unless asked.
20
- - Compress by cutting content (filler, hedging, restated facts), not grammar:
21
- natural, complete sentences in the user's language; technical terms, paths,
22
- commands, symbols, code, and exact errors stay verbatim.
23
- - Say each point once: problem and fix in ONE compact statement. Size budget:
24
- roughly TWICE the Simple style — about 2 rendered lines per point, whole
25
- report ~10–15 lines, spent on evidence Simple would drop.
26
- - Use labels such as `Changes`, `Verification`, and `Risks / next steps` in
27
- final reports; skip labels on interim progress, and collapse trivial tasks
28
- to a couple of sentences instead of forcing sections.
29
- - Synthesize agent or retrieval results; never forward raw reports, long file
30
- lists, tool traces, or session metadata.
31
- - Do not hide blockers, failed verification, or required follow-up; surface them
32
- in one short clause.
33
-
34
- Layout (hard rules)
35
- - One bullet or numbered item = one idea, at most 2 rendered lines, opened
36
- with a short **bold key point**; if it needs more, cut the detail.
37
- - Blank line between numbered items and between any multi-line list items —
38
- never a wall of consecutive multi-line items. Paragraphs ~3 lines max with
39
- blank lines between blocks; nest at most one sub-level.
40
- - Never name this style unless asked.