Skip to content

A2: role-specific runtime identity and readiness (R3 + R8 + R18) - #32

Draft
aurascoper wants to merge 11 commits into
docs/eeg-methods-scopefrom
feat/resolved-runtime-identity
Draft

A2: role-specific runtime identity and readiness (R3 + R8 + R18)#32
aurascoper wants to merge 11 commits into
docs/eeg-methods-scopefrom
feat/resolved-runtime-identity

Conversation

@aurascoper

@aurascoper aurascoper commented Jul 25, 2026

Copy link
Copy Markdown
Owner
head: 71c5c02
base: docs/eeg-methods-scope@9548da49

substantive_a2_commits: 9      # A/B/C + follow-ups D/E/F/G + H + I, none rewritten
merge_forward_commits:
  - 9eabad6                    # onto staging 41f559a8
  - fe20fec                    # onto staging 9548da49 (adds the A2 CI step, #42)

pr_diff_files: 23              # 21 through H; +2 in I, both A2 runtime lane
                               #   Sources/DialecticSession/RuntimeReport.swift
                               #   Tests/DialecticSessionTests/RuntimeVerificationTests.swift

readiness_semantics:           # commit H — evidence, not just usability
  configured: claude           # executable + prompt resolved; provider never contacted
  ready: ollama                # /api/tags reached, exact requested model matched
  strict_predicate: isReady
  usability_predicate: canAttemptGeneration
  presentation_branches_on_provider: false   # mutation-tested in both directions
  identity_mutated_by_successful_turn: false # session evidence stays separate

headless_verification:         # commit I — same split, harness side
  claude: {prompt: checked, provider: not_checked, model: not_checked}
  ollama: {prompt: checked, provider: checked, model: checked}
  not_checked_is_failure: false            # configuration-only dry run exits 0
  false_verified_claims_remaining: 0

R3:
  witness_role_separated: true
  witness_disabled_resolution_count: 0
  dead_adapter_prompt_field_removed: true
  harness_corrected: true

R8:
  UI_source: resolved_runtime_identity
  hardcoded_provider_strings: 0

R18:
  endpoint_probe: true
  exact_model_probe: true
  missing_model_disables_loop: true
  alternate_provider_attempted: false

A2_added_tests:
  real_provider_generation: false
  readiness_HTTP: mocked

new_runtime_authority: false
model_execution_authorized: false
promotion_status: not_eligible

The distinction the change rests on

Resolved runtime object   ≠   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.
RuntimeResolutionFailure carries one.

Keeping requested and resolved as separate pairs is what makes substitution
visible 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 — 1fdf48b identity and readiness

ResolvedRuntimeIdentity, RuntimeRole, RuntimeLocality, RuntimeReadiness,
sanitized failures, app-side Claude resolution through
ClaudeExecutableResolver, bounded Ollama readiness probing, identity storage
in 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 PATH and lastError renders in the UI; a
display site that must remember to sanitize will eventually forget.

Commit B — 3067cd8 role-correct Witness

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 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 witnessEnabled was consulted
only when deciding whether to use the Witness, after paying for its prompt
load and endpoint probe. WitnessResolution is 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): resolveWitnessRuntime
fixes 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 — 14c8b6a truthful presentation

"the claude CLI" and "Listening + Cloud" were literals inside a SwiftUI
body — the app's most consequential privacy claim, in the one place no test
could reach. Both had been wrong since runtime selection landed: with
NEURALCOMPOSE_RUNTIME=ollama the loop ran entirely on-device while the banner
reported cloud egress.

RuntimeIdentityPresentation computes every claim from the identity. Egress is
read from locality, never a provider name. Unknown counts as egress — before
the 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; what
is gone is its unconditional assertion.

Follow-ups D–G (added 2026-07-26; A/B/C unrewritten)

