Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
78db528
fix self-ref
Oct 9, 2026
6479d01
feat(entry): absorb EntrySpec into one EntryPredicate module
Oct 10, 2026
009daf2
refactor(config): extract config reader and registration detector fro…
Oct 10, 2026
9204d1d
test(entry): pin EntryPredicate match semantics at the seam
Oct 10, 2026
be72c00
refactor(scaffold): promote the template renderer from the test harne…
Oct 10, 2026
1d80c91
refactor(config): extract surgical splicer; editor becomes a composit…
Oct 10, 2026
9d267bd
feat(scaffold): fresh mode renders a conformant package tree from flags
Oct 10, 2026
3de7b3d
refactor(entry): suite config editor and status path consume EntryPre…
Oct 10, 2026
1bb0292
refactor(entry): generated package consumes the byte-synced EntryPred…
Oct 10, 2026
ab36332
merge issue-44-rearch: entry predicate rides the editor split
Oct 10, 2026
5de0d31
feat(scaffold): promote mode packages .opencode extensions with conse…
Oct 10, 2026
1d6eb58
docs(packager): mechanical scaffolding and promote flow delegate to t…
Oct 10, 2026
e140b63
feat(config): reader gains entries(scope, projectDir) over the lossle…
Oct 10, 2026
2a96b32
merge issue-44-rearch: entry-predicate template joins the renderer
Oct 10, 2026
79a6684
refactor(config): status path consumes ConfigReader.entries; config-e…
Oct 10, 2026
e8325af
Merge remote-tracking branch 'origin/im/48-config-reader' into issue-…
Oct 10, 2026
7609d97
refactor(env): process environment is read only in the Environment ad…
Oct 10, 2026
99b688d
refactor(env): ConfigReader takes the Environment; entries' warn bool…
Oct 10, 2026
769ef77
feat(sync): pair table classifies every template-suite pair (byte-syn…
Oct 10, 2026
725de41
refactor(env): Installer takes the Environment; installer tests use c…
Oct 10, 2026
b27fa56
docs(sync): conformance checklist and publisher instructions referenc…
Oct 10, 2026
9288f5d
refactor(env): StatusReporter takes the Environment; fetch and cache …
Oct 10, 2026
a7a2976
Merge branch 'issue-44-rearch' into im/49-env-injection
Oct 10, 2026
35ab23f
docs(adr): ADR-0013 core module carve-out design (design-twice, manif…
Oct 10, 2026
929c157
feat(core): carve shared machinery into src/core behind the ./core su…
Oct 10, 2026
00e5362
refactor(templates): templates become adapter renderings over the ope…
Oct 10, 2026
dd43a53
docs: templates documented as adapter renderings; ADR-0005 truth-sour…
Oct 10, 2026
8830fa9
review: harden promote retirement (issues from #20/#44 review)
Oct 10, 2026
b3e536c
review: coding-standards cleanup in core and scaffold
Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .opencode/opencode.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
"../index.ts"
"plugins": [
"./"
]

}
11 changes: 6 additions & 5 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,12 @@ covering stable OpenCode fundamentals.

**Templates**:
The static scaffolding files bundled with the package at `templates/`,
from which the packager and publisher render a generated package's code
files (plugin entry, manifest, name normalizer, registration detector, CLI,
installer). The structural source of truth for generated packages: example
repos may lag the corrected install pattern. Consumed at package-build
time, unlike references, which agents read for knowledge.
from which the packager, publisher, and scaffold CLI render a generated
package's adapter files (plugin entry, CLI, installer, package manifests).
Adapter renderings, not a structural source of truth (ADR-0013): the shared
machinery those files used to vendor is the `opencode-architect/core` module,
consumed by generated packages through a local `file:` dependency. Consumed
at package-build time, unlike references, which agents read for knowledge.

**Reference resolution**:
The load-time rewriting of backtick-quoted relative paths in agent prompts —
Expand Down
19 changes: 8 additions & 11 deletions agents/opencode-packager.md

Large diffs are not rendered by default.

12 changes: 4 additions & 8 deletions agents/opencode-publisher.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ You are an OpenCode extension publisher: you transform locally-packaged extensio

1. **Verify the incoming package.** Confirm the packager's structure exists: bundled content directories at the package root (`skills/`, `commands/`; a legacy `assets/` wrapper is recognized but non-default), `src/plugin.ts` with inline install logic, minimal `package.json` with a `content` declaration (`assets` or `code`) and an `exports` map declaring `"./server"` (per opencode-v2-facts §7, §14.6), `tsconfig.json`. Read the packager summary for extension name, description, included assets, dependencies, warnings, and the content declaration. Confirm every asset landed in the package and custom plugins or tools got their merge decisions. Cross-check the declaration against the bundled assets: `assets` requires skills/commands only — any agent, tool, or plugin file in the package contradicts it and returns to the orchestrator for repackaging; `code` is valid for any inventory. An invalid structure returns to the orchestrator for repackaging.

2. **Extract install logic to src/installer.ts.** Move install(), uninstall(), status(), scope detection, path resolution, and config management out of src/plugin.ts, keeping the manifest module (src/manifest.ts), plugin-name normalizer (src/plugin-name.ts), registration detector (src/registration.ts), and surgical config editor (src/plugin-config.ts) as separate files; update src/plugin.ts to call install() from src/installer.ts. Preserve the content-based deployment plan (ADR-0008): the installer reads the `content` declaration — assets-only packages copy-install by default (copy touches no config file) with `--mode plugin` as the opt-in; code-backed packages always register and `--mode copy` is a hard error. Preserve the invariants: manifest-gated idempotency (no `.version` markers; the plugin-mode no-op includes a present entry), semantic `@latest` plugin dedup written canonically as `name@latest` via the surgical editor only, abort-with-warning on unparseable config (never rewrite from `{}`), skip consumer-modified files unless `--force`, and root-config migration CLI-only behind explicit consent. Preserve the self-scoped cache hygiene (checklist A6): install() prunes the package's own cache copies (`<package>`, `<package>@latest`, `<package>@<version>`) on every invocation — including no-ops — best-effort with warn-and-continue, and clearPackageCache() backs the CLI's `clear-cache` subcommand.
2. **Verify the install logic is adapter-shaped.** src/installer.ts holds install(), uninstall(), status(), migrateRootConfig(), and the self-scoped cache pruning; src/plugin.ts only calls them; the machinery — manifest io, plugin-name matching, registration detection, the surgical config editor, path-form entry resolution, cache paths, and the status pipeline — is not vendored in the package: it comes from the `opencode-architect/core` subpath of the local `file:` dependency recorded in package.json (ADR-0013). Preserve the content-based deployment plan (ADR-0008): the installer reads the `content` declaration — assets-only packages copy-install by default (copy touches no config file) with `--mode plugin` as the opt-in; code-backed packages always register and `--mode copy` is a hard error. Preserve the invariants: manifest-gated idempotency (no `.version` markers; the plugin-mode no-op includes a present entry), semantic `@latest` plugin dedup written canonically as `name@latest` via the surgical editor only, abort-with-warning on unparseable config (never rewrite from `{}`), skip consumer-modified files unless `--force`, and root-config migration CLI-only behind explicit consent. Preserve the self-scoped cache hygiene (checklist A6): install() prunes the package's own cache copies (`<package>`, `<package>@latest`, `<package>@<version>`) on every invocation — including no-ops — best-effort with warn-and-continue, and clearPackageCache() backs the CLI's `clear-cache` subcommand.

3. **Create the CLI entry point.** Build src/cli.ts from `../templates/cli.template.txt`: install command calls install(scope, projectDir, { mode, force }) — it resolves the mode from the content declaration and surfaces `CopyModeUnsupportedError` as an explanatory exit-1 error — uninstall calls uninstall(scope, projectDir), status calls status(projectDir, online) and resolves the effective load per scope — the registration, the target config file, the raw entry as written, the resolved source on disk (npm package cache copy or path checkout, with the copy's age as a staleness hint), and an effective verdict that annotates a double-scope registration — with `--online` registry staleness (warn-and-continue, injectable fetch) and `--path <dir>` for another project directory, both rejected outside status — migrate calls migrateRootConfig only behind `--force` consent, and clear-cache calls clearPackageCache() — strictly self-only (this package's own cache copies, warn-and-continue, idempotent); it must not offer `--package` or `--all` modes, which exist only in the suite's own CLI.
3. **Create the CLI entry point.** Build src/cli.ts from `../templates/cli.template.txt`: install command calls install(scope, projectDir, { mode, force }) — it resolves the mode from the content declaration and surfaces `CopyModeUnsupportedError` as an explanatory exit-1 error — uninstall calls uninstall(scope, projectDir), status calls status(projectDir, online) and resolves the effective load per scope — the registration, the target config file, the raw entry as written, the resolved source on disk (npm package cache copy or path checkout, with the copy's age as a staleness hint), and an effective verdict that annotates a double-scope registration — with `--online` registry staleness (warn-and-continue, injectable fetch) and `--path <dir>` for another project directory, both rejected outside status — migrate calls migrateRootConfig only behind `--force` consent, and clear-cache calls clearPackageCache() — strictly self-only (this package's own cache copies, warn-and-continue, idempotent); it must not offer `--package` or `--all` modes (suite-only, per the declared divergences in `../references/template-sync-table.md` — that table, not prose, is the record of every template↔suite divergence).

4. **Expand package.json** from `../templates/package-full.template.json`: bin field for the CLI, scripts (check, test), expanded dependencies, npm fields (repository, bugs, license, author). The `exports` map keeps `"./server": "./index.ts"` — every distributed package must declare it; the root-index fallback is runtime-dependent and must not be relied on (per opencode-v2-facts §7, §13 row 9, §14.6). `"@opencode/plugin": "latest"` and `"effect": "latest"` stay unpinned (per opencode-v2-facts §15). Carry the packager's `content` declaration through unchanged — expansion adds npm fields, never alters the declaration.

Expand Down Expand Up @@ -72,14 +72,10 @@ Done when the package is live and the user has the registry URL plus consumer in
## Templates

- `../templates/package-full.template.json` - Full npm-ready package.json
- `../templates/installer.template.txt` - Shared install/uninstall/status module
- `../templates/plugin-name.template.txt` - Semantic plugin-name normalizer (src/plugin-name.ts)
- `../templates/manifest.template.txt` - Install manifest with per-file sha256 (src/manifest.ts)
- `../templates/registration.template.txt` - Read-only registration-scope detector (src/registration.ts)
- `../templates/plugin-config.template.txt` - Surgical config editor for the v2 `plugins` array (src/plugin-config.ts; legacy `plugin` entries tolerated read-only with an upgrade advisory — suite-level behavior, per opencode-v2-facts §13 row 10)
- `../templates/plugin-entry.template.txt` - Path-form plugin-entry resolver: matches a `file://` URL or filesystem entry to this package by `package.json` name (src/plugin-entry.ts)
- `../templates/installer.template.txt` - Adapter holding the package's policy constants and install/uninstall/status orchestration; machinery comes from `opencode-architect/core` (ADR-0013)
- `../templates/cli.template.txt` - bunx CLI entry point
- `../templates/prompts.template.txt` - Interactive confirmation helpers
- `../references/template-sync-table.md` - The sync table: every template↔suite pair classified (byte-synced / declared-divergent / fixture-covered) with the declared policy, plus the machinery pairs dissolved into the core dependency

## Code style rules

Expand Down
6 changes: 6 additions & 0 deletions docs/adr/0005-bundled-templates-resolved-like-references.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Bundled templates resolved like references

> **Superseded in part (ADR-0013):** the resolution mechanics below stand,
> but the templates are no longer a structural source of truth for generated
> packages — they are adapter renderings over the `opencode-architect/core`
> dependency, and the machinery templates have been dissolved into that
> module.

The packager and publisher need scaffolding files (plugin entry, package
manifests, CLI, installer) to render generated packages from. Shipping them
as `@assets/...` pointers broke in every deployed mode: nothing rewrote the
Expand Down
113 changes: 113 additions & 0 deletions docs/adr/0013-core-module-carve-out.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Core module carve-out: one deep module, two adapters

The suite CLI and the generated-package CLI carried genuinely identical
machinery in parallel: name matching, entry classification and path
resolution, config reading and surgical splicing, manifest io + per-file
hashes, cache paths, and the effective-version pipeline. Templates vendored
that machinery into every scaffolded package; the sync table existed mostly
to police the resulting duplication. This ADR records the carve-out of that
machinery into one deep module, exported as a slim subpath
(`package.json` → `"exports": { "./core": "./src/core/index.ts" }`), and the
shift of templates from structural source of truth to adapter renderings.

## Design twice

### Design A — one engine, two thin shells

Core exports a single package engine (`install` / `uninstall` / `status` /
`ensureAssets`) that owns the whole flow; the suite CLI and the generated CLI
become thin renderings over a policy object
(`{packageName, payload, assetLayoutDir, consentCallback, ...}`).

- Depth: maximal at the interface (one call per operation), but the depth is
fake — the two flows genuinely diverge (suite: legacy copy-install
migration, agents/references/templates payload, package-broad cache
clearing; generated: skills/commands payload, consent-gated root-config
migration, self-scoped cache hygiene). Reconciling them forces boolean and
callback parameters into the policy object — ad hoc parameterization at
exactly the seam this ticket says must declare policy instead.
- Locality: every suite change ripples through the shared engine into
generated-package behavior; the shells are thin, so divergence hides in
flags rather than living in code.
- Seam placement: the seam is a configuration object — policy arrives as
data, and nothing in the type system says which adapter may do what.

### Design B — machinery carve-out, adapters keep orchestration (winner)

Core exports the primitives that are genuinely identical — environment
adapter, scope bases, cache paths and cache-target removal, name normalizer,
entry predicate and path resolver, config reader, registration detector,
config splicer, plugin-config editor, manifest io + per-file sha256 hashes,
loaded-version resolver, registry version check, and the status/effective-
version pipeline (already packageName-parameterized with an injected
`ManifestLookup`). Each package keeps its own orchestration, which is where
the real policy lives, and declares that policy as named constants at the top
of its adapter (package name, payload names, asset layout, allowed cache
commands, scope coverage).

- Depth: each core module is deep in the ordinary sense — a small surface
(a class or a function family) over a large, invariant-carrying behavior
(ADR-0006's manifest gating, semantic entry matching, surgical splices,
ADR-0007 never-throw hooks all live inside core, not in adapters).
- Locality: the residual divergence (suite vs generated flows) sits in two
small adapters, and the sync table names exactly what diverges.
- Seam placement: the seam is the `opencode-architect/core` import path.
Policy is declared in adapter code, not threaded through parameters, and
the template sync table is the written policy of what each adapter keeps.

### Why B wins

A parameterizes what the ticket says to declare; B declares it. The
divergence manifest (references/template-sync-table.md) already names the
per-adapter policy — suite-only broad cache modes, generated-only consent-
gated migration — and B's seam makes that manifest executable: a divergence
is either an adapter-local constant or it is drift. A also couples the two
packages' release cadence at every behavior change, while B couples them
only where behavior is truly shared.

## Manifest-format compatibility rule

Before any generated package depends on the core module, the formats are
fixed as follows:

1. Core's manifest io is the generated-package format, **frozen** at the
carve-out: `<configBase>/<package>.manifest.json` holding
`{version, mode?, entry?, entryConfigPath?, files: Record<relPath,
sha256>}`. Readers accept everything earlier templates wrote: a missing
or malformed manifest reads as "not installed" (drift), a missing `mode`
reads as `"copy"`, per-file sha256 hashes are the only idempotency
mechanism (ADR-0006). Adopting the core dependency is not a manifest
migration — file name, location, keys, and tolerance are unchanged.
2. The suite adapter's own manifest (`opencode-architect.manifest.json`,
`{version, mode, entry, configPath, content-hash, hashes: [{path,
hash}]}`) is a legacy format owned by the suite Installer; core never
reads or writes it, and the file name is reserved.
3. The shared status pipeline is format-agnostic: it consumes manifests only
through the injected `ManifestLookup` surface (`{mode, version}` per
scope), so both formats feed one effective-version computation.

## Templates become adapter renderings

Templates stop being a structural source of truth. The machinery templates
(`plugin-name`, `plugin-entry`, `entry-predicate`, `manifest`,
`registration`, `plugin-config`) dissolve: their content is the core module,
and generated packages import it via the `./core` subpath of a `file:`
dependency on `opencode-architect` recorded at render time. `installer` and
`cli` templates shrink to adapter renderings — policy constants plus
orchestration over core calls — that the scaffold renderer emits.
ADR-0005's resolution mechanics (bundled assets under the installed package,
backtick-relative addressing) stand; its role for these files as the truth
source of generated-package structure is superseded by the core dependency.

