Skip to content

One persona definition for every repository, and the border follow-ups (nxf 6j6v.7k58, 6j6v.dcpd) - #17

Merged
cabcookie merged 4 commits into
mainfrom
feat/7k58-user-level-personas
Oct 4, 2026
Merged

cabcookie merged 4 commits into
mainfrom
feat/7k58-user-level-personas

Conversation

@cabcookie

@cabcookie cabcookie commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

What

6j6v.7k58: one definition, many repositories. Every workspace now reads a second declaration folder besides its own .nxs-personas/: the user-level folder <service home>/personas. The two are merged per name. A repository's own persona or channel hides the user-level one of the same name, and everything else is added. This delivers acceptance point 7 of the slice spec (§6 behaviour).

6j6v.dcpd: three follow-ups from the review of PR #14.

  1. Revoked trust. Trust revoked after a commission now cancels the border thread right away, with the reason "no longer trusted here". It no longer times out two hours later as "no sign of life".
  2. Cooperative depth cap. The depth cap across the border rests on the trusted peer's word. This is now documented next to external and the depth guard, in EN and DE.
  3. Stuck handover. A peer handover that runs past 60 s is killed and reaped instead of being left running.

The four open questions from the brief, answered against the code

  • Does the embedding seam read the same folder? Yes. Engine::definitions, Engine::directory, Engine::prime_as and Engine::persona_prime go through Definitions::resolve, which merges the two folders.
    • An app cannot choose another location, and that is deliberate. The personas an app starts run nxc, and nxc resolves the folder by the same rule. Two folders would split the app from its own sessions.
    • Definitions::resolve_with_user_dir exists for tests and for callers that have already resolved the folder.
  • Files beside a declaration. No path inside a declaration is resolved by the engine.
    • A prompt that says "relative to the root of this workspace" means the repository the session runs in, which is its working directory. A hurdle runs there too. That breaks the knowledge/ references in agents once a persona moves to the central folder.
    • A user-level persona is now told at session start where its declaration lives (Declared in: in the persona brief, PersonaBrief::declared_in).
    • The guide states the rule.
  • The freeze. It projects the merged catalogue, so it applies unchanged to user-level prompts and hurdles. a_user_level_declaration_is_frozen_per_operation proves it: an edit made mid-operation reaches the next operation, not the running one.
  • The service instance name. The folder is ServiceHome::personas() of the instance. The installed suite reads ~/.nexusflow/personas; a build reads one only under a named development instance (~/.nexusflow-x/personas) and never the production folder — enforced, not left to .envrc (after review: CI and a shell without direnv read none; guard test in one_definition_at_the_engine_seam).

Behaviour details

  • Duplicates are judged per folder, and the error names the folder. Handle form and flow cycles are judged on the merged team: a repository channel can break a user-level cycle.
  • A malformed user-level file fails the catalogue with its path, the same fail-closed rule as for a repository file.
  • No user-level declaration (no folder, or one holding only a README) gives a resolution identical to before, field for field (one wording change: a duplicate-name error now names the folder it is in). That keeps acceptance point 8.
  • Wire shape. DeclarationSource.user_path/from_user/shadowed and PersonaEntry/ChannelEntry.origin are left off the wire in that case.
  • prime partition. The whole-file channel rule now spans the merged channels.

Proof

  • Real binary (two_workspaces_on_one_machine): two repos with no pm file both run the user-level pm, and the border scenario runs on it. nxc list / --json name the source. A repo file of the same name hides it, and nxc list and nxs prime report that.
  • Live, real Claude sessions (--ignored, run locally, 22 s, green): one user-level pm definition for both repos. Beta's knowledge sits in BOARD.md in beta's repo, and the answer "0.300" reaches alpha's owner.
  • Manual run with the built binary.
    **Product manager** (handle: `pm`, from the user-level folder)
    ... this repository declares nobody of its own — ... From the user-level folder ~/.nexusflow-centraldemo/personas: persona `pm`, channel `planning` — not versioned with this repository.
    # beta with its own pm.yaml:
    ... This repository's own declaration HIDES the user-level persona `pm`.
    
  • Library tests:
    • one_definition_many_repositories (8 tests);
    • one_definition_at_the_engine_seam (the Engine seam, $HOME pinned, its own binary);
    • the freeze test;
    • an_answer_from_a_workspace_no_longer_trusted_cancels_it_with_that_reason, which was red before the fix;
    • the wait_or_stop unit tests, including reaping.
  • Local runs: the full cargo test -p nexus-chat passed (116 test binaries), as did the guide tests.

