prompt_manager 0.5.8 → 1.0.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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/README.md +206 -516
  4. data/Rakefile +0 -8
  5. data/docs/api/configuration.md +31 -327
  6. data/docs/api/constants.md +60 -0
  7. data/docs/api/index.md +14 -0
  8. data/docs/api/metadata.md +99 -0
  9. data/docs/api/parsed.md +98 -0
  10. data/docs/api/pm-module.md +131 -0
  11. data/docs/api/render-context.md +51 -0
  12. data/docs/architecture/design-decisions.md +70 -0
  13. data/docs/architecture/index.md +6 -0
  14. data/docs/architecture/processing-pipeline.md +112 -0
  15. data/docs/assets/css/custom.css +1 -0
  16. data/docs/assets/images/prompt_manager.gif +0 -0
  17. data/docs/assets/images/prompt_manager.mp4 +0 -0
  18. data/docs/examples/ai-agent-prompts.md +173 -0
  19. data/docs/examples/code-review-prompt.md +107 -0
  20. data/docs/examples/index.md +7 -0
  21. data/docs/examples/multi-file-composition.md +123 -0
  22. data/docs/getting-started/configuration.md +106 -0
  23. data/docs/getting-started/index.md +7 -0
  24. data/docs/getting-started/installation.md +10 -73
  25. data/docs/getting-started/quick-start.md +50 -225
  26. data/docs/guides/comment-stripping.md +64 -0
  27. data/docs/guides/custom-directives.md +115 -0
  28. data/docs/guides/erb-rendering.md +102 -0
  29. data/docs/guides/includes.md +146 -0
  30. data/docs/guides/index.md +11 -0
  31. data/docs/guides/parameters.md +96 -0
  32. data/docs/guides/parsing.md +127 -0
  33. data/docs/guides/shell-expansion.md +108 -0
  34. data/docs/index.md +54 -214
  35. data/lib/pm/configuration.rb +17 -0
  36. data/lib/pm/directives.rb +61 -0
  37. data/lib/pm/metadata.rb +17 -0
  38. data/lib/pm/parsed.rb +59 -0
  39. data/lib/pm/shell.rb +57 -0
  40. data/lib/pm/version.rb +5 -0
  41. data/lib/pm.rb +121 -0
  42. data/lib/prompt_manager.rb +2 -27
  43. data/mkdocs.yml +101 -66
  44. metadata +42 -101
  45. data/docs/.keep +0 -0
  46. data/docs/advanced/custom-keywords.md +0 -421
  47. data/docs/advanced/dynamic-directives.md +0 -535
  48. data/docs/advanced/performance.md +0 -612
  49. data/docs/advanced/search-integration.md +0 -635
  50. data/docs/api/directive-processor.md +0 -431
  51. data/docs/api/prompt-class.md +0 -354
  52. data/docs/api/storage-adapters.md +0 -462
  53. data/docs/assets/favicon.ico +0 -1
  54. data/docs/assets/logo.svg +0 -24
  55. data/docs/core-features/comments.md +0 -48
  56. data/docs/core-features/directive-processing.md +0 -38
  57. data/docs/core-features/erb-integration.md +0 -68
  58. data/docs/core-features/error-handling.md +0 -197
  59. data/docs/core-features/parameter-history.md +0 -76
  60. data/docs/core-features/parameterized-prompts.md +0 -500
  61. data/docs/core-features/shell-integration.md +0 -79
  62. data/docs/development/architecture.md +0 -544
  63. data/docs/development/contributing.md +0 -425
  64. data/docs/development/roadmap.md +0 -234
  65. data/docs/development/testing.md +0 -822
  66. data/docs/examples/advanced.md +0 -523
  67. data/docs/examples/basic.md +0 -688
  68. data/docs/examples/real-world.md +0 -776
  69. data/docs/examples.md +0 -337
  70. data/docs/getting-started/basic-concepts.md +0 -318
  71. data/docs/migration/v0.9.0.md +0 -459
  72. data/docs/migration/v1.0.0.md +0 -591
  73. data/docs/storage/activerecord-adapter.md +0 -348
  74. data/docs/storage/custom-adapters.md +0 -176
  75. data/docs/storage/filesystem-adapter.md +0 -236
  76. data/docs/storage/overview.md +0 -427
  77. data/examples/advanced_integrations.rb +0 -52
  78. data/examples/directives.rb +0 -102
  79. data/examples/prompts_dir/advanced_demo.txt +0 -79
  80. data/examples/prompts_dir/directive_example.json +0 -1
  81. data/examples/prompts_dir/directive_example.txt +0 -8
  82. data/examples/prompts_dir/todo.json +0 -1
  83. data/examples/prompts_dir/todo.txt +0 -7
  84. data/examples/prompts_dir/toy/8-ball.txt +0 -4
  85. data/examples/rgfzf +0 -44
  86. data/examples/simple.rb +0 -160
  87. data/examples/using_search_proc.rb +0 -68
  88. data/improvement_plan.md +0 -996
  89. data/lib/prompt_manager/directive_processor.rb +0 -47
  90. data/lib/prompt_manager/prompt.rb +0 -195
  91. data/lib/prompt_manager/storage/active_record_adapter.rb +0 -157
  92. data/lib/prompt_manager/storage/file_system_adapter.rb +0 -339
  93. data/lib/prompt_manager/storage.rb +0 -34
  94. data/lib/prompt_manager/version.rb +0 -5
  95. data/prompt_manager_logo.png +0 -0
