feat(genomic-intelligence-nim): hosted DNA-sequence prediction skill - #12
feat(genomic-intelligence-nim): hosted DNA-sequence prediction skill#12boldakov wants to merge 27 commits into
Conversation
Add a nim-skills/ skill wrapping Genomic Intelligence's hosted DNA
language models over the /v1/tasks/{task}/predict contract: promoter,
splice, enhancer, chromatin, expression, and annotation. Bearer-authed
via GI_API_KEY; ships a requests-only runner (sync + async), references,
demo FASTAs, and evals. Context: NVIDIA-BioNeMo#5.
Signed-off-by: Alexander Boldakov <boldakov@gmail.com>
…lugin-sync coverage
- register genomic-intelligence-nim in skills.sh.json and commit the
plugin_sync --write payload, so the plugin-sync CI gate passes
- add evals/config.yml (schema_version 1, runtime_env GI_API_KEY) per the
current nim-skills eval layout; keep one bounded eval case and defer the
network-heavy expression/annotation cases, mirroring evo2-nim
- correct the enhancer minimum to the user-visible 50 bp and clarify the
exact 9,198 bp expression window is a client-side guard, not an API limit
- stop pinning default model IDs in references: defaults resolve
server-side, GET /v1/tasks/{task}/models is authoritative
- fix an off-by-one in the expression fixture header (chr11:5222473-5231670
spans the 9,198 bases the record actually contains)
Signed-off-by: Alexander Boldakov <boldakov@gmail.com>
|
Updated the branch: merged current main and brought the skill in line with the layout changes that landed after this PR was opened.
Two questions we didn't want to guess on:
Also glad to add a row to the README skill catalog, or leave that to you. Broader scope question is still #5. |
…s_index Genomic Intelligence shipped a new expression contract to production on 2026-08-18 (gpu_service 2026.08.18.1). Expression now has its own published OpenAPI operation (POST /v1/tasks/expression/predict, schema ExpressionPredictRequest) instead of the generic templated one, and the request body gained a fifth field. What the skill got wrong: - the bound is 9,198-500,000 bp, not "exactly 9,198 bp". The model still scores exactly one 9,198 bp TSS-centred window, but the server slices it (sequence[tss_index-4599 : tss_index+4599]) before tokenizing; - tss_index (0-based, into the whitespace-stripped sequence, bounded by 4599 <= tss_index <= len-4599) is required unless the sequence is exactly 9,198 bp; - options is a closed object whose only, required, key is description; - violations are 422 validation_failed with no opt-out. Code changes: gi_client.predict() forwards tss_index; gi_predict.py gains --tss-index, replaces the exact-length gate with the real floor/ceiling plus tss_index bounds checks, threads the flag through the reproducibility command, and surfaces the response's tss_index / scored_window so a caller can catch an in-range-but-wrong offset (which returns a confident 200 for the wrong window). The other five tasks, gi_fetch --for-expression (still builds an exact 9,198 bp window, so it needs no tss_index), and the bundled 9,198 bp expression_hbb_k562.fa fixture are unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Alexander Boldakov <boldakov@gmail.com>
…teral paths Correct the skill against the settled /v1 contract (DEV 2026.08.19.4): - gi_predict.py: replace the 1 bp placeholder floor with the real per-task minimums (promoter 300, splice 100, enhancer 50, chromatin 200, annotation 1,000; expression unchanged at 9,198) and label them a mirror of gpu_service/core/limits.py, which the server publishes as minLength. - gi_predict.py: stop forwarding --description on non-expression tasks. Every *Options object is now additionalProperties:false, so that was a hard 422 rather than a no-op. - Document the six literal predict operations, the published composite workflow, the typed per-task options, the Prefer header, the closed error code enum, and the new bio_spec fields (request_max_bp, context_window_bp, trained_window_bp). - Correct "413 too long": an over-length sequence is 422 validation_failed; 413 means the 16 MiB body cap or the composite's sync_too_large. - Correct the "all six tasks share one request shape" claim in references/api.md, which contradicted the same file's own endpoint list. - State the floor-is-not-regime rule (a request above the floor but below context_window_bp is scored against a padded window) rather than implying the bound is biologically meaningful. - Note that tss_index errors report at loc ["body"], never body.tss_index, so clients must match on error.code. - Make the locus + --tss-index example producible: fetch out/locus.fa with gi_fetch --region first, and replace the invented offset with a computed placeholder. Verified against the live DEV schema (info.version 2026.08.19.4 (a5b1f88), 11 operations, no PredictRequest component). Signed-off-by: Alexander Boldakov <alexboldakov@gmail.com>
…ndow fields
- 504 is `timeout`, not `upstream_timeout`. The published enum has 21
values and no `upstream_timeout`; the wrong string could never match.
- The locus quick-start fetched HBB with the `--region` default of
`--strand 1`. HBB is minus-strand and expression never reverse-
complements, so the example silently scored the antisense window.
Adds `--strand -1` and the minus-strand offset formula
(REGION_END - TSS, not TSS - REGION_START).
- `--tss-index <offset>` was unquoted inside a bash fence, so the shell
read it as a redirection. Replaced with a computed TSS_INDEX variable;
the whole fence now passes `bash -n`.
- Expression reports `context_window_bp: null`; 9,198 is
`trained_window_bp`. Tables listed 9,198 under the context-window
column. Column relabelled to the field it holds.
- Dropped the default-model-ID inventory; `GET /v1/tasks/{task}/models`
owns it. Architecture descriptors kept.
Signed-off-by: Alexander Boldakov <boldakov@gmail.com>
… the locus example The published schema conveys strand zero times on the direct expression path: the operation description never mentions it, and reverse_complement is reachable only through the composite. The example therefore has to carry the requirement itself rather than lean on the contract -- a wrong-strand window returns a confident number, not an error. Signed-off-by: Alexander Boldakov <boldakov@gmail.com>
…c from staging Keeps the exact-window tripwire on the default path (no --tss-index still requires exactly 9,198 bp, because a mis-centred window returns a confident 200 with no client-side tell) and keeps --tss-index as the explicit opt-in to the 9,198-500,000 bp range the API allows. Also corrects expression's context_window_bp in the task table: it is null, not 9,198 -- that number is trained_window_bp. Adds the tests/ and evals/config.yml that only existed in the staging copy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Alexander Boldakov <alexboldakov@gmail.com>
…ask property Every predict operation declares the Prefer header and both 200 and 202. Verified live: promoter submitted with Prefer: respond-async returns 202 and polls to a normal payload; annotation with no Prefer returns 200 synchronously. Relabels the task table's "Mode" column "Recommended mode" and states the rule under the table. Renames TaskSpec.async_mode to async_default so the code stops asserting a constraint the API never had -- no behaviour change, annotation still defaults to async. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Alexander Boldakov <alexboldakov@gmail.com>
… runner docstring Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Alexander Boldakov <alexboldakov@gmail.com>
…Id header 413 sync_too_large omits request_id from the error body, so the client rendered "(request_id=None)" on it. The X-Request-Id header is set on every response, so GIError now falls back to it. Corrects the docs, which claimed request_id was always populated, and replaces the "req_..." placeholder with a real UUID shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Alexander Boldakov <alexboldakov@gmail.com>
…08.19.5
Today's GI ship landed after this text was written, so several claims went
stale within hours:
- 422 details is the declared {"errors": [...]} object, not a bare array.
Defensive handling of either shape is kept; only the prose was wrong.
- error.request_id is populated on every error path, including both 413
variants; success envelopes carry meta.request_id.
- bio_spec.max_seq_length_bp is retired and absent from the live response;
request_max_bp is the cap.
- Version notes hedging that PROD may still serve an older build are moot.
- Sync/async is a per-request delivery choice, never a task property; the
composite's 50,000 bp sync limit is the only forced case.
Also renames the client's synthesized non_json error code to http_error,
which is in the published 21-value enum; client-origin errors remain
distinguishable by carrying no request_id.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Alexander Boldakov <alexboldakov@gmail.com>
Both sides had unique work, so this merges rather than replacing either. From the remote (2026-08-10): the upstream main merge, the generated plugin payload under plugins/, evals/trigger_evals.json, and the move away from citing the private monorepo as the contract's source of truth. From this branch (2026-08-19): the per-task floors (promoter 300, splice 100, enhancer 50, chromatin 200, annotation 1,000, expression 9,198), the --tss-index opt-in that keeps the exact-window tripwire on the default path, timeout in place of the non-existent upstream_timeout, the HBB sense-strand example and its demo-header off-by-one, delivery mode as a per-request choice rather than a task property, the request_id header fallback, and tests/. Conflicts in SKILL.md, references/api.md, references/tasks.md and scripts/gi_predict.py resolved in favour of the 2026-08-19 work, which supersedes the remote on every contested line. Two remote claims were dropped as incorrect against the live contract: that the request floor is 1 bp, and that the API truncates or pads a mis-sized expression window (it returns 422 validation_failed). The remote's removal of gpu_service/ source references was kept and extended to the four places this branch still carried them - partner docs must not cite a private repo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Alexander Boldakov <alexboldakov@gmail.com>
scripts/plugin_sync.py --write, so plugins/bionemo-agent-toolkit/ carries the 2026-08-19 contract corrections instead of the 2026-08-10 snapshot. The payload is generated, not hand-maintained, and plugin-sync.yml fails a PR on drift. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Alexander Boldakov <alexboldakov@gmail.com>
Greptile SummaryThe PR adds a hosted Genomic Intelligence skill for six DNA prediction tasks, including sequence acquisition, synchronous and asynchronous inference, validation, reporting, evaluations, and a synchronized plugin payload.
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains in the previously reported paths. No blocking failure remains. Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant Fetch as gi_fetch.py
participant Ensembl
participant Predict as gi_predict.py
participant GI as Hosted Genomic Intelligence API
User->>Fetch: Gene, region, or expression request
Fetch->>Ensembl: Resolve locus and fetch sequence
Ensembl-->>Fetch: FASTA sequence
Fetch-->>User: Single-record FASTA
User->>Predict: Task, FASTA, and task options
Predict->>GI: Bearer-authenticated prediction request
alt Annotation async
GI-->>Predict: Job identifier
loop Until completion
Predict->>GI: Poll job
GI-->>Predict: Progress or result
end
else Synchronous task
GI-->>Predict: Prediction envelope
end
Predict-->>User: report.md, result.json, and JSON summary
Reviews (12): Last reviewed commit: "chore(genomic-intelligence-nim): include..." | Re-trigger Greptile |
…ot a bare array
The sample error envelope showed details: [], which was true until
gpu_service 2026.08.19.5. The service now emits the object the schema has
always declared: {errors: [{loc: [...], msg: ...}]}. Verified against
live PROD.
Regenerated the plugin-sync payload so the plugin-sync check stays green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Alexander Boldakov <alexboldakov@gmail.com>
…iring it Three defects, all of which produced a confident, well-formed, wrong result with nothing for the caller to notice. read_fasta silently deleted every character outside ACGTN and concatenated multi-record files into one chimeric sequence under the first record's name. Deleting an IUPAC ambiguity code shifts every base after it, so the model scored a sequence the caller never supplied. Both now raise FastaError (a ValueError subclass) naming the offending characters or record count. GeneLocus.tss fell back to gene-body coordinates when no canonical transcript was resolved. The field comments already condemned this — ACTB's gene body sits 33,301 bp from its TSS — and the fallback is invisible downstream: the window is still exactly 9,198 bp, so the client-side size gate passes, no tss_index is sent, and the API scores the wrong locus at full confidence. Correctly sized, wrongly centred, no client-side tell. It now raises. The runner caught only GIError, so connection failures, job timeouts and malformed async responses exited as tracebacks on routine hosted-service failures. They now surface as the runner's normal stderr diagnostic with a non-zero exit, matching the mapping gi_ensembl already did. SKILL.md and references/errors.md updated to describe refusal rather than repair. Plugin payload regenerated; all six bundled demo fixtures still parse. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Alexander Boldakov <alexboldakov@gmail.com>
…omments Audited against AUTHORING.md. Removes every pointer at the private service codebase (retired `gpu_service/*` paths, internal constants and symbols) and the internal release stamps that went with them, restating each as what the Genomic Intelligence API serves, with the published OpenAPI document as the authority. Also normalizes the research-use wording, removes pinned model IDs and prediction values from prose, and makes comment voice consistent. No contract facts changed; every bound, field and status code re-verified against the live schema. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…trands Ensembl reverse-complements a minus-strand region, so a window built as if the sequence always read low-to-high put the TSS at offset 4,598. The API scores sequence[tss_index-4599 : tss_index+4599] and defaults tss_index to 4,599 for a 9,198 bp submission, so every minus-strand gene was scored one base off. The window was still exactly 9,198 bp, so the client-side size gate passed and the API returned a confident score with nothing for the caller to notice. Confirmed live on HBB before fixing. Window arithmetic is now a pure helper with a regression test on both strands. Also: --demo must be asked for. Omitting --input silently ran the bundled fixture and produced a full report, real request id and all, for a sequence the caller never supplied. Plugin payload regenerated with scripts/plugin_sync.py --write. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…for input Regression guard for the fallback removed in the previous commit: no --input and no --demo must exit, and --demo must still resolve the bundled fixture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…envelopes Two defects from the review on PR NVIDIA-BioNeMo#12. read_fasta absorbed sequence lines appearing before the first '>' header into the first record. Such a file has exactly one header, so the multi-record check passed, and the API scored bases the caller never named while returning coordinates that did not describe what was submitted. The name-is-None guard existed but sat in the header branch, so nothing reached it. Successful responses were returned unvalidated. A non-JSON 2xx was turned into an error-shaped dict and handed back as a result, so a caller saw {"error": ...} with ok=true; an empty or non-object 200 reached the report writer and failed there as an AttributeError. Predict and job results now require the {data, meta} envelope. /health and /tasks/{task}/models are deliberately un-enveloped and are not checked. Also aligns whitespace handling with the API, which strips newlines, spaces and tabs before measuring length. A space-grouped body was rejected as a bad character, making the client stricter than the service it guards. Guards added for all three in tests/test_input_validation.py. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses: NVIDIA-BioNeMo#12 (comment) The previous commit wrapped only wait_for_job. predict() still returned _check() straight through, so a synchronous 200 with no data key reached the report writer, which wrote an empty report and reported ok=true. The intended edit was in that commit and matched no text, so it silently did nothing. Also tightens the check itself: presence of a data key is not enough, since a null or non-object data passes a presence test and then fails in the summarizer as an AttributeError. It must be an object. Both are pinned by tests, including one asserting predict() calls the validator so the same silent no-op cannot recur. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Good catch, that one was mine. The previous commit wrapped only
|
Addresses: NVIDIA-BioNeMo#12 (comment) submit_async read body["data"]["job_id"] straight off _check, so a malformed 2xx surfaced as KeyError when data was missing and TypeError when data was not an object. The CLI catches the first and not the second, and neither reads to the caller as a bad response. Found by the daily open-PR sweep, not by us: the earlier reply answered only the synchronous half of the reviewer's finding. Present in all four copies of the client, including ClawBio, which the reviewer cannot see. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses: NVIDIA-BioNeMo/bionemo-agent-toolkit#12 (comment) submit_async read body["data"]["job_id"] straight off _check, so a malformed 2xx surfaced as KeyError when data was missing and TypeError when data was not an object. The CLI catches the first and not the second, and neither reads to the caller as a bad response. Found by the daily open-PR sweep, not by us: the earlier reply answered only the synchronous half of the reviewer's finding. Present in all four copies of the client, including ClawBio, which the reviewer cannot see. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… a junction The reference listed data.sites' start/end without saying what the pair means. It bounds one variable-width tokenizer token (4-10 bp observed), comes with a token_index, and the exon/intron junction sits inside it, so reading either coordinate as the boundary base is wrong by up to about 10 bp in either orientation with nothing in the response to signal it. Payload regenerated with scripts/plugin_sync.py --write; --check reports the plugins/ copy byte-identical to source. Verified against the live contract (info.version 2026.08.20.11) and the pinned HBB fixture, whose eight sites span 4, 5, 6, 7 and 8 bp. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Alexander Boldakov <boldakov@gmail.com>
… not truthiness
`_require_envelope` guarantees `data` is an object and deliberately stops
there — it is shared by six tasks and must not encode any one task's schema.
Everything nested inside `data` was then guarded by `x or {}` / `x or []`,
which covers absent and null but not a *truthy* value of the wrong type.
A `data.summary` arriving as the string "all good" passes `or {}` unchanged
and raises AttributeError on the next `.get`. `or []` fails the same way in
the other direction: a bare string is iterable, so the row loop iterates its
characters and each `r.get` raises. Both surface as tracebacks rather than the
documented API-error diagnostic, because `_summarize` and `_write_report` run
after main()'s try/except has closed.
`_as_obj` / `_as_objs` replace the idiom at all 13 call sites in this file —
`data`, `summary`, `prediction`, `meta`, `meta.task_specific_counts`,
`regions`, `sites` and `transcripts`, in `_summarize`, `_headline_lines`,
`_write_report` and `main`. A malformed field is dropped and the report still
writes. Re-grepped: no `or {}` / `or []` remains outside the docstrings.
Sized before fixing, per the reviewer report naming only `summary`,
`prediction` and `meta`: the same shape is present in three further packagings
we ship — ClawBio `clawbio/gi/` (11 sites), proto-tools
`sequence_scoring/genomic_intelligence/` (29 across 8 files) and Phylo
`gi_predict.py` (10). Those are tracked as GI-057 and are not touched here.
Pinned by `TestNestedFieldsOfTheWrongType`, which drives `_summarize` and
`_write_report` with nine malformed bodies across every task and asserts the
report still writes, plus one well-formed body asserting the rows are still
rendered. Behavioural, not an `inspect.getsource` assertion — a source grep is
what missed the last one of these.
140 passed, 1 skipped; `plugin_sync.py --check` clean.
Addresses: NVIDIA-BioNeMo#12 (comment)
Signed-off-by: Alexander Boldakov <boldakov@gmail.com>
`_require_envelope` accepted `{"data": {}, "meta": {...}}`. All three of its
call sites read content out of `data` — a prediction payload, `data.job_id`,
a finished job's result — so an empty object is malformed for every one of
them. On the sync and job-result paths nothing else caught it: the runner
wrote a zero-valued report and printed `"ok": true` with no prediction in it.
Require `data` to be a non-empty object. The `data.job_id` check in
submit_async stays, since a non-empty `data` can still lack it.
Addresses: NVIDIA-BioNeMo#12 (comment)
Signed-off-by: Alexander Boldakov <boldakov@gmail.com>
…'t coerce it
The previous commit replaced `x or {}` with helpers that substituted an empty
object or array whenever a nested response field arrived with the wrong type.
That fixed the AttributeError traceback and introduced a worse failure: the
report writer runs after main()'s try/except has closed, so a `data.summary`
that arrives as a string produced a zero-valued report and printed
`"ok": true`. A malformed response became indistinguishable from a real
prediction of nothing — a silent wrong answer in place of a loud one.
`_as_obj` / `_as_objs` now raise `ResponseShapeError` naming the field, and
main() catches it around `_summarize` / `_write_report` and exits 2 with the
same "unexpected API response shape" diagnostic it gives any other malformed
body. Absent and null stay legitimate — a task with no `prediction` omits it —
so only a present, wrong-typed field is refused.
The tests that asserted `_summarize` *survives* a malformed body encoded the
behaviour being removed and now assert the refusal instead, including one that
drives main() end to end: GI-055 was a source-level edit that matched nothing
and still read as correct, so the durable check is the exit code, not a grep.
Suite: 150 passed, 1 skipped. Payload regenerated with plugin_sync.py --write.
Addresses: NVIDIA-BioNeMo#12 (comment)
Signed-off-by: Alexander Boldakov <boldakov@gmail.com>
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
|
Could a maintainer approve the workflow runs on this PR? As a fork PR they need a manual approval, and all 36 runs since the PR opened are sitting at Locally the skill's suite is 150 passed / 1 skipped and |
…in payload Upstream NVIDIA-BioNeMo#15 (47315cc) changed the plugin payload contract: plugin_sync.py's STRIP_FROM_PAYLOAD went from {"evals"} to the empty set, so a payload folder must now be a full copy of its source skill, evals/ included, because NVCARPS Tier 3 validates the payload and needs the eval dataset there. This skill's payload was generated under the previous rule and predates that change, so it carries no evals/. Running scripts/plugin_sync.py --check on the merge of this branch into upstream/main f62542c reports: [freshness] genomic-intelligence-nim: missing in payload: evals/config.yml [freshness] genomic-intelligence-nim: missing in payload: evals/evals.json [freshness] genomic-intelligence-nim: missing in payload: evals/trigger_evals.json The three files are copied byte for byte from nim-skills/genomic-intelligence-nim/evals/. After this commit the same check reports nothing against this skill. It does not report nothing overall. Four freshness failures remain on evo2-nim (SKILL.md, evals/evals.json, references/api.md, references/examples.md), and those reproduce on a pristine upstream/main worktree at f62542c with this branch absent, so plugin-sync is currently red on main independently of this PR. Signed-off-by: Alexander Boldakov <boldakov@gmail.com>
Summary
Adds a
nim-skills/genomic-intelligence-nimskill that wraps Genomic Intelligence's hosted DNA language models, following the existingnim-skills/pattern (aSKILL.mddriving a bearer-authed REST call). One skill, six tasks over the sharedPOST /v1/tasks/{task}/predictcontract:Inference is hosted - no local weights or GPU; the only dependency is
requests.Third-party hosted endpoint
Unlike the existing
nim-skills/(which target NVIDIA-operated endpoints), this skill calls an endpoint operated by Genomic Intelligence. The interface is identical to the hosted-NIM shape (HTTPS +Authorization: Bearer+ JSON); it is hosted-only. Flagging it explicitly so you can weigh in, happy to adjust naming. Context in #5.Why a
scripts/runner (not inline-only)The published
nim-skills/inline their Python inSKILL.md. This skill instead ships a small, self-contained requests-only runner, because the surface (six tasks, async jobs, and an exact 9,198 bp window for expression task) does not inline cleanly. Shippingscripts/is not prohibited by CONTRIBUTING, and other categories ship scripts too.SKILL.mdstill includes a minimal inline example for the simple sync case.What's included
SKILL.md- frontmatter + task table + runner usage + a minimal inline example.scripts/-gi_predict.py(one CLI, six tasks, sync + async, writes report + JSON),gi_client.py(the/v1client), and optionalgi_fetch.py/gi_ensembl.py(gene/region -> FASTA via public Ensembl, incl. TSS-centring).references/{api,tasks,authentication,sequence-acquisition,errors}.md.evals/{evals,trigger_evals}.json- matching the repo's eval schema (promoter, expression-window, and annotation-async cases).assets/demo/*.fa- one real reference FASTA per task (e.g. TP53 promoter, HBB splice, HBB-K562 9,198 bp expression window).Auth
Users set
GI_API_KEY(partner key, request at contact@genomicintelligence.ai). No key is committed; the skill resolves it from the environment.Licensing / DCO
Contribution dual-licensed
Apache-2.0 OR CC-BY-4.0; commits are DCO signed-off.