tim-inkeep 3488478ece fix(ok): repair skill lifecycle contracts (PRD-8343) (#4266)
* test(ok): reproduce PRD-8343 phantom re-projection

* fix(ok): make cross-scope skill moves truthful

* test(ok): reproduce PRD-8343 warning-code skew

* fix(ok): align imported skill warning codes

* test(ok): reproduce PRD-8343 ignored install mode

* fix(ok): reject unscoped install modes

* chore(ok): add PRD-8343 changeset

* fix(ok): route MCP cross-scope skill moves through move-scope, plumb warning codes everywhere

Review follow-up for the PRD-8343 skill lifecycle PR.

The MCP move tool now posts cross-level skill moves to POST /api/skill/move-scope, the same atomic endpoint the Skills Studio uses: the whole bundle moves, binary files included, the skills-lock entry transfers, and the prior editor hosts are re-projected. The endpoint accepts an optional toName so a renaming move lands directly under the new name with its SKILL.md name patched; nothing is written when that name is taken at the destination. The hand-composed PUT/PUT/DELETE path and its skippedBinaryFiles and bothScopes output fields are gone, and the tool prose goes back to describing re-projection truthfully.

warningCodes now ships on PUT /api/skill, POST /api/skill/reimport and both bulk result schemas, aligned 1:1 with warnings, under SKILL_AUTHORING_WARNING_CODES (named for the shared writer that mints them). PUT /api/skill no longer returns each warning twice on create. The MCP write tool exposes warnings and warningCodes in its structured skill payload.

install documents the mode-needs-add-or-convert refusal in both the description and the field text, and runs both argument-pair guards before contacting the server.

Tests: two-warning ordering cases in skills-write and import, a move.test.ts covering skill dispatch, and integration tests for the MCP move (hosts, binaries, lock entry, rename collisions) and for warning codes across every entry point. The changeset names @inkeep/open-knowledge so the note reaches the docs changelog.

* fix(PRD-8343): preserve skill recovery copies and close review gaps

* fix(PRD-8343): report partial skill moves and align warning contracts

* fix(PRD-8343): report unknown source loss and mirror bundle-file failures

The source-intactness probe on the retained-copy branch called parseSkillDir
without a guard, on the one branch that only runs after rmSync just threw a
permission error on that same tree, so a bundle subdirectory that blocks both
walks sent the request to the outer catch and replaced moveState
destination-retained with partially-applied, losing the pointer to the recovery
copy. Its fallback arm also checked one file and then selected the detail that
asserts nothing was lost. It is now a three-state intact/lossy/unknown probe
behind a try, and unknown gets its own wording that claims nothing.

MCP write's combined SKILL.md-plus-bundle-files result spread the inner
writeSkill text over the composed body, so on clients that hide content[] a
per-file failure was invisible beside isError true. handleSkillDuplicate
discarded both applySkillDelete cleanup results, reporting a clean 400 while a
stray copy stayed on disk.

The changeset said moveState rides every failure after the copy, which excludes
nothing-written, the first code in its own list.

* fix(PRD-8343): pass the source scope when checking unprojectable skill hosts

Main made resolvedHosts scope-aware so global-scope hosts resolve against
USER_SKILL_EDITOR_IDS. The unprojectable helper added on this branch still
called it with one argument, which broke the merged tree that CI builds and
turned every Open Knowledge check red. The helper only ever inspects
source-side hosts, so fromScope is the scope its siblings already pass.

* fix(PRD-8343): make the move-scope machine-readable fields describe what they report

droppedLocations no longer mixes bundle directories into a list of install
location ids. The placement-ledger term added nothing correct: knownSkillRootsFor
already registers each recorded root as a pseudo-host, so the two unprojectable
terms cover the same ground in install vocabulary, while the ledger term
contributed bundle paths including roots that get re-projected at the
destination. The rendered install nudge now carries the destination scope and
says custom roots resolve against the destination base, and the field rides
every failure rather than only some.

A retained destination copy is now marked on disk, so the retry it invites is
refused as destination-retained-blocking and tells the caller to reconcile
rather than delete what may be the only surviving copy of files a partial
removal deleted. Readability could never discriminate that state because the
pre-removal gate guarantees a readable SKILL.md.

Every move-scope failure now routes through one failMove helper backed by an
exhaustive touched-disk map, so a mistyped state cannot compile and settle runs
in one place. The two name-validation refusals and the three cross-scope
pre-flight refusals carry a state for the first time, and the outer catch
reports nothing-written when it fires before the first write instead of
claiming partial damage.

alignWarningCodes keeps the warning text when a peer sends codes it cannot pair,
so an empty warningCodes means what its description says it means. The Skills
Studio client parses droppedLocations and moveState instead of discarding them,
and refreshes on the transport failure path.

* fix(PRD-8343): extract move-scope identity first and report source state structurally

Hoist extractActorIdentity to the first statement of handleSkillMoveScope. The
failMove closure containing errorResponse was declared ahead of it, and the
attribution sweep scans handler bodies textually, so a handler whose runtime
ordering already satisfied precedent 24 still read as a violation.

Report the post-removal source condition as a structured sourceState member
(intact, lossy, unknown) beside moveState, so a caller can tell a disposable
duplicate from a copy that may hold the only surviving files without matching
on English. The unknown arm no longer asserts a read failure it did not
observe, and the rescan error is chained into the cause instead of discarded.

Give failCopiedDuplicate the policy its model already had: cache invalidation
through a handler-local settle on both paths, an explicit error log level, and
the frontmatter diagnostic rather than a bare error kind. Cover the 400 arm
with a malformed-frontmatter source.

Scope the changeset claim to the failures the handler reports, since the
transport-layer refusals emitted before it runs carry no code, and document
sourceState there.

Keep the bundled project skill under its 20,000-byte core-body gate.

* fix(PRD-8343): scope the retained-destination record and close the move-scope refusal gaps

Round 6's work was written but never published, so round 7 re-verified against a
HEAD that did not contain it. It lands here together with round 7's fixes.

Retained destinations move from an in-directory marker to a ledger at
.ok/local/skill-move-retained.json, and the 409 reads its sourceState, so an
intact source now says "remove it, then retry" on both the 500 and the retry
instead of contradicting itself. droppedLocations rides only the failures that
actually dropped placements, unprojectable resolves against the destination
scope, and alignWarningCodes keeps every warning while withholding only the
codes it cannot pair.

project/SKILL.md restores the two substrings validation-coverage-contract.test.ts
pins verbatim, which was the cause of the red test:shard1, and restores the two
facts the cross-level clause had dropped. The bytes came from four filler cuts
in the Reads/Writes bullets; body is 19,994 of 20,000.

extractActorIdentity gets its own recovery boundary in handleSkillMoveScope so
the last refusal without a moveState now carries one, and handleSkillDuplicate's
outer catch can reach settle() and reports the copy it left behind.

* fix(PRD-8343): derive the cross-level move split from what the destination can host

Round 8 review fixes.

The intact verdict now reads the same on the server and on the two client
surfaces that used to assert a deletion policy without consulting sourceState:
skills-api threads sourceState through MoveSkillScopeResult and splits the
retained-copy toast into a remove-and-retry arm for intact and the protective
arm otherwise, gated by an exhaustive Record over SkillMoveStateCode. That
toast now carries a stable id and no timer, so the one message in the flow
whose content is destructive if ignored no longer auto-dismisses.

The retained-destination ledger gains a three-state read, so an unreadable
file is no longer indistinguishable from an absent one: the write path
refuses to rewrite a ledger it could not read rather than replacing every
record it holds, and the 409 grows an arm that says it cannot tell whether
the occupant is a retained copy instead of advising deletion. Each record
binds to the content hash of the copy it describes, so a record whose copy
was replaced no longer vouches for the replacement, and it is cleared on
skill delete and on hash mismatch as well as on a completed move. The store
takes scope and name rather than a pre-composed key, derives its path from
getLocalDir, and writes through tracedAtomicFs, now hosted in fs-traced
beside the wrappers it composes.

droppedLocations reports what the sweep actually removed rather than what was
predicted, so a source-removal failure no longer reports an empty list for
placements it has already deleted. All seven copies of the cross-level claim
now derive the split from what the destination level can host, and the move
success text splits its remedy three ways: an install call for placeable
locations, the agents-hub equivalent for an editor that reads the hub at that
level, and an explicit no-placement-exists for one that does not. A lockstep
test pins the claim across every surface that states it.

alignWarningCodes renders a non-string warning as text instead of dropping it,
and a non-array warnings payload now yields one explicit unverified warning
rather than a code-paired all-clear. write, edit, import and install share the
warning contract text, and the three tools that were rendering the raw server
warnings instead of the normalized ones now render the normalized ones.

The duplicate handler splits its disk flag in two, so a mid-copy throw still
settles and the disclosure says the copy may be incomplete rather than
claiming a complete one. The two name refusals emit droppedLocations, and the
new disk artifact is listed on the page that promises to list every one.

* test(PRD-8343): pin droppedLocations on the two move-scope name refusals

The transport test's destination-retained body carried an agents entry, a
value that state cannot produce: agents resolves at both levels, so
unprojectable filters it out of droppedLocations. Retargeted to lm-studio,
which the state does produce, keeping the non-string coercion the test is
actually about.

* fix(PRD-8343): give the retention ledger a consumer and stop unreadable reading as mismatch

The unreadable-ledger state added last round reached neither the move tool
nor the Studio, so a caller saw moveState nothing-written glossed as a safe
retry. Declare retentionLedger on move's outputSchema, forward it through the
skill-target adapter, and back both copies with SKILL_RETENTION_LEDGER_CODES
plus an isSkillRetentionLedgerCode guard in core so the third copy is not
hand-rolled.

skillContentHash returned the empty string for three different facts, so an
occupant that could not be read was treated as an occupant that did not match:
the record was cleared and the delete-it advice printed. readSkillContentHash
now returns a two-arm result, the gate clears only on a proven mismatch, and
an unverifiable occupant emits retentionLedger occupant-unverifiable with the
same no-delete wording the unreadable ledger already gets. The write side
treats an unhashable destination as a retention-record failure rather than
persisting a record that can never match.

droppedLocations carried a description written for the success arm whose
remedy is harmful on a failure and whose opening clause contradicted its own
third sentence. Rewrite it to split success from failure, report the sweep
unfiltered on the failure arm, and widen the lockstep regex to admit "place".

Also: parameterize the warnings-field contract so import stops hand-copying
it, read the ledger before skill-delete clears it and on a same-scope rename
collision, and split skill-duplicate's post-mkdir flag so the "inspect or
remove it" detail cannot name a directory that was never created.

* fix(PRD-8343): surface the unverifiable-occupant warning in the Studio

skillMoveRetainedCopyToast gated on MOVE_SCOPE_RETAINED_DESTINATION, which
maps nothing-written to false, so both retention-ledger states returned
undefined and the human saw a bare name-collision message with none of the
server's do-not-delete caution. Parse retentionLedger off the error body and
branch on it before the moveState lookup, so the two cases stay separate from
the genuinely safe nothing-written arm.

Raise visibleToasts to 6: the retained-copy toasts now never expire, and a
bulk move raises one per failed skill, so past sonner's default cap of 3 the
earliest warnings stacked behind the visible three indefinitely.

Name all three paths that clear a retention record in the two docs rows that
promise completeness, not just the completed-move one.

* fix(PRD-8343): give droppedLocations one meaning and stop advising deletion of unread occupants

The success-semantics droppedLocations was reassigned above two failure exits
that still emitted it, so destination-unreadable and partially-applied shipped
the destination-unhostable set under a clause asserting the skill did not move.
The reassignment is gone and the success response passes the value directly, so
the variable holds the source-side sweep on every path failMove can reach. The
tool description now reads the split off moveState rather than off success.

describeRetainedOccupant returned detail undefined for a destination the server
could not read, and the call site fell through to "Delete or rename it first" on
a signal the delta computed one line above and never consulted. It now returns a
verdict discriminant. The call site derives one conditional spread from it, and
unreadableOccupant retires. The new arm distinguishes an absent directory from
an unreadable one, because a skill resolved at a non-default host root also
reads as not-ok and must stay an ordinary collision.

readSkillContentHash reported the stringified fs exception, which embeds the
absolute path, into a caller-visible detail on two endpoints. It now reports the
errno code and carries the error as a cause for the log. Same for the ledger
store. The unrecorded-retention note branches on whether the copy was readable,
so it stops implying a copy is safe to recover from when hashing just failed.

The skill-delete loss log gated on a bare key match, so reconciling a retained
copy in place left a record that still matched by key and reported lost files
that were recovered. It now requires a proven hash match, carries the requestId,
and logs at error like its sibling. The rename collision clears a proven-stale
record and renders its detail through errorTextWithDetail, which the same-scope
MCP adapter previously discarded.

* fix(PRD-8343): drain the retained-copy toasts and carry the failure split into the docs

The retained-copy toast is the only duration Infinity toast in the app with a
per-entity id and no dismisser, so a bulk move failing on N skills left N
toasts mounted for the rest of the session. Raising visibleToasts widened the
window without draining the stack, because sonner keeps toasts past the ceiling
mounted rather than removing them. The id is now single-sourced and both
moveSkillScope success arms dismiss the record's toast, so the population is
bounded by the lifecycle rather than by the visible window. The visibleToasts
override this branch added is reverted, since it was an app-wide knob for four
unrelated persistent notices.

The dom test that pins this hook's toast guidance drove moveState and
sourceState but never retentionLedger, so the composition point that carries
the field to a user was the one tier with no coverage. It is pinned now, and
deleting the wiring line reds it.

The changeset and both docs pages stated the droppedLocations re-add advice
unconditionally, which is only true on a success. Each gains the failure
caveat as an appended sentence. The changeset also names retentionLedger, its
two codes and the core exports, so an integrator stops reading nothing-written
on a 409 as an ordinary safely-retryable collision.

One unrelated repair: the skill scope move dialog test mocks the skills api
module totally, so adding an export made the hook throw while every test still
reported passing and vitest exited 1.

* fix(PRD-8343): finish the deferred skill-lifecycle backlog and the round-11 review

Closes the eight pending recommendations plus the nine findings from review
5157621497, in one batch. NOT TESTED: at the user's explicit instruction nothing
was executed for this batch, and both the commit and the push skip verification.

Server and core:

- readSkillContentHash returns a four-arm discriminated union, so a destination
  directory the server read successfully and found has no SKILL.md gets its own
  stray-occupant verdict and no longer ships retentionLedger. That code now means
  only "the ledger could not be read" or "the occupant could not be read".
- The verdict to wire-code mapping is an exhaustive Record, so a new verdict kind
  cannot silently degrade a refusal into a bare nothing-written.
- The retained-destination ledger read carries its cause through to both 409 sites,
  and the schema-mismatch reason no longer stringifies unvalidated file content.
- DELETE /api/skill reads the ledger before hashing, so an ordinary delete no
  longer walks and hashes the whole bundle for a diagnostic that cannot fire. The
  delete log gained an unverifiable arm, and the success response carries a
  bounded warnings entry when it removed a proven retained recovery copy.
- failMove takes a discriminated producer union, and the collision callsite builds
  a whole outcome per verdict rather than casting independent optionals.
- Cross-scope moves record one bounded outcome metric per invocation and one
  structured outcome log on the response's own problem instance. Per-line UUID
  minting is gone. commitOkArtifactWrite reports flushed, unavailable or failed,
  and the deferred branch now says only that this move did not establish its
  Revert baseline, naming the destination rather than the old name.
- describeFmEditError moved to its own module with all eight arms table-tested.
- The four settle effects are one hoisted helper, and the three copy-then-patch
  name sites share applySkillDirNameSync, which writes through temp and rename.

MCP and the agent contract:

- CROSS_LEVEL_TRANSFER_CLAUSE carries the success/failure split, so the tool
  description, both input describes and the crossScope text stop telling an agent
  to install droppedLocations at the destination on a failure. The project
  SKILL.md router bullet says the same. A companion assertion pins the split
  across every surface that states the remedy.
- The retentionLedger absence clause is scoped to the destination collision it is
  actually about, instead of contradicting moveState's own describe six lines up.
- deleteSkill threads warnings into both the text and structuredContent, and
  moveSkillCrossScope decodes failures through the shared tolerant reader.

Studio:

- The retained-copy toast drains from the transport functions that own its id, so
  a delete clears it the same way the server clears the record. The caller-side
  obligation and its export are gone.
- A bulk move now emits one truthful aggregate naming every affected skill with
  conservative recovery advice, and suppresses the unqualified success toast.
- Both delete dialogs render successful-deletion warnings, per skill in bulk.
- Move failures decode through interpretSkillMoveFailure, so a contradictory or
  incomplete peer result stays cautionary and can never produce remove-it advice.

Sibling stores refuse to persist over a file that exists but cannot be read,
rather than replacing every record it held. Reads stay fail-soft.

The fs-copy observer runs the caller's predicate before consuming its one-shot
latch, so an unrelated copy can no longer steal the slot.

* fix(PRD-8343): give the presence marker a reader and gate the whole unverified triple

The presence-exempt marker moved to content/skills-write.ts with the
applyPatchToFm call it exempts, but its registered reader still scanned
only api-extension.ts, so guard-marker-readers.test.mjs reported it
unread and the lint job went red. Widen both halves together: the
registry scans skills-write.ts, and the presence guard derives its
expected count from both files. Removing the marker now fails the guard
again, which is what makes it load-bearing rather than decorative.

The MCP cross-scope move adapter gated only sourceState on a coherent
outcome, so the unverified arm still shipped moveState and
retentionLedger, a field combination the tool's own output schema
documents as impossible. Gate all three, and pin the withholding.

* fix(PRD-8343): supply warningCodes on the relocated skill success payloads

The branch made warningCodes a required member of SkillPutSuccessSchema and
SkillReimportSuccessSchema. PRD-8403 relocated those handlers to src/http
before that landed, so their success payloads omit it: successResponse fails
its own schema validation and answers 500 with no cause and no detail. The
merge typechecked because the payload object is only checked against the
schema at runtime.

PUT /api/skill and the reimport up-to-date path now carry it, matching the
warnings expression each site already uses. SkillRevertSuccessSchema and the
two template schemas do not declare warningCodes, so their payloads are
unchanged.

Fixes the 8 failures across api-skill-document-composition,
api-skill-file-composition, api-skill-recovery-composition and
api-skills-native-preservation, all of which pass on main.

* test(PRD-8343): supply the required outcome in the failed-move mock

MoveSkillScopeResult's failure arm declares outcome as required, and
useMoveSkillScope passes it straight to skillMoveRetainedCopyToast, which
reads outcome.kind. The release-the-write-flag test mocked a failure without
it, so the hook threw TypeError before reaching the assertion. Its sibling at
:119 already supplies outcome via interpretSkillMoveFailure; this mock now
does the same.

The other 51 test:dom failures on this head are in DocumentContext.dom.test.ts
and reproduce on origin/main, so they are not this branch's.

* fix(PRD-8343): re-apply delete, rename and duplicate onto the relocated routes

The origin/main merge ported handleSkillMoveScope into skills-document-routes
but took main's side for the other three handlers, which are the pre-PR
versions. 119 lines of this branch's work went with them: handleSkillDelete
lost its warnings member and its clearSkillMoveRetention call, handleSkillMove
lost the same retention cleanup, and handleSkillDuplicate lost its collision
handling.

Under the re-projection behaviour this branch implements, that cleanup is
load-bearing rather than cosmetic: a retention record left behind by a delete
is read by the later collision path as a real retained copy whose occupant is
now absent.

invalidateSkillCatalog joins settleSkillCatalog as a threaded dependency
rather than a local reimplementation, for the same reason as before — the real
one also refreshes the content filter and schedules the ignore rebuild.

* fix(PRD-8343): drop the imports the handler port left unused

applySkillDirNameSync and tracedWriteFileSync lost their last callers in
skills-document-routes when the three handlers came across, and biome's
noUnusedImports is error-on-warnings in the lint gate.

* fix(PRD-8343): recover the dropped round-12 work and stop the create double-count

Recovers the in-flight work from the iteration this branch lost, rebased onto
the relocated route modules, and closes the Critical the last round raised
against the merge.

The create branch of skill-put no longer concatenates composed and writer
warnings. 82607df0b had already fixed that double-count; main's relocation
reintroduced it in warnings, and the earlier warningCodes repair on this
branch mirrored the concat instead of noticing, so every authoring warning and
its code were counted twice on create. wr already carries the composed
warnings.

Bookkeeping that runs after the destructive step no longer converts a
completed operation into a 500. Delete guards uninstallSkillFromHostDirs;
rename guards rekeySkillLockEntry, removeSkillInstall and recordSkillInstall.
Each failure logs and adds a warning to the success response, which is what
the caller can act on: the content change landed, only its records did not.

Also recovered: the cause field on the three .ok/local store readers that
dropped it, the retained-destination classification collapsed into one
exhaustive Record in core, the flat wire fields dropped from
MoveSkillScopeResult's failure arm in favour of outcome alone, the batch
retained-copy toast drain, the delete-time unverifiable warning arm, and the
move.ts errors clause.

api-skill-store-refusal.test.ts arrives with this work and is honest about
what remains: 8 of its 11 cases pass. The three still red cover the
cross-scope move guard, the retained-slot-unverifiable verdict, and the
within-level rename collision wording.

* fix(PRD-8343): close the remaining move-scope and warning-contract findings

The store-throws finding is now closed at all three call sites. The cross-scope
move guards its post-move bookkeeping the way delete and rename already did, so
a completed move whose records could not be written answers 200 with a warning
rather than reporting itself partially applied.

describeRetainedOccupant no longer describes a directory it read as one it
could not read. The undiscriminated `record !== null && !occupant.ok` arm now
lets `no-manifest` through to its own arm first, which is what gives
`retained-slot-unverifiable` a producer; both still resolve to the
`occupant-unverifiable` ledger code, so the wire contract is unchanged.

The collision detail no longer names an operation the caller did not invoke: a
within-level rename refused on an unreadable occupant said "a cross-scope move
will not overwrite it". The paired expectation in skill-mcp-move.test.ts moves
with it.

SkillMoveFailureOutcome's `unverified` arm now forbids the three fields the
adapter already withholds on it, so the type no longer permits a combination
the code excludes by hand, and moveState's describe no longer opens by
promising presence on every failure two sentences before saying when it is
withheld.

UNREADABLE_WARNINGS_TEXT ships from core and the MCP copy re-exports it; the
duplication was recorded as forced by a boundary the same file already crosses.

The changeset records the two behaviour changes it was missing: the stores
refusing to rewrite what they could not read, and the `warnings` member that
refusal made necessary on delete, rename and move-scope. mcp.mdx's advisory-
warnings section names `delete`, `move` and `install` on a skill target and
says how their plain-string warnings differ from the kind-discriminated ones.

api-skill-store-refusal.test.ts is 11/11.

* fix(PRD-8343): close the remaining minors and considers

The move-scope counter and its structured log now emit from one place, so a
terminal path cannot record one without the other. The four that recorded only
the counter — the actor-identity failure, the two name refusals, and the
success path — carry their context through it; failMove hands in the instance,
status and dropped-location count it used to log separately.

The bulk delete dialog's per-skill warning rows are a list rather than spans in
a flex div.

Stale SKILL.md.tmp.<pid>.<ms> siblings are swept before each atomic write
rather than only unlinked on a failure that returns. A crash between write and
rename previously left one inside the hashed bundle, where it counts as a
bundle file. All three writers in skills-write share the sweep, not just the
rename path the finding named.

The marker registry asserts that its presence-exempt scan set and the reader
that models it name the same files. They were computed independently, so a file
added to one and not the other left the marker unread with nothing failing.

Not done, deliberately:

- successResponse taking `schema: z.ZodType, body: unknown` as unrelated
  parameters is a real finding — it is what let this branch ship a body missing
  a required member as a runtime 500. Typing body as z.input<S> reds 41 call
  sites across the server, none of them this branch's. That is its own change.
- skill-mcp-move.test.ts's lossy ternary still has its hash-mismatch half
  unfixtured. Reaching it through the HTTP surface needs a partial delete that
  leaves SKILL.md but removes a sibling, which depends on unlink ordering the
  test cannot pin; and both halves report the same sourceState, so the fixture
  would not distinguish them anyway. It wants a unit test of the computation.

* fix(PRD-8343): give the rename collision the next step its sibling states

The same-scope rename 409 dropped `detail` entirely on the ordinary
collision, because describeRetainedOccupant returns undefined for exactly the
plain-collision verdict. The response was a bare "already exists" with no next
step, while the move-scope 409 consuming the same classifier falls back to
'Delete or rename it first; this move will not overwrite it.' The rename now
uses the same fallback.

* revert(PRD-8343): let an unverified outcome keep the values it decoded

Narrowing SkillMoveFailureOutcome's unverified arm to forbid the three fields
over-applied the review finding. The withholding it names happens in the MCP
adapter, which gates emission on `coherent`; interpretSkillMoveFailure is the
reader, and it deliberately keeps what the server actually said so a caller can
see it while treating the triple as untrustworthy.

skills-api.move-scope.dom.test.ts pins that: a body carrying
`moveState: destination-retained` with no `sourceState` is incoherent, and the
decoded moveState survives on the unverified outcome. Dropping it made the
client discard the one diagnostic it had.

moveState's describe keeps its correction — it no longer promises presence two
sentences before saying when the adapter withholds.

---------

GitOrigin-RevId: a076209864d4f81082b52ce51274c2777deae359
2026-09-10 18:25:51 +00:00

Open Knowledge Skills

Agent Skills published by Open Knowledge. Each skill teaches an agent one repeatable task, and every one of them is plain markdown you can read before you install it.

skills/ is generated. The skills there are projected from the Open Knowledge source tree, so each skill directory's leaf name matches its SKILL.md frontmatter name. The layout groups by role:

  • skills/core/ holds the platform and meta skills.
  • skills/starter-packs/<pack>/ holds one folder per starter pack: the pack's orientation skill, its member skills as sibling directories, and a README explaining how they interact.

Install

Add this repository as a Claude Code plugin marketplace, then install what you want:

/plugin marketplace add inkeep/open-knowledge-skills
/plugin install open-knowledge@open-knowledge-skills

open-knowledge is the platform skill: it teaches an agent to read, search, link, and write markdown through Open Knowledge's MCP server. The rest are optional packs.

What's published here

Install a pack by name, the same way: /plugin install <pack>@open-knowledge-skills.

Core

Skill What it does
open-knowledge The runtime contract for working inside an Open Knowledge project: reading, searching, linking, and writing markdown through its MCP server.
open-knowledge-discovery What Open Knowledge is, how to install it on a repository, and how to share a project with collaborators.
open-knowledge-write-skill Authoring a new Agent Skill: the frontmatter contract, progressive-disclosure structure, and how to install it into your editors.

Starter packs

Each pack ships an orientation skill that explains how to work in that project shape. Some add task skills alongside it.

Pack Orientation skill Also ships
software-lifecycle proposals to decisions to specs to postmortems frame-a-proposal, write-a-spec, record-a-decision, write-a-postmortem, review-a-design
knowledge-base source-grounded research: sources to research to articles research-with-sources, consolidate-notes
codebase-wiki an agent-authored, source-grounded wiki of the surrounding codebase
personal-crm a typed-entity vault of people, companies, and meetings, each with a rewritable summary and an append-only timeline
note-taking a flat notes folder plus a daily journal, for when you just want to write
writing-workflow a three-stage drafting flow: ideas to drafts to published
worldbuilding a fiction encyclopedia of characters, settings, factions, and lore
okf-knowledge-base a knowledge base conformant with Google's Open Knowledge Format from the first commit

The task skills stand on their own. write-a-spec and record-a-decision do not need an Open Knowledge project, or the rest of their pack, to be useful.

You can also copy any skill straight into your agent's skills folder (.claude/skills/, .cursor/skills/, .codex/skills/, or the vendor-neutral .agents/skills/). Open Knowledge can install and update them for you, and keeps their version history.

Writing your own

template/SKILL.md.example is a starting point. The field that matters most is description: it is a routing rule, not a title, and it is what both skills.sh and the host agent match on to decide whether to load the skill at all.

Read CONTRIBUTING.md before opening a PR.

License

MIT

S
Description
open-knowledge-discovery: Read when the user asks what OpenKnowledge is, wants to install it on a repository, wants to open or preview a single markdown file that is not part of an…; okf-knowledge-base: Open Knowledge Format (OKF) v0.2 guidance. Use when creating, reading, reviewing, or maintaining an OKF bundle; responding to OpenKnowledge okf plugin…; open-knowledge-write-skill: Use when the user wants to create, author, write, or design a new Agent Skill (a SKILL.md) — for OpenKnowledge or…
Readme MIT 584 KiB
Languages
JavaScript 100%