Jérôme Benoit [Tue, 2 Jun 2026 19:48:39 +0000 (21:48 +0200)]
chore(release-please): override next release to 4.8.0
The BREAKING CHANGE footer in 8bb806d describes a log message wording
change consumed by external monitors, not a SemVer-breaking API change.
This override forces release-please to ship 4.8.0 (minor) instead of 5.0.0.
Cleanup: this 'release-as' field MUST be removed in a follow-up commit
once the 4.8.0 release PR merges, otherwise subsequent runs will
re-propose 4.8.0.
Jérôme Benoit [Wed, 27 May 2026 22:33:50 +0000 (00:33 +0200)]
refactor: relocate simulator config to src/utils and enforce barrel discipline
- Move ConfigurationSchema/Migrations/Validation from charging-station to utils
- Rename ConfigurationUtils.logPrefix to configurationLogPrefix (barrel anti-collision)
- Expose public APIs through component barrels (charging-station, ocpp, utils, worker)
- Migrate 38 test imports from deep paths to barrels
- Document TDZ-cycle exceptions in OCPPError and ConfigurationMigrations
* feat(config): scaffold simulator configuration schema and validator skeleton
- Add ConfigurationMigrations.ts: CURRENT_CONFIGURATION_SCHEMA_VERSION=1,
coerceConfigurationVersion, applyConfigurationMigration, migrateV0ToV1
with DEPRECATED_KEY_REMAPPINGS (~25 legacy keys)
- Add ConfigurationSchema.ts: strict Zod v4 schema for all config sections
(log, worker, performanceStorage, uiServer, stationTemplateUrls) with
deprecated keys as .optional()
- Add ConfigurationValidation.ts: validateConfiguration pipeline
(guard→clone→coerce→migrate→safeParse→transform) + ConfigurationValidationError
extends BaseError with structured fieldErrors
- Add $schemaVersion: 1 to config-template.json
- Add ConfigurationFixtures.ts test helpers
- Add ConfigurationSchema.test.ts (63 tests), ConfigurationMigrations.test.ts (44 tests)
- Wire validateConfiguration into Configuration.getConfigurationData() with
hard-throw at boot (console.error+chalk+process.exit(1))
- Hot-reload snapshot rollback: pre-clear snapshot, restore on failure
- Replace ConfigurationData interface with z.infer<typeof ConfigurationSchema>
- Migrate all consumers atomically (SimulatorState, Configuration, types barrel)
- Delete checkWorkerProcessType and checkWorkerElementsPerWorker (subsumed by schema)
- Delete ConfigurationMigration.ts (logic moved to ConfigurationMigrations.ts)
- Add ConfigurationValidation.test.ts (39 tests), Configuration-hot-reload.test.ts
- Add ConfigurationValidation-perf.test.ts (p99 < 50ms budget)
- Add error message snapshot test
- Update README.md with $schemaVersion documentation
* feat(config): remove legacy ConfigurationMigration.ts and stale call sites
- Delete src/utils/ConfigurationMigration.ts (deprecated-key logic now
owned by ConfigurationMigrations.ts v0→v1 migration step)
- Remove checkDeprecatedConfigurationKeys import and call site from
Configuration.getStationTemplateUrls() — validation pipeline in
getConfigurationData() already handles deprecated key remapping
* chore(config): silence lint warnings on configuration migrations
- Add JSDoc descriptions for setAtPath() params
- Add JSDoc descriptions and @returns for migrateV0ToV1
- Whitelist 'emerg' (syslog level) and 'REMAPPINGS' in cspell dictionary
- Extract deprecated-key sweep into pure remapDeprecatedKeys() that
reports warnings and field errors via return value instead of side
effects. Runs unconditionally regardless of $schemaVersion so v1
configs still containing deprecated keys never silently drop user
values (B3).
- Replace silent-drop setAtPath with collision- and intermediate-aware
variant: equal-value writes are idempotent no-ops, unequal values
produce a typed field error (B4), non-object intermediates are
reported and stop traversal instead of overwriting user data (N7).
- Type DEPRECATED_KEY_REMAPPINGS as Record<string, string | null>;
null marks deprecated keys with no canonical destination, replacing
the autoReconnectMaxRetries self-mapping that silently dropped the
user value (B2). Add 'worker.elementStartDelay' as a dotted source
key so nested deprecations live in the same single source of truth.
- Refactor ConfigurationValidationError: primary constructor takes
FieldError[] + a context with explicit phase ('migration' | 'schema');
static fromZodError() factory wraps Zod failures. Error messages now
carry the phase in their tag for clearer diagnostics.
- Switch deprecation-warning channel from logger.warn to console.warn
to break a re-entrant boot path where the Logger proxy lazily
resolved Configuration.getConfigurationSection('log'), recursing into
the validation pipeline (B1). transformConfiguration is now a pure
deep clone — its previous warning loop moved upstream to the sweep.
- Delete dead post-migration remapping in Configuration.ts:
deprecatedLogKeyMap, deprecatedWorkerKeyMap, the
delete configurationData.workerPoolStrategy mutation, the cast hiding
the legacy 'supervisionURLs' read, and the worker.elementStartDelay
fallback (B6). All deprecation handling now flows through the
migration's single source of truth.
- Make hot-reload reload loop async end-to-end. Awaits the change
callback inside the same lock so subsequent reloads cannot interleave
with an in-flight callback. Adds configurationFileReloadPending so
events arriving during a reload coalesce into exactly one drain
reload after the current one completes (N8).
- Polish schema: use z.enum(NativeEnum) directly for ApplicationProtocol
and ApplicationProtocolVersion to preserve literal-type narrowing on
UIServerConfiguration (N4); drop the BaseConfigurationSchema alias
(N5).
* test(config): cover migration edge cases and hot-reload rollback
Tests aligned with the rebuilt validation pipeline:
- Split migration tests into a remapDeprecatedKeys block (per-key sweep,
null-destination removal for autoReconnectMaxRetries, equal-value
collision idempotency, unequal-value collision field error, non-object
intermediate field error, nested worker.elementStartDelay) and a lean
applyConfigurationMigration block (version-bump only, immutability).
- Replace the empty self-mapping branch with explicit hasOwnProperty
removal assertions to defeat the previously vacuous test.
- ConfigurationValidation tests: switch deprecation-warning channel
spies from logger.warn to console.warn (B1 regression also asserts
logger.warn is never called from the pipeline), tolerate
schema-incompatible remap targets so the warning fires before the
downstream throw, add B3 sweep / B6 SSOT / future-version pipeline /
ConfigurationValidationError shape (FieldError[] + phase, fromZodError
factory, schema-phase aggregation) tests, and update the error-message
snapshot to include the new [phase] tag.
- transformConfiguration immutability test verifies a fresh validation
is unaffected by mutating a prior return value.
- ConfigurationSchema tests: strict parity for performanceStorage and
uiServer.authentication; StationTemplateUrl entry constraints
(empty file, negative numberOfStations, deprecated numberOfStation,
unknown key); worker.elementsPerWorker and poolMaxSize/MinSize
positive-integer constraints; log.statisticsInterval non-negative
integer constraints; bidirectional schema/DEPRECATED_KEY_REMAPPINGS
sync meta-test that walks both top-level and nested @deprecated
describe markers.
- Hot-reload tests rebuilt around the async runReloadLoop: validation
failure asserts the logged error is a ConfigurationValidationError;
JSON parse error path asserts configurationData and section cache are
fully restored (Gap 7); a sentinel watcher survives a failed reload;
N8 rapid double-save coalesces into exactly one drain reload reflecting
the latest content; flag reset is exercised on both paths.
- Configuration validation perf test: relative p99 budget (20× median
with a 1ms floor) plus an absolute 500ms catastrophic ceiling,
replacing the flaky absolute 50ms threshold (N1).
- Fixtures: new buildInvalidJsonString, buildV1WithDeprecatedKey
(handles top-level and dotted source keys), and
buildV0WithDeprecatedKeyCollision builders for the new test cases.
* docs(config): trim verbose JSDocs and harmonize with Template* conventions
Polish pass after PR audit: bring comments and docstrings in line with
the existing TemplateMigrations / TemplateValidation / TemplateSchema
patterns, drop ceremonial AI-generated narration, and fix two precision
defects.
- ConfigurationMigrations.ts: trim getAtPath / setAtPath / remapDeprecatedKeys /
migrateV0ToV1 / applyConfigurationMigration JSDocs to match Template*
density; drop the historical "now handled unconditionally upstream"
paragraph (AGENTS.md "exclude historical evolution").
- ConfigurationValidation.ts: collapse the duplicated pipeline narration
(the function-level JSDoc already enumerates stages, the inline
// Stage N — markers were removing them); shorten the
ConfigurationValidationError class docstring; tighten transformConfiguration
JSDoc and drop the aspirational "future cross-field invariants" sentence;
collapse the 5-line re-entrancy comment to a 2-line rationale.
- ConfigurationSchema.ts: fix two precision defects in JSDoc — stale
module name (ConfigurationMigration → ConfigurationMigrations) and wrong
subject (numberOfStation is not in the remap table; nothing auto-migrates
it). Disambiguate WorkerConfigurationSchema description (resourceLimits
is bridged, not deprecated; elementStartDelay is the deprecated alias).
- Configuration.ts: shrink runReloadLoop and performReload JSDocs to two-
line summaries matching the surrounding private-method conventions in
the same file; collapse the 4-line ESLint-disable rationale to a single
-- suffix on the disable directive.
- Test fixtures and headers: drop internal review codes (B1/B3/B4/B6/N7/N8/
Gap-7/RG-4) from JSDocs and test names — they had no lookup table and
would be opaque to future maintainers; trim test-file headers to the
one-line Template* shape.
* refactor(config): address post-audit findings on validation pipeline
* fix(config): drop incompatible v0 remaps and tighten validation invariants
Addresses 4 PR review findings cross-validated by 2 oracles.
- ConfigurationMigrations: drop `useWorkerPool` (boolean→enum),
`distributeStationToTenantEqually` and `distributeStationsToTenantsEqually`
(boolean→enum), and `uiWebSocketServer` (legacy shape→strict object) to
`null` (warn-and-delete) — same pattern as `workerPoolStrategy` and
`autoReconnectMaxRetries`. v0 configurations carrying any of these
legacy keys would otherwise auto-write a value the strict schema
rejects, killing the simulator at boot. Migration now warns and asks
the user to set the canonical key explicitly. Fixes the BLOCKING
finding from the PR review (regression on 'v0 configs remain valid').
- ConfigurationMigrations: remove redundant `out.$schemaVersion = 1`
from `migrateV0ToV1`. The `applyConfigurationMigration` loop is the
single owner of version stamping; per-step writes are silently
overwritten and become misleading once a second migration is added.
- ConfigurationSchema: tighten `StationTemplateUrlSchema.numberOfStations`
from `.nonnegative()` to `.positive()` — `0` was schema-valid but
semantically meaningless (a template entry that spawns no station)
and conflicts with `isValidNumberOfStations()` which rejects `<= 0`.
Deprecated `numberOfStation` and `provisionedNumberOfStations` keep
`.nonnegative()` (back-compat / 'none provisioned' is meaningful).
- ConfigurationValidation: import `isEmpty` directly from
`utils/Utils.js` instead of the `utils/index.js` barrel, which
re-exports `Configuration` and creates the cycle
`ConfigurationValidation → utils/index → Configuration → ConfigurationValidation`.
Mirrors the existing direct-path `BaseError` import in
`ConfigurationMigrations.ts` for the same TDZ-cycle-avoidance reason.
* docs(config): fix precision defects in schema JSDocs and README
- ConfigurationSchema.ts: correct StorageConfigurationSchema JSDoc —
'URI' is accepted but not auto-migrated (not in DEPRECATED_KEY_REMAPPINGS);
narrow ConfigurationSchema JSDoc meta-test scope claim to 'top-level
and worker.* keys' to match actual test coverage.
- README.md: clarify that deprecated-key remapping is unconditional
(runs on every load, not only on v0 migration).
* fix(config): keep numberOfStations as .nonnegative() to preserve disabled-entry convention
Revert the .positive() tightening from 95cd26dd: docker/config.json ships
5 stationTemplateUrls entries with `numberOfStations: 0` (used as a
'keep on disk but disabled' convention), and this file is copied to
src/assets/config.json by docker/Dockerfile at image build time. With
.positive(), Docker images built from this branch would fail to start
on validateConfiguration's strict parse → process.exit(1).
Bootstrap loops already tolerate 0 (no-op, no crash). The original
.nonnegative() correctly models the on-disk convention; isValidNumberOfStations()
in UIServerSecurity.ts is for runtime UI add-station requests, a
separate concern from config-file parsing.
* docs(config): trim re-entrancy and clone rationales to one-liners
Jérôme Benoit [Tue, 26 May 2026 17:40:45 +0000 (19:40 +0200)]
ci(renovate): enforce 3-day minimum release age for npm packages
Extend the Renovate config with the official 'security:minimumReleaseAgeNpm'
preset so that Renovate waits 3 days after publication before creating PRs
for any npm/pnpm dependency. This adds a buffer against unpublished or
freshly-broken releases (e.g. malicious packages, npm unpublish window,
transient registry/lockfile resolution issues).
Add `atomicWriteFile` and `atomicWriteFileSync` to `src/utils/FileUtils.ts`,
implementing the canonical write-then-rename pattern with optional fsync
durability and best-effort temp file cleanup on failure. Errors are funnelled
through the existing `handleFileException` helper so callers stay aligned
with the project-wide file error reporting contract.
Migrate the five non-atomic disk writes uncovered by the audit:
- `BootstrapStateUtils.writeStateFile` replaces its inline tmp+rename with
the new primitive (single source of truth, gains fsync durability).
- `ChargingStation.saveConfiguration` replaces `writeFileSync` so charging
station OCPP configuration JSON cannot be torn by a crash mid-write.
- `JsonFileStorage.storePerformanceStatistics` drops the persistent
`openSync('w')` file descriptor design (which truncated the file at byte
zero on every sample) and uses the atomic primitive instead. Also fixes
the previous fire-and-forget `runExclusive(...).catch()` pattern by
awaiting the lock.
- `OCPP20CertificateManager` writes installed PEM certificates atomically.
Add `FileType.Certificate` so PEM writes can flow through
`handleFileException` with an accurate file type label.
Concurrent writers to the same path must still be serialized externally
(typically via `AsyncLock`); the primitive does not implement an internal
queue, matching how every existing call site already locks. The
`createWriteStream` diagnostics archive in OCPP 1.6 `GetDiagnostics` is
intentionally left as-is since the file is ephemeral (FTP-uploaded then
discarded).
Targets Node >= 22 so `writeFile`/`writeFileSync` natively expose the
`flush: true` option used for the fsync step.
* fix(utils): align atomic write call sites on a single error path
Address review feedback on PR #1871. The atomic write primitive logs and
re-throws by default via `handleFileException`. Three call sites kept
their pre-migration outer error wrappers, producing double handling:
- `ChargingStation.saveConfiguration`: the `.catch` handler attached to
the fire-and-forget `AsyncLock.runExclusive(...).catch(...)` chain
re-threw inside the catch callback, leaking an unhandled promise
rejection on every config write failure (disk full, EACCES, EROFS,
...). Pass `{ throwError: false }` to that `handleFileException`
call so the rejection is fully absorbed. The retry semantics are
preserved: when `atomicWriteFileSync` throws, the `endMeasure` and
`configurationFileHash` updates inside the lock callback are skipped,
so the next `saveConfiguration` invocation will retry the write.
- `JsonFileStorage.storePerformanceStatistics`: drop the redundant
outer `try/catch` and pass `{ throwError: false }` to the primitive,
matching the `BootstrapStateUtils.writeStateFile` template. Failures
now produce a single error log instead of one error log followed by
a second warn-level log.
- `OCPP20CertificateManager.storeCertificate`: replace the empty
`logPrefix` with a string carrying `stationHashId` and the module
origin so the new error log carries actionable context. The outer
`try/catch` in `storeCertificate` only stringifies the error into
the structured `{ success: false, error }` result and does not call
`handleFileException`, so there is no double handling here — only
the missing context to fix.
* refactor(utils): consolidate atomic write options and address audit findings
- FileUtils: include threadId in temp filename for cross-worker uniqueness;
fold errorParams into AtomicWriteOptions (single trailing options bag);
document SIGKILL leak, durability scope, and per-field defaults; drop
redundant 'utf8' as BufferEncoding cast.
- BootstrapStateUtils.writeStateFile: drop the undefined placeholder.
- ChargingStation.saveConfiguration: route fs failures (already logged at
error by handleFileException) to debug, and surface non-fs failures from
the lock body at error level via a typeof-guarded ErrnoException check.
- OCPP20CertificateManager.storeCertificate: take a required logPrefix from
the caller (replaces the synthetic prefix); drop the redundant manual
mkdir+pathExists block (atomicWriteFile's default ensureDir handles it);
write certificates with mode 0o600; document the per-path serialization
rationale (paths keyed by certificate serial number). Both
OCPP20IncomingRequestService callers updated to pass
chargingStation.logPrefix(); the manager unit tests are migrated.
- JsonFileStorage.storePerformanceStatistics: migrate to AtomicWriteOptions
with errorParams; set flush: false on this hot telemetry path (durability
across crashes is not required for performance records); add 5 focused
unit tests covering happy path, overwrite, Map serialization, runtime
storage directory removal, and parallel writes.
- FileUtils tests: tighten assert.throws/rejects with { code: 'ENOENT' } and
add a deterministic test that asserts the destination remains intact and
the temp file is cleaned up when the rename step fails.
* fix(performance-storage): decode file URIs into native filesystem paths
JsonFileStorage parsed the storage URI via new URL(uri) and used
.pathname directly as a filesystem path. On Windows, file: URLs yield
WHATWG-formatted pathnames such as '/C:/Users/...' which mkdirSync
interprets relative to the current drive, producing corrupt paths like
'D:\\C:\\Users\\...' and breaking JsonFileStorage on windows-latest CI.
Use fileURLToPath() (the standard Node.js inverse of pathToFileURL) to
decode file: URIs into the native path on every platform. Non-file
schemes (typically jsonfile:./relative-path) keep .pathname semantics
for backward compatibility with existing user configurations.
The new tests in tests/performance/storage/JsonFileStorage.test.ts
exercise this code path with pathToFileURL(absolutePath), which now
round-trips correctly across POSIX and Windows.
Jérôme Benoit [Fri, 22 May 2026 01:07:50 +0000 (03:07 +0200)]
chore(deps): update @ai-hero/sandcastle to 0.5.11
Refresh the patch via 'pnpm patch'/'pnpm patch-commit' and pin the
patch key to the exact version so context drift on future bumps
fails loudly instead of applying silently.
Patch semantics are unchanged: it still adds the 'thinking' option
to PiOptions and threads '--thinking <level>' into the pi agent
provider's print command. Only context lines and blob hashes are
refreshed for 0.5.11 (DEFAULT_MODEL bumped to claude-opus-4-7
upstream).
Jérôme Benoit [Mon, 18 May 2026 21:49:47 +0000 (23:49 +0200)]
refactor(bootstrap): factorize UI server start/stop logic
Extract idempotent helpers and remove duplication across the four
UI server lifecycle call sites:
- public startUIServer() now contains the actual logic instead of
delegating to a private wrapper; start() calls it directly.
- new private stopUIServer() helper replaces the two inline stop
blocks in gracefulShutdown() and restart().
- restart() guards syncUIServerTemplates() on uiServerStarted to
avoid redundant template sync (twice when re-enabling, wasted
call when the UI server is disabled or stopped). startUIServer()
performs the sync itself when it actually starts the server.
Jérôme Benoit [Mon, 18 May 2026 00:54:16 +0000 (02:54 +0200)]
fix(sandcastle): resolve TDZ in strategies/index.ts module init
Move the file-private validation regexes `STRATEGY_KEY_PATTERN` and
`CONTROL_TAG_PATTERN` (with their JSDoc verbatim) above the eager call
`STRATEGY_BY_KEY = indexByKey(STRATEGY_REGISTRY)`, so they are
initialized before `indexByKey` reads them at module evaluation.
Jérôme Benoit [Sun, 17 May 2026 22:45:10 +0000 (00:45 +0200)]
feat(sandcastle): make strategy dispatch registry-driven (#1862)
* feat(sandcastle): make strategy dispatch registry-driven
Replace the hardcoded `sandcastle` label / single-strategy implementation
with a registry mapping each strategy key to its actor/critic loop and
finalization config. Labels (`sandcastle-<key>`) and branch prefixes
(`agent/<key>`) are derived from the key, so adding a strategy is one
folder + one registry line.
- New `.sandcastle/strategies/index.ts` (registry, indexed view with
load-time uniqueness assertion, `labelOf`/`branchPrefixOf` helpers).
- `TaskSpec.strategyKey` propagates the chosen strategy through the pipeline.
- `GithubIssueSource` fetches issues per registered label, deduplicates
with a multi-label warning, validates planner output (strategy echo and
branch number bound to issue id), and builds its prompt sanitizer once
from the registry (universal core + per-strategy `controlTags`).
- `GITHUB_ISSUE_LABEL` and `GIT_BRANCH_PREFIX` constants removed; no
back-compat shim.
Cutover (one-time, also noted in `.github/workflows/sandcastle.yml`):
1. `gh label edit sandcastle --name sandcastle-implement`
2. Close or merge open PRs whose head branches start with `agent/issue-`.
* fix(sandcastle): validate strategy key and controlTags at registry load
Extend `indexByKey` to enforce the kebab-case contract on `StrategyEntry.key`
(documented but unchecked) and an XML-name-safe contract on `controlTags`.
Failures throw at module load with a precise message, matching the existing
duplicate-key behaviour, so registry mistakes (typos, wrong casing, empty
tag) cannot silently produce undiscoverable GitHub labels, invalid git
branches or empty regex alternatives in the prompt sanitizer.
* fix(sandcastle): tighten control-tag sanitizer against prefix matches
The deny-list regex `</?(?:tag1|tag2)[^>]*>` matched tag-name prefixes
because `[^>]*` swallowed the trailing characters: `<plant>` matched
alternative `plan`, `<reviewer>` matched `review`, and so on. Insert a
zero-width lookahead `(?=[\\s/>])` after the alternation so only XML
tag-name terminators (whitespace, `/`, or `>`) are accepted. The
deny-list remains entirely registry-driven.
* refactor(sandcastle): parallelise issue fetch and clarify multi-label warning
- Fan out per-strategy `gh issue list` calls with `Promise.all`. The
dedup pass still iterates the resolved array in registry order, so the
"first registered wins" semantics is preserved. Failure semantics are
unchanged: any rejected fetch propagates as before.
- Render labels via `labelOf` (no more hard-coded `sandcastle-*` glob),
name both winner and dropped GitHub labels, and append a ready-to-paste
`gh issue edit --remove-label` remediation hint.
* fix(sandcastle): reject prefix-overlapping strategy keys at registry load
Two registered keys related by a kebab-prefix (e.g. 'foo' and 'foo-1') yield
overlapping branch-detection regexes: a branch 'agent/foo-1-42-slug' is matched
by both '^agent/foo-(\d+)-' (capturing '1' as the issue id) and
'^agent/foo-1-(\d+)-' (capturing '42'), and the registry-order tie-break in
`fetchIssuesWithOpenPRs` then attributes the open PR to the wrong issue.
Extend `indexByKey` with a pairwise prefix-incomparable check that throws at
module load with a precise message, in line with the existing
`STRATEGY_KEY_PATTERN`, `CONTROL_TAG_PATTERN`, and duplicate-key checks
introduced in 89be4bd3. Today's single-entry registry passes unchanged.
* refactor(sandcastle): unify prompt taxonomy and decouple planner from orchestrator
Adopt the Inputs/Task/Output/Rules/Done section structure across the
plan, actor and critic prompts; drop `Agent` from the role headers;
deduplicate intra-prompt boilerplate (verbatim instructions, repeated
quality-gate blocks, multiply-explained confidence levels). Total prompt
size: 217 \u2192 142 lines.
Decouple the planner output from registry-known data: the planner now
emits a kebab-case `slug` only; the orchestrator builds
`branch = \`\${source.branchPrefix}-\${id}-\${slug}\`` and copies
`strategyKey` from the registry-resolved source. The validator drops the
`strategyKey` echo check and the full-branch regex, validates the slug
shape via `SLUG_PATTERN` and a `MAX_SLUG_CHARS` cap. The planner JSON
view is also projected to `{ body, labels, number, title }` so
`branchPrefix`/`strategyKey` cannot leak back into the prompt.
Rename actor variable `TASK_ID` \u2192 `ISSUE_NUMBER` for precision; drop the
`## Planner Analysis` section header from `buildPlanContext` so
`{{PLAN_CONTEXT}}` interpolates cleanly inside the actor's Inputs
section.
The sandcastle library's PromptArgumentSubstitution
(`@ai-hero/sandcastle/dist/PromptArgumentSubstitution.js`) applies the
regex \`/\\{\\{\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\}\\}/g\` globally with no
Markdown awareness, so a placeholder appearing both inside an
`## Inputs` bullet (`\`{{KEY}}\` \u2014 description.`) and on a bare line
below was substituted twice. For multi-line values (`ISSUE_BODY`,
`PLAN_CONTEXT`, `FINDINGS`, `ISSUES_JSON`, `ACCEPTANCE_CRITERIA`)
this corrupted the inline-code span and doubled the prompt token cost
on every actor and critic round.
Reference variables by bare name (`\`KEY\``) in the `## Inputs` bullets
and in narratives that mention multi-line values; keep the bare-line
`{{KEY}}` injections downstream as the single substitution sites.
Scalar variables (`ISSUE_NUMBER`, `ISSUE_TITLE`, `BRANCH`,
`BASE_BRANCH`, `NONCE`) remain interpolated where their single-line
value belongs in the rendered prose or commands.
* refactor(sandcastle): centralize MAX_SLUG_CHARS in constants module
`MAX_SLUG_CHARS` is a tunable peer of `MAX_TITLE_CHARS`,
`CONTEXT_HASH_RADIUS`, and `HASH_PREFIX_LENGTH`, all already in
`constants.ts`. Move it there to honour the AGENTS.md "single source of
truth: canonical defaults" rule. `SLUG_PATTERN` stays local in
`task-source.ts`: it is a structural validator regex, not a tunable.
* fix(sandcastle): align PR-coverage branch regex with slug grammar
The PR-coverage regex used to dedup open PRs in `fetchIssuesWithOpenPRs`
omitted the trailing slug, while `validatePlanEntry` enforces a strict
kebab-case slug shape via `SLUG_PATTERN`. A stale or malformed branch
like `agent/implement-42-` would match the dedup regex and suppress
retries forever.
Lift the slug body into a shared `SLUG_PATTERN_BODY` constant reused by
both `SLUG_PATTERN` (the plan-entry validator) and the per-strategy
`branchPatterns` so the two definitions stay in lock-step.
* fix(sandcastle): sanitize planner-supplied issue title at trust boundary
`validatePlanEntry` validated typeof, length and control characters on
`item.title` but stored the raw value verbatim in `TaskSpec`, exposing
both the actor prompt (`{{ISSUE_TITLE}}`) and the GitHub PR title (via
`finalizer.ts`) to control tags such as `<system>` or `<plan>` that a
prompt-injected planner could echo from an issue body.
Route `item.title` through the same registry-derived `sanitizeForPrompt`
already used for `body`, `rootCauseHypothesis` and `acceptanceCriteria`,
trim whitespace, and reject the entry if the sanitised title is empty.
Length and control-character checks remain on the raw input as a fast
fail-fast path before sanitisation.
* fix(sandcastle): retry on all-invalid plan and dedupe planner ids
`validatePlan` returned `[]` both for legitimate empty plans and for
plans where every entry failed validation. The caller treats `[]`
identically ("No actionable issues. Exiting."), so a systemic planner
mistake silently terminated the nightly run as if there were genuinely
no work, wasting the entire retry budget without diagnostic.
It also accepted duplicate ids: a planner emitting two entries for the
same issue number produced two `TaskSpec`s sharing one branch, so
`main.ts` would spawn two concurrent sandboxes and create duplicate PRs.
Restructure `validatePlan` to:
- Track `seenIds: Set<string>`; on duplicate, warn and drop the second
occurrence (first registered wins, mirrors the multi-label dedup
pattern in `fetchAndSanitizeIssues`).
- Detect `parsed.issues.length > 0 && validated.length === 0`: log an
error and return `null` so the existing retry loop at lines 119-177
re-invokes the planner instead of exiting silently.
feat: resolve #314 — Add charging station template Zod validation with schema versioning (#1860)
* feat: add charging station template Zod validation with schema versioning
Add Zod v4 runtime validation for charging station templates with
schema versioning support. This replaces scattered imperative checks
with a declarative schema pipeline that validates, migrates, and
transforms templates at parse time.
New files:
- TemplateMigrations.ts: CURRENT_SCHEMA_VERSION, coerceVersion(),
migration registry with migrateV0ToV1()
- TemplateSchema.ts: Zod schemas with topology union, loose + strict
variants, MeterValues value coercion (number -> string)
- TemplateValidation.ts: validateTemplate() pipeline,
transformTemplate(), TemplateValidationError
Integration:
- getTemplateFromFile() now calls validateTemplate() instead of bare
JSON.parse(...) as ChargingStationTemplate
- Cache key updated to hash:vN format incorporating schema version
- Removed checkTemplate(), checkConnectorsConfiguration(),
checkEvsesConfiguration(), warnTemplateKeysDeprecation() from
Helpers.ts (logic absorbed into schema/pipeline)
- Exported getConfiguredMaxNumberOfConnectors and
getMaxNumberOfConnectors from Helpers.ts for connector setup
Template changes:
- Added "$schemaVersion": 1 to all 15 template JSON files
- coerceVersion(null|undefined) now returns 0 so legacy templates
without $schemaVersion go through v0->v1 migration; deprecated keys
(supervisionUrl, authorizationFile, payloadSchemaValidation,
mustAuthorizeAtRemoteStart) are renamed instead of being silently
swallowed by looseObject [HIGH]
- transformTemplate uses Math.max(numberOfConnectors[]) as the
worst-case bound for the randomConnectors auto-trigger; new
Helpers.ts pair getMaxConfiguredNumberOfConnectors (deterministic
upper bound) and pickConfiguredNumberOfConnectors (random pick)
centralize array semantics; getConfiguredMaxNumberOfConnectors now
delegates to pickConfiguredNumberOfConnectors [HIGH]
- BaseTemplateSchema.$schemaVersion is now z.literal(CURRENT_SCHEMA_VERSION)
(post-migration assertion); deprecated keys removed from the v1
schema and explicitly rejected by a superRefine block with a clear
diagnostic [MEDIUM]
- transformTemplate drops the unreachable templateMaxConnectors < 0
branch [LOW]
- validateTemplate widens to accept unknown and rejects null,
non-plain-object and array payloads with a clear BaseError instead
of crashing later in coerceVersion [LOW]
- SignedMeterValueSchema, UnitOfMeasureSchema and WsOptionsSchema
replace z.looseObject({}) with typed shapes plus .catchall(z.unknown())
for forward-compatible vendor extensions [LOW]
- TemplateValidationError now surfaces '(migrated from vX -> vY)' in
its message so post-migration validation failures are visible to
operators
Tests: update coerceVersion(null|undefined) expectation to 0; add
end-to-end legacy migration, deprecated-key rejection (Schema and
Validation layers), null/string/array payload guards, array-form
numberOfConnectors auto-trigger regression, dead-branch regression,
migratedFrom message presence, and unit tests for the two new
Helpers.ts helpers.
- normalize $schemaVersion to coerced integer in validateTemplate so the
no-migration path (when version === CURRENT) also satisfies the strict
z.literal(CURRENT_SCHEMA_VERSION) check; was failing for user-authored
templates with string "$schemaVersion": "1" [F1]
- harmonize coerceVersion error wording on "non-negative integer" across
all rejection branches; the previous "positive integer" message was
contradictory since 0 is a valid input (legacy/pre-versioning sentinel
triggering v0 migration) [F2]
- introduce SCHEMA_VERSION_STRING_PATTERN (^\\d+$) gate in coerceVersion
to reject permissive Number() coercions ('1.0', '0x1', '1e0', ' 1 ',
'', '+1', '01a'); pattern mirrors the canonical non-negative-integer
string pattern used in TemplateSchema for connector/EVSE keys [F6]
- replace for...in with Object.entries destructuring in Evses topology
validation, harmonizing with the prior-art pattern in Helpers.ts and
removing the redundant template.Evses[evseKey] re-lookup [F3]
- tighten getMaxConfiguredNumberOfConnectors cast to readonly number[]
matching the helper signature [F5/F7]
- document the cache-bound warning emission frequency in
transformTemplate's JSDoc: warnings fire once per (templateFile,
schemaVersion) cache miss rather than per station instance, which is
template-scoped on purpose [F4]
Tests: assert string "$schemaVersion": "1" is accepted and normalized
to numeric 1; battery of 8 permissive numeric strings rejected with
harmonized wording; coerceVersion rejection branches all use the same
"non-negative integer" diagnostic.
* fix(template): broaden wsOptions.headers and harden template pipeline
- templateHash uses SRI-style `${algorithm}:${schemaVersion}:${contentHash}`
computed over the validated template, restoring whitespace-insensitive
cache semantics from pre-PR main while keeping schema-version-bump
invalidation deterministic.
- Migration registry refactored to sequential n→n+1 chain so future
schema versions append one function instead of rewriting prior
migrations (no behavior change at v1).
- validateTemplate clones its input via structuredClone at the
validation boundary, honoring the repository immutability convention.
BREAKING CHANGE: external log monitors keyed on the literal string
"Failed to read charging station template file" must also match
"Invalid charging station template payload (not a JSON object)" for
JSON-shape errors; file I/O errors retain their previous wording via
handleFileException.
* test: extract mockLoggerWarnDebug helper to dedupe migration logger spies
Replaces 8 occurrences of the warn+debug t.mock.method pair across
TemplateMigrations.test.ts and TemplateValidation.test.ts with a typed
helper sibling to createLoggerMocks.
* chore(deps): deduplicate pnpm-lock entries for @rolldown/pluginutils and ws
feat: resolve #1020 — Persist minimal simulator state and reconstruct template indexes on startup (#1858)
* feat: persist minimal simulator state and reconstruct template indexes on startup
- Add persistState boolean to ConfigurationData (default: true)
- Add SimulatorState to FileType enum
- Add simulatorState to AsyncLockType enum
- Implement BootstrapStateUtils with:
- readStateFile: reads and validates state.json with schema version check
- writeStateFile: atomic write via tmp+rename with AsyncLock
- reconstructTemplateIndexes: scans per-station config files to rebuild indexes
- deleteStateFile: safe file deletion
- Integrate into Bootstrap:
- reconstructTemplateIndexes called in start() before worker spawn
- State file written on start() (started:true) and stop() (started:false)
- shouldAutoStart() reads state file to control auto-start behavior
- persistStateEnabled getter checks persistState config and SIMULATOR_COLD_START env
- Update start.ts to conditionally start based on shouldAutoStart()
- Handle edge cases: corrupt files, missing fields, incompatible schema version
- Add comprehensive tests for all state persistence and reconstruction scenarios
Closes #1020
* refactor(bootstrap): harmonize persisted state feature with codebase conventions
Address review findings on PR #1858:
- HIGH #1: Phase split start() lifecycle. Add public Bootstrap.startUIServer()
that always brings up the UI server (and reconstructs template indexes
before accepting requests). start.ts unconditionally calls startUIServer()
and only gates Bootstrap.start() on shouldAutoStart(), so a persisted
stopped state no longer turns the simulator into a zombie process.
- HIGH #2: Add canonical default. New defaultPersistState constant and
Configuration.getPersistState() accessor matching the existing
defaultUIServerConfiguration / defaultWorkerConfiguration pattern.
Bootstrap.persistStateEnabled now delegates instead of inlining ?? true.
- MEDIUM #3: Document persistState tunable and SIMULATOR_COLD_START
environment override in README.md.
- MEDIUM #4: writeStateFile and deleteStateFile now swallow filesystem
errors via handleFileException with throwError:false, mirroring
watchJsonFile and the storage layer. Persistence failures no longer
surface as misleading 'Startup error' / 'Shutdown error'.
- MEDIUM #5: reconstructTemplateIndexes runs inside startUIServer() before
uiServer.start(), closing the race where UI requests could arrive before
index reconstruction completes.
- LOW #7: Add formatLogPrefix() utility and use it in BootstrapStateUtils,
removing the leading-space artifact when logPrefixFn is undefined.
- LOW #8: On state file schema mismatch, quarantine to <path>.v<N>.bak
instead of silently deleting, allowing forensics during partial schema
rollouts. JSON parse errors still delete (true corruption, unrecoverable).
- LOW #9: Move state.json from dist/assets/configurations/ to dist/assets/.
Drops the fragile basename-based filter from reconstructTemplateIndexes
and separates control file from per-station configuration files.
Drop shouldAutoStart() from IBootstrap interface (boot-time concern, no UI
service consumer needs it). The method stays public on the concrete
Bootstrap class for start.ts.
Tests: align fixtures with new state.json location, add quarantine assertion,
add non-fatal writeStateFile assertion, drop obsolete state.json filter test.
* test(bootstrap): align BootstrapStateUtils tests with project style guide
- Rename BootstrapState.test.ts to BootstrapStateUtils.test.ts to match the
source module name (TEST_STYLE_GUIDE.md §1: 'Files: ModuleName.test.ts').
- Add mandatory standardCleanup() in afterEach (TEST_STYLE_GUIDE.md §3),
matching the convention used by Configuration.test.ts, FileUtils.test.ts,
ErrorUtils.test.ts and ConfigurationKeyUtils.test.ts.
* fix(bootstrap): address review findings on persisted simulator state
Distinguish UI-initiated stop from signal/restart via a private
StopReason enum, so SIGINT/SIGTERM/SIGQUIT and config-reload restarts
no longer flip the persisted state to stopped (HIGH-1).
Short-circuit persistStateEnabled when the UI server is disabled, since
persistence has no recovery channel without a UI; warn once on the
inconsistent configuration (HIGH-2).
Reconstruct template indexes via a shared prepareTemplateStatistics
helper called from constructor and restart, not only from startUIServer,
to prevent index collisions on config-reload of UI-added stations
(MED-3).
Move state file from dist/assets/state.json (wiped by pnpm build) to
dist/assets/configurations/.simulator-state.json; filter dot-prefixed
metadata files in reconstructTemplateIndexes so the state file co-located
with charging station configurations is silently skipped (MIN-10).
Clean up the .tmp file on atomic-rename failure in writeStateFile and
guard readStateFile against JSON null/primitive/array content with an
explicit message (MIN-7, MIN-8).
Unify path computations into readonly assetsDir/configurationsDir/
stateFilePath fields, deduplicate setChargingStationTemplates via a
syncUIServerTemplates helper, and route the SIMULATOR_COLD_START env
var name through Constants.ENV_SIMULATOR_COLD_START (MIN-5, MIN-6).
Export DEFAULT_PERSIST_STATE and add the persistState entry to
config-template.json so the tunable is discoverable (MED-4).
Update README to document the new state file path, the UI server
requirement, and the signal-shutdown semantics.
Test coverage added for tmp cleanup, JSON null/primitive/array guards,
and dot-prefixed metadata filtering; the misleading 'read-only
directory' test is renamed to reflect the actual OS-rejected-path
scenario (MIN-9).
* docs(bootstrap): clarify persisted state semantics from review feedback
- README: drop developer-internals sentence on template index reconstruction
from the persistState row
- TemplateStatistics: document the `added` (process-scoped) vs `indexes`
(process+disk-scoped) distinction exposed via SimulatorState
- IBootstrap: document the contract scope (UI-server-facing, excluding
process-lifecycle helpers and the internal StopReason)
- formatLogPrefix: document the trailing-space contract
- reconstructTemplateIndexes: refine the warn message wording for non-station
configuration files (level unchanged)
* refactor(utils): default formatLogPrefix prefix to logPrefix
Avoids silent loss of the timestamp when callers do not pass a module-specific
prefix function. Aligns with the existing convention where every module-level
logPrefix delegates to Utils.logPrefix.
Jérôme Benoit [Mon, 11 May 2026 19:20:30 +0000 (21:20 +0200)]
refactor: consolidate object-check utilities to eliminate duplication
Replace type() + isObject with a single isPlainObject helper.
Make isJsonObject and assertIsJsonObject delegate through it
instead of duplicating the check logic independently.
Jérôme Benoit [Mon, 11 May 2026 18:43:22 +0000 (20:43 +0200)]
refactor: remove unnecessary type assertions across monorepo
- Remove all @typescript-eslint/no-unnecessary-type-assertion violations
- Add assertIsJsonObject/isJsonObject utilities for runtime-safe narrowing
- Restructure OCPP16RequestService.buildRequestPayload with proper
runtime validation (assertIsJsonObject + OCPPError) replacing unsafe cast
- Use _syncResult assignment pattern in OCPPResponseService for
no-floating-promises compliance while preserving isAsyncFunction pattern
- Refine AsyncLock.runExclusive fn parameter as union of function types
for proper isAsyncFunction type guard narrowing
- Configure varsIgnorePattern: '^_' for @typescript-eslint/no-unused-vars
- Remove unused type imports in test files
renovate[bot] [Mon, 11 May 2026 13:14:03 +0000 (15:14 +0200)]
chore(deps): update pnpm to v11 (#1850)
* chore(deps): update pnpm to v11
* fix: migrate pnpm settings from package.json to pnpm-workspace.yaml
pnpm 11 no longer reads the 'pnpm' field from package.json.
Move patchedDependencies and allowBuilds to pnpm-workspace.yaml.
Remove dead pnpm field from package.json.
fix: resolve #1244 — add per-connector maximum power support (#1843)
* feat(charging-station): add per-connector maximum power support
Add maximumPower field to ConnectorStatus representing the physical
limitation of each connector cable/plug (thermal current rating).
Per OCPP Device Model, AvailablePowerMaxLimit is defined at the
Connector component level. The connector maximumPower acts as a
hardware cap in the power computation pipeline alongside the station-
level powerDivider sharing mechanism.
- Add ConnectorStatus.maximumPower?: number (in W)
- Initialize at boot via initializeConnectorsMaximumPower(): default
is stationPower / staticConnectorCount (using static count, not
dynamic powerDivider which can be 0 in shared mode at init)
- Clamp in getConnectorMaximumAvailablePower as additional min() term
- Use in getConnectorChargingProfilesLimit as primary cap (falls back
to stationPower/powerDivider for backward compat)
- Update 6 shared-mode templates with explicit maximumPower per
connector (= station power for DC shared-bus stations)
Resolves #1244
* chore(sandcastle): update validation and main scripts
* chore: sync release-please manifests and sandcastle prompt
* fix(charging-station): exclude index 0 from staticCount in getDefaultConnectorMaximumPower
The staticCount calculation included EVSE 0 and connector 0, while runtime
getPowerDivider excludes them. This caused connector hardware caps to be
more restrictive than intended (e.g., stationPower/3 instead of
stationPower/2 on a 2-EVSE station with EVSE 0 defined).
* [autofix.ci] apply automated fixes
* refactor(charging-station): add NaN guard to connectorHardwareMaximumPower in min()
Align the connectorHardwareMaximumPower entry with the same null/NaN guard
pattern used by all other entries in the min() call for consistency and
defensive robustness.
* docs: document per-connector maximumPower in template examples
Add maximumPower field to Connectors and Evses section examples.
Clarify powerSharedByConnectors behavior description.
Jérôme Benoit [Fri, 8 May 2026 18:45:44 +0000 (20:45 +0200)]
fix(ui/web): smooth icon-btn danger hover shadow and remove dead token
- Add box-shadow to .modern-icon-btn transition shorthand so the danger
variant hover glow animates instead of snapping
- Remove --skin-shadow-color (declared line 61 but never consumed)
Jérôme Benoit [Fri, 8 May 2026 18:30:30 +0000 (20:30 +0200)]
fix(ui/web): fix editable pill hover visibility in light themes
Refactor pill background to use scoped --_pill-bg and --_pill-bg-hover
custom properties (MD3 pattern). Each variant co-locates its base and
hover background values, and light-mode overrides reassign both.
The hover rule simply consumes var(--_pill-bg-hover), eliminating the
specificity battle that prevented the hover effect from appearing in
light themes.
Jérôme Benoit [Fri, 8 May 2026 18:00:49 +0000 (20:00 +0200)]
refactor(ui/web): remove redundant connector status column from classic skin
The connector status is now accessible via the Set Status dropdown in
the Actions column, making the dedicated read-only Status column
redundant. Move status/error-code selects to the top of Actions and
style them to fill the column width consistently with buttons.
Jérôme Benoit [Fri, 8 May 2026 15:08:22 +0000 (17:08 +0200)]
fix(sandcastle): patch pi thinking option and replace type indirections
Apply pnpm patch for @ai-hero/sandcastle porting PR #584 (pi --thinking
flag). Wire the thinking option through agentProvider() and replace
Awaited<ReturnType<...>> indirections with direct type imports (Sandbox,
SandboxRunResult, RunResult, PiOptions).
Jérôme Benoit [Fri, 8 May 2026 14:09:44 +0000 (16:09 +0200)]
fix(sandcastle): wire reasoning effort through to agent providers
Pass AGENT_*_EFFORT constants through agentProvider() to the opencode
provider's variant flag. LoopStrategy gains actorEffort/criticEffort
optional overrides following the same pattern as actorModel/criticModel.
- opencode: effort mapped to --variant CLI flag
- pi: no effort support (provider limitation, param silently ignored)
Jérôme Benoit [Thu, 7 May 2026 23:48:28 +0000 (01:48 +0200)]
refactor(sandcastle): remove plannerOutput from TaskSpec
Raw agent stdout will be handled by sandcastle's own debug/logging
mechanism rather than stored in-memory on TaskSpec. Structured fields
(acceptanceCriteria, rootCauseHypothesis, confidence, issueType) remain
as the sole inter-agent communication channel.
Jérôme Benoit [Thu, 7 May 2026 23:33:21 +0000 (01:33 +0200)]
refactor(sandcastle): remove redundant lastFindings from LoopResult
Derive last-round findings from roundHistory.at(-1)?.findings in
finalizer.ts instead of maintaining a separate field. The PR body
now shows all critic findings from the final round (including LOW
confidence) for full transparency.
Jérôme Benoit [Thu, 7 May 2026 23:20:58 +0000 (01:20 +0200)]
fix(sandcastle): assign opus to actor, sonnet to planner, increase planner iterations
Swap model assignments: claude-opus-4.6 (high effort) for the actor,
claude-sonnet-4.6 (medium effort) for the planner. Increase planner
maxIterations from 1 to 5 so it can actually read AGENTS.md and
project context before producing its analysis.
Jérôme Benoit [Thu, 7 May 2026 23:04:27 +0000 (01:04 +0200)]
feat(sandcastle): enrich planner with acceptance criteria and root cause hypothesis
The planner now produces structured analysis per issue: issueType,
confidence, rootCauseHypothesis, and acceptanceCriteria. These flow
into the actor prompt (confidence-gated hypothesis + criteria) and the
critic prompt (criteria as verification checklist).
- Confidence controls plan specificity: high → full context, medium/low → criteria only
- All planner-generated fields are sanitized and length-bounded
- Critic evaluates observable outcomes, never plan adherence
- Backward-compatible: missing fields result in empty template variables
Jérôme Benoit [Thu, 7 May 2026 22:36:27 +0000 (00:36 +0200)]
feat(sandcastle): add roundHistory to LoopResult and plannerOutput to TaskSpec
Replace the unused onRoundComplete callback with a structured
roundHistory array that accumulates RoundSnapshot per round
(including post-loop validation retry). Attach raw planner stdout
to TaskSpec.plannerOutput for downstream verification use.
This enables a future planner-verification step to receive the full
findings history alongside the original plan context.
fix(ui): allow changing status of individual connectors (#1834)
* feat(ui/web): allow changing status of individual connectors
Add a 'Set Status' action to the connector UI in both modern and classic
skins. Users can simulate OCPP connector statuses (e.g. Faulted, Unavailable)
directly from the dashboard.
- UIClient.setConnectorStatus sends STATUS_NOTIFICATION via existing ProcedureName
- useConnectorActions exposes setConnectorStatus with pending.setStatus guard
- Modern skin: SetConnectorStatusDialog presents a status picker in a modal
- Classic skin: inline <select> triggers status change on change event
- Test helpers and composable tests updated accordingly
* fix: correct OCPP version handling for connector status changes
- Add OCPP20ConnectorStatusEnumType enum to ui-common
- Fix UIClient.setConnectorStatus to send version-aware payload:
connectorStatus field for OCPP 2.0.x, status field for OCPP 1.6
- Update useConnectorActions to accept ocppVersion and pass it through;
widen status parameter type to union of 1.6 and 2.0.x enums;
accept optional onSuccess callback per action invocation
- Fix SetConnectorStatusDialog to close only after action resolves
(pass close as onSuccess instead of calling it immediately)
- Show version-appropriate status options in SetConnectorStatusDialog
- Forward ocppVersion and onRefresh from ConnectorRow to dialog
- Propagate need-refresh event through ConnectorRow → StationCard → ModernLayout
- Add tests for new OCPP 2.0.x paths and dialog behaviour
* [autofix.ci] apply automated fixes
* feat(ui): complete connector status change with error simulation and local state update
- Fix classic skin to show version-appropriate status options (OCPP 1.6/2.0.x)
- Pass ocppVersion to useConnectorActions in classic skin
- Replace passthrough STATUS_NOTIFICATION handler with sendAndSetConnectorStatus
to update in-memory connector state and emit connectorStatusChanged event
- Add OCPP 1.6 errorCode support (issue requests error simulation capability)
- Add OCPP16ChargePointErrorCode enum to ui-common
- Add error code selector in both skins (OCPP 1.6 only, hidden for 2.0.x)
- Remove unnecessary re-exports from useConnectorActions composable
- Update tests for new errorCode parameter and passthrough behavior change
Resolves the outstanding HIGH findings from automated review.
* fix(ui): polish connector status — reactivity consistency, JSDoc, tests, init from current status
- Wrap props in computed() in SetConnectorStatusDialog for consistency
- Fix JSDoc on mountDialog test helper to satisfy jsdoc/require-jsdoc
- Initialize selectedStatus from connector.status for OCPP 2.0.x too
- Add 5 unit tests for classic skin CSConnector status change behavior
* refactor(ui): remove redundant onRefresh from connector actions
The server already pushes a REFRESH notification via WebSocket when
connector state changes (connectorStatusChanged → buildUpdatedMessage →
workerEventUpdated → scheduleClientNotification → REFRESH broadcast).
The onRefresh callback in useConnectorActions duplicated this by manually
calling getChargingStations() after each action. Remove it along with
the need-refresh event bubbling chain in the modern skin.
The useAsyncAction onRefresh mechanism remains available for composables
where the server does NOT push updates (e.g., useStationActions).
Factor version-aware StatusNotification payload construction into
ui-common alongside existing buildAuthorize/Start/StopTransactionPayload
builders. Both CLI and Web UI now use the shared builder, eliminating
duplicated OCPP 1.6 vs 2.0.x branching logic.
Remove string fallback from status/errorCode parameters — the builder
accepts only the OCPP enum types. The CLI casts user input at the call
site, keeping the shared API type-safe.
* [autofix.ci] apply automated fixes
* fix(ui-common): remove invalid Occupied from OCPP16ChargePointStatus enum
Occupied is an OCPP 2.0.x-only connector status. It was erroneously
included in the OCPP 1.6 enum, causing the UI dropdown to offer an
invalid status option for OCPP 1.6 stations. Tests referencing it are
updated to use OCPP20ConnectorStatusEnumType.OCCUPIED or a valid 1.6
status as appropriate.
* fix(ui-common): make ChargePointStatus a union of OCPP 1.6 and 2.0.x enums
ChargePointStatus was aliased to OCPP16ChargePointStatus only, which
made ConnectorStatus.status unable to represent OCPP 2.0.x values like
Occupied. Now it is OCPP16ChargePointStatus | OCPP20ConnectorStatusEnumType,
matching the src/ canonical ConnectorStatusEnum pattern.
* refactor(ui-common): use ChargePointStatus type alias instead of inline union
Replace all occurrences of the verbose
'OCPP16ChargePointStatus | OCPP20ConnectorStatusEnumType' inline union
with the existing ChargePointStatus type alias across ui-common, web UI,
and CLI.
* [autofix.ci] apply automated fixes
* fix: add connectorId guard to handleStatusNotification for consistency
Other broadcast channel handlers (handleMeterValues, UNLOCK_CONNECTOR,
LOCK_CONNECTOR) throw BaseError when connectorId is missing. Without
this guard, a malformed request would silently succeed.
* fix(ui): address review feedback — clickable status pill, stale state sync, error-code apply
Modern skin:
- Replace 'Set Status' button with clickable status pill (edit icon on
hover, tooltip with status, aria-haspopup=dialog). More compact UX per
reviewer request (DerGenaue).
- Add .modern-pill--editable CSS with hover border, focus ring, and
fade-in edit icon.
Classic skin:
- Add watch on props.connector.status to sync selectedStatus ref when
server pushes new state (fixes stale dropdown after external changes).
- Add @change handler on error-code select so changing errorCode alone
also triggers a StatusNotification (previously only status change did).
Addresses review feedback from hyperspace-insights, copilot, and
DerGenaue.
Backend:
- Add errorCode field to ConnectorStatus (src/ and ui-common)
- Persist errorCode in sendAndSetConnectorStatus alongside status
- Move errorCode defaulting from buildStatusNotificationRequest to
OCPP16RequestService.buildRequestPayload via spread default pattern:
{ errorCode: NO_ERROR, ...commandParams }
- Remove Partial<> cast and ?? fallback from the builder — it now
simply passes through commandParams.errorCode (always defined by
the time it reaches the builder)
UI:
- Status pill tooltip shows errorCode when present and not NoError
(e.g., 'Faulted (ConnectorLockFailure)'), per DerGenaue's request
- ConnectorStatus.errorCode propagates automatically through existing
buildUpdatedMessage → spread serialization chain
Introduce AGENT_PROVIDER constant to switch between pi and opencode backends.
pi streams JSON output immediately, avoiding the opencode idle timeout bug
where git check-ignore indexing produces zero stdout.
Also removes redundant copyToWorktree since onSandboxReady pnpm install
handles node_modules via the mounted pnpm store.
Jérôme Benoit [Thu, 7 May 2026 18:26:40 +0000 (20:26 +0200)]
fix(sandcastle): revert idle timeout to 300s and remove redundant copyToWorktree
The 600s timeout was a misguided workaround for serena MCP init. The actual
root cause is an opencode bug: zero stdout during its git check-ignore
indexing phase. Removing copyToWorktree since pnpm install in onSandboxReady
already handles node_modules via the mounted pnpm store.