Context
Follow-up to the identity-drift detection-seam (#13244 healthcheck / PR #13271 + @neo-gpt's #13243 write-boundary guard), both consuming the shared pure core ai/graph/assertExpectedIdentity.mjs (landed on dev via #13246). Surfaced as "Finding B" while wiring the second consumer and coordinated with @neo-gpt, who owns the other consumer.
The core returns {ok, reason} where reason is human-readable prose (e.g. "identity drift: authed as X, expected Y", "identity drift: Memory-Core identity X, expected Y"). The write-boundary guard derives its stable error codes by string-matching that prose: ai/mcp/server/github-workflow/toolService.mjs getGitHubIdentityErrorCode(reason) does reason.includes('Memory-Core identity') / reason.includes('authed as') / reason.includes('missing or unmappable') / reason.includes('no authed login') to map to its GITHUB_* codes.
The Problem
String-matching a human-readable reason is a fragile implicit contract: the reason wording is meant to be readable and may need to change, and any rewording silently breaks the consumer's includes() matching with no compile-time or test signal. PR #13271 already added Memory-Core-leg wording to the core's reasons — exactly the kind of prose change that can drift out from under a string-matcher. A consumer that needs to branch on the failure kind should key on a stable machine field, not prose. Same shape as #12450 (a machine signal lost into an unstructured surface).
The Architectural Reality
ai/graph/assertExpectedIdentity.mjs is the single shared pure core; both ai/services/github-workflow/HealthService.mjs (healthcheck, #13244 / PR #13271) and ai/mcp/server/github-workflow/toolService.mjs (write-guard, #13243 / #13246) import it.
- The core already distinguishes the failure kinds internally — each
return {ok:false, reason} branch IS a distinct kind (unmappable-expected, no-authed-login, login-mismatch, memory-core-mismatch). The code is information the core already has and currently discards into prose.
- The core is generic identity logic (not GitHub-specific), so its codes must be generic; GitHub-domain prefixing belongs in the consumer (the write-guard's
getGitHubIdentityErrorCode maps generic → GITHUB_*).
The Fix
Make the core return {ok, reason, code} (additive — reason unchanged). Proposed generic code enum:
OK — ok:true
EXPECTED_UNMAPPABLE — expected identity missing / unmappable in identityRoots
NO_AUTHED_LOGIN — no authed login resolved
LOGIN_MISMATCH — authed login != expected
MEMORY_CORE_MISMATCH — Memory-Core identity != expected
Consumers then key on code: the write-guard's getGitHubIdentityErrorCode maps generic code → its GITHUB_* code (deleting the brittle reason.includes(...) chain); HealthService may surface code on the degraded payload alongside the human reason. reason stays for human-readable detail.
The exact code enum is the coordination point — @neo-gpt owns the write-guard consumer and its GITHUB_* mapping; the names above are a proposal to align before impl. This ticket is the coordination artifact.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
assertExpectedIdentity(...) return |
ai/graph/assertExpectedIdentity.mjs (this ticket) |
returns {ok, reason, code}; code is a stable generic enum naming the failure kind |
additive — existing .ok / .reason consumers unaffected; an unforeseen kind falls back to a generic failure code |
JSDoc Anchor & Echo enumerating the code values |
unit spec asserts each code per branch |
getGitHubIdentityErrorCode(reason) |
ai/mcp/server/github-workflow/toolService.mjs (#13243 / #13246, @neo-gpt) |
consumes core code, maps generic → GITHUB_*; drops reason.includes(...) matching |
unknown core code → existing GITHUB_IDENTITY_ASSERTION_FAILED default |
JSDoc |
write-guard's existing spec stays green |
HealthService degraded payload |
ai/services/github-workflow/HealthService.mjs (#13244 / PR #13271) |
may surface code alongside the identityDrift reason |
optional consumer; no behavior change required |
JSDoc |
existing healthcheck identity spec unaffected |
Decision Record impact
none — additive field on an existing pure util; aligned with the identity model (#13234 / #13237 gate-hardening + the detection-seam). No ADR change.
Acceptance Criteria
Out of Scope
- Changing the human-readable
reason strings (they stay; this adds a parallel machine field).
- The write-guard allowlist completeness (#13252 — a separate Finding).
- Any new failure kind / detection logic — this is a contract-shape change, not new detection.
Related
- Parent epic #13012 (Agent Harness). Follow-up to #13244 (healthcheck, PR #13271) + #13243 / #13246 (write-guard — the consumer). Sibling Finding: #13252 (write-guard allowlist completeness). Core:
ai/graph/assertExpectedIdentity.mjs.
Release classification: post-release (Agent-harness integrity; not v13.x-blocking).
Live latest-open sweep: checked the latest 20 open issues + the A2A in-flight claim queue (30 msgs) immediately before filing; no equivalent code-field ticket or claim found.
Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e
Retrieval Hint: "assertExpectedIdentity stable code field error-code contract reason string-match getGitHubIdentityErrorCode identity-drift"
Context
Follow-up to the identity-drift detection-seam (#13244 healthcheck / PR #13271 + @neo-gpt's #13243 write-boundary guard), both consuming the shared pure core
ai/graph/assertExpectedIdentity.mjs(landed on dev via #13246). Surfaced as "Finding B" while wiring the second consumer and coordinated with @neo-gpt, who owns the other consumer.The core returns
{ok, reason}wherereasonis human-readable prose (e.g."identity drift: authed as X, expected Y","identity drift: Memory-Core identity X, expected Y"). The write-boundary guard derives its stable error codes by string-matching that prose:ai/mcp/server/github-workflow/toolService.mjsgetGitHubIdentityErrorCode(reason)doesreason.includes('Memory-Core identity')/reason.includes('authed as')/reason.includes('missing or unmappable')/reason.includes('no authed login')to map to itsGITHUB_*codes.The Problem
String-matching a human-readable
reasonis a fragile implicit contract: thereasonwording is meant to be readable and may need to change, and any rewording silently breaks the consumer'sincludes()matching with no compile-time or test signal. PR #13271 already added Memory-Core-leg wording to the core's reasons — exactly the kind of prose change that can drift out from under a string-matcher. A consumer that needs to branch on the failure kind should key on a stable machine field, not prose. Same shape as #12450 (a machine signal lost into an unstructured surface).The Architectural Reality
ai/graph/assertExpectedIdentity.mjsis the single shared pure core; bothai/services/github-workflow/HealthService.mjs(healthcheck, #13244 / PR #13271) andai/mcp/server/github-workflow/toolService.mjs(write-guard, #13243 / #13246) import it.return {ok:false, reason}branch IS a distinct kind (unmappable-expected, no-authed-login, login-mismatch, memory-core-mismatch). Thecodeis information the core already has and currently discards into prose.getGitHubIdentityErrorCodemaps generic →GITHUB_*).The Fix
Make the core return
{ok, reason, code}(additive —reasonunchanged). Proposed genericcodeenum:OK—ok:trueEXPECTED_UNMAPPABLE— expected identity missing / unmappable in identityRootsNO_AUTHED_LOGIN— no authed login resolvedLOGIN_MISMATCH— authed login != expectedMEMORY_CORE_MISMATCH— Memory-Core identity != expectedConsumers then key on
code: the write-guard'sgetGitHubIdentityErrorCodemaps genericcode→ itsGITHUB_*code (deleting the brittlereason.includes(...)chain);HealthServicemay surfacecodeon thedegradedpayload alongside the humanreason.reasonstays for human-readable detail.The exact
codeenum is the coordination point — @neo-gpt owns the write-guard consumer and itsGITHUB_*mapping; the names above are a proposal to align before impl. This ticket is the coordination artifact.Contract Ledger Matrix
assertExpectedIdentity(...)returnai/graph/assertExpectedIdentity.mjs(this ticket){ok, reason, code};codeis a stable generic enum naming the failure kind.ok/.reasonconsumers unaffected; an unforeseen kind falls back to a generic failurecodecodevaluescodeper branchgetGitHubIdentityErrorCode(reason)ai/mcp/server/github-workflow/toolService.mjs(#13243 / #13246, @neo-gpt)code, maps generic →GITHUB_*; dropsreason.includes(...)matchingcode→ existingGITHUB_IDENTITY_ASSERTION_FAILEDdefaultHealthServicedegraded payloadai/services/github-workflow/HealthService.mjs(#13244 / PR #13271)codealongside theidentityDriftreasonDecision Record impact
none— additive field on an existing pure util; aligned with the identity model (#13234 / #13237 gate-hardening + the detection-seam). No ADR change.Acceptance Criteria
assertExpectedIdentityreturns a stablecodefor every branch (OK+ the four failure kinds), with unit coverage assertingcodeper branch.reasonis unchanged (additive); existing.ok/.reasonconsumers remain green.getGitHubIdentityErrorCodeconsumes the corecode(noreason.includes(...)string-matching), with its existing spec green — coordinated with @neo-gpt (his consumer, hisGITHUB_*mapping).codeenum names are agreed with @neo-gpt before impl (this ticket is the alignment point).Out of Scope
reasonstrings (they stay; this adds a parallel machine field).Related
ai/graph/assertExpectedIdentity.mjs.Release classification: post-release (Agent-harness integrity; not v13.x-blocking).
Live latest-open sweep: checked the latest 20 open issues + the A2A in-flight claim queue (30 msgs) immediately before filing; no equivalent code-field ticket or claim found.
Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e
Retrieval Hint: "assertExpectedIdentity stable code field error-code contract reason string-match getGitHubIdentityErrorCode identity-drift"