auxilo-mcp 0.9.6 → 0.9.7

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
@@ -33,7 +33,7 @@ Enable extraction and a session-end hook runs when your agent finishes a session
33
33
  1. The hook hands the runner the path to the session transcript.
34
34
  2. The runner reads the transcript on your machine and scrubs it with a fail-closed secret filter: 24 patterns covering API keys, tokens, private keys, JWTs, connection strings, cookies, email addresses, phone numbers, and internal IPs. If a rescan still finds a match, the run stops and nothing is sent.
35
35
  3. Your own model client (your claude CLI, on your subscription) drafts learnings from the scrubbed text and screens them again.
36
- 4. Clean drafts publish to the marketplace with a 7-day retraction window. Anything a screen flags waits in your private pending queue instead.
36
+ 4. Drafts held for review appear in three server-defined lanes: **Ready to publish**, **Needs a score**, and **Needs your eyes**. The last lane includes the server's reason so you can inspect the exception directly.
37
37
 
38
38
  ### What never leaves your machine
39
39
 
@@ -41,15 +41,17 @@ Your raw transcripts. They are read and scrubbed on your machine, and they are n
41
41
 
42
42
  ### How publishing works
43
43
 
44
- Extraction defaults to seamless: a draft that passes every screen (secrets, sensitivity, injection, near-duplicate, quality) publishes right away, and you can retract it for 7 days. A draft that any screen flags waits in a pending queue only you can see.
44
+ Extraction defaults to seamless: a draft that passes every screen (secrets, sensitivity, injection, near-duplicate, quality) publishes right away, and you can retract it for 7 days. A draft held for review waits in a pending queue only you can see, grouped by the server's Ready to publish / Needs a score / Needs your eyes verdict.
45
45
 
46
46
  ```bash
47
- npx auxilo review # approve, reject, or skip each queued draft
48
- npx auxilo status # clients, hooks, queue depth, consent state
49
- npx auxilo disable # kill switch: extraction stops immediately
47
+ npx auxilo review --list # show all three lanes and each exception reason
48
+ npx auxilo review --approve-ready # select exactly the server's ready_to_publish lane
49
+ npx auxilo review # approve, reject, view, or skip one draft at a time
50
+ npx auxilo status # clients, hooks, queue depth, consent state
51
+ npx auxilo disable # kill switch: extraction stops immediately
50
52
  ```
51
53
 
52
- Approve a queued draft and it goes live in the marketplace. Reject it and it stays private. Prefer approve-first for everything? Switch your account to manual mode in account settings and every draft waits for you.
54
+ Every bulk approval prints the exact selection and requires you to type its count. `--min-quality 16` narrows the ready lane; values below 14 explicitly reach into Needs a score and print a warning before the same counted confirmation. Approve a queued draft and it goes live in the marketplace. Reject it and it stays private. Prefer approve-first for everything? Switch your account to manual mode in account settings and every draft waits for you.
53
55
 
54
56
  Extraction off? Your agent can still contribute in-session: tell it to submit a learning with the `auxilo_contribute` tool.
55
57
 
package/bin/auxilo-cli.js CHANGED
@@ -32,19 +32,45 @@ const HOME = os.homedir();
32
32
  // question N+1 arrives with question N's buffer and the next prompt hangs
33
33
  // on EOF (the 0.8.1 consent step silently never completed).
34
34
  let sharedRl = null;
35
+ let bufferedLines = [];
36
+ let lineWaiters = [];
37
+ let readlineEnded = false;
35
38
  function getRl() {
36
39
  if (!sharedRl) {
37
- sharedRl = readline.createInterface({ input: process.stdin, output: process.stdout });
40
+ readlineEnded = false;
41
+ sharedRl = readline.createInterface({
42
+ input: process.stdin,
43
+ output: process.stdout,
44
+ terminal: !!process.stdin.isTTY,
45
+ });
46
+ sharedRl.on('line', (line) => {
47
+ const answer = line.trim();
48
+ const waiter = lineWaiters.shift();
49
+ if (waiter) waiter(answer);
50
+ else bufferedLines.push(answer);
51
+ });
52
+ sharedRl.on('close', () => {
53
+ readlineEnded = true;
54
+ while (lineWaiters.length) lineWaiters.shift()('');
55
+ });
38
56
  }
39
57
  return sharedRl;
40
58
  }
41
59
  function closeRl() {
42
- if (sharedRl) { sharedRl.close(); sharedRl = null; }
60
+ if (sharedRl) sharedRl.close();
61
+ sharedRl = null;
62
+ bufferedLines = [];
63
+ lineWaiters = [];
64
+ readlineEnded = false;
43
65
  }
