ima-claude 2.20.0 → 2.25.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 (79) hide show
  1. package/README.md +48 -9
  2. package/dist/cli.js +1 -1
  3. package/package.json +1 -1
  4. package/plugins/ima-claude/.claude-plugin/plugin.json +1 -1
  5. package/plugins/ima-claude/agents/explorer.md +29 -15
  6. package/plugins/ima-claude/agents/implementer.md +58 -13
  7. package/plugins/ima-claude/agents/memory.md +19 -19
  8. package/plugins/ima-claude/agents/reviewer.md +56 -34
  9. package/plugins/ima-claude/agents/tester.md +59 -16
  10. package/plugins/ima-claude/agents/wp-developer.md +66 -21
  11. package/plugins/ima-claude/hooks/bootstrap.sh +42 -44
  12. package/plugins/ima-claude/hooks/prompt_coach_digest.md +14 -17
  13. package/plugins/ima-claude/hooks/prompt_coach_system.md +10 -12
  14. package/plugins/ima-claude/personalities/README.md +17 -6
  15. package/plugins/ima-claude/personalities/enable-efficient.md +61 -0
  16. package/plugins/ima-claude/personalities/enable-terse.md +71 -0
  17. package/plugins/ima-claude/skills/agentic-workflows/SKILL.md +35 -71
  18. package/plugins/ima-claude/skills/architect/SKILL.md +54 -168
  19. package/plugins/ima-claude/skills/compound-bridge/SKILL.md +41 -94
  20. package/plugins/ima-claude/skills/design-to-code/SKILL.md +43 -78
  21. package/plugins/ima-claude/skills/discourse/SKILL.md +79 -194
  22. package/plugins/ima-claude/skills/discourse-admin/SKILL.md +41 -103
  23. package/plugins/ima-claude/skills/docs-organize/SKILL.md +63 -203
  24. package/plugins/ima-claude/skills/ember-discourse/SKILL.md +90 -200
  25. package/plugins/ima-claude/skills/espocrm/SKILL.md +14 -23
  26. package/plugins/ima-claude/skills/espocrm-api/SKILL.md +79 -192
  27. package/plugins/ima-claude/skills/functional-programmer/SKILL.md +33 -237
  28. package/plugins/ima-claude/skills/gh-cli/SKILL.md +26 -65
  29. package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +71 -104
  30. package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +32 -22
  31. package/plugins/ima-claude/skills/ima-brand/SKILL.md +18 -23
  32. package/plugins/ima-claude/skills/ima-copywriting/SKILL.md +68 -179
  33. package/plugins/ima-claude/skills/ima-doc2pdf/SKILL.md +32 -102
  34. package/plugins/ima-claude/skills/ima-editorial-scorecard/SKILL.md +38 -63
  35. package/plugins/ima-claude/skills/ima-editorial-workflow/SKILL.md +69 -114
  36. package/plugins/ima-claude/skills/ima-email-creator/SKILL.md +16 -22
  37. package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +21 -37
  38. package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +39 -120
  39. package/plugins/ima-claude/skills/jquery/SKILL.md +107 -233
  40. package/plugins/ima-claude/skills/js-fp/SKILL.md +75 -296
  41. package/plugins/ima-claude/skills/js-fp-api/SKILL.md +52 -162
  42. package/plugins/ima-claude/skills/js-fp-react/SKILL.md +47 -270
  43. package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +55 -209
  44. package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +59 -204
  45. package/plugins/ima-claude/skills/livecanvas/SKILL.md +19 -32
  46. package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +92 -162
  47. package/plugins/ima-claude/skills/mcp-context7/SKILL.md +32 -64
  48. package/plugins/ima-claude/skills/mcp-gitea/SKILL.md +98 -188
  49. package/plugins/ima-claude/skills/mcp-github/SKILL.md +60 -124
  50. package/plugins/ima-claude/skills/mcp-memory/SKILL.md +1 -177
  51. package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +58 -115
  52. package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +32 -87
  53. package/plugins/ima-claude/skills/mcp-serena/SKILL.md +54 -80
  54. package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +40 -63
  55. package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +75 -116
  56. package/plugins/ima-claude/skills/php-authnet/SKILL.md +32 -65
  57. package/plugins/ima-claude/skills/php-fp/SKILL.md +50 -129
  58. package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +25 -73
  59. package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +103 -463
  60. package/plugins/ima-claude/skills/playwright/SKILL.md +69 -220
  61. package/plugins/ima-claude/skills/prompt-starter/SKILL.md +33 -83
  62. package/plugins/ima-claude/skills/prompt-starter/references/code-review.md +38 -0
  63. package/plugins/ima-claude/skills/py-fp/SKILL.md +78 -384
  64. package/plugins/ima-claude/skills/quasar-fp/SKILL.md +54 -255
  65. package/plugins/ima-claude/skills/quickstart/SKILL.md +7 -11
  66. package/plugins/ima-claude/skills/rails/SKILL.md +63 -184
  67. package/plugins/ima-claude/skills/resume-session/SKILL.md +14 -35
  68. package/plugins/ima-claude/skills/rg/SKILL.md +61 -146
  69. package/plugins/ima-claude/skills/ruby-fp/SKILL.md +66 -163
  70. package/plugins/ima-claude/skills/save-session/SKILL.md +10 -39
  71. package/plugins/ima-claude/skills/scorecard/SKILL.md +24 -38
  72. package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +42 -71
  73. package/plugins/ima-claude/skills/skill-creator/SKILL.md +79 -250
  74. package/plugins/ima-claude/skills/task-master/SKILL.md +11 -31
  75. package/plugins/ima-claude/skills/task-planner/SKILL.md +44 -153
  76. package/plugins/ima-claude/skills/task-runner/SKILL.md +61 -143
  77. package/plugins/ima-claude/skills/unit-testing/SKILL.md +59 -134
  78. package/plugins/ima-claude/skills/wp-ddev/SKILL.md +38 -120
  79. package/plugins/ima-claude/skills/wp-local/SKILL.md +26 -108
