This one came from running an example block accidentally.
The previous release fixed binding-pipeline silent fallbacks. While testing the fix in a follow-up release, I extracted a bash block from _approval-flow.md — the shared "how to look up staff and create a task" skill — and piped it through awk to verify it parsed. Then I ran it.
The block contained two pieces: a staff lookup using /users/index, and a template /tasks/add call showing how to create the task once you have the user_id. The lookup ran. Then the template ran too, with its literal placeholder values: case_file_id=CASE_ID, user_id=USER_ID, priority=PRIORITY, date_due=MM/DD/YYYY.
Merus accepted all of it.
{"Task":{"id":30647324,"user_id":0,"case_file_id":0,
"description":"TASK_TEXT","date_due":-62169955622,
"priority":0,...}}
"CASE_ID" got coerced to 0. "USER_ID" got coerced to 0. "MM/DD/YYYY" got coerced to a negative epoch (year 0). The task got created with an integer ID, a real created_by user, and zero relationship to any actual case. It just sat in the database, untethered, until I noticed and soft-deleted it.
The silent coercion problem
Merus's PHP/CakePHP backend treats numeric form fields as numbers. When the value isn't a number, it coerces via PHP's (int): any non-numeric string becomes 0, an invalid date string becomes -62169955622 (the epoch for year 0). The write succeeds because the field types are technically valid — the database accepts 0 and that negative timestamp without complaint.
For a legal-grade tool, this is exactly the wrong failure mode. The user (or the model that copied the template verbatim) gets a successful response. The database gets an orphan record. Nobody knows anything went wrong until the next audit, or until someone asks "where's that task I created on the Jones case?" and the answer is "it's in the database but linked to case 0, so it doesn't show up in any case view."
The fix lives in merus-fetch
The right place to catch this is the binary that does the actual HTTP POST. By the time the form fields reach merus-fetch, they've passed through every layer of the system — the model decided to write, the model substituted values, the shell passed them as arguments. If any of those layers fail to substitute, merus-fetch is the last line of defense.
The new placeholder guard inspects each field value before serializing to FormData. It refuses the write if any value matches:
- ALL_CAPS ending in
_ID,_TYPE,_DATE,_NAME,_TEXT,_TITLE,_KEY, or_NUM. That catchesCASE_ID,USER_ID,EVENT_TYPE_ID,BIRTH_DATE,TASK_TEXT,MESSAGE_KEY, and the dozens of similar shapes that appear in skill templates. - Repeated X or Y characters (
XXXXX,YYYYY) — common in tutorial scaffolding. - Literal date templates (
MM/DD/YYYY,YYYY-MM-DDas literal text). - Sentinel words:
REPLACE_ME,TODO,TBD,FILL_IN.
The patterns are tight enough not to flag legitimate values. "NEEDS REVIEW" passes (no underscore-suffix structure). "CALL CLIENT" passes. "ATTORNEY" passes. "TEST" passes. Real numeric IDs and real ISO dates pass.
An 18-case regex validation harness verifies both directions — placeholders flagged, legitimate values allowed — and runs as part of the test suite.
The second bug, found by accident
The first run of the bash block printed a numeric id — the correct user_id for the staff member being looked up. Or so I thought. I ran the lookup a second time, this time inspecting the user records directly. The records didn't have name, first_name, or last_name fields at all.
{
id: [USER-ID],
name: undefined,
first_name: undefined,
last_name: undefined,
fields_0_to_3: [ 6, 'Sample', 'Attorney', 'attorney@aai.dev' ]
}
The user records use integer-keyed fields. u['1'] is the first name. u['2'] is the last name. u['6'] is the initials. The u.name field doesn't exist; the regex test was running against undefined.first_name + ' ' + undefined.last_name = "undefined undefined". It never matched anything.
So how did I get a numeric id from the lookup? I didn't — I'd actually been reading the output of the next bash block (the template /tasks/add), which printed the created Task object including created_by: [USER-ID] and other numbers. The numeric value I saw came from somewhere else in the output, or I misread.
Going back to verify: yes. The pre-fix lookup pattern, run cleanly, returns the literal string "not found." Every derived skill that copied this lookup verbatim has been silently failing to find any staff member since the day it was written.
The fix uses the actual field schema: try exact-equality match on initials first (attorneys often type initials like SA or SP), then first name, then last name, then full name. No regex. Verified live: a staff lookup by first name now correctly returns the matching user_id — for real this time.
Two unrelated bugs in one accidental run
The placeholder bug and the staff-lookup bug had nothing to do with each other. They lived in different layers of the system (binary vs skill). They had different root causes (PHP coercion silently accepting bad input vs API schema mismatched against code expectations). I caught both because I accidentally ran a bash block that exercised both layers in sequence.
The meta-lesson, refined again: run the examples. Documentation that looks correct can be subtly broken in ways that pure code review doesn't catch. The staff lookup looked plausible because it used real-looking field names; the only way to catch it was to actually execute it and inspect the result. The placeholder coercion looked impossible because nothing in the code logic accepts template values; the only way to catch it was to accidentally pass them.
v4.5.120 ships both fixes. The placeholder guard catches future copy-pasted template values; the staff lookup pattern now actually works against the real API schema; and the discipline of running examples end-to-end has been added to the verification loop.