Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/user/features/agent-invocation.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ int exitCode = await app.RenderAsync(request, result, text); // the CLI's

- `TryBind(route, arguments, out request, out error)` names the route by its catalog `pattern` (ordinal). `arguments` is a JSON object; `default(JsonElement)` binds no arguments. No argv is parsed: the binder sees only the route's literal words, so every parameter and option comes from JSON. Keys, types, `null`, arrays, defaults, and the first failure reported are the same as `--json-args` without argv.
- `NuruBindError.Kind` is `UnknownRoute`, `NotAnObject`, `UnknownKey`, `MissingValue`, `WrongType`, `InvalidValue`, or `UnexpectedArgument`. `Key` is the JSON key the failure is about, or null. `Message` is the line `--json-args` writes to stderr.
- A JSON member (an object or array property of an operation) whose value does not deserialize, such as an unknown enum name inside an array item, a nested type mismatch, or a nested value that does not parse, is `InvalidValue` for that member. `Message` names the member and the JSON path inside it, for example `Steps: $[0].Tool: The JSON value could not be converted to ...`. It is never a crash or a `Faulted` handler error.
- `InvokeAsync(request, cancellationToken)` runs the same configuration, services, command scope, `INuruBehavior` chain, and handler (or mediator dispatch) as the CLI. It returns the handler's result boxed, without writing it: for `OneOf<T, SharedProblemDetails>` the `OneOf` itself, and null for `void` or `Unit`. Handler exceptions propagate. It throws `InvalidOperationException` when route execution stops before the handler (for example, no handler is registered), and `ArgumentException` for a request another app bound.
- `RenderAsync(request, result, terminal)` writes the result as the CLI does for that route: a registered or declared `IResponseRenderer<T>`, problem details to stderr, otherwise the output strategy (`returnType.kind`). It returns the exit code the CLI would: the problem status, otherwise 0. `RenderAsync(request, result, terminal, context)` renders the result of `InvokeAsync(request, context, …)` as the rest of that call: it starts from the exit code the handler set (`NuruCallContext.CurrentExitCode`), resolves paths against the call's working directory, and returns the call's exit code (the problem status overwrites the handler's code). The daemon and MCP hosts render this way.
- The binder and invoker are attached with the catalog, by the generated `Build()` interceptor, so an app that is built but never run supports them. The code is generated: no reflection, AOT-safe.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Disposition — task 530

**Date:** 2026-11-14
**Outcome:** clean
**Rounds:** 1
**Final open count:** 0

## Summary

Round 1 (general, effort 2) raised no findings. A JSON-member deserialization failure becomes `NuruBindError` `InvalidValue` and follows the existing bind-error path (exit 1, `error=Invalid`) in process, through the daemon, and over McpHost. A handler exception stays `Faulted`. The goal restatement matches the task. The beta cut and the Roslynk follow-up stay recorded later steps.

## Exception log (if accepted-exceptions)

None.

## Escalations

None.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Review framework — task 530

**Date:** 2026-11-14
**Host task:** kanban/in-progress/530-json-member-binding-a-jsonexception-is-a-bind-error-invalid-never-faulted-or-a-crash/
**Diff scope:** branch `task/530-json-member-binding-a-jsonexception-is-a-bind` vs `origin/master` (`origin/master...HEAD`). Product delta is commit `6e507acb` plus the executable bit in `c01bcaa1`: `json-args-emitter.assign.cs`, `json-args-emitter.validation.cs`, `nuru-bind-error.cs`, `documentation/user/features/agent-invocation.md`, `tests/timewarp-nuru-mcphost-tests/mcphost-06-json-member-bind-errors.cs` (+343/−6).
**Plan / brief:** A `JsonException` (and the serializer's `NotSupportedException` / `InvalidOperationException`, plus `FormatException`) while deserializing a JSON object or array member becomes a `NuruBindError` (`InvalidValue`) for that member: exit 1 / `error=Invalid` in process, through the daemon, and over McpHost. A handler exception stays `Faulted`. The Nuru beta cut and the Roslynk follow-up stay later steps.
**Effort:** 2 (general only; by-diff budget, not extra specialists)
**Reviewer roster:** general
**Session IDs:** review session id not exposed (print mode). Turn cap 120 (--max-turns; this cursor session uncapped).

## Budget (by-diff)

- Lines changed: 730
- Effort: 2
- TCB hits: none
- Roster axes: general
- Turn cap: 120 (--max-turns; cursor uncapped)

## Ground rules

- Reviewers are read-only on product code; they write only under `review/round-N/`
- Severity: bug | suggestion | nit — Status starts as open
- Do not invent issues to fill space; zero issues is a valid outcome
- Address the diff and surrounding call sites; re-verify falsifiable claims by reading the diff and the kitchen `## Results` and `### How to validate` record
- Do not re-run a full build, a full test run, an audit, or the hooks; those belong to the implementer's recorded results, the host audit node, and CI
- Exception: one targeted command may check one suspected bug; the finding names the command and the reason. A full build, full test run, or audit is never that command
- A missing or thin recorded result is a finding, not a reason to run the gates
- Prior rounds are immutable; new work goes in `round-(N+1)/`
- This round did not run `./bin/dev build`, `./bin/dev test`, `ganda repo audit`, or the git hooks
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Round 1 — general
**Date:** 2026-11-14
**Scope reviewed:** `git diff origin/master...HEAD` for the task 530 product paths (`json-args-emitter.assign.cs`, `json-args-emitter.validation.cs`, `nuru-bind-error.cs`, `documentation/user/features/agent-invocation.md`, `tests/timewarp-nuru-mcphost-tests/mcphost-06-json-member-bind-errors.cs`), checked against `task.md` `## Goal (implementer restatement)`, `## Requirements`, and `## Results` / `### How to validate`. Surrounding call sites: `EmitFill` / `EmitBindMethod`, `NuruProblems.FromBindError`, `NuruApp.WriteBindErrorAsync`, `NuruMcpToolInvoker.CallAsync`, and `NuruDaemonServer` invoke.

## Summary

The generated `ValueShape.Json` assign wraps `JsonSerializer.Deserialize` in `catch (Exception) when (NuruBindError.IsJsonMemberFailure)`. That filter is `JsonException`, `NotSupportedException`, `InvalidOperationException`, or `FormatException`. The catch sets `__ok = false` and records `NuruBindError.ForJsonMember` (`InvalidValue`) with `??=`, so the first failure wins. `EmitBindMethod` then returns false and `EmitBindCase` calls `WriteBindErrorAsync` and returns 1 without invoking the handler. `NuruProblems.Invalid` maps every bind failure to problem type `Invalid`. MCP forwards before local `TryBind`; the CLI `--json-args` path forwards the raw object after route selection and before `EmitBindCase`. The daemon's invoke path calls `TryBind` and renders `FromBindError`; its `RunCallAsync` catch still maps an escaped handler exception to `Faulted`.

`ForJsonMember` names the member and, when `JsonException.Path` is non-empty, the path (`Steps: $[0].Tool`). It uses `exception.Message` and strips the serializer's ` Path: … | LineNumber: …` suffix. There is no stack trace. The route pattern passed in is the already-escaped literal from `EmitBindMethod`.

The new runfile covers the three required cases (unknown enum in an array item, nested type mismatch, unparsable nested Guid) through `TryBind`, in-process `--json-args`, `UseProblemRendererForErrors` with `--no-daemon`, McpHost, and a real `NuruDaemonServer` for both McpHost and the CLI. A handler exception stays `Faulted` over McpHost (local and forwarded) and still escapes the in-process CLI.

The restatement matches the task. `FormatException` is an addition on top of the named `JsonException` / `NotSupportedException` / `InvalidOperationException` list; it covers a typed-ID converter that throws `FormatException` directly, which the task's examples include. The Nuru beta cut stays unchecked, with the Roslynk follow-up written under `### Follow-ups`. That matches the task's "when bumping" follow-up and the rule that cutting a release waits for an explicit ask.

`## Results` records `./bin/dev build` (0 warnings), `./bin/dev test` (CI multi-mode 2190, 2184 passed, 6 skipped, 0 failed; standalone 0 failed), and `ganda repo audit` (44 passed, 0 failed). `### How to validate` names the runfile and expects 5 tests. This round did not re-run those gates.

## Issues

No issues.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Round 1 — merged findings
**Date:** 2026-11-14
**Sources:** general

## Counts

| Severity | open | fixed | wontfix |
|----------|------|-------|---------|
| bug | 0 | 0 | 0 |
| suggestion | 0 | 0 | 0 |
| nit | 0 | 0 | 0 |

## Issues

No issues raised.

## Duplicates / conflicts

- Single reviewer. Nothing to collapse.
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# JSON member binding: a JsonException is a bind error (Invalid), never Faulted or a crash

## Description

Found in Roslynk task 009 (fork PR TimeWarpEngineering/Roslynk#15), 2026-10-23. Steven confirmed that this is
ours and not Peter Morris's.

The generated `--json-args` / McpHost binder (`__JsonBind_N`, emitted by
`source/timewarp-nuru-analyzers/generators/emitters/json-args-emitter.cs`) deserializes a JSON member with
`JsonSerializer.Deserialize(JsonElement, Type, JsonSerializerContext)` and does not catch `JsonException`.
Scalar members already get a `NuruBindError` (for example `startItem: "first"` gives `error=Invalid`, exit 1).
A value that fails **inside** an object or array member does not. Examples are an unknown string-enum value,
a typed ID that does not parse, or a nested type mismatch. The exception escapes:

- **In process** (`--no-daemon`, or any non-daemon Nuru app): an unhandled `JsonException` crashes the
process, exit 134.
- **Through the daemon / McpHost:** the call becomes `error=Faulted` (handler-exception path), not a binding
problem.

Reproduction against Roslynk at 009's head (an enum value outside `MultiQueryOp`):

```bash
roslynk multi_query --no-daemon --json-args '{"solutionId":"<sln>","operations":[{"tool":"rename_symbol"}]}'
# Unhandled exception. System.Text.Json.JsonException: The JSON value could not be converted to
# Morris.Roslynk.Features.MultiQuery.MultiQueryOperation. Path: $[0].tool ... -> exit 134
```

Every TimeWarp contract with an enum, a typed ID or a nested object in a JSON member hits this. That is why it
is our need.

## Requirements

- The JSON-member binder catches `JsonException` (and the `NotSupportedException`/`InvalidOperationException`
that the serializer throws for unbindable shapes, if any reach it) and turns it into a `NuruBindError` for
that member. It then follows the same path as scalar bind failures: usage text, or with
`UseProblemRendererForErrors()` an `error=Invalid` problem, exit 1. In process and forwarded, CLI and
McpHost (`isError` with the `error=Invalid` outline), the result is the same as the 508-018 behavior.
- The message names the member and the JSON path from the exception (`operations: $[0].tool ...`). Do not
leak a stack trace.
- A handler exception still becomes Faulted. Only binding changes.
- Tests: an unknown enum value in an array member, a type mismatch in a nested object, and an unparsable
nested value. Cover in process, through the daemon and through McpHost. No crash, and exit 1 / `isError`.
- Ship in a Nuru prerelease. Follow-up for Roslynk, when bumping: tighten
`McpSurfaceTests.WhenAMultiQueryOperationNamesAToolOutsideTheEnum_ThenTheWholeCallFails` to
`error=Invalid`, and add a `--no-daemon` CLI test.

## Goal (implementer restatement)

The generated JSON-member binder (`__JsonBind_N`, from `json-args-emitter`) calls
`JsonSerializer.Deserialize` for object/array members without a guard, so a value that fails *inside* the
member (unknown enum name in an array item, nested type mismatch, nested value that does not parse) escapes
as an exception: a crash (exit 134) in process, `error=Faulted` through the daemon/McpHost.

Acceptance:

- A serializer failure on a JSON member (`JsonException`, plus `NotSupportedException`,
`InvalidOperationException`, `FormatException` from the serializer or a converter) becomes a
`NuruBindError` (`InvalidValue`) for that member, so it flows like scalar bind failures: usage text, or
`error=Invalid` with `UseProblemRendererForErrors()`, exit 1.
- Same result in process (`--json-args`, `--no-daemon`), forwarded through the daemon, and over McpHost
(`isError` with `error=Invalid`).
- The message names the member and the JSON path inside it (`Steps: $[0].Tool: ...`), with no stack trace.
- A handler exception is still `Faulted`.
- Tests cover an unknown enum in an array member, a nested-object type mismatch and an unparsable nested
value, in process, through the daemon and over McpHost.
- Release in a Nuru beta and the Roslynk follow-up are later steps (release waits for an explicit ask).

## Checklist

- [x] JSON-member deserialization failures become `NuruBindError`
- [x] `error=Invalid` / exit 1 in process, forwarded and over MCP; no crash
- [x] Tests
- [ ] Released in a Nuru beta
- [x] Roslynk follow-up noted (see ## Results)

## Session

- Created: Roslynk cockpit 1287ec9b-6d3e-4805-9dd9-5ef6634981c8 (2026-10-23)
- 2026-11-14 implementer-claude (ganda task work, implement oracle): merged origin/master, guarded the
JSON-member deserialize, added runtime helpers, tests and a doc line. Audit boy-scout: `ganda repo audit
--fix` created the gitignored `.vscode/settings.json` for the advisory `vscode-window-icon` warning (no
tracked change). One audit run showed a transient `kanban-landed` failure (another live claim kitchen
out of sync with origin-home); the rerun passed with no change on this branch.
- The post-commit attest then failed `runfile-executable`: the new test runfile (untracked when the audit
ran) lacked the executable bit. Fixed with `chmod +x` + `git update-index --chmod=+x`; the audit rerun
passed (44 passed, 0 failed).
- Implement oracle: implemented by implementer-claude (claude, vendor anthropic, model claude-opus-5-5), session not reported, max-turns 500, Done — 2026-11-13T20:20:29Z
- Review oracle: reviewer-cursor (cursor, vendor xai, model grok-4.7), body tw-implementation-review, effort 2, roster general, turn cap 120 recorded by the host budget, this cursor session uncapped (no --max-turns flag), session id not exposed, round 1, disposition clean — 2026-11-14
- Review oracle: review by reviewer-cursor (cursor, vendor xai, model grok-4.7-high), session not reported, max-turns uncapped (no --max-turns flag; bound 120), artifacts general.md, merged.md, disposition.md — 2026-11-13T20:28:33Z

## Review

- Round 1: Done

## Notes

- Related: 526 (schema for converter-bound types). This task is binding only, not schema.

## Results

### What changed

- `source/timewarp-nuru-analyzers/generators/emitters/json-args-emitter.assign.cs`: the `ValueShape.Json`
assign wraps `JsonSerializer.Deserialize` in `try`/`catch (Exception) when
(NuruBindError.IsJsonMemberFailure(...))`, sets `__ok = false` and records
`NuruBindError.ForJsonMember(key, pattern, exception)`. The route pattern is threaded in from `EmitFill`
(`json-args-emitter.validation.cs`). The bind method then returns the failure like any scalar bind error,
so `--json-args`, `TryBind`, the daemon and McpHost all report it as a bind error.
- `source/timewarp-nuru/invocation/nuru-bind-error.cs`: two public static helpers called by generated code.
`IsJsonMemberFailure` is true for `JsonException`, `NotSupportedException`, `InvalidOperationException`
and `FormatException` (a typed-ID converter that parses text). `ForJsonMember` builds an `InvalidValue`
error: `Error: Invalid value for '<key>' on '<pattern>'. <key>: <json path>: <serializer message without
the " Path: … | LineNumber …" suffix>`. No stack trace.
- `documentation/user/features/agent-invocation.md`: one bullet on JSON-member failures being
`InvalidValue`.
- Tests: `tests/timewarp-nuru-mcphost-tests/mcphost-06-json-member-bind-errors.cs` (also in the CI
multi-mode assembly). An `[Operation]` with `IReadOnlyList<JbStep> Steps` and `JbOptions? Options` covers
an unknown enum name in an array item (`Steps: $[0].Tool`), a nested-object type mismatch
(`Options: $.Limits.Max`) and an unparsable nested Guid (`Steps: $[0].Target`). It checks `TryBind`
(kind `InvalidValue`, key, message, no stack trace), in-process `--json-args` with plain errors (exit 1),
with `UseProblemRendererForErrors()` and with `--no-daemon` (exit 1, `error=Invalid`), McpHost in process
(`isError`, `error=Invalid`), and forwarded through a real `NuruDaemonServer` for McpHost and for the CLI
via `AddServeRoute()` (same text as in process). A handler exception stays `error=Faulted` over McpHost
(local and forwarded) and still escapes the in-process CLI as itself.

### Gates

| Gate | Command | Result |
|------|---------|--------|
| Build | `./bin/dev build` | Pass, 0 warnings, 0 errors |
| Test | `./bin/dev test` | Pass: CI multi-mode 2190 total, 2184 passed, 6 skipped, 0 failed; standalone phase 0 failed |
| Audit | `ganda repo audit` | Pass: 44 passed, 0 failed, 1 skipped (after `--fix` for the advisory `vscode-window-icon`; see ## Session) |

### How to validate

Smoke:

```bash
ganda runfile cache --clear
dotnet run tests/timewarp-nuru-mcphost-tests/mcphost-06-json-member-bind-errors.cs
```

Expect: 5 tests pass. Before this change the in-process `--json-args` cases crash with an unhandled
`System.Text.Json.JsonException`, and McpHost reports `error=Faulted` instead of `error=Invalid`.

### Follow-ups

- Release: ships in the next Nuru 4.0.0 beta (cutting a release waits for an explicit ask; the "Released
in a Nuru beta" box stays open until then).
- Roslynk, when bumping to that beta: tighten
`McpSurfaceTests.WhenAMultiQueryOperationNamesAToolOutsideTheEnum_ThenTheWholeCallFails` to
`error=Invalid`, and add a `--no-daemon` CLI test for the same input (exit 1, no crash).

### Review

Round 1, roster general, effort 2, turn cap 120. Session id not exposed. Counts: bug 0/0/0, suggestion 0/0/0, nit 0/0/0 (open/fixed/wontfix). Disposition `clean`. No wontfix and no escalation. The restatement matches the task: `FormatException` is an extra filter for a typed-ID converter, and the beta cut stays a later step.

Paths: `review/review-framework.md`, `review/round-1/general.md`, `review/round-1/merged.md`, `review/disposition.md`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Disposition — task 573

**Date:** 2026-11-14
**Outcome:** clean
**Rounds:** 1
**Final open count:** 0

## Summary

Round 1 (general, effort 2) found no bugs, suggestions, or nits. The generator factory makes the four shipped nested handlers visible to CA1812, the consumer check compiles that content with the rule on and without suppression, and the recorded build, test, and audit results cover the change. The restatement matches the task.

## Exception log (if accepted-exceptions)

None.

## Escalations

- None.
Loading
Loading