<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Weiwen Weng</title>
    <description>The latest articles on DEV Community by Weiwen Weng (@weiwen-weng).</description>
    <link>https://dev.to/weiwen-weng</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4009979%2F76a06806-6611-4222-8751-3fbd9654b83e.jpg</url>
      <title>DEV Community: Weiwen Weng</title>
      <link>https://dev.to/weiwen-weng</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/weiwen-weng"/>
    <language>en</language>
    <item>
      <title>Test Cases as Markdown in Git</title>
      <dc:creator>Weiwen Weng</dc:creator>
      <pubDate>Wed, 21 Oct 2026 12:18:13 +0000</pubDate>
      <link>https://dev.to/weiwen-weng/test-cases-as-markdown-in-git-2e7l</link>
      <guid>https://dev.to/weiwen-weng/test-cases-as-markdown-in-git-2e7l</guid>
      <description>&lt;p&gt;A spreadsheet works until two people edit it. A hosted test tool works until the team is six people and the UI is already in the way. If the work already lives in a Git repository, the manual test cases can live there too.&lt;/p&gt;

&lt;p&gt;This is the layout I use. One Markdown file per case. YAML front matter for the fields you filter and link. A separate folder each time you execute. The files stay readable in any editor, and an agent can write a case because the schema is the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;Solo developers and small teams who already have a repo. You want a procedure a person can run, a history of what changed, and a link from the case back to the ticket.&lt;/p&gt;

&lt;p&gt;Automated tests stay in code. This format is the spec a human follows, and the record of a manual run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The case and the run are different files
&lt;/h2&gt;

&lt;p&gt;The case is the procedure. It should change when the product changes.&lt;/p&gt;

&lt;p&gt;The run is one execution. It changes every time someone marks a case pass or fail.&lt;/p&gt;

&lt;p&gt;Put both in the same file and every test session rewrites the spec. The diff used to mean "the login steps changed." After a few runs it means "someone tested this on Tuesday." Review becomes noise, and a merge between two testers gets worse.&lt;/p&gt;

&lt;p&gt;Split them. In &lt;a href="https://gitoza.com" rel="noopener noreferrer"&gt;Gitoza&lt;/a&gt; the tree looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;What it holds&lt;/th&gt;
&lt;th&gt;When it changes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.gitoza/test/cases/&amp;lt;project&amp;gt;/…/&amp;lt;case_id&amp;gt;.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Title, fields, steps, expected result&lt;/td&gt;
&lt;td&gt;When the procedure changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.gitoza/test/runs/&amp;lt;run_id&amp;gt;/head.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run title, tickets, release&lt;/td&gt;
&lt;td&gt;When the run is created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.gitoza/test/runs/&amp;lt;run_id&amp;gt;/cases.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which cases are in the run&lt;/td&gt;
&lt;td&gt;When membership changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.gitoza/test/runs/&amp;lt;run_id&amp;gt;/results.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pass, fail, skip&lt;/td&gt;
&lt;td&gt;During execution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The case id is the filename. &lt;code&gt;AUTH-001.md&lt;/code&gt; is case &lt;code&gt;AUTH-001&lt;/code&gt;. One case, one file. Creating a case in the app assigns an id like &lt;code&gt;TC-…&lt;/code&gt;. A file you or an agent write can use any id of letters, numbers, underscores, and hyphens, as long as the filename matches.&lt;/p&gt;

&lt;p&gt;Run files are YAML front matter only. A YAML mapping between &lt;code&gt;---&lt;/code&gt; fences, with an empty body. The case file is the one with a real Markdown procedure underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  YAML front matter is the row. The body is the procedure.
&lt;/h2&gt;

&lt;p&gt;Fields you want to filter, assign, or link go between the &lt;code&gt;---&lt;/code&gt; lines. The steps a person follows go below them.&lt;/p&gt;

&lt;p&gt;A file is valid with a title:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify login error message&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is enough for the app to index it. A case you will actually run picks up a few more fields. &lt;code&gt;priority&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, and &lt;code&gt;tags&lt;/code&gt; are for search. &lt;code&gt;requirement_ids&lt;/code&gt; is the list of tickets this case covers. &lt;code&gt;params&lt;/code&gt; holds data that changes by environment, so the steps can stay the same on staging and production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify login error message&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;active&lt;/span&gt;
&lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;auth&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;smoke&lt;/span&gt;
&lt;span class="na"&gt;requirement_ids&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GITO-BUG001&lt;/span&gt;
&lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;staging&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gu"&gt;## Steps&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Open the login page.
&lt;span class="p"&gt;2.&lt;/span&gt; Enter invalid credentials.
&lt;span class="p"&gt;3.&lt;/span&gt; Submit the form.

&lt;span class="gu"&gt;## Expected&lt;/span&gt;

A clear error message is shown. A valid login redirects to the dashboard (see [[GITO-BUG001]]).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;status&lt;/code&gt; is the life of the case: &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;draft&lt;/code&gt;, or &lt;code&gt;deprecated&lt;/code&gt;. Review is a different field, &lt;code&gt;approve_status&lt;/code&gt; (&lt;code&gt;draft&lt;/code&gt;, &lt;code&gt;approved&lt;/code&gt;, &lt;code&gt;rejected&lt;/code&gt;). Using one field for both makes a filter lie about what is ready to run.&lt;/p&gt;

&lt;p&gt;One action per numbered step. In a manual run, that step is the thing on screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traceability is text in the repo
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;requirement_ids&lt;/code&gt; on the case lists the tickets it covers. In the body, &lt;code&gt;[[GITO-BUG001]]&lt;/code&gt; links to that ticket. Open the ticket and this case is listed under &lt;strong&gt;Tested by&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The link is a string in the file. Copy the repository and the link is still there. Close the desktop app and the link is still there.&lt;/p&gt;

&lt;p&gt;A run points the other way. &lt;code&gt;head.md&lt;/code&gt; lists the tickets that run covers, and can name the release it was executed against. On the ticket, those runs show up as &lt;strong&gt;Covered by runs&lt;/strong&gt;. Results stay in the run folder. They are not copied back into the case.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;run_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;R-A1B2C3&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login smoke, staging&lt;/span&gt;
&lt;span class="na"&gt;requirement_ids&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GITO-BUG001&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;results.md&lt;/code&gt; is sparse. Only cases that have been executed appear. Counts are derived when you open the run, so a stale &lt;code&gt;passed: 4&lt;/code&gt; is not something two people have to merge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;run_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;R-A1B2C3&lt;/span&gt;
&lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;.gitoza/test/cases/MyProject/01_auth/AUTH-001.md&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;passed&lt;/span&gt;
    &lt;span class="na"&gt;executed_at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-04-02T09:15:00Z"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app writes more bookkeeping around these keys (dates, review, comments). The split above is the part that keeps history readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review the diff on its own branch
&lt;/h2&gt;

&lt;p&gt;A case change is an ordinary Git diff. A step was added. A tag was removed. A ticket id was linked. That diff is the review.&lt;/p&gt;

&lt;p&gt;These files live on a branch named &lt;code&gt;gitoza&lt;/code&gt;, separate from application code. A pull request for a feature stays about the feature. Test and ticket edits are reviewed on their own branch: you look at a changelist, then confirm, and fetch, rebase, and push happen in the background. If two people edited the same item, sync pauses on &lt;strong&gt;Keep mine&lt;/strong&gt; or &lt;strong&gt;Keep shared version&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Git history of the case file is the audit log. You can see who changed the expected result, and you can check out an older copy of the procedure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes the repository painful
&lt;/h2&gt;

&lt;p&gt;Three habits break this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshots committed as binaries.&lt;/strong&gt; A PNG in the case history makes every clone heavier, and the diff stops being text. Refer to the image from the Markdown and keep the bytes in asset storage. The commit stays a documentation change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two people rewriting the same case.&lt;/strong&gt; A Markdown procedure merges badly when both sides rewrite the steps. Smaller files limit the damage: one case per file, membership in &lt;code&gt;cases.md&lt;/code&gt;, outcomes in &lt;code&gt;results.md&lt;/code&gt;. When an overlap does happen, stop and pick a version. A silent merge in the middle of a YAML block is how a case disappears from search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pasting automation output into the case.&lt;/strong&gt; A JUnit file is a result from a build, in the same family as &lt;code&gt;results.md&lt;/code&gt;. Upload the XML to the place your CI results already go, and read it there. On the case, &lt;code&gt;automated: true&lt;/code&gt; is enough to say code already covers the procedure. The stack trace from last night's job belongs with the build, beside the case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let an agent write the file
&lt;/h2&gt;

&lt;p&gt;Give the agent the path and the headings. It does not need a plugin.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create &lt;code&gt;.gitoza/test/cases/&amp;lt;project&amp;gt;/&amp;lt;folder&amp;gt;/&amp;lt;case_id&amp;gt;.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The filename is the case id. Letters, numbers, underscores, and hyphens only.&lt;/li&gt;
&lt;li&gt;Start the YAML front matter with &lt;code&gt;title:&lt;/code&gt;. Add &lt;code&gt;priority&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;, and &lt;code&gt;requirement_ids&lt;/code&gt; when you know them.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;## Steps&lt;/code&gt; and &lt;code&gt;## Expected&lt;/code&gt;. One action per numbered step.&lt;/li&gt;
&lt;li&gt;Put the ticket id in &lt;code&gt;requirement_ids&lt;/code&gt;. If the sentence mentions the ticket, write &lt;code&gt;[[TICKET-ID]]&lt;/code&gt; too.&lt;/li&gt;
&lt;li&gt;Leave pass, fail, and skip out of the case file. Those belong in a run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Refresh the index after the file is on disk. Search and filters read the YAML front matter. The body renders as a page. After that, edit in the UI. The raw file is there when you want a diff or an agent. Day to day you can ignore it.&lt;/p&gt;

&lt;p&gt;A title alone is a valid case. Fill the other fields when the case is real.&lt;/p&gt;