Commit D — d0d5dd7 unresolved locality + canonical model identity.
RuntimeLocality.unresolved for the unknown-provider path — egress stays
assumed (involvesNetworkEgress == true), the label says "Egress unverified"
instead of claiming a broker topology nothing knows. isSubstitution now
compares canonical model names, so the probe's one allowed equivalence
(untagged → :latest) no longer reads as a substitution alarm while any other
difference still discloses.

Commit E — db01432 Witness runtime fingerprint.
witnessGeneratorFingerprint on DialecticalCompetition /
DialecticalTurnEvent, captured through the Witness's own metadata box (never
shared with the pole's) and gated on witnessAttempted. App and
dialectic-session both get it via the existing
attachMetadataCaptureFromAdapter() call. Pre-field logs decode unchanged; a
nil fingerprint encodes as an absent key. analyze_dialectic.py reports
Witness 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 — 4c5e275 failed identity visible after disablement.
RuntimeIdentityPresentation.lastAttemptLines renders
"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 — b3a1ddb packaged Apple Silicon acceptance record.
docs/evaluation/a2-apple-silicon-acceptance.md: build/package/smoke/codesign
all 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 RuntimeFactory does
NOT canonicalize untagged → :latest while the app path does — follow-up
alignment flagged, not fixed here.

CI status at b3a1ddb

