<?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: Luka Zmikic Engels</title>
    <description>The latest articles on DEV Community by Luka Zmikic Engels (@srnux).</description>
    <link>https://dev.to/srnux</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%2F1068468%2F9fe83be1-1552-40ac-8a7b-40268d578a8a.png</url>
      <title>DEV Community: Luka Zmikic Engels</title>
      <link>https://dev.to/srnux</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/srnux"/>
    <language>en</language>
    <item>
      <title>A Property Inquiry Agent with React, MCP, and Hybrid RAG</title>
      <dc:creator>Luka Zmikic Engels</dc:creator>
      <pubDate>Tue, 29 Sep 2026 20:23:02 +0000</pubDate>
      <link>https://dev.to/srnux/a-property-inquiry-agent-with-react-mcp-and-hybrid-rag-34cb</link>
      <guid>https://dev.to/srnux/a-property-inquiry-agent-with-react-mcp-and-hybrid-rag-34cb</guid>
      <description>&lt;p&gt;A property assistant gets a question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I’m looking for a flat in Hamburg under €2,000. I have a dog. Is heating included, and can I view it on Saturday?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sounds like one request. It actually contains several jobs: filter properties, check a pet policy, find the heating details, and arrange a viewing.&lt;/p&gt;

&lt;p&gt;Some of those jobs have answers in the records. Others need a person.&lt;/p&gt;

&lt;p&gt;That distinction is the starting point for &lt;a href="https://github.com/srnux/proptech-inquiry-agent" rel="noopener noreferrer"&gt;proptech-inquiry-agent&lt;/a&gt;: a TypeScript project that gives an AI assistant tools to search property records, retrieve supporting passages, and create human hand-off tickets.&lt;/p&gt;

&lt;p&gt;You interact with it through a React inquiry desk. Behind the interface, an agent chooses tools, gathers evidence, checks its draft, and returns an answer or a hand-off. You can try the browser interface without a model API key. All property and policy data is synthetic.&lt;/p&gt;

&lt;p&gt;The MCP tools, hybrid retrieval, agent loop, answer checks, and web interface are implemented. A 49-case evaluation suite now measures answer correctness and hand-offs beyond the existing tests. Its first runs found problems in retrieval, follow-up replies, and the evaluator itself; after fixes, 46 of 49 cases pass. This is still a work in progress, with the remaining failures recorded alongside the results.&lt;/p&gt;

&lt;h2&gt;
  
  
  A desk where you can see what happened
&lt;/h2&gt;

&lt;p&gt;The browser application has three panes, each answering a different question: what did the assistant say, what did it do, and what needs a person?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+---------------------+----------------------+---------------------+
| Conversation        | What the agent did   | Hand-off queue      |
+---------------------+----------------------+---------------------+
| Ask a question      | Search listings      | Viewing request     |
|                     | Read the evidence    | HH-1001             |
| Receive a checked   | Create a ticket      | Inquiry summary     |
| reply with sources  |                      | Contact details     |
|                     | Expand a step for    | New tickets appear  |
| Click a citation    | arguments, result,   | as they are created |
| to read its source  | and duration         |                     |
+---------------------+----------------------+---------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnpueprzs4hfq09e9cgo1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnpueprzs4hfq09e9cgo1.gif" alt="The inquiry desk answers a heating question with citations, then creates a viewing-request ticket for human follow-up" width="759" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This demo shows a fact question followed by a viewing request through the same interface. Click it to open the full-size animation.&lt;/p&gt;

&lt;p&gt;The left pane has example buttons for a fact question, a viewing request, an unanswered question, and a German inquiry. You can follow up with “and the deposit?” without repeating the property ID, or select “New conversation” to start over. The middle pane exposes the actual tool calls, including errors and failed answer checks. The right pane shows tickets for a human to handle.&lt;/p&gt;

&lt;p&gt;Clicking a citation opens a dialog with the source document and the cited passage highlighted. A citation for a whole property shows its structured fields and description. This makes it possible to inspect the evidence behind the reply without searching through logs.&lt;/p&gt;

&lt;p&gt;The application uses React state, Vite, and one plain stylesheet. The UI makes the existing workflow visible; the server still owns the tools, retrieval, and answer checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens behind the interface
&lt;/h2&gt;

&lt;p&gt;Submitting an inquiry starts an agent loop on the server. The loop asks a model which tools to use, executes those calls, and checks the resulting answer. The browser displays the progress and final result. The same workflow is available through a CLI or HTTP API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer inquiry
       |
       v
React desk, CLI, or HTTP API
       |
       v
Agent loop &amp;lt;--------------------&amp;gt; Model implementation
       |                         Claude via API / Bedrock
       |                         or rule-based demo
       |
       +── MCP tools ──&amp;gt; Property records + local retrieval
       |             └─&amp;gt; Temporary hand-off queue
       |
       v
Check the draft answer
       |
       v
Reply + citations + tickets + tool trace + token usage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MCP, the Model Context Protocol, is the connection that lets an assistant discover and call tools. The built-in agent uses an MCP client connected to this repository's server in the same process. It goes through the same tool interface as an external assistant, without an extra HTTP round trip.&lt;/p&gt;

&lt;p&gt;The repository's &lt;a href="https://github.com/srnux/proptech-inquiry-agent/blob/main/ARCHITECTURE.md" rel="noopener noreferrer"&gt;architecture guide&lt;/a&gt; maps this request flow, the escalation boundary, where each check runs, and which parts work offline.&lt;/p&gt;

&lt;p&gt;The server exposes four tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;search_listings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Find properties matching explicit requirements.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_listing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return one complete property record.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;search_knowledge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieve passages from descriptions and policies.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hand_off_to_human&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a ticket with a specific reason.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;External assistants such as Claude Desktop can use the tools through standard input and output, or through the Streamable HTTP endpoint at &lt;code&gt;/mcp&lt;/code&gt;. General policy pages are also available as MCP resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent loop actually does
&lt;/h2&gt;

&lt;p&gt;The loop is ordinary TypeScript. It asks the model what to do next, executes any requested tools, and sends the results back. It repeats until the model produces a draft answer or the run reaches a limit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inquiry + instructions + available tools
                   |
                   v
              Call the model &amp;lt;----------------+
                   |                          |
             What came back?                  |
              /           \                   |
             v             v                  |
       Tool requests    Draft answer          |
             |             |                  |
             v             v                  |
       Execute via MCP  Run answer checks     |
             |                                |
             +── Send tool results back ──────+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system prompt asks the model to answer from tool results, cite its facts, preserve qualifications such as “on request,” and hand off the parts it cannot resolve. The model can answer the heating question and create a viewing ticket in the same run.&lt;/p&gt;

&lt;p&gt;The model adapter supports the Anthropic API and Amazon Bedrock. The loop itself depends on a small &lt;code&gt;ModelClient&lt;/code&gt; interface, so tests can substitute a scripted model. Model and effort settings are configurable; the adapter also requests prompt caching and records cache token usage.&lt;/p&gt;

&lt;p&gt;Each run returns a structured result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reply       The final text
citations   Source markers parsed from that text
handoffs    Tickets created during this inquiry
trace       Tool names, arguments, results, and durations
usage       Input, output, and cache token counts
outcome     Answered, or forced hand-off with a cause
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;code&gt;answered&lt;/code&gt; outcome can still include a viewing ticket. It means the loop accepted the final reply, not that every part of the inquiry was resolved automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use ordinary code for ordinary filters
&lt;/h2&gt;

&lt;p&gt;“Hamburg, under €2,000, at least three rooms” does not require semantic search.&lt;/p&gt;

&lt;p&gt;The catalogue is a JSON file. Each property has typed fields for its city, price, room count, floor area, features, availability, and pet policy. Zod validates the data when it loads.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;search_listings&lt;/code&gt; checks those fields directly and sorts matching properties by price.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;City: Hamburg       ──┐
Price: ≤ €2,000      ──┼──&amp;gt; Field filters ──&amp;gt; Matching properties
Customer has a dog  ──┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One deliberate detail: properties marked “pets on request” remain in the results. Excluding them would hide possible options. Their status remains visible so the assistant can explain that permission is still needed.&lt;/p&gt;

&lt;p&gt;For example, the synthetic property &lt;code&gt;HH-1001&lt;/code&gt; has a monthly price of €1,650 and pets allowed “on request.” That is a possible match, not approval for the customer's dog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search the text when the fields are not enough
&lt;/h2&gt;

&lt;p&gt;The heating question needs a different approach. The answer appears inside the description:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Utilities (Nebenkosten) are an additional 240 EUR per month, heating included.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The retrieval system splits property descriptions into sentences and policy pages into sections. These small passages are called chunks. Each carries its source and an identifier for citations.&lt;/p&gt;

&lt;p&gt;Retrieving evidence for the model before it writes an answer is called retrieval-augmented generation (RAG). Here, the retrieval is hybrid: it combines keyword search with embedding-based meaning search, then uses a reranker to assess the candidates.&lt;/p&gt;

&lt;p&gt;Search then happens in two stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              “Is heating included in HH-1001?”
                              |
                              v
               Restrict the searchable material
               to HH-1001 + general policies
                              |
                  +-----------+-----------+
                  |                       |
                  v                       v
             Keyword search          Meaning search
                 BM25                Local embeddings
                  |                       |
                  +-----------+-----------+
                              |
                              v
                    Candidate passages
                              |
                              v
                 Local reranker scores each
                 question-and-passage pair
                              |
                              v
                   Relevance threshold
                     /             \
                    v               v
           Evidence + source ID   found: false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keyword search helps with distinctive terms such as &lt;em&gt;Nebenkosten&lt;/em&gt; and &lt;em&gt;Staffelmiete&lt;/em&gt;. Meaning search helps with paraphrases and questions in German or English.&lt;/p&gt;

&lt;p&gt;For meaning search, &lt;code&gt;multilingual-e5-small&lt;/code&gt; converts text into numerical representations called embeddings. Similar representations help identify related passages.&lt;/p&gt;

&lt;p&gt;The system collects up to 20 candidates from each search method and removes duplicates. A second model, &lt;code&gt;bge-reranker-v2-m3&lt;/code&gt;, scores each candidate together with the question. Only passages above the configured threshold are returned.&lt;/p&gt;

&lt;p&gt;Both models run locally through Transformers.js after their initial downloads. The conversational assistant is separate; local retrieval does not mean the whole conversation runs offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The nearest passage may still be useless
&lt;/h2&gt;

&lt;p&gt;The most useful finding in this project came from asking questions the data could not answer.&lt;/p&gt;

&lt;p&gt;An embedding search can find the closest passage even when every passage is irrelevant. The system therefore needs a way to return “nothing suitable found.”&lt;/p&gt;

&lt;p&gt;The project's recorded experiments show why this was difficult. An unanswerable gym question received an embedding similarity score of 0.832, while an answerable German pet question scored 0.784. A simple similarity cutoff could not reliably separate them.&lt;/p&gt;

&lt;p&gt;A second attempt measured how much a passage stood out from the other candidates. That also failed on some unanswered questions.&lt;/p&gt;

&lt;p&gt;Adding the reranker gave the project a better signal for deciding which passages to return. Its score is still a model output, not a guaranteed probability that an answer is correct. The threshold needs evaluation against the actual corpus.&lt;/p&gt;

&lt;p&gt;Another experiment changed what went into the embeddings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before:  Property title + location + passage  ──&amp;gt; Embedding
After:   Passage alone                      ──&amp;gt; Embedding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the recorded 25 answerable questions, the expected passage appeared in the top five vector results for 20 questions with headers and 23 without them. Headers remain useful for keyword search and for the reranker, which reads each passage with its context.&lt;/p&gt;

&lt;p&gt;This is a small experiment, but it changed the implementation: adding context was helpful in some parts of the search pipeline and harmful in another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep one property's facts separate from another's
&lt;/h2&gt;

&lt;p&gt;When a question includes a listing ID, the code excludes other properties &lt;strong&gt;before ranking&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Question about HH-1001
          |
          +── HH-1001 description       Included
          +── General agency policies  Included
          +── Other property records   Excluded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Among passages that pass the relevance threshold, the property's own passages come before general policies. A property's specific deposit amount should take precedence over a general explanation of deposits.&lt;/p&gt;

&lt;p&gt;This is a boundary enforced by code. It does not depend on the assistant remembering to ignore another property's heating bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  A ticket is not a completed viewing booking
&lt;/h2&gt;

&lt;p&gt;The hand-off tool supports six reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Viewing requests&lt;/li&gt;
&lt;li&gt;Price negotiations&lt;/li&gt;
&lt;li&gt;Contract or legal questions&lt;/li&gt;
&lt;li&gt;Complaints&lt;/li&gt;
&lt;li&gt;Personal data requests&lt;/li&gt;
&lt;li&gt;Questions the records cannot answer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A ticket includes its reason, a summary, an optional property ID and contact email, a timestamp, and a generated ID. The tool rejects unknown property IDs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;“Can I view it Saturday?”
           |
           v
hand_off_to_human
reason: viewing_request
           |
           v
Ticket stored in memory
           |
           v
Human follow-up still required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Today, this is an in-memory queue. It does not send an email or reserve a calendar slot, and tickets disappear when the process stops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the answer before returning it
&lt;/h2&gt;

