Skip to content

docs(roadmap): mark 1.P Done + pre-1.Q hardening (condition-edge validation + Sonnet-reviewed) - #21

Merged
cemililik merged 5 commits into
mainfrom
development
Jun 14, 2026
Merged

docs(roadmap): mark 1.P Done + pre-1.Q hardening (condition-edge validation + Sonnet-reviewed)#21
cemililik merged 5 commits into
mainfrom
development

Conversation

@cemililik

@cemililikcemililik commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

A post-1.P-merge bookkeeping + pre-1.Q hardening batch. Docs-and-validation only on the engine side
(one small dag.ts parse-time check + its test); no engine-behavior change to a run.

Commits

ec99615 — roadmap: 1.P ✅ Done

Marks the node-type handlers (1.P, PR #20) Done across phase-1-engine-and-llm.md, current.md, CLAUDE.md,
README.md; 1.Q (human gate) named as the next workstream.

2b19f7f — deferred-tasks hardening (1.M validation)

A pre-1.Q triage of deferred-tasks.md. Most open items are legitimately blocked (a future workstream,
an external key/license, or a recorded decision); two were actionable now:

  • dag.ts (validateStructuralEdge): reject a plain (handle-less) edge whose from is a condition
    node — it would wire a dependent the handler's selected never names (a silently-dead node), or be
    redundant with a branch target. Reuses the invalid_handle issue; safe (no fixture/spec/test used one).
    Pinned by dag.test.ts + documented in workflow-yaml-spec.md §edges.
  • Bookkeeping: the secret-into-run.outputs runtime-taint item is closed at the source by 1.P
    (buildExpressionScope masks secret inputs, so a transform/condition/merge_fn can never read a raw
    secret to launder). The ctx.*-threading item is annotated as a run-lifecycle + seam change (async
    resolveContext at run start) — best folded into 1.Q/1.R, not a drive-by.

d3eca05 + 01b5933 — Sonnet pre-merge review, folded

Two multi-dimensional Sonnet reviews (each finding adversarially verified) audited this batch with a
focus on verifying every ✅-marked item is genuinely resolved. Verdict MERGE-WITH-NITS — zero
blocker/high, zero real defects. The secret-taint closure was confirmed airtight by an exhaustive path
trace (no pipe/filter unwrap, no scope bypass), and all audited resolved-items match the code. Folded:

  • The handle-less-edge rejection message + the §edges note were reworded (they overstated "dead node" —
    a plain edge to a branch target is also rejected, but that target IS activated, so it's redundant).
  • A test pinning that the rejection is unconditional (even to the branch's own target).
  • run-plan.md invalid_handle enumeration updated.
  • scope.ts now documents the load-bearing secret invariant (outputsRecord does not re-mask; a future
    handler emitting ctx.inputs-derived data must maskSecretInputs first).
  • sse-event-schema.md: a MaskedSecret marker can appear in node:completed.output / run:completed.outputs
    (surfaces must render it as a redacted placeholder).

Validation

  • pnpm turbo run lint typecheck test build format:check: green — 578 core tests.
  • Leakwatch: 0. Engine-deps allowlist: unchanged. Seam holds; zero platform imports.

🤖 Generated with Claude Code

Summary by Sourcery

Mark node-type handlers roadmap item 1.P as completed and harden pre-1.Q validation and contracts around condition edges and secret masking in outputs.

Bug Fixes:

  • Prevent handle-less edges originating from condition nodes by rejecting them at DAG validation time, including edges to branch targets.

Enhancements:

  • Clarify and document the invariant that secret-typed inputs are masked at ingress and never re-masked in outputs, and require future handlers to preserve this.
  • Document that MaskedSecret markers can appear in node and run outputs and must be treated as redacted placeholders.
  • Extend the run-plan invalid_handle error definition and workflow YAML spec to describe the new condition-edge validation behavior.

Documentation:

  • Update roadmap, README, and CLAUDE-facing docs to record 1.P (node-type handlers) as done and identify 1.Q (human gate) as the next workstream.
  • Annotate deferred-tasks items to close the secret-into-run.outputs taint follow-up and clarify the async context-resolution plan for future phases.

Tests:

  • Add DAG validation tests asserting that plain edges from condition nodes are always rejected, including edges to the condition's own branch target.

Summary by CodeRabbit

  • Documentation

    • Clarified workflow validation rules requiring condition nodes to use specific handle forms rather than plain edges
    • Enhanced security documentation regarding masked secret markers in workflow outputs
    • Updated Phase 1 completion status reflecting AgentRunner and node-type handlers
  • Bug Fixes

    • Fixed potential secret exposure in run outputs through masking gates
  • Tests

    • Added validation test cases for condition node edge constraints

cemililikand others added 4 commits June 14, 2026 16:00
…e next workstream
PR #20 merged — the six non-agent NodeExecutor arms (condition / transform / fan_out / fan_in /
input / output) behind a dispatching executor, executor-only, with the pre-merge BLOCKER secret-leak
fixed via a secretInputNames masking gate on NodeExecContext.
- phase-1-engine-and-llm.md: §1.P → ✅ Done (PR #20); top status block updated; 1.Q/1.R/1.S/1.AC remain.
- current.md: 1.P ✅ Done in both status passages; the human gate (1.Q) named as next.
- CLAUDE.md + README.md: status lines advanced to 1.P done, 1.Q next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…condition (1.M)
A pre-1.Q triage of deferred-tasks.md. Most open items are legitimately blocked (a future
workstream, an external key/license, or a recorded decision); two were actionable now.
- dag.ts (1.M validation): `validateStructuralEdge` now rejects a plain (handle-less) edge whose
`from` is a `condition` node — such an edge would wire a dependent the handler's `selected` never
names, a silently-dead downstream. Reuses the `invalid_handle` issue; safe (no fixture/spec/test
used one — conditions route via `branches` + `nodeId:when` handles). Pinned by dag.test.ts and
documented in workflow-yaml-spec.md §edges.
- Bookkeeping: the **secret-into-`run.outputs` runtime-taint** item (ADR-0029(c)) is closed at the
source by 1.P — `buildExpressionScope` masks secret inputs, so a transform/condition/merge_fn can
never read a raw secret to launder; no runtime taint needed. Checked off.
deferred-tasks.md triage recorded: `ctx.*` threading is the highest-value open engine gap but is a
run-lifecycle + seam change (async `resolveContext` at run start) — best folded into 1.Q/1.R, not a
drive-by. Everything else stays legitimately deferred.
pnpm turbo run lint typecheck test build format:check: green (577 core tests).
Leakwatch: 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-NITS)
The Sonnet review of the hardening pass returned MERGE-WITH-NITS (zero blocker/high; the secret-taint
closure confirmed substantively correct). Folds the confirmed mediums/lows.
- DOC-1 (medium): the handle-less-condition-edge rejection message + the workflow-yaml-spec §edges note
overstated "silently dead node" — a plain edge to a node that IS a branch target is ALSO rejected, but
that target IS activated (via materialization), so it's redundant, not dead. Reworded both (dag.ts
message + the spec note) to cover both cases (redundant-with-a-branch-target OR a never-activated node).
- T-1 (medium): added a dag.test.ts case pinning that a plain edge from a condition to its OWN branch
target is also rejected — the rejection is unconditional (unlike the `parallel_of` "redundant-if-agrees"
leniency); the existing test only covered the dead-downstream (stray) topology.
- DOC-2 (low): run-plan.md's `invalid_handle` enumeration now lists the new plain-edge-from-condition case.
- TRIAGE-02 (nit): clarified `merge_fn` → fan_in `merge_fn` in the secret-taint closure entry.
Skipped (with reason): the double-push of unknown_edge_target + invalid_handle for a missing `to` (both
true, harmless, parse fails regardless); assertion-strength + test-title nits (subsumed by the new test);
TRIAGE-01/03/04 doc-annotation polish (the substance is already accurate).
pnpm turbo run lint typecheck test build format:check: green (578 core tests).
Leakwatch: 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…findings)
The re-run of the two socket-failed Sonnet dimensions confirmed every ✅ claim GENUINE — an exhaustive
(a)–(g) trace verified the secret-taint closure is airtight (no pipe/filter unwrap, no scope bypass), and
the audited resolved-items (handle-less edge, six handlers + dispatcher + secretInputNames gate,
tool-output spill, Sonar fixes, concurrent-agent test, reportDuplicates) all match the code. No real
defects; the actionable items are doc/comment improvements that make implicit invariants explicit.
- AUDIT-9 (medium): scope.ts `outputsRecord` now documents the load-bearing secret invariant — it does
NOT re-mask; `run.outputs` stays raw-secret-free solely because the `input` handler masks at the ingress
and expression handlers read through `buildExpressionScope`. A future handler emitting `ctx.inputs`-derived
data MUST `maskSecretInputs` first. (Defense against a future tool/human-gate/subworkflow handler.)
- SEC-3 (low): sse-event-schema.md §security now states a `MaskedSecret` marker can appear in
`node:completed.output` / `run:completed.outputs` (the input node emits masked inputs) and surfaces must
render it as a redacted placeholder.
- AUDIT-6 (nit): corrected the `readBracket` description in deferred-tasks.md (it's a thin dispatcher, not
literally "3-line").
AUDIT-2 was a false positive (no `node-handlers.test.ts:618-655` citation exists in deferred-tasks.md).
pnpm turbo run lint typecheck test build format:check: green (578 core tests). Leakwatch: 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sourcery-ai