After the review (2ec45af)

All findings resolved or declined with reason — see the mitigation comment. Integrity #1 (may a repository hide a gated user-level declaration) is an owner decision: 6j6v.3mgy.

For the reviewer

  • CI is the gate for clippy and the full workspace suite. Neither ran locally, per project rule. Locally I ran only: cargo fmt --check, nexus-chat in full, nxs-guide, the touched nxs test file, and the nxs peers unit tests. Nothing beyond what CI checks needs re-running locally.
  • Facade breaking: pub fields were added to DeclarationSource, PersonaEntry, ChannelEntry and PersonaBrief. The fragment says so.
  • Not done here (owner steps / out of scope):
    • moving real declarations into the central folder;
    • external: in any app-read repo;
    • SKILL.md, 6j6v.1z1j, 6j6v.xjh3, 6j6v.v0pj, 6j6v.vvw6.

🤖 Generated with Claude Code

https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT

cabcookie and others added 4 commits October 4, 2026 18:37
… folder, merged per name (nxf 6j6v.7k58)

Beside a workspace's own `.nxs-personas/`, every workspace reads the user-level folder
`<service home>/personas` — `~/.nexusflow/personas` for the installed suite and an embedding
app, `~/.nexusflow-<name>/personas` for a development build. A persona or channel the
repository declares hides the user-level one of the same name; everything else is added.

- The merge lives in `Definitions::resolve` alone, so the CLI, every `Engine` verb, `nxs prime`
  and a spawned persona's own `nxc` see one team. Duplicates are judged per folder (naming it);
  handle form and flow cycles on the merged team.
- `DeclarationSource` carries `user_path`, `from_user` and `shadowed`, all off the wire when
  there is no user-level declaration, so such a machine reads exactly as before (point 8).
- `nxc list` / `Engine::directory` stamp `origin: user` per entry and print the account
  whenever the user-level folder takes part; `nxs prime` says it under "Declarations".
- A user-level persona's session start says where its declaration lives: a relative path in a
  prompt means the repository the session runs in, never the declaration's folder.
- The freeze projects the merged catalogue, so it holds for user-level hurdles and prompts;
  proved by an edit mid-operation.
- The readers that loaded the folder directly (thread/search channel policy, list, prime) go
  through the merged catalogue now.

Proved through the real binary: two repositories without a pm file both run the user-level
pm, the commission scenario runs on it, a repository file hides it out loud; the live test
with real sessions now uses one user-level pm for both repositories (22 s, green).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT
…a stuck handover is stopped, the cooperative depth is written down (nxf 6j6v.dcpd)

1. When the caller's workspace stops trusting the receiver while a border thread is open, the
   receiver's newest message is unvouched and steers nothing. The caller's coordinator now
   cancels at once with "the answer arrived from <peer>, a workspace no longer trusted here"
   and wakes the commissioner, instead of "no sign of life" two hours later.
2. The depth across the border rests on the trusted coordinator's stamp — within the trust
   model (spec section 4); said where `external` and the depth guard are documented.
3. A peer handover past HANDOVER_WAIT is killed and reaped, so the long-lived service no
   longer accumulates stuck children.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT
…agment's facade impact

The two single-test binaries set HOME by hand, which the source gate
every_pinned_home_pins_the_instance refuses: a pinned home without the instance resolves
another directory under this repo's .envrc. They now apply nxs_test_support::pinned_home_env()
to their own process. The dcpd fragment says `facade: changed` (a behaviour change behind
unchanged signatures: an unvouched answer now cancels the border thread).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT
…ly policy reads, named revoked-trust cause, group stop

