devrites 3.2.0 → 3.2.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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to DevRites are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and DevRites adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Releases are generated automatically by [semantic-release](https://semantic-release.gitbook.io/) from Conventional Commits on `main`.
4
4
 
5
+ ## [3.2.1](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.0...v3.2.1) (2026-07-24)
6
+
7
+ ### Fixed
8
+
9
+ * **ci:** govern temporary npm audit exceptions ([c2219e0](https://github.com/ViktorsBaikers/DevRites/commit/c2219e0ff11f11ee3f2e2ffa7c5d35b480c5eb76))
10
+ * **rite:** repair doctor compatibility ([811f339](https://github.com/ViktorsBaikers/DevRites/commit/811f339d22d3ec079549a5d50838d7596c72c7da))
11
+
5
12
  ## [3.2.0](https://github.com/ViktorsBaikers/DevRites/compare/v3.1.0...v3.2.0) (2026-07-24)
6
13
 
7
14
  ### Added
package/README.md CHANGED
@@ -25,7 +25,7 @@ final commit, push, and tag, and it requires a typed `GO` confirmation.
25
25
  Unattended runs may create local WIP checkpoint commits along the way, but only
26
26
  Ship collapses and pushes them.
27
27
 
28
- **Status:** [`v3.2.0`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.0): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
28
+ **Status:** [`v3.2.1`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.1): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
29
29
 
30
30
  ## Quick start
31
31
 
@@ -77,11 +77,13 @@ hazards: ok
77
77
  `3`): the binary won't silently mis-parse newer state.
78
78
  - Additive schema changes (older state read by a newer binary) are always fine.
79
79
 
80
- The pack version is discovered from `.claude/devrites.version` or the project
81
- `package.json`; when neither exists the pack is reported `unknown` and no skew is
82
- asserted. Doctor also reports linked-worktree/submodule identity, merge/rebase
83
- state, host-artifact drift, and project extensions that have artifacts but no
84
- optional `provenance.json`.
80
+ The pack version comes from the installer-owned
81
+ `.claude/devrites.manifest`. The old `.claude/devrites.version` marker remains
82
+ readable for compatibility; without either source the pack is reported
83
+ `unknown` and no skew is asserted. The project's own `package.json` is never
84
+ treated as DevRites provenance. Doctor also reports linked-worktree/submodule
85
+ identity, merge/rebase state, host-artifact drift, and project extensions that
86
+ have artifacts but no optional `provenance.json`.
85
87
 
86
88
  ### Root safety at command dispatch
87
89
 
@@ -218,8 +218,8 @@ func gitOperation(projectDir, resolvedGitDir string) string {
218
218
  // versions. It exits nonzero for an unsafe root or newer state schema. Missing
219
219
  // optional data and warnings remain diagnostic.
220
220
  func cmdDoctor(args []string, stdout, stderr io.Writer) int {
221
- if len(args) != 0 {
222
- fmt.Fprintln(stderr, "usage: devrites-engine doctor")
221
+ if len(args) > 1 || len(args) == 1 && args[0] != "--verbose" {
222
+ fmt.Fprintln(stderr, "usage: devrites-engine doctor [--verbose]")
223
223
  return exitUsage
224
224
  }
225
225
  facts, resolveErr := rootfacts.Resolve(os.Getenv("DEVRITES_ROOT"))
@@ -5,13 +5,13 @@ package doctor
5
5
 
6
6
  import (
7
7
  "crypto/sha256"
8
- "encoding/json"
9
8
  "fmt"
10
9
  "io/fs"
11
10
  "os"
12
11
  "path/filepath"
13
12
  "strings"
14
13
 
14
+ "github.com/devrites/devrites/internal/devritespaths"
15
15
  "github.com/devrites/devrites/internal/rootfacts"
16
16
  "github.com/devrites/devrites/internal/state"
17
17
  "github.com/devrites/devrites/internal/version"
@@ -160,20 +160,32 @@ func (r *Report) Render() string {
160
160
  return b.String()
161
161
  }
162
162
 
163
- // packVersion discovers the installed DevRites pack version. It prefers an
164
- // explicit marker (.claude/devrites.version) and falls back to the npm
165
- // package.json at the project root; either absent leaves the pack Unknown so no
166
- // false skew is asserted.
163
+ // packVersion discovers the installed DevRites pack version from the install
164
+ // manifest. The old standalone marker remains readable for compatibility.
167
165
  func packVersion(projectDir string) string {
168
- if v := firstLine(filepath.Join(projectDir, ".claude", "devrites.version")); v != "" {
166
+ if v := manifestVersion(filepath.Join(projectDir, filepath.FromSlash(devritespaths.ManifestName))); v != "" {
169
167
  return v
170
168
  }
171
- if v := packageJSONVersion(filepath.Join(projectDir, "package.json")); v != "" {
169
+ if v := firstLine(filepath.Join(projectDir, ".claude", "devrites.version")); v != "" {
172
170
  return v
173
171
  }
174
172
  return Unknown
175
173
  }
176
174
 
175
+ func manifestVersion(path string) string {
176
+ raw, err := os.ReadFile(path)
177
+ if err != nil {
178
+ return ""
179
+ }
180
+ const prefix = "# devrites-version:"
181
+ for _, line := range strings.Split(string(raw), "\n") {
182
+ if strings.HasPrefix(line, prefix) {
183
+ return strings.TrimSpace(strings.TrimPrefix(line, prefix))
184
+ }
185
+ }
186
+ return ""
187
+ }
188
+
177
189
  func firstLine(path string) string {
178
190
  raw, err := os.ReadFile(path)
179
191
  if err != nil {
@@ -200,8 +212,8 @@ func driftChecks(projectDir string) []string {
200
212
  if isDir(filepath.Join(projectDir, ".claude", "agents")) && !isDir(filepath.Join(projectDir, ".codex", "agents")) {
201
213
  checks = append(checks, "WARN: Claude agents installed but Codex agent mirror .codex/agents is missing")
202
214
  }
203
- if firstLine(filepath.Join(projectDir, ".claude", "devrites.version")) == "" && isDir(filepath.Join(projectDir, ".claude", "skills")) {
204
- checks = append(checks, "WARN: installed skills have no .claude/devrites.version marker")
215
+ if packVersion(projectDir) == Unknown && isDir(filepath.Join(projectDir, ".claude", "skills")) {
216
+ checks = append(checks, "WARN: installed skills have no versioned .claude/devrites.manifest provenance")
205
217
  }
206
218
  if generatedClaudeDrift(projectDir) {
207
219
  checks = append(checks, "[DRV-GENERATED-DRIFT] WARN: pack/generated/claude differs from canonical pack/.claude; run `bash scripts/build-host-artifacts.sh`")
@@ -292,17 +304,3 @@ func isDir(path string) bool {
292
304
  info, err := os.Stat(path)
293
305
  return err == nil && info.IsDir()
294
306
  }
295
-
296
- func packageJSONVersion(path string) string {
297
- raw, err := os.ReadFile(path)
298
- if err != nil {
299
- return ""
300
- }
301
- var pkg struct {
302
- Version string `json:"version"`
303
- }
304
- if err := json.Unmarshal(raw, &pkg); err != nil {
305
- return ""
306
- }
307
- return strings.TrimSpace(pkg.Version)
308
- }
@@ -64,12 +64,13 @@ func TestDiagnoseOKWhenAligned(t *testing.T) {
64
64
  func TestDiagnoseWarnsWhenBinaryOlderThanPack(t *testing.T) {
65
65
  withBinaryVersion(t, "1.0.0")
66
66
  projectDir := t.TempDir()
67
- // Pack marker newer than the binary → warn, not refuse.
67
+ // Installed pack newer than the binary → warn, not refuse.
68
68
  claude := filepath.Join(projectDir, ".claude")
69
69
  if err := os.MkdirAll(claude, 0o755); err != nil {
70
70
  t.Fatal(err)
71
71
  }
72
- if err := os.WriteFile(filepath.Join(claude, "devrites.version"), []byte("2.0.0\n"), 0o644); err != nil {
72
+ manifest := "# devrites-version: 2.0.0\n.claude/skills/rite/SKILL.md\tabc\n"
73
+ if err := os.WriteFile(filepath.Join(claude, "devrites.manifest"), []byte(manifest), 0o644); err != nil {
73
74
  t.Fatal(err)
74
75
  }
75
76
  root := t.TempDir()
@@ -90,6 +91,33 @@ func TestDiagnoseWarnsWhenBinaryOlderThanPack(t *testing.T) {
90
91
  }
91
92
  }
92
93
 
94
+ func TestPackVersionUsesManifestBeforeLegacyMarker(t *testing.T) {
95
+ projectDir := t.TempDir()
96
+ claude := filepath.Join(projectDir, ".claude")
97
+ if err := os.MkdirAll(claude, 0o755); err != nil {
98
+ t.Fatal(err)
99
+ }
100
+ if err := os.WriteFile(filepath.Join(claude, "devrites.manifest"), []byte("# devrites-version: 3.2.0\n"), 0o644); err != nil {
101
+ t.Fatal(err)
102
+ }
103
+ if err := os.WriteFile(filepath.Join(claude, "devrites.version"), []byte("2.9.0\n"), 0o644); err != nil {
104
+ t.Fatal(err)
105
+ }
106
+ if got := packVersion(projectDir); got != "3.2.0" {
107
+ t.Fatalf("packVersion() = %q, want installer manifest version 3.2.0", got)
108
+ }
109
+ }
110
+
111
+ func TestPackVersionIgnoresProjectPackageJSON(t *testing.T) {
112
+ projectDir := t.TempDir()
113
+ if err := os.WriteFile(filepath.Join(projectDir, "package.json"), []byte(`{"version":"99.0.0"}`), 0o644); err != nil {
114
+ t.Fatal(err)
115
+ }
116
+ if got := packVersion(projectDir); got != Unknown {
117
+ t.Fatalf("packVersion() = %q, want %q without DevRites install provenance", got, Unknown)
118
+ }
119
+ }
120
+
93
121
  func TestDiagnoseRefusesNewerStateSchema(t *testing.T) {
94
122
  withBinaryVersion(t, "2.6.1")
95
123
  root := t.TempDir()
@@ -143,7 +171,7 @@ func TestDiagnoseReportsHostArtifactDrift(t *testing.T) {
143
171
  t.Fatal("expected drift checks")
144
172
  }
145
173
  out := r.Render()
146
- for _, want := range []string{"devrites-engine hooks", "Codex skill mirror", "devrites.version marker"} {
174
+ for _, want := range []string{"devrites-engine hooks", "Codex skill mirror", "devrites.manifest provenance"} {
147
175
  if !strings.Contains(out, want) {
148
176
  t.Fatalf("doctor output missing %q:\n%s", want, out)
149
177
  }
@@ -25,6 +25,21 @@ func TestDoctorPrintsTriangle(t *testing.T) {
25
25
  }
26
26
  }
27
27
 
28
+ func TestDoctorVerboseMatchesPlainDoctor(t *testing.T) {
29
+ root := newWorkspace(t)
30
+ plainOut, plainErr, plainCode := runDevrites(t, root, "doctor")
31
+ verboseOut, verboseErr, verboseCode := runDevrites(t, root, "doctor", "--verbose")
32
+ if plainCode != 0 || verboseCode != 0 {
33
+ t.Fatalf("doctor exits: plain=%d verbose=%d", plainCode, verboseCode)
34
+ }
35
+ if verboseCode != plainCode || verboseOut != plainOut || verboseErr != plainErr {
36
+ t.Fatalf(
37
+ "doctor --verbose differs from doctor:\nplain: code=%d stdout=%q stderr=%q\nverbose: code=%d stdout=%q stderr=%q",
38
+ plainCode, plainOut, plainErr, verboseCode, verboseOut, verboseErr,
39
+ )
40
+ }
41
+ }
42
+
28
43
  func TestDoctorReportsStaleActiveWithPasteableRepair(t *testing.T) {
29
44
  root := newWorkspace(t)
30
45
  if err := os.WriteFile(filepath.Join(root, "ACTIVE"), []byte("ghost\n"), 0o644); err != nil {
@@ -19,7 +19,7 @@ It is **read-only**: it never edits the workspace, never advances a phase, never
19
19
  ## Workflow
20
20
  1. Run the diagnose core verbosely (resolve across install layouts):
21
21
  ```bash
22
- devrites-engine doctor --verbose; echo "doctor rc=$?"
22
+ devrites-engine doctor; echo "doctor rc=$?"
23
23
  ```
24
24
  1a. **Surface the learnings nudge**: point the user at `/rite-learn` when a pattern recurs across
25
25
  shipped features (read-only; silent when there's nothing to say):
@@ -68,7 +68,7 @@ progress`, but it follows the compact labels and one-next-action rule from
68
68
  ```
69
69
  Done: DevRites health checked; <OK | n issues>.
70
70
  Changed: workspace only
71
- Evidence: devrites-engine doctor --verbose rc=<0|1>; health <skipped|PASS|WARN|FAIL>; reindex <skipped|result>; learnings nudge <summary|none>; extensions/overrides <ok|n issues>
71
+ Evidence: devrites-engine doctor rc=<0|1>; health <skipped|PASS|WARN|FAIL>; reindex <skipped|result>; learnings nudge <summary|none>; extensions/overrides <ok|n issues>
72
72
  Open: <none | issue count and top issue>
73
73
  Next: <single command for the most urgent issue>
74
74
  Record: not applicable
@@ -42,7 +42,7 @@ Plan, Vet, Converge, and Build phase contracts needed by the assessment.
42
42
 
43
43
  0. **Orient and normalize structure.** Read the active slug or `$ARGUMENTS`. Run:
44
44
  ```bash
45
- devrites-engine doctor --verbose; echo "doctor rc=$?"
45
+ devrites-engine doctor; echo "doctor rc=$?"
46
46
  ```
47
47
  A binary/pack integrity mismatch stops at `/rite-doctor`; do not run migration or
48
48
  mutate semantic artifacts. Once healthy, orient without writing:
@@ -19,7 +19,7 @@ It is **read-only**: it never edits the workspace, never advances a phase, never
19
19
  ## Workflow
20
20
  1. Run the diagnose core verbosely (resolve across install layouts):
21
21
  ```bash
22
- devrites-engine doctor --verbose; echo "doctor rc=$?"
22
+ devrites-engine doctor; echo "doctor rc=$?"
23
23
  ```
24
24
  1a. **Surface the learnings nudge**: point the user at `/rite-learn` when a pattern recurs across
25
25
  shipped features (read-only; silent when there's nothing to say):
@@ -68,7 +68,7 @@ progress`, but it follows the compact labels and one-next-action rule from
68
68
  ```
69
69
  Done: DevRites health checked; <OK | n issues>.
70
70
  Changed: workspace only
71
- Evidence: devrites-engine doctor --verbose rc=<0|1>; health <skipped|PASS|WARN|FAIL>; reindex <skipped|result>; learnings nudge <summary|none>; extensions/overrides <ok|n issues>
71
+ Evidence: devrites-engine doctor rc=<0|1>; health <skipped|PASS|WARN|FAIL>; reindex <skipped|result>; learnings nudge <summary|none>; extensions/overrides <ok|n issues>
72
72
  Open: <none | issue count and top issue>
73
73
  Next: <single command for the most urgent issue>
74
74
  Record: not applicable
@@ -42,7 +42,7 @@ Plan, Vet, Converge, and Build phase contracts needed by the assessment.
42
42
 
43
43
  0. **Orient and normalize structure.** Read the active slug or `$ARGUMENTS`. Run:
44
44
  ```bash
45
- devrites-engine doctor --verbose; echo "doctor rc=$?"
45
+ devrites-engine doctor; echo "doctor rc=$?"
46
46
  ```
47
47
  A binary/pack integrity mismatch stops at `/rite-doctor`; do not run migration or
48
48
  mutate semantic artifacts. Once healthy, orient without writing:
@@ -35,7 +35,7 @@ It is **read-only**: it never edits the workspace, never advances a phase, never
35
35
  ## Workflow
36
36
  1. Run the diagnose core verbosely (resolve across install layouts):
37
37
  ```bash
38
- devrites-engine doctor --verbose; echo "doctor rc=$?"
38
+ devrites-engine doctor; echo "doctor rc=$?"
39
39
  ```
40
40
  1a. **Surface the learnings nudge**: point the user at `$rite-learn` when a pattern recurs across
41
41
  shipped features (read-only; silent when there's nothing to say):
@@ -84,7 +84,7 @@ progress`, but it follows the compact labels and one-next-action rule from
84
84
  ```
85
85
  Done: DevRites health checked; <OK | n issues>.
86
86
  Changed: workspace only
87
- Evidence: devrites-engine doctor --verbose rc=<0|1>; health <skipped|PASS|WARN|FAIL>; reindex <skipped|result>; learnings nudge <summary|none>; extensions/overrides <ok|n issues>
87
+ Evidence: devrites-engine doctor rc=<0|1>; health <skipped|PASS|WARN|FAIL>; reindex <skipped|result>; learnings nudge <summary|none>; extensions/overrides <ok|n issues>
88
88
  Open: <none | issue count and top issue>
89
89
  Next: <single command for the most urgent issue>
90
90
  Record: not applicable
@@ -58,7 +58,7 @@ Plan, Vet, Converge, and Build phase contracts needed by the assessment.
58
58
 
59
59
  0. **Orient and normalize structure.** Read the active slug or `$ARGUMENTS`. Run:
60
60
  ```bash
61
- devrites-engine doctor --verbose; echo "doctor rc=$?"
61
+ devrites-engine doctor; echo "doctor rc=$?"
62
62
  ```
63
63
  A binary/pack integrity mismatch stops at `$rite-doctor`; do not run migration or
64
64
  mutate semantic artifacts. Once healthy, orient without writing:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devrites",
3
- "version": "3.2.0",
3
+ "version": "3.2.1",
4
4
  "description": "DevRites: a disciplined senior-engineer workflow pack for Claude Code and Codex",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://github.com/ViktorsBaikers/DevRites#readme",
@@ -54,7 +54,7 @@
54
54
  "test:fast": "node scripts/run-tests.mjs --fast",
55
55
  "test:parallel": "node scripts/run-tests.mjs",
56
56
  "test:serial": "node scripts/run-tests.mjs --serial",
57
- "audit": "npm audit --audit-level=moderate",
57
+ "audit": "node scripts/check-npm-audit.mjs",
58
58
  "size:baseline": "node scripts/check-instruction-size-baseline.mjs --write",
59
59
  "release": "semantic-release",
60
60
  "release:dry": "semantic-release --dry-run --no-ci"
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import { readFileSync } from 'node:fs';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
8
+ const args = process.argv.slice(2);
9
+ const option = (name, fallback) => {
10
+ const index = args.indexOf(name);
11
+ return index >= 0 ? args[index + 1] : fallback;
12
+ };
13
+ const input = option('--input', '');
14
+ const exceptionsPath = resolve(option('--exceptions', resolve(root, 'scripts', 'npm-audit-exceptions.json')));
15
+ const blocking = new Set(['moderate', 'high', 'critical']);
16
+ const failures = [];
17
+ const fail = (message) => failures.push(message);
18
+ const load = (path) => JSON.parse(readFileSync(path, 'utf8'));
19
+
20
+ let report;
21
+ if (input) {
22
+ report = load(resolve(input));
23
+ } else {
24
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
25
+ const result = spawnSync(npm, ['audit', '--json'], { cwd: root, encoding: 'utf8' });
26
+ if (result.error || ![0, 1].includes(result.status)) {
27
+ console.error(result.stderr || result.error?.message || `npm audit exited ${result.status}`);
28
+ process.exit(1);
29
+ }
30
+ try {
31
+ report = JSON.parse(result.stdout);
32
+ } catch {
33
+ console.error('FAIL: npm audit did not return valid JSON');
34
+ process.exit(1);
35
+ }
36
+ }
37
+
38
+ const concrete = new Map();
39
+ let affected = 0;
40
+ for (const [packageName, vulnerability] of Object.entries(report.vulnerabilities || {})) {
41
+ if (!blocking.has(vulnerability.severity)) continue;
42
+ affected++;
43
+ for (const via of vulnerability.via || []) {
44
+ if (!via || typeof via !== 'object' || !blocking.has(via.severity)) continue;
45
+ const id = via.url?.match(/GHSA-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}/i)?.[0];
46
+ if (!id) {
47
+ fail(`${packageName}: blocking advisory has no GHSA identifier`);
48
+ continue;
49
+ }
50
+ const finding = concrete.get(id) || {
51
+ id,
52
+ package: via.name || packageName,
53
+ range: via.range || vulnerability.range,
54
+ source: via.url,
55
+ nodes: [],
56
+ };
57
+ finding.nodes = [...new Set([...finding.nodes, ...(vulnerability.nodes || [])])].sort();
58
+ concrete.set(id, finding);
59
+ }
60
+ }
61
+ if (affected > 0 && concrete.size === 0) fail('blocking audit findings have no concrete advisory records');
62
+
63
+ const exceptions = load(exceptionsPath);
64
+ if (!Array.isArray(exceptions)) fail('npm audit exceptions must be a JSON array');
65
+ const today = new Date().toISOString().slice(0, 10);
66
+ const seen = new Set();
67
+ for (const exception of Array.isArray(exceptions) ? exceptions : []) {
68
+ const required = ['id', 'package', 'range', 'source', 'owner', 'reason', 'expires'];
69
+ const missing = required.filter((field) => !exception?.[field]);
70
+ if (!Array.isArray(exception?.nodes) || exception.nodes.length === 0) missing.push('nodes');
71
+ if (missing.length) {
72
+ fail(`${exception?.id || 'unknown exception'}: missing ${missing.join(', ')}`);
73
+ continue;
74
+ }
75
+ if (!/^GHSA-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}$/i.test(exception.id) || !/^\d{4}-\d{2}-\d{2}$/.test(exception.expires)) {
76
+ fail(`${exception.id}: invalid id or expiry`);
77
+ continue;
78
+ }
79
+ if (seen.has(exception.id)) fail(`${exception.id}: duplicate exception`);
80
+ seen.add(exception.id);
81
+ if (exception.expires < today) fail(`${exception.id}: exception expired ${exception.expires}`);
82
+
83
+ const finding = concrete.get(exception.id);
84
+ if (!finding) {
85
+ fail(`${exception.id}: exception is stale; advisory is absent`);
86
+ continue;
87
+ }
88
+ for (const field of ['package', 'range', 'source']) {
89
+ if (exception[field] !== finding[field]) {
90
+ fail(`${exception.id}: ${field} mismatch (expected ${finding[field]})`);
91
+ }
92
+ }
93
+ if (JSON.stringify([...exception.nodes].sort()) !== JSON.stringify(finding.nodes)) {
94
+ fail(`${exception.id}: nodes mismatch (expected ${finding.nodes.join(', ')})`);
95
+ }
96
+ }
97
+ for (const finding of concrete.values()) {
98
+ if (!seen.has(finding.id)) fail(`${finding.id}: blocking advisory is not excepted`);
99
+ }
100
+
101
+ for (const message of failures) console.error(`FAIL: ${message}`);
102
+ if (failures.length) process.exit(1);
103
+ if (affected === 0) {
104
+ console.log('npm-audit: no moderate-or-higher advisories');
105
+ } else {
106
+ for (const exception of exceptions) {
107
+ console.log(`npm-audit: temporary ${exception.id} exception owned by ${exception.owner}, expires ${exception.expires}`);
108
+ }
109
+ console.log(`npm-audit: ${concrete.size} exact temporary exception(s), ${affected} affected package record(s)`);
110
+ }
@@ -0,0 +1,14 @@
1
+ [
2
+ {
3
+ "id": "GHSA-r292-9mhp-454m",
4
+ "package": "tar",
5
+ "range": "<=7.5.20",
6
+ "nodes": [
7
+ "node_modules/npm/node_modules/tar"
8
+ ],
9
+ "source": "https://github.com/advisories/GHSA-r292-9mhp-454m",
10
+ "owner": "release-maintainers",
11
+ "reason": "npm@11.18.0 bundles tar@7.5.19 through @semantic-release/npm. The plugin invokes npm for authentication and publishing operations over trusted local source; it does not select or extract members from untrusted tar archives. Remove this exception as soon as npm bundles tar@7.5.21 or newer.",
12
+ "expires": "2026-08-07"
13
+ }
14
+ ]