Skip to content

feat: plugin manager - #35

Merged
sanchitrk merged 40 commits into
mainfrom
feat-plugin-install
Sep 22, 2026
Merged

sanchitrk merged 40 commits into
mainfrom
feat-plugin-install

Conversation

@sanchitrk

Copy link
Copy Markdown
Contributor

No description provided.

sanchitrk and others added 30 commits September 18, 2026 00:28
…dering

Plugin entries gain two long-form spellings alongside the module string:
`{ package }` resolves by location and installs, `{ plugin }` configures an
already-loaded plugin by ID and never installs. Exactly one key per entry;
an entry matching nothing is ignored rather than fetched.
Setup order is now declared instead of positional. A plugin states
`kind: "tool" | "prompt"` and the harness runs domains in table order, so a
prompt plugin always sees the full tool set regardless of which settings
layer contributed each entry. Within a domain, written order is kept and a
later entry with the same ID wins.
Settings discovery reads `.jsonc` (comments, trailing commas) with `.json`
as a fallback, and the project layer searches the startup directory and its
ancestors, nearest wins. Plugin entries accumulate across layers with
`{ "plugin": "<id>", "enabled": false }` as the per-project escape hatch.
A file that exists but cannot be read, parsed, or decoded now fails the
boot instead of being skipped.
`plugin add` validates by installing and importing before it writes;
`plugin remove` drops the module entry and any configuration written
against it. Both edit through jsonc-parser, preserving comments and
formatting, and write atomically. Default target is the project file,
`-g`/`--global` the user-wide one.
Also: `protocol` is no longer reserved — it is a typed catalog override, so
a local OpenAI-compatible endpoint can be pointed at correctly.
`Harness.layer` takes `cwd`, which makes tests hermetic now that discovery
walks up. Lint and typecheck cover `test/` in every package.
…ot the process's

- Rename the discovery root to `hostDir` and keep `hostCwd` for the process's own directory, so the two
  meanings of `cwd` stop sharing a name; `run --cwd` still means the session directory and is untouched
- Store `host_dir` on the session, nullable with no default, and carry it through `Session.create`, `fork`
  and a new `Session.link` that assigns or clears it on a session that already exists
- Make `Settings.Service.load` take the root as an argument rather than binding one per layer, since one
  process serves sessions in different projects, or in none
- Have `State.snapshot` read the session's root each exchange, so `link` applies at the next one; absent
  reads the user layer alone, with no fallback to the process's directory that would select a stranger's
  project and look correct
- Accept an optional `hostDir` on `session.create` over RPC and report it on `SessionInfo`; the CLI passes
  its shell's directory and gains no flag
- Find a project the way git finds a repository: the nearest ancestor holding a `.codework` directory, so
  an empty marker claims the answer and an inner project never merges with an outer one
- Delete `codework.json*` outright — one layout, one place a relative plugin entry can anchor to
- Create `.codework/` and print the path when `plugin add` runs where no ancestor is a project, because
  that decides where every later entry lands and a mistyped `cd` is how it goes wrong
… spawning pnpm

- Parse every source form in one place: `Target` covers local paths, registry specs and git remotes,
  replacing the two half-parsers that each read a slice of the same grammar
- Own the three rules parsing decides: whether re-resolving could yield different bytes, the canonical
  spelling two specs must share to be one install, and which sources are refused outright
- Install with `@npmcli/arborist` in process, so nothing has to be on PATH first, `ignoreScripts` is
  enforced rather than requested, and the installed identity comes back as data
- Resolve staleness with `pacote`, bypassing its metadata cache, because a staleness check that reads a
  cache is not one
- Recover a git install's commit from the lockfile arborist writes, and fail when it cannot be found;
  falling back to the manifest version would report an update forever and never apply one
- Opt in to git sources with `allowGit: "root"`: npm 12 refuses them by default, and `root` permits the
  source the user named while still refusing one a dependency drags in
- Hand arborist the real path of the staging directory, or it returns a tree with no root edges and the
  install silently reports that it installed nothing