- A build reads a user-level folder only under a NAMED development instance, never the
  production one, and an unresolvable home or instance means no folder rather than an error
  (Code #2, #3; Integrity #3, #4). Guard test: a build under the production instance does not
  see personas planted in the production folder.
- An existing but unreadable user-level folder is an error, not a silence (Integrity #5).
- `threads show`, `search`, `Engine::thread` and `Engine::search` read the merged channels
  only (`merged_channels`), so a malformed persona file cannot make a thread unreadable (Code #1).
- `nxc prime`, `Engine::prime_as` and `nxs prime --persona` resolve the catalogue once
  (`prime_with`) (Code #5).
- The revoked-trust cancel fires only on an answer not yet served, so a question delivered
  before the revocation does not cancel; two negative controls, mutation-checked (Test #1,
  Integrity #7).
- A stuck peer handover runs in its own process group, which is killed as a whole and then
  reaped by polling; the sidecar it started has a group of its own and survives; start-idempotence
  rests on the admission claim (Code #7, Integrity #6). Test drops the wall-clock assertion and
  gates `true` on unix (Test #3, #4).
- The freeze test now edits a user-level hurdle too (Code #6); loader failing cases for
  channels, a missing and an unreadable folder, a dangling member (Test #2).
- A user-level `external` admits only from workspaces the running repository trusts — tested
  through the binary and written in the guide (Integrity #2); the guide no longer calls a hiding
  "never silent" but says where it is shown (Integrity #1, the lock question filed as 6j6v.3mgy).
- Stale prose and wraps (Code #4, #9).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT
@cabcookie

Copy link
Copy Markdown
Member Author

Mitigation of the review (head 2ec45af)

Finding Disposition Notes
Integrity #1: a repo can hide gated user-level declarations ⏭️ Declined → owner decision 6j6v.3mgy Repo-wins is the contract of spec §6. A lock, refusal or more disclosure changes it. Guide now says exactly where a hiding is shown, and no longer says "never silent".
Integrity #2: user-level external opens every repo ✅ Documented + tested Admission needs the receiving repo's own trust by name. Test a_user_level_external_admits_nobody_into_a_repository_that_trusts_nobody (real binary). Guide paragraph in EN/DE.
Code #2 / Integrity #3: hermeticity rests on .envrc ✅ Fixed A build reads a user-level folder only under a named dev instance, never the production one. Guard test plants a persona in the production folder and asserts that a build does not see it.
Code #3 / Integrity #4: the environment fails the catalogue ✅ Fixed user_declarations_dir() -> Option. No home or a bad instance means no folder.
Code #1: thread/search fail on any persona file ✅ Fixed New merged_channels, which reads channels only. Test with a malformed user persona file.
Integrity #5: an unreadable folder is silent ✅ Fixed It is now an io error naming the folder. Unix test.
Test #1 / Integrity #7: no negative control; a delivered message is cancelled on ✅ Fixed Only an answer not yet served cancels. Two controls added: revoked before any answer, and a question delivered before the revocation. The second is mutation-checked (red without the served check).
Test #2: loader failing cases ✅ Fixed Malformed channels.yaml, duplicate channel, dangling member (still advisory), missing folder, unreadable folder.
Code #6: hurdle freeze untested ✅ Fixed The freeze test now also edits a user-level preconditions: hurdle mid-operation.
Integrity #6 / Code #7: grandchildren, ignored kill, idempotence ✅ Fixed The child gets its own process group, which is killed as a whole and then reaped by polling. The persona sidecar has its own group (worker.rs) and survives. A restart is idempotent through the admission claim (UPDATE … WHERE state='submitted'). Grandchild test added. Still true: a pass spends 60 s on a stuck peer.
Code #5: multiple resolutions ✅ Fixed prime_with. nxc prime, Engine::prime_as and nxs prime --persona each resolve once.
Code #4: dead validate_declared_team, stale prose ✅ Partly Kept, because it is public and pinned by tests/prime_validation.rs. Its doc now says it covers one folder. The stale references are fixed.
Code #8: "sentence for sentence" ✅ Claim corrected in the PR body The duplicate error now names its folder.
Code #9: nits ✅ Fixed Wrap, the escaped apostrophe, the long doc lines.
Test #3 / #4 ✅ Fixed The wall-clock assertion is dropped, and the true test is gated with #[cfg(unix)].
Integrity #8: ping-pong within the trust model ⏭️ Declined Covered by the documented cooperative cap (spec §4: trusting a workspace means trusting its coordinator). The depth stamp is the only bound, and 6j6v.dcpd item 2 asked for documentation of it.
Integrity #9 (Info) — No action.

Local runs: cargo test -p nexus-chat --no-fail-fast passes in full, as do the nxs lib tests (322), two_workspaces_on_one_machine (5), and the live test with real sessions again (22 s). fmt is clean. Clippy and the full workspace run are left to CI.

@cabcookie
cabcookie merged commit 44f80f4 into main Oct 4, 2026
12 checks passed
@cabcookie cabcookie mentioned this pull request Oct 4, 2026
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.

1 participant