rp-telemetry
Capture per-run telemetry that tells us what to improve in the skills and the underlying
Wix tooling. This skill stays active alongside the migration skills for the entire
run; the migration skills themselves are unchanged and know nothing about telemetry.
The one rule that governs everything you record
Observation, not diagnosis. Every field records what was observed — what happened,
what was expected, what actually occurred, where, and how often. Never record a root
cause, a fix, a recommendation, or a workaround-as-solution. There is deliberately no
field for them; the schema rejects unknown fields. Root cause is derived later, at review
time, by a reviewing agent with full context — not asserted by you in the moment.
Three discipline rules apply to every free-text field (what_happened, expected,
actual):
- Types, not instances. Refer to entity types and classes, never specific client
data.
entity_type: "product" — never a product's name, SKU, price, or body text.
- Secret-safe. No credentials, tokens, URLs, config values, or file contents — the
recorder also runs a mechanical scrub as a last line of defense, but do not rely on it.
- Short and structural. A sentence or two, max 400 chars. Prefer stating
expected-vs-actual over narrating. The coded fields carry the structure.
- No remediation narration. "The canonical lib was fixed and re-synced" is a fix
story, not an observation — even when true (dev-mode backports). Record what was
observed to work ("adding fieldsets=FULL returned the field; the lookup succeeded")
and leave the repair to the execution log.
The recorder
All telemetry is persisted through the bundled recorder — never hand-write or edit
run-telemetry.json or telemetry/ files. Run it from this resource directory
(see CONVENTIONS.md), pointing at the active migration project:
node scripts/rp-telemetry.js <command> [...] --project <abs path to migrations/<project>>
The recorder owns everything mechanical: schema validation (it rejects invalid enums and
unknown fields — fix and retry, never guess around it), timestamps, per-class event
folding, the run/attempt/session resume model, the privacy scrub, and the well-formedness
gate at finalize. It prints one JSON result per call; {"ok":false,...} lists exactly
what to fix.
| Call |
When |
start '<dims-json>' |
At run begin — before any other pipeline step. Resumes an unfinalized run automatically. |
dims '<dims-json>' |
Whenever a dimension becomes known mid-run (platform version and extensions after discovery, site_id after provisioning). |
stage start <stage> / stage end <stage> --outcome <outcome> |
At every stage boundary. Outcomes: passed, halted, failed, skipped. |
meter [--api-ms N --model-ms N --script-ms N --input-tokens N …] |
Measured latency/token counts for a stage. Call it whenever you have real numbers; see "Metering" below. |
wait start [--halt <subtype> --skill <s> [--what '<text>']] / wait end |
The moment the run halts for the user, and the moment it resumes. This is how user latency stays out of active_ms — never estimate elapsed time yourself. Pass --halt for a needs-user halt and the recorder emits the paired halt_needs_user event for you. |
record '<event-json>' |
The moment something observable happens (taxonomy below). |
finalize '<rollup-json>' |
When the run reaches a terminal state. |
rebuild [--attempt <n>] [--push] |
Only on request, to re-assemble a past run's signal document from its archived journal after a recorder fix — never during a run. --push re-emits the rebuilt run to the BI sink (backfill after an outage; idempotent — the reviewer dedupes at query time). |
status |
To orient after a resume. |
Run lifecycle
start as the first telemetry act of the run, with whatever dimensions are already
known:
node scripts/rp-telemetry.js start '{"source_platform":"wordpress",
"source_site_url":"https://client-site.example","source_acquisition":"public_storefront",
"delivery_mode":"management","destination_strategy":"new_site",
"runtime_env":{"agent_runtime":"claude-code","model":"<model id>"}}' --project <dir>
start on a project with an unfinalized run resumes it (same run, same attempt,
one more session) and closes any open wait interval. start after a finalized run
opens the next attempt. Never try to manage run identity yourself.
source_acquisition is an open set of class tokens, but reuse an established one
(admin_api, public_storefront, public_content, file_export) rather than
minting a synonym — cross-run folding depends on stable tokens.
Skills provenance is auto-stamped — you don't pass it. At start the recorder
stamps two identifiers onto the run so every record can be traced back to the skills
that produced it:
skills_version — the bundle's semver release label (from VERSION). Coarse and
hand-bumped by design; it is the group-by/order-by key that binds issues to a
release line ("all issues on 1.2.x", "regressed since 1.1.0").
skills_commit — the exact source commit the bundle was built from: the precise
vendored snapshot within a release. Many commits ship under one hand-bumped
skills_version, so a run reporting 1.0.0 is otherwise unattributable to the
change that produced its issue. The recorder resolves it, in order of authority,
from the sourceCommit stamped into .publish-manifest.json at publish time (the
only source that works in a partner runtime, and the only correct one once the
bundle is vendored into wix/skills), else a dev-mode git rev-parse --short HEAD
of the source checkout, else null.
Both may be overridden by passing skills_version / skills_commit in the start
dims (a runtime with better provenance than the recorder can infer), but normally you
leave them to auto-resolve.
Stage boundaries as the pipeline moves. Map orchestrator steps to stages like this:
| Stage |
Covers |
config |
Everything before discovery: project resolution, config files, up-front input collection |
discovery |
Source discovery (rp-discovery + source adapter) |
mcp_gate |
The Wix MCP prerequisite gate between discovery and mapping |
mapping |
rp-mapper producing the mapping plan |
mapping_review |
The mapping review checkpoint (user-facing) |
setup_discovery |
rp-setup-discovery |
codegen |
rp-import-codegen |
approval_gate |
The execution-plan approval gate (user-facing) |
setup_provisioning |
Site creation, app installs, collections — rp-execute-setup |
storefront_build |
The website-mode wix-headless build + release; skipped in management mode |
extract |
Source extraction to disk, before any write (rp-execute-import) |
import |
The import writes (rp-execute-import) |
finish |
Verification spot-checks, completion reporting, handoff |
Stages bind to when the work actually runs, not to their canonical order. If the
orchestration runs extraction early (e.g. extract + dry-run before the approval gate,
inside the codegen phase), close the open stage, bracket the extraction in its own
extract stage, then reopen — a stage may be entered more than once, and its
active_ms sums the entries. Booking a real extraction into codegen (and leaving
extract as a milliseconds-long token stage) is exactly the mis-attribution the
extract stage exists to prevent.
When the discovery stage ends, always stamp what it learned:
dims '{"source_platform_version":"...","source_extensions":["..."]}' — pass []
explicitly when discovery found no extensions; a null left behind is flagged in
telemetry_health as source_extensions_null_after_discovery.
wait start / wait end around every needs-user halt: credential requests, the
mapping-review checkpoint, the approval gate, any halt-to-needs-user. Pass the halt
class on the same call —
wait start --halt missing_input --skill rp-mapper --what "run halted at the mapping-review checkpoint awaiting approval"
— and the recorder emits the paired halt_needs_user event mechanically; a stalled
wait with no halt event in its stage is flagged in telemetry_health
(wait_without_halt_event:<stage>). If the session is about to end on a halt, leave
the wait open — the resume's start closes it, so overnight user latency lands in
waiting_ms where it belongs. If you resume real work while a wait is open (e.g.
investigating something before the user has answered), wait end first and
wait start again when you go back to waiting — active work must never be booked as
waiting.
record events as they happen (next section). Record in the moment, not
retrospectively — improvisation and halts are only reliably knowable when they occur.
finalize once, at a terminal state, with the rollup:
node scripts/rp-telemetry.js finalize '{"terminal_state":"completed",
"volumes":[{"entity_type":"product","target":"native","target_surface":"stores/v3",
"discovered":142,"planned":142,"attempted":142,"succeeded":139,"failed":3,
"skipped":0,"already_imported":0}],
"verification":[{"subject":"product","method":"query_back","checked":10,"passed":10,"failed":0}],
"operator_acceptance":"accepted"}' --project <dir>
volumes come from the execution artifacts (manifest, audit log, crosswalk):
planned and target/target_surface from the approved plan;
already_imported is crosswalk-skipped scope from a prior attempt — never fold it
into skipped.
- Include a volume row for every entity type discovery found in use — including
types excluded by user decision or with no clean Wix target (
discovered: N, skipped: N, target: none where no target exists, planned 0 or null). "What we
cannot or chose not to import" must be signal-layer arithmetic, never an evidence
dig — rows only for imported types silently erase the excluded half of the approved
plan.
verification makes the finish step's spot-checks countable. checked: 0 is an
honest "written but unverified" — report it rather than papering over it.
- Do not finalize a halt you expect the user to resume — leave the run unfinalized
with the wait open. Finalize with
halted_needs_user only when the run is genuinely
being closed out in a stalled state. A user who declines the plan is
abandoned_by_user (plus a user_decision event with subtype: declined), never
halted_needs_user or failed.
Metering — splitting active_ms into where the time actually went
active_ms alone cannot locate a bottleneck: it is wall-clock between two stage boundaries, fusing
model reasoning + subprocess execution + remote API latency + defect-repair time into one
number. meter splits it, and cost is derived from the token counts.
# a generated script reporting its own measured work
node scripts/rp-telemetry.js meter --api-ms 250000 --api-calls 73 --api-retries 2 --script-ms 1200 --project <dir>
# the agent runtime reporting its own usage for the stage it just finished
node scripts/rp-telemetry.js meter --model-ms 42000 --input-tokens 8000 --output-tokens 1500 --cache-read-tokens 120000 --project <dir>
Derive the API half mechanically — do not hand-count it. After a generated import runs:
node scripts/meter-from-audit.js --project <migration dir> --stage import # --dry-run to preview
It reads logs/import-audit.ndjson and emits measured api_ms / api_calls / api_retries.
It exists because a bulk write logs one audit row per item, each carrying the batch's total
latency — summing rows multiplies one call's latency by its item count, which on a real run
reported 1,050,992 ms of API time inside a stage that only existed for 346,505 ms. The helper
collapses rows per distinct (runId, endpoint, batch, latencyMs), so a batch counts once while
genuinely separate calls each count.
Fields: durations model_ms / api_ms / script_ms; counts input_tokens, output_tokens,
cache_read_tokens, cache_write_tokens, api_calls, api_retries. Defaults to the open stage;
--stage <stage> targets another. Repeated calls accumulate, so each script invocation and each
stage re-entry reports independently.
Rules that keep the numbers trustworthy:
- Measure, never estimate. Every field is a number something actually observed — a script's own
elapsed time, an audit log's summed
latencyMs, the runtime's reported usage. If you do not have
a measurement, omit the field; the gap is reported as unattributed_ms and flagged, which is far
more useful than a guess.
- Do not meter the same interval twice. Meters sum, so re-reporting a stage's API time after a
resume double-counts it. The recorder flags
stage_over_attributed:<stage> when attributed time
exceeds elapsed time, because unattributed_ms clamps at zero and would otherwise hide it.
- Cost is derived, never recorded. Pass
model_pricing_snapshot in the start dims
({ "<model>": { "input_per_mtok": 15, "output_per_mtok": 75, "cache_read_per_mtok": 1.5 } }) and
the rollup computes cost.estimated_cost_usd. A stored dollar figure silently goes wrong when list
prices change; tokens plus a dated snapshot stay recomputable. Without a snapshot, cost is null
with cost_basis: "no_pricing_snapshot" — never a fabricated number.
contained_recovery is derived, not self-reported. Any error or pipeline_defect in a stage
marks it, because a stage that spent its time debugging is the one an agent is least likely to
remember to flag. timing.stages_with_recovery lists them, so a clean run and a thrash are
distinguishable instead of both reading as "this is what the stage costs".
The rollup's timing.agentic_ms vs timing.deterministic_ms (and agentic_share) is the number
that shows whether moving work into deterministic code is paying off. agentic_share is null when
nothing was metered rather than 0, which would falsely read as a fully deterministic run.
Collecting operator_acceptance
At the finish handoff, ask the operator one plain question: does the migrated result look
right to them — accepted, rework_needed, or rejected? Record their answer in
finalize. If the run never reaches finish or they do not answer, it stays unknown.
This is the only field separating "completed and good" from "completed and unusable" —
ask it, but never pressure or interpret; their verdict as given, coarse by design.
Event taxonomy — when to record what
Events are folded per problem class by the recorder (same type + stage + entity type +
API surfaces + app + error code + subtype fold into one event with a count), so record
every occurrence class once and pass count when you observed many at once — e.g. a
generated script reporting 4,000 identical write failures is one record call with
"count": 4000. Distinct problems are distinct subtypes or error codes, not bigger
counts.
Every event needs: event_type, stage (defaults to the open stage), skill (the
active rp-* resource, e.g. "rp-mapper"), severity (blocking | degraded |
cosmetic | info), and what_happened. Add entity_type, wix_api_surface (e.g.
stores/v3), source_api_surface (endpoint class like wp/v2/posts — never a URL),
wix_app_id, and error_code whenever they apply — they are the cross-run group-by keys.
halt_needs_user — the run stopped at a defined needs-user state. Prefer the
mechanical form — wait start --halt <subtype> … emits it for you (lifecycle step 3);
record it yourself only for a halt with no wait interval. subtype: missing_input
(missing/invalid required input or credential) | manual_only (genuinely manual step,
no API) | systemic_failure (systemic failure or data-loss risk).
manual_action_required — the execution plan flagged something as "can't be done,
needs manual action" (e.g. a storage-plan upgrade), whether or not the run halted on
it. subtype: plan_or_billing | dashboard_only | external_dependency | other.
error — an API or script error, including recovered ones. Carry error_code
(required — it is the discriminator; no subtype), retry_count, and recovered.
Record one event per resolved retry chain, after the outcome is known — the final
recovered, the total retry_count — never one call per attempt (per-attempt calls
fold into one class, and the failing first attempt's fields would bury the recovery).
When recovery required a change (not just a retry), actual must record what
change made it succeed, as an observation: "retried with the description as plain
text instead of HTML; the write succeeded" — never "the fix is X". If that change
deviated from the documented path — including a change to a planned command or
value, like simplifying an input the plan specified — also emit a paired
skill_coverage_gap (undocumented_workaround).
fidelity_loss — the migration technically proceeded but lost something.
subtype: dropped_field | unverified_enum | no_target (source entity with no
clean Wix target) | coerced_value. For a loss triggered by specific records, inline
the offending record's sanitized shape in observed_shapes — field names and
types and which field was dropped/coerced, never values.
api_gap — a missing or insufficient Wix API capability. subtype:
missing_api | missing_capability (API exists but cannot express the operation) |
internal_only (capability exists but is not publicly exposed) | other. Carry
wix_api_surface (required) and error_code — that signature is matched centrally
across runs. Never write to any backlog file; the event is the whole per-run
obligation.
skill_coverage_gap — the standing self-report, and the highest-value signal
for improving the skills. Whenever you act beyond what the active skill explicitly
told you to do — you guess a value, work around a missing instruction, resolve an
ambiguity by judgment, or handle a case the skill does not cover — record it at that
moment. subtype: guessed_value | undocumented_workaround |
ambiguous_instruction | path_not_covered. Describe the situation and the action
taken as an observation: "skill did not specify which enum value maps to X, so a value
was chosen to proceed" — never "the skill should add Y". If you are unsure whether
something counts as improvisation, it does — record it.
user_decision — the user's answer at every defined checkpoint or fork: the
mapping-review checkpoint, the approval gate, and intake forks (comments
anonymization, delivery mode, media reachability, …). subtype: accepted |
declined | deferred | amended; decision_point (required) names the fork, e.g.
mapping_review, approval_gate, comments_anonymization. Record the decision, not
the user's reasoning verbatim. An amended acceptance whose change exposed a mapping
problem additionally surfaces that problem as fidelity_loss or
skill_coverage_gap. A declined at a terminal checkpoint pairs with
terminal_state: abandoned_by_user.
pipeline_defect — our own migration machinery misbehaved: not a Wix API error,
not a source failure, not your improvisation. subtype: state_inconsistency (two
pipeline state artifacts disagree) | ordering_violation (a step ran before its
prerequisite's output existed) | record_defect (a pipeline record/log is malformed
or misleading as evidence) | other. Name the artifacts and their states in
what_happened — names and states, never contents.
evidence_refs — rare, not routine
Author every event to be self-sufficient: coded fields + bounded free text +
observed_shapes should let a reviewer triage it without opening any artifact. Add
evidence_refs ({"artifact":"execution-log.md","locator":"## Import"}) only for the
exceptional event whose signal genuinely cannot carry the full observation. Use
project-root-relative paths and prefer section headings as locators (line ranges break
when files regenerate). Never reference secret-bearing config files.
What never to do
- Never hand-write, edit, or re-read
run-telemetry.json or telemetry/ files — the
recorder appends; you only call it.
- Never record client data values, names, URLs beyond the recorded source origin, or
secrets — in any field, including shapes and locators.
- Never record a fix, root cause, or recommendation — observations only.
- Never estimate durations — timing comes from
stage/wait boundary calls.
- Never skip a rejected call: fix the listed fields and retry. Rejections are counted
against telemetry health either way.
- Never create or update improvement/feature-request backlog files during a run — those
are maintained centrally from many runs' telemetry, not per run.
- Never delete or trim project artifacts for telemetry reasons — capture is a
non-destructive view; size is not a reason to drop anything.
1---2name: rp-telemetry-23description: Always-active telemetry companion for RePlatform migration runs. Records what happened during a run — halts, errors, fidelity losses, API gaps, skill coverage gaps, user decisions, pipeline defects — through a validated recorder script, plus the run rollup (stages, timings, volumes, verification). Loaded by the orchestrator at run start and kept active for the whole run.4---56# rp-telemetry78Capture per-run telemetry that tells us what to improve in the skills and the underlying9Wix tooling. This skill stays active alongside the migration skills for the **entire**10run; the migration skills themselves are unchanged and know nothing about telemetry.1112## The one rule that governs everything you record1314**Observation, not diagnosis.** Every field records what was observed — what happened,15what was expected, what actually occurred, where, and how often. Never record a root16cause, a fix, a recommendation, or a workaround-as-solution. There is deliberately no17field for them; the schema rejects unknown fields. Root cause is derived later, at review18time, by a reviewing agent with full context — not asserted by you in the moment.1920Three discipline rules apply to every free-text field (`what_happened`, `expected`,21`actual`):2223- **Types, not instances.** Refer to entity *types* and *classes*, never specific client24 data. `entity_type: "product"` — never a product's name, SKU, price, or body text.25- **Secret-safe.** No credentials, tokens, URLs, config values, or file contents — the26 recorder also runs a mechanical scrub as a last line of defense, but do not rely on it.27- **Short and structural.** A sentence or two, max 400 chars. Prefer stating28 expected-vs-actual over narrating. The coded fields carry the structure.29- **No remediation narration.** "The canonical lib was fixed and re-synced" is a fix30 story, not an observation — even when true (dev-mode backports). Record what was31 observed to work ("adding fieldsets=FULL returned the field; the lookup succeeded")32 and leave the repair to the execution log.3334## The recorder3536All telemetry is persisted through the bundled recorder — **never hand-write or edit37`run-telemetry.json` or `telemetry/` files.** Run it from this resource directory38(see `CONVENTIONS.md`), pointing at the active migration project:3940```bash41node scripts/rp-telemetry.js <command> [...] --project <abs path to migrations/<project>>42```4344The recorder owns everything mechanical: schema validation (it rejects invalid enums and45unknown fields — fix and retry, never guess around it), timestamps, per-class event46folding, the run/attempt/session resume model, the privacy scrub, and the well-formedness47gate at finalize. It prints one JSON result per call; `{"ok":false,...}` lists exactly48what to fix.4950| Call | When |51|---|---|52| `start '<dims-json>'` | At run begin — before any other pipeline step. Resumes an unfinalized run automatically. |53| `dims '<dims-json>'` | Whenever a dimension becomes known mid-run (platform version and extensions after discovery, `site_id` after provisioning). |54| `stage start <stage>` / `stage end <stage> --outcome <outcome>` | At every stage boundary. Outcomes: `passed`, `halted`, `failed`, `skipped`. |55| `meter [--api-ms N --model-ms N --script-ms N --input-tokens N …]` | Measured latency/token counts for a stage. Call it whenever you have real numbers; see "Metering" below. |56| `wait start [--halt <subtype> --skill <s> [--what '<text>']]` / `wait end` | The moment the run halts for the user, and the moment it resumes. This is how user latency stays out of `active_ms` — never estimate elapsed time yourself. Pass `--halt` for a needs-user halt and the recorder emits the paired `halt_needs_user` event for you. |57| `record '<event-json>'` | The moment something observable happens (taxonomy below). |58| `finalize '<rollup-json>'` | When the run reaches a terminal state. |59| `rebuild [--attempt <n>] [--push]` | Only on request, to re-assemble a past run's signal document from its archived journal after a recorder fix — never during a run. `--push` re-emits the rebuilt run to the BI sink (backfill after an outage; idempotent — the reviewer dedupes at query time). |60| `status` | To orient after a resume. |6162### Run lifecycle63641. **`start`** as the first telemetry act of the run, with whatever dimensions are already65 known:6667 ```bash68 node scripts/rp-telemetry.js start '{"source_platform":"wordpress",69 "source_site_url":"https://client-site.example","source_acquisition":"public_storefront",70 "delivery_mode":"management","destination_strategy":"new_site",71 "runtime_env":{"agent_runtime":"claude-code","model":"<model id>"}}' --project <dir>72 ```7374 `start` on a project with an unfinalized run **resumes** it (same run, same attempt,75 one more session) and closes any open wait interval. `start` after a finalized run76 opens the next attempt. Never try to manage run identity yourself.7778 `source_acquisition` is an open set of class tokens, but reuse an established one79 (`admin_api`, `public_storefront`, `public_content`, `file_export`) rather than80 minting a synonym — cross-run folding depends on stable tokens.8182 **Skills provenance is auto-stamped — you don't pass it.** At `start` the recorder83 stamps two identifiers onto the run so every record can be traced back to the skills84 that produced it:8586 - `skills_version` — the bundle's semver release label (from `VERSION`). Coarse and87 hand-bumped by design; it is the group-by/order-by key that binds issues to a88 *release line* ("all issues on 1.2.x", "regressed since 1.1.0").89 - `skills_commit` — the exact source commit the bundle was built from: the precise90 *vendored snapshot* within a release. Many commits ship under one hand-bumped91 `skills_version`, so a run reporting `1.0.0` is otherwise unattributable to the92 change that produced its issue. The recorder resolves it, in order of authority,93 from the `sourceCommit` stamped into `.publish-manifest.json` at publish time (the94 only source that works in a partner runtime, and the only correct one once the95 bundle is vendored into `wix/skills`), else a dev-mode `git rev-parse --short HEAD`96 of the source checkout, else `null`.9798 Both may be overridden by passing `skills_version` / `skills_commit` in the `start`99 dims (a runtime with better provenance than the recorder can infer), but normally you100 leave them to auto-resolve.1011022. **Stage boundaries** as the pipeline moves. Map orchestrator steps to stages like this:103104 | Stage | Covers |105 |---|---|106 | `config` | Everything before discovery: project resolution, config files, up-front input collection |107 | `discovery` | Source discovery (`rp-discovery` + source adapter) |108 | `mcp_gate` | The Wix MCP prerequisite gate between discovery and mapping |109 | `mapping` | `rp-mapper` producing the mapping plan |110 | `mapping_review` | The mapping review checkpoint (user-facing) |111 | `setup_discovery` | `rp-setup-discovery` |112 | `codegen` | `rp-import-codegen` |113 | `approval_gate` | The execution-plan approval gate (user-facing) |114 | `setup_provisioning` | Site creation, app installs, collections — `rp-execute-setup` |115 | `storefront_build` | The `website`-mode `wix-headless` build + release; `skipped` in `management` mode |116 | `extract` | Source extraction to disk, before any write (`rp-execute-import`) |117 | `import` | The import writes (`rp-execute-import`) |118 | `finish` | Verification spot-checks, completion reporting, handoff |119120 Stages bind to **when the work actually runs**, not to their canonical order. If the121 orchestration runs extraction early (e.g. extract + dry-run before the approval gate,122 inside the codegen phase), close the open stage, bracket the extraction in its own123 `extract` stage, then reopen — a stage may be entered more than once, and its124 `active_ms` sums the entries. Booking a real extraction into `codegen` (and leaving125 `extract` as a milliseconds-long token stage) is exactly the mis-attribution the126 `extract` stage exists to prevent.127128 When the `discovery` stage ends, **always** stamp what it learned:129 `dims '{"source_platform_version":"...","source_extensions":["..."]}'` — pass `[]`130 explicitly when discovery found no extensions; a null left behind is flagged in131 `telemetry_health` as `source_extensions_null_after_discovery`.1321333. **`wait start` / `wait end`** around every needs-user halt: credential requests, the134 mapping-review checkpoint, the approval gate, any halt-to-needs-user. Pass the halt135 class on the same call —136 `wait start --halt missing_input --skill rp-mapper --what "run halted at the mapping-review checkpoint awaiting approval"`137 — and the recorder emits the paired `halt_needs_user` event mechanically; a stalled138 wait with no halt event in its stage is flagged in `telemetry_health`139 (`wait_without_halt_event:<stage>`). If the session is about to end on a halt, leave140 the wait open — the resume's `start` closes it, so overnight user latency lands in141 `waiting_ms` where it belongs. If you resume real work while a wait is open (e.g.142 investigating something before the user has answered), `wait end` first and143 `wait start` again when you go back to waiting — active work must never be booked as144 waiting.1451464. **`record`** events as they happen (next section). Record in the moment, not147 retrospectively — improvisation and halts are only reliably knowable when they occur.1481495. **`finalize`** once, at a terminal state, with the rollup:150151 ```bash152 node scripts/rp-telemetry.js finalize '{"terminal_state":"completed",153 "volumes":[{"entity_type":"product","target":"native","target_surface":"stores/v3",154 "discovered":142,"planned":142,"attempted":142,"succeeded":139,"failed":3,155 "skipped":0,"already_imported":0}],156 "verification":[{"subject":"product","method":"query_back","checked":10,"passed":10,"failed":0}],157 "operator_acceptance":"accepted"}' --project <dir>158 ```159160 - `volumes` come from the execution artifacts (manifest, audit log, crosswalk):161 `planned` and `target`/`target_surface` from the **approved** plan;162 `already_imported` is crosswalk-skipped scope from a prior attempt — never fold it163 into `skipped`.164 - Include a volume row for **every** entity type discovery found in use — including165 types excluded by user decision or with no clean Wix target (`discovered: N,166 skipped: N`, `target: none` where no target exists, `planned` 0 or null). "What we167 cannot or chose not to import" must be signal-layer arithmetic, never an evidence168 dig — rows only for imported types silently erase the excluded half of the approved169 plan.170 - `verification` makes the finish step's spot-checks countable. `checked: 0` is an171 honest "written but unverified" — report it rather than papering over it.172 - Do **not** finalize a halt you expect the user to resume — leave the run unfinalized173 with the wait open. Finalize with `halted_needs_user` only when the run is genuinely174 being closed out in a stalled state. A user who declines the plan is175 `abandoned_by_user` (plus a `user_decision` event with `subtype: declined`), never176 `halted_needs_user` or `failed`.177178### Metering — splitting `active_ms` into where the time actually went179180`active_ms` alone cannot locate a bottleneck: it is wall-clock between two stage boundaries, fusing181**model reasoning + subprocess execution + remote API latency + defect-repair time** into one182number. `meter` splits it, and cost is derived from the token counts.183184```bash185# a generated script reporting its own measured work186node scripts/rp-telemetry.js meter --api-ms 250000 --api-calls 73 --api-retries 2 --script-ms 1200 --project <dir>187# the agent runtime reporting its own usage for the stage it just finished188node scripts/rp-telemetry.js meter --model-ms 42000 --input-tokens 8000 --output-tokens 1500 --cache-read-tokens 120000 --project <dir>189```190191**Derive the API half mechanically — do not hand-count it.** After a generated import runs:192193```bash194node scripts/meter-from-audit.js --project <migration dir> --stage import # --dry-run to preview195```196197It reads `logs/import-audit.ndjson` and emits measured `api_ms` / `api_calls` / `api_retries`.198It exists because a bulk write logs **one audit row per item, each carrying the batch's total199latency** — summing rows multiplies one call's latency by its item count, which on a real run200reported 1,050,992 ms of API time inside a stage that only existed for 346,505 ms. The helper201collapses rows per distinct `(runId, endpoint, batch, latencyMs)`, so a batch counts once while202genuinely separate calls each count.203204Fields: durations `model_ms` / `api_ms` / `script_ms`; counts `input_tokens`, `output_tokens`,205`cache_read_tokens`, `cache_write_tokens`, `api_calls`, `api_retries`. Defaults to the open stage;206`--stage <stage>` targets another. Repeated calls **accumulate**, so each script invocation and each207stage re-entry reports independently.208209Rules that keep the numbers trustworthy:210211- **Measure, never estimate.** Every field is a number something actually observed — a script's own212 elapsed time, an audit log's summed `latencyMs`, the runtime's reported usage. If you do not have213 a measurement, omit the field; the gap is reported as `unattributed_ms` and flagged, which is far214 more useful than a guess.215- **Do not meter the same interval twice.** Meters sum, so re-reporting a stage's API time after a216 resume double-counts it. The recorder flags `stage_over_attributed:<stage>` when attributed time217 exceeds elapsed time, because `unattributed_ms` clamps at zero and would otherwise hide it.218- **Cost is derived, never recorded.** Pass `model_pricing_snapshot` in the `start` dims219 (`{ "<model>": { "input_per_mtok": 15, "output_per_mtok": 75, "cache_read_per_mtok": 1.5 } }`) and220 the rollup computes `cost.estimated_cost_usd`. A stored dollar figure silently goes wrong when list221 prices change; tokens plus a dated snapshot stay recomputable. Without a snapshot, cost is `null`222 with `cost_basis: "no_pricing_snapshot"` — never a fabricated number.223- **`contained_recovery` is derived, not self-reported.** Any `error` or `pipeline_defect` in a stage224 marks it, because a stage that spent its time debugging is the one an agent is least likely to225 remember to flag. `timing.stages_with_recovery` lists them, so a clean run and a thrash are226 distinguishable instead of both reading as "this is what the stage costs".227228The rollup's `timing.agentic_ms` vs `timing.deterministic_ms` (and `agentic_share`) is the number229that shows whether moving work into deterministic code is paying off. `agentic_share` is `null` when230nothing was metered rather than `0`, which would falsely read as a fully deterministic run.231232### Collecting `operator_acceptance`233234At the finish handoff, ask the operator one plain question: does the migrated result look235right to them — `accepted`, `rework_needed`, or `rejected`? Record their answer in236`finalize`. If the run never reaches finish or they do not answer, it stays `unknown`.237This is the only field separating "completed and good" from "completed and unusable" —238ask it, but never pressure or interpret; their verdict as given, coarse by design.239240## Event taxonomy — when to record what241242Events are folded per problem class by the recorder (same type + stage + entity type +243API surfaces + app + error code + subtype fold into one event with a `count`), so record244every occurrence class once and pass `count` when you observed many at once — e.g. a245generated script reporting 4,000 identical write failures is **one** `record` call with246`"count": 4000`. Distinct problems are distinct subtypes or error codes, not bigger247counts.248249Every event needs: `event_type`, `stage` (defaults to the open stage), `skill` (the250active `rp-*` resource, e.g. `"rp-mapper"`), `severity` (`blocking` | `degraded` |251`cosmetic` | `info`), and `what_happened`. Add `entity_type`, `wix_api_surface` (e.g.252`stores/v3`), `source_api_surface` (endpoint class like `wp/v2/posts` — never a URL),253`wix_app_id`, and `error_code` whenever they apply — they are the cross-run group-by keys.2542551. **`halt_needs_user`** — the run stopped at a defined needs-user state. Prefer the256 mechanical form — `wait start --halt <subtype> …` emits it for you (lifecycle step 3);257 record it yourself only for a halt with no wait interval. `subtype`: `missing_input`258 (missing/invalid required input or credential) | `manual_only` (genuinely manual step,259 no API) | `systemic_failure` (systemic failure or data-loss risk).2602612. **`manual_action_required`** — the execution plan flagged something as "can't be done,262 needs manual action" (e.g. a storage-plan upgrade), whether or not the run halted on263 it. `subtype`: `plan_or_billing` | `dashboard_only` | `external_dependency` | `other`.2642653. **`error`** — an API or script error, including recovered ones. Carry `error_code`266 (required — it is the discriminator; no subtype), `retry_count`, and `recovered`.267 Record **one event per resolved retry chain**, after the outcome is known — the final268 `recovered`, the total `retry_count` — never one call per attempt (per-attempt calls269 fold into one class, and the failing first attempt's fields would bury the recovery).270 When recovery required a *change* (not just a retry), `actual` must record **what271 change made it succeed**, as an observation: "retried with the description as plain272 text instead of HTML; the write succeeded" — never "the fix is X". If that change273 deviated from the documented path — including a change to a *planned command or274 value*, like simplifying an input the plan specified — also emit a paired275 `skill_coverage_gap` (`undocumented_workaround`).2762774. **`fidelity_loss`** — the migration technically proceeded but lost something.278 `subtype`: `dropped_field` | `unverified_enum` | `no_target` (source entity with no279 clean Wix target) | `coerced_value`. For a loss triggered by specific records, inline280 the offending record's **sanitized shape** in `observed_shapes` — field names and281 types and which field was dropped/coerced, never values.2822835. **`api_gap`** — a missing or insufficient Wix API capability. `subtype`:284 `missing_api` | `missing_capability` (API exists but cannot express the operation) |285 `internal_only` (capability exists but is not publicly exposed) | `other`. Carry286 `wix_api_surface` (required) and `error_code` — that signature is matched centrally287 across runs. **Never write to any backlog file; the event is the whole per-run288 obligation.**2892906. **`skill_coverage_gap`** — **the standing self-report, and the highest-value signal291 for improving the skills.** Whenever you act beyond what the active skill explicitly292 told you to do — you guess a value, work around a missing instruction, resolve an293 ambiguity by judgment, or handle a case the skill does not cover — record it **at that294 moment**. `subtype`: `guessed_value` | `undocumented_workaround` |295 `ambiguous_instruction` | `path_not_covered`. Describe the situation and the action296 taken as an observation: "skill did not specify which enum value maps to X, so a value297 was chosen to proceed" — never "the skill should add Y". If you are unsure whether298 something counts as improvisation, it does — record it.2993007. **`user_decision`** — the user's answer at every defined checkpoint or fork: the301 mapping-review checkpoint, the approval gate, and intake forks (comments302 anonymization, delivery mode, media reachability, …). `subtype`: `accepted` |303 `declined` | `deferred` | `amended`; `decision_point` (required) names the fork, e.g.304 `mapping_review`, `approval_gate`, `comments_anonymization`. Record the decision, not305 the user's reasoning verbatim. An `amended` acceptance whose change exposed a mapping306 problem additionally surfaces that problem as `fidelity_loss` or307 `skill_coverage_gap`. A `declined` at a terminal checkpoint pairs with308 `terminal_state: abandoned_by_user`.3093108. **`pipeline_defect`** — our own migration machinery misbehaved: not a Wix API error,311 not a source failure, not your improvisation. `subtype`: `state_inconsistency` (two312 pipeline state artifacts disagree) | `ordering_violation` (a step ran before its313 prerequisite's output existed) | `record_defect` (a pipeline record/log is malformed314 or misleading as evidence) | `other`. Name the artifacts and their states in315 `what_happened` — names and states, never contents.316317### `evidence_refs` — rare, not routine318319Author every event to be **self-sufficient**: coded fields + bounded free text +320`observed_shapes` should let a reviewer triage it without opening any artifact. Add321`evidence_refs` (`{"artifact":"execution-log.md","locator":"## Import"}`) only for the322exceptional event whose signal genuinely cannot carry the full observation. Use323project-root-relative paths and prefer section headings as locators (line ranges break324when files regenerate). Never reference secret-bearing config files.325326## What never to do327328- Never hand-write, edit, or re-read `run-telemetry.json` or `telemetry/` files — the329 recorder appends; you only call it.330- Never record client data values, names, URLs beyond the recorded source origin, or331 secrets — in any field, including shapes and locators.332- Never record a fix, root cause, or recommendation — observations only.333- Never estimate durations — timing comes from `stage`/`wait` boundary calls.334- Never skip a rejected call: fix the listed fields and retry. Rejections are counted335 against telemetry health either way.336- Never create or update improvement/feature-request backlog files during a run — those337 are maintained centrally from many runs' telemetry, not per run.338- Never delete or trim project artifacts for telemetry reasons — capture is a339 non-destructive view; size is not a reason to drop anything.