@cyber-dash-tech/revela 0.14.0 → 0.15.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/plugin.ts CHANGED
@@ -23,6 +23,7 @@ import { seedBuiltinDomains } from "./lib/domain/domains"
23
23
  import { buildPrompt } from "./lib/prompt-builder"
24
24
  import { ACTIVE_PROMPT_FILE } from "./lib/config"
25
25
  import { ctx } from "./lib/ctx"
26
+ import { formatCommandIntentSystemBlock, setPendingCommandIntent, takePendingCommandIntent } from "./lib/command-intent"
26
27
  import { preRead } from "./lib/read-hooks"
27
28
  import { postRead } from "./lib/read-hooks"
28
29
  import { extractPdfText } from "./lib/read-hooks/extractors/pdf"
@@ -48,9 +49,8 @@ import { buildPptxNotesPrompt, handlePptx, parsePptxArgs, resolvePptxDeck } from
48
49
  import { handleEdit } from "./lib/commands/edit"
49
50
  import { handleInspect } from "./lib/commands/inspect"
50
51
  import { handleRefine } from "./lib/commands/refine"
51
- import { formatDeckHtmlContractReport, validateDeckHtmlContract } from "./lib/deck-html/contract"
52
- import { ensureEditableDeckOpenForChange } from "./lib/edit/open"
53
- import { hasLiveEditorSessionForFile } from "./lib/edit/server"
52
+ import { formatArtifactQAReport, runArtifactQA } from "./lib/qa/artifact"
53
+ import { ensureRefineDeckOpenForChange } from "./lib/refine/open"
54
54
  import { handleDesignsPreview } from "./lib/commands/designs-preview"
55
55
  import {
56
56
  parseDesignsNewArgs,
@@ -59,8 +59,9 @@ import {
59
59
  buildDesignsEditPrompt,
60
60
  } from "./lib/commands/designs-new"
61
61
  import { buildInitPrompt } from "./lib/commands/init"
62
+ import { buildResearchPrompt } from "./lib/commands/research"
62
63
  import { handleBrief, parseBriefArgs } from "./lib/commands/brief"
63
- import { buildNarrativeViewPrompt, handleNarrative, parseNarrativeArgs } from "./lib/commands/narrative"
64
+ import { buildNarrativeViewPrompt, handleNarrative, parseNarrativeArgs, parseStoryArgs } from "./lib/commands/narrative"
64
65
  import { parseRememberArgs, buildRememberPrompt } from "./lib/commands/remember"
65
66
  import { buildDeckPrompt, buildDeckReviewPrompt, buildReviewPrompt } from "./lib/commands/review"
66
67
  import {
@@ -71,7 +72,6 @@ import {
71
72
  } from "./lib/decks-memory"
72
73
  import {
73
74
  buildDecksStatePromptLayer,
74
- checkDeckStateWriteReadiness,
75
75
  DECKS_STATE_FILE,
76
76
  extractDecksStateTargetsFromPatch,
77
77
  hasDecksState,
@@ -96,7 +96,6 @@ import pptxTool from "./tools/pptx"
96
96
  import createEditTool from "./tools/edit"
97
97
  import { RESEARCH_PROMPT, RESEARCH_AGENT_SIGNATURE } from "./lib/agents/research-prompt"
98
98
  import { NARRATIVE_REVIEWER_PROMPT, NARRATIVE_REVIEWER_SIGNATURE } from "./lib/agents/narrative-reviewer-prompt"
99
- import { formatReport, runComplianceQA } from "./lib/qa"
100
99
  import { extractDesignClasses } from "./lib/design/designs"
101
100
  import { log, childLog } from "./lib/log"
102
101
 
@@ -152,53 +151,29 @@ const server: Plugin = (async (pluginCtx) => {
152
151
  const client = pluginCtx.client
153
152
  const workspaceRoot = pluginCtx.directory
154
153
  const blockedDeckWrites = new Map<string, string>()
155
- const blockedDeckPatches = new Map<string, string>()
154
+ const blockedPatches = new Map<string, string>()
156
155
 
157
- async function appendComplianceReport(filePath: string, output: any): Promise<void> {
158
- if (!isDeckHtmlPath(filePath)) return
156
+ async function runPostWriteArtifactQA(filePath: string, output: any): Promise<boolean> {
157
+ if (!isDeckHtmlPath(filePath)) return true
159
158
 
160
159
  try {
161
160
  let vocabulary
162
161
  try {
163
162
  vocabulary = extractDesignClasses()
164
163
  } catch {
165
- // Design may not be installed or may have no markers — skip compliance.
164
+ // Design may not be installed or may have no markers — skip compliance vocabulary.
166
165
  }
167
166
 
168
- const report = runComplianceQA(filePath, vocabulary)
169
- if (report.totalIssues === 0) return
170
-
171
- appendToolResult(
172
- output,
173
- "---\n\n**[revela design compliance]** Static check completed:\n\n" +
174
- formatReport(report)
175
- )
176
- } catch (e) {
177
- childLog("compliance").warn("static compliance failed", {
178
- filePath,
179
- error: e instanceof Error ? e.message : String(e),
180
- })
181
- }
182
- }
183
-
184
- async function appendDeckHtmlContractReport(filePath: string, output: any): Promise<void> {
185
- if (!isDeckHtmlPath(filePath)) return
186
-
187
- try {
188
- const report = validateDeckHtmlContract(workspaceRoot, filePath)
189
- if (report.status === "valid" || report.status === "skipped") return
190
-
191
- appendToolResult(
192
- output,
193
- "---\n\n**[revela deck HTML contract]** Slide identity check failed:\n\n" +
194
- formatDeckHtmlContractReport(report) +
195
- "\n\nFix every `<section class=\"slide\">` to use the matching 1-based `data-slide-index` from DECKS.json before inspection or export."
196
- )
167
+ const report = await runArtifactQA({ workspaceRoot, filePath, vocabulary })
168
+ appendToolResult(output, "---\n\n" + formatArtifactQAReport(report))
169
+ return report.passed
197
170
  } catch (e) {
198
- childLog("deck-contract").warn("deck HTML contract report failed", {
171
+ childLog("artifact-qa").warn("post-write artifact QA failed", {
199
172
  filePath,
200
173
  error: e instanceof Error ? e.message : String(e),
201
174
  })
175
+ appendToolResult(output, "---\n\n## Artifact QA: FAILED\n\nError running artifact QA: " + (e instanceof Error ? e.message : String(e)))
176
+ return false
202
177
  }
203
178
  }
204
179
 
@@ -206,17 +181,38 @@ const server: Plugin = (async (pluginCtx) => {
206
181
  return input?.sessionID ?? input?.session?.id ?? input?.context?.sessionID ?? ""
207
182
  }
208
183
 
209
- function ensureEditorOpenAfterDeckChange(filePath: string, sessionID: string): void {
184
+ function queueWorkflowCommand(input: {
185
+ sessionID: string
186
+ name: string
187
+ mode: "narrative" | "deck-render"
188
+ visibleText: string
189
+ hiddenPrompt: string
190
+ output: any
191
+ }): void {
192
+ ctx.enabled = true
193
+ buildPrompt({ mode: input.mode })
194
+ setPendingCommandIntent({
195
+ sessionID: input.sessionID,
196
+ name: input.name,
197
+ mode: input.mode,
198
+ visibleText: input.visibleText,
199
+ hiddenPrompt: input.hiddenPrompt,
200
+ })
201
+ input.output.parts.length = 0
202
+ input.output.parts.push({ type: "text", text: input.visibleText } as any)
203
+ }
204
+
205
+ function ensureRefineOpenAfterDeckChange(filePath: string, sessionID: string): void {
210
206
  if (!isDeckHtmlPath(filePath) || !sessionID) return
211
207
 
212
208
  try {
213
- ensureEditableDeckOpenForChange("", {
209
+ ensureRefineDeckOpenForChange("", {
214
210
  client,
215
211
  sessionID,
216
212
  workspaceRoot,
217
213
  })
218
214
  } catch (e) {
219
- childLog("edit").warn("failed to ensure visual editor after deck change", {
215
+ childLog("refine").warn("failed to ensure Refine after deck change", {
220
216
  filePath,
221
217
  error: e instanceof Error ? e.message : String(e),
222
218
  })
@@ -239,7 +235,7 @@ const server: Plugin = (async (pluginCtx) => {
239
235
  opencodeConfig.command ??= {}
240
236
  opencodeConfig.command["revela"] = {
241
237
  template: "",
242
- description: "Revela — AI slide deck generator (enable/disable, manage designs & domains)",
238
+ description: "Revela — narrative artifact workspace (init, research, story, make, refine, design)",
243
239
  }
244
240
 
245
241
  // Register the research subagent.
@@ -329,12 +325,14 @@ const server: Plugin = (async (pluginCtx) => {
329
325
  throw new Error("__REVELA_DISABLE_HANDLED__")
330
326
  }
331
327
  if (sub === "init") {
332
- buildPrompt({ mode: "narrative" })
333
- output.parts.length = 0
334
- output.parts.push({
335
- type: "text",
336
- text: buildInitPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot }),
337
- } as any)
328
+ queueWorkflowCommand({
329
+ sessionID,
330
+ name: "init",
331
+ mode: "narrative",
332
+ visibleText: "Initialize Revela workspace.",
333
+ hiddenPrompt: buildInitPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot }),
334
+ output,
335
+ })
338
336
  return
339
337
  }
340
338
  if (sub === "remember") {
@@ -343,28 +341,74 @@ const server: Plugin = (async (pluginCtx) => {
343
341
  await send(parsed.error)
344
342
  throw new Error("__REVELA_REMEMBER_USAGE_HANDLED__")
345
343
  }
346
- buildPrompt({ mode: "narrative" })
347
- output.parts.length = 0
348
- output.parts.push({
349
- type: "text",
350
- text: buildRememberPrompt({ memory: parsed.memory, exists: hasDecksState(workspaceRoot) }),
351
- } as any)
344
+ queueWorkflowCommand({
345
+ sessionID,
346
+ name: "remember",
347
+ mode: "narrative",
348
+ visibleText: "Remember Revela workspace preference.",
349
+ hiddenPrompt: buildRememberPrompt({ memory: parsed.memory, exists: hasDecksState(workspaceRoot) }),
350
+ output,
351
+ })
352
+ return
353
+ }
354
+ if (sub === "research") {
355
+ if (param) {
356
+ await send("`/revela research` does not accept arguments yet. Add the research question in normal chat, or run it to work from open story gaps.")
357
+ throw new Error("__REVELA_RESEARCH_USAGE_HANDLED__")
358
+ }
359
+ queueWorkflowCommand({
360
+ sessionID,
361
+ name: "research",
362
+ mode: "narrative",
363
+ visibleText: "Research Revela story gaps.",
364
+ hiddenPrompt: buildResearchPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot }),
365
+ output,
366
+ })
352
367
  return
353
368
  }
354
369
  if (sub === "review") {
355
370
  if (param) {
356
- await send("`/revela review` no longer accepts a deck name. It reviews the current workspace narrative. Use `/revela deck --review` for deck/artifact readiness.")
371
+ await send("`/revela review` no longer accepts a deck name. It is a compatibility alias for `/revela story`. Use `/revela make deck --review` for deck/artifact readiness.")
357
372
  throw new Error("__REVELA_REVIEW_USAGE_HANDLED__")
358
373
  }
359
- buildPrompt({ mode: "narrative" })
360
- output.parts.length = 0
361
- output.parts.push({
362
- type: "text",
363
- text: buildReviewPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot }),
364
- } as any)
374
+ queueWorkflowCommand({
375
+ sessionID,
376
+ name: "review",
377
+ mode: "narrative",
378
+ visibleText: "Review Revela story readiness.",
379
+ hiddenPrompt: buildReviewPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot }),
380
+ output,
381
+ })
382
+ return
383
+ }
384
+ if (sub === "story") {
385
+ const parsed = parseStoryArgs(param)
386
+ if (!parsed.ok) {
387
+ await send(parsed.error)
388
+ throw new Error("__REVELA_STORY_USAGE_HANDLED__")
389
+ }
390
+ queueWorkflowCommand({
391
+ sessionID,
392
+ name: "story",
393
+ mode: "narrative",
394
+ visibleText: "Open Revela story workspace.",
395
+ hiddenPrompt: buildNarrativeViewPrompt({ workspaceRoot, language: parsed.args.language }),
396
+ output,
397
+ })
365
398
  return
366
399
  }
367
400
  if (sub === "narrative") {
401
+ if (!param) {
402
+ queueWorkflowCommand({
403
+ sessionID,
404
+ name: "narrative",
405
+ mode: "narrative",
406
+ visibleText: "Open Revela story workspace.",
407
+ hiddenPrompt: buildNarrativeViewPrompt({ workspaceRoot, language: "en" }),
408
+ output,
409
+ })
410
+ return
411
+ }
368
412
  const parsed = parseNarrativeArgs(param)
369
413
  if (!parsed.ok) {
370
414
  await send(parsed.error)
@@ -374,12 +418,14 @@ const server: Plugin = (async (pluginCtx) => {
374
418
  await handleNarrative({ workspaceRoot, openBrowser: true, language: parsed.args.language }, send)
375
419
  throw new Error("__REVELA_NARRATIVE_HANDLED__")
376
420
  }
377
- buildPrompt({ mode: "narrative" })
378
- output.parts.length = 0
379
- output.parts.push({
380
- type: "text",
381
- text: buildNarrativeViewPrompt({ workspaceRoot, language: parsed.args.language }),
382
- } as any)
421
+ queueWorkflowCommand({
422
+ sessionID,
423
+ name: "narrative view",
424
+ mode: "narrative",
425
+ visibleText: "Open read-only Revela narrative map.",
426
+ hiddenPrompt: buildNarrativeViewPrompt({ workspaceRoot, language: parsed.args.language }),
427
+ output,
428
+ })
383
429
  return
384
430
  }
385
431
  if (sub === "brief") {
@@ -391,26 +437,62 @@ const server: Plugin = (async (pluginCtx) => {
391
437
  await handleBrief({ workspaceRoot, outputPath: parsed.args.outputPath }, send)
392
438
  throw new Error("__REVELA_BRIEF_HANDLED__")
393
439
  }
440
+ if (sub === "make") {
441
+ const target = args[1]?.toLowerCase() ?? ""
442
+ const makeParam = args.slice(2).join(" ")
443
+ if (target === "deck") {
444
+ if (makeParam && makeParam !== "--review") {
445
+ await send("Usage: `/revela make deck` starts approved-narrative deck handoff; `/revela make deck --review` reviews deck/artifact readiness.")
446
+ throw new Error("__REVELA_MAKE_DECK_USAGE_HANDLED__")
447
+ }
448
+ queueWorkflowCommand({
449
+ sessionID,
450
+ name: makeParam ? "make deck --review" : "make deck",
451
+ mode: "deck-render",
452
+ visibleText: makeParam ? "Review Revela deck artifact readiness." : "Make Revela deck from approved story.",
453
+ hiddenPrompt: makeParam
454
+ ? buildDeckReviewPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot })
455
+ : buildDeckPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot }),
456
+ output,
457
+ })
458
+ return
459
+ }
460
+ if (target === "brief") {
461
+ const parsed = parseBriefArgs(makeParam)
462
+ if (!parsed.ok) {
463
+ await send(parsed.error.replace("/revela brief", "/revela make brief"))
464
+ throw new Error("__REVELA_MAKE_BRIEF_USAGE_HANDLED__")
465
+ }
466
+ await handleBrief({ workspaceRoot, outputPath: parsed.args.outputPath }, send)
467
+ throw new Error("__REVELA_MAKE_BRIEF_HANDLED__")
468
+ }
469
+ await send("Usage: `/revela make deck [--review]` or `/revela make brief [workspace-relative-output.md]`.")
470
+ throw new Error("__REVELA_MAKE_USAGE_HANDLED__")
471
+ }
394
472
  if (sub === "deck") {
395
473
  if (param && param !== "--review") {
396
- await send("Usage: `/revela deck` starts approved-narrative deck handoff; `/revela deck --review` reviews deck/artifact readiness.")
474
+ await send("Usage: `/revela deck` starts approved-narrative deck handoff; `/revela deck --review` reviews deck/artifact readiness. These are compatibility aliases for `/revela make deck`.")
397
475
  throw new Error("__REVELA_DECK_USAGE_HANDLED__")
398
476
  }
399
477
  if (!param) {
400
- buildPrompt({ mode: "deck-render" })
401
- output.parts.length = 0
402
- output.parts.push({
403
- type: "text",
404
- text: buildDeckPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot }),
405
- } as any)
478
+ queueWorkflowCommand({
479
+ sessionID,
480
+ name: "deck",
481
+ mode: "deck-render",
482
+ visibleText: "Make Revela deck from approved story.",
483
+ hiddenPrompt: buildDeckPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot }),
484
+ output,
485
+ })
406
486
  return
407
487
  }
408
- buildPrompt({ mode: "deck-render" })
409
- output.parts.length = 0
410
- output.parts.push({
411
- type: "text",
412
- text: buildDeckReviewPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot }),
413
- } as any)
488
+ queueWorkflowCommand({
489
+ sessionID,
490
+ name: "deck --review",
491
+ mode: "deck-render",
492
+ visibleText: "Review Revela deck artifact readiness.",
493
+ hiddenPrompt: buildDeckReviewPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot }),
494
+ output,
495
+ })
414
496
  return
415
497
  }
416
498
  if (sub === "refine") {
@@ -423,7 +505,7 @@ const server: Plugin = (async (pluginCtx) => {
423
505
  }
424
506
  if (sub === "edit") {
425
507
  if (param) {
426
- await send("`/revela edit` is deprecated and does not accept a target. Use `/revela refine` for the unified refinement workspace.")
508
+ await send("`/revela edit` has been removed. Use `/revela refine` for the unified reading, inspection, and editing workspace.")
427
509
  throw new Error("__REVELA_EDIT_USAGE_HANDLED__")
428
510
  }
429
511
  await handleEdit({ client, sessionID, workspaceRoot }, send)
@@ -457,17 +539,94 @@ const server: Plugin = (async (pluginCtx) => {
457
539
  await handleDesignsAdd(param, send)
458
540
  throw new Error("__REVELA_DESIGNS_ADD_HANDLED__")
459
541
  }
542
+ if (sub === "design") {
543
+ const designAction = args[1]?.toLowerCase() ?? "list"
544
+ const designParam = args.slice(2).join(" ")
545
+ if (designAction === "list") {
546
+ if (designParam) {
547
+ await send("Usage: `/revela design list`.")
548
+ throw new Error("__REVELA_DESIGN_LIST_USAGE_HANDLED__")
549
+ }
550
+ await handleDesignsList(send)
551
+ throw new Error("__REVELA_DESIGN_LIST_HANDLED__")
552
+ }
553
+ if (designAction === "use") {
554
+ if (!designParam) {
555
+ await send("Usage: `/revela design use <name>`.")
556
+ throw new Error("__REVELA_DESIGN_USE_USAGE_HANDLED__")
557
+ }
558
+ await handleDesignsActivate(designParam, send)
559
+ throw new Error("__REVELA_DESIGN_USE_HANDLED__")
560
+ }
561
+ if (designAction === "add") {
562
+ if (!designParam) {
563
+ await send("Usage: `/revela design add <url|github:user/repo|local-path>`.")
564
+ throw new Error("__REVELA_DESIGN_ADD_USAGE_HANDLED__")
565
+ }
566
+ await handleDesignsAdd(designParam, send)
567
+ throw new Error("__REVELA_DESIGN_ADD_HANDLED__")
568
+ }
569
+ if (designAction === "rm" || designAction === "remove") {
570
+ if (!designParam) {
571
+ await send("Usage: `/revela design rm <name>`.")
572
+ throw new Error("__REVELA_DESIGN_RM_USAGE_HANDLED__")
573
+ }
574
+ await handleDesignsRemove(designParam, send)
575
+ throw new Error("__REVELA_DESIGN_RM_HANDLED__")
576
+ }
577
+ if (designAction === "preview") {
578
+ await handleDesignsPreview(designParam, send)
579
+ throw new Error("__REVELA_DESIGN_PREVIEW_HANDLED__")
580
+ }
581
+ if (designAction === "new") {
582
+ const parsed = parseDesignsNewArgs(designParam)
583
+ if (!parsed.ok) {
584
+ await send(parsed.error.replaceAll("/revela designs-new", "/revela design new"))
585
+ throw new Error("__REVELA_DESIGN_NEW_USAGE_HANDLED__")
586
+ }
587
+ queueWorkflowCommand({
588
+ sessionID,
589
+ name: `design new ${parsed.name}`,
590
+ mode: "deck-render",
591
+ visibleText: `Create Revela design ${parsed.name}.`,
592
+ hiddenPrompt: buildDesignsNewPrompt({ name: parsed.name, base: parsed.base }),
593
+ output,
594
+ })
595
+ return
596
+ }
597
+ if (designAction === "edit") {
598
+ const parsed = parseDesignsEditArgs(designParam)
599
+ if (!parsed.ok) {
600
+ await send(parsed.error.replaceAll("/revela designs-edit", "/revela design edit"))
601
+ throw new Error("__REVELA_DESIGN_EDIT_USAGE_HANDLED__")
602
+ }
603
+ queueWorkflowCommand({
604
+ sessionID,
605
+ name: `design edit ${parsed.name}`,
606
+ mode: "deck-render",
607
+ visibleText: `Edit Revela design ${parsed.name}.`,
608
+ hiddenPrompt: buildDesignsEditPrompt({ name: parsed.name }),
609
+ output,
610
+ })
611
+ return
612
+ }
613
+ await send("Usage: `/revela design [list|use <name>|new <name>|edit <name>|preview [name]|add <source>|rm <name>]`.")
614
+ throw new Error("__REVELA_DESIGN_USAGE_HANDLED__")
615
+ }
460
616
  if (sub === "designs-new") {
461
617
  const parsed = parseDesignsNewArgs(param)
462
618
  if (!parsed.ok) {
463
619
  await send(parsed.error)
464
620
  throw new Error("__REVELA_DESIGNS_NEW_USAGE_HANDLED__")
465
621
  }
466
- output.parts.length = 0
467
- output.parts.push({
468
- type: "text",
469
- text: buildDesignsNewPrompt({ name: parsed.name, base: parsed.base }),
470
- } as any)
622
+ queueWorkflowCommand({
623
+ sessionID,
624
+ name: `designs-new ${parsed.name}`,
625
+ mode: "deck-render",
626
+ visibleText: `Create Revela design ${parsed.name}.`,
627
+ hiddenPrompt: buildDesignsNewPrompt({ name: parsed.name, base: parsed.base }),
628
+ output,
629
+ })
471
630
  return
472
631
  }
473
632
  if (sub === "designs-edit") {
@@ -476,11 +635,14 @@ const server: Plugin = (async (pluginCtx) => {
476
635
  await send(parsed.error)
477
636
  throw new Error("__REVELA_DESIGNS_EDIT_USAGE_HANDLED__")
478
637
  }
479
- output.parts.length = 0
480
- output.parts.push({
481
- type: "text",
482
- text: buildDesignsEditPrompt({ name: parsed.name }),
483
- } as any)
638
+ queueWorkflowCommand({
639
+ sessionID,
640
+ name: `designs-edit ${parsed.name}`,
641
+ mode: "deck-render",
642
+ visibleText: `Edit Revela design ${parsed.name}.`,
643
+ hiddenPrompt: buildDesignsEditPrompt({ name: parsed.name }),
644
+ output,
645
+ })
484
646
  return
485
647
  }
486
648
  if (sub === "designs-preview") {
@@ -508,8 +670,14 @@ const server: Plugin = (async (pluginCtx) => {
508
670
  if (args.notes) {
509
671
  try {
510
672
  const deck = resolvePptxDeck(workspaceRoot, args.filePath)
511
- output.parts.length = 0
512
- output.parts.push({ type: "text", text: buildPptxNotesPrompt(deck) } as any)
673
+ queueWorkflowCommand({
674
+ sessionID,
675
+ name: "pptx --notes",
676
+ mode: "deck-render",
677
+ visibleText: "Export Revela deck to PPTX with speaker notes.",
678
+ hiddenPrompt: buildPptxNotesPrompt(deck),
679
+ output,
680
+ })
513
681
  return
514
682
  } catch (e) {
515
683
  const msg = e instanceof Error ? e.message : String(e)
@@ -633,6 +801,11 @@ const server: Plugin = (async (pluginCtx) => {
633
801
  error: e instanceof Error ? e.message : String(e),
634
802
  })
635
803
  }
804
+ const sessionID = extractSessionID(input)
805
+ const commandIntent = takePendingCommandIntent(sessionID)
806
+ if (commandIntent) {
807
+ prompt += "\n\n" + formatCommandIntentSystemBlock(commandIntent)
808
+ }
636
809
  if (output.system.length > 0) {
637
810
  output.system[output.system.length - 1] += "\n\n" + prompt
638
811
  } else {
@@ -651,7 +824,7 @@ const server: Plugin = (async (pluginCtx) => {
651
824
 
652
825
  // ── Pre-tool processing ────────────────────────────────────────────────
653
826
  // - read: intercept DOCX/PPTX/XLSX before read executes.
654
- // - write/apply_patch: gate decks/*.html on DECKS.json readiness.
827
+ // - write/apply_patch: protect DECKS.json, but do not block deck HTML edits.
655
828
  "tool.execute.before": async (input, output) => {
656
829
  log.info("[hook] tool.execute.before fired", { tool: input.tool, enabled: ctx.enabled, isResearch: ctx.isResearchAgent })
657
830
  if (!ctx.enabled) return
@@ -676,32 +849,6 @@ Next step: use \`revela-decks\` with action \`init\`, \`upsertDeck\`, \`upsertSl
676
849
  childLog("decks-state").warn("blocked direct DECKS.json write", { filePath, blockedPath })
677
850
  return
678
851
  }
679
- if (!isDeckHtmlPath(filePath)) return
680
- if (hasLiveEditorSessionForFile(workspaceRoot, filePath)) return
681
-
682
- const readiness = checkDeckStateWriteReadiness(workspaceRoot, filePath) ?? {
683
- ready: false,
684
- slug: basename(filePath, ".html") || "deck",
685
- blocker: `No ${DECKS_STATE_FILE} exists. Use revela-decks init/upsertDeck/upsertSlides/review before writing deck HTML.`,
686
- blockers: [`No ${DECKS_STATE_FILE} exists.`],
687
- }
688
- if (readiness.ready) return
689
-
690
- const blockedDir = join(workspaceRoot, ".opencode", "revela", "blocked-writes")
691
- mkdirSync(blockedDir, { recursive: true })
692
- const blockedPath = join(blockedDir, `${readiness.slug}.blocked.md`)
693
- ;(output.args as any).filePath = blockedPath
694
- ;(output.args as any).content = `# Revela Blocked Deck Write
695
-
696
- The attempted write to \`${filePath}\` was blocked.
697
-
698
- Reason: ${readiness.blocker}
699
-
700
- Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FILE}, then write only after the matching deck has \`writeReadiness.status\` set to \`ready\` and no blockers.
701
- `
702
- blockedDeckWrites.set(filePath, readiness.blocker)
703
- childLog("decks-memory").warn("blocked deck write", { filePath, blockedPath, blocker: readiness.blocker })
704
- return
705
852
  }
706
853
 
707
854
  if (input.tool === "apply_patch") {
@@ -726,49 +873,10 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
726
873
  +Next step: use \`revela-decks\` with action \`init\`, \`upsertDeck\`, \`upsertSlides\`, or \`review\`.
727
874
  *** End Patch`
728
875
  setPatchTextArg(args, blockedPatch)
729
- blockedDeckPatches.set(blockedRelativePath, blocker)
876
+ blockedPatches.set(blockedRelativePath, blocker)
730
877
  childLog("decks-state").warn("blocked direct DECKS.json patch", { targets: stateTargets, blockedPath: blockedRelativePath })
731
878
  return
732
879
  }
733
-
734
- const targets = extractDeckHtmlTargetsFromPatch(patchText)
735
- if (targets.length === 0) return
736
- if (targets.every((target) => hasLiveEditorSessionForFile(workspaceRoot, target))) return
737
-
738
- const blocked = targets
739
- .map((target) => ({
740
- target,
741
- readiness: checkDeckStateWriteReadiness(workspaceRoot, target) ?? {
742
- ready: false,
743
- slug: basename(target, ".html") || "deck",
744
- blocker: `No ${DECKS_STATE_FILE} exists. Use revela-decks init/upsertDeck/upsertSlides/review before patching deck HTML.`,
745
- blockers: [`No ${DECKS_STATE_FILE} exists.`],
746
- },
747
- }))
748
- .find((item) => !item.readiness.ready)
749
- if (!blocked) return
750
-
751
- const blockedDir = join(workspaceRoot, ".opencode", "revela", "blocked-writes")
752
- mkdirSync(blockedDir, { recursive: true })
753
- const blockedRelativePath = `.opencode/revela/blocked-writes/${blocked.readiness.slug}-${Date.now()}.blocked.md`
754
- const blockedPatch = `*** Begin Patch
755
- *** Add File: ${blockedRelativePath}
756
- +# Revela Blocked Deck Patch
757
- +
758
- +The attempted patch touching \`${blocked.target}\` was blocked.
759
- +
760
- +Reason: ${blocked.readiness.blocker}
761
- +
762
- +Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FILE}, then patch only after the matching deck has \`writeReadiness.status\` set to \`ready\` and no blockers.
763
- *** End Patch`
764
- setPatchTextArg(args, blockedPatch)
765
- blockedDeckPatches.set(blockedRelativePath, blocked.readiness.blocker)
766
- childLog("decks-memory").warn("blocked deck patch", {
767
- target: blocked.target,
768
- blockedPath: blockedRelativePath,
769
- blocker: blocked.readiness.blocker,
770
- })
771
- return
772
880
  }
773
881
 
774
882
  if (input.tool === "read") {
@@ -788,7 +896,7 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
788
896
  // PDF: extract text, remove base64. Images: jimp compress.
789
897
  //
790
898
  // Also reports writes/patches blocked by the DECKS.json prewrite gate and
791
- // runs lightweight static design compliance after successful deck changes.
899
+ // runs artifact QA before opening Refine after successful deck changes.
792
900
  "tool.execute.after": async (input, output) => {
793
901
  if (!ctx.enabled) return
794
902
 
@@ -805,7 +913,7 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
805
913
  return
806
914
  }
807
915
 
808
- // ── Report blocked deck writes and run static compliance ──────────
916
+ // ── Report blocked state writes and run artifact QA ───────────────
809
917
  if (input.tool === "write") {
810
918
  const filePath: string = input.args?.filePath ?? ""
811
919
  const blockedReason = blockedDeckWrites.get(filePath)
@@ -819,20 +927,19 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
819
927
  )
820
928
  return
821
929
  }
822
- await appendComplianceReport(filePath, output)
823
- await appendDeckHtmlContractReport(filePath, output)
824
- ensureEditorOpenAfterDeckChange(filePath, extractSessionID(input))
930
+ const qaPassed = await runPostWriteArtifactQA(filePath, output)
931
+ if (qaPassed) ensureRefineOpenAfterDeckChange(filePath, extractSessionID(input))
825
932
  return
826
933
  }
827
934
 
828
- if (input.tool === "apply_patch" && blockedDeckPatches.size > 0) {
829
- const [blockedPath, blockedReason] = blockedDeckPatches.entries().next().value ?? []
830
- if (blockedPath) blockedDeckPatches.delete(blockedPath)
935
+ if (input.tool === "apply_patch" && blockedPatches.size > 0) {
936
+ const [blockedPath, blockedReason] = blockedPatches.entries().next().value ?? []
937
+ if (blockedPath) blockedPatches.delete(blockedPath)
831
938
  appendToolResult(
832
939
  output,
833
- "---\n\n**[revela prewrite gate]** Deck HTML patch was blocked.\n\n" +
940
+ "---\n\n**[revela prewrite gate]** Patch was blocked.\n\n" +
834
941
  `${blockedReason}\n\n` +
835
- "Run `/revela review` or complete the same DECKS.json review workflow before patching the deck."
942
+ "Use the `revela-decks` tool for controlled workspace state changes."
836
943
  )
837
944
  return
838
945
  }
@@ -841,18 +948,16 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
841
948
  const patchText = extractPatchTextArg(input.args as Record<string, unknown>)
842
949
  const targets = patchText ? extractDeckHtmlTargetsFromPatch(patchText) : []
843
950
  for (const target of targets) {
844
- await appendComplianceReport(target, output)
845
- await appendDeckHtmlContractReport(target, output)
846
- ensureEditorOpenAfterDeckChange(target, extractSessionID(input))
951
+ const qaPassed = await runPostWriteArtifactQA(target, output)
952
+ if (qaPassed) ensureRefineOpenAfterDeckChange(target, extractSessionID(input))
847
953
  }
848
954
  return
849
955
  }
850
956
 
851
957
  if (input.tool === "edit") {
852
958
  const filePath = extractEditFilePath(input.args)
853
- await appendComplianceReport(filePath, output)
854
- await appendDeckHtmlContractReport(filePath, output)
855
- ensureEditorOpenAfterDeckChange(filePath, extractSessionID(input))
959
+ const qaPassed = await runPostWriteArtifactQA(filePath, output)
960
+ if (qaPassed) ensureRefineOpenAfterDeckChange(filePath, extractSessionID(input))
856
961
  return
857
962
  }
858
963
  },