net-snmp 3.26.3 → 3.28.0

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
@@ -351,6 +351,7 @@ Actions
351
351
  - `11 - EUnexpectedReport`
352
352
  - `12 - EResponseNotHandled`
353
353
  - `13 - EUnexpectedResponse`
354
+ - `14 - ENotInTimeWindow`
354
355
 
355
356
  ## snmp.OidFormat
356
357
  - `oid - oid`
@@ -1555,7 +1556,11 @@ an object, possibly empty, and can contain the following fields:
1555
1556
 
1556
1557
  The `callback` parameter is a callback function of the form
1557
1558
  `function (error, notification)`. On an error condition, the `notification`
1558
- parameter is set to `null`. On successful reception of a notification, the error
1559
+ parameter is set to `null`. Where the error was produced while processing a
1560
+ received packet - such as an authorization failure for a community or user
1561
+ not in the receiver's local authorization lists - the error will include an
1562
+ `rinfo` attribute containing the sender socket details of the offending packet.
1563
+ On successful reception of a notification, the error
1559
1564
  parameter is set to `null`, and the `notification` parameter is set as an object
1560
1565
  with the notification PDU details in the `pdu` field and the sender socket details
1561
1566
  in the `rinfo` field. For example:
@@ -1677,6 +1682,11 @@ an object, possibly empty, and can contain the following fields:
1677
1682
  * `mibOptions` - a MIB options object that is passed to the `Mib` instance - see the MIB section
1678
1683
  for further details on this - defaults to the empty object.
1679
1684
 
1685
+ As for the receiver, errors that the agent produces while processing a received
1686
+ packet - such as an authorization failure, or a request the agent cannot serve -
1687
+ include an `rinfo` attribute containing the sender socket details of the offending
1688
+ packet.
1689
+
1680
1690
  The `mib` parameter is optional, and sets the agent's singleton `Mib` instance.
1681
1691
  If not supplied, the agent creates itself a new empty `Mib` singleton. If supplied,
