Skip to content

The PM of one repo commissions the PM of another, and the answer comes back (nxf 6j6v.70dy, first slice) - #14

Merged
cabcookie merged 8 commits into
mainfrom
feat/70dy-pm-talks-to-pm
Oct 4, 2026
Merged

cabcookie merged 8 commits into
mainfrom
feat/70dy-pm-talks-to-pm

Conversation

@cabcookie

Copy link
Copy Markdown
Member

What this does

On one machine, a persona of one workspace commissions a persona of another workspace and gets the answer back. Both repos stay what they are: the receiver runs its persona in its own working copy, with its own board and memory, and only the border thread crosses.

alpha/pm:  nxc send --to test/beta/pm "When does the access rule ship?"
           → recorded in alpha's own log, handed over
beta:      access checked (trusted? role listed in addressable.external?) → beta's pm starts
beta/pm:   nxc reply "With version 0.300."
           → handed back, alpha's pm is woken and passes it on to its owner

Tickets: 6j6v.ewbj (first, it is the precondition), 6j6v.q32p, 6j6v.t5xb, 6j6v.4gp2, 6j6v.szc5. Epic 6j6v.70dy. Specification and direction: the coordinator workspace's 2026-10-03-schnitt-1-pm-spricht-mit-pm-design.md and …-standards-und-foederation-richtung.md.

The pieces

ewbj A session that owes a reply was granted the bare, auto-approved Bash, so tools: [] meant a full shell. It is now Bash(nxc reply:*) for any declared list without Bash. tools absent and tools: [Bash] behave as before. Measured against the pinned SDK 0.3.215: the heredoc and one-line reply forms run; nxc send, writes, gh, curl, chains, pipes, redirections and $(…) are refused. A live smoke shows a tools: [] persona answering and failing to touch a file, and it goes red when the rule is widened.
q32p Stored workspace name <owner>/<repo> ([workspace] name in .nxs/config.toml), derived from the git origin without the host on first use. nxs name [<owner>/<repo>] prints or sets it. normalize_remote moved to nxs_foundation::workspace_name, so the stream id and the name are one parse. <name>/<persona> is an address.
t5xb addressable: { external: ["*/pm", "nxsflow/manufakt-io/pm"] } is a new variant Addressable::OnlyWithExternal, added as a variant rather than a field so Only { .. } matches in embedding code don't break. nxs sync trust add --workspace <name> trusts a neighbour's replica key by name. Refusals name their reason: workspace unknown, not on this machine, not trusted, role not admitted, no such persona, depth limit reached.
4gp2 crate::border. The hidden verb nxc handover (Engine::handover) copies only the border thread's ops between the two logs via Store::apply: signatures are verified, and op ids make it idempotent, so there is no echo. Each run acts only in its own workspace. send/reply on a border thread run it at once and then the peer's handover as a child process in the peer's root. The service runs it every 5 s where .nxs/border-open is present. Depth travels in the signed refs.border. Two hours without a sign of life cancel the thread. nxc withdraw takes it back on both sides.
szc5 StatusThread.border {peer, direction, state, reason} with the four A2A states, in both workspaces and on Engine::status. The CLI shows — across the border to test/beta: completed.

Decisions the specification does not make (also noted on the epic)

  1. Trust is needed on both sides. The answer arrives as an op signed by the receiver's key, and an untrusted op discharges nothing (the existing pzkb rule). So send refuses up front, with the command to run, when the caller's workspace does not trust the receiver.
  2. A person cannot commission from outside. Without a session there is no role to stamp, and the slice excludes it.
  3. addressable stays strict (deny_unknown_fields). An older engine fails on external:. This is stated in the personas guide (EN/DE) and in the changelog. No declaration in this repo carries external:.
  4. Withdrawing an operation cancels its border threads and wakes nobody. The deadline wakes the commissioner with the reason.
  5. In the receiver, the border thread is the root. Its foreign parent is read as no parent (PARENT_WITHIN_THE_BORDER), so the operation does not hang under a phantom. A question asked across the border is not shown as NEEDS DECISION there.

Existing defects this path ran into, fixed here

  • Spawned sessions did not inherit NXS_SERVICE_INSTANCE, so a dev build's persona read the production service home.
  • A wake on the way back (ChainMove::Unwind) never carried the obligation the woken session still has. A persona without tools: was woken with an answer and then refused its own nxc reply. This applies in-house as much as across the border.
  • The CLI timer did not forward schedule_delivery/schedule_resume. The trait default booked a held delivery as a channel tick on a session id ("no such thread"), so on the command line the service never delivered a held answer.
  • The sidecar reminded a commissioner whose sub-round answer was already held for it, and then substituted its reply. A held answer now keeps the sub-round in flight (waiting_on_sub_round).

Evidence

Live, two real git repos (test/alpha, test/beta), real sidecar and model, a dev-instance service (2026-10-03):

Run Result
Scenario, with service alpha's pm asked beta's pm and told its owner "…ships with version 0.300"
Scenario, without service Same result. The answer was delivered by the immediate handover in send/reply and needed no service.
Coder "Refused by test/beta/pm: role not admitted." Nothing started in beta.
Question beta's pm asked back with --escalate. It woke alpha's pm, which escalated to its owner. Nothing in beta was woken.
Depth Started at NXC_HOP=28: alpha 29 → beta 30 → alpha 31 → beta 32, then "hop 33 exceeds the cap of 32", and the answers unwound back to the owner.
ewbj A tools: [] persona answered and could not touch.

