Skip to content

docs(skills): require a proven positive before trusting a negative result - #2755

Open
tpavay wants to merge 4 commits into
kunchenguid:mainfrom
tpavay:fm/fm-negative-result-rule
Open

docs(skills): require a proven positive before trusting a negative result#2755
tpavay wants to merge 4 commits into
kunchenguid:mainfrom
tpavay:fm/fm-negative-result-rule

Conversation

@tpavay

@tpavay tpavay commented Aug 21, 2026

Copy link
Copy Markdown

Intent

Add one rule to firstmate's diagnostic reasoning: a negative result is not evidence until the method has been shown to produce a positive.

Evidence this comes from: in one session, firstmate told the captain six false things, all the same shape - reporting that something was ABSENT using a method never shown capable of detecting PRESENCE. An invoked subcommand that does not exist, counted with grep -c, returned 0 and was reported as "no leftover data" (the data existed). An API answering "which subcollections does this document have" returned none when three existed. A URL with literal parentheses inside a shell loop broke every request, and the empty output was reported to the captain as an empty production leaderboard that actually held 13 records. A grep pattern that also matched a second unintended path shape reported 19 where the truth was 6. A production storage bucket was reported as holding zero objects when it held 264. A tree comparison keyed by a name that seventeen nodes shared silently dropped all but one, producing 19 false differences. Two of these were presented to the captain as corroborating each other, which made the wrong answer more convincing. The only check that did not go wrong was the one first run against data known to be present.

The change: add a short section to .agents/skills/diagnostic-reasoning/SKILL.md, because that skill already owns how firstmate reasons about evidence and already separates observed facts from hypotheses; this is the missing rule about a class of observed "fact" that is really a tooling artifact. The rule, in the skill's own voice and register: a zero, an empty result, or a "not found" is a claim about the method as much as about the world, so before reporting absence run the same method against data known to exist and treat it as having established nothing if it cannot find that; distinguish three outcomes rather than two (found, confirmed absent, the query failed), with tooling that renders a failure as an empty result being the trap, so an unproven empty is treated as a failure until shown otherwise; two agreeing checks are not corroboration when they share a failure mode, since a wrong pattern and a wrong endpoint can agree with each other, so corroboration requires an independent method rather than a repeated one; and this binds hardest on captain-facing statements, because "there is nothing there" is a conclusion the captain will act on and carries the same burden as any other load-bearing finding.

Deliberate constraints, chosen by the requester and not accidents of the diff:

  • Keep it tight. That skill is deliberately concise and is loaded before every bug scoping, so a long addition gets skimmed. A short subsection was requested, not a treatise. The section is about twelve lines on purpose.
  • Do not restate the general reasoning procedure already in the file, and do not duplicate anything owned by another skill. The new section deliberately does not repeat the existing reproduction, trigger/mask/symptom, divergent-path, counterfactual, or disconfirming-evidence material.
  • Stay tool-agnostic. Any Firebase-specific or tool-specific guidance is explicitly out of scope - a separate skill is being built in the AscendApp repo for that. The failure is not about Firebase; the same mistake is available with grep, find, a log search, or an API listing. The failure examples in the new section are therefore generic by design (a subcommand that does not exist, a malformed request, an endpoint that cannot see what was asked about, a pattern matching the wrong shape, a key collision that silently drops rows) even though each traces to a concrete real incident.
  • Placement between "Test the causal explanation" and "Scope and act on the result" is deliberate: the rule is about the quality of gathered evidence, so it precedes acting on it.

One decision made while doing the work, beyond the original request, and already flagged to the requester for possible reversion: the skill's load trigger was widened. The original trigger fired only "before scoping a reported bug and before acting on a diagnostic report", so firstmate stating "the bucket is empty" would never have loaded the skill and the new rule would not be read at the moment it matters. The trigger now also reads "before reporting that something is absent, empty, missing, or not found". This was patched in place in four existing lines rather than added as new ones: the SKILL.md frontmatter description, the SKILL.md opening body line, and the two existing trigger lines in AGENTS.md (section 7's intake line and section 13's agent-only skill registry line). AGENTS.md grew by zero lines, which is deliberate under the repo's size-discipline rule that every AGENTS.md line is paid by every session of every fleet member.

Also in scope per the request: a pointer from anywhere that genuinely needs one. The widened trigger is that pointer; no new pointer file or section was added.