@@ -5,122 +5,87 @@ description: "Convert design screenshots into working WordPress code through a t
5
5
 
6
6
  # Design to Code
7
7
 
8
- Orchestrate the transformation of design screenshots into working WordPress code. Two phases, one skill — produce the implementation prompt (Phase A), then execute it (Phase B).
9
-
10
- You are the orchestrator. You analyze designs, compose prompts, delegate implementation, and verify results. You do NOT implement directly — you delegate to `ima-claude:wp-developer` agents.
11
-
12
- ---
8
+ Two-phase workflow: screenshots implementation prompt (Phase A) working WordPress code (Phase B). You orchestrate; delegate implementation to `ima-claude:wp-developer`.
13
9
 
14
10
  ## Mode Selection
15
11
 
16
- Determine the mode from the user's input:
17
-
18
12
  ```
19
13
  What did the user provide?
20
14
  ├── Screenshots/mockups (no existing prompt)
21
- │ → Phase A: Design Prompt
22
- │ → Read references/phase-a-design-to-prompt.md
23
-
24
- ├── An existing implementation prompt
25
- │ → Phase B: Prompt → Code
26
- │ → Read references/phase-b-prompt-to-code.md
27
-
15
+ │ → Phase A → references/phase-a-design-to-prompt.md
16
+ ├── Existing implementation prompt
17
+ → Phase B → references/phase-b-prompt-to-code.md
28
18
  └── Screenshots + "implement this" / "full pipeline"
29
19
  → Phase A then Phase B in sequence
30
- → Phase A produces prompt → user reviews → Phase B executes
31
20
  ```
32
21
 
33
- ---
34
-
35
22
  ## Required Skills
36
23
 
37
- Always load alongside this skill:
38
- - **`ima-brand`** — color palette, typography, mixins (needed for both phases)
39
- - **`ima-bootstrap`** — utility classes, grid system, components
40
-
41
- Phase B additionally needs:
42
- - **`php-fp-wordpress`** — WordPress development patterns, security, shortcodes
43
-
44
- ---
24
+ - **`ima-brand`** color palette, typography, mixins (both phases)
25
+ - **`ima-bootstrap`** — utility classes, grid, components
26
+ - **`php-fp-wordpress`** — WordPress patterns (Phase B only)
45
27
 
46
28
  ## Phase A: Design → Prompt
47
29
 
48
- Transform design screenshots into a detailed, section-by-section implementation prompt. Output: a ~200-300 line prompt file matching the team's established template structure.
49
-
50
- Before starting, search Qdrant for `design-to-prompt` to recall prior lessons.
51
-
52
- ### Steps (read `references/phase-a-design-to-prompt.md` for detailed procedures)
53
-
54
- 1. **GATHER** — Fetch Jira context + receive screenshots + explore codebase (parallel)
55
- 2. **ANALYZE** — Load brand palette from `ima-brand` (**must complete before COMPOSE**)
56
- 3. **CROP** — Full view → section detection → detail crops (iterative PIL cropping)
57
- 4. **EXTRACT** — Per crop: exact text, icons, colors, layout, spacing
58
- 5. **MAP** — Visual elements → brand variables, components → existing shortcodes
59
- 6. **COMPOSE** — Write prompt using `references/prompt-template.md` structure
60
- 7. **VALIDATE** — Re-check each section against its crop for accuracy
30
+ Output: ~200-300 line prompt file matching team template. Search Qdrant for `design-to-prompt` before starting.
61
31
 