&lt;p&gt;I use this layout in Gitoza, a local desktop app for tickets, a wiki, and tests stored as Markdown in Git. Sync commits to the &lt;code&gt;gitoza&lt;/code&gt; branch after you review the changelist. There is no account.&lt;/p&gt;

&lt;p&gt;Source (Apache-2.0): &lt;a href="https://github.com/gitoza-io/gitoza" rel="noopener noreferrer"&gt;github.com/gitoza-io/gitoza&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>git</category>
      <category>markdown</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why Your AI IDE Wants Task Tickets as Markdown in the Repo</title>
      <dc:creator>Weiwen Weng</dc:creator>
      <pubDate>Thu, 24 Sep 2026 13:03:30 +0000</pubDate>
      <link>https://dev.to/weiwen-weng/why-your-ai-ide-wants-task-tickets-as-yaml-in-the-repo-mgg</link>
      <guid>https://dev.to/weiwen-weng/why-your-ai-ide-wants-task-tickets-as-yaml-in-the-repo-mgg</guid>
      <description>&lt;p&gt;&lt;em&gt;Update:&lt;/em&gt; Same idea as before — structured fields + a readable body in the repo. The files are now &lt;code&gt;.md&lt;/code&gt; (YAML front matter + Markdown body) instead of &lt;code&gt;.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is my actual workday before I changed anything: write code in the IDE, alt-tab to the board to check what the ticket said, alt-tab back, forget half of it, alt-tab again. And when I finally got an AI agent doing real work for me, it could see all my code and none of my tasks. I was the API between the two.&lt;/p&gt;

&lt;p&gt;MCP into Jira doesn't really fix this. It gives the agent a way to &lt;em&gt;query&lt;/em&gt; the board, but that's another server, another auth flow, more schema in the context window — and the ticket still isn't in the pull request. The board updates later, by hand, if anyone remembers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do instead
&lt;/h2&gt;

&lt;p&gt;Tickets are just files in the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.gitoza/tasks/tickets/{project}/{id}.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you actually need on top of that is a UI that renders the file and lets you edit it. Nobody wants to manage tasks by poking at raw markup all day.&lt;/p&gt;

&lt;p&gt;Front matter for the machine, Markdown for the human:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lockout after failed logins&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bug&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;open&lt;/span&gt;
&lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;auth&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gu"&gt;## Description&lt;/span&gt;

Brief context for the agent and the reviewer.

&lt;span class="gu"&gt;## Acceptance criteria&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what that looks like in Gitoza:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/GSL3KB42cw0" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Two things fall out of this format, and both matter more than I expected.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Markdown body&lt;/strong&gt; means it renders. Description, acceptance criteria, checklists — you get a readable ticket in any preview pane, no special tooling required.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;front matter&lt;/strong&gt; means state is structured. &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;priority&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt; are real fields, not prose, so you can filter and roll them up without parsing English.&lt;/p&gt;

&lt;p&gt;And for the agent it's just a file it already indexes. No tool call, no JQL, no "fetch issue then map fields." I can say &lt;em&gt;"AUTH-102 is done, update it"&lt;/em&gt; or &lt;em&gt;"draft a ticket for this bug"&lt;/em&gt; and it edits the ticket the same way it edits code. Way fewer steps, way fewer things to get wrong — a wrong field value shows up in the diff instead of silently landing in someone's SaaS board.&lt;/p&gt;

&lt;p&gt;The other half of it: the ticket ships in the same PR as the change. Reviewers see scope and acceptance criteria next to the implementation, and after merge the file in the repo is the truth. No row someone forgot to flip.&lt;/p&gt;

&lt;h2&gt;
  
  
  I run the tool on itself
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://gitoza.com" rel="noopener noreferrer"&gt;Gitoza&lt;/a&gt;, an open-source desktop app for exactly this (tickets, wiki, test cases, and runs as Markdown in Git), and I plan Gitoza with it. Every feature, every bug, every release is a ticket file in the project's own repo. Releases get planned, scoped, and closed without me opening a browser tab.&lt;/p&gt;

&lt;p&gt;That's really the whole pitch. I stopped context-switching, and the agent finally got to see both halves of the job.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://gitoza.com" rel="noopener noreferrer"&gt;gitoza.com&lt;/a&gt; · &lt;a href="https://github.com/gitoza-io/gitoza" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cursor</category>
      <category>git</category>
      <category>markdown</category>
    </item>
    <item>
      <title>Skip MCP for Jira: Tickets as Markdown in Git</title>
      <dc:creator>Weiwen Weng</dc:creator>
      <pubDate>Fri, 11 Sep 2026 08:52:22 +0000</pubDate>
      <link>https://dev.to/weiwen-weng/stop-configuring-mcp-for-jira-the-git-native-alternative-cik</link>
      <guid>https://dev.to/weiwen-weng/stop-configuring-mcp-for-jira-the-git-native-alternative-cik</guid>
      <description>&lt;p&gt;The usual way to give Cursor a Jira or Linear ticket is middleware: an MCP server, a token, a tool schema. It works until the MCP process dies, the token rotates, and the agent spends a turn explaining it cannot list issues. You are back in the browser. That is a lot of moving parts for "read this task."&lt;/p&gt;

&lt;p&gt;The agent already edits your &lt;code&gt;.ts&lt;/code&gt; and &lt;code&gt;.rs&lt;/code&gt; files. Tickets do not need a different transport. Put them on disk, in Git, next to the code.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  agent[Cursor Agent]
  md[Ticket and wiki Markdown]
  git[Git]
  sqlite[(SQLite cache)]
  ui[Gitoza UI]

  agent --&amp;gt;|read and write| md
  md --&amp;gt; git
  md --&amp;gt;|incremental index| sqlite
  sqlite --&amp;gt;|lists and search| ui
  md --&amp;gt;|open detail| ui&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Same folder for the agent and the UI. Git keeps history. SQLite is only a cache so lists stay fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote tickets break the loop the agent is good at
&lt;/h2&gt;

&lt;p&gt;Read a file, change a file, show a diff. That is the loop. A remote ticket is none of those things.&lt;/p&gt;

&lt;p&gt;The first time I wired Cursor to Jira through MCP it lasted about a day. Then the token rotated, the container needed a restart, and the chat window had no tools. Firewall rules and &lt;code&gt;mcp.json&lt;/code&gt; drift are not "project context." They are ops for a read query.&lt;/p&gt;

&lt;p&gt;The token is usually wider than the repo, too. Scoping every Jira project is tedious, so people ship &lt;code&gt;read:jira-work&lt;/code&gt; and call it done. That ACL does not show up in the PR.&lt;/p&gt;

&lt;p&gt;And when the agent updates a remote issue, the write is live. No staging area. If it hallucinates a status flip across ten tickets, there is no &lt;code&gt;git reset&lt;/code&gt;. You get whatever the API already committed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tickets as Markdown in the repo
&lt;/h2&gt;

&lt;p&gt;If the agent can patch &lt;code&gt;src/lib.rs&lt;/code&gt;, it should be able to patch the ticket that says why. In Gitoza the tree looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.gitoza/
  tasks/tickets/
    Gitoza_handbook/
      .project.md            # ticket_prefix: GITO
      GITO-AIDEMO.md         # one file = one ticket
      GITO-WIKPDF.md
      releases/
        1.2.0.md
  wiki/
    01-overview/
      W-HBK000.md
    03-tickets/
      W-HBK010.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One project folder, one file per ticket, and the filename stem is the ID (&lt;code&gt;GITO-AIDEMO&lt;/code&gt;). There are no nested hierarchies—just a flat, predictable tree that both grep and an LLM context window can parse in milliseconds.&lt;/p&gt;

&lt;p&gt;The schema is small on purpose. Identity lives in the filename, so you can omit &lt;code&gt;ticket_id&lt;/code&gt; from the front matter. Extra keys are optional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Demo:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Document&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;native&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lifecycle&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;release"&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;task&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;open&lt;/span&gt;
&lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;medium&lt;/span&gt;
&lt;span class="na"&gt;reporter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cursor Agent&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;native&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;demo&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;qa&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

We need a full flowchart of how native tickets move from open to release.

&lt;span class="gu"&gt;## Problem&lt;/span&gt;
Remote PM tools give agents no staging area and no rollback.

&lt;span class="gu"&gt;## Acceptance criteria&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Mermaid lifecycle diagram in this body
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Linked from [[wiki:W-HBK010|Tickets overview]]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the document Gitoza renders and the document Cursor opens. Wiki pages under &lt;code&gt;.gitoza/wiki/&lt;/code&gt; use the same shape: title, optional tags, Markdown body. No second format to teach the model.&lt;/p&gt;

&lt;p&gt;Give it one example file and it copies the neighbor. It is not guessing a Jira JSON blob through a tool schema.&lt;/p&gt;

&lt;p&gt;A prompt that actually works — no MCP, no API key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Search .gitoza/tasks/tickets/Gitoza_handbook/ for any ticket about
exporting wiki pages to PDF.

If none exists, CREATE:
  .gitoza/tasks/tickets/Gitoza_handbook/GITO-WIKPDF.md

Frontmatter: title, type: task, status: open, priority: medium,
tags: [handbook, wiki], reporter: Cursor Agent.

Body: problem statement, 3-bullet AC, and [[wiki:W-HBK010|Tickets overview]].

If a match exists, report its id. Do not duplicate. No MCP.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent writes a file. Git sees an untracked path. Done.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQLite is a cache, not the database
&lt;/h2&gt;

&lt;p&gt;Markdown on disk is the source of truth. SQLite is there so the UI can virtualize thousands of rows without walking the tree on every keystroke.&lt;/p&gt;

