@cyber-dash-tech/revela 0.10.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.
- package/README.md +54 -28
- package/README.zh-CN.md +54 -28
- package/lib/commands/designs.ts +2 -2
- package/lib/commands/domains.ts +2 -2
- package/lib/commands/enable.ts +19 -19
- package/lib/commands/help.ts +5 -3
- package/lib/commands/init.ts +30 -19
- package/lib/commands/inspect.ts +1 -1
- package/lib/commands/pdf.ts +33 -5
- package/lib/commands/pptx.ts +14 -9
- package/lib/commands/refine.ts +1 -1
- package/lib/commands/review.ts +115 -1
- package/lib/deck-html/contract.ts +252 -0
- package/lib/decks-state.ts +111 -28
- package/lib/document-materials/extract.ts +20 -0
- package/lib/edit/resolve-deck.ts +13 -2
- package/lib/inspect/open.ts +3 -1
- package/lib/narrative-state/hash.ts +52 -0
- package/lib/narrative-state/normalize.ts +307 -0
- package/lib/narrative-state/project-compat.ts +14 -0
- package/lib/narrative-state/readiness.ts +289 -0
- package/lib/narrative-state/render-plan.ts +207 -0
- package/lib/narrative-state/types.ts +139 -0
- package/lib/prompt-builder.ts +59 -26
- package/lib/qa/export-gate.ts +8 -1
- package/lib/refine/open.ts +3 -1
- package/lib/workspace-state/actions.ts +71 -0
- package/lib/workspace-state/compat.ts +10 -0
- package/lib/workspace-state/evidence-status.ts +267 -0
- package/lib/workspace-state/graph.ts +544 -0
- package/lib/workspace-state/render-targets.ts +182 -0
- package/lib/workspace-state/rendered-artifacts.ts +43 -0
- package/lib/workspace-state/repository.ts +43 -0
- package/lib/workspace-state/research-attachments.ts +130 -0
- package/lib/workspace-state/review-snapshots.ts +127 -0
- package/lib/workspace-state/types.ts +122 -0
- package/package.json +1 -1
- package/plugin.ts +53 -3
- package/skill/NARRATIVE_SKILL.md +64 -0
- package/tools/decks.ts +233 -6
- package/tools/pdf.ts +9 -1
- package/tools/pptx.ts +10 -0
- package/tools/research-save.ts +15 -0
- package/tools/workspace-scan.ts +29 -1
package/plugin.ts
CHANGED
|
@@ -48,6 +48,7 @@ import { buildPptxNotesPrompt, handlePptx, parsePptxArgs, resolvePptxDeck } from
|
|
|
48
48
|
import { handleEdit } from "./lib/commands/edit"
|
|
49
49
|
import { handleInspect } from "./lib/commands/inspect"
|
|
50
50
|
import { handleRefine } from "./lib/commands/refine"
|
|
51
|
+
import { formatDeckHtmlContractReport, validateDeckHtmlContract } from "./lib/deck-html/contract"
|
|
51
52
|
import { ensureEditableDeckOpenForChange } from "./lib/edit/open"
|
|
52
53
|
import { hasLiveEditorSessionForFile } from "./lib/edit/server"
|
|
53
54
|
import { handleDesignsPreview } from "./lib/commands/designs-preview"
|
|
@@ -59,7 +60,7 @@ import {
|
|
|
59
60
|
} from "./lib/commands/designs-new"
|
|
60
61
|
import { buildInitPrompt } from "./lib/commands/init"
|
|
61
62
|
import { parseRememberArgs, buildRememberPrompt } from "./lib/commands/remember"
|
|
62
|
-
import { buildReviewPrompt } from "./lib/commands/review"
|
|
63
|
+
import { buildDeckPrompt, buildDeckReviewPrompt, buildReviewPrompt } from "./lib/commands/review"
|
|
63
64
|
import {
|
|
64
65
|
extractDeckHtmlTargetsFromPatch,
|
|
65
66
|
extractPatchTextArg,
|
|
@@ -177,6 +178,27 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
async function appendDeckHtmlContractReport(filePath: string, output: any): Promise<void> {
|
|
182
|
+
if (!isDeckHtmlPath(filePath)) return
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const report = validateDeckHtmlContract(workspaceRoot, filePath)
|
|
186
|
+
if (report.status === "valid" || report.status === "skipped") return
|
|
187
|
+
|
|
188
|
+
appendToolResult(
|
|
189
|
+
output,
|
|
190
|
+
"---\n\n**[revela deck HTML contract]** Slide identity check failed:\n\n" +
|
|
191
|
+
formatDeckHtmlContractReport(report) +
|
|
192
|
+
"\n\nFix every `<section class=\"slide\">` to use the matching 1-based `data-slide-index` from DECKS.json before inspection or export."
|
|
193
|
+
)
|
|
194
|
+
} catch (e) {
|
|
195
|
+
childLog("deck-contract").warn("deck HTML contract report failed", {
|
|
196
|
+
filePath,
|
|
197
|
+
error: e instanceof Error ? e.message : String(e),
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
180
202
|
function extractSessionID(input: any): string {
|
|
181
203
|
return input?.sessionID ?? input?.session?.id ?? input?.context?.sessionID ?? ""
|
|
182
204
|
}
|
|
@@ -304,6 +326,7 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
304
326
|
throw new Error("__REVELA_DISABLE_HANDLED__")
|
|
305
327
|
}
|
|
306
328
|
if (sub === "init") {
|
|
329
|
+
buildPrompt({ mode: "narrative" })
|
|
307
330
|
output.parts.length = 0
|
|
308
331
|
output.parts.push({
|
|
309
332
|
type: "text",
|
|
@@ -317,6 +340,7 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
317
340
|
await send(parsed.error)
|
|
318
341
|
throw new Error("__REVELA_REMEMBER_USAGE_HANDLED__")
|
|
319
342
|
}
|
|
343
|
+
buildPrompt({ mode: "narrative" })
|
|
320
344
|
output.parts.length = 0
|
|
321
345
|
output.parts.push({
|
|
322
346
|
type: "text",
|
|
@@ -326,9 +350,10 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
326
350
|
}
|
|
327
351
|
if (sub === "review") {
|
|
328
352
|
if (param) {
|
|
329
|
-
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.")
|
|
330
354
|
throw new Error("__REVELA_REVIEW_USAGE_HANDLED__")
|
|
331
355
|
}
|
|
356
|
+
buildPrompt({ mode: "narrative" })
|
|
332
357
|
output.parts.length = 0
|
|
333
358
|
output.parts.push({
|
|
334
359
|
type: "text",
|
|
@@ -336,6 +361,28 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
336
361
|
} as any)
|
|
337
362
|
return
|
|
338
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
|
+
}
|
|
339
386
|
if (sub === "refine") {
|
|
340
387
|
if (param) {
|
|
341
388
|
await send("`/revela refine` does not accept a target. It opens the only HTML deck in `decks/`.")
|
|
@@ -423,7 +470,7 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
423
470
|
throw new Error("__REVELA_DOMAINS_RM_HANDLED__")
|
|
424
471
|
}
|
|
425
472
|
if (sub === "pdf") {
|
|
426
|
-
await handlePdf(param, send)
|
|
473
|
+
await handlePdf(param, send, workspaceRoot)
|
|
427
474
|
throw new Error("__REVELA_PDF_HANDLED__")
|
|
428
475
|
}
|
|
429
476
|
if (sub === "pptx") {
|
|
@@ -742,6 +789,7 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
|
|
|
742
789
|
return
|
|
743
790
|
}
|
|
744
791
|
await appendComplianceReport(filePath, output)
|
|
792
|
+
await appendDeckHtmlContractReport(filePath, output)
|
|
745
793
|
ensureEditorOpenAfterDeckChange(filePath, extractSessionID(input))
|
|
746
794
|
return
|
|
747
795
|
}
|
|
@@ -763,6 +811,7 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
|
|
|
763
811
|
const targets = patchText ? extractDeckHtmlTargetsFromPatch(patchText) : []
|
|
764
812
|
for (const target of targets) {
|
|
765
813
|
await appendComplianceReport(target, output)
|
|
814
|
+
await appendDeckHtmlContractReport(target, output)
|
|
766
815
|
ensureEditorOpenAfterDeckChange(target, extractSessionID(input))
|
|
767
816
|
}
|
|
768
817
|
return
|
|
@@ -771,6 +820,7 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
|
|
|
771
820
|
if (input.tool === "edit") {
|
|
772
821
|
const filePath = extractEditFilePath(input.args)
|
|
773
822
|
await appendComplianceReport(filePath, output)
|
|
823
|
+
await appendDeckHtmlContractReport(filePath, output)
|
|
774
824
|
ensureEditorOpenAfterDeckChange(filePath, extractSessionID(input))
|
|
775
825
|
return
|
|
776
826
|
}
|
|
@@ -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
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin"
|
|
2
2
|
import {
|
|
3
|
-
applyEvidenceCandidates,
|
|
4
3
|
createDeckSpec,
|
|
5
4
|
DECKS_STATE_FILE,
|
|
6
5
|
normalizeWorkspaceDeckState,
|
|
@@ -18,15 +17,53 @@ import {
|
|
|
18
17
|
type SlideSpec,
|
|
19
18
|
} from "../lib/decks-state"
|
|
20
19
|
import { upsertSourceMaterial } from "../lib/source-materials"
|
|
20
|
+
import { recordWorkspaceAction } from "../lib/workspace-state/actions"
|
|
21
|
+
import { applyEvidenceBindings } from "../lib/workspace-state/evidence-status"
|
|
22
|
+
import { attachResearchFindings } from "../lib/workspace-state/research-attachments"
|
|
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
|
+
}
|
|
21
58
|
|
|
22
59
|
export default tool({
|
|
23
60
|
description:
|
|
24
61
|
`Read and update ${DECKS_STATE_FILE}, Revela's workspace deck state file. ` +
|
|
25
62
|
"Use this tool instead of writing or patching the state file directly. " +
|
|
26
|
-
"It stores active deck specs, per-slide content/layout/components, and computes
|
|
63
|
+
"It stores workspace narrative state, active deck specs, per-slide content/layout/components, and computes narrative or deck readiness.",
|
|
27
64
|
args: {
|
|
28
65
|
action: tool.schema
|
|
29
|
-
.enum(["read", "init", "upsertDeck", "upsertSlides", "review", "applyEvidenceCandidates", "remember"])
|
|
66
|
+
.enum(["read", "init", "upsertDeck", "upsertSlides", "upsertNarrative", "compileDeckPlan", "review", "reviewNarrative", "approveNarrative", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
|
|
30
67
|
.describe("Action to perform on DECKS.json."),
|
|
31
68
|
summary: tool.schema.boolean().optional().describe("For read: return a compact summary instead of full state."),
|
|
32
69
|
goal: tool.schema.string().optional().describe("For upsertDeck: deck goal."),
|
|
@@ -42,6 +79,69 @@ export default tool({
|
|
|
42
79
|
objections: tool.schema.array(tool.schema.string()).optional().describe("Likely stakeholder objections or questions the narrative should handle."),
|
|
43
80
|
risks: tool.schema.array(tool.schema.string()).optional().describe("Risks, assumptions, caveats, or tradeoffs that should travel with the narrative."),
|
|
44
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."),
|
|
45
145
|
design: tool.schema.string().optional().describe("For upsertDeck: active design name."),
|
|
46
146
|
domain: tool.schema.string().optional().describe("For upsertDeck: active domain name."),
|
|
47
147
|
memory: tool.schema.string().optional().describe("For remember: explicit user or workflow preference to store."),
|
|
@@ -118,6 +218,12 @@ export default tool({
|
|
|
118
218
|
notes: tool.schema.string().optional().describe("Implementation notes for this slide."),
|
|
119
219
|
})).optional().describe("For upsertSlides: complete or partial slide specs."),
|
|
120
220
|
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."),
|
|
221
|
+
findingsFile: tool.schema.string().optional().describe("For attachResearchFindings: workspace-relative researches/{topic}/{axis}.md file to attach to researchPlan."),
|
|
222
|
+
researchAxis: tool.schema.string().optional().describe("For attachResearchFindings: researchPlan axis to attach the findings file to. Required when filename matching would be ambiguous."),
|
|
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."),
|
|
121
227
|
},
|
|
122
228
|
async execute(args, context) {
|
|
123
229
|
try {
|
|
@@ -126,8 +232,20 @@ export default tool({
|
|
|
126
232
|
const defaultSlug = workspaceDeckSlug(workspaceRoot)
|
|
127
233
|
|
|
128
234
|
if (args.action === "init") {
|
|
235
|
+
const discovered: SourceMaterial[] = []
|
|
129
236
|
for (const material of (args.sourceMaterials ?? []) as SourceMaterial[]) {
|
|
130
237
|
upsertSourceMaterial(state, material, material.status ?? "discovered")
|
|
238
|
+
discovered.push(material)
|
|
239
|
+
}
|
|
240
|
+
if (discovered.length > 0) {
|
|
241
|
+
recordWorkspaceAction(state, {
|
|
242
|
+
type: "source.discovered",
|
|
243
|
+
actor: "revela-decks",
|
|
244
|
+
inputs: { count: discovered.length },
|
|
245
|
+
outputs: { paths: discovered.map((material) => material.path), statuses: discovered.map((material) => material.status ?? "discovered") },
|
|
246
|
+
summary: `Registered ${discovered.length} discovered source material${discovered.length === 1 ? "" : "s"}.`,
|
|
247
|
+
nodeIds: discovered.map((material) => `source:${material.path}`),
|
|
248
|
+
})
|
|
131
249
|
}
|
|
132
250
|
writeDecksState(workspaceRoot, state)
|
|
133
251
|
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, state }, null, 2)
|
|
@@ -176,18 +294,127 @@ export default tool({
|
|
|
176
294
|
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, deck: next.activeDeck ? next.decks[next.activeDeck] : undefined }, null, 2)
|
|
177
295
|
}
|
|
178
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
|
+
|
|
179
336
|
if (args.action === "review") {
|
|
180
337
|
const reviewed = reviewDeckState(state, undefined, { workspaceRoot })
|
|
338
|
+
const targetId = activeReviewTargetId(reviewed.state)
|
|
339
|
+
const snapshot = latestReviewSnapshotForTarget(reviewed.state, targetId)
|
|
340
|
+
recordWorkspaceAction(reviewed.state, {
|
|
341
|
+
type: "review.performed",
|
|
342
|
+
actor: "revela-decks",
|
|
343
|
+
inputs: { activeDeck: state.activeDeck },
|
|
344
|
+
outputs: {
|
|
345
|
+
slug: reviewed.result.slug,
|
|
346
|
+
status: reviewed.result.status,
|
|
347
|
+
ready: reviewed.result.ready,
|
|
348
|
+
blockerCount: reviewed.result.blockers.length,
|
|
349
|
+
warningCount: reviewed.result.warnings.length,
|
|
350
|
+
issueCount: reviewed.result.issues.length,
|
|
351
|
+
evidenceCandidateCount: reviewed.result.evidenceCandidates?.length ?? 0,
|
|
352
|
+
snapshotId: snapshot?.id,
|
|
353
|
+
inputHash: snapshot?.inputHash,
|
|
354
|
+
targetId: snapshot?.targetId,
|
|
355
|
+
},
|
|
356
|
+
status: "success",
|
|
357
|
+
summary: `Reviewed deck readiness: ${reviewed.result.ready ? "ready" : "blocked"}.`,
|
|
358
|
+
nodeIds: [`artifact:${reviewed.state.decks[reviewed.result.slug]?.outputPath ?? reviewed.result.slug}`, ...(snapshot ? [snapshot.id] : [])],
|
|
359
|
+
})
|
|
181
360
|
writeDecksState(workspaceRoot, reviewed.state)
|
|
182
361
|
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: reviewed.result }, null, 2)
|
|
183
362
|
}
|
|
184
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
|
+
|
|
185
403
|
if (args.action === "applyEvidenceCandidates") {
|
|
186
404
|
const candidateIds = args.candidateIds ?? []
|
|
187
405
|
if (candidateIds.length === 0) return JSON.stringify({ ok: false, error: "candidateIds are required for applyEvidenceCandidates" })
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
406
|
+
const result = applyEvidenceBindings(workspaceRoot, candidateIds)
|
|
407
|
+
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result }, null, 2)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (args.action === "attachResearchFindings") {
|
|
411
|
+
if (!args.findingsFile?.trim()) return JSON.stringify({ ok: false, error: "findingsFile is required for attachResearchFindings" })
|
|
412
|
+
const result = attachResearchFindings(workspaceRoot, {
|
|
413
|
+
findingsFile: args.findingsFile,
|
|
414
|
+
researchAxis: args.researchAxis,
|
|
415
|
+
status: args.researchStatus,
|
|
416
|
+
})
|
|
417
|
+
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result }, null, 2)
|
|
191
418
|
}
|
|
192
419
|
|
|
193
420
|
if (args.action === "remember") {
|
package/tools/pdf.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { existsSync } from "fs"
|
|
|
9
9
|
import { resolve } from "path"
|
|
10
10
|
import { exportToPdf } from "../lib/pdf/export"
|
|
11
11
|
import { assertExportQAPassed } from "../lib/qa/export-gate"
|
|
12
|
+
import { recordRenderedArtifact, workspaceRelative } from "../lib/workspace-state/rendered-artifacts"
|
|
12
13
|
|
|
13
14
|
export default tool({
|
|
14
15
|
description:
|
|
@@ -35,8 +36,15 @@ export default tool({
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
try {
|
|
38
|
-
|
|
39
|
+
const root = directory || process.cwd()
|
|
40
|
+
await assertExportQAPassed(filePath, { workspaceRoot: root })
|
|
39
41
|
const result = await exportToPdf(filePath)
|
|
42
|
+
recordRenderedArtifact(root, {
|
|
43
|
+
sourceHtmlPath: workspaceRelative(resolve(root), filePath),
|
|
44
|
+
outputPath: result.outputPath,
|
|
45
|
+
type: "pdf",
|
|
46
|
+
actor: "revela-pdf",
|
|
47
|
+
})
|
|
40
48
|
return JSON.stringify({ ok: true, ...result }, null, 2)
|
|
41
49
|
} catch (e: any) {
|
|
42
50
|
return JSON.stringify({ ok: false, error: e?.message ?? String(e) })
|
package/tools/pptx.ts
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
import { tool } from "@opencode-ai/plugin"
|
|
8
8
|
import { existsSync } from "fs"
|
|
9
9
|
import { resolve } from "path"
|
|
10
|
+
import { assertDeckHtmlContractValid } from "../lib/deck-html/contract"
|
|
10
11
|
import { exportToPptx } from "../lib/pptx/export"
|
|
12
|
+
import { recordRenderedArtifact, workspaceRelative } from "../lib/workspace-state/rendered-artifacts"
|
|
11
13
|
|
|
12
14
|
export default tool({
|
|
13
15
|
description:
|
|
@@ -42,12 +44,20 @@ export default tool({
|
|
|
42
44
|
const progress: string[] = []
|
|
43
45
|
|
|
44
46
|
try {
|
|
47
|
+
const root = directory || process.cwd()
|
|
48
|
+
assertDeckHtmlContractValid(root, filePath)
|
|
45
49
|
const result = await exportToPptx(filePath, {
|
|
46
50
|
speakerNotes: normalizeSpeakerNotes(speakerNotes),
|
|
47
51
|
onProgress: (event) => {
|
|
48
52
|
progress.push(event.message)
|
|
49
53
|
},
|
|
50
54
|
})
|
|
55
|
+
recordRenderedArtifact(root, {
|
|
56
|
+
sourceHtmlPath: workspaceRelative(resolve(root), filePath),
|
|
57
|
+
outputPath: result.outputPath,
|
|
58
|
+
type: "pptx",
|
|
59
|
+
actor: "revela-pptx",
|
|
60
|
+
})
|
|
51
61
|
return JSON.stringify({ ok: true, ...result, progress }, null, 2)
|
|
52
62
|
} catch (e: any) {
|
|
53
63
|
return JSON.stringify({ ok: false, error: e?.message ?? String(e), progress })
|
package/tools/research-save.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin"
|
|
2
2
|
import { mkdirSync, writeFileSync } from "fs"
|
|
3
3
|
import { join } from "path"
|
|
4
|
+
import { hasDecksState, readDecksState, writeDecksState } from "../lib/decks-state"
|
|
5
|
+
import { recordWorkspaceAction } from "../lib/workspace-state/actions"
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Format today's date as YYYY-MM-DD
|
|
@@ -88,6 +90,19 @@ export default tool({
|
|
|
88
90
|
|
|
89
91
|
writeFileSync(filePath, fileContent, "utf-8")
|
|
90
92
|
|
|
93
|
+
if (hasDecksState(workspaceDir)) {
|
|
94
|
+
const state = readDecksState(workspaceDir)
|
|
95
|
+
recordWorkspaceAction(state, {
|
|
96
|
+
type: "research.findings_saved",
|
|
97
|
+
actor: "revela-research-save",
|
|
98
|
+
inputs: { topic: topicKey, axis: fileKey, sourceCount: args.sources?.length ?? 0 },
|
|
99
|
+
outputs: { path: relPath, sources: args.sources ?? [] },
|
|
100
|
+
summary: `Saved research findings for ${topicKey}/${fileKey}.`,
|
|
101
|
+
nodeIds: [`finding:${relPath}`],
|
|
102
|
+
})
|
|
103
|
+
writeDecksState(workspaceDir, state)
|
|
104
|
+
}
|
|
105
|
+
|
|
91
106
|
return JSON.stringify({ ok: true, path: relPath })
|
|
92
107
|
} catch (e: any) {
|
|
93
108
|
return JSON.stringify({ error: e.message || String(e) })
|
package/tools/workspace-scan.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { readdirSync, statSync, existsSync } from "fs"
|
|
|
3
3
|
import { join, relative, extname, resolve, sep, isAbsolute } from "path"
|
|
4
4
|
import { sourceMaterialMetadata } from "../lib/source-materials"
|
|
5
5
|
import type { SourceMaterial } from "../lib/decks-state"
|
|
6
|
+
import { hasDecksState, readDecksState, writeDecksState } from "../lib/decks-state"
|
|
7
|
+
import { recordWorkspaceAction } from "../lib/workspace-state/actions"
|
|
6
8
|
|
|
7
9
|
const DOC_EXTENSIONS = new Set([
|
|
8
10
|
".pdf", ".docx", ".doc", ".xlsx", ".xls",
|
|
@@ -16,6 +18,17 @@ const EXCLUDE_DIRS = new Set([
|
|
|
16
18
|
"designs", "domains", // Exclude revela plugin assets
|
|
17
19
|
])
|
|
18
20
|
|
|
21
|
+
const EXCLUDE_FILENAMES = new Set([
|
|
22
|
+
"AGENTS.md",
|
|
23
|
+
"DECKS.md",
|
|
24
|
+
"README.md",
|
|
25
|
+
"README.zh-CN.md",
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
function isExcludedFile(entry: string): boolean {
|
|
29
|
+
return entry.startsWith("~$") || EXCLUDE_FILENAMES.has(entry)
|
|
30
|
+
}
|
|
31
|
+
|
|
19
32
|
type FileEntry = {
|
|
20
33
|
path: string
|
|
21
34
|
type: string
|
|
@@ -82,6 +95,8 @@ function scanDir(dir: string, rootDir: string, results: FileEntry[], maxDepth: n
|
|
|
82
95
|
if (stat.isDirectory()) {
|
|
83
96
|
scanDir(fullPath, rootDir, results, maxDepth, depth + 1)
|
|
84
97
|
} else if (stat.isFile()) {
|
|
98
|
+
if (isExcludedFile(entry)) continue
|
|
99
|
+
|
|
85
100
|
const ext = extname(entry).toLowerCase()
|
|
86
101
|
if (DOC_EXTENSIONS.has(ext)) {
|
|
87
102
|
const sourceMaterial = sourceMaterialMetadata(fullPath, rootDir)
|
|
@@ -102,7 +117,7 @@ export default tool({
|
|
|
102
117
|
"Scan the current workspace for document and data files that can be used as research input. " +
|
|
103
118
|
"Returns a structured list of all found files with their type and size. " +
|
|
104
119
|
"Searches for: PDF, Word (docx/doc), Excel (xlsx/xls), PowerPoint (pptx/ppt), CSV, Markdown, and text files. " +
|
|
105
|
-
"Excludes node_modules, .git, dist,
|
|
120
|
+
"Excludes node_modules, .git, dist, the researches/ output directory, project docs, and Office lock files. " +
|
|
106
121
|
"Use this as the first step before reading workspace documents.",
|
|
107
122
|
args: {
|
|
108
123
|
path: tool.schema
|
|
@@ -145,6 +160,19 @@ export default tool({
|
|
|
145
160
|
const results: FileEntry[] = []
|
|
146
161
|
scanDir(scanRoot, workspaceDir, results, maxDepth, 0)
|
|
147
162
|
|
|
163
|
+
if (hasDecksState(workspaceDir)) {
|
|
164
|
+
const state = readDecksState(workspaceDir)
|
|
165
|
+
recordWorkspaceAction(state, {
|
|
166
|
+
type: "workspace.scanned",
|
|
167
|
+
actor: "revela-workspace-scan",
|
|
168
|
+
inputs: { path: args.path, maxDepth },
|
|
169
|
+
outputs: { found: results.length, paths: results.map((file) => file.path) },
|
|
170
|
+
summary: `Scanned workspace documents and found ${results.length} file${results.length === 1 ? "" : "s"}.`,
|
|
171
|
+
nodeIds: results.map((file) => `source:${file.sourceMaterial.path}`),
|
|
172
|
+
})
|
|
173
|
+
writeDecksState(workspaceDir, state)
|
|
174
|
+
}
|
|
175
|
+
|
|
148
176
|
if (results.length === 0) {
|
|
149
177
|
return JSON.stringify({
|
|
150
178
|
found: 0,
|