Skip to content

chore(cowork): adopt cowork-harness 0.16.0 (additive — H10 decider fix) - #6

Closed
yaniv-golan wants to merge 193 commits into
mainfrom
chore/cowork-harness-0.16.0
Closed

chore(cowork): adopt cowork-harness 0.16.0 (additive — H10 decider fix)#6
yaniv-golan wants to merge 193 commits into
mainfrom
chore/cowork-harness-0.16.0

Conversation

@yaniv-golan

Copy link
Copy Markdown
Contributor

What

Bumps the cowork-harness target 0.15.0 → 0.16.0 across the three tracked touchpoints (CI exact pin, rerecord.sh floor, cowork-tests/README.md).

Why

0.16.0 is the H10 decider work — the exact --decider-llm gate-whiff class that stalled 5 of 6 live real-doc runs last campaign:

  • --decider-llm now replies by option NUMBER (harness maps number → canonical label) → no more option-echo whiff; also answers multiSelect gates.
  • New --decider-model <id> to throw a stronger model at ambiguous judgment gates (live/authoring only — never reaches a committed cassette).
  • Scripted choose:/--answer gains a prefix anchor (boundary startsWith, uniqueness-guarded, fails loud on ambiguity — prefix-only, fires at record time only).
  • New docs/debugging.md on-ramp.

Additive — no re-record forced

Cassette format stays v6, baseline 1.15200.0, agent ELF 2.1.181 — all unchanged (CHANGELOG flags none). Verified by the token-free gate on 0.16.0 against the 13 committed cassettes:

gate result
lint GREEN (0 err / 0 warn)
privacy (allowlist) GREEN (13 clean)
email canary trips (PII tripwire live)
replay ok=True, all 13 success

Replay re-emits the recorded answers and never runs the decider, so replay-green proves format compat; the new prefix-anchor tier is lowest-priority and fires only at record time, so it cannot change a passing replay. The pre-existing staleness WARN (agents/cap-table.md shared-root edits + 2 smokes on the old baseline) is the separate release-cadence re-record item, not introduced here.

--fail-on-skill-drift remains deliberately unadopted (cap-table under active modification + shared roots → would red all 13).

🤖 Generated with Claude Code

yaniv-golan and others added 30 commits June 11, 2026 07:14
ci.yml's typecheck job ran mypy on five skills but omitted
founder-skills/skills/cap-table/scripts/, which CLAUDE.md's Type Checking
section requires — a recurrence of the bug where competitive-positioning
was silently missing from the matrix. Cap-table is the largest script
surface and was shipping untypechecked in CI.

Add the cap-table mypy line, guard the un-stubbed optional pdfplumber
import with a type:ignore so the dir typechecks cleanly under dev extras,
and add test_ci_mypy_matrix_matches_claude_md so any skill listed in
CLAUDE.md's Type Checking section but missing from ci.yml fails CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README advertised five coaching agents and omitted cap-table (shipped in
0.5.0) entirely. Update the intro to six agents, add a cap-table section
matching the existing skill-section format (one-line desc, what-it-does,
example prompts, SKILL.md link), and add cap-table to the auto-activation
list. Correct the Python badge and Development requirement from 3.12+ to
3.10+ to match pyproject's requires-python = >=3.10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CONTRIBUTING's pre-push mypy block and skill-template list omitted
cap-table; add it to both so contributors typecheck the newest skill.
Correct the Python requirement from 3.12+ to 3.10+ to match pyproject.
SECURITY's Scope listed scripts for five skills but not cap-table, whose
extractors parse untrusted founder documents (PDF/DOCX/XLSX) — add it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…le test

Rewrite the Release Process line that called sync-test-repo.sh 'the actual
user-facing distribution event'. It is a local untracked TESTING step
pushing to the private test repo; users install from the marketplace clone
that tracks main, so plugin.json#version on main is what they see — now
reconciled with VERSIONING.md. Align the e2e cost/duration figures with the
pyproject marker and CHANGELOG (~5-20 min, ~$5-15) instead of the outlier
~2-5/run and 60-180s. Add test_visualize_cap_table.py to the Repository
Structure list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…le release cmd

