release: promote develop to main (v0.18.0 — skill generation modes) - #1251
Merged
Merged
Conversation
chore: sync main → develop after v0.17.0
`domains/skills/generation/types/generation.ts` and `types/streaming.ts` re-declared `GeneratedSkill`, `SkillStreamEvent` and an unused `LlmOptions` but had zero importers — the generation service imports the canonical declarations from `shared/types/index.ts`. Removing them before #1242 extends `GeneratedSkill` (mode + references/assets) so the extension lands in exactly one place. Part of #1242. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
`routes.ts` was 548 lines — over the 500-line file limit — and mixed three concerns: request parsing, the SSE transport + quota reconcile, and ZIP-to-prompt context extraction. Pure move, no behaviour change: - `streaming.ts` now owns `preflight`, `resolveKeepAliveMs` and `streamGenerationEvents` (the #808/#827 ordering guarantees travel with their doc comments). - `packageContext.ts` now owns `analyzePackageContent`. `routes.ts` drops to 354 lines so the `mode` parsing in #1242 has room to land without breaching the limit again. The existing route tests exercise both helpers through the mounted routes and still pass. Part of #1242. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
The four `generate*` generators each carried an identical 60-line preamble + stream loop (resolve defaults → abort check → `generation_start` → pump tokens → map abort/provider failures to an `error` frame). That duplication is what would have pushed `service.ts` past the 500-line limit once mode handling lands in #1242, and it meant any fix to the loop had to be applied four times. - `begin()` owns the preamble and `streamLlm()` owns the token pump; both are `AsyncGenerator`s consumed with `yield*` so the emitted event sequence is byte-for-byte what each generator produced before (the existing per-generator log labels are threaded through). - `completeLlm()` owns the non-streaming call the single-turn retry uses. - `generatedSkillSchema` + the parse/clean/validate routine move to `validation.ts` (`parseGeneratedSkill`); `parseAndValidate` stays on the service as a thin delegate because the tests call it directly. No behaviour change — the 82 generation tests pass unmodified. Part of #1242. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
Contract root for skill-generation modes:
- `GENERATION_MODES` / `GenerationMode` (`"simple" | "advanced"`) and
`DEFAULT_GENERATION_MODE = "advanced"`. Defaulting to `advanced`
keeps every existing caller — agents already integrated against
`POST /skills/generate` — on exactly the behaviour they had before
modes existed.
- `GeneratedSkill` gains `references` and `assets` (same
`{ filename, content }` shape as `scripts`), the advanced-mode
materials the skill package format already allows at the root but
the generator never produced. The Zod schema defaults both to `[]`
so older model output and the integration fixture (which omits the
array fields) still validate; the JSON contract is text-only, so
binary assets remain an upload-only concern.
Part of #1242.
Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
Adds the LLM-side half of the simple/advanced contract: - `SIMPLE_GENERATION_SYSTEM_PROMPT` — the schema block the model copies from lists only name / description / category "plain" / tags / readmeBody. Not offering `scripts`, `references`, `assets` or the runtime fields at all is the strongest lever before server validation; the prose additionally says why (the package is one SKILL.md) and how to handle API/tool tasks inline. - `GENERATION_SYSTEM_PROMPT` (advanced) now documents `references[]` (on-demand supporting docs) and `assets[]` (text-only run-time resources) with field rules and a worked example, so the model knows the package format allows them. - `getGenerationSystemPrompt(mode)` is the single selector; `buildDirectGenerationPrompt(query, mode = "advanced")` threads it so its previously unused `instructions` field becomes the source of truth for the service in the next commit. - `SIMPLE_MODE_RETRY_INSTRUCTION` — appended to the user turn on the one corrective retry a simple-mode violation gets. Tests pin the structural contract (no file arrays in the simple schema block, references/assets documented in the advanced prompt) rather than snapshotting prose. The hardcode sweep still passes — no URLs or model ids in the new text. Part of #1242. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
Server-side half of the simple/advanced contract.
`validation.ts`
- `validateGeneratedSkill(raw, mode)` returns a discriminated result
(`ok` | `invalid_json` | `schema` | `mode_violation`) so callers
know *why* an answer was rejected and can pick the right retry
instruction. In `simple` mode a schema-valid answer is a
`mode_violation` when `category !== "plain"` or any of `scripts`,
`references`, `assets`, `runtimes`, `dependencies`, `envVars` is
non-empty; the message names the offending fields.
- `parseGeneratedSkill` stays as the boolean-style wrapper the
intrinsically-simple OpenAPI / source-code generators use.
`service.ts`
- `generateStream` / `generateStreamWithHistory` take a
`GenerateOptions` object (`signal`, `modelOverride`, `mode`) instead
of positional args; `mode` defaults to `advanced` and selects both
the system prompt and the validator.
- Single-turn: every first rejection still gets exactly one
non-streaming retry, now with the instruction that matches the
rejection (simple-mode nudge vs. "output valid JSON"). The retry is
validated against the same mode, so "invalid JSON first, scripted on
retry" still ends in `error`.
- Multi-turn: invalid-JSON answers keep the existing no-retry rule
(a refinement turn may legitimately be prose). A simple-mode
violation is the one case that now retries — as a continuation of
the conversation (offending answer → assistant turn, corrective
instruction → user turn) — because the "SKILL.md only" guarantee is
what agents integrate against. A second violation ends the stream
with `error` and no `generation_complete`.
- `parseAndValidate` is removed from the service; its tests move to
`validation.test.ts`.
Routes pass `{ signal, modelOverride }` for now; the `mode` field on
the request body lands in the next commit.
Part of #1242.
Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
Callers can now choose the package shape: `mode: "simple" | "advanced"` on both the JSON body (single-turn `prompt` and multi-turn `messages`) and the multipart form. Omitted or empty → `advanced`, i.e. exactly what every caller got before, so no existing agent integration changes behaviour. An unknown value — or a multipart `mode` that arrives as a file or a repeated field — fails with 400 `invalid_mode` naming the accepted values. The check runs before `preflight()`, so like the other body validations it can never strand a reserved quota slot (#808); the route tests assert that neither `resolveModel` nor `checkAllowed` is reached. `mode` is included in the request `info` log lines so a violation retry in the service log can be correlated with what the caller asked for. `from-source` / `from-openapi` are unchanged: they always produce a plain, file-less skill and do not take `mode`. Part of #1242. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
The request bodies for `POST /skills/generate` are hand-written JSON Schema (the handler parses manually, so nothing derives them), which means `openapi.json` would have silently gone stale without this. - `mode` property (enum, default `advanced`, `invalid_mode` rejection, the exact simple-mode rule) on both the JSON and multipart bodies; the multipart variant also states that an attached `package` is still read as context in full even in simple mode. - `GENERATION_STREAM_CONTRACT` lists the new `references[]` / `assets[]` members of `raw` and how to assemble the package from it. - The operation description spells out the retry matrix per input shape, including the one multi-turn exception (a simple-mode violation retries once, then ends on `error`), and the cost contract wording covers that case. - `from-source` / `from-openapi` state that they are always the equivalent of `simple` and that a `mode` key is ignored, so agents do not try to send it there. Contract tests (route parity, description substance, examples, problem+json responses) pass. Part of #1242. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
The page was 591 lines — over the 500-line file limit — before the mode toggle (#1242) adds to its composer row. Pure extraction, markup and behaviour unchanged: - `hooks/useGenerativeDrawer` owns the hover / pin / Esc state and the new-iteration hint. The two `setState`-in-effect transitions became the "adjust state during render" guard the react-hooks lint wants (same fix as #888 in ModelPicker) — the extracted hook tripped the rule where the inlined page had not. Now covered by its own test. - `components/skill/generative/GenerativeEmptyHero` — eyebrow + headline + the three prompt starters (the starters were a module-level helper on the page; they are static copy so they live with the hero now). - `components/skill/generative/GenerativePackageRailTab` — the right-edge tab with its hint rings / tooltip / warning dot. The page drops to 403 lines. Part of #1242. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
The generation parser only ever turned `scripts[]` into a folder. With
advanced mode the server's `generation_complete.raw` can also carry
`references[]` and `assets[]` (same `{ filename, content }` shape), so
the preview now builds one folder per non-empty array, in
scripts → references → assets order, with a per-folder fallback
filename when the model omits one. Empty or missing arrays leave no
folder behind, which is what makes a simple-mode preview a single
SKILL.md.
Adds the parser's first test file: fence/prose cleanup, metadata
extraction (including the legacy `env` / `npmDependencies` spellings),
SKILL.md assembly, the folder matrix above, the `readmeMd` migration,
and the non-JSON fallback path.
Part of #1242.
Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
Every pre-stream gate on `POST /skills/generate` answers with an RFC 7807 body, but the SSE client threw it away and reported only `HTTP 400: Bad Request`. That would have made the new `invalid_mode` rejection (#1242) — and the existing `prompt_too_long`, `quota_exceeded`, `MODEL_NOT_ENABLED` ones — opaque in the chat. `describeHttpFailure` now prefers `detail`, then `title`, and falls back to the status line for a non-JSON or empty body, matching what the assistant stream client already does. Also adds the client's first test file: request shape (URL, bearer header, exact body), event whitelist and ordering, frames split across chunks, the problem+json / non-JSON / empty failure bodies, network failure, and abort. Part of #1242. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
- `types/skillPackage.ts`: `GENERATION_MODES` / `GenerationMode`,
mirroring the server contract.
- `generateStreamApi`: `mode` joins `modelId` in the request body. The
stale `model` key — which the server never read (it reads `modelId`)
— is dropped rather than copied as a pattern.
- `useSkillGeneration.sendMessage(content, { modelId, mode })` replaces
the positional `modelId` argument so a third per-send option does
not turn into a fourth positional; `mode` is also recorded on the
`skill_gen.started` analytics event next to `modelId`.
- The page passes `{ modelId }` for now; the mode toggle that supplies
`mode` lands in the next commit.
Adds the hook's first test file (request params per turn, phase
machine through start / tokens / complete / error, multi-turn
transcript accumulation with a mode switch between turns, token
batching, abort, reset, preview editing) and a `mode` body case in the
stream client tests.
Part of #1242.
Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
Lets the user pick the package shape before each turn.
- `GenerationModeToggle` — a `role="radiogroup"` with two `role="radio"`
segments in the composer-chip vocabulary ModelPicker / QuotaInline
already use (JetBrains Mono micro-label, `rounded-sm` hairline on
`bg-elevated/40`, ember for the selected segment only, explicit
focus-visible ring). Left/Right/Up/Down move the selection; roving
tabindex keeps one tab stop. Locked (`disabled`, dimmed,
`aria-disabled`) while a generation streams so the mode cannot
change under an in-flight request.
- `usePreferredGenerationMode` — localStorage-backed like the model
pick (`ornn.preferredMode.skillGen`), validated against
`GENERATION_MODES`, cross-tab synced, tolerant of blocked storage.
Defaults to `advanced`, which is also the server default, so a fresh
browser and an omitted field mean the same thing.
- `useGenerationModeCopy` — one source for the label + hint strings so
the segments' `title`/aria and the always-visible hint row under the
composer ("SIMPLE · SKILL.md only — …") cannot drift; hover alone is
not an acceptable affordance per docs/DESIGN.md.
- The composer row gains `flex-wrap` so the three chips restack on
narrow viewports; `mode` is sent with every turn, so a user can
switch between refinements ("now add a script" → advanced).
- i18n: six `generative.mode*` keys in en + zh; the unused
`generative.note` ("plain or runtime-based only") is removed because
it no longer describes the feature. A `generative`-namespace parity
test now guards en/zh key drift the way skillsetParity does.
Tests cover the toggle (ARIA, click, keyboard wrap, disabled), the
preference hook (default, round-trip, bad value, storage event,
throwing storage), and the copy hook.
Part of #1242.
Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
The three agent manuals (`ornn-agent-manual-cli`, `ornn-agent-manual-http`, `chrono-ai-service-manual`) carry byte-identical §7 text, so the same edit is applied to all three: - §7.0 now spells out the parsed shape of `generation_complete.raw` — including the new `references[]` / `assets[]` arrays — instead of "<SKILL.md + scripts as JSON>", and states the simple-mode guarantee (plain category, all six file/runtime arrays empty). - §7.1 gains a field table for `mode` and the previously undocumented `modelId`, shows both in the JSON, multipart and multi-turn examples, explains the retry matrix per body shape, and adds `INVALID_MODE` to the endpoint table and the §1.8 legend. - §7.2 / §7.3 state that from-source / from-openapi are always the equivalent of `simple` and ignore `mode`. - SKILL.md build-flow step 1 in each manual mentions the two modes; each touched SKILL.md bumps `version` / `lastUpdated` so agents that compare against the registry copy (§0) notice the update. Publishing the bumped manuals to the registry is a separate operational step. `skills/ornn-agent-manual-http/SKILL.md` is an assistant KB source, so the digest is regenerated (`bun run build:assistant-kb`) to keep the `assistant-kb-freshness` CI gate green. Part of #1242. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
Minor bump for both packages: a new optional request field on `POST /skills/generate` plus new arrays in the generation output contract (api), and the mode toggle + references/assets preview (web). Part of #1242. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
Drives `mode` through the real Hono app, settings-driven model resolution, quota buckets and the generation service with only the LLM client injected (the harness seam the charge tests already use): - `mode: "bogus"` → 400 `invalid_mode` problem+json, no bucket row. - `simple` + scripted first answer → `validation_error` (retrying) → the `complete()` retry carries the simple prompt and the corrective instruction → `generation_complete` with the plain answer → charged once. - `simple` + scripted answer twice (multi-turn) → terminal `error`, no `generation_complete`, the offending answer replayed as an assistant turn on the retry, still charged once (skill_error). - multipart `mode=simple` reaches the service. - omitted mode (advanced) passes references / assets straight through. The gateway mock reuses one `text` for stream and retry, so the test composes its stream with a `complete()` that answers differently — the simple-mode contract is exactly "first answer bad, retry good". Part of #1242. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
Review found a hole in the simple-mode guarantee: the mode check ran only on a schema-VALID document, so a multi-turn answer that carried `scripts` / `references` / `assets` but tripped any other schema rule (over-long description, uppercase tag, …) was reported as `schema`, and the multi-turn path — which deliberately does not retry bad JSON — delivered it verbatim in `generation_complete`. The web preview would then show `scripts/` folders in Simple mode. `validateGeneratedSkill` now parses first, applies the simple-mode check on the raw object (any non-empty file/runtime array, a non-plain category, or an `outputType`), and only then runs the schema — so a files-carrying answer is a `mode_violation` no matter what else is wrong with it, and gets the corrective retry. Two more things the same review turned up in this module: - `outputType` is now a simple-mode violation. The comment claiming the frontmatter builder ignores it was wrong: the builder emits `output-type` and the frontmatter schema rejects it on a plain skill, so a stray value made the generated SKILL.md unpublishable. - The parser no longer throws for `readmeMd: null` / non-string `readmeMd`, or for JSON that is `null`, an array or a scalar (the legacy migration had moved outside the try block during the refactor). Those are `invalid_json` / `schema` rejections; an escaping TypeError would have killed the generator and ended the SSE stream with no terminal frame. Tests cover each case, including the multi-turn abort-after-first- answer guard that had no coverage. Part of #1242. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
The route doc promised that a repeated `mode` form field (which
`parseBody({ all: true })` surfaces as a string array) is rejected
like an unknown string, but nothing exercised it. Reproduced against
the mounted routes: two `mode` parts → 400 `invalid_mode`, quota never
reserved.
Part of #1242.
Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
With a roving tabindex, the previously focused segment drops to
`tabIndex=-1` as soon as the selection changes, so an arrow key that
changed the value but left focus behind stranded keyboard users on an
untabbable button. Each segment now keeps a ref and `move()` focuses
the newly selected one.
The test that should have caught this was vacuous: it asserted
`toHaveBeenLastCalledWith("advanced")` twice in a row, so the wrap
case passed even if the second keypress did nothing. It now clears the
mock between keypresses, asserts the call count, and checks
`document.activeElement` after an arrow key.
Part of #1242.
Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
Two of the fallback branches introduced with the problem+json parsing were unexercised: a body with `title` but no `detail`, and a `response.text()` that rejects (body stream lost). Both are pinned so the status-line fallback cannot silently regress. Part of #1242. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
Six accuracy fixes to the OpenAPI operation text and the three agent manuals, all found by cross-checking the docs against the code: - `raw` is the model's verbatim answer, so `scripts` / `references` / `assets` may be omitted — "always present" was wrong; callers must treat a missing array as empty. - The simple-mode guarantee now also names `outputType` and reads "empty or absent", matching the validator. - `mode` accepts omitted / `null` / `""` for the default; the text previously implied `null` would be rejected. - from-source / from-openapi are *prompted* to produce a plain, file-free skill but do not enforce it server-side; "always produces the equivalent of simple" overstated that. - The multi-turn simple-mode retry ends on `error` unless the retry is a valid, file-free skill — not only "if the model still emits files". - Manuals use the wire value `invalid_mode` (matching the newer legend entries and the OpenAPI spec) instead of `INVALID_MODE`, and the §7.0 publish cross-reference points at §3.1 Create skill, not §2.2 (`/readyz`). No SKILL.md changed, so the KB digest is unaffected (regenerated: no diff). Contract tests pass. Part of #1242. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
…es (#1244) Four high advisories published since the last green `develop` run (2026-08-07) turned the required `audit` check red for every PR: - js-yaml 4.3.1 → 4.3.2 GHSA-2883-xcg3-v3hh (via @changesets/cli) - nanoid 3.3.17 → 3.3.19 GHSA-2v37-7h3g-55p8 (via vite) - browserslist 4.28.2 → 4.29.0 + update-browserslist-db 1.2.3 → 1.3.3 GHSA-c83g-rgw3-j3cx / GHSA-73wf-gq98-2v4g (via eslint-plugin-react-hooks → @babel/core) All are transitive dev / build tooling, none ships in ornn-api or ornn-web, so the bump goes through the root `overrides` block the repo already uses for transitive fixes (undici, brace-expansion, js-yaml) rather than adding direct dependencies. nanoid is pinned to the 3.x line vite requires, not 6.x. Verified with the exact CI command (`bun audit --audit-level=high --ignore=… ×3`): 0 vulnerabilities; lint, typecheck and `build:web` unchanged. Empty changeset — no shipped artifact changes. Closes #1244. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
…tive-bumps chore(deps): override js-yaml, nanoid, browserslist to patched releases (#1244)
…-generation-modes
`multipart mode=simple form field reaches the service` timed out at 5005 ms on a shared CI runner — the four tests in this file that boot their own harness (fresh app + MongoMemoryServer, needed to inject the LLM double) were on Bun's 5 s default, which is fine locally and on an idle runner but not under load. They now get the same 30 s budget the file's `afterAll` cleanup already has. Part of #1242. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
…ration-modes feat: skill generation modes — simple (SKILL.md only) vs advanced (#1242)
Dated notes file the develop → main gate requires (Step 0 of the release flow in CLAUDE.md). Covers everything on develop since v0.17.0: the skill generation modes feature (#1242) and the audit gate unblock (#1244, tooling-only, folded into the technical bullets). Empty changeset — docs only. Closes #1249. Claude-Session: https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh
…s-20260917 docs: release notes for the next release (skill generation modes)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Promotes
developtomainfor the next release. Pending changesets:skill-generation-modes(minor, both packages) and two empty ones → the bot will openrelease/v0.18.0.Contents since v0.17.0:
auditgate unblocked (transitive dev-dep overrides)..github/release-notes-20260917.md.Linked issue
Closes #1249
Type of change
develop→main)Changeset
.changeset/*.mdfiles for the release workflow to consume.Testing
Every commit passed the full CI matrix on its own PR;
check-release-notesgates this PR on the dated notes file.🤖 Generated with Claude Code
https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh