Skip to content

Release: develop → main (GitHub source auto-sync + chrono-bucket downloads)#1200

Merged
chronoai-shining merged 57 commits into
mainfrom
develop
Jul 7, 2026
Merged

Release: develop → main (GitHub source auto-sync + chrono-bucket downloads)#1200
chronoai-shining merged 57 commits into
mainfrom
develop

Conversation

@chronoai-shining

Copy link
Copy Markdown
Collaborator

Promote developmain for the next release. Carries the accumulated features + unconsumed changesets + the dated release-notes file (.github/release-notes-20260707.md).

Highlights this cycle:

  • GitHub source auto-sync — scheduled drift detection, opt-in auto-publish of drifted GitHub-sourced skills, and auto-sync status badges on the skill detail page.
  • chrono-bucket download migration — skill package downloads now stream through an ornn-api route instead of presigned storage URLs.
  • Raised skill description cap — 1024 → 1536 characters.

Merging this triggers the changeset-release workflow (opens the release/v<next> → main version-bump PR).

chronoai-shining and others added 30 commits June 30, 2026 18:24
…1180)

Claude Code auto-invokes a skill by matching its frontmatter `description`
against user intent — the SKILL.md body is not consulted for routing. The
old description was a static capability summary ("search / pull / execute /
build / upload / share") with no trigger phrases and no mention of
skillsets, plugin export, ownership transfer, GitHub sync, audits, or
quota/model selection, so whole request categories never routed here.

Rewrite it as an explicit "load and follow this WHENEVER the user asks
to…" trigger list that names the concrete phrases an agent will hear,
including every skillset operation and Claude Code plugin export. Kept to
1400 chars to stay under Claude Code's ~1536-char description budget so no
trigger is truncated, with the newer skillset/plugin triggers included
rather than buried past the cap. Also expand the in-doc "always load"
bullet for §2.16 to surface plugin-export + ownership-transfer.

Part of #1180. Related to #1085.
The manual documented `PUT /skillsets/:id/permissions` in three places —
the §1.5 permission catalogue, api-reference §5a.6, and the §5a SDK line
(`setSkillsetPermissions` / `set_skillset_permissions`). No such endpoint
exists: skillset visibility is DERIVED from member skills, not owner-set,
and the route was deliberately omitted (#1136 — routes.ts carries an
explicit "there is deliberately NO PUT /skillsets/:id/permissions" note).
Documenting it sends an agent to a 404.

Excise all three references. The §1.5 row now points at the real
`PUT /skillsets/:id/plugin-export` + `POST /skillsets/:id/transfer-ownership`
(documented in the follow-up commit). The dead SDK methods are dropped from
the manual's SDK line here; the SDK itself still ships `setSkillsetPermissions`
against the removed route — that is separate SDK drift tracked in #1013.

Part of #1180. Closes #399.
…ract (#1180)

Bring the skillset section in line with the code:

- Preamble: correct "visibility-scoped / visibility mirrors skills" — a
  skillset's visibility is DERIVED from members (#1136), not owner-set.
  Add two model notes: derived visibility (memberVisibilityState semantics,
  unreadableMembers, no-permissions-endpoint / expose-members widen-reach)
  and auto-revision (system-managed <major>.<minor>, reactive re-cut).
- §5a.1 create / §5a.5 publish: drop the phantom `version` request field
  (revisions are system-assigned/auto-bumped), tighten member constraints
  (2..100, ≤115 chars, no `skillset:` nesting).
- §5a.2 get: replace the stale response with the real SkillsetDetail —
  memberVisibilityState, unreadableMembers, publicMemberCount, exportAsPlugin,
  pluginConfig — and flag isPrivate/sharedWith*/grants as inert legacy.
- §5a.4 closure: document the ClosureNode item shape { ref, name, version,
  depth, guid?, skillHash? }.
- Add §5a.6 plugin-export (#1157, ≥2-public-members precondition) and
  §5a.7 transfer-ownership (#1123); renumber delete/search.
- Add §5a.10 skillset error-code table; add transferSkillsetOwnership to
  the SDK line; correct the search params (q, scope enum, result shape).

Part of #1180. Closes #1085.
The skillset workflow was physically misplaced (after §3 Conventions and
§4 References), deferred its contract to the WRONG file (the sibling
`chrono-ai-service-manual`'s `ornn-api-reference.md` instead of this
bundle's local `references/api-reference.md`, which already ships §5a),
repeated the phantom "permissions … mirror the skill endpoints" claim, and
offered no runnable commands.

Move it to its numeric home right after §2.15, point it at the local §5a,
and turn it into a real operational recipe: runnable `nyxid proxy request`
snippets for search/closure, create, publish, plugin-export, transfer, and
delete; a "no version field / revisions auto-bump" warning; and a
derived-visibility troubleshooter ("why can't my teammate see the skillset
I shared" → expose the member skills, there is no permissions endpoint).
Also fix the stale "thirteen use cases" intro (§2 now spans 16
subsections).

Part of #1180. Closes #1085.
Stamp the skill's registry version 1.3 → 1.4 and lastUpdated, reflecting
the trigger-oriented description + skillset-lifecycle refresh in this PR.

Empty changeset: the change is skill documentation only — the skill is
registry-sourced, not bundled into the ornn-api / ornn-web images, so no
package version bump. Publishing the refreshed content to the live
registry is a separate operational step (manual §2.3).

Part of #1180.
The cron-expression Zod refine lived inline in the mirror settings section. The upcoming source-sync section (#1175) needs the identical validator, so lift it into sections/cronSchedule.ts and have mirror import it. Pure refactor — no behavior change; the validation semantics (empty = disabled, else cron-parser must accept) are unchanged.
New admin-editable 'sourceSync' section carrying the config for automatic GitHub source sync: enabled flag, a service-account githubToken (declared in secretFields so it is encrypted at rest, mid-masked on GET, and redacted on export — same machinery as mirror.appPrivateKey), a pollSchedule cron, minCheckIntervalMinutes, and the autoPublish master switch (consumed by later phases #1176/#1177).

The section ships inert (enabled=false, empty token, autoPublish=false) so merging changes nothing at runtime. Wire getSourceSync() through the SettingsService interface + impl, and update the three exportImport test fakes that structurally implement the interface.

Part of #1175
Add an optional Authorization bearer to every api.github.com request in the pull path (raw.githubusercontent.com downloads stay anonymous — different host, separate budget, and no reason to leak the token to a CDN), so source reads can escape the unauthenticated 60-req/hr-per-IP ceiling. Verified against GitHub docs: authenticated requests get 5,000/hr and a conditional 304 does not count against it.

Add resolveRefHeadSha(): the cheap drift probe. It resolves a branch/tag HEAD via the lightweight git/ref endpoint (~200-byte body vs the multi-KB commits API) with If-None-Match support (304 = free no-op). A 40-hex ref is a pinned commit and short-circuits with zero HTTP calls; a heads 404 falls back to tags before throwing the typed GitHubSourceNotFoundError (kept as a domain error rather than the HTTP-layer AppError so this low-level util stays decoupled).

Part of #1175
Add upstreamHeadSha, etag, lastCheckedAt, and a driftState enum (in_sync | drifted | changed_unversioned | broken) to SkillSource — on both the internal type and the serialized response shape (etag stays internal-only; it is a cache detail, not client-facing). This turns drift from something only discoverable via an active preview dry-run into passive, queryable state.

mapDoc coerces the new fields with the same absent-field care as lastSyncedAt (no fabricated Invalid Date). Add updateSourceDriftState(guid, patch): a targeted setter that $sets only the source.* drift dot-paths — it never rewrites the whole source object (so it can't clobber a concurrent refresh's lastSyncedCommit) and deliberately does not bump updatedOn/updatedBy, since a background drift check is not a user edit.

Part of #1175
)

Add runSourceDriftCheck (in its own module — service.ts is already 2200+ lines) which probes the upstream HEAD, compares it to lastSyncedCommit, and persists the verdict: 304/equal -> in_sync, differing -> drifted (stores upstreamHeadSha + etag), GitHubSourceNotFoundError -> broken. It NEVER re-pulls or publishes. Transient (non-404) failures re-throw so a network blip is not mislabelled as broken; the scheduler (#1176) will retry.

SkillService gains a thin checkSourceDrift(guid) delegating to it, plus resolveSourceSyncToken() (settings token > ORNN_SOURCE_SYNC_GITHUB_TOKEN env fallback > anonymous). The token is threaded into the existing create/refresh pull paths too, so the manual refresh authenticates when a token is configured. Token is trimmed and never logged. Config + bootstrap wire the env fallback and the settings reader.

Part of #1175
Two skill-lifecycle endpoint families existed in ornn-api but were absent
from the manual:

- §3.16 POST /skills/:id/transfer-ownership (#1123) — ADMIN-tier hand-off,
  prior owner kept as READ, target must be a known Ornn user.
- §3.17 GET/PUT/DELETE /skills/:id/dist-tags[/:tag] (#463) — npm-style
  named version pointers, `latest` auto-managed + immutable, tag grammar
  /^[a-z][a-z0-9-]{0,49}$/, resolvable via @<tag> refs.

Both grounded verbatim against crud/routes.ts + service.ts (auth tiers,
scopes, bodies, response shapes, error codes).

Part of #1180. Closes #399.
…1180)

- §8a POST /assistant/chat (#970) — repo-aware, non-agentic Q&A SSE with
  the real four-event contract (chat_start / chat_text_delta / chat_finish
  / chat_error), 30/min rate limit, and `assistant`-surface quota charging.
  Verified event names + limit against assistant/chatService.ts + routes.ts.
- §6.3 GET /skill-manifest-schema.json (#464) — public draft-2020-12 schema
  for SKILL.md frontmatter, flagged as the one endpoint that returns a raw
  (un-enveloped) body for editor `$schema` consumption.

TOC gains an 8a entry.

Part of #1180. Closes #399.
- §2.4 GET /github/repo — public mirror coordinates {owner,repo,branch,
  enabled} (credentials never exposed); the install-snippet source.
- §13.8 POST /github/repo — admin direct mirror-config patch (kill-switch,
  coords, GitHub App creds; secrets mid-masked; confirmAbandonOldRepo guard).
- §11.13 GET /me/launch-promo — caller's promo claim status (#724).
- §13.11 admin launch-promo award + recent-claims observability (#724).

Part of #1180. Closes #399.
…ted-github-source-reads

[Feature] Authenticated + conditional GitHub source reads and drift-detection model (#1175)
…1180)

- §14.3: the per-model PATCH body was missing the `assistant` surface flags
  (enabledForAssistant / defaultForAssistant) added by #970, and the section
  intro said "two SSE surfaces" — now three (playground/skillGen/assistant).
  Added the ≥1-flag + atomic-default invariants and MODEL_NOT_FOUND/
  MODEL_REMOVED errors.
- Quota scope: the manual documented `ornn:quota:admin` for quota /
  redemption-codes / dashboard-stats, but that string exists nowhere in the
  API — QUOTA_ADMIN_PERMISSION is an alias whose value is `ornn:admin:skill`
  (quota/types.ts:36). Corrected §1.5 (both files), §13.6, and §13.7 to the
  real scope and folded the redundant row into `ornn:admin:skill`.

Part of #1180. Closes #399.
The refresh grew beyond skillsets to a full api-reference drift pass
(skill transfer-ownership, dist-tags, assistant SSE, manifest schema,
github/repo mirror coords, launch-promo, per-model assistant surface,
quota-scope fix). Reflect that in the changeset summary. Still docs-only.

Part of #1180.
…t-refresh

docs(skill): refresh ornn-agent-manual-cli to v1.4 — trigger description + skillset lifecycle + api-reference drift
resolveRefHeadSha now classifies a GitHub rate-limit response (429, or 403 with X-RateLimit-Remaining:0 / a Retry-After header) into a typed GitHubRateLimitError carrying the recommended wait (from Retry-After or X-RateLimit-Reset). Other non-OK statuses stay generic 'transient' errors. This lets the batch drift scheduler back off cleanly instead of hammering a limited API.

Part of #1176
Extract the probe-result-to-verdict mapping (classifyProbeResult) and the settings>env>anonymous token resolution (pickSourceSyncToken) out of the single-skill drift path so the upcoming batch job can reuse them verbatim — the batch probes once per (repo,ref) group and classifies each member against its own lastSyncedCommit. No behavior change: runSourceDriftCheck and SkillService.resolveSourceSyncToken now delegate to the shared helpers.

Part of #1176
Add findGithubSourcedSkills({ notCheckedSince }) — enumerates github-sourced skills due for a check (excluding pinned 40-hex refs via a $not-regex, and skills checked more recently than the cutoff), returning light { guid, source, ownerId } rows. Back it with a partial index over github sources ordered by lastCheckedAt. Extract the source-coercion out of mapDoc into a shared coerceSkillSource helper so both read paths stay consistent.

Part of #1176
Add the skill.source_broken category to the canonical NOTIFICATION_CATEGORIES vocabulary (so the boot migration's allow-list can't wipe it) and a notifySourceBroken() method that tells a skill's owner their linked GitHub source could not be resolved, deep-linking to the skill so they can re-link it.

Part of #1176
)

runSourceDriftJob enumerates due github-sourced skills, coalesces them by (repo,ref) so a shared upstream costs exactly one probe, and persists each skill's drift verdict. A broken source marks the skill broken and notifies its owner; a rate-limit signal short-circuits the rest of the tick (the next fire retries); pinned refs are skipped; probes run under a bounded concurrency with optional jitter. No single skill's persist/notify/probe failure is allowed to abort the run. Records state only — no publish (that is #1177).

Part of #1176
createSourceSyncScheduler clones the mirror scheduler's multi-pod-safe Agenda pattern (per-fire row lock on agendaJobs) for the inbound drift job: a source-drift-check job whose cron cadence is driven by settings.sourceSync.pollSchedule (Asia/Singapore), plus a 1-minute sync tick that re-registers it cluster-wide when an admin changes the cadence. Exposes getScheduledRunStatus() for parity with the mirror scheduler.

Part of #1176
Construct + start the source-sync scheduler next to the mirror scheduler, injecting the drift job with the skill repo, settings, env token fallback, and the notification service. A start failure logs and leaves this pod without the scan rather than crashing boot; shutdown stops it before the mirror scheduler.

Part of #1176
…ft-scheduler

[Feature] Scheduled GitHub source drift-check job (#1176)
chronoai-shining and others added 27 commits July 1, 2026 16:14
…#1185)

The v1.4 description (#1183) was an unquoted YAML plain scalar containing
"SKILLSET request: bundle" — a ': ' colon-space makes the yaml parser read
a nested mapping, so ingest/refresh failed with INVALID_FRONTMATTER
("Nested mappings are not allowed in compact mappings"). skip_validation
does not bypass it — YAML parse precedes format validation. The value was
also ~1400 chars vs the schema's 1024 cap (skillFrontmatter.ts:225).

Reword the offending clause (colon → semicolon/comma) and trim to 993
chars while keeping the trigger-oriented content. Verified with the API's
own yaml.parse + validateSkillFrontmatter (both pass).

Fixes #1185. Regression from #1183.
fix(skill): valid YAML + ≤1024 description for ornn-agent-manual-cli frontmatter
Add the skill.source_drift_detected / skill.auto_synced / skill.auto_sync_failed PlatformActivityAction telemetry events, the skill.auto_synced / skill.auto_sync_failed notification categories (in the canonical vocabulary so the boot migration can't wipe them), and notifyAutoSynced / notifyAutoSyncFailed methods that tell an owner a version auto-shipped (from→to) or why a re-publish was refused.

Part of #1177
Add SkillService.autoPublishFromSource(guid): re-pulls + publishes a drifted github-sourced skill under a dedicated SYSTEM_SYNC_ACTOR (so the audit trail shows auto-sync, not the linking user), running the SAME validation as a manual refresh. It never throws for known outcomes — it returns a typed AutoPublishOutcome: published / changed_unversioned (updateSkill's VERSION_NOT_INCREMENTED guard fires before any write, so the immutable version is never clobbered) / validation_failed (broken) / skipped (idempotency: not drifted / already-synced HEAD) / error (transient, left drifted for retry). Reuses updateSkill's authoritative guards rather than duplicating a pre-pull version check.

Part of #1177
On a detected drift the job now emits skill.source_drift_detected (always) and, when settings.sourceSync.autoPublish is on, invokes the injected autoPublish callback and maps the outcome to an owner notification + telemetry (auto_synced / auto_sync_failed). Guarded so an auto-publish failure or throw never aborts the tick. Adds autoPublished / autoSyncFailed to the run summary.

Part of #1177
…1177)

Inject SkillService.autoPublishFromSource + the analytics emitter into the drift job so the scheduled scan auto-publishes when sourceSync.autoPublish is on. Ships off by default (the setting defaults false).

Part of #1177
…sh-on-drift

[Feature] Unattended auto-publish of drifted GitHub-sourced skills (#1177)
The detail-response serializer now includes source.driftState, source.lastCheckedAt (ISO), and source.upstreamHeadSha so the frontend can render the auto-sync badge from the last scheduled check without a bespoke endpoint. etag stays internal (a conditional-request cache detail, not client-facing).

Part of #1178
…1178)

Extend the frontend SkillSource with driftState / upstreamHeadSha / lastCheckedAt, and add the skill.source_broken / skill.auto_synced / skill.auto_sync_failed notification categories.

Part of #1178
…#1178)

Add SourceDriftBadge (DESIGN.md state tokens: success/info/warning/danger, each paired with copy + a tooltip) and render it in GitHubOriginChip (next to 'Synced from GitHub', manual Refresh button preserved) and the AdvancedOptionsModal 'Currently linked' block. Bilingual strings added to en.json + zh.json.

Part of #1178
…#1178)

useSourceDriftProbe re-reads the skill detail once when a github source's last drift check is stale (ref-guarded, staleness-gated, no timer/storm; backend surfaces driftState on GET so a plain refetch suffices). Wire it into useSkillDetail and add CATEGORY_LABEL entries for the three new notification categories in both the modal and the notifications page.

Part of #1178
… Code)

Claude Code truncates the combined `description` (+ optional `when_to_use`)
at 1536 chars when deciding whether to auto-invoke a skill (the
`skillListingMaxDescChars` default). Our schema capped `description` at
1024, so a description rich enough to use Claude Code's full routing budget
could not validate — the only way to ship one was `skip_validation`, which
also downgrades every other frontmatter check to best-effort.

Raise the cap to 1536 in the api schema (new `SKILL_DESCRIPTION_MAX`
constant), the ornn-web mirror schema, and the format-rules text, so a
richer description validates normally. Add a boundary test (1536 passes,
1537 fails). Skillset `description` caps are intentionally left at 1024 —
different field, different surface.

Part of #1180.
With the cap raised to 1536 (prior commit), expand the description to use
Claude Code's full routing budget: enumerate the skill triggers (search /
pull / run / build / publish / visibility / audit / deprecate / diff /
analytics / service-bind / GitHub sync / dist-tags / transfer) AND the
skillset triggers (bundle / create / publish / closure / plugin-export /
transfer / derived-visibility), plus natural-language phrases. 1472 chars,
under the 1536 cap with margin.

Double-quoted the YAML value so an embedded colon can never break the
frontmatter parse again (the v1.4 regression, #1185). Verified with the
API's yaml.parse + validateSkillFrontmatter. Bump 1.4 → 1.5.

Part of #1180.
[Feature] Surface auto-sync status: drift badges + notifications (#1178)
…ption-cap

feat(api): raise skill description cap to 1536 + richer manual description
…#1196)

chrono-bucket (replacing chrono-storage) removed the presigned-URL and
object-copy endpoints and now serves downloads from a streaming
GET /api/buckets/:bucket/objects/download?key=. Adapt ornn-api:

- StorageClient: add downloadObject() (raw-bytes streaming read); remove
  the now-dead getPresignedUrl() and copy() (copy had no callers).
- SkillService: route the internal byte reads (diff, /json, rescan)
  through downloadObject; add a gated getPackageBytes() that funnels
  version->storageKey resolution + the visibility gate (shared with
  getSkillJson), and stop minting a client-facing presigned URL in
  buildDetailResponse. SkillDetailResponse.presignedPackageUrl is dropped
  so no direct-to-storage URL ever reaches a client.
- Register GET /skills/:idOrName/versions/:version/download — the route
  the TS SDK's downloadPackage() already targets — streaming the ZIP
  through ornn-api (application/zip, attachment). No analytics pull is
  recorded: the endpoint also backs the web file-tree viewer, so counting
  UI views would inflate the pull metric (programmatic reads use /json).
- AuditService: fetch package bytes via skillService.getPackageBytes
  (SYSTEM_ACTOR) instead of the skill doc's presigned URL, deleting the
  wasted presigned round-trip and cast-riddled fallback (#995).
- Drop presignedPackageUrl from the OpenAPI schema + spec; document the
  new download path.

Part of #1196. Closes #995.
The web viewer used to fetch the skill ZIP straight from chrono-bucket /
MinIO via the presigned URL on SkillDetail. chrono-bucket removed
presigned URLs and now sits behind the NyxID proxy, so route the
download through ornn-api instead:

- apiClient: extract rawFetchWithRetry (auth + refresh + 401-retry in one
  place) and add apiGetBinary(), an authenticated GET returning raw bytes.
- useSkillPackage(guid, version): fetch GET /skills/:idOrName/versions/
  :version/download via apiGetBinary + JSZip, replacing fetch(presignedUrl).
  A non-2xx surfaces as ApiClientError -> the same translated
  "download failed" payload the direct-fetch path produced.
- Point the three callers (useSkillDetail, usePlaygroundSession,
  SkillsetMemberViewer) at (skill.guid, skill.version) and drop
  presignedPackageUrl from the SkillDetail type + test fixtures.

Part of #1196.
#1196)

The new GET /skills/:idOrName/versions/:version/download route is
optionalAuth and anonymous-reachable, so its object-level (BOLA) gate
lives entirely in SkillService.getPackageBytes. That gate is a distinct
copy from getSkillJson's, and the route test mocks getPackageBytes — so
nothing exercised the real gate. Deleting the canReadSkill check would
have shipped green while streaming private-skill ZIPs to anonymous
callers. Add real-service tests (private+stranger -> skill_not_found
before any download; public+stranger -> bytes; private+owner/SYSTEM_ACTOR
-> bytes) so that regression can never pass silently.

Part of #1196.
useSkillPackage called apiGetBinary("/skills/:guid/versions/:version/
download") without the /api/v1 prefix that every other apiClient path
carries, so the browser's request 404'd at the NyxID proxy and the skill
detail page showed "failed to load package contents". The in-repo route
and the SDK both work; only the web path was wrong. Component tests mock
useSkillPackage wholesale, so nothing caught it — add a focused hook test
asserting the exact proxied path (/api/v1/skills/:guid/versions/:version/
download) alongside the fix.

Part of #1196.
…ket-download-proxy

feat: proxy skill package downloads through ornn-api for chrono-bucket (#1196)
…60707

docs: release notes for the next release (GitHub source auto-sync)
@chronoai-shining chronoai-shining merged commit 2687d96 into main Jul 7, 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