Files
the-custodian/workplans/CUST-WP-0045-activity-core-daily-triage-runner.md
2026-06-05 13:11:41 +02:00

721 lines
30 KiB
Markdown

---
id: CUST-WP-0045
type: workplan
title: "Activity-Core Daily Triage Runner Cutover"
domain: custodian
repo: the-custodian
status: finished
owner: custodian
topic_slug: custodian
planning_priority: high
planning_order: 45
created: "2026-05-19"
updated: "2026-06-04"
state_hub_workstream_id: "d9d9a3ec-f736-4041-beac-bb92c7ad314e"
---
# CUST-WP-0045 - Activity-Core Daily Triage Runner Cutover
## Goal
Move the Daily State Hub WSJF Triage runner from the Codex app automation
substrate to owned activity-core infrastructure.
The outcome should be a reliable daily run at 07:20 Europe/Berlin that produces
the same review artifact promised by `CUST-WP-0044`: a dated working-memory
note, a State Hub `daily_triage` progress event, and an auditable activity-core
run record.
## Context
On 2026-05-19 the Codex app automation fired at the scheduled time, but did not
complete a useful run:
- two `Daily State Hub WSJF Triage` sessions were created at 07:20 Europe/Berlin
- both session files contained only session metadata
- no prompt execution, report, tool call, working-memory note, or final answer
was recorded
- State Hub had no `daily_triage` progress event for that date
- the recorded session cwd values used Windows-style `C:\home\worsch\...`
paths rather than the intended WSL paths
This shows the schedule is present but the launch substrate is not trustworthy
enough for an unattended Custodian operating habit.
activity-core already provides the pieces that should own this class of work:
- Temporal cron schedules with timezone and misfire-policy handling
- `ActivityDefinition` markdown ingestion via `ACTIVITY_DEFINITION_DIRS`
- `state-hub` context resolver hooks
- ActivityRun logging and Temporal workflow history
- rule/instruction model design in `ACT-ADR-003`
- deployment/runbook paths for the Railiance environment
The missing work is to connect those existing capabilities to this judgement
report use case without building a second scheduler or a parallel priority
database.
## Scope
In scope:
- Extend activity-core so the existing daily triage ActivityDefinition can run
as the primary scheduler.
- Reuse the existing prompt at
`runtime/prompts/daily_statehub_wsgi_triage.md`.
- Reuse the existing ActivityDefinition at
`activity-definitions/daily-statehub-wsjf-triage.md`.
- Extend activity-core's State Hub context resolver for the queries this
report already needs.
- Add or finish the instruction/report execution path described by activity-core
ADR-003.
- Write the report to Custodian working memory and log `event_type:
daily_triage` in State Hub.
- Disable the Codex app automation after activity-core is validated, so there
is only one daily runner.
Out of scope:
- Rewriting the WSJF rubric or report template; that belongs to `CUST-WP-0044`.
- Creating a new scheduler, cron daemon, or separate automation database.
- Automatically changing workplan status, priority, canon, secrets, deployment,
or external commitments from the daily report.
- Retiring the workstation fallback or deploying HA activity-core before the
relevant Railiance deployment work is approved.
## Runner Decision
Primary target runner: activity-core Temporal schedule.
Temporary fallback runner: Codex app automation, only until activity-core has
completed a manual run and at least one scheduled canary run.
Cutover rule: do not enable both runners at the same time. The handoff is:
1. Activity-core definition remains disabled while the Codex automation is the
only runner.
2. Activity-core is validated with a manual trigger using the same definition.
3. Codex automation is paused.
4. Activity-core definition is enabled and schedules are synced.
5. The next scheduled run is checked for a working-memory note, State Hub
progress event, and ActivityRun row.
## Tasks
### T01 - Capture Failure Evidence And Runner Boundary
```task
id: CUST-WP-0045-T01
status: done
priority: high
state_hub_task_id: "01f57ed4-0473-42bf-b61c-0491f7ac7e2c"
```
Record the 2026-05-19 failed automation evidence in the implementation notes
for this workplan and, if useful, in the CUST-WP-0044 calibration notes.
Confirm the desired runner boundary:
- activity-core owns schedule, retries, run log, and context resolution
- State Hub remains the read model and progress sink
- the-custodian owns the prompt, report template, and governance guardrails
- Codex app automation is a temporary fallback only
Done when the failure mode and cutover target are explicit enough that future
agents do not try to fix this by adding another local cron path.
### T02 - Extend Activity-Core State Hub Context Resolver
```task
id: CUST-WP-0045-T02
status: done
priority: high
depends_on: [CUST-WP-0045-T01]
state_hub_task_id: "c4303b24-6f6b-445e-8e2e-94441589a7f2"
```
Extend activity-core's existing `state-hub` context resolver instead of adding
bespoke HTTP fetch logic to the Custodian repo.
Required queries:
- `state_summary` -> `GET /state/summary`
- `next_steps` -> `GET /state/next_steps`
- `workplan_index` -> `GET /workstreams/workplan-index`
- `hub_inbox` -> `GET /messages/?to_agent=hub&unread_only=true`
The resolver should keep the existing `STATE_HUB_URL` configuration pattern,
use bounded timeouts, and return `{}` on resolver failure so the workflow can
still fall back to the offline brief/prompt contract.
Done when activity-core tests cover all four new query names and the existing
`domain_summary` and `repo_sbom_status` behavior remains intact.
### T03 - Implement Instruction Report Execution
```task
id: CUST-WP-0045-T03
status: done
priority: high
depends_on: [CUST-WP-0045-T02]
state_hub_task_id: "e766ff2e-1887-49e6-9c66-598bb395e76c"
```
Finish the activity-core instruction/report execution path needed for judgement
runs like daily triage.
Reuse the existing rule/instruction model from `ACT-ADR-003`:
- parse a fenced `instruction` block from the ActivityDefinition
- apply any instruction condition before running the report
- render the canonical prompt with explicit trusted context fields
- call the approved model/agent adapter through the existing org LLM path where
available
- validate the output against a small daily-triage report schema
- record model, prompt hash, validation result, and source instruction id in
the activity-core audit trail
This task should not introduce another scheduler or a one-off daily-triage
script. The deliverable is a reusable instruction execution capability that
this report can use and future judgement activities can share.
Done when activity-core can run a synthetic instruction ActivityDefinition and
produce a validated report payload under test.
### T04 - Add Working-Memory And State Hub Progress Sinks
```task
id: CUST-WP-0045-T04
status: done
priority: high
depends_on: [CUST-WP-0045-T03]
state_hub_task_id: "04e56428-d3a8-4aa7-a6e1-172c974ece3a"
```
Add deterministic output sinks for report instructions.
For this activity, the sink must:
- write one dated note under
`/home/worsch/the-custodian/memory/working/`
- post one State Hub progress event with `event_type: daily_triage`
- include the activity id, run id, scheduled time, and report summary
- be idempotent by activity-core run id and local date
- refuse to edit `canon/`, `workplans/`, or other canonical files
Done when a manual activity-core trigger creates exactly one working-memory
note and one State Hub progress event, and a retry does not duplicate either.
### T05 - Update And Validate The Daily Triage ActivityDefinition
```task
id: CUST-WP-0045-T05
status: done
priority: medium
depends_on: [CUST-WP-0045-T02, CUST-WP-0045-T03, CUST-WP-0045-T04]
state_hub_task_id: "0c6d54ec-7ed1-4e80-9cfa-ccb914e65fbf"
```
Update `activity-definitions/daily-statehub-wsjf-triage.md` so it is executable
by activity-core.
Expected changes:
- keep the trigger at `20 7 * * *`, timezone `Europe/Berlin`
- keep `misfire_policy: skip`
- add the report instruction block that references the canonical prompt
- keep `enabled: false` until manual validation passes
- document the single-runner cutover rule in the file
Validate using activity-core's existing parser and sync commands with
`ACTIVITY_DEFINITION_DIRS=/home/worsch/the-custodian`.
Done when the definition parses, syncs into activity-core, and appears as a
paused Temporal schedule while disabled.
### T06 - Canary Cutover And Disable Codex Automation
```task
id: CUST-WP-0045-T06
status: done
priority: high
depends_on: [CUST-WP-0045-T05]
state_hub_task_id: "545162d7-0198-4519-a30b-06e88c6db915"
```
Run the cutover safely.
Sequence:
1. Manually trigger the activity-core definition and verify output.
2. Pause or delete the Codex app automation
`daily-state-hub-wsjf-triage`.
3. Set the activity-core definition to `enabled: true`.
4. Sync activity definitions and Temporal schedules.
5. Confirm the Temporal schedule is unpaused and points at
`RunActivityWorkflow`.
6. Check the next 07:20 run for a working-memory note, State Hub progress event,
ActivityRun row, and Temporal workflow history.
Done when activity-core is the only enabled runner and the first scheduled run
has completed successfully.
### T07 - Observability And Missed-Run Handling
```task
id: CUST-WP-0045-T07
status: done
priority: medium
depends_on: [CUST-WP-0045-T06]
state_hub_task_id: "b977c721-cadc-461f-8ffb-715d438e4c31"
```
Document and, where cheap, automate how to tell whether the daily run happened.
The runbook should include:
- Temporal schedule and workflow checks
- activity-core ActivityRun query
- State Hub `daily_triage` progress-event query
- working-memory note path check
- expected behavior when the activity-core host is offline at 07:20
- the chosen missed-run behavior: `skip`, not catch-up
Done when the operator can answer "did it run today?" from owned telemetry
without inspecting Codex Desktop session internals.
### T08 - Three Daily Runs And CUST-WP-0044 Calibration
```task
id: CUST-WP-0045-T08
status: done
priority: medium
depends_on: [CUST-WP-0045-T06, CUST-WP-0045-T07]
state_hub_task_id: "f4a985fd-8cce-4175-983e-cf3b437e19a5"
```
Run three consecutive daily canaries from activity-core and compare the
recommendations with actual follow-up work.
Feed the result back into `CUST-WP-0044-T06`:
- calibrate WSJF scoring weights
- tune report length
- adjust loose-end detection thresholds
- confirm stale-but-intentionally-parked work is treated correctly
- decide whether daily notes are useful enough as a standing habit
Done when CUST-WP-0044 can close its calibration task using activity-core runs,
not Codex app automation runs.
## Implementation Notes - 2026-05-19
T01 is complete. The 2026-05-19 failed Codex automation run is captured in this
workplan's context, and the runner boundary is explicit: activity-core owns the
schedule, retries, context resolution, run log, and audit trail; State Hub stays
the read model and progress sink; the-custodian owns the prompt and guardrails.
T02 is complete in activity-core. The existing `state-hub` context resolver now
supports the daily triage queries `state_summary`, `next_steps`,
`workplan_index`, and `hub_inbox` while preserving `domain_summary` and
`repo_sbom_status`. Resolver failures return `{}` so the workflow can degrade
to offline context instead of failing the whole run.
T03 is complete in activity-core. `RunActivityWorkflow` now evaluates
instruction blocks after rules, using the existing instruction executor and a
small llm-connect HTTP client boundary. Instruction results carry task specs,
optional report payloads, prompt hash, model, validation status, review flag,
and condition metadata. A lightweight daily triage report schema is available
at `schemas/daily-triage-report.json` so report payloads can be validated under
test before T04 wires the deterministic working-memory and State Hub sinks.
T04 is complete in activity-core. Instruction definitions can now declare
`report_sinks`; report payloads are persisted through deterministic sink code
instead of model-authored file operations. The first two sink types are
`working-memory` and `state-hub-progress`. Working-memory writes refuse
canonical Custodian `canon/` and `workplans/` paths, use run-id/date based
idempotency, and State Hub progress posting deduplicates by activity run id and
instruction id before posting.
T05 is complete. The daily triage ActivityDefinition now uses a single trusted
scalar `context.daily_triage_digest` instead of raw State Hub JSON. The digest
is built in activity-core from safe identifiers, counts, statuses, priority
fields, health labels, and shortened titles, while excluding task descriptions,
message bodies, and other free-text command surfaces. The digest also carries a
`deterministic_scoring` extension marker so a later high-criticality path can
move especially high-gain/high-effort candidate scoring into code without
changing the ActivityDefinition contract.
T06 is partially validated but blocked before cutover. A local activity-core
dev stack was started, the Custodian ActivityDefinition directory synced into
activity-core, and the paused Temporal schedule for the disabled daily triage
definition was created. The first sync exposed reusable activity-core gaps that
were fixed there instead of bypassed here:
- file-authored ActivityDefinition slug ids now map to stable UUIDv5 DB ids
- schedule sync no longer uses raw `NOT IN :ids` SQL that asyncpg rejects
- ADR-style context sources without an explicit `name` validate against the
domain model
- the worker now registers the existing instruction/report activities
Manual trigger canary evidence, using a local-only llm-connect mock response so
no State Hub digest data left the workstation:
- workflow id:
`activity-6fca51fa-387a-4fd0-bc4e-d62c29eb859a:manual-6a6e5950-2338-45c4-9054-573dda9c87cc`
- Temporal status: `COMPLETED`
- activity-core run id: `2164cb88-8415-5c96-9e31-e47a41cf4e67`
- working-memory note:
`memory/working/daily-triage-2026-05-19-2164cb88.md`
- State Hub progress event: `e42c0ada-8111-4d88-9791-821252cd04a2`
The real Claude-backed llm-connect trigger was not run in that pass. The
execution wrapper blocked it because private State Hub workstream/task digest
data would be sent to an external LLM provider. The operator then clarified that
`llm-connect` is the intended backend boundary for LLM providers and depth
tuning. Follow-up implementation keeps that boundary explicit: activity-core
passes model/depth configuration through llm-connect, provider routing remains
inside llm-connect, and the ActivityDefinition declares a balanced
`custodian-triage-balanced` profile for calibration.
The llm-connect depth path is now reusable instead of daily-triage-specific:
- activity-core `InstructionDef` accepts `temperature`, `max_tokens`,
`max_depth`, and `model_params`
- activity-core sends those values to llm-connect as `RunConfig`
- llm-connect server mode now preserves the full `RunConfig` via
`RunConfig.from_dict`
- the daily triage ActivityDefinition starts with
`model: custodian-triage-balanced`, `max_depth: 2`, and
`model_params.reasoning_effort: medium`
Remaining T06 work is now operational cutover: run the real llm-connect backend
selected by the operator, verify real report quality, pause Codex automation,
set the ActivityDefinition to `enabled: true`, sync schedules, and check the
next 07:20 run.
Verification:
- `uv run pytest tests/test_state_hub_context_resolver.py -q`:
6 passed
- activity-core parser validation with
`ACTIVITY_DEFINITION_DIRS=/home/worsch/the-custodian`:
parsed the daily triage definition, cron trigger, trusted instruction, and
report sinks
- `uv run pytest -q` in activity-core:
107 passed, 1 skipped
- activity-core focused T06 validation:
`uv run pytest tests/test_sync_activity_definitions.py
tests/test_instruction_evaluation.py tests/test_report_sinks.py -q`:
10 passed
- activity-core full suite after T06 fixes:
`uv run pytest -q`:
110 passed, 1 skipped
- activity-core llm-connect depth pass-through full suite:
`uv run pytest -q`:
114 passed, 1 skipped
- llm-connect focused server validation:
`uv run pytest tests/test_server.py -q`:
10 passed
- llm-connect full suite:
`PYTHONPATH=. uv run pytest -q`:
173 passed
## Implementation Notes - 2026-05-21
T06 remains in progress; no cutover was performed and the Codex automation must
remain the fallback runner. The daily triage ActivityDefinition is still
`enabled: false`.
Real llm-connect canary attempt 1 reached the activity-core workflow but failed
before report persistence:
- workflow id:
`activity-6fca51fa-387a-4fd0-bc4e-d62c29eb859a:manual-d0317873-5e09-4849-a57a-6edff7fada2c`
- Temporal status: `COMPLETED`
- activity-core run id: `9b8486b5-0495-5d3f-8b7b-dc078a7c097b`
- worker evidence: llm-connect returned HTTP 200 twice, but activity-core
rejected the instruction output as invalid JSON
- persistence evidence: no working-memory note and no State Hub
`daily_triage` progress event were written
Diagnosis showed that server-mode llm-connect was resolving the older
`/usr/bin/claude` CLI instead of the working user install at
`/home/worsch/.local/bin/claude`. A direct llm-connect probe through the older
CLI returned the literal content `Execution error`, while the user install could
return raw JSON. Restarting llm-connect with the user CLI path made a small
probe return `{"ok": true}` through the HTTP boundary.
Real llm-connect canary attempt 2 used the working Claude CLI path but still did
not produce a persisted report:
- workflow id:
`activity-6fca51fa-387a-4fd0-bc4e-d62c29eb859a:manual-2de56ad6-0f82-48f0-8184-f357bd22f658`
- Temporal status: `COMPLETED`
- activity-core run id: `953a1f46-e57b-58e1-b4a2-2e41e804a972`
- worker evidence: first llm-connect call returned HTTP 200, then activity-core
retried because the output was not schema-valid JSON; the retry returned
HTTP 500
- persistence evidence: no working-memory note and no State Hub
`daily_triage` progress event were written
The follow-up fix keeps the existing activity-core/llm-connect boundary:
- activity-core now loads an instruction's existing `output_schema` and forwards
that schema to llm-connect as `model_params.json_schema`
- llm-connect's Claude Code adapter now prefers
`LLM_CONNECT_CLAUDE_CLI_PATH`, `CLAUDE_CLI_PATH`, or the user-local
`/home/worsch/.local/bin/claude` before falling back to `claude`
- llm-connect's Claude Code adapter maps `model_params.json_schema` to the
native Claude CLI `--json-schema` option
- the Custodian ActivityDefinition now points at the domain-owned absolute
schema path `/home/worsch/the-custodian/schemas/daily-triage-report.json`
and asks for JSON only as a fallback
The patched schema probe could not be completed because the local Claude Code
session limit was reached; the CLI reported:
`You've hit your session limit · resets 3:40am (Europe/Berlin)`.
Next T06 step after the limit resets, or after llm-connect routes this profile
to another approved provider, is to rerun the manual trigger with the patched
schema path and verify all three evidence surfaces before pausing Codex or
enabling the activity-core schedule.
Verification:
- activity-core focused executor tests:
`uv run pytest tests/rules/test_executor.py -q`:
22 passed
- llm-connect focused Claude Code/factory tests:
`PYTHONPATH=. uv run pytest tests/test_claude_code.py tests/test_factory.py -q`:
18 passed
- activity-core full suite:
`uv run pytest -q`:
115 passed, 1 skipped
- llm-connect full suite:
`PYTHONPATH=. uv run pytest -q`:
175 passed
## Implementation Notes - 2026-05-23
`CUST-WP-0046` verified the separate hourly RecentlyOnScope activity-core
schedule, but did not retire the Codex app automation
`daily-state-hub-wsjf-triage`. That automation is still the fallback for this
daily WSJF triage cutover while this workplan's ActivityDefinition remains
`enabled: false`.
The State Hub decision recorded under `CUST-WP-0046` is to keep the Codex
automation active until this workplan completes its own daily WSJF canary and
explicit pause/delete cutover step.
## Implementation Notes - 2026-06-01
T06 remains `in_progress`. No canary was rerun in this session. The blocker
is the same as on 2026-05-21: the patched real-LLM probe was never executed
after the Claude CLI session limit cleared. Eleven days passed without a
retry; the activity-core dev stack is currently down (only `infra-postgres-1`
from another project is running on the workstation).
The fixes that should make the canary succeed are merged and unchanged:
- activity-core `cf92f0d` — forward instruction `output_schema` to llm-connect
as `model_params.json_schema`
- activity-core `5c4f96e` — pass `temperature`, `max_tokens`, `max_depth`,
`model_params` through to llm-connect `RunConfig`
- llm-connect `b12d1af` — Claude Code adapter maps `json_schema` to native
`--json-schema` CLI option
- llm-connect `82e3c07` — server mode preserves the full `RunConfig`
Session deliverable: a separate cutover runbook capturing the exact host-mode
command sequence to bring the dev stack up, sync the ActivityDefinition from
`ACTIVITY_DEFINITION_DIRS=/home/worsch/the-custodian`, run the smoke probe,
trigger the canary, verify all three evidence surfaces, and only then flip
`enabled: true` and sync schedules:
- file: `workplans/CUST-WP-0045-cutover-runbook.md`
- commit: `8ef5399 Add CUST-WP-0045 T06 cutover runbook`
The runbook also documents two operational gotchas the 2026-05-21 attempts hit:
- The repo `.env` uses Docker network hostnames (`temporal:7233`,
`app-db:5432`, `nats:4222`); host-mode worker/API processes must override
these on the command line because `make` auto-loads the file.
- Triggering the canary from inside an active Claude Code session shares the
CLI quota with llm-connect's Claude Code adapter and can reproduce the
`Execution error` / HTTP 500 failure. Run from a fresh terminal account.
The current `daily_triage_digest` (built with the live State Hub and the
ActivityDefinition's params) was inspected: 10,175 bytes, totals across 13
active topics / 13 active workstreams / 149 todo tasks, 12 open workstreams
in the digest with `hf-wp-0001`, `cust-wp-0044`, `cust-wp-0045`, `cust-wp-0046`
at the priority head. This is substantive context — a working real-LLM canary
should produce non-trivial recommendations, not the `"summary":"ok"` stub from
the 2026-05-19 mocked run.
T07 and T08 remain `todo`. The cutover runbook overlaps with T07's evidence
queries but does not cover T07's steady-state "did it run today?" framing or
the missed-run policy documentation, so T07 is not satisfied by this session.
## Implementation Notes - 2026-06-02
T06 is `done`. The patched canary completed end-to-end against a real LLM and
produced all three evidence surfaces in one run.
**Canary run**
- workflow id:
`activity-6fca51fa-387a-4fd0-bc4e-d62c29eb859a:manual-14811b78-f9fd-4f60-9304-50fdd863960a`
- activity-core run id: `f9b97749-c1d0-5746-ab18-89932bef47c1`
- Temporal status: `COMPLETED`, runtime `12.85s`
- working-memory note:
`memory/working/daily-triage-2026-06-02-f9b97749.md` (2,675 bytes,
9 recommendations covering work-next/needs-human/revisit/split/park,
including a correct self-aware recommendation on `cust-wp-0045` itself
and a `park` on `adhoc-2026-06-01`)
- State Hub progress event: `935244fa-b438-488c-a11a-42e1a84e3d59`
(`event_type: daily_triage`, full report nested under `detail.report`)
**Backend used for the canary**
The canary ran via llm-connect against OpenRouter `anthropic/claude-sonnet-4`,
not the local Claude Code CLI. The choice was operational: the local CLI
backend shares quota with any active Claude Code session (so it could not be
driven from inside one), and the JSON schema mode that the local CLI exposes
hit a series of envelope-shape and routing bugs that were patched but never
fully verified end-to-end on a long prompt. The next operator-scheduled run
will inherit whatever llm-connect is currently configured with.
**Bug chain found and fixed during T06**
llm-connect:
- `9de0f49` — Claude Code adapter passes `--output-format json` whenever
`--json-schema` is set, then unwraps the CLI envelope. Without this,
`--print` returned conversational text on stdout and the structured
payload went to a sidecar channel the adapter never read.
- `435da49` — envelope unwrap prefers any field whose value parses as JSON
and skips telemetry keys (`type`, `usage`, `total_cost_usd`, …) so a prose
preamble in `result` no longer wins over the structured payload elsewhere
in the envelope.
- `cd4551c` — OpenRouter adapter translates `model_params.json_schema` to
the OpenAI Chat Completions `response_format` wrapper and drops Claude /
llm-connect-specific keys (`reasoning_effort`, `max_depth`). The previous
naive `payload.update(config.model_params)` triggered an OpenRouter 400.
- `583ab57` — OpenRouter `response_format.json_schema.strict` defaults to
`False` because most real schemas do not satisfy OpenAI strict mode
(`additionalProperties: false` everywhere, all properties required).
- `1b01f0e` — OpenRouter adapter honours an explicit constructor `--model`
even when that value equals the adapter's hardcoded default. Without this
fix, `--model anthropic/claude-sonnet-4` silently fell through to
`RunConfig.model_name` (which defaults to `"gpt-4"`), routing every call
to OpenAI's gpt-4 — which does not accept `response_format: json_schema`
and 400'd. This bug masqueraded as the strict-mode and translation
problems above for hours.
activity-core:
- `c79d098` — `_ACTIVITY_TIMEOUT` is now `ACTIVITY_TIMEOUT_SECONDS` env-
configurable (default 900s). Two racing 5-minute timeouts had been
killing the worker side just before llm-connect could write the
response, surfacing as `BrokenPipeError` server-side.
- `4b4e162` — `instruction_output_error` warning now includes a 2KB raw
output preview so the next failure of this shape is one grep away
from diagnosis instead of requiring code edits.
**Operational cutover — NOT done in this session**
T06's original done-criteria included activity-core being "the only enabled
runner" and "the first scheduled run has completed successfully." The
technical canary half is complete and proven; the operational toggle remains
explicitly deferred to operator action:
1. Pause the Codex Desktop automation `daily-state-hub-wsjf-triage`.
2. Edit `activity-definitions/daily-statehub-wsjf-triage.md` to set
`enabled: true`.
3. Run `make sync-all && make sync-schedules` in activity-core to unpause
the Temporal schedule.
4. Watch the next 07:20 Europe/Berlin tick; verify all three evidence
surfaces persist on a scheduled (not manual) run.
This is a prerequisite for **T08** (three daily canaries against the new
runner for CUST-WP-0044 calibration). Until the operator performs the
cutover, the Codex automation remains the active runner and the
activity-core schedule stays paused.
**Verification**
- activity-core full suite: `uv run pytest -q` → 120 passed, 1 skipped
- llm-connect full suite: `PYTHONPATH=. uv run pytest -q` → 179 passed
- Live canary against State Hub on 2026-06-02 at 12:52 UTC → three evidence
surfaces, identical run_id across all four (file, progress event,
ActivityRun row, Temporal result)
## Implementation Notes - 2026-06-04
T07 is done by reusing the State Hub WSJF Triage review page from
`STATE-WP-0053` instead of adding another Custodian-local status surface.
Evidence:
- `STATE-WP-0053` is `finished`; its dashboard page is reachable from
Workstreams -> WSJF Triage and implemented in the State Hub dashboard at
`dashboard/src/wsjf-triage.md`.
- The page reads `/progress/?event_type=daily_triage&limit=14` and
`/workstreams/workplan-index`, shows live/last-updated state, recent reports,
report detail, linked recommendations, a 14-day pattern view, and run
metadata.
- The detail metadata includes `scheduled_for`, `activity_core_run_id`,
`activity_id`, `instruction_id`, and the working-memory note path, which is
enough for the operator to answer "did it run today?" from owned
State Hub/activity-core telemetry without inspecting Codex Desktop session
internals.
- The deeper command-level diagnostics remain in
`workplans/CUST-WP-0045-cutover-runbook.md`, including Temporal schedule /
workflow and ActivityRun checks.
- The schedule and missed-run policy remain documented in
`activity-definitions/daily-statehub-wsjf-triage.md`: daily at 07:20
Europe/Berlin with `misfire_policy: skip`. The chosen behavior is no
catch-up; if the activity-core host is offline at 07:20, the absence is
visible as no new report/event for that date.
T08 is done by the same 2026-06-04 calibration pass that closes
`CUST-WP-0044-T06`. Evidence:
- `memory/working/daily-triage-2026-06-02-f9b97749.md`
- `memory/working/daily-triage-2026-06-03-6d2737e3.md`
- `memory/working/daily-triage-2026-06-04-65e273bf.md`
- `docs/daily-statehub-wsjf-calibration-2026-06-04.md`
The three reports were produced by activity-core and were compared against the
actual follow-up work. The loop correctly kept `CUST-WP-0044` and
`CUST-WP-0045` at the top until this closeout, kept human-gated work separate,
and surfaced `CUST-WP-0046` as a blocked/revisit candidate instead of asking
for unsafe fallback retirement. Calibration tightened the executable report
schema so future runs include explicit WSJF ranks and component scores. With
T06, T07, and T08 complete, the activity-core daily triage runner cutover is
finished.
## Acceptance Criteria
- The daily State Hub WSJF triage runs from activity-core, not Codex app cron.
- The Codex app automation is disabled or removed before the activity-core
schedule is enabled.
- The daily run leaves all three evidence surfaces: working-memory note, State
Hub `daily_triage` progress event, and activity-core ActivityRun/Temporal
history.
- "Did it run today?" can be answered from State Hub and activity-core
telemetry.
- A powered-off workstation no longer matters once activity-core is running on
the chosen always-on host.
- If the chosen activity-core host is offline at 07:20, the missed run is
skipped by policy and the absence is visible in the runbook checks.
- CUST-WP-0044's three-run calibration is completed using the new runner.
## Notes
The immediate Codex app automation failure could be patched by chasing the
Windows/WSL launch path issue. That is not the preferred durable fix. The
preferred fix is to make the existing activity-core ActivityDefinition the
primary runner and keep all scheduling, audit, context resolution, and failure
visibility in owned infrastructure.