- Name plugin failures once, as a tag for code and a lowercase `plugin-` reason for people and logs
- Drop the pnpm spawn along with its process spawner and platform layers
…generations

- Address an entry by the full digest of its canonical spec, under a readable slug, so two versions of
  one plugin sit side by side and a collision can never return the wrong package
- Publish by renaming a staged directory into a numbered generation, leaving earlier ones intact for
  exchanges that already hold URLs into them
- Keep the previous generation reachable, because the module registry is keyed by URL and never evicts,
  so installing over a path would hand back the old code on the next import
- Validate a staged artifact before writing its marker, so a package that turns out not to be a plugin
  is never published and the generation already in use stays the answer
- Carry what validation learned back to the caller instead of filing it: the store records packages, and
  knowing a plugin's id would mean encoding the result of running code it should ignore
- Add `index.json` as an accelerator that disk always overrules, so deleting it costs a scan and nothing
  else
- Move the install lock to one per store entry, so two plugins install at once and two installs of one
  plugin do not
- Expire superseded generations at the end of an install that published something, keeping the two newest
  whatever their age
- Drop the old flat cache layout along with `package.ts`
- Separate the two passes by cost: loading a module means a store lookup and an ESM import, while
  configuring one is pure data over what is already in memory
- Produce a pool of loaded modules keyed by ID, carrying every string each one answers to, so a later
  entry can address a module by name, spec or path
- Run the config pass at every exchange, so an options edit, an enabled flip or a reordering takes effect
  on the next turn with nothing re-imported
- Hold the pool behind a mutable cell, read once at the top of a snapshot, so a future reload lands
  between exchanges and never inside one
- Report an entry naming a module the pool does not hold, instead of reaching for the network from an
  exchange
- Seed built-ins into the pool at construction, so disabling one goes through the same path as disabling
  anything else
- Keep the combined helper as the composition of the two passes, for callers with no reason to hold a pool

Project plugins now follow the session's host directory rather than the process's, which is only visible
once both changes are in: a session that was never placed reads no project settings, so entries from a
project file are loaded and then selected by nobody.
- Ask two questions at the top of every snapshot: is a configured entry filed but not loaded, and has a
  loaded module been superseded by a newer generation
- Run a load pass only when one of them says yes, so the common case costs a lookup per filed module and
  nothing else
- Record where each loaded module came from, since comparing generations is what makes an update finish:
  asking only whether an entry is loaded still answers yes after one
- Require the resolve-only installer as a parameter, so an exchange cannot reach a registry by
  construction rather than by convention
- Leave a module the store does not hold alone, and report it instead, so installing stays something a
  person asks for
- Skip anything with no generation to compare, which is every built-in, supplied object and local path
- Read the loaded set once, at the top, so an exchange already running is never disturbed
- Materialise what a checkout already declares with a no-argument install, which is what lets boot stay
  offline and name a remedy instead of reaching for the network
- Print what each entry resolved to, using the declared id as proof the whole chain worked: an id cannot
  be known without importing, so its absence is where the reason goes
- Recompute that reason every run rather than storing it, because a recorded failure goes stale the
  moment someone fixes the cause
- Cache the opposite kind of fact, staleness from the network, in the store index where it survives a
  command exiting
- Ask the network through a parameter, so the store keeps its own promise never to open a socket
- Refresh only what is already installed, since not installed is not outdated and install is the verb
  for that
- Re-anchor a local path to the settings file receiving it, and write it absolute when relative would
  name a different file from where it is read
- Replace the loader's phase-tagged failure with the four reasons, and let an installer's failure through
  untouched, so one vocabulary reaches a person and a log line

The old error shape was reporting a package that is not a plugin as an install problem, with a hint
pointing at the registry. Two vocabularies for one failure was the cause.
- Cover the one case an exchange cannot notice: a local plugin edited in place, where nothing in
  settings changed and no generation moved because a local source is never filed
