Skip to content

feat(cli): define Zoo protocol contracts - #1150

Open
taltas wants to merge 24 commits into
mainfrom
fm/zoo-cli-contracts
Open

feat(cli): define Zoo protocol contracts#1150
taltas wants to merge 24 commits into
mainfrom
fm/zoo-cli-contracts

Conversation

@taltas

@taltas taltas commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Stack

Position 1 of 6 in the Zoo CLI stack.

  • Base/parent: main
  • Child: fm/zoo-cli-headless-api (planned)
  • Native stack evidence: gh stack init and local stack tracking succeeded; gh stack submit --auto --open --remote origin returned Stacked PRs are not enabled for this repository, so this chain uses the approved classic base-branch fallback while preserving native local ancestry.

Scope

  • Adds private @roo-code/zoo-protocol with strict versioned host commands/events and public result/NDJSON schemas.
  • Defines capability negotiation, monotonic sequence validation, ACK plus DONE/ERROR lifecycle validation, and one authoritative terminal result.
  • Defines stable outcomes, machine error codes, and deterministic exit-code mapping.
  • Redacts secret-shaped keys and text before event buffering.
  • Adds a deterministic fake-provider semantic parity oracle with text, tool-pairing, and delegation/root-authority golden scenarios.
  • Preserves inherited apps/cli unchanged.

Acceptance Evidence

  • pnpm --dir packages/zoo-protocol test (16 tests)
  • pnpm --dir packages/zoo-protocol check-types
  • pnpm --dir packages/zoo-protocol exec eslint --prune-suppressions --max-warnings=0 src
  • pnpm --dir packages/zoo-protocol build
  • Repository pre-commit lint and pre-push typecheck passed.

Risk

This PR defines private contracts only and does not activate the extension or expose the zoo executable. The main compatibility risk is prematurely freezing schemas; protocol and public schema versions are explicit so later breaking changes fail negotiation rather than parsing ambiguously.

flowchart LR
    P[zoo parent] -->|strict HostCommand v1| C[@roo-code/zoo-protocol]
    C -->|validated command| H[zoo-host]
    H -->|ACK + DONE/ERROR| C
    H -->|monotonic normalized events| C
    C --> R[one authoritative root result]
    C --> N[stdout-pure NDJSON]
Loading

Summary by CodeRabbit

  • New Features

    • Added a standardized protocol for task commands, events, results, errors, and capability negotiation.
    • Added lifecycle validation for task streams, command acknowledgements, sequencing, and terminal results.
    • Added consistent outcome classifications and process exit-code mapping.
    • Added automatic redaction of credentials, tokens, passwords, and other sensitive values.
    • Added semantic parity checks to verify consistent task behavior.
  • Tests

    • Added comprehensive coverage for protocol contracts, validation, lifecycle handling, redaction, and parity scenarios.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4bf10d5b-b28a-44e4-a26c-b73083bb69f1

📥 Commits

Reviewing files that changed from the base of the PR and between 978e2d8 and 4a80564.

📒 Files selected for processing (1)
  • packages/zoo-protocol/src/redaction.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/zoo-protocol/src/redaction.ts

📝 Walkthrough

Walkthrough

Added the @roo-code/zoo-protocol package. It defines version negotiation, host and public stream contracts, lifecycle validation, outcomes, redaction, semantic parity utilities, package tooling, and contract tests.

Changes

Zoo protocol package

