Skip to content

feat(capture): flag-gated #xray:capture navigation marker (27 K.4)#182

Closed
bryanmatthewsimonson wants to merge 4 commits into
claude/phase-27-k1-capture-skillfrom
claude/phase-27-k4-capture-marker
Closed

feat(capture): flag-gated #xray:capture navigation marker (27 K.4)#182
bryanmatthewsimonson wants to merge 4 commits into
claude/phase-27-k1-capture-skillfrom
claude/phase-27-k4-capture-marker

Conversation

@bryanmatthewsimonson

Copy link
Copy Markdown
Owner

Summary

Phase-27 slice K.4 — the capture-automation enabler, built after live preflight found the K.1 skill's driving paths dead against the real connector. Stacked on #181.

What the live run proved

claude-in-chrome cannot navigate to chrome-extension:// URLs (forces https://) and refuses to attach its debugger to extension pages — so every extension-page surface the skill leaned on (tabs.sendMessage, IDB reads, xray:llm:suggest) is unreachable. Synthetic keys don't fire command shortcuts; OS keystrokes into browsers are tier-blocked. Navigation of ordinary pages is the only verb an agent has.

The enabler

Opening <url>#xray:capture on any ordinary page triggers the same UI.openReader() capture the toolbar button fires — iff the new captureAutomation flag is on (default OFF, pin-tested; Options → Advanced toggle whose copy says exactly what it gates). The marker is stripped before capture; the outcome is stamped on the page DOM (data-xray-captured: ok/error/flag-off) so the driver verifies without extension access. Toolbar/shortcut/menu capture paths are untouched and unconditional.

Skill rewritten against reality

