Live verification on SP21: users.create raised "permission 'Modify' not
granted to user '(nobody)'". The scripts now log in as 'Owner' with empty
password (CODESYS default) when nobody is logged on; new optional
adminUser/adminPassword args override for protected projects.
1. find_object_by_path: final name verification used the original
full_path instead of the dot->slash normalized segments, so
dot-separated paths ('Application.MyPOU') traversed correctly but
failed the final check and returned None.
2. User-arbitrary values (plcPath/plcDirectory, passwords, comments,
project-info fields, device parameter name/value, task event,
device credentials) were interpolated into r"..."/r"""...""" Python
literals unescaped -- a quote or triple-quote in the value broke the
generated script (or injected code). Templates now take pre-escaped
literals via pyStringLiteral().
Reviewed-range: dead49a..e9aa714. Third reviewer finding (task.priority
must be int) was rejected: SP21 ScriptTaskConfigObject.pyi types the
priority/interval/interval_unit setters as str.
* feat(live-values): --live-values-interval <ms> CLI flag
Replaces the hardcoded 500ms poll with a configurable interval.
Clamped to [100, 60000] -- below 100 the read_variable IPC round-
trip-per-var dominates and the pump can't keep up; above 60000 a
session shorter than the interval would never see any update.
ServerConfig.liveValuesIntervalMs?: number plumbs through bin.ts
(parseInt + clamp) into the LiveValuesPump constructor and the
'Live-values pump started (<ms>ms)' log line.
Default unchanged at 500ms, so existing --live-values invocations
behave identically.
* test(e2e): regression coverage for the four 2026-04-29 fixes
Five new template-prep assertions:
1. set_symbol_access mutation lookup goes to get_all_signatures FIRST
(PR #7). The configured-view probe is tracking-only and must come
AFTER both compile=False/True all-signatures lookups -- if the order
ever flips, the mutation hits the read-only view and CODESYS rejects
the assignment with 'can only be changed in the list of all
signatures/data types'.
2. set_symbol_access int->SymbolAccess coerce (PR #9). The fallback
block must use type(maximal_access) and call enum_cls(int_val)
BEFORE 'var.configured_access = requested_access'. Without it,
non-zero ints are rejected with 'must be zero'.
3. set_signature_access_bulk same coerce, lazily on the first variable
in the for-loop (PR #9). Verifies loop -> coerce -> assign order.
4. create_project without deviceName preserves the no-swap path
(backwards compat for PR #8). Empty DEVICE_NAME substitutes; the
if-DEVICE_NAME branch is in the template but the runtime gates it.
5. create_project deviceName swap (PRs #8/#10/#11/#12 combined).
Verifies:
- PromptHandling.NONE setter is rendered (script_prompt_handling
is read-only -- earlier code silently failed).
- device_repository accessed via script_engine, not as a builtin.
- update() is the first-attempt mutation, remove+add only fires
'if not update_ok'.
- project.close() + precompilecache delete render after swap.
Vitest: 27/27 in tests/integration/e2e.test.ts (was 22).
---------
Co-authored-by: Karstein Phobic Nyvold Kvistad <karstein.kvistad@maritimerobotics.com>
Adds remove_library as the symmetric counterpart to add_library.
Script (src/scripts/remove_library.py):
- IronPython 2.7 / ASCII-only; no f-strings, no KeyboardInterrupt in
except-Exception clauses.
- Placeholder substitution: LIBRARY_NAME (bare) and LIBRARY_FQN_OR_NAME
(bare or "Name, Version (Company)").
- Locates the project Library Manager via the same three-step discovery
used in add_library.py: has_library_manager/get_library_manager on the
project, first-level child walk, then find("Library Manager") fallback.
- Pre-check: walks lm.references using _ref_name_matches logic (handles
placeholder "#Name" and managed "Name, Version (Company)" forms).
If the library is not referenced the script exits SCRIPT_SUCCESS with
the "Library Not Present:" marker -- idempotent, same convention as
add_library's dedup no-op branch.
- If found: calls lm.remove_library(existing_name) per the SP22 stub
(ScriptLibManObject.pyi: remove_library(name: str)), confirms removal
from lm.references, then project.save().
- Emits SCRIPT_SUCCESS or SCRIPT_ERROR with traceback on exception.
Server (src/server.ts):
- Tool registered immediately after add_library (~line 2067).
- Reads "Library Not Present:" marker to pick idempotent vs removed
wording -- same marker-driven pattern as add_library's dedup wording.
- Thin surface: projectFilePath + libraryName (required) +
libraryFqnOrName (optional, for multi-version disambiguation).
Tests (tests/integration/e2e.test.ts):
- Template-prep test asserts: no leftover {PLACEHOLDER}s, substituted
values present, remove_library call present, references walk present,
"Library Not Present" marker present, SCRIPT_SUCCESS/SCRIPT_ERROR
markers present.
- All 22 tests pass.
References:
- helpme-codesys.com scripting engine > ScriptLibManObject
- SP22 stub: Stubs/scriptengine/ScriptLibManObject.pyi (remove_library
at line 455, references property at line 464)
Co-authored-by: Karstein Phobic Nyvold Kvistad <karstein.kvistad@maritimerobotics.com>
vitest tests/integration/e2e.test.ts:
- 10 new template-prep assertions, one per Symbol Configuration tool.
Each asserts no leftover {PLACEHOLDER} in the rendered script and
that the relevant CODESYS API method names appear (get_all_signatures,
application.create_symbol_config, configured_access, etc.).
- Drive-by: fix the existing add_library e2e test that was missing the
ALLOW_UNRESOLVED placeholder (added when the resolution gate landed
in d414c77). Test was previously failing on /\{[A-Z_]+\}/ regex.
Suite is now 107/107 green (excluding the orphan .worktrees/phobics-tui
suite that's been failing pre-this-change).
README.md:
- Tool count 31 -> 41.
- New "Symbol Configuration Tools" section under Library Management
with one row per tool.
- Project-structure tool count footnote updated.
docs/function-test-2026-04-28.md:
- Append "Symbol Configuration tools (added 2026-04-28 evening)" block
with a per-tool vitest/live status grid.
- Live SP22 column is deferred: the MCP tool list is negotiated at
Claude Code session start and doesn't refresh mid-session, so the
new tools aren't callable in the session that built them. Document
the round-trip cycle so a fresh session can run it.
- Note the SymbolAccess enum-value probe risk per the plan
(docs/superpowers/plans/2026-04-28-symbol-config-tools.md).
Python 3 ast.parse run against all 10 new + 1 helper scripts -- 11/11
parsed cleanly (catches the obvious IronPython 2.7 syntax issues that
Py3 would also flag).
Empirical failure: rename_object Application/ST_Sample -> ST_SampleRenamed updated the struct's own TYPE header but Application/PLC_PRG kept 's : ST_Sample;' -- the old name -- breaking the project.
Root cause: scriptengine.ScriptObject.rename()/set_name() is a node-local rename only; the IDE's project-wide Rename refactor lives above the scripting layer (no documented find_references() / refactor variant).
Fix: after the local rename succeeds, walk every text-bearing object (textual_declaration / textual_implementation), word-boundary regex-replace \bOldName\b -> NewName via a callback (so backslashes in NewName don't get interpreted as backrefs), set_text the changed nodes, save once. New optional updateReferences param defaults to true; pass false for the legacy minimal-rename behaviour.
Docs: https://content.helpme-codesys.com/en/ScriptingEngine/ScriptObject.html
Risk: false positives in comments / string literals are theoretically
possible but rare for IEC identifiers. Documented in the tool description.
The target node itself is skipped during the references walk (matched by
get_id()) so the rename's already-updated TYPE/FUNCTION_BLOCK/PROGRAM
header isn't double-rewritten.
Two new vitest e2e checks added: assert UPDATE_REFERENCES=1 renders the
re.escape + word-boundary regex, and UPDATE_REFERENCES=0 still produces
a fully-substituted script with no leftover placeholders.
### Manual smoke test
1. Open a project with: a DUT 'ST_Sample', a POU 'PLC_PRG' with
'VAR s : ST_Sample; END_VAR', and a third POU referencing 'ST_Sample.foo'.
2. mcp__codesys__rename_object objectPath=Application/ST_Sample
newName=ST_SampleRenamed.
3. Expect SCRIPT_SUCCESS with 'References Updated In: 2 node(s)'.
4. mcp__codesys__compile_project should succeed (no unresolved-symbol
errors for ST_Sample).
5. With updateReferences=false, the same rename should leave PLC_PRG
stale and compile_project should fail -- validates the opt-out.
6. Word-boundary check: rename 'Foo' -> 'Bar' must NOT touch 'FooBar'
or 'BarFoo' anywhere.
Empirical failure: add_library('Standard') on a project that already had Standard, * (System) silently created a SECOND direct Standard reference, pulling in unresolved transitive deps (e.g. yellow-warning IoStandard 3.1.3.1).
Root cause: script always called add_library() without checking lm.references first; never called add_placeholder() so the result was a direct (non-* (System)) reference.
Fix: (1) walk lm.references for an existing entry by bare name and no-op with a confirmation message unless force=true; (2) default to add_placeholder() so transitive deps resolve at compile (matches the modern '<Name>, * (System)' convention); (3) keep add_library() reachable via direct=true; (4) on miss, dump dir(lm) so unknown SPs surface the actual API.
Docs: https://content.helpme-codesys.com/en/ScriptingEngine/ScriptLibManObject.html
Mildly breaking for callers: previous behaviour was always direct
add_library(); pass direct=true to restore. The dedup default also flips
'add second copy' to 'no-op' -- pass force=true to restore.
Test updated: tests/integration/e2e.test.ts now passes USE_DIRECT='0' and
FORCE_DUP='0' alongside LIBRARY_NAME, asserts add_placeholder + dedup
strings appear in the rendered script.
### Manual smoke test
1. mcp__codesys__add_library libraryName=Standard against a project that
already has Standard listed: expect SCRIPT_SUCCESS with body 'Library
Already Present: Standard' and NO second entry in the Library Manager.
2. mcp__codesys__add_library libraryName=Util on a project without Util:
expect a new entry rendered as 'Util, * (System)' (placeholder, not
direct).
3. mcp__codesys__add_library libraryName=Standard direct=true: expect a
direct (non-* (System)) reference even on dedup hit if also force=true.
4. mcp__codesys__list_project_libraries should reflect each result.
Prior behaviour: lm.add_library(LIBRARY_NAME) was called with a string,
which always hits the placeholder overload of ScriptLibManObject.add_library
(see helpme-codesys.com "ScriptLibManObject" / local SP22 stub
ScriptLib/Stubs/scriptengine/ScriptLibManObject.pyi). If the named
placeholder is not registered in the IDE, the resulting reference fails
to resolve at load time and the next project open throws
Library Manager: Error: Could not open library 'X'.
(Reason: The placeholder library 'X' could not be resolved.)
after which script_engine.projects.primary returns None and the project
is effectively bricked until the binary is reverted. add_library reported
SUCCESS in this scenario.
Fix:
1. Pre-resolve LIBRARY_NAME via the IDE-level
library_manager.find_library(name) global. If found, pass the
resulting ManagedLib to lm.add_library(...) -- the V3.5.5.0 ManagedLib
overload which produces a managed reference instead of a placeholder.
2. After the add, walk lm.references to locate the new entry and verify
it resolved (managed -> always; placeholder -> non-empty
effective_resolution per ScriptPlaceholderReference in the stub).
3. If the reference did not resolve, call lm.remove_library(name) to back
out the bad reference and refuse to save the project, returning an
actionable error instead.
Also tightened the libman lookup to use the documented
has_library_manager / get_library_manager() container API
(ScriptLibManObjectContainer in the stub) instead of name-searching for
"Library Manager" as a tree node, matching what list_project_libraries.py
already does. The legacy name-search fallback is preserved for older SPs.
Regression test added in tests/integration/e2e.test.ts asserting the
rendered script template carries the resolution gate, the managed-overload
preference, the back-out call, and that primary_project.save() in the
add_library body lives downstream of the _is_resolved gate.
Followup: lm.remove_library(name) is documented for SP22 and is the
clean back-out path. On SPs that lack remove_library (none observed in
the 3.5.21+ docs but possible on truly old branches) the script reports
the constraint and exits non-zero rather than silently saving a bad
reference.
Bug: calling set_pou_code with implementationCode only (declarationCode
omitted) would wipe the POU's PROGRAM/VAR...END_VAR block in the binary.
After such a call mirror_export classified the POU as 'UNKNOWN' (no
PROGRAM/FUNCTION_BLOCK keyword in the empty declaration), and the var
block disappeared from the .st mirror file.
Root cause: the TS wrapper substituted '' (empty string) into the Python
template when declarationCode was undefined, giving DECLARATION_CONTENT
= "". The Python script then took the truthy-ish branch (empty string
is not None) and called decl_obj.replace('') -- wiping textual_decl.
Fix: pass explicit SET_DECLARATION / SET_IMPLEMENTATION boolean flags
from the TS wrapper, gate the replace() calls on those flags. Empty
string remains a valid intentional value (caller wants to wipe).
- Reproduced on MCPTest2: PLC_PRG declaration block was wiped between
v1.3.0.0 and v1.3.1.0 by exactly this code path.
- Regression test added in tests/integration/e2e.test.ts covering the
omitted-declarationCode path.
- Existing set_pou_code test extended to assert SET_DECLARATION /
SET_IMPLEMENTATION are emitted in the rendered script.
v0.3.0 returning-watcher architecture — background thread polls for commands
and marshals execution onto the CODESYS UI thread, keeping the IDE fully
responsive between operations. File-based IPC with atomic writes, async mutex
command serialization, headless fallback, and 35 passing tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>