62
- **Output**: Save prompt to `docs/designs/{ticket}/PROMPT.md` and Serena memory as `{feature-name}-plan`.
32
+ | Step | Action |
33
+ |------|--------|
34
+ | GATHER | Fetch Jira context + receive screenshots + explore codebase (parallel) |
35
+ | ANALYZE | Load brand palette from `ima-brand` — must complete before COMPOSE |
36
+ | CROP | Full view → section detection → detail crops (iterative PIL cropping) |
37
+ | EXTRACT | Per crop: exact text, icons, colors, layout, spacing |
38
+ | MAP | Visual elements → brand variables, components → existing shortcodes |
39
+ | COMPOSE | Write prompt using `references/prompt-template.md` structure |
40
+ | VALIDATE | Re-check each section against its crop for accuracy |
63
41
 
64
- After Phase A, present the prompt to the user. Stop here unless running full pipeline.
65
-
66
- ---
42
+ Save prompt to `docs/designs/{ticket}/PROMPT.md` and Serena memory as `{feature-name}-plan`. Present to user; stop here unless running full pipeline.
67
43
 
68
44
  ## Phase B: Prompt → Code
69
45
 
70
- Execute an implementation prompt to produce working WordPress code. Input: a structured prompt (from Phase A or user-provided).
71
-
72
- Before starting, search Qdrant for `design-to-code` to recall prior lessons.
46
+ Search Qdrant for `design-to-code` before starting.
73
47
 
74
- ### Steps (read `references/phase-b-prompt-to-code.md` for detailed procedures)
75
-
76
- 1. **RESEARCH** Brand SCSS files + current code + component libraries (parallel explorers)
77
- 2. **ARCHITECTURE** New file vs modify, function reuse, component migration decision
78
- 3. **DECOMPOSE** Stories by page section; Story 1 = foundation, Stories 2-N = parallel fills, final = polish
79
- 4. **IMPLEMENT** Delegate to `ima-claude:wp-developer` per story with precise prompts
80
- 5. **REVIEW** Verify copy, colors, element order, asset paths (orchestrator review before visual test)
81
- 6. **VISUAL-QA** Compile SASS → screenshot desktop + mobile → compare to design → iterate
82
-
83
- ---
48
+ | Step | Action |
49
+ |------|--------|
50
+ | RESEARCH | Brand SCSS files + current code + component libraries (parallel explorers) |
51
+ | ARCHITECTURE | New file vs modify, function reuse, component migration decision |
52
+ | DECOMPOSE | Stories by page section; Story 1 = foundation, Stories 2-N = parallel fills, final = polish |
53
+ | IMPLEMENT | Delegate to `ima-claude:wp-developer` per story with precise prompts |
54
+ | REVIEW | Verify copy, colors, element order, asset paths before visual test |
55
+ | VISUAL-QA | Compile SASS → screenshot desktop + mobile → compare to design → iterate |
84
56
 
85
57
  ## Critical Guardrails
86
58
 
87
- Read `references/guardrails.md` for the complete set. The top 5 (each learned from a real failure):
59
+ Full set in `references/guardrails.md`. Top 5:
88
60
 
89
- 1. **Never hardcode colors**always brand SCSS variables or Bootstrap utilities
90
- 2. **Always verify asset paths exist** — Glob/grep before referencing; check existing usage patterns
91
- 3. **Always provide exact copy text**never let agents paraphrase; include verbatim text in quotes
92
- 4. **Load brand palette BEFORE composition** — informs every color reference on first pass
93
- 5. **Check site header/footer first** — don't build custom components that duplicate existing site elements
61
+ 1. Never hardcode colors — use brand SCSS variables or Bootstrap utilities
62
+ 2. Always verify asset paths exist — Glob/grep before referencing
63
+ 3. Always provide exact copy text — include verbatim text in quotes, never let agents paraphrase
64
+ 4. Load brand palette BEFORE composition — informs every color reference on first pass
65
+ 5. Check site header/footer first — don't build components that duplicate existing site elements
94
66
 
95
- ---
96
-
97
- ## Agent Delegation Model
67
+ ## Agent Delegation
98
68
 
99
69
  | Role | Agent | When |
100
- |---|---|---|
70
+ |------|-------|------|
101
71
  | Orchestrator | opus (you) | All phases — research, planning, decomposition, delegation, review, surgical fixes |
102
72
  | Codebase explorer | `ima-claude:explorer` (haiku) | GATHER/RESEARCH: find existing shortcodes, templates, SCSS files |
103
73
  | Implementer | `ima-claude:wp-developer` (sonnet) | IMPLEMENT: write PHP/SCSS with skills: ima-brand, ima-bootstrap, php-fp-wordpress |
104
- | Reviewer | `ima-claude:reviewer` (sonnet, read-only) | REVIEW: brand compliance + accessibility audit (for larger implementations) |
105
-
106
- Orchestrator does surgical fixes (<5 lines) directly via Edit tool. Anything larger → delegate to wp-developer.
74
+ | Reviewer | `ima-claude:reviewer` (sonnet, read-only) | REVIEW: brand compliance + accessibility audit (larger implementations) |
107
75
 
