dotmd-cli 0.75.0 → 0.76.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/dotmd.mjs CHANGED
@@ -1594,6 +1594,18 @@ async function main() {
1594
1594
  throw err;
1595
1595
  }
1596
1596
  _resolvedConfig = config;
1597
+ // The one place that reaches an OpenCode session running without the
1598
+ // integration — whatever verb it reaches for first. stderr, so `--json`
1599
+ // consumers are untouched, and once per session so it informs rather than
1600
+ // nags. `install`/`doctor`/`update` are skipped: they report this themselves,
1601
+ // and telling `dotmd install opencode` to run `dotmd install opencode` is
1602
+ // noise.
1603
+ if (!['install', 'doctor', 'update', 'hud', 'guard'].includes(command)) {
1604
+ const { degradedIdentityNotice } = await import('../src/host-integration.mjs');
1605
+ const notice = degradedIdentityNotice(config?.repoRoot, { version: pkg.version });
1606
+ if (notice) process.stderr.write(`${notice}\n`);
1607
+ }
1608
+
1597
1609
  const suppressSideEffects = effectiveDryRun || command === 'hud' || passiveMachineContext;
1598
1610
  Object.defineProperty(config, '_execution', {
1599
1611
  value: { dryRun, passive: command === 'hud' || passiveMachineContext, suppressSideEffects, gitStaleness },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.75.0",
3
+ "version": "0.76.1",
4
4
  "description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, lifecycle, and AI summaries.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -16,6 +16,7 @@
16
16
  // reports that the integration is missing and names the command; it never
17
17
  // installs. Passive commands touch nothing.
18
18
 
19
+ import { createHash } from 'node:crypto';
19
20
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
20
21
  import os from 'node:os';
21
22
  import path from 'node:path';
@@ -166,6 +167,59 @@ export function describeSessionIdentity(opts = {}) {
166
167
  };
167
168
  }
168
169
 
170
+ // --- Telling the session it is running degraded -----------------------------
171
+ //
172
+ // Every other surface here has to be sought out: `doctor`, `install`, the npm
173
+ // postinstall line. But the degraded mode is SILENT — with the OPENCODE_PID
174
+ // fallback in place, `use`/`set`/`baton` all work, they just share one identity
175
+ // across every session in the OpenCode process. Nobody goes looking for a
176
+ // diagnostic about a thing that appears to work, so this comes to them.
177
+ //
178
+ // Warning, not error. Failing closed would undo the fallback that unblocked
179
+ // OpenCode in the first place, and punish a setup that is working.
180
+ //
181
+ // Once per session per repo, via a marker under the gitignored .dotmd/. Silent
182
+ // under DOTMD_NO_HINTS=1, the switch the repeat-failure hints already use.
183
+ const NOTICE_DIR = 'notices';
184
+
185
+ function noticeMarkerPath(repoRoot, key) {
186
+ const digest = createHash('sha256').update(key).digest('hex').slice(0, 32);
187
+ return path.join(path.resolve(repoRoot), '.dotmd', NOTICE_DIR, `${digest}`);
188
+ }
189
+
190
+ // Returns the notice text the first time it applies in a session, then null.
191
+ // `record: false` answers without consuming the once-per-session budget.
192
+ export function degradedIdentityNotice(repoRoot, opts = {}) {
193
+ const { env = process.env, homedir = os.homedir(), version, record = true } = opts;
194
+ if (env.DOTMD_NO_HINTS === '1') return null;
195
+ // Cheap env check first: a user on any other host pays nothing for this.
196
+ if (!env.OPENCODE && !env.OPENCODE_PID) return null;
197
+
198
+ const identity = describeSessionIdentity({ env, homedir, version });
199
+ if (identity.scope === 'session') return null;
200
+
201
+ const installed = opencodeStatus({ env, homedir, version }).exists;
202
+ const lines = installed
203
+ ? ['[dotmd] This OpenCode session started before the dotmd integration loaded, so its plan',
204
+ ' ownership is still process-scoped. Restart OpenCode to pick it up.']
205
+ : ['[dotmd] OpenCode detected, dotmd integration not installed — plan ownership is scoped to',
206
+ ' the OpenCode process, so another session in it can release plans this one claimed,',
207
+ ' and no session-start briefing runs. Fix once: dotmd install opencode'];
208
+
209
+ if (!record) return lines.join('\n');
210
+ if (!repoRoot) return lines.join('\n');
211
+ const marker = noticeMarkerPath(repoRoot, `opencode:${identity.id}:${installed ? 'restart' : 'install'}`);
212
+ if (existsSync(marker)) return null;
213
+ try {
214
+ mkdirSync(path.dirname(marker), { recursive: true });
215
+ writeFileSync(marker, '', 'utf8');
216
+ } catch {
217
+ // Unwritable .dotmd: say it once anyway rather than stay silent about a
218
+ // real ownership weakness. Worst case it repeats.
219
+ }
220
+ return lines.join('\n');
221
+ }
222
+
169
223
  // --- Claude Code -----------------------------------------------------------
170
224
  //
171
225
  // Claude Code has a real plugin registry, so dotmd drives `claude plugin`
package/src/new.mjs CHANGED
@@ -101,7 +101,16 @@ const BUILTIN_TEMPLATES = {
101
101
  'related_plans:',
102
102
  'related_docs:',
103
103
  ].join('\n'),
104
- body: (t, ctx) => `
104
+ body: (t, ctx) => {
105
+ // Same full-body shortcut the plan templates use, and for the same
106
+ // reason: a body that already authors `## Section` headings is a complete
107
+ // document, not section-content for one slot. Nesting it under `## Overview`
108
+ // produced a second `# Title` under the scaffold's, a placeholder summary
109
+ // line above the author's own opening, and the skeleton's trailing
110
+ // sections after a document that had already ended.
111
+ const authored = fullBodyShortcut(t, ctx?.bodyInput);
112
+ if (authored !== null) return authored;
113
+ return `
105
114
  # ${t}
106
115
 
107
116
  > One-line summary of what this doc covers.
@@ -116,7 +125,8 @@ ${ctx?.bodyInput?.trim() ?? ''}
116
125
 
117
126
  ## Related Documentation
118
127
 
119
- `,
128
+ `;
129
+ },
120
130
  },
121
131
  plan: {
122
132
  description: 'Execution plan — build-up shape (Problem → Phases → Closeout) with phase status markers and Version History',