sourcery-aiBot commented Jun 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

Marks roadmap item 1.P (node-type handlers) as Done across docs, and hardens the engine by adding structural validation to reject plain edges from condition nodes while documenting and pinning the secret-masking invariants and SSE/event behavior; no runtime behavior change beyond stricter parse-time validation.

Sequence diagram for new condition-edge structural validation

sequenceDiagram
participant WorkflowYAMLParser
participant buildRunPlan
participant validateStructuralEdge
participant Issues
WorkflowYAMLParser->>buildRunPlan: buildRunPlan(workflowDoc)
loop for each edge
buildRunPlan->>validateStructuralEdge: validateStructuralEdge(edge, fromNode, locator, issues)
alt edge.from is condition and edge.handle is absent
validateStructuralEdge->>Issues: issues.push(invalid_handle)
validateStructuralEdge-->>buildRunPlan: return
else other edge
validateStructuralEdge-->>buildRunPlan: addEdge(fromBase, edge.to)
end
end
buildRunPlan-->>WorkflowYAMLParser: throw WorkflowGraphError(issues)
Loading

File-Level Changes

ChangeDetailsFiles
Add structural validation to reject plain (handle-less) edges originating from condition nodes and test it.
  • Extend validateStructuralEdge to treat any handle-less edge from a condition node as an invalid_handle issue with an explanatory message.
  • Document in workflow-yaml-spec and run-plan docs that condition nodes must route via branches[].target_node or nodeId:when handles and that plain edges from conditions are rejected.
  • Add dag.test cases covering rejection of plain edges from condition nodes, including edges to their own branch targets.
packages/core/src/dag.ts
packages/core/src/dag.test.ts
docs/reference/contracts/workflow-yaml-spec.md
docs/reference/shared-core/run-plan.md
Clarify and lock in secret-masking invariants for inputs and outputs, and document MaskedSecret behavior in events.
  • Annotate outputsRecord in scope.ts with the invariant that it passes outputs through verbatim and relies on upstream masking of secret-typed inputs, noting the requirement for future handlers to mask ctx.inputs-derived data.
  • Update deferred-tasks to mark the secret-into-run.outputs taint item as closed via buildExpressionScope masking and to describe remaining secret-related items.
  • Document that MaskedSecret markers can appear in node:completed.output and run:completed.outputs/run:failed.partialOutputs and must be treated as redacted.
  • Clarify that the input handler emits masked inputs and ties this to run-plan output capture docs.
packages/core/src/engine/node-handlers/scope.ts
docs/roadmap/deferred-tasks.md
docs/reference/contracts/sse-event-schema.md
docs/reference/shared-core/run-plan.md
Update roadmap and status documentation to mark node-type handlers (1.P) as complete and position 1.Q as the next workstream.
  • Mark 1.P as Done in the phase-1-engine-and-llm roadmap and describe its implementation as a dispatching NodeExecutor with six non-agent handlers and secretInputNames masking.
  • Update current roadmap, CLAUDE.md, and README.md to reflect that node-type handlers have landed (PR feat(core): node-type handlers (1.P) — condition/transform/fan-out/fan-in/input/output #20) and that the human gate (1.Q) is next.
  • Tighten wording around cognitive complexity fixes and other already-completed items in deferred-tasks to reflect their final state.
docs/roadmap/phases/phase-1-engine-and-llm.md
docs/roadmap/current.md
CLAUDE.md
README.md
docs/roadmap/deferred-tasks.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitaiBot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cemililik, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 1 hour, 39 minutes, and 6 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d189f3c1-99fd-4ab4-a9e0-4ba26ae4223a

📥 Commits

Reviewing files that changed from the base of the PR and between 01b5933 and 5acd6f5.

📒 Files selected for processing (3)
  • docs/reference/contracts/sse-event-schema.md
  • packages/core/src/dag.test.ts
  • packages/core/src/dag.ts
📝 Walkthrough

Walkthrough

Adds a runtime guard in validateStructuralEdge (dag.ts) that rejects plain (handle-less) edges originating from condition nodes with an invalid_handle issue, backed by two new test cases. Accompanying documentation updates reflect 1.P completion, document the secret-masking invariant in scope.ts and the SSE schema, and advance the roadmap to 1.Q.

Changes

Condition node edge validation

Layer / File(s)Summary
DAG guard and tests
packages/core/src/dag.ts, packages/core/src/dag.test.ts
validateStructuralEdge now records an invalid_handle issue and returns early when a plain (handle-less) edge originates from a condition node. Two new Vitest cases assert rejection for a plain target and the condition's own branch target.
Spec and run-plan docs
docs/reference/contracts/workflow-yaml-spec.md, docs/reference/shared-core/run-plan.md, docs/roadmap/deferred-tasks.md
workflow-yaml-spec.md states handle-less edges from condition nodes are rejected at parse time; run-plan.md expands the invalid_handle bullet; deferred-tasks.md marks the validation gap as fixed with test and spec attribution.

Phase 1 completion documentation

Layer / File(s)Summary
Secret masking invariant
packages/core/src/engine/node-handlers/scope.ts, docs/reference/contracts/sse-event-schema.md, docs/roadmap/deferred-tasks.md
scope.ts adds a load-bearing comment stating run.outputs passthrough does not re-mask secrets. sse-event-schema.md clarifies MaskedSecret may appear in node:completed.output and must be treated as a redacted placeholder. Deferred-tasks marks the secret-into-run.outputs taint as resolved.
Roadmap and status updates
CLAUDE.md, README.md, docs/roadmap/current.md, docs/roadmap/phases/phase-1-engine-and-llm.md, docs/roadmap/deferred-tasks.md
All roadmap and status files are updated to mark 1.O (AgentRunner join) and 1.P (node-type handlers via createDispatchingNodeExecutor, secretInputNames masking) as complete and set 1.Q (human gate) as the next workstream.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • HodeTech/Relavium#16: Directly within the same DAG builder / RunPlan handle-validation workstream — the validateStructuralEdge guard and dag.test.ts assertions in this PR extend the same edge-validation logic introduced there.

Poem

🐇 Hop, hop — the condition gate is sealed tight,
No handle-less edges shall pass in the night!
The secret stays masked, run.outputs stays clean,
Six executor arms and a wait_first machine.
Milestone 1.P is done, the roadmap shines bright —
On to 1.Q, little rabbit, keep coding with might! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe pull request title clearly summarizes the main changes: marking workstream 1.P as complete and applying pre-1.Q hardening through condition-edge validation and documentation updates. The title is specific, concise, and directly reflects the primary objectives.
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch development

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-aisourcery-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The new invalid_handle error message for plain condition edges in validateStructuralEdge is quite long and repeats explanation already present in docs; consider shortening it and/or extracting a shared constant so the error text and spec stay aligned.
  • The two new tests in dag.test.ts share almost identical graph setup; you could factor the shared YAML into a small helper or fixture to reduce duplication and make future edge-case additions easier.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- The new `invalid_handle` error message for plain condition edges in `validateStructuralEdge` is quite long and repeats explanation already present in docs; consider shortening it and/or extracting a shared constant so the error text and spec stay aligned.
- The two new tests in `dag.test.ts` share almost identical graph setup; you could factor the shared YAML into a small helper or fixture to reduce duplication and make future edge-case additions easier.
## Individual Comments### Comment 1
<locationpath="docs/reference/contracts/sse-event-schema.md"line_range="139" />
<code_context>
+The same `{ secret: true, ref }`**`MaskedSecret`** marker can also appear in **`node:completed.output`** (for an `input` node, which emits the masked inputs) and therefore in **`run:completed.outputs`** / **`run:failed.partialOutputs`** wherever a `secret`-typed input would otherwise surface — the engine masks `secret` inputs at the ingress so a raw secret never reaches an output payload (see [run-plan.md §output capture](../shared-core/run-plan.md)). **A surface rendering node/run outputs must treat a `MaskedSecret` object as a redacted placeholder, not displayable data.**
</code_context>
<issue_to_address>
**suggestion (typo):** Clarify the phrasing in “A surface rendering node/run outputs…” for grammatical correctness.
The phrase “A surface rendering node/run outputs must treat…” is ungrammatical. Please reword, e.g. “Any surface rendering of node/run outputs must treat a `MaskedSecret` object as a redacted placeholder…” or “A surface rendering of node/run outputs must treat…”.
```suggestionThe same `{ secret: true, ref }` **`MaskedSecret`** marker can also appear in **`node:completed.output`** (for an `input` node, which emits the masked inputs) and therefore in **`run:completed.outputs`** / **`run:failed.partialOutputs`** wherever a `secret`-typed input would otherwise surface — the engine masks `secret` inputs at the ingress so a raw secret never reaches an output payload (see [run-plan.md §output capture](../shared-core/run-plan.md)). **Any surface rendering of node/run outputs must treat a `MaskedSecret` object as a redacted placeholder, not displayable data.**```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment threaddocs/reference/contracts/sse-event-schema.md Outdated

@gemini-code-assistgemini-code-assistBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements validation to reject plain (handle-less) edges originating from a condition node, ensuring routing from conditions is explicitly handled via branches or named handles. It also updates documentation regarding the landing of node-type handlers (1.P), the prevention of secret leaks via MaskedSecret placeholders in outputs, and adds corresponding unit tests. Feedback is provided to guard the new edge validation in packages/core/src/dag.ts to prevent redundant and confusing error messages when the edge endpoints themselves are invalid.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment threadpackages/core/src/dag.ts Outdated
}
return; // a handled edge's dependency comes from branch materialization, never a second edge here
}
if (fromNode?.type === 'condition') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If either the source node (fromNode) is undefined or the target node (edge.to) does not exist in the graph, the edge already has a critical structural error (unknown_edge_target) which is reported above. Checking fromNode?.type === 'condition' and pushing an invalid_handle error in those cases leads to redundant and confusing error messages for the user.

