@cyber-dash-tech/revela 0.11.0 → 0.12.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,9 +1,14 @@
1
1
  /**
2
- * Prompt builder — assembles the three-layer system prompt.
2
+ * Prompt builder — assembles Revela system prompts.
3
3
  *
4
- * Layer 1: SKILL.md — core protocol (conversation flow, HTML rules, quality)
5
- * Layer 2: DOMAIN.md domain knowledge (report structure, terminology)
6
- * Layer 3: DESIGN.md visual style (colors, fonts, animations, layout)
4
+ * Narrative mode:
5
+ * Layer 1: NARRATIVE_SKILL.md audience/decision/claim/evidence readiness
6
+ * Layer 2: DOMAIN.md domain reasoning guidance when present
7
+ *
8
+ * Deck-render mode:
9
+ * Layer 1: SKILL.md — legacy deck render protocol (HTML rules, quality)
10
+ * Layer 2: DOMAIN.md — domain structure/terminology
11
+ * Layer 3: DESIGN.md — visual style (colors, fonts, animations, layout)
7
12
  *
8
13
  * When the active DESIGN.md has @section markers, only the global section,
9
14
  * layouts section, and a generated component index are injected into the
@@ -37,22 +42,43 @@ import { childLog } from "./log"
37
42
 
38
43
  const promptLog = childLog("prompt-builder")
39
44
 
40
- /** Path to SKILL.md shipped with this package. */
45
+ export type PromptMode = "narrative" | "deck-render"
46
+
47
+ export interface BuildPromptOptions {
48
+ mode?: PromptMode
49
+ designName?: string
50
+ domainName?: string
51
+ }
52
+
53
+ /** Path to deck-render SKILL.md shipped with this package. */
41
54
  const SKILL_MD_PATH = resolve(__dirname, "..", "skill", "SKILL.md")
55
+ const NARRATIVE_SKILL_MD_PATH = resolve(__dirname, "..", "skill", "NARRATIVE_SKILL.md")
42
56
 
43
57
  /**
44
- * Build the combined system prompt and write it to _active-prompt.md.
58
+ * Build the active system prompt and write it to _active-prompt.md.
59
+ *
60
+ * Backward-compatible call form:
61
+ * - buildPrompt() builds the default narrative prompt.
62
+ * - buildPrompt("aurora", "general") builds the default narrative prompt with active metadata overrides.
63
+ *
64
+ * New call form:
65
+ * - buildPrompt({ mode: "narrative" }) avoids design/HTML instructions.
66
+ * - buildPrompt({ mode: "deck-render" }) preserves the legacy deck render prompt.
45
67
  *
46
- * @param designName - Override design (defaults to active)
47
- * @param domainName - Override domain (defaults to active)
48
68
  * @returns The path to the written file.
49
69
  */
50
- export function buildPrompt(designName?: string, domainName?: string): string {
51
- const design = designName || activeDesign()
52
- const domain = domainName || activeDomain()
53
-
54
- // Layer 1 — SKILL.md
55
- const coreSkill = readFileSync(SKILL_MD_PATH, "utf-8")
70
+ export function buildPrompt(options?: BuildPromptOptions): string
71
+ export function buildPrompt(designName?: string, domainName?: string): string
72
+ export function buildPrompt(optionsOrDesignName?: BuildPromptOptions | string, legacyDomainName?: string): string {
73
+ const options = typeof optionsOrDesignName === "object" && optionsOrDesignName !== null
74
+ ? optionsOrDesignName
75
+ : { designName: optionsOrDesignName, domainName: legacyDomainName }
76
+ const mode: PromptMode = options.mode || "narrative"
77
+ const design = options.designName || activeDesign()
78
+ const domain = options.domainName || activeDomain()
79
+
80
+ // Layer 1 — core skill for the selected prompt mode.
81
+ const coreSkill = readFileSync(mode === "deck-render" ? SKILL_MD_PATH : NARRATIVE_SKILL_MD_PATH, "utf-8")
56
82
 
57
83
  // Check for preview.html
58
84
  const designDir = join(DESIGNS_DIR, design)
@@ -73,30 +99,37 @@ export function buildPrompt(designName?: string, domainName?: string): string {
73
99
  })
74
100
  }
75
101
 
76
- // Layer 3 — DESIGN.md: marker-aware or full-text fallback
77
- const designSkill = buildDesignLayer(design)
102
+ // Layer 3 — DESIGN.md: deck-render only. Narrative mode must not inject
103
+ // visual CSS, layout catalogs, component indexes, or HTML skeleton rules.
104
+ const designSkill = mode === "deck-render" ? buildDesignLayer(design) : ""
78
105
 
