upstream-radar 0.33.0 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,6 +29,7 @@
29
29
  <a href="#see-one-incident">See one incident</a> ·
30
30
  <a href="#install-in-dsh">Install in DSH</a> ·
31
31
  <a href="#notify-feishu-or-an-https-endpoint">Notify Feishu</a> ·
32
+ <a href="#observe-dsh-plugin-upstream-changes">Observe upstream changes</a> ·
32
33
  <a href="#run-the-proof">Run the proof</a> ·
33
34
  <a href="#run-it-in-github-actions">Run in GitHub Actions</a> ·
34
35
  <a href="https://github.com/MicroMilo/upstream-radar/issues/new?template=trial.yml">Share feedback</a> ·
@@ -51,7 +52,9 @@ It looks only at the current directory and local DSH profile metadata. It recomm
51
52
  | Your goal | Start here | What you get |
52
53
  | --- | --- | --- |
53
54
  | Keep a live DSH Agent informed | [`setup`](#install-in-dsh) | A profile-aware monitor that refreshes the installed graph and routes only changed incidents to the matching Agent. |
55
+ | Observe DSH plugin upstream changes | [`observe`](#observe-dsh-plugin-upstream-changes) | A scheduled old → new comparison of GitHub commits, npm releases, manifests, and optional lockfile graphs; only meaningful changes wake an Agent. |
54
56
  | Respond to the first alert | [`radar next`](#install-in-dsh) | One read-only command selects the highest-priority incident and points to the DSH task, verified analysis, or next check. |
57
+ | Check a DSH profile before starting it | [`profile-check`](#check-a-dsh-profile-before-starting-it) | Reads the actual lockfile and patch rows and blocks missing loader packages, duplicate loader ids, and release-age rollback risk. |
55
58
  | Add a scheduled CI gate | [GitHub Actions example](examples/github-actions/upstream-radar.yml) | A frozen check from a reviewed config or one lockfile, with a concise Job Summary and a machine-readable JSON report. |
56
59
  | Check a plugin before installing it | [`graph` / `init` for npm or pnpm lockfiles](#inspect-an-npm-or-pnpm-lockfile-before-installation) | Exact dependency paths and OSV/GitHub Advisory results without running the plugin or its lifecycle scripts. |
57
60
  | Review one exact published artifact | `upstream-radar inspect npm:<package>@<exact-version> --deep` | Package, dependency, vulnerability, and provenance evidence for one release. |
@@ -84,6 +87,28 @@ Next: Review the fixed version with the DSH Agent in this project.
84
87
 
85
88
  The useful part is the exact path and project-specific next step—not another generic list of vulnerable package names.
86
89
 
90
+ Want to try it on a real published DSH plugin immediately?
91
+
92
+ ```bash
93
+ npx --yes upstream-radar@0.33.1 inspect npm:dsh-feishu-bot@0.15.4 --deep
94
+ ```
95
+
96
+ This runs from an otherwise empty directory and returns a short admission,
97
+ coverage, dependency-count, vulnerability-count, and next-step summary.
98
+
99
+ Want to see a real author-actionable result? This exact published DSH plugin
100
+ currently cannot produce a complete dependency graph in a clean npm resolver:
101
+
102
+ ```bash
103
+ npx --yes upstream-radar@0.33.1 inspect \
104
+ npm:@sanqi-normal/dsh-webui-market-plugin@0.5.4 \
105
+ --deep --fail-on never
106
+ ```
107
+
108
+ The result is `review / incomplete`, with the concrete cause
109
+ `@deepseek-ai/dsh-compact@^0.0.1-rc.1` not published. No DSH profile, plugin
110
+ execution, or LLM is required. See the [reproducible author report](examples/dsh/reports/sanqi-market-plugin-dependency-resolution.md).
111
+
87
112
  Tried the demo or a real DSH setup? [Share a short trial result](https://github.com/MicroMilo/upstream-radar/issues/new?template=trial.yml) with the versions, path, and redacted outcome. Never include source code, secrets, or private paths.
88
113
 
89
114
  Every command has its own short guide: `npx --yes upstream-radar@latest setup --help`, `npx --yes upstream-radar@latest inspect --help`, and `npx --yes upstream-radar@latest radar status --help` are useful starting points when you are not sure which path to choose.
@@ -144,6 +169,181 @@ pnpm dlx --package=upstream-radar@latest upstream-radar radar watch ./upstream-r
144
169
 
145
170
  Remove `--once` to keep a local monitor alive. This is a lightweight CLI surface for demos, CI, and diagnosis; the native DSH bundle remains the recommended always-on path because it can deliver the task to a live Agent.
146
171
 
172
+ ## Check a DSH profile before starting it
173
+
174
+ When the concern is “will this profile boot with the packages and patch rows it
175
+ actually has?”, use the static profile check first:
176
+
177
+ ```bash
178
+ pnpm run build
179
+ node dist/src/cli.js profile-check "$DSH_HOME/profiles/web" \
180
+ --report ./dsh-profile-check.md
181
+ ```
182
+
183
+ For the shortest answer, add `--summary`:
184
+
185
+ ```bash
186
+ pnpm dlx --package=upstream-radar@latest upstream-radar profile-check \
187
+ "$DSH_HOME/profiles/web" --summary
188
+ ```
189
+
190
+ When `DSH_HOME` contains exactly one profile with third-party bundles, the
191
+ directory can be omitted:
192
+
193
+ ```bash
194
+ npx --yes upstream-radar@latest profile-check --summary
195
+ ```
196
+
197
+ With no eligible profile, or more than one, Radar prints the names it found and
198
+ asks for an explicit directory; it never guesses between multiple profiles.
199
+
200
+ It prints only the status, the important evidence, the reason, and the next
201
+ repair. The exit code remains `2` for a blocked profile and `0` for a pass.
202
+
203
+ It reads the profile manifest, pnpm/npm lockfile, package metadata,
204
+ `pnpm-workspace.yaml`, and `cordis.patch.yml`. It catches the two concrete
205
+ failure shapes from [dsh-web-ui #71](https://github.com/zhu1090093659/dsh-web-ui/issues/71)
206
+ and [#35](https://github.com/zhu1090093659/dsh-web-ui/issues/35): a loader row
207
+ that names a package absent from the locked profile, and the same loader id
208
+ being inserted twice. It also points out a pnpm `minimumReleaseAge` policy that
209
+ does not exempt the plugin, because that can keep a newly fixed plugin on an
210
+ older release during its cooling window.
211
+
212
+ This is deliberately a pre-start check: no network, installation, plugin code,
213
+ DSH process, Agent, or LLM is involved. A blocked result exits with code `2`.
214
+ The complete replay is `pnpm run showcase:dsh-profile-check`; it runs the
215
+ public case before the fix, after the manual package workaround, and after the
216
+ correct bundled-carrier fix.
217
+
218
+ The short, author-facing result is `pnpm run showcase:dsh-case`. It turns the
219
+ same three static checks into one repair story: the old profile is blocked,
220
+ manually adding the missing package creates a duplicate loader, and the
221
+ bundled-carrier update reaches `pass`. If an OpenAI-compatible `issue-locator`
222
+ model is available, pass its env file with
223
+ `ISSUE_LOCATOR_ENV_FILE=/path/to/issue-locator/.env`; the model only explains
224
+ the already-checked facts. If the endpoint is unavailable, the command still
225
+ prints the deterministic evidence explanation and records that the fallback was
226
+ used. Add `:report` to write the [case analysis result](examples/dsh/reports/dsh-web-ui-issue-71-analysis.json).
227
+
228
+ We also ran the current static checks against the first 50 entries in the DSH
229
+ plugin registry. That batch found **0 confirmed runtime dependency
230
+ vulnerabilities**. It did find real monitoring-quality problems—development-only
231
+ dependencies mixed into source lockfiles, three plugin lockfiles whose root
232
+ version lagged the source manifest, and a tarball format the scanner could not
233
+ parse. The [batch report](examples/dsh/reports/dsh-batch-50-2026-08-17.md)
234
+ keeps those results honest; it is not marketed as a vulnerability hit list.
235
+
236
+ ## Observe DSH plugin upstream changes
237
+
238
+ This is the upstream-change loop: instead of polling every vulnerability source on
239
+ every run, Radar remembers one observation point per plugin and asks what changed
240
+ since then.
241
+
242
+ ```text
243
+ targets.yml
244
+
245
+ GitHub commit + npm package metadata + optional lockfile
246
+
247
+ observations.json
248
+
249
+ old → new comparison
250
+
251
+ only meaningful changes → DSH Agent task → report
252
+ ```
253
+
254
+ Start from the [copyable target example](examples/upstream-observer/targets.yml):
255
+
256
+ ```yaml
257
+ schema: upstream-radar.observer-targets/v1alpha1
258
+ targets:
259
+ - id: my-dsh-plugin
260
+ ecosystem: dsh
261
+ repository: acme/my-dsh-plugin
262
+ ref: main
263
+ package: my-dsh-plugin
264
+ packagePath: plugin/package.json
265
+ lockfile: plugin/pnpm-lock.yaml
266
+ lockfileType: pnpm
267
+ ```
268
+
269
+ Then run one cycle:
270
+
271
+ ```bash
272
+ export GITHUB_TOKEN='a read-only GitHub token'
273
+ pnpm run build
274
+ node dist/src/cli.js observe \
275
+ ./targets.yml \
276
+ --state ./observations.json \
277
+ --report ./upstream-radar-observer.md
278
+ ```
279
+
280
+ This command uses the checked-out source so it is runnable before the next npm
281
+ release. After a release includes `observe`, pin that exact version in CI rather
282
+ than relying on `latest`.
283
+
284
+ The first cycle only creates a baseline. Later cycles compare:
285
+
286
+ - the source commit and changed files;
287
+ - the published npm version and integrity value;
288
+ - the package entrypoint, exports, Node requirement, DSH bundle metadata and dependency declarations;
289
+ - the real npm or pnpm lockfile graph, when a lockfile is configured.
290
+
291
+ README/docs/tests-only changes advance the observation point without waking the
292
+ Agent. Runtime source, DSH bundle, package entry, dependency graph, npm version,
293
+ or npm integrity changes create an old → new task. If the Agent is not configured,
294
+ the task stays in `observations.json`; no plugin is installed or executed.
295
+
296
+ If you do not have a DSH wrapper configured yet, you can point the observer at
297
+ an existing issue-locator/OpenAI-compatible `.env` file instead:
298
+
299
+ ```bash
300
+ upstream-radar observe ./targets.yml \
301
+ --state ./observations.json \
302
+ --llm-env-file /path/to/issue-locator/.env
303
+ ```
304
+
305
+ Radar reads only the endpoint, API key, and model name for that call. It never
306
+ writes the key or endpoint into the observation state or report. The model is
307
+ called only after a meaningful upstream change; a baseline or docs-only change
308
+ does not call it. If the endpoint is unavailable, the deterministic change
309
+ record remains pending and can be retried with `--retry-pending`.
310
+
311
+ The env file may use the issue-locator names (`ISSUE_LOCATOR_LLM_*`), the common
312
+ OpenAI names (`OPENAI_BASE_URL`, `OPENAI_API_KEY`, `OPENAI_MODEL`), or `MODEL` /
313
+ `CODEX_MODEL` for the model name.
314
+ For ModelBest-style URLs, a 404 on `/llm/v1` also retries `/llm/openai/v1`.
315
+
316
+ The scheduled workflow is [examples/github-actions/upstream-observer.yml](examples/github-actions/upstream-observer.yml).
317
+ The checked-in workflow is a dogfood workflow for this repository: it checks out
318
+ and builds Radar before running the observer. It persists only the observation
319
+ point. A quiet run does not create a daily commit.
320
+
321
+ The workflow also supports three optional repository secrets—
322
+ `ISSUE_LOCATOR_LLM_BASE_URL`, `ISSUE_LOCATOR_LLM_API_KEY`, and
323
+ `ISSUE_LOCATOR_LLM_MODEL`. When all three exist, the job sends meaningful tasks
324
+ to the issue-locator/OpenAI-compatible model. When they do not exist, static
325
+ upstream observation still runs and the job does not pretend that model analysis
326
+ was performed.
327
+
328
+ ### The DSH Agent boundary
329
+
330
+ The observer accepts an explicit executable through `--dsh-agent-command`. It
331
+ writes one bounded, read-only task prompt to stdin and expects one JSON conclusion
332
+ on stdout. The command is started without a shell, and the prompt treats every
333
+ remote repository string and release field as untrusted evidence.
334
+
335
+ ```bash
336
+ upstream-radar observe ./targets.yml \
337
+ --state ./observations.json \
338
+ --dsh-agent-command /path/to/reviewed-dsh-agent-wrapper \
339
+ --dsh-agent-arg --json
340
+ ```
341
+
342
+ Radar does not guess an undocumented `dsh` CLI subcommand. A reviewed DSH
343
+ headless wrapper is the integration boundary; this keeps the observer usable in
344
+ GitHub Actions and lets the DSH adapter evolve without changing observation or
345
+ diff logic. Use `--retry-pending` to deliver tasks left by a previous run.
346
+
147
347
  ## Notify Feishu or an HTTPS endpoint
148
348
 
149
349
  To also notify a team-owned HTTPS endpoint when an incident changes, keep the endpoint outside the reviewed config and state:
@@ -322,7 +522,7 @@ cd my-dsh-plugin
322
522
  pnpm install --ignore-scripts
323
523
 
324
524
  # Read the exact graph before adding the plugin to a DSH profile.
325
- pnpm dlx --package=upstream-radar@0.33.0 upstream-radar graph pnpm-lock pnpm-lock.yaml --json
525
+ pnpm dlx --package=upstream-radar@0.33.1 upstream-radar graph pnpm-lock pnpm-lock.yaml --json
326
526
  ```
327
527
 
328
528
  The graph includes the exact DSH package versions and keeps unresolved optional peers visible. It does not load the generated plugin or run lifecycle scripts. After reviewing it, copy this complete workflow into `.github/workflows/upstream-radar.yml`:
@@ -344,7 +544,7 @@ jobs:
344
544
  runs-on: ubuntu-latest
345
545
  steps:
346
546
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
347
- - uses: MicroMilo/upstream-radar@v0.33.0
547
+ - uses: MicroMilo/upstream-radar@v0.33.1
348
548
  with:
349
549
  fail-on: high
350
550
  fail-on-compatibility: breaking
@@ -352,6 +552,22 @@ jobs:
352
552
 
353
553
  The Action auto-detects the one `pnpm-lock.yaml`, checks the same exact graph, and writes the result to the Job Summary. This is a pre-install and CI gate; it does not install the plugin into DSH. After the graph is reviewed, use the normal `dsh plugin` flow to install it and `upstream-radar setup` to start project-aware monitoring.
354
554
 
555
+ To check a real published DSH artifact directly, run one command:
556
+
557
+ ```bash
558
+ npx --yes upstream-radar@0.33.1 inspect npm:dsh-feishu-bot@0.15.4 --deep
559
+ ```
560
+
561
+ The checked result is `REVIEW`: registry integrity, signature, provenance, and
562
+ 89 resolved packages are verified; known vulnerabilities are `0`, while 12
563
+ optional dependency edges remain unresolved. That is a useful author result:
564
+ the empty vulnerability list is visible, but it is not mislabeled as a full
565
+ `ALLOW` decision.
566
+
567
+ The same exact tarball loads in both DSH `0.1.0-rc.6` and `0.1.0-rc.7` disposable
568
+ profiles. See the [real compatibility probe](examples/dsh/reports/dsh-feishu-bot-0.15.4-probe.md)
569
+ for the command and boundary.
570
+
355
571
  ## See one incident
356
572
 
357
573
  If an advisory affects only one of two installed `parser` versions, Radar reports the path that actually matched:
@@ -483,7 +699,11 @@ To demonstrate the host-runtime dependency path specifically, run `pnpm run show
483
699
 
484
700
  To see why one shared host bug should not page every plugin separately, run `pnpm run showcase:dsh-host-alert`. Two plugin roots share the same exact `@deepseek-ai/cordis` version; Radar emits one project event, keeps both exact paths, and creates one DSH analysis task. Add `:report` to refresh the checked-in [deduplication result](examples/dsh/reports/dsh-host-alert-dedup.json).
485
701
 
486
- To validate the actual first-use path against the real published [`dsh-cloudflare-browser-run@0.1.1`](https://www.npmjs.com/package/dsh-cloudflare-browser-run), run `pnpm run showcase:dsh-adoption`. It creates a disposable `DSH_HOME`, packs the exact Radar and plugin tarballs with lifecycle scripts disabled, lets DSH build its own host runtime, runs `setup --no-install`, `doctor`, a frozen OSV/npm/GitHub check, and the human-readable status surface. It does not start a DSH Agent or call a model, and it does not treat an empty finding list as a safety certificate. The checked-in [adoption result](examples/dsh/reports/adoption-smoke.json) records the last run's package counts and boundaries.
702
+ To validate the actual first-use path against several real published DSH plugins, run `pnpm run showcase:dsh-adoption`. It creates a disposable `DSH_HOME`, packs exact Radar and plugin tarballs with lifecycle scripts disabled, lets DSH build its own host runtime, runs `setup --no-install`, `doctor`, a frozen OSV/npm/GitHub check, and the human-readable status surface. The checked-in trial covers [`dsh-cloudflare-browser-run@0.1.1`](https://www.npmjs.com/package/dsh-cloudflare-browser-run), [`@open-agfs/dsh-agfs@0.1.9`](https://www.npmjs.com/package/@open-agfs/dsh-agfs), and [`dsh-feishu-bot@0.14.0`](https://www.npmjs.com/package/dsh-feishu-bot). The first two install and become monitorable; the Feishu bridge is intentionally recorded as blocked because a clean DSH profile stops on its transitive `protobufjs` build script until a human approves it. A blocked install is not reported as a clean security result. The showcase does not start a DSH Agent or call a model; the separate `try:dsh` proof covers that handoff. The checked-in [adoption result](examples/dsh/reports/adoption-smoke.json) records each plugin's install state, graph coverage, source health, and boundary.
703
+
704
+ The DSH Agent handoff is optional for dependency analysis. If you want to smoke-test that integration with a real published plugin, run `pnpm run try:dsh:real`. It installs the exact published `dsh-find-plugin@0.3.6` into a disposable headless profile and proves that a real DSH Agent receives, consumes, and writes back a Radar analysis task. The model is still a local deterministic stub; no plugin business action or paid endpoint is called. Set `DSH_REAL_PLUGINS` to another exact package only after checking its required profile and credentials.
705
+
706
+ For a public compatibility case, run `pnpm run try:dsh:public-case`. It replays [`dsh-web-ui #35`](https://github.com/zhu1090093659/dsh-web-ui/issues/35) and [`#71`](https://github.com/zhu1090093659/dsh-web-ui/issues/71): the old profile is blocked by a missing loader, the manual package workaround is blocked by a duplicate loader id, and the maintainer's bundled-carrier fix passes. The same compatibility event is then admitted to a real DSH `headless` session and written back as one verified `analysisResult`; see the checked-in [case result](examples/dsh/reports/dsh-web-ui-public-case.json). This is an end-to-end delivery proof with a local deterministic model stub, not a claim about online model quality. The command does not use your DSH credentials or call a paid model endpoint.
487
707
 
488
708
  To see the two-source vulnerability contract without contacting the network, run `pnpm run showcase:github-advisories`. It feeds the same parser issue through OSV and a deterministic GitHub Advisory Database client, proves that two reports become one Radar incident with explicit source provenance and a visible fixed-version conflict, then simulates three GitHub failures and recovery. The existing vulnerability remains active throughout; only the GitHub source-health incident changes.
489
709
 
@@ -492,7 +712,7 @@ To see the two-source vulnerability contract without contacting the network, run
492
712
  Before wiring a project into a compatibility gate, run the offline rule benchmark:
493
713
 
494
714
  ```bash
495
- pnpm dlx --package=upstream-radar@0.33.0 upstream-radar benchmark compatibility
715
+ pnpm dlx --package=upstream-radar@0.33.1 upstream-radar benchmark compatibility
496
716
  ```
497
717
 
498
718
  It covers six contracts: a safe patch, a change that only needs project analysis, an incompatible DSH peer, a publisher-declared breaking release, a vulnerable candidate dependency, and an incomplete candidate graph. The command does not access the network, install a package, load a plugin, or start DSH. It checks the behavior of Radar's deterministic rules and the `breaking`/`any` gates; it is not a runtime compatibility proof.
@@ -505,7 +725,7 @@ When you have an exact plugin artifact and want to know whether one exact DSH re
505
725
  # Pack an exact npm release without running its lifecycle scripts.
506
726
  npm pack --ignore-scripts dsh-plugin@1.2.3
507
727
 
508
- pnpm dlx --package=upstream-radar@0.33.0 upstream-radar probe dsh-load \
728
+ pnpm dlx --package=upstream-radar@0.33.1 upstream-radar probe dsh-load \
509
729
  ./dsh-plugin-1.2.3.tgz \
510
730
  --dsh-version 0.1.0-rc.6
511
731
  ```
@@ -531,7 +751,7 @@ It exercises a loadable bundle, a bundle patch DSH rejects, and a package that r
531
751
  To compare a plugin against more than one DSH release, use the matrix form:
532
752
 
533
753
  ```bash
534
- pnpm dlx --package=upstream-radar@0.33.0 upstream-radar probe dsh-matrix \
754
+ pnpm dlx --package=upstream-radar@0.33.1 upstream-radar probe dsh-matrix \
535
755
  ./dsh-plugin-1.2.3.tgz \
536
756
  --dsh-version 0.1.0-rc.3 \
537
757
  --dsh-version 0.1.0-rc.6 \
@@ -547,7 +767,7 @@ If your team wants the shortest scheduled CI gate before wiring a machine to a l
547
767
  ```yaml
548
768
  steps:
549
769
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
550
- - uses: MicroMilo/upstream-radar@v0.33.0
770
+ - uses: MicroMilo/upstream-radar@v0.33.1
551
771
  with:
552
772
  fail-on: high
553
773
  # Optional: also fail on deterministic DSH/plugin compatibility breaks.
@@ -556,12 +776,12 @@ steps:
556
776
  threat-intel: true
557
777
  ```
558
778
 
559
- The Action is a thin wrapper around `radar check --frozen --state :memory: --fail-on high --json`; when the optional compatibility input is enabled, it also passes `--fail-on-compatibility breaking` or `any`. `--frozen` is deliberate: it uses the graph in the reviewed config and does not try to read a developer's local DSH profile. `threat-intel` is false by default so an ordinary CI gate stays lean; set it to `true` when the Job Summary and raw JSON should include CISA KEV and FIRST EPSS prioritization evidence. Each run is independent, exits `2` when an active vulnerability or opted-in compatibility change meets its threshold, and exits `1` for an operational or source error. `breaking` catches confirmed or strong incompatibility signals; `any` catches every active compatibility event. The default is `never`, so vulnerability-only behavior stays unchanged. In addition to the raw JSON log, the Action writes a short escaped summary to the GitHub Job Summary so a scheduled failure immediately shows the affected package, exact path, published fix version when available, one-line priority evidence, and a suggested next step. The Action does not deliver a DSH Agent task or modify a branch; the native DSH bundle remains the always-on analysis path. Pin the Action to a release tag such as `v0.33.0`, and pin the checkout Action in your workflow according to your repository's policy.
779
+ The Action is a thin wrapper around `radar check --frozen --state :memory: --fail-on high --json`; when the optional compatibility input is enabled, it also passes `--fail-on-compatibility breaking` or `any`. `--frozen` is deliberate: it uses the graph in the reviewed config and does not try to read a developer's local DSH profile. `threat-intel` is false by default so an ordinary CI gate stays lean; set it to `true` when the Job Summary and raw JSON should include CISA KEV and FIRST EPSS prioritization evidence. Each run is independent, exits `2` when an active vulnerability or opted-in compatibility change meets its threshold, and exits `1` for an operational or source error. `breaking` catches confirmed or strong incompatibility signals; `any` catches every active compatibility event. The default is `never`, so vulnerability-only behavior stays unchanged. In addition to the raw JSON log, the Action writes a short escaped summary to the GitHub Job Summary so a scheduled failure immediately shows the affected package, exact path, published fix version when available, one-line priority evidence, and a suggested next step. The Action does not deliver a DSH Agent task or modify a branch; the native DSH bundle remains the always-on analysis path. Pin the Action to a release tag such as `v0.33.1`, and pin the checkout Action in your workflow according to your repository's policy.
560
780
 
561
781
  If the repository has no committed Radar config yet, the smallest setup is to omit `config`, `pnpm-lock`, and `npm-lock`. After checkout, the Action automatically uses the only one of `pnpm-lock.yaml` or `package-lock.json` that exists, generates a temporary reviewed config, and runs the same frozen check:
562
782
 
563
783
  ```yaml
564
- - uses: MicroMilo/upstream-radar@v0.33.0
784
+ - uses: MicroMilo/upstream-radar@v0.33.1
565
785
  with:
566
786
  fail-on: high
567
787
  ```
@@ -571,7 +791,7 @@ An existing `config` wins over auto-detection. If both lockfiles exist, or neith
571
791
  To review the exact plugin artifact before it enters DSH, add `inspect-package`:
572
792
 
573
793
  ```yaml
574
- - uses: MicroMilo/upstream-radar@v0.33.0
794
+ - uses: MicroMilo/upstream-radar@v0.33.1
575
795
  with:
576
796
  inspect-package: dsh-cloudflare-browser-run@0.1.1
577
797
  # review is the safe default; use block only when incomplete coverage may pass.
@@ -583,7 +803,7 @@ This downloads that exact npm tarball, verifies the registry integrity/signature
583
803
  If the repository has a pnpm lockfile but no committed Radar config yet, the Action can generate the config in the same job. See the [copyable pnpm workflow](examples/github-actions/upstream-radar-pnpm.yml):
584
804
 
585
805
  ```yaml
586
- - uses: MicroMilo/upstream-radar@v0.33.0
806
+ - uses: MicroMilo/upstream-radar@v0.33.1
587
807
  with:
588
808
  pnpm-lock: pnpm-lock.yaml
589
809
  fail-on: high
@@ -597,7 +817,7 @@ See the [copyable npm workflow](examples/github-actions/upstream-radar-npm.yml)
597
817
  The Action requires the caller to check out the repository first. It does not install the project's dependencies or run their lifecycle scripts; it only reads the committed graph and queries the configured upstream sources. For a fully explicit, lower-level invocation, the equivalent command is:
598
818
 
599
819
  ```bash
600
- pnpm dlx --package=upstream-radar@0.33.0 upstream-radar radar check \
820
+ pnpm dlx --package=upstream-radar@0.33.1 upstream-radar radar check \
601
821
  ./upstream-radar.config.json --frozen --state :memory: --fail-on high \
602
822
  --fail-on-compatibility breaking --json
603
823
  ```
@@ -605,7 +825,7 @@ pnpm dlx --package=upstream-radar@0.33.0 upstream-radar radar check \
605
825
  To add the optional DSH load matrix for a published plugin, provide an exact npm package and at least two exact DSH versions:
606
826
 
607
827
  ```yaml
608
- - uses: MicroMilo/upstream-radar@v0.33.0
828
+ - uses: MicroMilo/upstream-radar@v0.33.1
609
829
  id: radar
610
830
  with:
611
831
  config: upstream-radar.config.json
package/dist/src/cli.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import process from 'node:process';
3
3
  import { spawnSync } from 'node:child_process';
4
- import { access, readFile } from 'node:fs/promises';
4
+ import { access, readFile, writeFile } from 'node:fs/promises';
5
5
  import { dirname, join, resolve } from 'node:path';
6
6
  import { renderCompatibilityBenchmark, runCompatibilityBenchmark } from './compatibility-benchmark.js';
7
7
  import { assessCompatibilityChange } from './compatibility.js';
8
8
  import { probeDshLoad, probeDshLoadMatrix, renderDshLoadMatrix, renderDshLoadProbe } from './dsh-probe.js';
9
9
  import { createAnalysisTask, renderAgentAnalysisPrompt } from './dsh-analysis.js';
10
10
  import { createDoctorReport, renderDoctorReport } from './doctor.js';
11
+ import { checkDshProfile, renderDshProfileCheck, renderDshProfileCheckSummary } from './dsh-profile-check.js';
11
12
  import { createDemoReport, renderDemo } from './demo.js';
12
13
  import { GitHubReleaseClient } from './github-release.js';
13
14
  import { parseNpmLockGraph, parsePnpmLockGraph } from './graph.js';
@@ -30,6 +31,7 @@ import { createRadarNext, createRadarStatus, renderRadarNext, renderRadarStatus
30
31
  import { renderTextReport } from './render.js';
31
32
  import { scanDirectory } from './scan.js';
32
33
  import { CisaKevClient, EpssClient } from './threat-intel.js';
34
+ import { loadObservationState, parseObserverConfigText, renderObserverReport, runDshAgentCommand, runOpenAiCompatibleAgent, runObserver, saveObservationState, UpstreamObserverClient, } from './upstream-observer.js';
33
35
  import { TOOL_VERSION } from './version.js';
34
36
  import { eventsForRadarWebhookTarget, markRadarWebhookEventsDelivered, markRadarWebhookEventsDeliveredForRoute, normalizeRadarWebhookUrl, queueRadarWebhookEvents, queueRadarWebhookEventsForRoute, resolveRadarWebhookTargets, sendRadarWebhook, undeliveredRadarWebhookEvents, undeliveredRadarWebhookEventsForRoute, } from './webhook.js';
35
37
  const VALID_THRESHOLDS = new Set(['warn', 'review', 'block', 'never']);
@@ -186,6 +188,26 @@ and queries the implemented vulnerability checks. An empty finding list is not
186
188
  a safety certificate; check the coverage verdict before admitting the package.
187
189
  The default gate exits 2 for review or block; use --fail-on block when review
188
190
  should remain visible without failing CI.
191
+ `,
192
+ observe: `Upstream Radar — watch DSH plugin repositories and packages for meaningful upstream changes
193
+
194
+ Usage:
195
+ upstream-radar observe <targets.yml> [--state <observations.json>]
196
+ [--report <report.md>] [--dsh-agent-command <executable>]
197
+ [--dsh-agent-arg <argument>] [--llm-env-file <path>] [--retry-pending] [--json]
198
+
199
+ The first run creates a baseline. Later runs compare source commits, published
200
+ npm metadata, package manifests, and an optional npm/pnpm dependency graph. A
201
+ DSH Agent is called only for meaningful changes. Safety: does not install packages, run lifecycle scripts, load plugin code, or invoke a shell.
202
+
203
+ The Agent executable receives one read-only task prompt on stdin and should
204
+ return one JSON conclusion on stdout. If it is not configured, the task stays
205
+ in observations.json for a later explicit retry. As a simpler alternative,
206
+ --llm-env-file reads an OpenAI-compatible issue-locator/.env-style file for
207
+ only the model call. It accepts ISSUE_LOCATOR_LLM_*, OPENAI_*, or MODEL/CODEX_MODEL
208
+ keys; it never writes the key or endpoint to observations.json.
209
+ If a ModelBest-style base URL ends in /llm/v1, a 404 also retries the known
210
+ /llm/openai/v1 path.
189
211
  `,
190
212
  graph: `Upstream Radar — read a lockfile into the canonical dependency graph
191
213
 
@@ -195,6 +217,18 @@ Usage:
195
217
 
196
218
  This command is offline and does not install packages, run lifecycle scripts,
197
219
  load plugin code, or query vulnerability sources.
220
+ `,
221
+ 'profile-check': `Upstream Radar — check one DSH profile before starting it
222
+
223
+ Usage:
224
+ upstream-radar profile-check [profile-directory] [--patch <path>] [--report <path>] [--summary] [--json]
225
+
226
+ Reads the profile package manifest, lockfile, node_modules package metadata,
227
+ pnpm release-age policy, and cordis.patch.yml. It reports loader rows that
228
+ refer to missing packages and duplicate loader ids. It never installs packages,
229
+ starts DSH, loads plugin code, contacts a vulnerability source, or invokes a
230
+ DSH Agent/model. When the directory is omitted, the only DSH profile with
231
+ third-party bundles is selected automatically.
198
232
  `,
199
233
  probe: `Upstream Radar — test whether a DSH bundle loads in disposable profiles
200
234
 
@@ -371,7 +405,9 @@ Usage:
371
405
  upstream-radar doctor [config.json] [options]
372
406
  upstream-radar scan <directory> [--json] [--fail-on <warn|review|block|never>]
373
407
  upstream-radar inspect npm:<package>@<exact-version> [--deep] [--json] [--fail-on <warn|review|block|never>]
408
+ upstream-radar observe <targets.yml> [--state <observations.json>] [--report <report.md>] [--dsh-agent-command <executable>] [--dsh-agent-arg <argument>] [--llm-env-file <path>] [--retry-pending] [--json]
374
409
  upstream-radar graph <npm-lock|pnpm-lock> <lockfile> [--root <package>@<exact-version>] [--json]
410
+ upstream-radar profile-check [profile-directory] [--patch <path>] [--report <path>] [--summary] [--json]
375
411
  upstream-radar probe dsh-load <package.tgz> [--dsh-version <exact-version>] [--timeout <seconds>] [--keep-profile] [--json]
376
412
  upstream-radar probe dsh-matrix <package.tgz> --dsh-version <v1>[,<v2>,...] [--timeout <seconds>] [--keep-profile] [--json]
377
413
  upstream-radar demo [--json]
@@ -399,7 +435,9 @@ Commands:
399
435
  doctor check local Radar/DSH wiring without polling upstream sources
400
436
  scan bounded, read-only inspection of a local package directory
401
437
  inspect fetch and verify the exact npm artifact before inspecting its contents
438
+ observe compare upstream DSH plugin repositories and route only meaningful changes to a DSH Agent
402
439
  graph read a lockfile into the canonical dependency graph without installing packages
440
+ profile-check check a DSH profile's lockfile and patch rows without starting DSH
403
441
  probe run a bounded DSH bundle-load check or version matrix in disposable profiles
404
442
  demo show the exact-path-to-DSH handoff without network, DSH, or plugin installation
405
443
  quickstart choose the smallest first-use path without changing the environment
@@ -533,6 +571,57 @@ async function runGraph(args) {
533
571
  ].join('\n') + '\n');
534
572
  return 0;
535
573
  }
574
+ async function runDshProfileCheck(args) {
575
+ let profileDirectory;
576
+ let firstOption = 0;
577
+ if (args[0] !== undefined && !args[0].startsWith('-')) {
578
+ profileDirectory = args[0];
579
+ firstOption = 1;
580
+ }
581
+ let json = false;
582
+ let summary = false;
583
+ let reportPath;
584
+ let patchPath;
585
+ for (let index = firstOption; index < args.length; index += 1) {
586
+ const argument = args[index];
587
+ if (argument === '--json') {
588
+ json = true;
589
+ }
590
+ else if (argument === '--summary') {
591
+ summary = true;
592
+ }
593
+ else if (argument === '--patch' || argument === '--report') {
594
+ const value = args[index + 1];
595
+ if (value === undefined || value.startsWith('-'))
596
+ throw new Error(`${argument} requires a value`);
597
+ if (argument === '--patch')
598
+ patchPath = value;
599
+ else
600
+ reportPath = value;
601
+ index += 1;
602
+ }
603
+ else {
604
+ throw new Error(`unknown option for profile-check: ${argument}`);
605
+ }
606
+ }
607
+ if (profileDirectory === undefined) {
608
+ const profiles = await discoverDshProfiles();
609
+ if (profiles.length === 0) {
610
+ throw new Error('profile-check could not find a DSH profile with third-party bundles; pass <profile-directory> explicitly');
611
+ }
612
+ if (profiles.length > 1) {
613
+ throw new Error(`profile-check found multiple DSH profiles with third-party bundles (${profiles.join(', ')}); pass <profile-directory> explicitly`);
614
+ }
615
+ profileDirectory = resolveDshProfileDirectory(profiles[0]);
616
+ }
617
+ const report = await checkDshProfile({ profileDirectory, ...(patchPath === undefined ? {} : { patchFile: patchPath }) });
618
+ const jsonText = `${JSON.stringify(report, null, 2)}\n`;
619
+ if (reportPath !== undefined) {
620
+ await writeFile(resolve(reportPath), reportPath.endsWith('.json') ? jsonText : summary ? renderDshProfileCheckSummary(report) : renderDshProfileCheck(report));
621
+ }
622
+ process.stdout.write(json ? jsonText : summary ? renderDshProfileCheckSummary(report) : renderDshProfileCheck(report));
623
+ return report.status === 'blocked' ? 2 : 0;
624
+ }
536
625
  async function runTask(args) {
537
626
  const subcommand = args[0];
538
627
  if (subcommand !== 'list' && subcommand !== 'show' && subcommand !== 'ack') {
@@ -1303,6 +1392,85 @@ async function runRadar(args) {
1303
1392
  : `${renderRadarEvents(events)}Prepared ${analysisTasks.length} DSH compatibility analysis task(s).\n`);
1304
1393
  return 0;
1305
1394
  }
1395
+ async function runObserve(args) {
1396
+ const targetsPath = args[0];
1397
+ if (targetsPath === undefined || targetsPath.startsWith('-'))
1398
+ throw new Error('observe requires a targets.yml file');
1399
+ let statePath = 'observations.json';
1400
+ let reportPath;
1401
+ let agentCommand;
1402
+ let agentArgs = [];
1403
+ let llmEnvFile;
1404
+ let registry;
1405
+ let retryPending = false;
1406
+ let json = false;
1407
+ for (let index = 1; index < args.length; index += 1) {
1408
+ const argument = args[index];
1409
+ if (argument === '--json') {
1410
+ json = true;
1411
+ }
1412
+ else if (argument === '--retry-pending') {
1413
+ retryPending = true;
1414
+ }
1415
+ else if (argument === '--state' || argument === '--report' || argument === '--dsh-agent-command' || argument === '--dsh-agent-arg' || argument === '--llm-env-file' || argument === '--registry') {
1416
+ const value = args[index + 1];
1417
+ if (value === undefined || (value.startsWith('-') && argument !== '--dsh-agent-arg'))
1418
+ throw new Error(`${argument} requires a value`);
1419
+ if (argument === '--state')
1420
+ statePath = value;
1421
+ else if (argument === '--report')
1422
+ reportPath = value;
1423
+ else if (argument === '--dsh-agent-command')
1424
+ agentCommand = value;
1425
+ else if (argument === '--dsh-agent-arg')
1426
+ agentArgs.push(value);
1427
+ else if (argument === '--llm-env-file')
1428
+ llmEnvFile = value;
1429
+ else
1430
+ registry = value;
1431
+ index += 1;
1432
+ }
1433
+ else {
1434
+ throw new Error(`unknown option for observe: ${argument}`);
1435
+ }
1436
+ }
1437
+ const targetText = await readBoundedFile(targetsPath, 256 * 1024);
1438
+ const config = parseObserverConfigText(targetText);
1439
+ const previousState = await loadObservationState(statePath);
1440
+ const source = new UpstreamObserverClient({
1441
+ ...(process.env.GITHUB_TOKEN === undefined ? {} : { githubToken: process.env.GITHUB_TOKEN }),
1442
+ ...(registry === undefined ? {} : { registry }),
1443
+ });
1444
+ let agentOptions;
1445
+ if (agentCommand !== undefined) {
1446
+ agentOptions = {
1447
+ command: agentCommand,
1448
+ ...(agentArgs.length === 0 ? {} : { args: agentArgs }),
1449
+ };
1450
+ }
1451
+ if (agentCommand !== undefined && llmEnvFile !== undefined) {
1452
+ throw new Error('observe accepts either --dsh-agent-command or --llm-env-file, not both');
1453
+ }
1454
+ const result = await runObserver(config, previousState, {
1455
+ source,
1456
+ retryPending,
1457
+ ...(agentOptions === undefined ? {} : {
1458
+ agent: (task, prompt) => runDshAgentCommand(task, prompt, agentOptions),
1459
+ }),
1460
+ ...(llmEnvFile === undefined ? {} : {
1461
+ agent: (task, prompt) => runOpenAiCompatibleAgent(task, prompt, { envFile: resolve(llmEnvFile) }),
1462
+ }),
1463
+ });
1464
+ await saveObservationState(statePath, result.state);
1465
+ const reportJson = `${JSON.stringify(result.report, null, 2)}\n`;
1466
+ if (reportPath !== undefined) {
1467
+ await writeFile(resolve(reportPath), reportPath.endsWith('.json') ? reportJson : renderObserverReport(result.report));
1468
+ }
1469
+ process.stdout.write(json ? reportJson : renderObserverReport(result.report));
1470
+ if (result.report.errors.length > 0 || result.report.agent.failed > 0)
1471
+ return 1;
1472
+ return 0;
1473
+ }
1306
1474
  async function runQuickstart(args) {
1307
1475
  let directory = process.cwd();
1308
1476
  let json = false;
@@ -1766,10 +1934,14 @@ async function main(args) {
1766
1934
  return runDoctor(args.slice(1));
1767
1935
  if (command === 'graph')
1768
1936
  return runGraph(args.slice(1));
1937
+ if (command === 'profile-check')
1938
+ return runDshProfileCheck(args.slice(1));
1769
1939
  if (command === 'probe')
1770
1940
  return runProbe(args.slice(1));
1771
1941
  if (command === 'demo')
1772
1942
  return runDemo(args.slice(1));
1943
+ if (command === 'observe')
1944
+ return runObserve(args.slice(1));
1773
1945
  if (command === 'benchmark')
1774
1946
  return runBenchmark(args.slice(1));
1775
1947
  if (command === 'radar')