Skip to content

Flow Metrics & Analytics

LumenFlow tracks flow metrics to help teams identify bottlenecks and improve delivery performance.

The Software Delivery Pack owns these APIs. Import them from @hellmai/lumenflow-packs-software-delivery/metrics or its supported dora, flow, and telemetry subpaths. The former @hellmai/lumenflow-metrics package is a deprecated compatibility shell and contains no independent implementation.

  • DORA Metrics — The dora.dev 2025 canonical 5-metric model
  • Flow Analysis — Bottleneck detection and critical path calculation
  • Telemetry — Event emission for local NDJSON logs and cloud sync

DORA Metrics (2025 canonical 5-metric model)

Section titled “DORA Metrics (2025 canonical 5-metric model)”

LumenFlow tracks the five metrics defined by dora.dev and refreshed by the CDF Oct 2025 announcement. Aggregation follows DORA canonical guidance: mean for deployment frequency, median for lead time and FDRT.

MetricGroupFormulaUnitAggregationTarget
Deployment FrequencyThroughputcommits_in_window / days_in_window * 7/weekmeanDaily → weekly
Lead Time for ChangesThroughputWU cycle time = completed_at − claimed_athoursmedian< 24h
Failed Deployment Recovery (FDRT)Throughputmedian(time between paired EMERGENCY commits)hoursmedian< 1h
Change Failure Rate (CFR)Instabilityfailures / total_deployments * 100%ratio< 15%
Deployment Rework RateInstability(revert + hotfix commits) / total_deployments * 100%ratio< 5%
pnpm metrics:snapshot                 # All metrics, JSON output
pnpm metrics:snapshot --type dora     # DORA metrics only
pnpm metrics:snapshot --days 30       # 30-day window (normalised to per-week)
pnpm metrics:snapshot --dry-run       # Preview; no NDJSON written, no cloud sync

Example output:

DORA METRICS (2025 canonical 5-metric model)
Deployment Frequency: 6/week (elite)
Lead Time: 12h median (elite)
Failed Deployment Recovery Time: 0.5h median (elite)
Change Failure Rate: 8% (elite)
Deployment Rework Rate: 3% (elite)

When a workspace has a control_plane endpoint configured, DORA records are shipped to POST <endpoint>/api/v1/telemetry in batches of up to 1000 records. A typical metrics:snapshot run emits 5 records and fits in a single batch.

Records are first appended to the local telemetry/dora.ndjson file in the pack state directory, then the cloud sync worker reads from the persisted cursor offset and posts batched payloads. This gives offline resilience: retries resume from the last acknowledged offset.

Every record carries a tags bag the control plane can slice dashboards by. Values are primitive (string | number | boolean); missing values are omitted rather than emitted as empty strings.

TagSourceExample
source_typeHard-coded"dora"
calculated_byHard-coded"metrics:snapshot"
tierPer-metric classification"elite"
repogit config --get remote.origin.url → parsed owner/repo"my-org/my-repo"
branchgit rev-parse --abbrev-ref HEAD"lane/framework-metrics/<wu-id>"
commit_shagit rev-parse HEAD"deadbeef…"
serviceworkspace.yaml → service (or software_delivery.service)"control-plane"
environmentworkspace.yaml → environment, fallback LUMENFLOW_ENV"prod"
snapshot_window--days flag"7d", "30d"
pipelineCI_PIPELINE_NAME, fallback GITHUB_WORKFLOW"main-ci"
deploy_targetDEPLOY_TARGET"prod-eu"
workflow_run_idGITHUB_RUN_ID, fallback CI_PIPELINE_ID"987654"

Lead time and FDRT records additionally carry aggregation: "median", mean_hours, and p90_hours so trend dashboards can plot all three aggregations without re-running the CLI. CFR records carry failures + total_deployments; Deployment Rework Rate carries rework_commits + total_deployments.

pnpm cloud:connect                        # Interactive OAuth + workspace.yaml scaffolding
pnpm config:get --key control_plane       # Verify endpoint + sync_interval
pnpm metrics:snapshot                     # Emits NDJSON + triggers cloud sync when configured

See Workspace spec for the full control_plane schema.