108
- ---
76
+ Orchestrator does surgical fixes (<5 lines) directly via Edit. Anything larger → delegate to wp-developer.
109
77
 
110
78
  ## Qdrant Integration
111
79
 
112
- Before each phase, search Qdrant for prior lessons:
113
- - Phase A: `qdrant_find("design-to-prompt workflow")` — retrieves methodology, cropping techniques, composition decisions
114
- - Phase B: `qdrant_find("design-to-code implementation")` — retrieves decomposition patterns, delegation templates, QA patterns
115
-
116
- ---
80
+ - Phase A: `qdrant_find("design-to-prompt workflow")`
81
+ - Phase B: `qdrant_find("design-to-code implementation")`
117
82
 
118
83
  ## Related Skills
119
84
 
120
85
  | Skill | Relationship |
121
- |---|---|
86
+ |-------|------|
122
87
  | `ima-brand` | Required — color palette, typography, mixins |
123
88
  | `ima-bootstrap` | Required — utility classes, grid, components |
124
- | `php-fp-wordpress` | Required for Phase B — WordPress dev patterns |
125
- | `task-master` | Optional — for complex multi-page designs needing Epic > Story > Task decomposition |
126
- | `prompt-starter` | Pattern borrowed — Phase A follows its "builder not executor" philosophy |
89
+ | `php-fp-wordpress` | Required for Phase B |
90
+ | `task-master` | Optional — complex multi-page designs needing Epic > Story > Task |
91
+ | `prompt-starter` | Phase A follows its "builder not executor" philosophy |
@@ -5,59 +5,38 @@ description: "Discourse plugin development - plugin.rb, after_initialize, admin
5
5
 
6
6
  # Discourse Plugin Development
7
7
 
8
- Discourse plugins are Rails engines with conventions. Work with the framework — its plugin API, Guardian authorization system, and event hooks exist for good reason.
8
+ Discourse plugins are Rails engines. Work with the framework — Plugin API, Guardian, and event hooks exist for good reason.
9
9
 
10
- ## When to Use This Skill
10
+ ## Core Rules
11
11
 
12
- - Building or modifying Discourse plugins
13
- - Adding admin UI to a Discourse plugin
14
- - Implementing authentication hooks or user lifecycle extensions
15
- - Migrating data into Discourse (import scripts)
16
- - Reviewing Discourse plugin security
12
+ - `after_initialize` all plugin wiring goes here
13
+ - `Guardian` authorization layer; every user action checks it
14
+ - `register_*` / `add_*` use Plugin API hooks, not monkey-patches
15
+ - `StaffConstraint` required on all admin routes; never roll your own
16
+ - `RuboCop` — enforced; Discourse ships lint rules
17
17
 
18
- ## Core Philosophy
19
-
20
- > Discourse plugins are Rails. Discourse adds its own authorization layer (Guardian). Respect both.
21
-
22
- - **`after_initialize`** is where plugin logic wires into Discourse
23
- - **Guardian** is the authorization layer — every action checks it
24
- - **Plugin API** hooks (`register_*`, `add_*`) are the extension points — don't monkey-patch core
25
- - **StaffConstraint** protects admin routes — use it, never roll your own
26
- - **RuboCop** is enforced — Discourse ships lint rules
27
-
28
- **Foundation**: Reference `../rails/SKILL.md` for Rails security practices and `../ruby-fp/SKILL.md` for Ruby patterns.
18
+ Foundation: `../rails/SKILL.md` (security), `../ruby-fp/SKILL.md` (patterns).
29
19
 
30
20
  ## Plugin Structure
31
21
 
32
22
  ```
33
23
  my-plugin/
34
24
  ├── plugin.rb # Manifest + bootstrap (required)
35
- ├── about.json # Metadata for admin panel
25
+ ├── about.json
36
26
  ├── app/
37
- │ ├── controllers/
38
- │ └── admin/
39
- └── my_plugin_controller.rb
40
- │ ├── models/
41
- │ │ └── my_plugin_record.rb
42
- │ └── serializers/
43
- │ └── my_plugin_serializer.rb
27
+ │ ├── controllers/admin/my_plugin_controller.rb
28
+ ├── models/my_plugin_record.rb
29
+ │ └── serializers/my_plugin_serializer.rb
44
30
  ├── config/
45
- │ ├── locales/
46
- └── server.en.yml
47
- │ └── settings.yml # Site settings
48
- ├── db/
49
- │ └── migrate/
50
- └── 20250101000000_create_my_plugin.rb
51
- ├── assets/
52
- │ └── javascripts/
53
- │ └── admin/ # Ember components for admin UI
54
- ├── lib/
55
- │ └── my_plugin/ # Pure logic (no Discourse deps)
56
- └── spec/
57
- └── plugin_helper.rb
31
+ │ ├── locales/server.en.yml
32
+ │ └── settings.yml
33
+ ├── db/migrate/
34
+ ├── assets/javascripts/admin/ # Ember admin UI
35
+ ├── lib/my_plugin/ # Pure logic (no Discourse deps)
36
+ └── spec/plugin_helper.rb
58
37
  ```