79
106
  // Assemble header
80
- const header =
81
- `<!-- Active design: ${design} -->\n` +
82
- `<!-- Active domain: ${domain} -->\n` +
83
- `<!-- Design files: ${designDir}/ -->\n` +
84
- `<!-- - DESIGN.md metadata + style instructions (injected below) -->\n` +
85
- `${previewLine}\n\n`
86
-
87
- // Three-layer concatenation: Header SKILL Domain Design
107
+ const header = mode === "deck-render"
108
+ ? `<!-- Revela prompt mode: deck-render -->\n` +
109
+ `<!-- Active design: ${design} -->\n` +
110
+ `<!-- Active domain: ${domain} -->\n` +
111
+ `<!-- Design files: ${designDir}/ -->\n` +
112
+ `<!-- - DESIGN.md — metadata + style instructions (injected below) -->\n` +
113
+ `${previewLine}\n\n`
114
+ : `<!-- Revela prompt mode: narrative -->\n` +
115
+ `<!-- Active domain: ${domain} -->\n` +
116
+ `<!-- Design layer intentionally omitted in narrative mode. Use deck-render mode before writing deck artifacts. -->\n\n`
117
+
118
+ // Concatenation: Header → Skill → Domain → Design (deck-render only)
88
119
  const parts = [header, coreSkill]
89
120
  if (domainSkill) {
90
121
  parts.push(`\n\n---\n\n${domainSkill}`)
91
122
  }
92
- parts.push(`\n\n---\n\n${designSkill}`)
123
+ if (designSkill) {
124
+ parts.push(`\n\n---\n\n${designSkill}`)
125
+ }
93
126
 
94
127
  const prompt = parts.join("")
95
128
 
96
129
  // Write to _active-prompt.md
97
130
  mkdirSync(CONFIG_DIR, { recursive: true })
98
131
  writeFileSync(ACTIVE_PROMPT_FILE, prompt, "utf-8")
99
- promptLog.info("prompt rebuilt", { design, domain, bytes: prompt.length })
132
+ promptLog.info("prompt rebuilt", { mode, design, domain, bytes: prompt.length })
100
133
 
101
134
  return ACTIVE_PROMPT_FILE
102
135
  }
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "crypto"
2
2
  import type { DeckSpec, DecksState, EvidenceRef, NarrativeBrief, ResearchAxis, SlideSpec, SourceMaterial } from "../decks-state"
3
+ import type { NarrativeEvidenceBinding, NarrativeStateV1 } from "../narrative-state/types"
3
4
  import { renderTargetId } from "./render-targets"
4
5
  import type { GraphEdge, GraphEdgeType, GraphNode, GraphNodeType, RenderTarget, WorkspaceGraph } from "./types"
5
6
 
@@ -19,7 +20,7 @@ export function projectWorkspaceGraph(state: DecksState, options: ProjectWorkspa
19
20
  for (const material of state.workspace.sourceMaterials ?? []) addSourceMaterial(builder, material)
20
21
  for (const axis of deck.researchPlan ?? []) addResearchFinding(builder, axis)
21
22
 
22
- const narrativeId = addNarrative(builder, deck)
23
+ const narrativeId = addNarrative(builder, state, deck)
23
24
  for (const slide of deck.slides.slice().sort((a, b) => a.index - b.index)) addSlide(builder, slide)
24
25
  for (const slide of deck.slides.slice().sort((a, b) => a.index - b.index)) addSlideClaimsAndEvidence(builder, slide)
25
26
  const targets = renderTargetsForDeck(state, deck)
@@ -84,7 +85,9 @@ function addResearchFinding(builder: GraphBuilder, axis: ResearchAxis): void {
84
85
  })
85
86
  }
86
87
 
