Repository navigation
feat(observability): record delivery outcomes and complete SDK usage metrics - #1010
Merged
Merged
Conversation
…metrics Execution metrics (src/execution/usage.ts): - SdkUsageObserver reduces the SDK message stream to ExecutionMetrics, reported as StageExecutionResult.metrics on every outcome: usage with inputTokens, outputTokens, cacheReadInputTokens, and cacheCreationInputTokens, where unknown is null and never 0, and a source (result, partial, recovered, none); turnCount from num_turns; toolUseCount from tool_use blocks, deduplicated by block id across streamed messages, with per-tool counts; model, SDK, and Claude Code versions. - Result usage is the sum of modelUsage (every model call). A zeroed error result is not a measurement, so failures, cancellations, and timeouts keep the partial usage of the assistant messages seen. - toolCallCount and tokenUsage keep their values and are deprecated; tokenUsage no longer drops partial totals on zeroed error results. Delivery metrics records (src/telemetry/deliveryMetrics.ts): - Strict zod schemas (schemaVersion 1) for attempt, stage, and pipeline records. Outcomes reuse PipelineStageStatus, PipelineStatus, and PIPELINE_EXIT_CODES; attempts add timed_out and cancelled. Stage and pipeline records project the #951 verification and #953 approval records without reasons or proposed actions. - The scheduler reports every attempt; DeliveryMetricsRecorder pairs it with the adapter observation (attempt id, SDK session, resume) and adds stage and pipeline records at the end of the run, also when the run ends with an unexpected error. Records are returned on PipelineResult.metrics. Export and report: - With telemetry consent granted under privacy policy 1.1.0, ad-sdlc run appends the records to .ad-sdlc/scratchpad/metrics/<session>.jsonl. A sink failure is logged and counted and never changes the run. - ad-sdlc telemetry report aggregates exports and an optional supplied review file; every metric states its source, sample, missing count, and denominator. Human-review metrics come only from the supplied file. No monetary estimate is built in. - telemetry status shows the export state; telemetry enable shows the policy again for consent recorded under 1.0.0. Tests use recorded SDK message sequences and a fixture corpus with hand-computed totals; no live SDK call is made.
Prettier only (table alignment, quotes, spacing); no content change. The pre-commit hook formats staged Markdown and these two files were never formatted, so a separate commit keeps the #956 content diff of the next commit reviewable.
- New docs/guides/delivery-metrics.md: record schema, attempt outcomes, unknown versus zero, tool-use counting, compatibility of the legacy fields, export location and consent, the report and its supplied review file, PRD-001 KPI mapping, privacy defaults, and how the live acceptance lanes read the records. - docs/telemetry.md: privacy policy 1.1.0 and the delivery metrics export; docs/cli.md: run output lines, telemetry report, completion. - CHANGELOG (Added, Changed, Deprecated), README CLI table, guides index, observability guide pointer, and the execution module README.
DeliveryMetricsRecorder.finish now logs and counts a stage or pipeline record that cannot be built instead of throwing, and the error path reads the session through getSession(), which a concurrent dispose() may have cleared. A metrics failure can no longer change a run's outcome or replace its error.
✅ Document Traceability: PASS
|
This was referenced Oct 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Every
ad-sdlc runnow records what each execution attempt cost and how the run ended, in versioned records (schemaVersion: 1) for each attempt, stage, and run. Tool invocations are counted from SDKtool_useblocks (deduplicated by block id) and kept apart from turns (num_turns). Failed, retried, timed-out, and cancelled attempts keep the usage they consumed, and usage the SDK did not report isnull, never a measured 0. The records are returned onPipelineResult.metricsand inrun --format json; with telemetry consent they are appended to.ad-sdlc/scratchpad/metrics/<session-id>.jsonl. The newad-sdlc telemetry reportaggregates the exports, plus an optional review file a person supplies, into a report where every metric states its source, sample, missing count, and denominator.The vocabulary reuses #951 and #953 instead of inventing parallel names: stage records carry
PipelineStageStatus, run records carryPipelineStatusand thePIPELINE_EXIT_CODESstatus, and the verification and approval summaries are projections ofStageVerificationRecord/PipelineVerificationSummaryandApprovalDecisionRecord/PipelineApprovalSummary. Only attempts addtimed_outandcancelled, which are not stage statuses.Related Issue
Closes #956
Parent: #944. #957 depends on this; #955 attaches measured live results through this report.
Type of Change
toolCallCountandtokenUsagedeprecated with unchanged values; privacy policy 1.1.0; see Compatibility belowChanges Made
SDK usage observation (
src/execution/usage.ts,SdkExecutionAdapter.ts,types.ts)SdkUsageObserverreduces the SDK message stream intoExecutionMetrics, reported as the new optionalStageExecutionResult.metricson every outcome:usage { inputTokens, outputTokens, cacheReadInputTokens, cacheCreationInputTokens, source },turnCount,toolUseCount,toolUses(per tool name),modelId,models,sdkVersion(installed package),claudeCodeVersion(system/init).result(sum ofmodelUsage, which covers every model call including subagents, elseusage),partial(no usable result: the distinct assistant messages seen so far, a lower bound),recovered(resume reused a committed manifest; no SDK call, so a measured 0),none(every fieldnull). A zeroed error result is not treated as a measurement, because the SDK writes zeroed usage on crash results; previously it replaced the partial totals with zeros.tool_use(andserver_tool_use,mcp_tool_use) blocks of assistant messages, deduplicated by block id across the per-block streamed messages that sharemessage.id; subagent blocks count;stream_eventpartials do not; names that are not identifiers count asother.Stophook is registered. The SDKStophook does not fire for failed or cancelled executions, so usage is finalized at adapter completion; the TODO inhooks.tsis replaced by that explanation.Records (
src/telemetry/deliveryMetrics.ts,src/ad-sdlc-orchestrator/deliveryMetrics.ts,StageScheduler.ts,AdsdlcOrchestratorAgent.ts)attempt,stage, andpipelinerecords; a record with any undefined field is rejected before it is written. Common fields:schemaVersion,recordedAt,sessionId(kept on resume),runId(one perexecutePipelinecall).retryIndex,attemptId(theArtifactContext.attemptId, the same identity as the feat(execution): persist structured artifact manifests from stage execution #950 manifest),sdkSessionId,sdkResume, timestamps and duration,outcome(succeeded | failed | timed_out | cancelled),errorType(code or class name, never the message),execution(sdk | recovered | unobserved), usage, turns, tool uses, models, versions, manifest reference.SchedulerHost.onAttempt;classifyAttemptOutcome()maps aStageTimeoutError(also behind a cleanup failure) totimed_outand pipeline cancellation orEXEC-005tocancelled. The orchestrator pairs the event with the adapter observation of that attempt. Attempt records are written as attempts end, so a killed run keeps them.PipelineStageStatus,restored(pre-completed on resume), retries, attempt counts per outcome, sums of usage, turns, and tool uses (usage: nullwithout an attempt),errorType, the feat(vnv): define explicit verification coverage and delivery gate policies #951 verification projection (state, check counts, failed required checks) and the feat(orchestrator): make approval policy explicit and reviewable #953 approval projection (outcome, basis, boundaries, effects, decidedBy, reused; no reason, proposed action, or scope).PipelineStatus, exit status,resume { resumed, source: checkpoint | session, priorStatus }(newOrchestratorSession.resumeSource/resumedFromStatus), stage counts, attempt counts and retries, usage sums including failed and cancelled attempts, models, SDK and package versions, feat(vnv): define explicit verification coverage and delivery gate policies #951 and feat(orchestrator): make approval policy explicit and reviewable #953 summary projections. A run that ends with an unexpected error still writes its attempts, stages, and afailedrun record witherrorType.Export and consent (
src/telemetry/deliveryMetricsExport.ts,Telemetry.ts,src/cli/runCommand.ts,src/cli.ts)OrchestratorConfig.deliveryMetrics { sink?, consent? }: records are always built; they are written only to the sink created for the session.JsonlDeliveryMetricsSinkappends validated JSON lines, one write at a time; a failed write is logged, counted inmetrics.export.failed, and never changes the run.ad-sdlc runreads the existing telemetry consent (Telemetry.getDeliveryMetricsConsent()): the JSONL export is enabled only with consent granted under privacy policy 1.1.0 or later. The policy version is bumped to 1.1.0 and lists the local export; consent recorded under 1.0.0 reportsoutdated,telemetry enableshows the policy again for it, andtelemetry statusprints whether runs export delivery metrics. No workflow key or environment variable is added.Usage:line (attempts per outcome, tokens withunknownfor unreported fields, tool uses, turns) and aMetrics:line (export file, or the consent state).RunCommandDependencies.deliveryMetricsConsentreplaces the consent lookup in tests.Report (
src/telemetry/deliveryMetricsReport.ts,src/cli/metricsReportCommand.ts)ad-sdlc telemetry report [files...] [--project-dir <dir>] [--session <id>] [--supplied <file>] [--format text|json]. Without files it reads.ad-sdlc/scratchpad/metrics/*.jsonl. Invalid lines are listed with file and line, duplicates are dropped, and it exits 1 without exports or with unreadable input.source(observed/supplied),sample,missing, anddenominator(count and description): completed, verified (feat(vnv): define explicit verification coverage and delivery gate policies #951), resumed, and resumed-after-decision run rates; requested and rejected decisions (feat(orchestrator): make approval policy explicit and reviewable #953); totals and means of duration, the four token fields, turns, and tool uses for all attempts, each outcome, and retried attempts, with apartialcount for lower bounds; accepted pull requests, human rework minutes, and requirement evidence coverage from the supplied file only (n/awithout it, never inferred from CI). No monetary estimate: no price table is built in.CLI_COMMANDSgainstelemetry reportwith avalueslist for--format;docs/cli.mddocuments it.Docs
docs/guides/delivery-metrics.md: record schema, outcomes, unknown versus zero, tool-use counting, compatibility, export and consent, report and supplied file, PRD-001 KPI mapping, privacy defaults, and how the live lanes (test(import): prove installed CLI delivery and resume through the real SDK #955, test(enhancement): validate document updates and regressions on an existing project #957) read the records.docs/telemetry.md(policy 1.1.0, delivery metrics section,getDeliveryMetricsConsent),docs/cli.md,docs/observability.md(pointer),docs/guides/README.md,README.mdCLI table,src/execution/README.md,CHANGELOG.md(Added, Changed, Deprecated).docs/telemetry.mdanddocs/observability.md, which were never formatted (the pre-commit hook formats staged Markdown), so the third commit's content diff stays reviewable. The fourth commit makesDeliveryMetricsRecorder.finishlog and count a record it cannot build instead of throwing, and reads the session in the error path throughgetSession(), so a metrics failure can never replace a run's error.Compatibility
StageExecutionResult.toolCallCountkeeps its value (the larger of distinct assistant messages andnum_turns) and is@deprecatedin favor ofmetrics.turnCountandmetrics.toolUseCount; the CHANGELOG says it counts turns, not tool invocations.tokenUsagekeeps its shape and main-loop source and is@deprecatedin favor ofmetrics.usage. One documented change: a failed execution whose result carries zeroed usage keeps the partial assistant totals instead of zeros (also in the manifest'sexecution.tokenUsage). A result withoutusageno longer fails the execution with a type error.PipelineResult.metricsis not saved in session state.execution) carryno-deprecateddisables with a reason.Testing
executeRunCommandand the report command; transcript in the evidence folder)New tests (54), all offline:
tests/execution/usage.test.ts(11) on recorded SDK sequences typed against the installedSDKMessageunion (tests/execution/fixtures/usageStreams.ts, checked bynpm run test:sdk-contract): success (modelUsage over two models), one message with severaltool_useblocks plus a re-emitted block (exactly 5), partial failure with a zeroed error result (partial usage kept), a failure result with measured usage, cancellation mid-stream (partial usage, unknown cache fields, no turn count), a result without usage (allnull), a measured zero, subagent and non-identifier tool names, ignored stream events, SDK version lookup.tests/execution/SdkExecutionAdapter.metrics.test.ts(7): the adapter reports metrics on success, multi-tool, partial failure, max-turns failure, cancellation through a controlled query, no usage, and loader failure; legacy fields keep their values.tests/ad-sdlc-orchestrator/deliveryMetrics.test.ts(9), production scheduler andSdkExecutionAdapterwith a controlled query and fake timers: a failed first attempt with partial usage next to the successful retry (stage and run sums include it; sink order), a timed-out attempt, an attempt cancelled by disposal, outcome classification, a failing sink, no sink or a sink that cannot be created, an adapter without metrics, a run ended by an unexpected error, a malformed stage result counted instead of thrown.tests/integration/delivery-metrics.integration.test.ts(2), scripted SDK through the real adapter, scheduler, approval gate, and verifier: Import run paused at a decision and resumed (both runs in one export, restored stage, resume source and prior status, tool uses fromtool_useblocks, report resume rate), a typed rejection; privacy scan of the export andPipelineResult.metricsfor a planted API key, GitHub token, requirement marker, tool inputs, the typed rejection reason, and the project path.tests/telemetry/deliveryMetricsReport.test.ts(9): the report on a fixture corpus (tests/telemetry/fixtures/delivery-metrics/: four sessions, five runs including a resumed and an unrecorded run, 14 attempts of every outcome, missing usage, one duplicate, a torn line, a rejected free-text record, a supplied review file) against totals computed by hand (table in the test header).tests/telemetry/deliveryMetrics.test.ts(8): schema,nullversus 0 through JSON, strict rejection of free text and unknown values, consent states (granted,outdated,pending,denied) including the real consent file, JSONL sink.tests/cli/deliveryMetrics.test.ts(8):ad-sdlc runexports with granted consent and writes nothing when consent is pending, denied, or outdated; text summary lines;ad-sdlc telemetry reporton project exports, one session, given files with a supplied file, and its failure exits; completion values.tests/completion/CompletionGenerator.test.tsfor thereportsubcommand.Test Results
Plain
git diff origin/develop...HEAD --checkreportstrailing whitespaceonly for the CR of added lines insrc/ad-sdlc-orchestrator/types.ts, which is stored with CRLF in the index on develop (git ls-files --eol:i/crlf), as in #1007 and #1009.Checklist
Additional Notes
ad-sdlc telemetry report. No credentials were used.Telemetry.isEnabled()is unchanged; it still requiresconfig.enabled, which the CLI never sets, sotelemetry statuskeeps printingEnabled: Nobeside the newDelivery metrics:line. That predates this change and is listed as a follow-up.docs/telemetry.mdstill documents a workflowtelemetry:block thatad-sdlc validaterejects as retired; that predates this change and is left for docs(product): align product promises and onboarding with verified runtime behavior #960.