Skip to content

feat: crew member dispatch via per-session session_* mount on ACP backends - #8153

Merged
iamwhatever merged 1 commit into
mainfrom
feat/member-acp-backends
Sep 3, 2026
Merged

feat: crew member dispatch via per-session session_* mount on ACP backends#8153
iamwhatever merged 1 commit into
mainfrom
feat/member-acp-backends

Conversation

@CrysisDeu

@CrysisDeu CrysisDeu commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Problem / Motivation

A crew member's pinned DM thread is designed to be a conductor: it should dispatch real work into worker sessions it creates, patrol them, and report back — with zero operator configuration. Today it cannot: the session_* session-control tools live behind a per-agent server assignment on the on-disk agent template, and granting them there would hand session control to every session running that template, not just the member's DM. PR #8021 explored worker_* twin tools on kirocrew-core and was deliberately closed for the same objection in reverse: adding same-class tools to a core server grants them fleet-wide.

Why it matters

Members are the product's zero-configuration delegation surface. Without a session-scoped mount, an operator must either hand-edit agent templates (breaking zero-config and over-granting) or members stay chat-only. The per-session ACP channel — session/new mcpServers on the claude seam, _meta.kiro.customAgents on KAS — is the protocol-native way to give exactly one session extra tools, which is the capability this feature rides.

What changed (motivation → approach → change)