&lt;p&gt;When Cursor (or you) drops a new ticket, we do not rebuild the world. &lt;code&gt;git status --porcelain&lt;/code&gt;, keep files under &lt;code&gt;.gitoza/&lt;/code&gt;, upsert those rows. Past 200 changed files we just reindex; that is the escape hatch, not the common path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// rust-backend/src/lib.rs&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;INCREMENTAL_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;changed_tickets&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;INCREMENTAL_THRESHOLD&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tickets&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;reindex_tickets_for_repo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;tickets&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;apply_incremental_ticket_changes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;changed_tickets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ticket_audit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per file: parse, upsert, or delete the row if the file is gone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// rust-backend/src/tickets.rs&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;apply_incremental_ticket_changes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AuditStampSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;is_ticket_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;abs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;abs&lt;/span&gt;&lt;span class="nf"&gt;.is_file&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read_ticket_md&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;validate_ticket_file_path_for_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.is_err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nf"&gt;upsert_ticket_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;detail_to_upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="nf"&gt;.execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"DELETE FROM tickets WHERE repo_slug = ? AND file_path = ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nd"&gt;params!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lists hit SQLite. Opening a ticket re-reads the Markdown. &lt;code&gt;updated_at&lt;/code&gt; / &lt;code&gt;updated_by&lt;/code&gt; live in the index, not in the file, so agents do not invent timestamps. If the cache is stale, delete the rows and scan disk again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video: local agent, no MCP
&lt;/h2&gt;

&lt;p&gt;Copy the workspace path from Gitoza, open that folder in Cursor, ask it to find / create / edit tickets. Gitoza picks up the files when they hit disk.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/FreR5Gy2ilo" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=FreR5Gy2ilo" rel="noopener noreferrer"&gt;Watch the demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Handbook used in the recording: &lt;a href="https://github.com/gitoza-io/gitoza-handbook" rel="noopener noreferrer"&gt;github.com/gitoza-io/gitoza-handbook&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Collaboration: one branch, linear rebase
&lt;/h2&gt;

&lt;p&gt;The usual objection is merge-conflict hell in &lt;code&gt;.gitoza/tasks/&lt;/code&gt;. That happens if you copy GitHub Flow onto tickets: three branches, three people, one &lt;code&gt;GITO-1.md&lt;/code&gt;. We don't.&lt;/p&gt;

&lt;p&gt;One work branch, named &lt;code&gt;gitoza&lt;/code&gt;. Publish is fetch → add → commit → rebase onto &lt;code&gt;origin/gitoza&lt;/code&gt; → push.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
  fetch[fetch origin]
  add[add Markdown]
  commit[commit on gitoza]
  rebase[rebase onto origin/gitoza]
  conflict{Same ticket edited?}
  push[push]
  pick[Resolve in UI]
  history[Linear history]

  fetch --&amp;gt; add --&amp;gt; commit --&amp;gt; rebase --&amp;gt; conflict
  conflict --&amp;gt;|no| push
  conflict --&amp;gt;|yes| pick --&amp;gt; push
  push --&amp;gt; history&lt;/code&gt;&lt;/pre&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// crates/sync-engine/src/publish.rs&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;rebase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;git_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.repo_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"rebase"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-Xfind-renames=100%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;origin_ref&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Different tickets rebase cleanly. The same ticket is a real conflict, and the UI makes you pick a side. That is rarer than people expect, because each ticket is a small file.&lt;/p&gt;

&lt;p&gt;If the agent wrecks a batch of tickets, you already know the fix. In the app you can restore / discard local changes the same way you would in VS Code — or drop to Git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;--&lt;/span&gt; .gitoza/tasks/tickets/
&lt;span class="c"&gt;# committed locally, not pushed:&lt;/span&gt;
git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; HEAD~1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nobody has to pick a feature branch. They see a linear timeline on &lt;code&gt;gitoza&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because tickets live locally, the whole backlog works offline on a train or behind a strict enterprise firewall—zero outbound webhooks required.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://github.com/gitoza-io/gitoza" rel="noopener noreferrer"&gt;github.com/gitoza-io/gitoza&lt;/a&gt;&lt;/p&gt;

</description>
      <category>git</category>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>Link Tickets to Tests and Wiki in One Git Repo</title>
      <dc:creator>Weiwen Weng</dc:creator>
      <pubDate>Sun, 30 Aug 2026 17:47:59 +0000</pubDate>
      <link>https://dev.to/weiwen-weng/link-tickets-to-tests-and-wiki-in-one-git-repo-4l6m</link>
      <guid>https://dev.to/weiwen-weng/link-tickets-to-tests-and-wiki-in-one-git-repo-4l6m</guid>
      <description>&lt;p&gt;Most teams don't have a traceability problem. They have a glue problem.&lt;/p&gt;

&lt;p&gt;The ticket is in Jira. The spec is in Confluence. The cases are in TestRail or Xray. Last week's run is a spreadsheet someone exported. Every "link" is a URL into another product, another login, another spinner. It looks fine in a slide deck. Day to day it rots: the wiki still points at a ticket that got closed and reopened under a new key; the TMS still lists cases for a story that never shipped.&lt;/p&gt;

&lt;p&gt;There's a simpler pattern: &lt;strong&gt;keep the whole graph as files in one Git repo&lt;/strong&gt;. Tickets, wiki pages, test cases, and runs are just YAML on disk. Following a link means opening a file—not waiting on three APIs to agree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four kinds of files, one tree
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.gitoza/
  tasks/tickets/{project}/{ticket-id}.yaml
  wiki/{folders}/W-xxxxxx.yaml
  test/cases/{project}/…/{case-id}.yaml
  test/runs/R-XXXXXX/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The id is the filename. Tickets look like &lt;code&gt;AUTH-102.yaml&lt;/code&gt;. Wiki pages use &lt;code&gt;W-…&lt;/code&gt; stems. Runs use readable &lt;code&gt;R-…&lt;/code&gt; folder names. Requirement ids on tickets, cases, and runs are the same kind of string you'd put in a TMS field—except they live next to the body, in Git.&lt;/p&gt;

&lt;p&gt;Releases can sit beside tickets if you want them. Optional. You don't need a fourth SaaS for the graph to exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the links actually work
&lt;/h2&gt;

&lt;p&gt;You don't paste Jira URLs into Confluence and hope.&lt;/p&gt;

&lt;p&gt;In any Markdown body—ticket, wiki, case, or run—you insert a link that searches tickets and wiki pages and writes something like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[[AUTH-102]]&lt;/code&gt; — opens the ticket&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[[wiki:W-ABC123]]&lt;/code&gt; — opens the wiki page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bare &lt;code&gt;[[…]]&lt;/code&gt; is a ticket. Prefix &lt;code&gt;wiki:&lt;/code&gt; for a page. Unresolved targets get an amber dotted underline, so you see the hole before you sync.&lt;/p&gt;

&lt;p&gt;On the ticket itself, related lists come from the same ids: children, cases that test it, runs that cover it. Click a wiki link in a case or run and the page opens. No integration job. No "issue content" macro.&lt;/p&gt;

&lt;p&gt;A wiki spec can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Lockout after failed logins&lt;/span&gt;

Story: [[AUTH-102]]

See also [[wiki:W-ABC123|Auth overview]].
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A case carries a &lt;code&gt;requirement_id&lt;/code&gt; (tickets and runs can too), so coverage is a field you can search—not a comment someone forgot to update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the click feels instant
&lt;/h2&gt;

&lt;p&gt;SaaS linking is a network sport. Open a story → wait for Confluence → wait for Xray → the related-issues panel flashes empty, then fills, then jumps.&lt;/p&gt;

&lt;p&gt;When the YAML is already on disk, following a link is a local lookup. Search and related lists hit an index derived from the files—not a vendor round-trip. Offline still works. Ticket → wiki → run feels like switching files in an IDE, because that's basically what it is.&lt;/p&gt;

&lt;p&gt;You still sync when you want the team to see changes. Browsing doesn't wait for that push.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traceability without a second product
&lt;/h2&gt;

&lt;p&gt;Vendor TMS tools win on links: story → cases → execution. File-based setups need the same discipline—just as fields and filenames, not as glue.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;From&lt;/th&gt;
&lt;th&gt;To&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wiki spec&lt;/td&gt;
&lt;td&gt;Ticket&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;[[TICKET-ID]]&lt;/code&gt; in the body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ticket&lt;/td&gt;
&lt;td&gt;Wiki&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[[wiki:W-…]]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ticket&lt;/td&gt;
&lt;td&gt;Cases / runs&lt;/td&gt;
&lt;td&gt;requirement ids&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Case or run&lt;/td&gt;
&lt;td&gt;Wiki&lt;/td&gt;
&lt;td&gt;wiki link in the body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Case&lt;/td&gt;
&lt;td&gt;Automation&lt;/td&gt;
&lt;td&gt;filename id tagged on Playwright / Robot&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You don't get magic bi-directional sync with Jira out of a text file. You get a graph you can &lt;code&gt;grep&lt;/code&gt;, review in a PR, and open with no account. People who push back on "tests in Git" usually aren't wrong about the missing piece: without stable ids and a UI that follows them, it feels incomplete next to Xray. The ids were never the hard part. Putting tickets and wiki in the same tree as the cases is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent can walk the same graph
&lt;/h2&gt;

&lt;p&gt;If the spec, the ticket, and the run are files, an AI IDE can already read them. Useful asks look more like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Which open tickets in auth have no cases?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Does this run cover AUTH-102?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Which wiki page describes the lockout behavior?&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No MCP setup per product. The graph is already in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built for this
&lt;/h2&gt;

&lt;p&gt;I built a desktop app (Gitoza) around this layout: tickets, wiki, cases, and runs in one workspace, with a link picker and related lists on the ticket. Confirm changes before you push. Connect the product repo or a dedicated planning repo—the format doesn't care.&lt;/p&gt;

&lt;p&gt;Create a ticket (filename is the id), add a wiki page, insert &lt;code&gt;[[that-id]]&lt;/code&gt;, put the same requirement id on a case, drop it in a run, click through. Sync when you're ready.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gitoza.com" rel="noopener noreferrer"&gt;gitoza.com&lt;/a&gt; — free for personal use, no account.&lt;/p&gt;

