chore(cowork): adopt cowork-harness 0.16.0 (additive — H10 decider fix) - #6
Closed
yaniv-golan wants to merge 193 commits into
Closed
chore(cowork): adopt cowork-harness 0.16.0 (additive — H10 decider fix)#6yaniv-golan wants to merge 193 commits into
yaniv-golan wants to merge 193 commits into
Conversation
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>
…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
force-pushed
the
chore/cowork-harness-0.16.0
branch
from
June 26, 2026 07:38
9c9a997 to
f4784de
Compare
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Bumps the cowork-harness target 0.15.0 → 0.16.0 across the three tracked touchpoints (CI exact pin,
rerecord.shfloor,cowork-tests/README.md).Why
0.16.0 is the H10 decider work — the exact
--decider-llmgate-whiff class that stalled 5 of 6 live real-doc runs last campaign:--decider-llmnow replies by option NUMBER (harness maps number → canonical label) → no more option-echo whiff; also answers multiSelect gates.--decider-model <id>to throw a stronger model at ambiguous judgment gates (live/authoring only — never reaches a committed cassette).choose:/--answergains a prefix anchor (boundarystartsWith, uniqueness-guarded, fails loud on ambiguity — prefix-only, fires at record time only).docs/debugging.mdon-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:
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.mdshared-root edits + 2 smokes on the old baseline) is the separate release-cadence re-record item, not introduced here.--fail-on-skill-driftremains deliberately unadopted (cap-table under active modification + shared roots → would red all 13).🤖 Generated with Claude Code