Owner: rick. Status: test phase (private repos). Created 2026-08-22.
Restructure the img2threejs ecosystem so capabilities are plugins, modeled on deepseek-ai/deepseek-harness:
- Each plugin is its own git repo, versioned and released independently.
- One-command install per announced plugin (
img2 add <plugin-id>); explicitorg/reposources remain supported for plugins outside the official catalog. - Adding a future plugin (img2glb, glb2threejs, …) for an ALREADY-DECLARED slot requires
zero edits to the harness (registration only). Declaring a NEW slot the pipeline does
not yet expose is a base-skill release, not a plugin install —
glb2threejswill require a base-skill edit for exactly this reason, and that is stated honestly rather than folded into the zero-edits claim. - The design is NOT anchored on the current img2threejs repo layout.
- deepseek-harness (verified from source): plugins are tiny uniform packages;
composition is data rows
{id, name, config, disabled}; no privileged core; fail-loud composition audit; dump==mount via shared code path; discovery by GitHub topic, no central registry; per-plugin docs/tests co-located. - Current repo audit (275 tracked files, ~62.3K LOC): single flat skill; SKILL.md
(620 lines) routes by literal path strings to 43 tools + ~30 docs; 28 gates with no
registry and no shared verdict format; state written into the shared checkout instead of
the user's project; ~47
parents[N]root computations in forge/ that fail silently on any move; clean seams worth reusing: HOSTS table in bin/cli.mjs, FamilyAdapter registry, integrations/ out-of-process contract, buildPasses override. - Adversarial design review — 5 blockers found and resolved; decisions below.
org img2threejs
├── img2 # THE HARNESS (this repo)
│ ├── bin/img2.mjs # CLI: install / add / remove / list / doctor / sync (Node ≥18, zero deps)
│ ├── img2_core/ # Python pkg (stdlib only): paths, state envelope+lock, gate_runner, api
│ ├── docs/PLUGIN_CONTRACT.md
│ └── tests/
├── plugin-hello-cube # minimal REAL plugin: image → threejs-code, 1 gate, 1 step
├── plugin-img2glb # image → glb (wraps the TRELLIS mesh3d integration)
├── plugin-img2threejs # (later) today's 62K-LOC skill, migrated
└── plugin-glb2threejs # (future) — must require zero harness edits to add
User-facing flow:
npx @img2threejs/img2@latest install # $IMG2_HOME (~/.img2), host links, settings merge
img2 add img2glb # catalog ID → Git source, tag/SHA pin, row, host links
img2 doctor # fail-loud static audit of every row
img2 sync --check # generated edge list == manifests' declared edges (CI-able)
CLI 0.4.0 adds official-ID shorthand and branded, responsive human tables. Catalog and version/doctor/capability JSON remain machine-readable; plugin/core schemas are unchanged.
| # | Decision | Rejected alternative & why |
|---|---|---|
| D1 | Per-plugin host symlink img2-<id>; no router skill |
Router-only: one truncated description hides N capabilities; selection is documented as stochastic (a 0.5 trigger rate counts as passing), and a ~1%-of-context listing budget drops descriptions starting with the least-invoked skill — exactly what an optional per-edge provider is |
| D2 | All generated artifacts in $IMG2_HOME/generated/; img2 capabilities is the runtime reader and recomputes from the registry and manifests, never parsing the routes.json cache directly |
Index inside the harness checkout: dirties git, breaks img2 update (tracked-dirt refusal) |
| D3 | Flat plugins.json rows, whole-row replacement |
dsh-style layered patches: we have no profiles/bundles — YAGNI |
| D4 | Python core linked via generated _img2_local.py + IMG2_HOME fallback; core is a real package; require_core_api(n) |
pip install from git: PEP 668 breaks Homebrew/Debian; N interpreters on PATH = silent wrong-env installs |
| D5 | State envelope in <workspace>/.img2/state.json, plugin-scoped subtrees, core-owned lock |
Today's behavior (state in shared checkout) is an existing defect, not a precedent |
| D6 | Version mismatch BLOCKS (add + sync + runtime assert); harness declares max readable plugin schema | Warn-and-continue: wrong core + plausible screenshot can pass the visual gate by luck |
| D7 | Trust: default source = img2threejs/*, --allow-any-source escape, pin tags/SHA, doctor static-only |
Open add <any-url> reproduces the classic curl-pipe-sh problem inside agent skills |
| D8 | Harness repo named img2 (npx github:img2threejs/img2), CLI img2, topic img2threejs-plugin |
harness is meaningless outside org context; img2-plugin topic too generic; npm names verified free |
| D9 | Gate contract: argv in → one JSON verdict envelope on stdout, exit 0/1/2 | Status quo: 29 review scripts, 26 print ad-hoc JSON, no shared helper — the runner must define the contract |
| D10 | Steps as data with after: deps, topo-sorted, cycles = doctor error |
if/elif profile insertion (today's workflow_state.py) cannot accept third-party steps |
| D11 | Capability conflicts allowed: doctor warns, img2 capabilities refuses to resolve the edge and names both claimants |
A resolver/solver — YAGNI |
| D12 | Chains (image→glb→threejs) EXCLUDED from test phase | Depends on artifact-kind contracts not yet written; hand-glued chain proves nothing |
| D13 | Owner's preference: step and gate override is permitted; flexibility over paternalism, because a fork removes the gate AND the record | — |
| CUT | spec_search in core, env/hostRequirements manifest fields, img2 test, enable/disable, layering |
No consumer today; each is an hour's work the day a consumer appears |
D13 is deliberately mechanism-free: every other row in this log carries the alternative it
rejected and why; D13's justification is not yet measured, so no mechanism is recorded until
it is. It authorizes the overrides reservation in PLUGIN_CONTRACT.md §6, not a built
feature.
Phase 0 — research + adversarial design review. DONE (this document).
Phase 1 — contract + plan docs. DONE: docs/PLUGIN_CONTRACT.md is the normative spec.
Phase 2 — build the thin slice (private repos, parallel):
img2: CLI (install/add/remove/list/doctor/sync),img2_core(paths, state, gate_runner,require_core_api), unit tests, legacy~/.img2threejsmigration.plugin-hello-cube: image → deterministic cube .js; 1 gate (cube-structure, real verdict envelope); 1 step; tests. Exists to exercise every contract surface with zero domain risk.plugin-img2glb: portintegrations/mesh3d/generate_reference_mesh.py; declares image→glb;--probeoffline mode (validates inputs, emits plan JSON, no network) so acceptance tests are deterministic; live TRELLIS call stays manual.
Phase 3 — acceptance run (fresh $IMG2_HOME + fake $HOME in a scratch dir):
all 10 tests below, binary pass/fail, results in the report.
Phase 4 — review with owner. Repos stay private; walk through results; go/no-go for migration phases.
Follow-on change order, each its own change, in sequence: plugin-img2glb (offline step
row + declared artifact kind) → plugin-hello-cube (<image> fix, exerciser edge, version
bump, new tag) → base-skill slot → SKILL.md heading merges. The base-skill slot step is
BLOCKED on an owner decision, not an agent's: two working copies of the base skill were
unmerged (img2threejs on main, 613-line SKILL.md, vs feat/npx-skill-installer on the
remote, 620-line SKILL.md, differing by 16 files / 1760 insertions / 838 deletions), and
every line number in the base-skill change had to be re-derived against whichever was
chosen — see "Decisions settled 2026-08-23" below for the resolution.
Phase 5 — turn the base pipeline into data (separate change, NOT now): see the
route-pipeline-slots-through-capability-providers design's D-J for the reframing, the
corrected step/gate inventory, and its five recorded costs (a)-(f) — this is not "move
62K LOC into a plugin", it is "turn the pipeline into data without changing what it means."
Ordering constraint this phase MUST respect (two restructures must not race over forge/ —
a call-time failure mode): land or explicitly abandon support-all-glove-subtypes (29/44)
and separate-cs2-tracks-from-forge (0/39) before migrating. Both proposals live in
openspec-from-img2/changes/ and must be adopted into the OpenSpec root or explicitly
retired first, because separate-cs2-tracks-from-forge restructures the same forge/ tree
the migration moves — see "Decisions settled 2026-08-23" below for the resolution. The
"717-line uncommitted diff" support-all-glove-subtypes was said to carry was NEVER in a
working tree: git status --porcelain in the checkout that cited it showed only
?? graphify-out/, so the figure came from proposal prose, not a working tree, and nothing
was lost when that checkout was deleted.
Phase 6 — go public: publish img2 to npm (name verified free — reserve it), repos
public, topic img2threejs-plugin on every plugin, README quickstarts, deprecate the old
installer path in img2threejs/img2threejs with a pointer.
npx-equivalent install on a clean$HOME→ host symlinks +$IMG2_HOMElayout, exit 0.img2 add img2threejs/plugin-img2glb→ clone, row withresolvedSha, host symlink, exit 0, AND harness checkoutgit status --porcelainEMPTY (the zero-core-edits claim, mechanically).- Same
addagain → non-zero, names the existing row; no silent overwrite. - Fresh agent session in an EMPTY project dir, prompt "turn this image into a GLB" →
model discovers and invokes
img2-img2glbwith zero permission prompts. img2 sync --checknon-zero after a manifest edit; zero afterimg2 sync.- Both plugins' gates run through
img2_core.gate_runner, emit the same envelope; a deliberately failing gate stops the workflow. - Two plugins write state under their own key without clobbering; concurrent invocation serializes on the lock.
requires.harness: ">=99"→addanddoctorfail loud, naming the row.doctorfails a plugin containingparents[N]or a cross-plugin import (static only).img2 remove→ no dangling host symlink on ANY host, no orphan row.
Thin slice built and tagged v0.1.0 on all three repos (harness 1013a62, hello-cube
da8babc, img2glb 353a8ea). Unit suites: harness 22 node + 32 python; hello-cube 6; img2glb 12 — all green.
Note: the v0.1.1 launcher release bumped package.json without re-running the suite, leaving two
version assertions hardcoded to 0.1.0 red on main until they were changed to derive the expected
value from the exported harnessVersion(). Re-run both suites after any version bump.
Acceptance: tests 1–3, 5–10 pass as written (test 2's zero-core-edit check: both
harness checkouts git status --porcelain empty after add). Test 4 passed its
structural half (symlink → $IMG2_HOME, valid frontmatter, settings merge); the
live-model half is the remaining manual step: open a real host session in an empty dir
and ask for an image→GLB conversion. Substitution: private-repo clones used file://
local paths under the sandbox fake-HOME (https creds unavailable there); the
newest-tag-resolution path is now exercisable for real since v0.1.0 exists.
Findings fixed before tagging: (1) _img2_local.py fallback only resolved next to the
running script — sync now writes it into the clone root AND tools/ (contract §8
amended, regression-tested with an env-empty subprocess); (2) lost-update window in
state read-modify-write — update_plugin_state() added holding the lock across the whole
cycle (contract §11 amended; the old pattern measurably lost 20/40 writes under the new
concurrency test).
route-pipeline-slots-through-capability-providers sections 1-7 applied. Suites green: 59 node +
36 python. Delivered: the three live v0.1.1 defects closed (metacharacter refusal in
commandFinding, confirmOrThrow in cmdAdd for out-of-org sources, resolve_workspace refusing
any skill or plugin checkout); img2 capabilities with the always-JSON envelope, argv-tokenised step
commands and per-row failure isolation; img2 --version --json advertising contract: 14 and the
derived commands list; img2 doctor --json, the multi-capability WARN, the base-host-link report,
and the deletion of the false "the model picks by description" claim; plugin.json refusing an
overrides key and img2 add --force preserving user-authored row keys.
Acceptance re-run in a fake-HOME sandbox: test 18 — after img2 add --link plugin-img2glb the
harness checkout is porcelain-clean and img2 capabilities --from-kind image --to-kind glb answers
with img2glb; test 19 — --version --json lists capabilities in commands.
Two defects found during manager verification, not by the implementing agents, both fixed with regression tests:
img2 doctorfailed on a fresh install.cmdDoctor's drift check had no never-synced guard, so the first command a new user runs afterimg2 installexited 1 with "generated artifacts are out of sync".cmdCapabilitieshad the guard;cmdDoctordid not. The asymmetry was the bug.- The capabilities envelope reported the harness version in its
versionfield, where the envelope's own schema version belongs — a consumer branching on it would have read"0.1.1"instead of1. The harness version is advertised by--version --jsonand nowhere else.
.omc/ and _img2_local.py added to .gitignore so the zero-core-edits claim is literally true of
git status --porcelain, not true modulo operational dirt.
"Adding a plugin requires zero edits to the harness" is true for a provider serving an
already-declared capability edge, and it is mechanically tested: after img2 add, the harness
checkout stays git status --porcelain-clean (acceptance test 2, re-run and passing).
It is NOT true for a new KIND of capability. Declaring a new slot — a point where the base pipeline
asks for a provider it has never asked for — is a base-skill release, because the base skill is what
holds the question. Concretely: adding a second image → glb provider costs nothing; adding
glb2threejs means the base skill must acquire a place that asks for glb → threejs-code. The
research base flagged this as the known cost of this model (a router must be updated per capability
kind, which is an Open-Closed violation), and it is recorded here rather than left implied by a claim
that reads as absolute.
plugin-img2glb— add an offline-capable step row. Its only row hardcodes the live TRELLIS path, andgradio_clientis absent on the maintainer's machine, so the step dies on import today. Nothing downstream can be verified without a provider whose step runs offline.plugin-hello-cube— done ahead of order at v0.1.1 (<image>→{image}); its remaining item is whether it should stop declaring the product's ownimage → threejs-codeedge.- Base-skill consultation — the base skill asks
img2 capabilitiesat an existing optional step. Step 1c of The Loop ("Optional fidelity evidence adapters") is already scoped exactly for this: on the mainline, explicitly optional, and its authority boundary is already written indocs/integrations/reference_fidelity_tooling.md("external tools may produce evidence and diagnostics; they may not…"). No new slot is required, which removes the objection that the GLB-mediated section is only reachable once a GLB already exists. - Row schema / pipeline-as-data — gated on the lab experiment's measured field count, not on a
guess. See the lab findings on branch
lab/pipeline-as-data.
Owner decision recorded: work branches from main; feat/npx-skill-installer stays unmerged on the
remote.
All ten original acceptance tests re-run in a fresh fake-HOME sandbox against the shipped harness.
9 pass, 1 manual.
| # | Result | Evidence |
|---|---|---|
| 1 | pass | install --from → $IMG2_HOME layout complete, exit 0 |
| 2 | pass | row carries resolvedSha, host symlink created, harness checkout porcelain-clean |
| 3 | pass | repeat add exits 1 naming the existing row |
| 4 | manual | needs a live agent session; not automatable |
| 5 | pass | sync --check 0 → 1 after a manifest edit → 0 after sync |
| 6 | pass | both plugins emit img2.gate-run; hello-cube exits 0, img2glb's blocking gate exits 1 |
| 7 | pass | two concurrent writers, 15 increments each, both subtrees exact, lock respected |
| 8 | pass | requires.harness ">=99.0.0" → add exits 1 naming both versions |
| 9 | pass | doctor exits 1 catching BOTH parents[N] and the cross-plugin import |
| 10 | pass | remove leaves zero dangling symlinks on any host, receipts cleared |
Plus the two tests this change added: 18 (zero-core-edits extended to the query surface) and 19
(old-harness detected as absence through --version --json).
| Risk | Mitigation |
|---|---|
| Gate-contract adaptation of 28 legacy gates blows the schedule | Contract proven on 2 gates now; legacy adapts behind a wrapper shim in Phase 5, gate-by-gate |
| Host permission behavior differs from docs | Acceptance test 4 runs against the real host before anything else depends on the layout |
| TRELLIS endpoint flaky/needs network | --probe offline mode is the tested path; live call manual |
| Two restructures race over forge/ | Explicit ordering rule in Phase 5; harness phase touches zero existing files |
| Legacy installs orphaned | img2 install migrates ~/.img2threejs; IMG2THREEJS_HOME honored one release |
- Deferred:
img2 doctor --strict(FAIL on any WARN).cmdDoctorreturnsEXIT.OKwith any number of warnings (bin/img2.mjs, end ofcmdDoctor), so no WARN can fail an automated check today. Shipping--strictnow would silently convert existing advisory WARNs into build breaks for existing installs. This change's own enforcement of its warnings is the query's machine-readableproblems[]/providers[]split with its own exit codes (D-E), not--strict.--strictbecomes load-bearing the day the first WARN-level finding must fail a build, and every such finding known today (an active override, a missing declared Python package) is itself deferred, so there is nothing yet for it to enforce. - Override precondition, not yet a contract SHALL: override does not ship until the gate
ledger has a writer, a home, and a test that fails when a disabled or overridden gate is
omitted from the run record, and until
img2 doctor's surfacing of an active override is specified down to its message and exit consequence. Evidence:img2_core/gate_runner.pyis per-plugin-directory, prints its aggregate to stdout and persists nothing (:147), and the only durable per-pass record (reviewHistoryin the sculpt spec) has no field that could hold a gate roster — so two reports, one with all gates and one with three disabled, are byte-indistinguishable today. - Deferred:
requires.python.packagesdetect-and-report (D-K). When built: an optional additiverequires.python.packageslist inplugin.json(absent means unknown, no schema bump), and a doctor WARN naming the plugin, the missing package, the probed interpreter, the install command, and which steps still work without it. The CORRECTED probe ispython3 -I -c "import importlib.util,sys; sys.exit(0 if importlib.util.find_spec('X') else 3)", run with the cwd set OUTSIDE every clone; declared package names MUST match^[A-Za-z_][A-Za-z0-9_]*$and be top-level only (find_spec('a.b')still importsa); the interpreter is derived from the step row'sargv[0], never assumed to bepython3on doctor's PATH. Why the correction matters:python3 -c "import X"puts the cwd onsys.path[0], so it executes a plugin-suppliedX.pyduringimg2 doctor— exactly what §4's static-trust boundary forbids. - What a future
glb2threejsprovider is allowed to do.img2threejs/SKILL.md:88states the raw GLB is never pixel evidence and its topology/materials are never copied into the factory;:34plusCLAUDE.md's code-only promise forbid photogrammetry, mesh extraction, or downloaded art packs; the existingintegrations/contract permits an external tool to produce evidence and diagnostics but not to silently provide meshes, decide hidden geometry, mutate the accepted source, or approve a pass. A provider that translates GLB geometry into Three.js code contradicts that identity; a provider that derives evidence from a GLB (proportions, semantic regions, comparison baselines, capture profiles) is consistent with it. The base skill already performsglb → threejs-codein the GLB-mediated track, so the open question is which implementation is permitted, not whether the step exists. Deferred to the owner — see "Decisions settled 2026-08-23" below for how it was closed. - The closed placeholder set lands ahead of pipeline-as-data and constrains it. The base
pipeline's own step commands use
{reference}(9),{spec}(8),{pass_id}(4),{path},{total},{step_id},{pass_count},{expected_id},{current_pass}, none of which are in{plugin_dir}/{workspace}/{image}. Phase 5 must therefore either widen the permitted set (undoing part of this change's hardening) or rewrite every step command before it can express the base pipeline assteps.jsonrows. The guard is still correct to land first.
Three owner decisions, closing the corresponding open questions above and in Phase 5:
(a) Base-skill work branches from main, NOT from feat/npx-skill-installer. main
carries the 613-line SKILL.md and does not contain the npx-installer commits, which stay
unmerged on their own branch. No line number for the deferred base-skill change may be
derived from the 620-line variant.
(b) The eleven proposals previously living only in openspec-from-img2/ are ADOPTED into
the OpenSpec root: support-all-glove-subtypes (29/44), separate-cs2-tracks-from-forge
(0/39), route-cs2-family-through-adapter-registry (0/15), guide-unsupported-cs2-families
(0/22), add-npx-skill-installer (80/83), plus six glove/typecheck changes. Phase 5's
ordering rule now points at live changes in the OpenSpec root rather than at a backup
directory.
(c) glb2threejs is not scheduled and no phase reaches it. If it is ever built it
follows whatever SKILL.md defines at that time, which today means the evidence-producing
kind, because SKILL.md:88 forbids copying GLB topology or materials into the factory. The
open question above is therefore CLOSED by deferring to the product contract rather than by
choosing a design.