Deterministic:

  • crates/chat/tests/a_persona_commissions_across_the_border.rs: 16 tests at the engine seam between two real workspaces. They cover every acceptance point of the specification's section 9 except 7 and 9, plus no echo, the deadline, withdrawal, and a held answer.
  • crates/nxs/tests/two_workspaces_on_one_machine.rs: the real binary. It covers nxs name, trust by name, the peer's handover as a child process, and status in both directories.
  • Lib tests: the address parse, the persona_chat guard, the held-answer rule, and the CLI timer's forwarding.
  • Mutation-checked: removing each of the guard, the role check, the trust check, the op filter, the timer forward, the wake obligation, the held-answer wiring, the deadline, the NEEDS-DECISION exclusion and the border parent turns its test red.

Local gates run:

  • cargo fmt --check.
  • Lean cargo check for chat and nxs.
  • The node sidecar suite.
  • The changed and directly related test files: 26 chat test binaries plus the lib tests, and the new nxs test.
  • Clippy and the full suite are CI's, per project rule.

Not in this PR

  • 6j6v.7k58 (one definition for every repo; acceptance point 7) and the manufakt.io desktop app (point 9).
  • No fallback when the service is not running. An answer held for a caller that is still in its turn is delivered only by the service, in-house as well as across the border. That is 6j6v.v0pj, which asks the owner whether a fallback is wanted. With the service it works; it is measured above.

🤖 Generated with Claude Code

https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT

cabcookie and others added 8 commits October 3, 2026 21:08
…`, not the shell (nxf 6j6v.ewbj)

A trigger that carries a reply obligation granted the bare `Bash`, which the sidecar
auto-approves, so a persona declared with `tools: []` held an unscoped shell whenever it
owed an answer. The grant is now decided from the declaration:

- `tools` absent: the bare `Bash`, as before;
- a list naming `Bash` or `Bash(nxc reply:*)`: nothing more;
- any other list, `[]` included: `Bash(nxc reply:*)`.

The sidecar puts the tool a granted or declared rule names into the SDK's base `tools`
list and the rule itself into `allowedTools`. Measured against the pinned SDK 0.3.215:
the heredoc and one-line reply forms run; `nxc send`, writes, `gh`, `curl`, chains,
pipes, redirections and `$(...)` are refused. A live smoke shows a `tools: []` persona
answering and failing to touch a file; it goes red when the rule is widened again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT
…ine (nxf 6j6v.q32p, 6j6v.4gp2)

The thinnest path through every layer, proved between two real repositories with the
real sidecar and the service running: the PM of one repo asks the PM of another and
passes the answer on to its owner.

- A workspace has a stored name (`[workspace] name` in config.toml, derived from the git
  origin on first use, `nxs name`); `<owner>/<repo>/<persona>` is an address.
- `nxc send --to <address>` records the commission in the caller's own log and hands
  the border thread over; `nxc handover` (hidden, run by `send`/`reply` and by the
  service on its pass) copies only that thread's ops both ways and acts in its own
  workspace: the receiver admits and starts its persona, the caller is woken.
- `addressable.external` admits callers from other workspaces by workspace and role;
  `nxs sync trust add --workspace` trusts a neighbour by name.

Three existing defects on that path, fixed here because the scenario cannot pass with
them: spawned sessions did not inherit NXS_SERVICE_INSTANCE; a wake on the way back
granted no `nxc reply` to a persona without `tools:`; the CLI timer booked a held
delivery as a channel tick on a session id, so the service never delivered it. And a
sub-round whose answer is held for a busy commissioner now still counts as in flight,
so its sidecar no longer reminds it into a substituted reply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT
…ides (nxf 6j6v.t5xb, 6j6v.4gp2, 6j6v.szc5)

- A border thread follows the four A2A states in both workspaces, derived from its
  newest message, and `nxc status` / `Engine::status` show it with the other side's
  name on the row; in the receiver the border thread is the root of its operation.
- Two hours without a sign of life (a message on the thread, or the receiver's
  persona transcript) cancel a working border thread: the caller is woken with the
  reason, the receiver stops its persona.
- Withdrawing an operation takes its border threads back in the other workspace too,
  and an open border thread counts as work to withdraw.
- Tests at the engine seam between two real workspaces: the scenario, the coder's
  "role not admitted", "not trusted" on either side, a foreign pm against
  `personas: [pm]`, a question routed to the commissioner, the depth cap across both
  workspaces, only the border thread crossing, no echo, the deadline and withdrawal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT
…(nxf 6j6v.70dy)

- Personas guide (EN/DE): "From another workspace" — the address, `external`, trust on
  both sides, the refusal reasons, the states, the deadline, withdrawal, and the roll-out
  caveat for engines older than `external`. limits-and-safety says what outside text
  means for a persona's declaration.
- E5c section 1 and 6: the owner lifted the cross-repo clause on 2026-10-03.
- Changelog: the slice (facade: breaking — new pub fields on constructible structs) and
  the held-delivery fixes.
- A withdrawal cancels border threads without waking anybody; the deadline wakes the
  commissioner through a call of its own, so the write-surface gate sees what each can
  start. A question asked across the border is not NEEDS DECISION in the receiving
  workspace. Border tables are declared machine-local.
- Tests: the nxs binary with two real git repositories (name, trust by name, the
  handover as a child process, status on both sides); persona_chat's border guard; the
  held-answer rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT
…ins (nxf 6j6v.4gp2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT
…ests (nxf 6j6v.70dy)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT
@cabcookie
cabcookie merged commit 4c17dc4 into main Oct 4, 2026
13 checks passed
cabcookie added a commit that referenced this pull request Oct 4, 2026
…s (nxf 6j6v.7k58, 6j6v.dcpd) (#17)

* One persona definition for every repository: a user-level declaration 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

* Border follow-ups from the review of PR #14: revoked trust is named, 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

* Pin the in-process home through pinned_home_env, and mark the dcpd fragment'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

* Mitigate the review of PR #17: hermetic user-level folder, channel-only 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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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