87
- function addNarrative(builder: GraphBuilder, deck: DeckSpec): string | undefined {
88
+ function addNarrative(builder: GraphBuilder, state: DecksState, deck: DeckSpec): string | undefined {
89
+ if (hasCanonicalNarrative(state.narrative)) return addCanonicalNarrative(builder, state.narrative, deck)
90
+
88
91
  const brief = deck.narrativeBrief
89
92
  if (!hasNarrativeBrief(brief)) return undefined
90
93
 
@@ -122,6 +125,84 @@ function addNarrative(builder: GraphBuilder, deck: DeckSpec): string | undefined
122
125
  return narrativeId
123
126
  }
124
127
 
128
+ function addCanonicalNarrative(builder: GraphBuilder, narrative: NarrativeStateV1, deck: DeckSpec): string {
129
+ addNode(builder, {
130
+ id: narrative.id,
131
+ type: "narrativeIntent",
132
+ label: narrative.thesis?.statement || deck.goal || narrative.id,
133
+ data: compactData({
134
+ status: narrative.status,
135
+ goal: deck.goal,
136
+ audience: narrative.audience.primary || deck.audience,
137
+ language: deck.language,
138
+ beliefBefore: narrative.audience.beliefBefore,
139
+ beliefAfter: narrative.audience.beliefAfter,
140
+ decisionOrAction: narrative.decision.action,
141
+ thesis: narrative.thesis?.statement,
142
+ }),
143
+ })
144
+
145
+ for (const claim of narrative.claims) {
146
+ addNode(builder, {
147
+ id: claim.id,
148
+ type: "claim",
149
+ label: claim.text,
150
+ data: compactData({
151
+ text: claim.text,
152
+ kind: claim.kind,
153
+ importance: claim.importance,
154
+ evidenceRequired: claim.evidenceRequired,
155
+ evidenceStatus: claim.evidenceStatus,
156
+ supportedScope: claim.supportedScope,
157
+ unsupportedScope: claim.unsupportedScope,
158
+ caveats: claim.caveats,
159
+ source: "canonicalNarrative",
160
+ }),
161
+ })
162
+ addEdge(builder, "contains", narrative.id, claim.id)
163
+ }
164
+
165
+ for (const binding of narrative.evidenceBindings) {
166
+ const supportId = addNarrativeEvidenceSupportNode(builder, binding)
167
+ addEdge(builder, "supports", supportId, binding.claimId, compactData({
168
+ strength: binding.strength,
169
+ source: binding.source,
170
+ quote: binding.quote,
171
+ url: binding.url,
172
+ sourcePath: binding.sourcePath,
173
+ location: binding.location,
174
+ findingsFile: binding.findingsFile,
175
+ caveat: binding.caveat,
176
+ supportScope: binding.supportScope,
177
+ unsupportedScope: binding.unsupportedScope,
178
+ }))
179
+ }
180
+
181
+ for (const objection of narrative.objections) {
182
+ addNode(builder, {
183
+ id: objection.id,
184
+ type: "objection",
185
+ label: objection.text,
186
+ data: compactData({ text: objection.text, priority: objection.priority, response: objection.response }),
187
+ })
188
+ addEdge(builder, "contains", narrative.id, objection.id)
189
+ addEdge(builder, "challenges", objection.id, objection.claimId || narrative.id)
190
+ }
191
+
192
+ for (const risk of narrative.risks) {
193
+ addNode(builder, {
194
+ id: risk.id,
195
+ type: "risk",
196
+ label: risk.text,
197
+ data: compactData({ text: risk.text, severity: risk.severity, mitigation: risk.mitigation }),
198
+ })
199
+ addEdge(builder, "contains", narrative.id, risk.id)
200
+ addEdge(builder, "constrained_by", risk.claimId || narrative.id, risk.id)
201
+ }
202
+
203
+ return narrative.id
204
+ }
205
+
125
206
  function addSlide(builder: GraphBuilder, slide: SlideSpec): void {
126
207
  addNode(builder, {
127
208
  id: slideNodeId(slide.index),
@@ -222,6 +303,29 @@ function addEvidenceSupportNode(builder: GraphBuilder, evidence: EvidenceRef): s
222
303
  return id
223
304
  }
224
305
 
306
+ function addNarrativeEvidenceSupportNode(builder: GraphBuilder, binding: NarrativeEvidenceBinding): string {
307
+ if (binding.findingsFile?.trim()) {
308
+ const id = findingNodeId(binding.findingsFile)
309
+ addNode(builder, {
310
+ id,
311
+ type: "finding",
312
+ label: binding.findingsFile,
313
+ data: compactData({ findingsFile: binding.findingsFile, source: binding.source, quote: binding.quote, location: binding.location, caveat: binding.caveat }),
314
+ })
315
+ return id
316
+ }
317
+
318
+ const sourceKey = binding.sourcePath || binding.source || binding.url || "unknown-narrative-evidence-source"
319
+ const id = sourceNodeId(sourceKey)
320
+ addNode(builder, {
321
+ id,
322
+ type: "source",
323
+ label: sourceKey,
324
+ data: compactData({ source: binding.source, sourcePath: binding.sourcePath, url: binding.url }),
325
+ })
326
+ return id
327
+ }
328
+
225
329
  function addArtifact(builder: GraphBuilder, deck: DeckSpec, target: RenderTarget, narrativeId: string | undefined, targets: RenderTarget[]): void {
226
330
  const artifactId = artifactNodeId(target.outputPath ?? deck.outputPath)
227
331
  addNode(builder, {
@@ -308,6 +412,20 @@ function hasNarrativeBrief(brief: NarrativeBrief | undefined): boolean {
308
412
  )
309
413
  }
310
414
 
415
+ function hasCanonicalNarrative(narrative: NarrativeStateV1 | undefined): narrative is NarrativeStateV1 {
416
+ return Boolean(
417
+ narrative?.audience.primary?.trim() ||
418
+ narrative?.audience.beliefBefore?.trim() ||
419
+ narrative?.audience.beliefAfter?.trim() ||
420
+ narrative?.decision.action?.trim() ||
421
+ narrative?.thesis?.statement?.trim() ||
422
+ narrative?.claims.length ||
423
+ narrative?.evidenceBindings.length ||
424
+ narrative?.objections.length ||
425
+ narrative?.risks.length,
426
+ )
427
+ }
428
+
311
429
  function hasEvidenceDetail(evidence: EvidenceRef): boolean {
312
430
  return Boolean(
313
431
  evidence.quote?.trim() ||
@@ -88,8 +88,11 @@ export type WorkspaceActionType =
88
88
  | "source.extracted"
89
89
  | "research.findings_saved"
90
90
  | "research.findings_attached"
91
+ | "narrative.upserted"
92
+ | "deck.plan_compiled"
91
93
  | "evidence.candidate_generated"
92
94
  | "evidence.binding_applied"
95
+ | "narrative.approved"
93
96
  | "review.performed"
94
97
  | "artifact.rendered"
95
98
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "OpenCode plugin that turns AI into an HTML slide deck generator",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
package/plugin.ts CHANGED
@@ -60,7 +60,7 @@ import {
60
60
  } from "./lib/commands/designs-new"
61
61
  import { buildInitPrompt } from "./lib/commands/init"
62
62
  import { parseRememberArgs, buildRememberPrompt } from "./lib/commands/remember"
63
- import { buildReviewPrompt } from "./lib/commands/review"
63
+ import { buildDeckPrompt, buildDeckReviewPrompt, buildReviewPrompt } from "./lib/commands/review"
64
64
  import {
65
65
  extractDeckHtmlTargetsFromPatch,
66
66
  extractPatchTextArg,
@@ -326,6 +326,7 @@ const server: Plugin = (async (pluginCtx) => {
326
326
  throw new Error("__REVELA_DISABLE_HANDLED__")
327
327
  }
328
328
  if (sub === "init") {
329
+ buildPrompt({ mode: "narrative" })
329
330
  output.parts.length = 0
330
331
  output.parts.push({
331
332
  type: "text",
@@ -339,6 +340,7 @@ const server: Plugin = (async (pluginCtx) => {
339
340
  await send(parsed.error)
340
341
  throw new Error("__REVELA_REMEMBER_USAGE_HANDLED__")
341
342
  }
343
+ buildPrompt({ mode: "narrative" })
342
344
  output.parts.length = 0
343
345
  output.parts.push({
344
346
  type: "text",
@@ -348,9 +350,10 @@ const server: Plugin = (async (pluginCtx) => {
348
350
  }
349
351
  if (sub === "review") {
350
352
  if (param) {
351
- await send("`/revela review` no longer accepts a deck name. It reviews the current workspace deck.")
353
+ await send("`/revela review` no longer accepts a deck name. It reviews the current workspace narrative. Use `/revela deck --review` for deck/artifact readiness.")
352
354
  throw new Error("__REVELA_REVIEW_USAGE_HANDLED__")
353
355
  }
356
+ buildPrompt({ mode: "narrative" })
354
357
  output.parts.length = 0
355
358
  output.parts.push({
356
359
  type: "text",
@@ -358,6 +361,28 @@ const server: Plugin = (async (pluginCtx) => {
358
361
  } as any)
359
362
  return
360
363
  }
364
+ if (sub === "deck") {
365
+ if (param && param !== "--review") {
366
+ await send("Usage: `/revela deck` starts approved-narrative deck handoff; `/revela deck --review` reviews deck/artifact readiness.")
367
+ throw new Error("__REVELA_DECK_USAGE_HANDLED__")
368
+ }
369
+ if (!param) {
370
+ buildPrompt({ mode: "deck-render" })
371
+ output.parts.length = 0
372
+ output.parts.push({
373
+ type: "text",
374
+ text: buildDeckPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot }),
375
+ } as any)
376
+ return
377
+ }
378
+ buildPrompt({ mode: "deck-render" })
379
+ output.parts.length = 0
380
+ output.parts.push({
381
+ type: "text",
382
+ text: buildDeckReviewPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot }),
383
+ } as any)
384
+ return
385
+ }
361
386
  if (sub === "refine") {
362
387
  if (param) {
363
388
  await send("`/revela refine` does not accept a target. It opens the only HTML deck in `decks/`.")
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: revela-narrative
3
+ description: Build trusted narrative readiness before rendering deck artifacts
4
+ compatibility: opencode
5
+ ---
6
+
7
+ # Revela — Narrative Workspace
8
+
9
+ You help the user turn source materials, research, and intent into a trusted communication narrative before any deck is rendered.
10
+
11
+ Default mode is narrative-first. Do not generate HTML slides, choose visual layouts, fetch design components, or ask for slide count unless the user explicitly enters a deck-render workflow.
12
+
13
+ ## Core Job
14
+
15
+ Build and review the narrative state around:
16
+ - primary audience and stakeholder context
17
+ - audience belief before and desired belief after
18
+ - decision or action required
19
+ - thesis or central recommendation
20
+ - central claims and their evidence boundaries
21
+ - objections, risks, assumptions, caveats, and unsupported scope
22
+ - narrative approval state and whether approval is stale
23
+
24
+ ## Workspace State
25
+
26
+ Use `DECKS.json` as Revela's current compatibility workspace-state file. Do not write or patch it directly.
27
+
28
+ Use `revela-decks` for state operations:
29
+ - `read` to inspect current workspace state
30
+ - `init` to register discovered source material candidates during workspace initialization
31
+ - `upsertNarrative` to preserve canonical audience, decision, thesis, claims, evidence bindings, objections, and risks
32
+ - `upsertDeck` or `upsertSlides` only when explicitly needed by a deck/artifact workflow prompt
33
+ - `reviewNarrative` to run deterministic narrative readiness
34
+ - `approveNarrative` only when the user explicitly approves or requests an override
35
+
36
+ Never treat `writeReadiness.status`, old review snapshots, existing `decks/*.html`, or saved research actions as narrative approval.
37
+
38
+ ## Narrative Review Rules
39
+
40
+ When reviewing, call `revela-decks` action `reviewNarrative` and report the tool result as authoritative.
41
+
42
+ Use this report shape:
43
+ - `Narrative readiness: <status>`
44
+ - `Narrative hash: <hash>` when available
45
+ - blockers first, with issue type, claim text when available, and suggested next action
46
+ - warnings second, as residual risks
47
+ - approval state last, clearly distinguishing `ready_for_approval`, `approved`, stale approval, and render override
48
+
49
+ If evidence is missing, say what is missing and what should happen next. Do not invent quotes, sources, page locations, URLs, caveats, or research findings.
50
+
51
+ If research findings were saved but not attached or bound, describe them as unattached research state, not proof.
52
+
53
+ If the narrative is ready for approval, ask the user whether to approve or revise it. Do not approve automatically.
54
+
55
+ ## Boundaries
56
+
57
+ - Do not write or overwrite `decks/*.html` in narrative mode.
58
+ - Do not call `revela-decks review` in narrative mode; that is the deck/artifact gate.
59
+ - Do not apply evidence candidates, bind evidence, or rewrite slide text unless the user explicitly asks.
60
+ - Do not fetch design CSS, layouts, components, chart rules, or HTML skeletons in narrative mode.
61
+ - Do not store secrets, credentials, tokens, or sensitive personal information.
62
+ - Do not infer long-term user preferences from one-off tasks.
63
+
64
+ When the user wants deck/artifact readiness, direct them to `/revela deck --review`. When they want to render a deck, wait for the explicit deck workflow.
package/tools/decks.ts CHANGED
@@ -21,15 +21,49 @@ import { recordWorkspaceAction } from "../lib/workspace-state/actions"
21
21
  import { applyEvidenceBindings } from "../lib/workspace-state/evidence-status"
22
22
  import { attachResearchFindings } from "../lib/workspace-state/research-attachments"
23
23
  import { activeReviewTargetId, latestReviewSnapshotForTarget } from "../lib/workspace-state/review-snapshots"
24
+ import {
25
+ approveNarrativeState,
26
+ recordNarrativeApprovalAction,
27
+ recordNarrativeReviewAction,
28
+ reviewNarrativeState,
29
+ } from "../lib/narrative-state/readiness"
30
+ import { compileDeckPlanFromNarrative } from "../lib/narrative-state/render-plan"
31
+ import { normalizeCanonicalNarrativeState, normalizeNarrativeState } from "../lib/narrative-state/normalize"
32
+ import { narrativeToBrief } from "../lib/narrative-state/project-compat"
33
+ import type { NarrativeStateV1 } from "../lib/narrative-state/types"
34
+
35
+ function mergeNarrativeInput(current: NarrativeStateV1, input: Partial<NarrativeStateV1>): Partial<NarrativeStateV1> {
36
+ return {
37
+ ...current,
38
+ ...input,
39
+ id: current.id,
40
+ version: 1,
41
+ audience: {
42
+ ...current.audience,
43
+ ...(input.audience ?? {}),
44
+ },
45
+ decision: {
46
+ ...current.decision,
47
+ ...(input.decision ?? {}),
48
+ },
49
+ thesis: input.thesis ? { ...current.thesis, ...input.thesis } as NarrativeStateV1["thesis"] : current.thesis,
50
+ claims: input.claims ?? current.claims,
51
+ evidenceBindings: input.evidenceBindings ?? current.evidenceBindings,
52
+ objections: input.objections ?? current.objections,
53
+ risks: input.risks ?? current.risks,
54
+ approvals: current.approvals,
55
+ updatedAt: new Date().toISOString(),
56
+ }
57
+ }
24
58
 
25
59
  export default tool({
26
60
  description:
27
61
  `Read and update ${DECKS_STATE_FILE}, Revela's workspace deck state file. ` +
28
62
  "Use this tool instead of writing or patching the state file directly. " +
29
- "It stores active deck specs, per-slide content/layout/components, and computes write readiness.",
63
+ "It stores workspace narrative state, active deck specs, per-slide content/layout/components, and computes narrative or deck readiness.",
30
64
  args: {
31
65
  action: tool.schema
32
- .enum(["read", "init", "upsertDeck", "upsertSlides", "review", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
66
+ .enum(["read", "init", "upsertDeck", "upsertSlides", "upsertNarrative", "compileDeckPlan", "review", "reviewNarrative", "approveNarrative", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
33
67
  .describe("Action to perform on DECKS.json."),
34
68
  summary: tool.schema.boolean().optional().describe("For read: return a compact summary instead of full state."),
35
69
  goal: tool.schema.string().optional().describe("For upsertDeck: deck goal."),
@@ -45,6 +79,69 @@ export default tool({
45
79
  objections: tool.schema.array(tool.schema.string()).optional().describe("Likely stakeholder objections or questions the narrative should handle."),
46
80
  risks: tool.schema.array(tool.schema.string()).optional().describe("Risks, assumptions, caveats, or tradeoffs that should travel with the narrative."),
47
81
  }).optional().describe("For upsertDeck: 0.9 Narrative Compiler brief used to review story intent before writing."),
82
+ narrative: tool.schema.object({
83
+ status: tool.schema.enum(["draft", "needs_research", "needs_user_confirmation", "ready_for_approval", "approved"]).optional(),
84
+ audience: tool.schema.object({
85
+ primary: tool.schema.string().optional(),
86
+ secondary: tool.schema.array(tool.schema.string()).optional(),
87
+ beliefBefore: tool.schema.string().optional(),
88
+ beliefAfter: tool.schema.string().optional(),
89
+ decisionContext: tool.schema.string().optional(),
90
+ successCriteria: tool.schema.array(tool.schema.string()).optional(),
91
+ }).optional(),
92
+ decision: tool.schema.object({
93
+ action: tool.schema.string().optional(),
94
+ owner: tool.schema.string().optional(),
95
+ deadline: tool.schema.string().optional(),
96
+ decisionType: tool.schema.enum(["approve", "invest", "prioritize", "align", "choose", "understand", "other"]).optional(),
97
+ consequenceOfNoDecision: tool.schema.string().optional(),
98
+ }).optional(),
99
+ thesis: tool.schema.object({
100
+ id: tool.schema.string().optional(),
101
+ statement: tool.schema.string().optional(),
102
+ confidence: tool.schema.enum(["high", "medium", "low"]).optional(),
103
+ caveat: tool.schema.string().optional(),
104
+ }).optional(),
105
+ claims: tool.schema.array(tool.schema.object({
106
+ id: tool.schema.string().optional(),
107
+ kind: tool.schema.enum(["context", "problem", "opportunity", "evidence", "recommendation", "risk", "assumption", "ask"]).optional(),
108
+ text: tool.schema.string().describe("Claim text."),
109
+ importance: tool.schema.enum(["central", "supporting", "background"]).optional(),
110
+ evidenceRequired: tool.schema.boolean().optional(),
111
+ evidenceStatus: tool.schema.enum(["supported", "partial", "weak", "missing", "not_required"]).optional(),
112
+ supportedScope: tool.schema.string().optional(),
113
+ unsupportedScope: tool.schema.string().optional(),
114
+ caveats: tool.schema.array(tool.schema.string()).optional(),
115
+ })).optional(),
116
+ evidenceBindings: tool.schema.array(tool.schema.object({
117
+ id: tool.schema.string().optional(),
118
+ claimId: tool.schema.string().describe("Canonical claim id this evidence supports."),
119
+ source: tool.schema.string().describe("Source file, URL, research finding, or material name."),
120
+ sourcePath: tool.schema.string().optional(),
121
+ findingsFile: tool.schema.string().optional(),
122
+ quote: tool.schema.string().optional(),
123
+ location: tool.schema.string().optional(),
124
+ url: tool.schema.string().optional(),
125
+ caveat: tool.schema.string().optional(),
126
+ supportScope: tool.schema.string().optional(),
127
+ unsupportedScope: tool.schema.string().optional(),
128
+ strength: tool.schema.enum(["strong", "partial", "weak"]).optional(),
129
+ })).optional(),
130
+ objections: tool.schema.array(tool.schema.object({
131
+ id: tool.schema.string().optional(),
132
+ text: tool.schema.string().describe("Objection text."),
133
+ claimId: tool.schema.string().optional(),
134
+ priority: tool.schema.enum(["high", "medium", "low"]).optional(),
135
+ response: tool.schema.string().optional(),
136
+ })).optional(),
137
+ risks: tool.schema.array(tool.schema.object({
138
+ id: tool.schema.string().optional(),
139
+ text: tool.schema.string().describe("Risk, assumption, caveat, or tradeoff text."),
140
+ claimId: tool.schema.string().optional(),
141
+ severity: tool.schema.enum(["high", "medium", "low"]).optional(),
142
+ mitigation: tool.schema.string().optional(),
143
+ })).optional(),
144
+ }).optional().describe("For upsertNarrative: canonical narrative state fields to merge into DECKS.json. Replaces provided arrays, preserves approvals."),
48
145
  design: tool.schema.string().optional().describe("For upsertDeck: active design name."),
49
146
  domain: tool.schema.string().optional().describe("For upsertDeck: active domain name."),
50
147
  memory: tool.schema.string().optional().describe("For remember: explicit user or workflow preference to store."),
@@ -124,6 +221,9 @@ export default tool({
124
221
  findingsFile: tool.schema.string().optional().describe("For attachResearchFindings: workspace-relative researches/{topic}/{axis}.md file to attach to researchPlan."),
125
222
  researchAxis: tool.schema.string().optional().describe("For attachResearchFindings: researchPlan axis to attach the findings file to. Required when filename matching would be ambiguous."),
126
223
  researchStatus: tool.schema.enum(["done", "read"]).optional().describe("For attachResearchFindings: optional explicit status to set on the matched research axis."),
224
+ approvalNote: tool.schema.string().optional().describe("For approveNarrative: optional note explaining the approval or override."),
225
+ approvalBy: tool.schema.enum(["user", "override"]).optional().describe("For approveNarrative: use override only for explicit render overrides, not normal strategic approval."),
226
+ approvalScope: tool.schema.enum(["narrative", "render_override"]).optional().describe("For approveNarrative: narrative approval or explicit render override scope."),
127
227
  },
128
228
  async execute(args, context) {
129
229
  try {
@@ -194,6 +294,45 @@ export default tool({
194
294
  return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, deck: next.activeDeck ? next.decks[next.activeDeck] : undefined }, null, 2)
195
295
  }
196
296
 
297
+ if (args.action === "upsertNarrative") {
298
+ if (!args.narrative) return JSON.stringify({ ok: false, error: "narrative is required for upsertNarrative" })
299
+ const current = state.narrative ?? normalizeNarrativeState(state)
300
+ const merged = mergeNarrativeInput(current, args.narrative as Partial<NarrativeStateV1>)
301
+ const normalized = normalizeCanonicalNarrativeState(merged, state.activeDeck ?? defaultSlug)
302
+ if (!normalized) return JSON.stringify({ ok: false, error: "narrative could not be normalized" })
303
+ state.narrative = normalized
304
+
305
+ const deckKey = state.activeDeck
306
+ if (deckKey && state.decks[deckKey]) {
307
+ state = upsertDeck(state, {
308
+ ...state.decks[deckKey],
309
+ slug: deckKey,
310
+ audience: normalized.audience.primary || state.decks[deckKey].audience,
311
+ narrativeBrief: narrativeToBrief(normalized),
312
+ })
313
+ }
314
+
315
+ recordWorkspaceAction(state, {
316
+ type: "narrative.upserted",
317
+ actor: "revela-decks",
318
+ inputs: { hadExistingNarrative: Boolean(current), providedFields: Object.keys(args.narrative as object) },
319
+ outputs: {
320
+ narrativeId: normalized.id,
321
+ status: normalized.status,
322
+ claimCount: normalized.claims.length,
323
+ evidenceBindingCount: normalized.evidenceBindings.length,
324
+ objectionCount: normalized.objections.length,
325
+ riskCount: normalized.risks.length,
326
+ },
327
+ status: "success",
328
+ summary: `Updated canonical narrative state with ${normalized.claims.length} claim${normalized.claims.length === 1 ? "" : "s"}.`,
329
+ nodeIds: [normalized.id],
330
+ })
331
+
332
+ writeDecksState(workspaceRoot, state)
333
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, narrative: state.narrative, deck: state.activeDeck ? state.decks[state.activeDeck] : undefined }, null, 2)
334
+ }
335
+
197
336
  if (args.action === "review") {
198
337
  const reviewed = reviewDeckState(state, undefined, { workspaceRoot })
199
338
  const targetId = activeReviewTargetId(reviewed.state)
@@ -222,6 +361,45 @@ export default tool({
222
361
  return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: reviewed.result }, null, 2)
223
362
  }
224
363
 
364
+ if (args.action === "compileDeckPlan") {
365
+ const compiled = compileDeckPlanFromNarrative(state)
366
+ if (compiled.result.compiled) {
367
+ recordWorkspaceAction(compiled.state, {
368
+ type: "deck.plan_compiled",
369
+ actor: "revela-decks",
370
+ inputs: { narrativeId: compiled.state.narrative?.id, activeDeck: compiled.state.activeDeck },
371
+ outputs: {
372
+ narrativeHash: compiled.result.narrativeHash,
373
+ slideCount: compiled.result.slideCount,
374
+ outputPath: compiled.state.activeDeck ? compiled.state.decks[compiled.state.activeDeck]?.outputPath : undefined,
375
+ },
376
+ status: "success",
377
+ summary: `Compiled deck plan from canonical narrative with ${compiled.result.slideCount} slide${compiled.result.slideCount === 1 ? "" : "s"}.`,
378
+ nodeIds: [compiled.state.narrative?.id, compiled.state.activeDeck ? `artifact:${compiled.state.decks[compiled.state.activeDeck]?.outputPath ?? compiled.state.activeDeck}` : undefined].filter((item): item is string => Boolean(item)),
379
+ })
380
+ }
381
+ writeDecksState(workspaceRoot, compiled.state)
382
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: compiled.result, deck: compiled.state.activeDeck ? compiled.state.decks[compiled.state.activeDeck] : undefined, narrative: compiled.state.narrative }, null, 2)
383
+ }
384
+
385
+ if (args.action === "reviewNarrative") {
386
+ const reviewed = reviewNarrativeState(state)
387
+ recordNarrativeReviewAction(reviewed.state, reviewed.result)
388
+ writeDecksState(workspaceRoot, reviewed.state)
389
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: reviewed.result, narrative: reviewed.state.narrative }, null, 2)
390
+ }
391
+
392
+ if (args.action === "approveNarrative") {
393
+ const approved = approveNarrativeState(state, {
394
+ approvedBy: args.approvalBy,
395
+ scope: args.approvalScope,
396
+ note: args.approvalNote,
397
+ })
398
+ recordNarrativeApprovalAction(approved.state, approved.result)
399
+ writeDecksState(workspaceRoot, approved.state)
400
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: approved.result, narrative: approved.state.narrative }, null, 2)
401
+ }
402
+
225
403
  if (args.action === "applyEvidenceCandidates") {
226
404
  const candidateIds = args.candidateIds ?? []
227
405
  if (candidateIds.length === 0) return JSON.stringify({ ok: false, error: "candidateIds are required for applyEvidenceCandidates" })