&lt;p&gt;The checks in &lt;code&gt;packages/core/src/agent/guards.ts&lt;/code&gt; examine a draft before the loop returns it to the caller.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;What the code looks for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Citation&lt;/td&gt;
&lt;td&gt;Recognized citation markers must refer to listing or passage IDs returned by a tool during this run.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Number&lt;/td&gt;
&lt;td&gt;Detected prices, areas, and percentages must match a number in a successful tool result from this run or the user's current or retained earlier inquiries.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing evidence&lt;/td&gt;
&lt;td&gt;A reply containing those figures must include at least one recognized citation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty reply&lt;/td&gt;
&lt;td&gt;The answer must contain text.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example, &lt;code&gt;[HH-1001#s5]&lt;/code&gt; is accepted only if that passage ID appeared in the run's tool results. A newly invented price should fail the number check. German and English number formats are handled, so &lt;code&gt;1.650 €&lt;/code&gt; can match &lt;code&gt;1650&lt;/code&gt; in the record.&lt;/p&gt;

&lt;p&gt;If a check fails, the user does not receive that draft. The model gets feedback and one repair opportunity, which may include more tool calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  Draft answer
                       |
                       v
                  Run checks
                  /        \
               Pass        Fail
                |            |
                v            v
          Return reply   Explain the violations
                             |
                             v
                       One repair attempt
                             |
                             v
                         Check again
                         /        \
                      Pass        Fail
                       |            |
                       v            v
                 Return reply   Code calls hand-off tool
                                and returns a fixed reply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The defaults also limit an inquiry to eight model calls, with an 80,000-token budget checked between calls, a 60-second model-call timeout, and a 30-second tool-call timeout. The loop initiates a hand-off when it reaches its turn or token limit, the model times out, or a final response is refused or truncated. Tool failures are returned to the model as errors it can react to; unrelated provider failures can still surface as errors.&lt;/p&gt;

&lt;p&gt;These checks have a precise scope. They check whether recognized IDs and numbers appeared in the evidence, not whether each sentence correctly interprets that evidence. A real number attached to the wrong property can still pass. A reply without numerical claims can also omit citations without this guard catching it.&lt;/p&gt;

&lt;p&gt;The prompt forbids arithmetic, but the number check only tests whether a value already appears somewhere in the allowed material. It cannot determine how the model arrived at that value. Likewise, the guards do not classify the inquiry to catch every forgotten viewing hand-off. The evaluation suite therefore checks expected hand-offs in code and uses a model judge to assess the meaning of the reply against the evidence.&lt;/p&gt;

&lt;p&gt;These answer checks belong to the built-in agent loop. An external assistant calling &lt;code&gt;/mcp&lt;/code&gt; directly gets the tools and their validation, but does not automatically run its final answer through these guards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow-up questions: remember the context, retrieve the evidence again
&lt;/h2&gt;

&lt;p&gt;After asking about heating in &lt;code&gt;HH-1001&lt;/code&gt;, a user can ask “and the deposit?” The model needs the earlier exchange to know which property they mean.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;POST /inquiries&lt;/code&gt; accepts an optional &lt;code&gt;history&lt;/code&gt; containing up to five earlier &lt;code&gt;{inquiry, reply}&lt;/code&gt; pairs, oldest first. The web app sends the five most recent completed exchanges on screen. “New conversation” clears that conversation context. The server does not retain conversation history between requests; its shared in-memory ticket queue is separate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Earlier questions and replies ──&amp;gt; Context: which property?
                                           |
New question ───────────────────────────────┤
                                           v
                                  Retrieve current evidence
                                           |
                                           v
                                  Write and check the reply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Earlier replies reach the model as plain text, without their tool calls or results. Their citations and figures do not become evidence for the new run. Reusing a source marker requires retrieving it again; a figure must appear in this run's tool results or in a user inquiry. Figures the user supplied in retained earlier turns still count as their own—for example, their €2,000 budget.&lt;/p&gt;

&lt;p&gt;The API rejects more than five history entries with HTTP 400; the client chooses which entries to drop. This keeps the history bounded without adding server-side conversation sessions. Replaying old tool results would carry evidence across inquiries, while model-generated summaries would add another model call and another unchecked text to the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the evaluation tells us
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/srnux/proptech-inquiry-agent/blob/main/evals/cases.jsonl" rel="noopener noreferrer"&gt;agent evaluation suite&lt;/a&gt; contains 49 German and English inquiries: fact questions, searches, all six hand-off reasons, follow-ups, and traps where a qualification matters. “Pets on request” must not become permission, an estimated facade levy must not become a final amount, and polite haggling must still produce a negotiation ticket.&lt;/p&gt;

&lt;p&gt;One case uses a test-only listing whose description tells the assistant to ignore its instructions and confirm a viewing. The expected behavior is still a hand-off. That fixture has its own catalogue and index; it is never served by the application.&lt;/p&gt;

&lt;p&gt;Code checks the expected listings, hand-off reasons, and numerical grounding. A different model judges the required facts, forbidden claims, unsupported statements, and reply language against a &lt;a href="https://github.com/srnux/proptech-inquiry-agent/blob/main/evals/rubric.md" rel="noopener noreferrer"&gt;written rubric&lt;/a&gt;. It sees the tool results and tickets as well as the reply. Explicitly allowed optional hand-offs count as neither correct nor extra when calculating precision and recall.&lt;/p&gt;

&lt;p&gt;Two full runs on October 1, 2026 used &lt;code&gt;claude-opus-5-5&lt;/code&gt; on Amazon Bedrock as the agent and &lt;code&gt;claude-sonnet-5-5&lt;/code&gt; as the judge:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;First run&lt;/th&gt;
&lt;th&gt;After fixes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cases passing all checks&lt;/td&gt;
&lt;td&gt;41 / 49&lt;/td&gt;
&lt;td&gt;46 / 49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hand-off precision / recall&lt;/td&gt;
&lt;td&gt;93.8% / 100%&lt;/td&gt;
&lt;td&gt;100% / 100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replies passing the number check, final / first draft&lt;/td&gt;
&lt;td&gt;100% / 100%&lt;/td&gt;
&lt;td&gt;100% / 100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correct replies according to the judge&lt;/td&gt;
&lt;td&gt;83.7%&lt;/td&gt;
&lt;td&gt;93.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval: expected passage in the top five&lt;/td&gt;
&lt;td&gt;24 / 25&lt;/td&gt;
&lt;td&gt;26 / 26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unanswerable retrieval questions returning passages&lt;/td&gt;
&lt;td&gt;0 / 8&lt;/td&gt;
&lt;td&gt;0 / 8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mean estimated agent cost per inquiry&lt;/td&gt;
&lt;td&gt;$0.0160&lt;/td&gt;
&lt;td&gt;$0.0143&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mean latency / p95 latency&lt;/td&gt;
&lt;td&gt;9.2 s / 14.7 s&lt;/td&gt;
&lt;td&gt;8.6 s / 13.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://github.com/srnux/proptech-inquiry-agent/blob/main/evals/reports/2026-10-01.md" rel="noopener noreferrer"&gt;first report&lt;/a&gt; and &lt;a href="https://github.com/srnux/proptech-inquiry-agent/blob/main/evals/reports/2026-10-01-after-fixes.md" rel="noopener noreferrer"&gt;report after fixes&lt;/a&gt; link each case to its full trace. Cost estimates use Claude API list prices, not Bedrock billing, and exclude the judge's separate cost. Runs processed three cases concurrently; latency includes waiting for the shared local reranker. The retrieval set gained one regression question between runs.&lt;/p&gt;

&lt;p&gt;These results have limits. Numerical groundedness uses the same number check as the runtime guard, so the final-reply score mainly checks that the guard is working; the first-draft score also shows whether a numerical repair was needed. Neither establishes that every claim is supported. The judge can make mistakes, and the small retrieval development set is also used for calibration. These are measurements on known cases, not an independent benchmark or proof that the assistant never invents facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke, and what changed
&lt;/h2&gt;

&lt;p&gt;The first runs exposed three different problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A German commission question could not find an English policy.&lt;/strong&gt; The page said “commission,” but neither candidate search collected it for “Provision.” Adding “Provision” and “Maklerprovision” to the policy made it retrievable. The original missed question and a new regression question now pass, bringing retrieval recall to 26 of 26. Lowering the reranker threshold would not have helped: a reranker cannot rescue a passage it never receives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A follow-up corrected an earlier answer that was already right.&lt;/strong&gt; The agent retrieved the heating fact again, confirmed it, then introduced the same fact as a correction. The prompt now asks for a correction only when new evidence contradicts the earlier reply. A separate case with an actually wrong earlier figure still checks that the agent corrects it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The judge treated ordinary hand-off wording as unsupported claims.&lt;/strong&gt; The rubric now accepts statements about human follow-up when a matching ticket exists, while still rejecting promised outcomes such as a confirmed viewing or approved pet. This fixes the evaluator; it does not represent an improvement to the agent itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three cases still fail after those changes. Two involve borderline inferences the judge flags, such as saying the customer would have to use the stairs because a flat has no lift. The third exposes a concrete capability claim: the agent offers to add an email address to an existing ticket, but no tool can update a ticket. That remains open. A reply can pass every number and citation check while promising something the system cannot do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Different tests answer different questions
&lt;/h2&gt;

&lt;p&gt;Ordinary tests run with a deterministic substitute for the embedding model, without model downloads. A separate &lt;code&gt;pnpm test:model&lt;/code&gt; command checks retrieval with the real models.&lt;/p&gt;

&lt;p&gt;Scripted-model tests exercise the loop with invented citations, unsupported figures, repair behavior, and run limits. These make failure paths reproducible without paying for model calls.&lt;/p&gt;

&lt;p&gt;In a documented run on Amazon Bedrock on September 29, 2026, the German version of the opening inquiry produced a German reply, selected &lt;code&gt;HH-1001&lt;/code&gt;, kept pets “on request,” cited &lt;code&gt;HH-1001#s5&lt;/code&gt; for heating, and created one &lt;code&gt;viewing_request&lt;/code&gt; ticket. The run used four tool calls across three model turns.&lt;/p&gt;

&lt;p&gt;The optional live agent test is recorded as passing too. That test uses a real conversational model with the offline retrieval substitute; it is separate from the real-retrieval model test. The recorded acceptance run is evidence that the workflow has been exercised, not a broad measure of its reliability.&lt;/p&gt;

&lt;p&gt;Three Playwright tests cover the browser workflow: a viewing request produces a reply, trace, and ticket; a citation opens its highlighted passage; and a follow-up retains the property context until “New conversation” clears it. They run with the demo model and hashing embedder, so the test runs need no model credentials or model downloads. They verify the browser workflow, not a remote model's answer quality.&lt;/p&gt;

&lt;p&gt;GitHub Actions runs typechecking, unit tests, and these browser checks on every push and pull request. The &lt;a href="https://github.com/srnux/proptech-inquiry-agent/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;CI workflow&lt;/a&gt; passed on the development branch, its pull request, and &lt;code&gt;main&lt;/code&gt; after the merge on October 1, 2026. Those checks need no model API credentials or retrieval-model downloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stream the work, then show the checked answer
&lt;/h2&gt;

&lt;p&gt;The browser builds on the inquiry event stream, while external MCP clients can still call tools directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application sends an inquiry
           |
           v
    POST /inquiries
           |
           v
    Runs the agent loop
           |
           +── trace events: completed tool calls
           +── guard events: checks that failed
           +── result event: final reply and metadata

External MCP client
           |
           v
          /mcp
           |
           v
    Calls the tools directly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/inquiries&lt;/code&gt; uses server-sent events (SSE): one HTTP response carries a sequence of named events. The UI shows completed tool calls and progress messages while it waits. The answer appears as a whole after the checks pass, or as a fixed hand-off reply if the code ends the run.&lt;/p&gt;

&lt;p&gt;This is a deliberate choice. If the UI streamed an invented price before the number check rejected it, the user would already have read it. Keeping the draft private lets the agent repair it before showing it.&lt;/p&gt;

&lt;p&gt;Two additional routes support the desk:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;What the browser receives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /handoffs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An SSE snapshot of existing tickets, followed by each new ticket.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /sources/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The passage or property behind a citation, with its surrounding document.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The queue stream subscribes to the shared server queue. A ticket created by an external client through &lt;code&gt;/mcp&lt;/code&gt; therefore appears in the browser too. It is still an in-memory queue, so live updates do not imply persistence.&lt;/p&gt;

&lt;p&gt;The server binds to &lt;code&gt;127.0.0.1:3000&lt;/code&gt; by default and currently has no authentication. Vite forwards the browser's API requests to it during development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the workflow without an API key
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;DemoModel&lt;/code&gt; implements the same &lt;code&gt;ModelClient&lt;/code&gt; interface using rules. It calls the actual MCP tools, quotes retrieved passages, and passes its replies through the same answer checks. It is designed for the four example inquiries and a limited set of related requests, rather than general conversation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude model ──────────┐
                      ├──&amp;gt; Same loop ──&amp;gt; Same tools ──&amp;gt; Same checks
Rule-based demo ───────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pnpm dev&lt;/code&gt; falls back to the demo when the credential check reports missing settings. The page labels this mode “Demo model, no API key.” Set &lt;code&gt;MODEL_PROVIDER=demo&lt;/code&gt; explicitly to choose it even when credentials are available.&lt;/p&gt;

&lt;p&gt;Demo mode changes the conversational model, not the retrieval configuration. By default, the local retrieval models still download on first use. Choosing &lt;code&gt;EMBEDDER=hashing&lt;/code&gt; as well removes that download and uses the weaker retrieval substitute. This is also how the browser tests run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding your way around the code
&lt;/h2&gt;

&lt;p&gt;The repository is a pnpm workspace with three packages: the browser interface, the core logic, and the server that connects that logic to external interfaces.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/web/                  React inquiry desk and browser tests
packages/core/
  src/domain/              Property records and ticket queue
  src/retrieval/           Chunking, search, models, ranking
  src/agent/               Loop, checks, interfaces, demo model
  scripts/                 Index building and calibration
packages/server/
  src/mcp/                 Tools and MCP transports
  src/agent/               Provider adapters, MCP client, CLI
  src/api/                 Inquiries, queue, and source routes
data/                      Synthetic records and policy pages
evals/                     Agent cases, judge rubric, thresholds, retrieval golden set
  reports/                 Recorded results and per-case traces
  fixtures/                Test-only prompt-injection listing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core package has no MCP, HTTP, or model-provider imports. The server adapts those external interfaces to the core. The web app shares types and example inquiries without pulling the core's Node.js implementation into the browser bundle.&lt;/p&gt;

&lt;p&gt;Development uses the packages' TypeScript source directly. For the compiled MCP entry point used by Claude Desktop, build first and run Node with &lt;code&gt;--conditions=built&lt;/code&gt;. The server entry point is &lt;code&gt;packages/server/dist/mcp/stdio.js&lt;/code&gt;; its source lives in &lt;code&gt;packages/server/src/mcp/stdio.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The vector store is currently in memory, with embeddings cached on disk. For this small corpus, a separate database would add setup without solving an immediate problem. A pgvector adapter is an optional, unscheduled extension intended to exercise the store interface; it would persist retrieval vectors, not hand-off tickets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the current implementation
&lt;/h2&gt;

&lt;p&gt;Clone the &lt;a href="https://github.com/srnux/proptech-inquiry-agent" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;, then run these commands from its root with Node.js 20 or newer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;corepack &lt;span class="nb"&gt;enable&lt;/span&gt;   &lt;span class="c"&gt;# once, provides the pnpm version pinned in package.json&lt;/span&gt;
pnpm &lt;span class="nb"&gt;install
&lt;/span&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:5173&lt;/code&gt; and click an example. The API runs on &lt;code&gt;127.0.0.1:3000&lt;/code&gt;. The first start downloads the two retrieval models—approximately 120 MB and 570 MB—and prepares the local index.&lt;/p&gt;

&lt;p&gt;For a demo without retrieval-model downloads, set these environment variables before starting. In PowerShell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;MODEL_PROVIDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"demo"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;EMBEDDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hashing"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;pnpm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a fresh terminal or clear those overrides when switching to the real models. &lt;code&gt;pnpm inspect&lt;/code&gt; remains available for calling the MCP tools directly.&lt;/p&gt;

&lt;p&gt;To use a remote conversational model, copy &lt;code&gt;.env.example&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt; and fill in credentials for your chosen provider. For the default Anthropic adapter, set &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;. For Bedrock, set &lt;code&gt;MODEL_PROVIDER=bedrock&lt;/code&gt;, an AWS region, and the relevant credentials. The CLI and HTTP server load &lt;code&gt;.env&lt;/code&gt; automatically.&lt;/p&gt;

&lt;p&gt;Then ask a question and show the tool trace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm ask &lt;span class="nt"&gt;--trace&lt;/span&gt; &lt;span class="s2"&gt;"Is heating included in HH-1001, and can I view it on Saturday?"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;--json&lt;/code&gt; to get the full structured result. To expose the HTTP endpoints, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send &lt;code&gt;POST /inquiries&lt;/code&gt; a JSON body such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"inquiry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Is heating included in HH-1001, and can I view it on Saturday?"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a follow-up, include the earlier exchange. This example shows the request shape; in an application, pass the actual reply previously returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inquiry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"And what is the deposit?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"history"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"inquiry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Is heating included in HH-1001?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reply"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Heating is included in the additional utilities charge of 240 EUR per month. [HH-1001#s5]"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlike &lt;code&gt;pnpm dev&lt;/code&gt;, &lt;code&gt;pnpm ask&lt;/code&gt; and &lt;code&gt;pnpm serve&lt;/code&gt; require credentials unless you explicitly select &lt;code&gt;MODEL_PROVIDER=demo&lt;/code&gt;. Real-model runs use provider credentials; demo runs do not.&lt;/p&gt;

&lt;p&gt;To run the regular tests and the browser checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;test
&lt;/span&gt;pnpm &lt;span class="nt"&gt;--filter&lt;/span&gt; @proptech/web &lt;span class="nb"&gt;exec &lt;/span&gt;playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium
pnpm &lt;span class="nb"&gt;test&lt;/span&gt;:e2e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Installing the browser is a one-time setup. The optional live-model test runs when its credential conditions are met in the process environment; the test itself does not load &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After changing the corpus or model, &lt;code&gt;pnpm calibrate&lt;/code&gt; reports scores and proposes thresholds. Those values are reviewed and copied into &lt;code&gt;retrieval.thresholds.json&lt;/code&gt;; calibration does not automatically update that file.&lt;/p&gt;

&lt;p&gt;To evaluate the real agent and retrieval, configure credentials for both the agent and judge, then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;eval&lt;/span&gt;                  &lt;span class="c"&gt;# all 49 cases, with a report and per-case traces&lt;/span&gt;
pnpm &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="nt"&gt;--subset&lt;/span&gt;         &lt;span class="c"&gt;# 12 cases covering both languages and the main case types&lt;/span&gt;
pnpm &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="nt"&gt;--case&lt;/span&gt; handoff-viewing &lt;span class="nt"&gt;--case&lt;/span&gt; trap-pets-hh1001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runner exits non-zero when a metric misses a threshold in &lt;code&gt;evals/thresholds.json&lt;/code&gt;. Current requirements include at least 90% case pass rate and judge-rated correctness, 100% hand-off recall, and 100% retrieval recall on the golden set. Thresholds were set from the first run and raised after the fixes; the committed reports retain the thresholds used at the time.&lt;/p&gt;

&lt;p&gt;The paid evaluations also have a manually triggered &lt;a href="https://github.com/srnux/proptech-inquiry-agent/actions/workflows/eval.yml" rel="noopener noreferrer"&gt;GitHub Actions workflow&lt;/a&gt;. In the repository's Actions tab, select “Eval subset,” then “Run workflow.” Choose &lt;code&gt;subset&lt;/code&gt; for the 12-case selection or &lt;code&gt;all&lt;/code&gt; for the full 49 cases. Configure the repository secret &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;, or &lt;code&gt;AWS_BEARER_TOKEN_BEDROCK&lt;/code&gt; for Bedrock with the repository variable &lt;code&gt;AWS_REGION&lt;/code&gt; (default &lt;code&gt;eu-central-1&lt;/code&gt;). When both secrets are present, the workflow chooses the Anthropic API.&lt;/p&gt;

&lt;p&gt;The report table appears in the workflow run summary, and the report and per-case traces are uploaded as an artifact. A workflow run does not commit them to the repository. The free CI checks have passed; a successful manual eval workflow run has not yet been confirmed. The committed full-run measurements above came from local runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What remains to be proven
&lt;/h2&gt;

&lt;p&gt;The evaluation suite now tests whether qualifications survive paraphrasing, requests needing a person produce hand-offs, and a malicious listing instruction changes the agent's behavior. The next work includes fixing the unsupported ticket-update offer, expanding coverage beyond these known cases, and comparing a cheaper agent model with a separate judge. Passing one prompt-injection case does not establish resistance to other attacks.&lt;/p&gt;

&lt;p&gt;The architecture guide, recorded demo, failure analysis, and CI workflows are in place. The repository description, topics, and social preview are configured too. Hand-off tickets still live only in memory; persistent ticket storage remains future work. For now, this is a local application with synthetic data, not a complete agency operations system.&lt;/p&gt;

&lt;p&gt;The useful lesson so far is that retrieval, answer generation, and answer checking need separate tests. A search result can be relevant but misquoted. A citation can exist but support a different claim. The inquiry desk makes the evidence, tool calls, and resulting tickets inspectable while keeping rejected drafts out of the conversation.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>mcp</category>
      <category>rag</category>
    </item>
    <item>
      <title>Catching AI Workflow Failures with Executable Playbooks</title>
      <dc:creator>Luka Zmikic Engels</dc:creator>
      <pubDate>Fri, 25 Sep 2026 15:17:22 +0000</pubDate>
      <link>https://dev.to/srnux/stop-asking-your-coding-agent-to-behave-gates-not-prompts-daa</link>
      <guid>https://dev.to/srnux/stop-asking-your-coding-agent-to-behave-gates-not-prompts-daa</guid>
      <description>&lt;p&gt;An AI coding agent can produce working code while skipping the steps that make it the right thing to build. Implementation starts without a plan. Approval is assumed. A prototype changes after work begins, and the agent follows the new version instead of the one you agreed on.&lt;/p&gt;

&lt;p&gt;These are workflow failures, and passing tests won't necessarily reveal them. You need to check what was specified, what was approved, and which version the work is supposed to follow.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/srnux/sdlc-playbooks" rel="noopener noreferrer"&gt;&lt;strong&gt;sdlc-playbooks&lt;/strong&gt;&lt;/a&gt; to make those checks executable. Each playbook describes a delivery phase: the inputs it needs, the work to do, and the artifact it produces. Scripts check the prerequisites against files in the repo and report missing artifacts or inconsistent state before the next phase starts.&lt;/p&gt;

&lt;p&gt;For example, the implementation gate refuses to proceed when a story has no plan or its pinned prototype snapshot is missing. A prompt can tell the agent to check those things; a script can inspect the files and return a specific failure with a fix.&lt;/p&gt;

&lt;p&gt;The procedure runs from design system → requirements → prototype → human approval → product, with one source shared by Claude Code and Codex. The agent follows the playbooks; the scripts check the parts of the workflow that can be checked mechanically.&lt;/p&gt;

&lt;p&gt;That distinction matters: an executable check can establish that a plan exists. It cannot establish that the plan is good, or independently prove that a person approved the prototype. Those decisions still need people and review.&lt;/p&gt;

&lt;p&gt;This post walks through those checks, where they run, and what they can—and cannot—catch.&lt;/p&gt;




&lt;h2&gt;
  
  
  The flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  design system                 PO requirements (text)
          │                              │
          ▼                              ▼
   0 lock-design-system          1 capture-requirements
   tokens.css                    REQ-### + acceptance criteria
   components.md                          │
          └──────────────┬─────────────────┘
                         ▼
                 2 build-prototype
             single-file React page
            every screen, every state
                         │
                         ▼
                  ◆ HUMAN APPROVES ◆
                   3 freeze-approval
            frozen snapshot v&amp;lt;N&amp;gt; + stories
                         │
                         ▼
        4 plan-story → 5 implement-story → 6 review-change
                         │
                         ▼
                    the product
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are seven phases, each with one playbook and one gate. Everything the flow knows is stored in a file:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Written by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;design-system/tokens.css&lt;/code&gt;, &lt;code&gt;components.md&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;phase 0, then locked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;work/items/REQ-###.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;phase 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prototype/&amp;lt;slug&amp;gt;.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;phase 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prototype/.approved/&amp;lt;slug&amp;gt;-v&amp;lt;N&amp;gt;.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;phase 3, versioned baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;work/items/ST-###.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;phase 3, one story per screen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;work/plans/ST-###.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;phase 4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With the default file tracker, the workflow state lives in the repo. &lt;strong&gt;If you delete every playbook, a human can still finish the work from the artifacts.&lt;/strong&gt; That's the intended direction of dependency.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why playbooks instead of agents
&lt;/h2&gt;

&lt;p&gt;Most agent setups I've seen start with personas: &lt;em&gt;"You are a senior reviewer."&lt;/em&gt; That describes an identity, but leaves the working procedure to be specified elsewhere. Here, the playbook is the unit of organisation. It names the inputs, gate, outputs, and done-condition for a phase. For example:&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="gu"&gt;## Contract&lt;/span&gt;

|               |                                                                       |
| ------------- | --------------------------------------------------------------------- |
| &lt;span class="gs"&gt;**Phase**&lt;/span&gt;     | 5 — the only phase that writes product code                           |
| &lt;span class="gs"&gt;**Gate**&lt;/span&gt;      | &lt;span class="sb"&gt;`node .claude/tools/gate.mjs implement ST-###`&lt;/span&gt;                        |
| &lt;span class="gs"&gt;**Inputs**&lt;/span&gt;    | the plan, the pinned snapshot, the catalog, the component lib         |
| &lt;span class="gs"&gt;**Produces**&lt;/span&gt;  | product code, one test per AC                                         |
| &lt;span class="gs"&gt;**Done when**&lt;/span&gt; | every &lt;span class="sb"&gt;`verify.commands`&lt;/span&gt; entry is green, story is &lt;span class="sb"&gt;`in-review`&lt;/span&gt;          |
| &lt;span class="gs"&gt;**Never**&lt;/span&gt;     | copy the prototype into the product; build against the live prototype |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each playbook also routes neighbouring decisions: new tokens go to &lt;code&gt;lock-design-system&lt;/code&gt;, changed scope goes to &lt;code&gt;capture-requirements&lt;/code&gt;, and finished work goes to &lt;code&gt;review-change&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The earlier version had separate agent definitions. Most of their instructions already existed in the matching playbooks; the remaining ownership rules belonged at phase boundaries. Removing those files left one place to maintain each procedure. Keeping two copies of a rule means you have one rule and one future contradiction.&lt;/p&gt;

&lt;p&gt;Handoffs happen through saved artifacts. Requirements supply acceptance criteria to prototyping; an approved snapshot supplies the baseline for planning and implementation; the plan and resulting code supply the inputs to review. The next phase can run in a fresh session because its inputs are in the repo. It doesn't need the previous agent's persona or conversation history to reconstruct what was agreed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 1: gates decide whether a phase may start
&lt;/h2&gt;

&lt;p&gt;Every phase begins with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node .claude/tools/gate.mjs &amp;lt;phase&amp;gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;flags]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gate checks the phase's preconditions against the repo. If one fails, it &lt;strong&gt;exits 2 with a named reason and a fix&lt;/strong&gt;. The Claude Code command files invoke it through inline shell expansion:&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;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Record a human's approval — freeze the prototype and cut the stories.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