A connected control plane sees more than the original four NDJSON sources (gates, flow, dora, costs). LumenFlow routes every audited local stream to an existing transport — the telemetry registry, the kernel-event envelope, or a memory/signal port — or records why it is excluded.

Six more telemetry source ids join the registry, bringing it to ten append-only NDJSON sources synced with offset tracking. Local files are in the software-delivery pack state directory:

Source idLocal fileDefault payload mode
llm-classificationtelemetry/llm-classification.ndjsonfull (PII-free at emit)
lane-signalstelemetry/lane-signals.ndjsonfull
toolstelemetry/tools.ndjsonmetadata-only (hashes + sizes)
methodologytelemetry/methodology.ndjsonfull
prompt-linttelemetry/prompt-lint.ndjsonfull
incidentsincidents/*.ndjson (directory)metadata-only (stack → hash)

The sidecar tick and session-end flush keep these sources current for a connected session, not just at gate-run time.

Each cloud-synced stream is configured under software_delivery.telemetry.cloud_sync.streams.<source-id> with enabled and an optional payload_mode:

software_delivery:
  telemetry:
    cloud_sync:
      streams:
        tools:
          enabled: true
          payload_mode: metadata-only # default for tools/memory/incidents
        incidents:
          enabled: true
          payload_mode: metadata-only
        llm-classification:
          enabled: true
          payload_mode: full # PII-free streams default to full

payload_mode has two values:

  • metadata-only (default for the free-text-bearing streams — tools, memory, incidents): free-text fields are replaced by a content hash plus a byte size (e.g. arg_sha256 + arg_bytes, stack_sha256 + stack_bytes). Structured low-risk fields (tool name, exit code, duration, lane, WU id, memory type, tags) pass through unredacted.
  • full: the complete payload is sent. Streams that are already free of user content (llm-classification, lane-signals, methodology, prompt-lint) default to full.

Redaction is applied in mappers, not emitters: the local file always keeps the complete record for local debugging (cost:summary, flow:report, forensic inspection). Only the wire projection is redacted, and flipping a stream to full changes only the mapper output, never the on-disk file.

Force-bypass and skip-gates audit logs are cloud-visible as schema-validated kernel events on POST /api/v1/events, forming a governance event domain:

Kernel eventSource log
governance:force_bypass_recordedforce-bypasses.log
governance:gates_skippedskip-gates-audit.log

Both carry full governance evidence (the audit trail is the point). A force bypass is itself audited and surfaces as a governance:force_bypass_recorded event, so bypasses cannot hide.

wu:done attaches a versioned delivery_change_manifest.v1 object to the existing task_completed kernel event. This gives connected control planes a source-independent description of what the completed WU changed:

  • base and head commit identities;
  • declared code_paths and paths changed outside that declaration;
  • added, modified, deleted, and renamed files, including both rename paths;
  • source, test, documentation, configuration, environment-template, dependency, migration, infrastructure, and generated-file classifications;
  • safely detected environment-variable names and configuration keys; and
  • full summary counts plus explicit completeness and truncation metadata.

The manifest is deterministic and bounded to 200 entries per detail array. Summary counts always describe the full resolved change set. When details are bounded, completeness.complete is false and truncated_fields identifies which arrays are partial.

The payload never includes file contents, patches, environment-variable values, secrets, credentials, or token-shaped values. Existing completion events without the optional manifest remain valid.

Connected services should store the nested object by task_id and use it to project role-appropriate /work views. This lets managers and third-party systems inspect migration, dependency, test, configuration, and scope impact without repository access or independent Git analysis.

Connected workspaces add an optional wu_telemetry.v1 snapshot to their existing WU lifecycle events. wu:create and wu:claim send provisional snapshots. After gates pass, wu:prep sends a checkpoint with a provisional change manifest. wu:done sends the final snapshot and authoritative final manifest on task_completed. Consumers that only understand the legacy event shape can ignore the additive fields.

The rich snapshot gives an authorised Mission Control everything needed to render active and completed work without checking out the repository: stable WU/workspace/project identity, lifecycle and ownership, lane/priority/type, declared scope, dependencies and acceptance, checkpoint/gate/evidence summaries, plus deterministic repository navigation. When a canonical browser URL is available, navigation.wu_web_url links directly to the governed WU spec in the repository; source ref, exact commit, and known PR/CI/evidence links are included separately.

The contract is strict, bounded, and content-free. Raw YAML, notes, completion prose, environment values, patches, source contents, local/worktree paths, token-shaped values, credential assignments, and clone or credential-bearing URLs are not valid telemetry. Cloud still applies workspace/role authorization, and opening a private repository link still requires the reader’s own Git-provider access.

Shared memory and A2A signals route through dedicated memory/signal sync ports rather than the telemetry registry. Memory rows default to metadata-only (body → hash + size); signals send their structured keys verbatim and apply the redaction policy only to free-text bodies.

Sync health is loud and, when configured, enforceable at wu:done via software_delivery.telemetry.cloud_sync.enforcement:

software_delivery:
  telemetry:
    cloud_sync:
      enforcement: warn # off | warn | block (default warn)
Levelwu:done behaviour
offNo gate. Health still surfaces in the doctors.
warnDefault. Prints per-stream pending counts + remediation; never blocks.
blockBlocks wu:done only on a deterministic failure class (HTTP 4xx auth/config rejection, missing token).

Transient failures (timeouts, 5xx, DNS, connection resets) never block, even under block — they degrade to a warning so a flaky network cannot brick wu:done.

Sync health is also a first-class line in both doctors:

pnpm lumenflow:doctor      # main doctor — telemetry sync-health section
pnpm compute:doctor        # compute doctor — same per-source health

Each reports per-source last_success_at / last_error, so a silent skip is no longer the only observable state.

pnpm flow:bottlenecks

This analyzes your WU flow to identify:

  • Lane Congestion — Lanes exceeding an explicit wip_limit
  • Blocked WUs — Work units waiting on dependencies
  • Stale WUs — WUs in progress for too long
  • Critical Path — WUs blocking the most downstream work

Capture point-in-time metrics for dashboards or CI:

pnpm metrics:snapshot                    # Full snapshot, also written as JSON to the metrics snapshot file
pnpm metrics:snapshot --type dora        # DORA only
pnpm metrics:snapshot --days 30          # 30-day window; value still reported per-week
pnpm metrics:snapshot --json             # Print the stable JSON document instead of the table

metrics:snapshot --type all and --type flow add a dispatchability and throughput section derived from the gate_attempt ledger and the work-unit lifecycle records. No external script produces these numbers: quoting one means quoting a command anyone can re-run. --type lanes and --type dora are unchanged and carry N/A for this section, so the delivery-flow auto-emit path pays none of its cost.

FieldMeaning
dispatchable_readyReady work units that are dependency-clear, free of code-path overlap with any active claim, and inside lane WIP
gate_run_cost_per_dayGate-attempt duration p50 and p90 grouped by UTC day, over the --days window
first_attempt_landingShare of landed work units whose gate ledger records no failed attempt
ownerless_in_progressActive claims that are not bound to a recoverable owner (see below)
unread_ack_required_signalsAck-required coordination signals with no acked/rejected receipt

What ownerless_in_progress measures — and what it does not

Section titled “What ownerless_in_progress measures — and what it does not”

An in_progress record counts as bound only when all of these hold:

CheckedHow
It names an owner (assigned_to)Field present and non-empty
It carries the canonical branch (claimed_branch)Field present and non-empty
Its declared worktree_path existsFilesystem check, resolved against the main checkout
A session record exists for itThe session record for the WU exists in the main checkout

The worktree check applies only to the claimed modes that own one. A branch-pr claim (the cloud shape) and a branch-only claim have no worktree by design — wu:claim writes worktree_path only when it created one — so they are asked for a branch and a session record and nothing more. A record that declares no claimed_mode predates the field and is treated as worktree backed.

Anything else is unbound, and the missing piece is named in the gaps histogram beside the count (missing_owner, missing_branch, missing_worktree, missing_session; one record can show several). A bare total would hide which remedy applies — a claim with a vanished worktree needs wu:recover, one with no branch binding needs a different fix.

Session records persist after a work unit lands, so missing_session only discriminates claims made before session records were minted on the claim path. On a board of current claims it is a real signal; on an old one it is expected, and the histogram is what makes the difference visible.

Process liveness is not measured. Nothing durable on the claim path records a process id, so this metric never asserts that an owning process is alive or dead. The payload states this explicitly as "liveness_probed": false, and the human table prints process liveness not measured, so the count cannot be read as a liveness claim it did not earn.

Field presence alone is not sufficient: a record keeps its assigned_to and worktree_path strings long after the worktree and the session are gone, which is how a “zero ownerless claims” reading can coexist with a board full of dead claims.

Both surfaces are emitted from the same captured object: the stable JSON document (--json, and the file written for the latest snapshot) and the human table printed by default.

Every classifier reports considered, classified and excluded with a histogram of exclusion reasons:

│ Dispatchable ready share: 10.2% (5/49 ready)
│   considered=412 classified=5 excluded=407 (code_path_overlap=14, dependency_blocked=30, lane_wip_exhausted=5, not_ready=358)

A metric that reported only its own numerator could hide an under-matching filter behind a confident, small, wrong answer. The counts make the size of the population, and everything dropped from it, part of the result.

The population is every work unit that landed inside the --days window. A landed work unit is excluded under one of three named reasons:

Exclusion reasonMeaning
gate_retry_observedIts gate ledger records at least one failed attempt
no_ledger_evidenceIt has no gate_attempt row at all, so it never ran gates
no_parseable_completed_atIts record has no parseable completed_at, so it cannot be placed in a window

Records with no completed_at are legacy records and are excluded as no_parseable_completed_at.

wu:prep writes its gate_attempt rows, and each governed command its lifecycle_duration row, to the worktree’s event ledger, usually without committing them. wu:done carries these rows onto main in its own metadata commit. This holds on both of its ledger resets: the preflight reset, and the reset before it re-runs gates. So a work unit that passes wu:prep first time and lands straight away counts as a first attempt.

These numbers are also a gate, not only a report. pnpm release runs a throughput ratchet before it creates a tag: it reads a recorded baseline, takes a fresh measurement from this same governed command, and refuses the release when a metric has moved in its bad direction beyond the configured tolerance.

software_delivery:
  gates:
    throughput_baseline:
      path: .lumenflow/throughput-baseline.json
      tolerance_percent: 25
      tolerance_floor_hours: 0.5

tolerance_floor_hours (default 0.5) is an absolute floor applied to the hour-valued claim-to-done series’ threshold: max(baseline * (1 + tolerance_percent / 100), tolerance_floor_hours). Without it a baseline of 0h (a genuine, but tiny, sample) turns a relative tolerance into an absolute-zero gate that refuses any non-zero candidate.

Record or re-record the baseline — never hand-write it:

pnpm throughput:baseline:record

That reads this command through the same projection the release comparison uses, over the window the existing baseline declares, and refuses to write a document the comparison could not read back.

Baseline metricDirectionDerived from
gate_run_p50_mslower is bettermedian per-day p50 in gate_run_cost_per_day
gate_run_p90_mslower is bettermedian per-day p90 in gate_run_cost_per_day
warm_path_prep_mslower is betterelapsed time of a warm, evidence-reusing prep
first_attempt_landing_share_percenthigher is betterfirst_attempt_landing.share_percent

Because gate_run_cost_per_day is reported per calendar day and the raw per-attempt durations are not recoverable from the stable output shape, the baseline reduces the window to the median of the per-day values, taken independently per percentile.

That statistic is calibrated against this project’s ledger rather than preferred: with a per-day spread of 3.6x (p50) and 2.7x (p90), recording the worst day and comparing at 20% blocked 7 of 10 regression-free 7-day windows, while the per-day median at the shipped 25% tolerance blocks 1 of 10.

The baseline’s window_days decides the candidate’s window: percentiles taken over different windows are not comparable, so a candidate measured over a different one fails closed rather than being compared anyway.

Claim-to-done phases and lifecycle command wall time

Section titled “Claim-to-done phases and lifecycle command wall time”

Gate-run cost alone does not capture claim-to-done time, so the baseline also records two series derived from the WU event ledger, never re-derived by this ratchet:

  • claim_to_done_series — median and p90 hours per named phase (claim_to_first_commit, implementation, prep_wall_incl_retries, prep_to_done, done, blocked), for the whole fleet (overall.<phase>) and per declared size band (<sizeBand>.<phase>), reusing the claim-to-done-by-phase section pnpm metrics:snapshot --type flow --json already computes (the same heartbeat and lifecycle_duration ledger events that section derives from). A phase/band with no completed WUs is omitted rather than recorded as a fabricated zero.
  • lifecycle_command_wall_time_ms_median — median wall time (ms) per governed lifecycle command (wu-claim, wu-brief, wu-prep, wu-done, wu-rebase, wu-recover), read directly from every lifecycle_duration event on the ledger within the baseline’s window.

lifecycle_duration recording is controlled per workspace by software_delivery.wu.lifecycleDurations (default false); the environment variable LUMENFLOW_LIFECYCLE_DURATIONS (1 on, 0 off) overrides it for one invocation. Durations are written to the gitignored heartbeat journal of the main checkout, never the tracked WU event ledger, so every worktree’s durations land in one place and survive the worktree’s removal.

The release check compares the series it records. pnpm throughput:baseline:record records lifecycle_command_wall_time_ms_median from the ledgers and prints the considered, matched and skipped counts of both populations (claim-to-done WUs and lifecycle_duration events). In a workspace that records durations it refuses to write a baseline with no lifecycle medians, because a release would then compare nothing. A workspace that does not record durations declares the series absent (null) and the check prints that. A governed command with no baseline median is named in the printed output as not compared, so a blind spot is never mistaken for a pass.

The claim-to-done series includes overall.claim_to_done (and <sizeBand>.claim_to_done): the whole claimed_at..completed_at span, which is the speed number itself. When the release cut finds the candidate’s median claim-to-done, or any governed lifecycle command median, worse than the recorded baseline beyond the tolerance, it refuses and prints a before/after table: metric, baseline value, candidate value and the limit it crossed.

Both series compare per key at the same tolerance_percent: a size band or command with no data in the candidate’s window is skipped (not a failure — a short release window legitimately has gaps), but a series the baseline declares and the candidate cannot supply at all fails closed, and a regression on any present key refuses the release. A claim-to-done key backed by fewer than 5 completed WUs on either side is also skipped: weighing a rare size band’s one or two completions at the shared tolerance_percent would cause false-positive refusals.

The p90 half of a claim_to_done_series comparison is informational at every sample count — it never refuses a release on its own; the median gate is unaffected at any sample count. p90 over a skewed distribution is decided by one or two slow WUs, so it cannot separate signal from noise. p90 is always printed as an informational line (naming the key, both p90 values and both sample counts) so the value is never hidden, but it can never appear as a regression.

gate_run_p50_ms, gate_run_p90_ms and warm_path_prep_ms are recorded and reported, but are not refusal-capable: a release whose only change is a gate-run-cost regression, with claim_to_done_series and lifecycle_command_wall_time_ms_median unchanged, passes. first_attempt_landing_share_percent refuses (it measures landing success, not gate cost), as do claim_to_done_series and lifecycle_command_wall_time_ms_median.

A metric recorded as null is a declared absence for that workspace. An absence declared on both sides passes and is reported; a metric the baseline declares present that the candidate cannot supply is a failure, as are a missing, unparseable or window-less baseline, one carrying a metric that is neither a number nor null, one declaring every metric absent, a metrics command that will not run, and a non-finite value. An unmeasured release is never a pass.

The complementary per-run control is the opt-in prep latency budget, which fails an individual wu:prep that overruns the budget for its weight class once a workspace enables it — see the Gates reference.

code_paths overlap blocks a claim that collides with work already in progress. The opposite direction is just as expensive and was previously silent: an accepted claim withholds every ready work unit that overlaps it, including higher-priority ones.

wu:claim prints this report before any mutation, and orchestrate:init-status prints it for every active claim in an initiative:

Shadow report for <wu-id> [P2]: considered=412 classified=14 excluded=398 (lower_priority=9, no_overlap=331, not_ready=57, self=1)
  This claim withholds 14 ready work unit(s) of equal or higher priority:
    - <wu-id> [P0] (Framework: Core Lifecycle): shared/config.ts
  Governed alternatives:
    1. Claim the highest-priority withheld unit first: pnpm wu:claim --id <wu-id> --lane "Framework: Core Lifecycle"
    2. Narrow this claim by governed edit: pnpm wu:edit --id <wu-id> --replace-code-paths "<narrower-path>"

The report is advisory: it never blocks a claim and never fails a status view. A board it cannot read degrades to a printed note naming the cause, because a broken advisory must not become a broken claim path.

Overlap is computed over declared code_paths only. Both sides of the intersection are expanded against the repository’s tracked files, so the answer is a set of real files rather than a string comparison of patterns — but a file a work unit touches without declaring it in code_paths is invisible here, in the same way it is invisible to the overlap check that refuses a claim. The report therefore under-reports exactly as far as code_paths under-declares.

Declarations are expanded with the same expression semantics as the blocking claim overlap check: a directory entry (with or without a trailing slash) covers everything beneath it, ./x and x are the same declaration, and platform backslash separators are normalised before matching. This is not a detail — a directory declaration compared as a raw string against enumerated file paths matches nothing, which would let a claim holding a whole directory report that it withholds no work at all.

Only alternatives wu:claim would accept are offered. A ready work unit whose dependency chain is unresolved, or whose lane has no WIP headroom, is excluded from the report (dependency_blocked, lane_wip_exhausted in the counts) rather than named as “claim this first” — the dispatchable-share metric and this report evaluate one shared predicate, so they cannot disagree about what stops a claim.

The report prints its own cost as a trailing line (Shadow report cost: <n>ms over <n> WU records). It runs last in the claim preflight, after every non-mutating refusal and before any claim state is written, so a refused claim does not pay for it.

LumenFlow scores action efficiency against a baseline. It holds two halves per WU: the terminal outcome, and the sizing_estimate declared before work began. For each done WU with gate_attempt ledger evidence, flow:report, wu:status, and metrics:snapshot compute:

efficiency_d      = min(1, baseline_d / actual_d)   for d in {gate_attempts, commits, tokens}
efficiency         = geometric mean over AVAILABLE dimensions
trajectory_score   = completion x efficiency          completion in {0, 1}
  • gate_attempts is self-baselined from the WU’s own ledger — the ideal is exactly one attempt per distinct gate the WU ran.
  • commits and tokens are baselined from the wu-sizing-guide tier ceiling matching the WU’s declared sizing_estimate.strategy, or from the lane+type rolling median of other done WUs when no tiered estimate was declared (baseline_source: inferred).
  • A dimension with no actual value (for example no costs.ndjson rows for that WU) is omitted, never imputed.

A WU with no gate_attempt ledger evidence shows no efficiency line at all — this is advisory, fleet-level signal, not a gate: it is not wired into eval:run or cert:verify. See Trajectory Supervision for the classifier and redirect surface that produces the underlying ledger.

pnpm flow:report --days 30 --format table
│ <wu-id>   2026-08-21  Framework: Core  Append-only gate_att  eff=1.00 traj=1.00 baseline=tier

The gate_attempt ledger records per-gate cost; the heartbeat records which phase a claim was in; the wall time of the governed lifecycle commands themselves is recorded separately (wu:claim, wu:brief, wu:prep, wu:done, wu:rebase, wu:recover). Every invocation of one of those six commands now appends a lifecycle_duration event to the WU event ledger — command, wuId, start, duration_ms, and outcome (success/failure) — recorded directly by the shared command dispatch the heartbeat rides, never cadence-gated: one event per invocation, including every wu:prep retry.

Recording is OFF by default. Enable it per workspace with pnpm config:set --key software_delivery.wu.lifecycleDurations --value true, or per invocation with LUMENFLOW_LIFECYCLE_DURATIONS=1. With it off, no governed command writes a ledger file or lock directory. When on, the event goes to the gitignored journal of the main checkout, not the tracked ledger.

A governed command that fails writes its lifecycle_duration row with outcome: failure plus two extra fields: refusal_code (the failure’s typed code, else its error name, else UNTYPED_REFUSAL) and active_claims (how many claims were in progress on the ledger at that moment, omitted when the ledger cannot be read). The row schema is a loose object, so older readers ignore the fields. Every such failure also appends exactly one structured line to flow.log: message: "lifecycle_refusal", stage: "command", command, wuId, refusal_code, active_claims and detail.

The cadence heartbeat is separate. Its holder proof re-reads (up to three attempts) when the claim refs moved while it was reading them, so unrelated commits landing do not drop the liveness row. If the proof still refuses, no heartbeat is written and one lifecycle_refusal line with stage: "heartbeat" is appended to flow.log instead. Count refusals by refusal_code in either place.

pnpm metrics:snapshot also prints per-command wall time p50/p90 and gate admission queue-wait p50/p90 (queue_wait_ms on the gate-admission gate_attempt rows) over the --days window; --json adds the same figures under lifecycleLatency.

pnpm metrics:snapshot --type flow (and --type all) combines this new evidence with the existing heartbeat ledger to split each done WU’s claimed_at..completed_at span into named phases, led by claim_to_done (the whole span):

  • claim_to_first_commit — claim to the WU’s earliest attributed commit
  • implementation — heartbeat phase implement
  • prep_wall_incl_retries — sum of every wu-prep lifecycle_duration event
  • prep_to_done — the gap between the last wu-prep event and the first wu-done event (review-wait/idle time)
  • done — sum of every wu-done lifecycle_duration event
  • blocked — heartbeat phase hold

Median and p90 hours are reported per phase, both overall and grouped by the WU’s declared sizing_estimate.strategy (falling back to unspecified), alongside considered/matched/skipped population counts. A WU with no lifecycle_duration events is simply excluded from the prep/done/prep-to-done phases’ population rather than reporting a fabricated zero.

pnpm metrics:snapshot --type flow
│ CLAIM-TO-DONE BY PHASE, MEDIAN/P90                          │
│ Overall:
│   claim_to_first_commit     n=12 median=0.4h p90=1.1h
│   implementation            n=12 median=2.1h p90=5.8h
│   prep_wall_incl_retries    n=9  median=0.3h p90=0.9h
│   prep_to_done              n=9  median=0.2h p90=1.4h
│   done                      n=9  median=0.1h p90=0.3h
│   blocked                   n=12 median=0.0h p90=0.6h
│ Size band: checkpoint-resume (n=7)
│   ...
│   considered=13 matched=12 skipped=1

LumenFlow emits structured NDJSON telemetry in the software-delivery pack state directory. File names below are relative to that directory:

FilePurpose
telemetry/gates.ndjsonGate execution events (duration, pass/fail, WU, lane)
flow.logWU lifecycle events (wu:claim, wu:prep, wu:done)
telemetry/dora.ndjsonDORA metric records with canonical tag bag
telemetry/costs.ndjsonLLM cost events (model, tokens, USD)
telemetry/brief-metrics.ndjsonwu:brief prompt token reports by WU, client, section
telemetry/llm-classification.ndjsonLLM classification lifecycle events
telemetry/lane-signals.ndjsonLane signal events
telemetry/tools.ndjsonTool invocation events (metadata-only by default)
telemetry/methodology.ndjsonMethodology metrics
telemetry/prompt-lint.ndjsonPrompt-lint metrics
incidents/*.ndjsonIncident records (metadata-only by default)

See Full-stream telemetry routing above for which of these stream to the control plane and at what payload mode.

pnpm wu:brief --id <wu-id> --client <client> --report-tokens prints the same total and section-level token accounting it appends to brief-metrics.ndjson. The file uses schema-versioned control-plane event records and stays local by default; hosted, self-hosted, and third-party consumers are downstream of the same neutral event shape.

CommandDescription
pnpm metrics:snapshotCapture 5-metric DORA snapshot plus dispatchability and throughput
pnpm orchestrate:init-statusInitiative progress, including the ready work its claims withhold
pnpm flow:reportGenerate DORA + gate + WU flow report
pnpm flow:bottlenecksIdentify workflow bottlenecks and critical path
pnpm cost:summary --briefSummarize local wu:brief token metrics by WU, client, and date
  1. Review metrics weekly

    Schedule a weekly review of flow metrics to identify trends before they become problems.

  2. Set WIP limits only where needed

    If a lane is consistently at 100%+ capacity, consider splitting the lane, adding capacity, or reducing WU scope.

  3. Address blockers quickly

    Blocked WUs create cascading delays. Prioritize unblocking over new work.

  4. Track trends, not absolutes

    DORA research emphasises continuous improvement over hitting specific numbers. Watch the slope, not the intercept.