1682
1692
  the `Mib` instance needs to be created and populated as per the [Mib Module](#mib-module)
@@ -1723,7 +1733,9 @@ receiver's community authorization list, the receiver will not accept the notifi
1723
1733
  instead returning a error of class `RequestFailedError` to the supplied callback
1724
1734
  function. Similarly, if a v3 notification is received with a user whose name is
1725
1735
  not in the receiver's user authorization list, the receiver will return a
1726
- `RequestFailedError`. If the `disableAuthorization` option is supplied for the
1736
+ `RequestFailedError`. These errors include an `rinfo` attribute containing the
1737
+ sender socket details of the unauthorized packet, so that its origin can be
1738
+ identified. If the `disableAuthorization` option is supplied for the
1727
1739
  receiver on start-up, then these local authorization list checks are disabled for
1728
1740
  community notifications and noAuthNoPriv user notifications. Note that even with
1729
1741
  this setting, the user list is *still checked* for authNoPriv and authPriv notifications,
@@ -3755,6 +3767,18 @@ Example programs are included under the module's `example` directory.
3755
3767
 
3756
3768
  * Document how MIB scalar and table handlers should interact with `mibRequest.instanceNode.value` for Get vs Set operations
3757
3769
 
3770
+ # Version 3.27.0 - 11/09/2026
3771
+
3772
+ * Fix SNMPv3 requests being rejected with `usmStatsNotInTimeWindows` once a session has been open for a while - the authoritative engine time is now advanced with a monotonic local clock and resynchronised from every authenticated response, instead of staying frozen at the value learned during discovery
3773
+
3774
+ * Discard SNMPv3 responses that fall outside the USM time window, reported as the new `ResponseInvalidCode.ENotInTimeWindow`
3775
+
3776
+ # Version 3.28.0 - 11/09/2026
3777
+
3778
+ * Add an `rinfo` attribute to errors that a receiver or agent produces while processing a received packet - covering unknown community and unknown user authorization failures, authentication and privacy level mismatches, digest and decryption failures, and unsupported PDU type rejections - so the sender of an offending packet can be identified
3779
+
3780
+ * Replace `.npmignore` with a `files` allowlist in `package.json`, so the published package ships only the module, its libraries, examples and reference files
3781
+
3758
3782
  # License
3759
3783
 
3760
3784
  Copyright (c) 2020 Mark Abrahams <mark@abrahams.co.nz>
package/index.js CHANGED
@@ -9,10 +9,13 @@ const util = require ("util");
9
9
  const crypto = require ("crypto");
10
10
  const mibparser = require ("./lib/mib");
11
11
  const Buffer = require('buffer').Buffer;
12
+ const { performance } = require ("perf_hooks");
12
13
 
13
14
  var DEBUG = false;
14
15
  var STRICT_INT_RANGE_CHECKS = false;
15
16
 
17
+ const USM_TIME_WINDOW_SECONDS = 150;
18
+
16
19
  const MIN_SIGNED_INT32 = -2147483648;
17
20
  const MAX_SIGNED_INT32 = 2147483647;
18
21
  const MIN_UNSIGNED_INT32 = 0;
@@ -280,7 +283,8 @@ var ResponseInvalidCode = {
280
283
  10: "ECommunityNoMatch",
281
284
  11: "EUnexpectedReport",
282
285
  12: "EResponseNotHandled",
283
- 13: "EUnexpectedResponse"
286
+ 13: "EUnexpectedResponse",
287
+ 14: "ENotInTimeWindow"
284
288
  };
285
289
 
286
290
  _expandConstantObject (ResponseInvalidCode);
@@ -2084,6 +2088,18 @@ var Session = function (target, authenticator, options) {
2084
2088
  this.reqs = {};
2085
2089
  this.reqCount = 0;
2086
2090
 
2091
+ // Local notion of the authoritative (remote) engine's snmpEngineBoots and
2092
+ // snmpEngineTime, per RFC 3414 section 2.3. engineTimeBase is the
2093
+ // snmpEngineTime learned at engineTimeReceivedAt (a monotonic millisecond
2094
+ // reading), and the current notion is derived from the two by
2095
+ // getEngineTime(). latestReceivedEngineTime is the highest snmpEngineTime
2096
+ // ever received from the authoritative engine, and exists solely to stop a
2097
+ // replayed message from holding our notion of time back.
2098
+ this.engineTimeBoots = null;
2099
+ this.engineTimeBase = null;
2100
+ this.engineTimeReceivedAt = null;
2101
+ this.latestReceivedEngineTime = null;
2102
+
2087
2103
  const dgramMod = options.dgramModule || dgram;
2088
2104
  this.dgram = dgramMod.createSocket (this.transport);
2089
2105
  this.dgram.unref();
@@ -2425,6 +2441,21 @@ Session.prototype.onMsg = function (buffer) {
2425
2441
  if ( ! message.processIncomingSecurity (this.user, req.responseCb) )
2426
2442
  return;
2427
2443
 
2444
+ // RFC 3414 sections 2.3 and 3.2 step 7b: synchronise our notion of the
2445
+ // authoritative engine's time from this message, then discard the message if
2446
+ // it falls outside the time window. Both are no-ops for SNMPv1 and v2c, and
2447
+ // for v3 messages which were not authenticated.
2448
+ this.setEngineTime (message);
2449
+ if ( ! this.isInTimeWindow (message) ) {
2450
+ req.responseCb (new ResponseInvalidError ("Message with engineBoots '"
2451
+ + message.msgSecurityParameters.msgAuthoritativeEngineBoots
2452
+ + "' and engineTime '" + message.msgSecurityParameters.msgAuthoritativeEngineTime
2453
+ + "' is outside the time window of engineBoots '" + this.engineTimeBoots
2454
+ + "' and engineTime '" + this.getEngineTime ().engineTime + "'",
2455
+ ResponseInvalidCode.ENotInTimeWindow));
2456
+ return;
2457
+ }
2458
+
2428
2459
  if (message.version != req.message.version) {
2429
2460
  req.responseCb (new ResponseInvalidError ("Version in request '"
2430
2461
  + req.message.version + "' does not match version in "
@@ -2974,7 +3005,93 @@ Session.prototype.walk = function () {
2974
3005
  return this;
2975
3006
  };
2976
3007
 
3008
+ // RFC 3414 section 2.3: between authentic messages, the non-authoritative
3009
+ // engine's notion of the authoritative engine's snmpEngineTime advances with
3010
+ // the local clock. A monotonic clock source is used, so that a step of the
3011
+ // system clock cannot move our notion of time. Returns null while no notion
3012
+ // has been established.
3013
+ Session.prototype.getEngineTime = function () {
3014
+ if ( this.engineTimeReceivedAt == null )
3015
+ return null;
3016
+ var elapsedSeconds = Math.floor ((performance.now () - this.engineTimeReceivedAt) / 1000);
3017
+ var engineBoots = this.engineTimeBoots;
3018
+ var engineTime = this.engineTimeBase + elapsedSeconds;
3019
+ // RFC 3414 section 2.2.2: when snmpEngineTime reaches its maximum value,
3020
+ // snmpEngineBoots is incremented and snmpEngineTime is reset to zero.
3021
+ // snmpEngineBoots latches at its maximum value and never wraps.
3022
+ if ( engineTime > MAX_SIGNED_INT32 ) {
3023
+ engineBoots = Math.min (engineBoots + Math.floor (engineTime / (MAX_SIGNED_INT32 + 1)),
3024
+ MAX_SIGNED_INT32);
3025
+ engineTime = engineTime % (MAX_SIGNED_INT32 + 1);
3026
+ }
3027
+ return {
3028
+ engineBoots: engineBoots,
3029
+ engineTime: engineTime
3030
+ };
3031
+ };
3032
+
3033
+ // RFC 3414 section 3.2 step 7b(1): update our notion of the authoritative
3034
+ // engine's time from an authentic message, but only when that message advances
3035
+ // it - either snmpEngineBoots has increased, or snmpEngineTime is higher than
3036
+ // any snmpEngineTime yet seen for the current snmpEngineBoots. The latter
3037
+ // comparison is against the highest value received rather than against our
3038
+ // locally advanced notion, so that a fast local clock cannot lock the session
3039
+ // out of ever resynchronising.
3040
+ Session.prototype.setEngineTime = function (message) {
3041
+ var params = message.msgSecurityParameters;
3042
+ if ( ! params || ! message.hasAuthentication () )
3043
+ return;
3044
+ var engineBoots = params.msgAuthoritativeEngineBoots;
3045
+ var engineTime = params.msgAuthoritativeEngineTime;
3046
+ var advances = this.engineTimeBoots == null
3047
+ || engineBoots > this.engineTimeBoots
3048
+ || ( engineBoots == this.engineTimeBoots
3049
+ && engineTime > this.latestReceivedEngineTime );
3050
+ if ( ! advances )
3051
+ return;
3052
+ this.engineTimeBoots = engineBoots;
3053
+ this.engineTimeBase = engineTime;
3054
+ this.latestReceivedEngineTime = engineTime;
3055
+ this.engineTimeReceivedAt = performance.now ();
3056
+ };
3057
+
3058
+ // RFC 3414 section 3.2 step 7b(2): an authentic message from the authoritative
3059
+ // engine is outside the time window - and so must be discarded - if our notion
3060
+ // of its snmpEngineBoots has latched at its maximum value, if the message
3061
+ // disagrees with our notion of snmpEngineBoots, or if it disagrees with our
3062
+ // notion of snmpEngineTime by more than the time window. This must be
3063
+ // evaluated after setEngineTime (), so that a message which legitimately
3064
+ // advances our notion of time is never rejected by it.
3065
+ Session.prototype.isInTimeWindow = function (message) {
3066
+ var params = message.msgSecurityParameters;
3067
+ if ( ! params || ! message.hasAuthentication () )
3068
+ return true;
3069
+ var notion = this.getEngineTime ();
3070
+ if ( ! notion )
3071
+ return true;
3072
+ if ( notion.engineBoots == MAX_SIGNED_INT32 )
3073
+ return false;
3074
+ if ( params.msgAuthoritativeEngineBoots != notion.engineBoots )
3075
+ return false;
3076
+ return Math.abs (params.msgAuthoritativeEngineTime - notion.engineTime)
3077
+ <= USM_TIME_WINDOW_SECONDS;
3078
+ };
3079
+
3080
+ // RFC 3414 section 2.3: an outgoing request carries our current notion of the
3081
+ // authoritative engine's snmpEngineBoots and snmpEngineTime, not the values
3082
+ // learned when the session was first synchronised.
3083
+ Session.prototype.advanceEngineTime = function () {
3084
+ if ( ! this.msgSecurityParameters )
3085
+ return;
3086
+ var notion = this.getEngineTime ();
3087
+ if ( ! notion )
3088
+ return;
3089
+ this.msgSecurityParameters.msgAuthoritativeEngineBoots = notion.engineBoots;
3090
+ this.msgSecurityParameters.msgAuthoritativeEngineTime = notion.engineTime;
3091
+ };
3092
+
2977
3093
  Session.prototype.sendV3Req = function (pdu, feedCb, responseCb, options, port, allowReport) {
3094
+ this.advanceEngineTime ();
2978
3095
  var message = Message.createRequestV3 (this.user, this.msgSecurityParameters, pdu);
2979
3096
  var reqOptions = options || {};
2980
3097
  var req = new Req (this, message, feedCb, responseCb, reqOptions);
@@ -3141,6 +3258,17 @@ Listener.formatCallbackData = function (pdu, rinfo) {
3141
3258
  };
3142
3259
  };
3143
3260
 
3261
+ // Returns a callback that annotates errors with the origin of the packet being
3262
+ // processed, so consumers can identify the source of unauthorized or invalid messages
3263
+ Listener.rinfoErrorCallback = function (target, rinfo) {
3264
+ return function (error, data) {
3265
+ if ( error && rinfo && ! error.rinfo ) {
3266
+ error.rinfo = rinfo;
3267
+ }
3268
+ target.callback (error, data);
3269
+ };
3270
+ };
3271
+
3144
3272
  Listener.processIncoming = function (buffer, authorizer, callback) {
3145
3273
  var message = Message.createFromBuffer (buffer);
3146
3274
  var community;
@@ -3447,12 +3575,13 @@ Receiver.prototype.getAuthorizer = function () {
3447
3575
 
3448
3576
  Receiver.prototype.onMsg = function (socket, buffer, rinfo) {
3449
3577
 
3578
+ const callback = Listener.rinfoErrorCallback (this, rinfo);
3450
3579
  let message;
3451
3580
 
3452
3581
  try {
3453
- message = Listener.processIncoming (buffer, this.authorizer, this.callback);
3582
+ message = Listener.processIncoming (buffer, this.authorizer, callback);
3454
3583
  } catch (error) {
3455
- this.callback (new ProcessingError ("Failure to process incoming message", error, rinfo, buffer));
3584
+ callback (new ProcessingError ("Failure to process incoming message", error, rinfo, buffer));
3456
3585
  return;
3457
3586
  }
3458
3587
 
@@ -3469,13 +3598,13 @@ Receiver.prototype.onMsg = function (socket, buffer, rinfo) {
3469
3598
  // The only GetRequest PDUs supported are those used for SNMPv3 discovery
3470
3599
  if ( message.pdu.type == PduType.GetRequest ) {
3471
3600
  if ( message.version != Version3 ) {
3472
- this.callback (new RequestInvalidError ("Only SNMPv3 discovery GetRequests are supported"));
3601
+ callback (new RequestInvalidError ("Only SNMPv3 discovery GetRequests are supported"));
3473
3602
  return;
3474
3603
  } else if ( message.hasAuthentication() ) {
3475
- this.callback (new RequestInvalidError ("Only discovery (noAuthNoPriv) GetRequests are supported but this message has authentication"));
3604
+ callback (new RequestInvalidError ("Only discovery (noAuthNoPriv) GetRequests are supported but this message has authentication"));
3476
3605
  return;
3477
3606
  } else if ( ! message.isReportable () ) {
3478
- this.callback (new RequestInvalidError ("Only discovery GetRequests are supported and this message does not have the reportable flag set"));
3607
+ callback (new RequestInvalidError ("Only discovery GetRequests are supported and this message does not have the reportable flag set"));
3479
3608
  return;
3480
3609
  }
3481
3610
  let reportMessage = message.createReportResponseMessage (this.engine, this.context, UsmErrorType.UNKNOWN_ENGINE_ID);
@@ -3486,16 +3615,16 @@ Receiver.prototype.onMsg = function (socket, buffer, rinfo) {
3486
3615
  // Inform/trap processing
3487
3616
  // debug (JSON.stringify (message.pdu, null, 2));
3488
3617
  if ( message.pdu.type == PduType.Trap || message.pdu.type == PduType.TrapV2 ) {
3489
- this.callback (null, this.formatCallbackData (message, rinfo) );
3618
+ callback (null, this.formatCallbackData (message, rinfo) );
3490
3619
  } else if ( message.pdu.type == PduType.InformRequest ) {
3491
3620
  message.pdu.type = PduType.GetResponse;
3492
3621
  message.buffer = null;
3493
3622
  message.setReportable (false);
3494
3623
  this.listener.send (message, rinfo, socket);
3495
3624
  message.pdu.type = PduType.InformRequest;
3496
- this.callback (null, this.formatCallbackData (message, rinfo) );
3625
+ callback (null, this.formatCallbackData (message, rinfo) );
3497
3626
  } else {
3498
- this.callback (new RequestInvalidError ("Unexpected PDU type " + message.pdu.type + " (" + PduType[message.pdu.type] + ")"));
3627
+ callback (new RequestInvalidError ("Unexpected PDU type " + message.pdu.type + " (" + PduType[message.pdu.type] + ")"));
3499
3628
  }
3500
3629
  };
3501
3630
 
@@ -5043,12 +5172,13 @@ Agent.prototype.tableRowStatusHandlerInternal = function (createRequest) {
5043
5172
 
5044
5173
  Agent.prototype.onMsg = function (socket, buffer, rinfo) {
5045
5174
 
5175
+ const callback = Listener.rinfoErrorCallback (this, rinfo);
5046
5176
  let message;
5047
5177
 
5048
5178
  try {
5049
- message = Listener.processIncoming (buffer, this.authorizer, this.callback);
5179
+ message = Listener.processIncoming (buffer, this.authorizer, callback);
5050
5180
  } catch (error) {
5051
- this.callback (new ProcessingError ("Failure to process incoming message", error, rinfo, buffer));
5181
+ callback (new ProcessingError ("Failure to process incoming message", error, rinfo, buffer));
5052
5182
  return;
5053
5183
  }
5054
5184
 
@@ -5083,7 +5213,7 @@ Agent.prototype.onMsg = function (socket, buffer, rinfo) {
5083
5213
  } else if ( message.pdu.type == PduType.GetBulkRequest ) {
5084
5214
  this.getBulkRequest (socket, message, rinfo);
5085
5215
  } else {
5086
- this.callback (new RequestInvalidError ("Unexpected PDU type " +
5216
+ callback (new RequestInvalidError ("Unexpected PDU type " +
5087
5217
  message.pdu.type + " (" + PduType[message.pdu.type] + ")"));
5088
5218
  }
5089
5219
  };
package/package.json CHANGED
@@ -1,10 +1,19 @@
1
1
  {
2
2
  "name": "net-snmp",
3
- "version": "3.26.3",
3
+ "version": "3.28.0",
4
4
  "description": "JavaScript implementation of the Simple Network Management Protocol (SNMP)",
5
5
  "author": "Mark Abrahams <mark@abrahams.co.nz>",
6
6
  "license": "MIT",
7
7
  "main": "index.js",
8
+ "files": [
9
+ "index.js",
10
+ "lib/",
11
+ "example/",
12
+ "ref/",
13
+ "CONTRIBUTING.md",
14
+ "README.cn.md",
15
+ "!example/test.js"
16
+ ],
8
17
  "directories": {
9
18
  "example": "example"
10
19
  },
@@ -1,54 +0,0 @@
1
- ---
2
- name: git-commit
3
- description: Creates a git commit summarizing all current working tree changes (staged and unstaged). Use when the user asks to commit, save progress, or invoke /git-commit.
4
- ---
5
-
6
- # Git Commit
7
-
8
- ## When to use
9
-
10
- - When the user asks to commit current changes.
11
- - When invoked via `/git-commit`.
12
-
13
- ## Arguments
14
-
15
- - `dry-run` — perform steps 1–3 only (gather context, identify files, draft commit message) and then **ask the user** whether to proceed with the actual commit or cancel. Do **not** stage or commit until the user confirms.
16
-
17
- ## Workflow
18
-
19
- 1. **Gather context** — run these commands in parallel:
20
- - `git status` — to see all tracked modifications and untracked files.
21
- - `git diff` and `git diff --cached` — to see unstaged and staged changes.
22
- - `git log --oneline -10` — to match the repository's commit message style.
23
-
24
- 2. **Stage all relevant changes** — add all modified and untracked files that are part of the current work. Use specific file paths rather than `git add -A`. Do **not** stage files that likely contain secrets (`.env`, credentials, etc.) — warn the user if any are present.
25
-
26
- 3. **Draft the commit message** — analyse the diff and write a concise commit message:
27
- - One summary line (imperative mood, under 72 characters) describing the *why* / *what* of the change.
28
- - If the change is non-trivial, add a blank line followed by bullet points elaborating key changes.
29
- - **Don't pad small commits**: if the summary line already captures the change, omit the body entirely. Most single-purpose commits need only the summary line.
30
- - **Stay high-level**: never reference code-level details like variable names, function names, method calls, or file paths in the commit message. Describe *what changed for the user or system*, not *which functions were modified*.
31
- - Match the style and conventions visible in the recent git log.
32
-
33
- 4. **Commit** — create the commit using a heredoc for the message:
34
- ```bash
35
- git commit -m "$(cat <<'EOF'
36
- <summary line>
37
-
38
- <optional body>
39
- EOF
40
- )"
41
- ```
42
-
43
- 5. **Verify** — run `git status` after the commit to confirm it succeeded and the working tree is clean (or shows only intentionally unstaged files).
44
-
45
- 6. **Show the commit** — run `git log -1` and display the **entire** commit message (summary line and body) to the user so they can review it. Do not truncate or abbreviate — if the message has bullet points or a multi-line body, include all of it in your response.
46
-
47
- ## Rules
48
-
49
- - Never amend an existing commit unless the user explicitly asks.
50
- - Never push to the remote unless the user explicitly asks.
51
- - Never use `--no-verify` or skip pre-commit hooks.
52
- - If a pre-commit hook fails, fix the issue, re-stage, and create a **new** commit (do not amend).
53
- - Never stage `.env`, credential files, or other secrets.
54
- - Never add `Co-Authored-By`, `Authored-By`, `Made-with` or any other attribution trailer to commit messages.
@@ -1,97 +0,0 @@
1
- ---
2
- name: prepare-release
3
- description: Prepare a release by running lint + tests, bumping the package.json version, and appending a version summary to README.md. Use when the user asks to "prepare a release", "cut a release", or invokes /prepare-release. Stops immediately on lint/test failure so the user can fix issues before continuing.
4
- ---
5
-
6
- # Prepare Release
7
-
8
- ## When to use
9
-
10
- - User asks to prepare / cut a release.
11
- - User invokes `/prepare-release`.
12
- - Immediately before the release commit — this skill stages no files and creates no commit; hand off to `/git-commit` afterwards.
13
-
14
- ## Release model
15
-
16
- The release content is the union of **(a)** commits since the last tag and **(b)** uncommitted user-facing changes in the working tree. Both are valid sources; the typical flow is that the fix being released sits uncommitted alongside the version bump and README update, and the user then commits them all together in a single release commit.
17
-
18
- Consequences:
19
- - Do **not** object to uncommitted changes in the working tree — they are expected and are part of what is being released. Treat them as release content, not as a blocker.
20
- - Do **not** require at least one committed change since the last tag. The release may consist entirely of working-tree changes that have not yet been committed.
21
- - When drafting the README bullets in step 5, summarise the user-facing changes visible across **both** sources (committed commits since the last tag **and** the diff of the working tree before this skill began modifying it).
22
- - The only changes this skill itself creates are in `package.json`, `package-lock.json`, and `README.md`. Everything else in the working tree was already the user's in-progress release content.
23
-
24
- ## Arguments
25
-
26
- - `patch` (default) — bumps the patch component, e.g. `3.26.1` → `3.26.2`.
27
- - `minor` — bumps the minor component and resets patch, e.g. `3.26.1` → `3.27.0`.
28
- - `major` — bumps the major component and resets minor/patch, e.g. `3.26.1` → `4.0.0`.
29
-
30
- If the user supplies anything else, stop and ask for clarification.
31
-
32
- ## Workflow
33
-
34
- Run steps in order. **If any step fails, stop immediately, report the failure, and do not proceed.** Do not modify `package.json`, `package-lock.json`, or `README.md` until lint and tests pass.
35
-
36
- ### 1. Pre-flight checks (parallel)
37
-
38
- - `git status --short` — capture the working tree state. Per the release model above, treat uncommitted changes as part of the release content, not as a blocker. Still surface the list to the user so they can spot anything that looks unintended (e.g. a modified file they don't remember touching) before the eventual release commit.
39
- - `git rev-parse --abbrev-ref HEAD` — confirm the current branch. If it is not `master`, warn the user and ask whether to continue.
40
- - `git log --oneline $(git describe --tags --abbrev=0 2>/dev/null || git log --format=%H | tail -1)..HEAD` — collect commits since the last tag (or since the beginning if no tags exist). These feed the README summary in step 5 alongside the working-tree diff.
41
- - `git diff` and `git diff --cached` — capture the uncommitted user-facing changes. These also feed the README summary in step 5.
42
- - Read current version from `package.json`.
43
-
44
- Only stop at pre-flight if **both** the commit list and the uncommitted diff are empty — in that case there is genuinely nothing to release.
45
-
46
- ### 2. Lint
47
-
48
- Run `npm run lint`. **Stop on non-zero exit.** Report the linter output verbatim and do not continue.
49
-
50
- ### 3. Tests
51
-
52
- Run `npm test`. **Stop on non-zero exit.**
53
-
54
- Exception: the `Subagent` tests in `test/subagent.test.js` require a locally-running AgentX master (`snmpd`) and will fail with `ECONNREFUSED` if one is not present. If the only failures are Subagent `ECONNREFUSED` failures, report them and ask the user whether to proceed (they may have intentionally skipped running snmpd). All other failures are hard stops.
55
-
56
- ### 4. Bump version
57
-
58
- Run `npm version <patch|minor|major> --no-git-tag-version`. This updates both `package.json` and `package-lock.json` atomically without creating a commit or tag. Capture the new version string for the README entry.
59
-
60
- If the argument was omitted, default to `patch`.
61
-
62
- ### 5. Append README version summary
63
-
64
- - Read `README.md`.
65
- - Locate the `# License` heading near the end.
66
- - Insert a new version section **immediately before** `# License`, separated by a blank line above and below, in exactly this format:
67
-
68
- ```
69
- # Version X.Y.Z - DD/MM/YYYY
70
-
71
- * <concise user-facing summary of change 1>
72
-
73
- * <concise user-facing summary of change 2>
74
- ```
75
-
76
- Notes:
77
- - Date is **today's date** in `DD/MM/YYYY` (day/month/year) — use the date from the environment, not a hardcoded value.
78
- - Each bullet begins with a single space, then `*`, then a space. There is a blank line between bullets (match the style of the existing entries).
79
- - Bullets describe **user-visible changes** — not refactors, lint fixes, test additions, or internal tidy-ups. Write them like release notes, not commit subjects. Draw from **both** the commit list gathered in step 1 and the uncommitted working-tree diff; merge/reword where helpful so each line stands on its own.
80
- - If there is only one user-visible change, include just one bullet.
81
- - Do not touch any other part of `README.md`.
82
-
83
- ### 6. Summary
84
-
85
- Report to the user, concisely:
86
- - New version number.
87
- - Bullets added to `README.md`.
88
- - The full list of uncommitted files now staged for the release commit — both the pre-existing working-tree changes and the three files this skill modified (`package.json`, `package-lock.json`, `README.md`). Flag anything that looks unexpected.
89
-
90
- Do **not** run `git add`, `git commit`, `git tag`, or `npm publish` — those are the user's decision. The next step is typically `/git-commit`.
91
-
92
- ## Things to watch for
93
-
94
- - If `npm version` fails because the working tree is dirty, it has not been run with `--no-git-tag-version` correctly — re-check the command.
95
- - If the README insertion point (`# License`) cannot be found, stop and ask the user — do not guess.
96
- - If the lint or test commands are missing from `package.json`, stop and report.
97
- - Never bypass a failing check with `--no-verify`, `SKIP=`, or similar; the user must fix the underlying issue.
@@ -1,111 +0,0 @@
1
- ---
2
- name: propose-action
3
- description: Review a GitHub issue or PR by number and propose a concrete action plan — either a reasoned recommendation not to action it, or the code and test changes required to action it. Use when the user asks to "propose an action" for an issue/PR, "look at issue/PR #N", or invokes /propose-action with an issue or PR number. Ends by asking whether to proceed, proceed with modifications, or abandon.
4
- ---
5
-
6
- # Propose Action
7
-
8
- ## When to use
9
-
10
- - The user asks to review a GitHub issue or PR and recommend what to do about it.
11
- - The user invokes `/propose-action <number>`.
12
-
13
- This skill **plans only** for steps 1–5 — it does not edit code, run tests, commit, push, or comment on the issue/PR during the proposal phase. Implementation happens after the user approves the plan in step 5's closing question. Once the user has approved and you have carried out the agreed actions, step 6 produces a ready-to-paste reply to the issue/PR aligned with what was actually done — but never posts it.
14
-
15
- ## Arguments
16
-
17
- - `<number>` (required) — the GitHub issue or PR number. Bare integer, or `#N`, or a full GitHub URL are all acceptable forms.
18
-
19
- If the number is missing, ambiguous, or does not resolve to an issue/PR in the current repository, stop and ask the user to clarify.
20
-
21
- ## Workflow
22
-
23
- ### 1. Identify the target
24
-
25
- - Determine the current repository: `gh repo view --json nameWithOwner -q .nameWithOwner`.
26
- - Resolve the number to either an issue or a PR. GitHub numbers them in the same namespace, so try PR first, then issue:
27
- - `gh pr view <number> --json number,title,state,author,body,headRefName,baseRefName,isDraft,mergeable,additions,deletions,changedFiles,labels,comments,reviews`
28
- - If that fails, `gh issue view <number> --json number,title,state,author,body,labels,comments,assignees`
29
- - Record whether the target is an **issue** or a **PR** — the two branches below differ.
30
-
31
- ### 2a. If the target is an issue
32
-
33
- Gather context in parallel:
34
- - The issue body and all comments (from the `gh issue view` JSON above).
35
- - `gh issue view <number> --json linkedPullRequests` — check for linked PRs that may already address it.
36
- - Identify the files or subsystems the issue points at. Search the repo (`Grep`, `Glob`, `Read`) for the relevant code. Do not guess — read the actual files the issue describes.
37
- - If the issue references specific OIDs, error messages, function names, or SNMP behaviours, locate them in the source.
38
-
39
- ### 2b. If the target is a PR
40
-
41
- Gather context in parallel:
42
- - PR body, comments, and reviews (from the `gh pr view` JSON above).
43
- - `gh pr diff <number>` — the full diff.
44
- - `gh pr view <number> --json files -q '.files[].path'` — changed files.
45
- - Read the **current** version of each changed file on the base branch so you understand what the PR is replacing, not just what it adds.
46
- - If the PR closes or references an issue (via `Fixes #N`, `Closes #N`, etc.), also fetch that issue for context.
47
- - Check CI status if available: `gh pr checks <number>`.
48
-
49
- ### 3. Form a judgement
50
-
51
- Decide which of these recommendations fits, and be willing to recommend **against** actioning:
52
-
53
- - **Do not action** — the issue is invalid, already fixed, out of scope, based on a misunderstanding, a duplicate, or the PR is the wrong approach / would regress behaviour / conflicts with project direction. Explain *why* clearly, citing the code or prior commits that support the conclusion.
54
- - **Action as-is** — the issue is valid and the fix is clear, or the PR is correct and should be merged as submitted.
55
- - **Action with modifications** — the issue is valid but the obvious fix is wrong; or the PR has the right idea but needs specific changes before it can be merged.
56
- - **Needs more information** — the report is plausible but underspecified. List the exact questions to ask the reporter / author before any code change is possible.
57
-
58
- Base the judgement on what the code actually does today, not on what the issue/PR claims. If the two disagree, surface the disagreement.
59
-
60
- ### 4. Draft the proposal
61
-
62
- Structure the written proposal in this order. Keep it concrete — name files, line numbers, function names, and the specific behaviours being changed.
63
-
64
- 1. **Target** — `Issue #N: <title>` or `PR #N: <title>`, plus one sentence on its state (open/closed/draft/merged, CI status for PRs).
65
- 2. **Summary of the report** — one short paragraph in your own words describing what the reporter is asking for or proposing. Do not just quote the body.
66
- 3. **Findings** — what the code currently does, whether the report is accurate, and any related context (linked issues/PRs, prior commits, existing tests covering this area). Cite `file:line` locations.
67
- 4. **Recommendation** — one of the four options from step 3, stated plainly in a sentence or two.
68
- 5. **Proposed changes** (omit if the recommendation is *do not action* or *needs more information*):
69
- - **Code** — for each file to change, list the edit as a bullet: `path/to/file.js:<line-range> — <what changes and why>`. For non-trivial edits, include a short before/after sketch (a few lines, not a full diff).
70
- - **Tests** — list tests to **add**, **modify**, or **delete**, each with the file and a one-line description of what the test asserts. If no test changes are needed, say so explicitly and justify (e.g. "covered by existing test at `test/x.test.js:42`").
71
- - **Docs / README** — note any README or inline-doc updates required. Release-note bullets are handled by `/prepare-release`; do not pre-empt them here.
72
- 6. **Risks and unknowns** — anything that could make the plan wrong: behaviours you could not verify, edge cases the tests would not catch, compatibility concerns, or assumptions about the reporter's environment.
73
- 7. **Out of scope** — closely-related issues you noticed while investigating but are **not** proposing to fix in this action. Keeps the scope honest.
74
-
75
- ### 5. Closing question
76
-
77
- End the response with exactly one question, offering three choices:
78
-
79
- > Proceed with this plan as-is, proceed with modifications (tell me what to change), or abandon it?
80
-
81
- Do not start implementing until the user answers. If the user says "proceed", begin implementing the plan in the next turn. If the user asks for modifications, revise the proposal and ask the same question again. If the user says to abandon, stop and skip step 6.
82
-
83
- ### 6. Ready-to-paste issue/PR reply
84
-
85
- After the agreed actions have been carried out (code edits, tests, docs, or a reasoned decision not to action), produce a single ready-to-paste reply for the issue/PR. This is the **last** thing you output, rendered in a fenced block or delimited by `---` so the user can copy it cleanly.
86
-
87
- The reply must be aligned with **what actually happened**, not with the original plan — if the user asked for modifications during implementation, reflect the modified outcome. If a step was skipped or failed, say so honestly; do not describe work that was not done.
88
-
89
- Tailor the content to the recommendation from step 3:
90
-
91
- - **Action as-is / with modifications** — acknowledge the report, give the user a short, concrete summary of the fix (with the key code change or snippet inline if it helps them adopt it), point at the files/sections that changed (e.g. "updated in `README.md` under *Scalar providers*"), and note when it will ship (e.g. "will land in the next release" — do not invent a version number unless `package.json` has already been bumped). If there's a workaround the reporter can apply before the release, include it.
92
- - **Do not action** — explain the reasoning in the reporter's terms, citing the code or prior history that supports the decision. Be respectful: the reporter put effort into the report. Offer an alternative path if one exists (a different API, a related issue, a config change). If the issue should be closed, say so; do not close it yourself.
93
- - **Needs more information** — list the specific questions you need answered, each as its own bullet. Explain briefly *why* each is needed so the reporter can see what would unblock progress.
94
-
95
- Style:
96
-
97
- - Match the tone of prior replies in this repository if they are visible in the issue/PR thread — don't suddenly adopt a different register.
98
- - Write as the maintainer would: first person, conversational, no corporate voice, no marketing copy, no emoji unless the repo's existing replies use them.
99
- - Do not include a signature, do not add attribution trailers (no "generated by", no `Co-Authored-By`), do not tag the reporter more than once.
100
- - Keep it focused — one screen of text is almost always enough. If the reply needs code blocks, prefer short, complete snippets over long diffs.
101
-
102
- Hand the reply to the user with one sentence above it saying where to paste it (e.g. "Ready-to-paste reply for issue #297:"). Do **not** run `gh issue comment` / `gh pr comment` / `gh issue close` / `gh pr merge` — the user posts it.
103
-
104
- ## Rules
105
-
106
- - **Never** edit files, run tests, or modify git state during steps 1–5. Planning only.
107
- - **Never** post a comment on the issue/PR, add labels, request reviews, merge, or close — not in step 6, not ever within this skill. Those are the user's decision.
108
- - **Never** fabricate line numbers or function names — if you cite `file:line`, you must have read it.
109
- - If `gh` is not authenticated or the number does not exist, stop and report the error verbatim.
110
- - If the issue/PR is in a different repository than the current working directory, stop and confirm with the user before proceeding — the plan would target the wrong codebase.
111
- - Keep the proposal focused. A plan that touches ten files for a one-line bug is a signal to re-read the issue, not to write a bigger plan.