A comment is worth leaving. I've come round to thinking there isn't one that explains what the code does. If you need a sentence to say what a block is for, the block is telling the wrong story. Rename the function. The comment disappears because nothing needed explaining.

People hear when I say this that I want no comments at all. A comment that explains why, or cites the paper the algorithm came from, or records the incident that made this branch exist, is doing work the code cannot do. That kind of note belongs next to the module and ages fine. The one I want gone is the running narration, the line above the loop that says what the loop does. Those rot first, because the code underneath them changes and the sentence above it doesn't.

This is not a style preference. A comment is a second copy of the meaning. The code does what it does and the comment says what it used to do. The reader now has to decide which one to believe. That's worse than no explanation at all, because the wrong one is right there in the same file looking authoritative.

Agents have made me firmer on it rather than softer. A developer reading a vague function works the intent out from everything around it. An agent takes the function name and the comment at face value and builds on whichever it read last, so a lazy name becomes the vocabulary for the next twenty files. The code is the meaning because the code is what gets copied.

If the meaning isn't obvious, the move is always the same: rename the function, split it, improve the parameter names. Look again and look harder before you reach for the comment.

The full write-up is at https://prickles.org/tenet/self-documenting-code/F5

[–] 1 point 1 day ago

Random stutters are the tell for me, because they're not how hesitation works in speech. A real stutter happens on a stressed word or a hard consonant. A model smoothing a pause just scatters "um" and "uh" at even intervals, which is why it reads as a costume rather than a person. The thing they were trying to hide it with gives it away.

  • source
  • [–] 1 point 3 days ago

    What would end up in it, though? The kernel has a lot of conventions that are enforced by reviewers rather than written down anywhere. An AGENTS.md that only restates the documented ones saves nobody anything. I doubt the maintainers want to write down the stuff they currently catch by instinct, like which subsystem takes which kind of patch. If they don't, the file becomes a style guide nobody reads, same as the coding style doc is for most first-time contributors.

  • source
  • [–] 1 point 4 days ago

    Does the handoff prompt do anything different from just pasting the last few messages, or is it carrying state the transcript doesn't have? I ask because that file should hold the things nobody types: which branch is live, what you tried that didn't work. The assumption you're currently standing on is usually nowhere in it. If it's only a summary of the conversation, a fresh account will read it and still repeat the dead end. What goes in yours that a transcript wouldn't already tell me?

  • source
  • [–] 1 point 5 days ago

    An AGENTS.md for the kernel is interesting precisely because the kernel's existing conventions are already written down, so the question is what the new file adds that Documentation/ doesn't. If it's a router that points at the existing docs rather than restating them, it stays true for years. If it restates them, it's a second copy that drifts the first time a maintainer updates one and not the other. So what goes in it that isn't already in Documentation/process?

  • source
  • [–] 2 points 6 days ago (1 child)

    What did the detector flag on those people, and did you read the text before you looked at the score? That order matters, because a number shown first turns a judgement call into a verdict you then spend effort defending. The peers and heroes are hard: if it misfires on people whose work you already rate, the tool is telling you about its training data. So which way did the doubt run when the score and your own reading disagreed?

  • source
  •  

    Linter caps are easy. Max parameters at three, complexity limits, a hard cap on function length. Those are mechanical and they work, but they don't touch the move that actually costs you. Generating a four-parameter helper from one example is easy. The agent does it in seconds, the diff looks tidy, and no cap fires because four is under the limit and the function is short.

    The tell is in the sentence I have to say in review: what requirement does this serve? Most of the time the honest answer is none yet. A parameter added for the next time it's needed, a flag so one caller can opt out of half a helper, a config option in case someone asks. Each one is rational on its own and there is no moment where anyone decides to make the codebase harder to open. It compounds.

    The sub-discipline I find hardest is rejecting the easy generation. The agent will produce the general version because the general version is what the shape suggests, and accepting it feels like good taste rather than the opposite. The version I want instead serves the requirement in front of me. A note so the next person can generalise when the requirement arrives.

    The 1973 line still holds. Simplicity is a necessary condition. The cap only enforces the part of it you can count.

    https://prickles.org/tenet/simplicity/F4

    [–] 1 point 1 week ago

    :(

    What does the file change for an agent that already reads the tree? If the answer is "it points at the docs", the agent was going to find them anyway, and if the answer is "it tells it which subsystems it must not touch", that's a policy decision the kernel has never written down for humans either, so who signs off on it. What happens when AGENTS.md and the README disagree and the agent follows the wrong one is the whole argument against putting it in the tree at all.

  • source
  • [–] 1 point 1 week ago

    Nobody says Unix out loud when they write these files, they say what the agent should do when it gets confused. Half of them still read like man pages, which is fine for a reader who already knows the tool and useless for a model meeting it cold. I keep the file terse and specific rather than long and correct, because the length buys nothing. If a convention can't survive being written as one line, the agent was never going to follow it anyway.

  • source
  • [–] 1 point 1 week ago

    Your routing section works because it puts the decision in the instructions rather than in the agent's judgement, which is the only place it can be checked. Most CLAUDE.md files I see list what the agent should do and leave the when to the model. Put the subagent, the trigger and the handoff in one place and they can be reviewed. The rest of the file is a style guide that would survive in any agent's context.

  • source
  •  

    I've watched DRY get applied as a reflex more than as a rule. Two similar lines appear and a helper is extracted, and six months later that helper is a flag for every caller and a name that no longer covers any of them. The failure isn't that the team deduplicated. It's that they deduplicated on appearance and skipped the second sentence Hunt and Thomas wrote, the one with the word knowledge in it.

    Code that looks alike isn't necessarily the same fact. Two loops that happen to iterate the same way for different reasons are two facts. Merge them and the next change to one becomes a change to both, which is exactly the rot DRY exists to stop. The test I use is whether the two copies would have to change together. If they would, they're one fact. If they wouldn't, they're two facts that currently rhyme.

    The boundary matters as much as the count. Inside a bounded context, deduplication is cheap insurance, because both copies answer to the same owner and the same change. Across a context boundary, a shared helper fuses two products into a single change, and now every team that touches one has to think about the other. The coupling costs more than the duplication did.

    So I extract on the second occurrence inside a context. A little copied code is cheaper than a helper with a name that means three things.

    Fair to the reflex: the reflex exists because duplicated knowledge really does rot, and neglected copies drift until there's no way to tell which is right. The rule is sound. The trigger is wrong.

    The full write-up is at https://prickles.org/tenet/dont-repeat-yourself/F3

    [–] 1 point 2 weeks ago (1 child)

    Fair, but I want to push on this one. Modal editing and a config you maintain yourself teach you to think in composable commands, sure, but plenty of people arrive at that thinking from a debugger or a shell and never touch vim. So once the novelty wears off and it's just the tool you happen to open, what then?

  • source
  •  

    I have watched teams treat the agent like a goldfish. Every session opens with the same paragraph in the chat: we use Tailwind, no inline styles, single quotes, named exports, please don't add comments. By the third session someone has pasted it from a Notion page. By the tenth, half the team has a slightly different version. By the thirtieth, two engineers are arguing about which one is right and the agent is following whichever paragraph landed last.

    The fix has been sitting in Anthropic's own engineering writing since 2024. Commit the brief. Put it in the repo, in a file the agent reads at the start of every session, and let it be versioned like everything else the team owns.

    Put the brief under 200 lines. Anthropic's recommendation is load-bearing.

    You read the brief or you do not. That is what the line count decides.

    A brief that grows into a wall of text gets skimmed the way a new joiner skims a 900-line onboarding doc. The paragraphs that matter end up buried under the ones that do not.

    Keeping it short makes the team choose what changes the agent's output, rather than dumping every convention the codebase has ever had.

    The other thing the cap does is make the brief a living document. A short file is cheap to edit, so it gets edited when the convention changes. A long one is not, so it rots, and then someone starts a fresh paragraph in the chat because the committed version no longer reflects reality. That is how you end up with thirty versions again.

    The longer version is at https://prickles.org/tenet/persistent-brief/AI2

     

    I have lost count of the times I have read a call with five positional arguments and had to count commas backwards to work out which one is the timeout and which is the retry count. The fix has been in the catalogue since 1999: introduce a parameter object. Bundle the fields that keep travelling together and give the bundle a name and a type. The call sites stop reading like SQL with the column names removed.

    The trigger I use is the third caller. Two call sites passing the same group can be coincidence. A third one tells you the concept has been in the domain all along without anyone naming it. A third caller passing the same group is the point at which the group wants a name. Waiting for a lint warning misses this, because the warning fires on width and the problem is that the group has no name.

    The cap is editorial and we set it the same way, max-params at 3. But the rule catches the symptom rather than the cause. A constructor taking four loose strings passes nothing and fails everything, while a four-argument call where three of them are genuinely independent is fine. The rule makes you look.

    A parameter object does not automatically improve things. Once a function takes a parameter object, every caller has to build one, and if that object is a bag with no methods you have moved the comma-counting to the construction site. The object should be the place the validation lives, so an invalid one cannot be built. If it is only a struct, you have added a hop.

    Naming the object usually turns up a domain word that was not in the codebase before. That part still surprises me. The extraction is mechanical, but the name is a decision. You were avoiding that decision every time you typed that fifth argument.

    I run the same reasoning on agents now. An agent handed a five-argument signature will keep adding positional arguments, because the shape it sees is the shape it copies. It cannot count commas backwards either.

    The longer version is at https://prickles.org/tenet/parameter-object/S3

     

    Everyone I've worked with agrees that naming matters. Almost nobody spends any time on it. The name gets picked while the code is half written and then it sticks, because renaming feels like fuss once the thing works.

    The check I use in review is to read the function name and the parameters and say in one sentence what the call does. If I can do that without opening the body, fine. If I can't, either the name is bad or the function is doing two jobs and no single name will cover it. In my experience it's the second more often than you'd think, so I run this before I look at length or complexity. It finds the same problem earlier.

    The best names are the ones the product owner already uses. The worst describe the machinery: process, handle, manager, helper, data. Each is a placeholder for a noun nobody has bothered to find yet. The type should agree with the name too. A parameter called id typed as string takes anything. Call it userId with a UserId type and the call site, the signature and the compiler all say the same thing.

    The fair objection is width, and code that borrows abbreviations from the paper it implements. Width is an editor problem. For code that follows a paper, cite the paper next to the module and the abbreviation is the honest name for its readers. For ordinary application code I'd hold the line.

    Agents have made me stricter about this. They abbreviate out of habit and it's easy to wave through proc because the prompt said processing. A developer who meets that later opens the body and works it out. An agent takes the name at face value and builds on it, so one lazy name becomes the vocabulary for everything around it.

    I write these up at https://prickles.org/tenet/intention-revealing-names/F2 if the longer version is any use.

     

    Nobody argues with SRP in principle. Every developer I've worked with agrees a function should do one thing. The argument is always at the example, whether a given function is one thing or six, and that is the part the rule itself doesn't settle.

    Two things have done most of the work for me.

    The name. If you need an "and" to describe what the function does, it's two jobs. Saying it out loud forces the description to be honest, and once you have said "it validates the order and then submits it" the extraction is obvious without anyone arguing about line counts.

    The actor question, from Clean Architecture: who pays when this changes. Two sets of people filing bugs against the same function is a boundary problem however short that function is. Parnas got to the same place in 1972 by listing the design decisions likely to change and giving each one a module that hides it.

    Length and complexity limits catch the same smell and I do run them, but they are proxies. A long function with a single reason to change is fine, and a short one serving two teams isn't.

    This has mattered more since agents started writing the first draft. A developer reading a fuzzy boundary works the intent out from the surrounding code. An agent takes the boundary as given and builds on it.

    I write these up at https://prickles.org/tenet/single-responsibility-principle/F1 if the longer version is useful.

     

    One of the tenets from a software-craft site I've been building. The idea is to keep state and dependencies as local as you can, and only promote something to a wider scope (module, shared, then global) once it's actually used widely enough to earn it. Most of the 'shared' code I've had to untangle started local and got hoisted up a tier too early.

    view more: next ›