@ztffn/presentation-generator-plugin 1.4.7 → 1.6.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/.claude-plugin/plugin.json +1 -1
- package/DEFERRED-FIXES.md +39 -0
- package/hooks/enforce-style-schema.sh +14 -8
- package/hooks/hooks.json +10 -0
- package/hooks/inject-plugin-root.sh +32 -0
- package/hooks/pre-validate-presentation-json.sh +1 -1
- package/hooks/validate-presentation-json.sh +1 -1
- package/package.json +1 -1
- package/scripts/outline_to_graph.py +9 -4
- package/scripts/verify_node_count.py +46 -0
- package/skills/outline-format/SKILL.md +3 -1
- package/skills/presentation-generator/SKILL.md +20 -41
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "presentation-generator",
|
|
3
3
|
"description": "Generate complete graph-based presentations from natural language briefs and project documents. Pipeline: content extraction, narrative design, deterministic graph generation, and visual styling.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Huma"
|
|
7
7
|
},
|
package/DEFERRED-FIXES.md
CHANGED
|
@@ -4,6 +4,45 @@ Issues that require architectural changes and should be addressed in targeted, f
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## v1.6.0 fixes ✓ resolved
|
|
8
|
+
|
|
9
|
+
**UX: Phase 1 bash command visible in permission dialog**
|
|
10
|
+
The Phase 1 `find` one-liner (shipped in v1.5.0) still appeared as a bash
|
|
11
|
+
permission prompt. Added `inject-plugin-root.sh` — a `PostToolUse` hook on the
|
|
12
|
+
`Skill` tool that silently resolves PLUGIN_ROOT and injects it as
|
|
13
|
+
`additionalContext` when the orchestrator skill loads. Phase 1 now reads the
|
|
14
|
+
injected value from context with no bash at all. Cowork fallback uses two
|
|
15
|
+
ordered `Read` probes of known install paths — also no bash.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## v1.5.0 fixes ✓ resolved
|
|
20
|
+
|
|
21
|
+
**Bug: Style agent always launched as `undefined`**
|
|
22
|
+
`enforce-style-schema.sh` returned `updatedInput: { prompt: "..." }` which replaced
|
|
23
|
+
the entire tool_input, dropping `subagent_type`. Fixed by merging the modified prompt
|
|
24
|
+
into the original tool_input: `updatedInput: ($orig + {prompt: $prompt})`.
|
|
25
|
+
|
|
26
|
+
**Bug: Content brief triggered presentation validator**
|
|
27
|
+
Hook patterns `*_temp/presentation*.json` matched `_temp/presentation-content-brief.json`,
|
|
28
|
+
causing the validator to reject it. Removed the broad arm — only `*_temp/presentation-draft.json`
|
|
29
|
+
needs validation now.
|
|
30
|
+
|
|
31
|
+
**Bug: Bare agent names caused first-attempt failures**
|
|
32
|
+
SKILL.md used `presentation-content` etc; Task tool requires fully-qualified
|
|
33
|
+
`presentation-generator:presentation-content`. Fixed in all three phases (2, 3, 6).
|
|
34
|
+
|
|
35
|
+
**UX: Visual intent annotations leaking into slide content**
|
|
36
|
+
Narrative agents sometimes write `**Visual intent:** bookend` inside `**Content:**` blocks.
|
|
37
|
+
Added `**Visual intent:**` as a recognized field marker in `extract_fields()` so it acts
|
|
38
|
+
as a boundary, preventing bleed into `data.content`. Documented in `outline-format/SKILL.md`.
|
|
39
|
+
|
|
40
|
+
**UX: Intimidating inline scripts in permission prompts**
|
|
41
|
+
Phase 1 multi-line if/elif replaced with a single `find` one-liner.
|
|
42
|
+
Phase 5 Python heredoc extracted into `scripts/verify_node_count.py`.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
7
46
|
## Issue 3 — `topic` badge truncation ✓ resolved
|
|
8
47
|
|
|
9
48
|
**Was:** Badge clips at ~180px wide when `topic` strings are long.
|
|
@@ -60,11 +60,17 @@ Your report MUST include the exact validator terminal output."
|
|
|
60
60
|
# Build the modified prompt
|
|
61
61
|
MODIFIED_PROMPT="${PREAMBLE}${ORIGINAL_PROMPT}${SUFFIX}"
|
|
62
62
|
|
|
63
|
-
# Return updatedInput
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
# Return updatedInput: merge modified prompt into original tool_input so
|
|
64
|
+
# subagent_type (and any other fields) are preserved.
|
|
65
|
+
ORIGINAL_INPUT=$(jq '.tool_input' <<< "$INPUT")
|
|
66
|
+
|
|
67
|
+
jq -n \
|
|
68
|
+
--argjson orig "$ORIGINAL_INPUT" \
|
|
69
|
+
--arg prompt "$MODIFIED_PROMPT" \
|
|
70
|
+
'{
|
|
71
|
+
hookSpecificOutput: {
|
|
72
|
+
hookEventName: "PreToolUse",
|
|
73
|
+
permissionDecision: "allow",
|
|
74
|
+
updatedInput: ($orig + {prompt: $prompt})
|
|
75
|
+
}
|
|
76
|
+
}'
|
package/hooks/hooks.json
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# inject-plugin-root.sh — PostToolUse hook for Skill tool.
|
|
3
|
+
# Fires after the presentation-generator skill loads and injects
|
|
4
|
+
# PLUGIN_ROOT into additionalContext so the orchestrator never needs bash.
|
|
5
|
+
|
|
6
|
+
INPUT=$(cat)
|
|
7
|
+
SKILL=$(echo "$INPUT" | jq -r '.tool_input.skill // .tool_input.name // empty')
|
|
8
|
+
|
|
9
|
+
# Only run for the presentation-generator skill
|
|
10
|
+
case "$SKILL" in
|
|
11
|
+
*presentation-generator*) ;;
|
|
12
|
+
*) exit 0 ;;
|
|
13
|
+
esac
|
|
14
|
+
|
|
15
|
+
# Locate the plugin root
|
|
16
|
+
PLUGIN_ROOT=$(find -L .claude ~/.claude \
|
|
17
|
+
-path "*/presentation-generator/scripts/outline_to_graph.py" \
|
|
18
|
+
2>/dev/null | head -1 | sed 's|/scripts/outline_to_graph.py||')
|
|
19
|
+
|
|
20
|
+
[ -z "$PLUGIN_ROOT" ] && exit 0
|
|
21
|
+
|
|
22
|
+
CONTEXT="PLUGIN_ROOT: $PLUGIN_ROOT"
|
|
23
|
+
ESCAPED=$(echo "$CONTEXT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
|
|
24
|
+
|
|
25
|
+
cat <<EOF
|
|
26
|
+
{
|
|
27
|
+
"hookSpecificOutput": {
|
|
28
|
+
"hookEventName": "PostToolUse",
|
|
29
|
+
"additionalContext": $ESCAPED
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
EOF
|
|
@@ -12,7 +12,7 @@ FILE=$(jq -r '.tool_input.file_path // empty' <<< "$INPUT")
|
|
|
12
12
|
|
|
13
13
|
# Only validate presentation JSON
|
|
14
14
|
case "$FILE" in
|
|
15
|
-
*presentations/*.json|*_temp/presentation-draft.json
|
|
15
|
+
*presentations/*.json|*_temp/presentation-draft.json) ;;
|
|
16
16
|
*) exit 0 ;;
|
|
17
17
|
esac
|
|
18
18
|
|
package/package.json
CHANGED
|
@@ -155,12 +155,17 @@ def extract_fields(text):
|
|
|
155
155
|
"""Extract Key message, Content, Speaker notes, Transition from a content block."""
|
|
156
156
|
fields = {}
|
|
157
157
|
|
|
158
|
-
# Define field markers in order
|
|
158
|
+
# Define field markers in order.
|
|
159
|
+
# visual_intent is listed between content and speaker_notes so the parser
|
|
160
|
+
# treats it as a boundary — stopping content extraction before the annotation
|
|
161
|
+
# if the narrative agent places it there. The extracted value is stored but
|
|
162
|
+
# not used in the output JSON (it is metadata for the style agent only).
|
|
159
163
|
markers = [
|
|
160
|
-
("key_message",
|
|
161
|
-
("content",
|
|
164
|
+
("key_message", r"\*\*Key message:\*\*"),
|
|
165
|
+
("content", r"\*\*Content:\*\*"),
|
|
166
|
+
("visual_intent", r"\*\*Visual intent:\*\*"),
|
|
162
167
|
("speaker_notes", r"\*\*Speaker notes:\*\*"),
|
|
163
|
-
("transition",
|
|
168
|
+
("transition", r"\*\*Transition to next:\*\*"),
|
|
164
169
|
]
|
|
165
170
|
|
|
166
171
|
# Find positions of all markers
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
verify_node_count.py — Checks that the JSON node count matches the outline declaration.
|
|
4
|
+
Usage: python3 verify_node_count.py <outline.md> <presentation.json>
|
|
5
|
+
Exit: 0 if counts match, 1 if mismatch.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
if len(sys.argv) != 3:
|
|
16
|
+
print("Usage: python3 verify_node_count.py <outline.md> <presentation.json>")
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
outline = Path(sys.argv[1]).read_text(encoding="utf-8")
|
|
20
|
+
|
|
21
|
+
# Scope counts to their respective sections to avoid false matches
|
|
22
|
+
spine_m = re.search(r"^## SPINE\b", outline, re.MULTILINE)
|
|
23
|
+
dd_m = re.search(r"^## DRILL-DOWNS\b", outline, re.MULTILINE)
|
|
24
|
+
cont_m = re.search(r"^## CONTENT PER SLIDE\b", outline, re.MULTILINE)
|
|
25
|
+
|
|
26
|
+
spine_text = outline[spine_m.end():dd_m.start()] if spine_m and dd_m else ""
|
|
27
|
+
dd_text = outline[dd_m.end():cont_m.start()] if dd_m and cont_m else ""
|
|
28
|
+
|
|
29
|
+
spine_count = len(re.findall(r"^\d+\.\s+\*\*", spine_text, re.MULTILINE))
|
|
30
|
+
drill_count = len(re.findall(r"^-\s+\*\*\d+\.\d+\*\*", dd_text, re.MULTILINE))
|
|
31
|
+
expected = spine_count + drill_count
|
|
32
|
+
|
|
33
|
+
data = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8"))
|
|
34
|
+
actual = len(data["nodes"])
|
|
35
|
+
|
|
36
|
+
print(f"Outline declares {expected} nodes "
|
|
37
|
+
f"({spine_count} spine + {drill_count} drill-down), JSON has {actual}")
|
|
38
|
+
|
|
39
|
+
if actual < expected:
|
|
40
|
+
print(f"MISMATCH: {expected - actual} node(s) missing — "
|
|
41
|
+
f"inspect DRILL-DOWNS headers and child entry format in the outline")
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
main()
|
|
@@ -100,6 +100,7 @@ One H3 block per slide for every spine and drill-down node declared above:
|
|
|
100
100
|
The detailed slide content. Use bullet points and markdown formatting.
|
|
101
101
|
- Point one with a specific number or named entity
|
|
102
102
|
- Point two — consequence or contrast
|
|
103
|
+
**Visual intent:** workhorse
|
|
103
104
|
**Speaker notes:**
|
|
104
105
|
What the presenter says that is not on screen. Talking points, anticipated questions, timing.
|
|
105
106
|
**Transition to next:**
|
|
@@ -128,7 +129,8 @@ Rules:
|
|
|
128
129
|
- Spine header: `### SPINE N: Title`
|
|
129
130
|
- Drill-down header: `### SPINE N.M: Title` — same N.M notation as DRILL-DOWNS section
|
|
130
131
|
- Every node declared in SPINE and DRILL-DOWNS must have a corresponding content block
|
|
131
|
-
- Field markers must be exactly: `**Key message:**`, `**Content:**`, `**Speaker notes:**`, `**Transition to next:**`
|
|
132
|
+
- Field markers must be exactly: `**Key message:**`, `**Content:**`, `**Visual intent:**` (optional), `**Speaker notes:**`, `**Transition to next:**`
|
|
133
|
+
- `**Visual intent:**` is optional metadata for the style agent. If included, place it **after** `**Content:**` and **before** `**Speaker notes:**` on its own line. The parser treats it as a boundary so it never bleeds into `data.content` in the JSON. Valid values: `bookend`, `workhorse`, `breathing-room`, `chapter-opener`, `impact`, `evidence`.
|
|
132
134
|
- Do NOT add parenthetical context like `(drill-down under Slide 3)` after the title
|
|
133
135
|
|
|
134
136
|
---
|
|
@@ -15,19 +15,24 @@ You are an orchestrator. Execute these 7 phases in order. Do not skip phases. Do
|
|
|
15
15
|
|
|
16
16
|
## Phase 1 — Setup
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
**Locate the plugin root.**
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
First, check if your context already contains a line beginning with `PLUGIN_ROOT:`
|
|
21
|
+
(injected automatically by a hook when running in Claude Code). If found, use that
|
|
22
|
+
value directly — you are done with this step.
|
|
23
|
+
|
|
24
|
+
If not found (Cowork mode, or hook not active), use the **Read tool** to probe
|
|
25
|
+
these paths in order — stop at the first one that returns file contents:
|
|
26
|
+
|
|
27
|
+
1. `.claude/plugins/presentation-generator/scripts/outline_to_graph.py`
|
|
28
|
+
2. `~/.claude/plugins/presentation-generator/scripts/outline_to_graph.py`
|
|
29
|
+
|
|
30
|
+
PLUGIN_ROOT is everything before `/scripts/outline_to_graph.py` in the successful
|
|
31
|
+
path (e.g. `.claude/plugins/presentation-generator`).
|
|
32
|
+
|
|
33
|
+
If neither Read succeeds, stop and tell the user the plugin is not installed.
|
|
29
34
|
|
|
30
|
-
Save
|
|
35
|
+
Save PLUGIN_ROOT — you need it for every later phase.
|
|
31
36
|
|
|
32
37
|
Then extract from the user's message:
|
|
33
38
|
- **TOPIC**: what is being presented
|
|
@@ -46,7 +51,7 @@ Say: **"Phase 2 — Extracting content from documents"**
|
|
|
46
51
|
|
|
47
52
|
Call the Task tool with exactly these parameters:
|
|
48
53
|
|
|
49
|
-
- subagent_type: `presentation-content`
|
|
54
|
+
- subagent_type: `presentation-generator:presentation-content`
|
|
50
55
|
- description: `Extract content brief`
|
|
51
56
|
- prompt: (copy this template, fill in the bracketed values)
|
|
52
57
|
|
|
@@ -76,7 +81,7 @@ Say: **"Phase 3 — Designing narrative structure"**
|
|
|
76
81
|
|
|
77
82
|
Call the Task tool with exactly these parameters:
|
|
78
83
|
|
|
79
|
-
- subagent_type: `presentation-narrative`
|
|
84
|
+
- subagent_type: `presentation-generator:presentation-narrative`
|
|
80
85
|
- description: `Design narrative outline`
|
|
81
86
|
- prompt: (copy this template, fill in the bracketed values)
|
|
82
87
|
|
|
@@ -136,33 +141,7 @@ If exit code is non-zero, show the errors and stop.
|
|
|
136
141
|
After a successful run, verify node count against the outline:
|
|
137
142
|
|
|
138
143
|
```bash
|
|
139
|
-
python3 -
|
|
140
|
-
import json, re, sys
|
|
141
|
-
|
|
142
|
-
with open("_temp/presentation-outline.md") as f:
|
|
143
|
-
outline = f.read()
|
|
144
|
-
|
|
145
|
-
# Scope counts to their respective sections to avoid false matches
|
|
146
|
-
spine_m = re.search(r"^## SPINE\b", outline, re.MULTILINE)
|
|
147
|
-
dd_m = re.search(r"^## DRILL-DOWNS\b", outline, re.MULTILINE)
|
|
148
|
-
cont_m = re.search(r"^## CONTENT PER SLIDE\b", outline, re.MULTILINE)
|
|
149
|
-
|
|
150
|
-
spine_text = outline[spine_m.end():dd_m.start()] if spine_m and dd_m else ""
|
|
151
|
-
dd_text = outline[dd_m.end():cont_m.start()] if dd_m and cont_m else ""
|
|
152
|
-
|
|
153
|
-
spine_count = len(re.findall(r"^\d+\.\s+\*\*", spine_text, re.MULTILINE))
|
|
154
|
-
drill_count = len(re.findall(r"^-\s+\*\*\d+\.\d+\*\*", dd_text, re.MULTILINE))
|
|
155
|
-
expected = spine_count + drill_count
|
|
156
|
-
|
|
157
|
-
with open("presentations/{SLUG}/{SLUG}.json") as f:
|
|
158
|
-
data = json.load(f)
|
|
159
|
-
actual = len(data["nodes"])
|
|
160
|
-
|
|
161
|
-
print(f"Outline declares {expected} nodes ({spine_count} spine + {drill_count} drill-down), JSON has {actual}")
|
|
162
|
-
if actual < expected:
|
|
163
|
-
print(f"MISMATCH: {expected - actual} node(s) missing — inspect DRILL-DOWNS headers and child entry format in the outline")
|
|
164
|
-
sys.exit(1)
|
|
165
|
-
EOF
|
|
144
|
+
python3 "{PLUGIN_ROOT}/scripts/verify_node_count.py" _temp/presentation-outline.md "presentations/{SLUG}/{SLUG}.json"
|
|
166
145
|
```
|
|
167
146
|
|
|
168
147
|
If this count check fails: read the DRILL-DOWNS section of `_temp/presentation-outline.md`, identify format mismatches against `outline-format/SKILL.md`, and do NOT proceed to Phase 6 until the outline is corrected and Phase 5 re-run.
|
|
@@ -175,7 +154,7 @@ Say: **"Phase 6 — Applying visual styling"**
|
|
|
175
154
|
|
|
176
155
|
Call the Task tool with exactly these parameters:
|
|
177
156
|
|
|
178
|
-
- subagent_type: `presentation-style`
|
|
157
|
+
- subagent_type: `presentation-generator:presentation-style`
|
|
179
158
|
- description: `Apply visual treatments`
|
|
180
159
|
- prompt: (copy this template, fill in the bracketed values)
|
|
181
160
|
|