&lt;p&gt;Traceability that depends on three SaaS products is an integration project. Traceability that is filenames, &lt;code&gt;[[links]]&lt;/code&gt;, and requirement ids in one repo is just files.&lt;/p&gt;

</description>
      <category>git</category>
      <category>yaml</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Task Tickets as Markdown in Git — Ask If It's Implemented</title>
      <dc:creator>Weiwen Weng</dc:creator>
      <pubDate>Fri, 14 Aug 2026 08:33:31 +0000</pubDate>
      <link>https://dev.to/weiwen-weng/task-tickets-as-yaml-in-git-ask-if-its-implemented-2k60</link>
      <guid>https://dev.to/weiwen-weng/task-tickets-as-yaml-in-git-ask-if-its-implemented-2k60</guid>
      <description>&lt;p&gt;SaaS boards and the codebase drift apart. A &lt;strong&gt;task ticket&lt;/strong&gt; stays &lt;strong&gt;open&lt;/strong&gt; after the code shipped. Another is marked &lt;strong&gt;done&lt;/strong&gt; and never landed. Managers follow the board; engineers follow the repo. Nobody has a cheap way to ask: &lt;em&gt;is this actually in the code?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The fix is not a better dashboard. It is &lt;strong&gt;task tickets as Markdown on disk&lt;/strong&gt;—plus wiki Markdown—so an AI IDE can read them like any other file. Gitoza is the local-first UI for that layout. Files live in Git so the team can sync: the &lt;strong&gt;product repo&lt;/strong&gt;, or a &lt;strong&gt;dedicated Git repo&lt;/strong&gt; for planning. What matters is local files, not a vendor database.&lt;/p&gt;

&lt;h2&gt;
  
  
  When task tickets drift from the codebase
&lt;/h2&gt;

&lt;p&gt;Cloud trackers live in a different universe from the branch you just merged. That is &lt;strong&gt;ticket–code drift&lt;/strong&gt;: the board and Git no longer describe the same work.&lt;/p&gt;

&lt;p&gt;Typical mess:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Implementation is in &lt;code&gt;main&lt;/code&gt;. The ticket is still &lt;strong&gt;open&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The ticket is &lt;strong&gt;done&lt;/strong&gt;. The function was never written—or it was reverted.&lt;/li&gt;
&lt;li&gt;Two people file the same bug two weeks apart. The board grows; the work doubles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You cannot ask Jira “does this match the repo?” without glue, exports, and hope. The agent in your IDE already sees the code. It cannot see a SaaS board unless you copy-paste.&lt;/p&gt;

&lt;p&gt;When &lt;strong&gt;Markdown task tickets&lt;/strong&gt; sit &lt;strong&gt;on disk&lt;/strong&gt;, the agent can open them. Give it the ticket folder and the code folder—same clone or two checkouts—and the question becomes ordinary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdown task tickets and a Git wiki on disk
&lt;/h2&gt;

&lt;p&gt;Gitoza stores &lt;strong&gt;Git-based task management&lt;/strong&gt; as files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.gitoza/tasks/tickets/{project}/{ticket-id}.md
.gitoza/wiki/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;ticket id is the filename&lt;/strong&gt;. YAML front matter holds &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;assignee&lt;/code&gt;, &lt;code&gt;priority&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, tags. The Markdown body holds the description. Wiki pages (sidebar: &lt;strong&gt;Library&lt;/strong&gt;) are nested Markdown under &lt;code&gt;.gitoza/wiki/&lt;/code&gt;—docs, diagrams, the notes that used to live in a Confluence tab nobody opens.&lt;/p&gt;

&lt;p&gt;Those paths live in &lt;strong&gt;a Git repo you connect as a workspace&lt;/strong&gt;. Many teams keep task tickets beside the product. Others use a dedicated planning repo so tickets are not mixed into application history. Either way the format is the same: &lt;strong&gt;Markdown with front matter on disk&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Browse and edit in the desktop app: tree + detail, same pattern as Tickets. Search hits the local index. No spinner waiting on a vendor API. That is why a &lt;strong&gt;local-first ticket tracker&lt;/strong&gt; feels quicker than SaaS—the source of truth is disk, not a round-trip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask an AI IDE if a ticket is already implemented
&lt;/h2&gt;

&lt;p&gt;Point an AI IDE (Cursor, VS Code with an agent, or similar) at the Markdown task tickets—and at the source tree, even if that is a second folder. Then ask what a Kanban column cannot answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Is AUTH-102 implemented in this branch?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;This ticket is still open—did the code already land?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;This ticket is done—where is the change?&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those three close the &lt;strong&gt;status ↔ code&lt;/strong&gt; gap. You are not trusting the board. You are checking the files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check if a bug ticket already exists
&lt;/h2&gt;

&lt;p&gt;The same files stop duplicate work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Is there already a bug ticket about lockout after failed logins?&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is yes, you do not file another ticket, and you do not implement the same fix twice. That is how &lt;strong&gt;Markdown tickets in Git&lt;/strong&gt; keep the backlog clean—search on disk, not another Jira query.&lt;/p&gt;

&lt;p&gt;You only attach the ticket files you care about. Retrieval is &lt;code&gt;grep&lt;/code&gt; and the local index. Native files beat a Jira MCP bridge. The agent does not care whether those files came from the product remote or a dedicated tickets remote.&lt;/p&gt;

&lt;h2&gt;
  
  
  How teams sync task tickets with Git
&lt;/h2&gt;

&lt;p&gt;Local-first does not mean solo-only. You edit on disk, then &lt;strong&gt;Sync&lt;/strong&gt; on the &lt;code&gt;gitoza&lt;/code&gt; branch when you want the team to see it. Git is the bus. That is the part that scares people: &lt;em&gt;Git merge conflicts&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The fear is larger than the rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Git conflicts on Markdown tickets are rarer than they feel
&lt;/h2&gt;

&lt;p&gt;Two people changing &lt;strong&gt;the same task ticket&lt;/strong&gt; is not automatically a conflict. Git merges by line. You conflict when both sides rewrite the &lt;strong&gt;same field&lt;/strong&gt; (or the same stretch of body)—not because you both touched the file.&lt;/p&gt;

&lt;p&gt;Gitoza is shaped so that happens less often:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No competing timestamps in the ticket file.&lt;/strong&gt; Last-updated metadata is not a hardcoded clock stamped into the file on every keystroke. Audit fields are owned by the local index; Git history already has &lt;strong&gt;commit time&lt;/strong&gt;. Two people editing different fields do not fight over “who saved last.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orthogonal edits merge.&lt;/strong&gt; One person adds tags. Another changes &lt;code&gt;status&lt;/code&gt;. A third updates the body. A fourth reassigns. Those are different lines. Git usually just takes both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comments append.&lt;/strong&gt; New comments go on the end. That is an add, not a rewrite of the same paragraph—so two people commenting do not collide the way two people rewriting a description do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When it does collide, there is a UI.&lt;/strong&gt; Resolve Conflicts is &lt;strong&gt;keep mine&lt;/strong&gt;, &lt;strong&gt;keep theirs&lt;/strong&gt;, or mixed per file. You do not need the command line for the routine case.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Conflicts still happen if two people rewrite the same sentence of the body at once. That is the honest case—and it is the same class of edit that would clobber a field in a SaaS form, except here you see both versions.&lt;/p&gt;

&lt;p&gt;The system is stable because the files are small, the schema is boring, and the merge rules match how people actually work: status here, tags there, comments at the bottom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gitoza: local-first task tickets, wiki, and optional tests
&lt;/h2&gt;

&lt;p&gt;Gitoza is &lt;strong&gt;task and ticket management in Git&lt;/strong&gt;: Tickets, Releases, Library (wiki), optional tests under &lt;code&gt;.gitoza/test/&lt;/code&gt;. Connect the product repo or a dedicated planning repo. Confirm Changes before you push. Conflict panels when rebase needs a decision. Your AI IDE reads the same Markdown without MCP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;A board that is not on disk will always drift. &lt;strong&gt;Gitoza keeps task tickets as Markdown&lt;/strong&gt;—and wiki as Markdown—so the AI IDE has something it can actually read. Same repo as the code, or a dedicated Git repo. That is enough to ask whether work is implemented, whether a ticket already exists, and whether the backlog is lying—without waiting on SaaS. Git sync is how the team shares; field-level merges, appended comments, and a keep-mine / keep-theirs UI are why that sync is less intimidating than the word “conflict” suggests.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://github.com/gitoza-io/gitoza" rel="noopener noreferrer"&gt;github.com/gitoza-io/gitoza&lt;/a&gt;&lt;/p&gt;

</description>
      <category>git</category>
      <category>markdown</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why Markdown Tickets Beat Jira MCP for AI IDEs Like Cursor</title>
      <dc:creator>Weiwen Weng</dc:creator>
      <pubDate>Mon, 10 Aug 2026 15:14:29 +0000</pubDate>
      <link>https://dev.to/weiwen-weng/why-yaml-tickets-beat-jira-mcp-for-ai-ides-like-cursor-38c</link>
      <guid>https://dev.to/weiwen-weng/why-yaml-tickets-beat-jira-mcp-for-ai-ides-like-cursor-38c</guid>
      <description>&lt;p&gt;Your AI IDE already reads the repo. If tickets are Markdown with front matter next to the code, the agent can search the backlog the same way it reads a module — &lt;strong&gt;no MCP, no JQL translator, no cloud hop for every ask&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;MCP into Jira is the bolt-on path. At a few thousand tickets — or around &lt;strong&gt;10,000&lt;/strong&gt; — the gap shows up in latency, hit rate, and junk in the context window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Native path: tickets as Markdown
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.gitoza/tasks/tickets/{project}/{ticket-id}.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;ticket id is the filename&lt;/strong&gt; (without &lt;code&gt;.md&lt;/code&gt;). With stable keys (&lt;code&gt;status&lt;/code&gt;, &lt;code&gt;assignee&lt;/code&gt;, &lt;code&gt;priority&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;), an agent can answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Open bugs in auth assigned to me&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;What is still open on this release?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Which tickets mention the checkout API?&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Retrieval is local — IDE index, &lt;code&gt;grep&lt;/code&gt;, or a SQLite-backed app index. You only pay network time for the model, not for fetching every ticket over REST.&lt;/p&gt;

