{"slug":"tag-audit-and-takeoff","title":"tag-audit-and-takeoff","summary":"Count-based quantity takeoff and tag completeness auditing for construction drawings. Vision + OCR reconciliation, sheet markup, Excel QTO output. Triggers: 'tag audit', 'quantity takeoff', 'QTO', 'count fixtures'.","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-10-19T21:17:04.793182Z","repo":{"url":"https://github.com/dleerdefi/claude-code-construction","stars":44,"forks":13,"license":"MIT","updatedAt":"2026-10-18T06:12:41Z"},"bodyHtml":"<hr>\n<h2>name: tag-audit-and-takeoff\ndescription: &gt;\nCount-based quantity takeoff and tag completeness auditing for construction\ndrawings. Vision + OCR reconciliation, sheet markup, Excel QTO output.\nTriggers: 'tag audit', 'quantity takeoff', 'QTO', 'count fixtures'.\ndisable-model-invocation: true</h2>\n<h1>Tag Audit &amp; Quantity Takeoff</h1>\n<h2>Purpose</h2>\n<p>Count-based QTO for tagged construction elements — fixtures, devices,\ndoors, equipment, and any element identified by a tag/symbol on drawings.\nDoes NOT do area (sqft) or linear (lf) takeoffs — only discrete counts.</p>\n<p>The QTO is a byproduct of the audit. The real value: the completeness\nengine that finds what's tagged, what's missing, and produces a defensible\ncount with full provenance for every number.</p>\n<p>Does NOT: do area/linear takeoffs, create outbound documents autonomously,\noverride user-approved groupings, or assume quantities without provenance.</p>\n<hr>\n<h2>Step 0: Detect Operating Mode</h2>\n<p>Read <code>.construction/project.yaml</code> at the project root: present and readable means AgentCM mode (note its <code>contract_version</code>), absent means flat-file mode. A listing that shows the file does not decide it: read it.</p>\n<p><strong>AgentCM mode</strong> (<code>.construction/project.yaml</code> exists):</p>\n<ul>\n<li>Follow <code>${CLAUDE_PLUGIN_ROOT}/reference/agentcm-mode.md</code>: which source wins, what a missing file or an empty search means, sheet ids, and how to record a correction of AgentCM's data (a missed tag goes through the ingest in Step 8)</li>\n<li>Read <code>.construction/CLAUDE.md</code> for project context and where things are,\nexcept its database section: never run <code>query.sh</code> or <code>query.ps1</code>, whatever\nthe guide offers</li>\n<li>Read <code>.construction/database.yaml</code> for <code>project_id</code> and <code>api_url</code>, nothing else in it</li>\n<li>Everything this skill reads comes from AgentCM's files, never its API, so it\nruns the same with AgentCM closed; only the ingest (Step 8) needs AgentCM</li>\n<li>The <code>{sheet_id}</code> in Step 8's payload is\nthe <code>id</code> of the entry in <code>.construction/index/sheet_index.yaml</code> whose\n<code>sheet_number</code> is that sheet. Never guess it from the number. If that file is\nmissing, or its entries have no <code>id</code> (an older sheet-splitter wrote a <code>pages:</code>\nlist with <code>filename</code> and <code>page_index</code>), say so and stop.</li>\n<li>Look at a sheet through AgentCM's crop script, a box at a time:\n<code>sh .construction/crop.sh &lt;sheet&gt; &lt;x&gt; &lt;y&gt; &lt;w&gt; &lt;h&gt;</code> (or\n<code>powershell -File .construction/crop.ps1 &lt;sheet&gt; &lt;x&gt; &lt;y&gt; &lt;w&gt; &lt;h&gt;</code>) renders\nthat box of the sheet's PDF page as a PNG and prints its path. <code>&lt;sheet&gt;</code> is\nthe sheet's number as <code>.construction/INDEX.md</code> names it; the box is 0 to 1\nfrom the top left of the sheet. Never rasterize whole sheets, and never ask\nfor <code>rasters/</code> or <code>graph/export</code>: they are not part of AgentCM's contract.</li>\n<li>A sheet's text, each line with its box: <code>.construction/sheets/&lt;sheet&gt;.tsv</code> (Steps 3-4)</li>\n<li>Write results back via API: <code>POST {api_url}/projects/{project_id}/tag-detections/ingest</code>, sent from the outbox (Step 8)</li>\n</ul>\n<p><strong>Flat File mode</strong> (no <code>.construction/project.yaml</code>):</p>\n<ul>\n<li>Discover sheet images from CLAUDE.md or user-provided paths; rasterize sheet\nPDFs with <code>\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \"${CLAUDE_PLUGIN_ROOT}/scripts/pdf/rasterize_page.py\" \"&lt;pdf&gt;\" &lt;page&gt; --dpi 200 --output \".construction/skills/tag-audit-and-takeoff/pages/&lt;sheet&gt;.png\"</code>\n(working data: never <code>/tmp</code> or any other folder outside the project)</li>\n<li>Vision-only pipeline (Steps 2-3 skipped)</li>\n<li>Write marked-up PNGs and the Excel workbook to the project directory; the QTO\nJSON goes to <code>.construction/skills/tag-audit-and-takeoff/qto/</code> (Step 8)</li>\n</ul>\n<h2>Step 1: User Scopes the Task</h2>\n<p>User provides: (1) tag type — e.g., \"plumbing fixtures\", \"doors\",\n\"light fixtures\" (see <code>references/tag-types.md</code> for full list), and\n(2) sheet scope — specific sheets, a discipline, or \"all sheets\".\nConfirm scope before proceeding.</p>\n<p><strong>REQUIRED: Tag type resolution (AgentCM mode).</strong> The <code>tag_type</code> sent to\nAgentCM is one of contract 1's canonical names, listed in\n<code>${CLAUDE_PLUGIN_ROOT}/reference/agentcm-mode.md</code> (Tag types), and nothing\nelse. Never invent one, and never send the takeoff's grouping as a type:</p>\n<ul>\n<li>room tags → <code>room_tag</code>; door tags → <code>door_tag</code></li>\n<li>single-reference callouts (detail bubbles, section marks, interior\nelevations; <code>detail_callout</code>, <code>section_callout</code>, <code>callout_single_ref</code>) →\n<code>simple_callout</code>; multi-reference callouts (elevation markers with N/E/S/W\nspokes; <code>multi_callout</code>, <code>callout_multi_ref</code>) → <code>multi_detail_callout</code></li>\n<li>fixtures, devices and equipment (plumbing fixtures, light fixtures,\ndiffusers, any other element tag) → <code>element_tag</code></li>\n</ul>\n<p name=\"\">Report to the user: \"Resolved type: '' (from '')\" and get\nconfirmation. If the request fits none of these, or two, ask the user.</p>\n<p><strong>The takeoff's grouping comes from the files (AgentCM mode).</strong> Plumbing\nfixtures, light fixtures and equipment are groups of the report, never types:\na tag's code (<code>WC-1</code>, <code>L2</code>) belongs to the schedule whose key column holds it,\n<code>.construction/schedules/&lt;sheet&gt;-&lt;n&gt;.tsv</code> (INDEX.md's Schedules and legends\ntable names each), and <code>.construction/tags.tsv</code> gives its <code>kind</code> (the lines\nstarting with the code and a tab). Each line item's <code>group</code> is that schedule,\nnamed as INDEX.md names it, with its sheet; a code found in no schedule is\ngrouped as <code>not in any schedule</code> and listed (<code>references/qto-output-format.md</code>).\nA takeoff of an <code>element_tag</code> group counts the codes of its schedule.</p>\n<h2>Step 1.5: Check Existing Coverage [REQUIRED GATE] (AgentCM mode only)</h2>\n<p><strong>HARD GATE — you MUST complete this step before proceeding to Step 2.</strong>\nDo not skip this step.</p>\n<p>Before scanning sheets, read what's already tagged per sheet, from AgentCM's\nfiles (never its API; the same whether AgentCM is open or closed):</p>\n<ul>\n<li><strong>accepted:</strong> the lines of <code>.construction/sheets/&lt;sheet&gt;.groups.tsv</code> whose\n<code>type</code> is the resolved type (box <code>x y w h</code>, <code>text</code>), and the lines of\n<code>.construction/tags.tsv</code> for that sheet with <code>accepted</code> in <code>how</code> and the\nkind of that tag (<code>room</code> for room_tag, <code>door</code> for door_tag; for element_tag,\nthe codes in the takeoff's scope). A <code>tags.tsv</code> line is a point (<code>x y</code>): one\ninside a <code>.groups.tsv</code> box of the type is that group, the same tag, whatever\nits text's word order (\"101 OFFICE\" there is \"OFFICE 101\" in <code>.groups.tsv</code>).\nOne inside no such box gets the box Step 8 gives a tag: around its own lines\nin the sheet's <code>.tsv</code> (the lines its text is made of, at that point)</li>\n<li><strong>pending:</strong> the lines of <code>.construction/sheets/&lt;sheet&gt;.tsv</code> with the\nresolved type in <code>tag</code> and <code>pending</code> in <code>how</code>, grouped into tags as Step 5\ngroups (\"STORAGE\" over \"202\" is one tag)</li>\n<li><strong>waiting:</strong> the detections in the payloads in\n<code>.construction/skills/tag-audit-and-takeoff/outbox/</code> (an earlier run's, saved\nwhile AgentCM was closed): not yet in AgentCM</li>\n<li><strong>behind:</strong> the sheets <code>.construction/pending.tsv</code> lists, when it exists\n(read its columns by their header names; <code>sheet</code> names the sheet): their\nfiles are behind or were edited outside AgentCM. Every tag on such a sheet is\nto confirm, <code>reason</code> \"this sheet's file is behind or edited\", never\nconfirmed, even when its file says accepted</li>\n</ul>\n<p>Report to user: \"Sheet A-1.4: 12 tags already detected (8 accepted, 4 pending, 0 waiting).\"</p>\n<p><strong>Reconciliation rules (the count, <code>references/qto-output-format.md</code>):</strong></p>\n<ul>\n<li><strong>Confirmed</strong> is the accepted tags, and only those: the accepted existing\ntags, and a tag AgentCM answers is held by an accepted card (Step 8), on a\nsheet <code>pending.tsv</code> doesn't list.</li>\n<li><strong>To confirm in AgentCM</strong> is every tag with a card not yet accepted, or on\nits way to one: the pending existing tags, the tags waiting in the outbox,\nthe tags this run sends that become cards, and the tags on a sheet that is\nbehind. A tag the PE rejected in AgentCM is in no group (Step 8).</li>\n<li><strong>Seen on the drawing, not in AgentCM</strong> is every tag the drawing shows that\nthe sheet's <code>.tsv</code> has no text for (Step 8): no card, so nothing to accept.</li>\n<li><strong>Possible total</strong> is all three.</li>\n<li><strong>One tag, once.</strong> A tag found in this run is an existing tag when it is on\nthe same sheet, its text matches (upper case, spaces collapsed) and one\nbox's center falls inside the other box. Keep the existing tag, with its\n<code>detection_id</code>, and drop this run's sighting of it.</li>\n<li><strong>Send only the new ones:</strong> the tags found in this run that match no\nexisting tag, and nothing else, go to ingest (Step 8). Nothing new, no POST.</li>\n<li><strong>EXCLUDE</strong> from the neighborhood search (Steps 3-4) the lines an existing\ntag already holds: a line whose box's center falls inside an existing tag's\nbox</li>\n<li>AgentCM checks again at ingest: texts a card already holds come back\n<code>already_held</code> with that card, never as a second card (Step 8)</li>\n</ul>\n<p><strong>Run it twice, get the same numbers.</strong> A second run matches its sightings to\nthe first run's tags: pending in AgentCM's files once they are refreshed,\nwaiting in the outbox, or, while the files have not caught up, through\nAgentCM's answer: a tag sent again comes back <code>already_held</code> with the first\nrun's card and counts once. The same confirmed, to confirm and possible total.</p>\n<p><strong>Before proceeding to Step 2, report coverage and get confirmation:</strong>\n\"Existing coverage:  sheets have  accepted,  pending detections\nfor this tag type.  sheets have zero coverage.\nProceed with the vision scan of the  sheets in scope?\"\nEvery sheet in scope is looked at, covered or not: existing tags are matched,\nnever skipped (one tag, once), and coverage only narrows the neighborhood\nsearch within a sheet (the EXCLUDE rule above), never which sheets are scanned.\nWait for explicit user confirmation before continuing.</p>\n<h2>Step 2: Vision Tag Identification</h2>\n<p><strong>CRITICAL — Vision is MANDATORY for this step.</strong></p>\n<ul>\n<li>You MUST read each sheet's image via vision to identify tags</li>\n<li>You may NOT substitute text pattern matching (regex, searches of the\nsheet files) for visual identification — a text search finds text, not tags</li>\n<li>The sheet's text is used ONLY in Steps 3-4 to spatially anchor\nvision-identified tags, not to replace the visual identification step</li>\n<li>If you cannot get an image of a sheet, FLAG it as\n\"skipped — image unavailable\" and move to the next sheet</li>\n<li>Do NOT report OCR text matches as \"detected tags\"</li>\n</ul>\n<p>Examine each sheet image. Identify all visible tags of the requested\ntype. Return tag texts and view type classification — no coordinates.</p>\n<p><strong>In AgentCM mode</strong> a sheet is read in boxes through <code>crop.sh</code> (Step 0): one\nbox per view, from the <code>## view</code> headings in <code>.construction/sheets/&lt;sheet&gt;.txt</code>\n(each heading gives the view's area as x, y, w, h), or quadrants\n(<code>0 0 0.5 0.5</code>, <code>0.5 0 0.5 0.5</code>, <code>0 0.5 0.5 0.5</code>, <code>0.5 0.5 0.5 0.5</code>) for a\nsheet without views. If <code>crop.sh</code> refuses a box as too large to read (exit 2),\nsplit it in four and crop those. Keep each PNG with its box: a point at pixel\n(px, py) in a crop of box (x, y, w, h) sized W × H pixels is at sheet position\n(x + px / W × w, y + py / H × h).</p>\n<p>The views or quadrants cover the whole sheet. Never crop only around the tags\nthe sheet's text names: a tag drawn as linework, with no text layer, has no\nline in the sheet files, and only these boxes show it. It is a seen tag\n(Step 8). In the 2026-10-14 runs, crops around the text-known room tags\nmissed STAIR 107, drawn as outlines at the sheet's right edge.</p>\n<p>Vision must classify each tag's view: FLOOR PLAN, ENLARGED PLAN,\nDETAIL, SECTION, or ELEVATION. This determines record type in Step 5.\nClassification signals are in <code>references/tag-types.md</code>.</p>\n<p><strong>In Flat File mode:</strong> This is the primary (and only) identification step.\nProvide rough locations (\"upper-left quadrant\") for markup purposes.</p>\n<h2>Step 3: Text-Anchor Search (AgentCM mode only)</h2>\n<p>For each vision-identified tag, find its spatial anchor in the sheet's\n<code>.construction/sheets/&lt;sheet&gt;.tsv</code> (one line of text per row: <code>x y w h source tag how label para view zone text</code>, the text last) using the <strong>most\ndistinctive word</strong> in the tag text. \"WALK-IN FREEZER\" → search \"FREEZER\".\n\"WOMEN'S RESTROOM\" → search \"WOMEN\".</p>\n<pre><code>grep -i \"FREEZER\" \".construction/sheets/&lt;sheet&gt;.tsv\"\n</code></pre>\n<p>Multiple hits expected — each is a potential tag location. Zero hits\n→ try alternate words. Still zero → flag \"vision-only, no OCR anchor\".</p>\n<h2>Step 4: Spatial Neighborhood Query (AgentCM mode only)</h2>\n<p>For each anchor, pull the sheet's nearby lines: those whose <code>x</code> and <code>y</code> fall\nwithin the padding of the anchor's. Padding values by tag type in\n<code>references/spatial-params.md</code>. Target: 8-25 candidates per tag.</p>\n<pre><code>awk -F'\\t' 'NR &gt; 1 &amp;&amp; $1 &gt;= {anchor_x - pad} &amp;&amp; $1 &lt;= {anchor_x + pad} &amp;&amp; $2 &gt;= {anchor_y - pad} &amp;&amp; $2 &lt;= {anchor_y + pad}' \".construction/sheets/&lt;sheet&gt;.tsv\"\n</code></pre>\n<p>Adaptive: double padding if &lt; 3 items returned, reduce 30% if &gt; 40.</p>\n<h2>Step 5: Grouping</h2>\n<p>Give Claude the vision tag text and the small candidate set (~15-20\nitems). Group constituent items following <code>references/grouping-rules.md</code>\n— text reconstruction, spatial coherence, include/exclude criteria,\nconfidence scoring.</p>\n<p><strong>Batching</strong>: Process ALL tags per sheet in one call. 12 tags × ~200\ntokens each = ~2400 tokens. Never load the sheet's whole <code>.tsv</code>.</p>\n<p><strong>Flat File mode</strong>: Skip grouping. Tags come from vision only. Mark\nall detections as <code>confidence: \"vision_only\"</code> in output.</p>\n<p><strong>Validation checkpoint (after first 3 sheets):</strong>\nAfter grouping tags on the first 3 sheets, STOP and present 5 sample\ngrouped detections to the user. Show for each:</p>\n<ul>\n<li>Vision-identified text</li>\n<li>OCR-reconstructed text (from constituents)</li>\n<li>Confidence score</li>\n<li>Number of constituent lines</li>\n<li>Approximate sheet location (combined bounding box)</li>\n</ul>\n<p>Get explicit user confirmation: \"Grouping quality acceptable? Proceed\nwith remaining  sheets?\" Do not continue until confirmed.</p>\n<h2>Step 6: Sheet Markup</h2>\n<p><strong>REQUIRED for all modes.</strong> Mark up each sheet with visual highlights\nso the user can see exactly what was identified.</p>\n<p><strong>QTO markup must show ALL tagged items per sheet</strong> — not just newly detected.\nTake the existing tags from Step 1.5 (accepted, pending and waiting, read from\nthe files) and include them in the markup alongside new detections. Use color to distinguish:</p>\n<ul>\n<li><code>--color \"green\"</code> for accepted items (from previous detection/review)</li>\n<li><code>--color \"blue\"</code> for newly detected items (this session)</li>\n<li><code>--color \"orange\"</code> for pending review items</li>\n</ul>\n<p>Run the markup script once per color group per sheet, layering onto the same\noutput image (use the previous output as <code>--base</code> for the next pass).</p>\n<pre><code># Build items JSON with one entry per detected tag\n# Then call the shared markup script:\n\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \\\n  \"${CLAUDE_SKILL_DIR}/scripts/markup_tags.py\" \\\n  --base \"{sheet_image_path}\" \\\n  --items \"{items_json_path}\" \\\n  --output \"{output_path}\" \\\n  --color \"blue\" \\\n  --label-style \"numbered\"\n</code></pre>\n<p>Items JSON format (pixel coordinates):</p>\n<pre><code>[\n  {\"x\": 2500, \"y\": 1800, \"shape\": \"circle\", \"radius\": 20, \"label\": \"WALK-IN FREEZER\"},\n  {\"x\": 3100, \"y\": 2200, \"shape\": \"circle\", \"radius\": 20, \"label\": \"DRY STORAGE\"}\n]\n</code></pre>\n<p>Convert normalized 0-1 coordinates to pixels using sheet image dimensions.</p>\n<p><strong>In AgentCM mode</strong> the base images are the <code>crop.sh</code> PNGs from Step 2, one\nmarked-up image per box: give <code>--base</code> the crop's path and the items' pixel\npositions within that crop (sheet position minus the box's x and y, divided by\nits w and h, times the image's width and height). The crops live in AgentCM's\n<code>.construction/crops/</code>; write the marked-up PNGs to the project directory.</p>\n<p><strong>If PDF is available</strong>, also create native PDF annotations:</p>\n<pre><code>\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \\\n  \"${CLAUDE_PLUGIN_ROOT}/scripts/pdf/annotate_pdf.py\" \\\n  --pdf \"{pdf_path}\" --items \"{items_json}\" --output \"{output_pdf}\" \\\n  --author \"Claude Code QTO\"\n</code></pre>\n<h2>Step 7: Record Classification</h2>\n<p>Record type depends on view type. Full data model in\n<code>references/quantity-model.md</code>.</p>\n<p><strong>Floor plan tags → Element Instance.</strong> One tag = one installed item.\n<strong>Detail/section/elevation tags → Element Type Definition.</strong> Template.</p>\n<h2>Step 8: Write Results</h2>\n<p><strong>Pre-ingest confirmation (REQUIRED before POSTing):</strong>\nBefore making ANY ingest API calls, present this summary to the user:</p>\n<ul>\n<li>Total new detections to ingest:  (found in this run, matching no existing tag)</li>\n<li>Total existing:  accepted,  pending,  waiting in the outbox</li>\n<li>Resolved tag type: </li>\n<li>3-5 sample detections (tag_text, confidence, box)</li>\n<li>Any flagged items (low confidence, no OCR anchor, skipped sheets)</li>\n</ul>\n<p>\"Ready to ingest  new detections as ''? (Y/N)\"</p>\n<p>Do NOT POST to the ingest endpoint until the user explicitly confirms.\nIngesting without confirmation is a blocking violation of this skill.\nThe server enforces a maximum of 100 detections per request — batch\nyour ingest calls per sheet.</p>\n<p>Write the payloads and the QTO JSON with the Write tool, never with a script,\n<code>python -</code>, a heredoc or a file in <code>/tmp</code>: <code>qto_to_xlsx.py</code> checks the QTO\nJSON, and AgentCM checks each detection.</p>\n<p><strong>AgentCM mode:</strong> write each sheet's payload, <code>{\"sheet_id\": \"{sheet_id}\", \"detections\": [...]}</code>,\nto <code>.construction/skills/tag-audit-and-takeoff/outbox/tag-detections-ingest-&lt;sheet&gt;-&lt;YYYYMMDD-HHMMSS&gt;.json</code>\nand send it as <code>${CLAUDE_PLUGIN_ROOT}/reference/agentcm-mode.md</code> says under\nWriting back; if AgentCM is closed, the payload waits there and the PE is told:</p>\n<pre><code>curl -sS -X POST \"{api_url}/projects/{project_id}/tag-detections/ingest\" \\\n  -H \"Content-Type: application/json\" \\\n  -d @\".construction/skills/tag-audit-and-takeoff/outbox/tag-detections-ingest-&lt;sheet&gt;-&lt;YYYYMMDD-HHMMSS&gt;.json\" \\\n  -w \"\\nHTTP %{http_code}\\n\"\n</code></pre>\n<p>Each detection: <code>{ bounding_box, tag_text, tag_type, confidence, view_type, room_id }</code>.</p>\n<ul>\n<li><code>bounding_box</code>: <code>{ x, y, width, height }</code>, 0 to 1 from the sheet's top left:\nthe box around the tag's own lines in the sheet's <code>.tsv</code> (Steps 4-5), from\ntheir smallest <code>x</code> and <code>y</code> to their largest <code>x + w</code> and <code>y + h</code>. AgentCM\ntakes the texts whose centres fall inside it and that no card holds.</li>\n<li><code>tag_type</code>: the canonical name resolved in Step 1. <code>view_type</code> and <code>room_id</code>\nwhen known. Leave <code>element_ids</code> out: AgentCM goes by the box.</li>\n<li>A tag with no line in the sheet's <code>.tsv</code> (vision only, Step 3) is not sent:\nthere is no text for a card to hold. It is seen on the drawing, not in\nAgentCM (below), <code>ingest</code> <code>not_sent</code>.</li>\n</ul>\n<p><strong>Read AgentCM's answer, every detection; never assume success.</strong> HTTP 200\nmeans the request was well formed, not that each tag was taken. <code>results[]</code>\nholds one entry per detection, in order (<code>index</code> is its place in\n<code>detections</code>), each with one <code>outcome</code>:</p>\n<ul>\n<li><code>created</code>: a new pending card, <code>suggestion_id</code>. To confirm; the instance's\n<code>detection_id</code> is that id, <code>ingest</code> <code>created</code>.</li>\n<li><code>already_held</code>: a card already holds those texts, <code>held_by</code>\n<code>{suggestion_id, status}</code>. The tag is that card, counted once:\n<code>detection_id</code> its id, <code>status</code> <code>accepted</code> when the card is accepted, else\n<code>to_confirm</code>; <code>ingest</code> <code>already_held</code>. Never a second instance of it.</li>\n<li><code>rejected</code>: the PE already turned this tag down in AgentCM\n(<code>rejected_by.suggestion_id</code>). Off the to-confirm list and out of the count:\n<code>status</code> <code>rejected</code>, <code>detection_id</code> that card's id, <code>ingest</code> <code>rejected</code>. The\nexport lists it once under \"Rejected in AgentCM\"; never report it again, as\nfound, missed or to confirm.</li>\n<li><code>no_free_text</code>: no text's centre is inside the box, so the box was wrong.\nLook at the sheet's <code>.tsv</code> around the crop: if the tag's text has lines\nthere, redraw the box around them and send that detection once more; its\nanswer is the outcome. No text at all in that area: the tag is seen on the\ndrawing, not in AgentCM (below), <code>ingest</code> <code>no_free_text</code>.</li>\n<li><code>new_type: true</code> on any entry: a type outside contract 1's list was sent,\na bug in this skill. Say so: name the type, and tell the PE a new tag kind\nwas created in AgentCM by mistake.</li>\n<li>HTTP 400 (its message names the detection's index) or 404 (an unknown\nproject or sheet): AgentCM refused the request and created nothing. Show the\nPE its answer; the payload stays in <code>outbox/</code>.</li>\n</ul>\n<p><strong>Seen on the drawing, not in AgentCM</strong> (the drawing shows the tag; AgentCM\nhas no text for it): <code>status</code> <code>seen</code>, <code>bounding_box</code> estimated from the crop,\n<code>crop</code> the PNG <code>crop.sh</code> printed. Not to confirm: no card exists, so nobody\ncan accept it in AgentCM; it is in the possible total. Report it once in\ncorrections.tsv, which says nothing when the same tag is already there:</p>\n<pre><code>\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \"${CLAUDE_PLUGIN_ROOT}/scripts/corrections_append.py\" --once \\\n  --sheet &lt;sheet&gt; --x &lt;x&gt; --y &lt;y&gt; --w &lt;w&gt; --h &lt;h&gt; --kind tag --crop \"&lt;crop&gt;\" --agentcm \"\" --drawing \"&lt;tag text&gt;\"\n</code></pre>\n<p><code>corrections</code> is <code>appended</code>, or <code>reported earlier, line N</code> when it prints\nREPORTED EARLIER. To clear one, the PE draws its text on AgentCM's canvas and\ntags it; the next run finds it pending or accepted.</p>\n<p>Report each sheet's outcomes: \"A-101: 3 sent: 2 created, 1 already held (pending); 1 seen, not in AgentCM.\"\nResults appear as highlighted overlays in AgentCM's canvas UI.</p>\n<p>In both modes, once the QTO JSON is saved (below), record a finding in <code>.construction/skills/tag-audit-and-takeoff/findings/</code>:</p>\n<pre><code>\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \"${CLAUDE_PLUGIN_ROOT}/scripts/graph/write_finding.py\" \\\n  --skill \"tag-audit-and-takeoff\" --type \"qto_{tag_type}\" \\\n  --title \"QTO: {tag_type_display} — {confirmed} confirmed, {possible} possible across {N} sheets\" \\\n  --output-file \"{qto_json_path}\" \\\n  --data '{\"tag_type\": \"...\", \"confirmed\": ..., \"to_confirm\": ..., \"seen_not_in_agentcm\": ..., \"possible_total\": ..., \"sheets\": [...], \"ingested\": ...}'\n</code></pre>\n<p><strong>CRITICAL — use the EXACT schema from <code>references/qto-output-format.md</code>.</strong>\nThe Excel script is rigid and parses specific key names. Required keys:</p>\n<ul>\n<li><code>line_items[]</code> (NOT <code>qto_lines</code>) — one entry per unique element/designation</li>\n<li>Each line item has <code>instance_details[]</code> with <code>detection_id</code>, <code>sheet</code>, <code>tag_text</code>, <code>room</code>, <code>bounding_box</code>, <code>status</code> (<code>accepted</code>, <code>to_confirm</code>, <code>seen</code> or <code>rejected</code>), <code>source</code>, <code>confidence</code>; <code>reason</code> on a tag of a sheet that is behind; <code>crop</code> and <code>corrections</code> on a <code>seen</code> tag</li>\n<li><code>scope{}</code> with <code>mode</code> (<code>agentcm</code> or <code>flat_file</code>), <code>completeness{}</code>, <code>issues[]</code></li>\n</ul>\n<p><strong>What the count holds</strong> (Step 1.5): every tag once, in one of four groups.\n<code>accepted</code> instances are the confirmed count; a tag with a card not yet\naccepted, waiting in the outbox or on a sheet that is behind is <code>to_confirm</code>,\nwith its <code>source</code> (<code>pending_in_agentcm</code>, <code>waiting_in_outbox</code>, or\n<code>found_this_run</code> with <code>ingest</code> <code>created</code>, <code>already_held</code> or <code>outbox</code>); a tag\nAgentCM has no text for is <code>seen</code>; a tag the PE rejected is <code>rejected</code>, in no\nnumber; derived instances are to confirm too. In flat-file mode nothing is\naccepted: every tag is <code>to_confirm</code>. The export computes the numbers per item\n(confirmed, to confirm, seen not in AgentCM, possible total), prints them, and\nrefuses a file that lists the same tag twice or a <code>seen</code> tag without its crop.\nReport the export's lines, never your own tally, and the caveat in its words.</p>\n<p><strong>Both modes:</strong> Save the QTO JSON (working data) to\n<code>.construction/skills/tag-audit-and-takeoff/qto/{tag_type}_{timestamp}.json</code> and the\nmarked-up PNGs (deliverables) to the project directory.</p>\n<p><strong>Excel export</strong> (both modes): Pass that QTO JSON as <code>{qto_json_path}</code>\nand invoke the export script to produce a styled multi-sheet workbook:</p>\n<pre><code>\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \\\n  \"${CLAUDE_SKILL_DIR}/scripts/qto_to_xlsx.py\" \\\n  --data \"{qto_json_path}\" \\\n  --project \"{project_name}\" \\\n  --scope \"{tag_type_display}\" \\\n  --output \"QTO_{tag_type}_{date}.xlsx\"\n</code></pre>\n<p>The workbook has 6 sheets: QTO Summary (confirmed, to confirm and possible\ntotal per line item, with the caveat), Instance Detail (every detection with\nprovenance), Type Definitions (templates applied to rooms), Completeness\n(coverage metrics), To Confirm in AgentCM (every tag not yet accepted), and\nRejected in AgentCM (tags the PE already turned down there, in no number).</p>\n<hr>\n<h2>Reference Resolution (Type Defs → Derived Instances)</h2>\n<p>After type definitions are identified from details/sections/enlarged\nplans, resolve where they apply. Full logic: <code>references/quantity-model.md</code>.</p>\n<p>Three methods (in order of reliability):</p>\n<ol>\n<li><strong>Callout reference</strong> — detail bubble on floor plan references source</li>\n<li><strong>Room type match</strong> — room tags match the type def's parent view label</li>\n<li><strong>Schedule cross-ref</strong> — finish/room schedule references the detail</li>\n</ol>\n<p>Deduplication: direct instances always take precedence over derived.</p>\n<hr>\n<h2>Completeness Metrics</h2>\n<p>Compare detected counts against schedules (door, fixture, equipment)\nwhen available. Report: \"[tag type]: X detected, Y expected, Z%\ncoverage. Gaps on sheets: [list].\" No schedule → report raw counts.</p>\n<hr>\n<h2>Allowed Scripts</h2>\n<p><strong>Allowed scripts — exhaustive list.</strong> Only execute these scripts during this skill:</p>\n<ul>\n<li><code>${CLAUDE_PLUGIN_ROOT}/scripts/pdf/rasterize_page.py</code> — rasterize a sheet PDF page to PNG (flat file mode)</li>\n<li>AgentCM's <code>.construction/crop.sh</code> / <code>crop.ps1</code> (a box of a sheet as a PNG), in AgentCM mode</li>\n<li><code>scripts/markup_tags.py</code> — sheet markup with tag highlights</li>\n<li><code>${CLAUDE_PLUGIN_ROOT}/scripts/pdf/annotate_pdf.py</code> — native PDF annotations</li>\n<li><code>scripts/qto_to_xlsx.py</code> — QTO Excel export (6-sheet workbook; computes and prints confirmed, to confirm and possible total, and lists rejected tags apart; refuses a tag listed twice)</li>\n<li><code>${CLAUDE_PLUGIN_ROOT}/scripts/issue_manager.py</code> — log schedule/drawing count mismatches to the Issue Registry (command and severity mapping in construction-guide)</li>\n<li><code>${CLAUDE_PLUGIN_ROOT}/scripts/graph/write_finding.py</code> — record the QTO as a finding (Step 8)</li>\n<li><code>${CLAUDE_PLUGIN_ROOT}/scripts/corrections_append.py</code> — AgentCM mode: record a correction of AgentCM's data other than a missed tag (<code>reference/agentcm-mode.md</code>), and, with <code>--once</code>, a tag seen on the drawing that AgentCM has no text for (Step 8)</li>\n</ul>\n","files":[{"path":"agents/openai.yaml","sizeBytes":300,"isText":true},{"path":"references/detection-record.schema.json","sizeBytes":3599,"isText":true},{"path":"references/grouping-rules.md","sizeBytes":4203,"isText":true},{"path":"references/qto-output-format.md","sizeBytes":11178,"isText":true},{"path":"references/quantity-model.md","sizeBytes":5814,"isText":true},{"path":"references/spatial-params.md","sizeBytes":3351,"isText":true},{"path":"references/tag-types.md","sizeBytes":3760,"isText":true},{"path":"scripts/markup_tags.py","sizeBytes":3539,"isText":true},{"path":"scripts/qto_to_xlsx.py","sizeBytes":32928,"isText":true},{"path":"scripts/shared.py","sizeBytes":554,"isText":true},{"path":"SKILL.md","sizeBytes":25996,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-10-19T21:17:27.480013Z","sha256":"754FFE213976BC1D2D9CF1E956871B7FF86C59F066FB75F7D0C1409EA11EFC14","sizeBytes":34252},"review":null,"source":{"repositoryUrl":"https://github.com/dleerdefi/claude-code-construction","path":"skills/tag-audit-and-takeoff","license":"MIT","commit":"5c46e16c67cdc82ee8bcbb0fdd8ba316d792d4ec","subtreeSha":"29A0A1947090E86A38020F51E0B1F833DEEC6E4FDCFF32010D310BB371332FD7","lastSyncedAt":"2026-10-19T21:17:01.926806Z"},"reviewedAt":"2026-10-19T21:18:00.431076Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/dleerdefi/claude-code-construction/tree/main/skills/tag-audit-and-takeoff"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dleerdefi-claude-code-construction@llmmart"},{"target":"git","command":"git clone https://github.com/dleerdefi/claude-code-construction.git"}]}