- Append a counter to a local plugin's URL, since the module registry is keyed by URL and would
  otherwise hand back the module it already holds against an unchanged path
- Keep the previous set when a reload fails, and report why, so a server does not empty its tool
  registry over a typo in a settings file
- Read the process's own root when rebuilding, not one session's view, or the pool narrows to the
  caller's project and drops every other project's plugins
- Expose it over RPC and as a command against a running server, with no error channel, because a
  failed reload is a result rather than a fault
- Leave the CLI without a local form: every run is a fresh process, so the load pass already happened
  at startup
- Carry what the file says, what it resolves to, and which file it was, since flattening every layer's
  entries loses attribution that cannot be recovered: two layers may declare the same string
- Derive the flat list from it, so the loader's input is unchanged and there is one source of truth
- Print the written form in plugin list, because that is the string a person will search their settings
  for, rather than the anchored path it resolves to
- Name the declaring file under --verbose and in the drift warning, which is the question worth
  answering once several layers accumulate entries
…n failure

- Load resolve-only everywhere, so a start reports what is missing instead of fetching it: a boot that
  installs waits on the network, fails offline, and lets timing decide which code runs
- Serialise the exchange-time load pass, and let a waiter adopt what the holder just produced, so two
  sessions reaching the boundary together do the work once
- Carry the declaring settings file on every plugin failure, since several layers accumulate entries and
  which one to edit is the question
- Attach it where the loader knows it, leaving an answer the store or installer already gave, because a
  more specific one is better
- Add session link, so a session created without a host directory can be given one later instead of
  being destroyed and recreated
- Apply it at the next exchange, since settings are already re-read there: no reload and no restart,
  the project layer simply starts or stops being read
- Identify the session by id and default the path to the shell's directory, because a project cannot
  identify a session and the CLI already runs on the host
- Keep it apart from session relink, which moves where the work happens rather than where settings are
  discovered
- Treat a local path that exists as bytes already here, so linking a project whose plugins are local
  loads them; without this the boundary check only ever acted on store entries
- Leave a local source with no generation, so it can never be found superseded and reload stays the
  only way to pick up an edit to one

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Name the question callers actually ask with hasHostLink, rather than leaving every call site to spell
  out that an absent key means no project
- Keep it off the wire, where the value it derives from is already present and a second field could
  only disagree
- Take a session id on plugin add, since a session carries a host directory that was declared on the
  host and is the only other trustworthy answer to which project
- Refuse a project id for the same job: a project is env-independent by design and its spaces name
  locations inside an env, so walking the host from one finds a stranger's project rather than failing
- Refuse rather than fall back to the shell's directory, which would write a project nobody named
- Raise session-not-linked when the session has no project, and say that linking is the remedy

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e package is staged

- `options` now takes the host directory: staging lives under the cache, so a project `.npmrc` was
  never read and a private scope silently resolved against the public registry -- 404, or a public
  package of the same name. Threaded through `download`, `reify`, `probe`, `Store.add`/`update`.
- Drop `--prefix`. npm pins its local prefix to one and stops walking, which would miss a token at
  a repository root while the person stands in `packages/x`. Without it npm walks up as it would
  if run there, which is the only rule a user can predict.
- `probe` reads the same chain as the install; consulting a different registry than the fetch
  answers about a package nothing is ever installed from.
- Answers Q2. Git auth needs nothing added: npm spawns git with `process.env` whenever the caller
  names no `env`, so the agent, `~/.gitconfig` and `~/.ssh` arrive by themselves. Verified against
  a real private repository over ssh and https, for `reify` as well as `probe`.
- Tests: a project `.npmrc` two directories up is read and a staging one is not; `options` names no
  `env`, so a later prune cannot break private git silently; an unreachable remote fails rather
  than waiting for a password; the authenticated half is gated on `CODEWORK_LIVE_PRIVATE_GIT`.
- Spec: §5.4.1 "Where auth comes from", Q2 closed, T10 recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…chine