Marker-driven capture with DOM-stamp verification and per-URL pacing; frontier enumeration from a user-provided workspace-backup export; PDFs handed to the user as a ?pdf= reader link; suggest-pass scouting marked not-runnable (the reader's Suggest button is the human path). Connector limits documented at the top so future runs don't rediscover them.

Tests: 1,797 green (1 new pin) · build clean · lint 0 errors.

🤖 Generated with Claude Code

bryanmatthewsimonson and others added 2 commits July 16, 2026 17:36
Live preflight of the K.1 skill against the real connector found both
driving paths dead: claude-in-chrome cannot navigate to
chrome-extension:// URLs (it forces an https prefix) AND refuses to
attach its debugger to extension pages — so extension-page JS
(chrome.tabs.sendMessage, IDB reads, xray:llm:suggest) is unreachable,
synthetic keys do not fire command shortcuts, and OS keystrokes into
browsers are tier-blocked. Navigation of ordinary pages is the only
verb an agent has. This is the K.4 slice the decision doc anticipated
"where browser-driving is impractical".

- content script: on init, if location.hash === "#xray:capture" AND
  the new captureAutomation flag is on, strip the marker
  (history.replaceState; the URL normalizer would drop it from stored
  URLs regardless), wait 1.5s for dynamic pages to settle, and trigger
  the SAME UI.openReader() the toolbar path uses. The outcome lands on
  document.documentElement.dataset.xrayCaptured (ok / error /
  flag-off) so the driver verifies from ordinary page context — no
  extension access needed.
- captureAutomation joins FLAGS_DEFAULTS (default OFF, pin-tested) +
  an Options → Advanced toggle whose copy says exactly what it gates:
  the marker only; toolbar/shortcut/menu capture stay unconditional;
  captures pages, nothing more — no accepting, no publishing.
- SKILL.md rewritten against verified reality: marker-driven capture
  with DOM-stamp verification, frontier enumeration from a
  user-provided workspace-backup export (extension IDB unreachable),
  PDFs handed to the user as a ?pdf= reader link, and suggest-pass
  scouting marked not-runnable by the connector (the reader Suggest
  button is the human path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
17 confirmed findings from the 21-agent review of the S.1..K.2 diff
(one — the skill IDB-creation hazard — was already erased by the K.4
rewrite). Applied on the stack tip so every PR in the chain carries
them at merge:

MAJORS
- llm-review blockedReason now gates finding/baseline rows on subject-
  entity acceptance (the F.3 identity join was order-dependent:
  accepting a finding before its subject entity silently landed it
  label-keyed — fragmented and unpublishable). Label-only subjects
  stay acceptable.
- EDIT_FIELDS.baseline gains the subject_ref field (correcting a
  baseline subject via the label was cosmetic while the stale ref kept
  the identity keyspace); both subject field labels now state the
  precedence rule (ref wins; blank ref = label).
- Re-running synthesis carries record.triage forward — content-derived
  keys make re-proposed pairs keep their status; without this every
  corpus-v2 re-run resurrected every dismissed proposal. Plus the pure
  partitionProposals seam extracted from the DOM render + tests
  (partition, unknown-status-stays-open, flipped-endpoint stability).

MINORS
- case-view local-chip pass: endpoints canonicalized ONCE into a
  ref-indexed map (was rows x all-links x canon re-parses — ~40x on a
  loaded workspace) and capped at 6 chips/row matching the published
  cap, with a "+N more local" disclosure.
- Reduce prompt: art keys are for READING the index; anywhere the tool
  asks for article_hash, supply the FULL hash from `articles` — never
  an art key.
- Finding modal: switching between two baselined subjects no longer
  carries a stale cross-subject baselineRef; editing a finding whose
  subject is no longer tagged falls to the custom slot with its own
  label instead of silently re-pointing at the first choice.
- Role validation message interpolates the live ROLES (was a
  hardcoded pre-F.6 list).
- Baseline delete clears baseline_ref on referencing findings (with
  the blast radius stated in the confirm); the dangling-ref render
  branch is now tested (generic no-weight tooltip, nothing fabricated).
- 30062 parser role tolerance pinned (journalist parses; a foreign
  "prosecutor" passes through on read — the NIP_DRAFT MUST, tested).
- DIGEST_CLAIM_CAP x art-key interaction pinned (the articles map
  derives from the SAME capped slice).
- Stale entitiesById comment corrected (S.2 made a builder read it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bryanmatthewsimonson

Copy link
Copy Markdown
Owner Author

A trailing commit on this branch applies all 17 confirmed findings from the Phase-27 adversarial review (3 majors: the order-dependent F.3 identity join, the cosmetic baseline subject edit, and triage lost on re-run). Findings span the whole S/F/K chain; they ride here at the tip so the stack carries them at merge — details in the commit message.

Found in the first live agent-driven capture run, on archive.is —
several COVID-corpus frontier links are archive.is snapshots of
paywalled reporting, and the marker fired exactly once there, then
never again:

  SecurityError: Failed to execute replaceState on History: A history
  state object with URL https://d8w6fmknl1l6hl.archive.is/7gYpy cannot
  be created in a document with origin https://archive.is

archive.is serves snapshots under a randomized subdomain and sets a
<base href> to it. replaceState resolves a RELATIVE argument against
the document base URI — not its origin — so the rewrite targeted
another host and threw. The strip sat before UI.openReader() inside
the same try, so a cosmetic address-bar tidy killed the capture it
preceded. It appeared to work on the first hit only because the base
tag had not been applied yet — which is also why that capture archived
the interstitial instead of the article.

- Rebuild the replacement URL ABSOLUTE from location.origin (a
  relative one is another site\s <base href> to redirect), and give
  the strip its OWN try/catch: cosmetics can never abort load-bearing
  work behind them. A failed strip now logs and continues; the URL
  normalizer drops fragments from stored URLs regardless.
- A hash-only navigation (<url> → <url>#xray:capture) is a
  same-document navigation, so init() never re-ran and the marker
  silently no-oped; a hashchange listener now covers it. The marker
  branch is extracted to maybeAutomationCapture() and stamps "error"
  on any throw instead of staying silent.
- SKILL.md: SPA settle times (~10s), the stamp-is-not-content rule
  (check title/length — an "ok" stamp on an interstitial is the real
  failure mode), re-capture via full navigation, console-first
  debugging, and an honest per-URL reporting section.
- JOURNAL entry: the generalizable lesson is that a try-block wrapping
  a nice-to-have around a must-have inherits the nice-to-have\s
  failure modes.

No unit test would have caught either bug — both need a real page with
a cross-origin base tag. Found by running the tool for real.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bryanmatthewsimonson

Copy link
Copy Markdown
Owner Author

Field-tested and fixed. The first live capture run (12 COVID-corpus articles) surfaced a real bug in this slice on archive.is: the marker-strip's replaceState resolved its relative argument against the page's cross-origin <base href>, threw SecurityError, and — because the strip sat before UI.openReader() in the same tryaborted the capture it was supposed to precede. It only appeared to work on the first hit because the base tag hadn't applied yet (which archived the interstitial instead of the article).

Fixed here: the replacement URL is built absolute from location.origin, the strip gets its own try/catch (cosmetics never abort the capture), the marker branch stamps error on any throw instead of failing silently, and a hashchange listener covers the same-document-navigation no-op. Skill doc gains SPA settle times, the stamp-is-not-content rule, and console-first debugging. JOURNAL 2026-07-17 records the generalizable lesson.

Run result: 11/12 captured cleanly with zero paywalls — Proximal Origin, the WHO study, Rootclaim, the DEFUSE reporting, both Huanan market papers. Tests 1,802 green.

Model picker (llm-prompts.js LLM_MODELS) gains Claude Fable 5 and
Claude Sonnet 5. The table is the single source of truth — the Options
dropdown renders it verbatim and every pass resolves through
resolveModel() — so the additions are the table rows plus the pin test
asserting every offered id resolves to itself (a picker entry that
silently fell back to the default would be a lie).

Payload compatibility verified rather than assumed: the passes send
only model/max_tokens/system/tools/tool_choice/messages — no
temperature, top_p, top_k, or thinking/budget_tokens, which are exactly
the params Fable 5 and Sonnet 5 reject with a 400. Both work unchanged.
Fable 5 needs no thinking config (thinking is always on there; an
explicit disabled would 400).

Refusal handling (the real gap): Fable 5 runs safety classifiers that
can DECLINE a request — HTTP 200, stop_reason "refusal", empty or
partial content. Only the lens pass handled that; the other six fell
through to extractToolInput() → null → "the model did not return a
structured extract", blaming malformed output for a guardrail and
sending the user hunting a bug that does not exist. A shared
refusalResult() helper now generalizes the lens pass's own §6 rule
("a guardrail firing is its own state") across suggest, audit, audit
module, corpus map, corpus reduce, and hypothesis edges — carrying the
safety category when present, stating it is not a key/network/X-Ray
problem, and naming the workaround (switch model in Options).
stop_details is treated as optional, per the API contract.

This matters concretely for the COVID-origins corpus: it is
bio-adjacent, and Fable 5's classifiers target research biology — a
refusal there is plausible and must read as a guardrail, not a bug.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bryanmatthewsimonson

Copy link
Copy Markdown
Owner Author

Also on this branch: the LLM model picker now offers Claude Fable 5 (claude-fable-5) and Claude Sonnet 5 (claude-sonnet-5).

Adding them was a two-row change — LLM_MODELS is the single source of truth and the Options dropdown renders it verbatim. Payload compatibility was verified, not assumed: the passes send only model/max_tokens/system/tools/tool_choice/messages, and never temperature/top_p/top_k/thinking — which are exactly the params Fable 5 and Sonnet 5 reject with a 400. Both work unchanged.

The real find: only the lens pass handled stop_reason: "refusal". Fable 5 runs safety classifiers that decline with HTTP 200 + an empty content array; the other six passes fell through to "the model did not return a structured extract" — blaming malformed output for a guardrail. A shared refusalResult() now generalizes the lens pass's own rule (a guardrail firing is its own state) across all seven, carrying the safety category and naming the workaround. This is not hypothetical for the COVID corpus: it's bio-adjacent, and Fable 5's classifiers target research biology.

Tests 1,804 green.

@bryanmatthewsimonson

Copy link
Copy Markdown
Owner Author

Landed on main via the #183 consolidation merge (the stacked-merge cascade closed/retargeted intermediates; every commit from this PR is in main).

@bryanmatthewsimonson
bryanmatthewsimonson deleted the claude/phase-27-k4-capture-marker branch July 18, 2026 05:19
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.

1 participant