Session log
Newest entry last. Keep each entry to about 15 lines; findings that others depend on go to research/ and are linked.
Template:
## S<nn> <title> (<date>, <branch/PR links>)
Status: done | partial (what remains)
Done: <3-6 bullets>
Findings: <facts learned that others depend on, with evidence: command, output summary, link>
Decisions: <new or changed D-ids>
Changed in the plan: <files and sections updated>
Open questions: <new ones>
Post-merge checks: <what must hold after the squash merge; the next session verifies and records it>
Next session pickup: <session id, exact first steps, anything to prepare (manual steps for the maintainer)>
Pre-port: Valkey compatibility run (2026-11-07)
Status: done.
Done: Python integration tests run against a throwaway valkey/valkey-bundle container, nothing committed (D-5).
Findings: JSON.*, Lua, Streams, FT.CREATE/FT.INFO and ACLs work; FT.SEARCH returns a flat array even on RESP3 and breaks the Python code. Details in Valkey findings.
Decisions: D-5 done.
Freeze and plan restructure (2026-11-07)
Status: done. Done:
python-archivecreated frommainat12c1415unchanged, no tag. Stale local refs removed (feature/aiomqtt-v3, already deleted on GitHub and fully merged; an orphanedremote/mainref). Five Dependabotuvbranches remain open on GitHub (closed in S01).porting.mdandporting-review.mdreplaced bydocs/porting/(plan, decisions, sessions, log, research). The review findings were applied to their target sections; the review file was not kept.- No Gitleaks history scan before the freeze: the project never used credentials; the first full-history run in S03 covers the shared history.
Decisions: D-21 (OPC UA stack is MIT since v2). D-7, D-17, D-18 gained the review points.
Session ids renumbered (old → new): S0a removed (done above); S0b → S01, S0c → S02, S0d → S03, S1 → S04, S2 → S05, S3 → S06 (core contracts) and S07 (data layer, SFC rules), S4 → S08, S5 → S09, S6 → S10, S6b → S11 (inspection, MCP read tools) and S12 (maintenance), S7 → S13, S8 → S14, S12 → S15 (pure core) and S19 (Valkey host), S9 → S16, S10 → S17, S11 → S18, S13 → S20, S14 → S21, S15 → S22, S16 → S23, S17 → S24, S18 → S25, S19 → S26.
Next session pickup: S01 (python-archive is on GitHub; the plan itself was merged through its own PR before S01). The first S01 commit removes Python.
Plan updates (2026-11-07, later)
Status: done.
Decisions: D-22 (one GitHub stack per milestone, one squash-merged PR per session), D-23 (Claude Code replaces the Copilot setup). Carried-over Python decisions now start at D-24.
Findings: sanri/zenoh-csharp (NuGet Zenoh-CS 0.5.0) wraps zenoh-c 1.10.0 and is the preferred Zenoh candidate; checked in S04.
Changed in the plan: library table, S01, S03, S04, S11, S25, S26, working rules; research files on CI scanning and MCP.
Done: GitHub CLI 2.103.3 installed in ~/.local/bin (checksum verified), logged in, gh auth setup-git; python-archive is on GitHub at 12c1415; Dependabot uv PRs #31 to #35 closed with a comment.
Changed in the plan: S01 switches GitHub Pages from the gh-pages branch (legacy mode, peaceiris/actions-gh-pages) to deployment through Actions.
Maintainer decisions (2026-11-07)
Decisions: D-24 (MIT licence), D-25 (authoring experience is a primary goal), D-26 (valkey-bundle image); D-18 revised (outage grace, deadline from the recipe's timeout_seconds). Carried-over Python decisions now start at D-27.
Done: .NET SDK 10.0.403 installed in ~/.dotnet with dotnet-install.sh; clang and zlib1g-dev installed by the maintainer. ~/.dotnet still has to be added to PATH (maintainer).
D-17 revised: late-response rule (finding 4) and outcome-based transition conditions (IEC 61131-3 compatible: named Boolean transitions, priority order, "true" as default) decided.
Open for S07: max_attempts default, outcome mapping per type_id, routable timeout/error outcomes.
Done: GitHub secret scanning and push protection enabled (2026-11-07); no alerts at the time of the check.
S01 Repository reset, scaffolding, agent setup (2026-11-07, #38, follow-up s01-handoff)
Status: done. Done:
- Python removed from
main(first commit); MITLICENSE, README banner,SECURITY.mdwithout thepyproject.tomlreference.compose.yamlonvalkey-bundle, images pinned by digest, Pythonappprofile dropped. Dependabot:nugetreplacesuv,pipfor/docs, plusdocker-composefor the digest pins. - Scaffolding:
global.json(10.0.403, MTP runner),Directory.Build.props, central packages,nuget.configwith source mapping,.editorconfig,MicroDCS.slnx,MicroDCS.CoreandMicroDCS.Core.Tests(one test). Build, test,format --verify-no-changesand locked restore pass with zero warnings. - Docs: zensical 0.1.0 pinned with hashes (
docs/requirements.txt);docs.ymlbuilds and deploys through Actions. WIP announcement bar (overrides/main.html), "Not yet ported" on every Python page, nav "Port to .NET (WIP)"; tables, anchors and mermaid checked in a local build. D-11 in "Southbound Connectivity";development.mdhas the .NET toolchain and the cloud setup script. - Claude Code:
CLAUDE.md(102 lines), four path rules,/session-startand/session-end(stack workflow),.claude/settings.jsonwith allowlist and hooks (restore at session start, whitespace format of edited*.cs)..mcp.jsonleft absent until S11; no.devcontainer. - GitHub: squash merge only, delete head branches,
mainprotected (PR required, 0 approvals, admins included, linear history, conversation resolution), milestones M0 to M3,gh-stackv0.3.0 installed (official extension; stacked PRs usable with gh 2.103.3).
Findings: clang was missing despite the earlier log; the maintainer installed it (clang 18.1.3), and a throwaway AOT console publish ran. Dependabot needs the docker-compose ecosystem for digests in compose files. The format hook uses dotnet format whitespace --folder (about 1.4 s) instead of a full dotnet format (about 4 s).
Decisions: D-27 to D-30 carried over from Python (sorted list), D-31 .NET conventions (S01 defaults taken).
Changed in the plan: index (D-27 to D-31, session status), decisions, research file added.
Open questions: none new. Not verified: the cloud setup script and NuGet reachability from a claude.ai/code environment (needs a cloud session).
Post-merge (done): Pages source switched to Actions, github-pages environment allows main only, #38 squash-merged (8748d43), docs workflow deployed (build and deploy green), live site shows the banner and "Port to .NET (WIP)", gh-pages deleted, Dependabot alerts and security updates on (alerts were off and are a prerequisite). gh stack merge refuses a single PR ("not a stacked pull request": a GitHub stack needs two or more PRs); gh pr merge --squash used instead, now in /session-end. Three stale dependabot/uv/* branches without PRs remain on GitHub (harmless).
Next session pickup: S02. Run /session-start S02. All S01 PRs are merged, so start a new M0 stack from main: git switch main && git pull --ff-only && gh stack init s02-ci-container. Workflow names change in S02 (docs.yml is already named docs; the new .NET ci.yml should be named ci) before required checks are added to the main protection. Maintainer, optional: paste the setup script from docs/development.md into the claude.ai/code environment and run dotnet restore in a cloud session to verify NuGet reachability.
S02 CI, container, compose harness, CloudEvents AOT measurement (2026-11-07, #40, follow-up #41)
Status: done. Done:
ci.yml(workflowci) has three jobs.dotnet: locked restore with audit, Release build, tests, format,dotnet package list --vulnerable.container: AOT image,microdcs smokein it, non-root and no-shell check.integration: compose harness plus the tests container's own exit code. All three ran green on #40.src/MicroDCS.App(microdcs smoke);scripts/aot-publishwith the D-14 allowlist;Dockerfilefromsdk:10.0-noble-aottoruntime-deps:10.0-noble-chiseled, digest-pinned, uid 1654, no shell, 25.5 MB.compose.test.yaml(Valkey and Mosquitto on ports 6380/1884, ephemeral) plustests/MicroDCS.IntegrationTests(raw ValkeyPING, MQTT 5 CONNACK). They skip on the host withoutMICRODCS_TEST_*; in compose--fail-skips on.- Fixed an S01 bug in
Directory.Build.props: thesrc/andtests/groups matchedContains('/src/')on the absolute path, so every project matched in a/srccontainer build. The match now uses the repo-relative path.src/projects restorelinux-x64;linux-arm64, so a RID publish works in locked mode.
Findings: the CloudEvents 2.9.0 AOT binary works at runtime, but only with source-generated JsonSerializerOptions passed to the formatter (measurement). PR #381 is open and 2.9.0 is the latest release. compose up --exit-code-from tests exits 0 when tests never started, and a one-shot service aborts the run early; both are handled in ci.yml and documented in docs/development.md. The arm64 AOT link needs an arm64 host or a cross toolchain; follow-up #41 adds native arm64 CI (D-32).
Decisions: D-14 gained the measured result and the codec rule (no revision). Follow-up #41: D-32 (linux-x64 and linux-arm64, native CI builds), D-22 revised (no post-merge follow-up PR; the next session verifies "Post-merge checks").
Changed in the plan: index (status, library table, Q-TEST-1 owner S23, event row), D-14, research/cloudevents.md; CLAUDE.md, docs/development.md, /session-end DoD.
Open questions: none new.
Post-merge (done for #40): squash-merged (729e8a8), ci and docs green on main.
Follow-up #41: container became a matrix with container (linux-x64) on ubuntu-24.04 and container (linux-arm64) on ubuntu-24.04-arm, plus an image architecture check. The session skills now use "Post-merge checks" instead of follow-up PRs.
Post-merge checks (for #41): ci green on main with all four jobs; required checks on main are dotnet, container (linux-x64), container (linux-arm64), integration (GitHub Actions app id 15368, strict), set before the merge.
Next session pickup: S03. Run /session-start S03. All S02 PRs are merged, so start a new M0 stack from main: git switch main && git pull --ff-only && gh stack init s03-scanning. S03 adds jobs to ci.yml and a security.yml; add their job names to the required checks (gh api -X PATCH .../branches/main/protection/required_status_checks with the full checks list, app id 15368). Maintainer before S03: SonarQube Cloud project and SONAR_TOKEN secret (Actions and Dependabot), see CI scanning.
S03 Security and quality scanning (2026-11-07, #42, self-test #43 closed)
Status: done. Previous session post-merge: verified (ci and docs green on main, the four required checks set).
Done:
- Every
uses:pinned by commit SHA; docs pip install wheel-only (Sonar S8541).compose.scan.yaml(Gitleaks 8.30.1, Trivy 0.76.2 by digest; Trivy cosign-verified),scripts/scan,.gitleaks.toml(canary rule),trivy.yaml,.trivyignore.yaml, opt-in.githooks/pre-commit. security.yml:gitleaks(PR range; full history onmainand daily),trivy-fs,trivy-image(SARIF to code scanning, CycloneDX SBOMssbom-sourceandsbom-image). Thedotnetjob inci.ymlruns SonarScanner for .NET with coverage (Microsoft.Testing.Extensions.CodeCoverage, VS coverage XML).SonarAnalyzer.CSharpas a global package reference: Sonar C# rules fail local and CI builds alike (maintainer decision). Agent rule.claude/rules/security.md, docs (development, security waiver table), README badges.
Findings (details): Sonar ran as Automatic Analysis before S03 and is now CI-based. Trivy does not check compose files or workflows. The full git history (python-archive included) has no Gitleaks findings. Self-test on #43: gitleaks, trivy-fs and the Sonar gate went red; a Sonar C# bug fails the build itself. Timings: gitleaks 7 s, trivy-fs 30 s, trivy-image 1.5 min, Sonar adds ~40 s to dotnet.
Decisions: D-20 confirmed (no revision), with corrections in the research file.
Changed in the plan: index (status, D-20 row), D-20, research/ci-scanning.md, sessions.md (S22: k8s Sonar findings); CLAUDE.md, .claude/rules/.
Open questions: none new. Optional, not added: zizmor/actionlint in CI, SonarQube MCP server for agents.
Post-merge checks: ci and security green on main (gitleaks scans the full history there); SonarQube Cloud main analysis shows C# and coverage; required checks on main: dotnet, integration, container (linux-x64), container (linux-arm64), gitleaks, trivy-fs, trivy-image (app 15368) and SonarCloud Code Analysis (app 12526), strict, set before the merge; Dependabot (Insights → Dependency graph → Dependabot) lists compose.scan.yaml and .config/dotnet-tools.json.
Post-merge fix (#44): docs failed on main because python-version: 3.x resolved to CPython 3.15, for which pyyaml 6.0.3 has no wheel, and the S03 wheel-only install refuses to build it; the docs Python is now pinned to 3.14. ci, security, the Sonar gate on main and Dependabot coverage of compose.scan.yaml and .config/dotnet-tools.json were verified after the merge.
Follow-up (#45): actionlint and zizmor in scripts/scan workflows and the required security.yml job workflows; /session-end unstacks merged single-PR stacks locally; optional SonarQube MCP setup documented.
Next session pickup: S04. Run /session-start S04. M0 is complete, so start the M1 stack from main: git switch main && git pull --ff-only && gh stack init s04-library-spike. New packages now also pass the Sonar C# rules in the build; spike code under spikes/ is excluded from the Sonar analysis but not from the build analyzers. Run scripts/scan before commits touching workflows, Docker or configuration. No maintainer preparation needed.
S04 Spike: library stack, AOT and Valkey behaviour (2026-11-07, branch s04-library-spike)
Status: done (Zenoh Get crash unresolved, handed to S25). Previous session post-merge: verified (ci, security, docs green on main; the nine required checks are set).
Done:
- One AOT console project per library plus a combined one (
spikes/, deleted at the end), run against a temporary compose stack with NATS, zenohd and Artemis. - Valkey: Lua string scripts,
TIMEthen write,XAUTOCLAIMwith deleted ids, stream id as event time, polling versus nudge latency, valkey-json/search versus hash plus sets, reconnect. - MQTTnet v5 features, NATS.Net (JetStream, KV, request/reply), Kestrel with MCP over HTTP, AMQPNetLite against Artemis, Zenoh-CS against zenohd.
Findings (library spike): Redis, MQTTnet, NATS.Net, Kestrel and MCP are AOT clean and work at runtime (24.3 MB together). AMQPNetLite has 13 warnings, all in Amqp.Serialization. Zenoh-CS: pub/sub with attachments works, Session.Get against a queryable crashes (free(): invalid pointer, AOT and JIT); suspected stale ZGetOptions struct, not verified because the sandbox denied building the cloned source. Nudge gives p50 1 ms against half the poll interval; no performance reason to drop valkey-json and valkey-search.
Decisions: D-33 (answers Q-ARCH-1: AMQP in an optional package, Zenoh library open).
Changed in the plan: index (status, library table, Q-ARCH-1, Q-DATA-1, Q-DATA-2, D-33), decisions, sessions (S24, S25), research/library-spike.md.
Open questions: none new. Not covered: arm64 runs (CI is the reference), ZenohDotNet.
Post-merge checks: ci, security and docs green on main (docs-only change).
Next session pickup: S05 (OPC UA ISA-95 spike, no dependency on S04) or S06 (design, depends on S04). Run /session-start S05 or S06. Start from main after the S04 merge: git switch main && git pull --ff-only && gh stack init <branch>. Nothing to prepare for the maintainer; for S25 later, decide whether the Zenoh-CS fix goes upstream.
Follow-up (S04, correction): AMQPNetLite's 13 AOT warnings came from the metapackage's serializer. AMQPNetLite.Core alone is AOT clean and works at runtime, so D-33 was revised (no optional package for AOT reasons) and S24, the index and the library spike were corrected. The same PR carries the SonarLint connected-mode setting in .vscode/settings.json. Post-merge checks: ci, security and docs green on main.
S05 Spike: OPC UA ISA-95 for models only (2026-11-07, branch s05-opcua-isa95-spike)
Status: done. Previous session post-merge: verified (ci, security and docs green on main for #46 and the follow-up #47; nine required checks set).
Done:
- Throwaway AOT projects (not committed) with
Opc.Ua.ISA952.0.1 as plain models: V1 and V2 objects, OPC UA JSON encode and decode, schema generation, publish AOT; an own NodeSet2 model with a Work Master subtype through the SDK source generator; the stack's server package read for its state logic and V1/V2 design. - Direction from the maintainer after the first result: NodeSet path, V1 and V2, check whether the stack's state machine would be the right thing to use; then: the transition rules come from the NodeSet (as in Python), and the extended Work Master gets its own CloudEvent type. All tried or recorded; D-34 rewritten twice.
Findings (OPC UA spike): packages target net10.0, MIT. Models work without a server and publish AOT clean (9 to 12 MB). JsonEncoder/JsonDecoder give usable verbose JSON, System.Text.Json on the generated types does not; three codec traps. NodeSet2 plus the source generator creates a Work Master subtype that resolves the V2 model from the referenced assembly, AOT clean; as a subtype inside a job order it decodes as the base type. V1 works with the same codec. The stack has a version-neutral core and V1/V2 conversions (internal), the right architecture; its engine is lock/dictionary based and entangled with notifications. The transition rules are data in the V2 NodeSet (34 references, causes and effects; MIT, UA-Nodeset), including a Prepare machine the engine lacks; V1 has no state machine in its NodeSet and its behaviour differs (Stop removes the order), per the upstream guide docs/ISA95.md, which was read in full: neutral store contract, execution-system transitions, Annex B ReturnStatus, NodeSet pinning. S16 generates the table from the NodeSet.
Decisions: D-34 (answers Q-ARCH-2 for job models and Q-ARCH-4).
Changed in the plan: index (status, OPC UA row, Q-ARCH-2, Q-ARCH-4, ISA-95 row, D-34, session table), decisions, sessions (S16), research/opcua-spike.md, zensical.toml nav.
Open questions: none new. Maintainer action: ask upstream (UA-.NETStandard) whether the neutral job model, conversions and rules can be public, and whether the Work Master NodeSet2 is welcome. Not covered: binary/XML encodings, arm64, line-by-line rules comparison (S16), V1 to V2 field conversions, schema validation of incoming payloads.
Post-merge checks: ci, security and docs green on main (docs-only change).
Next session pickup: S06 (design: core contracts, depends on S04, done). Run /session-start S06. Start from main after the S05 merge: git switch main && git pull --ff-only && gh stack init <branch>. S06 records OPC UA JSON verbose as the job order payload in the envelope contract (D-34). The upstream question is the maintainer's; it does not block S06.
S06 Design: core contracts (2026-11-08, branch s06-core-contracts)
Status: done. Previous session post-merge: verified (ci, security, docs green on main for #48; nine required checks set). CloudEvents checkpoint (D-14): PR #381 still open, 2.9.0 latest.
Done:
- Python cross-check made part of the workflow (maintainer request):
/session-startand/session-endand the working rules require a parity list; S06 has about 75 rows, none open (core contracts). - Authoring API drafted as three example processors and confirmed at a checkpoint with the maintainer (D-35);
[Outgoing]kept after readingsfc_engine.py(the SFC dispatches push commands by type id). - Compiling stubs in
MicroDCS.Core(Authoring,Messaging,Transport,Inspection,Hosting), 14 unit tests for the pure rules; build, format,dotnet test, locked restore and AOT publish with smoke run are clean. Findings: hidden payload fields (_xtravel as CloudEvent attributes), the misnamed pre/post hooks, ack of duplicates, singletonpost_startlease, five-phase shutdown and the SFC observer hooks were in no plan document; all have a disposition.TransactionDedupeDAOandCounterDAOare unused in Python and dropped. Decisions: D-35 authoring API, D-36 envelope and payload codec seam, D-37 pipeline, transport contract, delivery and error model, D-38 address model, scope rule (level-0 rejected by default, R-6), state-topic port, D-39 hosting and configuration (APP_+__), D-40 inspection contracts and management listener. Changed in the plan: index (decisions, Q-STRAT-4, Q-ARCH-2/3/6/7, Q-OPS-1, session table, working rule), sessions (S06 reading list, S08 block), decisions, research/core-contracts.md, zensical.toml nav, both session skills. Open questions: none new. Not designed here on purpose: generator internals and the diagnostic list (S08), the publisher (S18), per-transport renderers (S13, S14), the OPC UA payload codec (S16). Post-merge checks:ci,securityanddocsgreen onmain(the docs build covers the new nav entry and the table fix in the decisions summary). Next session pickup: S07 (design: data layer and SFC rules, depends on S04 and S06, both done). Run/session-start S07. Start frommainafter the S06 merge:git switch main && git pull --ff-only && gh stack init <branch>. Read the Python cross-check rule first: S07 needs a parity list forredis.py(key schema, Lua scripts, DAOs) andsfc_engine.pybefore any decision; S06's list covers only dedupe, the post-start lock and the push-command dispatch.IDedupeStoreandIStateTopicPort(D-37, D-38) are the contracts the Valkey layer (S09) implements. S08 and S07 can run in parallel.
S07 Design: data layer and SFC rules (2026-11-08, branch s07-data-sfc-rules)
Status: done. Previous session post-merge: verified (ci, security, docs green on main for #49).
Done:
- Python cross-check of
redis.py,sfc_engine.py, the recipe model and the publisher's stream use: 30 plus 40 parity rows, none open (data layer, SFC rules). - Data layer decided with a written comparison: hash documents with explicit indexes, no valkey-json or valkey-search; key schema, id-only identity, scope registry, version-guarded idempotent writes, stream trimming rule, nudge plus fallback poll.
- Maintainer decision: Valkey cluster mode is out of scope for good; resilience through independent island deployments (D-12 revised), so no hash tags.
- SFC open points of D-17 and D-18 settled in D-44; both are now decided.
- Compiling stubs:
MicroDCS.Sfc(ActionKey,SfcOutcome,SfcCapabilities,IRecipeInterpreter) andMicroDCS.Valkey(KeySchema) with two test projects. Findings: the Python engine never readsqualifier,timeout_seconds,conditionorrecovery_idle_threshold_s; it reloads the work master at every step (a changed work master changes running jobs); it acks failed work items; the finish script breaks JSON on a quote in the error text. Decisions: D-41 to D-44; D-12 revised; D-17, D-18 decided. Changed in the plan: index (decisions, Q-DATA-1/2/4, Q-ARCH-8, session table), sessions (S09 block), decisions, two new research pages, zensical nav. Open questions: none new. Post-merge checks:ci,securityanddocsgreen onmain; the nine required checks still match the workflow job names (new test projects are covered bydotnet test). Next session pickup: S08 (core, depends on S06), S09 (Valkey layer, depends on S07) and S15 (SFC core, depends on S07) can start in any order. Run/session-start <id>. Start frommainafter the S07 merge:git switch main && git pull --ff-only && gh stack init <branch>. S09 starts fromKeySchemaand the key table in the data layer page; S15 fromMicroDCS.Sfcand D-44 (architecture test: no references); read the parity rows named in each block.
S08 Core: envelope, processor API, hosting, in-memory transport, test harness (2026-11-08, branch s08-core)
Status: done. Previous session post-merge: verified (docs green on main for #50; ci and security green at the time of the check; nine required checks set). CloudEvents checkpoint (D-14): PR #381 still open, 2.9.0 latest.
Done:
CloudEventsEnvelopeCodecon upstream CloudEvents, JSON and binary payload descriptors,[CarriedAs]carry and restore.- New
MicroDCS.Generators(Roslyn): payload descriptors,IDescribedProcessor.Describe(), all handler kinds and lifecycle hooks, MDCS001 to MDCS011. - Hosting:
AddMicroDcs/AddProcessor/AddInMemoryTransport, source-generated options binding, validator, hosted service with the shutdown phases,MinimalPipeline, in-memory transport, dedupe store, state topics. - New
MicroDCS.Testing(ProcessorHarnesswith a fake clock); greetings demo processor inMicroDCS.App;microdcs smokenow also runs the core end to end under Native AOT. - Docs: Processors in .NET with the option table and diagnostics. About 3k lines including tests, above the size rule; kept in one PR at the maintainer's request.
Findings: a generator cannot add
[JsonSerializable]entries (generators do not see each other's output); a carried property also needs[JsonIgnore]; CloudEvents extension names cannot contain hyphens;cond ? memory : nullturns into an emptyReadOnlyMemorythroughbyte[](caught by a test). Acceptance rows and their tests: core contracts. Decisions: revisions of D-35, D-36, D-37, D-39 (no new id). Changed in the plan: decisions, index (session table), core-contracts research,zensical.tomlnav, new docs page. Open questions: none new. Reply routing without a reply address (events intent) is revisited in S10. Post-merge checks:ci,securityanddocsgreen onmain; the nine required checks still match the job names (four new test projects are covered bydotnet test). Next session pickup: S09 (Valkey layer, depends on S07), S10 (pipeline, depends on S08 and S09), S15 (SFC core) and S16 (job models, depends on S05 and S08) can start; recommended next: S09, then S10. S10 replacesMinimalPipeline(keep the order of steps andHostLifecycleTests/GreetingsTestsas regression tests) and takes over the outgoing queue, expiry timers, activity source and fault handling.IDedupeStoreandIStartLeaseare the seams S09 implements. Start frommainafter the merge:git switch main && git pull --ff-only && gh stack init <branch>.
S09 Valkey layer (2026-11-08, branch s09-valkey-layer, stacked on s08-core)
Status: done. Previous session post-merge: S08 (#51) is still open and stacked below; S07 verified (ci, security, docs green on main for #50).
Done:
MicroDCS.Valkey: options (MicroDcs:Valkey) with validator, fail-fast connection,ValkeyContextwith the unknown-outcome write protocol (R-4, operation id in the hash), health check, meter.- DAOs on hash documents with explicit indexes (D-41): job orders, job responses (by-order, by-state), work masters, reference lists, job acceptance. One put script per type; version guard, scope check, change entry, scope registry and nudge in one step.
ValkeyDedupeStoreandValkeyStartLease(replace the in-memory defaults in either registration order).ValkeyStreams(noMAXLEN, acknowledged-only trim, autoclaim with lost-id log and counter),ValkeyPoller(nudge plus fallback poll, scope registry,IStreamConsumer,IPollerTask).SfcStateDaowith the generic compare-and-set script (D-44), due-index, active set, heartbeat, recipe store;ValkeyIndexMaintenance(verify against a scan, rebuild).- 22 integration tests against compose Valkey (including a killed connection, lost replies and nudge versus fallback) and unit tests for options and registration. Docs: Valkey layer in .NET. Parity rows R-1 to R-30: data layer, none open.
Findings: a script must get every key in
KEYS, so the by-state key is built from a state read before the script and checked inside it;RedisValue.TryParsesucceeds for a missing value;CLIENT KILLis a deterministic reconnect test. The Valkey library is not referenced by the App yet, so the AOT publish does not cover it; the AOT analyzers run on its build (IsAotCompatible), and the App gets the wiring in S10. Decisions: none new. Changed in the plan: index (session table), data-layer research (S09 status), new docs page and nav entry. Open questions: none new. Q-DATA-4 / R-4 is answered by the operation-id protocol. Post-merge checks:ci,securityanddocsgreen onmain; the nine required checks still match the job names; composetestscontainer exited with 0 including the new Valkey tests. Next session pickup: S10 (pipeline, depends on S08 and S09) once #51 and this PR are merged:git switch main && git pull --ff-only && gh stack init s10-pipeline. S10 replacesMinimalPipelineand wiresAddMicroDcsValkeyin the App. S15 (SFC core) and S16 (job models) are independent and can start in any order; S19 builds the SFC host onSfcStateDaoandIStreamConsumer.
S10 Pipeline (2026-11-08, branch s10-pipeline, stacked on s09-valkey-layer)
Status: done. Previous session post-merge: S08 (#51) and S09 (#52) are still open and stacked below; S07 verified (ci, security, docs green on main for #50, nine required checks set).
Done:
MessagePipelinereplacesMinimalPipeline: claim, expiry cancel, scope check, dispatch, replies, mark done; outcomesAck/Drop/Fault; error event to the reply address on a drop; a fault stops the host unlessDeliveryProfile.CanRedeliver.- Outgoing events:
IEventPublisheron the pipeline, bounded queue and pump per binding (D-28), type filter, commands carry the instance reply address (ITransport.GetReplyAddress). - Expiry timers (P-12) with
[OnExpired], shutdown phases 3 and 5, oneActivitySourceandMeternamedMicroDCSwith W3C context,MicroDCS.Securitylog category. - App references
MicroDCS.Valkey;microdcs smokehasvalkey-registration; AOT publish clean with StackExchange.Redis. bench/MicroDCS.Benchmarks(R-18); 25 pipeline tests, App smoke tests; docs: pipeline section in Processors in .NET. Parity list S10: core contracts, none open. Findings: Python keys the expiry timer by the command'scausationidwhen set, so a response cannot cancel it (changed, keyed by command id). A timer whose clock delay is created after a yield misses a fake-clock advance (fixed: create it synchronously). AnActivityListeneris process wide, so tests filter by trace id. A subject with an invalid scope segment silently became an address without scope when publishing (fixed, D-38 says unpublishable); the options validator already rejects a queue size above the maximum, so no capping exists. The generator throwsIndexOutOfRangeExceptioninstead of a diagnostic when the[Processor]argument does not resolve (not fixed, see pickup). Decisions: revision of D-37 (no new id). Changed in the plan: decisions (D-37), index (status, OpenTelemetry and R-18 rows), core-contracts research, docs page,.editorconfig(bench/), Dockerfile (restore inputs forbench/), solution. Open questions: none new. Dedupe lease versus slowest handler stays a configuration check for S13. Verified (CI fixes included: Sonar rules indotnet formatand new-code coverage 76 % to above 80 %,bench/excluded from coverage): build 0 warnings, tests green (22 skipped on the host), format clean,CI=true dotnet restore, AOT publish anddocker runsmoke, compose integration tests (testsexited 0),scripts/scanclean. The Sonar quality gate runs on the PR. Post-merge checks:ci,securityanddocsgreen onmain; the nine required checks still match the job names; the Docker image builds with thebench/restore inputs; expiry tests stay stable in CI (no sleeps, fake clock). Next session pickup: S11 (inspection, depends on S09 and S10) or S13 (MQTT transport, depends on S10). Merge #51, #52 and this PR first (gh stack merge), thengit switch main && git pull --ff-only && gh stack init <branch>. S13 implementsITransportincludingGetReplyAddress,CanRedeliver(false: MQTT cannot nack in-session), message workers, subscription names and the$share/renderer. Small fix to take along: the generator crash on an unresolved[Processor]argument should report a diagnostic (add a test inMicroDCS.Generators.Tests).
S11 Inspection layer and MCP read tools (2026-11-08, branch s11-inspection)
Status: done. Previous session post-merge: verified for #54 (ci, security, docs green on main; the nine required checks match the job names). Image build with the bench/ restore inputs is exercised by this session's Docker build.
Done:
MicroDCS.Inspection:InspectionService(result cap, redaction, rate limit, audit line), the management listener (own Kestrel host, off by default, loopbackHostcheck, bearer token off loopback,/health/*),IManagementModule,EventInjector(write group through the pipeline).- Core:
PipelineDecisionLogring buffer (zero size while off), inspectorsapp,processors,pipeline,Management:*options with start-time validation.ValkeyInspectorinMicroDCS.Valkey. MicroDCS.Mcp: ten read tools andinject_event, genericWithTools<T>registration; AOT clean.microdcs serve, smoke checkinspection-mcp, compose profileapp(token secret),.mcp.json,CLAUDE.mdsection, Inspection and MCP.- Generator: unresolved
[Processor]argument reports MDCS012 (pickup item from S10). - 25 new tests (service, listener, MCP contract against the in-memory host, options, ring buffer, Valkey inspector in compose). No Python behaviour replaced;
log_payloadsstays dropped (C-5). Findings: S11 implementation findings (AOT result,RequestDelegateoverload in libraries, optional MCP arguments need= null,aot-publishrewrites the Generators lock file). Decisions: D-19 decided (refinements in its entry); D-40 note. Changed in the plan: decisions, index, inspection research, docs (new page, nav, diagnostics table),CLAUDE.md, compose. Open questions: REST adapter moved to S21 (Q-OPS-1). Verified: build 0 warnings, 206 tests green (24 integration skipped on host), format clean,CI=true dotnet restore, AOT publish (allowlist only) withsmokeanddocker run, compose integration tests (24 passed,testsexited 0),scripts/scanclean,zensical build. The Sonar quality gate runs on the PR. Post-merge checks:ci,security,docsgreen onmain; required checks unchanged;docker compose --profile app up --waitstill starts withMICRODCS_MCP_TOKENset. Next session pickup: S12 (maintenance tools, depends on S11) or S13 (MQTT transport). Rungit switch main && git pull --ff-only && gh stack init <branch>. S12 addsMicroDCS.Maintenanceas anIManagementModule-style dev-only package (plan/apply tokens, environment guard, snapshots; an architecture test keeps it out ofMicroDCS.Inspection). S13 implementsITransport(GetReplyAddress,CanRedeliverfalse) and registers a transport inspector (transport.mqtt). Afterscripts/aot-publish, rungit checkout src/MicroDCS.Generators/packages.lock.json.
S12 Maintenance tools with snapshot and restore (2026-11-08, branch s12-maintenance, stacked on s11-inspection)
Status: done. Previous session post-merge: not verifiable yet, #55 (S11) was still open with green checks when S12 started; the next session checks ci, security and docs on main for both.
Done:
MicroDCS.Maintenance:IMaintenanceOperations(plan dedupe release, plan scope purge, plan restore, apply, list snapshots),ValkeyMaintenance,KeyImage(exact stored text, digest),SnapshotStore(flushed JSON files, retention, size cap),PlanStore(single-use tokens), options with the environment guard at start, optional{prefix}:envmarker (KeySchema.EnvironmentMarker).- Apply: digest recheck, snapshot first (no snapshot, no change), one Lua script with compare-and-delete on document versions; restore as one transaction, refuses on differing content unless
overwrite. MicroDCS.Mcp:maint_dedupe_release_plan,maint_purge_scope_plan,maint_restore_plan,maint_apply,maint_snapshots_list, registered only withMicroDcs:Maintenance:Enabled. Compose profileapp: Development, marker service, snapshots in/tmp..microdcs/git- and docker-ignored.- Tests: 34 unit (guard refusal outside Development/Test, options, plan store, snapshot write/list/retention/size cap/unwritable/path ids, digest, architecture), 3 MCP contract, 8 compose integration (round trips with exact state equality, version mismatch, conflict and overwrite, cap, unwritable directory, marker).
Findings: S12 implementation findings (
booltool arguments need the JSON context entry;HostingEnvironmentis internal). Python parity: none, Python has no maintenance tooling. Decisions: D-45 (new). Open: broker-side purge (S18). Changed in the plan: decisions, index, inspection research,docs/inspection.md(maintenance section and settings table),CLAUDE.md, compose. Verified: build 0 warnings, 251 tests (32 integration skipped on host), compose integration 32 passed 0 skipped, format clean,CI=true dotnet restore, AOT publish (allowlist only) withsmoke, image with profileapprun end to end (plan, apply, snapshot list over MCP),scripts/scanclean,zensical build. Sonar quality gate runs on the PR. Post-merge checks:ci,security,docsgreen onmain; required checks unchanged;docker compose --profile app up --waitstarts withMICRODCS_MCP_TOKENset (thevalkey-markerservice must exit 0). Next session pickup: S13 (MQTT transport, depends on S10) or S15/S16. Rungit switch main && git pull --ff-only && gh stack init <branch>once S11 and S12 are merged; otherwisegh stack checkout s12-maintenanceandgh stack add <branch>. S13 implementsITransport(GetReplyAddress,CanRedeliverfalse) and registers a transport inspector (transport.mqtt). Later sessions add theirmaint_*tools as operations onIMaintenanceOperationswith an integration round trip (D-45). Afterscripts/aot-publish, rungit checkout src/MicroDCS.Generators/packages.lock.json.
S15 SFC core library, in-memory host and explain/dry-run tools (2026-11-08, branch s15-sfc-core)
Status: done. Previous session post-merge: verified (ci, security, docs green on main for #56). Size: about 1.8k lines in MicroDCS.Sfc, 0.7k in the simulation and tool, 1.5k of tests; larger than the size rule because the tools were asked for in the same PR.
Done:
MicroDCS.Sfc(no references, architecture test): recipe model with the Python PascalCase wire names and the IEC 61131-3 names in C# (SfcDivergencewithSequences,SfcActionBlock,SfcActionQualifierwith all 11 qualifiers),SfcGraph.Buildwith 16 validation codes (SFC000toSFC016),SfcInterpreter.EvolveoverSfcStateand the eventsStart,ActionCompleted,ActionFailed,EventReceived,Tick(with outage extension),Resume,Cancel; commandsStartWork,CancelWork; continuationsDefer,Complete,Fault,Cancelled; dispositionsApplied,Absorbed,Rejected.- JSON: source-generated context, explicit wire names, schemas generated from the model into
schemas/sfc_recipe.schema.json(replaces the Python one) andschemas/sfc_state.schema.json; tests regenerate and compare (UPDATE_SCHEMAS=1rewrites). MicroDCS.Sfc.Simulation:SfcSimulation(virtual clock, scripted equipment, transcript, evaluation limit),SfcScriptand its text parser,SfcExplainer,BuildPermissive.MicroDCS.Mcp:sfc_explainandsfc_dryrun, read-only and pure, always registered; checked in the AOT binary over/mcp.- Tests: 145 in
MicroDCS.Sfc.Tests(build codes, step semantics, Python scenarios, simulation, schemas, architecture), 2 MCP contract tests added. Findings: S15 test parity and design details; Python parity: no row open. Decision refinements are recorded as a dated revision of D-44 (PascalCase wire names,Evolve, standard names for divergence, sequence and qualifier,Cancelled, failure rule for the default transition,alwaysbecomestrue). Changed in the plan: decisions, index, sessions, sfc-rules research,docs/sfc-core.md(new, nav entry),docs/inspection.md,CLAUDE.md,sessions.md(transport checklist for S24, S25),compose.test.yaml(Mosquitto password file). Verified: build 0 warnings, 377 tests (32 integration skipped on host), format clean,CI=true dotnet restore, AOT publish (allowlist only) withsmoke,docker buildanddocker runof the image, compose integration 32 passed 0 skipped (tests container exited 0),scripts/scanclean,sfc_dryruncalled on the AOT binary over/mcp,zensical build. The Sonar quality gate runs on the PR. Review: the cloud review found ten issues, all fixed with regression tests (iterative cycle search and size cap,sfc_*tools audited and rate limited, action-name and null checks, selection outcomes per taken sequence,CancelWorkon the last expiry,Roundfor loops, SFC011 independent of parameters, one shared build path). For S20: pull-event selector matching must carry the selector on the event and relax SFC011. Open questions: recipes in the docs useCondition: "always", which is nowtrue(S23 docs port); recipe lookup by hash for the two tools (S19). Post-merge checks:ci,security,docsgreen onmain; required checks unchanged. Next session pickup: S13 (MQTT transport) or S16 (job models); S19 needs S17 as well. Rungit switch main && git pull --ff-only && gh stack init <branch>once S15 is merged, otherwisegh stack checkout s15-sfc-coreandgh stack add <branch>. For S19: the host loadsSfcStateand the graph byRecipeHash(sfc:recipe:{hash}), callsSfcInterpreter.EvolvewithAtfrom the stream entry id, and writesresult.State,result.Commandsandstate.NextDeadlinethrough the generic script; mapSfcFaultandSfcCompleteto the job triggers, and feedSfcSnapshot.LiveWorkfrom the outbox.sfc_explaincan take thevalkey_documentkindsfcstateonce S19 stores states in the core format; a recipe lookup by hash is the follow-up for both tools. Afterscripts/aot-publish, rungit checkout src/MicroDCS.Generators/packages.lock.json.
S13 MQTT transport (2026-11-08, branch s13-mqtt-transport)
Status: done. Previous session post-merge: verified (ci, security, docs green on main for #58; nine required checks set). Size: about 0.9k lines in MicroDCS.Transport.Mqtt, 0.9k of tests.
Done:
MicroDCS.Transport.Mqtt(MQTTnet 5.2, AOT clean):MqttTransport(ITransportandIStateTopicPort),MqttAddressRenderer($share/, discriminator parse),MqttMessageMapper(binary mode, response topic, correlation data, expiry),MqttClientOptionsFactory(credentials, password file, SAT, TLS, client certificate),MqttOptionswith validator, reconnect loop, manual acknowledgement after the pipeline,transport.mqttinspector,AddMqttTransport.servepicks the transport from the bindings; compose profileappruns the greetings demo over Mosquitto; docs pagemqtt-transport.md.- Pipeline fix found by the real transport: a reply to a response message has no destination and faulted the host; now logged and dropped (regression test in
PipelineTests). - Tests: 51 unit (renderer, mapper, validator, client options), 9 compose integration (reply, duplicate and acknowledgement, scope rejection with error event, undecodable, command reply topic, response, retained publish/delete, shared subscription, reconnect by session takeover).
Findings: MQTT transport. Python parity: no row open. A duplicate that arrives while the first is still running waits for the full dedupe lease (T-20, as in Python); not changed.
Decisions: D-46 (new). Open questions: OpenTelemetry row narrowed to NATS.Net and StackExchange.Redis (S14).
Changed in the plan: decisions, index, research,
docs/inspection.md,CLAUDE.md,zensical.toml, compose, solution,Directory.Packages.props. Verified: build 0 warnings, 438 tests (41 integration skipped on the host),docker buildanddocker runof the image, format clean,CI=true dotnet restore, AOT publish (allowlist only) and the AOT binary answered a hello over Mosquitto, compose integration 41 passed 0 skipped (testsexited 0),scripts/scanclean,zensical build. The Sonar quality gate runs on the PR. Post-merge checks:ci,securityanddocsgreen onmain; required checks unchanged; theintegrationjob runs the nine new MQTT tests with 0 skipped. Next session pickup: S14 (NATS transport, depends on S13) or S16 (job models). Rungit switch main && git pull --ff-only && gh stack init <branch>once S13 is merged, otherwisegh stack checkout s13-mqtt-transportandgh stack add <branch>. S14 implementsITransportandIStateTopicPortand compares withMqttTransport: any MQTT leak inITransportbecomes a decision applied to MQTT too.MqttTestHostandMqttTestClientintests/MicroDCS.IntegrationTests/Mqttare the pattern for the NATS tests. Candidate for S19: cap the dedupe claim wait so a duplicate is not held for a whole lease. Afterscripts/aot-publish, rungit checkout src/MicroDCS.Generators/packages.lock.json.
S14 NATS transport (2026-11-08, branch s14-nats-transport, stacked on s13-mqtt-transport)
Status: done. Previous session post-merge: not verifiable yet (#59 was still open, all 13 checks green); the next session verifies it for S13 and S14 together. Size: about 1.1k lines in MicroDCS.Transport.Nats, 1.1k of tests.
Done:
MicroDCS.Transport.Nats(NATS.Net 3.3.2, AOT clean):NatsTransport(ITransportandIStateTopicPort),NatsAddressRenderer,NatsMessageMapper(headers,Nats-TTL,Nats-Msg-Id,Mdcs-Reply-To),NatsClientOptionsFactory(user/password, token, creds, NKey, TLS),NatsOptionswith validator (also the NATS bindings),transport.natsinspector,AddNatsTransport.- JetStream stream per prefix, durable consumer per binding (shared group = shared consumer), inbox replies as plain messages, KV state topics with message TTL, nak and poison termination, consume loop restart after reconnect.
servepicksnatsfrom the bindings; compose servicenatsin the dev stack and the test harness (digest-pinned 2.15, three accounts); CI coverage job starts it. - Tests: 49 unit, 12 compose integration (reply, duplicate, scope, undecodable, command header, inbox, state topics with expiry, shared and unshared consumers, nak and poison, reconnect through a cut TCP proxy, account isolation).
Findings: NATS transport. The abstraction check changed no core interface;
Retainis now documented as an MQTT-only hint (XML docs, D-47). Open: L-7, a faulted message is redelivered while its dedupe claim is held (waits for the 30 s lease): S19 releases the claim on fault and caps the wait. Decisions: D-47 (new). Open questions: OpenTelemetry row narrowed to StackExchange.Redis (owner S21); R-20 answered for NATS. Changed in the plan: decisions, index, research,docs/nats-transport.md,docs/inspection.md,CLAUDE.md,zensical.toml, compose files, CI, solution,Directory.Packages.props. Verified: build 0 warnings, 499 tests (53 integration skipped on the host), format clean,CI=true dotnet restore, AOT publish (allowlist only) and the AOT binary processed a hello over NATS, compose integration 53 passed 0 skipped (testsexited 0),scripts/scanclean,zensical build. The Sonar quality gate runs on the PR. Post-merge checks:ci,securityanddocsgreen onmain; required checks unchanged; theintegrationjob runs the 12 NATS tests with 0 skipped. Next session pickup: S16 (job models and state machine; depends on S05, S08) is the next open session of the critical path. After S13 and S14 are merged, rungit switch main && git pull --ff-only && gh stack init <branch>; otherwisegh stack checkout s14-nats-transportandgh stack add <branch>. S18 (job order publisher) usesIStateTopicPort, implemented by MQTT (retained) and NATS (KV with TTL). S19 takes L-7 (release the dedupe claim on a fault, cap the claim wait). Afterscripts/aot-publish, rungit checkout src/MicroDCS.Generators/packages.lock.json.
M1 review fixes (2026-11-08, branch m1-review-fixes)
Status: done. Previous session post-merge: verified (ci, security, docs green on main for a44b649, which carries S13 and S14; S13's own squash commit got no runs because both PRs merged two seconds apart). Size rule lifted by the maintainer for this PR: about 1.2k lines of code and 1k of tests.
Done:
- Three reviews of
mainafter M1: two cloud reviews scoped through review-only PRs (#62 Core and Valkey, #63 MQTT and NATS, closed unmerged) and a local security review of Inspection, MCP and Maintenance. 24 findings, all fixed: M1 review (RV-1 to RV-24). - Core: queues open before the transports start, every shutdown phase runs after the grace period, foreign cancellations are faults, expiry timers per binding, all-or-nothing enqueue, expiry bound, ambiguous descriptors, in-memory dedupe eviction. L-7 and T-20 done here instead of S19: a fault releases its claim (
IDedupeStore.ReleaseAsync), a duplicate polls at most every second, the Valkeydonemarker has no lease. - Transports: MQTT reconnect loop (failed subscribe, lost wakeup), publish attempt cap, options per reconnect; NATS consumer recreate, reconnect epoch,
MaxMsgs1 withAckProgress, hashed shared consumer name, auth callback; workers survive any exception;ReplyAddressPolicywithAllowedReplyPrefixes(D-48). - Maintenance: fingerprint per target in the delete script, conditions in the restore transaction;
Origincheck on loopback; the dev MCP start command works again. - Credential rotation as a rule for every transport (D-49): the same compose test on MQTT and NATS,
SecretFilein Core. The test found that NATS.Net stops reconnecting for good after two rejected attempts;IgnoreAuthErrorAbortis set. Mosquitto in the harness has a password file now. - Tests: 44 unit and 11 compose tests added (554 from 499); checked that the RV-1, RV-3, RV-4 and RV-6 tests and both RV-17 rotation tests fail without their fix.
Findings: tests not written and why are listed in the research file (RV-10, RV-11, RV-12, RV-15, RV-16). Hardening for S21/S22: plain-HTTP bearer token off loopback, no authentication on loopback in a shared pod network namespace, snapshot file mode.
Decisions: D-48, D-49 (new); D-28, D-46, D-47 revised.
Changed in the plan: decisions, index, research (
m1-review.mdnew,core-contracts.md,mqtt-transport.md,nats-transport.md),docs/processors-dotnet.md(AllowedReplyPrefixes, reply rule),docs/mqtt-transport.md,docs/nats-transport.md,docs/inspection.md,CLAUDE.md. Verified: build 0 warnings, 554 tests (490 passed, 64 integration skipped on the host), compose integration 64 passed 0 skipped (testsexited 0), format clean,CI=true dotnet restore, AOT publish (allowlist only) withsmoke,docker buildanddocker run,scripts/scanclean,zensical build. The Sonar quality gate runs on the PR. Post-merge checks:ci,security,docsgreen onmain; required checks unchanged; theintegrationjob runs 64 tests with 0 skipped. Next session pickup: S16 (job models and state machine; depends on S05, S08). Rungit switch main && git pull --ff-only && gh stack init s16-job-models. S19 no longer needs to fix L-7. New transports (S24, S25) follow the checklist at the top of their session blocks (D-48, D-49). Afterscripts/aot-publish, rungit checkout src/MicroDCS.Generators/packages.lock.json.
S16 Job models and state machine (2026-11-08, branch s16-job-models)
Status: done. Previous session post-merge: verified (ci, security, docs green on main for fecb854, required checks unchanged, integration ran 64 tests with 0 skipped). Size rule lifted by the maintainer for this PR: about 3.4k lines of C# with tests, including the conversions adapted from upstream and the generated table.
Done:
src/MicroDCS.JobControl: neutral records and V1/V2 conversions (adapted fromUA-.NETStandard2.0.1, MIT notice kept), OPC UA JSON verbose codec andOpcUaJsonPayloadDescriptor<T>, required-id validation, runtime JSON Schema, Work Master extension as NodeSet2 modelurn:microdcs:jobcontrolgenerated at build time.- Pure
JobStateMachine.Apply(version, state, operation): V2 from a table generated bytools/MicroDCS.JobControl.TableGenout of the vendored NodeSet (34 transitions, pinning and drift tests), hand-written supplement, execution-system transitions, V1 rules,ReturnStatusbitmap, V2 state array codec with the Prepare sub-states. -
172 unit tests: Python state machine and lifecycle scenarios, upstream vectors, lossless same-version round trips for orders and responses.
JobControlSmokecovers the models in the AOT binary. -
/code-review highon the PR: 7 findings, all fixed with regression tests where a test can show it:BeginExecutionrestarted held orders, Work Master extension lost its inherited fields, stack size limits capped recipes at 64 KB, mismatched state queries matched real orders, explicit defaults were dropped, sub-state browse names had no namespace, one redundant branch. The step is now part of/session-end.
Findings (job models, with the Python parity list, no row open): the V1 spec has no transition table and no V1/V2 state mapping, so the allowed-from sets come from the upstream provider. Bit 2 of ReturnStatus exists only in the V1 table. Sub-state transitions including Close have no effect in the NodeSet and emit nothing, unlike the upstream engine. The AOT binary is 33 MB.
Decisions: D-34 revised (V1 state mapping follows upstream, maintainer choice; CloudEvent type ids of the Work Master).
Changed in the plan: index, decisions, research (job-models.md new), docs/job-control.md (new), zensical.toml, CLAUDE.md, Dockerfile (restore layer copies tools/), ci.yml (Sonar exclusions for tools/ and the conversions), .claude/skills/session-end and the definition of done (code review step).
Open questions: none new. The maintainer action from S05 (ask upstream about publishing the neutral model) stays open.
Verified: build 0 warnings, 726 tests (662 passed, 64 integration skipped on the host), format clean, CI=true dotnet restore, AOT publish (allowlist only) with smoke, docker build and docker run, compose integration 64 passed 0 skipped (tests exited 0; no transport changed), scripts/scan clean, zensical build. The Sonar quality gate runs on the PR. No inspector: the project holds no state.
Post-merge checks: ci, security, docs green on main; required checks unchanged; the integration job runs 64 tests with 0 skipped.
Next session pickup: S17 (Machinery Jobs processor and job DAOs; depends on S10, S16). Run git switch main && git pull --ff-only && gh stack init s17-machinery-jobs. [CloudEventType(Codec = OpcUaJson)] is rejected by MDCS010, so register OpcUaJsonPayloadDescriptor<T> by hand (D-35 allows it) or extend the generator. Start from the parity rows marked "moved to S17" in job models. A store in JobOrderState.Initial means the order does not exist; call JobControlValidation first, because the decoder accepts missing ids. After a NodeSet change, regenerate the table with the command in src/MicroDCS.JobControl/NodeSets/README.md. After scripts/aot-publish, run git checkout src/MicroDCS.Generators/packages.lock.json.
S17 Machinery Jobs processor and job DAOs (2026-11-09, branch s17-machinery-jobs)
Status: done. Previous session post-merge: verified (ci, security, docs green on main for 2beb528, required checks unchanged). Size rule lifted by the maintainer: about 2.9k lines of C# with tests.
Done:
src/MicroDCS.MachineryJobs: 13 Job Control V2 commands, station configuration, status and retained metadata events,IJobStoreover the Valkey DAOs, acceptance check withMicroDcs:JobAcceptance, inspectorjobs.- Wire shapes as data types in the extension NodeSet (model 1.1.0); the generator now emits descriptors for
Codec = OpcUaJson. - Valkey: the order and its
startwork entry in one script, version-guarded removal with a change entry.serveruns the processor when its binding is configured. - MCP
job_orders,job_order,maint_job_delete_plan(new maintenance target kindzsetmember). - Compose: job store, job delete and restore, MQTT end to end (store, start, cancel, configuration, retained metadata).
/code-review high: 10 findings, 8 fixed with regression tests where a test can show it. Not fixed: a command redelivered after another command wrote over the order is applied again (an operation history per order is out of proportion, D-50); stalestartentries are not withdrawn, S19 must re-validate (D-50).
Findings (Machinery Jobs, with the Python parity list, no row open): Python never ran a recipe after Store then Start, never deleted an order, and its publisher handles only Clear. In ProcessorHarness a repeated event id stops at the dedupe layer; a redelivery test needs a store that always claims. Checked that the three redelivery tests fail without their fix.
Decisions: D-50 (new); D-35 revised.
Changed in the plan: index, decisions, research (machinery-jobs.md new, job-models.md), docs/job-control.md, docs/processors-dotnet.md, docs/inspection.md, zensical.toml, CLAUDE.md.
Open questions: none new.
Verified: build 0 warnings, 829 tests (745 passed, 84 integration skipped on the host), format clean, CI=true dotnet restore, AOT publish (allowlist only) with smoke, docker build and docker run, compose integration 84 passed 0 skipped (tests exited 0), scripts/scan clean, zensical build. serve with the binding against Valkey: job_orders and processors_list answered over MCP. The AOT smoke round-trips a StoreCall but does not run the processor. The Sonar quality gate runs on the PR.
Post-merge checks: ci, security, docs green on main; required checks unchanged; the integration job runs 84 tests with 0 skipped.
Next session pickup: S18 (job order publisher and state topics; depends on S13, S17). Run git switch main && git pull --ff-only && gh stack init s18-job-order-publisher. Read the change entries of changes:{scope}, handle both Cancel and Clear as removals (D-50), and add publisher_status, maint_retained_clear and maint_publisher_reset. S19 must re-read the order before acting on a start entry (D-50). After scripts/aot-publish, run git checkout src/MicroDCS.Generators/packages.lock.json.
S18 Job order publisher and state topics (2026-11-09, branch s18-job-order-publisher)
Status: done. Previous session post-merge: verified (ci, security, docs green on main for 13fc09d, required checks unchanged). About 2.4k lines of C# with tests.
Done:
src/MicroDCS.MachineryJobs/Publishing:JobOrderPublisheronIStateTopicPortkeepsstate-index,order/{id}andresult/{id}, on MQTT retained and NATS KV. Poller hand-off, lease, cursors and sequence in Valkey, rebuild from documents.- Roles (Q-ARCH-9): a publisher-only instance connects without subscribing, a clean MQTT session and no NATS command streams.
InMemoryTransportimplements the port. - The processor refuses job order ids outside the segment rule. New wire type
JobOrderStateIndexDataType(extension NodeSet 1.2.0). - Inspector
publisher, MCPpublisher_status,maint_retained_clear_plan,maint_publisher_reset_plan;IStateTopicCatalogin Core. - Compose: split roles over MQTT, failover between two publishers, publisher-only on NATS KV, lease and cursor store, maintenance round trip.
/code-review high: 10 findings, 10 fixed, 9 with a regression test and the MQTT session with a client-options unit test. NATS delete markers now have a 1 h time to live.
Findings (job order publisher, with the Python parity list, no row open): a publish inside the poller would stall the SFC host in S19, because consumers run one after the other and an MQTT publish waits through a reconnect. Python lost a result stored before the job ended and let topics of idle orders run out. RV20_... in the NATS tests failed twice in about seven host runs on its 1 s ack window; widened to 2 s and it passed since.
Decisions: D-51 (new).
Changed in the plan: index, decisions, sessions (S19, S22), research (job-publisher.md new), docs/job-control.md, docs/inspection.md, zensical.toml, CLAUDE.md.
Open questions: Q-ARCH-9 closed.
Verified: build 0 warnings, 885 tests (790 passed, 95 integration skipped on the host), format clean, CI=true dotnet restore, AOT publish (allowlist only) with smoke, docker build and docker run, compose integration 95 passed 0 skipped (tests exited 0), scripts/scan clean, zensical build. The AOT smoke round-trips the state index but does not run the publisher; publisher_status was not called against a running serve. The Sonar quality gate runs on the PR.
Post-merge checks: ci, security, docs green on main; required checks unchanged; the integration job runs 95 tests with 0 skipped.
Next session pickup: S19 (SFC Valkey host; depends on S09, S15, S17). Run git switch main && git pull --ff-only && gh stack init s19-sfc-valkey-host. Run the SFC host under the Processor role only (D-51). Re-read the order before acting on a start entry (D-50). Register work consumers with ValkeyPoller, which runs them one after the other: a consumer must not wait on a broker inside PollAsync, see the hand-off in JobOrderPublisher. After scripts/aot-publish, run git checkout src/MicroDCS.Generators/packages.lock.json.
S19 SFC Valkey host (2026-11-09, branch s19-sfc-valkey-host)
Status: done. Previous session post-merge: the S18 runs on main first failed with zero jobs in a GitHub Actions outage, a re-run passed (ci, security, docs green for 6c37991), required checks unchanged. Scope lifted by the maintainer (full tool sketch): about 3.2k lines of C# with tests.
Done:
src/MicroDCS.Sfc.Hosting: work consumer in the poller, compare-and-set of state, due-index and work entries, push dispatch, response path, deadline sweep with the outage rule,XAUTOCLAIMand dead stream, Processor role only,MicroDcs:Sfcoptions.- Core:
MessagePipelineimplementsIOutgoingCommands;IActionSinktakes the response with its outcome;[Outgoing(Outcomes = ...)],ActionOutcomefrom[OnResponse].JobExecutionin Machinery Jobs applies Begin, Complete (with job response) and Abort. SFC state format 1.1 (startedAt). - MES Abort and Stop write a
cancelentry in the order script. Python left the recipe running. - Inspector
sfc; MCPsfc_job,sfc_due,sfc_work_pending, write groupsfc_tick,sfc_redispatch,sfc_abort, maintenancemaint_sfc_job_delete_plan,maint_sfc_rebuild_due_index_plan,maint_stream_requeue_plan,maint_stream_ack_plan. /code-review high: 10 findings, 10 fixed, 7 with a regression test (three are docs, a doc comment and the delivery count, whose test covers the new helper only).
Findings (SFC host, with the Python parity list, no row open): the time of start cannot be the entry id, an order that waited in the stream lost its attempt without a command (found by compose under load). A tick that finds nothing must put the job back on the due-index. Pause and Resume still do not hold the recipe, and it can complete a job the MES paused (S20a).
Decisions: D-52 (new); D-35 and D-44 revised.
Changed in the plan: index, decisions, sessions (S20a and S27 new), research (sfc-host.md new, sfc-rules.md), docs/sfc-core.md, docs/inspection.md, docs/processors-dotnet.md, zensical.toml, CLAUDE.md, /session-end milestone range.
Open questions: none new.
Verified: build 0 warnings, 940 tests (821 passed, 119 integration skipped on the host), format clean, CI=true dotnet restore, AOT publish (allowlist only) with smoke, docker build and docker run, compose integration 119 passed 0 skipped (tests exited 0), scripts/scan clean, zensical build. serve from the AOT binary against Valkey: the eight sfc_* tools listed, sfc_job answered over MCP. The AOT smoke does not run a recipe; the end-to-end run is the compose test on the JIT build. The Sonar quality gate runs on the PR.
Post-merge checks: ci, security, docs green on main; required checks unchanged; the integration job runs 119 tests with 0 skipped.
Next session pickup: S20 (SFC pull events, recovery, multi-instance; depends on S19). Run git switch main && git pull --ff-only && gh stack init s20-sfc-recovery. Turn on SfcFeature.PullEvent in SfcGraphs.CapabilitiesOf and implement SfcActionSink.EventReceivedAsync. resume entries and XAUTOCLAIM already work; the start-up recovery scan does not exist yet. S20 runs two replicas in compose. After scripts/aot-publish, run git checkout src/MicroDCS.Generators/packages.lock.json.
S20 SFC pull events, recovery, multi-instance (2026-11-09, branch s20-sfc-recovery)
Status: done. Previous session post-merge: verified (ci, security, docs green for 98faaf4, required checks unchanged, the tests run uses --fail-skips on).
Done:
- Pull events:
Selectorin the recipe,SfcFeature.PullEventon,[Incoming(Outcomes = ...)],eventwork entry, waits index, one event one job. - Recovery scan per scope on first sight; it resends nothing with a deadline and repairs lost index entries. Maintenance snapshots, restores and deletes include the waits index.
- Replica tests with two real containers of the AOT image, SIGKILL through the Docker API: kill mid-job, takeover by a later replica, outage longer than a deadline, sweep latency (
scripts/replica-tests, in the CIintegrationjob). - Inspector kind
sfc.waits, MCPsfc_waits.sfc_dryrununderstands selectors. /code-review high: 10 findings, 10 fixed, 5 with a regression test. The others need 1000+ jobs or a timed race or fault: scan failure, stale-entry race, wait-check fault, contention, the candidate cap.
Findings (SFC host, with the Python parity list, no row open): resending on restart, as Python did, uses up MaxAttempts and races the outage tick, so the sweep owns every action with a deadline. Sweep latency with the default 1 s interval and two replicas: p50 662 and 730 ms, max 1009 and 1018 ms. A processor whose handler answers its own type without a reply address feeds itself through its events address; the greetings demo does (docs/sfc-core.md).
Not covered: a real kill of a consumer that holds an unacknowledged entry (the XAUTOCLAIM path has the in-process S19 test). Each event offered to {jobId} selectors reads the waiting jobs one by one; no index on selector values.
Decisions: D-53 (new); D-44 and D-52 revised.
Changed in the plan: index, decisions, research (sfc-host.md, sfc-rules.md, data-layer.md), docs/sfc-core.md, docs/inspection.md, docs/processors-dotnet.md, CLAUDE.md.
Open questions: none new.
Verified: build 0 warnings, 963 tests (830 passed, 133 integration skipped on the host), format clean, CI=true dotnet restore, AOT publish (allowlist only) with smoke, docker build and docker run, compose integration 129 passed 0 skipped (tests exited 0), scripts/replica-tests 4 passed 0 skipped, scripts/scan clean, zensical build. The Sonar quality gate runs on the PR.
Post-merge checks: ci, security, docs green on main; required checks unchanged; the integration job runs the main tests with 0 skipped and the 4 replica tests.
Next session pickup: S20a (SFC hold and release; depends on S20). Run git switch main && git pull --ff-only && gh stack init s20a-sfc-hold. Read the S20a block, D-52, D-18 and docs/porting/research/sfc-host.md. A pull-event wait held by a pause must not time out; decide how an event that arrives during a hold is treated, since the core records responses but S20 drops events with no wait. After scripts/aot-publish, run git checkout src/MicroDCS.Generators/packages.lock.json. Nothing to prepare.
S20a SFC hold and release (2026-11-10, branch s20a-sfc-hold)
Status: done. Previous session post-merge: docs and security green for dfaf25e, required checks unchanged; ci failed on locked restore because setup-dotnet followed rollForward: latestPatch to SDK 10.0.404, whose implicit ILLink and ILCompiler references (10.0.15) contradict the lock files (10.0.14). Fixed here: global.json pins the patch (rollForward: patch, maintainer's choice), docs/development.md updated.
Done:
- Core:
SfcHoldandSfcRelease,heldAtin state format 1.2 (schema regenerated; every write now carries the current minor). While held nothing fires or is dispatched, responses, failures and events for work already out are recorded (maintainer chose to record events), deadlines do not expire. Release moves open deadlines by the hold and re-issues open push attempts with their command id. - Order script writes
holdon entry into Interrupted (Pause, internal Hold) andreleaseon Interrupted to Running. The consumer aligns the recipe with the order and repeats until both agree, with server time. A dispatch of a held job is not sent; asent:{action}marker in the state hash, written after the command is queued, sends each command id once.sfc_jobshowsheldSince,sfc_explainsays held. /code-review high: 10 findings, 10 fixed, 7 with a regression test (a crash between queue and marker is not simulated; the other two are docs and dead code).
Findings (SFC host, parity row H-19 changed, no row open): a hold entry cannot decide by its name, because two consumers can take Pause and Resume entries in either order; the in-process routing map cannot tell which commands another instance sent, hence the marker in the state hash. A redelivered dispatch entry no longer resends a marked command.
Decisions: D-52 revised.
Changed in the plan: index, decisions, research (sfc-host.md), docs/sfc-core.md, docs/inspection.md, docs/development.md.
Open questions: none new.
Verified: build 0 warnings, 985 tests (848 passed, 137 integration skipped on the host), format clean, CI=true dotnet restore, AOT publish (allowlist only) with smoke, docker build and docker run, compose integration 133 passed 0 skipped (tests exited 0), scripts/replica-tests 4 passed, scripts/scan clean, zensical build. The Sonar quality gate runs on the PR.
Post-merge checks: ci (with setup-dotnet installing exactly 10.0.403), security, docs green on main; required checks unchanged; the integration job runs the main tests with 0 skipped and the 4 replica tests.
Next session pickup: S21 (HTTP surface and observability; depends on S17). Run git switch main && git pull --ff-only && gh stack init s21-http-surface. Read the S21 block, Q-OPS-1, D-19 and D-40. An SDK patch upgrade is now a deliberate change of global.json, the lock files (CI=true dotnet restore after dotnet restore --force-evaluate) and the Dockerfile SDK digest together. After scripts/aot-publish, run git checkout src/MicroDCS.Generators/packages.lock.json. Nothing to prepare.
S21 HTTP surface and observability (2026-11-10, branch s21-http-surface)
Status: done. Previous session post-merge: verified (ci with integration and 4 replica tests, security, docs green for 0da761c; required checks unchanged).
Done:
MicroDCS.Telemetry: OTLP only (maintainer's choice, no/metrics), on when an OTLP endpoint is set; sources and metersMicroDCS*,NATS.Net, runtime and ASP.NET Core;service.instance.idfrom the options. Registered beforeAddMicroDcsso the providers listen before the transports deliver.- Spans: producer
send {type}(traceparent names it; dispatched SFC commands had no trace context), consumer span with transport and subscription (InboundMessage.Subscription),sfc {action}per work entry withmicrodcs.sfc.work.duration; refused publishes marked as errors; probes not recorded. - Readiness:
transportsandvalkeychecks (idempotentAddMicroDcsHealthCheck). It always answered ready before: no check was registered, and it looked in the listener's own container. - REST (read-only,
ApiEnabled, off by default):/api/v1/jobs,/api/v1/jobs/{id},/api/v1/openapi.json(checked in atsrc/MicroDCS.Inspection/openapi.json, test compares routes); rate-limit window per caller, one token per request. /code-review high: 10 findings, 10 fixed, 7 with a regression test (registration order, SFC cancel outcome and the skipped change scan are not shown by a test).
Findings (research, parity rows O-1 to O-9, none open): StackExchange.Redis 3.4 has no ActivitySource or Meter and the contrib instrumentation is not stable, so no Valkey spans. AddHealthChecks adds a second hosted service (HealthCheckPublisherHostedService); code that took the single hosted service now picks MicroDcsHostedService.
Decisions: D-54 new, D-40 revised.
Changed in the plan: index (D-54, Q-OPS-1 and OpenTelemetry rows closed, S21 done), decisions, research (http-observability.md), docs/observability.md (new), docs/inspection.md, docs/development.md, compose.yaml (profile app: API on, OTLP to the Aspire dashboard), CLAUDE.md.
Open questions: none new.
Verified: build 0 warnings, 1007 tests (870 passed, 137 integration skipped on the host), format clean, CI=true dotnet restore, AOT publish (allowlist only) with smoke (new telemetry check, REST in inspection-mcp), docker build and docker run, compose integration 133 passed 0 skipped, scripts/replica-tests 4 passed, scripts/scan clean, zensical build; live serve with Valkey: REST routes, OTLP traces, metrics and logs reached a local receiver, readiness 503 not ready: valkey with Valkey stopped and back to ready after.
Post-merge checks: ci, security, docs green on main; required checks unchanged; the integration job runs 133 tests with 0 skipped and the 4 replica tests.
Next session pickup: S22 (container, Kubernetes, supply chain, security review; depends on S21). Run git switch main && git pull --ff-only && gh stack init s22-k8s-security. Read the S22 block, D-51, D-32, D-20, D-54 and HTTP surface and observability. Probes use /health/live and /health/ready on the management listener; a pod-address listener needs BearerTokenFile (probes stay open) and ApiEnabled only if the MES queries jobs. docs/security.md is still the Python page and must cover the management listener, its bearer token and the REST audit log. Nothing to prepare.
S22 Container, Kubernetes, supply chain, security review (2026-11-11, branch s22-k8s-security)
Status: done. Previous session post-merge: verified (ci, security, docs green for #71 on main; required checks unchanged).
Done:
- Image publishing (D-55):
publishandpublish-manifestinci.ymlpushghcr.io/aschamberger/microdcsonmainandv*tags, cosign keyless signatures per platform and on the index, two CycloneDX attestations per platform (scripts/scan sbom: app lock file and OS packages), verification against the workflow identity. Release builds use no caches. - Manifest:
deploy/config.yaml,deploy/k8s.yaml(two Deployments by role, probes, PDB, three NetworkPolicies, secrets as files),deploy/kustomization.yaml(image pin,Alwayspull while it is themaintag),deploy/valkey-island-acl.conf(single source for docs, tests and kind). Sonar S6870/S6897 fixed. scripts/k8s-testwithtests/k8s: kind 0.33 / Kubernetes 1.37, Pod Securityrestricted, egress and ingress probes, token, Valkey ACL log empty; CI jobkubernetes. kind, kubectl, kubeconform and cosign installed locally with checksums.- Valkey credentials (Python parity):
User,PasswordFile(read before every connect, empty file refused),TlsCaFile,RequireTls; no tiebreaker, configuration channel,INFO/CONFIG/CLUSTER; default server version 7.2. Harnessvalkey-acl: the Valkey tests run as the island ACL user, RV17 rotation test, 19 isolation cases. - Docs:
docs/security.mdrewritten (trust model, TLS, ACLs, management listener, verification, zones and conduits, IEC 62443),docs/deployment.mdnew (R-12 limits, R-20 tenancy, R-21 support, not-verified list),SECURITY.mdscope. Management listener: ASP.NET Core logs at Warning by default (ManagementLoggerFactory), inheritedHTTP_PORTScleared. /code-review high: 10 findings, 9 fixed (ACL rule allowed FLUSHALL/CLIENT KILL/KEYS across islands, empty password file skipped AUTH, pre-release tags movedlatest, mutable tag withoutAlways, test env leaks, assumed server version 6.0, logging default at the wrong altitude, ACL rule in three copies); not fixed: publisher Deployment duplicates the processor (kept readable as an example; the kind test enforces the hardening on both).
Findings: deployment and security review (parity rows K-1 to K-19, none open; the "Done in the session" behaviours checked). A rejected AUTH on a Valkey with a passwordless default user falls back silently, so credential tests need valkey-acl. The SFC host needs TIME. scripts/aot-publish rewrites src/MicroDCS.Generators/packages.lock.json (adds the RID), which breaks the locked restore in Docker: reverted here, S22a fixes it.
Decisions: D-55 new, D-49 revised (Valkey).
Changed in the plan: index (D-55, Q-OPS-2, R-12, R-20, R-21 closed, Q-OPS-3 to S24/S25, S22 done, S22a new), sessions (S22a block), decisions, research (deployment-security.md), docs/security.md, docs/deployment.md (new), docs/development.md, docs/valkey-layer.md, CLAUDE.md, compose.test.yaml (valkey-acl).
Open questions: none new. Sonar security hotspots to review by the maintainer: plain-HTTP management URL (deploy/config.yaml, known gap in docs/security.md), HTTP to pod IPs in scripts/k8s-test (test cluster).
Verified: build 0 warnings, 1039 tests (882 passed, 157 integration skipped on the host), format clean, CI=true dotnet restore, AOT publish (allowlist only) with smoke, docker run of the image, compose integration 153 passed 0 skipped, scripts/replica-tests 4 passed, scripts/k8s-test from scratch (14 checks, 170 s), scripts/scan and scripts/scan image clean, zizmor pedantic down to 2 informational, zensical build without warnings. Not verifiable before the merge: the publish jobs (they run only on pushes).
Post-merge checks: ci green on main including publish (linux-x64), publish (linux-arm64) and publish-manifest, whose summary names the index digest; cosign verify of that digest with the identity in docs/security.md passes from a workstation; the GHCR package microdcs is public (set it once in the package settings if not); security, docs green; required checks include kubernetes.
Next session pickup: S22a (CI runtime review; depends on S22). Run git switch main && git pull --ff-only && gh stack init s22a-ci-runtime. Read the S22a block, D-20, D-32, D-55 and .github/workflows/. First measure: gh run list --branch main --workflow ci --limit 10 and gh run view <id> --json jobs for wall time per job. Also fix the scripts/aot-publish lock-file side effect. Nothing to prepare.
S22a CI runtime review (2026-11-12, branch s22a-ci-runtime, PR #73)
Status: done. Previous session post-merge: verified (ci green on main with publish and publish-manifest; cosign verify of index sha256:5f47df5f… and both platform digests passes from a workstation; GHCR package public, anonymous pull works; security, docs green; required checks include kubernetes).
Done:
- Measured eight runs (CI runtime): PR critical path 8 to 11 min, integration suite twice (about 5 min each), runtime image built five times. Cause of the 5 min: nine
SfcHostTestswaited out a 30 s command expiry in shutdown phase 5 (D-37) because the rig stopped withCancellationToken.None; a 1 s grace period takes the suite from 6:26 to 35 s. - Integration suite once, on the host in
dotnetwith coverage and--fail-skips on(D-15 revised);integrationruns the replica tests.scripts/ci-changeswith the path rules (D-56): docs-only PRs build and analyse only;container (linux-arm64)(D-32 revised) andkuberneteson PRs only for image, manifest or option changes, always onmain, tags and a dailycischedule. scripts/aot-publishrestores without a RID, then publishes--no-restore(reproduced the Generators lock-file change onmain; fixed); unsupported RIDs are refused.- After: full run 4.4 min, documentation-only run 2.5 min (probe PR #74, closed). Required check
changesadded (11 checks). /code-review high: 9 findings, 6 fixed (fail-open change detection, kind waiting on the arm64 leg and a one-day artifact so the image sharing was dropped, RID check, rig dispose infinally, stale rule and comment texts), 1 partly (kind trigger widened to option classes and registrations, not all ofsrc/); not fixed: the composetestsentrypoint no longer runs in CI (maintainer's host-only choice, recorded in D-15), full clone intrivy-image(seconds on this repository).
Findings: a test host stopped without a grace period waits for every open command's expiry. Skipped non-matrix jobs report success for required checks; a matrix job skipped at job level reports under its unexpanded name, so container skips its steps. Six PR runs from the 2026-11-11 Actions outage cannot be cancelled or deleted ("not queued yet"); a new push started fresh runs.
Decisions: D-56 new; D-15 and D-32 revised.
Changed in the plan: index (D-15, D-32, D-56, next free id D-57, S22a done, S27 moved into M2 before S23 at the maintainer's request), sessions (S27 block moved), decisions, research (ci-runtime.md new), docs/development.md (CI section), docs/deployment.md, CLAUDE.md, .claude/rules/compose-ci.md and tests.md, compose.test.yaml header.
Verified: build 0 warnings, 1039 tests (882 passed, 157 integration skipped on the host), whole solution with the CI flags against the harness 1035 passed 0 skipped, format clean, CI=true dotnet restore, AOT publish (allowlist only) with smoke and no lock-file change, scripts/scan clean, actionlint and zizmor clean, zensical build clean, PR #73 all checks green (image build, docker run smoke, replica tests, kind, Sonar gate). Not run locally: the compose tests container (the harness ports were handed to a parallel S27 session; the same suite ran green in CI).
Open questions: none new.
Post-merge checks: ci green on main with all jobs (push runs everything) including publish and publish-manifest; the first scheduled ci run (04:41 UTC) runs both container legs and kubernetes and skips dotnet and integration; required checks are the eleven listed in D-56 (changes included); security and docs green.
Next session pickup: S27 (SFC equipment hold; depends on S20a). If S27 was started in parallel in a worktree on top of s22a-ci-runtime, rebase it onto main after this merge (git rebase --onto main s22a-ci-runtime) and resolve the session table and log; otherwise git switch main && git pull --ff-only && gh stack init s27-sfc-equipment-hold. Read the S27 block, D-52, D-17, D-44, the S20a log entry and docs/sfc-core.md. S27 takes decision ids from D-57. Nothing to prepare.
S27 SFC equipment hold (2026-11-12, branch s27-sfc-equipment-hold)
Status: done. Previous session post-merge: verified (ci, docs, security green on main for 8f37d70; required checks are the eleven of D-56). Not verifiable yet: the first scheduled ci run (04:41 UTC on 2026-11-13); S23 checks it.
Done:
- Recipe format v1.1.0 (
dataschema .../sfc-recipe/v1.1.0/, v1.0.0 still read): per push actionHoldandRestart, build errors SFC019 and SFC020, featureEquipmentHoldwith the settingMicroDcs:Sfc:EquipmentHold(defaulttrue), schemas regenerated, state format 1.3. - Core: a pause sends hold commands (
{actionKey}#{n}:hold, own deadline that runs while held).heldends the attempt and the release restarts the work, once and without using an attempt. Any other answer falls back to S20a. Answers are matched by the command id the host now passes. Host: own sent markersent:{action}:hold; a hold of work that never went out is answered by the host. Inspector and explainer show the hold. - Compose tests with a test equipment processor: hold and restart to the end of the job, a refused hold, a hold of unsent work, a v1.0.0 recipe with hold logic aborted.
/code-review high: 9 findings. 7 fixed (MCP tools refused hold recipes, a lateheldleft the equipment without a restart, a v1.0.0 recipe could carry hold logic, a hold or restart answer type was accepted outside its command, the resume path was duplicated,!without a comment,CapabilitiesOfdefaulted the flag on), 4 of them with a regression test. 1 accepted: the hold check against an in-flight dispatch (a few ms window falls back to not held, the safe side). 1 not changed: the two:holdsuffixes are a command id and a hash field, different things.
Findings (SFC host, parity row H-29: Python has no hold logic, no row open): the host always knows the command id of a response (causation id), which is enough to tell a hold answer from the work's answer without a wire change.
Decisions: D-57 new; D-52 revised.
Changed in the plan: index (D-57, next free id D-58, S27 done, next session S23), decisions, research (sfc-host.md), docs/sfc-core.md (Equipment hold section, build errors, settings), docs/inspection.md.
Verified: build 0 warnings, 1067 tests (906 passed, 161 integration skipped on the host), format clean, CI=true dotnet restore, AOT publish (allowlist only) with smoke, docker build and docker run, compose integration 157 passed 0 skipped (tests exited 0), scripts/replica-tests 4 passed, scripts/scan clean, zensical build clean. The Sonar quality gate runs on the PR.
Open questions: none new.
Post-merge checks: ci green on main with all jobs including publish and publish-manifest; security and docs green; required checks unchanged (eleven, D-56); the scheduled ci run of 2026-11-13 runs both container legs and kubernetes.
Next session pickup: S23 (docs port, parity acceptance, release; all of M2 is done). Run git switch main && git pull --ff-only && gh stack init s23-parity-release. Read the S23 block, Q-TEST-1, Q-TEST-2, Q-TEST-3 and the "Not yet ported" pages (docs/sfc_engine.md and docs/information-model-standards.md still name recipe v1.0.0; the current one is v1.1.0, D-57). Nothing to prepare.