Master's verify-export-jsdoc landed mid-stack; complete the six missing
@param/@returns on the workflow trio's public surface (and the services
catalog they regenerate into).
The previous commit rewrote the whole file through a JSON pretty-printer,
reformatting every existing entry; restore the established compact style with
the four new entries appended to the tools.md group.
The Codex simplification concern plus the duplication comment on the spawn
apply, resolved by deletion: the backend-lifetime holds are gone, so the
runtime registers at the first structured run and disposes when the last
settles — a deployment that never passes outputSchema carries no always-on
global state, and there is no per-backend acquisition block left to extract.
The driver spec now drives an INLINE spawn-shaped provider over
startInProcessRun, which removes the spawn/fork devDependencies (the
test-only workspace cycle); plugin-level structured coverage moves to the
backends' own specs (capture through the shipped plugin, mid-run backend
unload, seeded fork capture). tools.md, the driver README, and both backend
READMEs describe the run-scoped lifetime; the module-graph regenerates
without the cycle edges.
The cross-seam blocker: structured_output recorded its value in the tool
BODY, before tools/post-execute could block the call — a PostToolUse hook's
block turned the logged result into isError while readResult still returned
structured success and the continuation veto ended the turn.
Two-phase commit: the body validates and STAGES (RunState.pending); a fourth
runtime listener on tools/post-execute — prepend, so await next() returns the
composed final decision — promotes the stage to captured only on an accepted
call, and clears it on every path. A block now yields a consistent pair: the
model and log see the isError feedback, the run settles error with no
structured value, and the turn continues so the model can react. Regressions:
block denies the capture end-to-end; accept-with-replacement still commits.
Three Codex findings on json-schema.ts, one discipline:
- required-declared and every value check now use Object.hasOwn — 'in' let
inherited names (toString) satisfy required, dodge additionalProperties:
false, and validate a declared property against the value's prototype
member instead of a carried one
- isObjectLike now means PLAIN JSON object (proto chain of at most one link,
realm-agnostic): a Date annotation or a Map-as-properties no longer passes
structurally and serializes lossily — they fail loud as subset violations
- startInProcessRun asserts BEFORE the defensive structuredClone, so a
hostile schema fails as OutputSchemaError, never a raw DataCloneError
Also the type-equiv catalog gap: tools.md gains the structured-output subset
vocabulary (4 blocks) with matching manifest entries. The driver index also
drops the runtime internals from its public re-export (runs acquire it
internally; no external consumer remains — see the following commit).
- run-gates.ts docSyncLeafGates() gains verify-export-jsdoc — CI lanes
and the pre-push hook execute this leaf list, not the doc-sync npm
script, so the gate was previously unenforced there (proven by
SessionForkErrorCode landing undocumented via a master merge while
checks stayed green; now documented). Same wiring gap fixed for
master's verify-config-catalog, which was also missing from the list.
- The heritage exemption now recovers the base's return surface: a void
base return carried no @returns duty, so an override returning a
concrete result documents it itself (annotated overrides run the
standard check; unannotated ones are classified by the checker so
faithful void overrides need no boilerplate annotation). Three new
negative-path tests pin it; RFC and module doc updated.
- AGENTS.md states each principle inline instead of citing RFCs (eight
citations removed; high-level doc links kept) and the editing section
now carries the self-containedness rule.
- Generated catalogs/graphs regenerated for the shifted line pointers.
A name resolved through an export list (or a default-export identifier)
mapped back to its whole VariableStatement, and checkDecl walked every
declarator — so a private sibling sharing the statement with an exported
const was wrongly required to carry JSDoc.
The scope dispatch is now two-phase: requests accumulate per statement
(null = whole statement for a direct export modifier or ambient scope;
name sets union across lists, so two lists naming different declarators
of one statement both count), then each surfaced statement is checked
once with the declarator filter. Regressions pin the private-sibling
skip, the cross-list union, and the default-export sibling.
The #192 review discussion, preserved where design rationale lives: a
schema-object library (zod/schemastery) cannot sit at a wire-data boundary;
ajv replaces only the value walker while the subset gate — the module's
point — stays hand-written; provider JSON mode guarantees valid JSON, not
schema-conforming JSON, and would trade away mid-run tools and in-turn
validation retry for it. Strict tool schemas are named as the accepted
upgrade path when the provider ships them.
The coverage CI lane timed out (5000ms/test) in verify-export-jsdoc.spec.ts:
every test builds a ts.Program, and the fixture branch of
loadCompilerOptions omitted noLib, so each of the 34 tests parsed the
full default lib — measured 231ms/program bare, ~1.5s/test under
coverage instrumentation locally, past 5s on a 2-core runner. Fixtures
are self-contained and nothing in the walk resolves a lib symbol:
noLib + types:[] drops program creation to ~1ms and the spec's
coverage-mode test time from 53s to 0.16s. The real-repo branch
(tsconfig.base.json options) is untouched.
Master's generated config catalog (#188, flattened paths #191) now records
plugin Configs; the nudge removal dropped structuredNudgeRetries from both
backends, so the regenerated catalog loses those rows.
The markers are touched mid-script, so runPoint's continuation chain (child
exit → merge → the listener's detached .then) can still be in flight when the
marker poll resolves; a vitest worker that exits first leaves the inject
condition's short-circuit path uncounted. Observed as a CI-only 99.03%
branch-coverage flake on hooks-claude — surfaced by this branch shifting suite
timing, latent before it. Two macrotask rounds pin the path deterministically.
The parent branch dropped structuredNudgeRetries; the two workflow-side test
setups stop passing it, and the RFC's foundation paragraph now describes the
current design (final-ASSEMBLY enforcement logged via request/header, the
post-capture pre-execute deny, the start() schema snapshot, no re-prompt)
instead of the retired agent/request + nudge shape.
Two human review directives:
- No re-prompt. A structured child that finishes a turn cleanly without
calling structured_output settles error to the parent immediately —
readResult already carried that mapping; the nudge loop only delayed it.
Deletes the loop, its cancellation-window guard, STRUCTURED_OUTPUT_NUDGE,
and the structuredNudgeRetries Config on both backends.
- FIXME in the structured module doc: per-agent/per-session tool registry and
prompt assembly would dissolve the final-assembly enforcement dance (the
placeholder tool, the swap, the strip, the global-registration lifetime).
Residuals from the Codex re-review:
1. A system delta's insert was flattened to one token, so deltas differing
only in inserted-line count compared equal. Now one {{system}} token per
inserted line — position AND extent survive, content does not.
2. The live uniformity guard folded only request/header snapshots, so a
mid-run header CHANGE (request/header-delta) could diverge from the pin
invisibly. Non-pinning runs now assert zero header-delta events: a
scenario that legitimately changes its header mid-run exists to show
that change, so it must pin (fail-loud until it does).
Codex review findings on the pinned-header change:
1. scrubRequestHeaders flattened a request/header-delta's whole
system/tools payload to one token, so two meaningfully different deltas
compared equal. Now the structural facts survive — keepStart/keepEnd
line positions, added/removed/changed tool NAMES — and only the bulk
(inserted prompt lines, schema bodies) is tokenized.
2. The one-pin design rested on an unasserted premise (all sessions
compose the same header). Every non-pinning scenario now asserts, live,
that each request/header its run produces equals the pinned fixture's
header (both sides normalized against their own volatile values), so a
session-dependent header fails loud until it gets its own pin.
Verified the guard bites: perturbing the pinned fixture's prompt fails
a non-pinned scenario with the intended message.
3. RFC de-slopped per docs/AGENTS.md: no PR reference, no SHOULD
spec-speak; Decision/Verification/Consequences updated for 1 and 2.
Codex round-3: alias prose matches the gate's strength only when the
target's own contract is prose-only. An export-import alias to a
function, class, or namespace target (or an unresolvable one) is now
refused — those carry signature/member contracts the alias cannot
hold; export the declaration directly instead. Const/enum/interface/
type-alias targets keep the self-documentation contract.
Tests pin the refusal for function, class, and namespace targets;
module doc and RFC updated.
Two bot findings on the structured runtime:
- Terminal means terminal WITHIN the step: a model response listing
structured_output before further tool calls executed those calls after the
final answer was accepted (the turn-continuation veto only fires at step
end). A third runtime listener now denies every call for a captured agent
at the tools/pre-execute gate — dispatch skipped, isError result naming the
contract. Calls preceding the capture in the same response are untouched.
- The output schema is structuredClone'd before the subset assertion: the
caller keeps its reference, so asserting and attaching the original let a
post-start() mutation drift the enforced schema away from the asserted one.
The clone pins assertion, model-visible parameters, and validation to one
value.
The manifest excluded docs/tool-catalog/ and docs/persistence-catalog/ as
directories; the trailing slash is a deliberate path boundary, so the
flattened single files no longer matched and lost their generated-doc
classification — the gate would have accepted a hand-added .zh.md twin it
must reject. The entries now name the files.
Codex review of the draft: the abort-on-dispose tests asserted only that
dispose() resolved and no warning fired — a regression back to untracked
fire-and-forget runs would pass both. The hook script now records its
shell PID before sleeping, and after dispose the test asserts
process.kill(pid, 0) throws: the drain resolves only after the killed
run settled (the child reaped), so ESRCH-by-dispose-return is
deterministic under the correct implementation, while an alive or
unreaped process fails it. Verified the discriminator by gutting drain()
locally: exactly these two tests fail.
Review findings on the config catalog:
The transitive-paste closure deduplicated by bare name, so two DIFFERENT
package-local declarations sharing a name would silently collapse to the
first one pasted — and a reference satisfied by the wrong declaration is a
wrong catalog, not just a thin one. Every resolution is now identity-checked
by source pointer: reaching the same declaration again is benign, while a
name that resolves to two distinct declarations, to conflicting imports, or
to a declaration in one file and an import in another is a violation — a
verbatim fence has a single flat namespace, so the fix is a rename at the
source. A spec case pins the two-declarations collision.
docs/config-catalog.md also joins the translation-pairing exclusions: it is
a generated English-only doc, and without the entry the gate would accept a
hand-added .zh.md twin it must reject.
Codex round-2 review found three adjacent fail-open shapes:
- Wrapped function expressions escaped classification: parentheses,
as/satisfies casts, and non-null assertions are now peeled before the
arrow/function test (initializers and default exports), and a
single-call-signature type literal counts as the surface signature.
A literal mixing call/construct signatures with anything else is
refused outright — no single signature to hold the tags against.
- The blanket 'export import X = N.member' skip was unsound (the target
can be a non-exported namespace member no walk visits): an alias now
documents itself.
- The heritage extra-parameter duty missed binding-pattern extras,
which no base declaration can name: they now trigger the standard
binding-pattern violation.
Five new negative-path tests pin the closed shapes; module doc and RFC
updated.
Restack on the carved-out foundation (#192), per review feedback on #170.
The seam files resolve to the carve-out's revision — its prompt-order
neutrality fix (backends no longer inject 'tools'; the structured runtime
gates its own capture-tool registration) restores the subagent tools to
master's front position, so every recorded fixture is re-recorded on the
stacked tree and the authored error-finish/cancel headers re-patched to the
stacked tool list ([subagent, subagent_fork, workflow, todo_write, ...]).
Every session.jsonl fixture embedded the full composed system prompt and
complete tool-schema list in its request/header event (~8 KB on one line,
identical across the suite), so any prompt or tool-schema edit forced a
re-record or hand-edit of every fixture — see the dynamic-workflows PR for
the churn pattern this removes.
Now exactly one scenario (text-turn, flagged pinsHeader) commits and
compares that content verbatim; every other fixture stores and compares it
as {{system}}/{{tools}} tokens via the new pure scrubRequestHeaders
normalizer (applied to both compare sides and to record-mode writes, so a
re-record cannot reintroduce the content). request/header-delta payloads
are scrubbed the same way; config/reason stay verbatim — a model swap
SHOULD churn every fixture, a prompt edit should not. Replay is unaffected:
script derivation reads only assistant/chunk events.
Fixture meta-guards enforce the split: non-pinning fixtures must be fixed
points of the scrub, the pinning fixture must not be, and exactly one
scenario pins. Committed fixtures migrated through the same function.
Docs: pinned-header RFC (implemented/testing), base snapshot RFC + testing
policy + llm-replay module doc/README updated.