Goal: member DM sessions get the existing kirocrew-dashboard session_* tools, scoped to that one session, on wire-capable ACP backends (KAS, claude seam). kiro-cli v2 reads its template from disk and exposes no per-session channel, so it is explicitly out of scope: a member session on it runs as plain chat (tools not mounted, never mounted-and-refused).

  • Per-member backend routingagent.member_acp_backend (default kas) + a member auto-route at the single provider-construction point (config/loader.py factory closure), gated by ACP_BACKENDS_MEMBER_DISPATCH = {claude, kas} and resolve_selected_backend (same governance/selectability gate as the persisted field; denied/unknown degrades to kiro = plain chat).
  • Per-session mountmembers.member_dispatch_session_server() builds the session-level mcpServers element carrying KIROCREW_SESSION_KEY in its env (the same per-process trust channel AcpClient already uses, and the one path the KAS projection's env stripping never touches). AcpRuntime.create_session and load_session inject it — session/load re-initializes the session's MCP servers, so a resume that skipped the injection stripped a member thread of its tools mid-conversation (observed on pod).
  • KAS wire projectionto_client_custom_agent(member_dispatch=True) adds @kirocrew-dashboard to tools and merges _MEMBER_DASHBOARD_GRANTS into the allowedTools input before the governance ceiling filter. The member grant set is the conductor's read/create set plus session_send/session_stop: the write verbs are safe to auto-approve for a member specifically because authorize_target's created_by ownership fence bounds them to worker sessions the member itself opened (403 not_creator otherwise) — a fence the conductor does not have.
  • Claude seamAcpClient._append_member_dispatch_server appends the same entry to the session array, honoring the mirror's permission-surface precondition (no mount when Crew does not own settings.local.json).
  • Warm-pool bypassbypass_member pool-decision arm: a pooled child is spawned with no session key on the default backend, so a warm hit would skip both the member backend route and the mount (found by pod e2e).
  • Key spellingis_member_session_key accepts member-, dashboard_member-, and the canonical session-map alias dashboard:member- (the spelling that actually reaches the provider factory; found by pod e2e).
  • No double registration — with member_dispatch=True the dashboard server name joins the KAS projection's subtraction set, so an agent spec that already declares kirocrew-dashboard (the opt-in assignable set) is not projected alongside the session-level injection (same class as the pooled-stub subtraction).
  • Server-side authorization (relanded from feat: crew members drive worker sessions with zero configuration #8021, unchanged in substance) — member callers bypass the agent.session_control switch but are bound by created_by ownership recorded at slot birth and rehydrated on restart; not_creator drain classification registered as non-containment. Because the rehydrated created_by is an input to an authorization decision, the session-history store joins _WRITE_PROTECTED_HOME_PATHS (the rotation.yaml / alias-record precedent): transcripts stay readable, but an agent file-edit can no longer forge a victim slot's creator attribution and collect ownership after a restart.
  • Context injection — the member operating-mode block teaches the session_* spellings and is gated on _member_backend_can_dispatch(), so a degraded backend never teaches tools the session does not hold.

Relands the reusable server-side pieces from #8021 (closed by design discussion — see its closing comment) on the per-session-mount architecture that discussion converged on. Related: issue #8073 (operator ceiling for member dispatch) — partially addressed here by the backend capability gate; the members.dispatch config switch itself is in flight as PR #8083, which is additive and composes with this change. Pre-push adversarial review (GPT + Opus lanes) found no blocking issues; its one substantive fix (the double-registration case above) is included, and the two claude-lane gaps it raised are deferred with rationale to issue #8152 (claude is a non-default member backend and both degradations are graceful — an approval prompt, or an honest tools-unavailable report).

Tests

  • test/test_member_dispatch_mount.py (new, 24 tests): capability set pinned to {claude, kas}; entry shape + identity env; key spellings incl. the colon alias; KAS projection (tools append, member grants through the ceiling, * passthrough, spec non-mutation, default untouched); claude append (member / non-member / unowned permission surface / kiro backend / same-name dedup); runtime flag threading; pool bypass key recognition.
  • test/test_member_session_control.py (new): member switch bypass + created_by ownership boundary (relanded from feat: crew members drive worker sessions with zero configuration #8021, worker_* classes removed).
  • test/test_queue_drain_revalidation.py: not_creator registered as a non-containment refusal.
  • test/test_kas_agents.py, test/test_acp_runtime.py: stub signatures extended for the new keyword.
  • Full suite: 82790 passed; 19 failures all in known host-environment areas with zero overlap with this change.

Manual verification

Pod e2e (isolated kirocrew pod, KAS backend, default config): member DM completed the full dispatch loop — session_create (worker created) → session_send ("reply PONG") → session_read_message (worker replied exactly PONG) → session_stop — with zero approval stalls and zero configuration. The e2e surfaced and this PR fixes four real integration bugs (warm-pool bypass, colon key spelling, missing write-verb grants, resume losing the mount).

Screenshots / video

N/A — backend-only change; no UI surface modified.

@CrysisDeu
CrysisDeu requested a review from a team as a code owner September 3, 2026 11:31
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 9d4b244aa5b94db3ff42e93ddf17f57f9583c0ca and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 9d4b244

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 9d4b244aa5b94db3ff42e93ddf17f57f9583c0ca: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — 🟡 CONCERNS

Design-level review of 9d4b244aa5b94db3ff42e93ddf17f57f9583c0ca — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: CONCERNS

Sound per-session-mount architecture; the risk is shipping an always-on, auto-approved session-control surface before its dedicated kill switch (#8083) exists.

Watch

  • Kill-switch gap window. "an operator who turned session control off has NOT thereby disabled member dispatch... There is currently no separate switch" — until feat(members): add a members.dispatch switch and a worker/session schema parity test #8083 lands, an operator who explicitly disabled agent.session_control still has member DMs creating, driving (session_send auto-approved), and stopping sessions; the only off switch is deleting the member. Sequencing this PR after (or with) the members.dispatch switch closes the window.
  • The entire write-grant justification rests on two string conventions: the member- caller-key prefix (grants the switch bypass) and created_by key equality (bounds reach). Both are naming-convention-as-authorization; the security.py write-protection covers file-edit forgery but the design admits shell writes to the sessions store sit outside it (accepted precedent, but this is the first of the four "authorization-input" files whose forgery yields tool-call capability rather than routing). A human should confirm no path lets a non-member slot acquire a member- prefixed key.

Suggestions

  • Surface the plain-chat degradation in the member thread itself (one system line), not only a gateway log — on a default kiro-cli install the headline zero-config feature silently doesn't exist, and the log is the only witness.

[DESIGN-REVIEWED] 9d4b244

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — 🟡 CONCERNS

Premise-level review of 9d4b244aa5b94db3ff42e93ddf17f57f9583c0ca — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All greps done; I have what I need to issue the review.

First-Principles-Verdict: CONCERNS

Every capability here is derived from the zero-config member contract or a pod-observed defect — except the "explicit caller pick" backend arm, which has zero callers.

What this change ships

Intent: let a crew member's DM thread dispatch, patrol, and stop worker sessions with zero operator setup — an ADDITION.

  1. Member DMs get session_* tools mounted per session on KAS/claude — justified
  2. Member callers bypass the agent.session_control off-switch — justified (switch defaults False; zero-config is the stated contract; spec updated same commit)
  3. Members bounded to sessions they created (created_by fence, persisted + rehydrated) — justified
  4. Session-history store becomes agent-write-protected — justified (authz input, documented class precedent)
  5. New config key agent.member_acp_backend, default kas — justified (2 consumers counted; kiro-cli protocol limit is the derived cause)
  6. Member DMs silently run a different backend than the fleet default — justified, declared
  7. Member sessions skip the warm pool — justified (pod-observed defect)
  8. Member operating-mode context block, capability-gated — justified
  9. Factory acp_backend= kwarg / explicit-pick precedence arm — zero consumers
  10. Predicate accepts three member-key spellings — justified, symptom-level (see Watch)

Watch

  • Description: "explicit caller pick > member-DM auto-route > configured default" — the explicit arm exists only in tests. Grepped every provider-factory call site (factory(, extra_factory_kwargs, "acp_backend" across src/): 0 callers pass acp_backend.
  • is_member_session_key's three spellings patch one instance of ad-hoc dashboard:/dashboard_ prefix-stripping; I counted 8+ sibling sites (mcp_gateway/claim.py:63, mcp_gateway/stub.py:401, chat_slack.py:500, session_allocation.py:720, chat_runner.py:4991, token_auth.py:1954, history_search.py:726…). Normalizing keys at one boundary is the general fix — genuinely larger than this change, so accepted-and-deferred.

Subtractions

  • Drop the acp_backend: str | None = None factory kwarg (config/loader.py:4082) and the explicit arm of select_provider_backend (members.py:114) — 0 consumers; the member route + configured default is the whole live behavior.
  • The dedup-and-append mount block is spelled 3× (acp/client.py:159, runtime.py create_session and load_session — the two runtime blocks are ~18 identical lines); shrink to one helper beside member_dispatch_session_server.
  • Fold SessionAllocationService._is_member_key (1 consumer, session_allocation.py:1216) into an inline lazy import — the wrapper adds a name for nothing.

[FIRST-PRINCIPLES-REVIEWED] 9d4b244

@CrysisDeu
CrysisDeu force-pushed the feat/member-acp-backends branch from db15d6b to 700e82b Compare September 3, 2026 11:41
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 9d4b244aa5b94db3ff42e93ddf17f57f9583c0ca — this comment is updated in place on each push.

Review details

I've traced the candidate's mechanics against the actual code. Confirmed facts:

  • The candidate's evidence quotes exist verbatim (security.py comment at diff lines 1007-1012; slot._created_by = str(meta["created_by"]) in chat_persistence).
  • The bash-write path is genuinely open at the Crew gate: {prefix}/sessions is added to _WRITE_PROTECTED_HOME_PATHS (file-edit tool gate) but deliberately NOT to _WRITE_PROTECTED_BASH_LEAVES. security.py:6402-6403 confirms this is the same, explicitly-verified posture as config.json (is_sensitive_bash_command("echo x > …/config.json") is None) — an accepted, documented design decision, with keystone ceiling files reserved for full _SENSITIVE_HOME_DIRS blocking.

Falsification: the exploit's input condition (a) reduces to "the OS sandbox is not confining writes to ~/.kiro/crew" — an "if the sandbox is off" conditional the discovery pass itself could not establish and which I cannot ground to a concrete shipped configuration from code I opened. The PR consciously places created_by's file on the config.json integrity posture (file-edit gate + OS sandbox as the load-bearing layer), matching three sibling authorization-input files. The chain therefore rests on an unestablished conditional and a deliberately-matched accepted posture, not on a concrete, occurring input. It does not clear 80.

No self-originated finding survives scrutiny: the _member_caller bypass, the not_creator ownership fence (enforced even with the switch on), the dual-path rehydration, the per-session mount, and the pool bypass are each guarded coherently and covered by the new tests.

No findings.

[OPUS-REVIEWED] 9d4b244

Verdict parsed from the review's SHA-scoped output markers for commit 9d4b244aa5b94db3ff42e93ddf17f57f9583c0ca.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 9d4b244aa5b94db3ff42e93ddf17f57f9583c0ca: <one-sentence reason>

@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 3, 2026
@CrysisDeu
CrysisDeu force-pushed the feat/member-acp-backends branch from 700e82b to ebc532c Compare September 3, 2026 11:42
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author
  • Member routing violates harness parity H1/H13 span=9ec07ed2ded9 — fixed (partially adopted; the demanded full revert would remove the feature and is disproportional)

The finding's legitimate core is H3/H13's "exactly ONE selection gate; the Kiro construction path gains no second check". Fixed in ebc532c by collapsing the factory-body branching into a single call to the selection gate's per-session half (members.select_provider_backend: explicit caller pick > member-DM auto-route > configured default). The factory body now carries zero branching of its own. H1 is untouched: agent.acp_backend still defaults to kiro, a non-member session still receives exactly the configured default (passed through unchanged, not re-resolved), and the member arm goes through the same resolve_selected_backend governance gate as the persisted field, so a denied/unknown member backend degrades to kiro. Pinned by TestSelectProviderBackend (explicit-wins, member-route, default-passthrough-unresolved, denied-degrades) plus the existing test_harness_parity.py suite, all green. The full revert the finding demands would remove per-member backend routing entirely — the maintainer-approved core of this PR — while the invariant's own text requires only that the selection stay a single gate, which the fix restores.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 3, 2026
@CrysisDeu
CrysisDeu force-pushed the feat/member-acp-backends branch from ebc532c to ac500e7 Compare September 3, 2026 11:58
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author
  • Authorization trusts agent-writable transcript metadata span=1ce50744f47d — fixed (integrity fence adopted; the demanded revert of restoration + member authorization is disproportional)

The finding's core is correct: the rehydrated created_by is an input to an authorization decision, and the session-history store it is read from was agent-writable — a forged victim entry would hand a member send/read/stop after one restart. Fixed in ac500e7 by adding the session-history directory to _WRITE_PROTECTED_HOME_PATHS, the exact class this list already fences (rotation.yaml's shift schedule, the Connections alias ownership record — both "input to an authorization decision" precedents): transcripts stay READABLE (grepping one's own conversations is routine and legitimate), agent file-edit WRITES are refused, and the gateway's own persistence is unaffected because it does not route through the tool gate. Pinned by TestSessionHistoryWriteProtected (write-protected AND not read-sensitive). Like the app-source precedent, the entry is deliberately not paired into the bash-leaf matcher, which would deny routine bash reads; the file-edit gate is the enforcement point, on the same footing that list already accepts. The demanded full revert — dropping restoration and the member authorization — would strand every member-dispatched worker as not_creator after any gateway restart, removing the cross-restart patrol capability this PR exists to provide, when the invariant actually violated was the STORE's integrity, not the restoration's semantics.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 3, 2026
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/ai-review override gpt ac500e7: H13's pinned scope is the registration seam (ProviderRegistry, create_provider_factory per its own row in harness-parity.md), not per-request dispatch inside the provider/runtime, where seven shipped positive-dispatch call sites already exist; the added call is inert on kiro (getattr + set membership + string check, cannot raise, passes an empty default) and the cross-model local review assessed this exact line pair as requiring no change.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author
  • Member dispatch changes the Kiro construction path span=25513d154ea8 — rebutted (override posted for this head)

H13's own row in docs/system-specs/modules/harness-parity.md pins its scope to the REGISTRATION seam — platform/interfaces.py (ProviderRegistry) and config/loader.py (create_provider_factory) — and providers/acp.py is not in it. Per-request positive dispatch inside the provider/runtime is the shipped norm this file already carries (is_kas_backend, the seven == ACP_BACKEND_KAS call sites documented at runtime.load_session, which name exactly this scope question and resolve it the same way). The added evaluation on the kiro path is inert by construction: _member_session_key() is a getattr default + frozenset membership + string-prefix check — none can raise — and for every non-member kiro session it passes member_session_key="", the parameter's default, so kiro behavior is bit-identical. The prior H13 finding on this PR (span=9ec07ed2ded9) was fixed because it named a real violation at the pinned seam (factory-body branching); this one applies the invariant outside its pinned scope, where satisfying it would require reverting the feature rather than reshaping it.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Human judgment recorded

@CrysisDeu marked the gpt AI finding as false positive, not applicable, or explicitly accepted for ac500e7af01460b4b420dd9763a9521e03ffd843.

H13's pinned scope is the registration seam (ProviderRegistry, create_provider_factory per its own row in harness-parity.md), not per-request dispatch inside the provider/runtime, where seven shipped positive-dispatch call sites already exist; the added call is inert on kiro (getattr + set membership + string check, cannot raise, passes an empty default) and the cross-model local review assessed this exact line pair as requiring no change.

This decision applies only to this commit. A new push requires a new judgment.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

Legitimate, and already tracked: this is Gap 2 of issue #8152 (filed from this PR's own pre-push review), whose acceptance criteria include exactly the demanded coupling — inject the operating-mode block iff the session actually holds the tools. It is deferred rather than fixed here because the affected corner is doubly non-default (claude member backend AND a project-owned .claude/settings.local.json), the failure mode is graceful (the member honestly reports the tools unavailable, observed in this PR's pod e2e), and the correct fix carries the mount OUTCOME into context construction — a seam change wider than this PR's KAS-default scope. Excluding claude from the gate wholesale, as suggested, would instead break the OWNED-surface claude case where the mount does happen.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 3, 2026
NicholasRBowers pushed a commit that referenced this pull request Sep 3, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
NicholasRBowers pushed a commit that referenced this pull request Sep 4, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
NicholasRBowers pushed a commit that referenced this pull request Sep 4, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN: a first-turn
  MemberRulesUnreadable abort leaves a warm session (the provider
  client outlives the failed context build), and warm turns skipped
  both member-section paths — running a bounded member with no bounds.
  build_message now validates rules on every warm member turn, and the
  dashboard runner re-arms the reinjection flag when a member DM's
  first turn dies before landing, so the section (fresh rules included)
  is re-delivered on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
NicholasRBowers pushed a commit that referenced this pull request Sep 4, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
NicholasRBowers pushed a commit that referenced this pull request Sep 4, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
NicholasRBowers pushed a commit that referenced this pull request Sep 4, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
NicholasRBowers pushed a commit that referenced this pull request Sep 4, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
NicholasRBowers pushed a commit that referenced this pull request Sep 4, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
@bolichen97

Copy link
Copy Markdown
Collaborator

Open PR relationship audit

This is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion.

Relationship findings

  • PR #6237 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #6237: REBASE. A second per-verb-name grant list now depends on the names this PR retires, and it rewrote a doc the PR also rewrites. Files: src/kiro_crew/agent.py.

No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit.

CrysisDeu added a commit that referenced this pull request Sep 4, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
CrysisDeu added a commit that referenced this pull request Sep 4, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
CrysisDeu added a commit that referenced this pull request Sep 4, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
CrysisDeu added a commit that referenced this pull request Sep 4, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
CrysisDeu added a commit that referenced this pull request Sep 4, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
CrysisDeu added a commit that referenced this pull request Sep 4, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
CrysisDeu added a commit that referenced this pull request Sep 4, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
CrysisDeu added a commit that referenced this pull request Sep 4, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
CrysisDeu added a commit that referenced this pull request Sep 5, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
CrysisDeu added a commit that referenced this pull request Sep 5, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
CrysisDeu added a commit that referenced this pull request Sep 5, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
CrysisDeu added a commit that referenced this pull request Sep 5, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
CrysisDeu added a commit that referenced this pull request Sep 5, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
CrysisDeu added a commit that referenced this pull request Sep 5, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
CrysisDeu added a commit that referenced this pull request Sep 5, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
CrysisDeu added a commit that referenced this pull request Sep 5, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
chenmingwei23 pushed a commit that referenced this pull request Sep 5, 2026
A member DM session previously differed from an ordinary session only by
the [CURRENT AGENT] label. Member turns now inject a [MEMBER IDENTITY]
block with four layers of distinct ownership:

1. identity — derived from the crew record (name, description,
   triggers); auto-generated so an empty description still has a floor
2. behavior — product-owned working protocol: worker-not-Q&A-bot,
   front-desk-vs-workshop dispatch, a four-rung stuck ladder that
   escalates only at permission / reachability / one-way-door walls,
   zero-context escalation drafts validated by a context-free subagent,
   and quiet-run reporting
3. permanent rules — user-owned, stored at trust/member-rules/<slug>.json
   under the keystone-gated trust/ subtree so the member cannot rewrite
   its own safety boundary; GET/PUT /api/members/{slug}/rules is the
   only write path (human dashboard action, app tokens denied)
4. briefing — member-owned working memory at members/<slug>/briefing.md,
   agent-writable by design, injection-capped with a visible marker

Precedence is the injection order. The section builder degrades to an
ordinary crew session on any failure EXCEPT an unreadable rules file:
for the one safety-relevant layer that degrade is the fail-open, so
MemberRulesUnreadable propagates and aborts the turn until the user
repairs or clears the file.

Review follow-ups (post-rebase, addressing bolichen97's review; feature
authored by CrysisDeu):

- read_member_briefing now opens through the central
  pinned_fs.open_in_pinned_parent helper, pinning the ancestor chain
  (members/<slug>/ is agent-writable too — a swapped parent redirected
  the whole traversal past the leaf-only O_NOFOLLOW; same shape as
  #2446). Fails closed where the pinned walk is unavailable.
- member_briefing_supported() names the layer-4 availability; on
  platforms where briefing reads fail closed (Windows) the section now
  says "not available on this platform" and item 6 of [HOW YOU WORK] is
  softened instead of instructing upkeep of a never-injected file.
- Rebased onto main over the crew-member operating-mode block (#8153):
  both blocks render — dispatch mechanics first, then the four layers.
- Scrub coverage test now carries a fullwidth NFKC forgery per minted
  header, and a regression test pins the symlinked-parent refusal
  red-before-green.
- The layer-3 fail-closed contract now holds PER-TURN across the whole
  session lifecycle (branch table in build_message): a first-turn
  MemberRulesUnreadable abort leaves a warm session whose turns skipped
  both member-section paths, and an idle-expired session restored via
  session/load carried a STALE [PERMANENT RULES] snapshot. build_message
  now validates rules on every warm member turn, re-injects the CURRENT
  member section on slim resume (rules gate included), and the dashboard
  runner re-arms the reinjection flag when a member DM's first turn dies
  before landing, so the section (fresh rules included) is re-delivered
  on the first turn after the user repairs the file.
- Feature-map row for Crew Members now names the GET/PUT
  /api/members/{slug}/rules endpoints.
- The PUT collision scan uses the central _member_names_for_slug helper
  (grammar filter + MemberSlugError tolerance) instead of an ad-hoc
  comprehension a hand-edited config key could crash.
- Rules saves and clears are crash-durable: atomic_write's file fsync
  covers the data, but the publishing rename, the first-save directory
  creation, and the clearing unlink are directory entries — fsync_dir
  (raising on save, best_effort on the already-committed clear) makes
  the PUT's 200 survive a power-off instead of resurrecting cleared
  rules or losing saved ones.
- The rules GET is owner-gated like the PUT: rules are the owner's
  private safety instructions, and any allowed Slack user can mint a
  dashboard session, so the read would otherwise disclose them.
- Escaped lone surrogates in the rules payload are refused with a coded
  400 (rules_not_encodable) at the PUT boundary, and write_member_rules
  rejects UTF-8-unencodable text before any state changes — previously
  the UnicodeEncodeError from the write escaped as a mislabeled
  "too long" error, and escaping surrogates into the file would only
  defer the crash to prompt-encode time inside the member's turn.

- The feature-map Crew Members row lists the rules API as a combined
  `GET,PUT /api/members/{slug}/rules` entry, keeping the row within the
  feature map's four-route limit.

- The member first-turn re-arm lives in the runner's finally (the one
  block on every exit path), not the except arm alone: a user Stop's
  cancelled completion never raises, so the except-arm-only re-arm left
  the warm session running without [PERMANENT RULES] after a stopped
  first turn.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants