- 2026-11-03 -- Fixed (release CI):
FileDependencies.walklists every folder in name order, andExportRun(fbx_media_selfcontained=)is removed (file_utils/file_dependencies.py,core_utils/engines/scene_export/export_profile.py).os.walklists in the file system's own order (NTFS sorts, ext4 does not), so of two identical copies of a missing file a Linux run resolved to a different one than a Windows run; subfolders and files now come case-folded name first on every platform, forfind_filesand every DCC texture walk alike.fbx_media_selfcontained, retired 2026-10-05 for 0.15.0 (every FBX deliverable embeds its media, so nothing read it), reached the end of its 30-day window: a caller still passing it gets aTypeErrornaming it. Two test classes resolve their temp folder, since the results they compare name files resolved and a Windows runner's TEMP is spelled with 8.3 short names (RUNNER~1); one test's assigned lambda is adef(ruff E731).- Test(s):
test_file_dependencies.pynewtest_the_walk_lists_in_name_order_whatever_the_file_system_gives(red first on the unsorted walk);test_export_profile.pytest_the_self_contained_flag_is_removed(was the warning test);test_agent_instructions.pyandtest_operations.pyTestBatchOperationHostpass under an 8.3 short TEMP (5 red first there);ruff check .clean.
- Test(s):
- 2026-11-03 --
migratesettles a carrier an import left beside the scene's own, andis_carrierknows Blender's numbering (core_utils/engines/scene_export/scene_store.py). A plain file import (Maya File > Import, Blender Append) never merges the other scene's carriers, so itsdata_export1/data_export.001was read and refreshed by nothing, yet every export shipped it and readers took its records for a referenced module's: a production assembly's carried a pre-renamelightmap_metadataand a version-1 handoff, and the deliverable verifier failed the file.SceneStoreBase.migrate(run on open and before every export commit) now removes such a copy when nobody takes anything from it: a merge would bring nothing in, and a reader none of its records (each is shadowed by the scene's own copy, under an old key, or in a version the verifier refuses; the handoff goes with them). One that holds more (shots, an undeclared channel, a current record the scene's own lacks, a keyed value) stays, named, the user's tomerge_carriers/discard_carriers. A host names its copies through the new_leftover_carriershook (default: none).is_carrieralso matches Blender'sdata_export.001, so the verifier and the FBX and GLB passes read a Blender copy as they read Maya's.- Test(s):
test_scene_records.py+9 (TestLeftoverCarriers,TestStore.test_a_carrier_is_named_as_every_host_numbers_a_copy), red first.
- Test(s):
- 2026-11-03 -- Fixed (review):
AgentInstructions.syncremoves only its own pack files and writes only what it can read, whole; the repo hook runs without a shell and survives a file it cannot read (core_utils/agent_instructions.py; every repo's.agents/session_hook.pyand.claude/settings.jsonresynced). A pointer link could name any file:[principles](docs/principles.md)deleted a user's file when the pack moved, and an absolute or..link one outside the repo. A moved pack is now removed only from.agents/packsor.claude/rules, under its own name; a link outside the codebase is no record; apacks=name must be a plain name. Asettings.json, a vendored pack or its source that is not UTF-8 is refused asAGENTS.mdis (a UTF-16 settings file lost itspermissions); a rewritten file keeps its CRLF and byte-order mark; each file is written to a.partand renamed onto it; asession_hook.pysync did not write is refused (one the earlier template wrote is replaced). The hook is registered in exec form,"command": "python", "args": ["${CLAUDE_PROJECT_DIR}/.agents/session_hook.py"](the expansion measured in Claude Code 2.1.317): PowerShell 5.1 rejected the||shell string, so a lone clone on Windows without Git Bash got no packs. Thepython3fallback is gone: a virtual environment orpython-is-python3providespython, and without oneAGENTS.mdstill links the packs.checkreports the shell form as stale. The hook skips a pack or aCLAUDE.mdabove it that it cannot read, and never fails the session: one pack that was not UTF-8 cost every pack.- Test(s):
test_agent_instructions.pynewTestSyncLeavesWhatItDoesNotOwn(7) andTestHook+2 (test_a_pack_it_cannot_read_is_skipped_and_the_others_are_added,test_a_claude_md_above_it_cannot_read_costs_nothing), each red first.
- Test(s):
- 2026-11-03 -- Fixed (review):
ApiSurface.readkeeps no parse tree, and a file it could not read once is read again; a cache write leaves no.part; a default search survives an editable install that is no package (core_utils/api_surface/,core_utils/package_manager.pyeditable_packages). The process-wide AST memo kept every tree it parsed: 925 MB resident after reading the seven workspace packages, 275 MB now. Each file is parsed once per read, and a module whose files are all unchanged is not walked again, so a re-read of pythontk after a one-file edit takes 0.13 s (1.6 s before; 6.3 s with the per-read parse alone). A file a sync client or a scanner held during a read (PermissionError) was remembered as no module, in the process and on disk, until it changed; a read that meets one is now kept nowhere. A cache write whose rename Windows refused left its multi-MB.part: the rename waits out a reader's hold (WinError 5 or 32), the.partgoes whatever happens, the sweep removes a.partolder than 10 minutes and passes over an entry another process swept first, and a refresh (os.utime) it cannot make no longer turns a hit into a miss. A tree under a temp folder is kept in the process only (test sandboxes had left 551 entries in the user cache, now removed), and each Python version keeps its own entry. A defaultsearch(theapi.searchoperation) passes over, and logs, a registered package it cannot locate, raising only for one it was given;editable_packageslists only names that import as packages, and reads a distribution with no name or adirect_url.jsonthat is not an object.- Test(s):
test_api_surface.pynew inTestReadCache(6: a file it could not read, no tree outlives its read, a refused rename, the refresh, the sweep, an edit re-walks only what it reaches), newTestReadKeptOnDisk(2), newtest_a_default_search_skips_a_registered_package_it_cannot_locate,test_editable_packages_reads_each_distribution_installed_editable+6 cases,test_a_tree_read_once_is_kept_nowhereholds_PARSEDand_WALKEDtoo; red first except the edit test, written with the module memo (it fails when a missing base file is not watched).
- Test(s):
- 2026-11-03 -- Fixed (review): code saves only what it saves itself; only code is interrupted on a cancel, and in Blender too; the code's own
KeyboardInterruptno longer ends the instance; the plan and apply servers work in one instance; a code operation is never served on the plan server (core_utils/operation_catalog/batch_operation_host.py,session_operation_host.py,operation_server.py,core_utils/python_console.py).- Code has no plan, so the save rule that saves "the scenes a live run of an operation that plans changed" no longer saves for it. A read-only snippet naming a scene rewrote the file, and in Blender a .blend was upgraded on every look. What code leaves unsaved is held: the next call that opens scenes is refused until code saves or discards it.
- The cancel's interrupt watches only code. An operation with checkpoints stops at one, keeping its journal; interrupted, its changes were lost from the outcome and nothing was held, so the next call that opened scenes discarded them unasked.
- An application that gave Python no SIGINT handler gets Python's default before the watch arms (background Blender, measured in 5.1:
interrupt_mainwas dropped, so a cancelled loop ran on until the grace killed the instance). Measured after: awhile Truecancelled at 1 s stops at 3.1 s, and the same Blender keeps its names. - A
KeyboardInterruptthe code raises itself ispartial, and the instance and its names live on. - Held changes are marked after any live change, and the host's own flag says whether a file has them: an untitled scene a run built with
scenesnamed was dropped before. - The key a call without scenes goes to is kept in the session's folder (
current), so the plan server no longer planned against another Unity project than the apply server worked in. - A session folder name cut at 80 characters keeps a hash of the whole.
OperationServerserves acodeentry on the apply server alone, however it is declared.PythonConsole.runraisesSyntaxErrorfor code that cannot be parsed at all (NUL bytes).- The instance's output pipe is closed with the log.
- Test(s):
test_session_operation_host.pynewtest_the_codes_own_keyboard_interrupt_keeps_the_instance_and_its_names,test_code_saves_only_what_it_saves_itself_and_what_it_leaves_is_held,test_every_process_serving_the_session_works_in_its_current_instance, the plan-server case intest_code_is_served_where_changes_are_never_as_a_plan;test_python_console.py(NUL bytes). The Blender cancel was measured with a kept Blender.
- 2026-11-03 --
API_INDEX.md,API_REGISTRY.md/.jsonandAPI_CHANGES.mdare no longer committed: the API is searched live (api.search;python -m pythontk ops run api.search "query=<words>"); newApiSurface.read(cache=False)reads a tree once and keeps it nowhere (core_utils/api_surface/api_surface.py; theapi_surface,symbol_record,deprecation,module_resolverand__main__docstrings;docs/README.mdand the sub-package READMEs; CI'sapi-retirementsjob,publish.yml). m3trik'scheck_api_surface.pyreads a release's source copied to temp through it; kept, each such read would leave megabytes per package in the per-user cache until the sweep.- Test(s):
test_api_surface.pynewtest_a_tree_read_once_is_kept_nowhere.
- Test(s):
- 2026-11-03 -- Fixed: a kept instance held a scene the code had saved as unsaved; code that did not compile came back
partial; a call naming a scene in a host's default project started a second instance of it (core_utils/operation_catalog/batch_operation_host.pynew_mark,operation_host.py,session_operation_host.py_instance_for,_operation_catalog.py,core_utils/python_console.py). Three defects the Unity and Blender end-to-end runs over MCP found. (1) A code run marked what the instance holds as changed AFTER it ran, so a save the code made was undone by the mark: every later call that opened scenes was refused. A run that keeps no journal is now marked BEFORE it runs, so the code's own save clears the mark as the application's would; a run whose journal names a change is still marked after. (2)PythonConsole.runraisesSyntaxErrorwhen the code does not parse (none of it ran), and acodeoperation that raises one isfailed, notpartial: a typo no longer holds the scene as unsaved. (3) A call without scenes now stays on the instance the session last worked in, else the host's default (instance([])), so a host whose default is a project (Unity's sandbox) keeps ONE instance for it whether a call names its scenes or not;scenesmay name a folder a host opens whole (a Unity project).- Test(s):
test_session_operation_host.pynewtest_code_that_saves_its_change_leaves_nothing_held,test_code_that_does_not_compile_fails_and_holds_nothing,test_its_default_project_is_one_instance_with_or_without_its_scenes(each red first);test_python_console.py(a syntax error raisesSyntaxError).
- Test(s):
- 2026-11-03 -- New effect
code: runs code it is given, may change anything, no plan and no undo (core_utils/operation_catalog/operation.pyOperation.CODE,OperationEntry.EFFECTS,operation_provider.py,operation_server.py; every*.python.execdeclares it).operations.checkholds every change to a dry run and told a console to take a context, which would only echo its code;codeis the one change with none (the check flags acodeoperation that takes a context instead), its tool says it runs code and is served on the change server alone. The C# contract'sEffectgainsCodewith it (base architecture rev 215).- Test(s):
test_operations.pynewtest_code_runs_with_no_plan_and_a_code_operation_that_plans_is_a_problem(red first).
- Test(s):
- 2026-11-03 -- Code is applied, never planned, and a cancel interrupts it; an application that serves a session itself is kept, one instance per project (
core_utils/python_console.py,core_utils/operation_catalog/batch_operation_host.pynewserve/instancestrategies andINTERRUPT_AFTER,session_operation_host.py,_operation_catalog.py,operation_server.py).PythonConsole.runtook a context only to echo its code back as a "plan", which told nobody anything and put a code tool on the read-only server; it takes none now, so every*.python.execis an apply-only tool (ops runrefuses it: no dry run) and the batch host counts a live run that keeps no journal as a possible change (the Blender save hook still fires). A cancelled call that reached no checkpoint was waited out, then its instance killed with every name the console held; 2 s after the cancel it is now interrupted as Ctrl+C would, the instance and its names kept (never while it saves; a native call is reached when it returns, the grace kill still the last resort). A runner whose application serves its own run folders (Unity's editor, in C#) could not be kept;servenow starts one serving a session folder,instancekeys a session's instances by what a call's scenes name (Unity: the project), the last one named taking a call that names none, andappmay take that key (the editor version a project names).- Test(s):
test_python_console.pytest_code_has_no_dry_run_so_its_operation_applies_only;test_session_operation_host.pynewtest_a_cancel_interrupts_running_code_and_keeps_the_instance_and_its_names,test_code_is_served_where_changes_are_never_as_a_plan,TestAnApplicationThatServesItself(2: one instance per project, the last one staying, each launched with its own executable; a catalog keeps it, one that cannot serve a session is refused) (red first).
- Test(s):
- 2026-11-03 --
ApiSurface.readkeeps each package's read in the per-user cache, and reads of one package at once are one read (core_utils/api_surface/api_surface.py). Every process parsed every package again: an MCP session's firstapi_searchwaited 15.5 s (measured: the server's start-up read and the search parsed the same packages at once). A read is now kept on disk (UserConfig.cache_dir('api_surface'), the sidecar mapping plus each symbol's docstring, file and retirement date), keyed by the package's file stamps and the reader's own source, and a second asker waits for a read in progress. Measured over the seven packages: a new process's first search 0.6 to 0.9 s (16.7 s the first time ever);generate_api_registry.py --check30.1 s, then 9.1 s, its output unchanged. Reads unread for 14 days are swept.- Test(s):
test_api_surface.pynewTestReadCache(3: a new process reads from the cache until the source changes, docstrings and retirement dates included; another reader's cache is not read; reads at once parse each file once; red first).
- Test(s):
- 2026-11-03 -- Fixed (review): a kept instance's lease ran out during a call longer than
keep; newSessionOperationHost.claimtakes a session for one runner alone; the plain Python's server no longer tells a client to passscenes(core_utils/operation_catalog/session_operation_host.py,operation_server.py). The lease was renewed only around a call, so a test chunk longer thankeep(900 s) lost its instance the moment it finished (measured withkeep=3and a 6 s call); the caller now renews it while it waits. Two test runs on one checkout shared one instance, against the rule that a reused instance has no other run using it; a runner that resets what its instance holds now claims it (an OS lock in the session's folder, let go bycloseor the process's end). A server serving onlypython.execnamedpython_among the hosts its scene rules cover.- Test(s):
test_session_operation_host.pynewtest_a_call_longer_than_its_keep_does_not_end_it,test_a_session_claimed_by_one_process_is_refused_to_another(red first),TestPlainPythonHost(instructions).
- Test(s):
- 2026-11-03 -- Fixed: a live run's change was neither saved nor guarded where the host's dirty flag misses edits made from code;
SceneFilesmay now havechanged()(core_utils/operation_catalog/batch_operation_host.py). The save rule and the unsaved-changes refusal both read the host's own flag, and Blender's stays clear after a data-API edit (material.name = ...), so ablender.python.execthat changed an opened .blend saved nothing, and a session opening another file discarded the change unasked. After a live run that changed something, the instance now calls the adapter'schanged()when it has one, before the flag is read.- Test(s):
test_operations.pynewtest_a_host_whose_flag_misses_code_edits_is_told_so_the_change_is_saved(red first).
- Test(s):
- 2026-11-03 -- New: a session instance a runner keeps beyond itself, and folders it reloads in place,
SessionOperationHost(keep=, reload=)(core_utils/operation_catalog/session_operation_host.py). A test runner wants the instance one run starts to serve the next, but a session instance was bound to the process that started it and restarted on any change to code it loaded, the package under test included.keep(seconds) launches it detached (AppLauncher.launch), with no owner watch and a temp dir of its own in the session's folder (a test sandbox's root goes with its run; a crash's dump stays out of the user's temp), and it ends by itselfkeepseconds after the last call (theJobQueuelease, renewed by every call and while it starts);closeleaves it to that lease.reloadnames folders whose modules it imports afresh before each call and never restarts for; a change elsewhere still restarts it. Every safety property stands: launched by the tooling, alive while it holdsinstance.lock, killed by pid only while that lock is held. MCP sessions are unchanged (no keep, no reload).- Test(s):
test_session_operation_host.pynewTestKeptBeyondItsStarter(2: it outlives the process that started it and ends once idle; a folder it reloads is current at each call with no restart; red first).
- Test(s):
- 2026-11-03 -- Library functions are reached by search and code, never wrapped one by one:
python.execruns code for a function no application owns, in a kept plain Python registered as thepythonhost (PureSceneFiles.operation_host,PureSceneFiles.HOST); a search covers every package installed editable (PackageManager.editable_packages); the server reads them while the client starts (OperationServer.warm); a kept instance watches every module it loaded from code it was handed (HandoffBridge.own_python_folders); removed:Operation.fixed/OperationEntry.fixed,OperationProvider.candidates(operations.candidates) andModuleAttributeResolver.side_effect_free, all unreleased (operations.py+operations.json,pyproject.toml,core_utils/operation_catalog/,core_utils/api_surface/api_surface.py,core_utils/package_manager.py,core_utils/handoff/app_handoff.py,core_utils/module_resolver.py). Withapi.searchand a host'spython.execreaching the whole API, a curated operation per library function only covered a handful of thousands, so the hosts declare their console and their view alone, and what only served curation went:fixed(a wrapper's pinned arguments) and the candidate scan that drafted wrappers.api.describesaid a pythontk or uitk function runs "in any Python", but no tool ran plain Python:python.execdoes, in a session instance like Maya's (its tool takes noscenes). A search named no uitk, tentacle, extapps or unitytk symbol (only packages registering operations or a host); it now covers the code installed editable too, measured 12.5 s cold for the seven packages here, so the server reads them in the background at start (0.26 s warm). A kept instance restarted when a module under its own import roots changed, but went stale on one its console imported from elsewhere (a uitk in a pythontk session); it now watches its whole handed path, never its own Python's install.- Test(s):
test_session_operation_host.pynewtest_code_imported_from_outside_its_roots_is_watched_too,TestPlainPythonHost(2);test_api_surface.pynewtest_registered_adds_each_package_installed_editable_after_them,test_editable_packages_reads_each_distribution_installed_editable;test_operations.pynewtest_a_server_that_serves_search_reads_the_packages_while_the_client_starts; thefixedand candidate tests removed (red first for the new ones).
- Test(s):
- 2026-11-03 -- Removed:
SceneExporterBase.confirm_check_override, its deprecation window closed (due in 0.14.0) (core_utils/engines/scene_export/scene_exporter.py). A run asksdecide_check_failurethe moment each check fails; the after-the-run consent outlived its window, so the registry's expiry gate failed.- Test(s):
test_scene_exporter.pytest_the_after_the_run_consent_is_gonereplaces the alias's window test.
- Test(s):
- 2026-11-02 -- New: a host's calls kept in one instance per session over MCP,
SessionOperationHost;PythonConsole;JobQueue.discard; images back over MCP (core_utils/operation_catalog/session_operation_host.py, new;core_utils/python_console.py, new;batch_operation_host.py,_operation_catalog.py,operation_server.py,file_utils/job_queue.py,__init__.py). Every host call launched a fresh mayapy or Blender (measured: 5-7 s and 3.4 s), so an agent paid a launch per read and nothing it built lived past one call.SessionOperationHostwraps a host'sBatchOperationHostand keeps one instance per session name and host: the same run folders (request, entry, cancel, saving, and now a progress file), served in order from aJobQueuein the temp root, started byAppLauncher.spawnbound to the process that started it (and watching it, should the binding not take), shared by every runner given the session in any process. The instance holds an OS lock on a file in the session folder for its whole life: a held lock is what says it is up (a heartbeat lags while a host command holds the interpreter; a pid may name another process by now), and only then is it stopped by the pid it recorded; a dead instance's record is cleared before the next starts. Before each call it compares each module it loaded from its import roots with the file as it was when loaded, and ends when one changed; a fresh instance takes the call. Whenever another instance than a runner's last call's serves a call (a code change, a crash, a stop) its outcome says what was lost (session.restarted). Opening scenes over unsaved changes is refused, as an application asks before it discards them (SceneFiles.unsaved(), optional;PureSceneFileshas it). A cancel withdraws a call still waiting its turn, and cancels one waiting for the instance to start; grace and timeout stop the instance only for a call it runs, and a call queued past the timeout is withdrawn.runnever raises; the instance's output reachesinstance.logas it comes.OperationCatalog(session=)keeps each registered Python-strategy runner (close()ends the instances this process started);ops mcpnames the session fromPYTHONTK_OPS_SESSIONor Claude Code'sCLAUDE_CODE_SESSION_ID(set in each server's own environment), so its two servers share one instance per host, and ends them when the client disconnects. The server's instructions, tool descriptions andscenesschema say kept or fresh; a result naming an image under the temp root comes back as MCP image content (never one elsewhere: a person's texture is no capture).PythonConsole.run(code, ctx=None)runs code in a namespace that lasts: the last expression's value (the start of itsreprwhen huge), what it printed (its end kept when long), a traceback from the snippet's own frames; a dry run lists the code. The hosts'python.execoperations declare it.BatchOperationHostshares its bootstrap script with the session, and the run-folder protocol both serve is one internal base.OperationCatalog.hostbuilds one runner per host however many calls ask at once.JobQueue.discardforgets a finished job and withdraws a queued one, best effort. Measured over MCP, both servers on one session: Maya 4.5 s to start, then 73 ms a call; Blender 3.2 s, then 65 ms; whatmaya_python_execbuilt onops-apply, theopstools read.- Test(s): new
test_session_operation_host.py(20: calls share one instance that keeps what they leave; runners of one session share it and only its starter ends it; scenes open first and a live plan saves them; a code change restarts it, with the issue; progress and cancel; a queued call cancelled is withdrawn and stops nothing; a dead instance is lost with its call and replaced; a failing start refused with why; a command runner cannot be kept; and, each red first against the first build (found by review): a failed start no longer fails the next, a call waiting when its instance dies runs in the next, an instance gone between calls is reported, another runner stops an instance hung in a native call, a module written before its first import is current, scenes are refused over unsaved changes, a cancel during the start ends the call; the catalog keeps registered runners; the server says kept or fresh; the session from the environment; image content only from the temp root), newtest_python_console.py(7),test_job_queue.py(2:discard).test_operations.py's batch, server and run suites pass unchanged.
- Test(s): new
- 2026-11-02 -- One engine reads the public API for every reader:
ApiSurface,PackageSurface,ModuleSurface,ClassSurface(core_utils/api_surface/, new);HelpMixin,python -m pythontk,Operation,OperationProvider.candidatesand theapi.search/api.describeoperations read through it;api.call,OperationProvider.surfaceandcore_utils/operation_catalog/api_surface.pyremoved (never released) (help_mixin.py,symbol_record.py,__main__.py,operation_catalog/operation.py,operation_provider.py,_operation_catalog.py,operations.py+operations.json,__init__.py,test/surface_snapshot.json). One symbol was described five ways: the registry's static walk (in m3trik's generator),HelpMixin's live read, the operations' own AST walk (OperationProvider.surface), the CLI's index rows and four docstring-summary rules, each cutting a summary differently.ApiSurfaceis now the one reader. Its static half,read(package), is the registry's walk moved here verbatim (ast, nothing imported; private bases across files, retirements and theirremove_in/since, ALL_CAPS constants), kept until a source file changes. The registry it writes is identical but for its summaries.retirementsreads the sub-symbolDeprecationcalls. Its live half,members/record/enrich/flags/signature_of/location/source_of/defined_in, isHelpMixin's reading moved out; the mixin now only renders, its public methods unchanged (aboutgainssignature=).exportsis the root namespace resolved live:--indexand the surface snapshot read it.summaryis the one rule: the first sentence of the first paragraph, wrapped lines joined, reST as Markdown, with an optionallimit(the registry's one line).searchanddescriberead the source: every public symbol (classes, instance methods, properties and constants too, since a host's code reaches them all), anddescribegives the whole docstring andfile:line.registeredandhostread the entry points, whose group names (PROVIDER_GROUP,HOST_GROUP) are defined here once.api.callgoes: code run in the host replaces it. Every registry's summaries change once. A live read that cannot place a symbol says so asHelpMixin.wherealways has (ApiSurface.NO_LOCATION), and a search checks each module's package path once, not each symbol's (a warm search of every registered package: 1.4 s, now 0.3 s). - 2026-11-02 -- A package hidden by its repo folder is told to install in compat editable mode (
core_utils/operation_catalog/operation_provider.py). The default editable finder loses to a same-named folder onsys.path, the working directory included, so the package imports as an empty namespace;import_problemnames the fix:pip install --config-settings editable_mode=compat -e <repo>, or the folder onPYTHONPATH. - 2026-11-02 -- An operation declares the arguments it always passes,
Operation(..., fixed={...})/OperationEntry.fixed; a code hook is no argument; only an operation context gives a dry run; a named result reads as its name (core_utils/operation_catalog/operation.py,operation_host.py,operation_provider.py,operation_server.py). Populating mayatk's catalog from real library functions, end to end through the MCP servers in fresh mayapy and Blender instances, found four gaps. A read that also repairs (get_ngons(repair=),get_redundant_flat_keys(remove=)) exposed its change switch to the plan server:fixedholds it, out of the argument schema (a caller passing it is refused) and passed on every call, in this process or a fresh instance. A callable parameter (progress_callback) became an untyped argument no caller could fill: one with a default is left out, a required one refused.Operation.takes_contextwent by the namectxalone, soFbxUtils.publish(ctx: ExportContext)read as plannable and would have been handed anOpContext: actxcounts only unannotated (the convention) or annotated as anOpContext; another kind keeps its default and gives no dry run. A Blender datablock in a result read as itsrepr, and as a key with its memory address: a named object reads as its name. The candidate scan draftedset_tangent_infoandstamp_bake_infoas reads (a read ending now never outweighs a leading change verb), offered Painter and Toolbag plug-in code as Maya operations (a module under a folder that is no package is passed over) and drafts a read's change switch held off. Theopsserver tells a client that a fresh instance has nothing selected, a tool's description names what it always passes, and each tool carries theOutcomeschema without its prose (the instructions describe it once): the workspace's 34 tools list in 70 KB, not 129 KB.- Test(s):
test_operations.pynewtest_only_an_operation_context_gives_a_dry_run,test_a_code_hook_is_left_out_and_a_required_one_refused,test_a_fixed_argument_leaves_the_schema_and_always_reaches_the_call,test_a_fixed_argument_is_a_parameter_given_json,test_a_named_object_reads_as_its_name_as_a_value_and_a_key,test_a_tool_says_what_it_always_passes_and_carries_a_terse_outcome_schema;test_candidates_lists_what_could_be_declaredextended (a change verb's read ending, a held-off switch, a plug-in folder), each red first.
- Test(s):
- 2026-11-02 -- Onboarding and upkeep are part of the tool:
OperationProviderand the operationsoperations.init,operations.candidates,operations.catalog,operations.check;ModuleAttributeResolver.side_effect_free(core_utils/operation_catalog/operation_provider.py, new;_operation_catalog.py,core_utils/module_resolver.py,operations.py+operations.json,__init__.py). Adding a codebase was hand work (a declaration module, an entry point, a catalog) and finding what to declare was a one-off sweep; a workspace script kept the catalogs.OperationProviderowns one package's cycle, as operations so they run from the command line, over MCP or a script alike:initwrites the declaration module, joinspyproject.toml'spythontk.operationstable and writes the first catalog, each only where missing (a dry run lists them);candidatesreads the package's source without importing it, imports only modules whose import runs nothing but definitions (the lazy loader's test, now public asModuleAttributeResolver.side_effect_free), and lists each public function or static/class methodOperationitself accepts as it stands (typed arguments, a docstring), not yet declared, with a suggested id (host=leads it) and a draft; a function that cannot plan is drafted only when its name says it reads (get_,..._status), never as a read the plan server would run unasked;write_catalogrewrites a catalog (moved fromOperationCatalog.write, withdeclarations,document,catalog_path,render; never released);checkholds every registered package: a current catalog, a change that can plan, typed arguments, twins (maya.x,blender.x) that share their effect and their common arguments (a host may add its own), each an error issue, a stale catalog's fix theoperations.catalogcall; a catalog another language writes (a file, or a registered module path holding only its.json) is validated and held to the same rules, never called stale, its no-dry-run message in no one language's words;initedits only the package's ownpyproject.toml(its project folder, or the one above asrc/layout). A package hidden by a same-named folder onsys.path(a repo folder reached through the workspace root) imported as an empty namespace, so its runner failed as a bareAttributeError;check,candidates,initand the host loader now name the folder and the fix. Theops catalogcommand is gone (the operations replace it), and a command whose run reports an error issue exits 1, as a linter does (instructions.checkincluded). Measured on the workspace: mayatk 1,027 candidates (9 that plan, 190 reads by name), pythontk 1,096;checknamed blendertk as hidden by its repo folder in the shared.venv.- Test(s):
test_operations.pynewTestOperationProvider(7: a catalog planned then written once, a change that cannot plan and an untyped argument, twins, issues with their fix, a package hidden by its repo folder, candidates, ...) andTestProviderInit(3: plans then scaffolds once, joins an existing table, needs a package and its pyproject), each red first; the build tests onOperationProvider;test_upkeep_runs_as_operations,test_an_error_issue_fails_the_command.
- Test(s):
- 2026-11-02 --
AppLauncher.launch(hidden=True): a GUI the caller drives through its own channel starts on the private desktop;get_window_titlesfinds a hidden app's windows (core_utils/app_launcher/_app_launcher.py,_hidden.py,_desktop.py). A test pass that drives a GUI Maya over its command port put its window on the user's desktop, where it took their focus.launch(hidden=True)starts the app on the private desktoprun(hidden=True)uses (Windows; a no-op elsewhere, as forrun): it runs as usual -- measured: a GUI Maya 2025 boots, opens its port, renders a Viewport 2.0 playblast on the GPU and gets Qt activation, focus and key events there -- while the user's foreground never changes. It ends with the caller, however that ends, whateverdetachedsays: an app no one can see could never be closed once its launcher is gone.get_window_titles(andwait_for_ready's window check, now the same lookup) also reads the private desktop, sinceEnumWindowssees only the caller's, and counts each window once (a caller on the private desktop sees it twice).- Test(s):
test_app_launcher.pynewTestLaunchHidden(4: its desktop, its end with a caller killed outright though detached, its window found by pid and never on the user's desktop, a no-op where it cannot hide), red first;TestRunHidden's child scripts moved to a shared base, whose user's desktop is the input desktop (a hidden test run's own is not);_INTERACTIVEholds on the private desktop (a fresh one may hold no window to enumerate yet: 5 window tests skipped there);test_wait_for_readyandtest_get_window_titleslaunch the base interpreter, so the PID they watch owns the window (a venvpython.exeis a launcher: both skipped themselves in every venv run). Hidden, throughaffected_tests.py: 85 passed, 10 skipped (POSIX only).
- Test(s):
- 2026-11-02 --
CLI.dispatchlets an option sit among a command's positionals, and takesprog;CLI.get_parsertakesprog(core_utils/cli.py). argparse's subcommand parse gives a*positional nothing once an option interrupts it, then refuses the rest as unrecognized, under the program's usage line:python -m pythontk ops run ID --provider M NAME=VALUEfailed so. The named command's own parser now parses intermixed (--helpand an unknown command still get the program's usage), andprognames the program a usage line shows (python -m pythontk ops run, not__main__.py). EveryCLI.dispatchtable gains it (LookMatch, LookCapture, LookLoop, mayatk'sLightLayerBaker).- Test(s):
test_cli.pynewtest_an_option_may_sit_between_positionals(red first);test_operations.pynewtest_an_option_may_sit_between_the_id_and_its_arguments,test_a_catalog_file_is_a_provider,test_the_instructions_name_the_hosts_it_serves.
- Test(s):
- 2026-11-02 -- New: the operation catalog as an MCP server,
OperationServer; agent instructions as a checked tree,AgentInstructions, with pythontk's own operationsinstructions.check/sync/measure/duplicates(core_utils/operation_catalog/operation_server.py,core_utils/agent_instructions.py,operations.py+operations.json,pyproject.toml).python -m pythontk ops mcpserves one tool per operation (its id with.as_; input schema = the argument schema, plusscenesfor a host operation; result = theOutcomeas structured content): reads and dry runs only, so a client may allow the server whole, whileops mcp --applyserves only the tools that make changes, under the same names, so a permission rule per server asks before each change. Progress reaches the client as MCP progress notifications, a cancelled request cancels the run's scope, in-process operations take turns. It needs the official SDK, the newpythontk[mcp]extra (mcp>=2.3,<3, Python 3.10+); nothing else imports it.AgentInstructionskeeps the layout any agent reads and Claude Code loads as measured:AGENTS.mdrules at every level, aCLAUDE.mdstub beside each, shared packs vendored under a drift check (.agents/packs/, or.claude/rules/for a pack withpaths:frontmatter), a pointer block naming them, and a stdlib SessionStart hook a repo carries for a session rooted in it alone (a workspace root imports its packs instead);checkreports each break as an issue whose fix issync,measuremodels what a session loads,duplicatesfinds rules written twice. Also:Change.KINDSaddscreateanddelete(OpContext.should_create/should_delete); an id is at most 34 characters (Operation.ID_MAX: served by a plugin, a tool is namedmcp__plugin_<plugin>_<server>__<id>, measured, within Claude's 64) and no host-less id may start with a host's name; a committed catalog is found through a repo folder that shadows its package (an empty namespace import) and with no module at all (a catalog another language writes); argument descriptions are spelled as Markdown too.- Test(s):
test_operations.py+14 (73:TestOperationServer(5: one tool-name clash is left out and reported, never fatal; a progress token of 0),TestOperationServerStdio(a JSON-RPC round trip, skipped without the SDK), the new kinds, the id rules, the catalog location, a summary read paste.g., a host refusing the dry run of a change with noctx, a live run killed after the grace ispartial, a cancel never cuts a save short, two live runs on one scene take turns, a scene saved elsewhere mid-run is not saved over); newtest_agent_instructions.py(17: sync's plan and writes, the hook in a standalone repo and under a provider, path-scoped packs, check's codes, measure's launch and touched loads, duplicates; a CLAUDE.md that is not UTF-8 is refused and left as it was, a stub's other imports are kept, a decorator in a code block is no import, the hook skips only the packs a CLAUDE.md above imports, a pack's vendored copies are read once, pythontk's own catalog is current).
- Test(s):
- 2026-11-02 -- New: operations run in a FRESH instance of their application:
BatchOperationHost,SceneFiles,PureSceneFiles,OpRequest(core_utils/operation_catalog/batch_operation_host.py,outcome.py,_operation_catalog.py,operation_host.py,operation.py).OpRequestis the batch protocol as a typed payload: a fresh run folder per call,request.jsonin (the op, its arguments, the dry run, the scenes to open),outcome.jsonout.BatchOperationHostis the runner a host library configures (which executable, how it runs a script, which packages it imports, itsSceneFiles) and registers underpythontk.operation_hosts: it launches a NEW instance per call throughScriptRunner, so an operation never reaches a session a person has open, and inside itserveopens the scenes, runs the call throughPureOperationHost, saves the scenes a finished live run changed (a dry run lists those saves with its plan; a run that does not finish saves nothing and says so) and writes the outcome. How the instance serves the folder is a strategy: a Python script callingserveby default, orcommandfor an application that serves it itself (a C# server in a Unity project), withdiagnosereading why an instance wrote no outcome. A live run holds its scenes, so two calls in one process changing one scene take turns, and it never saves over a scene file changed on disk since it opened it (another process saved it first). Cancelling, through the caller's scope or the ambient one, drops a cancel file the run polls at its checkpoints (the launch runs outside the ambient scope); a run still goinggraceseconds after the cancel is killed, but never while it saves its scenes, and a live run killed (by the grace or the timeout) ispartial, naming any scene it was saving; an outcome written before an instance hung on exit still stands; progress comes back asProgressRelaymarkers; an instance that dies without an outcome is reported (failed, orpartialfor a live change), never raised.OperationCatalog.run(..., scenes=)andops run|apply --scene PATHpass the scenes (refused for an operation with no host, or a file that does not exist);OperationHost.runtakes theOpRequest. An id an application runs starts with its host (maya.material.rename;Operation.id_error), so twin libraries' ids never collide. An operation's summary is its docstring's first sentence, wrapped lines joined and reST code spelled as Markdown.- Test(s):
test_operations.py+15 (59): newTestBatchOperationHost(10, a fresh Python process standing in for the application: a read on an opened scene, a dry run's plan and its listed save, a live save, a failed run that saves nothing, a cancel at a checkpoint, a process that dies, scenes refused where none open, a missing executable, a newer request format, the request's schema), host-prefixed ids, scenes reaching the host, the summary;surface_snapshot.json+4 root names.
- Test(s):
- 2026-11-02 -- New: operations, the named actions every front end calls alike:
Operation,OperationEntry,OperationCatalogDocument,OpContext,Outcome,Issue,Change,OpCall,OperationHost,PureOperationHost,OperationCatalog(core_utils/operation_catalog/;__main__.py).Operationdeclares one: an id (<area>.<action>, led by its host when it names one), the function it calls ("module:qualified.name"), its effect (read;state: what the running application holds, one undo step;files) and its host; the function's typed signature and Google docstring are its argument schema, and its arguments arrive as the types it declares (a path, an enum member, aSchemaSpec), so the library code it calls imports none of this.OperationEntryandOperationCatalogDocumentare the published forms, typedSchemaSpecpayloads every reader and writer shares, another language's included.OpContextis what a run is handed:should_change/should_write(the dry run: the same body lists its changes without making them), issues, progress and cancel (each aCancelScopecheckpoint).OutcomeandIssue(withChangeandOpCall) are what a run hands back (statusdone/partial/cancelled/failed; an issue's fix is an operation call).OperationProviderwrites and checks a provider's committed catalog (<module>.jsonbeside its declaration module),OperationCatalogdiscovers providers through thepythontk.operationsentry points without importing them (or one named by--provider: a declaration module, built live, or a catalog file another language's tool writes outside any package, read as it stands) (a provider that fails to load or ships a malformed document is a reported problem, never a crash), validates a call against its JSON Schema (an unknown argument is refused, not ignored), dry-runs anything that changes unlessapply, and hands it to its host: this process (PureOperationHost, which keeps what an operation prints off stdout and contains aSystemExit) or the runner an application's library registers underpythontk.operation_hosts(OperationHost).python -m pythontk ops list|describe|search|run|apply|catalogis the command line.- Test(s): new
test_operations.py(46), including each finding of the change's independent review (aSchemaSpec-typed argument made the catalog unwritable, red first; a failed live run that keeps no journal readfailed; an annotation that does not resolve dropped every type; a malformed document killeddiscover; NaN defaults;printcorrupting the JSON; the arguments' types);surface_snapshot.json+11 root names.
- Test(s): new
- 2026-11-02 --
SchemaSpec.annotation_schema(tp): the JSON Schema of one plain annotation, by the rulesjson_schemareads a field with; a path type (pathlib.Path,os.PathLike) is a JSON string (core_utils/schema_spec.py). For readers that type values with annotations rather than a spec (an operation's signature). A path-typed field now publishes{"type": "string"}and aTYPEDspec refuses a non-string there; no spec in the ecosystem has one.- Test(s):
test_schema_spec.pynewtest_annotation_schema_reads_a_plain_annotation_by_the_same_rules.
- Test(s):