Skip to content

FE-1497: Build the controller read path and the SDCPN plugin package - #9325

Open
lunelson wants to merge 3 commits into
mainfrom
ln/fe-1497-controller-read-path
Open

FE-1497: Build the controller read path and the SDCPN plugin package#9325
lunelson wants to merge 3 commits into
mainfrom
ln/fe-1497-controller-read-path

Conversation

@lunelson

@lunelson lunelson commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

🌟 What is the purpose of this PR?

Brunch is an elicitation harness: an AI interviewer that draws a model of a process out of a human expert and projects it into a Petrinaut net. Until this branch the harness could store what the expert said but could not answer the question the interview turns on — is the model complete yet? — and the SDCPN plugin existed only as a specification document the interviewer was told about. This pull request builds the two epicentres that S-007 put first: the controller read path (FE-1497), which folds stored captures into an elicited model and evaluates it against the plugin file's demands, and the SDCPN plugin as code (FE-1482), a workspace package that loads plugin.md at module load and declares the one proposal type a kind-and-slot plugin needs. The dev app mounts an SDCPN elicitor alongside the Gherkin one, so FE-1404's condition-3 run has an agent to talk to once this stack merges.

Draft: the code and gates are complete; it is stacked above #9322 and inherits its merge order.

🔗 Related links

🚫 Blocked by

  • FE-1404 / #9322 (stack parent), which carries the plugin file's first version, ADR-0006, and the spec consolidation this code implements.

🔍 What does this change?

Read path — five new modules in packages/core/src, all exported from @hashintel/brunch-agent:

  • plugin-file.ts parses an ADR-0006 plugin file: six fixed headings in order, the version, the three machine-read tables (Kinds; Must know rows with typed precision and the "not applicable" flag; Patterns indexed by the kinds their trigger names), and the static floor as per-kind counts. A contract break throws PluginFileError at load.
  • slot-assertion.ts is the one proposal payload a kind-and-slot plugin needs — {kind, node, slot, precision?, assertion: value | absence} — narrowed by createSlotAssertionSchema(file) to the kinds and (kind, slot) pairs the file names.
  • elicited-model.ts folds active captures into nodes keyed kind:node, each slot in one of four states (value, absence, conflict, divergence). Unreadable payloads go to unmapped; nothing is interpreted. A prescribed/practiced pair that differs is a divergence; anything else that differs is a conflict; an open conflicting issue pins a slot in conflict even when the readings agree.
  • completion.ts is evaluateCompletion(model, demands): pure, no persisted status; refuses a foreign plugin version; checks the floor, then the objective's dependency slice, then every row inside the slice in the spec's diagnostic order. Nodes outside every objective's slice are reported, not demanded. precisionSatisfies encodes the ladder (named < number < range < spread; spelled out satisfies itself and named).
  • cue.ts turns a report into a sweep list (failing slots plus the patterns indexed on their kinds) and a completion-cue signal that states the verdict and never decides whether to continue.

SDK and bindingdefinePlugin gains file?: PluginFile; targetDomain is renamed targetFormalism to match ADR-0006. The Flue binding threads the file through: slot-assertion guidance in the extraction prompt, the completion cue in the brunch_sweep tool result, the file's prose appended to the instructions. Harness facts still reach the model only through tool results and signals.

Plugin packagepackages/plugin-sdcpn: plugin.md (moved from docs/specs/sdcpn-plugin.md with git mv) is imported ?raw, parsed at module load, and declared as the single slot-asserted proposal type. Dependencies are exactly @hashintel/brunch-agent and valibot. Named in kernel §12.2 so the topology gate admits it; the fourteen documents that linked the spec are repointed; the context README lists the package.

Dev appsrc/agents/sdcpn-elicitor.ts is a thin 'use agent' module mounted at /agents/sdcpn, shaped like the Gherkin one. Routes are a record, the session factory takes the target so each agent's history reader speaks to its own mount, and the browser UI selects the agent with ?target=sdcpn (Gherkin stays the default). Petrinaut's chat transport is unchanged.

🏗️ Agent notes

Stacked above #9322 (ln/fe-1404-run-condition-3). One branch, two issues, per the branch-to-issue rule stated on the parent branch: FE-1497 and FE-1482 are built together because the plugin package is the read path's first consumer and the read path is what makes the plugin file more than prose.

Commits

  • 9878a182d0 Build the controller read path and the SDCPN plugin package
  • 01d088723e Mount the SDCPN elicitor in the dev app