We should guard this check to ensure we only validate condition routing when both endpoints of the edge are valid.

Suggested change
if(fromNode?.type==='condition'){
if(fromNode!==undefined&&nodesById.has(edge.to)&&fromNode.type==='condition'){

…#21 review)
Addresses the PR #21 review of the new handle-less-condition-edge validation.
- dag.ts (the valid finding): the condition-routing check now also requires `nodesById.has(edge.to)`,
so an edge with a missing `to` (or `from`) — already reported as `unknown_edge_target` — does NOT
also push a redundant/confusing `invalid_handle` for the same edge. `addEdge` no-ops on a phantom
endpoint, so the fall-through is safe. Pinned by a new dag.test.ts case (condition edge to a
nonexistent target ⇒ only `unknown_edge_target`, never `invalid_handle`).
- dag.ts: shortened the `invalid_handle` message to the actionable instruction (route via
`branches[].target_node` or the `nodeId:when` handle form); the redundant-vs-dead rationale lives in
the code comment + workflow-yaml-spec.md §edges, not the error string.
- sse-event-schema.md: grammar fix ("A surface rendering node/run outputs" → "Any surface rendering of
node/run outputs").
Skipped (with reason): a shared error-message constant (over-engineering for one call site); factoring
the two condition-edge tests into a fixture (dag.test.ts's idiom is self-contained inline YAML per test
— ~40 tests follow it; a 2-test helper would break that consistency for ~3 lines).
pnpm turbo run lint typecheck test build format:check: green (579 core tests). Leakwatch: 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cemililik
cemililik merged commit 0a0019b into mainJun 14, 2026
6 checks passed
@sonarqubecloud

Copy link
Copy Markdown

cemililik added a commit that referenced this pull request Sep 1, 2026
… the transport
W4 Step 1 — CR-40, and the 2.5.5 connect-timeout queue (#35, G32, #205) plus the
orphaned-child half of #21. Two of the three measurements ADR-0088 opens with
become assertions here.
**A connect had no upper bound.** Measured: a transport whose `start()` never
resolves hung `connectSdkTransport` indefinitely. Passing the SDK a
`RequestOptions` would not have fixed it — `Client.connect` awaits
`transport.start()` FIRST and with no options at all, and `start()` is where the
hang lives. So the window opens before any I/O, the whole `client.connect()` is
raced against it, and the REMAINING time is handed to the SDK's own `timeout` so
its internal 60 s default can neither cut an authored 120 s window short nor
outlive one. Four named deadlines replace four inherited or absent ones: stdio
connect 120 s (a cold `npx` is the canonical example), network connect 30 s,
discovery 60 s for the whole paged walk, call 60 s.
**Cancellation was dropped.** Measured: an aborted `tools/call` was still pending
500 ms later. The capability closure took `(input)` and ignored its second
parameter, and `McpConnection.callTool(name, args)` had nowhere to put a signal.
Both round-trips now take an `AbortSignalLike`, bridged to a real `AbortSignal`
at the SDK fence — `@relavium/shared` keeps `types: []` rather than widening its
signal type to a DOM class.
**A signal does not run `finally`.** Measured with a host that spawns a child
exactly the way the SDK does: `SIGTERM` printed no teardown line and left the
child alive with `ppid 1`. `run` and `agent run` tear down in a `finally`, which
Node's default signal handling never unwinds. A cooperative guard on all four
signals reaps the children and exits 128+signo, bounded so a wedged transport
cannot turn Ctrl-C into a hung shell.
`connect_timeout_ms` is authored on both server schemas, ceiling 600 000 ms,
rejected above it rather than clamped (ADR-0086's shape). It is an inline
CONNECTION field, so it is refused alongside a by-name `ref` — no precedence rule
is needed because none is possible — and it enters the duplicate-declaration
fingerprint for the reason `allow_local_endpoint` already does. It is deliberately
NOT in the ADR-0084 consent digest: that answers "is this the same program", and a
timeout changes nothing about what executes.
**One of my own tests was hollow, and mutation caught it.** `raceDeadline`
originally took a `dispose` callback said to be load-bearing; removing its call
left every test green, because `connectSdkTransport`'s own `catch` already closes
the client on every failure path. The parameter had no owner, so it is gone rather
than left as a second mechanism nobody exercises — and the close assertion now
binds the `catch`, verified by removing it (2 red).
Every new test is break-verified: the race removed (6 red), the SDK timeout
handoff dropped (1 red), the catch's close removed (2 red), the signal forwarding
reverted (2 red), the guard's close removed (8 red), its latch removed (1 red).
`pnpm run ci` and `pnpm coverage` both exit 0; `deadlines.ts` is at 100%.
Refs: ADR-0088, ADR-0082, ADR-0084, ADR-0086
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cemililik added a commit that referenced this pull request Sep 1, 2026
W4 is complete on `development`: five steps, eight review rounds, `CR-40`/`CR-41`/`CR-42`
plus the whole 2.5.5 MCP queue, `#202`, `G19`, and the MCP half of `#21`.
The closing register names, per item, the code that closes it AND the test that fails if
that code is reverted — exit criterion 7's requirement, and the answer to the failure this
phase keeps finding, where a register records a state the code does not hold.
Three things are stated that a status line would otherwise hide.
**What the wave cost.** Two previously-working configurations are now refused: a remote
`websocket` server, and a project config redefining a global MCP registration. Both are in
the reference specs with the remedy named, and both are consequences of decisions rather
than side effects.
**What it did not close.** Three residuals go to `deferred-tasks.md` rather than being
implied by a green register: the admission and the dialer canonicalize a url differently
(fails closed, so it costs usability not safety); the MCP hop does not go through
`withEgressTimeout`; and `allow_local_endpoint` may still name a host whose resolution a
LAN-adjacent attacker steers — narrowed by the dialer's refusal of a public or metadata
answer, not eliminated. `#297` is satisfied incidentally rather than by a coverage
threshold, and says so instead of being claimed.
**What the reviews found.** Eight rounds, three blockers and nine highs — and every
blocker was in a FIX rather than in the original code. The recurring shape was a guarantee
asserted in a comment, an ADR or a commit message with nothing between it and the code:
a §2.3 clause I wrote and never implemented, a fetch nothing proved arrived, a claim that
"both checks now redden" when only one did. The wave's own tests were the other half —
four could not fail at all, each stopping at a seam one layer above the thing it claimed
to prove. That is recorded in the register rather than smoothed over, because the next
wave inherits the habit, not the outcome.
`current.md`, the phase doc and `CLAUDE.md` now agree on 28 of 48 closed. `W3`'s blocker
is untouched and still first in its residuals.
Refs: ADR-0088
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cemililik added a commit that referenced this pull request Sep 1, 2026
…egister overclaiming
**§7.2 was decided and never implemented.** `ToolDef.source` records that a description was
written by an MCP server — the party the hostile-MCP class defends against — and both
lowering sites dropped it, so the model received the server's text indistinguishable from the
operator's. The ADR calls sanitization-plus-provenance "the honest mechanism"; only half of it
existed. Advisory by design, exactly as §7.2 says, and now asserted on the request a provider
actually receives rather than one seam above it.
**`#208` claimed a test that does not exist.** The register cited `errors.test.ts` for the
host-supplied client version; that file has no version test, and nothing else covered it — a
deleted `setMcpClientVersion` call or a composition-root rewire was invisible. Now read back
from a real server's handshake, which is where a server sees it.
Four more register rows described a `W4` that was more complete than the code: `CR-42` did not
stop paging, `#207`'s reporter could not fire, `CR-41`'s dialer was optional and its metadata
carve-out incomplete, and `#21`'s e2e could not catch the orphan the smoke check now does. Each
row now names what actually closes it and where the earlier claim was wrong.
`W4` is marked **REVIEW-BLOCKED** rather than complete in all four status homes. Five merge
blockers in one wave is not a detail to leave implied by a green register, and the phase doc
records the pattern: the mechanisms were built and the bindings between them were assumed.
Also recorded rather than rushed: the MCP schema compiler's denylist silently DROPS
unsupported assertions — `{"type":"number","exclusiveMinimum":0}` compiles and accepts `0`,
contradicting ADR-0052's fail-closed claim. Pre-existing, not a `W4` regression, and the fix
(a per-type supported-key allowlist) starts refusing schemas that work today, so it needs its
own change with its own compatibility review.
Refs: ADR-0088
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto 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

@cemililik