@@ -0,0 +1,64 @@
1
+ # Comment Stripping
2
+
3
+ HTML comments are stripped before any other processing stage.
4
+
5
+ ## Automatic Stripping
6
+
7
+ Comments are removed during `PM.parse`:
8
+
9
+ ```markdown
10
+ <!-- This comment will be removed -->
11
+ ---
12
+ title: My Prompt
13
+ ---
14
+ Content here. <!-- This too -->
15
+ ```
16
+
17
+ After parsing:
18
+
19
+ ```ruby
20
+ parsed = PM.parse(source)
21
+ parsed.content #=> "\nContent here. \n"
22
+ ```
23
+
24
+ ## Multiline Comments
25
+
26
+ Multiline comments are fully removed:
27
+
28
+ ```markdown
29
+ <!--
30
+ This entire block
31
+ is removed
32
+ -->
33
+ ---
34
+ title: Example
35
+ ---
36
+ Visible content.
37
+ ```
38
+
39
+ ## Comments Before Metadata
40
+
41
+ Comments before the YAML front-matter are stripped first, so the metadata parser sees clean input:
42
+
43
+ ```markdown
44
+ <!-- Editor note: draft version -->
45
+ ---
46
+ title: My Prompt
47
+ ---
48
+ Content
49
+ ```
50
+
51
+ ## Direct Access
52
+
53
+ The comment stripping method is available as a standalone utility:
54
+
55
+ ```ruby
56
+ PM.strip_comments("Hello <!-- removed --> World")
57
+ #=> "Hello World"
58
+ ```
59
+
60
+ ## Use Cases
61
+
62
+ - **Editor notes** -- Leave comments for prompt authors that don't appear in the rendered output
63
+ - **Disabled sections** -- Temporarily comment out parts of a prompt
64
+ - **Documentation** -- Annotate prompt files without affecting the AI input
@@ -0,0 +1,115 @@
1
+ # Custom Directives
2
+
3
+ Register custom methods that become available inside ERB templates.
4
+
5
+ ## Registering a Directive
6
+
7
+ ```ruby
8
+ PM.register(:read) { |_ctx, path| File.read(path) }
9
+ PM.register(:env) { |_ctx, key| ENV.fetch(key, '') }
10
+ PM.register(:run) { |_ctx, cmd| `#{cmd}`.chomp }
11
+ ```
12
+
13
+ Use them in any prompt file:
14
+
15
+ ```markdown
16
+ ---
17
+ title: Deploy Prompt
18
+ ---
19
+ Hostname: <%= read '/etc/hostname' %>
20
+ Environment: <%= env 'DEPLOY_ENV' %>
21
+ Recent commits: <%= run 'git log --oneline -5' %>
22
+ ```
23
+
24
+ ## Aliases
25
+
26
+ Register multiple names for the same directive by passing additional names:
27
+
28
+ ```ruby
29
+ PM.register(:webpage, :website, :web) { |_ctx, url| fetch_page(url) }
30
+ ```
31
+
32
+ All names point to the same block. Use any of them in ERB:
33
+
34
+ ```markdown
35
+ <%= webpage 'https://example.com' %>
36
+ <%= web 'https://example.com' %>
37
+ ```
38
+
39
+ Duplicate detection still applies — if any name is already registered, an error is raised.
40
+
41
+ ## The RenderContext
42
+
43
+ The first argument to every directive block is a `PM::RenderContext` with access to the current render state:
44
+
45
+ | Field | Type | Description |
46
+ |-------|------|-------------|
47
+ | `directory` | String | Directory of the file being rendered |
48
+ | `params` | Hash | Merged parameter values |
49
+ | `metadata` | PM::Metadata | Current file's metadata |
50
+ | `depth` | Integer | Include nesting depth (0 for top-level) |
51
+ | `included` | Set | File paths already in the include chain |
52
+
53
+ ```ruby
54
+ PM.register(:current_file) { |ctx| ctx.metadata.name || 'unknown' }
55
+ PM.register(:nesting) { |ctx| ctx.depth.to_s }
56
+ ```
57
+
58
+ The context is always the first argument. Additional arguments come from the ERB call:
59
+
60
+ ```ruby
61
+ PM.register(:greet) { |_ctx, name| "Hello, #{name}!" }
62
+ ```
63
+
64
+ ```markdown
65
+ <%= greet 'Alice' %>
66
+ ```
67
+
68
+ ## Duplicate Registration
69
+
70
+ Registering a name that already exists raises an error:
71
+
72
+ ```ruby
73
+ PM.register(:include) { |_ctx, path| path }
74
+ #=> RuntimeError: Directive already registered: include
75
+ ```
76
+
77
+ This protects built-in directives from being overwritten.
78
+
79
+ ## Resetting Directives
80
+
81
+ Remove all custom directives and restore only the built-ins:
82
+
83
+ ```ruby
84
+ PM.reset_directives!
85
+ ```
86
+
87
+ After reset, only `include` is registered.
88
+
89
+ ## Directives in Included Files
90
+
91
+ Custom directives are available in included files too. They share the same directive registry:
92
+
93
+ ```markdown
94
+ <!-- parent.md -->
95
+ <%= include 'child.md' %>
96
+
97
+ <!-- child.md -->
98
+ <%= read '/etc/hostname' %>
99
+ ```
100
+
101
+ ## Name Format
102
+
103
+ Both symbols and strings are accepted:
104
+
105
+ ```ruby
106
+ PM.register(:my_helper) { |_ctx| "works" }
107
+ PM.register('other_helper') { |_ctx| "also works" }
108
+ ```
109
+
110
+ ## Listing Directives
111
+
112
+ ```ruby
113
+ PM.directives
114
+ #=> { include: #<Proc>, read: #<Proc>, ... }
115
+ ```
@@ -0,0 +1,102 @@
1
+ # ERB Rendering
2
+
3
+ ERB templates are evaluated on demand when `to_s` is called.
4
+
5
+ ## Basics
6
+
7
+ Any `<%= expression %>` in the content is evaluated as Ruby:
8
+
9
+ ```markdown
10
+ ---
11
+ title: Math
12
+ ---
13
+ Result: <%= 2 + 2 %>
14
+ Today: <%= Time.now.strftime('%Y-%m-%d') %>
15
+ ```
16
+
17
+ ```ruby
18
+ parsed = PM.parse('math.md')
19
+ parsed.to_s #=> "Result: 4\nToday: 2025-01-15\n"
20
+ ```
21
+
22
+ ## Template Parameters
23
+
24
+ Parameters declared in the YAML front-matter are available as local methods inside ERB:
25
+
26
+ ```markdown
27
+ ---
28
+ title: Greeting
29
+ parameters:
30
+ name: World
31
+ ---
32
+ Hello, <%= name %>!
33
+ ```
34
+
35
+ ```ruby
36
+ parsed = PM.parse('greeting.md')
37
+ parsed.to_s #=> "Hello, World!"
38
+ parsed.to_s('name' => 'Alice') #=> "Hello, Alice!"
39
+ ```
40
+
41
+ See [Parameters](parameters.md) for details on required vs default values.
42
+
43
+ ## Registered Directives
44
+
45
+ Custom directives registered with `PM.register` are also available in ERB:
46
+
47
+ ```ruby
48
+ PM.register(:upcase) { |_ctx, str| str.upcase }
49
+ ```
50
+
51
+ ```markdown
52
+ ---
53
+ title: Example
54
+ ---
55
+ <%= upcase 'hello' %>
56
+ ```
57
+
58
+ See [Custom Directives](custom-directives.md) for the full guide.
59
+
60
+ ## Built-in Directives
61
+
62
+ PM includes one built-in directive:
63
+
64
+ - **`include`** -- Include and render another prompt file. See [Including Files](includes.md).
65
+
66
+ ## Disabling ERB
67
+
68
+ Set `erb: false` in the file's YAML metadata:
69
+
70
+ ```markdown
71
+ ---
72
+ title: Raw Template
73
+ erb: false
74
+ ---
75
+ This <%= expression %> is preserved as-is.
76
+ ```
77
+
78
+ When ERB is disabled:
79
+
80
+ - `to_s` returns the content without template evaluation
81
+ - Parameters are ignored
82
+ - The `include` directive does not execute
83
+ - `metadata.includes` is an empty array after `to_s`
84
+
85
+ ### Global Default
86
+
87
+ ```ruby
88
+ PM.configure { |c| c.erb = false }
89
+ ```
90
+
91
+ Per-file metadata always overrides the global setting. A file with `erb: true` gets ERB rendering even when the global default is `false`.
92
+
93
+ ## Pipeline Position
94
+
95
+ ERB rendering is the last stage of the pipeline, executed on demand:
96
+
97
+ 1. Strip HTML comments
98
+ 2. Extract YAML metadata
99
+ 3. Shell expansion (at parse time)
100
+ 4. **ERB rendering** (at `to_s` time)
101
+
102
+ This means shell variables are already expanded before ERB sees the content. If content contains both `$VAR` and `<%= param %>`, the shell variable is resolved first, then ERB fills in the parameters.
@@ -0,0 +1,146 @@
1
+ # Including Files
2
+
3
+ The `include` directive lets you compose prompts from multiple files.
4
+
5
+ ## Basic Usage
6
+
7
+ ```markdown
8
+ ---
9
+ title: Full Review
10
+ parameters:
11
+ code: null
12
+ ---
13
+ <%= include 'common/header.md' %>
14
+
15
+ Review this code:
16
+ <%= code %>
17
+
18
+ <%= include 'common/footer.md' %>
19
+ ```
20
+
21
+ ```ruby
22
+ parsed = PM.parse('full_review.md')
23
+ puts parsed.to_s('code' => File.read('app.rb'))
24
+ ```
25
+
26
+ ## How Includes Work
27
+
28
+ Each included file goes through the full processing pipeline:
29
+
30
+ 1. HTML comments stripped
31
+ 2. YAML metadata extracted
32
+ 3. Shell expansion applied
33
+ 4. ERB rendered (with the parent's parameter values)
34
+
35
+ The included file's rendered content replaces the `<%= include ... %>` tag.
36
+
37
+ ## Path Resolution
38
+
39
+ Include paths are resolved relative to the **parent file's directory**:
40
+
41
+ ```
42
+ prompts/
43
+ review.md # contains: <%= include 'common/header.md' %>
44
+ common/
45
+ header.md # resolved from prompts/common/header.md
46
+ ```
47
+
48
+ ## Parameter Passing
49
+
50
+ The parent's parameter values are passed to included files. Child files can use the same parameter names:
51
+
52
+ ```markdown
53
+ <!-- parent.md -->
54
+ ---
55
+ parameters:
56
+ name: null
57
+ ---
58
+ <%= include 'greeting.md' %>
59
+ Done.
60
+ ```
61
+
62
+ ```markdown
63
+ <!-- greeting.md -->
64
+ ---
65
+ title: Greeting
66
+ ---
67
+ Hello, <%= name %>!
68
+ ```
69
+
70
+ ```ruby
71
+ parsed = PM.parse('parent.md')
72
+ parsed.to_s('name' => 'Alice')
73
+ #=> "Hello, Alice!\nDone."
74
+ ```
75
+
76
+ ## Nested Includes
77
+
78
+ Includes can be nested. File A can include File B, which includes File C:
79
+
80
+ ```markdown
81
+ <!-- a.md -->
82
+ <%= include 'b.md' %>
83
+
84
+ <!-- b.md -->
85
+ <%= include 'c.md' %>
86
+
87
+ <!-- c.md -->
88
+ Content from C
89
+ ```
90
+
91
+ ## Circular Include Detection
92
+
93
+ PM detects circular includes and raises an error:
94
+
95
+ ```markdown
96
+ <!-- circular_a.md -->
97
+ <%= include 'circular_b.md' %>
98
+
99
+ <!-- circular_b.md -->
100
+ <%= include 'circular_a.md' %>
101
+ ```
102
+
103
+ ```ruby
104
+ PM.parse('circular_a.md').to_s
105
+ #=> RuntimeError: Circular include detected: /path/to/circular_a.md
106
+ ```
107
+
108
+ ## Includes Metadata
109
+
110
+ After calling `to_s`, the parent's metadata has an `includes` key with a tree of what was included:
111
+
112
+ ```ruby
113
+ parsed = PM.parse('full_review.md')
114
+ parsed.to_s('code' => source)
115
+
116
+ parsed.metadata.includes
117
+ #=> [
118
+ # {
119
+ # path: "/prompts/common/header.md",
120
+ # depth: 1,
121
+ # metadata: { title: "Header", ... },
122
+ # includes: []
123
+ # },
124
+ # {
125
+ # path: "/prompts/common/footer.md",
126
+ # depth: 1,
127
+ # metadata: { title: "Footer", ... },
128
+ # includes: []
129
+ # }
130
+ # ]
131
+ ```
132
+
133
+ Nested includes form a tree -- each entry's `includes` array contains its own children.
134
+
135
+ The `includes` array is `nil` before `to_s` is called and is reset on each call.
136
+
137
+ ## Requirements
138
+
139
+ - The `include` directive requires file context. Using it with string-parsed prompts raises an error:
140
+
141
+ ```ruby
142
+ PM.parse("---\n---\n<%= include 'other.md' %>").to_s
143
+ #=> RuntimeError: include requires a file context (use PM.parse with a file path)
144
+ ```
145
+
146
+ - When `erb: false`, the include directive does not execute and `metadata.includes` is an empty array.
@@ -0,0 +1,11 @@
1
+ # Guides
2
+
3
+ In-depth guides for each PM feature.
4
+
5
+ - [Parsing Prompts](parsing.md) -- File paths vs raw strings, metadata extraction
6
+ - [Parameters](parameters.md) -- Required and default parameters, rendering with `to_s`
7
+ - [Shell Expansion](shell-expansion.md) -- Environment variables and command substitution
8
+ - [ERB Rendering](erb-rendering.md) -- Template evaluation and disabling
9
+ - [Including Files](includes.md) -- Composing prompts from multiple files
10
+ - [Custom Directives](custom-directives.md) -- Registering your own ERB helpers
11
+ - [Comment Stripping](comment-stripping.md) -- HTML comment removal
@@ -0,0 +1,96 @@
1
+ # Parameters
2
+
3
+ Parameters are declared in the YAML `parameters` hash and used as ERB template variables.
4
+
5
+ ## Declaring Parameters
6
+
7
+ ```markdown
8
+ ---
9
+ title: Code Review
10
+ parameters:
11
+ language: ruby
12
+ code: null
13
+ style_guide: ~/guides/default.md
14
+ ---
15
+ Review the following <%= language %> code using the style guide
16
+ at <%= style_guide %>:
17
+
18
+ <%= code %>
19
+ ```
20
+
21
+ ## Required vs Default
22
+
23
+ - **`null` value** -- the parameter is **required**. `to_s` raises `ArgumentError` if not provided.
24
+ - **Any other value** -- used as the **default**. Can be overridden when calling `to_s`.
25
+
26
+ ```ruby
27
+ parsed = PM.parse('code_review.md')
28
+
29
+ parsed.metadata.parameters
30
+ #=> {"language" => "ruby", "code" => nil, "style_guide" => "~/guides/default.md"}
31
+ ```
32
+
33
+ ## Rendering with to_s
34
+
35
+ `to_s` accepts a Hash of parameter values:
36
+
37
+ ```ruby
38
+ # Supply required params, accept defaults for the rest
39
+ parsed.to_s('code' => File.read('app.rb'))
40
+
41
+ # Override a default
42
+ parsed.to_s('code' => File.read('app.py'), 'language' => 'python')
43
+ ```
44
+
45
+ Symbol keys work too:
46
+
47
+ ```ruby
48
+ parsed.to_s(code: File.read('app.rb'))
49
+ ```
50
+
51
+ ### All Defaults
52
+
53
+ When every parameter has a default value, `to_s` needs no arguments:
54
+
55
+ ```ruby
56
+ parsed.to_s #=> renders with all defaults
57
+ ```
58
+
59
+ ### Missing Required Parameters
60
+
61
+ Calling `to_s` without supplying a required parameter raises `ArgumentError`:
62
+
63
+ ```ruby
64
+ parsed.to_s
65
+ #=> ArgumentError: Missing required parameters: code
66
+ ```
67
+
68
+ The error message lists all missing parameters.
69
+
70
+ ## Parameters in Included Files
71
+
72
+ When a file includes another file via `<%= include 'child.md' %>`, the parent's parameter values are passed to the child. The child can use the same parameter names:
73
+
74
+ ```markdown
75
+ <!-- parent.md -->
76
+ ---
77
+ title: Parent
78
+ parameters:
79
+ name: null
80
+ ---
81
+ <%= include 'greeting.md' %>
82
+ ```
83
+
84
+ ```markdown
85
+ <!-- greeting.md -->
86
+ ---
87
+ title: Greeting
88
+ ---
89
+ Hello, <%= name %>!
90
+ ```
91
+
92
+ ```ruby
93
+ parsed = PM.parse('parent.md')
94
+ parsed.to_s('name' => 'Alice')
95
+ #=> "Hello, Alice!"
96
+ ```
@@ -0,0 +1,127 @@
1
+ # Parsing Prompts
2
+
3
+ `PM.parse` is the main entry point. It accepts a file path, a Symbol, a single word, or a raw string.
4
+
5
+ ## File Paths
6
+
7
+ A source is treated as a file path when it:
8
+
9
+ - Is a `Pathname` object
10
+ - Responds to `to_path`
11
+ - Is a String ending in `.md`
12
+ - Is a Symbol (`.md` is appended, e.g. `:code_review` → `code_review.md`)
13
+ - Is a single word containing only letters, digits, or underscores (`.md` is appended, e.g. `"code_review"` → `code_review.md`)
14
+
15
+ ```ruby
16
+ # String file path
17
+ parsed = PM.parse('code_review.md')
18
+
19
+ # Pathname
20
+ parsed = PM.parse(Pathname.new('code_review.md'))
21
+
22
+ # Symbol (basename — .md appended)
23
+ parsed = PM.parse(:code_review)
24
+
25
+ # Single word (basename — .md appended)
26
+ parsed = PM.parse('code_review')
27
+ ```
28
+
29
+ When parsing a file, PM adds these keys to the metadata automatically:
30
+
31
+ | Key | Type | Description |
32
+ |-----|------|-------------|
33
+ | `directory` | String | Absolute path to the file's parent directory |
34
+ | `name` | String | Filename (e.g., `code_review.md`) |
35
+ | `created_at` | Time | File creation timestamp |
36
+ | `modified_at` | Time | Last modification timestamp |
37
+
38
+ ### prompts_dir
39
+
40
+ Relative file paths are resolved against `PM.config.prompts_dir`:
41
+
42
+ ```ruby
43
+ PM.configure { |c| c.prompts_dir = '~/.prompts' }
44
+
45
+ PM.parse('agents/summarize.md')
46
+ #=> reads ~/.prompts/agents/summarize.md
47
+ ```
48
+
49
+ Absolute paths bypass `prompts_dir`:
50
+
51
+ ```ruby
52
+ PM.parse('/etc/prompts/system.md')
53
+ #=> reads /etc/prompts/system.md regardless of prompts_dir
54
+ ```
55
+
56
+ ## Raw Strings
57
+
58
+ Any other string is parsed directly:
59
+
60
+ ```ruby
61
+ parsed = PM.parse("---\ntitle: Hello\n---\nContent here")
62
+
63
+ parsed.metadata.title #=> "Hello"
64
+ parsed.content #=> "\nContent here\n"
65
+ ```
66
+
67
+ String-parsed prompts do not have `directory`, `name`, `created_at`, or `modified_at` in their metadata.
68
+
69
+ !!! warning "Include limitations"
70
+ The `include` directive requires file context to resolve relative paths. It raises an error when used with string-parsed prompts.
71
+
72
+ ## Metadata Extraction
73
+
74
+ YAML front-matter is delimited by `---` fences:
75
+
76
+ ```markdown
77
+ ---
78
+ title: My Prompt
79
+ provider: openai
80
+ model: gpt-4
81
+ temperature: 0.3
82
+ ---
83
+ The prompt content starts here.
84
+ ```
85
+
86
+ All YAML keys become accessible via dot notation on the `PM::Metadata` object:
87
+
88
+ ```ruby
89
+ parsed.metadata.title #=> "My Prompt"
90
+ parsed.metadata.provider #=> "openai"
91
+ parsed.metadata.temperature #=> 0.3
92
+ ```
93
+
94
+ Boolean keys also get predicate methods:
95
+
96
+ ```ruby
97
+ parsed.metadata.shell #=> true
98
+ parsed.metadata.shell? #=> true
99
+ ```
100
+
101
+ ### No Metadata
102
+
103
+ Files without front-matter are valid. The metadata object is empty and the entire content is preserved:
104
+
105
+ ```ruby
106
+ parsed = PM.parse("Just plain text")
107
+ parsed.metadata.title #=> nil
108
+ parsed.content #=> "Just plain text"
109
+ ```
110
+
111
+ ### Bracket Accessor
112
+
113
+ You can also access metadata with bracket notation:
114
+
115
+ ```ruby
116
+ parsed[:title] #=> "My Prompt"
117
+ ```
118
+
119
+ ## Pipeline Stages
120
+
121
+ After parsing, the content has been through these stages:
122
+
123
+ 1. HTML comments stripped
124
+ 2. YAML metadata extracted
125
+ 3. Shell expansion applied (if `shell: true`)
126
+
127
+ ERB rendering happens later, on demand, when `to_s` is called.