Adapted from opencode's npm fixtures, which serve a packument over loopback and `git init` a
repository, so the real arborist and pacote paths are exercised with no network.

- Their `.npmrc` tests hand the loader the directory holding the file, so they prove the loader
  and never that a call site passes a sensible directory -- which is why T10 is green there. Ours
  drives `Store.add`, so a regression to the staging path fails loudly.
- `@fixture/plugin` exists on no public registry: an install that read the wrong `.npmrc` cannot
  quietly succeed against the real one.
- Guards for `audit: false`, `allowGit: "root"` and git commit recovery, all hermetic; the live
  suite keeps its own copies against the public registry.
- Every guard confirmed by breaking what it defends. Two needed a second pass: `ignore-scripts` is
  no longer enough to run a script on npm 12 (`allow-scripts`, npm/rfcs#868, blocks an unreviewed
  package), and arborist ignores a tarball's `scripts` unless the packument advertises
  `hasInstallScript`, so the first version passed without our flag set at all. It now turns off
  both npm gates from the project's own `.npmrc` and fails when `ignoreScripts: true` is removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… shell's directory

`plugin add --session <id>` resolved the project to write into and left every other question
anchored to the directory the command ran in -- which, for the server or UI the flag exists for, is
not the project and often is not a project at all.

- `plugin add ./tool --session <id>` imported `<cwd>/tool` while writing an entry naming
  `<project>/tool`: it validated one file and recorded another, or failed to find a path that was
  there all along.
- The `.npmrc` chain came from the shell's directory too, so a private scope resolved without the
  token that lives in the project being written to. The same bug as the staging-directory one, a
  layer up: the write target was honest and nothing else was.
- `from = linked ?? cwd` is resolved once, before the install, and `inspect`, `written` and
  `spellings` all take it.
- Test drives the real CLI against a linked session from an unrelated directory, and fails with
  `plugin-not-found` against the old anchoring.
- Spec: §12.7 no longer lists the `reload` trigger as deferred -- the server exists and
  `plugin.reload` is in the contract, exactly as that section predicted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… an .npmrc chain anchors

Both consumers resolve one against `process.cwd()` in silence: `parse` would anchor `./plugin.ts`
there, and npm answers a relative `cwd` with a `localPrefix` of whatever repository the process is
inside -- measured, not assumed. For a server the process directory is nobody's project, so the
result is a plausible answer to a question no one asked.

- `rooted` guards `parse` and `options`; the check sits outside `tryPromise`, so it cannot be read
  as npm being unreachable and swallowed by the fallback to empty config.
- A defect, not a typed error. Nothing a user types reaches it -- every reference goes through
  `parse` and every directory comes from `Global.resolve`, a resolved cwd or a session's `hostDir`
  -- so a relative one means a new call site got it wrong, and a stack trace names it. The whole
  suite passed unchanged, which is that claim tested.
- Necessary, not sufficient, and the tests say so: the staging path was absolute and wrong for
  three months, and T7's two `/workspace/a` are both absolute and name different machines. This
  removes the spelling that is wrong silently, not the class of mistake.
- Spec: T11, and a note in §5.4.1 on what absoluteness cannot supply.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t against the server's own

`Session.create` and `Session.link` called `resolve` on the `hostDir` they were handed. The CLI
already resolves against the shell's directory before sending, so that only ever fired for other
clients -- and the only directory a server can resolve against is its own.

Measured before the fix: a client asking to be linked to `my-project` was linked to
`<the server's repository>/my-project`, branded `AbsolutePath` and persisted, with nothing
reporting a problem. That field selects a settings file, and that file names plugins the server
imports into its own process, so an invented path is not merely wrong configuration.

- `rooted` moves to `util/path.ts`, now shared by plugin sources, npm config and sessions rather
  than copied; `AbsolutePath` is a bare brand with no check, so the assertion has to be explicit.
- Declared means declared: absolute, or the caller has not said which directory it means.
- Test links a session to a relative path over RPC and expects a refusal; it fails against the old
  resolving behaviour.
