Summary
bcli batch run --set key=<ISO-date-string> silently runs the value through PyYAML's implicit type resolver instead of keeping it a string. 2026-08-23 becomes a Python datetime.date object, which then flows unconverted into the JSON body of the step's HTTP call. httpx's JSON encoder has no date handler and raises TypeError while building the request.
Because that TypeError lands in the same catch-all except Exception block that handles every other kind of step failure, the ledger and the run summary record the step as "failed" / not-succeeded with no distinction from "never sent." In the live Slice-0 write-contract spike (turbine repo, docs/slice0/REPORT-04-write-contracts.md §5b item 2), a two-step batch built exactly this way reported "0/2 steps succeeded" (process exit 1) — and bcli's own SQLite batch ledger correctly recorded both steps status=failed, with error_message: "Object of type date is not JSON serializable", for every one of the three --set-based runs that hit this crash (~/.config/bcli/batch/d3534f6e2e47456fa2f88e9e9462b373.db, 230b422529914437a9c0a0c588cfcef5.db, 4fb0c1d4162c4c2a96022a4a9f454264.db — runs at 00:09:30Z/00:09:54Z/00:09:54Z on 2026-08-24; intent_ts→outcome_ts deltas of ~27ms, consistent with a pre-send crash and no HTTP round-trip). Nothing was committed by any of these three runs. A same-lineNo record (GENERAL/TSPIKE4/920) that was initially attributed to this crash was traced, via bcli's own per-run ledgers, to a separate, successful run (0d48d030f36b46e0945e17d31ac81fd8 — the equivalent --params-file invocation, run without --yes, exit 0, ledger run.state=completed, step 1 status=committed) — not this bug. This is still a live instance of the "ambiguous outcome" hazard bcli's own batch ledger (src/bcli/batch/ledger.py) exists to prevent, in the narrower sense described in Root cause item 4: a pre-send encoding crash and a genuinely ambiguous post-send failure both collapse to the same "failed" ledger status.
Documented workaround: pass dates through --params <file> with the value quoted in the YAML (posting_date: "2026-08-23"), not through --set.
Steps to reproduce
A — the exact coercion + crash mechanism, reproducible offline (no BC needed):
python3 -c "
import yaml, json
v = yaml.safe_load('2026-08-23')
print(type(v), v) # <class 'datetime.date'> 2026-08-23
json.dumps({'postingDate': v}) # raises TypeError
"
This is exactly what bcli_cli/commands/batch_cmd.py::_smart_parse_value does to every --set key=value argument, and exactly what bcli/client/_transport.py's outgoing request encoding then does to the resulting workflow parameter — see Root cause below for the exact call chain.
B — the live, end-to-end instance (BC sandbox, SBEnvAug26/LLC), as run and recorded in the turbine repo's Slice-0 spike:
bcli --profile admin-sandbox --env SBEnvAug26 --company LLC --format json \
batch run duplicate-design-3.batch.yaml \
--result-out batch-<n>.json \
--set journal_template_name=GENERAL --set journal_batch_name=TSPIKE4 \
--set account_no=1110-000000-010 --set amount=100 \
--set posting_date=2026-08-23 \
--set external_document_no=TURBINE-SPIKE4-003 \
--set idem_key=<uuid>
where duplicate-design-3.batch.yaml has two post steps against genJournalLines, each with postingDate: "${{ params.posting_date }}" in its data: block (full source below, "Root cause"). Observed: process exit 1, "0/2 steps succeeded" printed to stdout, and bcli's own ledger (~/.config/bcli/batch/<run-id>.db) correctly recording both steps status=failed with the TypeError message above — nothing was sent, nothing was committed, by this run. (A record at the same lineNo, GENERAL/TSPIKE4/920, does show up on a later read-back, but per bcli's own ledgers it was created by a separate, successful run — see Summary above, not this one.) Evidence trail (in the turbine repo, not bcli):
docs/slice0/REPORT-04-write-contracts.md:86 (§5b item 2, the narrative finding)
docs/slice0/evidence/04-write-contracts/ledger.md:22 (the reconciliation row for the orphan record, with the discovery narrative)
docs/slice0/evidence/04-write-contracts/session-commands.log:183-192 (the exact failing invocations, all exitCode: 1, and the later --params-based workaround runs)
spikes/04-bc-write-contracts/src/scenarios/batch/duplicate-design-3.batch.yaml and duplicate-design-3-params.generated.yaml (fixture + the quoted-string workaround params file)
Expected
--set posting_date=2026-08-23 should behave the same as putting posting_date: "2026-08-23" in a --params YAML file: the value stays a string, gets sent to BC as "2026-08-23", and the step either succeeds or fails on its own merits. And a request that fails to serialize before any bytes reach BC should be distinguishable, in the ledger's own status column, from a request whose outcome is genuinely unknown after a network-level failure — today both collapse to the same "failed" status (see Root cause item 3 and Suggested fix 3).
Root cause
1. --set applies YAML implicit typing to every value, with no override for date-shaped strings.
_smart_parse_value (src/bcli_cli/commands/batch_cmd.py:44-55) runs the raw CLI string through yaml.safe_load(raw) to get --set vendor_no=V00011 → str, --set amount=100 → int, etc. (per its own docstring). PyYAML's default (YAML 1.1) resolver also recognizes YYYY-MM-DD as a timestamp and returns a datetime.date — confirmed directly: yaml.safe_load("2026-08-23") → datetime.date(2026, 8, 23). Nothing in _smart_parse_value or its caller _parse_set_params (batch_cmd.py:58-69) special-cases date/datetime results the way it already special-cases a yaml.YAMLError (falls back to the raw string at line 54-55). _build_workflow_params (batch_cmd.py:97-134) then merges this dict into the workflow's params, with --set given the highest priority (line 122), so this silently overrides even a correctly-typed default or params-file value for the same key.
2. The workflow resolver is deliberately "type-preserving" for a bare ${{ }} reference, so the raw date object reaches the request body unconverted.
resolve_references (src/bcli/workflow/_resolver.py:22-48) treats a step field whose entire string is one ${{ params.<key> }}/${{ steps.<name>.<field> }} reference (matched by FULL_REFERENCE_PATTERN, line 17-19) as "type-preserving": it returns the raw Python value from context.params unchanged (line 38-40, via _resolve_param, line 64-70) rather than stringifying it — stringification only happens for embedded references (line 43-46). In the reproducing batch YAML, postingDate: "${{ params.posting_date }}" is exactly such a bare reference, so the resolved step's data["postingDate"] is the raw datetime.date, not "2026-08-23".
(This is also why the documented workaround — putting the date in a --params <file> YAML with the value quoted — actually works: _load_params_file (batch_cmd.py:72-83) parses the whole params file as one YAML document, where an explicitly quoted scalar like "2026-08-23" is honored as a string by YAML's own grammar. A --set value, by contrast, arrives from the shell with any quoting already stripped, so _smart_parse_value re-parses it as a bare, unquoted scalar and YAML's implicit resolver — not the author's intent — decides the type.)
3. That raw date object then fails at JSON-encoding time, inside the CLI's own HTTP call, not at parse time.
The mutating step calls client.post(endpoint, data or {}, ...) (batch_cmd.py:746-749) → AsyncBCClient.post (src/bcli/client/_async.py:239-260) → BCTransport.post/_request (src/bcli/client/_transport.py:519-528, 127-217), which sends the body via self._client.request(method, url, params=params, json=json_body, headers=headers) (_transport.py:199-205). httpx's own JSON encoder (httpx._content.encode_json) calls the stdlib json.dumps(json, ensure_ascii=False, separators=(",", ":"), allow_nan=False) with no default= handler, which raises TypeError: Object of type date is not JSON serializable for any dict containing the raw date. Confirmed directly against the pinned httpx==0.28.1 (also the version installed in this environment) via httpx.MockTransport: the mock handler is never invoked and the TypeError surfaces before any request reaches the transport. Contrast this with the ledger's own _body_hash helper (batch_cmd.py:164-178), which already anticipates non-JSON-native values in a step body and uses json.dumps(body, sort_keys=True, default=str) — the outgoing HTTP request body itself is the one place in this path that isn't defended the same way.
4. The resulting exception is caught by the same generic handler as every other step failure, so the ledger and the run summary can't tell "never sent" apart from "sent, but something after that blew up."
Every action branch in _execute_batch's per-step loop (batch_cmd.py:707-893) is one try, ending in except Exception as e: (batch_cmd.py:877-893), which unconditionally appends {"status": "error", ...} to results and calls ledger.write_outcome(step_id=..., status="failed", ...) (batch_cmd.py:885-891) — there is no branch that distinguishes a client-side encoding failure (which, taken alone for a single request, fails before any socket write) from a genuinely ambiguous network-level failure. The run-level rollup in run_batch (batch_cmd.py:427-436) sums results by status == "ok" to build the printed "N/M steps succeeded" line and to decide final_state — a step recorded as "error" is never counted as succeeded, and its ledger row (src/bcli/batch/ledger.py::write_outcome, ledger.py:388-419) is stamped "failed", not "committed". Per Ledger's own module docstring (ledger.py:1-40) and compute_run_state (ledger.py:450-498), status="failed" is exactly the value that keeps a run from ever being classified "partially_committed" on that step — and it is the same status a genuinely ambiguous post-send failure would also receive, so the ledger's status column alone cannot distinguish "definitely never sent" (this bug) from "sent, outcome unknown" (a real network-level failure) without an independent read-back.
Suggested fix
-
Stop --set from producing non-string/non-JSON-native types for date-shaped input. In _smart_parse_value (batch_cmd.py:44-55), after yaml.safe_load(raw), check isinstance(parsed, (datetime.date, datetime.datetime, datetime.time)) and fall back to returning raw unchanged (mirroring the existing except yaml.YAMLError: return raw behavior) — the function's own docstring only documents int/float/bool/str as intended outcomes; a date object was never a design goal, it's PyYAML's timestamp resolver firing as a side effect. This alone makes --set posting_date=2026-08-23 behave like the quoted-string workaround.
-
Make the outgoing request encoder defensive, independent of where a bad type comes from. In BCTransport._request (_transport.py:199-205), send the body as content=json.dumps(json_body, default=str).encode("utf-8") with an explicit application/json content-type, instead of relying on httpx's own strict json= kwarg. This is the single choke point for every mutating call bcli makes; hardening it here catches this whole class of "some Python object that isn't a JSON scalar slipped into a request body" bug regardless of which upstream code path produced it (workflow params, a future step-chaining bug, a user-authored plugin, etc.), the same way _body_hash (batch_cmd.py:175) already does for hashing.
-
Don't let a request-encoding failure share a ledger status with a genuinely ambiguous one. A TypeError raised while building/serializing the request (i.e., before any bytes reach the wire) is categorically different from a network error or a BC-side rejection — the former means "definitely not sent," the latter means "unknown, needs a read-back." Today both are folded into the same except Exception → status="failed" outcome (batch_cmd.py:877-893, ledger.py:388-419). At minimum, catch serialization/encoding failures separately (they're always raised before the await self._client.request(...)/await self._transport.post(...) calls actually touch the network) and record a distinct ledger status for them, so the ledger's status column keeps meaning what its own docstring says it means.
Fix 1 is the minimal patch that removes the crash entirely; fix 2 is cheap defense-in-depth at the one real choke point; fix 3 is the structural fix for the underlying "ambiguous outcome" class this bug happens to be one instance of (and which DESIGN.md §8 in the turbine repo already treats as a first-class risk).
Notes
- This report packages up
docs/slice0/REPORT-04-write-contracts.md §5b item 2 (live, unplanned finding from the turbine repo's Slice-0 write-contract spike, 2026-08-23/24, BC sandbox SBEnvAug26/LLC) as a standalone bcli bug, per that report's "Second candidate bcli bug report" note.
- I independently verified, in this environment, both (a)
yaml.safe_load("2026-08-23") → datetime.date, and (b) that httpx.AsyncClient.request(..., json=<dict containing that date>) raises TypeError via a MockTransport before the mock handler is ever invoked (httpx==0.28.1, matching the version installed alongside bcli in this environment). I have not re-run the actual failing bcli batch run command against a live BC sandbox in this session (no BC credentials here, and the underlying record is exactly the kind of write this report is about) — the mechanism above is derived by direct code reading plus the two isolated reproductions, and cross-checked against the live spike's own written record (ledger.md:22, session-commands.log:183-192) rather than re-executed end-to-end.
- An apparent contradiction I flagged in an earlier draft of this report — a single POST whose body contains the bad
date should fail before any network I/O, yet a record at the same lineNo shows up in the spike's cleanup ledger — is resolved by attribution, not by mechanism. Checked directly against bcli's own per-run SQLite ledgers (~/.config/bcli/batch/<run-id>.db): every --set-based run that hit this crash failed both steps pre-send, exactly as predicted, and committed nothing. The lineNo-920 record traces to a different, successful run (0d48d030f36b46e0945e17d31ac81fd8) that used the --params-file workaround invoked without --yes — a separate Turbine-harness issue (a no---yes "preflight" that still writes, because the admin-sandbox profile has no disable_writes guard), not a symptom of this bug.
- Real record IDs quoted above (
GENERAL/TSPIKE4/920, systemId 42349c44-509f-f111-8072-7c1e52bd3e92) are BC sandbox records (SBEnvAug26), already deleted and re-fetch-verified gone as part of the spike's cleanup (docs/slice0/REPORT-04-write-contracts.md §7).
Summary
bcli batch run --set key=<ISO-date-string>silently runs the value through PyYAML's implicit type resolver instead of keeping it a string.2026-08-23becomes a Pythondatetime.dateobject, which then flows unconverted into the JSON body of the step's HTTP call.httpx's JSON encoder has nodatehandler and raisesTypeErrorwhile building the request.Because that
TypeErrorlands in the same catch-allexcept Exceptionblock that handles every other kind of step failure, the ledger and the run summary record the step as"failed"/ not-succeeded with no distinction from "never sent." In the live Slice-0 write-contract spike (turbinerepo,docs/slice0/REPORT-04-write-contracts.md§5b item 2), a two-step batch built exactly this way reported "0/2 steps succeeded" (process exit 1) — and bcli's own SQLite batch ledger correctly recorded both stepsstatus=failed, witherror_message: "Object of type date is not JSON serializable", for every one of the three--set-based runs that hit this crash (~/.config/bcli/batch/d3534f6e2e47456fa2f88e9e9462b373.db,230b422529914437a9c0a0c588cfcef5.db,4fb0c1d4162c4c2a96022a4a9f454264.db— runs at 00:09:30Z/00:09:54Z/00:09:54Z on 2026-08-24;intent_ts→outcome_tsdeltas of ~27ms, consistent with a pre-send crash and no HTTP round-trip). Nothing was committed by any of these three runs. A same-lineNo record (GENERAL/TSPIKE4/920) that was initially attributed to this crash was traced, via bcli's own per-run ledgers, to a separate, successful run (0d48d030f36b46e0945e17d31ac81fd8— the equivalent--params-file invocation, run without--yes, exit 0, ledgerrun.state=completed, step 1status=committed) — not this bug. This is still a live instance of the "ambiguous outcome" hazardbcli's own batch ledger (src/bcli/batch/ledger.py) exists to prevent, in the narrower sense described in Root cause item 4: a pre-send encoding crash and a genuinely ambiguous post-send failure both collapse to the same"failed"ledger status.Documented workaround: pass dates through
--params <file>with the value quoted in the YAML (posting_date: "2026-08-23"), not through--set.Steps to reproduce
A — the exact coercion + crash mechanism, reproducible offline (no BC needed):
This is exactly what
bcli_cli/commands/batch_cmd.py::_smart_parse_valuedoes to every--set key=valueargument, and exactly whatbcli/client/_transport.py's outgoing request encoding then does to the resulting workflow parameter — see Root cause below for the exact call chain.B — the live, end-to-end instance (BC sandbox,
SBEnvAug26/LLC), as run and recorded in theturbinerepo's Slice-0 spike:where
duplicate-design-3.batch.yamlhas twopoststeps againstgenJournalLines, each withpostingDate: "${{ params.posting_date }}"in itsdata:block (full source below, "Root cause"). Observed: process exit 1, "0/2 steps succeeded" printed to stdout, and bcli's own ledger (~/.config/bcli/batch/<run-id>.db) correctly recording both stepsstatus=failedwith theTypeErrormessage above — nothing was sent, nothing was committed, by this run. (A record at the same lineNo,GENERAL/TSPIKE4/920, does show up on a later read-back, but per bcli's own ledgers it was created by a separate, successful run — see Summary above, not this one.) Evidence trail (in theturbinerepo, notbcli):docs/slice0/REPORT-04-write-contracts.md:86(§5b item 2, the narrative finding)docs/slice0/evidence/04-write-contracts/ledger.md:22(the reconciliation row for the orphan record, with the discovery narrative)docs/slice0/evidence/04-write-contracts/session-commands.log:183-192(the exact failing invocations, allexitCode: 1, and the later--params-based workaround runs)spikes/04-bc-write-contracts/src/scenarios/batch/duplicate-design-3.batch.yamlandduplicate-design-3-params.generated.yaml(fixture + the quoted-string workaround params file)Expected
--set posting_date=2026-08-23should behave the same as puttingposting_date: "2026-08-23"in a--paramsYAML file: the value stays a string, gets sent to BC as"2026-08-23", and the step either succeeds or fails on its own merits. And a request that fails to serialize before any bytes reach BC should be distinguishable, in the ledger's ownstatuscolumn, from a request whose outcome is genuinely unknown after a network-level failure — today both collapse to the same"failed"status (see Root cause item 3 and Suggested fix 3).Root cause
1.
--setapplies YAML implicit typing to every value, with no override for date-shaped strings._smart_parse_value(src/bcli_cli/commands/batch_cmd.py:44-55) runs the raw CLI string throughyaml.safe_load(raw)to get--set vendor_no=V00011→str,--set amount=100→int, etc. (per its own docstring). PyYAML's default (YAML 1.1) resolver also recognizesYYYY-MM-DDas a timestamp and returns adatetime.date— confirmed directly:yaml.safe_load("2026-08-23")→datetime.date(2026, 8, 23). Nothing in_smart_parse_valueor its caller_parse_set_params(batch_cmd.py:58-69) special-cases date/datetime results the way it already special-cases ayaml.YAMLError(falls back to the raw string at line 54-55)._build_workflow_params(batch_cmd.py:97-134) then merges this dict into the workflow'sparams, with--setgiven the highest priority (line 122), so this silently overrides even a correctly-typed default or params-file value for the same key.2. The workflow resolver is deliberately "type-preserving" for a bare
${{ }}reference, so the rawdateobject reaches the request body unconverted.resolve_references(src/bcli/workflow/_resolver.py:22-48) treats a step field whose entire string is one${{ params.<key> }}/${{ steps.<name>.<field> }}reference (matched byFULL_REFERENCE_PATTERN, line 17-19) as "type-preserving": it returns the raw Python value fromcontext.paramsunchanged (line 38-40, via_resolve_param, line 64-70) rather than stringifying it — stringification only happens for embedded references (line 43-46). In the reproducing batch YAML,postingDate: "${{ params.posting_date }}"is exactly such a bare reference, so the resolved step'sdata["postingDate"]is the rawdatetime.date, not"2026-08-23".(This is also why the documented workaround — putting the date in a
--params <file>YAML with the value quoted — actually works:_load_params_file(batch_cmd.py:72-83) parses the whole params file as one YAML document, where an explicitly quoted scalar like"2026-08-23"is honored as a string by YAML's own grammar. A--setvalue, by contrast, arrives from the shell with any quoting already stripped, so_smart_parse_valuere-parses it as a bare, unquoted scalar and YAML's implicit resolver — not the author's intent — decides the type.)3. That raw
dateobject then fails at JSON-encoding time, inside the CLI's own HTTP call, not at parse time.The mutating step calls
client.post(endpoint, data or {}, ...)(batch_cmd.py:746-749) →AsyncBCClient.post(src/bcli/client/_async.py:239-260) →BCTransport.post/_request(src/bcli/client/_transport.py:519-528,127-217), which sends the body viaself._client.request(method, url, params=params, json=json_body, headers=headers)(_transport.py:199-205).httpx's own JSON encoder (httpx._content.encode_json) calls the stdlibjson.dumps(json, ensure_ascii=False, separators=(",", ":"), allow_nan=False)with nodefault=handler, which raisesTypeError: Object of type date is not JSON serializablefor any dict containing the rawdate. Confirmed directly against the pinnedhttpx==0.28.1(also the version installed in this environment) viahttpx.MockTransport: the mock handler is never invoked and theTypeErrorsurfaces before any request reaches the transport. Contrast this with the ledger's own_body_hashhelper (batch_cmd.py:164-178), which already anticipates non-JSON-native values in a step body and usesjson.dumps(body, sort_keys=True, default=str)— the outgoing HTTP request body itself is the one place in this path that isn't defended the same way.4. The resulting exception is caught by the same generic handler as every other step failure, so the ledger and the run summary can't tell "never sent" apart from "sent, but something after that blew up."
Every action branch in
_execute_batch's per-step loop (batch_cmd.py:707-893) is onetry, ending inexcept Exception as e:(batch_cmd.py:877-893), which unconditionally appends{"status": "error", ...}toresultsand callsledger.write_outcome(step_id=..., status="failed", ...)(batch_cmd.py:885-891) — there is no branch that distinguishes a client-side encoding failure (which, taken alone for a single request, fails before any socket write) from a genuinely ambiguous network-level failure. The run-level rollup inrun_batch(batch_cmd.py:427-436) sumsresultsbystatus == "ok"to build the printed "N/M steps succeeded" line and to decidefinal_state— a step recorded as"error"is never counted as succeeded, and its ledger row (src/bcli/batch/ledger.py::write_outcome,ledger.py:388-419) is stamped"failed", not"committed". PerLedger's own module docstring (ledger.py:1-40) andcompute_run_state(ledger.py:450-498),status="failed"is exactly the value that keeps a run from ever being classified"partially_committed"on that step — and it is the same status a genuinely ambiguous post-send failure would also receive, so the ledger'sstatuscolumn alone cannot distinguish "definitely never sent" (this bug) from "sent, outcome unknown" (a real network-level failure) without an independent read-back.Suggested fix
Stop
--setfrom producing non-string/non-JSON-native types for date-shaped input. In_smart_parse_value(batch_cmd.py:44-55), afteryaml.safe_load(raw), checkisinstance(parsed, (datetime.date, datetime.datetime, datetime.time))and fall back to returningrawunchanged (mirroring the existingexcept yaml.YAMLError: return rawbehavior) — the function's own docstring only documents int/float/bool/str as intended outcomes; adateobject was never a design goal, it's PyYAML's timestamp resolver firing as a side effect. This alone makes--set posting_date=2026-08-23behave like the quoted-string workaround.Make the outgoing request encoder defensive, independent of where a bad type comes from. In
BCTransport._request(_transport.py:199-205), send the body ascontent=json.dumps(json_body, default=str).encode("utf-8")with an explicitapplication/jsoncontent-type, instead of relying on httpx's own strictjson=kwarg. This is the single choke point for every mutating callbclimakes; hardening it here catches this whole class of "some Python object that isn't a JSON scalar slipped into a request body" bug regardless of which upstream code path produced it (workflow params, a future step-chaining bug, a user-authored plugin, etc.), the same way_body_hash(batch_cmd.py:175) already does for hashing.Don't let a request-encoding failure share a ledger status with a genuinely ambiguous one. A
TypeErrorraised while building/serializing the request (i.e., before any bytes reach the wire) is categorically different from a network error or a BC-side rejection — the former means "definitely not sent," the latter means "unknown, needs a read-back." Today both are folded into the sameexcept Exception→status="failed"outcome (batch_cmd.py:877-893,ledger.py:388-419). At minimum, catch serialization/encoding failures separately (they're always raised before theawait self._client.request(...)/await self._transport.post(...)calls actually touch the network) and record a distinct ledger status for them, so the ledger'sstatuscolumn keeps meaning what its own docstring says it means.Fix 1 is the minimal patch that removes the crash entirely; fix 2 is cheap defense-in-depth at the one real choke point; fix 3 is the structural fix for the underlying "ambiguous outcome" class this bug happens to be one instance of (and which
DESIGN.md§8 in theturbinerepo already treats as a first-class risk).Notes
docs/slice0/REPORT-04-write-contracts.md§5b item 2 (live, unplanned finding from theturbinerepo's Slice-0 write-contract spike, 2026-08-23/24, BC sandboxSBEnvAug26/LLC) as a standalonebclibug, per that report's "Second candidate bcli bug report" note.yaml.safe_load("2026-08-23")→datetime.date, and (b) thathttpx.AsyncClient.request(..., json=<dict containing that date>)raisesTypeErrorvia aMockTransportbefore the mock handler is ever invoked (httpx==0.28.1, matching the version installed alongsidebcliin this environment). I have not re-run the actual failingbcli batch runcommand against a live BC sandbox in this session (no BC credentials here, and the underlying record is exactly the kind of write this report is about) — the mechanism above is derived by direct code reading plus the two isolated reproductions, and cross-checked against the live spike's own written record (ledger.md:22,session-commands.log:183-192) rather than re-executed end-to-end.dateshould fail before any network I/O, yet a record at the same lineNo shows up in the spike's cleanup ledger — is resolved by attribution, not by mechanism. Checked directly against bcli's own per-run SQLite ledgers (~/.config/bcli/batch/<run-id>.db): every--set-based run that hit this crash failed both steps pre-send, exactly as predicted, and committed nothing. The lineNo-920 record traces to a different, successful run (0d48d030f36b46e0945e17d31ac81fd8) that used the--params-file workaround invoked without--yes— a separate Turbine-harness issue (a no---yes"preflight" that still writes, because theadmin-sandboxprofile has nodisable_writesguard), not a symptom of this bug.GENERAL/TSPIKE4/920, systemId42349c44-509f-f111-8072-7c1e52bd3e92) are BC sandbox records (SBEnvAug26), already deleted and re-fetch-verified gone as part of the spike's cleanup (docs/slice0/REPORT-04-write-contracts.md§7).