59
38
 
60
- ## plugin.rb: Manifest + Bootstrap
39
+ ## plugin.rb
61
40
 
62
41
  ```ruby
63
42
  # frozen_string_literal: true
@@ -68,63 +47,44 @@ my-plugin/
68
47
  # authors: Your Name
69
48
  # url: https://github.com/yourorg/my-plugin
70
49
 
71
- # Autoloading — define module first, then require engine
72
50
  module ::MyPlugin
73
51
  PLUGIN_NAME = "my-plugin"
74
52
  end
75
53
 
76
54
  require_relative "lib/my_plugin/engine"
77
55
 
78
- # All wiring happens inside after_initialize
79
56
  after_initialize do
80
- # Register settings, hooks, extensions here
81
- # This runs after Discourse core is fully loaded
57
+ # All wiring here runs after Discourse core is fully loaded
82
58
  end
83
59
  ```
84
60
 
85
- ## after_initialize: The Plugin Entry Point
61
+ ## after_initialize
86
62
 
87
63
  ```ruby
88
64
  after_initialize do
89
- # Extend models — use class_eval in after_initialize, not top-level monkey-patches
90
65
  User.class_eval do
91
66
  has_one :my_plugin_profile, dependent: :destroy
92
67
  end
93
68
 
94
- # Register custom fields
95
69
  register_post_custom_field_type('wp_original_id', :integer)
96
70
  register_topic_custom_field_type('imported_from', :string)
97
71
 
98
- # Wire into Discourse events (preferred over overriding methods)
99
- on(:user_created) do |user|
100
- MyPlugin::UserSetup.call(user)
101
- end
102
-
103
- on(:post_created) do |post, opts, user|
104
- MyPlugin::PostSync.call(post, user)
105
- end
72
+ on(:user_created) { |user| MyPlugin::UserSetup.call(user) }
73
+ on(:post_created) { |post, opts, user| MyPlugin::PostSync.call(post, user) }
106
74
 
107
- # Add serializer extensions
108
- add_to_serializer(:user, :wp_user_id) do
109
- object.custom_fields['wp_user_id']
110
- end
75
+ add_to_serializer(:user, :wp_user_id) { object.custom_fields['wp_user_id'] }
111
76
  end
112
77
  ```
113
78
 
114
79
  ## Admin Routes + Controller
115
80
 
116
- Every admin route needs `StaffConstraint` — never expose admin actions without it.
117
-
118
81
  ```ruby
119
- # In plugin.rb — register the admin nav link
82
+ # plugin.rb
120
83
  add_admin_route 'my_plugin.title', 'my-plugin'
121
84
 
122
- # Wire the route
123
85
  Discourse::Application.routes.append do
124
- get '/admin/plugins/my-plugin' => 'admin/my_plugin#index',
125
- constraints: StaffConstraint.new
126
- post '/admin/plugins/my-plugin/action' => 'admin/my_plugin#action',
127
- constraints: StaffConstraint.new
86
+ get '/admin/plugins/my-plugin' => 'admin/my_plugin#index', constraints: StaffConstraint.new
87
+ post '/admin/plugins/my-plugin/action' => 'admin/my_plugin#action', constraints: StaffConstraint.new
128
88
  end
129
89
  ```
130
90
 
@@ -133,21 +93,15 @@ end
133
93
  # frozen_string_literal: true
134
94
 
135
95
  class ::Admin::MyPluginController < ::Admin::AdminController
136
- # Admin::AdminController already requires:
137
- # - User is logged in
138
- # - User is staff (moderator or admin)
139
- # For admin-only actions, add:
96
+ # AdminController enforces: logged_in + staff
97
+ # For admin-only actions:
140
98
  before_action :ensure_admin, only: [:dangerous_action]
141
99
 
142
100
  def index
143
- render json: {
144
- stats: MyPlugin::Stats.summary,
145
- settings: SiteSetting.my_plugin_enabled
146
- }
101
+ render json: { stats: MyPlugin::Stats.summary, settings: SiteSetting.my_plugin_enabled }
147
102
  end
148
103
 
149
104
  def action
150
- # Strong parameters for any mutating action
151
105
  attrs = params.require(:my_plugin).permit(:field_one, :field_two)
152
106
  result = MyPlugin::SomeService.call(attrs.to_h.symbolize_keys)
153
107
 