Interface decisions to look at

  • Slot absences ride inside content.value. The capture envelope's own {absence} variant has no address (no kind, node, or slot), so it cannot carry a per-slot absence. A slot absence is assertion: {absence, pointer?} inside the assertion payload. The envelope variant stays unusable for slots; that is a finding for the capture-store spec, not fixed here.
  • The floor is counts only. The "order spelled out" half of the SDCPN floor sentence is carried by the ordering/flow Must-know row, not the floor parser.
  • "Inferred and confirmed" is an explicit superseding capture. completionDemands(file, { acceptedStatuses }) defaults to ["explicit"]; a confirmation is a new explicit capture that supersedes the inferred one.
  • The anchor kind is objective by convention (ANCHOR_KIND); its at least N row is the dependency slice. The fold does not read the anchor from the file.
  • Spec invariants 17–19 (deferral licensing) are a session-control computation over the report and are not implemented by evaluateCompletion; the test file records this.

Gates

Verified at the tip: core 232/232 (topology, docs-index, control-surfaces, Flue-placement gates included); binding-flue 16/16; plugin-gherkin 2/2; plugin-sdcpn 4/4; dev app build + 32/32 (the bundle check now reads every agent module and confirms both identities are bound); tsgo --noEmit clean in every package; Oxlint 0 errors (inherited warnings only); oxfmt clean; lefthook pre-commit passed on both commits.

Dependents resolve @hashintel/brunch-agent through dist/, so a stale build fails their tests with the old field name even when the type-check passes. turbo run test:unit orders the builds; running vitest directly does not.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing

📜 Does this require a change to the docs?

The changes in this PR:

  • require changes to docs which are made as part of this PR

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • affected the execution graph, and the turbo.json's have been updated to reflect this
    • a new workspace package, @hashintel/brunch-agent-plugin-sdcpn, with its own turbo.json (copied from the Gherkin plugin), and a new dependency edge from @apps/brunch-agent to it.

⚠️ Known issues

  • The completion cue is delivered as part of the brunch_sweep tool result, so the model sees it only after a sweep. No harness-side settlement or stop decision consumes the report yet; that is the session-control work of invariants 17–19.
  • The envelope-level {absence} variant in the capture store has no slot address and is not used by the fold (see agent notes).
  • apps/brunch-agent has no faux-provider integration test for the SDCPN agent yet; registration and mounting are covered, an end-to-end SDCPN conversation is not.

🐾 Next steps

  • Run FE-1404's condition 3 against /agents/sdcpn from a branch above this one.
  • A projection (project/validate, ADR-0005) for the plugin package, once the read path has been exercised by a real run.
  • Decide where the report's deferral licensing (invariants 17–19) lives.