- The whole suite passed unchanged, so no existing caller was relying on the resolve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- serialize concurrent plugin settings edits across CLI processes
- validate and atomically replace runtime event definitions
- preserve process-wide plugins across session follow and reload
- cache and report the actual available plugin revision
- add regression coverage for concurrency, reload, and cache behavior
- avoid creating project markers during plugin removal
- preserve loader errors when validating plugin updates
- exclude package configuration patches from lifecycle commands
- keep git source identities collision-free
- retain settings provenance during exchange-boundary loading
- key registry artifacts by effective registry and spec
- fail closed when npm configuration files are unreadable
- make sandbox host cwd explicit across controller boundaries
- validate absolute paths in schemas and RPC payloads
- add regressions for registry isolation, cwd propagation, and path decoding
…eclaring project names

- Registry-keyed identity is now consistent end to end: install, resolve,
  check, update, boot load and exchange rebuilds all anchor a reference's
  `.npmrc` chain at the project that declared it, not the process's own
  directory, so a linked session no longer misses a private-registry entry
  or silently substitutes the public package.
- `anchor` climbs from a settings file's `.codework/` directory to the
  project root, matching the boundary npm reads a project `.npmrc` at.
- `Pool.Origin` records the declaring file so a retained module re-resolves
  through the same registry on union rebuilds; the session's own
  declarations win where both name one reference.
- `plugin add` resolves its destination file before installing, so the
  artifact is keyed under the chain that will declare it, and a failed
  reference still leaves no `.codework/` marker behind.
- An unreadable npm configuration now warns and continues with npm
  defaults instead of failing the install, matching npm's own behaviour.
- Adds regression coverage for declaring-project anchoring, per-entry
  registry contexts in a shared pool, and the warn-and-continue path.
A retained origin -- a module loaded for another session that nobody still
configures -- was unioned into every rebuild unconditionally. When its
source vanished (a deleted file, a removed store entry) the union `load`
failed, and since origins only update on a successful load, every later
exchange and `plugin reload` kept failing until restart.

- `follow` and `rebuild` now check each retained origin through `filed`
  before unioning; one that no longer resolves drops out of the pool with
  a warning naming the reference instead of failing the pass.
- A session's own configured references stay strict: they are covered by
  the drift check and `load` still fails on them.
- `known` (declaring-file anchors) is merged only from surviving origins.
- A session that still configures a dropped reference sees it in the
  `missing` drift report with the `plugin install` remedy.
- Regression test covers a dead retained origin dropping out while the
  current session's module loads.
An ephemeral kernel notice for each plugin pool transition, so listeners
can observe module lifecycle without diffing the pool themselves.

- `plugin.updated` carries `status: loaded | dropped | failed`, the
  configured reference, the plugin id when known, the declaring settings
  file, and the error message on failure.
- Emitted from `activate` after the pool swap lands: `loaded` when a
  module instance changes (new plugin, superseding generation, re-imported
  local file), `dropped` when an origin leaves the pool.
- `failed` publishes on a rejected `plugin reload` and on an exchange's
  `follow` error, with `reference` when the failure names one.
- Ephemeral by design: settings and the store are the durable record;
  replaying transitions would only restate what the pool already says.
- Kernel-owned type -- it lives in `EventList.Definitions`, so plugins
  cannot publish it and it cannot collide with a `plugin.<id>.*` type.
- E2E test drives a real harness through loaded -> dropped -> failed.
…er layer only

`Settings.load`'s `hostDir` is a session property by contract, but boot and
`reload` passed the process's own `hostCwd` to it. That made the process
behave like a session anchored at its launch directory: the project-layer
discovery walk climbs ancestors, so a `.codework/` found anywhere above the
launch directory silently configured the process -- a server started in
/var/usr could pick up a stranger's project settings.

- Boot and `rebuild` now load the user layer and the explicit override
  only; project plugins still arrive at a session's first exchange through
  `follow`'s union, and survive reloads through retained origins.