@@ -160,26 +114,18 @@ class ::Admin::MyPluginController < ::Admin::AdminController
160
114
  end
161
115
  ```
162
116
 
163
- ## Guardian: Authorization Layer
164
-
165
- Guardian is Discourse's authorization system. Always check it for user-facing actions.
117
+ ## Guardian
166
118
 
167
119
  ```ruby
168
- # Check permissions before acting
169
120
  def update_post
170
121
  post = Post.find(params[:id])
171
-
172
- # guardian.can_edit_post? checks ownership, trust level, staff status
173
- unless guardian.can_edit_post?(post)
174
- return render json: failed_json, status: :forbidden
175
- end
122
+ return render json: failed_json, status: :forbidden unless guardian.can_edit_post?(post)
176
123
 
177
124
  post.update!(body: params[:body])
178
125
  render json: PostSerializer.new(post, scope: guardian).as_json
179
126
  end
180
127
 
181
- # Extending Guardian for plugin-specific permissions
182
- # In after_initialize:
128
+ # Extend Guardian in after_initialize:
183
129
  module ::Guardian::MyPluginExtensions
184
130
  def can_use_my_feature?
185
131
  authenticated? && (is_staff? || user.trust_level >= 2)
@@ -189,69 +135,42 @@ end
189
135
  Guardian.prepend(::Guardian::MyPluginExtensions)
190
136
  ```
191
137
 
192
- ## Security Non-Negotiables
138
+ ## Security
193
139
 
194
- ### 1. Never Raw SQL with Interpolation Use ActiveRecord or Named Params
140
+ ### SQL — no interpolation
195
141
 
196
142
  ```ruby
197
- # BAD — SQL injection
143
+ # BAD
198
144
  User.where("username = '#{params[:username]}'")
199
145
  DB.query("SELECT * FROM users WHERE id = #{user_id}")
200
146
 
201
- # GOOD — ActiveRecord parameterization
147
+ # GOOD
202
148
  User.where(username: params[:username])
203
- User.where("created_at > ?", params[:since])
204
-
205
- # GOOD — Discourse DB helper with named bind params (always use this for raw SQL)
206
149
  DB.query("SELECT * FROM users WHERE id = :id", id: user_id.to_i)
207
- DB.query("UPDATE users SET flag = TRUE WHERE id = :id", id: user_id.to_i)
208
- ```
209
-
210
- ### 2. Staff-Only Admin Routes
211
-
212
- ```ruby
213
- # ALWAYS use StaffConstraint on admin routes
214
- get '/admin/plugins/my-plugin' => 'admin/my_plugin#index',
215
- constraints: StaffConstraint.new
216
-
217
- # Admin::AdminController gives you these checks automatically:
218
- # - ensure_logged_in
219
- # - ensure_staff
220
- # For admin-only (not just moderator), add ensure_admin
221
150
  ```
222
151
 
223
- ### 3. No Sensitive Data in Logs
152
+ ### Logs no sensitive values
224
153
 
225
154
  ```ruby
226
- # BAD — logs hash values, tokens, passwords
227
- Rails.logger.warn("[MyPlugin] Processing hash: #{user_hash}")
228
- Rails.logger.info("[MyPlugin] Token: #{token}")
155
+ # BAD
156
+ Rails.logger.warn("[MyPlugin] Hash: #{user_hash}")
229
157
 
230
- # GOOD — log what happened, not the sensitive value
158
+ # GOOD
231
159
  Rails.logger.info("[MyPlugin] Processing user #{user.id}")
232
- Rails.logger.warn("[MyPlugin] Auth failed for user #{user.id}")
233
160
  ```
234
161
 
235
- ### 4. Custom FieldsValidate Types
162
+ ### Custom fieldsregister types
236
163
 
237
164
  ```ruby
238
- # Register field types to prevent type confusion
239
165
  register_user_custom_field_type('my_plugin_id', :integer)
240
- register_post_custom_field_type('source_url', :string)
241
-
242
- # Whitelist custom fields for API/serializer exposure
243
166
  DiscoursePluginRegistry.serialized_current_user_fields << 'my_plugin_id'
244
-
245
- # Access safely — always coerce type
246
167
  user_id = (user.custom_fields['my_plugin_id'].to_i rescue nil)
247
168
  ```
248
169
 
249
- ### 5. Rate Limiting on Sensitive Endpoints
170
+ ### Rate limiting
250
171
 
251
172
  ```ruby
252
- # For endpoints that test credentials or perform expensive operations
253
173
  RateLimiter.new(current_user, "my_plugin_sensitive_action", 5, 1.minute).performed!
254
- # Raises RateLimiter::LimitExceeded if over limit
255
174
  ```
256
175
 
257
176
  ## Site Settings
@@ -261,7 +180,7 @@ RateLimiter.new(current_user, "my_plugin_sensitive_action", 5, 1.minute).perform
261
180
  plugins:
262
181
  my_plugin_enabled:
263
182
  default: false
264
- client: false # server-side only unless UI needs it
183
+ client: false
265
184
  my_plugin_max_items:
266
185
  default: 100
267
186
  min: 1
@@ -270,29 +189,22 @@ plugins:
270
189
  ```
271
190
 
272
191
  ```ruby
273
- # Access in code
274
- SiteSetting.my_plugin_enabled
275
- SiteSetting.my_plugin_max_items
276
-
277
- # Guard features
278
192
  return unless SiteSetting.my_plugin_enabled
193
+ SiteSetting.my_plugin_max_items
279
194
  ```
280
195
 
281
196
  ## Migrations
282
197
 
283
198
  ```ruby
284
- # db/migrate/20250101000000_create_my_plugin_records.rb
285
199
  # frozen_string_literal: true
286
-
287
200
  class CreateMyPluginRecords < ActiveRecord::Migration[7.0]
288
201
  def change
289
202
  create_table :my_plugin_records do |t|
290
203
  t.integer :user_id, null: false
291
- t.string :source_id, null: false
292
- t.text :data
204
+ t.string :source_id, null: false
205
+ t.text :data
293
206
  t.timestamps
294
207
  end
295
-
296
208
  add_index :my_plugin_records, :user_id
297
209
  add_index :my_plugin_records, :source_id, unique: true
298
210
  add_foreign_key :my_plugin_records, :users
@@ -300,20 +212,17 @@ class CreateMyPluginRecords < ActiveRecord::Migration[7.0]
300
212
  end
301
213
  ```
302
214
 
303
- ## Import Script Pattern (Standalone Ruby)
304
-
305
- Import scripts inherit from `ImportScripts::Base` and run outside the Rails request cycle.
215
+ ## Import Script
306
216
 
307
217
  ```ruby
308
218
  # frozen_string_literal: true
309
-
310
219
  require_relative "base"
311
220
 
312
221
  class ImportScripts::MyImport < ImportScripts::Base
313
222
  def initialize
314
223
  super
315
224
  @client = Mysql2::Client.new(
316
- host: ENV.fetch('SOURCE_DB_HOST'),
225
+ host: ENV.fetch('SOURCE_DB_HOST'),
317
226
  username: ENV.fetch('SOURCE_DB_USER'),
318
227
  password: ENV.fetch('SOURCE_DB_PASSWORD'),
319
228
  database: ENV.fetch('SOURCE_DB_NAME')
@@ -329,26 +238,16 @@ class ImportScripts::MyImport < ImportScripts::Base
329
238
  private
330
239
 
331
240
  def import_users
332
- puts "Importing users..."
333
-
334
- # Parameterized query — never interpolate
335
241
  users = @client.query(
336
242
  "SELECT id, email, username, display_name FROM wp_users WHERE user_status = 0",
337
243
  as: :hash
338
244
  )
339
-
340
- create_users(users) do |user|
341
- {
342
- id: user['id'],
343
- email: user['email'],
344
- username: normalize_username(user['username']),
345
- name: user['display_name']
346
- }
245
+ create_users(users) do |u|
246
+ { id: u['id'], email: u['email'], username: normalize_username(u['username']), name: u['display_name'] }
347
247
  end
348
248
  end
349
249
 
350
250
  def normalize_username(raw)
351
- # Pure function — transform only, no side effects
352
251
  raw.to_s.strip.downcase.gsub(/[^a-z0-9_]/, '_').truncate(20)
353
252
  end
354
253
  end
@@ -359,10 +258,8 @@ ImportScripts::MyImport.new.perform
359
258
  ## Testing
360
259
 
361
260
  ```ruby
362
- # spec/plugin_helper.rb — loads Discourse test env
363
261
  require 'rails_helper'
364
262
 
365
- # spec/requests/admin/my_plugin_spec.rb
366
263
  RSpec.describe Admin::MyPluginController do
367
264
  fab!(:admin) { Fabricate(:admin) }
368
265
  fab!(:user) { Fabricate(:user) }
@@ -370,27 +267,24 @@ RSpec.describe Admin::MyPluginController do
370
267
  before { sign_in(admin) }
371
268
 
372
269
  describe "GET #index" do
373
- it "returns success for admin" do
270
+ it "returns 200 for admin" do
374
271
  get "/admin/plugins/my-plugin.json"
375
272
  expect(response.status).to eq(200)
376
273
  end
377
274
  end
378
275
 
379
276
  describe "authorization" do
380
- it "rejects non-staff" do
277
+ it "rejects non-staff with 404" do
381
278
  sign_in(user)
382
279
  get "/admin/plugins/my-plugin.json"
383
- expect(response.status).to eq(404) # Discourse returns 404 for staff routes
280
+ expect(response.status).to eq(404)
384
281
  end
385
282
  end
