A2: role-specific runtime identity and readiness (R3 + R8 + R18) - #32
A2: role-specific runtime identity and readiness (R3 + R8 + R18)#32aurascoper wants to merge 11 commits into
Conversation
A resolved runtime object is not a resolved runtime identity. The runtime
generates; the identity is an immutable, sanitized description that telemetry,
readiness logic and the privacy UI consume — and that exists on the failure
path too, so a runtime that did not resolve can still be described.
ResolvedRuntimeIdentity keeps requested and resolved provider/model as separate
pairs, making substitution visible rather than inferable, plus role, locality,
readiness, prompt profile, the hash of the bytes actually transmitted, and the
provider-reported model digest where one exists (Ollama; the Claude CLI reports
none, so it stays nil rather than fabricated).
RuntimeLocality exists because a local executable does not imply local
inference. The Claude CLI is a local binary brokering to a remote service;
Ollama on a non-loopback host is remote however local the config looks. Both
are egress. Classifying by where the process runs would make the privacy banner
assert on-device inference for the one path that leaves the machine.
Readiness is established before the loop starts and never by generating:
- Claude resolves through ClaudeExecutableResolver — the app path now uses
the same exact resolver the harness got in A1 — and loads and hashes its
prompt. No request reaches Anthropic.
- Ollama gets a bounded GET /api/tags confirming the daemon is up and holds
the exact requested model. No prompt is sent.
A readiness failure disables the loop and attempts no alternate provider:
substituting Claude when Ollama is unavailable would turn a local-inference
choice into unrequested egress.
RuntimeResolutionFailure carries a public message and an internal detail,
sanitized at construction rather than at each display site. The resolver's own
error embeds the full environment PATH and lastError is rendered in the UI, so
a display site that must remember to sanitize would eventually forget.
Mutation-tested; each fails a named test:
M4 remove the /api/tags probe → 6 tests
M5 report a missing model as present → 2
M6 substitute Claude on Ollama failure → 2
M8 classify the Claude CLI on-device → 1
M9 interpolate PATH into the message → 1
M10 delete the enabled→disabled write → 1 (closes A1's known M4 gap)
M11 report an unavailable runtime ready → 1
Deferred to the next commit, and marked in place: the Witness is still resolved
even when the profile disables it, nothing yet rejects a runtime resolved for
the wrong role, and the adapter's dead systemPrompt field survives — retained
so this commit lands without also rewriting Witness resolution, not as a new
compatibility mechanism.
No test added here makes a real provider request; readiness HTTP is mocked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Witness had no runtime of its own. In the harness it wrapped the *pole* runtime and passed the Witness prompt into the adapter's stored-but-unread `systemPrompt` field, so the Witness reported one prompt and transmitted another — the pole's. A "non-voiced observer that names what both poles avoided" was, in fact, a third pole. The dead field is now gone. Removing it is what forces the fix: with nowhere to put a prompt that does not take effect, a role-specific prompt has to be resolved into the runtime that sends it. Both the app and the harness now resolve the Witness separately, through `PromptProfile.witness`. Two runtimes can share a provider and a model and still be distinct identities. The Witness and the poles differ by role, prompt profile and prompt hash, and the tests assert those differ even when provider and model are identical. A disabled role now resolves nothing. The Witness was previously resolved unconditionally and `witnessEnabled` consulted only afterwards, when deciding whether to *use* it — so a profile with no Witness still paid for a prompt load and, on the Ollama path, an endpoint probe, and made an unused role's readiness a precondition for a loop that would never call it. `WitnessResolution` is a three-case enum rather than an optional because "the profile has no Witness" and "the Witness failed to resolve" must lead to different behaviour: the first is normal, the second fails the requested configuration closed rather than quietly degrading to a two-voice exchange the user did not select. The role-consistency guard closes a gap types cannot. `resolveWitnessRuntime` fixes the role internally, but the injected closure still returns the same tuple type whatever role it resolved, so a caller could hand the Witness a pole runtime and it would be filed under the Witness identity. A mismatch now fails closed. Mutation-tested; each fails a named test: M1 accept a pole runtime as the Witness → 2 tests M2 map the Witness role to the pole prompt → 3 M3 resolve the Witness while disabled → 1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The privacy banner asserted "the claude CLI" and "Listening + Cloud" whenever the loop was enabled. Both had been wrong since runtime selection landed: with NEURALCOMPOSE_RUNTIME=ollama the loop ran entirely on-device while the banner reported cloud egress to Anthropic. A privacy indicator that is wrong in the reassuring direction is worse than none; this one was wrong in both directions at different times, which is the same defect. Both strings were literals inside a SwiftUI `body`, so the app's most consequential privacy claim was the one part of the codebase no test could reach. `RuntimeIdentityPresentation` moves the claims into a plain value type computed from `ResolvedRuntimeIdentity`, and the view renders it. Every field derives from a resolved identity; none derives from the mode name, the provider spelling, or a default. Egress is read from `locality`, never from a provider name — the Claude CLI is a local executable whose inference is remote, and Ollama on a non-loopback host is remote despite being "local" in every other sense. Neither is derivable from a label. Unknown counts as egress. Before the first resolution there is no identity to read, and the banner must not claim on-device operation it cannot substantiate, so the default is the alarming one rather than the reassuring one. A runtime that is not ready renders its failure rather than its name, and in red, so an unavailable runtime cannot read as running. The Witness renders "Disabled" when the profile has none — distinct from a Witness that failed. Mutation-tested: M7 hardcode the cloud badge regardless of locality → 1 test "Listening + Cloud" still exists as one branch of a locality conditional; what is gone is its unconditional assertion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ntity
Two identity fixes, both cases of the identity asserting something it does
not know.
RuntimeLocality gains a fourth case, .unresolved. The unknown-provider path
used to classify itself .localBrokerToRemoteService with a comment admitting
"there is nothing to classify" — conservative about egress, but semantically
a claim of a known topology for a provider nothing knows anything about.
Unknown is not on-device, and it is not a known remote broker either:
.unresolved keeps involvesNetworkEgress == true (the banner must not claim
safety it cannot substantiate) while displaying "Egress unverified" (the
banner must not claim knowledge it does not have). This matches the honest
conservative state the Android implementation already uses.
isSubstitution now compares models canonically. OllamaReadinessProbe
deliberately accepts a stored `name:latest` for an untagged request — the
daemon stores an untagged pull under :latest, so they are the same model —
and the identity records the stored name. Comparing raw strings meant one
resolution was simultaneously accepted as the same model by readiness and
reported as a provider/model substitution by identity. canonicalModelName
maps an untagged name to name:latest (empty stays empty, so the failure-path
identity fabricates no phantom tag); any difference beyond that one implicit
tag — an explicit non-latest tag, a provider swap — still discloses.
Regression coverage:
- unknown provider → .unresolved, egress-conservative, "Egress unverified",
never rendered as On-device by the presentation
- qwen2.5 → qwen2.5:latest is no longer a substitution alarm (the previous
test pinned the buggy disagreement and has been inverted)
- qwen2.5 → qwen2.5:0.5b and ollama → claude still are
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DialecticalTurnEvent carried one generatorFingerprint, documented as the generator that produced the candidates. A persisted Reflective turn could therefore prove that a Witness ran and what it found, but not which provider, model, prompt profile, or prompt hash produced the finding — and the bug A2 fixed was precisely "reported Witness prompt ≠ prompt actually transmitted". The wiring is proven statically and in focused tests; the turn record could not attest it independently. DialecticalCompetition and DialecticalTurnEvent gain an optional witnessGeneratorFingerprint. The loop gives the Witness its own metadata box and callback (recordWitnessMetadata), parallel to the pole capture and never shared with it — filing both in one box would make the last writer win, a turn record attesting the pole prompt for the Witness. attachMetadataCaptureFromAdapter now wires both generators, so the app (AppViewModel) and the headless harness (dialectic-session), which both already call it, start persisting Witness provenance with no call-site change. The fingerprint is gated on witnessAttempted: a turn on which the Witness never ran must not attest a Witness identity, however recent the box contents. Decoding stays backward-compatible in both directions: pre-field logs decode with nil (missing Optional key), and a nil fingerprint encodes as an absent key, byte-shape-identical to the pre-field format. analyze_dialectic.py's provenance rollup now reports witness-fingerprint counts and models, plus a witness/pole prompt-hash collision count — the collision is the old bug's signature and must read 0. Tests: pole and Witness fingerprints captured separately with distinct prompt hashes on the same logged event; no Witness attestation when the profile disables it; codec round-trip of both fingerprints; a pole-fingerprint-only log decodes with the Witness field nil. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fail-closed path stores the unavailable identity and then sets hypnagogicLoopEnabled = false — after which the expanded privacy view's Network row rendered only "Disabled at runtime", hiding the requested provider, requested model, locality, and readiness failure that the identity was designed to preserve. lastError still carried the sanitized message, so this was not a privacy failure; it was a mismatch between the architectural claim (failure still has an identity) and the final UI (failure's identity was invisible). RuntimeIdentityPresentation gains lastAttemptLines(dialogue:witness:) — "Last attempt — Dialogue: Ollama · qwen2.5:0.5b · On-device · Model unavailable" — computed as data so the claim is testable, like every other runtime string this banner makes. The disabled Network row now renders those lines under "Disabled at runtime". Empty before the first enable attempt, so a fresh app still reads as plainly disabled. Every rendered field comes from the sanitized identity, never the raw error, and the active-listening badge stays hidden because the loop is not running (it remains gated on the toggle). Tests: no lines before any attempt; a failed dialogue attempt stays visible with the requested pair, locality, and failure label (and never "Ready"); a failed Witness attempt is reported alongside the dialogue attempt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sanitized acceptance record for PR #32 at head 4c5e275 (A/B/C + D/E/F) on Apple Silicon / macOS 26.5.2 — observations only, pending items listed as pending rather than assumed. Observed and recorded: - build, full test suite (616/0, exit 0), package-app-bundle, smoke-packaged-resources (all 5 packaged-layout loads + the missing-bundle guard), and codesign --verify --deep --strict, all PASS - real-daemon headless acceptance via dialectic-session: exact Ollama model resolves ready with a full fingerprint and no LLM call; a missing model fails closed (exit 1) with no substitution; Claude resolves as configured-only (executable + prompt, no provider request) - a divergence found by the untagged-model fixture: the harness's RuntimeFactory rejects an untagged request whose :latest form is installed, while the app's LiveRuntimeFactory/OllamaReadinessProbe canonicalizes it (and since commit D reports it as the same model) — flagged for follow-up alignment - a local-environment caveat: one UDP stream test kills the test process silently on this machine (reproduced at the untouched base 14c8b6a; CI green), so local `swift test` must be judged by exit code Pending (operator required, per the A2 report): the packaged GUI mode×provider matrix, real Claude operational acceptance including a Reflective turn with the new Witness fingerprint, cancellation/mode-change races, and the superseded-Reflective rerun. No private transcripts; the harness's model-list enumeration is noted but not reproduced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mechanical merge-forward onto final staging baseNo acceptance conclusions edited, no operator matrix run, PR left as draft. Both input SHAs verified against the expected pins before merging. Merged with A–G ancestry: all seven presentPR diff still the A2 delta21 files, exact-matched against the expected path set — no staging files leaked into the diff. CI on the merge-forwardThe Swift count is 417 executed, not the 616 carried in the acceptance record. Recorded as observed rather than assumed; the record's figure should be reconciled before the final evidence update. The 7 skips matter for the operator matrixAll are environment-gated, and four of them cover exactly what the operator acceptance exists to prove — so CI green does not discharge those gates: The remaining three are the known fixture-gated regressions ( The two packaged-app tests un-skip only after |
Second merge-forward — A2 suites now execute in CIAncestors preserved: A–G plus Three surfaces, recorded separatelyThe discoverability guard emitted
On the 417 vs 616 questionNot a contradiction, and now measurable. One deliberate omission
Still draftNo operator matrix run, no acceptance conclusions edited. The packaged GUI matrix, real-Claude turns, Reflective fingerprint capture, deliberate-failure gates, and the readiness-semantics decision remain open and are human work. |
`RuntimeReadiness` had one success case, and the two resolution paths reached
it on very different evidence.
Ollama GET /api/tags, exact requested model matched observed fact
Claude executable resolved, prompt loaded nothing contacted
Both rendered the word "Ready". On the Claude path an expired `claude login`,
an unreachable network, or a model the account cannot use all still resolved
as ready, because no request is made to Anthropic before enablement.
That left the failure side of the type strictly more precise than the success
side: six distinct reasons a runtime cannot be used, one undifferentiated
reason it can. `configured` closes the gap in the domain model rather than in
the view, so a future runtime's evidence semantics is a property of its
resolution, not an undocumented convention in `PrivacyIndicatorView`.
configured constructible, unverified Claude
ready endpoint + exact model verified Ollama
unavailable must not be used both
Two predicates, because the existing single one cannot answer both questions.
`isReady` stays strict; `canAttemptGeneration` covers configured|ready. The
distinction is load-bearing at `PrivacyIndicatorView.identityColor`, which
paints "not ready" red — keying it on `isReady` would have marked every Claude
session as a fault, trading the over-claim for its mirror image. The colour
answers "is this broken?"; the unverified/verified nuance lives in the line
text, where a nuance belongs.
A successful generation is *session* evidence and deliberately does not mutate
the identity. The identity records what resolution proved; rewriting it after
a good turn would erase the distinction this split exists to make.
Adding a case does not disturb the wire format — synthesized enum `Codable`
keys on the case name, not a declaration ordinal — but that is a property of
the compiler rather than of this type, so both legacy encodings are asserted
rather than assumed.
No existing assertion broke, which is itself the finding:
`testDefaultResolutionIsStillClaude` checked provider, model, substitution and
generator type and never looked at readiness, so the Claude success path's
readiness value was wholly unasserted. That is how the over-claim survived.
`RuntimeIdentityPresentationTests` fixture keyed its resolved fields on
`== .ready`, which would have handed a configured runtime the empty *failure*
shape; it now keys on `canAttemptGeneration`.
The two presentation tests were verified against the mutation they exist to
catch — branching `displayReadiness` on `resolvedProvider == "claude"` — which
fails both, in both directions. One asserts an Ollama-provider identity with
`configured` renders as configured; the other, a Claude-provider identity with
`ready` renders as verified. No provider-spelling implementation satisfies both.
deterministic suites 423 executed, 5 skipped, 0 failures
A2 app + session 147 executed, 0 failures
swift test list 627 discovered (was 619)
PR diff stays at 21 files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Commit H — readiness evidence split (
|
| before | after | |
|---|---|---|
| Claude resolution | .ready |
.configured — executable + prompt resolved, provider never contacted |
| Ollama resolution | .ready |
.ready — /api/tags reached, exact requested model matched |
| strict predicate | isReady |
isReady (unchanged semantics, now genuinely strict) |
| usability predicate | — | canAttemptGeneration (configured | ready) |
| display | "Ready" for both |
"Configured" / "Endpoint + model verified" |
Presentation was not keyed on provider spelling — that would reintroduce exactly the inference R8 removed. Both new presentation tests were run against that mutation (displayReadiness branching on resolvedProvider == "claude") and fail in both directions: an Ollama-provider identity carrying configured must render as configured, and a Claude-provider identity carrying ready must render as verified. No provider-branching implementation satisfies both.
PrivacyIndicatorView.identityColor moved to canAttemptGeneration. It paints "not ready" red; leaving it on isReady would have marked every Claude session as a fault — the mirror image of the over-claim being fixed.
A successful generation is session evidence and does not mutate the identity. The identity records what resolution proved.
Why no existing test broke
testDefaultResolutionIsStillClaude asserted provider, model, substitution, and generator type — and never readiness. The Claude success path's readiness value was wholly unasserted, which is how the over-claim survived. Separately, the RuntimeIdentityPresentationTests fixture keyed its resolved fields on == .ready and would have handed a configured runtime the empty failure shape; it now keys on canAttemptGeneration.
Wire format
Synthesized enum Codable keys on the case name, so adding configured leaves {"ready":{}} and {"unavailable":{"_0":"modelMissing"}} unchanged. Asserted in both directions rather than assumed.
CI on 2367281
guard discovered: NeuralComposeAppTests=142 DialecticSessionTests=5
deterministic 423 executed, 7 skipped, 0 failures (was 417)
A2 app + session 147 executed, 0 skipped, 0 failures (was 145)
NeuralComposeEEG 53 passed
Tests/eval 89 collected, 89 passed, 0 deselected
swift test list 627 discovered (was 619)
Swift CI coverage 562 → 570. PR diff remains exactly the same 21 files — set-compared against the pre-commit list, not just counted.
Still draft. Remaining work is physical: packaged build + signing, the 8-cell matrix, the failure/race matrix, and the final-head acceptance record.
Commit H made the app path truthful. The headless harness still recreated the
exact ambiguity it removed.
`RuntimeReport.verify` returned `(promptLoaded, true, true)` for Claude, with a
comment stating plainly that no call had been made. So `--dry-run` printed:
transport reachable: yes
model available: yes
✓ transport reachable
✓ model exists
No LLM call was made.
Four claims of verification and, four lines later, an admission that nothing was
verified. The acceptance document had already read this correctly as
"configured only", but the executable emitted stronger evidence than it held,
and the harness is the surface an operator reads when there is no UI.
The defect is the boolean. Two states force a skipped check to be reported as
either a pass or a failure, and this path picked pass. `VerificationStatus`
adds the missing third state:
passed ran, condition held
failed ran, condition did not hold
notChecked did not run — no evidence either way
runtime prompt provider exact model
claude passed/failed notChecked notChecked
ollama passed/failed passed/failed passed/failed
`notChecked` is not failure. `hasFailure` is true only for an actual `failed`,
so a configuration-only Claude dry run still exits 0 — verified against the
built binary, which now prints:
Verification:
prompt loaded: yes
provider reachable: not checked — no generation request made
exact model present: not checked — no generation request made
✓ runtime configured
✓ prompt loaded
– provider and model were not operationally verified
✓ fingerprint generated
Ollama keeps every claim it earns; its branch really does probe /api/tags for
the exact model, and reports `✓ endpoint reachable` / `✓ exact model present`.
Wording moved out of `main.swift` into `verificationLines` / `dryRunSummaryLines`,
which return `[String]`. It was formatted inline at two call sites, which is
precisely why no test could assert on what it claimed — the same shape as R8's
privacy strings buried in a SwiftUI body.
Scope grows 21 → 23 files. `RuntimeReport.swift` and the new test file are
A2 runtime lane, not a D0-owned cross-lane file; the 21-file bound was a
reviewability guard, not a reason to keep shipping a false diagnostic.
A2 app + session 152 executed, 0 failures (was 147)
deterministic 423 executed, 0 failures
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Commit I — headless harness truthfulness (
|
| runtime | prompt | provider | exact model |
|---|---|---|---|
| claude | passed/failed | notChecked | notChecked |
| ollama | passed/failed | passed/failed | passed/failed |
notChecked is not failure — hasFailure is true only for an actual failed, so a configuration-only dry run still exits 0. Verified against the built binary:
Verification:
prompt loaded: yes
provider reachable: not checked — no generation request made
exact model present: not checked — no generation request made
✓ runtime configured
✓ prompt loaded
– provider and model were not operationally verified
✓ fingerprint generated
No LLM call was made. exit 0
Ollama keeps every claim it earns — its branch really does probe /api/tags for the exact model, and still reports ✓ endpoint reachable / ✓ exact model present.
The wording moved out of main.swift into verificationLines / dryRunSummaryLines, which return [String]. It was formatted inline at two call sites — the same shape as the R8 privacy strings buried in a SwiftUI body, and the same reason nothing could assert on it.
Scope
21 → 23 files, both additions in the A2 runtime lane:
Sources/DialecticSession/RuntimeReport.swift
Tests/DialecticSessionTests/RuntimeVerificationTests.swift
ci.yml untouched — the D0 ownership rule holds.
CI on 71c5c02
guard discovered: NeuralComposeAppTests=142 DialecticSessionTests=10
deterministic 423 executed, 7 skipped, 0 failures
A2 app + session 152 executed, 0 skipped, 0 failures (was 147)
NeuralComposeEEG 53 passed
Tests/eval 89 collected, 89 passed, 0 deselected
Swift CI coverage 570 → 575. DialecticSessionTests 5 → 10.
This is the last automated truthfulness seam. Everything remaining is operator acceptance on this head.
The distinction the change rests on
The runtime generates. The identity is an immutable, sanitized description that
telemetry, readiness logic and the privacy UI consume — and that exists on the
failure path too, so a runtime that did not resolve can still be described.
RuntimeResolutionFailurecarries one.Keeping
requestedandresolvedas separate pairs is what makes substitutionvisible rather than inferable. A UI rendering only the resolved provider cannot
distinguish "you asked for Ollama and got it" from "you asked for Ollama and
something handed you Claude".
Commit A —
1fdf48bidentity and readinessResolvedRuntimeIdentity,RuntimeRole,RuntimeLocality,RuntimeReadiness,sanitized failures, app-side Claude resolution through
ClaudeExecutableResolver, bounded Ollama readiness probing, identity storagein
AppViewModel.Locality exists because a local executable does not imply local inference.
The Claude CLI is a local binary brokering to a remote service; Ollama on a
non-loopback host is remote however local the config looks. Classifying by
where the process runs would make the banner assert on-device inference for
the one path that leaves the machine.
Readiness never generates. Claude resolves an executable and loads/hashes a
prompt — no request reaches Anthropic. Ollama gets a bounded
GET /api/tags—no prompt is sent. A failure disables the loop and attempts no alternate
provider.
Sanitization is enforced at construction, not at display. The resolver's
error embeds the full environment
PATHandlastErrorrenders in the UI; adisplay site that must remember to sanitize will eventually forget.
Commit B —
3067cd8role-correct WitnessThe Witness had no runtime of its own. In the harness it wrapped the pole
runtime and passed the Witness prompt into the adapter's stored-but-unread
systemPromptfield — so it reported one prompt and transmitted another. A"non-voiced observer that names what both poles avoided" was in fact a third
pole. This was live in
dialectic-session, not only in the app.Removing the dead field is what forces the fix: with nowhere to put a prompt
that does not take effect, the prompt has to be resolved into the runtime that
sends it.
A disabled role now resolves nothing — previously
witnessEnabledwas consultedonly when deciding whether to use the Witness, after paying for its prompt
load and endpoint probe.
WitnessResolutionis a three-case enum because"no Witness in this profile" and "the Witness failed" must diverge: the first is
normal, the second fails the requested configuration closed.
Role-consistency guard (not in the original brief):
resolveWitnessRuntimefixes the role internally, but the injected closure returns the same tuple type
whatever role it resolved, so a caller could still hand the Witness a pole
runtime. A mismatch now fails closed.
Commit C —
14c8b6atruthful presentation"the claude CLI"and"Listening + Cloud"were literals inside a SwiftUIbody— the app's most consequential privacy claim, in the one place no testcould reach. Both had been wrong since runtime selection landed: with
NEURALCOMPOSE_RUNTIME=ollamathe loop ran entirely on-device while the bannerreported cloud egress.
RuntimeIdentityPresentationcomputes every claim from the identity. Egress isread from
locality, never a provider name. Unknown counts as egress — beforethe first resolution there is nothing to read, and the banner must not claim
on-device operation it cannot substantiate.
"Listening + Cloud"still exists as one branch of a locality conditional; whatis gone is its unconditional assertion.
Follow-ups D–G (added 2026-07-26; A/B/C unrewritten)
Commit D —
d0d5dd7unresolved locality + canonical model identity.RuntimeLocality.unresolvedfor the unknown-provider path — egress staysassumed (
involvesNetworkEgress == true), the label says "Egress unverified"instead of claiming a broker topology nothing knows.
isSubstitutionnowcompares canonical model names, so the probe's one allowed equivalence
(untagged →
:latest) no longer reads as a substitution alarm while any otherdifference still discloses.
Commit E —
db01432Witness runtime fingerprint.witnessGeneratorFingerprintonDialecticalCompetition/DialecticalTurnEvent, captured through the Witness's own metadata box (nevershared with the pole's) and gated on
witnessAttempted. App anddialectic-sessionboth get it via the existingattachMetadataCaptureFromAdapter()call. Pre-field logs decode unchanged; anil fingerprint encodes as an absent key.
analyze_dialectic.pyreportsWitness provenance plus a witness/pole prompt-hash collision count (the old
wrong-prompt bug's signature; must be 0). A persisted Reflective turn can now
independently attest which runtime/prompt produced the finding.
Commit F —
4c5e275failed identity visible after disablement.RuntimeIdentityPresentation.lastAttemptLinesrenders"Last attempt — Dialogue: Ollama · qwen2.5:0.5b · On-device · Model
unavailable" in the expanded Network row while the toggle is off. Sanitized
identity only; empty before the first attempt; active badge stays hidden.
Commit G —
b3a1ddbpackaged Apple Silicon acceptance record.docs/evaluation/a2-apple-silicon-acceptance.md: build/package/smoke/codesignall PASS at head; real-daemon headless acceptance (exact model ready with full
fingerprint and no LLM call; missing model fails closed, no substitution;
Claude configured-only). Found divergence: the harness's
RuntimeFactorydoesNOT canonicalize untagged →
:latestwhile the app path does — follow-upalignment flagged, not fixed here.
CI status at
b3a1ddbRun 30191318717: build PASS; "Test (deterministic suites)" failed on
EEGChannelHealthProviderTests/testProviderStaysFreshWithContinuousIngestPastTheTimeoutWindow("stale" ≠ "healthy", 4 channel assertions). Classified pre-existing
nondeterministic timing test, not an A2 regression:
git diff 23c56ea..b3a1ddb -- Tests/BCIEEGTests Sources/BCIEEGis empty —the failing test and provider are byte-identical to the merged base.
1c9ce1f("one matrix runner passed, the other failed on the SAME commit…Not a regression"). This test kept its tight margins then.
staleTimeoutSecto 0.2 s (productiondefault 2.0 s) with 50 ms inter-burst sleeps; any >200 ms runner stall
between the drain task's last ingest and the query flips all four channels
to
.stale.Recommended follow-up (out of A2 scope, mirrors the accepted
1c9ce1fprecedent): widen this test the same way —
staleTimeoutSec0.2 → 2.0 s(the production default), inter-burst sleeps 50 → 250 ms, keeping sleep ≪
timeout with ~8× oversleep tolerance. Until then the correct CI policy for
this failure signature is a targeted re-run of the job, not a code change in
this PR.
Mutation results — 11 required, 11 caught
Applied to a clean tree at
14c8b6a, verified, reverted. Exact patches below./api/tagsprobeExact patches
Each is a single substitution;
swift test --filter <suite>then restore.Suites: M1/M3/M7/M10 →
NeuralComposeAppTests; the rest →BCICloudBridgeTests.All mutations reverted; worktree clean; verified byte-identical against an
out-of-band copy.
The harness itself was wrong three times — worth recording
Green output is not evidence, and this run proved it on its own terms.
git checkout -- <file>on an uncommitted tree destroysthe work under test, not the mutation. It wiped two files mid-run and left
mutations stuck in the untracked ones. Restored and re-verified.
grep error:to detect compile failures matched XCTest's own assertionformat (
File.swift:NN: error: -[Suite testX]), scoring all 11 genuinefailures as "rejected at compile time".
2 tests. The table above records the manual result.
Two of those errors flattered the outcome and one alarmed it. The ad-hoc
harness is deliberately not committed — it is not hermetic. The durable
evidence is the regression tests, this table, and these patches.
Verification at
14c8b6aBCICoreTestsBCICloudBridgeTestsNeuralComposeAppTestsDialecticSessionTestsBCIClassifierTestsBCIVoiceTestsEach commit builds independently (
1fdf48b,3067cd8,14c8b6a— all clean)../Scripts/build.shclean ·git diff --checkclean.Verification at
b3a1ddb(after D/E/F/G)Full local suite: 616 tests, 0 failures, 10 skipped, exit 0, built and run
after each of D/E/F. (Local caveat, machine-specific: one pre-existing UDP
stream test kills the test process silently on the dev machine — reproduced at
untouched
14c8b6a, green on CI — so local runs skip it and are judged byexit code; recorded in the Commit G acceptance doc.) CI deterministic-suites
selection at this head: 415 executed, 7 skipped.
Remaining operator gates before marking ready
Per the A2 report and
docs/evaluation/a2-apple-silicon-acceptance.md§4 —none of these is claimed by this PR:
{Claude, Ollama}, one turn or one deliberate readiness failure each.
and Reflective turns — the Reflective turn showing the new per-turn
Witness fingerprint). Decides whether
RuntimeReadinessneeds a distinctconfiguredstate.Semantic gates at the tip:
git grep '"the claude CLI"\|"Listening + Cloud"' -- Sources→ only doccomments describing the removed defect.
git grep systemPrompt -- …/GenerationRuntimeTextGeneratingAdapter.swift→only the doc comment recording the field's removal.
Provider requests. No test added by this PR makes one; readiness HTTP is
served by
MockURLProtocol. This is not blanket-true of the suite — thepre-existing
testLiveRuntimeHitsOllamacontacts a localhost Ollama daemon whenone is running.
Deviations, all reported
RuntimeRole.mirroradded. The mirror loop resolves its own runtime witha third prompt profile; filing it under
.dialecticwould make the identityassert a prompt the runtime does not carry. It stores under
dialogueRuntimeIdentityalongside.dialectic.failing test.
cancelPendingHypnagogicReconcile()— an internal method, not an exposedtask property. The only capability tests need is "stop the pending
reconcile"; no production caller uses it.
of the factory, so it arrives with the factory. B adds M1 and M3.
marked legacy in place, and removes it in B — a delay, not a new mechanism.
Untouched: EEG acquisition/preprocessing, W0 schemas, Qwen policy, R9, R16, R1,
packaging, experiments, promotion status.
The ADR-011 amendment forbidding a shadow-policy evaluator from conforming to
GenerationRuntime/TextGenerating/MetadataPublishingTextGeneratingisnot in this PR; it lands as a later governance amendment.
🤖 Generated with Claude Code