Repo conventions followed deliberately: one full sentence per line in tracked Markdown, plain dash rather than em dash, no agent name as commit co-author. No new test was added: the change is prose in an agent-loaded skill with no executable interface, and the repo's own testing rule forbids tests that assert implementation-source bytes. bin/fm-doc-audience-check.sh passes, tests/fm-documentation-audiences.test.sh passes, and bin/fm-lint.sh reports no changed lint targets because no scripts were touched.

What Changed

  • Adds a "Trust a negative only after the method has produced a positive" section to .agents/skills/diagnostic-reasoning/SKILL.md, placed between the causal-explanation and scoping sections. It requires running the same method against data known to exist before reporting absence, splits outcomes into three (found / confirmed absent / the query failed) with an unproven empty treated as a failure, rules out corroboration between two checks that share a failure mode, and binds hardest on captain-facing statements. The failure examples are deliberately tool-agnostic, and the section stays at roughly a dozen lines so it does not get skimmed.
  • Widens the skill's load trigger to also fire before reporting that a search, query, or scan found nothing, so the rule is loaded at the moment it applies. Patched in place across the four existing trigger lines - the SKILL.md frontmatter description, the SKILL.md opening line, and section 7's intake line and section 13's agent-only registry line in AGENTS.md - leaving AGENTS.md at zero net line growth.
  • Review feedback narrowed that trigger from bare absence to query-derived absence, keeping routine digest ABSENT markers and healthy-empty-queue relays out of it, and added a fallback for targets with no known-present sample: run the control against the nearest comparable populated location, or state the missing control explicitly rather than claiming confirmed absence.

No new tests: the change is prose in an agent-loaded skill with no executable interface. bin/fm-doc-audience-check.sh and tests/fm-documentation-audiences.test.sh pass; the remaining failures in the changed-file test set are pre-existing on main per issue #2753 (three red specs, plus the two lint scripts exiting 127 from a missing actionlint), which is also the source of the Lint stage's exit-code-127 warning.

Risk Assessment

✅ Low: Documentation-only prose change to one agent-loaded skill plus two in-place AGENTS.md trigger lines, with both prior findings fixed exactly as instructed, all four trigger surfaces verified identical and no stale wording left anywhere, zero net AGENTS.md line growth, and no executable interface touched.

Testing

I rendered the edited skill as an agent reads it and screenshotted it, confirming the new twelve-line section sits between "Test the causal explanation" and "Scope and act on the result" and that the widened trigger reaches all four intended lines (SKILL.md frontmatter and opening line, AGENTS.md sections 7 and 13) with AGENTS.md unchanged at 573 lines. To show the rule does real work rather than just reading well, I reproduced all six failure shapes it exists to catch against this repo's own data - a subcommand that does not exist returning 0 while 20 skills are checked in, a parenthesized URL in a shell loop returning silently empty, a listing that cannot see one level down, a pattern matching an unintended second path, a name key collapsing 21 rows to 1, and two broken checks agreeing on 0 - and showed each false absence being caught by the positive control the new section requires. bin/fm-doc-audience-check.sh and the targeted documentation-audiences and ensure-agents-md tests pass; the changed-file suite's 5 failures are the pre-existing reds and missing-actionlint 127s documented in issue #2753, all unrelated to this Markdown-only diff. The worktree is clean and no transient artifacts remain.

  • Evidence: Rendered diagnostic-reasoning SKILL.md with the new section highlighted in place (local file: /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T/no-mistakes-evidence/01M0JSR3NFAJEC5DNGNNNW77JG/skill-rendered.png)