- `--user-config-dir` is pre-resolved at the harness boundary: `~` expands
  to home and a relative spelling resolves against `hostCwd`, so a session
  `hostDir` can never move the override's anchor.
- `hostCwd` doc corrected: it is the process-level directory (sandbox
  default, `.npmrc`/parse fallback, override base), not a project-settings
  root.
- The resolve-only boot invariant moves to the user layer: an
  unresolvable user-layer entry still fails startup rather than fetching;
  a project-layer entry is now its session's drift report, not a
  process-level failure.
- Tests pin a launch inside a project's subtree never importing its
  plugin (import side-effect sentinel), project entries surfacing as
  session drift, and relative `--user-config-dir` resolving against
  `hostCwd` with a different session `hostDir`.
sanchitrk and others added 10 commits September 22, 2026 15:58
Plugin authors had to depend on @codeworksh/harness to write a plugin, which
meant installing the loader, the npm store, the control plane and the database
to get at a handful of types. Extract the contract into its own package so a
plugin package depends only on what it actually compiles against.

The split is one rule: @codeworksh/plugin owns the contract, @codeworksh/harness
owns the assembly. Moved out are the plugin definition (define, Plugin, Mount,
SharedPluginContext, PluginRegistry, the `plugins` entry shapes), tool authoring
(define/make/implement/register/provide, ToolProgress, ToolExecutionError), the
sandbox mount and driver SDK, and the schemas they close over -- event, settings,
location, project/space/repo, and the session and message id brands. Loading,
installing, .npmrc anchoring, the executor, the registry buckets and the Location
layers all stay in the harness.

The harness keeps every module path it had: each vacated file is now a one-line
re-export, so the ~80 files importing them are untouched and @codeworksh/harness
/effect and /sandbox still resolve. Only location.ts and plugin/context.ts split
rather than move whole, keeping their layers and the kernel-event guard. Net
+70/-2368 in packages/harness/src, no behaviour change.

