stablyai/orca · error

Targeted update changed the non-targeted control skill

Error message

Targeted update changed the non-targeted control skill

What it means

The round-trip's central invariant: a targeted 'skills update <targetName> --global' must NOT touch the control skill (orchestration). After the update, the script hashes controlCanonical and compares to controlBefore; a mismatch means the targeted update leaked into the non-targeted skill.

Source

Thrown at config/scripts/verify-skill-update-roundtrip.mjs:238

  if (
    shape === 'copy' &&
    targetProviderAfter !== targetProviderBefore &&
    targetProviderAfter !== currentSkill(targetName).packageDigest
  ) {
    throw new Error('Independent provider copy changed to an unexpected package identity')
  }
  if (shape === 'copy') {
    // Why: hosted 1.5.17 replaces copies with aliases while equivalent local runs
    // retain the copy. Both prove this input topology must remain ineligible.
    const outcome = targetProviderStat.isSymbolicLink()
      ? 'converged to an alias'
      : targetProviderAfter === targetProviderBefore
        ? 'remained a historical copy'
        : 'converged as a copy'
    console.log(`[skill-update-roundtrip] independent copy ${outcome}`)
  }
  if ((await packageDigestAt(controlCanonical)) !== controlBefore) {
    throw new Error('Targeted update changed the non-targeted control skill')
  }
  if ((await packageDigestAt(await realpath(controlProvider))) !== controlProviderBefore) {
    throw new Error('Targeted update changed the non-targeted control provider placement')
  }
  const controlProviderStat = await lstat(controlProvider)
  if (shape === 'symlink' && !controlProviderStat.isSymbolicLink()) {
    throw new Error('Targeted update changed the non-targeted control topology')
  }
} finally {
  await rm(sandbox, { recursive: true, force: true })
}

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Inspect the lock fixture written before the update — the control skill must be pinned at controlBefore so it's not eligible.
  2. Diff controlCanonical before/after to see what the update changed.
  3. File a CLI bug: a targeted update by name must not modify other skills' canonical placements.
  4. Pin a CLI version whose targeted update is correctly scoped.
Defensive patterns

Strategy: validation

Validate before calling

// Before the targeted update, confirm the lock pins the control skill so it's not eligible:
const lock = JSON.parse(await readFile(lockPath, 'utf8'))
const controlPinned = lock.skills?.some((s) => s.name === controlName && s.releaseRevision === currentSkill(controlName).releaseRevision)
if (!controlPinned) {
  throw new Error(`Lock does not pin control skill ${controlName}; targeted update may leak into it`)
}

Prevention

When it happens

Trigger: The CLI's targeted update doesn't actually scope to the named skill and rewrites all canonical placements; the lock file was set up wrong so the control skill appears eligible; a shared file between skills got rewritten.

Common situations: CLI regression that ignores the skill-name argument and updates globally; lock-file fixture in the test missing the control skill's pin so it looks updatable; a refactor that made skills share a directory and a targeted update mutates the shared dir.

Related errors


AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12). Data as JSON: /api/errors/1f8c9dc28a1922c0. Report an issue: GitHub.