Evidence: HTML source of the rendered skill surface
<!doctype html><meta charset=utf-8><title>diagnostic-reasoning SKILL.md</title>
<style>
body{background:#12141a;color:#dfe3ec;font:15px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;margin:0;padding:40px}
main{max-width:900px;margin:0 auto}
h1{font-size:26px;margin:0 0 6px;color:#fff;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
h2{font-size:19px;margin:30px 0 10px;color:#9ecbff}
p{margin:0 0 12px}
ul{margin:0 0 12px;padding-left:22px}
li{margin:0 0 6px}
strong{color:#ffd479}
.fm{background:#181b23;border:1px solid #2b3040;border-radius:8px;padding:14px 16px;margin:0 0 24px}
.fmlabel{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#7d8699;margin-bottom:8px}
pre{margin:0;white-space:pre-wrap;font:13px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;color:#b9c2d4}
section.new{position:relative;background:rgba(63,185,80,.08);border-left:3px solid #3fb950;border-radius:0 8px 8px 0;padding:4px 20px 14px;margin:26px 0}
section.new h2{color:#7ee787;margin-top:16px}
.badge{position:absolute;top:-11px;left:20px;background:#3fb950;color:#0b1015;font:600 11px/1 -apple-system,sans-serif;letter-spacing:.06em;text-transform:uppercase;padding:5px 9px;border-radius:4px}
</style><main><div class="fm"><div class="fmlabel">SKILL.md frontmatter (the description the harness indexes)</div><pre>name: diagnostic-reasoning
description: &gt;-
  Agent-only procedure for diagnosing reported bugs.
  Use before scoping a reported bug, before acting on a diagnostic report, and before reporting that a search, query, or scan found nothing.
  Owns end-user-aligned reproduction, causal separation, divergent-path and history inspection, counterfactual testing, disconfirming evidence, and when a negative result may be trusted.
user-invocable: false
metadata:
  internal: true</pre></div>
<h1>diagnostic-reasoning</h1>
<p>Use this procedure before scoping a reported bug, before acting on a diagnostic report, and before reporting that a search, query, or scan found nothing.<br>This skill is the single owner of Firstmate&#x27;s bug-diagnosis reasoning procedure.<br>Firstmate applies it when briefing delegated investigation and evaluating the resulting evidence, without taking over project-specific investigation itself.</p>
<h2>Establish the observed behavior</h2>
<p>Start from the end user&#x27;s experience rather than an internal error string or an implementation hypothesis.<br>Require an end-to-end reproduction aligned with the real user path whenever it is feasible and safe.<br>If a faithful reproduction is not feasible, record the exact limitation and use the closest representative path without presenting it as equivalent evidence.<br>Capture the expected behavior, observed behavior, setup, inputs, and repeatability before assigning a cause.</p>
<p>Separate these three facts explicitly:</p>
<ul>
<li>The <strong>initiating trigger</strong> is the event, input, or transition that starts the faulty behavior.</li>
<li>The <strong>masking condition</strong> is the independent state, environment, timing, cache, configuration, or path difference that hides or exposes the fault.</li>
<li>The <strong>visible symptom</strong> is what the end user or operator can actually observe.</li>
</ul>
<p>Do not collapse those facts into one label.<br>A masking condition may explain why a fault appears only sometimes without being the initiating cause, and the visible symptom may be several layers downstream from both.</p>
<h2>Test the causal explanation</h2>
<p>Inspect the failing path and a proven path where the intended behavior is known to work.<br>Compare their inputs, state transitions, dependencies, timing, and control flow to find the earliest meaningful divergence.<br>Inspect relevant history, including blame, commits, migrations, and prior implementations, when it can explain why the paths diverged or which invariant was intended.<br>Do not treat the most recent nearby change as causal without evidence.</p>
<p>Identify the smallest counterfactual that should change the outcome if the leading explanation is true.<br>Change one condition at a time where practical, and record whether the symptom appears, disappears, or remains unchanged.<br>Seek disconfirming evidence deliberately: name what observation would falsify the leading explanation, run that check when feasible, and retain contradictory results instead of explaining them away.<br>Compare the final explanation against the proven path and show why the proposed causal boundary accounts for both the failure and the success.</p>
<section class="new"><div class="badge">added by this change</div>
<h2>Trust a negative only after the method has produced a positive</h2>
<p>A zero, an empty result, or a &quot;not found&quot; is a claim about the method as much as about the world.<br>Before reporting absence, run the same method against data known to exist; if it cannot find that, it has established nothing about the target.<br>When the target itself holds nothing to validate against, run that control against the nearest comparable populated location: a sibling path, the same shape in another environment, anywhere the method should succeed.<br>If even that is impossible, state the missing control explicitly rather than presenting the empty result as confirmed absent.</p>
<p>Distinguish three outcomes, never two:</p>
<ul>
<li><strong>Found</strong> is a positive the method demonstrably can produce.</li>
<li><strong>Confirmed absent</strong> is a negative from a method already shown to find what is there.</li>
<li><strong>The query failed</strong> is everything else: a subcommand that does not exist, a malformed request, an endpoint that cannot see what was asked about, a pattern that matches the wrong shape, or a key collision that silently drops rows.</li>
</ul>
<p>Tooling that renders a failure as an empty result is the trap, so treat an unproven empty as a failure until shown otherwise.<br>Two agreeing checks are not corroboration when they share a failure mode, because a wrong pattern and a wrong endpoint can agree with each other; independent method, not repeated method.<br>This binds hardest on captain-facing statements, since &quot;there is nothing there&quot; is a conclusion the captain will act on and carries the same burden as any other load-bearing finding.</p>
</section>
<h2>Scope and act on the result</h2>
<p>A diagnosis brief should ask for the reproduction, trigger/mask/symptom separation, divergent and proven path comparison, relevant history, smallest counterfactual, and disconfirming evidence in the report.<br>A diagnostic report should distinguish observed facts from hypotheses and state any unresolved uncertainty that could change the recommended scope.<br>Before acting on the report, verify that its claimed cause explains the end-user reproduction and the proven path without relying on an untested masking condition.<br>If a load-bearing element is missing, route a focused follow-up investigation instead of treating confidence or implementation detail as proof.<br>A diagnosis or implementation-ready recommendation is evidence, not authorization to change code.<br>Implementation still requires the captain&#x27;s request or another existing lifecycle authority, and the reproduction should become the regression test when a fix is authorized.</p></main>
Evidence: Negative-result drill: the six incident shapes reproduced against this repo, each caught by the required positive control

1. A subcommand that does not exist, counted with grep -c $ git ls-file --cached '.agents/skills//SKILL.md' | grep -c SKILL.md -> 0 naive claim to the captain: "no agent-only skills are checked in" positive control required by the rule: run the same method against data known to exist $ git ls-file --cached AGENTS.md | grep -c AGENTS.md # AGENTS.md certainly exists -> 0 (the method cannot find a file that is definitely there) verdict: THE QUERY FAILED, not confirmed absent. truth = 20 skills. 2. A malformed request inside a shell loop (literal parentheses) $ for u in "$url"; do bash -c "curl -s $u"; done 2>/dev/null -> 0 lines of output, and the command printed no error the caller could see naive claim to the captain: "the production leaderboard is empty" positive control: run the same fetch against a record set known to be populated -> 0 lines (the fetch returns empty even where rows are known to exist) verdict: THE QUERY FAILED. truth = 3 records. 5. A key collision that silently drops rows $ git ls-files '/SKILL.md' | awk -F/ '{print $NF}' | sort -u | wc -l -> 1 verdict: 21 rows collapsed into 1 because every row shares the key 'SKILL.md'. 6. Two agreeing checks that share a failure mode check A -> 0 ; check B -> 0 ; independent method -> 20 verdict: corroboration requires an independent method, not a repeated one.


1. A subcommand that does not exist, counted with grep -c
=========================================================
  $ git ls-file --cached '.agents/skills/*/SKILL.md' | grep -c SKILL.md
  -> 0
  naive claim to the captain: "no agent-only skills are checked in"
  positive control required by the rule: run the same method against data known to exist
  $ git ls-file --cached AGENTS.md | grep -c AGENTS.md   # AGENTS.md certainly exists
  -> 0   (the method cannot find a file that is definitely there)
  verdict: THE QUERY FAILED, not confirmed absent. truth = 20 skills.

2. A malformed request inside a shell loop (literal parentheses)
================================================================
  the fetch, as it was actually written - the URL is interpolated unquoted into the loop body:
  $ for u in "$url"; do bash -c "curl -s $u"; done   2>/dev/null
  -> 0 lines of output, and the command printed no error the caller could see
  naive claim to the captain: "the production leaderboard is empty"
  positive control: run the same fetch against a record set known to be populated
  -> 0 lines   (the fetch returns empty even where rows are known to exist)
  verdict: THE QUERY FAILED. truth = 3 records, readable by an independent method:
  $ cat '$tmp/board(prod).txt' -> record-1 record-2 record-3 

3. An endpoint that cannot see what was asked about
===================================================
  question: which skills live under .agents/skills ?
  $ find .agents/skills -maxdepth 1 -type f -name 'SKILL.md' | wc -l
  -> 0
  naive claim to the captain: "that tree has no skills"
  positive control: ask the same way somewhere the answer is known to be non-empty
  $ find .agents/skills/diagnostic-reasoning -maxdepth 1 -type f -name 'SKILL.md' | wc -l
  -> 1   (the control succeeds, so the shape of the question was wrong, not the world)
  verdict: the listing could not see one level down. truth = 20 skills.

4. A pattern that also matches a second, unintended path shape
==============================================================
  $ find . -path ./.git -prune -o -name SKILL.md -print | wc -l   # 'count the agent-only skills'
  -> 21
  naive claim to the captain: "there are 21 agent-only skills"
  the unintended shape the pattern also matched:
    ./skills/stow/SKILL.md
  verdict: inflated count. truth = 20 agent-only skills.

5. A key collision that silently drops rows
===========================================
  $ git ls-files '*/SKILL.md' | awk -F/ '{print $NF}' | sort -u | wc -l   # compare two trees keyed by name
  -> 1
  naive claim to the captain: "the two trees differ, one side has a single skill"
  verdict: 21 rows collapsed into 1 because every row shares the key 'SKILL.md'.

6. Two agreeing checks that share a failure mode
================================================
  check A (git ls-file --cached | grep -c) -> 0
  check B (git ls-file --others | grep -c) -> 0
  both agree on 0, which reads as corroboration but is one shared failure: the subcommand does not exist
  independent method:
  $ git ls-files '.agents/skills/*/SKILL.md' | wc -l -> 20
  verdict: corroboration requires an independent method, not a repeated one.

Every naive line above would have been reported to the captain as 'nothing there'.
Each one is caught by the control the new section requires before reporting absence.
Evidence: Drill script (read-only, runs against the checkout)
#!/usr/bin/env bash
# Drill: run each failure shape named in the new skill section against this repo,
# first the way that produced a false "absent", then the positive control the
# rule requires. Read-only; every command below touches only the checkout.
set -u
cd "${1:?repo root required}"

hr() { printf '\n%s\n%s\n' "$1" "$(printf '=%.0s' $(seq ${#1}))"; }
say() { printf '  %s\n' "$*"; }

TRUTH_AGENT_SKILLS=$(git ls-files '.agents/skills/*/SKILL.md' | wc -l | tr -d ' ')

hr '1. A subcommand that does not exist, counted with grep -c'
say "$ git ls-file --cached '.agents/skills/*/SKILL.md' | grep -c SKILL.md"
naive=$(git ls-file --cached '.agents/skills/*/SKILL.md' 2>/dev/null | grep -c 'SKILL.md')
say "-> $naive"
say "naive claim to the captain: \"no agent-only skills are checked in\""
say "positive control required by the rule: run the same method against data known to exist"
say "$ git ls-file --cached AGENTS.md | grep -c AGENTS.md   # AGENTS.md certainly exists"
ctl=$(git ls-file --cached AGENTS.md 2>/dev/null | grep -c 'AGENTS.md')
say "-> $ctl   (the method cannot find a file that is definitely there)"
say "verdict: THE QUERY FAILED, not confirmed absent. truth = $TRUTH_AGENT_SKILLS skills."

hr '2. A malformed request inside a shell loop (literal parentheses)'
tmp=$(mktemp -d); printf 'record-1\nrecord-2\nrecord-3\n' > "$tmp/board(prod).txt"
url="file://$tmp/board(prod).txt"
say "the fetch, as it was actually written - the URL is interpolated unquoted into the loop body:"
say '$ for u in "$url"; do bash -c "curl -s $u"; done   2>/dev/null'
naive=$( (for u in "$url"; do bash -c "curl -s $u"; done) 2>/dev/null | wc -l | tr -d ' ')
say "-> $naive lines of output, and the command printed no error the caller could see"
say "naive claim to the captain: \"the production leaderboard is empty\""
say "positive control: run the same fetch against a record set known to be populated"
printf 'control-1\ncontrol-2\n' > "$tmp/known(good).txt"
ctl=$( (for u in "file://$tmp/known(good).txt"; do bash -c "curl -s $u"; done) 2>/dev/null | wc -l | tr -d ' ')
say "-> $ctl lines   (the fetch returns empty even where rows are known to exist)"
say "verdict: THE QUERY FAILED. truth = $(wc -l < "$tmp/board(prod).txt" | tr -d ' ') records, readable by an independent method:"
say "$ cat '\$tmp/board(prod).txt' -> $(tr '\n' ' ' < "$tmp/board(prod).txt")"
rm -rf "$tmp"

hr '3. An endpoint that cannot see what was asked about'
say 'question: which skills live under .agents/skills ?'
say "$ find .agents/skills -maxdepth 1 -type f -name 'SKILL.md' | wc -l"
naive=$(find .agents/skills -maxdepth 1 -type f -name 'SKILL.md' | wc -l | tr -d ' ')
say "-> $naive"
say "naive claim to the captain: \"that tree has no skills\""
say "positive control: ask the same way somewhere the answer is known to be non-empty"
say "$ find .agents/skills/diagnostic-reasoning -maxdepth 1 -type f -name 'SKILL.md' | wc -l"
ctl=$(find .agents/skills/diagnostic-reasoning -maxdepth 1 -type f -name 'SKILL.md' | wc -l | tr -d ' ')
say "-> $ctl   (the control succeeds, so the shape of the question was wrong, not the world)"
say "verdict: the listing could not see one level down. truth = $TRUTH_AGENT_SKILLS skills."

hr '4. A pattern that also matches a second, unintended path shape'
say "$ find . -path ./.git -prune -o -name SKILL.md -print | wc -l   # 'count the agent-only skills'"
naive=$(find . -path ./.git -prune -o -name 'SKILL.md' -print | wc -l | tr -d ' ')
say "-> $naive"
say "naive claim to the captain: \"there are $naive agent-only skills\""
say "the unintended shape the pattern also matched:"
find . -path ./.git -prune -o -name 'SKILL.md' -print | grep -v '^\./\.agents/skills/' | sed 's/^/    /'
say "verdict: inflated count. truth = $TRUTH_AGENT_SKILLS agent-only skills."

hr '5. A key collision that silently drops rows'
say "$ git ls-files '*/SKILL.md' | awk -F/ '{print \$NF}' | sort -u | wc -l   # compare two trees keyed by name"
naive=$(git ls-files '*/SKILL.md' | awk -F/ '{print $NF}' | sort -u | wc -l | tr -d ' ')
rows=$(git ls-files '*/SKILL.md' | wc -l | tr -d ' ')
say "-> $naive"
say "naive claim to the captain: \"the two trees differ, one side has a single skill\""
say "verdict: $rows rows collapsed into $naive because every row shares the key 'SKILL.md'."

hr '6. Two agreeing checks that share a failure mode'
a=$(git ls-file --cached '.agents/skills/*/SKILL.md' 2>/dev/null | grep -c 'SKILL.md')
b=$(git ls-file --others '.agents/skills/*/SKILL.md' 2>/dev/null | grep -c 'SKILL.md')
say "check A (git ls-file --cached | grep -c) -> $a"
say "check B (git ls-file --others | grep -c) -> $b"
say "both agree on 0, which reads as corroboration but is one shared failure: the subcommand does not exist"
say "independent method:"
say "$ git ls-files '.agents/skills/*/SKILL.md' | wc -l -> $TRUTH_AGENT_SKILLS"
say "verdict: corroboration requires an independent method, not a repeated one."

printf '\n%s\n' "Every naive line above would have been reported to the captain as 'nothing there'."
printf '%s\n' "Each one is caught by the control the new section requires before reporting absence."
Evidence: Intent constraint checks: AGENTS.md zero growth, section placement, section length, repo prose conventions

### AGENTS.md size discipline base 573 lines target 573 lines ### Section order in SKILL.md 18:## Establish the observed behavior 34:## Test the causal explanation 46:## Trust a negative only after the method has produced a positive 63:## Scope and act on the result ### em dash in changed files: none (plain dash only) ### one sentence per line: none violated

### AGENTS.md size discipline (intent: 'AGENTS.md grew by zero lines')
base   573 lines
target 573 lines

### Section order in SKILL.md (intent: new section sits between 'Test the causal explanation' and 'Scope and act on the result')
18:## Establish the observed behavior
34:## Test the causal explanation
46:## Trust a negative only after the method has produced a positive
63:## Scope and act on the result

### New section length (intent: 'about twelve lines on purpose')
17

### Repo conventions: em dash present anywhere in the changed files?
none (plain dash only)

### One full sentence per line in the added prose (lines with an interior '. ' would violate it)
none - every sentence is on its own line
Evidence: Trigger wiring: the widened clause present in all four lines firstmate reads

--- SKILL.md opening body line: Use this procedure before scoping a reported bug, before acting on a diagnostic report, and before reporting that a search, query, or scan found nothing. --- AGENTS.md section 7 intake line: 280:Load diagnostic-reasoning before scoping a reported bug, before acting on a diagnostic report, and before reporting that a search, query, or scan found nothing. --- AGENTS.md section 13 skill registry line: 528:- diagnostic-reasoning - load before scoping a reported bug, before acting on a diagnostic report, and before reporting that a search, query, or scan found nothing. lines containing the absence clause: 4 (expect 4) prose lines added to the skill, excluding heading and blanks: 11

### The four trigger lines firstmate actually reads (intent: patched in place, 4 lines, no new lines)
--- .agents/skills/diagnostic-reasoning/SKILL.md frontmatter description:
  Agent-only procedure for diagnosing reported bugs.
--- .agents/skills/diagnostic-reasoning/SKILL.md opening body line:
Use this procedure before scoping a reported bug, before acting on a diagnostic report, and before reporting that a search, query, or scan found nothing.
--- AGENTS.md section 7 intake line:
280:Load `diagnostic-reasoning` before scoping a reported bug, before acting on a diagnostic report, and before reporting that a search, query, or scan found nothing.
--- AGENTS.md section 13 agent-only skill registry line:
528:- `diagnostic-reasoning` - load before scoping a reported bug, before acting on a diagnostic report, and before reporting that a search, query, or scan found nothing.

### Do all four carry the same widened trigger clause?
lines containing the absence clause: 4 (expect 4)

### Prose lines added to the skill, excluding heading and blanks (intent: about twelve)
11
- Outcome: ⚠️ 1 info across 1 run (18m2s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • ⚠️ AGENTS.md:280 - The widened load trigger "before reporting that something is absent, empty, missing, or not found" has no carve-out for routine operational absence, which firstmate reports on nearly every turn: the session-start digest prints explicit ABSENT markers (AGENTS.md:174), firstmate relays "no registered secondmates" / "captain.md absent" (AGENTS.md:147), and "an empty queue is healthy" (AGENTS.md:237, :368). Read literally, each of those relays now requires loading diagnostic-reasoning, which conflicts with AGENTS.md:526 ("load them only at their precise triggers") and dilutes the trigger precision that makes the agent-only registry cheap. Consider scoping the clause to absence established by a query firstmate ran - e.g. "before reporting that something searched for or queried is absent, empty, missing, or not found" - so digest-sourced ABSENT markers and queue state stay out of it. This is the decision the author already flagged for possible reversion; the specific conflict with the digest's own absence vocabulary is the part worth deciding on.
  • ℹ️ .agents/skills/diagnostic-reasoning/SKILL.md:48 - The rule requires running the method against "data known to exist" before reporting absence, and line 57 says to treat an unproven empty as a failure until shown otherwise. When no known-present sample exists - a newly created bucket, an empty namespace, a collection that genuinely never had rows - there is nothing to run the positive control against, so "confirmed absent" becomes permanently unreachable and every true absence is classified as a query failure. The file already models the escape valve for exactly this shape at line 21 ("If a faithful reproduction is not feasible, record the exact limitation and use the closest representative path without presenting it as equivalent evidence"). A half-line - fall back to a second independent method, or state the absence with the missing control disclosed - would close it without breaking the deliberate ~12-line budget.

🔧 Fix: narrow absence trigger to query results; add positive-control fallback
✅ Re-checked - no issues remain.

⚠️ **Test** - 1 info
  • ℹ️ Five tests in the changed-file set fail on this machine for reasons unrelated to this Markdown-only diff: fm-composer-lib, fm-kimi-harness and fm-muse-harness are the three specs already documented as red on main in issue firstmate repo: bin/fm-lint.sh fails 127 on main (actionlint missing) + 3 pre-existing red specs #2753, and fm-lint plus fm-lint-workflows fail because actionlint is not installed (fm-lint-workflows.sh: actionlint not found; install actionlint 1.7.12 for CI parity), also issue firstmate repo: bin/fm-lint.sh fails 127 on main (actionlint missing) + 3 pre-existing red specs #2753. Installing actionlint would be a system-level change outside this worktree, so I left the environment alone.
  • bin/fm-doc-audience-check.sh - passes, 70 surfaces and 258 local links validated
  • bin/fm-test-run.sh tests/fm-documentation-audiences.test.sh - passes, including the owner-pointer and local-link assertions that cover the edited AGENTS.md and SKILL.md
  • bin/fm-test-run.sh --changed - 32 scripts, 27 pass; the 5 failures are pre-existing per issue #2753 (3 specs red on main, 2 lint scripts exit 127 from missing actionlint)
  • Manual: rendered .agents/skills/diagnostic-reasoning/SKILL.md to HTML and captured a full-page screenshot via chrome-devtools-axi screenshot --full-page, showing the new section highlighted between "Test the causal explanation" and "Scope and act on the result"
  • Manual: bash negative-result-drill.sh - reproduced all six incident shapes from the intent against this repo's real data (nonexistent subcommand + grep -c, parenthesized URL in a shell loop, listing endpoint that cannot see one level down, pattern matching a second path shape, SKILL.md key collision collapsing 21 rows to 1, two broken checks falsely corroborating), each paired with the positive control that exposes it
  • Manual constraint checks: git show 3d125ad:AGENTS.md | wc -l vs wc -l &lt; AGENTS.md (573 = 573, zero growth); grep -n &#39;^## &#39; on SKILL.md for section placement; count of prose lines in the new section (11); git diff | grep &#39;^+.*—&#39; (no em dashes); grep -c of the widened trigger clause across both files (4 of 4 expected lines)
⚠️ **Document** - 1 info
  • ℹ️ .agents/skills/diagnostic-reasoning/SKILL.md:65 - The skill's closing checklist at .agents/skills/diagnostic-reasoning/SKILL.md:65 enumerates what a diagnosis brief must ask for (reproduction, trigger/mask/symptom separation, divergent and proven path comparison, relevant history, smallest counterfactual, disconfirming evidence) and was not extended to cover the new negative-result rule. This may be deliberate: the intent explicitly asks for a tight section that does not restate existing procedure, and the new rule binds mainly on the reporter's own captain-facing statements rather than on brief contents. But a delegated investigation that reports "no occurrences found" is exactly the case the new section governs, and the brief spec currently does not ask for the positive control behind such a claim. Resolving it either way is a scope call for the author: extend that one sentence with the positive-control requirement, or leave the checklist as the pre-existing procedure summary only.
⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 127)
✅ **Push** - passed

✅ No issues found.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

VISION verdict: align. An unproven empty result is a query failure, not evidence of absence. That keeps the interface honest under load. The load-trigger widening is scoped to query-derived absence, so digest ABSENT markers and healthy-empty-queue relays stay out of it. Token cost is the extra skill load at those moments, bounded by the carve-out.

Class: default-behavior.

Reviewed the full diff vs base. Docs-only: a short section on .agents/skills/diagnostic-reasoning/SKILL.md plus in-place trigger lines in AGENTS.md (zero net AGENTS.md growth). Positive-control fallback for a target with no known-present sample is present. No executable interface.

Security: no.
Fork CI: approved on this HEAD (CI + Require no-mistakes). Checks are not yet green.
Ahead 2 / behind 1. mergeable MERGEABLE, mergeStateStatus UNSTABLE.
Overlap: AGENTS.md also in #2684/#2757 (this PR only edits the diagnostic-reasoning trigger). Not the held pair, not spawn-freshen.

Waiting on CI including no-mistakes, not on the captain. Not merge-eligible yet. Not a captain-flag.

@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Reviews (3): Last reviewed commit: "no-mistakes: apply CI fixes" | Re-trigger Greptile

Comment thread .agents/skills/diagnostic-reasoning/SKILL.md
tpavay added 4 commits August 21, 2026 19:26
A zero, an empty result, or a "not found" is a claim about the method as
much as about the world. Reporting absence from a method never shown to
find what is there produces confident false statements, and two checks
that share a failure mode read as corroboration while being wrong
together.

Add a short section to the diagnostic-reasoning skill: prove the method
against data known to exist before trusting a negative, distinguish
found / confirmed absent / the query failed rather than only present or
absent, require an independent method for corroboration, and hold
captain-facing absence claims to the same burden as any other
load-bearing finding.

Widen the skill's load trigger, in its own description and in AGENTS.md
sections 7 and 13, to cover reporting that something is absent, empty,
missing, or not found, so the rule is loaded where that class of claim
is actually made rather than only during bug scoping.
@tpavay
tpavay force-pushed the fm/fm-negative-result-rule branch from e36148e to 3b857c1 Compare August 22, 2026 00:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants