Skip to content

release: promote develop to main (v0.18.0 — skill generation modes) - #1251

Merged
chronoai-shining merged 29 commits into
mainfrom
develop
Sep 17, 2026
Merged

chronoai-shining merged 29 commits into
mainfrom
develop

Conversation

@chronoai-shining

Copy link
Copy Markdown
Collaborator

Summary

Promotes develop to main for the next release. Pending changesets: skill-generation-modes (minor, both packages) and two empty ones → the bot will open release/v0.18.0.

Contents since v0.17.0:

Linked issue

Closes #1249

Type of change

  • Release promotion (developmain)

Changeset

  • Carries the pending .changeset/*.md files for the release workflow to consume.

Testing

Every commit passed the full CI matrix on its own PR; check-release-notes gates this PR on the dated notes file.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Pi6Ymxei9vAupEWmt3gjxh

chronoai-shining and others added 29 commits August 7, 2026 15:41
`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)
`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)
@chronoai-shining
chronoai-shining merged commit 6963f69 into main Sep 17, 2026
21 checks passed
Sign up for free to 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