upstream-radar 0.33.0 → 0.33.2

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,10 +52,12 @@ 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
- | Review one exact published artifact | `upstream-radar inspect npm:<package>@<exact-version> --deep` | Package, dependency, vulnerability, and provenance evidence for one release. |
60
+ | Review one exact published artifact | `upstream-radar inspect <package>@<exact-version> --deep` | Package, dependency, vulnerability, and provenance evidence for one release. |
58
61
  | Publish and maintain a DSH plugin | [Plugin author path](#for-dsh-plugin-authors) | Start from a real DSH scaffold, review its locked graph, and add a two-step CI gate before users install it. |
59
62
  | Send changed events to Feishu | [Feishu or HTTPS webhook](#notify-feishu-or-an-https-endpoint) | Native Feishu V2 text, environment-only secrets, durable acknowledgement, and retry. |
60
63
 
@@ -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.2 inspect 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.2 inspect \
104
+ @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,177 @@ 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
+ npx --yes upstream-radar@0.33.2 observe \
274
+ ./targets.yml \
275
+ --state ./observations.json \
276
+ --report ./upstream-radar-observer.md
277
+ ```
278
+
279
+ This uses the published CLI; pin the exact version in CI rather than relying on
280
+ `latest`. The first cycle only creates a baseline. Later cycles compare:
281
+
282
+ - the source commit and changed files;
283
+ - the published npm version and integrity value;
284
+ - the package entrypoint, exports, Node requirement, DSH bundle metadata and dependency declarations;
285
+ - the real npm or pnpm lockfile graph, when a lockfile is configured.
286
+
287
+ README/docs/tests-only changes advance the observation point without waking the
288
+ Agent. Runtime source, DSH bundle, package entry, dependency graph, npm version,
289
+ or npm integrity changes create an old → new task. If the Agent is not configured,
290
+ the task stays in `observations.json`; no plugin is installed or executed.
291
+
292
+ If you do not have a DSH wrapper configured yet, you can point the observer at
293
+ an existing issue-locator/OpenAI-compatible `.env` file instead:
294
+
295
+ ```bash
296
+ upstream-radar observe ./targets.yml \
297
+ --state ./observations.json \
298
+ --llm-env-file /path/to/issue-locator/.env
299
+ ```
300
+
301
+ Radar reads only the endpoint, API key, and model name for that call. It never
302
+ writes the key or endpoint into the observation state or report. The model is
303
+ called only after a meaningful upstream change; a baseline or docs-only change
304
+ does not call it. If the endpoint is unavailable, the deterministic change
305
+ record remains pending and can be retried with `--retry-pending`.
306
+
307
+ The env file may use the issue-locator names (`ISSUE_LOCATOR_LLM_*`), the common
308
+ OpenAI names (`OPENAI_BASE_URL`, `OPENAI_API_KEY`, `OPENAI_MODEL`), or `MODEL` /
309
+ `CODEX_MODEL` for the model name.
310
+ For ModelBest-style URLs, a 404 on `/llm/v1` also retries `/llm/openai/v1`.
311
+
312
+ The scheduled workflow is [examples/github-actions/upstream-observer.yml](examples/github-actions/upstream-observer.yml).
313
+ The checked-in workflow is a dogfood workflow for this repository: it checks out
314
+ and builds Radar before running the observer. It persists only the observation
315
+ point. A quiet run does not create a daily commit.
316
+
317
+ The workflow also supports three optional repository secrets—
318
+ `ISSUE_LOCATOR_LLM_BASE_URL`, `ISSUE_LOCATOR_LLM_API_KEY`, and
319
+ `ISSUE_LOCATOR_LLM_MODEL`. When all three exist, the job sends meaningful tasks
320
+ to the issue-locator/OpenAI-compatible model. When they do not exist, static
321
+ upstream observation still runs and the job does not pretend that model analysis
322
+ was performed.
323
+
324
+ ### The DSH Agent boundary
325
+
326
+ The observer accepts an explicit executable through `--dsh-agent-command`. It
327
+ writes one bounded, read-only task prompt to stdin and expects one JSON conclusion
328
+ on stdout. The command is started without a shell, and the prompt treats every
329
+ remote repository string and release field as untrusted evidence.
330
+
331
+ ```bash
332
+ upstream-radar observe ./targets.yml \
333
+ --state ./observations.json \
334
+ --dsh-agent-command /path/to/reviewed-dsh-agent-wrapper \
335
+ --dsh-agent-arg --json
336
+ ```
337
+
338
+ Radar does not guess an undocumented `dsh` CLI subcommand. A reviewed DSH
339
+ headless wrapper is the integration boundary; this keeps the observer usable in
340
+ GitHub Actions and lets the DSH adapter evolve without changing observation or
341
+ diff logic. Use `--retry-pending` to deliver tasks left by a previous run.
342
+
147
343
  ## Notify Feishu or an HTTPS endpoint
148
344
 
149
345
  To also notify a team-owned HTTPS endpoint when an incident changes, keep the endpoint outside the reviewed config and state:
@@ -322,7 +518,7 @@ cd my-dsh-plugin
322
518
  pnpm install --ignore-scripts
323
519
 
324
520
  # 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
521
+ pnpm dlx --package=upstream-radar@0.33.2 upstream-radar graph pnpm-lock pnpm-lock.yaml --json
326
522
  ```
327
523
 
328
524
  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 +540,7 @@ jobs:
344
540
  runs-on: ubuntu-latest
345
541
  steps:
346
542
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
347
- - uses: MicroMilo/upstream-radar@v0.33.0
543
+ - uses: MicroMilo/upstream-radar@v0.33.2
348
544
  with:
349
545
  fail-on: high
350
546
  fail-on-compatibility: breaking
@@ -352,6 +548,22 @@ jobs:
352
548
 
353
549
  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
550
 
551
+ To check a real published DSH artifact directly, run one command:
552
+
553
+ ```bash
554
+ npx --yes upstream-radar@0.33.2 inspect dsh-feishu-bot@0.15.4 --deep
555
+ ```
556
+
557
+ The checked result is `REVIEW`: registry integrity, signature, provenance, and
558
+ 89 resolved packages are verified; known vulnerabilities are `0`, while 12
559
+ optional dependency edges remain unresolved. That is a useful author result:
560
+ the empty vulnerability list is visible, but it is not mislabeled as a full
561
+ `ALLOW` decision.
562
+
563
+ The same exact tarball loads in both DSH `0.1.0-rc.6` and `0.1.0-rc.7` disposable
564
+ profiles. See the [real compatibility probe](examples/dsh/reports/dsh-feishu-bot-0.15.4-probe.md)
565
+ for the command and boundary.
566
+
355
567
  ## See one incident
356
568
 
357
569
  If an advisory affects only one of two installed `parser` versions, Radar reports the path that actually matched:
@@ -483,7 +695,11 @@ To demonstrate the host-runtime dependency path specifically, run `pnpm run show
483
695
 
484
696
  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
697
 
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.
698
+ 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.
699
+
700
+ 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.
701
+
702
+ 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
703
 
488
704
  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
705
 
@@ -492,7 +708,7 @@ To see the two-source vulnerability contract without contacting the network, run
492
708
  Before wiring a project into a compatibility gate, run the offline rule benchmark:
493
709
 
494
710
  ```bash
495
- pnpm dlx --package=upstream-radar@0.33.0 upstream-radar benchmark compatibility
711
+ pnpm dlx --package=upstream-radar@0.33.2 upstream-radar benchmark compatibility
496
712
  ```
497
713
 
498
714
  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 +721,7 @@ When you have an exact plugin artifact and want to know whether one exact DSH re
505
721
  # Pack an exact npm release without running its lifecycle scripts.
506
722
  npm pack --ignore-scripts dsh-plugin@1.2.3
507
723
 
508
- pnpm dlx --package=upstream-radar@0.33.0 upstream-radar probe dsh-load \
724
+ pnpm dlx --package=upstream-radar@0.33.2 upstream-radar probe dsh-load \
509
725
  ./dsh-plugin-1.2.3.tgz \
510
726
  --dsh-version 0.1.0-rc.6
511
727
  ```
@@ -531,7 +747,7 @@ It exercises a loadable bundle, a bundle patch DSH rejects, and a package that r
531
747
  To compare a plugin against more than one DSH release, use the matrix form:
532
748
 
533
749
  ```bash
534
- pnpm dlx --package=upstream-radar@0.33.0 upstream-radar probe dsh-matrix \
750
+ pnpm dlx --package=upstream-radar@0.33.2 upstream-radar probe dsh-matrix \
535
751
  ./dsh-plugin-1.2.3.tgz \
536
752
  --dsh-version 0.1.0-rc.3 \
537
753
  --dsh-version 0.1.0-rc.6 \
@@ -547,7 +763,7 @@ If your team wants the shortest scheduled CI gate before wiring a machine to a l
547
763
  ```yaml
548
764
  steps:
549
765
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
550
- - uses: MicroMilo/upstream-radar@v0.33.0
766
+ - uses: MicroMilo/upstream-radar@v0.33.2
551
767
  with:
552
768
  fail-on: high
553
769
  # Optional: also fail on deterministic DSH/plugin compatibility breaks.
@@ -556,12 +772,12 @@ steps:
556
772
  threat-intel: true
557
773
  ```
558
774
 
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.
775
+ 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.2`, and pin the checkout Action in your workflow according to your repository's policy.
560
776
 
561
777
  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
778
 
563
779
  ```yaml
564
- - uses: MicroMilo/upstream-radar@v0.33.0
780
+ - uses: MicroMilo/upstream-radar@v0.33.2
565
781
  with:
566
782
  fail-on: high
567
783
  ```
@@ -571,7 +787,7 @@ An existing `config` wins over auto-detection. If both lockfiles exist, or neith
571
787
  To review the exact plugin artifact before it enters DSH, add `inspect-package`:
572
788
 
573
789
  ```yaml
574
- - uses: MicroMilo/upstream-radar@v0.33.0
790
+ - uses: MicroMilo/upstream-radar@v0.33.2
575
791
  with:
576
792
  inspect-package: dsh-cloudflare-browser-run@0.1.1
577
793
  # review is the safe default; use block only when incomplete coverage may pass.
@@ -583,7 +799,7 @@ This downloads that exact npm tarball, verifies the registry integrity/signature
583
799
  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
800
 
585
801
  ```yaml
586
- - uses: MicroMilo/upstream-radar@v0.33.0
802
+ - uses: MicroMilo/upstream-radar@v0.33.2
587
803
  with:
588
804
  pnpm-lock: pnpm-lock.yaml
589
805
  fail-on: high
@@ -597,7 +813,7 @@ See the [copyable npm workflow](examples/github-actions/upstream-radar-npm.yml)
597
813
  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
814
 
599
815
  ```bash
600
- pnpm dlx --package=upstream-radar@0.33.0 upstream-radar radar check \
816
+ pnpm dlx --package=upstream-radar@0.33.2 upstream-radar radar check \
601
817
  ./upstream-radar.config.json --frozen --state :memory: --fail-on high \
602
818
  --fail-on-compatibility breaking --json
603
819
  ```
@@ -605,7 +821,7 @@ pnpm dlx --package=upstream-radar@0.33.0 upstream-radar radar check \
605
821
  To add the optional DSH load matrix for a published plugin, provide an exact npm package and at least two exact DSH versions:
606
822
 
607
823
  ```yaml
608
- - uses: MicroMilo/upstream-radar@v0.33.0
824
+ - uses: MicroMilo/upstream-radar@v0.33.2
609
825
  id: radar
610
826
  with:
611
827
  config: upstream-radar.config.json
@@ -749,7 +965,7 @@ The bounded pre-install scanner remains available as a supporting collector:
749
965
 
750
966
  ```bash
751
967
  pnpm dlx --package=upstream-radar@latest upstream-radar scan /path/to/dsh-plugin
752
- pnpm dlx --package=upstream-radar@latest upstream-radar inspect npm:dsh-cloudflare-browser-run@0.1.1 --deep
968
+ pnpm dlx --package=upstream-radar@latest upstream-radar inspect dsh-cloudflare-browser-run@0.1.1 --deep
753
969
  ```
754
970
 
755
971
  The default text gate exits `2` for `review` or `block`, which is useful when every uncertainty needs a human decision. If CI should fail only on hard blocks while still printing review evidence, add `--fail-on block`.
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']);
@@ -177,7 +179,7 @@ are recorded as bounded evidence.
177
179
  inspect: `Upstream Radar — inspect one exact npm artifact before installation
178
180
 
179
181
  Usage:
180
- upstream-radar inspect npm:<package>@<exact-version> [--deep] [--json]
182
+ upstream-radar inspect [npm:]<package>@<exact-version> [--deep] [--json]
181
183
  [--registry <https-url>] [--fail-on <warn|review|block|never>]
182
184
 
183
185
  --deep downloads the exact tarball, verifies npm integrity/signatures and
@@ -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
 
@@ -370,8 +404,10 @@ Usage:
370
404
  upstream-radar init --npm-lock <package-lock.json> [--root <package>@<exact-version>] [options]
371
405
  upstream-radar doctor [config.json] [options]
372
406
  upstream-radar scan <directory> [--json] [--fail-on <warn|review|block|never>]
373
- upstream-radar inspect npm:<package>@<exact-version> [--deep] [--json] [--fail-on <warn|review|block|never>]
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')