The state-hub resolver was calling GET /sbom/status?repo={slug}, which State
Hub does not expose. Real SBOM routes are /sbom/, /sbom/{slug},
/sbom/snapshots/, /sbom/snapshots/{id}, /sbom/ingest/, /sbom/report/licences/.
The weekly-sbom-staleness ActivityDefinition was passing params {repos: all}
and the resolver was reading params.get("repo_slug", ""), so the URL
collapsed to /sbom/status?repo= and 404'd. _fetch_json swallowed the error,
the rule context.repos.sbom_age_days > 30 evaluated against {} and never
matched, and the weekly SBOM check has been a silent no-op for as long as
the route mismatch has existed.
Resolver now supports two modes selected by params:
- single-repo: {repo_slug: foo} → GET /sbom/{foo}, returns
{repo_slug, last_sbom_at, sbom_age_days, has_sbom}
- bulk: {repos: all} → GET /repos/, computes per-repo age, returns the
worst repo's fields hoisted to the top of the result alongside
stale_count, total_count, worst_* fields, and the full per-repo list
Never-scanned repos get a 99999 sentinel age so threshold rules treat
them as very stale without forcing the rule to special-case None.
Hoisting the worst entry to the top preserves the existing rule
expression context.repos.sbom_age_days > 30 (and target_repo:
context.repos.repo_slug, though that field is a separate interpolation
gap tracked as ADHOC-2026-06-01-T02). The integration tests'
aspirational per-repo iteration model is left intact.
Live validation against State Hub on 2026-06-01:
- single: activity-core → 36 days since 2026-04-26 ingest
- bulk: 48 repos total, 46 stale (>30d), worst is info-tech-canon (never
scanned), rule expression evaluates True
Tests: 120 passed, 1 skipped.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Surfaced while bringing up the dev worker for the CUST-WP-0045 T06 cutover.
weekly-sbom-staleness fires its state-hub resolver with query
repo_sbom_status, which hits GET /sbom/status?repo=. State Hub does not
expose that route, so _fetch_json returns {} and the rule
context.repos.sbom_age_days > 30 silently no-ops. The weekly SBOM check has
been a no-op for as long as the route mismatch has existed. Logged as a
low-priority adhoc rather than promoting to a workplan because the resolver
and definition both need a one-line decision (single-repo vs fan-out), not
multi-phase design.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
All 12 tasks unblocked (broker decision resolved: NATS + JetStream).
Work interrupted on workstation due to WSL2 Docker pull issues.
Note captures build order, file names, key design decisions, and
state hub IDs for seamless pickup on CoulombCore.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
activities.py — persist_task_instance (new):
Idempotent INSERT ... ON CONFLICT (id) DO NOTHING on task_instances.
task_id passed in from workflow (derived from workflow_id via uuid5).
Registered on task-execution-tq.
workflows.py — TaskExecutorWorkflow (T19):
Derives stable task_id = uuid5(NAMESPACE_URL, workflow_id).
Calls persist_task_instance → status=done, returns immediately.
Real execution logic to replace stub in a later workstream.
worker.py — T20:
Registers persist_task_instance on task-execution-tq Worker.
Both queues fully wired: orchestrator-tq and task-execution-tq.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Inserts an ActivityRun row via the shared session factory.
Accepts run_payload dict with activity_id, scheduled_for (ISO-8601 or
None), context_snapshot, tasks_spawned, version_used.
Returns run_id as a str UUID.
fired_at is set server-side to now(UTC).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
activities.py:
- init_session_factory(url): module-level async_sessionmaker init,
called once from worker.py before workers start
- load_activity_definition(activity_id): queries activity_definitions
by UUID, returns JSON-serialisable dict; raises ApplicationError
(non_retryable=True) if row not found
worker.py:
- reads ACTCORE_DB_URL at startup, fails fast if missing
- calls init_session_factory() before connecting to Temporal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
src/activity_core/seed.py: inserts one example ActivityDefinition
('example-heartbeat', cron every minute, static context source,
log_message task template). Idempotent — skips by name on re-run.
Run with:
ACTCORE_DB_URL=postgresql+asyncpg://actcore:actcore@localhost:5433/actcore \
python -m activity_core.seed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prefix all image names with docker.io/ to avoid registry ambiguity
on hosts where containerd/Podman default to docker.io but the pull
fails without an explicit registry prefix.
Also marks T07 (smoke-test Temporal cluster and UI) as done in the
workplan now that the stack boots cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CLAUDE.md: read .custodian-brief.md as Step 1 (offline-safe orientation
before MCP call); matches pattern now standard across all domain repos
- T07 (Smoke test Temporal): remove stale Docker TLS blocking_reason;
status → todo (WSL2 MTU issue resolved by implementing on CoulombCore)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>