Tagging is no longer 'a courtesy for git log archeology' — pushing a vX.Y.Z
tag triggers the deck-review-e2e-smoke release gate, and distribution waits
for green; document the tag -> wait-for-green ordering in How to Release.
Reconcile the 'No Version Bump Needed' list with version-check.yml's
requires_bump filter in both directions: all *.md is exempt (so SKILL.md /
reference rewrites are bumps by policy, not mechanically forced) and any
other founder-skills/* file forces a bump (including plugin.json metadata).
Replace the GNU-only 'head -n -1' in the gh release recipe with portable
'sed $d', which works on macOS BSD head.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
VERSIONING.md mandates every version entry start with a ### Highlights
section, but 0.5.0, 0.4.7, 0.4.6, 0.4.5, 0.4.4, 0.4.2, 0.4.1, 0.4.0, and
0.3.1 lacked it (0.5.0 had lead prose but no heading). Add content-neutral
one-liners drawn from each entry's own text. Also fix the 0.5.0 hardening
bullet whose escape description read '< escaped to <' (source and target
identical) — the actual escape is < to \u003c.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CLAUDE.md's Plugin Structure rule states marketplace.json must contain only
name, owner, plugins. The manifest carried a fourth top-level description
key. Remove it to comply; the per-plugin description is retained. claude
plugin validate still passes (now with an informational warning that no
marketplace description is set).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cmd_merge now deep-merges nested dicts via _deep_merge instead of a
single-level dict.update that silently clobbered sibling keys in nested
structures (e.g. fundraising.round). Protected metric fields remain gated
by _check_protected_fields, which runs before the merge.

Also document the update-identity subcommand in the module docstring and
correct the cmd_validate comment (it is a gate; no stdout output). Add a
regression test asserting nested sibling keys survive a merge.

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

Add --pretty and -o/--output flags. Success output is now a JSON object
{"path": ...} on stdout (or written to the -o file, with a receipt object
on stdout). Not-found/ambiguous messages stay on stderr. Update existing
tests to parse the JSON path and add coverage for the JSON object, --pretty,
and -o receipt behaviors.

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

- aoa: check full-ratchet first with specific markers (full ratchet /
  lowest price / reduced to the price) before the generic broad-based
  conversion-price-adjustment patterns, so full-ratchet AOAs are no longer
  misclassified as broad-based weighted average.
- safes: CLA marker uses \bCLA\b word boundaries (no longer counts
  'clause'/'class'/'claim'); store only investor-name detectability, never
  the captured real party name.
- term_sheets: route all progress/summary output to stderr; -o now writes a
  full aggregate report dict (not just the per-file list); add re.IGNORECASE
  to classify_doc_type / classify_jurisdiction / detect_law_firms for parity
  with sibling scripts.
- convertibles: replace insecure tempfile.mktemp with mkstemp for
  zip-member extraction.
- captables: drop "portfolio" corpus phrasing; remove unreachable row-1
  fallback dead code in the header scan.
- All five scripts: add PEP 723 inline metadata declaring pdfplumber / xlrd /
  python-docx as each uses.

Add test_corpus_scripts.py covering the AD-ordering, CLA word-boundary, and
case-insensitive jurisdiction behavior changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fund_profile.py, detect_conflicts.py, and score_dimensions.py now take a
required --run-id and set result.metadata.run_id as the last step before
serialization (overriding any stdin metadata). Without this the conflict_check
and score_dimensions artifacts lacked run_id, so the agent's Context B run_id
parity check blocked the final coaching step on every run. SKILL.md's Step 4 /
5a / 7 pipes pass --run-id "$RUN_ID" so the existing 'producer script adds it'
note is now accurate.

Also: detect_conflicts dedup tolerates non-string company/type values instead
of crashing on .strip(); score_dimensions reports the true list position in the
non-dict item error; prior_artifacts stub template uses skipped:true.

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

compose_report.py now derives coaching_payload.consensus_strength from
discussion.json partner_verdicts (strong=3 matching, mixed=2-1, weak otherwise)
so the agent's Context B success payload has a defined source for the field.

Reorder composition so status, the rendered Warnings section, validation.warnings,
and coaching_payload all observe the same final warnings list: render all sections
except Warnings, pre-scan the body for the marker substring, append MARKER_COLLISION,
compute status, then render and splice the Warnings section. Previously status was
computed and Warnings rendered before MARKER_COLLISION was appended, so a colliding
body produced status 'clean' with an invisible warning.

Add isinstance guards to renderers and the PARTNER_UNANIMITY check (partner_verdicts,
archetypes, check_size_range, by_category counts, scorecard items) and to the
ORPHANED_CONFLICT normalization so malformed agent artifacts degrade to warnings
instead of crashing with AttributeError.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h model

Add the metadata.run_id row + example to every artifact, fix fund_profile sources
to object[] with url/title (matching fund_profile.py validation), correct the
producer step numbers and producer attributions for conflict_check (Context A
dispatch piped through detect_conflicts.py) and partner_assessment (dedicated
ic-sim agent dispatched three times in parallel), and the prior_artifacts stub
form. Fix the READ-FIRST preamble step numbers and dispatch order.

Add regression tests: required --run-id and metadata injection for all three
producers, consensus_strength derivation, marker-collision ordering, non-string
company dedup, score_dimensions item-index, and renderer malformed-input tolerance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CHECKLIST sub-agent returns items only. checklist.py defaulted
input_mode to "conversation" (mis-gating NARR_03/EVID_04 in deck/document
runs) and left metadata empty (no run_id for the Context B verifier to
grep, deterministically blocking literal runs at Step 7c).

Add --input-mode {deck,conversation,document} and --run-id flags with
precedence CLI > stdin JSON > default. The main thread stamps both via the
Step 6 pipe; the sub-agent body now notes input_mode/metadata.run_id are
CLI-stamped. Establish INPUT_MODE in Step 2 and document the run_id source
in artifact-schemas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- compose/visualize/explore _load_artifact: return _CORRUPT for non-dict
  top-level JSON (array/string/number) so wrong-shape valid JSON degrades to
  the CORRUPT_ARTIFACT/placeholder path instead of crashing on .get()
- compose: single >=75 'strong' threshold in the executive summary so the
  score label and the prose paragraph never disagree on [70,75)
- compose: reorder so the Warnings section is spliced AFTER the marker
  prescan can append MARKER_COLLISION; str()-coerce non-string view_id
- score_moats: reject non-string evidence with a structured error instead
  of a TypeError traceback
- explore: disclose the 3D-tab Plotly CDN dependency in the docstring;
  drop the dead duplicate display:flex on #3d-axes-bar so it stays hidden
  until render3D populates it
- validate_landscape: remove the dead no-op --stdin flag

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-criterion note claimed EVID_02 auto-gates to not_applicable in deck
mode, contradicting the gating table, the gated-items summary, and
checklist.py (which gates only EVID_04 in deck mode). An agent following the
prose would hand-mark EVID_02 n/a and inflate deck-mode scores. State that
EVID_02 is active in all modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- checklist --input-mode/--run-id precedence + gating (items-only input)
- score_moats non-string evidence -> structured error, no traceback
- compose/visualize/explore non-dict artifact -> CORRUPT/placeholder, no crash
- compose executive-summary single 'strong' threshold ([70,75) not 'strong')
- compose non-string view_id does not crash
- explore 3d-axes-bar initially hidden + docstring discloses Plotly CDN
- validate_landscape --stdin flag removed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…run-id, fails closed on -o validation errors

The producer emitted evidence/notes as null when the sub-agent omitted them,
but checklist.schema.json types both as plain strings — so a valid all-pass
run tripped a false-positive high-severity SCHEMA_VIOLATION in compose_report.
Now those keys are omitted when absent. --run-id is required (matching every
sibling producer; the artifact's metadata.run_id is schema-required). In -o
(artifact-producer) mode, invalid input now prints to stderr and exits 1 with
no artifact written, instead of writing an error-shaped file with an ok:true
receipt. The committed fixture is regenerated from real producer output and
now validates against the schema.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…formed shapes, count acknowledged warnings

Reorder compose so the body is rendered first, the marker pre-scan appends
MARKER_COLLISION, then status is computed and the Warnings section spliced in
— previously status could read 'clean' while a MARKER_COLLISION warning sat in
the warnings list. Add _as_dict coercion to every per-row section iterator
(checklist by_category, failed/warned items, slide reviews, missing slides,
full-checklist appendix) so a malformed-but-schema-passing artifact degrades
gracefully instead of raising AttributeError. The stderr summary now counts
'acknowledged' warnings and drops the always-zero 'info' bucket. The schema
validator now rejects bool for type=number (not just type=integer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ile gains --confidence

setup_run.py now decides resume vs. fresh run by comparing the answered
gate_state.json's run_id against --run-id, and emits gate_answer / gate_run_id
/ resume. A resume is valid only when the answer is non-empty AND the run_id
matches; on a fresh (non-resume) --clean run it deletes a stale answered
gate_state.json so a prior completed run cannot be misread as a resume — which
previously caused a fresh review of the same company to reuse the old run's
artifacts.

stage_profile.py adds --confidence {high,low} (default high), meaningful with
--rebuild-stage: when rebuilding to the detected stage at low confidence the
evidence line reads 'Founder unsure; proceeding with detected stage at low
confidence'. Previously --rebuild-stage hardcoded high, contradicting the
gate branches that require low.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n visualizer

_chart_provenance_summary called float() on raw deck claims and sizing values
guarded only by 'is not None', so a string claim (e.g. "$5B") aborted the whole
HTML render. Add a _try_float helper mirroring _compute_delta's tolerance and skip
the provenance/delta cell instead of crashing. Also render a present-but-null notes
field as empty instead of the literal 'None', and align REQUIRED/OPTIONAL_ARTIFACTS
with compose_report.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…legend color + gauge docstring

gate_state.py answer now catches JSONDecodeError and rejects non-dict JSON
with a clean stderr message + exit 1, matching cmd_emit, instead of raising a
raw traceback / AttributeError. visualize.py: the 'Missing expected slide'
legend swatch now uses #9ca3af to match the chart's dashed-line stroke, and
the _chart_score_gauge docstring no longer describes baseline-extended arcs
and a score arc the code does not draw.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
market_sizing.py, sensitivity.py, and checklist.py now accept --run-id and stamp
metadata.run_id into their output as the last step. This is what lets compose's
STALE_ARTIFACT detection and the Step 8 Context B run_id self-verification operate
on these artifacts. The flag is optional and backward compatible (no metadata key
emitted when absent). Also dedup growth_rate/years coercion errors in market_sizing
'both' stdin mode and count only analyzed scenarios in the sensitivity -o receipt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ext-B behavior

Step 0 now always calls setup_run.py --clean --run-id and branches on its
resume output instead of pre-reading gate_state.json and rehydrating RUN_ID
from it. The gate re-invocation check adds a run_id comparison. Gate branches
use the staged-stdin invocation of stage_profile.py with explicit --confidence
(high for an explicit different stage, low for unsure/best-effort) and the
required --run-id/-o. Step 9 removes a leftover gate_state.json. Dispatch
templates use a resolved <REFS> placeholder (not literal CLAUDE_PLUGIN_ROOT,
which does not resolve in the Cowork VM). Context B is described as receiving
the coaching_payload and editing via marker (not reading report.md).
Available Scripts lists all 8 workflow scripts; fundraise-readiness export is
marked (future).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tatus and Warnings section

Render all sections except Warnings, run the marker pre-scan and append
MARKER_COLLISION, compute status, then splice the Warnings section — so a marker
collision is reflected in both validation.status and the rendered report body
instead of only the JSON warnings array. Also derive coaching_payload.confidence
from the checklist score_pct (>=85 high / >=60 medium / else low) so Context B reads
it directly rather than fabricating it. Guard accepted_warnings against non-dict
entries, regroup the low-severity codes under their own header, correct the
17-checks docstring, and truthify the --strict help to high/medium-severity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…matrix, confidence, compose reorder, guards

Adds/updates regression tests: checklist omits null evidence/notes and its -o
output validates against the schema; -o validation failure exits 1 with no
write; the committed fixture matches the producer shape. setup_run resume
matrix (answered+match keeps gate, answered+mismatch deletes stale gate,
unanswered preserves). stage_profile --confidence high/low branches.
compose MARKER_COLLISION makes status 'warnings' not 'clean'; malformed
by_category does not crash; acknowledged warnings are counted in stderr.
gate_state answer handles corrupt/non-dict files. schema validator rejects
bool as number. visualize legend color matches the chart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ducer contract

SENSITIVITY_TEST dispatch templates (SKILL.md + agent) now emit per-range
confidence so sensitivity.py's auto-widening fires. Producer pipes pass --run-id.
Step 8 reads confidence from report.json coaching_payload.confidence instead of
nonexistent sizing.json/checklist.json fields. artifact-schemas.md: document
metadata.run_id on every artifact, add score_pct to the checklist summary,
reconcile inputs.json fields to the SKILL.md template (stage/sector/
target_segments/revenue_model; drop target_customer), drop reference_file_read,
renumber producer steps, and add quality_tier/segment_match to the Step 4 sources
template. Remove an internal issue-tracker reference from the skill body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… math + validators

dict.get(default) only applies to missing keys, not keys present with an
explicit JSON null. The corrections coercion layer writes None for blank/
cleared cells, so the math producers and validators crashed (TypeError /
AttributeError) on realistic inputs.

- runway.py: add _num() helper; guard cash.debt, IIA grant fields,
  ils_expense_fraction, runway_target_months.
- unit_economics.py: guard headcount count/salary/burden in the ARR/FTE
  path and the magic-number S&M loop. Drop the unreachable < $1M-ARR
  Rule-of-40 elif (already handled by the first branch).
- validate_inputs.py: report null headcount count/salary as a Layer-1
  TYPE_ERROR rather than crashing the sanity arithmetic; guard metadata
  == null in validate().
- validate_extraction.py: guard null headcount count in scale-plausibility;
  add a minimum-length floor to _fuzzy_match so a 1-2 char cell cannot
  false-pass the COMPANY_NAME anti-hallucination gate.

Regression tests pipe each null shape and assert no crash / correct report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…+ Warnings render

Reorder compose so the warnings list is final before status and the
Validation Warnings section are computed: render every section except
Warnings, run the marker pre-scan (which may append MARKER_COLLISION),
compute status, then splice the Warnings section in. Previously status and
the rendered Warnings section were finalized before the pre-scan, so a
MARKER_COLLISION-only run reported status "clean" with a non-empty
warnings list and the warning never rendered in the report.

Also port the negative-value branch into _fmt_usd (negatives fell through
to "$-1,500,000.00"; now "-$1.5M", matching visualize.py), and drop the
dead CHECKLIST_FAILURES severity-downgrade block (the severity map already
maps it to medium unconditionally).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iter hardening

- apply_corrections.py: add -o support that writes the status JSON to a
  file and emits a {"ok": true, "path", "bytes"} receipt on stdout (matching
  the shared script convention). Guard the legacy 'corrected' branch: a
  non-dict 'corrected' now yields a structured INVALID_PAYLOAD error instead
  of a raw traceback.
- verify_review.py: -o now emits the JSON receipt on stdout (was writing the
  raw result with nothing on stdout) and makedirs the parent. Drop the unused
  model_format parameter (and the now-dead inputs load) from _check_existence.
- review_inputs.py: /api/check logs each validate/metric exception to stderr
  instead of bare `except: pass`; _write_static now makedirs parents and
  guards the root path, matching every other output writer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
yaniv-golan and others added 24 commits June 23, 2026 22:53
…t to label anchors

The two deferred items from the 0.10.0 adoption plan, done under 0.10.0.

§5 — re-record the 6 cap-table cassettes that went stale vs the recent gate-quality
edits (SKILL.md / lane-3-freeform.md). All 6 recorded ✓ success and now pass their
own staleness check (no more WARN for cap-table). Synthetic data only.

§3 — migrate the 4 cap-table scenarios off the blanket positional `choose: first`
(0.10.0 lint flags it as order-dependent). Each now pins its observed gates by EXACT
option label (anchored on the option-1 choice the prior `first` recorded, so behavior
is preserved but order-independent), with `on_unanswered: first` as the variance
backstop (preserves the old catch-all safety without the `answers: choose: first` the
lint flags). Validated with 0.10.0 verify-run answer-coverage: every fired gate matched
a scripted answer (no paid re-record needed for the migration). lint advisory cleared
for these 4 (info 20 -> 16).

Verification: all 12 cassettes replay success (none stalled); privacy 12 clean; the 6
cap-table cassettes fresh. The 5 fleet-smoke cassettes remain stale — entirely because
agents/cap-table.md (a fleet-wide shared root) changed in the earlier role-contract fix;
that's pre-existing, out of scope here, and WARN-only in CI. The fleet scenarios' own
choose:first is left as-is (harness docs endorse `first` for the labels-drift case; not
re-recorded in this batch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K2ohjtyuhRyL83ijyguTBQ
…ap-table.md staleness)

The fleet smokes were stale because agents/cap-table.md (a fleet-wide shared root)
changed in the earlier role-contract fix. Re-recorded all 5 under 0.10.0 — all
✓ success + replay green. Full staleness check now reports 12 cassette(s) clean
(the entire fleet is fresh; no remaining WARN). Synthetic data only; their
choose:first answers are unchanged (harness docs endorse `first` for labels-drift).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K2ohjtyuhRyL83ijyguTBQ
…e coverage gap)

Closes the biggest MEDIUM gap in the cap-table test plan: note_conversion had ZERO
LLM-in-the-loop coverage (the 7-branch enum is fully pytest-covered in test_cap_table.py;
no cassette exercised the runtime path). Adds a Lane-4 conversational scenario where the
founder describes a convertible note + an incoming qualified financing; the skill
reconstructs the note and runs note_conversion (cap_conversion: cap beats discount).

Modeled on cap-table-safe-full. Asserts hard-fact note reconstruction
(convertible_notes.0.principal=500000, valuation_cap=8000000) + structural presence
(scenarios.json, as-converted FD shares > 0) + parity. The one gate it fires — the note's
"Company Capitalization" denominator confirm — is pinned by the `recommended` keyword
(label-stable, not positional `first`), with on_unanswered:first as the backstop.

Verified under 0.10.0: record ✓ success (18 artifacts), replay ✓ success, privacy clean,
lint clean (no positional-choose advisory), verify-run all 10 asserts pass + answer
coverage 1/1 matched. Synthetic data only (Cadence / invented). README scenarios table +
counts updated (8 cap-table cassettes, 13 total).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K2ohjtyuhRyL83ijyguTBQ
…key (was silently dropped)

Real correctness bug surfaced by the cap-table test plan (BBWA CP1 verification). When a founder
asks for a down-round anti-dilution adjustment, the model sometimes writes the intent under the WRONG
key (`anti_dilution`, e.g. `"anti_dilution": "bbwa"`) instead of the canonical
`anti_dilution_protection`. The inputs schema permits extra keys (additionalProperties unset), so the
wrong key passed validation but cap_state.py read only `anti_dilution_protection` — defaulting AD to
"none" and SILENTLY SKIPPING the down-round adjustment the founder explicitly requested. This violates
the skill's anti-silent-drop ethos.

cap_state.py now resolves anti-dilution to the canonical enum, tolerating the wrong key and common
abbreviations (bbwa/nbwa/ratchet), and emits `W_ANTI_DILUTION_NONCANONICAL` (or `_UNRECOGNIZED`) so the
recovery/omission is never silent. The deterministic BBWA CP1-divisor math itself was already fully
pytest-covered (test_bbwa_divisor_uses_cp1_not_oip etc.) — this fixes the path INTO it.

- cap_state.py: `_resolve_anti_dilution` helper + a pre-pass that normalizes in place (so the
  comprehension + the ad_a_denominator default read the canonical value) + warning surfacing.
- test_cap_table.py: TDD'd — `test_anti_dilution_recovered_from_wrong_key` (was RED: AD silently
  "none"), `test_anti_dilution_canonical_field_unchanged_no_warning`.
- inputs-skeleton.md: canonical-field guidance (write `anti_dilution_protection` + the enum; never
  `anti_dilution`/`bbwa`).

Full non-e2e suite 2335 passed; ruff + mypy clean. Follow-up (consolidation): re-record the
cap-table-priced-ad cassette against this fix + add an assert that cap_state AD protection is
broad_based_weighted_average (so the silent drop can't regress in the cassette layer).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K2ohjtyuhRyL83ijyguTBQ
…lution lands

Completes the BBWA silent-drop fix end-to-end. The priced-ad cassette previously baked in the
bug (recorded cap_state preferred_series[0].anti_dilution_protection = "none" despite the founder
requesting BBWA). Re-recorded against the fixed cap_state.py: the founder's BBWA request now lands
as broad_based_weighted_average. Added a regression assert pinning that value so the silent drop
can never reappear in the cassette layer. Replay ✓, privacy + staleness clean. Synthetic data only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K2ohjtyuhRyL83ijyguTBQ
…tion_pool field hint

Issue 1 of the skill-hardening plan (two targeted parts; the generic unknown-key detector was
deliberately cut as over-engineered — generic catch-all is deferred to Phase-3 reject-at-the-gate).

Part A — surface the AD recovery warnings on BOTH render paths. _resolve_anti_dilution emits
W_ANTI_DILUTION_NONCANONICAL/_UNRECOGNIZED into cap_state.warnings[], but no founder-facing markdown
rendered them (the shipped AD fix was invisible). compose_report now renders them via a
_render_warning_callouts helper that matches the AD family by PREFIX (interpolated sentences, not bare
codes). concise_report — the dominant route, since a standalone anti-dilution question routes to concise
mode — gains a --cap-state arg + render branch, and SKILL.md wires --cap-state into the Step-5-concise
invocation (without that the script gains an unused flag and still drops the warning). SKILL.md Step 4
now instructs confirming a recovered AD term with the founder.

Part B — option_pool fields are `required`, so a mis-keyed `authorized_shares` is REJECTED at validation
(not silently zeroed). The validator's required-missing error now hints the resembling sibling
("did you write 'authorized_shares'?") so the rejection isn't a dead-end.

TDD: red-first tests for both render paths + the validator hint; 351 passed, ruff + mypy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K2ohjtyuhRyL83ijyguTBQ
…ties enforcement

Records the intentional-extras inventory as git-tracked, test-locked code so the eventual Phase-3
work (enforce additionalProperties: false — the generic mis-key catch-all, deliberately deferred)
is a mechanical follow-through, not a re-derivation. No runtime behavior change.

- cap_state._INTENTIONAL_NON_SCHEMA_KEYS: the keys producers legitimately read off inputs/instruments
  objects that aren't schema properties (oip/ocp/anti_dilution/voting_rights_multiple, vesting,
  option_pool.exercised/expired_or_forfeited, warrants.exercised_flag). cap_state-only; a producer-wide
  sweep is a Phase-3 prerequisite.
- _cap_table_schema_validator.py: a DEFERRED (Phase 3) signpost in the docstring with the prereq
  checklist, at the exact code site a future implementer hits.
- Guard tests: one pins the known extras; one asserts NONE of them are actually declared schema props
  (locks against the founder_id-class mis-classification the plan's reviews caught).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K2ohjtyuhRyL83ijyguTBQ
…ot a bare question

Issue 2 of the hardening plan. SKILL.md instructed the fast-assess exit to end on a literal bare
question ("…want the full review…?") and left the concise exit underspecified — a final assistant turn
ending in "?" with no tool call is exactly cowork-harness 0.10.0's new `stalled` verdict. Both closings
now prescribe a STATEMENT that still offers the next step. cap-table-only (no other skill instructs a
closing question); allow_stall is documented in the cowork-tests README as the opt-out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K2ohjtyuhRyL83ijyguTBQ
…Catalog

Issue 3 of the hardening plan (skill side). The canonical-phrasing block templated only 2 of ~12
recurring AskUserQuestion gates; the rest drifted every run, the root of cassette-answer fragility.
Expanded it into a Gate Catalog with canonical question text + option labels for the Tier-2 recurring
gates: cap-base confirmation (S2), note "Company Capitalization" denominator (S3), note maturity default
(S3), qualified-financing threshold (S3), existing-review routing (S0), pool top-up intent (fast-assess).

Critically, the denominator label drops the "(Recommended)" suffix — that per-run suffix is what made
the note cassette's choose-anchor fragile. Re-anchored cap-table-note-conversion.yaml's denominator
answer to a stable fragment of the new label (`choose: "Fully-diluted"`), authored in the same change.

The drift guard now pins the expanded catalog AND asserts the denominator label carries no
"(Recommended)" suffix. Skill-side templating is necessary-but-not-sufficient (LLM output is
probabilistic); harness fragment anchors + verify-run answer-coverage are the complement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K2ohjtyuhRyL83ijyguTBQ
…hanges + robustify gate anchors

The Issue 1-3 skill edits (warning renderers, validator hint, Gate Catalog, stall-fix closings) re-staled
all 8 cap-table cassettes. Re-recorded them all; 13/13 replay success (none stalled), privacy + staleness
clean, priced-ad's anti-dilution assert preserved (broad_based_weighted_average).

Robustified the scenario gate anchors against label drift (the re-record surfaced it live — the Gate
Catalog made the cap-base gate fire reliably, and the model still paraphrases option labels run-to-run):
- safe-full / priced-ad: on_unanswered:first + label-pin only the assert-relevant gates (cap-base
  "Confirmed", scenario "priced round"); they were "fires NO gates" but the now-reliable cap-base gate fires.
- antihallucination / carta / fast-assess: dropped brittle full-label answers entirely — their asserts don't
  depend on the gate choices, so on_unanswered:first (option 1) is paraphrase-proof.
- note-conversion: dropped the denominator anchor (the model paraphrased the label; the asserts don't depend
  on the denominator) → on_unanswered:first lands the canonical "Fully-diluted pre-financing" option 1.

The lesson — label-anchor a choose: only for gates whose choice affects an assert — is captured in the
open-issues follow-up plan (Issue A). Synthetic data only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K2ohjtyuhRyL83ijyguTBQ
…r + docs)

Bump the token-free CI replay gate's exact pin 0.10.0 -> 0.12.0 and the
rerecord.sh floor to >=0.12.0. The 0.12.0 RunResult.fingerprint is an additive
persisted field (cassettes stay v6, not a format bump), so all 13 committed
cassettes replay green on 0.12.0 -- verified by running the full token-free gate
(lint 0 errors, privacy clean, email canary trips, replay 13/13 success).

The floor must be >=0.12.0 because 0.12.0 ships baseline Desktop 1.15200.0; a
re-record on an older harness would capture the prior 1.14271.0 baseline and be
stale on landing. Agent ELF is unchanged at 2.1.181, so no :2 image rebuild is
needed -- only a release-cadence re-record (deferred) clears the WARN-lane
"baseline moved 1.14271.0 -> 1.15200.0" staleness.

README: document the new 0.12.0 verify-run exit-2 stale-run refusal, and correct
the now-wrong claim that "baseline moved" doesn't fire on CI -- the harness ships
its baseline constant (does not read local Desktop), so it fires identically on
CI and locally on the continue-on-error staleness lane.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENmJEMvqLG9VAAke7PfpVi
…t-to-assumed cap base

Three coupled gate-quality fixes the real-document testing surfaced.

C — concise mode silently dropped 3 of 4 warning families (only W_ANTI_DILUTION
rendered), so a standalone quick question on an unconfirmed cap base showed no
"DIRECTIONAL, not founder-confirmed" caveat. Extract a shared renderer
(_warning_callouts.render_warning_callouts) used by both compose_report and
concise_report so the routes cannot diverge; concise now renders all four
families. Named _warning_callouts (not _warnings): _warnings is a CPython builtin,
so a _warnings.py sibling is unreachable on sys.path.

B — the mandatory cap-base gate fired inconsistently (the model skipped it when
the founder stated the base inline, using real names + no flag → no warning).
Flip cap_state.py to default-to-assumed: W_CAP_BASE_ASSUMED fires on any equity
base unless cap_base_source == "confirmed", moving the compliance burden to the
safe side and making it a real pytest invariant. Lane-3 carve-out: the freeform
emit stamps cap_base_source="confirmed" (the sheet is the source of truth).
SKILL.md reworded (confirm even when stated inline; default-to-assumed semantics).

A — codify the anchor-discipline rule in cowork-tests/README.md: label-anchor a
choose: only for a gate whose choice changes an assert; else on_unanswered: first
(paraphrase-proof). The extract-safe anchor drops are re-record-coupled (validated
via verify-run) and land with the next cassette re-record.

All TDD (red→green). Full suite 2350 passed, 1 skipped; ruff + mypy clean (0 net
errors added). Re-stales the 8 cap-table cassettes (WARN-only; replay green) until
the deferred release-cadence re-record.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENmJEMvqLG9VAAke7PfpVi
…pty cap base

A live run on a real freeform spreadsheet exposed a silent-drop: the structure
sub-agent emitted blocks keyed row_range/columns instead of cell_range/
column_role_map, so the mapper extracted zero records yet returned ok:true and
wrote empty inputs.json (0 founders) / instruments.json (0 safes) with no blocker.
A run that didn't notice would have computed a cap table on an empty base.

freeform_mapper.map_freeform now:
- (1a) requires a non-empty cell_range AND column_role_map on every equity block;
  a mis-keyed block raises a blocker that names the wrong keys and the right ones.
- (1b) global backstop: >=1 equity block declared but 0 records mapped (accumulators
  measured pre-merge, so keep-existing duplicates don't false-fire) raises an
  `emit` blocker instead of writing an empty cap base.
- per-block warning when any equity block's cell_range yields 0 data rows, catching
  the mixed partial-drop case (one block populated, another silently dropped) that
  the global backstop can't see.
- stamps cap_base_source="confirmed" only when an equity base was actually produced.

extract_cap_table.py gains a dedicated schema/empty next_action (checked before the
off-contract branch) steering a re-dispatch with the correct field names rather than
the founder. agents/cap-table.md reinforces that the fields are exactly cell_range/
column_role_map (the doc already specified them; the model drifted anyway).

TDD: 6 new tests red->green. Full suite 2356 passed; contract/drift 80 passed; ruff
+ mypy clean. Deterministic repro of the exact failing blocks now returns ok:false +
cell_range blockers. Re-stales the 8 cowork cassettes (WARN; deferred re-record).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENmJEMvqLG9VAAke7PfpVi
…issuance date

Transparency symmetry: the preferred-series path already warns when it applies the
missing-date sentinel (1900-01-01), but the SAFE and note paths applied it silently
— a founder couldn't tell a real date from a defaulted one. Both paths now emit the
same "issuance_date defaulted … (confirm)" warning, matching the preferred path.

Surfaced by the real-document test campaign (lawyer-built sheets routinely omit
per-instrument issue dates). TDD: 2 red→green tests. Full freeform suite 37 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENmJEMvqLG9VAAke7PfpVi
…enance

A confirmed cap base says nothing about HOW it was produced — deterministically
parsed from a spreadsheet vs hand-built by the model reading a PDF/Carta/chat.
The real-document campaign showed model-built bases passing as confirmed with zero
warnings (the dangerous "looks verified, isn't" case), while the FD-reconciliation
and cross-footing alternatives were both proven blocked (no captured stated total;
cap_state sums the rows itself, so cross-footing is tautological).

Adds a new optional metadata.cap_base_provenance (free string, lineage — distinct
from cap_base_source's confirmed/assumed):
- freeform_mapper stamps "deterministic_mapped" ONLY when IT produced the equity
  this call (gated on the mapper accumulators, not inputs.get, so equity merged
  from existing_inputs can't inherit a false deterministic claim).
- cap_state emits W_CAP_BASE_RECONSTRUCTED when a base is cap_base_source=confirmed
  AND not deterministic_mapped AND has equity. The trigger keys on the ABSENCE of
  the machine-set marker, so it needs no model self-report (the same safe-side flip
  as the Issue-B default-to-assumed). Mutually exclusive with W_CAP_BASE_ASSUMED by
  construction (that requires != confirmed).
- _warning_callouts renders a fifth family with deliberately SOFTENED text ("not
  produced by the deterministic mapper — entered manually or from a document;
  confirm against source"), never "reconstructed by the model" — the trigger can't
  prove model-reconstruction (a Lane-4 founder-paste is confirmed, not fabricated).
- SKILL.md instructs Lane-1/2/4 hand-builds to set "model_reconstructed".

Known limit (documented): per-base provenance can't catch PARTIAL reconstruction
(some sections mapped, one hand-added). Vetted across three adversarial reviews.
TDD: red->green. Full suite 2365 passed; mypy + ruff clean. Re-stales the 8
cap-table cassettes (WARN-only) -> deferred re-record.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENmJEMvqLG9VAAke7PfpVi
…vision extraction

An image-only PDF (no text layer) is read by raw model vision (no OCR in the sandbox), which silently
under-extracts dense cap tables (the Siteaware real-doc failure: 0 preferred / 0 common captured, FD ~8%
off, 56% of FD from one low-confidence warrant — presented as a confident artifact).

- pdf_probe.py: PER-PAGE image-only detection (classify_pages over per-page char counts; image-only iff a
  majority of pages fall below a ~100-char floor, or no readable text at all). Per-page, NOT whole-doc — a
  whole-doc total would clear the floor on a 17-page doc with one text cover page and miss exactly the
  Siteaware shape.
- cap_state emits W_VISION_EXTRACTION_LOW_CONFIDENCE when metadata.extraction_mode == "vision_image_pdf";
  _warning_callouts renders it so the structured artifacts carry the same low-confidence caveat the
  narrative already does. WARN-and-proceed (not a hard stop) — consistent with the proceed-with-warning
  posture; no unbuildable OCR-capability trigger (OCR is absent today; the trigger is image_only alone).
- SKILL.md: before relying on a .pdf cap table read by vision, run pdf_probe; on image_only set
  extraction_mode + tell the founder it's low-confidence + proceed.

TDD: red->green (per-page classification incl. the Siteaung one-text-page shape; cap_state warning;
renderer; contract drift guard). Full suite 2374 passed; ruff + mypy clean. Re-stales the cap-table
cassettes (WARN-only) -> deferred re-record.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENmJEMvqLG9VAAke7PfpVi
…s the printed grand total

Catches a holder/class dropped or mis-entered during a manual Carta rebuild by cross-footing the computed
cap base against the figure Carta itself prints — the one non-circular reconciliation anchor available
(the printed total is computed by Carta, independent of the rows the model rebuilds).

- extract_cap_table._extract_carta_fd_total: parse the Summary Cap Table's 'Totals' row, 'Fully Diluted
  Shares' column (EXACT header match, so the sibling 'Fully Diluted Shares with …' column isn't picked).
  Built against the real Carta Summary layout; captured into the carta receipt as summary.fully_diluted.
- inputs.schema: new optional top-level stated_totals {fully_diluted, source}.
- cap_state: compute as_converted_totals.reconciliation (computed vs stated, abs + relative ppm) and emit
  a suppressible W_FD_RECONCILE_DELTA when |ppm| > 1000 (0.1%). RELATIVE threshold so a real export's
  ~3 ppm conversion-rounding stays quiet but a real dropped-holder divergence (~%) fires.
- _warning_callouts: renders the W_FD_RECONCILE_DELTA family (prefix-matched, carries the numbers).
- SKILL.md (Lane 2): copy the carta-captured summary.fully_diluted into inputs.stated_totals.

Honest scope: insurance, not a fix for an observed failure — on the real Carta export in hand the delta
was ~3 ppm (within threshold, no alert by design); it guards a future export with a real drop. Only the
Carta lane has a genuinely-independent printed total (freeform totals are formulas over the same rows →
circular → out of scope per review). TDD: red->green (parser incl. the with-variant distractor; ppm
threshold; renderer; contract). Full suite 2382 passed; mypy + ruff clean. Re-stales cassettes (WARN).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENmJEMvqLG9VAAke7PfpVi
…freeform pipeline (A3)

Closes the deferred items by correcting a false premise: the OCR runtime is NOT missing — the full-parity
agent image (cowork-agent-full:2) ships tesseract + pdftoppm, so an image-only cap-table PDF can be OCR'd
to structured cells instead of eyeballed by raw vision (the Siteaung P-1 hollow-artifact failure).

- extract_pdf_tables.py (binary-only, NO new Python deps → runs in the full-parity image as-is):
  pdftoppm rasterizes pages, `tesseract … tsv` gives per-word boxes, and tsv_words_to_grid reconstructs a
  row×column grid by clustering words (rows by y-tolerance, columns by >1.5-word-width x-gaps). Emits the
  SAME --mode=grid payload Lane 3 consumes, so an OCR'd PDF flows into the F1-hardened
  SPREADSHEET_STRUCTURE_DETECTION + freeform_mapper machinery (extraction_mode=ocr_image_pdf).
- SKILL.md (B0/B3 guard, upgraded): on image_only, TRY OCR first → freeform grid; A3 = copy any printed
  grand fully-diluted total from the OCR grid into stated_totals so cap_state cross-foots it
  (W_FD_RECONCILE_DELTA). Fall back to the vision LOW_CONFIDENCE warn only if OCR is unavailable/fails.

The grid reconstruction is pure + unit-tested; the binary OCR path is integration-tested end-to-end
(generate an image-only PDF → OCR → assert recovered cells) when tesseract/pdftoppm are present. Full
suite 2388 passed; mypy + ruff clean. B1 (the OCR agent image) already exists; no cross-repo change needed
since the producer uses the binaries already shipped in it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENmJEMvqLG9VAAke7PfpVi
…r + docs); no re-record

Bump the token-free CI replay pin 0.12.0 -> 0.15.0 and the rerecord.sh floor to >=0.15.0. 0.13/0.14/0.15
carry NO breaking changes — cassette format stays v6, agent ELF 2.1.181, baseline 1.15200.0 — so the bump
is purely additive and forces NO re-record (verified: the full token-free gate — lint, privacy, email
canary, replay 13/13 — is green on 0.15.0 against the committed cassettes).

Deliberately NOT adopting 0.14's `--fail-on-skill-drift` release gate: the cap-table skill is under active
modification and the fleet shares roots (scripts/references/agents), so it would red all 13 cassettes on
every skill PR. The staleness lane stays WARN-only (continue-on-error); the accumulated skill + baseline
staleness clears on the next release-cadence rerecord.sh (deferred — still actively modifying the skill).

New-since-0.12 capabilities noted in the CI comment + cowork-tests/README (inspect / partial-run salvage /
capability pre-flight / `runs gc`->`prune` / JSON staleness exposure / refined staleness diagnosis). README
pin text corrected (was a stale 0.10.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENmJEMvqLG9VAAke7PfpVi
…log, override declaration, skeleton + OCR coverage

Closes the cheap, high-confidence half of the open-findings plan (the rest — transpose SUPPORT and a
base backward-verifier — are deferred to a measured next effort). TDD throughout.

- L1-A (the real correctness fix): freeform_mapper no longer crashes or silently emits garbage on a
  transposed / mis-mapped block. `_f`/`_i` are crash-safe (non-numeric in a numeric role → None → the
  caller's required-field blocker, not an uncaught ValueError), and a new per-role type-coherence guard
  (`_orientation_blocker`) fails loud when a numeric-role column is predominantly non-numeric, or a name
  column is predominantly numeric / holds a whole-cell field-label. WHOLE-CELL match only, so legit entity
  names ("Class A Holdings", "500 Startups") pass. Honest scope: this closes the crash + the type/field-label
  mis-map; the section-label all-numeric transpose stays a documented residual owned by transpose SUPPORT.
- GATE-A (+folded GATE-B): expanded the Gate Catalog with the recurring company-context gates (Stage,
  Jurisdiction, IIA grants, SAFE terms) that drifted run-to-run in live testing, with canonical labels +
  a no-embellishment rule (never append names/counts/sector/"(Recommended)" to a catalog label).
- R4-A: a SOURCE-OVERRIDE DECLARATION instruction — a confident reinterpretation that changes the numbers
  (e.g. "SAFEs already converted") must be named + counsel-flagged + surfaced in the cap-base confirm gate,
  not baked silently into a confirmed zero-warning base. Instruction-only; reuses existing machinery.
- C-2 (skeleton): inputs-skeleton names the recurring schema-thrash (holder_id not batch_id; warrants live
  in instruments.json; the plan_type enum), cutting the most-repeated Lane-2/4 friction.
- COV-A: a synthetic multi-holder cap table rasterized to a TRUE image-only PDF (asserts the image_only
  precondition) OCR'd end-to-end — closes the gap that B2's table path never met a real scanned cap table.

Full suite 2394 passed; ruff + mypy clean. SKILL.md changed (architectural surface — e2e dispatch optional).
Plan: docs/internal/2026-06-25-captable-reliability-fix-plan.md (v6, after four adversarial reviews).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENmJEMvqLG9VAAke7PfpVi
… option_grants fields

Two follow-ups from the real-file verification of the reliability lead PR (NakAI live run + adversarial
review), both doc/instruction-only:

- R4-A (SOURCE-OVERRIDE DECLARATION): drop the infeasible "(b) emit a counsel item" — counsel_packet items
  are RULE-PACK-generated from matched rules (counsel_packet.py:38), so the model cannot author one for an
  ad-hoc override. The override's real home is the report narrative's assumptions/caveats + the cap-base
  confirm gate, both of which the model DOES control. The NakAI run did exactly that (named it + a diligence
  flag + a dedicated confirm gate); the "missing part b" was an impossible instruction, now corrected so the
  requirement matches what's achievable.
- inputs-skeleton: document option_grants[] — usually leave it EMPTY (the option_pool aggregate covers the
  pool); it lives in instruments.json not inputs.json; not supported in Lane-3 freeform (use an
  option_pool_block); required fields are id/holder_id/grant_date/shares_granted/strike_price/plan_type (not
  quantity/exercise_price/name). Prevents the recurring option_grants field-name thrash seen on the NakAI run.

Full suite 2394 passed. No test pinned the old wording.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENmJEMvqLG9VAAke7PfpVi
…on an empty (pool-only) base (R-5)

The §Decision measurement (L1-B vs R4-B) hit its stall-exit — neither clears the bar on today's corpus
(R4-B: 0 catchable cases; L1-B: ~1-2 sheets reached but 0 wrong-answers-fixed, since L1-A already made
transpose fail-loud). The measurement's corollary: the one OBSERVED genuinely-misleading output is the
humans/Flip case (R-5) — a full report+explorer+counsel packet on a 0-founder, ~100%-unallocated-pool base.
That's higher harm-per-effort than either survivor, so it's the real next item.

cap_state now emits W_BASE_VACUOUS when as_converted_totals has 0 common AND 0 preferred but a positive
fully-diluted total (i.e. the FD is entirely an option pool — no real holders). Flagged INDEPENDENT of
cap_base_source: a CONFIRMED empty base is still vacuous. _warning_callouts renders a clear caveat ("No real
cap-table base — this deliverable is not meaningful yet ... the donut/FD/%s do not describe a real company").
High-precision: a cap table with a pool but zero founders/common/preferred is a data-capture failure, not a
real company. TDD (vacuous warns incl. when confirmed; normal base doesn't; renderer covers it). Full suite
2397 passed; ruff + mypy clean. (Visual donut/FD suppression in the report is a heavier follow-up — the
warning + caveat is the cheap, high-value half.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENmJEMvqLG9VAAke7PfpVi
Comment on lines +28 to +80
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: npm i -g 'cowork-harness@0.16.0' # EXACT pin (not a >= floor): pre-1.0 minors can break — a future cassette-format bump would replay-FAIL committed cassettes in CI, which cannot re-record. Bump deliberately after the ~30s token-free gate below (lint + privacy + replay) is green on the new version. This 0.15.0→0.16.0 bump was verified that way: lint + privacy + canary + replay all green on 0.16.0 against the committed cassettes. 0.13/0.14/0.15/0.16 carry NO breaking changes, cassette format stays **v6**, agent ELF stays 2.1.181, baseline stays 1.15200.0 → the bump is purely additive, no re-record forced. (Replay re-emits the RECORDED answers — it never runs the decider — so replay-green proves format compat, not decider behavior; 0.16's new scripted-`choose:` prefix-anchor tier is lowest-priority + fires only at RECORD time, so it cannot change a passing replay.) New since 0.12.0: `inspect <run>` (artifact digest, works on torn-down runs) + partial-run salvage on an unanswered gate + capability pre-flight `requires_capabilities`/exit-3 (0.13; `runs gc`→top-level `prune`, decider near-miss label tolerance); `replay --output-format json` exposes `staleness[]`/`skippedAssertions` + a `--fail-on-skill-drift` release gate (0.14); refined staleness diagnosis — a scoped skill now reports BOTH its own + shared-root drift, per-file detail on the replay lane (0.15); **H10 decider fix (0.16): `--decider-llm` replies by option NUMBER (no more option-echo whiff) + answers multiSelect; new `--decider-model <id>` override (live/authoring only — never reaches a committed cassette); scripted `choose:`/`--answer` prefix-anchor (boundary `startsWith`, fails loud on ambiguity — prefix-only); `docs/debugging.md` on-ramp**. DELIBERATELY NOT adopting `--fail-on-skill-drift` here: the cap-table skill is under active modification and the fleet shares roots (`scripts/`/`references/`/`agents/`), so it would red all 13 on every skill PR — keep the staleness gate WARN-only (continue-on-error) below; the baseline+skill staleness clears on the next release-cadence `rerecord.sh`. Carries forward: `record --concurrency` (0.11); bundled PyYAML + live-decider flags + `stalled` verdict (0.10); git-tracked staleness boundary + fileSigs (0.9); class-scoped allows (0.5) + `--skip-staleness`/`--skip-privacy` (0.6).

- name: Lint scenarios (no silent false-greens)
run: cowork-harness lint cowork-tests/scenarios/ # 0.9.0: lint accepts a directory

- name: Privacy gate (HARD)
# The PII guard that actually matters here is **synthetic-only recording**: every cassette's
# subject is a fictional company (Cadence / Acmecorp), so no real founder/company data exists to
# leak. Given that, the allowlist tunes the default scanner classes — using 0.5.0's CLASS-SCOPED
# flags so an allow can't bleed across classes (the 0.5.0 hardening):
# * currency (`--allow`) — synthetic dollar figures the skills emit.
# * DOMAIN class (`--allow-domain`) — allowed wholesale. The research/coaching skills
# (market-sizing, comp-pos, deck-review) cite 150+ PUBLIC third-party domains: public, non-PII;
# a real leak would surface a real NAME (caught by synthetic-only review) and a real EMAIL
# (caught below), not merely a domain. An enumerated 150-domain list would be unmaintainable.
# `--allow-domain` applies ONLY to domain findings — structurally it cannot clear an email
# (0.5.0 replaced our earlier `^...$`-anchored generic `--allow`, which had to be hand-anchored
# to avoid suppressing emails — see the F-2 footgun).
# * email (`--allow-email`) — only the SYNTHETIC domains (acmecorp.com — fictional deck co;
# example.com — RFC-2606 reserved, synthetic Carta stakeholders). Any OTHER email
# (e.g. a real founder@realstartup.com) FAILS the gate — the live PII tripwire.
# The email tripwire is the one guard this posture leans on hardest. The SAME $ALLOW is reused for
# an email CANARY below: a purpose-built cassette carrying a non-synthetic email
# (analyst@realstartup.com) MUST trip. If it stops tripping, the tripwire is disabled → job fails.
# Defining $ALLOW once means the gate and the canary can never drift apart.
run: |
# Canonical allowlist — single source of truth shared with cowork-tests/rerecord.sh.
source cowork-tests/privacy-allowlist.sh
# Real gate: committed cassettes must be clean under the allowlist.
cowork-harness verify-cassettes cowork-tests/cassettes/ --skip-staleness "${ALLOW[@]}"
# Email canary: the SAME allowlist must still FLAG a non-synthetic email. If verify-cassettes
# exits 0 here, the email class is no longer catching real addresses → fail loudly.
if cowork-harness verify-cassettes cowork-tests/canary/email-canary.cassette.json --skip-staleness "${ALLOW[@]}"; then
echo "::error:: email canary did NOT trip — the email PII tripwire is disabled (a real email would pass). Check --allow-email is not over-broad and that no generic --allow matches an address."
exit 1
fi
echo "email canary tripped as expected — email tripwire is live."

- name: Staleness gate (WARN — does not fail the job; see header F8)
continue-on-error: true
run: cowork-harness verify-cassettes cowork-tests/cassettes/ --skip-privacy

- name: Replay content + artifact_json assertions (token-free)
run: cowork-harness replay cowork-tests/cassettes/ --output-format json
… docs)

0.16.0 is the H10 decider work — number-based --decider-llm replies (kills the
option-echo whiff that stalled 5/6 live real-doc runs), multiSelect answering,
new --decider-model override, and a scripted prefix-anchor for choose:/--answer.

Additive bump: cassette format stays v6, baseline 1.15200.0, agent ELF 2.1.181 —
all unchanged. Verified by the token-free gate on 0.16.0 against the committed
cassettes: lint + privacy + email-canary + replay all green (replay re-emits the
recorded answers and never runs the decider, so it proves format compat). No
re-record forced; the pre-existing staleness WARN is the separate release-cadence
item, not introduced here.

- cowork-replay.yml: exact pin 0.15.0 -> 0.16.0; ledger appended (history kept;
  --fail-on-skill-drift still deliberately not adopted).
- rerecord.sh: floor >=0.15 -> >=0.16 (== CI pin); + a first-re-record caveat to
  diff cassettes/ for changed gate answers (the new prefix-anchor fires at record
  time only, prefix-only, fails loud on ambiguity).
- cowork-tests/README.md: current-pin note rewritten for 0.16.0; 0.15.0 note kept
  as history.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iY5dkoUQ4ZeXt5vKHUoZk
Signed-off-by: Yaniv Golan <yaniv@golan.name>
@yaniv-golan
yaniv-golan force-pushed the chore/cowork-harness-0.16.0 branch from 9c9a997 to f4784de Compare June 26, 2026 07:38
@yaniv-golan

Copy link
Copy Markdown
Contributor Author

Closing: this repo keeps local main ~192 commits ahead of origin/main (deliberate validate-then-release flow), so a branch off local-main carries all unpushed history into the PR (DCO flags the 182 unsigned historical commits). Landing the 0.16.0 adoption directly on local main per the repo's convention instead; local token-free gate (lint+privacy+canary+replay) is green on 0.16.0.

@yaniv-golan
yaniv-golan deleted the chore/cowork-harness-0.16.0 branch June 26, 2026 07:40
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