&lt;p&gt;Minimal example (&lt;code&gt;AUTH-102.md&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lockout after failed logins&lt;/span&gt;
&lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;medium&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;open&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bug&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Lockout after failed logins&lt;/span&gt;

&lt;span class="gu"&gt;## Description&lt;/span&gt;

Brief context for the agent and the team.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point the agent at &lt;code&gt;.gitoza/tasks/tickets/&lt;/code&gt; and ask. Same loop as code review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same ask over Jira MCP
&lt;/h2&gt;

&lt;p&gt;Useful when Atlassian must stay the system of record. For day-to-day agent work it adds a chain on every answer: natural language → tool call → JQL → REST → JSON → maybe another call if the first page was wrong.&lt;/p&gt;

&lt;p&gt;Fine for one known issue key. Heavy for wide or multi-step backlog questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Speed, precision, cost
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Markdown in the repo&lt;/th&gt;
&lt;th&gt;Jira over MCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AI access&lt;/td&gt;
&lt;td&gt;Native (files the IDE already sees)&lt;/td&gt;
&lt;td&gt;Tools + network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;Disk / local index&lt;/td&gt;
&lt;td&gt;Network + JQL + paging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Narrow asks&lt;/td&gt;
&lt;td&gt;High when keys are consistent&lt;/td&gt;
&lt;td&gt;Depends on JQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens&lt;/td&gt;
&lt;td&gt;Ticket text you chose&lt;/td&gt;
&lt;td&gt;Schemas + JSON bloat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quotas&lt;/td&gt;
&lt;td&gt;None for local reads&lt;/td&gt;
&lt;td&gt;Cloud / MCP caps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;Needs the cloud&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Precision caveat:&lt;/strong&gt; “summarize all 10k tickets” still will not fit one context window. Filter first, then summarize — locally that is search/&lt;code&gt;grep&lt;/code&gt;, not “hope the JQL was right.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost direction:&lt;/strong&gt; local plain text is cheaper per useful answer than tool-shaped round trips. Many checks never need an LLM — a terminal search is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skip the bridge
&lt;/h2&gt;

&lt;p&gt;If the team already works in an AI IDE, put the backlog where the agent already looks: the repo. Files need no tool schema, no JQL translation, and no second system for “what is open this sprint.”&lt;/p&gt;

&lt;p&gt;MCP is a workaround for a backlog trapped in a vendor API. Markdown tickets remove that trap — Git is the data plane.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://github.com/gitoza-io/gitoza" rel="noopener noreferrer"&gt;github.com/gitoza-io/gitoza&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>git</category>
      <category>markdown</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Manage Tickets in Git as Markdown — No Login, No Jira MCP, Works Offline</title>
      <dc:creator>Weiwen Weng</dc:creator>
      <pubDate>Sat, 08 Aug 2026 07:46:09 +0000</pubDate>
      <link>https://dev.to/weiwen-weng/manage-tickets-in-git-as-yaml-no-login-no-jira-mcp-works-offline-4j1n</link>
      <guid>https://dev.to/weiwen-weng/manage-tickets-in-git-as-yaml-no-login-no-jira-mcp-works-offline-4j1n</guid>
      <description>&lt;p&gt;Driving tickets through MCP into Jira sounds neat until auth, scopes, rate limits, and field mappings eat the day. The IDE still cannot see the backlog unless that glue is perfect.&lt;/p&gt;

&lt;p&gt;There is a simpler pattern — closer to Obsidian than to another SaaS hop: &lt;strong&gt;keep tickets as Markdown files in the repo&lt;/strong&gt;, then link them locally. No login. No vendor API for every lookup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tickets as files
&lt;/h2&gt;

&lt;p&gt;Put tickets under something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.gitoza/tasks/tickets/{project}/{ticket-id}.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Releases under &lt;code&gt;.gitoza/tasks/releases/&lt;/code&gt;. Links are just paths and IDs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ticket ↔ ticket (parent/child, &lt;code&gt;[[TICKET-ID]]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Ticket ↔ release&lt;/li&gt;
&lt;li&gt;Ticket ↔ test case in &lt;code&gt;.gitoza/test/cases/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open the file, follow the link, done. Structured enough for search and agents — not another cloud board.&lt;/p&gt;

&lt;p&gt;YAML front matter holds status, priority, assignees, and tags. The Markdown body holds the description — same shape as wiki pages and test cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this wins
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Offline by default.&lt;/strong&gt; Source of truth is on disk. Searching a release or following a link does not wait on VPN or a flaky API. Sync to a remote when you want to share; editing does not require that round-trip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI already speaks files.&lt;/strong&gt; Agents in Cursor / VS Code read Markdown with front matter well. Ask what is open this sprint, which tickets lack cases, what sits on a release — without standing up MCP to a tracker first. Prefer a local model? Same repo, same folders, no ticket payload leaving your machine for a hosted “AI” bolted onto a SaaS board.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One tree for planning and tests&lt;/strong&gt; (optional):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tickets&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.gitoza/tasks/tickets/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Releases&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.gitoza/tasks/releases/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cases / runs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.gitoza/test/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Tickets as files, Obsidian-style links, local speed, AI without a brittle cloud bridge — and &lt;strong&gt;no login&lt;/strong&gt; to start.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://github.com/gitoza-io/gitoza" rel="noopener noreferrer"&gt;github.com/gitoza-io/gitoza&lt;/a&gt;&lt;/p&gt;

</description>
      <category>git</category>
      <category>productivity</category>
      <category>markdown</category>
      <category>ai</category>
    </item>
    <item>
      <title>Markdown Test Cases in Git: What r/QualityAssurance Actually Said</title>
      <dc:creator>Weiwen Weng</dc:creator>
      <pubDate>Sun, 02 Aug 2026 16:10:08 +0000</pubDate>
      <link>https://dev.to/weiwen-weng/yaml-test-cases-in-git-what-rqualityassurance-actually-said-1kc5</link>
      <guid>https://dev.to/weiwen-weng/yaml-test-cases-in-git-what-rqualityassurance-actually-said-1kc5</guid>
      <description>&lt;p&gt;I asked &lt;a href="https://www.reddit.com/r/QualityAssurance/comments/1v47gei/managing_test_cases_as_yaml_files_inside_git_a/" rel="noopener noreferrer"&gt;r/QualityAssurance&lt;/a&gt; a blunt question: is managing manual test cases as Markdown files inside Git a good idea?&lt;/p&gt;

&lt;p&gt;The thread got lively. People were not hostile to the idea. Many were already sympathetic to &lt;strong&gt;tests as code&lt;/strong&gt;, a dedicated branch, AI helpers that can actually read the catalog. The pushback was more interesting than the praise. It lined up with problems I had hit building the same workflow for real teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the room agreed
&lt;/h2&gt;

&lt;p&gt;When you frame cases as plain text in a repo you control, the reaction is usually positive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No vendor database as the system of record&lt;/li&gt;
&lt;li&gt;Diffs and history that look like application code&lt;/li&gt;
&lt;li&gt;Collaboration through Git instead of another sync product&lt;/li&gt;
&lt;li&gt;AI tools (Cursor, Copilot, and the rest) that already live in the IDE&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That part is not controversial anymore. Engineers have watched automation live in Git for years. Manual cases still often sit in a SaaS form or a spreadsheet. Closing that gap feels overdue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where people who tried it got burned
&lt;/h2&gt;

&lt;p&gt;Agreement on the &lt;em&gt;idea&lt;/em&gt; is not the same as a durable practice.&lt;/p&gt;

&lt;p&gt;Commenters who had lived with &lt;strong&gt;XML dumps, folder trees, or home-grown markdown catalogs&lt;/strong&gt; were blunt: without tooling, the suite becomes hard to browse, easy to break, and painful to keep alive. Raw files scale poorly for day-to-day authoring. Someone always ends up maintaining a fragile convention that nobody else wants to touch.&lt;/p&gt;

&lt;p&gt;Others were clear they would &lt;strong&gt;stay on web tools like Xray&lt;/strong&gt; (and the Jira orbit around them). Not because Markdown is wrong — because managers get dashboards, links, and process they already trust. "Files in Git" sounds like an engineer hobby until it feels like a product.&lt;/p&gt;

&lt;p&gt;And then the &lt;strong&gt;traceability&lt;/strong&gt; question: how do you keep cases tied to user stories and bug tickets when you leave the vendor UI? Without a stable id story, file-based QA looks incomplete next to a TMS that speaks Jira natively.&lt;/p&gt;

&lt;p&gt;Those are fair objections. Ignoring them is how "test cases in Git" dies after a pilot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The missing piece was never Git — it was the UI
&lt;/h2&gt;

&lt;p&gt;Plenty of teams have tried putting cases in the repo. Almost nobody ships a &lt;strong&gt;local UI that renders and manages those files&lt;/strong&gt; the way a TMS renders rows in a database.&lt;/p&gt;

&lt;p&gt;That gap is the whole story.&lt;/p&gt;

&lt;p&gt;Reading Markdown in a text editor is not a test management workflow. Browsing suites, editing steps, recording Pass / Fail / Skip, and seeing automation coverage — that is. Once you have a client that treats the files as the source of truth, the day-to-day experience stops being "weird Git experiment" and starts looking like the web tools people already know. In practice, stability and feature coverage land in the same ballpark. Latency often wins: everything is on disk. There is no round trip to a hosted API every time you open a case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runs that do not rewrite history when cases change
&lt;/h2&gt;

&lt;p&gt;File-based catalogs usually fail at &lt;strong&gt;execution history&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You create a run, add cases, execute. Two weeks later someone edits step 3 on a case that was already in that run. If the run only stores a live path, yesterday's sign-off silently becomes today's wording. Auditors hate that. So do release leads.&lt;/p&gt;

&lt;p&gt;Git already has the answer if you use it deliberately. When Gitoza adds cases to a run, it records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;case path&lt;/strong&gt; in the repository&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;commit SHA&lt;/strong&gt; at the moment the case was picked (&lt;code&gt;picked_at_sha&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Results attach to that membership. Later edits to the live case file do not rewrite what was executed. You can still open the catalog at HEAD for new work; historical runs resolve content against the SHA they were pinned to. Versioning is not a separate "export a PDF" step — it is how the run is stored.&lt;/p&gt;

&lt;p&gt;That is the difference between "we keep Markdown in Git" and "runs are evidence."&lt;/p&gt;

&lt;h2&gt;
  
  
  Traceability without living inside Jira
&lt;/h2&gt;

&lt;p&gt;Vendor TMS tools win on &lt;strong&gt;links&lt;/strong&gt;: story → cases → bugs. File-based setups need the same discipline, just as fields.&lt;/p&gt;

&lt;p&gt;In Gitoza case front matter, that usually means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;requirement_ids&lt;/code&gt; for the user story or ticket keys&lt;/li&gt;
&lt;li&gt;tags for product area / smoke / risk&lt;/li&gt;
&lt;li&gt;optional custom keys under &lt;code&gt;params&lt;/code&gt; (for example &lt;code&gt;jira_key&lt;/code&gt;) when your tracker needs a second handle&lt;/li&gt;
&lt;li&gt;for automation, the case filename as the id, mirrored as a tag on Playwright or Robot specs — see &lt;a href="https://gitoza.com/blog/ai-link-manual-automated-test-cases" rel="noopener noreferrer"&gt;linking manual and automated cases&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not get magic bi-directional sync with Jira out of a text file. You get stable ids you can filter, export, and review in a PR — and you keep the catalog next to the code those stories shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managers and testers need different surfaces — same data
&lt;/h2&gt;

&lt;p&gt;This is the split the Reddit thread kept circling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;QA managers&lt;/strong&gt; lean on Xray, Jira, and similar products because reporting and process live there. That convenience is real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testers who write cases all day&lt;/strong&gt; pay for it in browser forms, slow saves, and UIs that were never designed for long-form authoring. Asking a tester to draft dozens of careful cases in a web TMS is a bit like asking a programmer to write production code in a ticket comment box. Possible. Not the job you want.&lt;/p&gt;

&lt;p&gt;Gitoza's bet is simple: keep the &lt;strong&gt;data plane&lt;/strong&gt; in Git (Markdown cases and runs you own), and give testers an &lt;strong&gt;IDE-shaped client&lt;/strong&gt; — browse, edit, run, sync — while managers still get review, dashboards, and history without renting the catalog to a vendor database.&lt;/p&gt;

&lt;p&gt;If your org will never leave Jira Test Management, stay there. If your pain is authoring speed, ownership, AI access, and audit trails that look like commits, file-based cases with a real UI are no longer a science project.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://github.com/gitoza-io/gitoza" rel="noopener noreferrer"&gt;github.com/gitoza-io/gitoza&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>qa</category>
      <category>git</category>
      <category>markdown</category>
    </item>
    <item>
      <title>Link Manual Test Cases to Playwright and Robot Specs</title>
      <dc:creator>Weiwen Weng</dc:creator>
      <pubDate>Wed, 22 Jul 2026 15:44:09 +0000</pubDate>
      <link>https://dev.to/weiwen-weng/link-manual-yaml-cases-to-playwright-and-robot-specs-5782</link>
      <guid>https://dev.to/weiwen-weng/link-manual-yaml-cases-to-playwright-and-robot-specs-5782</guid>
      <description>&lt;p&gt;Most teams already keep Playwright or Robot Framework specs next to the product. Manual cases often live somewhere else — a spreadsheet, a cloud TMS, or a wiki page nobody updates after the first release.&lt;/p&gt;

&lt;p&gt;That is why automation coverage is usually a quarterly guess: the catalog and the specs never share an id.&lt;/p&gt;

&lt;p&gt;Put both in the &lt;strong&gt;same Git repo&lt;/strong&gt;, give every manual case a stable id, and linking becomes a text match. Any IDE agent can do that if the cases are files it can already see — no custom AI product inside a test tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  One repo, two layers of the same suite
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Manual cases&lt;/strong&gt; are YAML files under &lt;code&gt;.gitoza-lite/test/cases/&lt;/code&gt; (VS Code / Cursor extension) or &lt;code&gt;.gitoza/test/cases/&lt;/code&gt; (Desktop app). The &lt;strong&gt;filename without &lt;code&gt;.yaml&lt;/code&gt; is the case id&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated tests&lt;/strong&gt; live wherever your team already puts them — &lt;code&gt;tests/&lt;/code&gt;, &lt;code&gt;e2e/&lt;/code&gt;, &lt;code&gt;robot/&lt;/code&gt; — in the same repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traceability&lt;/strong&gt; is a shared id: put that case id on the automation side as a tag (or name), and on the YAML side as &lt;code&gt;automated: true&lt;/code&gt; plus a &lt;code&gt;params&lt;/code&gt; pointer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A PR can change the case, the Playwright spec, and the link in one review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Draft manual cases in the IDE
&lt;/h2&gt;

&lt;p&gt;Open the repo in Cursor or VS Code. Point the agent at a few existing case files so it learns your title style, tags, and step length. Then feed it a user story, a ticket, or a screenshot.&lt;/p&gt;

&lt;p&gt;Ask for several cases at once, not one shallow step. A useful prompt looks like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read &lt;code&gt;.gitoza-lite/test/cases/shopflow/auth/&lt;/code&gt; for format. From ticket SHOP-184, draft three YAML cases (happy path, invalid password, locked account). Filename = case id. Use tags &lt;code&gt;auth&lt;/code&gt; and &lt;code&gt;smoke&lt;/code&gt; where it fits.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Save the files under the suite folder. Then open the &lt;a href="https://marketplace.visualstudio.com/items?itemName=gitoza.gitoza-yaml-test-cases" rel="noopener noreferrer"&gt;Gitoza Lite&lt;/a&gt; Test Repository tab to browse, edit, and — when you are ready — run them as a manual suite with Pass / Fail / Skip.&lt;/p&gt;

&lt;p&gt;The extension does not ship its own model. It keeps cases as plain YAML so whatever assistant you already use can read and write them.&lt;/p&gt;

&lt;p&gt;A minimal case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login with valid credentials&lt;/span&gt;
&lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;smoke&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;auth&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;active&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="c1"&gt;## Steps&lt;/span&gt;
&lt;span class="s"&gt;1. Open the login page&lt;/span&gt;
&lt;span class="s"&gt;2. Enter valid credentials&lt;/span&gt;

&lt;span class="c1"&gt;## Expected result&lt;/span&gt;
&lt;span class="s"&gt;User is redirected to the dashboard.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the file is &lt;code&gt;AUTH-001.yaml&lt;/code&gt;, the case id is &lt;code&gt;AUTH-001&lt;/code&gt;. That string is the join key for everything below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Put the case id on the automation side
&lt;/h2&gt;

&lt;p&gt;Before coverage can be marked, automation needs a handle that matches the YAML filename.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Playwright&lt;/strong&gt; — tags must start with &lt;code&gt;@&lt;/code&gt; (or include the id in the test title):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;login with valid credentials&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@AUTH-001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@smoke&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Robot Framework&lt;/strong&gt; — use &lt;code&gt;[Tags]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight robot_framework"&gt;&lt;code&gt;&lt;span class="gh"&gt;*** Test Cases ***&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;Login With Valid Credentials&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;Tags&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;AUTH-001&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;smoke&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nf"&gt;Open Login Page&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nf"&gt;Submit Valid Credentials&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nf"&gt;Dashboard Should Be Visible&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same idea for other runners: one stable token that equals the case id. Prefer the id over a free-text title match — titles drift; filenames should not. When matching Playwright tags to YAML, strip the leading &lt;code&gt;@&lt;/code&gt; so &lt;code&gt;@AUTH-001&lt;/code&gt; maps to &lt;code&gt;AUTH-001.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Flip &lt;code&gt;automated&lt;/code&gt; and fill &lt;code&gt;params&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Once specs carry the case id, close the loop on the YAML side — with Cursor or a small script:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Scan &lt;code&gt;tests/&lt;/code&gt; for Playwright tags matching case ids under &lt;code&gt;.gitoza-lite/test/cases/&lt;/code&gt;. For each hit, set &lt;code&gt;automated: true&lt;/code&gt; on the YAML case and add &lt;code&gt;params.playwright&lt;/code&gt; with the relative spec path. If a case id appears in tags but not in YAML, list the gaps. Do not set &lt;code&gt;automated: true&lt;/code&gt; unless the tag and filename match after stripping &lt;code&gt;@&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You end up with something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login with valid credentials&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;smoke&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;auth&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;playwright&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tests/auth/login.spec.ts&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="c1"&gt;## Steps&lt;/span&gt;
&lt;span class="s"&gt;1. Open the login page&lt;/span&gt;
&lt;span class="s"&gt;2. Enter valid credentials&lt;/span&gt;

&lt;span class="c1"&gt;## Expected result&lt;/span&gt;
&lt;span class="s"&gt;User is redirected to the dashboard.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Robot, the same field under &lt;code&gt;params&lt;/code&gt; might be &lt;code&gt;robot: tests/auth/login.robot&lt;/code&gt;. Teams often add a custom key as well — &lt;code&gt;auto_tag: AUTH-001&lt;/code&gt; or &lt;code&gt;suite: 01_user_authentication&lt;/code&gt; — so filters and scripts stay boring. &lt;code&gt;params&lt;/code&gt; is a free key-value map; agree on a small schema in the repo README and stick to it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;automated: true&lt;/code&gt; is the coverage flag. &lt;code&gt;params&lt;/code&gt; is the pointer. Tags on the YAML side stay useful for smoke, priority, and feature filters — they do not have to duplicate the automation path.&lt;/p&gt;

&lt;p&gt;Treat agent output like any other PR: review &lt;code&gt;git diff&lt;/code&gt; before merge. A wrong tag match that flips &lt;code&gt;automated: true&lt;/code&gt; is worse than a missing link — catch it in review, or run the same rules in CI so chat is optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Git beats a TMS link field
&lt;/h2&gt;

&lt;p&gt;In a vendor database, linking is usually a URL paste or a fragile title search. Agents cannot see the catalog unless you build API glue.&lt;/p&gt;

&lt;p&gt;In Git, the agent (or a script) reads cases and specs in one workspace, and &lt;code&gt;git diff&lt;/code&gt; shows exactly which cases flipped to automated. Coverage is "scan tags ↔ case ids," not "update the spreadsheet after the sprint."&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete loop
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Feature lands on a branch.&lt;/li&gt;
&lt;li&gt;Agent drafts or updates manual YAML under &lt;code&gt;.gitoza-lite/test/cases/…/{CASE-ID}.yaml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Automation engineer (or the same agent) implements Playwright / Robot and tags the test with &lt;code&gt;{CASE-ID}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Agent or script sets &lt;code&gt;automated: true&lt;/code&gt; and &lt;code&gt;params&lt;/code&gt; on the matching case.&lt;/li&gt;
&lt;li&gt;Manual testers execute what is still unautomated; Desktop dashboards can show automation rate when you use the full app.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Start with one suite. Do not try to backfill five years of TestRail in a weekend. Ship the next feature with case + spec + link in the same PR.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it (free VS Code extension):&lt;/strong&gt; &lt;a href="https://marketplace.visualstudio.com/items?itemName=gitoza.gitoza-yaml-test-cases" rel="noopener noreferrer"&gt;Gitoza on Marketplace&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Full post:&lt;/strong&gt; &lt;a href="https://gitoza.com/blog/ai-link-manual-automated-test-cases" rel="noopener noreferrer"&gt;gitoza.com/blog/ai-link-manual-automated-test-cases&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>robotframework</category>
    </item>
    <item>
      <title>Stop Using Raw WebDriver in Robot Framework</title>
      <dc:creator>Weiwen Weng</dc:creator>
      <pubDate>Thu, 09 Jul 2026 18:14:55 +0000</pubDate>
      <link>https://dev.to/weiwen-weng/stop-using-raw-webdriver-in-robot-framework-2jbd</link>
      <guid>https://dev.to/weiwen-weng/stop-using-raw-webdriver-in-robot-framework-2jbd</guid>
      <description>&lt;p&gt;A lot of Robot Framework projects still look like plain Selenium scripts with &lt;code&gt;.robot&lt;/code&gt; file extensions.&lt;/p&gt;

&lt;p&gt;Someone imports &lt;code&gt;webdriver&lt;/code&gt;, creates &lt;code&gt;driver = webdriver.Chrome()&lt;/code&gt;, then calls &lt;code&gt;find_element&lt;/code&gt; and &lt;code&gt;send_keys&lt;/code&gt; in Python helpers. Robot Framework runs the suite, but readable keywords, shared libraries, and consistent waits never show up in the tests.&lt;/p&gt;

&lt;p&gt;If you already use &lt;a href="https://robotframework.org/" rel="noopener noreferrer"&gt;Robot Framework&lt;/a&gt; with &lt;a href="https://robotframework.org/SeleniumLibrary/SeleniumLibrary.html" rel="noopener noreferrer"&gt;SeleniumLibrary&lt;/a&gt;, you do not need the raw WebDriver API. SeleniumLibrary gives you high-level keywords. The Page Object Model gives you structure. Together they keep tests short and UI changes localized.&lt;/p&gt;

&lt;p&gt;We published a small MIT template that shows the layout: &lt;a href="https://github.com/gitoza-io/rf-seleniumlibrary-pageobject-template" rel="noopener noreferrer"&gt;rf-seleniumlibrary-pageobject-template&lt;/a&gt;. It targets &lt;a href="https://www.saucedemo.com/" rel="noopener noreferrer"&gt;Sauce Demo&lt;/a&gt; — clone it, run four tests, fork the folder structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks when you mix in raw WebDriver
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user-name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send_keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;standard_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send_keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secret_sauce&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;login-button&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fine for a script. Painful in a growing suite.&lt;/p&gt;

&lt;p&gt;Locators spread across helpers and test files. Waits become &lt;code&gt;time.sleep(2)&lt;/code&gt; in one place and missing in another. You end up maintaining SeleniumLibrary &lt;strong&gt;and&lt;/strong&gt; a parallel WebDriver stack. CI fails on a Tuesday night and you are not sure which path opened the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before and after
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;driver.find_element(...).send_keys(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Login With Valid Credentials    ${VALID_USER}    ${VALID_PASSWORD}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locators in every file&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LoginLocators.USERNAME&lt;/code&gt; in one module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ad-hoc sleeps&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;wait_until_element_is_visible&lt;/code&gt; in &lt;code&gt;BasePage.click()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two browser stacks&lt;/td&gt;
&lt;td&gt;One &lt;code&gt;SeleniumLibrary&lt;/code&gt; instance per suite&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Four layers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Locators&lt;/td&gt;
&lt;td&gt;Selectors per screen&lt;/td&gt;
&lt;td&gt;&lt;code&gt;login_locators.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BasePage&lt;/td&gt;
&lt;td&gt;Shared waits and actions&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;click()&lt;/code&gt;, &lt;code&gt;enter_text()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page library&lt;/td&gt;
&lt;td&gt;Screen keywords&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LoginPage.login()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Robot test&lt;/td&gt;
&lt;td&gt;Scenario only&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Inventory Should Be Visible&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Folder layout in the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resources/locators/   → selectors
pages/                → Python page libraries
tests/                → thin .robot files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Locators stay in one place
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoginLocators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;USERNAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id:user-name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id:password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;SUBMIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id:login-button&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer stable attributes like &lt;code&gt;data-test&lt;/code&gt; when the app exposes them.&lt;/p&gt;

&lt;h2&gt;
  
  
  BasePage owns the waits
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seleniumlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_until_element_is_visible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seleniumlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@property&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;seleniumlib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;BuiltIn&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get_library_instance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SeleniumLibrary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Import &lt;code&gt;SeleniumLibrary&lt;/code&gt; once in &lt;code&gt;common.resource&lt;/code&gt;. Page objects look up that same instance — no &lt;code&gt;driver&lt;/code&gt; argument passed between keywords. If you create &lt;code&gt;driver = webdriver.Chrome()&lt;/code&gt; outside SeleniumLibrary, your page keywords will not see that browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  A thin test
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight robot_framework"&gt;&lt;code&gt;&lt;span class="gh"&gt;*** Test Cases ***&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;Valid Login&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nf"&gt;Login With Valid Credentials&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;${VALID_USER}&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;${VALID_PASSWORD}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nf"&gt;Inventory Should Be Visible&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No locators. No driver setup. Browser open/close in &lt;code&gt;Test Setup&lt;/code&gt; / &lt;code&gt;Test Teardown&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/gitoza-io/rf-seleniumlibrary-pageobject-template.git
&lt;span class="nb"&gt;cd &lt;/span&gt;rf-seleniumlibrary-pageobject-template
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
robot &lt;span class="nt"&gt;--pythonpath&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--outputdir&lt;/span&gt; results tests/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;--pythonpath .&lt;/code&gt; from the repo root so &lt;code&gt;pages.*&lt;/code&gt; imports resolve. Four passing tests = layout is wired.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating without a rewrite
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Move locators to &lt;code&gt;resources/locators/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;BasePage&lt;/code&gt; with &lt;code&gt;click&lt;/code&gt; / &lt;code&gt;enter_text&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Wrap login as the first page library&lt;/li&gt;
&lt;li&gt;Import &lt;code&gt;SeleniumLibrary&lt;/code&gt; once in &lt;code&gt;common.resource&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Rewrite one &lt;code&gt;.robot&lt;/code&gt; file, delete &lt;code&gt;webdriver&lt;/code&gt; imports from its helpers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Repeat screen by screen. Leave the rest alone until you touch that feature.&lt;/p&gt;

&lt;p&gt;Playwright and Cypress are valid for greenfield work. Plenty of teams still run RF and SeleniumLibrary in production — for them, dropping &lt;code&gt;find_element&lt;/code&gt; for Page Objects is a low-risk upgrade that pays off on the next UI refactor.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Full guide:&lt;/strong&gt; &lt;a href="https://gitoza.com/blog/robot-framework-seleniumlibrary-page-object-model" rel="noopener noreferrer"&gt;gitoza.com/blog/robot-framework-seleniumlibrary-page-object-model&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Template repo:&lt;/strong&gt; &lt;a href="https://github.com/gitoza-io/rf-seleniumlibrary-pageobject-template" rel="noopener noreferrer"&gt;github.com/gitoza-io/rf-seleniumlibrary-pageobject-template&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>robotframework</category>
      <category>selenium</category>
      <category>python</category>
    </item>
    <item>
      <title>Excel Test Case Management: When Spreadsheets Stop Working</title>
      <dc:creator>Weiwen Weng</dc:creator>
      <pubDate>Sun, 05 Jul 2026 13:45:31 +0000</pubDate>
      <link>https://dev.to/weiwen-weng/when-excel-stops-working-for-test-case-management-1i6k</link>
      <guid>https://dev.to/weiwen-weng/when-excel-stops-working-for-test-case-management-1i6k</guid>
      <description>&lt;p&gt;Most teams start with an Excel sheet. One tab per feature, columns for steps and expected results, maybe a status column someone colors green by hand. It is cheap, familiar, and fine for ten cases.&lt;/p&gt;

&lt;p&gt;Then the product grows. The sheet does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the sheet starts to hurt
&lt;/h2&gt;

&lt;p&gt;Excel stores text, not test runs. It does not know what "Pass" means in your release process, who executed a case yesterday, or which build you tested. Run history ends up in another tab, another file, or someone's memory.&lt;/p&gt;

&lt;p&gt;File names tell the rest of the story. &lt;code&gt;test-cases-v3-FINAL.xlsx&lt;/code&gt; becomes &lt;code&gt;v3-FINAL-John-edits.xlsx&lt;/code&gt; on a shared drive. Two people update the same row. Someone sorts a column and IDs no longer match the bug tracker. There is no diff, no reviewer, no clear answer to "who changed step 4 before we shipped."&lt;/p&gt;

&lt;p&gt;Meanwhile your app, automated tests, and CI live in Git. Manual cases sit in a file that never rides along with your repo history. When a feature changes, the sheet is usually one sync meeting behind.&lt;/p&gt;

&lt;p&gt;Spreadsheets are fine for a one-off checklist. They are a poor system of record once QA is ongoing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Excel vs files in Git
&lt;/h2&gt;

&lt;p&gt;The usual upgrade path is a cloud TMS. That solves collaboration, but your catalog moves into someone else's database — another login, per-seat pricing, awkward exports, and another place sensitive scenarios live.&lt;/p&gt;

&lt;p&gt;A lighter step for many teams is &lt;strong&gt;test cases as files in a repository you already control&lt;/strong&gt;. Each case is a small Markdown file: YAML front matter up top, steps and expected results in the body. Git gives you history and review without a separate process. A thin editor on top lets you browse suites, edit cases, and record runs — pass/fail and notes saved as structured files, not colored cells.&lt;/p&gt;

&lt;p&gt;A case looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login with valid credentials&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;smoke&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;auth&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gu"&gt;## Steps&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Open the login page
&lt;span class="p"&gt;2.&lt;/span&gt; Enter valid credentials

&lt;span class="gu"&gt;## Expected result&lt;/span&gt;
User is redirected to the dashboard.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is roughly how &lt;a href="https://gitoza.com" rel="noopener noreferrer"&gt;Gitoza&lt;/a&gt; works. You connect a repo (GitHub, GitLab, or any remote you already use), keep cases on a dedicated branch, and work from the open-source desktop app. Cases, runs, tickets, and wiki stay as Markdown in Git — the system of record is your repository, not another SaaS database.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changes
&lt;/h2&gt;

&lt;p&gt;Runs stop living in side tabs. You assign cases, record results, and the history stays next to the cases themselves — on the same branch, versioned with every sync.&lt;/p&gt;

&lt;p&gt;When a feature ships, you can see case changes in git commits — who changed what, and when — instead of chasing the latest spreadsheet on a shared drive. Tags, priorities, and links to automation specs are easier to keep consistent when each case is a file, not row 427 in a sheet that someone sorted last Tuesday.&lt;/p&gt;

&lt;p&gt;You do not need to rip out Excel tomorrow. If the sheet is fighting you on every release, moving cases into Git is often less painful than another "please use the latest file" email.&lt;/p&gt;

&lt;p&gt;More on the file format: &lt;a href="https://gitoza.com/blog/why-test-cases-belong-in-yaml" rel="noopener noreferrer"&gt;Markdown Test Cases in Git&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://github.com/gitoza-io/gitoza" rel="noopener noreferrer"&gt;github.com/gitoza-io/gitoza&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>qa</category>
      <category>opensource</category>
      <category>git</category>
    </item>
    <item>
      <title>Test Management Compliance: Cloud SaaS vs Local-First</title>
      <dc:creator>Weiwen Weng</dc:creator>
      <pubDate>Fri, 03 Jul 2026 16:03:51 +0000</pubDate>
      <link>https://dev.to/weiwen-weng/test-management-compliance-cloud-saas-vs-local-first-57p8</link>
      <guid>https://dev.to/weiwen-weng/test-management-compliance-cloud-saas-vs-local-first-57p8</guid>
      <description>&lt;p&gt;Security sends a forty-question vendor questionnaire. Legal asks whether your test cases contain customer PII or staging screenshots. Procurement forwards next year's per-seat renewal.&lt;/p&gt;

&lt;p&gt;The decision is not which UI looks best in a demo. It is whether you can keep sensitive test knowledge off a vendor's cloud — without staffing a server farm.&lt;/p&gt;

&lt;p&gt;Test cases are not harmless notes. They hold user stories, credentials in steps, production-like screenshots, and run history. For finance, healthcare, or EU-facing teams, that is operational data — not "just QA."&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud SaaS vs local-first at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Cloud SaaS&lt;/th&gt;
&lt;th&gt;Local-first (e.g. Gitoza)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where is the canonical copy?&lt;/td&gt;
&lt;td&gt;Vendor cloud DB&lt;/td&gt;
&lt;td&gt;Your repo / your storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does test data cross vendor boundary?&lt;/td&gt;
&lt;td&gt;Yes, by design&lt;/td&gt;
&lt;td&gt;No vendor DB; sync via your Git remote or BYO S3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit trail&lt;/td&gt;
&lt;td&gt;Vendor activity log&lt;/td&gt;
&lt;td&gt;Git commit history (author, timestamp, diff)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical small-team cost model&lt;/td&gt;
&lt;td&gt;Per-seat subscription (hosted data)&lt;/td&gt;
&lt;td&gt;Lower per-seat subscription; no hosted TMS DB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual tester UX&lt;/td&gt;
&lt;td&gt;Web UI&lt;/td&gt;
&lt;td&gt;Desktop UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exit strategy&lt;/td&gt;
&lt;td&gt;Export / API&lt;/td&gt;
&lt;td&gt;Files already yours&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Planning aid, not legal advice — your security team signs off.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where cloud TMS friction starts
&lt;/h2&gt;

&lt;p&gt;TestRail, Testmo, Qase, and similar tools store cases in a &lt;strong&gt;vendor-operated database&lt;/strong&gt;. Rollout is fast. Manual testers get web forms without a terminal.&lt;/p&gt;

&lt;p&gt;The compliance pitch is usually SOC 2, ISO 27001, and a DPA. That certifies &lt;strong&gt;the vendor's operation&lt;/strong&gt; — not whether &lt;strong&gt;your test catalog&lt;/strong&gt; belongs in their multi-tenant cloud.&lt;/p&gt;

&lt;p&gt;Question 12 on a typical security questionnaire: &lt;em&gt;Where is customer data processed?&lt;/em&gt; Your cases may include staging screenshots with real-looking records. Meanwhile your cases sit in a US region while your customers are in Frankfurt.&lt;/p&gt;

&lt;p&gt;On audit trails: activity logs help, but the system of record is still editable rows. Proving "we tested exactly this before release 4.2" often means trusting vendor retention — not a history your org already controls.&lt;/p&gt;

&lt;p&gt;Then there is seat math. Twenty testers at roughly $30–45 per seat per month adds up before renewals. Desktop tools like Gitoza also charge per seat — but you are not paying to host your test catalog in a vendor database.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about on-prem?
&lt;/h2&gt;

&lt;p&gt;Self-hosted TestRail Server or Jira + Xray behind the firewall can be right at enterprise scale. For a twenty-person QA org without a platform team, six figures all-in is not rare. Many regulated teams end up on cloud SaaS and hope the DPA holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local-first in one paragraph
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Local-first&lt;/strong&gt; keeps the canonical copy in &lt;strong&gt;files you control&lt;/strong&gt; — typically a Git repo — while a &lt;strong&gt;desktop app&lt;/strong&gt; gives manual testers the UI they expect from SaaS.&lt;/p&gt;

&lt;p&gt;No vendor database as system of record. You pay a per-seat license for the app — not a recurring fee to store your test catalog in a vendor cloud.&lt;/p&gt;

&lt;p&gt;With Gitoza, cases live on a dedicated &lt;code&gt;gitoza&lt;/code&gt; branch in a shadow clone; testers do not touch &lt;code&gt;main&lt;/code&gt;. Git history is the audit log — every change is a commit with author, timestamp, and diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist before you shortlist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inventory&lt;/strong&gt; — What sensitive data is in cases and runs today?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Residency&lt;/strong&gt; — Where must it live? Can US-hosted SaaS meet your contracts?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canonical copy&lt;/strong&gt; — If you stop paying, what do you still own without an export project?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History integrity&lt;/strong&gt; — Can an admin alter past results without a durable trail?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seat math&lt;/strong&gt; — Three-year cost at current headcount and +50% growth?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual tester path&lt;/strong&gt; — Can non-engineers run tests without CLI or VPN?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vendor count&lt;/strong&gt; — How many new subprocessors does this tool add to annual review?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration&lt;/strong&gt; — Realistic effort to move in &lt;em&gt;and&lt;/em&gt; out in one release cycle?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If 2, 3, and 7 are red, cloud SaaS needs a harder look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you pick the dashboard
&lt;/h2&gt;

&lt;p&gt;A vendor's SOC 2 report describes how &lt;em&gt;they&lt;/em&gt; run their platform. Your auditor will still ask where &lt;em&gt;your&lt;/em&gt; case library lived when release 4.2 shipped — and whether anyone could rewrite that history without leaving a trace.&lt;/p&gt;

&lt;p&gt;Those are different questions. Certified cloud TMS vendors serve plenty of regulated customers well. Local-first does not replace legal review or a signed DPA. It just moves custody: the canonical copy sits in your repo, not a vendor row you export at renewal time.&lt;/p&gt;

&lt;p&gt;If custody matters in your industry — and for many QA teams it does — answer that on paper before the UI demo wins the room.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try Gitoza:&lt;/strong&gt; &lt;a href="https://gitoza.com/#download" rel="noopener noreferrer"&gt;Desktop download&lt;/a&gt; · &lt;a href="https://marketplace.visualstudio.com/items?itemName=gitoza.gitoza-yaml-test-cases" rel="noopener noreferrer"&gt;VS Code extension&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>qa</category>
      <category>security</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