🛡 What tests cover this?

  • packages/core/test/plugin-file.test.ts — the synthetic fixture file, ten contract-violation cases, and the real SDCPN file (version, ten kinds, floor, 24 rows, 13 patterns, no domain words).
  • packages/core/test/elicited-model.test.ts — active-only fold, supersession and retraction, unmapped payloads, conflict vs divergence vs merge, revision stability.
  • packages/core/test/completion.test.ts — spec invariants 1–16, one or more tests each.
  • packages/core/test/cue.test.ts — sweep list matching, signal body, truncation.
  • packages/plugin-sdcpn/test/plugin.test.ts — the parsed file, proposal schema acceptance and refusal, an end-to-end fold + completion over the SDCPN rows.
  • packages/core/test/architecture/* — topology, docs index, Flue placement, and control surfaces, all green with the new package and agent module.
  • apps/brunch-agent/test/build-artifact.test.ts — both agent identities bound in the emitted bundle.

❓ How to test this?

  1. turbo run test:unit --filter @hashintel/brunch-agent --filter @hashintel/brunch-agent-plugin-sdcpn --filter @apps/brunch-agent
  2. Start the dev app (turbo run dev --filter @apps/brunch-agent) and open /?target=sdcpn; the interviewer's instructions carry the plugin file's sections, and after the first brunch_sweep the tool result includes a completion block naming the unsatisfied slots.
  3. Compare with / (Gherkin), which is unchanged.

📹 Demo

None yet; the SDCPN conversation is the FE-1404 run.

@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview Aug 27, 2026 9:04am
petrinaut Ready Ready Preview Aug 27, 2026 9:04am
petrinaut-docs Ready Ready Preview Aug 27, 2026 9:04am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Aug 27, 2026 9:04am

Request Review

lunelson commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

@cursor

cursor Bot commented Aug 26, 2026

Copy link
Copy Markdown

Merge activity

  • Aug 26, 5:52 PM UTC: Graphite rebased this pull request, because this pull request is set to merge when ready.

lunelson and others added 3 commits August 27, 2026 10:47
The harness can now answer "is the elicited model complete?" from the
capture store alone, and the SDCPN plugin is code that the harness loads,
not a spec the interviewer is told about.

Read path (FE-1497), five modules in `packages/core/src`:

- `plugin-file.ts` parses an ADR-0006 plugin file: the six fixed headings
  in order, the version, the three machine-read tables (kinds, must-know
  rows with typed precision and the not-applicable flag, patterns indexed
  by the kinds their trigger names), and the static floor as per-kind
  counts. Contract violations throw `PluginFileError` at load.
- `slot-assertion.ts` is the one proposal payload a kind-and-slot plugin
  needs: `{kind, node, slot, precision?, assertion: value | absence}`,
  restricted by `createSlotAssertionSchema(file)` to the kinds and slots
  the file names. It rides inside `content.value`; the envelope-level
  `{absence}` variant has no address and stays unusable for slots.
- `elicited-model.ts` folds active captures into nodes keyed `kind:node`
  and slots in one of four states (value, absence, conflict, divergence).
  Unreadable payloads are recorded as unmapped, never interpreted. A
  prescribed/practiced pair that differs is a divergence; anything else
  that differs is a conflict; an open conflicting issue pins a slot in
  conflict even when readings agree. The revision digests the active set.
- `completion.ts` is `evaluateCompletion(model, demands)`: pure, no
  persisted status, refuses foreign plugin versions, checks the floor,
  then the objective's dependency slice, then every row inside the slice
  in the diagnostic order the spec fixes. Nodes outside every objective's
  slice are reported, not demanded. `precisionSatisfies` encodes the
  ladder (named < number < range < spread; spelled out satisfies itself
  and named). Accepted statuses default to `explicit`; "inferred and
  confirmed" is an explicit superseding capture.
- `cue.ts` turns a report into a sweep list (failing slots plus the
  patterns indexed on their kinds) and a completion-cue signal that
  states the verdict and never decides whether to continue.

`definePlugin` gains `file?: PluginFile`; `targetDomain` is renamed
`targetFormalism` to match ADR-0006. The Flue binding threads the file
through: slot-assertion guidance in the extraction prompt, the cue in the
sweep tool's result, the file's prose appended to the instructions.
Harness facts still reach the model only through tool results and
signals.

Plugin package (FE-1482), `packages/plugin-sdcpn`: `docs/specs/sdcpn-
plugin.md` moves to `plugin.md` and is imported `?raw`, parsed at module
load, and declared as the single `slot-asserted` proposal type. Depends
on `@hashintel/brunch-agent` and valibot only; named in kernel §12.2 so
the topology gate admits it. Every document that linked the spec is
repointed.

Tests: the fixture plugin file plus contract-violation cases; the real
SDCPN file (version, ten kinds, floor, 24 rows, 13 patterns, no domain
words); fold semantics; completion invariants 1–16 mapped one test each
(17–19 are session-control and not implemented here); the cue; and an
end-to-end fold + completion over the SDCPN rows in the plugin package.

Gates: core 232/232, binding-flue 16/16, plugin-gherkin 2/2,
plugin-sdcpn 4/4, apps/brunch-agent 32/32; tsgo clean; oxlint 0 errors;
oxfmt applied. Dependents resolve the harness through `dist/`, so a
stale build fails their tests with the old field name until rebuilt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The target gallery gains its second entry. `src/agents/sdcpn-elicitor.ts`
is a thin `'use agent'` module, shaped like the gherkin one: it mounts
`useElicitation` with the `sdcpn` plugin and a session whose history
reader resolves conversations through the agent's own route. What the
interviewer asks, demands, and treats as complete comes from
`plugin.md` through the binding; this module holds none of it.

`routes.ts` names the routes as a record (`gherkin`, `sdcpn`) and the
session factory takes the target, so each agent's reader speaks to its
own mount. The browser UI selects the agent with `?target=sdcpn`;
gherkin stays the default tracer. Petrinaut's chat transport still
drives the gherkin elicitor and is unchanged.

`test/build-artifact.test.ts` now reads every module under `src/agents`
for pinned identities, so a second agent that silently failed to
register would fail the bundle check rather than pass it by omission.

Gates: dev app build + 32/32 tests (both identities bound in the
bundle); core architecture gates 119/119, including first-statement
directive, pinned literal, and no duplicated identity; tsgo clean;
oxlint 0 errors; oxfmt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
STEERING gains a gate row for completion-spec rules 17–19 (deferral
licensing): E1 now supplies the report and revision, but rule 18 makes
licensing false until a durable projection delivery exists, so the work
is gated on FE-1480 rather than opened as an issue.

The belief "the controller read path is small" carried its own tripwire
— stop and look if E1 exceeds the plugin file in size. It did. The row
now records the measured sizes, splits the parser from the engine, and
names the look: whether a stricter plugin-file format would shrink the
parser to a schema.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lunelson
lunelson force-pushed the ln/fe-1497-controller-read-path branch from 92cb856 to f8d631e Compare August 27, 2026 08:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps area/deps Relates to third-party dependencies (area) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) area/tests New or updated tests type/eng > frontend Owned by the @frontend team type/legal Owned by the @legal team

Development

Successfully merging this pull request may close these issues.

4 participants