mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
<!-- orca-pr-loc -->
<!-- Programmatic LoC summary. Do not edit by hand; rewritten on every commit. -->
| | Files | Added | Deleted | Net |
| :--- | ---: | ---: | ---: | ---: |
| Test | 6 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$544 | $\color{#cf222e}{\Huge{\mathbf{−}}}$49 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$495 |
| Prod | 36 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$1719 | $\color{#cf222e}{\Huge{\mathbf{−}}}$1703 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$16 |
<!-- /orca-pr-loc -->
## ELI5
Orca ships eight skill guides that agents read before running the CLI. Seven of them (everything except `orchestration`, which #16904 rewrites) were command catalogs that had drifted from the binary. This PR rewrites them so an agent reads the outcome, the done bar, and the safe-failure rule first, loads reference material only at the step that needs it, and never sees a command or flag the installed CLI does not define.
## What changed
- **Seven guides rewritten** to one standard: outcome spine first (Result / Done / Safe failure), conditions instead of case lists, one done bar, one autonomy envelope, references loaded at the point of use via `skills get <topic> --full`, every runnable invocation spelled `ORCA`. `orca-cli` is 424→260 always-loaded lines with three references (browser, automations, publishing); `orca-per-workspace-env` is 794→397 with five (provider-vercel, ssh-host, docker-ssh, windows-scripts, failure-modes).
- **Defects fixed in shipped guides:** `emulator camera` (no such command), iOS `permissions` (backend refuses it), Android pane described as "in development" (shipped in June), `relayGracePeriodSeconds: 0` documented as immediate teardown (it is unbounded), doctor `ok: true` hiding `warn`, an SSH exemplar setting both `jumpHost` and `proxyCommand`, a provisioned-root fetch from `origin`, the Linear unconfirmed-write rule keyed on four verbs when ten emit it. Linear and emulator descriptions dropped embedded commands and angle-bracket placeholders (651→329, 732→404 chars).
- **Generator bundles references.** `skill-guides/<name>/references/*.md` is appended to `--full`; `skills get` help says compact by default, full with references.
- **Stubs single-authored.** The resolver ladder, placeholder rule, and older-binary fallback shared by all eight installable `SKILL.md` files come from one `skill-stubs/_shared/cli-resolution.md` fragment composed by the generator. Projections were byte-identical before the content fixes.
- **Guards:** every `ORCA <cmd>` and flag in every guide and reference resolves against `COMMAND_SPECS` (this found the camera defect); descriptions ≤1024 chars with no angle-bracket tokens; reference routing checked both directions; an always-loaded size ratchet (300 lines) that guides may leave but never join. `orchestration` (440 lines on main) is recorded as an exception until #16904 lands its kernel.
## Relationship to #16904
Split out of #16904 so that PR carries only the orchestration guide. On main, `terminal send` has no `--wait-submit` / `--retry-request` and the orchestration kernel still carries the resolver ladder and worktree-selector rule, so this branch pins `accepted: true` for handoff receipts and leaves the orchestration pins where main has them. The merge in either direction is mechanical: #16904 rebased on this becomes a one-file `orchestration.md` change plus dropping the two exceptions.
## Standard
Compound Engineering's portable skill-authoring guidance (outcome spine, conditions not cases, pinned fragile commands with an ordered hatch, references at point of use). NVIDIA SkillEvaluator Tier 1 (`schema,pii,license,quality,unicode,lint`) was run on every guide; its deterministic checks pass, its template nudges (Instructions/Examples sections, 50–150 char descriptions) do not apply to Orca's stub architecture and were not applied.
## Testing
- `pnpm typecheck:tsc:cli` clean; `check:code-quality:changed` and `check:react-doctor:changed` 0 findings
- `pnpm verify:bundled-skill-guides` and skill-bundle manifest verify clean
- vitest over `config/scripts`, `src/cli/skill-guide-cli-parity.test.ts`, `src/cli/skills.test.ts`, `src/cli/specs/skills.test.ts`, `src/cli/help.test.ts`, `src/main/skills`: 240 files / 2,019 pass
- Live smoke on the built CLI of every `skills get <topic>` and `--full`, every emulator, linear, and vm verb named in the guides, and every projection's resolver, GNOME warning, and bounded fallback (done on the #16904 branch before the split; the guide bodies are identical here except the send-receipt vocabulary noted above)
## Deferred product decisions
Merging `orca-emulator` and `orca-emulator-android` into one skill with a platform branch; collapsing `linear-tickets` to a guide alias; a `skills get --reference <name>` selector so a gate table can load one file; a fresh-agent routing eval before trimming the `orca-cli` (1,015 chars) and `orchestration` descriptions, whose quoted triggers each fixed a routing misroute.
163 lines
4.9 KiB
JavaScript
163 lines
4.9 KiB
JavaScript
// Why: the resolver ladder, the placeholder rule, the no-guessing paragraph, and the
|
|
// older-binary fallback frame are byte-identical in every discovery stub and had already
|
|
// drifted wherever they were re-authored. One fragment owns them; each per-topic stub only
|
|
// marks where they land.
|
|
const SHARED_STUB_SOURCE = 'skill-stubs/_shared/cli-resolution.md'
|
|
const BLOCK_DEFINITION_PATTERN = /^<!-- block: (?<id>[a-z][a-z0-9-]*)(?<reflow> reflow)? -->$/u
|
|
const INSERTION_MARKER_PATTERN = /^<!-- shared: (?<id>\S+) -->$/u
|
|
const TOPIC_PLACEHOLDER = '{{topic}}'
|
|
// Why: the stub corpus is hand-wrapped at 92 columns. A topic-substituted paragraph must
|
|
// re-wrap to that width, or every topic ships a differently ragged copy of one sentence.
|
|
const REFLOW_WIDTH = 92
|
|
|
|
function countBackticks(text) {
|
|
let count = 0
|
|
for (const character of text) {
|
|
if (character === '`') {
|
|
count += 1
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
// Why: a backticked command must never be split across lines, so a code span is one token.
|
|
function atomicTokens(text, sourcePath) {
|
|
const tokens = []
|
|
let span = null
|
|
for (const word of text.split(/\s+/u)) {
|
|
if (!word) {
|
|
continue
|
|
}
|
|
if (span !== null) {
|
|
span += ` ${word}`
|
|
if (countBackticks(span) % 2 === 0) {
|
|
tokens.push(span)
|
|
span = null
|
|
}
|
|
continue
|
|
}
|
|
if (countBackticks(word) % 2 === 1) {
|
|
span = word
|
|
continue
|
|
}
|
|
tokens.push(word)
|
|
}
|
|
if (span !== null) {
|
|
throw new Error(`Shared stub block has an unclosed code span: ${sourcePath}`)
|
|
}
|
|
return tokens
|
|
}
|
|
|
|
function reflowParagraph(text, sourcePath) {
|
|
const lines = []
|
|
let current = ''
|
|
for (const token of atomicTokens(text, sourcePath)) {
|
|
if (!current) {
|
|
current = token
|
|
} else if (current.length + 1 + token.length <= REFLOW_WIDTH) {
|
|
current += ` ${token}`
|
|
} else {
|
|
lines.push(current)
|
|
current = token
|
|
}
|
|
}
|
|
if (current) {
|
|
lines.push(current)
|
|
}
|
|
return lines.join('\n')
|
|
}
|
|
|
|
// Lines before the first `<!-- block: -->` are the fragment's own header comment and are
|
|
// not projected. Input must already be LF-normalized.
|
|
function parseSharedStubBlocks(markdown, sourcePath) {
|
|
const blocks = new Map()
|
|
let open = null
|
|
const close = () => {
|
|
if (!open) {
|
|
return
|
|
}
|
|
const text = open.lines.join('\n').replace(/^\n+/u, '').replace(/\n+$/u, '')
|
|
if (!text) {
|
|
throw new Error(`Shared stub block is empty: ${sourcePath} (${open.id})`)
|
|
}
|
|
blocks.set(open.id, { text, reflow: open.reflow })
|
|
}
|
|
for (const line of markdown.split('\n')) {
|
|
const definition = BLOCK_DEFINITION_PATTERN.exec(line)
|
|
if (!definition) {
|
|
if (open) {
|
|
open.lines.push(line)
|
|
}
|
|
continue
|
|
}
|
|
close()
|
|
const { id, reflow } = definition.groups
|
|
if (blocks.has(id)) {
|
|
throw new Error(`Shared stub block is defined twice: ${sourcePath} (${id})`)
|
|
}
|
|
open = { id, reflow: Boolean(reflow), lines: [] }
|
|
}
|
|
close()
|
|
if (blocks.size === 0) {
|
|
throw new Error(`Shared stub source defines no blocks: ${sourcePath}`)
|
|
}
|
|
return blocks
|
|
}
|
|
|
|
function renderBlock(block, topic, sourcePath) {
|
|
const text = block.text.replaceAll(TOPIC_PLACEHOLDER, topic)
|
|
return block.reflow ? reflowParagraph(text, sourcePath) : text
|
|
}
|
|
|
|
// Why: an insertion that silently vanished would let a stub drop the safety ladder while the
|
|
// generator stayed green, so an unknown marker and a missing or repeated insertion both throw.
|
|
function renderSharedStubBody(stubBody, { topic, blocks, sourcePath }) {
|
|
const insertions = new Map()
|
|
const composed = stubBody
|
|
.split('\n')
|
|
.map((line) => {
|
|
const marker = INSERTION_MARKER_PATTERN.exec(line)
|
|
if (!marker) {
|
|
return line
|
|
}
|
|
const { id } = marker.groups
|
|
const block = blocks.get(id)
|
|
if (!block) {
|
|
throw new Error(
|
|
`Unknown shared stub block "${id}" in ${sourcePath}. Known blocks: ${[...blocks.keys()].join(', ')}`
|
|
)
|
|
}
|
|
insertions.set(id, (insertions.get(id) ?? 0) + 1)
|
|
return renderBlock(block, topic, SHARED_STUB_SOURCE)
|
|
})
|
|
.join('\n')
|
|
|
|
for (const [id, block] of blocks) {
|
|
const count = insertions.get(id) ?? 0
|
|
if (count !== 1) {
|
|
throw new Error(
|
|
`${sourcePath} must insert <!-- shared: ${id} --> exactly once; found ${count}.`
|
|
)
|
|
}
|
|
// Why: re-inlining a copy beside the marker is exactly the drift this fragment ends.
|
|
const [firstLine] = renderBlock(block, topic, SHARED_STUB_SOURCE).split('\n')
|
|
if (stubBody.includes(firstLine)) {
|
|
throw new Error(
|
|
`${sourcePath} re-inlines shared block "${id}"; insert it with a marker instead.`
|
|
)
|
|
}
|
|
}
|
|
if (composed.includes(TOPIC_PLACEHOLDER)) {
|
|
throw new Error(`Shared stub block left an unsubstituted placeholder in ${sourcePath}.`)
|
|
}
|
|
return composed
|
|
}
|
|
|
|
export {
|
|
REFLOW_WIDTH,
|
|
SHARED_STUB_SOURCE,
|
|
parseSharedStubBlocks,
|
|
reflowParagraph,
|
|
renderSharedStubBody
|
|
}
|