386
283
  end
387
284
 
388
- # Pure logic specs — no Discourse dependencies
389
285
  RSpec.describe MyPlugin::UserSetup do
390
- describe ".call" do
391
- it "normalizes username" do
392
- expect(described_class.normalize("John Doe!")).to eq("john_doe_")
393
- end
286
+ it "normalizes username" do
287
+ expect(described_class.normalize("John Doe!")).to eq("john_doe_")
394
288
  end
395
289
  end
396
290
  ```
@@ -398,43 +292,34 @@ end
398
292
  ## Security Checklist
399
293
 
400
294
  - [ ] `StaffConstraint.new` on all admin routes
401
- - [ ] Inherited from `Admin::AdminController` for admin endpoints
402
- - [ ] Strong parameters on all mutating endpoints
403
- - [ ] No string interpolation in SQL — use ActiveRecord or `DB.query` with `:named` params
404
- - [ ] `guardian.can_*?` checked before user-facing mutations
405
- - [ ] No sensitive values (tokens, hashes, passwords) in log output
406
- - [ ] Rate limiting on credential-testing or expensive endpoints
407
- - [ ] Custom field types registered (`register_*_custom_field_type`)
408
- - [ ] Site settings used for feature flags, not hardcoded booleans
295
+ - [ ] Inherit `Admin::AdminController` for admin endpoints
296
+ - [ ] Strong params (`require().permit()`) on all mutations
297
+ - [ ] No SQL interpolation — use ActiveRecord or `DB.query` with `:named` params
298
+ - [ ] `guardian.can_*?` before user-facing mutations
299
+ - [ ] No sensitive values in logs
300
+ - [ ] Rate limiting on credential-testing / expensive endpoints
301
+ - [ ] Custom field types registered
302
+ - [ ] Feature flags via SiteSettings, not hardcoded booleans
409
303
 
410
304
  ## Common Pitfalls
411
305
 
412
- | Pitfall | Correct Approach |
413
- |---------|-----------------|
414
- | Monkey-patching Discourse classes | Use `class_eval` / `prepend` in `after_initialize` |
415
- | Direct DB string interpolation | `DB.query("... WHERE id = :id", id: val)` |
416
- | Checking `current_user.staff?` in controller | Inherit `Admin::AdminController` instead |
417
- | Logging `user.password_hash` for debugging | Log `user.id` only |
418
- | `params[:field]` without strong params | Always `params.require().permit()` |
419
- | Hardcoded credentials in plugin.rb | `ENV.fetch('KEY')` or Rails credentials |
420
-
421
- ## When to Load Reference Files
422
-
423
- ### Security Examples
424
- **File**: [`references/security.md`](references/security.md)
425
- **Load when**: Implementing auth hooks, custom Guardian checks, SQL safety in import scripts
426
- **Contains**: Guardian extension patterns, DB.query vs ActiveRecord, rate limiting, log hygiene
306
+ | Pitfall | Fix |
307
+ |---------|-----|
308
+ | Monkey-patching Discourse classes | `class_eval` / `prepend` in `after_initialize` |
309
+ | SQL string interpolation | `DB.query(" WHERE id = :id", id: val)` |
310
+ | `current_user.staff?` check in controller | Inherit `Admin::AdminController` |
311
+ | Logging `user.password_hash` | Log `user.id` only |
312
+ | Raw `params[:field]` | Always `params.require().permit()` |
313
+ | Hardcoded credentials | `ENV.fetch('KEY')` |
427
314
 
428
- ### Admin UI (Ember)
429
- **File**: [`references/admin-ui.md`](references/admin-ui.md)
430
- **Load when**: Building admin panel Ember components
431
- **Contains**: Route setup, Ember component patterns, REST adapter, admin nav
315
+ ## Reference Files
432
316
 
433
- ### Import Scripts
434
- **File**: [`references/import-scripts.md`](references/import-scripts.md)
435
- **Load when**: Writing data migration scripts
436
- **Contains**: ImportScripts::Base lifecycle, batching, lookup maps, resume/idempotency
317
+ | File | Load when |
318
+ |------|-----------|
319
+ | [`references/security.md`](references/security.md) | Auth hooks, Guardian extensions, SQL safety in imports |
320
+ | [`references/admin-ui.md`](references/admin-ui.md) | Admin panel Ember components |
321
+ | [`references/import-scripts.md`](references/import-scripts.md) | Data migration: batching, lookup maps, idempotency |
437
322
 
438
323
  ---
439
324
 
440
- **Evidence Base**: Discourse Developer Docs, Discourse GitHub (discourse-solved, discourse-data-explorer), Discourse CVE history (2024–2026), Rails Security Guide.
325
+ **Evidence Base**: Discourse Developer Docs, discourse-solved, discourse-data-explorer, Discourse CVE history (2024–2026), Rails Security Guide.