@cyber-dash-tech/revela 0.8.9 → 0.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.8.9",
3
+ "version": "0.10.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
@@ -46,6 +46,8 @@ import {
46
46
  import { handlePdf } from "./lib/commands/pdf"
47
47
  import { buildPptxNotesPrompt, handlePptx, parsePptxArgs, resolvePptxDeck } from "./lib/commands/pptx"
48
48
  import { handleEdit } from "./lib/commands/edit"
49
+ import { handleInspect } from "./lib/commands/inspect"
50
+ import { handleRefine } from "./lib/commands/refine"
49
51
  import { ensureEditableDeckOpenForChange } from "./lib/edit/open"
50
52
  import { hasLiveEditorSessionForFile } from "./lib/edit/server"
51
53
  import { handleDesignsPreview } from "./lib/commands/designs-preview"
@@ -80,6 +82,8 @@ import mediaBatchSaveTool from "./tools/media-batch-save"
80
82
  import mediaSaveTool from "./tools/media-save"
81
83
  import researchImagesListTool from "./tools/research-images-list"
82
84
  import researchSaveTool from "./tools/research-save"
85
+ import inspectionContextTool from "./tools/inspection-context"
86
+ import inspectionResultTool from "./tools/inspection-result"
83
87
  import workspaceScanTool from "./tools/workspace-scan"
84
88
  import extractDocumentMaterialsTool from "./tools/extract-document-materials"
85
89
  import qaTool from "./tools/qa"
@@ -87,6 +91,7 @@ import pdfTool from "./tools/pdf"
87
91
  import pptxTool from "./tools/pptx"
88
92
  import createEditTool from "./tools/edit"
89
93
  import { RESEARCH_PROMPT, RESEARCH_AGENT_SIGNATURE } from "./lib/agents/research-prompt"
94
+ import { NARRATIVE_REVIEWER_PROMPT, NARRATIVE_REVIEWER_SIGNATURE } from "./lib/agents/narrative-reviewer-prompt"
90
95
  import { formatReport, runComplianceQA } from "./lib/qa"
91
96
  import { extractDesignClasses } from "./lib/design/designs"
92
97
  import { log, childLog } from "./lib/log"
@@ -204,7 +209,7 @@ const server: Plugin = (async (pluginCtx) => {
204
209
  }
205
210
 
206
211
  return {
207
- // ── Register /revela command + revela-research subagent ───────────────
212
+ // ── Register /revela command + Revela subagents ───────────────────────
208
213
  config: async (opencodeConfig) => {
209
214
  opencodeConfig.command ??= {}
210
215
  opencodeConfig.command["revela"] = {
@@ -235,6 +240,24 @@ const server: Plugin = (async (pluginCtx) => {
235
240
  // Give revela-research explicit websearch allow (overrides global deny below)
236
241
  ;(opencodeConfig.agent["revela-research"].permission as any).websearch = "allow"
237
242
 
243
+ // Register the read-only narrative reviewer subagent.
244
+ // It can inspect workspace state and referenced files, but cannot write or browse.
245
+ opencodeConfig.agent["revela-narrative-reviewer"] = {
246
+ description: "Revela narrative reviewer — read-only critique of narrative brief and slide-plan alignment",
247
+ mode: "subagent",
248
+ prompt: NARRATIVE_REVIEWER_PROMPT,
249
+ permission: {
250
+ edit: "deny",
251
+ bash: {
252
+ "*": "deny",
253
+ "ls *": "allow",
254
+ "ls": "allow",
255
+ },
256
+ webfetch: "deny",
257
+ websearch: "deny",
258
+ } as any,
259
+ }
260
+
238
261
  // Block websearch for the primary agent globally.
239
262
  // permission.ask hook is not triggered by OpenCode (no R.trigger call in binary).
240
263
  // tool.execute.before throw is swallowed (trigger().catch(()=>{})).
@@ -313,6 +336,14 @@ const server: Plugin = (async (pluginCtx) => {
313
336
  } as any)
314
337
  return
315
338
  }
339
+ if (sub === "refine") {
340
+ if (param) {
341
+ await send("`/revela refine` does not accept a target. It opens the only HTML deck in `decks/`.")
342
+ throw new Error("__REVELA_REFINE_USAGE_HANDLED__")
343
+ }
344
+ await handleRefine({ client, sessionID, workspaceRoot }, send)
345
+ throw new Error("__REVELA_REFINE_HANDLED__")
346
+ }
316
347
  if (sub === "edit") {
317
348
  if (param) {
318
349
  await send("`/revela edit` no longer accepts a target. It opens the only HTML deck in `decks/`.")
@@ -321,6 +352,14 @@ const server: Plugin = (async (pluginCtx) => {
321
352
  await handleEdit({ client, sessionID, workspaceRoot }, send)
322
353
  throw new Error("__REVELA_EDIT_HANDLED__")
323
354
  }
355
+ if (sub === "inspect") {
356
+ if (param) {
357
+ await send("`/revela inspect` does not accept a target. It opens the only HTML deck in `decks/`.")
358
+ throw new Error("__REVELA_INSPECT_USAGE_HANDLED__")
359
+ }
360
+ await handleInspect({ client, sessionID, workspaceRoot }, send)
361
+ throw new Error("__REVELA_INSPECT_HANDLED__")
362
+ }
324
363
  if (sub === "designs" && !param) {
325
364
  await handleDesignsList(send)
326
365
  throw new Error("__REVELA_DESIGNS_LIST_HANDLED__")
@@ -419,6 +458,8 @@ const server: Plugin = (async (pluginCtx) => {
419
458
  "revela-media-save": mediaSaveTool,
420
459
  "revela-research-images-list": researchImagesListTool,
421
460
  "revela-research-save": researchSaveTool,
461
+ "revela-inspection-context": inspectionContextTool,
462
+ "revela-inspection-result": inspectionResultTool,
422
463
  "revela-workspace-scan": workspaceScanTool,
423
464
  "revela-extract-document-materials": extractDocumentMaterialsTool,
424
465
  "revela-qa": qaTool,
@@ -481,7 +522,7 @@ const server: Plugin = (async (pluginCtx) => {
481
522
 
482
523
  // ── Inject three-layer prompt when enabled ─────────────────────────────
483
524
  // Skip injection for:
484
- // 1. revela-research subagent (has its own research-focused prompt)
525
+ // 1. Revela subagents (they have focused prompts)
485
526
  // 2. OpenCode internal agents (title, summary, compaction)
486
527
  "experimental.chat.system.transform": async (input, output) => {
487
528
  if (!ctx.enabled) return
@@ -498,6 +539,10 @@ const server: Plugin = (async (pluginCtx) => {
498
539
  }
499
540
  ctx.isResearchAgent = false
500
541
 
542
+ // Skip revela-narrative-reviewer subagent — it is read-only critique,
543
+ // not a deck-writing agent and not a research agent.
544
+ if (systemText.includes(NARRATIVE_REVIEWER_SIGNATURE)) return
545
+
501
546
  // Skip OpenCode internal system agents (title generator, summary, compaction)
502
547
  if (INTERNAL_AGENT_SIGNATURES.some((sig) => systemText.includes(sig))) return
503
548
 
package/skill/SKILL.md CHANGED
@@ -83,6 +83,7 @@ Create or update the active deck in `DECKS.json` through `revela-decks` actions
83
83
  `upsertDeck` and `upsertSlides`. Keep the deck spec current as work progresses:
84
84
  - `goal` — purpose and decision/context
85
85
  - `audience`, `language`, `outputPath`, and `theme`
86
+ - `narrativeBrief` — for substantial decision decks, the 0.9 compiler brief: audience belief before/after, decisionOrAction, narrativeArc, keyClaims, objections, and risks
86
87
  - `requiredInputs` — checklist state for prewrite readiness
87
88
  - `researchPlan` — axes, status, and findings files
88
89
  - `slides` — confirmed per-slide title, purpose, layout, components, content, evidence, visuals, and status
@@ -200,6 +201,26 @@ state updates. Do not write temporary hypotheses, unsupported conclusions,
200
201
  secrets, or inferred user preferences. User and workflow preferences require
201
202
  explicit user intent to remember.
202
203
 
204
+ #### Narrative Review via `revela-narrative-reviewer`
205
+
206
+ `revela-narrative-reviewer` is a read-only OpenCode subagent, **not a tool**.
207
+ Launch it through the Task tool with `subagent_type: "revela-narrative-reviewer"`
208
+ when a substantial decision deck needs independent rubric-based critique of the
209
+ Narrative Compiler brief and slide-plan alignment.
210
+
211
+ Use it after the narrative brief and slide specs are recorded in `DECKS.json`,
212
+ and before treating narrative quality as reviewed. The primary agent should not
213
+ self-certify semantic narrative quality. `revela-decks review` remains the
214
+ authoritative write-readiness gate; reviewer findings are advisory notes only.
215
+ The reviewer uses stable finding IDs such as `NB-001`, `KC-001`, `ASK-001`, and
216
+ `EV-001`. If the fixed rubric passes, it should return `Findings: none` rather
217
+ than inventing optional improvements.
218
+
219
+ The reviewer may read `DECKS.json`, slide specs, evidence refs, and existing
220
+ `researches/{workspace-key}/*.md` files referenced by the deck. It must not write
221
+ state, call `upsertDeck`, call `upsertSlides`, call `revela-decks review`, use
222
+ websearch/webfetch, or generate/edit HTML.
223
+
203
224
  #### AI Knowledge and User Questions
204
225
 
205
226
  Use AI knowledge only to fill remaining gaps around verified sources. Mark it
@@ -213,6 +234,8 @@ already checked and what specific missing information is needed.
213
234
 
214
235
  - **NEVER** use `websearch` directly from the primary agent; delegate web research to `revela-research` subagents
215
236
  - **NEVER** call `revela-research` as a tool; use Task with `subagent_type: "revela-research"`
237
+ - **NEVER** call `revela-narrative-reviewer` as a tool; use Task with `subagent_type: "revela-narrative-reviewer"`
238
+ - **NEVER** present `revela-narrative-reviewer` findings as authoritative `revela-decks review` blockers or readiness issues
216
239
  - **NEVER** collapse distinct research axes into one broad agent brief when parallel focused briefs would be clearer
217
240
  - **ALWAYS** use `revela-decks` action `read` before deciding what research is needed
218
241
  - **ALWAYS** read each `researches/{workspace-key}/{axis}.md` after agents complete
@@ -238,12 +261,17 @@ A 6-slide deck might be: Cover → Background → Content × 3 → Closing.
238
261
  An 8-slide deck might be: Cover → TOC → Background → Content × 3 → Summary → Closing.
239
262
  Never skip Cover, Background, or Closing regardless of deck length.
240
263
 
241
- **Every `<section class="slide">` must include a `slide-qa` attribute.** Set
242
- `slide-qa="true"` for content-heavy layouts (those marked ✓ in the Layout Index
243
- QA column of the active design). Set `slide-qa="false"` for structural or sparse
244
- layouts (cover, TOC, closing, quote, summary, etc.). When unsure, use `"false"`.
264
+ **Every `<section class="slide">` must include `slide-qa` and
265
+ `data-slide-index` attributes.** Set `slide-qa="true"` for content-heavy layouts
266
+ (those marked ✓ in the Layout Index QA column of the active design). Set
267
+ `slide-qa="false"` for structural or sparse layouts (cover, TOC, closing, quote,
268
+ summary, etc.). When unsure, use `"false"`.
269
+
270
+ `data-slide-index` is the canonical 1-based slide identity. It must match the
271
+ corresponding `DECKS.json` `slides[].index` value. Do not use 0-based
272
+ `data-index` as slide identity.
245
273
 
246
- Example: `<section class="slide" slide-qa="true" data-index="0">`
274
+ Example: `<section class="slide" slide-qa="true" data-slide-index="1">`
247
275
 
248
276
  The export QA path treats this as deck metadata. It is consumed when PDF/PPTX
249
277
  export runs preflight checks.
@@ -287,8 +315,17 @@ core rules and the visual design below.
287
315
 
288
316
  ### Phase 4 — Presentation Plan
289
317
 
290
- After all research is complete and findings have been read, present a detailed
291
- slide plan to the user **before writing any HTML**.
318
+ After all research is complete and findings have been read, present a compact narrative brief
319
+ and a detailed slide plan to the user **before writing any HTML**.
320
+
321
+ For substantial decision decks, first summarize the Narrative Compiler brief:
322
+ - Audience belief before: what the audience currently believes, assumes, or does not yet understand
323
+ - Audience belief after: what the audience should believe or understand after the deck
324
+ - Decision/action: the approval, decision, behavior, or next step the deck should drive
325
+ - Narrative arc: the intended story path, such as context -> tension -> evidence -> recommendation -> risk -> ask
326
+ - Key claims: the main claims the deck must prove
327
+ - Likely objections: stakeholder resistance or questions the story should handle
328
+ - Risks/assumptions: caveats, tradeoffs, or uncertainty that should travel with the recommendation
292
329
 
293
330
  Format the plan as a markdown table:
294
331
 
@@ -320,8 +357,9 @@ Then ask:
320
357
  - On change request → update the table and ask again
321
358
 
322
359
  After the user confirms the slide plan, update `DECKS.json` through `revela-decks`:
323
- - Call `upsertDeck` to mark completed `requiredInputs` only when explicitly satisfied.
360
+ - Call `upsertDeck` to preserve `narrativeBrief` when available and mark completed `requiredInputs` only when explicitly satisfied.
324
361
  - Call `upsertSlides` with the confirmed per-slide content, narrativeRole, layout, components, and evidence.
362
+ - For substantial decision decks, use Task with `subagent_type: "revela-narrative-reviewer"` for read-only rubric-based critique of narrativeBrief and slide-plan alignment. Ask for stable finding IDs and `Findings: none` when the rubric passes; do not ask the reviewer to write state, determine readiness, or brainstorm optional improvements.
325
363
  - Keep write readiness blocked until Phase 5 calls `revela-decks review` and the tool returns ready.
326
364
 
327
365
  ---
package/tools/decks.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { tool } from "@opencode-ai/plugin"
2
2
  import {
3
+ applyEvidenceCandidates,
3
4
  createDeckSpec,
4
5
  DECKS_STATE_FILE,
5
6
  normalizeWorkspaceDeckState,
@@ -10,6 +11,7 @@ import {
10
11
  writeDecksState,
11
12
  workspaceDeckSlug,
12
13
  type DeckSpec,
14
+ type NarrativeBrief,
13
15
  type RequiredInputs,
14
16
  type ResearchAxis,
15
17
  type SourceMaterial,
@@ -24,13 +26,22 @@ export default tool({
24
26
  "It stores active deck specs, per-slide content/layout/components, and computes write readiness.",
25
27
  args: {
26
28
  action: tool.schema
27
- .enum(["read", "init", "upsertDeck", "upsertSlides", "review", "remember"])
29
+ .enum(["read", "init", "upsertDeck", "upsertSlides", "review", "applyEvidenceCandidates", "remember"])
28
30
  .describe("Action to perform on DECKS.json."),
29
31
  summary: tool.schema.boolean().optional().describe("For read: return a compact summary instead of full state."),
30
32
  goal: tool.schema.string().optional().describe("For upsertDeck: deck goal."),
31
33
  audience: tool.schema.string().optional().describe("For upsertDeck: deck audience."),
32
34
  language: tool.schema.string().optional().describe("For upsertDeck: deck language."),
33
35
  outputPath: tool.schema.string().optional().describe("For upsertDeck: target output path, normally decks/{workspace-name}.html."),
36
+ narrativeBrief: tool.schema.object({
37
+ audienceBeliefBefore: tool.schema.string().optional().describe("What the audience currently believes, assumes, or does not yet understand."),
38
+ audienceBeliefAfter: tool.schema.string().optional().describe("What the audience should believe or understand after the deck."),
39
+ decisionOrAction: tool.schema.string().optional().describe("The decision, approval, action, or behavioral change the deck is meant to drive."),
40
+ narrativeArc: tool.schema.string().optional().describe("Compact story arc, such as context -> tension -> evidence -> recommendation -> ask."),
41
+ keyClaims: tool.schema.array(tool.schema.string()).optional().describe("Main claims the deck must prove or communicate."),
42
+ objections: tool.schema.array(tool.schema.string()).optional().describe("Likely stakeholder objections or questions the narrative should handle."),
43
+ risks: tool.schema.array(tool.schema.string()).optional().describe("Risks, assumptions, caveats, or tradeoffs that should travel with the narrative."),
44
+ }).optional().describe("For upsertDeck: 0.9 Narrative Compiler brief used to review story intent before writing."),
34
45
  design: tool.schema.string().optional().describe("For upsertDeck: active design name."),
35
46
  domain: tool.schema.string().optional().describe("For upsertDeck: active domain name."),
36
47
  memory: tool.schema.string().optional().describe("For remember: explicit user or workflow preference to store."),
@@ -106,6 +117,7 @@ export default tool({
106
117
  status: tool.schema.enum(["planned", "ready", "written", "qa_passed", "qa_failed"]).optional().describe("Slide production status."),
107
118
  notes: tool.schema.string().optional().describe("Implementation notes for this slide."),
108
119
  })).optional().describe("For upsertSlides: complete or partial slide specs."),
120
+ candidateIds: tool.schema.array(tool.schema.string()).optional().describe("For applyEvidenceCandidates: candidate IDs returned by revela-decks review to explicitly bind proposed evidenceDraft records into slide evidence."),
109
121
  },
110
122
  async execute(args, context) {
111
123
  try {
@@ -140,6 +152,7 @@ export default tool({
140
152
  audience: args.audience ?? existing?.audience,
141
153
  language: args.language ?? existing?.language,
142
154
  outputPath: args.outputPath ?? existing?.outputPath,
155
+ narrativeBrief: (args.narrativeBrief as NarrativeBrief | undefined) ?? existing?.narrativeBrief,
143
156
  theme: {
144
157
  design: args.design ?? existing?.theme?.design,
145
158
  domain: args.domain ?? existing?.theme?.domain,
@@ -164,11 +177,19 @@ export default tool({
164
177
  }
165
178
 
166
179
  if (args.action === "review") {
167
- const reviewed = reviewDeckState(state)
180
+ const reviewed = reviewDeckState(state, undefined, { workspaceRoot })
168
181
  writeDecksState(workspaceRoot, reviewed.state)
169
182
  return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: reviewed.result }, null, 2)
170
183
  }
171
184
 
185
+ if (args.action === "applyEvidenceCandidates") {
186
+ const candidateIds = args.candidateIds ?? []
187
+ if (candidateIds.length === 0) return JSON.stringify({ ok: false, error: "candidateIds are required for applyEvidenceCandidates" })
188
+ const applied = applyEvidenceCandidates(state, candidateIds, { workspaceRoot })
189
+ writeDecksState(workspaceRoot, applied.state)
190
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: applied.result }, null, 2)
191
+ }
192
+
172
193
  if (args.action === "remember") {
173
194
  const memory = args.memory?.trim()
174
195
  if (!memory) return JSON.stringify({ ok: false, error: "memory is required for remember" })
@@ -0,0 +1,22 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import { compileInspectionContext } from "../lib/inspection-context/compile"
3
+ import { normalizeWorkspaceDeckState, readOrCreateDecksState } from "../lib/decks-state"
4
+
5
+ export default tool({
6
+ description:
7
+ "Compile Revela's current DECKS.json into structured inspection context for debugging and future Evidence Inspector flows. " +
8
+ "This is read-only: it does not write artifacts, mutate DECKS.json, or generate user-facing files.",
9
+ args: {
10
+ slug: tool.schema.string().optional().describe("Optional deck slug to compile. Defaults to the active workspace deck."),
11
+ },
12
+ async execute(args, context) {
13
+ try {
14
+ const workspaceRoot = context.directory ?? process.cwd()
15
+ const state = normalizeWorkspaceDeckState(readOrCreateDecksState(workspaceRoot), workspaceRoot)
16
+ const inspectionContext = compileInspectionContext(state, args.slug)
17
+ return JSON.stringify({ ok: true, inspectionContext }, null, 2)
18
+ } catch (e: any) {
19
+ return JSON.stringify({ ok: false, error: e.message || String(e) })
20
+ }
21
+ },
22
+ })
@@ -0,0 +1,63 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import { completeInspectRequest } from "../lib/inspect/requests"
3
+ import type { InspectionResult } from "../lib/inspection-context/result"
4
+
5
+ const evidenceSourceItemSchema = tool.schema.object({
6
+ source: tool.schema.string().describe("Human-readable source label."),
7
+ sourcePath: tool.schema.string().optional(),
8
+ findingsFile: tool.schema.string().optional(),
9
+ location: tool.schema.string().optional(),
10
+ page: tool.schema.string().optional(),
11
+ url: tool.schema.string().optional(),
12
+ quote: tool.schema.string().optional(),
13
+ caveat: tool.schema.string().optional(),
14
+ })
15
+
16
+ export default tool({
17
+ description:
18
+ "Submit the final structured Evidence Inspector result for a pending /revela inspect request. " +
19
+ "Use only when responding to an inspection prompt. This updates the local browser inspector; it does not mutate DECKS.json or deck files.",
20
+ args: {
21
+ requestId: tool.schema.string().describe("Pending inspection request id from the inspection prompt."),
22
+ result: tool.schema.object({
23
+ version: tool.schema.number().describe("Must be 1."),
24
+ status: tool.schema.enum(["success", "no_match"]),
25
+ selectedText: tool.schema.string().optional(),
26
+ slide: tool.schema.object({
27
+ index: tool.schema.number(),
28
+ title: tool.schema.string(),
29
+ }).optional(),
30
+ matchConfidence: tool.schema.enum(["none", "low", "medium", "high"]),
31
+ cards: tool.schema.object({
32
+ purpose: tool.schema.object({
33
+ status: tool.schema.enum(["clear", "weak", "misplaced", "unknown"]),
34
+ role: tool.schema.string().optional(),
35
+ rationale: tool.schema.string(),
36
+ whyItMatters: tool.schema.string(),
37
+ }),
38
+ source: tool.schema.object({
39
+ status: tool.schema.enum(["supported", "weak", "unsupported", "not_needed", "unknown"]),
40
+ matchedClaim: tool.schema.string().optional(),
41
+ sources: tool.schema.array(evidenceSourceItemSchema),
42
+ warnings: tool.schema.array(tool.schema.string()),
43
+ gaps: tool.schema.array(tool.schema.string()),
44
+ caveats: tool.schema.array(tool.schema.string()),
45
+ rationale: tool.schema.string(),
46
+ }),
47
+ }),
48
+ stale: tool.schema.object({
49
+ stale: tool.schema.boolean(),
50
+ reason: tool.schema.string().optional(),
51
+ }).optional(),
52
+ }).describe("Final structured inspector result to render in the browser."),
53
+ },
54
+ async execute(args) {
55
+ try {
56
+ if (args.result.version !== 1) throw new Error("Inspection result version must be 1.")
57
+ const request = completeInspectRequest(args.requestId, args.result as InspectionResult)
58
+ return JSON.stringify({ ok: true, requestId: request.requestId, status: request.status })
59
+ } catch (e: any) {
60
+ return JSON.stringify({ ok: false, error: e.message || String(e) })
61
+ }
62
+ },
63
+ })