@letitcurl/harness-ingestor
v0.1.17
Published
Resumable Claude Code and Codex transcript importer for Measurement Platform, with adaptive uploads and performance reporting
Maintainers
Readme
Harness Ingestor
Import the current OS user's Claude Code and Codex JSONL transcripts into Measurement Platform, one JSON record per event, ready for PostgreSQL/Metabase queries. CLI usage comes first. App sessions are included only when they share the same supported local files. Antigravity and separate app database integrations are deferred.
Requires Node.js 24+ on macOS, Windows or Linux. Git is optional; absent Git/project configuration is recorded as unavailable. This is a one-shot command, not a watcher.
Quick start (published package)
npx @letitcurl/harness-ingestor@latest --help
npx @letitcurl/harness-ingestor@latest --days 7 --dry-run
npx @letitcurl/harness-ingestor@latest --days 7The last command prompts for a hidden Platform token and asks for confirmation. Full private transcript content is uploaded after consent; review the destination and categories first. No npm publishing token is needed to run the public package.
Run locally
Quick help: npx @letitcurl/harness-ingestor@latest --help.
Complete offline manual: npx @letitcurl/harness-ingestor@latest --manual
or read MANUAL.md. From this checkout, use
npm run local-test -- --help or npm run local-test -- --manual.
Unix man page
The build uses remark-man to generate a
section-one page from MANUAL.md, bundled at dist/man/man1/harness-ingestor.1.
From the checkout, run npm run build, then:
MANPATH="$PWD/dist/man:" man harness-ingestorAfter a published release, a global installation can use:
npm install --global @letitcurl/harness-ingestor@latest
MANPATH="$(npm root --global)/@letitcurl/harness-ingestor/dist/man:" man harness-ingestorAdd that directory to your shell's MANPATH to use bare man harness-ingestor.
The trailing colon preserves the system's default manual search path.
Older npm versions may register the package's man field automatically; npm 12
no longer does, so the explicit path works independently of registration.
No installer hook modifies shell configuration or system directories.
Native Windows shells generally lack Unix man; use --manual or run man in WSL.
Running via npx alone does not install a persistent system man page.
Options
--time-anchor now|latest-record (or --time-anchor=value) controls where the
lookback window ends. now is the default. latest-record requires --days and
uses each provider's newest valid effective timestamp at or before invocation time.
npm run local-test -- --days 7 --time-anchor=latest-record --dry-runClaude Code and Codex get independent historical windows, so older Claude history can be selected alongside newer Codex history. Windows are inclusive and fixed from captured snapshots. Timestamp fallback remains record → session → file mtime; filesystem creation time is not used. Future or invalid/oversized records cannot establish anchors. A provider with no eligible records selects nothing.
Confirmed records still determine historical anchors, but are skipped for uploading. Repeating an unchanged run sends no transcripts; new history advances the window and only unconfirmed records within it upload. Late arrivals inside the window are included. Wall-clock time alone does not move a historical anchor. Performance reports may still be sent after consent. Changing anchor mode does not reset resume.
The preview shows provider windows, counts and earliest pending timestamps. Dry-run
does not read authenticated resume state: confirmed/pending counts are not checked.
Historical mode adds a validation pass over snapshots; it is not a faster scan.
Performance selection metadata includes time_anchor, invoked_at, and
provider_windows; shared since/until are null in historical mode.
| Option | Meaning |
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| --days N, --days=N | Positive whole number of rolling 24-hour days; omitted means all time |
| --time-anchor now, --time-anchor=latest-record | End at invocation time (default), or each provider's latest eligible record; historical mode requires days |
| --blazingly-fast | Remove the default adaptive ceilings of 20 requests/second and 8 concurrent requests |
| --dry-run | Read/validate/count supported local records without authentication or network |
| --theme spaceship, --theme=default | Spaceship by default; original dashboard remains available |
| --ascii | Use ASCII decorations |
| --help | Print inline command, environment and exit-code reference |
| --manual | Print the bundled offline manual |
| --version | Print installed version |
Informational flags must be used alone. Duplicate or unknown options are errors.
--blazingly-fast keeps gradual ramp-up and error/offline backoff while removing the
request-rate and concurrency ceilings. Each window of at least 10 seconds and 20 verified
transcript successes checks p95 latency: at 1,000 ms or above, the target rate drops to 80%
of the lower of its current target and achieved throughput, and concurrency drops by 20%
(rounded down, minimum one). Continued slow windows reduce again; increases require 30 seconds
after the last reduction and a fresh window below 1,000 ms. Rate increases are limited by
125% of achieved throughput, preventing the target from growing beyond delivered performance.
This control uses the exact window p95; the dashboard shows a cumulative histogram estimate.
--fast and --plain belong only to the demo; redirected production output is plain automatically.
Days remain a command-line option; tokens/categories/state remain environment settings.
From this checkout, build and run the current code without publishing:
npm ci --ignore-scripts
npm run local-test -- --dry-run
npm run local-testTo select the last seven days:
npx @letitcurl/harness-ingestor@latest --days 7
npm run local-test -- --days 7Omitting --days imports all time. --days N requires a positive whole number and selects
individual records in a rolling window of N × 24 hours ending when the command starts
unless --time-anchor=latest-record is selected.
Both endpoints are inclusive, and the preview shows the exact UTC window. The filter uses
the record timestamp, then the session timestamp, then file modification time when timestamps
are absent (timestamp_source distinguishes transcript timestamps from file-time fallback). Future-dated records are
excluded from a day window. Record bodies and idempotency keys stay unchanged.
Combine with --dry-run to preview counts without uploading. Discovery still reads the
supported transcript files because old files can contain recent records.
npm run local-test runs npm run build followed by node dist/cli.js.
It uses your local transcripts and the real destination after confirmation.
For the offline two-panel terminal UI demo, run:
npm run demo
npm run demo -- --theme default
npm run demo -- --theme spaceship
npm run demo -- --theme spaceship --ascii
npm run demo -- --fast --plainThe roughly 25-second offline design preview shows scanning, review, upload progress,
separate HTTP activity and Adaptive decisions panels, rate ramp-up, HTTP 429,
cooldown, a temporary connection outage, recovery, and performance-report delivery.
Each panel retains ten entries and shrinks on smaller terminals. Resize the terminal
during the demo; Ctrl-C cancels and restores the screen. NO_COLOR=1 disables colors.
Spaceship is used when no theme is given. The original appearance is preserved as --theme default.
spaceship adds bordered telemetry/control panels, cyan/violet accents, provider bars,
status labels, and a relative target-rate sparkline. --ascii replaces its Unicode
decoration for terminals/fonts that do not display it correctly. Themes affect only the
interactive demo; plain logs and simulation scenarios remain unchanged. The real CLI also
supports --theme spaceship|default and --ascii, with spaceship selected by default.
--plain produces chronological logs; --fast runs the scenario without presentation delays.
All HTTP outcomes, timing, and adaptive decisions are scripted simulations, not a production controller or benchmark. The restart scene reloads a real temporary checkpoint journal and skips 18 synthetic records without mock requests. It does not prove crash-safe production persistence. No real transcripts, credentials, or network are accessed. Temporary fixtures and state are removed on normal exit or handled cancellation. A forced kill can leave the synthetic temporary directory behind. The demo needs no build and does not modify the production CLI. The real CLI implements its own scheduler, durable state, performance reports, and dual-panel renderer; the demo remains a scripted preview, not a load test.
Normal execution reads HARNESS_INGESTOR_TOKEN if set and nonblank, otherwise asks for a
hidden Platform User API token, captures and validates source
files, displays counts by harness, and asks once before uploading. The token stays in memory;
there is no token argument or saved credential file. --dry-run never prompts for a token or
makes network requests. --help and --version do not scan local files.
An environment token skips the token question only; upload confirmation is still required.
Supply it through your shell or secret manager; do not commit it or put its value in CLI arguments.
Terminal scanning uses cli-progress; uploads use a coordinated, at-most-10-FPS dashboard.
The scan display stops before the summary and confirmation prompt. Uploads show overall record progress and a
bounded window of transcript files with queued/uploading/retrying/done/failed states.
Two rolling panels show HTTP outcomes and adaptive decisions (up to ten lines each, reduced
for small terminals). Target/actual rates, in-flight slots, provider progress, local skips,
and p95 are visible. Retry delays are visible. Redirected output uses plain labeled lines
without cursor controls. Summary output is printed after restoring the terminal.
Interactive CLI bars are cyan, completed uploads green, retries yellow, failures red, and
queued files gray. NO_COLOR=1 or FORCE_COLOR=0 disables colors; FORCE_COLOR=1 enables
them in an interactive terminal even when NO_COLOR is set.
All records [===================> ] 50% 50/100 uploading
codex rollout-demo [========================================] 100% 30/30 done
claude-code demo [==========> ] 28% 20/70 retryingThe published distribution command is:
npx @letitcurl/harness-ingestor@latestnpm may retain its cache. There is no self-deletion.
Fixed destination and content scope
POST https://platform.hub.odt.co.th/api/v1/events
category_key: harness-transcriptThe default category is harness-transcript. To target another category that already exists,
is published and unarchived for the token's company, set HARNESS_INGESTOR_CATEGORY_KEY when
running the CLI. The selected category is shown in the pre-upload summary.
HARNESS_INGESTOR_CATEGORY_KEY=my-category npx @letitcurl/harness-ingestor@latestThe token determines the submitting account and company. The category must already exist, be published and unarchived. This client does not create accounts/categories or change Platform. There is no read-only token-introspection endpoint, so authentication is checked by the first confirmed upload, not during discovery.
Full source record content is uploaded without redaction or summarization, including tool output, code, and any secrets/private text embedded in it. Original files are preserved.
| Harness | Default roots under the current OS home | Supported files |
| ----------- | ----------------------------------------------- | ---------------------------------------------------------- |
| Claude Code | .claude/projects/ | Project-level .jsonl and nested subagents/ transcripts |
| Codex | .codex/sessions/, .codex/archived_sessions/ | Recursive rollout-*.jsonl |
CLAUDE_CONFIG_DIR replaces .claude; CODEX_HOME replaces .codex. These environment
variables are read, never changed. Apps using these trees are included automatically;
cloud-only sessions and app-specific databases are not searched. There is no whole-disk,
other-user, or Windows-to-WSL-profile scan. Run inside each environment where required.
Missing provider directories are normal. Unreadable directories/files and symbolic links are reported. Symlinks inside transcript roots are not followed. “All” means all accessible supported files found at discovery, not deleted history or files created after that scan. History indexes, auth files, skills, paste caches, browser exports and tool-result directories are excluded.
References: Claude directory layout, Codex rollout source.
Event shape and identity
context.body is the native source JSON object: message, metadata, tool event, or other
JSONL record. Arrays and nested objects stay queryable. Numbers use a lossless parser and
serializer rather than JavaScript's floating-point conversion. No conversation is flattened.
An abbreviated synthetic example:
{
"event": {
"category_key": "harness-transcript",
"idempotency_key": "claude-code:<source-hash>:record:12:<content-hash>",
"occurred_at": "2026-01-01T00:00:00.000Z",
"context": {
"format_version": 1,
"harness_type": "claude-code",
"source_id": "<source-hash>",
"session_id": "<source-session-id>",
"line_number": 12,
"content_sha256": "<content-hash>",
"body": { "type": "user", "message": { "role": "user", "content": "Synthetic example" } },
"raw_record": "{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"Synthetic example\"}}\n",
"identity": {
"local_username": "example",
"git_user_name": "Example Developer",
"git_user_email": "[email protected]",
"git_status": "available"
}
}
}
}The envelope also carries observed_at, timestamp_source, source_interface and
parent_session_id. raw_record preserves original text and line ending for accepted records;
it costs additional storage, while body is the query surface. Blank lines have no event.
Local username comes from the OS. Git name/email are the effective settings of the source project, including repository overrides, observed at import time. They are not proof of historical authorship or a GitLab login. Missing fields are null. Missing projects never fall back to an unrelated global Git identity. The first source project directory found in a file is used for its identity lookup. Source content may itself contain absolute paths.
Per-record timestamps are preferred, then a timestamp found elsewhere in the same transcript,
then file modification time. timestamp_source distinguishes transcript vs file fallback.
CLI/app designation is retained only when the source supplies a string designation;
otherwise source_interface is unknown. Parent relationships are preserved when available.
Hashes, retries and appending files
Keys use hashes, not UUIDs:
source_id = SHA-256(harness + NUL + logical source path)
content_sha256 = SHA-256(record JSON with recursively sorted object keys)
idempotency_key = <harness>:<source_id>:record:<physical-line-number>:<content_sha256>Canonical content hashes ignore object-key order and whitespace, preserving array order and
numeric spelling. Logical paths are relative to Claude's projects root, or the Codex rollout
basename so moving a rollout into archives does not change its identity. A repeated record at
two different positions remains two events. Transcript upload identities remain content-based.
Performance runs and private temporary filenames use random identifiers.
Appending messages leaves earlier keys unchanged. A private local receipt journal skips confirmed records without HTTP on restart. Receipts are fsynced only after validated server acknowledgements. Lost acknowledgements, a fresh ledger, or older imports require server replay to establish local confirmation. Accepted events retain original metadata. Changing existing records produces new content-hash revisions; inserting/deleting earlier lines also changes later position-based identities. No history is overwritten.
Metabase / PostgreSQL examples
Use your authorized Platform database connection/reporting surface. These examples target
the existing events_items and events_categories tables and explicitly scope by company;
they do not provision access, views, or dashboards. Configure company_id as a required
numeric Metabase variable, not a client-controlled tenant boundary for embedded dashboards.
Browse record types and message bodies:
SELECT
e.occurred_at,
e.context ->> 'harness_type' AS harness,
e.context ->> 'session_id' AS session_id,
(e.context ->> 'line_number')::bigint AS line_number,
e.context #>> '{identity,local_username}' AS local_username,
e.context #>> '{identity,git_user_name}' AS git_name,
e.context #>> '{identity,git_user_email}' AS git_email,
e.context #>> '{body,type}' AS record_type,
e.context #>> '{body,payload,type}' AS codex_payload_type,
e.context #> '{body,message}' AS claude_message,
e.context #> '{body,payload}' AS codex_payload
FROM events_items e
JOIN events_categories c ON c.id = e.event_category_id AND c.company_id = e.company_id
WHERE e.company_id = {{company_id}}
AND c.key = 'harness-transcript'
AND c.archived_at IS NULL
ORDER BY e.occurred_at, e.context ->> 'source_id', (e.context ->> 'line_number')::bigint;Count source records by harness and Git email (this is not a count of human messages):
SELECT e.context ->> 'harness_type' AS harness,
e.context #>> '{identity,git_user_email}' AS git_email,
count(*) AS records
FROM events_items e
JOIN events_categories c ON c.id = e.event_category_id AND c.company_id = e.company_id
WHERE e.company_id = {{company_id}}
AND c.key = 'harness-transcript' AND c.archived_at IS NULL
GROUP BY 1, 2;Records retain native provider shapes; do not blindly add cumulative token counters or assume every record is a message. Deduplicate historical revisions by source/line when your analysis requires latest state; the importer keeps all accepted facts immutable.
Throttling and failures
- Shared adaptive budget for transcripts, retries, and reports: starts at 2 requests/s and two in flight, ceilings 20 requests/s and eight in flight, 30-second request timeout.
- Healthy transcript windows (at least 10 seconds and 20 successes) increase the target by 25% and concurrency by one. Reports alone cannot cause ramp-up.
- Blazingly fast mode additionally uses the 1,000 ms p95 latency wall and achieved-throughput limit described above; successful but slow windows reduce pace instead of increasing it.
- HTTP 429 immediately halves the target/concurrency. Three other transient failures in ten seconds also reduce them, at most once per window. Minimum 0.1 requests/s and one slot; increasing after a reduction requires 30 healthy seconds.
- Five retries after the first attempt for HTTP 408/429 and 500/502/503/504.
- Three consecutive transport failures open the offline circuit. New records pause; only one pending-request probe runs at a time, with jittered exponential waits from approximately five seconds up to sixty seconds. Waits continue until recovery or Ctrl-C; offline probes do not exhaust the normal retry budget. Recovery resumes conservatively.
- Exponential backoff starts at one second with jitter; client delays cap at 60 seconds.
- Valid
Retry-Afterseconds or HTTP dates set a minimum wait, even above 60 seconds. - HTTP 200/201 counts as delivered only if category, key and returned source body match.
- Authentication/category failures, invalid successful responses and exhausted transient retries stop the run. Redirects are not followed with the bearer token.
- HTTP 400/409/413 and field-validation 422 errors mark the record failed and continue. Category-level validation stops. Error response bodies are never printed.
- Malformed UTF-8/JSON, duplicate object keys, non-object records, incomplete final JSON, records above 8 MiB, or requests above 16 MiB are reported without upload. They are never truncated, split into opaque fragments or silently counted as complete. Blank lines are ignored.
The live server request-size limit is unverified; a server 413 can reject records below the
client limit. Other server JSON/JSONB restrictions may also reject records. Precision changes
in a successful server response are detected; raw_record preserves the original numeric text.
Exit codes: 0 complete/dry-run; 1 failed or incomplete ingestion; 2 invalid arguments; 130 cancellation. Saved/replayed/failed/incomplete totals count records. Unreadable counts source issues. Cancellation prevents further scheduled requests; an in-flight request may already have been accepted, and a rerun safely replays it.
Durable state and performance reports
HARNESS_INGESTOR_STATE_DIR overrides the default user state directory: macOS
~/Library/Application Support/harness-ingestor, Linux $XDG_STATE_HOME/harness-ingestor
(fallback ~/.local/state/harness-ingestor), Windows %LOCALAPPDATA%/harness-ingestor.
Use a private local filesystem, not a shared/network drive. Windows permissions depend on
the user directory's inherited ACL. Namespace identity uses the endpoint, transcript category,
and an HMAC of the token with a local random salt; the token is never saved. Token rotation
conservatively establishes a new ledger through server replay.
A namespace lock prevents concurrent importers. A proven dead process lock can be recovered; an unknown/malformed owner or reused PID fails closed. Never remove a lock while an importer may still be running. Incomplete trailing receipt writes are discarded; corrupt complete receipts and checkpoint write failures stop ingestion. State stores hashed record keys, receipt outcomes, statistics with attribution, and pending reports/logs—not transcript content. Keep it to enable resume.
After confirmation, the CLI automatically submits statistics to harness-client-performance,
overridable with HARNESS_INGESTOR_PERFORMANCE_CATEGORY_KEY. The transcript, performance,
and harness-client-logs categories must already
be usable for the token. Reports are sent at upload start, every 60 seconds, and on exit.
No reports are sent for dry-run, declined confirmation, or an empty selection.
Each event has a stable harness-performance:<run-id>:<sequence> key and a queryable
context.body: schema/client/controller versions, run status, UTC timestamps, selection,
record counts, request/status/retry counts, payload bytes, durations, throughput, adaptation
history, and provider breakdowns. Reports contain no transcript text, tokens, raw server
responses, or local source paths. Their own HTTP measurements are excluded from transcript
metrics, but they share the rate limit and can trigger protective slowdown.
Request latency covers the HTTP attempt and response handling, excluding scheduler/backoff
waits. Successful, failed, and all-attempt histograms use versioned fixed upper bounds;
median/p95/p99 are bucket estimates, not exact samples. Merge histogram counts to aggregate
percentiles; never average p95 values. active_upload excludes offline waits but still includes
pacing/backoff. task_throttle_wait and task_backoff_wait sum waits across concurrent tasks;
they overlap and must NOT be added to wall-clock upload duration.
Histogram version 2 has 16 numeric upper_bounds_ms and 17 counts: the final
counter is the overflow bucket, explicitly described by overflow_above_ms: 60000.
There is no trailing null bound, which Rails would remove from incoming arrays.
Version 1 reports may have 16 stored bounds and 17 counters for the same reason;
their final counter also represents values above 60,000 ms.
Legacy queued reports retain their original payloads and idempotency keys. Only the performance sender tolerates removal of the known version-1 histogram's trailing null when checking a saved/replayed response. All other content must still match; transcript validation is unchanged. A response mismatch is reported explicitly and remains queued. To recover old reports, rerun the updated client on the original machine with the same token, categories and state directory, select a nonempty transcript window, and confirm. Already confirmed transcripts are skipped. Dry-run and declined consent never drain the outbox.
Statistics snapshots persist every five seconds. Hard-killed runs are marked interrupted on a later confirmed invocation, with potentially incomplete metrics. Missing heartbeats alone mean stale/unknown. Reports retry independently through a durable outbox. Unattempted progress reports may be coalesced; attempted payloads remain frozen. Graceful exit gives final delivery one shared five-second network grace period for performance reports and client logs, then leaves remaining events queued. Report failures do not undo receipt checkpoints or cause confirmed transcripts to be resent.
Performance reports include the same context.identity fields as selected transcripts:
local_username, git_user_name, git_user_email, and git_status. These are local/Git
attribution, not a verified Platform account identity. If selected transcripts have different
identities, context.identity is null and context.identities contains the distinct objects;
the CLI previews this before confirmation. Missing Git fields remain null. Recovery and
retries preserve the original attribution; older queued reports retain their original shape.
These identity fields are stored in the private performance outbox, never in client logs.
For Metabase, query the latest report per context.body.run_id, ordered by numeric
report_sequence. The reports are cumulative: do not sum every heartbeat's counts.
Scope queries through your authorized company-scoped reporting surface. A reporting outage
is shown as queued/deferred; a successful import does not imply all telemetry was delivered.
Latest cumulative performance snapshot per run (same company-scoping requirements as above):
SELECT DISTINCT ON (e.context #>> '{body,run_id}')
e.context #>> '{body,run_id}' AS run_id,
e.context #>> '{body,status}' AS status,
e.context #>> '{body,reported_at}' AS reported_at,
e.context #> '{body,records}' AS records,
e.context #> '{body,duration_ms}' AS duration_ms,
e.context #> '{body,requests,success_latency_ms}' AS successful_request_latency,
e.context #> '{body,adaptation}' AS adaptation
FROM events_items e
JOIN events_categories c ON c.id = e.event_category_id AND c.company_id = e.company_id
WHERE e.company_id = {{company_id}}
AND c.key = 'harness-client-performance'
ORDER BY e.context #>> '{body,run_id}',
(e.context #>> '{body,report_sequence}')::bigint DESC;Temporary storage and verification
Files are copied to private temporary snapshots before confirmation to avoid upload races. Unix permissions are 0700/0600; Windows uses the user's inherited temp-directory ACL. Allow temporary disk space for the corpus, including duplicate copies. Capture checks file identity, size and timestamps and retries twice if they change. Continuously changing files are reported incomplete. These checks are not a defense against hostile timestamp manipulation.
Normal exit/cancellation removes importer snapshots. Forced termination or cleanup errors
can leave harness-ingestor-* directories in the OS temp folder; remove only a confirmed
inactive importer directory. Original files and npm's cache are never deleted.
npm ci --ignore-scripts
npm run check
git diff --checkFor local readability fixes, run npm run lint:fix followed by npm run format.
The same scripts work with bun run lint:fix and bun run format.
ESLint enforces braces, strict equality, separate variable declarations, sorted imports/exports,
type-only imports, and unused-binding checks.
ESLint also rejects debugger statements, eval, string-based timers and Function constructors.
Prettier provides two-space indentation,
single quotes, LF line endings and a 100-column target (not a hard line-length limit).
npm run format:check and npm run lint are read-only and both run in npm run check.
Generated output and the generated npm lockfile are excluded from formatting.
One-command local CI
Install Bun (tested with the version in .bun-version), Node.js 24+, npm and Git, then run:
bin/ci
# Equivalent; also use these on Windows:
bun run ci
npm run ciThe Bun-powered runner prints timed stages and stops on the first failure: runtime checks,
npm ci --ignore-scripts, formatting, lint, type-checking, Node tests, Bun coverage, build, packed-package smoke,
and working-tree/staged whitespace checks. It resolves this repository from the script path,
so bin/ci also works when invoked by absolute path from another directory.
It permits pre-existing local edits but fails if checks change package.json or
package-lock.json; it never restores or discards your changes.
Use bun run ci --skip-install for an already-installed working copy, or bun run ci --offline
to install using a warm npm cache without network access. bin/ci --help performs no checks.
The default install may use the network and replaces node_modules using the npm lockfile.
No formatting fixes, publication, push, or live transcript import occurs in this runner.
Bun is the runner, not a replacement for the published CLI's Node runtime or npm packaging.
Bun's built-in bun ci only installs dependencies;
it is not the same as this project's bun run ci. The existing GitLab checks continue
to use npm run check, so this local helper does not require changing runner infrastructure.
Local execution was verified on macOS; Windows/Linux execution remains to be verified.
If mise warns about resolving .bun-version while offline, first check bun --version.
When the pinned version is already installed under ~/.bun, register that installation
without downloading it: mise link [email protected] ~/.bun. Do not label a different Bun
version as 1.3.14. The demo itself only needs Node; its full-screen dashboard is cleared
on exit, leaving the summary. Use npm run demo -- --plain to retain the activity log.
Test coverage
Run npm run test:coverage with Bun 1.3.14 (also pinned in the GitLab coverage job).
This uses Bun's built-in coverage, not c8
or an additional instrumentation dependency. It runs the same test files with Bun,
prints function/line coverage and uncovered lines, and writes coverage/lcov.info.
The LCOV file can be opened in an editor coverage viewer. Bun does not produce an
HTML report here. Generated coverage is ignored by Git and excluded from the npm package.
scripts/check-coverage.mjs requires at least 90% aggregate lines and functions from
Bun's LCOV report, failing on missing/malformed reports. Bun's built-in threshold is
per-file and is not used for this aggregate gate. Test files,
generated output and ESLint configuration are excluded. This is loaded-module
coverage, not proof that every repository file is exercised: subprocess-only CLI,
demo and packaging entry points are not represented by the parent's coverage.
Node tests and package smoke checks remain necessary for the actual shipped runtime.
No branch-coverage or per-file threshold is claimed.
bin/ci runs both Node tests and Bun coverage. npm run check remains the Node-only
verification command. GitLab runs a separate required coverage verification job,
extracts the line percentage and retains LCOV for 14 days, including failed runs
when a report was generated. This is a downloadable artifact, not GitLab inline
Cobertura annotations. Publication waits for the verification stage to succeed.
Important regression coverage includes the 7-day → 8-day → repeated 8-day import, both providers, local checkpoint skipping, performance reports on each run, immutable report retries, offline recovery, cancellation, state isolation, corrupt journals, checkpoint failures, terminal cleanup and invalid/duplicate flags. Passing percentages do not validate live server limits, native Windows/Linux behavior, power-loss durability or production performance. Those require separate verification.
Checks cover formatting, ESLint, TypeScript, synthetic files, mocked HTTP/clocks, progress rendering,
compilation and installation/execution of the packed npm artifact. The smoke install prefers the
local npm cache but can fetch missing runtime dependencies on a clean CI runner. No real transcripts
or live tokens are fixtures. GitLab's default job uses Node 24 on Linux; opt-in native shell
jobs need Node 24/Git, tags harness-windows and harness-macos, and HARNESS_NATIVE_CI=true.
This repository does not provision runners or settings. Windows/Linux runtime verification
and actual remote CI execution must be recorded separately when available.
private: true prevents accidental publication in the checkout. The protected GitLab release
pipeline creates a temporary scoped manifest and publishes only the tested archive. The checks
job writes release/ metadata and a SHA-256 checksum; the publish job verifies both before any
registry operation. This self-hosted GitLab requires a protected, masked/hidden granular
NPM_TOKEN with publish access to the package. npm Trusted Publishing currently supports
GitLab.com shared runners, not this GitLab host. Do not revoke the publishing token on
the assumption that an OIDC variable makes authentication work. See npm Trusted
Publishing. This publishing credential is
separate from the user's HARNESS_INGESTOR_TOKEN used for importing into Platform.
The release job runs only for protected main pushes in the intended GitLab project. It publishes
an immutable 0.1.<CI_PIPELINE_IID> version under the ci tag and advances latest only when
the registry state is newer. Remote CI execution, npm publication, live imports, Platform changes,
category creation, app-specific integration, other-user collection and background services remain
separate operational actions.
Debugging an interrupted import
Each non-dry run writes a private JSONL diagnostic file under diagnostics/ in the
local state directory (HARNESS_INGESTOR_STATE_DIR overrides that directory). The
final output prints its path and repeats any stop reason after the progress display
closes. Share this diagnostic file when investigating a failed import.
After confirmation, safe diagnostic events also upload to the existing harness-client-logs
category. Start, selection, progress every 60 seconds, errors/retries, stop reasons, and final
counts share the performance report's context.body.run_id. Each JSONL line becomes one
event, with parsed metadata at context.body and the original line at context.raw_record.
Keys use harness-client-log:<run-id>:<sequence> and remain unchanged on retry. Order activity
by numeric context.body.sequence; stop and summary events are prioritized over queued details.
Diagnostics contain the package version, failed/retried HTTP attempt metadata,
safe error codes, scan-local file numbers and line numbers, progress and final counts. They
exclude tokens, identities, transcript paths/content, request/response bodies, and
raw exception messages/stacks. HTTP status 0 means no response; -1 means the
attempt was aborted. Detailed events are capped at 2,000 per run, with the stop and
summary retained. Files use owner-only permissions on systems supporting Unix
permissions. Local JSONL files remain until manually deleted. New remote events are persisted
approximately every second to a separate private outbox before delivery; verified receipts
remove them from that outbox. Failed deliveries retry on a later confirmed run with the same
token, transcript category, and state directory. No client logs upload for dry runs, declined
confirmation, empty selections, or failures before confirmation. Dry runs create no local logs.
Hard termination can lose details not yet persisted to the outbox; existing queued events
remain retryable. Local file or delivery failures do not stop ingestion.
The CLI previews the log category and privacy details, shows delivery activity while uploading, and prints delivered/queued totals after closing progress. Queued events are not counted as delivered. A missing/unavailable log category or invalid token leaves events queued. The log sender shares request pacing without adding its own attempts to transcript statistics or logs.
incomplete combines invalid/oversized records with valid records left unfinished
when a run stops. The final output now shows these counts separately. failed
counts explicit upload failures; saved and replayed count verified receipts.
For an offline check of a local checkout, run npm run check. This includes mocked
HTTP upload-failure tests and npm run smoke, which installs the built package in
a temporary directory and checks CLI startup with isolated transcript roots. These
checks do not send test traffic to Platform. Run npm run build before invoking
npm run smoke on its own.