## Consequences

- Machinery bugs fix once; generated packages pick them up through the
dependency at their next install, not through re-scaffolding.
- Consumer packages gain a `file:`-based `opencode-architect` dependency;
scaffolding and promotion must establish the local link without registry
access (the rendered fixture proves the end-to-end path).
- The sync table loses its byte-synced machinery rows; what remains are the
adapter renderings and their declared policies.
- `promote` inherits the new shape automatically (it renders through the
same scaffold renderer) but must link the core dependency before running
the generated package's install.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"module": "index.ts",
"exports": {
".": "./index.ts",
"./server": "./index.ts"
"./server": "./index.ts",
"./core": "./src/core/index.ts"
},
"bin": {
"opencode-architect": "src/cli.ts"
Expand Down
5 changes: 4 additions & 1 deletion references/conformance-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ item; an item with no evidence found is a finding.
`<package>@*` idempotently (nothing cached is a success) with the same
warn-and-continue semantics; a `--package <name>` or `--all` mode on a
generated package's CLI is non-conformant — broad cache deletion belongs
to the suite's own CLI only. The load-time hook never deletes cache
to the suite's own CLI only (one of the declared generated↔suite
divergences in `references/template-sync-table.md`, which classifies every
template↔suite pair and its policy; do not restate pair states here).
The load-time hook never deletes cache
entries (deletion races OpenCode's in-flight installs, ADR-0007): when
bundled assets are absent (partial cache artifact), its advisory
instructs running `bunx <package> clear-cache` and reinstalling. A hook
Expand Down
Loading
Loading