Markdown Converter
Agent skill for markdown-converter
- Use `mix precommit` alias when you are done with all changes and fix any pending issues
Sign in to like and favorite skills
This is a web application written using the Phoenix web framework.
mix precommit alias when you are done with all changes and fix any pending issues:req (Req) library for HTTP requests, avoid :httpoison, :tesla, and :httpc. Req is included by default and is the preferred HTTP client for Phoenix apps<Layouts.app flash={@flash} ...> which wraps all inner contentMyAppWeb.Layouts module is aliased in the my_app_web.ex file, so you can use it without needing to alias it againcurrent_scope assign:
current_scope to <Layouts.app>current_scope error by moving your routes to the proper live_session and ensure you pass current_scope as needed<.flash_group> component to the Layouts module. You are forbidden from calling <.flash_group> outside of the layouts.ex modulecore_components.ex imports an <.icon name="hero-x-mark" class="w-5 h-5"/> component for for hero icons. Always use the <.icon> component for icons, never use Heroicons modules or similar<.input> component for form inputs from core_components.ex when available. <.input> is imported and using it will will save steps and prevent errors<.input class="myclass px-2 py-1 rounded-lg">)) class with your own values, no default classes are inherited, so your
custom classes must fully style the inputphx.gen.auth creates multiple router plugs and live_session scopes:
:fetch_current_user that is included in the default browser pipeline:require_authenticated_user that redirects to the log in page when the user is not authenticatedlive_session :current_user scope - For routes that need the current user but don't require authentication, similar to :fetch_current_userlive_session :require_authenticated_user scope - For routes that require authentication, similar to the plug with the same name@current_scope is assigned to the Plug connection and LiveView socketredirect_if_user_is_authenticated that redirects to a default path in case the user is authenticated - useful for a registration page that should only be shown to unauthenticated userslive_session, and pipeline you are placing the route, AND SAY WHYphx.gen.auth assigns the current_scope assign - it does not assign a current_user assign.current_user, always use the current_scope.user assign, never use @current_user in templates or LiveViewslive_session names. A live_session :current_user can only be defined once in the router, so all routes for the live_session :current_user must be grouped in a single blockcurrent_scope errors or the logged in session isn't displaying the right content, always double check the router and ensure you are using the correct plug and live_session as described belowLiveViews that require login should always be placed inside the existing
block:live_session :require_authenticated_user
scope "/", AppWeb do pipe_through [:browser, :require_authenticated_user] live_session :require_authenticated_user, on_mount: [{GameMasterCoreWeb.UserAuth, :require_authenticated}] do # phx.gen.auth generated routes live "/users/settings", UserLive.Settings, :edit live "/users/settings/confirm-email/:token", UserLive.Settings, :confirm_email # our own routes that require logged in user live "/", MyLiveThatRequiresAuth, :index end end
Controller routes must be placed in a scope that sets the
:require_authenticated_user plug:
scope "/", AppWeb do pipe_through [:browser, :require_authenticated_user] get "/", MyControllerThatRequiresAuth, :index end
LiveViews that can work with or without authentication, always use the existing
scope, ie::current_user
scope "/", MyAppWeb do pipe_through [:browser] live_session :current_user, on_mount: [{GameMasterCoreWeb.UserAuth, :mount_current_scope}] do # our own routes that work with or without authentication live "/", PublicLive end end
Controllers automatically have the
current_scope available if they use the :browser pipeline.
Elixir lists do not support index based access via the access syntax
Never do this (invalid):
i = 0 mylist = ["blue", "green"] mylist[i]
Instead, always use
Enum.at, pattern matching, or List for index based list access, ie:
i = 0 mylist = ["blue", "green"] Enum.at(mylist, i)
Elixir variables are immutable, but can be rebound, so for block expressions like
if, case, cond, etc
you must bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, ie:
# INVALID: we are rebinding inside the `if` and the result never gets assigned if connected?(socket) do socket = assign(socket, :val, val) end # VALID: we rebind the result of the `if` to a new variable socket = if connected?(socket) do assign(socket, :val, val) end
Never nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors
Never use map access syntax (
changeset[:field]) on structs as they do not implement the Access behaviour by default. For regular structs, you must access the fields directly, such as my_struct.field or use higher level APIs that are available on the struct if they exist, Ecto.Changeset.get_field/2 for changesets
Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common
Time, Date, DateTime, and Calendar interfaces by accessing their documentation as necessary. Never install additional dependencies unless asked or for date/time parsing (which you can use the date_time_parser package)
Don't use
String.to_atom/1 on user input (memory leak risk)
Predicate function names should not start with
is_ and should end in a question mark. Names like is_thing should be reserved for guards
Elixir's builtin OTP primitives like
DynamicSupervisor and Registry, require names in the child spec, such as {DynamicSupervisor, name: MyApp.MyDynamicSup}, then you can use DynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec)
Use
Task.async_stream(collection, callback, options) for concurrent enumeration with back-pressure. The majority of times you will want to pass timeout: :infinity as option
mix help task_name)mix test test/my_test.exs or run all previously failed tests with mix test --failedmix deps.clean --all is almost never needed. Avoid using it unless you have good reasonRemember Phoenix router
scope blocks include an optional alias which is prefixed for all routes within the scope. Always be mindful of this when creating routes within a scope to avoid duplicate module prefixes.
You never need to create your own
alias for route definitions! The scope provides the alias, ie:
scope "/admin", AppWeb.Admin do pipe_through :browser live "/users", UserLive, :index end
the UserLive route would point to the
AppWeb.Admin.UserLive module
Phoenix.View no longer is needed or included with Phoenix, don't use it
message.user.emailimport Ecto.Query and other supporting modules when you write seeds.exsEcto.Schema fields always use the :string type, even for :text, columns, ie: field :name, :stringEcto.Changeset.validate_number/2 DOES NOT SUPPORT the :allow_nil option. By default, Ecto validations only run if a change for the given field exists and the change value is not nil, so such as option is never neededEcto.Changeset.get_field(changeset, :field) to access changeset fieldsuser_id, must not be listed in cast calls or similar for security purposes. Instead they must be explicitly set when creating the structPhoenix templates always use
~H or .html.heex files (known as HEEx), never use ~E
Always use the imported
Phoenix.Component.form/1 and Phoenix.Component.inputs_for/1 function to build forms. Never use Phoenix.HTML.form_for or Phoenix.HTML.inputs_for as they are outdated
When building forms always use the already imported
Phoenix.Component.to_form/2 (assign(socket, form: to_form(...)) and <.form for={@form} id="msg-form">), then access those forms in the template via @form[:field]
Always add unique DOM IDs to key elements (like forms, buttons, etc) when writing templates, these IDs can later be used in tests (
<.form for={@form} id="product-form">)
For "app wide" template imports, you can import/alias into the
my_app_web.ex's html_helpers block, so they will be available to all LiveViews, LiveComponent's, and all modules that do use MyAppWeb, :html (replace "my_app" by the actual app name)
Elixir supports
if/else but **does NOT support if/else if or if/elsif. Never use else if or elseif in Elixir, always use cond or case for multiple conditionals.
Never do this (invalid):
<%= if condition do %> ... <% else if other_condition %> ... <% end %>
Instead always do this:
<%= cond do %> <% condition -> %> ... <% condition2 -> %> ... <% true -> %> ... <% end %>
HEEx require special tag annotation if you want to insert literal curly's like
{ or }. If you want to show a textual code snippet on the page in a <pre> or <code> block you must annotate the parent tag with phx-no-curly-interpolation:
<code phx-no-curly-interpolation> let obj = {key: "val"} </code>
Within
phx-no-curly-interpolation annotated tags, you can use { and } without escaping them, and dynamic Elixir expressions can still be used with <%= ... %> syntax
HEEx class attrs support lists, but you must always use list
[...] syntax. You can use the class list syntax to conditionally add classes, always do this for multiple class values:
<a class={[ "px-2 text-white", @some_flag && "py-5", if(@other_condition, do: "border-red-500", else: "border-blue-100"), ... ]}>Text</a>
and always wrap
if's inside {...} expressions with parens, like done above (if(@other_condition, do: "...", else: "..."))
and never do this, since it's invalid (note the missing
[ and ]):
<a class={ "px-2 text-white", @some_flag && "py-5" }> ... => Raises compile syntax error on invalid HEEx attr syntax
Never use
<% Enum.each %> or non-for comprehensions for generating template content, instead always use <%= for item <- @collection do %>
HEEx HTML comments use
<%!-- comment --%>. Always use the HEEx HTML comment syntax for template comments (<%!-- comment --%>)
HEEx allows interpolation via
{...} and <%= ... %>, but the <%= %> only works within tag bodies. Always use the {...} syntax for interpolation within tag attributes, and for interpolation of values within tag bodies. Always interpolate block constructs (if, cond, case, for) within tag bodies using <%= ... %>.
Always do this:
<div id={@id}> {@my_assign} <%= if @some_block_condition do %> {@another_assign} <% end %> </div>
and Never do this – the program will terminate with a syntax error:
<%!-- THIS IS INVALID NEVER EVER DO THIS --%> <div id="<%= @invalid_interpolation %>"> {if @invalid_block_construct do} {end} </div>
live_redirect and live_patch functions, instead always use the <.link navigate={href}> and <.link patch={href}> in templates, and push_navigate and push_patch functions LiveViewsAppWeb.WeatherLive, with a Live suffix. When you go to add LiveView routes to the router, the default :browser scope is already aliased with the AppWeb module, so you can just do live "/weather", WeatherLivephx-hook="MyHook" and that js hook manages its own DOM, you must also set the phx-update="ignore" attribute<script> tags in HEEx. Instead always write your scripts and hooks in the assets/js directory and integrate them with the assets/js/app.js fileAlways use LiveView streams for collections for assigning regular lists to avoid memory ballooning and runtime termination with the following operations:
stream(socket, :messages, [new_msg])stream(socket, :messages, [new_msg], reset: true) (e.g. for filtering items)stream(socket, :messages, [new_msg], at: -1)stream_delete(socket, :messages, msg)When using the
stream/3 interfaces in the LiveView, the LiveView template must 1) always set phx-update="stream" on the parent element, with a DOM id on the parent element like id="messages" and 2) consume the @streams.stream_name collection and use the id as the DOM id for each child. For a call like stream(socket, :messages, [new_msg]) in the LiveView, the template would be:
<div id="messages" phx-update="stream"> <div :for={{id, msg} <- @streams.messages} id={id}> {msg.text} </div> </div>
LiveView streams are not enumerable, so you cannot use
Enum.filter/2 or Enum.reject/2 on them. Instead, if you want to filter, prune, or refresh a list of items on the UI, you must refetch the data and re-stream the entire stream collection, passing reset: true:
def handle_event("filter", %{"filter" => filter}, socket) do # re-fetch the messages based on the filter messages = list_messages(filter) {:noreply, socket |> assign(:messages_empty?, messages == []) # reset the stream with the new messages |> stream(:messages, messages, reset: true)} end
LiveView streams do not support counting or empty states. If you need to display a count, you must track it using a separate assign. For empty states, you can use Tailwind classes:
<div id="tasks" phx-update="stream"> <div class="hidden only:block">No tasks yet</div> <div :for={{id, task} <- @stream.tasks} id={id}> {task.name} </div> </div>
The above only works if the empty state is the only HTML block alongside the stream for-comprehension.
Never use the deprecated
phx-update="append" or phx-update="prepend" for collections
Phoenix.LiveViewTest module and LazyHTML (included) for making your assertions
Form tests are driven by
Phoenix.LiveViewTest's render_submit/2 and render_change/2 functions
Come up with a step-by-step test plan that splits major test cases into small, isolated files. You may start with simpler tests that verify content exists, gradually add interaction tests
Always reference the key element IDs you added in the LiveView templates in your tests for
Phoenix.LiveViewTest functions like element/2, has_element/2, selectors, etc
Never tests again raw HTML, always use
element/2, has_element/2, and similar: assert has_element?(view, "#my-form")
Instead of relying on testing text content, which can change, favor testing for the presence of key elements
Focus on testing outcomes rather than implementation details
Be aware that
Phoenix.Component functions like <.form> might produce different HTML than expected. Test against the output HTML structure, not your mental model of what you expect it to be
When facing test failures with element selectors, add debug statements to print the actual HTML, but use
LazyHTML selectors to limit the output, ie:
html = render(view) document = LazyHTML.from_fragment(html) matches = LazyHTML.filter(document, "your-complex-selector") IO.inspect(matches, label: "Matches")
If you want to create a form based on
handle_event params:
def handle_event("submitted", params, socket) do {:noreply, assign(socket, form: to_form(params))} end
When you pass a map to
to_form/1, it assumes said map contains the form params, which are expected to have string keys.
You can also specify a name to nest the params:
def handle_event("submitted", %{"user" => user_params}, socket) do {:noreply, assign(socket, form: to_form(user_params, as: :user))} end
When using changesets, the underlying data, form params, and errors are retrieved from it. The
:as option is automatically computed too. E.g. if you have a user schema:
defmodule MyApp.Users.User do use Ecto.Schema ... end
And then you create a changeset that you pass to
to_form:
%MyApp.Users.User{} |> Ecto.Changeset.change() |> to_form()
Once the form is submitted, the params will be available under
%{"user" => user_params}.
In the template, the form form assign can be passed to the
<.form> function component:
<.form for={@form} id="todo-form" phx-change="validate" phx-submit="save"> <.input field={@form[:field]} type="text" /> </.form>
Always give the form an explicit, unique DOM ID, like
id="todo-form".
Always use a form assigned via
to_form/2 in the LiveView, and the <.input> component in the template. In the template always access forms this:
<%!-- ALWAYS do this (valid) --%> <.form for={@form} id="my-form"> <.input field={@form[:field]} type="text" /> </.form>
And never do this:
<%!-- NEVER do this (invalid) --%> <.form for={@changeset} id="my-form"> <.input field={@changeset[:field]} type="text" /> </.form>
<.form let={f} ...> in the template, instead always use <.form for={@form} ...>, then drive all form references from the form assign as in @form[:field]. The UI should always be driven by a to_form/2 assigned in the LiveView module that is derived from a changesetEfficiently manage all project tasks, status, and documentation using the Backlog.md CLI, ensuring all project metadata remains fully synchronized and up-to-date.
backlog board) and web UI (backlog browser)--plain flag provides clean text output for AI processingbacklog commandsbacklog task 1 --plain) and writing (
backlog task edit 1)backlog/tasks/ as task-<id> - <title>.md filesbacklog task create, backlog task edit, etc.--plain flag for AI-friendly output when viewing/listingALL task operations MUST use the Backlog.md CLI commands
backlog task edit and other CLI commandsbacklog task create to create new tasksbacklog task edit <id> --check-ac <index> to mark acceptance criteriaWhy? Direct file editing breaks metadata synchronization, Git tracking, and task relationships.
backlog/tasks/ (drafts under backlog/drafts/)task-<id> - <title>.md (e.g., task-42 - Add GraphQL resolver.md)backlog/docs/backlog/decisions/--plain flag when listing or viewing tasks for AI-friendly text output# DON'T DO THIS: 1. Open backlog/tasks/task-7 - Feature.md in editor 2. Change "- [ ]" to "- [x]" manually 3. Add notes directly to the file 4. Save the file
# DO THIS INSTEAD: backlog task edit 7 --check-ac 1 # Mark AC #1 as complete backlog task edit 7 --notes "Implementation complete" # Add notes backlog task edit 7 -s "In Progress" -a @agent-k # Multiple commands: change status and assign the task when you start working on the task
⚠️ FORMAT REFERENCE ONLY - The following sections show what you'll SEE in task files. Never edit these directly! Use CLI commands to make changes.
--- id: task-42 title: Add GraphQL resolver status: To Do assignee: [@sara] labels: [backend, api] --- ## Description Brief explanation of the task purpose. ## Acceptance Criteria <!-- AC:BEGIN --> - [ ] #1 First criterion - [x] #2 Second criterion (completed) - [ ] #3 Third criterion <!-- AC:END --> ## Implementation Plan 1. Research approach 2. Implement solution ## Implementation Notes Summary of what was done.
| What You Want to Change | CLI Command to Use |
|---|---|
| Title | |
| Status | |
| Assignee | |
| Labels | |
| Description | |
| Add AC | |
| Check AC #1 | |
| Uncheck AC #2 | |
| Remove AC #3 | |
| Add Plan | |
| Add Notes (replace) | |
| Append Notes | |
Always use CLI to create tasks:
# Example backlog task create "Task title" -d "Description" --ac "First criterion" --ac "Second criterion"
Use a clear brief title that summarizes the task.
Provide a concise summary of the task purpose and its goal. Explains the context without implementation details.
Understanding the Format:
- [ ] #1 Criterion text (unchecked) or - [x] #1 Criterion text (checked)Managing Acceptance Criteria via CLI:
⚠️ IMPORTANT: How AC Commands Work
--ac) accepts multiple flags: --ac "First" --ac "Second" ✅--check-ac 1 --check-ac 2 ✅--check-ac 1 --uncheck-ac 2 --remove-ac 3 ✅# Examples # Add new criteria (MULTIPLE values allowed) backlog task edit 42 --ac "User can login" --ac "Session persists" # Check specific criteria by index (MULTIPLE values supported) backlog task edit 42 --check-ac 1 --check-ac 2 --check-ac 3 # Check multiple ACs # Or check them individually if you prefer: backlog task edit 42 --check-ac 1 # Mark #1 as complete backlog task edit 42 --check-ac 2 # Mark #2 as complete # Mixed operations in single command backlog task edit 42 --check-ac 1 --uncheck-ac 2 --remove-ac 3 # ❌ STILL WRONG - These formats don't work: # backlog task edit 42 --check-ac 1,2,3 # No comma-separated values # backlog task edit 42 --check-ac 1-3 # No ranges # backlog task edit 42 --check 1 # Wrong flag name # Multiple operations of same type backlog task edit 42 --uncheck-ac 1 --uncheck-ac 2 # Uncheck multiple ACs backlog task edit 42 --remove-ac 2 --remove-ac 4 # Remove multiple ACs (processed high-to-low)
Key Principles for Good ACs:
Good Examples:
\\n sequences are not auto‑converted"Bad Example (Implementation Step):
The very first things you must do when you take over a task are:
# Example backlog task edit 42 -s "In Progress" -a @{myself}
Previously created tasks contain the why and the what. Once you are familiar with that part you should think about a
plan on HOW to tackle the task and all its acceptance criteria. This is your Implementation Plan.
First do a quick check to see if all the tools that you are planning to use are available in the environment you are
working in.
When you are ready, write it down in the task so that you can refer to it later.
# Example backlog task edit 42 --plan "1. Research codebase for references\n2Research on internet for similar cases\n3. Implement\n4. Test"
Once you have a plan, you can start implementing the task. This is where you write code, run tests, and make sure everything works as expected. Follow the acceptance criteria one by one and MARK THEM AS COMPLETE as soon as you finish them.
When you are done implementing a tasks you need to prepare a PR description for it. Because you cannot create PRs directly, write the PR as a clean description in the task notes. Append notes progressively during implementation using
--append-notes:
backlog task edit 42 --append-notes "Implemented X" --append-notes "Added tests"
# Example backlog task edit 42 --notes "Implemented using pattern X because Reason Y, modified files Z and W"
IMPORTANT: Do NOT include an Implementation Plan when creating a task. The plan is added only after you start the implementation.
backlog task edit <id> -s "In Progress" -a "...".backlog task edit <id> --plan "...".backlog task edit <id> --notes "..." (replace) or append progressively using --append-notes.IMPORTANT: Only implement what's in the Acceptance Criteria. If you need to do more, either:
backlog task edit 42 --ac "New requirement"backlog task create "Additional feature"# 1. Identify work backlog task list -s "To Do" --plain # 2. Read task details backlog task 42 --plain # 3. Start work: assign yourself & change status backlog task edit 42 -s "In Progress" -a @myself # 4. Add implementation plan backlog task edit 42 --plan "1. Analyze\n2. Refactor\n3. Test" # 5. Work on the task (write code, test, etc.) # 6. Mark acceptance criteria as complete (supports multiple in one command) backlog task edit 42 --check-ac 1 --check-ac 2 --check-ac 3 # Check all at once # Or check them individually if preferred: # backlog task edit 42 --check-ac 1 # backlog task edit 42 --check-ac 2 # backlog task edit 42 --check-ac 3 # 7. Add implementation notes (PR Description) backlog task edit 42 --notes "Refactored using strategy pattern, updated tests" # 8. Mark task as done backlog task edit 42 -s Done
A task is Done only when ALL of the following are complete:
backlog task edit <id> --check-ac <index> for eachbacklog task edit <id> --notes "..."backlog task edit <id> -s Done⚠️ NEVER mark a task as Done without completing ALL items above
| Task | ✅ DO | ❌ DON'T |
|---|---|---|
| View task | | Open and read .md file directly |
| List tasks | | Browse backlog/tasks folder |
| Check status | | Look at file content |
| Task | ✅ DO | ❌ DON'T |
|---|---|---|
| Check AC | | Change to in file |
| Add notes | | Type notes into .md file |
| Change status | | Edit status in frontmatter |
| Add AC | | Add to file |
| Action | Command |
|---|---|
| Create task | |
| With description | |
| With AC | |
| With all options | |
| Create draft | |
| Create subtask | |
| Action | Command |
|---|---|
| Edit title | |
| Edit description | |
| Change status | |
| Assign | |
| Add labels | |
| Set priority | |
| Action | Command |
|---|---|
| Add AC | |
| Remove AC #2 | |
| Remove multiple ACs | |
| Check AC #1 | |
| Check multiple ACs | |
| Uncheck AC #3 | |
| Mixed operations | |
| Action | Command |
|---|---|
| Add plan | |
| Add notes | |
| Add dependencies | |
The CLI preserves input literally. Shells do not convert
\n inside normal quotes. Use one of the following to insert real newlines:
backlog task edit 42 --desc $'Line1\nLine2\n\nFinal'backlog task edit 42 --plan $'1. A\n2. B'backlog task edit 42 --notes $'Done A\nDoing B'backlog task edit 42 --append-notes $'Progress update line 1\nLine 2'backlog task edit 42 --notes "$(printf 'Line1\nLine2')"backlog task edit 42 --notes "Line1nLine2"`Do not expect
"...\n..." to become a newline. That passes the literal backslash + n to the CLI by design.
Descriptions support literal newlines; shell examples may show escaped
\\n, but enter a single \n to create a newline.
| Action | Command |
|---|---|
| View task | |
| List tasks | |
| Filter by status | |
| Filter by assignee | |
| Archive task | |
| Demote to draft | |
| Problem | Solution |
|---|---|
| Task not found | Check task ID with |
| AC won't check | Use correct index: to see AC numbers |
| Changes not saving | Ensure you're using CLI, not editing files |
| Metadata out of sync | Re-edit via CLI to fix: |
🎯 If you want to change ANYTHING in a task, use the
command.
📖 Use CLI to read tasks, exceptionally READ task files directly, never WRITE to them.backlog task edit
Full help available:
backlog --help