A ticket tracker that is nothing but markdown files inside your repo. No database, and no board view stored anywhere — the one view there is gets drawn live from the folders.
Overview
A ticket tracker that is nothing but markdown files inside your repo. No database, and no board view stored anywhere — the one view there is gets drawn live from the folders. Built as a Claude Code plugin, though the format works just as well by hand. "Kerjaan" is Indonesian for "work" — the stuff that needs doing. Tickets are usually written for people who already know the codebase. That quietly locks out everyone else — the client, the manager, the colleague from another team — so every question they have has to be answered out loud by somebody who does know. kerjaan pushes tickets to be self-contained: one file is opened, with no context about the system at all, and the reader immediately understands what is happening and what is being asked. And because it is only files in a repo, the history travels with the code and there is no service to run, host, or pay for. settings.
README
kerjaan
A ticket tracker that is nothing but markdown files inside your repo. No database, and no board view stored anywhere — the one view there is gets drawn live from the folders. Built as a Claude Code plugin, though the format works just as well by hand.
“Kerjaan” is Indonesian for “work” — the stuff that needs doing.
The problem it solves
Tickets are usually written for people who already know the codebase. That quietly locks out everyone else — the client, the manager, the colleague from another team — so every question they have has to be answered out loud by somebody who does know.
kerjaan pushes tickets to be self-contained: one file is opened, with no
context about the system at all, and the reader immediately understands what is
happening and what is being asked.
And because it is only files in a repo, the history travels with the code and there is no service to run, host, or pay for.
The board
/.kerjaan/
├── backlog/ ideas, not ready to act on
├── todo/ the queue for one work session: ready, not started
├── in_progress/ being worked on
├── review/ done, not yet verified
├── done/ passed review
├── cancel/ abandoned
├── order.md the order to pick `todo` up in — optional
└── settings.md language, test command, long-lived branches,
side-by-side tests and worktree setup
settings.md is written once, the first time the board is used: Claude
proposes values read off the repo and asks you to confirm them. Its
test_command is run once per handover, not twice: see “The checks run once”
below.
File name: .md, for example
260905160401 Send notifications through Telegram.md.
The ID has the form yymmddhhmmxx — creation time down to the minute, plus a
two-digit sequence number.
What a ticket looks like
---
type: bug
priority: high
review: normal
labels: [telegram-bot]
reporter: mirza
assign_to: claude
created: 2026-09-05 16:04:01
updated: 2026-09-06 14:02:10
related: []
blocked_by: []
---
## Background
The Telegram bot checks for new messages every few seconds. When two messages
arrive at nearly the same moment, one of them is sometimes skipped and that
person never gets a reply.
## How to reproduce
1. Open a chat with the bot
2. Send three short messages within one second of each other
3. Wait ten seconds
Only two replies come back. The third message gets no answer at all, and
nothing anywhere says it was dropped.
## Request
Messages that arrive together must all still get a reply, with none lost.
## Done when
- [ ] The steps above produce three replies rather than two
- [ ] Run for a full day with no reports of a missed message
## History
- 2026-09-05 16:04:01 todo
- 2026-09-06 14:02:10 4dbcf082-290a-4f65-845c-ee75fd3b3e9c in_progress
## Notes
(none yet)
A move made inside a Claude Code session also names that session in its
History line, so claude --resume opens the conversation that
made it again, for as long as Claude Code keeps the transcript. A move made
from a plain terminal leaves the id out.
Five headings appear in every ticket. ## How to reproduce is the one
exception: bug tickets carry it, and feature and task tickets leave it out
entirely rather than filling it with “not applicable” — an empty section
teaches readers to skip sections.
Notice that it names no file, no function, and no library. Technical detail is not forbidden — but technical decisions belong to whoever executes the work, rather than being baked into the ticket as a constraint from the start.
The structure is always English. The prose is written in whatever language the
ticket’s readers actually speak: an Indonesian team writes Background,
Request, and the checklist in Indonesian, and the English headings above them
stay exactly as they are.
Installation
Inside Claude Code:
/plugin marketplace add mirzaakhena/kerjaan
/plugin install kerjaan
That is the whole installation. All three pieces arrive together and no settings file needs editing:
kerjaan/
├── skills/kerjaan/ the board: how tickets are written and moved
├── agents/ kerjaan-planner, who prepares the queue from the backlog,
│ kerjaan-worker, who executes one ticket,
│ and kerjaan-reviewer, the independent reviewer
└── hooks/hooks.json asks for that reviewer when a ticket enters review
The plugin applies in every project. It does nothing at all in a repo that has
no .kerjaan/ folder, so installing it globally costs nothing.
Installing the skill alone — git clone into ~/.claude/skills/ — still works
and still writes perfectly good tickets. What it loses is the worker, the
automatic review and the hook: those are the pieces only a plugin install can
register.
Usage
Usually it is enough to just ask in plain language — “note this down as a bug”, “move that to review”, “what are we working on” — and Claude reaches for the skill on its own, resolving the script paths by itself.
To run the commands by hand, locate them once and work from the repo root:
K=$(dirname "$(find ~/.claude/plugins -path '*kerjaan/skills/kerjaan/scripts/new-ticket.sh' | head -1)")
# create a ticket (prints the file path; then fill in its prose)
$K/new-ticket.sh backlog "Send notifications through Telegram"
# change a ticket
$K/update-ticket.sh 260905160401 --status in_progress
$K/update-ticket.sh 260905160401 --title "A clearer title"
$K/update-ticket.sh 260905160401 # after editing the prose
$K/update-ticket.sh 260905160401 --status in_progress --ack-unmerged
# start despite unmerged branches
$K/update-ticket.sh 260905160401 --accept-size
# the owner keeps a big ticket whole
$K/ticket-size.sh 260905160401 # is it too big? changes nothing
# run test_command and record what it ran on; then ask whether a version matches
$K/test-run.sh 260905160401
$K/test-run.sh --check 260905160401 HEAD # SAME / DIFFERENT / NO EVIDENCE
# a worktree for working two tickets side by side, and bringing its work back
$K/worktree.sh add 260905160401 ../repo-topic
$K/merge.sh sync 260905160401 # in the worktree: merge main in, before review
$K/merge.sh land 260905160401 # passed review: into main, one at a time
$K/merge.sh resolution 260905160401 # what the review after a merge postponed reads
$K/worktree.sh remove 260905160401 # a worktree given up on
$K/test-run.sh --review 260905160401 # the reviewer's run, queued with the rest
# the planner's tools for filling todo from the backlog
$K/queue.sh estimate 260905160401 src/notify.py # files the ticket is expected to change
$K/queue.sh overlap # pairs of tickets whose estimates share files
$K/queue.sh ask 260905160401 "" # a numbered question for the owner
$K/queue.sh answer 260905160401 1 ""
$K/queue.sh check # todo and order.md ready? READY / NOT READY
$K/queue.sh compare 260905160401 # estimate against what really changed
# finished tickets that no release carries yet
$K/release.sh unreleased # LAST RELEASE, then UNRELEASED: and the tickets
# read the board — free-form, no script involved
ls .kerjaan/in_progress/
grep -l 'priority: high' .kerjaan/todo/*.md
One session, many tickets
Hand a session one ticket or a whole queue, and the session you talk to never
writes the code itself. It acts as the foreman: it keeps the board, asks
you what needs asking, and gives each ticket to a fresh kerjaan-worker
subagent that starts with an empty context.
you hand over the tickets
↓
the foreman asks everything up front, in one message
↓
per ticket: the foreman moves it to in_progress and commits that
kerjaan-worker does it, moves it to review, commits work and
ticket together, reports
the foreman dispatches kerjaan-reviewer and waits for the verdict,
which the reviewer commits itself
sent back → the same worker is resumed to fix it,
and the same reviewer is resumed for round two
↓
one summary for you at the end
Why. A session that works tickets itself collects every file it read and every test it ran. By the second or third ticket its context is full, it gets compacted, and you end up opening a new session per ticket to start clean. Here, only short reports reach the foreman: every ticket starts clean, and the session stays light until the queue is finished. It also holds for a single ticket, so there is one way of working, not two.
What you are asked at the start. The open questions in every ticket of the queue, together, with the review level the planner suggests for each; and, for checks only a person can make — how it looks on a real phone, a day of real use — whether they are deferred or should hold the queue. Deferred ones become a hand scenario in the ticket, marked as awaiting you, and the reviewer does not reject the ticket over them.
What travels between tickets. The worker’s report ends with what the next ticket can reuse, the traps it hit, and anything a reviewer would catch. The foreman collects these, adds the reasons any ticket was sent back, and hands the whole list to the next worker. Workers never read each other’s history; the lessons still arrive.
A rejected ticket goes back to the worker who did it, resumed from its own transcript, so it fixes without reading the ticket and the code again. The next round goes to the same reviewer, resumed the same way. If the session was replaced and the old worker cannot be reached, a new one works from the ticket’s notes, and the summary says so.
When it stops on its own. A decision that is yours to make (the worker records the question, the options and its recommendation in the ticket, numbered the same way as the planner’s questions, and never guesses), the same ticket rejected twice by its reviewer, or a ticket the others build on failing. A question that does not hold the work up is written into its ticket and waits for the summary.
What you get at the end. One summary: which tickets passed and in which round, which stopped and why, every question the workers left, and every check still waiting for a person.
The rules the foreman follows are in
skills/kerjaan/references/foreman.md; the worker’s steps are in
agents/kerjaan-worker.md.
Choosing what is next
todo means ready, and readiness is checked rather than assumed: it is
whatever queue.sh check says, and that one script is the definition the
skill, the planner and the guides all point to (the conditions are spelled out
once, in the skill’s “Into todo”). A ticket waiting for another is ready once
the other is finished or queued in an earlier group, and a decision ticket
that changes no file says so instead of naming a made-up one. On top of that it means next — the ticket is holding others up, or
it is groundwork, or it is quick, or it sits next to work already in flight.
Size is treated as a reason to split a ticket, never as a reason to defer it,
because groundwork is almost always big and a board that defers by size buries
exactly the work everything else is waiting for.
Who reads the backlog. Ask “what can we work on today” and the session you
talk to does not read the backlog itself: it sends kerjaan-planner, a
subagent with a clean context, and keeps its own for the work in flight. The
planner judges each ticket’s readiness, splits the ones that are too big by
outcome, writes down which files each ticket is expected to change, and
proposes an order — which tickets may run together and why. Its questions,
from every ticket, reach you in one message; each is numbered in its ticket,
and a ticket with a question still open cannot be moved into todo at all.
Once you approve, the planner writes your answers into the tickets, moves them
into todo and writes order.md. Two tickets that only change the same files
may run side by side, with the order they land in decided up front; a ticket
that needs another’s result never runs beside it. After a ticket is done,
queue.sh compare shows what was estimated against what really changed.
How big is too big has a number: about 6 to 8 items in Done when. Past that
the review gets duller, the result shows up an hour later instead of a quarter
of an hour, the worker runs out of room more often, and one failing part holds
back all the rest. Whenever a script saves a ticket over the limit it prints a
warning with the count — the ticket is saved all the same — and the session
offers you a split by outcome, pieces you can each try on their own, before
the ticket goes to todo or starts. You can keep a ticket whole; that
decision is written into its Notes and the warning stops for it. The limit is
done_when_limit in settings.md, 8 when unset.
todo is the queue for one working session, and it is cleared as a whole
before any of it starts: every open question across every ticket in todo is
put to the user in one go and the answers written back into the tickets. Only
then does work begin, one ticket at a time or side by side — so the session can
run to the end without stopping to wait for anyone.
Work starts from a ticket. Ask Claude to fix or build something and it
searches the board first — if a ticket already covers the same outcome, that
one is updated instead — or creates one, and shows you its Request and
Done when before touching any code. A new session starts by looking in
in_progress/ and review/ for work somebody left behind, and says so before
starting anything new.
When a batch is finished — todo, in_progress and review all empty — the
session that just ran the batch reads the backlog and proposes what should come
next, while it still remembers the work. It proposes; you choose.
A folder has no order, though, and blocked_by only covers the hard case where
one ticket cannot start until another finishes. Softer sequencing lives in
.kerjaan/order.md, which covers todo/ and nothing else:
**First, on its own.** Everything below reads the settings file it introduces.
- 260905160401 Add the config loader
**Then these two, in any order.** Neither touches the other's files.
- 260905160502 Import screen
- 260905160503 Export screen
Order is position on the page; tickets sharing a group can run side by side;
the bold sentence carries the reasoning, which is the part that exists nowhere
else. update-ticket.sh keeps the file honest — a ticket leaving todo loses
its line, and a group loses its lead-in once its last ticket is gone. Once
the file is set, it is your choice already made: a session takes the next
ticket from the top of it without asking you again.
The file is optional. Write it the first time ordering actually matters, and
refill todo from backlog only when todo, in_progress and review are
all three empty — a ticket still in review can be handed straight back, so
until then the board does not yet know what it finished.
The viewer
The viewer shows every board you have as a kanban, from one server:
node viewer/server.mjs --scan ~/Workspace --open
Every folder directly under ~/Workspace that holds a .kerjaan/ is a
project, at http://127.0.0.1:4321//. The front page lists them
with how many tickets each holds per status; a board created later appears
without a restart.
--project makes the address it prints and opens that
project’s kanban instead of the front page.
A project opens as a kanban: one column per status, todo in the order
order.md gives, the other columns most recently touched first. Cards can be
filtered by type, priority, label and words in the title, and the filter lives
in the address, so a filtered view can be bookmarked. A card opens the ticket
itself, rendered, with every ticket it names one click away; each ticket has
its own address (//tiket/). The page follows the board as it
changes, the ticket you are reading included. Any column can be hidden and
shown again, still showing how many tickets it holds; cancel starts hidden,
and the browser remembers the choice. todo’s header opens order.md itself,
reasons and all.
Beside the project’s name, every active project (those not archived in this browser) is a button, by name, the open one marked, so another board is one click away. When they do not fit on one line the row scrolls sideways; on a phone it gets a line of its own under the name.
A card in done whose code is not on the main line yet says so: “belum
digabung”, with the name of every branch that carries its ID and holds
commits outside HEAD — the same rule update-ticket.sh applies when it
refuses to start the next ticket, so the board shows beforehand what that
refusal would name. A merge held for the owner is marked too, since its code
is not on main either. Branches in long_lived_branches are never named, and
the mark goes away by itself once the branch is merged or deleted, on an open
page as well. A project that is not a git repository shows no mark.
It only reads, needs Node 18 or newer and nothing else, and
listens on 127.0.0.1 only. --port picks another port (0 for any free
one). Its tests run with node --test from the repo root.
Two tickets at once
Tickets sharing a group in order.md have already been declared able to run
together — none waits for another’s result, and any files they both change are
named, with which lands first — which is the licence to run them side by side
in separate git worktrees, on branches whose names carry the ticket ID.
Nothing about that is written down. The ticket gains no branch: field — the
ID is already in the branch name, and git’s own worktree list is a registry
that cannot go stale. The worktree is created with .kerjaan/ left out of its
checkout, because the board lives in the repository and a second checkout of it
is a second board: the same ticket then reads in_progress in one tree and
todo in the other, with nothing to report the disagreement. Both scripts
refuse to run from a linked worktree for the same reason. What the board adds is noticing at the two moments where
forgetting turns silent: update-ticket.sh warns when a ticket reaches done
or cancel while its branch still holds unmerged commits, and an idle board —
todo, in_progress and review all empty — must have no worktrees left at
all.
A warning at done lands in the reviewer’s report, where it is easily lost, so
the next start is a gate instead: update-ticket.sh refuses to move a ticket
into in_progress while any local branch holds commits outside HEAD —
whatever it is named. Branches of tickets still in in_progress or review
are left alone, and so are the long_lived_branches in settings.md. For
anything else you decide: merge it, delete it, or start anyway with
--ack-unmerged, which records the branches in the new ticket. Claude never
passes that flag on its own.
Each change is tested on its own branch, which proves it alone; two changes
that pass alone can break each other once merged, without git noticing. So a
branch reaches main only after it was tested with all of main inside it. The
worker merges main in (merge.sh sync), resolves any conflict itself, and
tests that state; the move to review is refused until the branch holds all of
main’s code and its latest test run covers exactly what is committed. Once the
reviewer passes it, the foreman lands it with merge.sh land, right away and
without asking the owner, one merge at a time — and without running the tests
again, since what lands is exactly what was tested. When main has moved in
the meantime, the ticket goes back to its worker to merge again, and the
reviewer looks briefly at only the conflict resolution. A merge that fails
for any other reason holds that one ticket for the owner, with the reason in
its Notes; everything else carries on.
A worktree separates files, not the ports, local databases and caches a test
suite uses, so test runs from different worktrees take turns: test-run.sh
holds one lock for the whole repository, the reviewer’s runs included, and a
run that had to wait says so in the line it records. A board whose tests share nothing sets
parallel_tests: true. Each worktree gets a slot number and its ticket ID in
KERJAAN_SLOT and KERJAAN_TICKET, to name its own port or database by. The
worktrees are made with worktree.sh, which runs worktree_setup from
settings.md in each new one (copying .env, installing packages) and
worktree_teardown when it is removed; each ticket’s test data carries its
name, so one ticket’s teardown never removes another’s.
The reviewer checks for a worktree before reading any diff, because reviewing the main tree for work that lives on a branch would report “nothing changed” about a ticket that changed plenty.
Review happens by itself
review is the one status that does not sit still waiting for someone to
notice it. The moment a ticket lands there, the kerjaan-reviewer agent is
dispatched:
the worker runs update-ticket.sh --status review
↓
the PostToolUse hook fires — but only if the ticket really is in .kerjaan/review/
↓
inside the worker it says: stop and report; never dispatch a reviewer yourself
↓
the worker ends; with its report, the hook tells the foreman: dispatch the reviewer
↓
the foreman dispatches kerjaan-reviewer
↓
kerjaan-reviewer runs the ticket's own "Done when" checks itself
↓
it moves the ticket to done/, or back to in_progress/ with the evidence
Only the foreman dispatches reviewers, because only the foreman counts the
rounds, carries a verdict’s lesson to the next worker, and knows which worker
to resume. The hook tells the two apart by whether the move was made inside a
subagent, and remembers which worker moved which ticket: when that worker
ends — the Agent tool returning, or the notification of a worker run in the
background — the reminder arrives with its report, so a ticket does not sit
in review because the foreman’s attention was elsewhere. A worker that ends
without moving its ticket brings no reminder. When the foreman makes the move itself for a ticket that has been
reviewed before, the hook tells it to resume that reviewer rather than
dispatch a new one, and says which round this is. A round ends with one
rejection by the reviewer; a merge postponed — merge.sh land sending a
passed ticket back because main moved first — is no rejection, and the
review after it has no round number.
Whoever executed the ticket does not get to mark their own work done. The
last word belongs to something that did not write the code and has no stake in
it passing — and it judges by running the checks rather than by believing what
## Notes claims.
Telling Claude “mark that as done” sends the ticket to review too, so the
reviewer still has the last word. Say you want to skip review and it goes
straight to done instead, with a note in the ticket saying no review stood
behind it.
The worker hands the ticket off and ends — and nobody changes that ticket’s
code from then on, because the move was the claim that it is
finished and an edit made afterwards is one the reviewer judges without
knowing it happened. A failed review simply puts the ticket back in
in_progress, which is where a board is supposed to put unfinished work.
The reviewer never files tickets of its own. Things it notices outside the
ticket’s criteria go into a ### Suggested follow-up block in the reviewed
ticket’s notes. Once every ticket that was in flight has reached done, the
foreman collects those suggestions and offers them to the user as a single
multiple-choice question; the ones picked become tickets, and each answer —
ticketed or declined — is marked next to its suggestion so it is never offered
twice. An unmet criterion is never allowed to become one of those
suggestions, because a reviewer that can move a failure into a new ticket is
no longer a gate.
The checks run once
The worker runs the project’s test_command before handing the
ticket over, through test-run.sh. The script appends one line to the
ticket’s notes: when it ran, whether it passed, and a fingerprint of exactly
the content it ran on — every file git would see, new files included, with
.kerjaan/ left out so moving tickets never changes it.
The reviewer does not take that line’s word for anything that matters. It
asks git for the fingerprint of the version it is reviewing, and
test-run.sh --check answers:
- SAME — the run passed on exactly this content. The suite is not run a second time.
- DIFFERENT — something changed after the run, even one line, and the answer names the files. The reviewer runs the checks itself.
- NO EVIDENCE — no run recorded, the run failed, the content changed while
it ran, or
test_commandhas changed since. The reviewer runs the checks.
Committing after the run does not break the match, because a commit does not
change content. What is taken on trust is only that the run passed — the same
trust a CI badge asks for. Code the reviewer changes itself, such as a
deliberate break at strict, is always tested by the reviewer.
How deep the review goes
A ticket’s review field decides that, and it is optional — empty means
quick. quick reads the change against the criteria beside one run of the checks,
normal proves each criterion by running something, and strict also breaks
the code on purpose to see the tests catch it. The full table of what each
level does is in agents/kerjaan-reviewer.md,
“How thorough to be”.
The default is the cheap one on purpose. strict can install a project’s
dependencies from scratch and run the tests again for every piece of code it
breaks on purpose, which is the right price for a payment path and the wrong one for a reworded button —
and a review that always costs the most is a review people learn to skip.
Depth bounds effort, never honesty. Even quick judges the Done when list
line by line, refuses to treat ## Notes as evidence, and digs deeper on its
own when something does not add up. Every review writes the level it ran at
into the ticket, so a done never hides how much checking stood behind it.
Claude offers a higher level when the change lands somewhere unforgiving —
money, login, permissions, data you cannot get back — and the choice stays
yours. Tell it you want speed, or that this is a mock or an MVP, and it stops
offering and uses quick.
A ticket that fails review should not bounce for long. The reviewer reports every instance of a mistake it found, not only the first; the same worker, resumed, fixes the whole kind of mistake before resubmitting; a second review re-checks only what failed and what changed since; and after two returns Claude asks you whether to try again or accept the ticket with a follow-up, instead of looping on its own.
One limit worth knowing: the reviewer blocks on Done when and nothing else.
A checklist that only describes the happy path stays unblocked even at
strict, which will verify that happy path thoroughly and pass. If an
unvalidated input is what worries you, the fix is a criterion that says so —
not a deeper review.
Running the tests
From the repository root, with Node 18 or newer and nothing installed:
node --test
That one command runs the whole suite: every *.test.mjs under uji/ (the
scripts in skills/kerjaan/scripts/) and viewer/uji/ (the viewer). This
board’s own test_command, which test-run.sh runs before a ticket goes to
review, is the same run with two guards added (Node 22 or newer):
node --test --test-timeout=900000 --test-force-exit, so that a test which
hangs fails after 15 minutes instead of holding the run forever. Each test
builds a throwaway repository with a board under the system temp folder and
runs the real scripts against it; nothing in this checkout is changed. Expect
a few minutes for the whole run.
Naming a folder does not work: node --test uji/ tries to load uji/ as a
single module and fails with “Cannot find module”. To run part of the
suite, name the files, or a glob in quotes (Node 21 or newer):
node --test uji/update-ticket.test.mjs or node --test 'uji/*.test.mjs'.
The skill’s description is held to a trigger test: about 40 sentences in
uji/pemicu/kalimat.json, each run three times in a fresh claude -p
session, recording whether it calls the skill. That run uses Claude quota, so
the suite does not make it; uji/pemicu.test.mjs only checks the stored
results against the current sentences and description, and fails when either
changed. After changing one, run it again by hand (a few minutes, 120 calls):
node uji/pemicu/jalankan.mjs --keluar uji/pemicu/hasil-baru.json.
Releasing a new version
A Claude Code session does not run this repository. It runs the copy of the
plugin that /plugin install put on the machine, and /plugin update
replaces that copy only when the version in .claude-plugin/plugin.json has
changed. So a ticket that passed review and sits on main is in effect for
nobody until a release carries it, and nothing fails to say so.
When to bump the version. Whenever main holds finished tickets that change
what the plugin ships — anything under skills/, agents/, hooks/ or
viewer/ — release before expecting any session to behave differently. At the
latest, release when a batch ends: the foreman runs release.sh unreleased
once the board goes quiet, names the tickets no release carries, and offers
one. Tickets that changed only the board (.kerjaan/) or only the tests
(uji/) need no release; the script marks the board-only ones.
- patch (1.12.0 → 1.12.1): fixes that change no rule a session follows;
- minor (1.12.0 → 1.13.0): new behaviour, a new script, a new setting;
- major (1.x → 2.0.0): existing boards or settings must be changed by hand.
How. One commit that raises version in .claude-plugin/plugin.json
(marketplace.json carries no version and stays as it is), with the message
Release x.y.z followed by a summary in Indonesian of what changed, naming the
ID of every ticket it carries. release.sh reads those IDs, together with
what was already in done/ at that commit, to tell released tickets from the
rest. Then the owner pushes main, runs /plugin update kerjaan, and starts a
new session: one that is already open keeps the copy it started with.
Design principles
These five produced every rule in the skill, and they are enough to derive new rules for situations not covered yet.
1. One fact lives in one place. The title exists only in the file name. The
status exists only in the folder name. The ID exists only in the file name.
There is no title:, no status:, and no id: field — because two copies of
one fact will eventually disagree, and nothing tells you when that happens.
2. A ticket never states its own identity. It only states the identity of
other tickets. That is why related and blocked_by still carry IDs while
id: was removed.
3. Only operations that fail silently get locked into a script. A wrong
search command produces empty output you notice immediately, so searching is
left free. Moving a file without refreshing updated, on the other hand,
raises no error and only surfaces months later — so the two are bound into a
single command, making half an action impossible.
4. Structure is English; prose is for the reader. Folder names, frontmatter
keys, type, priority and review values, and the headings never vary. Not
sure which side something falls on? Ask whether the word is identical in every
ticket. If it is, it is structure.
5. The board serves whoever was not there. Not the person doing the work — they already know what they are doing. It serves the one who opens the repo tomorrow, or after a session ended mid-task, and needs to know what is in flight and where it stopped without reading a diff and guessing at intent.
This is why in_progress is used even for work that will finish in minutes, and
why entering review requires ticking the Done when list line by line rather
than trusting the feeling of being finished. Both rules cost the worker
something and pay the reader back. Whenever a step feels like pointless ceremony
to whoever is doing it, that is the signal to check who it was for.
Keeping the rules small
The rules a session reads — skills/kerjaan/SKILL.md, the guides in
skills/kerjaan/references/ and the agent definitions in agents/ — have a
budget of 18,000 words together. uji/skill-ringkas.test.mjs counts them
and fails when the budget is exceeded, so the suite goes red before a release
can carry a heavier skill.
Two habits keep it there:
- A new rule names the rule it removes or replaces. The ticket that adds
one says, in its
RequestorNotes, which existing rule, explanation or script option goes away or is folded into it — and the commit message repeats it. An incident is first a reason to sharpen a rule that exists, not to add one more. A rule that replaces nothing has to fit under the budget as it stands; when it does not, something else is cut first. - Ticket Notes are summaries. A worker records what it decided and what it
broke to prove its tests, one line each, never a full log of commands and
output (
agents/kerjaan-worker.md, step 5): a reviewer reads those Notes again, and so does every later session.
Deliberately absent
- No board view, index, or summary. The moment a summary exists it starts
going stale and quietly lying. To find out what is in flight, read the
folders. The one non-ticket file,
order.md, is not a counter-example: what it exists for — sequence and reasoning — is something no folder can express. It copies one fact, each ticket’s title, so the file reads as names rather than numbers, and the rename script keeps that copy in step. Nor is the viewer: it reads the folders again on every change and keeps nothing, so there is nothing in it to go stale. - No change log inside the ticket. Just
updated. Git already holds the full history. - Little opinion about git. Outside a chain, how tickets relate to commits is left to each repo’s own habits. A chain commits each ticket worked in the main tree in at most three commits — start, work, verdict — each holding its ticket’s board change, and leaves no board change uncommitted.
- Only three types:
bug,feature,task. Every additional type forces the author to think about categorisation, when what actually matters is the content.
License
MIT — see LICENSE.
Recommended Tools
Try a different keyword or remove a filter.
Install
npx skillfish add mirzaakhena/kerjaan