@rvry/mcp 0.12.2 → 1.0.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.
@@ -1,30 +1,118 @@
1
1
  /**
2
- * Local session writer -- writes a minimal markdown summary to .rvry/sessions/
3
- * when a session completes.
2
+ * Local session writer -- writes a client-owned markdown harvest to
3
+ * .rvry/<op>/<timestamp>-<slug>/harvest.md when a session completes.
4
+ *
5
+ * Optionally emits per-round files (round-N-<mode>.md) when the slash-command
6
+ * shim passes `rounds[]` (triggered by `--record` flag on the user side).
4
7
  *
5
8
  * PROTOCOL SURFACE: This file produces user-facing output. It must NEVER contain
6
- * constraint IDs, detection scores, mode names, gate verdicts, or any engine internals.
7
- * Only question + harvest data.
9
+ * constraint IDs, detection scores, mode names used as gate verdicts, or any
10
+ * engine internals. Only question + client-supplied harvest + recorded bodies.
8
11
  */
9
- interface LocalSessionData {
12
+ export type ChallengeVerdict = 'GO' | 'CONDITIONAL_GO' | 'NO_GO';
13
+ export type ChallengeBias = 'anchoring' | 'framing' | 'authority' | 'sunk_cost' | 'availability' | 'confirmation' | 'none';
14
+ export type ChallengeBlastRadius = 'contained' | 'cascading';
15
+ export type ChallengeAssumptionConfidence = 'high' | 'medium' | 'low';
16
+ export interface ChallengeAssumption {
17
+ assumption: string;
18
+ confidence: ChallengeAssumptionConfidence;
19
+ ifWrong: string;
20
+ bias: ChallengeBias;
21
+ biasEvidence: string;
22
+ }
23
+ export interface ChallengeFailureMode {
24
+ component: string;
25
+ howItFails: string;
26
+ blastRadius: ChallengeBlastRadius;
27
+ detection: string;
28
+ }
29
+ export interface ChallengeCounterArgument {
30
+ argument: string;
31
+ rebuttal: string;
32
+ }
33
+ export type ProblemSolveAdversarialVerdict = 'PASSED' | 'CAVEATS' | 'NEEDS_REVISION';
34
+ export type ProblemSolveBias = 'anchoring' | 'framing' | 'authority' | 'sunk_cost' | 'availability' | 'confirmation';
35
+ export interface ProblemSolveBiasOperating {
36
+ bias: ProblemSolveBias;
37
+ evidence: string;
38
+ sourceReflex?: string;
39
+ correction?: string;
40
+ }
41
+ export interface ProblemSolveFrameReversal {
42
+ originalFraming?: string;
43
+ invertedFraming?: string;
44
+ wouldReachSameConclusion?: boolean;
45
+ }
46
+ export interface ProblemSolveBiasAudit {
47
+ biasesOperating?: ProblemSolveBiasOperating[];
48
+ decisionStable?: boolean;
49
+ frameReversal?: ProblemSolveFrameReversal;
50
+ }
51
+ export interface ClientHarvest {
52
+ title?: string | null;
53
+ summary?: string;
54
+ keyFindings?: string[];
55
+ shimmer?: string;
56
+ /** Shimmer-op: one line per surfaced commitment with its fate. */
57
+ constraintLedger?: string[];
58
+ openQuestions?: string[];
59
+ followUps?: Array<{
60
+ question: string;
61
+ rationale: string;
62
+ }>;
63
+ verdict?: ChallengeVerdict;
64
+ confidence?: number;
65
+ mitigations?: string[];
66
+ assumptions?: ChallengeAssumption[];
67
+ edgeCases?: string[];
68
+ failureModes?: ChallengeFailureMode[];
69
+ counterArguments?: ChallengeCounterArgument[];
70
+ biasAudit?: ProblemSolveBiasAudit;
71
+ adversarialVerdict?: ProblemSolveAdversarialVerdict;
72
+ reversalConditions?: string[];
73
+ }
74
+ export interface ClientRound {
75
+ round: number;
76
+ mode: string;
77
+ body: string;
78
+ }
79
+ export interface LocalSessionData {
10
80
  sessionId: string;
11
81
  operationType: string;
12
82
  question: string;
13
83
  rounds: number;
14
- harvest: {
15
- summary: string;
16
- keyFindings: string[];
17
- openQuestions: string[];
18
- followUps: Array<{
19
- question: string;
20
- rationale: string;
21
- }>;
22
- };
84
+ /** Engine-side harvest (telemetry). Kept for potential future use; not written. */
85
+ harvest?: unknown;
86
+ /** Client-authored harvest (the synthesis Claude produced). Preferred source. */
87
+ clientHarvest?: ClientHarvest | null;
88
+ /** Per-round bodies for `--record` mode. */
89
+ clientRounds?: ClientRound[] | null;
90
+ /** Override "now" for deterministic tests. */
91
+ now?: Date;
92
+ /** Override base directory (default: process.cwd()). */
93
+ baseDir?: string;
23
94
  }
24
95
  /**
25
- * Write a minimal markdown summary of a completed session to .rvry/sessions/.
96
+ * Build the main harvest.md content. If `clientHarvest` is missing, emits the
97
+ * graceful-degradation fallback (question + upgrade notice).
98
+ *
99
+ * Per-op rendering (FR-16):
100
+ * - Challenge: verdict line below H1; Required Mitigations (when CONDITIONAL_GO);
101
+ * Assumptions table; Edge Cases bullets; Failure Modes table; Counter-Arguments.
102
+ * - Problem-solve: Adversarial Verdict line below H1; Bias Audit; Reversal Conditions.
103
+ * - Other ops: render unchanged.
104
+ *
105
+ * All op-specific sections are conditional on the corresponding field being
106
+ * present and non-empty. Absent field = absent section, never an empty section.
107
+ */
108
+ export declare function buildMarkdown(data: LocalSessionData, now: Date): string;
109
+ /**
110
+ * Build a per-round markdown file body.
111
+ */
112
+ export declare function buildRoundMarkdown(round: ClientRound, now: Date): string;
113
+ /**
114
+ * Write session folder: harvest.md (always) + optional per-round files.
26
115
  *
27
116
  * Disabled via RVRY_LOCAL_SESSIONS=0. Fails silently on any error.
28
117
  */
29
118
  export declare function writeLocalSession(data: LocalSessionData): Promise<void>;
30
- export {};
@@ -1,16 +1,34 @@
1
1
  /**
2
- * Local session writer -- writes a minimal markdown summary to .rvry/sessions/
3
- * when a session completes.
2
+ * Local session writer -- writes a client-owned markdown harvest to
3
+ * .rvry/<op>/<timestamp>-<slug>/harvest.md when a session completes.
4
+ *
5
+ * Optionally emits per-round files (round-N-<mode>.md) when the slash-command
6
+ * shim passes `rounds[]` (triggered by `--record` flag on the user side).
4
7
  *
5
8
  * PROTOCOL SURFACE: This file produces user-facing output. It must NEVER contain
6
- * constraint IDs, detection scores, mode names, gate verdicts, or any engine internals.
7
- * Only question + harvest data.
9
+ * constraint IDs, detection scores, mode names used as gate verdicts, or any
10
+ * engine internals. Only question + client-supplied harvest + recorded bodies.
8
11
  */
9
12
  import { mkdirSync, writeFileSync } from 'fs';
10
13
  import { join } from 'path';
14
+ /** Op label for H1 headings. Keyed by public tool name (old keys kept). */
15
+ const OP_LABELS = {
16
+ deepthink: 'RVRY Deepthink',
17
+ problem_solve: 'RVRY Problem-Solve',
18
+ think: 'RVRY Think',
19
+ rvry: 'RVRY',
20
+ challenge: 'RVRY Challenge',
21
+ meta: 'RVRY Meta',
22
+ shimmer: 'RVRY Shimmer',
23
+ };
24
+ function opLabel(operationType) {
25
+ return OP_LABELS[operationType] ?? `RVRY ${operationType}`;
26
+ }
27
+ function opFolder(operationType) {
28
+ return operationType.replace(/_/g, '-');
29
+ }
11
30
  /**
12
- * Slugify a question string for use in filenames.
13
- * Lowercases, replaces non-alphanumeric runs with hyphens, trims, and caps at maxLen chars.
31
+ * Slugify text for filenames/folders.
14
32
  */
15
33
  function slugify(text, maxLen = 50) {
16
34
  return text
@@ -20,9 +38,7 @@ function slugify(text, maxLen = 50) {
20
38
  .slice(0, maxLen)
21
39
  .replace(/-+$/, '');
22
40
  }
23
- /**
24
- * Format a Date as YYYYMMDD-HHMM for filenames.
25
- */
41
+ /** YYYYMMDD-HHMM for folder names. */
26
42
  function formatTimestamp(date) {
27
43
  const y = date.getFullYear().toString();
28
44
  const mo = (date.getMonth() + 1).toString().padStart(2, '0');
@@ -31,9 +47,7 @@ function formatTimestamp(date) {
31
47
  const mi = date.getMinutes().toString().padStart(2, '0');
32
48
  return `${y}${mo}${d}-${h}${mi}`;
33
49
  }
34
- /**
35
- * Format a Date as YYYY-MM-DD HH:MM for YAML frontmatter.
36
- */
50
+ /** YYYY-MM-DD HH:MM for italic date line. */
37
51
  function formatDate(date) {
38
52
  const y = date.getFullYear().toString();
39
53
  const mo = (date.getMonth() + 1).toString().padStart(2, '0');
@@ -43,72 +57,367 @@ function formatDate(date) {
43
57
  return `${y}-${mo}-${d} ${h}:${mi}`;
44
58
  }
45
59
  /**
46
- * Build the markdown content for a completed session.
60
+ * Title-case the first 4 words of a question as an H1 fallback.
47
61
  */
48
- function buildMarkdown(data, now) {
62
+ function fallbackTitleFromQuestion(question) {
63
+ const words = question
64
+ .trim()
65
+ .split(/\s+/)
66
+ .slice(0, 4)
67
+ .map((w) => w.replace(/[^\p{L}\p{N}'-]/gu, ''))
68
+ .filter((w) => w.length > 0);
69
+ if (words.length === 0)
70
+ return 'Session';
71
+ return words
72
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
73
+ .join(' ');
74
+ }
75
+ function isNonEmptyString(v) {
76
+ return typeof v === 'string' && v.trim().length > 0;
77
+ }
78
+ function isNonEmptyArray(v) {
79
+ return Array.isArray(v) && v.length > 0;
80
+ }
81
+ /**
82
+ * Build the main harvest.md content. If `clientHarvest` is missing, emits the
83
+ * graceful-degradation fallback (question + upgrade notice).
84
+ *
85
+ * Per-op rendering (FR-16):
86
+ * - Challenge: verdict line below H1; Required Mitigations (when CONDITIONAL_GO);
87
+ * Assumptions table; Edge Cases bullets; Failure Modes table; Counter-Arguments.
88
+ * - Problem-solve: Adversarial Verdict line below H1; Bias Audit; Reversal Conditions.
89
+ * - Other ops: render unchanged.
90
+ *
91
+ * All op-specific sections are conditional on the corresponding field being
92
+ * present and non-empty. Absent field = absent section, never an empty section.
93
+ */
94
+ export function buildMarkdown(data, now) {
95
+ const label = opLabel(data.operationType);
49
96
  const lines = [];
50
- // YAML frontmatter
51
- lines.push('---');
52
- lines.push(`session: ${data.sessionId}`);
53
- lines.push(`operation: ${data.operationType}`);
54
- lines.push(`date: ${formatDate(now)}`);
55
- lines.push(`rounds: ${data.rounds}`);
56
- lines.push('---');
57
- lines.push('');
58
- // Question
59
- lines.push(`# ${data.question}`);
60
- lines.push('');
61
- // Summary
62
- lines.push('## Summary');
63
- lines.push(data.harvest.summary);
64
- lines.push('');
65
- // Key Findings
66
- if (data.harvest.keyFindings.length > 0) {
67
- lines.push('## Key Findings');
68
- for (const finding of data.harvest.keyFindings) {
69
- lines.push(`- ${finding}`);
70
- }
97
+ const harvest = data.clientHarvest;
98
+ if (!harvest) {
99
+ // Fallback: no client synthesis provided.
100
+ const title = fallbackTitleFromQuestion(data.question);
101
+ lines.push(`# ${label} — ${title}`);
102
+ lines.push('');
103
+ lines.push(`*${formatDate(now)}*`);
104
+ lines.push('');
105
+ lines.push('### Prompt');
106
+ lines.push('');
107
+ lines.push(data.question);
108
+ lines.push('');
109
+ lines.push('## Summary');
71
110
  lines.push('');
111
+ lines.push('*(Synthesis not provided — upgrade your slash commands by re-running `rvry-mcp setup`.)*');
112
+ lines.push('');
113
+ return lines.join('\n');
114
+ }
115
+ const title = isNonEmptyString(harvest.title)
116
+ ? harvest.title.trim()
117
+ : fallbackTitleFromQuestion(data.question);
118
+ lines.push(`# ${label} — ${title}`);
119
+ // Op-specific verdict line directly below H1.
120
+ if (data.operationType === 'challenge') {
121
+ const verdictLine = renderChallengeVerdictLine(harvest);
122
+ if (verdictLine)
123
+ lines.push(verdictLine);
124
+ }
125
+ else if (data.operationType === 'problem_solve') {
126
+ const verdictLine = renderProblemSolveVerdictLine(harvest);
127
+ if (verdictLine)
128
+ lines.push(verdictLine);
129
+ }
130
+ lines.push(`*${formatDate(now)}*`);
131
+ lines.push('');
132
+ lines.push('### Prompt');
133
+ lines.push('');
134
+ lines.push(data.question);
135
+ lines.push('');
136
+ if (isNonEmptyString(harvest.shimmer)) {
137
+ lines.push('## Shimmer');
138
+ lines.push('');
139
+ lines.push(harvest.shimmer.trim());
140
+ lines.push('');
141
+ }
142
+ if (isNonEmptyString(harvest.summary)) {
143
+ lines.push('## Summary');
144
+ lines.push('');
145
+ lines.push(harvest.summary.trim());
146
+ lines.push('');
147
+ }
148
+ // Challenge-specific structured sections (in declared rendering order).
149
+ if (data.operationType === 'challenge') {
150
+ appendChallengeMitigations(lines, harvest);
151
+ appendChallengeAssumptions(lines, harvest);
152
+ appendChallengeEdgeCases(lines, harvest);
153
+ appendChallengeFailureModes(lines, harvest);
154
+ appendChallengeCounterArguments(lines, harvest);
155
+ }
156
+ // Problem-solve-specific structured sections.
157
+ if (data.operationType === 'problem_solve') {
158
+ appendProblemSolveBiasAudit(lines, harvest);
159
+ appendProblemSolveReversalConditions(lines, harvest);
160
+ }
161
+ // Shimmer-op commitment ledger: one line per surfaced commitment with fate.
162
+ appendConstraintLedger(lines, harvest);
163
+ if (isNonEmptyArray(harvest.keyFindings)) {
164
+ const findings = harvest.keyFindings.filter(isNonEmptyString);
165
+ if (findings.length > 0) {
166
+ lines.push('## Key Findings');
167
+ lines.push('');
168
+ for (const f of findings)
169
+ lines.push(`- ${f.trim()}`);
170
+ lines.push('');
171
+ }
172
+ }
173
+ if (isNonEmptyArray(harvest.openQuestions)) {
174
+ const qs = harvest.openQuestions.filter(isNonEmptyString);
175
+ if (qs.length > 0) {
176
+ lines.push('## Open Questions');
177
+ lines.push('');
178
+ for (const q of qs)
179
+ lines.push(`- ${q.trim()}`);
180
+ lines.push('');
181
+ }
182
+ }
183
+ if (isNonEmptyArray(harvest.followUps)) {
184
+ const fus = harvest.followUps.filter((f) => f && isNonEmptyString(f.question));
185
+ if (fus.length > 0) {
186
+ lines.push('## Follow-ups');
187
+ lines.push('');
188
+ for (const f of fus) {
189
+ if (isNonEmptyString(f.rationale)) {
190
+ lines.push(`- ${f.question.trim()} — ${f.rationale.trim()}`);
191
+ }
192
+ else {
193
+ lines.push(`- ${f.question.trim()}`);
194
+ }
195
+ }
196
+ lines.push('');
197
+ }
198
+ }
199
+ return lines.join('\n');
200
+ }
201
+ // ─────────────────────────────────────────────────────────────────────
202
+ // Challenge renderers
203
+ // ─────────────────────────────────────────────────────────────────────
204
+ const CHALLENGE_VERDICT_LABELS = {
205
+ GO: 'GO',
206
+ CONDITIONAL_GO: 'CONDITIONAL_GO',
207
+ NO_GO: 'NO_GO',
208
+ };
209
+ function renderChallengeVerdictLine(harvest) {
210
+ const verdict = harvest.verdict;
211
+ if (!verdict || !CHALLENGE_VERDICT_LABELS[verdict])
212
+ return null;
213
+ const confidenceText = typeof harvest.confidence === 'number'
214
+ ? ` (confidence: ${harvest.confidence.toFixed(1)})`
215
+ : '';
216
+ return `**Verdict: ${CHALLENGE_VERDICT_LABELS[verdict]}${confidenceText}**`;
217
+ }
218
+ function appendChallengeMitigations(lines, harvest) {
219
+ // Conditional: only render when verdict is CONDITIONAL_GO and mitigations are non-empty.
220
+ if (harvest.verdict !== 'CONDITIONAL_GO')
221
+ return;
222
+ const mitigations = (harvest.mitigations ?? []).filter(isNonEmptyString);
223
+ if (mitigations.length === 0)
224
+ return;
225
+ lines.push('## Required Mitigations');
226
+ lines.push('');
227
+ for (const m of mitigations)
228
+ lines.push(`- ${m.trim()}`);
229
+ lines.push('');
230
+ }
231
+ function appendChallengeAssumptions(lines, harvest) {
232
+ const assumptions = (harvest.assumptions ?? []).filter((a) => a && isNonEmptyString(a.assumption));
233
+ if (assumptions.length === 0)
234
+ return;
235
+ lines.push('## Assumptions');
236
+ lines.push('');
237
+ lines.push('| Assumption | Confidence | If Wrong | Bias | Bias Evidence |');
238
+ lines.push('| --- | --- | --- | --- | --- |');
239
+ for (const a of assumptions) {
240
+ lines.push(`| ${escapeTableCell(a.assumption)} | ${escapeTableCell(a.confidence ?? '')} | ${escapeTableCell(a.ifWrong ?? '')} | ${escapeTableCell(a.bias ?? '')} | ${escapeTableCell(a.biasEvidence ?? '')} |`);
72
241
  }
73
- // Open Questions
74
- if (data.harvest.openQuestions.length > 0) {
75
- lines.push('## Open Questions');
76
- for (const q of data.harvest.openQuestions) {
77
- lines.push(`- ${q}`);
242
+ lines.push('');
243
+ }
244
+ function appendConstraintLedger(lines, harvest) {
245
+ const entries = (harvest.constraintLedger ?? []).filter(isNonEmptyString);
246
+ if (entries.length === 0)
247
+ return;
248
+ lines.push('## Commitment Ledger');
249
+ lines.push('');
250
+ for (const entry of entries)
251
+ lines.push(`- ${entry.trim()}`);
252
+ lines.push('');
253
+ }
254
+ function appendChallengeEdgeCases(lines, harvest) {
255
+ const cases = (harvest.edgeCases ?? []).filter(isNonEmptyString);
256
+ if (cases.length === 0)
257
+ return;
258
+ lines.push('## Edge Cases');
259
+ lines.push('');
260
+ for (const c of cases)
261
+ lines.push(`- ${c.trim()}`);
262
+ lines.push('');
263
+ }
264
+ function appendChallengeFailureModes(lines, harvest) {
265
+ const modes = (harvest.failureModes ?? []).filter((m) => m && isNonEmptyString(m.component));
266
+ if (modes.length === 0)
267
+ return;
268
+ lines.push('## Failure Modes');
269
+ lines.push('');
270
+ lines.push('| Component | How It Fails | Blast Radius | Detection |');
271
+ lines.push('| --- | --- | --- | --- |');
272
+ for (const m of modes) {
273
+ lines.push(`| ${escapeTableCell(m.component)} | ${escapeTableCell(m.howItFails ?? '')} | ${escapeTableCell(m.blastRadius ?? '')} | ${escapeTableCell(m.detection ?? '')} |`);
274
+ }
275
+ lines.push('');
276
+ }
277
+ function appendChallengeCounterArguments(lines, harvest) {
278
+ const args = (harvest.counterArguments ?? []).filter((c) => c && isNonEmptyString(c.argument));
279
+ if (args.length === 0)
280
+ return;
281
+ lines.push('## Counter-Arguments');
282
+ lines.push('');
283
+ for (const c of args) {
284
+ lines.push(`- **For:** ${c.argument.trim()}`);
285
+ if (isNonEmptyString(c.rebuttal)) {
286
+ lines.push(` **Against:** ${c.rebuttal.trim()}`);
78
287
  }
288
+ }
289
+ lines.push('');
290
+ }
291
+ // ─────────────────────────────────────────────────────────────────────
292
+ // Problem-solve renderers
293
+ // ─────────────────────────────────────────────────────────────────────
294
+ const PROBLEM_SOLVE_VERDICT_LABELS = {
295
+ PASSED: 'PASSED',
296
+ CAVEATS: 'CAVEATS',
297
+ NEEDS_REVISION: 'NEEDS_REVISION',
298
+ };
299
+ function renderProblemSolveVerdictLine(harvest) {
300
+ const verdict = harvest.adversarialVerdict;
301
+ if (!verdict || !PROBLEM_SOLVE_VERDICT_LABELS[verdict])
302
+ return null;
303
+ return `**Adversarial Verdict: ${PROBLEM_SOLVE_VERDICT_LABELS[verdict]}**`;
304
+ }
305
+ function appendProblemSolveBiasAudit(lines, harvest) {
306
+ const audit = harvest.biasAudit;
307
+ if (!audit)
308
+ return;
309
+ const operating = (audit.biasesOperating ?? []).filter((b) => b && isNonEmptyString(b.bias) && isNonEmptyString(b.evidence));
310
+ const hasReversal = audit.frameReversal && (isNonEmptyString(audit.frameReversal.originalFraming) ||
311
+ isNonEmptyString(audit.frameReversal.invertedFraming) ||
312
+ typeof audit.frameReversal.wouldReachSameConclusion === 'boolean');
313
+ const hasStability = typeof audit.decisionStable === 'boolean';
314
+ if (operating.length === 0 && !hasReversal && !hasStability)
315
+ return;
316
+ lines.push('## Bias Audit');
317
+ lines.push('');
318
+ if (operating.length > 0) {
319
+ for (const b of operating) {
320
+ lines.push(`- **${b.bias.trim()}**: ${b.evidence.trim()}`);
321
+ if (isNonEmptyString(b.sourceReflex)) {
322
+ lines.push(` - Source reflex: ${b.sourceReflex.trim()}`);
323
+ }
324
+ if (isNonEmptyString(b.correction)) {
325
+ lines.push(` - Correction: ${b.correction.trim()}`);
326
+ }
327
+ }
328
+ lines.push('');
329
+ }
330
+ if (hasStability) {
331
+ lines.push(`Decision stable under bias check: ${audit.decisionStable ? 'yes' : 'no'}`);
79
332
  lines.push('');
80
333
  }
81
- // Follow-ups
82
- if (data.harvest.followUps.length > 0) {
83
- lines.push('## Follow-ups');
84
- for (const f of data.harvest.followUps) {
85
- lines.push(`- ${f.question} ${f.rationale}`);
334
+ if (hasReversal && audit.frameReversal) {
335
+ lines.push('### Frame Reversal');
336
+ lines.push('');
337
+ if (isNonEmptyString(audit.frameReversal.originalFraming)) {
338
+ lines.push(`- Original: ${audit.frameReversal.originalFraming.trim()}`);
339
+ }
340
+ if (isNonEmptyString(audit.frameReversal.invertedFraming)) {
341
+ lines.push(`- Inverted: ${audit.frameReversal.invertedFraming.trim()}`);
342
+ }
343
+ if (typeof audit.frameReversal.wouldReachSameConclusion === 'boolean') {
344
+ lines.push(`- Same conclusion under reversal: ${audit.frameReversal.wouldReachSameConclusion ? 'yes' : 'no'}`);
86
345
  }
87
346
  lines.push('');
88
347
  }
348
+ }
349
+ function appendProblemSolveReversalConditions(lines, harvest) {
350
+ const conds = (harvest.reversalConditions ?? []).filter(isNonEmptyString);
351
+ if (conds.length === 0)
352
+ return;
353
+ lines.push('## Reversal Conditions');
354
+ lines.push('');
355
+ for (const c of conds)
356
+ lines.push(`- ${c.trim()}`);
357
+ lines.push('');
358
+ }
359
+ /**
360
+ * Escape pipe and newline characters in markdown table cells.
361
+ * Keeps cell content on a single line and prevents pipe characters from
362
+ * breaking column alignment.
363
+ */
364
+ function escapeTableCell(value) {
365
+ return value
366
+ .replace(/\r?\n/g, ' ')
367
+ .replace(/\|/g, '\\|')
368
+ .trim();
369
+ }
370
+ /**
371
+ * Build a per-round markdown file body.
372
+ */
373
+ export function buildRoundMarkdown(round, now) {
374
+ const modeLabel = round.mode
375
+ ? round.mode.charAt(0).toUpperCase() + round.mode.slice(1)
376
+ : 'Round';
377
+ const lines = [];
378
+ lines.push(`# Round ${round.round} — ${modeLabel}`);
379
+ lines.push('');
380
+ lines.push(`*${formatDate(now)}*`);
381
+ lines.push('');
382
+ lines.push(round.body ?? '');
383
+ lines.push('');
89
384
  return lines.join('\n');
90
385
  }
386
+ function roundFilename(round) {
387
+ const modeSafe = (round.mode || 'round')
388
+ .toLowerCase()
389
+ .replace(/[^a-z0-9]+/g, '-')
390
+ .replace(/^-+|-+$/g, '') || 'round';
391
+ return `round-${round.round}-${modeSafe}.md`;
392
+ }
91
393
  /**
92
- * Write a minimal markdown summary of a completed session to .rvry/sessions/.
394
+ * Write session folder: harvest.md (always) + optional per-round files.
93
395
  *
94
396
  * Disabled via RVRY_LOCAL_SESSIONS=0. Fails silently on any error.
95
397
  */
96
398
  export async function writeLocalSession(data) {
97
- // Check opt-out
98
399
  if (process.env.RVRY_LOCAL_SESSIONS === '0')
99
400
  return;
100
401
  try {
101
- const now = new Date();
402
+ const now = data.now ?? new Date();
102
403
  const timestamp = formatTimestamp(now);
103
- const slug = slugify(data.question);
104
- const filename = `${timestamp}-${data.operationType}-${slug}.md`;
105
- const sessionsDir = join(process.cwd(), '.rvry', 'sessions');
106
- mkdirSync(sessionsDir, { recursive: true });
107
- const filePath = join(sessionsDir, filename);
108
- const content = buildMarkdown(data, now);
109
- writeFileSync(filePath, content, 'utf-8');
404
+ const slug = slugify(data.question) || 'session';
405
+ const folderName = `${timestamp}-${slug}`;
406
+ const baseDir = data.baseDir ?? process.cwd();
407
+ const sessionDir = join(baseDir, '.rvry', opFolder(data.operationType), folderName);
408
+ mkdirSync(sessionDir, { recursive: true });
409
+ const harvestPath = join(sessionDir, 'harvest.md');
410
+ writeFileSync(harvestPath, buildMarkdown(data, now), 'utf-8');
411
+ if (isNonEmptyArray(data.clientRounds)) {
412
+ for (const r of data.clientRounds) {
413
+ if (!r || typeof r.round !== 'number' || typeof r.mode !== 'string')
414
+ continue;
415
+ const fp = join(sessionDir, roundFilename(r));
416
+ writeFileSync(fp, buildRoundMarkdown(r, now), 'utf-8');
417
+ }
418
+ }
110
419
  }
111
420
  catch {
112
- // Fail silently -- this is a convenience feature, not critical path
421
+ // Fail silently -- convenience feature, not critical path.
113
422
  }
114
423
  }
package/dist/setup.d.ts CHANGED
@@ -9,4 +9,29 @@
9
9
  * npx --yes --package @rvry/mcp@latest rvry-mcp setup --client code (Claude Code only)
10
10
  * npx --yes --package @rvry/mcp@latest rvry-mcp setup --client desktop (Claude Desktop only)
11
11
  */
12
+ /** The /rvry shim (renamed from /think). Carries the Difficulty Gate + mode routing. */
13
+ declare const RVRY_SHIM_CONTENT: string;
14
+ /** Deprecation prefix for the /think tombstone command. */
15
+ declare const THINK_DEPRECATION_LINE = "FIRST: Tell the user that /think is now /rvry \u2014 the /think command will be removed in the next major version. Then proceed with the flow below.";
16
+ /** The /shimmer shim: 3-round reflective answering with client-owned harvest. */
17
+ declare const SHIMMER_SHIM_CONTENT = "Call the shimmer MCP tool with your question to start a reflective answering session.\n\nQuestion: $ARGUMENTS\n\nARGUMENT PARSING:\n1. If $ARGUMENTS contains the literal flag `--record`, remove it from the question text and REMEMBER that record mode is ON for this session.\n\nROUND 1 \u2014 OBSERVATION (status \"shimmer\"):\n1. The response's prompt field is your observation task. Work through it.\n2. Show the user your FULL observation, beginning with \"**RVRY SHIMMER**\" and the revery definition, exactly as the instruction field describes. Do NOT show a brief status line.\n3. End your observation with the commitments that surfaced, one per line as FORWARD:/FORBIDDEN:/QUESTION: lines \u2014 or the single line NOTHING if none surfaced.\n4. REMEMBER your observation text verbatim \u2014 you will pass it as harvest.shimmer on the final call.\n5. Call the shimmer tool again with your observation text as input and the sessionId.\n\nROUNDS 2-3 \u2014 ANSWER AND RESOLUTION (status \"active\"):\n1. Read the prompt field \u2014 this is your task. DO NOT display it to the user.\n2. Round 2 is the answer itself, working under the commitments you surfaced. Round 3 (if it occurs) resolves what the answer left unaddressed \u2014 engage each remaining commitment specifically or defer it with a reason.\n3. Show the user your answer as concise formatted markdown.\n4. Call the tool again with your full text as the input parameter, including the sessionId.\n - If record mode is ON, set `record: true` on every call and REMEMBER your round body as {round, mode, body} for the final call.\n\nFINAL CALL (the call whose response will have status \"complete\"):\nWhen you make the call that completes the session, also construct a `harvest` object on that SAME call:\n- title: a 1-4 word title capturing the topic\n- summary: 3-6 sentences of synthesis, leading with substance\n- keyFindings: 3-7 substantive bullet findings\n- shimmer: the full round-1 observation text you remembered, verbatim\n- constraintLedger: one line per surfaced commitment with its fate \u2014 addressed (and how), deferred (and why), or still open\n- openQuestions: [optional] remaining uncertainties\n- followUps: genuine next-question leads; omit the section if there are none\n\nIf record mode was ON, also include `rounds: [{round, mode, body}, ...]` on that final call with the bodies you remembered per round.\n\nDO NOT display: the prompt text you received (the round-1 observation you produce IS shown in full), constraint tables, quality scores, internal tracking data, or round numbers to the user.";
18
+ declare const SLASH_COMMANDS: {
19
+ file: string;
20
+ content: string;
21
+ label: string;
22
+ }[];
23
+ /**
24
+ * Install RVRY slash commands into the given directory.
25
+ * Only called when Claude Code is the selected client.
26
+ *
27
+ * RVRY owns these filenames: existing files are overwritten unconditionally,
28
+ * with the pre-existing content backed up to <name>.md.bak first (R-4).
29
+ *
30
+ * @param commandsDir - REQUIRED target directory. The real
31
+ * ~/.claude/commands path is resolved by runSetup and passed explicitly;
32
+ * there is deliberately no default so a bare call in a test can never
33
+ * touch the real directory.
34
+ */
35
+ declare function installSlashCommands(commandsDir: string): string[];
36
+ export { SLASH_COMMANDS, installSlashCommands, RVRY_SHIM_CONTENT, SHIMMER_SHIM_CONTENT, THINK_DEPRECATION_LINE };
12
37
  export declare function runSetup(): Promise<void>;