Run 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/BCIEEG is empty —
    the failing test and provider are byte-identical to the merged base.
  • The sibling test flaked identically on CI 2026-07-20 and was widened by
    1c9ce1f ("one matrix runner passed, the other failed on the SAME commit…
    Not a regression"). This test kept its tight margins then.
  • Mechanism: the test overrides staleTimeoutSec to 0.2 s (production
    default 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.
  • Local at head: 30/30 passes (15 idle + 15 under 12-way CPU load).

Recommended follow-up (out of A2 scope, mirrors the accepted 1c9ce1f
precedent): widen this test the same way — staleTimeoutSec 0.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.

# Mutation Result
M1 accept a pole runtime as the Witness 2 failed
M2 map the Witness role to the pole prompt 3 failed
M3 resolve the Witness while disabled 1 failed
M4 remove the /api/tags probe 6 failed
M5 report a missing model as present 2 failed
M6 substitute Claude after Ollama failure 2 failed
M7 hardcode the cloud badge 1 failed
M8 classify the Claude CLI on-device 1 failed
M9 interpolate PATH into the public message 1 failed
M10 delete the enabled→disabled write 1 failed
M11 report an unavailable runtime ready 1 failed

Exact patches

Each is a single substitution; swift test --filter <suite> then restore.

M1  AppViewModel.swift            guard resolved.identity.role == role else {   →  guard role == role else {
M2  LiveRuntimeFactory.swift      case .witness:   return .witness              →  case .witness:   return .wakingDialectical
M3  AppViewModel.swift            guard witnessEnabled else {                   →  guard witnessEnabled || true else {
M4  LiveRuntimeFactory.swift      switch await probe.probe(model: requestedModel) {
                                    →  switch OllamaReadinessProbe.ProbeOutcome.present(.init(name: requestedModel, digest: nil)) {
M5  OllamaReadinessProbe.swift    return .modelMissing(available: available.map(\.name))
                                    →  return .present(AvailableModel(name: model, digest: nil))
M6  LiveRuntimeFactory.swift      the `.modelMissing` throw  →  return try await makeClaude(role:profile:requestedProvider:
                                    requestedModel:"claude-sonnet-5", environment: ProcessInfo.processInfo.environment)
M7  RuntimeIdentityPresentation.swift
                                  self.badgeLabel = egress ? "Listening + Cloud" : "Listening · On-device"
                                    →  self.badgeLabel = "Listening + Cloud"
M8  LiveRuntimeFactory.swift      let locality: RuntimeLocality = .localBrokerToRemoteService  →  = .onDevice
M9  LiveRuntimeFactory.swift      "The claude CLI was not found. Install Claude Code and run `claude login`.",
                                    →  "The claude CLI was not found: \(error)",
M10 AppViewModel.swift            delete `hypnagogicLoopEnabled = false` from disableHypnagogicLoop
M11 ResolvedRuntimeIdentity.swift public var isReady: Bool { readiness == .ready }  →  { true }

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.

  1. Reverting with git checkout -- <file> on an uncommitted tree destroys
    the work under test, not the mutation. It wiped two files mid-run and left
    mutations stuck in the untracked ones. Restored and re-verified.
  2. grep error: to detect compile failures matched XCTest's own assertion
    format (File.swift:NN: error: -[Suite testX]), scoring all 11 genuine
    failures as "rejected at compile time".
  3. M5 was reported SURVIVED at the tip. Manual reproduction shows it fails
    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 14c8b6a

target result
BCICoreTests 202 (2 skipped)
BCICloudBridgeTests 104 (2 skipped)
NeuralComposeAppTests 136
DialecticSessionTests 5
BCIClassifierTests 13
BCIVoiceTests 21

Each commit builds independently (1fdf48b, 3067cd8, 14c8b6a — all clean).
./Scripts/build.sh clean · git diff --check clean.

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 by
exit 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:

  1. Packaged GUI matrix: {Mirror, Focused, Reflective, Contemplative} ×
    {Claude, Ollama}, one turn or one deliberate readiness failure each.
  2. Real Claude operational acceptance (authenticated CLI; Mirror, Focused,
    and Reflective turns — the Reflective turn showing the new per-turn
    Witness fingerprint). Decides whether RuntimeReadiness needs a distinct
    configured state.
  3. Cancellation / mode-change races while resolution is pending.
  4. Superseded-Reflective annotation + fixture rerun (post-merge step).

Semantic gates at the tip:

  • git grep '"the claude CLI"\|"Listening + Cloud"' -- Sources → only doc
    comments 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 — the
pre-existing testLiveRuntimeHitsOllama contacts a localhost Ollama daemon when
one is running.

Deviations, all reported

  • RuntimeRole.mirror added. The mirror loop resolves its own runtime with
    a third prompt profile; filing it under .dialectic would make the identity
    assert a prompt the runtime does not carry. It stores under
    dialogueRuntimeIdentity alongside .dialectic.
  • Role-consistency guard added, beyond the brief. Without it M1 had no
    failing test.
  • cancelPendingHypnagogicReconcile() — an internal method, not an exposed
    task property. The only capability tests need is "stop the pending
    reconcile"; no production caller uses it.
  • M2's coverage lands in Commit A, not B. The role→profile binding is part
    of the factory, so it arrives with the factory. B adds M1 and M3.
  • No temporary shim. Commit A retains the existing dead adapter field,
    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 / MetadataPublishingTextGenerating is
not in this PR; it lands as a later governance amendment.

🤖 Generated with Claude Code

aurascoper and others added 7 commits July 25, 2026 03:49
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>
@aurascoper

Copy link
Copy Markdown
Owner Author

Mechanical merge-forward onto final staging base

No acceptance conclusions edited, no operator matrix run, PR left as draft.

old_head           b3a1ddb32e925fb015e1c55255266b5b6209e30f
staging_head       41f559a8aed538c7fe71016590fdc19de4627a9d
merge_forward_sha  9eabad6fce8931d68d53d56db2b629ba39723ff5

Both input SHAs verified against the expected pins before merging. Merged with --no-ff; A–G were not rebased, squashed, or amended.

A–G ancestry: all seven present

1fdf48b  feat(runtime): add resolved identity and readiness probing
3067cd8  fix(dialectic): resolve Witness with a distinct prompt runtime
14c8b6a  fix(ui): display actual runtime provider and readiness
d0d5dd7  fix(runtime): distinguish unresolved locality and canonical model identity
db01432  fix(telemetry): persist Witness runtime fingerprint
4c5e275  fix(ui): retain failed runtime identity after loop disablement
b3a1ddb  test(runtime): record packaged Apple Silicon A2 acceptance

PR diff still the A2 delta

21 files, exact-matched against the expected path set — no staging files leaked into the diff. git diff --check clean, worktree clean (0 entries).

CI on the merge-forward

build-and-test     PASS   417 executed, 7 skipped, 0 failures
python-contracts   PASS
  NeuralComposeEEG        53 passed
  Tests/eval              89 collected, 89 selected, 89 passed, 0 deselected

The 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 matrix

All are environment-gated, and four of them cover exactly what the operator acceptance exists to prove — so CI green does not discharge those gates:

PackagedAppResourceTests.testPackagedAppContainsPromptBundle          no packaged .app
PackagedAppResourceTests.testLocatorResolvesEveryPromptInPackagedLayout  no packaged .app
OllamaHTTPTransportTests.testLiveRuntimeHitsOllama                    Ollama unreachable
GeneratorFingerprintTests.testAbstractionSmokeOllama                  Ollama unreachable

The remaining three are the known fixture-gated regressions (GoldenRecordingRegressionTests, two SemanticBGEReplayRegressionTests).

The two packaged-app tests un-skip only after Scripts/package-app-bundle.sh, and the two Ollama tests only with a live daemon — both conditions the operator pass will satisfy locally. Worth running swift test again after packaging, since those four are the CI blind spot this PR's acceptance is meant to cover.

@aurascoper

Copy link
Copy Markdown
Owner Author

Second merge-forward — A2 suites now execute in CI

previous head       9eabad6  (first merge-forward, onto staging 41f559a8)
new head            fe20fec6c9644c4a5d2aacd7f09322b20e280fe6
base                docs/eeg-methods-scope@9548da49  (includes #42)

Ancestors preserved: A–G plus 9eabad6, all eight verified. PR diff still exactly the same 21 application files — verified by set comparison, not count alone.

Three surfaces, recorded separately

CI deterministic Swift subset      417 executed,  7 skipped, 0 failures
CI A2 app/session suites           145 executed,  0 skipped, 0 failures
CI NeuralComposeEEG contracts       53 passed
CI Tests/eval                       89 collected, 89 selected, 89 passed, 0 deselected

The discoverability guard emitted discovered: NeuralComposeAppTests=140 DialecticSessionTests=5 — so the 145 is real execution, not a filter that matched nothing. Swift coverage in CI rises from 417 to 562 executed.

NeuralComposeAppTests is 140 here versus 116 on staging: this PR contributes 24 of them, including RuntimeIdentityPresentationTests (13). Those were previously running nowhere.

On the 417 vs 616 question

Not a contradiction, and now measurable. swift test list discovers 619 tests on this head. 417 is what the deterministic filter selects; 145 more come from the A2 step; the remainder are BCILLMTests and WorldModelDemoTests, unselected in CI by design. The historical 616 from 4c5e275 stands as a full-package local figure and is not restated as a final-head count — the final-head full-package number will be measured locally during operator acceptance.

One deliberate omission

RuntimeIdentityPresentationTests now exists here and would make a valid third sentinel in the discoverability guard. It is not added, because editing .github/workflows/ci.yml in this PR would make it a 22nd file and take ci.yml out of its named D0 owner. The per-target floors already guard that suite, and its 13 tests do execute inside the 145. Adding the third sentinel belongs in a one-line D0 follow-up after this merges.

Still draft

No 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>
@aurascoper

Copy link
Copy Markdown
Owner Author

Commit H — readiness evidence split (2367281)

Closes the last correctness item before the operator pass. Fixed in the domain model, not the view.

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>
@aurascoper

Copy link
Copy Markdown
Owner Author

Commit I — headless harness truthfulness (71c5c02)

Commit H fixed the app path; the harness still recreated the same ambiguity. RuntimeReport.verify returned (promptLoaded, true, true) for Claude with a comment stating no call had been made, so --dry-run printed four verification claims and then No LLM call was made.

The defect was the boolean: two states force a skipped check to be reported as a pass or a failure, and this path picked pass.

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 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant