@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/README.md +65 -46
- package/README.zh-CN.md +65 -46
- package/designs/starter/DESIGN.md +168 -171
- package/designs/starter/preview.html +2 -2
- package/designs/summit/DESIGN.md +283 -129
- package/lib/command-intent.ts +59 -0
- package/lib/commands/brief.ts +1 -1
- package/lib/commands/designs.ts +1 -1
- package/lib/commands/domains.ts +1 -1
- package/lib/commands/edit.ts +2 -21
- package/lib/commands/enable.ts +6 -6
- package/lib/commands/help.ts +16 -8
- package/lib/commands/init.ts +1 -1
- package/lib/commands/narrative.ts +26 -0
- package/lib/commands/research.ts +66 -0
- package/lib/commands/review.ts +52 -15
- package/lib/decks-state.ts +127 -8
- package/lib/design/designs.ts +1 -2
- package/lib/edit/prompt.ts +6 -5
- package/lib/edit/resolve-deck.ts +1 -1
- package/lib/narrative-state/render-plan.ts +10 -1
- package/lib/qa/artifact.ts +77 -0
- package/lib/qa/checks.ts +100 -10
- package/lib/qa/index.ts +8 -6
- package/lib/qa/measure.ts +85 -0
- package/lib/refine/open.ts +21 -1
- package/lib/refine/server.ts +127 -4
- package/lib/workspace-state/types.ts +1 -0
- package/package.json +1 -1
- package/plugin.ts +283 -178
- package/skill/NARRATIVE_SKILL.md +103 -25
- package/skill/SKILL.md +6 -11
- package/tools/decks.ts +29 -3
- package/tools/narrative-view.ts +1 -1
- package/tools/qa.ts +17 -11
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 {
|
|
52
|
-
import {
|
|
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
|
|
154
|
+
const blockedPatches = new Map<string, string>()
|
|
156
155
|
|
|
157
|
-
async function
|
|
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 =
|
|
169
|
-
|
|
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("
|
|
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
|
|
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
|
-
|
|
209
|
+
ensureRefineDeckOpenForChange("", {
|
|
214
210
|
client,
|
|
215
211
|
sessionID,
|
|
216
212
|
workspaceRoot,
|
|
217
213
|
})
|
|
218
214
|
} catch (e) {
|
|
219
|
-
childLog("
|
|
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 —
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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`
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
512
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
823
|
-
|
|
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" &&
|
|
829
|
-
const [blockedPath, blockedReason] =
|
|
830
|
-
if (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]**
|
|
940
|
+
"---\n\n**[revela prewrite gate]** Patch was blocked.\n\n" +
|
|
834
941
|
`${blockedReason}\n\n` +
|
|
835
|
-
"
|
|
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
|
|
845
|
-
|
|
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
|
|
854
|
-
|
|
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
|
},
|