44
66
 
45
67
  function ask(question) {
68
+ getRl();
69
+ process.stdout.write(question);
70
+ if (bufferedLines.length > 0) return Promise.resolve(bufferedLines.shift());
71
+ if (readlineEnded) return Promise.resolve('');
46
72
  return new Promise((resolve) => {
47
- getRl().question(question, (answer) => resolve(answer.trim()));
73
+ lineWaiters.push(resolve);
48
74
  });
49
75
  }
50
76
 
@@ -509,7 +535,7 @@ async function cmdDisable(flags) {
509
535
  // Operates ONLY on the caller's own pending learnings (account-scoped API key).
510
536
  //
511
537
  // Review-seamless (2026-07-18): the default view is now a triage summary table
512
- // (quality desc, clean before flagged), with bulk modes that batch through
538
+ // (quality-desc rows grouped into the server's three lanes), with bulk modes that batch through
513
539
  // POST /account/pending/bulk. HARD RULE for every bulk APPROVE path: print the
514
540
  // exact list and count first, then require the operator to TYPE THE COUNT.
515
541
  // There is no --yes bypass on any approve path; publishing always costs one
@@ -522,10 +548,18 @@ function fit(s, width) {
522
548
  return str.padEnd(width);
523
549
  }
524
550
 
525
- /** Short flag codes for a summary row: 'clean' or e.g. 'inj+sens'. */
551
+ /** Short lane/flag codes for a summary row, e.g. 'ready' or 'inj+sens'. */
526
552
  function shortFlags(row) {
527
- if (row.screens_passed) return 'clean';
528
- const map = { injection: 'inj', content_sensitivity: 'sens', near_duplicate: 'dup' };
553
+ const resolved = review.reviewLane(row);
554
+ if (resolved.lane === 'ready_to_publish') return 'ready';
555
+ if (resolved.lane === 'needs_score') return 'score';
556
+ const map = {
557
+ injection: 'inj',
558
+ content_sensitivity: 'sens',
559
+ near_duplicate: 'dup',
560
+ process_advice: 'advice',
561
+ account_vocab: 'vocab',
562
+ };
529
563
  return (row.flags || []).map((f) => map[f] || f).join('+') || 'flagged';
530
564
  }
531
565
 
@@ -535,13 +569,41 @@ function triageLine(row, n, total) {
535
569
  return ` ${String(n).padStart(String(total).length)}. q=${q} ${fit(shortFlags(row), 9)} ${fit(row.category || '', 20)} ${fit(row.title || '(no title)', 52)}`;
536
570
  }
537
571
 
538
- /** Print the triage summary: counts, then clean rows, then flagged rows. */
572
+ function groupSummaryRows(summary) {
573
+ const groups = {
574
+ ready_to_publish: [],
575
+ needs_score: [],
576
+ needs_your_eyes: [],
577
+ };
578
+ let versionSkew = false;
579
+ for (const row of summary.items || []) {
580
+ const resolved = review.reviewLane(row);
581
+ versionSkew = versionSkew || resolved.version_skew;
582
+ groups[resolved.lane].push(row);
583
+ }
584
+ const serverCounts = summary.counts && summary.counts.by_lane;
585
+ const counts = !versionSkew && serverCounts
586
+ ? {
587
+ ready_to_publish: serverCounts.ready_to_publish || 0,
588
+ needs_score: serverCounts.needs_score || 0,
589
+ needs_your_eyes: serverCounts.needs_your_eyes || 0,
590
+ }
591
+ : {
592
+ ready_to_publish: groups.ready_to_publish.length,
593
+ needs_score: groups.needs_score.length,
594
+ needs_your_eyes: groups.needs_your_eyes.length,
595
+ };
596
+ return { groups, counts, versionSkew };
597
+ }
598
+
599
+ /** Print the triage summary in the server's three-lane order. */
539
600
  function printSummaryTable(summary) {
540
- const items = summary.items || [];
541
- const clean = items.filter((r) => r.screens_passed);
542
- const flagged = items.filter((r) => !r.screens_passed);
601
+ const { groups, counts, versionSkew } = groupSummaryRows(summary);
543
602
 
544
- console.log(`\n${summary.pending_count} learning(s) pending your review: ${clean.length} passed every platform screen, ${flagged.length} flagged.`);
603
+ console.log(`\n${summary.pending_count} learning(s) pending your review: ${counts.ready_to_publish} ready to publish, ${counts.needs_score} need a score, ${counts.needs_your_eyes} need your eyes.`);
604
+ if (versionSkew) {
605
+ console.log('VERSION SKEW: this server did not return lane on every row; using the legacy screens-and-quality fallback.');
606
+ }
545
607
  const bands = summary.counts && summary.counts.by_quality_band;
546
608
  if (bands) {
547
609
  console.log(`Quality bands: 18-20: ${bands['18-20'] || 0} · 14-17: ${bands['14-17'] || 0} · 10-13: ${bands['10-13'] || 0} · below 10: ${bands.below_10 || 0} · unscored: ${bands.unscored || 0}`);
@@ -550,13 +612,20 @@ function printSummaryTable(summary) {
550
612
  console.log(`Near-duplicate clusters among your pending items: ${summary.near_dup_clusters.length} (review these together)`);
551
613
  }
552
614
 
553
- if (clean.length > 0) {
554
- console.log(`\nCLEAN (passed every screen) - sorted by quality:`);
555
- clean.forEach((r, i) => console.log(triageLine(r, i + 1, clean.length)));
615
+ if (groups.ready_to_publish.length > 0) {
616
+ console.log(`\nREADY TO PUBLISH (${counts.ready_to_publish}) - sorted by quality:`);
617
+ groups.ready_to_publish.forEach((r, i) => console.log(triageLine(r, i + 1, groups.ready_to_publish.length)));
556
618
  }
557
- if (flagged.length > 0) {
558
- console.log(`\nFLAGGED (a platform screen raised a signal) - review individually:`);
559
- flagged.forEach((r, i) => console.log(triageLine(r, i + 1, flagged.length)));
619
+ if (groups.needs_score.length > 0) {
620
+ console.log(`\nNEEDS A SCORE (${counts.needs_score}) - score or review individually:`);
621
+ groups.needs_score.forEach((r, i) => console.log(triageLine(r, i + 1, groups.needs_score.length)));
622
+ }
623
+ if (groups.needs_your_eyes.length > 0) {
624
+ console.log(`\nNEEDS YOUR EYES (${counts.needs_your_eyes}) - review individually:`);
625
+ groups.needs_your_eyes.forEach((r, i) => {
626
+ console.log(triageLine(r, i + 1, groups.needs_your_eyes.length));
627
+ if (r.why) console.log(` why: ${r.why}`);
628
+ });
560
629
  }
561
630
  console.log('');
562
631
  }
@@ -640,32 +709,43 @@ async function cmdReview(flags) {
640
709
  // ── --list: summary only, no mutations ────────────────────────────────────
641
710
  if (flags.list) return;
642
711
 
643
- // ── --approve-clean: bulk-approve items that passed EVERY platform screen
644
- // AND clear the quality threshold. Typed-count confirmation, no bypass. ──
645
- if (flags['approve-clean']) {
712
+ // ── --approve-ready: consume the server's ready_to_publish verdict.
713
+ // --approve-clean remains a hidden compatibility alias. A stricter
714
+ // threshold narrows the ready lane; a lower one explicitly reaches into
715
+ // needs_score. Typed-count confirmation, no bypass. ─────────────────────
716
+ if (flags['approve-ready'] || flags['approve-clean']) {
717
+ if (flags['approve-clean']) {
718
+ console.log('Note: --approve-clean was renamed to --approve-ready; the old flag remains a compatibility alias.');
719
+ }
646
720
  const minQuality = parseMinQuality(flags);
647
- const sel = review.selectForBulkApprove(rows, { mode: 'clean', minQuality });
648
- console.log(`approve-clean selection: ${sel.selected.length} of ${rows.length} pending (threshold: quality >= ${minQuality}).`);
649
- if (sel.excluded_flagged.length) console.log(` excluded ${sel.excluded_flagged.length} screen-flagged item(s) (review those individually).`);
721
+ const sel = review.selectForBulkApprove(rows, { mode: 'ready', minQuality });
722
+ console.log(`approve-ready selection: ${sel.selected.length} of ${rows.length} pending (threshold: quality >= ${minQuality}).`);
723
+ if (minQuality < review.DEFAULT_QUALITY_THRESHOLD) {
724
+ const approvableCount = Number.isFinite(summary.approvable_count)
725
+ ? summary.approvable_count
726
+ : review.selectForBulkApprove(rows, { mode: 'ready' }).selected.length;
727
+ console.log(`WARNING: selection goes beyond the server's approvable verdict (approvable_count=${approvableCount}); including ${sel.included_beyond_verdict.length} items from needs_score.`);
728
+ }
729
+ if (sel.excluded_flagged.length) console.log(` excluded ${sel.excluded_flagged.length} needs_your_eyes item(s) (review those individually).`);
650
730
  if (sel.excluded_low_quality.length) console.log(` excluded ${sel.excluded_low_quality.length} below the quality threshold.`);
651
731
  if (sel.excluded_unscored.length) console.log(` excluded ${sel.excluded_unscored.length} with no quality score (pass --min-quality 0 to include).`);
652
732
  if (sel.selected.length === 0) { console.log('Nothing qualifies. No changes made.'); return; }
653
733
 
654
734
  if (!await confirmByTypedCount(sel.selected, 'APPROVE and PUBLISH')) return;
655
735
  const totals = await runBulk({ apiKey, baseUrl, rows: sel.selected, decision: 'approve' });
656
- console.log(`\nDone. Approved ${totals.approved} (already approved earlier: ${totals.idempotent}, failed: ${totals.failed}). Flagged and below-threshold items stay pending.`);
736
+ console.log(`\nDone. Approved ${totals.approved} (already approved earlier: ${totals.idempotent}, failed: ${totals.failed}). Items outside the selection stay pending.`);
657
737
  return;
658
738
  }
659
739
 
660
- // ── --all: bulk-approve everything EXCEPT screen-flagged items unless
740
+ // ── --all: bulk-approve everything EXCEPT needs_your_eyes items unless
661
741
  // --include-flagged. Typed-count confirmation, no bypass. ───────────────
662
742
  if (flags.all) {
663
743
  const includeFlagged = flags['include-flagged'] === true;
664
744
  const sel = review.selectForBulkApprove(rows, { mode: 'all', includeFlagged });
665
- if (includeFlagged && sel.selected.some((r) => !r.screens_passed)) {
666
- console.log('WARNING: --include-flagged is set. This selection INCLUDES items a platform screen flagged (possible injection, sensitive content, or near-duplicates). Approving publishes them publicly.');
745
+ if (includeFlagged && sel.selected.some((r) => review.reviewLane(r).lane === 'needs_your_eyes')) {
746
+ console.log('WARNING: --include-flagged is set. This selection INCLUDES needs_your_eyes items (possible injection, sensitive content, process advice, account vocabulary, or near-duplicates). Approving publishes them publicly.');
667
747
  } else if (sel.excluded_flagged.length) {
668
- console.log(`Excluding ${sel.excluded_flagged.length} screen-flagged item(s) (pass --include-flagged to include them; safer to review those individually).`);
748
+ console.log(`Excluding ${sel.excluded_flagged.length} needs_your_eyes item(s) (pass --include-flagged to include them; safer to review those individually).`);
669
749
  }
670
750
  if (sel.selected.length === 0) { console.log('Nothing qualifies. No changes made.'); return; }
671
751
 
@@ -707,13 +787,14 @@ async function cmdReview(flags) {
707
787
  console.log('Rapid review. Per item: [y] approve (goes live) · [n] reject (stays private) · [v] view full body · [s] skip · [q] quit\n');
708
788
 
709
789
  let approved = 0, rejected = 0, skipped = 0;
710
- const ordered = rows; // clean-first, quality desc (server order preserved by printSummaryTable groups)
790
+ const ordered = rows; // quality-desc server order; the summary above groups lanes
711
791
  for (let i = 0; i < ordered.length; i++) {
712
792
  const row = ordered[i];
713
793
  console.log(triageLine(row, i + 1, ordered.length));
714
794
  let choice = '';
715
795
  for (;;) {
716
796
  choice = (await ask(' [y/n/v/s/q]? ')).toLowerCase().slice(0, 1);
797
+ if (!choice && readlineEnded) choice = 'q';
717
798
  if (choice === 'v') {
718
799
  const full = bodies.get(row.id);
719
800
  console.log(' ------------------------------------------------------------');
@@ -747,7 +828,46 @@ async function cmdReview(flags) {
747
828
 
748
829
  // ─── Entry point ────────────────────────────────────────────────────────────
749
830
 
750
- function usage() {
831
+ function usage(command) {
832
+ const blocks = {
833
+ setup: `Usage: auxilo setup [--re-auth] [--base-url <url>]
834
+
835
+ Interactively detect clients, register Auxilo, sign in, install the optional
836
+ extraction runner and SessionEnd hook, and record the extraction choice.`,
837
+ init: `Usage: auxilo init [--scope <read|earnings-read|contribute>] [--label <name>]
838
+ [--env-file <path>] [--save] [--json] [--no-browser]
839
+ [--base-url <url>]
840
+
841
+ Mint a scoped API key for CI or a second machine without running full setup.`,
842
+ status: `Usage: auxilo status
843
+
844
+ Show detected clients, auth, extraction mode, kill switch, SessionEnd hook,
845
+ last sweep, and pending queue depth.`,
846
+ review: `Usage: auxilo review [flags]
847
+
848
+ Render YOUR pending-review learnings in three server-defined lanes:
849
+ Ready to publish, Needs a score, and Needs your eyes.
850
+
851
+ Flags:
852
+ --list show the three-lane summary only; make no changes
853
+ --approve-ready select the server's ready_to_publish lane (default quality
854
+ floor 14; a higher --min-quality narrows it, while a lower
855
+ value explicitly reaches into needs_score)
856
+ --min-quality N quality threshold for --approve-ready (0-20)
857
+ --all approve everything except needs_your_eyes items
858
+ --include-flagged include needs_your_eyes with --all
859
+ --all-reject reject the whole batch [--yes for scripted incident use]
860
+ --base-url <url>
861
+
862
+ Every approval path prints the exact list and requires typing its count.`,
863
+ disable: `Usage: auxilo disable [--base-url <url>]
864
+
865
+ Disable background extraction locally and optionally revoke server consent.`,
866
+ };
867
+ if (command && blocks[command]) {
868
+ console.log(`\n${blocks[command]}\n`);
869
+ return;
870
+ }
751
871
  console.log(`
752
872
  Usage: auxilo <command>
753
873
 
@@ -771,18 +891,17 @@ Commands:
771
891
  status Show install/auth/extraction status.
772
892
  review Review YOUR pending-review learnings (from background extraction)
773
893
  and approve/reject before anything goes public. Default: triage
774
- summary table first (quality desc, clean vs flagged), then rapid
775
- y/n/s review.
894
+ summary first in the server's Ready to publish / Needs a score /
895
+ Needs your eyes lanes, then rapid y/n/s review.
776
896
  Flags:
777
897
  --list summary table only, no changes
778
- --approve-clean bulk-approve items that passed EVERY platform
779
- screen AND quality >= threshold (default 14;
780
- tune with --min-quality N). Prints the exact
781
- list, then requires typing the count.
782
- --all bulk-approve everything EXCEPT screen-flagged
783
- items (add --include-flagged to include them).
898
+ --approve-ready select the server's ready_to_publish lane
899
+ (default floor 14; tune with --min-quality N).
900
+ Prints the exact list, then requires its count.
901
+ --all bulk-approve everything EXCEPT needs_your_eyes
902
+ (add --include-flagged to include that lane).
784
903
  Same typed-count confirmation.
785
- --min-quality N quality threshold for --approve-clean (0-20)
904
+ --min-quality N quality threshold for --approve-ready (0-20)
786
905
  --all-reject reject the whole batch [--yes for scripted
787
906
  incident response; rejects stay private]
788
907
  --base-url <url>
@@ -797,6 +916,10 @@ Docs: https://auxilo.io · API: ${installer.DEFAULT_BASE_URL}
797
916
 
798
917
  async function main() {
799
918
  const cmd = process.argv[2];
919
+ const subcommandHelp = ['help', '--help', '-h'].includes(process.argv[3]);
920
+ if (['setup', 'init', 'status', 'review', 'disable'].includes(cmd) && subcommandHelp) {
921
+ return usage(cmd);
922
+ }
800
923
  const flags = parseFlags(process.argv);
801
924
  switch (cmd) {
802
925
  case 'setup': return cmdSetup(flags);
@@ -829,4 +952,12 @@ if (require.main === module) {
829
952
  run();
830
953
  }
831
954
 
832
- module.exports = { parseFlags, resolveBaseUrl, run };
955
+ module.exports = {
956
+ parseFlags,
957
+ resolveBaseUrl,
958
+ shortFlags,
959
+ groupSummaryRows,
960
+ printSummaryTable,
961
+ usage,
962
+ run,
963
+ };
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * True when a Claude Code SessionEnd collection contains an Auxilo extraction
5
+ * hook. Accepts both the legacy bare-command string and the current matcher
6
+ * group shape ({ hooks: [{ type: 'command', command }] }).
7
+ *
8
+ * @param {unknown} sessionEnd
9
+ * @returns {boolean}
10
+ */
11
+ function hasAuxiloSessionEndHook(sessionEnd) {
12
+ if (!Array.isArray(sessionEnd)) return false;
13
+
14
+ return sessionEnd.some((entry) => {
15
+ if (typeof entry === 'string') return entry.includes('auxilo-extract');
16
+ if (!entry || typeof entry !== 'object' || !Array.isArray(entry.hooks)) return false;
17
+ return entry.hooks.some((hook) =>
18
+ hook && typeof hook === 'object' &&
19
+ typeof hook.command === 'string' &&
20
+ hook.command.includes('auxilo-extract'));
21
+ });
22
+ }
23
+
24
+ module.exports = { hasAuxiloSessionEndHook };
package/lib/installer.js CHANGED
@@ -24,6 +24,7 @@
24
24
 
25
25
  const fs = require('fs');
26
26
  const path = require('path');
27
+ const { hasAuxiloSessionEndHook } = require('./hook-status.js');
27
28
 
28
29
  // ─── Constants ──────────────────────────────────────────────────────────────
29
30
 
@@ -75,6 +76,7 @@ const RUNNER_STACK = Object.freeze([
75
76
  ['lib/sensitivity-filter.js', 'lib/sensitivity-filter.js', 0o644],
76
77
  ['lib/extraction-index.js', 'lib/extraction-index.js', 0o644],
77
78
  ['lib/similarity.js', 'lib/similarity.js', 0o644],
79
+ ['lib/hook-status.js', 'lib/hook-status.js', 0o644],
78
80
  ['config/near-duplicate.json', 'config/near-duplicate.json', 0o644],
79
81
  ]);
80
82
 
@@ -1559,16 +1561,21 @@ async function getStatus(homeDir, opts = {}) {
1559
1561
  // 4. Sentinel + runner + hook install state
1560
1562
  const hookPath = hookScriptPathFor(homeDir);
1561
1563
  const runnerInstalled = fs.existsSync(path.join(binRootFor(homeDir), 'scripts', 'runner.js'));
1564
+ let hookInstalled = false;
1562
1565
  let hookRegistered = false;
1563
1566
  try {
1564
1567
  const settings = JSON.parse(
1565
1568
  fs.readFileSync(path.join(homeDir, '.claude', 'settings.json'), 'utf-8')
1566
1569
  );
1570
+ const sessionEnd = settings.hooks && settings.hooks.SessionEnd;
1571
+ // SPEC3-A3: installation detection accepts both the legacy string and the
1572
+ // current object shape through the same helper runner --status uses.
1573
+ hookInstalled = hasAuxiloSessionEndHook(sessionEnd);
1567
1574
  // LW-17: only STRUCTURED entries count — Claude Code silently ignores
1568
1575
  // bare-string entries (the 0.8.1 dead-hook bug), so reporting one as
1569
1576
  // "registered" would mask exactly the failure this status exists to catch.
1570
- hookRegistered = Array.isArray(settings.hooks && settings.hooks.SessionEnd) &&
1571
- settings.hooks.SessionEnd.some((h) =>
1577
+ hookRegistered = Array.isArray(sessionEnd) &&
1578
+ sessionEnd.some((h) =>
1572
1579
  h && typeof h === 'object' && Array.isArray(h.hooks) && h.hooks.some(isAuxiloCommandHook));
1573
1580
  } catch { /* no settings */ }
1574
1581
 
@@ -1592,7 +1599,8 @@ async function getStatus(homeDir, opts = {}) {
1592
1599
  accountMode,
1593
1600
  sentinel: sentinelPresent(homeDir),
1594
1601
  runnerInstalled,
1595
- hookInstalled: fs.existsSync(hookPath),
1602
+ hookInstalled,
1603
+ hookScriptInstalled: fs.existsSync(hookPath),
1596
1604
  hookRegistered,
1597
1605
  // LW-18 layer 1b: SessionStart held-count notice
1598
1606
  noticeRegistered: sessionStartNoticeRegistered(homeDir),
package/lib/review.js CHANGED
@@ -121,7 +121,7 @@ function formatFlags(l) {
121
121
 
122
122
  // ── Review-seamless additions (2026-07-18) ──────────────────────────────────
123
123
  //
124
- // Summary + bulk network calls, plus the pure approve-clean selection logic
124
+ // Summary + bulk network calls, plus the pure approve-ready selection logic
125
125
  // shared by the CLI, the MCP auxilo_review tool, and the triage report script.
126
126
  // One selection implementation everywhere, so what a dry run PRINTS is exactly
127
127
  // what a confirmed run DOES.
@@ -129,8 +129,12 @@ function formatFlags(l) {
129
129
  /** Server-enforced max decisions per bulk call; clients chunk at this size. */
130
130
  const BULK_CHUNK = 100;
131
131
 
132
- /** Default approve-clean quality threshold (repo quality gate: total >= 14/20). */
132
+ /** Default ready-to-publish quality threshold (repo quality gate: total >= 14/20). */
133
133
  const DEFAULT_QUALITY_THRESHOLD = 14;
134
+ const LANE_READY = 'ready_to_publish';
135
+ const LANE_NEEDS_SCORE = 'needs_score';
136
+ const LANE_NEEDS_EYES = 'needs_your_eyes';
137
+ const REVIEW_LANES = new Set([LANE_READY, LANE_NEEDS_SCORE, LANE_NEEDS_EYES]);
134
138
 
135
139
  /**
136
140
  * GET /account/pending/summary: compact triage rows + counts (no bodies).
@@ -233,46 +237,95 @@ function qualityClears(row, minQuality) {
233
237
  }
234
238
 
235
239
  /**
236
- * THE approve-clean selection. Operates on summary rows (screens_passed,
237
- * flags, quality) from GET /account/pending/summary.
240
+ * Resolve the server lane on a summary row. Old servers did not return lane;
241
+ * those rows fall back to the legacy screens+quality split and are marked as
242
+ * version-skewed so the caller can tell the operator.
238
243
  *
239
- * mode 'clean' (approve-clean): screens_passed AND quality >= minQuality.
240
- * mode 'all': every row EXCEPT screens-flagged ones unless includeFlagged
241
- * is set; no quality gate.
244
+ * @param {object} row
245
+ * @returns {{lane:string, version_skew:boolean}}
246
+ */
247
+ function reviewLane(row) {
248
+ if (row && REVIEW_LANES.has(row.lane)) {
249
+ return { lane: row.lane, version_skew: false };
250
+ }
251
+ if (!row || !row.screens_passed) {
252
+ return { lane: LANE_NEEDS_EYES, version_skew: true };
253
+ }
254
+ return {
255
+ lane: qualityClears(row, DEFAULT_QUALITY_THRESHOLD) ? LANE_READY : LANE_NEEDS_SCORE,
256
+ version_skew: true,
257
+ };
258
+ }
259
+
260
+ /**
261
+ * THE approve-ready selection. The server's lane is authoritative:
262
+ *
263
+ * - threshold 14 (default): exactly rows in ready_to_publish
264
+ * - threshold >14: narrows within ready_to_publish
265
+ * - threshold <14: includes qualifying needs_score rows too
266
+ * - needs_your_eyes is excluded unless mode=all + includeFlagged
242
267
  *
243
268
  * Returns the selection plus what was excluded and why, so every confirmation
244
269
  * surface can show the operator the exact consequences before anything runs.
245
270
  *
246
271
  * @param {Array<object>} rows
247
- * @param {object} [opts] { mode: 'clean'|'all', minQuality, includeFlagged }
248
- * @returns {{selected:Array<object>, excluded_flagged:Array<object>, excluded_low_quality:Array<object>, excluded_unscored:Array<object>, min_quality:number|null}}
272
+ * @param {object} [opts] { mode: 'ready'|'clean'|'all', minQuality, includeFlagged }
273
+ * @returns {{selected:Array<object>, included_beyond_verdict:Array<object>, excluded_flagged:Array<object>, excluded_low_quality:Array<object>, excluded_unscored:Array<object>, min_quality:number|null, version_skew:boolean}}
249
274
  */
250
275
  function selectForBulkApprove(rows, opts = {}) {
251
- const mode = opts.mode === 'all' ? 'all' : 'clean';
252
- const minQuality = mode === 'clean'
276
+ const mode = opts.mode === 'all' ? 'all' : 'ready';
277
+ const minQuality = mode === 'ready'
253
278
  ? (Number.isFinite(opts.minQuality) ? opts.minQuality : DEFAULT_QUALITY_THRESHOLD)
254
279
  : null;
255
280
  const includeFlagged = mode === 'all' && opts.includeFlagged === true;
256
281
 
257
282
  const selected = [];
283
+ const included_beyond_verdict = [];
258
284
  const excluded_flagged = [];
259
285
  const excluded_low_quality = [];
260
286
  const excluded_unscored = [];
287
+ let version_skew = false;
261
288
 
262
289
  for (const row of rows || []) {
263
290
  if (!row || !row.id) continue;
264
- if (!row.screens_passed && !includeFlagged) {
291
+ const resolved = reviewLane(row);
292
+ version_skew = version_skew || resolved.version_skew;
293
+
294
+ if (resolved.lane === LANE_NEEDS_EYES && !includeFlagged) {
265
295
  excluded_flagged.push(row);
266
296
  continue;
267
297
  }
268
- if (mode === 'clean' && !qualityClears(row, minQuality)) {
269
- (row.quality == null ? excluded_unscored : excluded_low_quality).push(row);
270
- continue;
298
+
299
+ if (mode === 'ready') {
300
+ if (resolved.lane === LANE_READY) {
301
+ // At the server floor, consume the verdict verbatim. A stricter
302
+ // operator threshold may narrow it, but never broaden it.
303
+ if (minQuality > DEFAULT_QUALITY_THRESHOLD && !qualityClears(row, minQuality)) {
304
+ (row.quality == null ? excluded_unscored : excluded_low_quality).push(row);
305
+ continue;
306
+ }
307
+ } else if (resolved.lane === LANE_NEEDS_SCORE) {
308
+ if (minQuality < DEFAULT_QUALITY_THRESHOLD && qualityClears(row, minQuality)) {
309
+ included_beyond_verdict.push(row);
310
+ } else {
311
+ (row.quality == null ? excluded_unscored : excluded_low_quality).push(row);
312
+ continue;
313
+ }
314
+ }
271
315
  }
316
+
272
317
  selected.push(row);
273
318
  }
274
319
 
275
- return { selected, excluded_flagged, excluded_low_quality, excluded_unscored, min_quality: minQuality };
320
+ return {
321
+ selected,
322
+ included_beyond_verdict,
323
+ excluded_flagged,
324
+ excluded_low_quality,
325
+ excluded_unscored,
326
+ min_quality: minQuality,
327
+ version_skew,
328
+ };
276
329
  }
277
330
 
278
331
  module.exports = {
@@ -287,5 +340,6 @@ module.exports = {
287
340
  chunkDecisions,
288
341
  submitBulkChunked,
289
342
  qualityClears,
343
+ reviewLane,
290
344
  selectForBulkApprove,
291
345
  };
package/mcp-server.js CHANGED
@@ -160,7 +160,7 @@ async function postBulkChunks(headers, decisions) {
160
160
  }
161
161
 
162
162
  const server = new Server(
163
- { name: 'auxilo', version: '0.9.4' },
163
+ { name: 'auxilo', version: '0.9.7' },
164
164
  {
165
165
  capabilities: { tools: {} },
166
166
  instructions: `You are connected to Auxilo, a knowledge marketplace where AI agents buy and sell operational learnings.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auxilo-mcp",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "mcpName": "io.github.silent-architects/auxilo",
5
5
  "description": "MCP server for Auxilo. Your agent stops solving the same problem twice: auto-extracted learnings, free self-unlocks, and earnings when other agents unlock yours.",
6
6
  "main": "mcp-server.js",
@@ -13,6 +13,7 @@
13
13
  "bin/",
14
14
  "lib/installer.js",
15
15
  "lib/review.js",
16
+ "lib/hook-status.js",
16
17
  "lib/sensitivity-filter.js",
17
18
  "lib/extraction-index.js",
18
19
  "lib/similarity.js",
package/scripts/runner.js CHANGED
@@ -39,6 +39,7 @@ const crypto = require('crypto');
39
39
  const { spawn } = require('child_process');
40
40
  const { scanText, SENSITIVITY_FILTER_VERSION } = require('../lib/sensitivity-filter.js');
41
41
  const { appendSubmittedLearning } = require('../lib/extraction-index.js');
42
+ const { hasAuxiloSessionEndHook } = require('../lib/hook-status.js');
42
43
  const { TranscriptSource } = require('./sources/source.interface.js');
43
44
  const { GenericJsonlSource } = require('./sources/generic-jsonl.js');
44
45
 
@@ -516,6 +517,7 @@ function sweeperManifest(repoRoot = path.resolve(__dirname, '..')) {
516
517
  ['lib/sensitivity-filter.js', 'lib/sensitivity-filter.js', 0o644],
517
518
  ['lib/extraction-index.js', 'lib/extraction-index.js', 0o644],
518
519
  ['lib/similarity.js', 'lib/similarity.js', 0o644],
520
+ ['lib/hook-status.js', 'lib/hook-status.js', 0o644],
519
521
  ['config/near-duplicate.json', 'config/near-duplicate.json', 0o644],
520
522
  // Client-side extraction (2026-07-02) — required by the sweep path since /extract went 410.
521
523
  // Missing from this manifest until 2026-07-19: installed sweepers crashed with
@@ -711,10 +713,10 @@ async function printStatus() {
711
713
  let hookInstalled = false;
712
714
  try {
713
715
  const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf-8'));
714
- hookInstalled = Array.isArray(settings.hooks?.SessionEnd) &&
715
- settings.hooks.SessionEnd.some(h => h.includes('auxilo-extract'));
716
+ hookInstalled = hasAuxiloSessionEndHook(settings.hooks?.SessionEnd);
716
717
  } catch { /* no settings file */ }
717
718
  console.log(`Hook installed: ${hookInstalled ? 'yes' : 'no'}`);
719
+ console.log(`Settings inspected: ${claudeSettingsPath}`);
718
720
 
719
721
  // 5. Last sweep ran at
720
722
  console.log(`Last sweep: ${ledger.lastSweep || 'never'}`);