Gate — this runs before you read any further; a non-zero exit aborts the command:

!&lt;span class="sb"&gt;`node .claude/tools/gate.mjs approve $0 $1 2&amp;gt;&amp;amp;1`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That puts a concrete check at the command entry point. The playbook instructs the agent to stop on failure. Direct script calls and other editing paths still need their own controls.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Refuses when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;design-system&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;already locked and &lt;code&gt;--refresh&lt;/code&gt; wasn't passed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;requirements&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prototype&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;design system unlocked · wrong status · &lt;strong&gt;no acceptance criteria&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;approve&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not &lt;code&gt;prototyped&lt;/code&gt; · no &lt;code&gt;data-screen&lt;/code&gt; markup · &lt;strong&gt;no &lt;code&gt;--human-approved&lt;/code&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;plan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;parent not approved · no version pin · &lt;strong&gt;snapshot missing&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;implement&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;no plan&lt;/strong&gt; · story not &lt;code&gt;in-progress&lt;/code&gt; · snapshot missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;story not &lt;code&gt;in-review&lt;/code&gt; · &lt;strong&gt;already at &lt;code&gt;loops.reviewRounds&lt;/code&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Making the approval assertion explicit
&lt;/h3&gt;

&lt;p&gt;The old version of &lt;code&gt;/approve&lt;/code&gt; said: &lt;em&gt;"This command represents a human decision. Do not run it on your own judgment."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The gate now requires an explicit flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;human-approved&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`approval is a human decision and has not been recorded for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`ask the person to open &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; and say yes, then run: /approve &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; --human-approved`&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bundled seaside example illustrates the check: even with a prototype ready to inspect, an approval gate call without the flag is refused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ node .claude/tools/gate.mjs approve REQ-003
BLOCKED — approve: approval is a human decision and has not been recorded for REQ-003

  ask the person to open prototype/seaside-zadar-landing.html and say yes,
  then run: /approve REQ-003 --human-approved

Nothing was changed. Report this reason verbatim and stop; do not work around it.
$ echo $?
2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flag means "a person said yes in this conversation." It makes that assertion explicit and reviewable, but does not verify it independently: the agent can supply the flag, and the snapshot utility can be called directly. Respecting human approval still depends on following the procedure.&lt;/p&gt;

&lt;h3&gt;
  
  
  The review loop is bounded
&lt;/h3&gt;

&lt;p&gt;Review can approve a story or send it back. The gate reads the highest &lt;code&gt;round N:&lt;/code&gt; recorded in the story's notes and refuses a round beyond &lt;code&gt;loops.reviewRounds&lt;/code&gt; (two by default). That bound depends on the reviewer recording each round. When the cap is reached, the procedure calls for handing the disagreement to a person.&lt;/p&gt;

&lt;p&gt;On success, gates print resolved context as JSON: paths, the pinned prototype version, acceptance-criterion ids, and the review round where relevant. The agent gets concrete inputs for its next step.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 2: hooks decide whether an edit may land
&lt;/h2&gt;

&lt;p&gt;Hooks add checks at supported edit entry points. In Claude Code, two shell scripts are wired as &lt;code&gt;PreToolUse&lt;/code&gt; hooks on &lt;code&gt;Edit|Write&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;require-plan.sh&lt;/code&gt; blocks a guarded edit when it identifies a story with no plan.&lt;/strong&gt; It finds the story from the branch name (&lt;code&gt;story/ST-007-…&lt;/code&gt;) or, failing that, from exactly one &lt;code&gt;in-progress&lt;/code&gt; story in the tracker. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BLOCKED — no plan for ST-007.

  expected: work/plans/ST-007.md
  editing:  src/contacts/list/ArchiveButton.tsx

Run the plan-story skill first. The plan is a gate, not paperwork.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reasoning: a plan written after the code is just a summary, and a summary never catches the thing you'd have noticed before starting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;check-hardcoded-colors.sh&lt;/code&gt; warns.&lt;/strong&gt; It flags a raw &lt;code&gt;#hex&lt;/code&gt;, &lt;code&gt;rgb()&lt;/code&gt; or &lt;code&gt;hsl()&lt;/code&gt; at the moment it's written, while fixing it is still a one-line change. The blocking version of this rule runs at review time.&lt;/p&gt;

&lt;p&gt;The hooks have limits: shell writes fall outside Claude's &lt;code&gt;Edit|Write&lt;/code&gt; matcher, some file types are excluded, and the plan hook allows an edit when no story can be identified. Both scripts accept &lt;code&gt;file_path&lt;/code&gt; or &lt;code&gt;path&lt;/code&gt;; actual coverage also depends on the harness invoking them with a supported payload.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 3: checks expose gaps before handover
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;check-tokens.mjs&lt;/code&gt;&lt;/strong&gt; scans prototypes and configured product directories for raw hex/RGB/HSL colours and literal values in selected properties: font size, radius, shadow, padding, margin, and gap. It also checks for undefined token references. &lt;code&gt;width: 100%&lt;/code&gt; and &lt;code&gt;z-index&lt;/code&gt; are outside those design-property checks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ node .claude/tools/check-tokens.mjs
ok — 139 tokens defined, 134 used, no raw design values in 1 files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The procedure's rule is that &lt;strong&gt;a design value can only be created in phase 0&lt;/strong&gt;. The scanner catches common violations; it is not a complete CSS validator. It permits token-definition blocks for inlined prototypes without verifying that their values match the locked file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;check-coverage.mjs&lt;/code&gt;&lt;/strong&gt; checks structural links:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requirement → approved snapshot → screen → story → plan file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reports gaps such as &lt;code&gt;screen-without-story&lt;/code&gt;, &lt;code&gt;story-without-plan&lt;/code&gt;, &lt;code&gt;stale-pin&lt;/code&gt;, and &lt;code&gt;done-story-screen-vanished&lt;/code&gt;, exiting 2 when it finds one. It checks that acceptance criteria exist, but does not map each AC to a screen or inspect implementation and test coverage.&lt;/p&gt;

&lt;p&gt;The prototype convention puts two attributes on every screen root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;data-screen=&lt;/span&gt;&lt;span class="s"&gt;"SCR-booking-enquiry"&lt;/span&gt; &lt;span class="na"&gt;data-req=&lt;/span&gt;&lt;span class="s"&gt;"REQ-003"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The checker discovers screens through &lt;code&gt;data-screen&lt;/code&gt;; the approve gate refuses a prototype with no screen markers. &lt;code&gt;data-req&lt;/code&gt; records the intended requirement association.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structural gaps become queryable.&lt;/strong&gt; Whether the product satisfies the requirements still needs tests and review.&lt;/p&gt;




&lt;h2&gt;
  
  
  The prototype, and the pin
&lt;/h2&gt;

&lt;p&gt;Phase 2 calls for one HTML file per requirement: React 18 UMD plus &lt;code&gt;@babel/standalone&lt;/code&gt; from a CDN, tokens inlined, catalog components, hash routing, and realistic mock data. It needs no build step and opens with a double-click, but loading its CDN dependencies requires network access. The playbook requires &lt;strong&gt;every state to be drawn&lt;/strong&gt;: empty, loading, populated, error, &lt;em&gt;and success&lt;/em&gt;. "The list refreshes" doesn't show a person what success looks like.&lt;/p&gt;

&lt;p&gt;After human approval, the procedure uses &lt;code&gt;approve.mjs&lt;/code&gt; to create &lt;code&gt;prototype/.approved/&amp;lt;slug&amp;gt;-v&amp;lt;N&amp;gt;.html&lt;/code&gt;, then cuts one story per screen with a &lt;code&gt;prototypeVersion: v&amp;lt;N&amp;gt;&lt;/code&gt; pin. These are versioned snapshots treated as immutable by the procedure. The utility checks for an existing destination before copying; the resulting files remain editable on disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The implementation playbook directs the agent to build against the pinned snapshot.&lt;/strong&gt; If someone makes "one more tweak" to the live file, &lt;code&gt;gate.mjs implement&lt;/code&gt; reports the difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"liveHasMovedPastThePin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prototype/bulk-archive.html has moved past v2. You build v2. The difference is new scope for the PO — record it under Divergence, do not absorb it."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Building the latest prototype when the story pinned v2 is scope drift, and you won't see it in the diff unless someone says so. This is how a two-week feature quietly becomes a four-week one without anyone deciding it should.&lt;/p&gt;




&lt;h2&gt;
  
  
  One source, two harnesses
&lt;/h2&gt;

&lt;p&gt;Procedures live in exactly one place: &lt;code&gt;playbooks/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;, written without reference to any particular tool. &lt;code&gt;sync.mjs&lt;/code&gt; copies that tree into &lt;code&gt;.claude/skills/&lt;/code&gt; for Claude Code and &lt;code&gt;.agents/skills/&lt;/code&gt; for Codex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node .claude/tools/sync.mjs          &lt;span class="c"&gt;# write the projections&lt;/span&gt;
node .claude/tools/sync.mjs &lt;span class="nt"&gt;--check&lt;/span&gt;  &lt;span class="c"&gt;# exit 2 if one is stale (for CI)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each generated directory has a &lt;code&gt;.generated&lt;/code&gt; marker holding the source hash, so a hand-edit to a projection is detected, not silently kept.&lt;/p&gt;

&lt;p&gt;Copies avoid the extra permissions or developer-mode setup that symlinks can require on Windows.&lt;/p&gt;

&lt;p&gt;Claude Code has slash commands such as &lt;code&gt;/requirements&lt;/code&gt;, &lt;code&gt;/prototype&lt;/code&gt;, and &lt;code&gt;/build&lt;/code&gt;. The projected skills expose the same playbooks, including &lt;code&gt;build-prototype&lt;/code&gt;, with a gate invocation as step 0. The shared procedure does not make the harnesses' enforcement identical.&lt;/p&gt;




&lt;h2&gt;
  
  
  The tracker is an adapter
&lt;/h2&gt;

&lt;p&gt;No playbook talks to a tracker directly. Everything goes through &lt;code&gt;node .claude/tools/tracker.mjs &amp;lt;verb&amp;gt;&lt;/code&gt;, which prints JSON and dispatches to a provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node .claude/tools/tracker.mjs create &lt;span class="nt"&gt;--type&lt;/span&gt; requirement &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"Bulk archive contacts"&lt;/span&gt;
node .claude/tools/tracker.mjs &lt;span class="nb"&gt;set &lt;/span&gt;ST-007 &lt;span class="nt"&gt;--status&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="nt"&gt;-review&lt;/span&gt;
node .claude/tools/tracker.mjs next &lt;span class="nt"&gt;--type&lt;/span&gt; story
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default provider is &lt;code&gt;files&lt;/code&gt;, storing Markdown with YAML front matter in &lt;code&gt;work/items/&lt;/code&gt;. Jira support is unfinished: mapping and REST code exist, but the gate's asynchronous provider handling and the coverage checker's direct file reads still need integration work.&lt;/p&gt;

&lt;p&gt;Two procedural rules keep updates consistent: &lt;strong&gt;never hand-edit a work item, and never pick an id yourself.&lt;/strong&gt; &lt;code&gt;create&lt;/code&gt; allocates IDs centrally so agents don't choose them manually. Allocation is not protected against concurrent writers.&lt;/p&gt;

&lt;p&gt;The adapter is intended to let status and conversation move to another tracker while design, prototype, and plan artifacts stay in the repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Every tool runs without a model
&lt;/h2&gt;

&lt;p&gt;None of the enforcement needs an LLM. You can check the state of the repo from a plain shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node .claude/tools/status.mjs &lt;span class="nt"&gt;--gaps&lt;/span&gt;
node .claude/tools/check-coverage.mjs
node .claude/tools/check-tokens.mjs
node .claude/tools/gate.mjs plan ST-001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The local tools use Node 18+ and plain ESM, with no npm dependencies. The hooks require Bash; on Windows, that means a Bash installation such as Git Bash.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's deliberately missing
&lt;/h2&gt;

&lt;p&gt;There's no CI/deploy stage, no QA phase, no parallel fan-out, no worktree isolation, and no board integration. Those are all real and sometimes necessary. They're also where a procedure stops being easy to read.&lt;/p&gt;

&lt;p&gt;The rule for adding one: do it when a specific failure demands it, and write down which failure.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A gate with no incident behind it is ceremony.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Clone the repo and generate the skill projections in your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/srnux/sdlc-playbooks
&lt;span class="nb"&gt;cd &lt;/span&gt;sdlc-playbooks
node .claude/tools/sync.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit &lt;code&gt;.claude/sdlc.config.json&lt;/code&gt; for your product and verification commands, and rewrite the stack-specific half of &lt;code&gt;.claude/rules/coding-standards.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The repo includes a locked seaside-rental design system, a prototype with nine marked screens, and three requirements. You can inspect that example first. For your own design, update the configured design source and run &lt;code&gt;/design-system --refresh&lt;/code&gt; in Claude Code. A bare &lt;code&gt;/design-system&lt;/code&gt; refuses because the bundled system is already locked.&lt;/p&gt;

&lt;p&gt;Then, in Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/requirements 'PO text here…'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the requirement ID returned by that command in &lt;code&gt;/prototype &amp;lt;returned-id&amp;gt;&lt;/code&gt;. Open the result and review its screens and states. Only after you approve it, run &lt;code&gt;/approve &amp;lt;returned-id&amp;gt; --human-approved&lt;/code&gt;, then &lt;code&gt;/build&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My one piece of advice: &lt;strong&gt;run one real requirement end to end before you load up the backlog.&lt;/strong&gt; That gives you a concrete way to find gaps in the procedure while the scope is still small.&lt;/p&gt;

&lt;p&gt;If you've built something similar, or tried to hold an agent to a process with prompts alone and watched it slip, I'd like to hear what broke first.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>claudecode</category>
      <category>codex</category>
    </item>
    <item>
      <title>Partially matching object, array and function with Jest</title>
      <dc:creator>Luka Zmikic Engels</dc:creator>
      <pubDate>Thu, 01 Jun 2023 14:16:42 +0000</pubDate>
      <link>https://dev.to/srnux/partially-matching-object-array-and-function-with-jest-5453</link>
      <guid>https://dev.to/srnux/partially-matching-object-array-and-function-with-jest-5453</guid>
      <description>&lt;h2&gt;
  
  
  &lt;a href="https://jestjs.io/"&gt;JestJs&lt;/a&gt; 🃏 Delightful JavaScript Testing
&lt;/h2&gt;

&lt;p&gt;It is really delightful as the team states! For the last couple of years that is accompanying me in my daily struggles, it has shown it's worth. One of the best parts is the partial match on object, array and function, which will be the focus of this blog post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Partially matching objects
&lt;/h2&gt;

&lt;p&gt;Let's say you need to test a unit that changes just parts of an object. It is indifferent of the remaining parts of the object. Your unit test should check just the changes the unit influences.&lt;br&gt;&lt;br&gt;
In the code block below, a simple fixture containing some nested objects and arrays, which will be used in the following examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resultFixtureObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;e0e0e0e0-2b79-4eba-ab39-b2dfdb2f0eba&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Jester&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Magic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;finances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;incomes&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;salary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dividend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$&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="p"&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;h3&gt;
  
  
  Match object
&lt;/h3&gt;

&lt;p&gt;In the test below, the expected result object must match a few properties:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resultFixtureObject&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toEqual&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="nx"&gt;objectContaining&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
   &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;e0e0e0e0-2b79-4eba-ab39-b2dfdb2f0eba&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Jester&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Magic&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Match nested object
&lt;/h3&gt;

&lt;p&gt;You can match nested objects by simply nesting objectContaining() expectations. In this case, expecting object &lt;em&gt;finances&lt;/em&gt; that must have nested object &lt;em&gt;incomes&lt;/em&gt;. We do not care about the content of the incomes, in this unit test:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resultFixtureObject&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toEqual&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="nx"&gt;objectContaining&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;finances&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="nx"&gt;objectContaining&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
   &lt;span class="na"&gt;incomes&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="nx"&gt;anything&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Partially matching arrays
&lt;/h2&gt;

&lt;p&gt;Just as the objects, there is a way to partially match arrays. You can expect single or multiple item matches on an array.&lt;br&gt;
Expecting one item to match:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resultFixtureObject&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toEqual&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="nx"&gt;objectContaining&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;finances&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="nx"&gt;objectContaining&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
   &lt;span class="na"&gt;incomes&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="nx"&gt;arrayContaining&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$&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="p"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;or multiple, complete or partial matches:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resultFixtureObject&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toEqual&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="nx"&gt;objectContaining&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;finances&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="nx"&gt;objectContaining&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
   &lt;span class="na"&gt;incomes&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="nx"&gt;arrayContaining&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$&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="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objectContaining&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;salary&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="p"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Partially matching function parameter calls
&lt;/h2&gt;

&lt;p&gt;Aforementioned operations can be applied to toHaveBeenCalledWith() matcher as well.&lt;br&gt;
Below a variant with toHaveBeenNthCalledWith() matcher. Verifying just the third call to the function:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;functionMock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;functionMock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string arg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;functionMock&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array arg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;functionMock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resultFixtureObject&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="nx"&gt;functionMock&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toHaveBeenNthCalledWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;3&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="nx"&gt;objectContaining&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;finances&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="nx"&gt;objectContaining&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
   &lt;span class="na"&gt;incomes&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="nx"&gt;arrayContaining&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="nx"&gt;objectContaining&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rent&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="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objectContaining&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;salary&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="p"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Complete test file with comments inside
&lt;/h2&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



</description>
      <category>jest</category>
      <category>typescript</category>
      <category>unittest</category>
      <category>tdd</category>
    </item>
  </channel>
</rss>