The harness externalizes the SDK in both its .d.mts and .mjs output rather than
inlining it, so a process holds one SandboxIO.Shell tag rather than two. Service
tag keys move to @codeworksh/plugin/* as the deterministicKeys diagnostic
requires; they are runtime-only and nothing persists them.

The root entry exports Plugin and Tool and nothing else. The other subpaths are
published because the harness imports them across the package boundary, not as
an authoring surface, and are deliberately not re-exported from it.

The four extras/ examples now build against @codeworksh/plugin instead of the
harness, which is what demonstrates the package stands on its own.
`src/sandbox.ts` re-exported `@codeworksh/plugin/sandbox` with `export *`. It is
also a pack entry, and a bundler cannot know what an external star-export
provides, so it emitted an empty namespace object: the daytona and vercel
drivers, which import the barrel by name, read `SandboxDriver.AbsolutePath` off
`undefined` and threw on load.

Only the published package was affected. The suite runs from source through vite
aliases, where the barrel resolves normally, so nothing caught it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A plugin is installed into the store as its own npm tree, so `effect` arrives
there a second time. Two copies of the same version are two module instances,
and they do not agree: a schema carrying a check rejects every value, and the
turn dies committing the assistant part with `SchemaError: Expected JSON value
at ["data"]["part"]`. A real plugin whose success type was `Schema.Finite`
registered, reached the model, ran its handler, and then failed with "Expected a
finite number" for 22800.

Deduplicate at resolution: while a plugin is imported, `effect` and
`@codeworksh/plugin` resolve as though this module had imported them. A symlink
into the store cannot do this — a generation is immutable and shared by every
project on the machine, while the harness doing the loading is whichever one is
running now.

The fixture that was supposed to cover this proved nothing. It took its second
instance by re-importing `effect` under a query string, and `effect`'s entry
re-exports from submodules whose specifiers carry no query, so they resolved to
the modules already loaded and `foreign.Schema === Schema`. It now tags the
whole subgraph, which is what a separate install produces — and with a real
second instance the old claim that one version "interoperates completely" turns
out to be false. The tests say what actually happens instead.

The same failure was attributed to effect@4.0.0-beta.107, i.e. to a version
difference. It reproduces at the pinned version with only the instance
separated, so the variable was never the version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ate work

`await import("pacote")` puts `resolve` and `manifest` on `default`: pacote is
CommonJS and builds `module.exports` in a way Node's named-export detection does
not see through. `probe` called them off the namespace, threw
`TypeError: pacote.resolve is not a function`, caught it, and reported
`plugin-resolve-failed: cannot reach <spec>` — blaming the network for a
programming error. `plugin check` and `plugin update` had therefore never
worked, for git or registry targets.

The ambient declaration named them as named exports, which is why this
type-checked. It now declares the default export that actually exists, so the
same mistake fails at compile time.

`plugin.npm.live.test.ts` already covers this — it probes a mutable branch and
asserts a 40-hex sha. It is gated behind CODEWORK_PLUGIN_LIVE=1 and had not run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…mpiled

A plugin distributed over git or npm has to ship built javascript, for two
reasons that compound: installs run with lifecycle scripts disabled, so a
`prepare` build never happens, and Node refuses to strip types under
`node_modules`. Until now that surfaced as the generic `plugin-import-failed`,
which tells an author their plugin is broken but not what to do about it.

The remedy is distinct — it belongs to the author's build, not to the user's
settings and not to `plugin install` — which is the bar this taxonomy sets for
earning a slug, and the same one `plugin-missing-dependency` cleared.

Matched on `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` rather than on the
message: the code is Node's documented identifier and exact. A local plugin is
outside `node_modules`, where types strip happily, so the single-file dev
workflow is unaffected — the test asserts both sides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`check` reuses an answer for an hour. That is right for a command someone runs
repeatedly and wrong for the minute after they pushed a plugin, where it reports
a moving branch as current while `update` — which always refreshes — moves it.
There was no way to ask again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every exports entry carried `"development": "./src/*.ts"`, and the tarball is
built from `dist/pack` with no sources in it. A consumer resolving under that
condition — vite dev, vitest, or this repo's own `start` script, which passes
`--conditions=development` — got ERR_MODULE_NOT_FOUND for a package that
installed cleanly. It affected every published package, not just the new one.

Publishing a condition the artifact cannot satisfy is never right, so it is
removed rather than repointed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two plugins published the way a third party publishes them: their own private
repositories, their own release tags, built output committed, and
`@codeworksh/plugin` taken from npm rather than from this workspace. The specs
are cloned from github, installed by arborist into a real store, imported by the
real loader, and run in a real session.

It is the only place the contract crossing the package boundary is the shipped
one, and the only place a tool whose success type carries a check runs from an
installed plugin — which is what makes it the end-to-end guard on the effect
deduplication.

Needs github and npm, plus credentials for the private repositories. Opt in with
CODEWORK_PLUGIN_LIVE=1, like the other live suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`session link` read "Point a session at the host directory its settings come
from", and the server contract said much the same. That defines the field by one
of its readers: settings discovery reads the anchor, so do plugin installs, and
so will whatever needs a host-side directory next. Anyone reading the help came
away thinking `hostDir` is a settings flag.

It is an anchor to a host directory for the session, and the wording now says
only that. The path argument names `.codework` as a landmark so the directory is
still recognisable, which is what COD-67 asked for, without making the anchor
mean configuration.

Closes COD-67

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sable

The CLI is the outer layer, so it says the thing a person can recognise: the
path argument suggests the directory holding your .codework configuration, and
an example links a project elsewhere on the machine.

It stays a suggestion rather than a definition -- "typically", named once, on the
argument. `hostDir` is still an anchor and the contract still says only that;
what reads it remains the reader's business.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sanchitrk
sanchitrk merged commit 688bade into main Sep 22, 2026
@sanchitrk
sanchitrk deleted the feat-plugin-install branch September 22, 2026 13:15
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