Layer / File(s) Summary
Package setup and public wiring
AGENTS.md, CLAUDE.md, packages/zoo-protocol/package.json, packages/zoo-protocol/*.config.*, packages/zoo-protocol/tsconfig.json, packages/zoo-protocol/eslint-suppressions.json, packages/zoo-protocol/src/index.ts
Added repository guidance, package metadata, shared tooling configuration, test configuration, lint suppressions, and public exports.
Version negotiation and host protocol
packages/zoo-protocol/src/version.ts, packages/zoo-protocol/src/host-commands.ts, packages/zoo-protocol/src/host-events.ts, packages/zoo-protocol/src/command-lifecycle.ts
Added protocol negotiation, strict host command and event schemas, bounded stream parsing, sequence validation, and command lifecycle validation.
Public stream and process outcomes
packages/zoo-protocol/src/public-events.ts, packages/zoo-protocol/src/outcomes.ts
Added run-result and stream-event schemas, negotiated-session checks, lifecycle validation, outcome definitions, and exit-code mapping.
Sensitive value redaction
packages/zoo-protocol/src/redaction.ts
Added text, structured-value, and buffered terminal-output redaction with canonicalization and fail-closed handling.
Semantic parity and contract validation
packages/zoo-protocol/src/parity.ts, packages/zoo-protocol/src/__tests__/contracts.test.ts
Added deterministic parity scenarios, fake-provider execution, semantic trace comparison, authoritative root-result validation, and contract tests.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Parent
  participant ProtocolNegotiation
  participant HostCommand
  participant HostEventParser
  participant PublicStream
  participant RootResult
  Parent->>ProtocolNegotiation: Send versions and required capabilities
  ProtocolNegotiation-->>Parent: Return negotiated version or incompatibility
  Parent->>HostCommand: Submit validated command
  HostCommand->>HostEventParser: Receive ordered host events
  HostEventParser->>PublicStream: Release redacted stream events
  PublicStream->>RootResult: Validate authoritative root settlement
  RootResult-->>Parent: Return outcome and exit metadata
Loading

Suggested labels: awaiting-review

Suggested reviewers: hannesrudolph

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the addition of Zoo protocol contracts, which is the main change in the pull request.
Description check ✅ Passed The description clearly covers scope, implementation purpose, testing evidence, compatibility risk, and the unchanged CLI scope.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fm/zoo-cli-contracts

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/zoo-protocol/src/redaction.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
packages/zoo-protocol/eslint.config.mjs (1)

1-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type the flat configuration as an array.

packages/config-eslint/base.js exports config as an array, and [...config] remains an array. The JSDoc declares Linter.Config, which describes one config object. Use the repository's flat-config array type.

Proposed fix
-/** `@type` {import("eslint").Linter.Config} */
+/** `@type` {import("eslint").Linter.Config[]} */

Verify the exact type name against the installed ESLint declarations before applying the change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/zoo-protocol/eslint.config.mjs` around lines 1 - 4, Update the JSDoc
type annotation above the default export in eslint.config.mjs from the
single-config Linter.Config type to ESLint’s flat-config array type, verifying
the exact declaration name in the installed ESLint types. Keep the existing
[...config] export unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/zoo-protocol/src/parity.ts`:
- Around line 3-11: Update SemanticTraceEntry to a discriminated union keyed by
literal type values, requiring the fields appropriate to each event;
specifically make task.result entries require a ZooOutcome outcome and preserve
required task identifiers where applicable. Update the validator to narrow on
the discriminant and reject task.result entries without outcome, including root
results.
- Around line 61-65: Replace the JSON.stringify-based comparison in the parity
comparison function with order-independent deep structural equality, normalizing
entries if needed so equivalent objects match regardless of property insertion
order. Preserve the existing { ok: true } result for equal values and generate
the difference message from normalized or structurally comparable
representations for unequal values.

In `@packages/zoo-protocol/src/public-events.ts`:
- Around line 162-165: Update validateStreamLifecycle in
packages/zoo-protocol/src/public-events.ts around lines 162-165 to count exactly
one task.result whose taskId equals rootTaskId, require that root result to be
the final event, and verify result.rootTaskId matches the event’s root task ID.
Extend packages/zoo-protocol/src/__tests__/contracts.test.ts lines 114-147 with
cases covering a child result followed by a root result, a child-only terminal
result, and mismatched event/result root task IDs.

In `@packages/zoo-protocol/src/redaction.ts`:
- Around line 20-29: Update redactValue so seen tracks only the current
recursion path: remove each processed object from seen after its array or object
branch completes, while preserving the circular-reference check. Add a
regression test covering repeated references such as the same child assigned to
left and right, ensuring both branches are fully redacted rather than marking
the second as circular.

---

Nitpick comments:
In `@packages/zoo-protocol/eslint.config.mjs`:
- Around line 1-4: Update the JSDoc type annotation above the default export in
eslint.config.mjs from the single-config Linter.Config type to ESLint’s
flat-config array type, verifying the exact declaration name in the installed
ESLint types. Keep the existing [...config] export unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6be73ecc-3a4b-421c-9f2a-a1ed63e68876

📥 Commits

Reviewing files that changed from the base of the PR and between 64d6e37 and a4405ce.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (15)
  • AGENTS.md
  • CLAUDE.md
  • packages/zoo-protocol/eslint.config.mjs
  • packages/zoo-protocol/package.json
  • packages/zoo-protocol/src/__tests__/contracts.test.ts
  • packages/zoo-protocol/src/host-commands.ts
  • packages/zoo-protocol/src/host-events.ts
  • packages/zoo-protocol/src/index.ts
  • packages/zoo-protocol/src/outcomes.ts
  • packages/zoo-protocol/src/parity.ts
  • packages/zoo-protocol/src/public-events.ts
  • packages/zoo-protocol/src/redaction.ts
  • packages/zoo-protocol/src/version.ts
  • packages/zoo-protocol/tsconfig.json
  • packages/zoo-protocol/vitest.config.ts

Comment on lines +3 to +11
export type SemanticTraceEntry = {
type: string
taskId?: string
parentTaskId?: string
toolCallId?: string
content?: string
outcome?: ZooOutcome
errorCode?: ZooErrorCode
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Enforce the required shape of task.result entries.

Every field except type is optional. This value type-checks and passes the validator:

{ type: "task.result", taskId: "root" }

The validator then returns true without a ZooOutcome. Define SemanticTraceEntry as a discriminated union with literal event types and required fields. At minimum, reject a root result whose outcome is undefined.

Minimum validation fix
-	return results.length === 1 && results[0]?.taskId === rootTaskId
+	return (
+		results.length === 1 &&
+		results[0]?.taskId === rootTaskId &&
+		results[0]?.outcome !== undefined
+	)

Also applies to: 68-70

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/zoo-protocol/src/parity.ts` around lines 3 - 11, Update
SemanticTraceEntry to a discriminated union keyed by literal type values,
requiring the fields appropriate to each event; specifically make task.result
entries require a ZooOutcome outcome and preserve required task identifiers
where applicable. Update the validator to narrow on the discriminant and reject
task.result entries without outcome, including root results.

Comment thread packages/zoo-protocol/src/parity.ts Outdated
Comment thread packages/zoo-protocol/src/public-events.ts
Comment thread packages/zoo-protocol/src/redaction.ts
@github-actions github-actions Bot added awaiting-review PR changes are ready and waiting for maintainer re-review and removed awaiting-review PR changes are ready and waiting for maintainer re-review labels Aug 5, 2026
Comment thread packages/zoo-protocol/src/redaction.ts Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (10)
packages/zoo-protocol/src/parity.ts (2)

260-261: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use prefix-length expressions instead of hand-computed offsets.

Line 261, Line 319, and Line 327 use the literals 4, 5, and 8 to strip the ask:, fail:, and timeout: prefixes. Line 244 already uses the clearer "delegate:".length form. A wrong literal does not throw. It produces a truncated identifier or an identifier that still contains part of the prefix. Use the same form in all four places.

♻️ Proposed refactor
-			const askId = turn.slice(4)
+			const askId = turn.slice("ask:".length)
-			const errorCode = failedErrorCodeSchema.parse(turn.slice(5))
+			const errorCode = failedErrorCodeSchema.parse(turn.slice("fail:".length))
-			const errorCode = zooErrorCodeSchema.parse(turn.slice(8))
+			const errorCode = zooErrorCodeSchema.parse(turn.slice("timeout:".length))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/zoo-protocol/src/parity.ts` around lines 260 - 261, Replace the
hand-computed slice offsets in the parity parsing logic with the corresponding
prefix string length expressions for the ask:, fail:, and timeout: cases,
matching the existing delegate: handling. Update the slices associated with
askId and the other parsed identifiers while preserving their current behavior.

350-350: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reject malformed directives instead of treating them as message content.

Line 350 is an unconditional fallthrough. Any turn that does not match a directive becomes message.upsert content. A typo in a fixture, for example tol:read_file:call-1:README.md or cancel without fields, becomes a plain message rather than an error. This harness is a parity oracle, so a silent reinterpretation produces a confusing trace diff instead of a clear fixture error.

Guard the fallthrough against known directive prefixes.

♻️ Proposed refactor
+		if (/^[a-z_]+:/.test(turn)) throw new Error(`Unknown fake-provider directive: ${turn}`)
 		trace.push({ type: "message.upsert", rootTaskId: "root", taskId: "root", content: turn })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/zoo-protocol/src/parity.ts` at line 350, Update the turn-processing
logic in parity.ts so the unconditional message.upsert fallthrough rejects
strings beginning with known directive prefixes when they fail directive
parsing, rather than recording them as message content. Preserve message.upsert
for ordinary non-directive turns, and report malformed directive input as a
clear fixture error.
packages/zoo-protocol/src/__tests__/contracts.test.ts (1)

2454-2469: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert error messages in the negative parity cases.

toThrow() without an argument passes for any thrown error. A fixture typo or an unrelated TypeError would still satisfy these assertions. Line 2453 already pins the message with toThrow("Invalid tool fixture"). Apply the same precision to the other rejection cases in this file (Lines 2461, 2469, 2482, 2500, 2508, 2517, 2533).

♻️ Proposed change for two of the cases
 		expect(() =>
 			runDeterministicFakeProvider({
 				id: "invalid-failure",
 				prompt: "Fail",
 				providerTurns: ["fail:task_timed_out"],
 				expected: [],
 			}),
-		).toThrow()
+		).toThrow(/task_timed_out/)
 		expect(() =>
 			runDeterministicFakeProvider({
 				id: "trailing",
 				prompt: "Cancel",
 				providerTurns: ["cancel:cancel-1:user", "trailing"],
 				expected: [],
 			}),
-		).toThrow()
+		).toThrow("Fake-provider terminal directives must be the final turn")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/zoo-protocol/src/__tests__/contracts.test.ts` around lines 2454 -
2469, Update the negative parity assertions in contracts.test.ts so the existing
runDeterministicFakeProvider cases use message-specific toThrow checks instead
of bare toThrow. Keep the current fixtures and expectations in each test, but
pin the thrown error text for the invalid-failure and trailing scenarios and the
other listed rejection cases in this file, matching the already-precise "Invalid
tool fixture" assertion pattern used earlier.
packages/zoo-protocol/src/outcomes.ts (1)

93-96: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Read all fields from the parsed value.

Line 94 takes outcome from exitContextSchema.parse(context). Lines 95-96 take errorCode and signal from the unparsed context. The two sources are identical today, because the schema applies no transform or coercion. If a transform or default is added later, errorCode and signal bypass it.

Destructure all three fields from the parse result.

♻️ Proposed refactor
 export function exitCodeFor(context: ExitContext): number {
-	const { outcome } = exitContextSchema.parse(context)
-	const errorCode = "errorCode" in context ? context.errorCode : undefined
-	const signal = "signal" in context ? context.signal : undefined
+	const parsed = exitContextSchema.parse(context)
+	const { outcome } = parsed
+	const errorCode = "errorCode" in parsed ? parsed.errorCode : undefined
+	const signal = "signal" in parsed ? parsed.signal : undefined
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/zoo-protocol/src/outcomes.ts` around lines 93 - 96, Update
exitCodeFor to destructure outcome, errorCode, and signal from the single
exitContextSchema.parse(context) result, removing reads of errorCode and signal
from the unparsed context.
packages/zoo-protocol/src/host-events.ts (3)

248-249: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow the sanitizeNonEvent parameter instead of casting.

sanitizeNonEvent accepts the full raw union and returns HostEvent through as HostEvent. The cast hides the redaction contract. The current call site at line 285 passes only non-event envelopes, so behavior is correct today. If a future branch passes an event envelope, the cast returns an unredacted raw stream event typed as a redacted HostEvent, and the compiler does not report it.

Exclude the event variant in the parameter type.

♻️ Proposed refactor
-	const sanitizeNonEvent = (event: z.infer<typeof rawHostEventDiscriminatedSchema>): HostEvent =>
+	const sanitizeNonEvent = (
+		event: Exclude<z.infer<typeof rawHostEventDiscriminatedSchema>, { type: "event" }>,
+	): HostEvent =>

Also applies to: 284-288

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/zoo-protocol/src/host-events.ts` around lines 248 - 249, Update
sanitizeNonEvent to accept a type that excludes the raw event envelope variant,
rather than the full raw host-event union, and remove the HostEvent cast from
its return path. Keep the existing non-event call site behavior unchanged while
ensuring future event-envelope calls are rejected by the compiler.

248-258: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse one command.error redaction helper.

sanitizeNonEvent repeats the redaction body of the hostEventSchema transform at lines 103-114. The two copies must stay identical, because both produce HostEvent values that callers treat as redacted. A new sensitive field added to zooErrorSchema must be handled in both places.

Extract the redaction into one function and call it from both sites.

♻️ Proposed refactor
+const redactCommandError = <T extends { error: { message: string; phase?: string } }>(event: T): T => ({
+	...event,
+	error: {
+		...event.error,
+		message: redactText(event.error.message),
+		phase: event.error.phase === undefined ? undefined : redactText(event.error.phase),
+	},
+})
+
 export const hostEventSchema = hostEventDiscriminatedSchema
 	.superRefine((event, context) => {
 		if (event.type === "event" && event.event.hostId !== event.hostId) {
 			context.addIssue({ code: z.ZodIssueCode.custom, message: "Normalized event hostId must match its host envelope" })
 		}
 	})
-	.transform((event) =>
-		event.type === "command.error"
-			? {
-					...event,
-					error: {
-						...event.error,
-						message: redactText(event.error.message),
-						phase: event.error.phase === undefined ? undefined : redactText(event.error.phase),
-					},
-				}
-			: event,
-	)
+	.transform((event) => (event.type === "command.error" ? redactCommandError(event) : event))
 	const sanitizeNonEvent = (event: z.infer<typeof rawHostEventDiscriminatedSchema>): HostEvent =>
-		event.type === "command.error"
-			? {
-					...event,
-					error: {
-						...event.error,
-						message: redactText(event.error.message),
-						phase: event.error.phase === undefined ? undefined : redactText(event.error.phase),
-					},
-				}
-			: event as HostEvent
+		event.type === "command.error" ? redactCommandError(event) : (event as HostEvent)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/zoo-protocol/src/host-events.ts` around lines 248 - 258, Extract the
duplicated command.error redaction object construction from sanitizeNonEvent and
the hostEventSchema transform into a shared helper function. Have both call
sites use that helper so all HostEvent redaction, including future
zooErrorSchema fields, remains consistent.

193-207: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Share one stream-event key function with public-events.ts.

eventKey duplicates streamEventKey in packages/zoo-protocol/src/public-events.ts (lines 293-307). Both must produce identical keys, because assign matches redactor output to queued envelopes by key. A field added to one copy and not the other silently breaks envelope matching and makes assign throw "Missing host envelope for buffered Zoo stream event".

Export streamEventKey from public-events.ts and import it here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/zoo-protocol/src/host-events.ts` around lines 193 - 207, Remove the
duplicated eventKey implementation in the host-events stream handling and import
the existing streamEventKey from public-events.ts. Export streamEventKey from
public-events.ts, then use that shared function wherever host-events currently
computes keys so assign matches redacted events and queued envelopes
consistently.
packages/zoo-protocol/src/public-events.ts (1)

283-286: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the pass-through branch explicit.

default: return streamEvent passes any unhandled event type through without redaction. Every current type that reaches this branch carries only identifiers, enums, numbers, or an already-redacted result, so the output is correct today.

A new event type with a free-text field is emitted unredacted unless the author adds a case. This is a redaction boundary, so make the pass-through set explicit and let the compiler report a new type.

List the pass-through types as explicit cases and assert exhaustiveness with a never check in default.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/zoo-protocol/src/public-events.ts` around lines 283 - 286, Update
the event redaction switch surrounding the default branch to enumerate every
currently pass-through event type as explicit cases returning streamEvent.
Replace the unconditional default return with an exhaustiveness check that
accepts never, so adding a new event type requires an explicit redaction
decision.
packages/zoo-protocol/src/redaction.ts (2)

3-4: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicate secret-value pattern.

secretValue and cliSecretValue have identical definitions. The two names imply a difference that does not exist. A future edit to one changes only some call sites.

Keep one constant, or give cliSecretValue the shell-specific value grammar it implies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/zoo-protocol/src/redaction.ts` around lines 3 - 4, Remove the
duplicated secret-value pattern in redaction.ts by updating the constants near
secretValue and cliSecretValue so they no longer share the same definition.
Either reuse a single shared constant for both call sites, or change
cliSecretValue to a distinct shell-specific grammar if that is the intended
behavior. Keep the existing redaction symbols and replace only the redundant
pattern source so future edits cannot diverge silently.

78-83: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Hoist the recompiled terminal-control pattern.

Line 80 rebuilds a regex from terminalControl.source on every call. redactText calls this function for every string field of every event, so the pattern is recompiled on a hot path.

String.prototype.matchAll does not mutate the source regex lastIndex, so a module-level global constant is safe here.

♻️ Proposed refactor
+const terminalControlGlobal = new RegExp(terminalControl.source, "g")
+
 export function requiresFailClosedRedaction(value: string): boolean {
 	if (unsafeTerminalEditing.test(value) || /(?:\r(?!\n)|[\v\f])/.test(value)) return true
-	return [...value.matchAll(new RegExp(terminalControl.source, "g"))].some(([control]) =>
+	return [...value.matchAll(terminalControlGlobal)].some(([control]) =>
 		containsSensitiveAssignment(control),
 	)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/zoo-protocol/src/redaction.ts` around lines 78 - 83, Hoist the
recompiled terminal-control pattern out of requiresFailClosedRedaction so the
hot path does not create a new RegExp on every call. Add a module-level constant
derived from terminalControl.source and reuse it inside
requiresFailClosedRedaction, keeping the existing matchAll and
containsSensitiveAssignment behavior unchanged. Ensure the shared regex is safe
to reuse across calls, since matchAll does not mutate lastIndex.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/zoo-protocol/src/host-events.ts`:
- Around line 155-192: Update inputSize to track only the active ancestor path:
remove each object from seen after its array or object branch has finished
processing, while retaining detection for objects encountered again before their
branch completes. Preserve the existing depth and size-limit behavior and follow
the cleanup pattern used by redactValue.

In `@packages/zoo-protocol/src/public-events.ts`:
- Around line 462-486: Update the terminal.output aggregation path in
public-events so it no longer rescans the full pending buffer on every delta.
Reuse a hoisted TextEncoder and track pending byte length incrementally instead
of calling new TextEncoder().encode(pending.text), replace the newline boundary
check on pending.text with an endsWith("\n")-style check, and avoid recomputing
pending.pem from canonicalizeRedactionText(pending.text) on every push by
updating PEM state incrementally or only evaluating it when the buffer ends at a
newline. Keep the existing overflow and emit(key) behavior in the same pending
state handling block.
- Around line 720-741: The `hostSequenceFor` and `causalTerminal` helpers are
doing repeated linear scans over `events` and `commandEvents`, which makes the
validation path quadratic. Precompute a reference-keyed event-to-sequence map
and a commandId-to-lifecycle map once before the per-event and final-pass loops,
then update `hostSequenceFor` and `causalTerminal` to read from those caches
instead of calling `indexOf` and `filter`. Preserve the current semantics for
duplicate event objects by keeping the first observed sequence, and keep the
existing ack/terminal ordering checks unchanged in `public-events.ts`.

In `@packages/zoo-protocol/src/redaction.ts`:
- Around line 22-32: Update redactText to cap input text length before applying
secretPatterns, truncating oversized content and appending “[REDACTED]” before
returning. Preserve existing redaction behavior for text within the limit, and
ensure the cap applies to all content processed through redactText.

---

Nitpick comments:
In `@packages/zoo-protocol/src/__tests__/contracts.test.ts`:
- Around line 2454-2469: Update the negative parity assertions in
contracts.test.ts so the existing runDeterministicFakeProvider cases use
message-specific toThrow checks instead of bare toThrow. Keep the current
fixtures and expectations in each test, but pin the thrown error text for the
invalid-failure and trailing scenarios and the other listed rejection cases in
this file, matching the already-precise "Invalid tool fixture" assertion pattern
used earlier.

In `@packages/zoo-protocol/src/host-events.ts`:
- Around line 248-249: Update sanitizeNonEvent to accept a type that excludes
the raw event envelope variant, rather than the full raw host-event union, and
remove the HostEvent cast from its return path. Keep the existing non-event call
site behavior unchanged while ensuring future event-envelope calls are rejected
by the compiler.
- Around line 248-258: Extract the duplicated command.error redaction object
construction from sanitizeNonEvent and the hostEventSchema transform into a
shared helper function. Have both call sites use that helper so all HostEvent
redaction, including future zooErrorSchema fields, remains consistent.
- Around line 193-207: Remove the duplicated eventKey implementation in the
host-events stream handling and import the existing streamEventKey from
public-events.ts. Export streamEventKey from public-events.ts, then use that
shared function wherever host-events currently computes keys so assign matches
redacted events and queued envelopes consistently.

In `@packages/zoo-protocol/src/outcomes.ts`:
- Around line 93-96: Update exitCodeFor to destructure outcome, errorCode, and
signal from the single exitContextSchema.parse(context) result, removing reads
of errorCode and signal from the unparsed context.

In `@packages/zoo-protocol/src/parity.ts`:
- Around line 260-261: Replace the hand-computed slice offsets in the parity
parsing logic with the corresponding prefix string length expressions for the
ask:, fail:, and timeout: cases, matching the existing delegate: handling.
Update the slices associated with askId and the other parsed identifiers while
preserving their current behavior.
- Line 350: Update the turn-processing logic in parity.ts so the unconditional
message.upsert fallthrough rejects strings beginning with known directive
prefixes when they fail directive parsing, rather than recording them as message
content. Preserve message.upsert for ordinary non-directive turns, and report
malformed directive input as a clear fixture error.

In `@packages/zoo-protocol/src/public-events.ts`:
- Around line 283-286: Update the event redaction switch surrounding the default
branch to enumerate every currently pass-through event type as explicit cases
returning streamEvent. Replace the unconditional default return with an
exhaustiveness check that accepts never, so adding a new event type requires an
explicit redaction decision.

In `@packages/zoo-protocol/src/redaction.ts`:
- Around line 3-4: Remove the duplicated secret-value pattern in redaction.ts by
updating the constants near secretValue and cliSecretValue so they no longer
share the same definition. Either reuse a single shared constant for both call
sites, or change cliSecretValue to a distinct shell-specific grammar if that is
the intended behavior. Keep the existing redaction symbols and replace only the
redundant pattern source so future edits cannot diverge silently.
- Around line 78-83: Hoist the recompiled terminal-control pattern out of
requiresFailClosedRedaction so the hot path does not create a new RegExp on
every call. Add a module-level constant derived from terminalControl.source and
reuse it inside requiresFailClosedRedaction, keeping the existing matchAll and
containsSensitiveAssignment behavior unchanged. Ensure the shared regex is safe
to reuse across calls, since matchAll does not mutate lastIndex.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 77d2b3d4-18cd-417d-856e-6f071ce2c6d8

📥 Commits

Reviewing files that changed from the base of the PR and between a4405ce and 150db93.

📒 Files selected for processing (12)
  • packages/zoo-protocol/eslint-suppressions.json
  • packages/zoo-protocol/package.json
  • packages/zoo-protocol/src/__tests__/contracts.test.ts
  • packages/zoo-protocol/src/command-lifecycle.ts
  • packages/zoo-protocol/src/host-commands.ts
  • packages/zoo-protocol/src/host-events.ts
  • packages/zoo-protocol/src/index.ts
  • packages/zoo-protocol/src/outcomes.ts
  • packages/zoo-protocol/src/parity.ts
  • packages/zoo-protocol/src/public-events.ts
  • packages/zoo-protocol/src/redaction.ts
  • packages/zoo-protocol/src/version.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/zoo-protocol/package.json
  • packages/zoo-protocol/src/index.ts
  • packages/zoo-protocol/src/host-commands.ts

Comment thread packages/zoo-protocol/src/host-events.ts
Comment thread packages/zoo-protocol/src/public-events.ts
Comment thread packages/zoo-protocol/src/public-events.ts
Comment thread packages/zoo-protocol/src/redaction.ts
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Aug 5, 2026
taltas added 17 commits August 5, 2026 14:00
@taltas
taltas force-pushed the fm/zoo-cli-contracts branch from 150db93 to 978e2d8 Compare August 5, 2026 18:01
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/zoo-protocol/src/__tests__/contracts.test.ts (2)

2338-2361: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Select parity scenarios by id instead of array position.

Lines 2339, 2341, 2348, and 2361 index parityScenarios by position. The assertions depend on which scenario sits at each index. If someone inserts or reorders a scenario in parity.ts, these tests check a different scenario and fail with an unclear message. Look up each scenario by its id.

♻️ Proposed helper
+const scenarioById = (id: string) => {
+	const scenario = parityScenarios.find((candidate) => candidate.id === id)
+	if (scenario === undefined) throw new Error(`Unknown parity scenario ${id}`)
+	return scenario
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/zoo-protocol/src/__tests__/contracts.test.ts` around lines 2338 -
2361, Update the tests around the parityScenarios references in “includes the
prompt in fake-provider semantics,” “includes tool identity and arguments in
fake-provider semantics,” and “detects child completion incorrectly settling the
root” to select scenarios by their id rather than array position. Reuse the
looked-up scenario consistently for both expected traces and provider inputs,
preserving the existing assertions.

2446-2535: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the expected error for each rejection case.

Line 2453 asserts .toThrow("Invalid tool fixture"). The remaining rejection cases use a bare .toThrow(). A bare .toThrow() passes on any thrown error, including an unrelated TypeError from a future refactor of runDeterministicFakeProvider. Add a message matcher to each case so the test proves the intended validation fired.

The same applies to lines 2480-2482, 2493-2500, 2501-2508, 2515-2517, and 2526-2533.

♻️ Example for the invalid-failure case
 		expect(() =>
 			runDeterministicFakeProvider({
 				id: "invalid-failure",
 				prompt: "Fail",
 				providerTurns: ["fail:task_timed_out"],
 				expected: [],
 			}),
-		).toThrow()
+		).toThrow(/timeout error code/i)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/zoo-protocol/src/__tests__/contracts.test.ts` around lines 2446 -
2535, Replace every bare .toThrow() in the rejection tests around
runDeterministicFakeProvider with message matchers for the specific validation
error each fixture is intended to trigger, including invalid-failure, trailing,
unresolved states, ghost, duplicate, reused identifiers, and extra
approval/cancellation fields. Preserve the existing .toThrow("Invalid tool
fixture") assertion and use the validation messages exposed by the
implementation rather than broad or empty matchers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/zoo-protocol/src/__tests__/contracts.test.ts`:
- Around line 2338-2361: Update the tests around the parityScenarios references
in “includes the prompt in fake-provider semantics,” “includes tool identity and
arguments in fake-provider semantics,” and “detects child completion incorrectly
settling the root” to select scenarios by their id rather than array position.
Reuse the looked-up scenario consistently for both expected traces and provider
inputs, preserving the existing assertions.
- Around line 2446-2535: Replace every bare .toThrow() in the rejection tests
around runDeterministicFakeProvider with message matchers for the specific
validation error each fixture is intended to trigger, including invalid-failure,
trailing, unresolved states, ghost, duplicate, reused identifiers, and extra
approval/cancellation fields. Preserve the existing .toThrow("Invalid tool
fixture") assertion and use the validation messages exposed by the
implementation rather than broad or empty matchers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c184f4ba-537a-4bb4-998a-b15bc3ea0dcc

📥 Commits

Reviewing files that changed from the base of the PR and between d0af4c0 and 978e2d8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (17)
  • AGENTS.md
  • CLAUDE.md
  • packages/zoo-protocol/eslint-suppressions.json
  • packages/zoo-protocol/eslint.config.mjs
  • packages/zoo-protocol/package.json
  • packages/zoo-protocol/src/__tests__/contracts.test.ts
  • packages/zoo-protocol/src/command-lifecycle.ts
  • packages/zoo-protocol/src/host-commands.ts
  • packages/zoo-protocol/src/host-events.ts
  • packages/zoo-protocol/src/index.ts
  • packages/zoo-protocol/src/outcomes.ts
  • packages/zoo-protocol/src/parity.ts
  • packages/zoo-protocol/src/public-events.ts
  • packages/zoo-protocol/src/redaction.ts
  • packages/zoo-protocol/src/version.ts
  • packages/zoo-protocol/tsconfig.json
  • packages/zoo-protocol/vitest.config.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • packages/zoo-protocol/eslint-suppressions.json
  • CLAUDE.md
  • AGENTS.md
  • packages/zoo-protocol/tsconfig.json
  • packages/zoo-protocol/vitest.config.ts
  • packages/zoo-protocol/package.json
  • packages/zoo-protocol/eslint.config.mjs
  • packages/zoo-protocol/src/outcomes.ts
  • packages/zoo-protocol/src/index.ts
  • packages/zoo-protocol/src/parity.ts
  • packages/zoo-protocol/src/host-commands.ts
  • packages/zoo-protocol/src/host-events.ts

@github-actions github-actions Bot removed the awaiting-review PR changes are ready and waiting for maintainer re-review label Aug 5, 2026
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants