@mrciphersmith/keryx 0.3.20 → 0.3.22

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/dist/cli.js CHANGED
@@ -9080,6 +9080,24 @@ var init_config = __esm(() => {
9080
9080
  });
9081
9081
 
9082
9082
  // src/security/detect/entropy.ts
9083
+ function isAdjacentLabel(content, start) {
9084
+ const before = lastLineOf(content.slice(Math.max(0, start - LABEL_WINDOW), start));
9085
+ if (ADJACENT_LABEL.test(before)) {
9086
+ return true;
9087
+ }
9088
+ if (before.trim().length > 0) {
9089
+ return false;
9090
+ }
9091
+ const lineStart = content.lastIndexOf(`
9092
+ `, start - 1);
9093
+ if (lineStart < 0) {
9094
+ return false;
9095
+ }
9096
+ const prevLineStart = content.lastIndexOf(`
9097
+ `, lineStart - 1) + 1;
9098
+ const prevLine = content.slice(prevLineStart, lineStart);
9099
+ return CONTINUATION_LABEL_LINE_RE.test(prevLine);
9100
+ }
9083
9101
  function isHexBlob(value) {
9084
9102
  return HEX_BLOB.test(value) && /[a-f]/i.test(value);
9085
9103
  }
@@ -9279,12 +9297,19 @@ function looksSecretShapedIn(text) {
9279
9297
  return secretShapedCandidatesIn(text).length > 0;
9280
9298
  }
9281
9299
  function labelledPieceQualifies(piece) {
9282
- const base = bareShapeQualifies(piece);
9283
- if (base.qualifies) {
9284
- return { entropy: base.entropy };
9285
- }
9286
9300
  if (isAllowShapedValue(piece)) {
9287
- return { entropy: base.entropy };
9301
+ return { entropy: shannonEntropy(piece) };
9302
+ }
9303
+ if (isHexBlob(piece)) {
9304
+ return { entropy: shannonEntropy(piece) };
9305
+ }
9306
+ if (!/[0-9]/.test(piece) && !/[+=]/.test(piece)) {
9307
+ return null;
9308
+ }
9309
+ const withoutSeparators = piece.replace(/[-_.+]/g, "");
9310
+ const entropy = shannonEntropy(withoutSeparators.length > 0 ? withoutSeparators : piece);
9311
+ if (entropy >= 3.6) {
9312
+ return { entropy };
9288
9313
  }
9289
9314
  return null;
9290
9315
  }
@@ -9323,9 +9348,9 @@ function detectEntropy(content) {
9323
9348
  if (urlSpans.some((u) => start < u.end && end > u.start)) {
9324
9349
  continue;
9325
9350
  }
9326
- const { qualifies, entropy } = bareShapeQualifies(value);
9327
- if (qualifies) {
9328
- matches.push(buildMatch(start, end, value, entropy));
9351
+ const found = labelledPieceQualifies(value);
9352
+ if (found !== null) {
9353
+ matches.push(buildMatch(start, end, value, found.entropy));
9329
9354
  }
9330
9355
  }
9331
9356
  TOKEN.lastIndex = 0;
@@ -9340,8 +9365,7 @@ function detectEntropy(content) {
9340
9365
  if (matches.some((existing) => start < existing.end && end > existing.start)) {
9341
9366
  continue;
9342
9367
  }
9343
- const before = lastLineOf(content.slice(Math.max(0, start - LABEL_WINDOW), start));
9344
- if (!ADJACENT_LABEL.test(before)) {
9368
+ if (!isAdjacentLabel(content, start)) {
9345
9369
  continue;
9346
9370
  }
9347
9371
  const head = TOKEN_HEAD.exec(value)?.[0] ?? value;
@@ -9363,9 +9387,10 @@ function detectEntropy(content) {
9363
9387
  }
9364
9388
  return matches.sort((a, b) => a.start - b.start);
9365
9389
  }
9366
- var SENSITIVE_LABEL_WORDS = "key|secret|token|password|passwd|api|credential|auth|bearer", caseless = (words) => words.replace(/[a-z]/g, (ch) => `[${ch}${ch.toUpperCase()}]`), ADJACENT_LABEL, LABEL_ASSIGNMENT_RE, TOKEN, TOKEN_HEAD, LABEL_WINDOW = 40, HEX_BLOB, FULL_GIT_SHA_RE, UUID_RE, INTEGRITY_RE, TAG_MAX_LETTER_RUN = 8, SLUG_HEX_TAIL_RE, URL_SPAN_RE, URL_TRAILING_PUNCTUATION_RE, URL_COMPONENT_MIN_LENGTH = 20;
9390
+ var SENSITIVE_LABEL_WORDS = "key|secret|token|password|passwd|api|credential|auth|bearer", caseless = (words) => words.replace(/[a-z]/g, (ch) => `[${ch}${ch.toUpperCase()}]`), FILLER_WORDS = "is|was|set|to|the|value|now", ADJACENT_LABEL, CONTINUATION_LABEL_LINE_RE, LABEL_ASSIGNMENT_RE, TOKEN, TOKEN_HEAD, LABEL_WINDOW = 40, HEX_BLOB, FULL_GIT_SHA_RE, UUID_RE, INTEGRITY_RE, TAG_MAX_LETTER_RUN = 8, SLUG_HEX_TAIL_RE, URL_SPAN_RE, URL_TRAILING_PUNCTUATION_RE, URL_COMPONENT_MIN_LENGTH = 20;
9367
9391
  var init_entropy = __esm(() => {
9368
- ADJACENT_LABEL = new RegExp(`(?:^|[^A-Za-z0-9])(?:${caseless(SENSITIVE_LABEL_WORDS)})[A-Za-z0-9_-]*["']?\\s*[:=]?\\s*["']?$`);
9392
+ ADJACENT_LABEL = new RegExp(`(?:^|[^A-Za-z0-9])(?:${caseless(SENSITIVE_LABEL_WORDS)})[A-Za-z0-9_-]*` + `(?:\\s+(?:${caseless(FILLER_WORDS)})){0,2}` + `["']?\\s*[:=]?\\s*["']?$`);
9393
+ CONTINUATION_LABEL_LINE_RE = new RegExp(`(?:^|[^A-Za-z0-9])(?:${caseless(SENSITIVE_LABEL_WORDS)})[A-Za-z0-9_-]*["']?\\s*[:=]?\\s*\\\\\\s*$`);
9369
9394
  LABEL_ASSIGNMENT_RE = new RegExp(`(?:^|[^A-Za-z0-9])(?:${SENSITIVE_LABEL_WORDS})[A-Za-z0-9_-]*"?\\s*=\\s*"?([A-Za-z0-9+/_=-]{6,})`, "gi");
9370
9395
  TOKEN = /[A-Za-z0-9+/=_-]{20,}(?:\.[A-Za-z0-9+/=_-]{6,})*/g;
9371
9396
  TOKEN_HEAD = /^[A-Za-z0-9+/=_-]+/;
@@ -18093,7 +18118,7 @@ var package_default;
18093
18118
  var init_package = __esm(() => {
18094
18119
  package_default = {
18095
18120
  name: "@mrciphersmith/keryx",
18096
- version: "0.3.20",
18121
+ version: "0.3.22",
18097
18122
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
18098
18123
  private: false,
18099
18124
  publishConfig: {
@@ -129152,6 +129177,37 @@ function createSystemWebWorkerRunner() {
129152
129177
 
129153
129178
  // src/harness/web/outbound-secret.ts
129154
129179
  init_service4();
129180
+ var HEX32_FRAGMENT = "[0-9a-f]{32}";
129181
+ var SHA40_FRAGMENT = "[0-9a-f]{40}";
129182
+ var GIST_HEX32_PATH_RE = new RegExp(`^/(?:[^/]+/)?(${HEX32_FRAGMENT})/?$`, "i");
129183
+ var GITHUB_COMMIT_OR_BLOB_PATH_RE = new RegExp(`/(?:commit|blob)/(${SHA40_FRAGMENT})(?:/|$)`, "i");
129184
+ var GITLAB_COMMIT_PATH_RE = new RegExp(`/-/commit/(${SHA40_FRAGMENT})(?:/|$)`, "i");
129185
+ function knownPublicIdentifierIn(url) {
129186
+ const host = url.hostname.toLowerCase();
129187
+ if (host === "gist.github.com") {
129188
+ return GIST_HEX32_PATH_RE.exec(url.pathname)?.[1] ?? null;
129189
+ }
129190
+ if (host === "github.com") {
129191
+ return GITHUB_COMMIT_OR_BLOB_PATH_RE.exec(url.pathname)?.[1] ?? null;
129192
+ }
129193
+ if (host === "gitlab.com") {
129194
+ return GITLAB_COMMIT_PATH_RE.exec(url.pathname)?.[1] ?? null;
129195
+ }
129196
+ return null;
129197
+ }
129198
+ function stripKnownPublicIdentifier(text) {
129199
+ let url;
129200
+ try {
129201
+ url = new URL(text);
129202
+ } catch {
129203
+ return text;
129204
+ }
129205
+ const identifier = knownPublicIdentifierIn(url);
129206
+ if (identifier === null) {
129207
+ return text;
129208
+ }
129209
+ return text.replace(identifier, "0".repeat(identifier.length));
129210
+ }
129155
129211
  function safeDecodeComponent(value) {
129156
129212
  try {
129157
129213
  return decodeURIComponent(value);
@@ -129195,13 +129251,14 @@ function anyComponentLooksSecret(text) {
129195
129251
  return false;
129196
129252
  }
129197
129253
  function containsOutboundSecret(text) {
129198
- if (detectSecrets(text).length > 0) {
129254
+ const scanText = stripKnownPublicIdentifier(text);
129255
+ if (detectSecrets(scanText).length > 0) {
129199
129256
  return true;
129200
129257
  }
129201
- if (isEntropyBackendEnabled() && detectEntropy(text).length > 0) {
129258
+ if (isEntropyBackendEnabled() && detectEntropy(scanText).length > 0) {
129202
129259
  return true;
129203
129260
  }
129204
- return anyComponentLooksSecret(text);
129261
+ return anyComponentLooksSecret(scanText);
129205
129262
  }
129206
129263
  async function recordOutboundSecretFinding(cwd, tool, subject) {
129207
129264
  try {
@@ -151074,6 +151131,15 @@ function reinsertMainQueueItem(items, at, item) {
151074
151131
  next.splice(insertAt, 0, item);
151075
151132
  return next;
151076
151133
  }
151134
+ function drainIdleMainQueue(items, options) {
151135
+ if (!options.isIdle())
151136
+ return false;
151137
+ const next = options.takeForced() ?? items.shift();
151138
+ if (next === undefined)
151139
+ return false;
151140
+ options.dispatch(next);
151141
+ return true;
151142
+ }
151077
151143
 
151078
151144
  // src/tui/queue-nav.ts
151079
151145
  var QUEUE_NAV_ACTIONS = ["force", "edit", "delete"];
@@ -159970,6 +160036,14 @@ Staying in the current session.
159970
160036
  const id = `mq${mainQueueSeq++}`;
159971
160037
  mainQueue.push({ id, question: line, displayQuestion: displayLine });
159972
160038
  paintMainQueue();
160039
+ drainIdleMainQueue(mainQueue, {
160040
+ isIdle: () => !destroyed && !chrome.isBusy() && !foregroundOperation.isActive && !forceHandoff.isAwaitingSettlement && leaseView()?.held() !== true,
160041
+ takeForced: () => forceHandoff.takeNext(),
160042
+ dispatch: (next) => {
160043
+ paintMainQueue();
160044
+ runLine(next.question);
160045
+ }
160046
+ });
159973
160047
  } else {
159974
160048
  appendUserEcho(otui, r, transcript, {
159975
160049
  id: `side-q${uid++}`,
package/dist/core.js CHANGED
@@ -4029,6 +4029,24 @@ var init_config2 = __esm(() => {
4029
4029
  });
4030
4030
 
4031
4031
  // src/security/detect/entropy.ts
4032
+ function isAdjacentLabel(content, start) {
4033
+ const before = lastLineOf(content.slice(Math.max(0, start - LABEL_WINDOW), start));
4034
+ if (ADJACENT_LABEL.test(before)) {
4035
+ return true;
4036
+ }
4037
+ if (before.trim().length > 0) {
4038
+ return false;
4039
+ }
4040
+ const lineStart = content.lastIndexOf(`
4041
+ `, start - 1);
4042
+ if (lineStart < 0) {
4043
+ return false;
4044
+ }
4045
+ const prevLineStart = content.lastIndexOf(`
4046
+ `, lineStart - 1) + 1;
4047
+ const prevLine = content.slice(prevLineStart, lineStart);
4048
+ return CONTINUATION_LABEL_LINE_RE.test(prevLine);
4049
+ }
4032
4050
  function isHexBlob(value) {
4033
4051
  return HEX_BLOB.test(value) && /[a-f]/i.test(value);
4034
4052
  }
@@ -4228,12 +4246,19 @@ function looksSecretShapedIn(text) {
4228
4246
  return secretShapedCandidatesIn(text).length > 0;
4229
4247
  }
4230
4248
  function labelledPieceQualifies(piece) {
4231
- const base = bareShapeQualifies(piece);
4232
- if (base.qualifies) {
4233
- return { entropy: base.entropy };
4234
- }
4235
4249
  if (isAllowShapedValue(piece)) {
4236
- return { entropy: base.entropy };
4250
+ return { entropy: shannonEntropy(piece) };
4251
+ }
4252
+ if (isHexBlob(piece)) {
4253
+ return { entropy: shannonEntropy(piece) };
4254
+ }
4255
+ if (!/[0-9]/.test(piece) && !/[+=]/.test(piece)) {
4256
+ return null;
4257
+ }
4258
+ const withoutSeparators = piece.replace(/[-_.+]/g, "");
4259
+ const entropy = shannonEntropy(withoutSeparators.length > 0 ? withoutSeparators : piece);
4260
+ if (entropy >= 3.6) {
4261
+ return { entropy };
4237
4262
  }
4238
4263
  return null;
4239
4264
  }
@@ -4272,9 +4297,9 @@ function detectEntropy(content) {
4272
4297
  if (urlSpans.some((u) => start < u.end && end > u.start)) {
4273
4298
  continue;
4274
4299
  }
4275
- const { qualifies, entropy } = bareShapeQualifies(value);
4276
- if (qualifies) {
4277
- matches.push(buildMatch(start, end, value, entropy));
4300
+ const found = labelledPieceQualifies(value);
4301
+ if (found !== null) {
4302
+ matches.push(buildMatch(start, end, value, found.entropy));
4278
4303
  }
4279
4304
  }
4280
4305
  TOKEN.lastIndex = 0;
@@ -4289,8 +4314,7 @@ function detectEntropy(content) {
4289
4314
  if (matches.some((existing) => start < existing.end && end > existing.start)) {
4290
4315
  continue;
4291
4316
  }
4292
- const before = lastLineOf(content.slice(Math.max(0, start - LABEL_WINDOW), start));
4293
- if (!ADJACENT_LABEL.test(before)) {
4317
+ if (!isAdjacentLabel(content, start)) {
4294
4318
  continue;
4295
4319
  }
4296
4320
  const head = TOKEN_HEAD.exec(value)?.[0] ?? value;
@@ -4312,9 +4336,10 @@ function detectEntropy(content) {
4312
4336
  }
4313
4337
  return matches.sort((a, b) => a.start - b.start);
4314
4338
  }
4315
- var SENSITIVE_LABEL_WORDS = "key|secret|token|password|passwd|api|credential|auth|bearer", caseless = (words) => words.replace(/[a-z]/g, (ch) => `[${ch}${ch.toUpperCase()}]`), ADJACENT_LABEL, LABEL_ASSIGNMENT_RE, TOKEN, TOKEN_HEAD, LABEL_WINDOW = 40, HEX_BLOB, FULL_GIT_SHA_RE, UUID_RE, INTEGRITY_RE, TAG_MAX_LETTER_RUN = 8, SLUG_HEX_TAIL_RE, URL_SPAN_RE, URL_TRAILING_PUNCTUATION_RE, URL_COMPONENT_MIN_LENGTH = 20;
4339
+ var SENSITIVE_LABEL_WORDS = "key|secret|token|password|passwd|api|credential|auth|bearer", caseless = (words) => words.replace(/[a-z]/g, (ch) => `[${ch}${ch.toUpperCase()}]`), FILLER_WORDS = "is|was|set|to|the|value|now", ADJACENT_LABEL, CONTINUATION_LABEL_LINE_RE, LABEL_ASSIGNMENT_RE, TOKEN, TOKEN_HEAD, LABEL_WINDOW = 40, HEX_BLOB, FULL_GIT_SHA_RE, UUID_RE, INTEGRITY_RE, TAG_MAX_LETTER_RUN = 8, SLUG_HEX_TAIL_RE, URL_SPAN_RE, URL_TRAILING_PUNCTUATION_RE, URL_COMPONENT_MIN_LENGTH = 20;
4316
4340
  var init_entropy = __esm(() => {
4317
- ADJACENT_LABEL = new RegExp(`(?:^|[^A-Za-z0-9])(?:${caseless(SENSITIVE_LABEL_WORDS)})[A-Za-z0-9_-]*["']?\\s*[:=]?\\s*["']?$`);
4341
+ ADJACENT_LABEL = new RegExp(`(?:^|[^A-Za-z0-9])(?:${caseless(SENSITIVE_LABEL_WORDS)})[A-Za-z0-9_-]*` + `(?:\\s+(?:${caseless(FILLER_WORDS)})){0,2}` + `["']?\\s*[:=]?\\s*["']?$`);
4342
+ CONTINUATION_LABEL_LINE_RE = new RegExp(`(?:^|[^A-Za-z0-9])(?:${caseless(SENSITIVE_LABEL_WORDS)})[A-Za-z0-9_-]*["']?\\s*[:=]?\\s*\\\\\\s*$`);
4318
4343
  LABEL_ASSIGNMENT_RE = new RegExp(`(?:^|[^A-Za-z0-9])(?:${SENSITIVE_LABEL_WORDS})[A-Za-z0-9_-]*"?\\s*=\\s*"?([A-Za-z0-9+/_=-]{6,})`, "gi");
4319
4344
  TOKEN = /[A-Za-z0-9+/=_-]{20,}(?:\.[A-Za-z0-9+/=_-]{6,})*/g;
4320
4345
  TOKEN_HEAD = /^[A-Za-z0-9+/=_-]+/;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -1587,10 +1587,12 @@ Fix: <concrete>
1587
1587
  - <what was checked> — <how>
1588
1588
 
1589
1589
  ### How this review was run
1590
- - **Run by:** @<gh-login> with `review-orchestrator`
1590
+ - **Workflow:** `review-orchestrator`
1591
1591
  - **Scope:** `<base>..<head>`, round N, PR #N
1592
- - **Orchestrator:** `review-orchestrator`
1593
- - **<actual model name>:** <reviewers on that model>
1592
+ - **Models:** <actual models and reviewers, or not recorded>
1593
+ - **Tools:** <tools actually invoked, or not recorded>
1594
+ - **Skills:** <skills actually used, or not recorded>
1595
+ - **Subagents:** <dispatched reviewers and models, or none when confirmed; otherwise not recorded>
1594
1596
  - **Not run:** <reviewer — reason>
1595
1597
  - **Verification:** <mode>; confirmed N, refuted N, unverifiable N, unverified N
1596
1598
  ```
@@ -1663,17 +1665,15 @@ Automation values, names unchanged:
1663
1665
 
1664
1666
  Default is do not publish. No resolvable PR number means skip and say so.
1665
1667
 
1666
- The comment is the report rendered from `templates/review-report.md`, English, with the domain file chosen above (`templates/pr-comment-frontend.md` / `templates/pr-comment-backend.md`) — never a summary written freehand. It does not use a tool heading, a finding table, or a meta table. It does not carry a co-author line, a `Generated with` trailer, or any sentence that names a vendor or a product as the author. Say who ran the orchestrator and which reviewers ran; do not sign the comment as them.
1668
+ The comment is the report rendered from `templates/review-report.md`, English, with the domain file chosen above (`templates/pr-comment-frontend.md` / `templates/pr-comment-backend.md`) — never a summary written freehand. It does not use a tool heading, a finding table, or a meta table. It does not carry a co-author line, a `Generated with` trailer, or any sentence that names a vendor or a product as the author. Name the workflow and reviewers that actually ran, with models, tools, skills and subagents. Never identify the operator from `gh auth status`, git config or a credential; do not sign the comment as a person, reviewer or model.
1667
1669
 
1668
1670
  The follow-up file path and the metadata rules (real model names, Run vs Not run, no `adaptive` in the model slot) live in that same template. Write the body to a temp file and post with `gh pr comment <n> --body-file <file>`.
1669
1671
 
1670
1672
  **No GitHub write without an approved draft.** Before ANY write — the comment, a thread reply, `keryx review comments reply`, a review — show the user the rendered body and wait for explicit approval of that body.
1671
1673
  Picking A or B above chooses *whether* to publish, not *what*; a comment is public and cannot be unsent. With no user to answer (a dispatched run), do not write: hand the rendered body back to the caller.
1672
1674
 
1673
- Re-read head and the thread immediately before posting. If head moved, re-check
1674
- the findings against the new head and name the commits that were not reviewed.
1675
- Do not post a finding another reviewer already filed on this head; if yours
1676
- extends theirs, cite theirs.
1675
+ Re-read head and thread before posting. If head moved, re-check findings
1676
+ and name unchecked commits. Do not duplicate findings; cite extended ones.
1677
1677
 
1678
1678
  ## Job Context Awareness
1679
1679
 
@@ -95,14 +95,19 @@ Do not list a checklist item that was not checked. Do not praise.
95
95
 
96
96
  ## How this review was run
97
97
 
98
- A list, not a meta table. Keep every fact the old Meta table carried.
98
+ A factual run inventory, not an author or account signature. List only steps
99
+ confirmed by the run record; never infer an operator identity from `gh auth`,
100
+ git config, a credential or a PR author. Use `not recorded` when evidence for
101
+ a tool, skill or model is unavailable; use `none` only when the run proves none.
99
102
 
100
103
  ```markdown
101
104
  ### How this review was run
102
- - **Run by:** @<gh-login> with `review-orchestrator`. Severity and lanes per the repository review rules, else the canonical rubric in this skill.
105
+ - **Workflow:** `review-orchestrator`; severity and lanes per the repository review rules, else the canonical rubric in this skill.
103
106
  - **Scope:** `<base>..<head>`, round N, PR #N. Unchecked commits: <list or none>.
104
- - **Orchestrator:** `review-orchestrator`
105
- - **<actual model name>:** <reviewers dispatched on it, comma-separated>
107
+ - **Models:** <actual model names and the reviewers that ran on each; or not recorded>.
108
+ - **Tools:** <tools actually invoked for this review and their roles; or not recorded>.
109
+ - **Skills:** <skills actually loaded or executed and their roles; or not recorded>.
110
+ - **Subagents:** <dispatched subagents, their reviewer roles and actual models; or none when confirmed, otherwise not recorded>.
106
111
  - **Fallback:** <reviewer> via `general-purpose` because <native agent type unavailable | no bundled agent>. Or `none`.
107
112
  - **Not run:** <reviewer> — <short reason>. Or `none`.
108
113
  - **Selection:** <auto-detected scope | explicit flags | optional groups the user picked>
@@ -118,8 +123,8 @@ Model rules, unchanged from the old metadata table:
118
123
  - Name the model that actually ran. Never write `adaptive`, `inherit`,
119
124
  `unsupported`, or `current-session` in the model slot.
120
125
  - Group reviewers under the model they ran on. One bullet per model.
121
- - `Model strategy` (`ask` or `adaptive`) goes on the Run by line or under
122
- Selection, not in place of a model name.
126
+ - `Model strategy` (`ask` or `adaptive`) goes under Selection, never in
127
+ place of an actual model name.
123
128
  - Keep **Run** complete. If Not run is long, group it with counts and notable
124
129
  names; the full list belongs in the follow-up file.
125
130