agentleFS
dylanroscoverdylanroscover/Embody/.claude/skills/create-extension/SKILL.md

create-extension

Skill213 starsChanged 2 months agoMITView on GitHub

MUST READ before calling create_extension. Required parameters, lifecycle methods, and wiring steps.

3 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

3 rules. 0 are common, 1 is distinctive, and 2 are very specific to this project.

  • 0

    Common

    Most of the 3 edited versions we read keep these.

  • 1

    Distinctive

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Use the create_extension MCP tool to create a fully-wired TD extension:

    line 9
  2. Distinctive:

    Always implement these in your extension class:

    line 29
  3. Very specific:

    If the extension lives inside a TDXN-strategy COMP (or the extension's ownerComp is one), onInitTD will fire before TDXN import …

    line 105

“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md
<!-- Generated by Embody/Envoy - Do not remove this comment - sha:6b85e409552f32cb -->

Create Extension Workflow

Use the create_extension MCP tool to create a fully-wired TD extension:

create_extension(
    parent_path='/path/to/parent',
    class_name='MyFeatureExt',
    name='MyFeature',           # Optional: COMP name
    code='...',                 # Optional: initial Python code
    promote=True,               # Optional: promote extension methods
    ext_name='MyFeature',       # Optional: extension name
    ext_index=0                 # Optional: extension index
)

This creates: baseCOMP + text DAT + extension wiring, initialized and ready to use.

Wiring by hand: the Extension Object parameter is a constant-mode Str par whose VALUE is Python (par.ext0object.val = "op('./MyFeatureExt').module.MyFeatureExt(me)" -- the Extensions page is the ext sequence on 2025 builds: ext0object, ext0name, ext0promote; there is no extension1). Never set its .expr: the extension comes back None with extensionsReady True and errors() empty; the only trace is a SyntaxError ... Context:(Extension 1) on scriptErrors() (get_op_errors kind: 'script').

Extension Lifecycle Methods

Always implement these in your extension class:

class MyFeatureExt:
    def __init__(self, ownerComp: COMP) -> None:
        self.ownerComp = ownerComp

    def onDestroyTD(self) -> None:
        """Called on old instance before TD reinitializes. Clean up callbacks, timers, etc."""
        pass

    def onInitTD(self) -> None:
        """Called at end of frame after init. Safe to access other extensions and cooked network."""
        pass

Naming Convention

Extension classes and source DATs must follow the NameExt convention:

  • Class: MyFeatureExt
  • DAT: MyFeatureExt (matches class name)

Three Namespace Tiers

TD promotes every capitalized member -- methods and class constants alike. There is no per-member opt-out, so the capital letter is the access modifier. Pick the tier by who calls it:

TierNameReached asWho calls it
1. Public APIUpperCamelCaseop.MyFeature.DoSomething()A user or an agent, deliberately
2. WiringlowerCamelCaseop.MyFeature.ext.MyFeature.onFrame()The COMP's own exec / callback / parexec DATs
3. Private_lowerCamelCaseinside the class onlyThe class itself

Test: could a user or an agent reasonably call this ON the COMP? If not, it is not tier 1. Promoting a frame hook is a design flaw, not a shortcut.

What makes an over-wide tier 1 harmful is concrete, not cosmetic: co-mounted extensions share one namespace and TD documents no precedence for a duplicate, so one name silently wins; and every promoted name -- constants included -- is reachable by any getattr(comp, name) dispatcher. Promoted members do NOT appear in dir(), so this is not an autocomplete argument.

Two traps:

  • .ext.<Name> resolves by the Extension Name parameter, not the class name. If ext_name='MyFeature' and the class is MyFeatureExt, then op.MyFeature.ext.MyFeature.x() works and op.MyFeature.ext.MyFeatureExt.x() raises.
  • op.MyFeature requires a Global OP Shortcut, and create_extension never sets one. Straight out of the tool, op.MyFeature raises AttributeError. Reach the COMP by a relative path (op('./MyFeature'), parent.Host.op('MyFeature')), or set par.opshortcut deliberately -- it is globally unique, so assigning a name already in use silently steals it from the previous holder. Reserve it for genuine project-wide singletons.

Pass parent_shortcut when descendants need to reach the COMP. It sets par.parentshortcut, which is what makes parent.MyFeature resolve and keeps the COMP's own callback DATs off depth-coupled parent() chains:

create_extension(parent_path='/project1', class_name='MyFeatureExt',
                 name='MyFeature', ext_name='MyFeature',
                 parent_shortcut='MyFeature')

It refuses to overwrite a shortcut the COMP already declares -- re-pointing one silently redirects every reference already using it -- and says so in the result's warning.

Both shortcuts can also be set by hand:

comp.par.parentshortcut = 'MyFeature'   # descendants reach it as parent.MyFeature
comp.par.opshortcut = 'MyFeature'       # project-wide: op.MyFeature -- singletons only

NEVER cache extension references in variables -- always call inline. Cached refs go stale on reinit.

Referencing From Inside the Extension

Capture the owner once and navigate from it. self.ownerComp is rung 1 of the referencing ladder in td-python.md; the bare global parent() is banned in an extension class body.

class MyFeatureExt:
    def __init__(self, ownerComp: COMP) -> None:
        self.ownerComp = ownerComp          # rung 1 -- everything else hangs off this

    def rebuild(self):
        target = self.ownerComp.op('./render1')     # own children
        cfg = self.ownerComp.par.Mode.eval()        # own parameters

Extensions Inside TDXN COMPs

If the extension lives inside a TDXN-strategy COMP (or the extension's ownerComp is one), onInitTD will fire before TDXN import reconstructs the network. Any state set up during onInitTD - created operators, parameter values, stored data - is overwritten when ImportNetwork runs with clear_first=True.

Always defer initialization:

def onInitTD(self):
    """Defer setup so it runs after TDXN import completes."""
    run('args[0].postInit()', self, delayFrames=5)

def postInit(self):
    """Safe to set up state here - TDXN import is complete."""
    # Create operators, set parameters, initialize state
    pass

This applies on project open, after every Ctrl+S (strip/restore cycle), and on manual TDXN reimport. The deferred method must be idempotent - it may run multiple times.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.