auxilo-mcp 0.9.2 → 0.9.4

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/mcp-server.js CHANGED
@@ -160,14 +160,16 @@ async function postBulkChunks(headers, decisions) {
160
160
  }
161
161
 
162
162
  const server = new Server(
163
- { name: 'auxilo', version: '0.9.2' },
163
+ { name: 'auxilo', version: '0.9.4' },
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.
167
167
 
168
168
  SEARCH BEFORE YOU DEBUG: When you encounter a non-trivial problem — an unfamiliar API, an unexpected error, an integration challenge — search Auxilo FIRST using auxilo_knowledge before spending tokens on trial-and-error. A free search that returns one verified answer saves minutes of debugging and dollars in API calls. Rule: if your first attempt fails, search Auxilo before trying a second approach.
169
169
 
170
- CONTRIBUTE AFTER YOU SOLVE: When you solve a non-trivial problem through debugging, experimentation, or combining information from multiple sources, submit the learning to Auxilo using auxilo_contribute. This includes: workarounds for undocumented behavior, edge cases, subtle bugs, API quirks, and solutions that required multiple attempts to find. Do NOT submit trivial lookups or well-documented standard approaches.
170
+ CONTRIBUTE AFTER YOU SOLVE: When you solve a non-trivial TECHNICAL problem through debugging, experimentation, or combining information from multiple sources, submit the learning to Auxilo using auxilo_contribute. This includes: workarounds for undocumented behavior, edge cases, subtle bugs, API quirks, and solutions that required multiple attempts to find. Do NOT submit trivial lookups or well-documented standard approaches.
171
+
172
+ TECHNICAL SCOPE (hard rule — the server refuses anything else): Auxilo accepts TECHNICAL learnings only — APIs, developer tools, code, infrastructure, data pipelines, monitoring, payment/crypto technology, debugging. Never submit interpersonal or communication strategy, copywriting/content/marketing insights, business or negotiation strategy, personal matters, or creative-writing technique. Categories: data-processing, web-interaction, code-execution, storage-state, payment-financial, monitoring. A technical learning about a messaging/email/notification API belongs under web-interaction or code-execution; content/data pipeline tech belongs under data-processing. SYSTEM-FACT TEST: submit ONLY when a system and a symptom are at the core — an error, an undocumented limitation, a reproducible behavior of an external tool/API/OS. Advice about how to work (process, workflow, methodology, decision practice) is not a learning and is held for review, regardless of quality score.
171
173
 
172
174
  QUALITY GATE: Before submitting, self-assess on four dimensions (1-5 each): Specificity, Actionability, Novelty, Completeness. Only submit if total >= 14/20 and no single dimension is below 3.
173
175
 
@@ -234,19 +236,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
234
236
  },
235
237
  {
236
238
  name: 'auxilo_contribute',
237
- description: 'Submit operational knowledge you discovered during this session to the Auxilo marketplace. WHEN TO USE: Call this AFTER you solve a non-trivial problem that required debugging, experimentation, or combining information from multiple sources. Call it when you discover undocumented behavior, a workaround, or a subtle edge case. Do NOT call it for trivial lookups or standard documentation answers. QUALITY GATE: Self-assess on Specificity, Actionability, Novelty, Completeness (1-5 each) and ALWAYS include your scores in quality_self_assessment — a submission WITHOUT it is held for manual review instead of publishing seamlessly. Only submit if total >= 14/20, no dimension below 3 (the server quarantines below-floor submissions for review). DEDUP: Search auxilo_knowledge first to avoid duplicates. SENSITIVITY (mandatory self-screen): never include secrets, credentials, API keys, PII, private filesystem paths, or proprietary/client business content — generalize to placeholders or omit; this is a PUBLIC marketplace. PRICING: Leave unlock_price unset to let the dynamic pricing engine calculate automatically (recommended). If setting manually: $0.05-$0.10 common techniques, $0.10-$1.00 specific solutions, $1.00-$10.00 novel discoveries, $10.00-$50.00 breakthroughs. Minimum $0.05, maximum $50.00. Free to submit — you earn 70% when others unlock. If the result is status pending_review, follow its how_to_review instructions (self-approval via `auxilo review`, the dashboard queue, or GET /account/pending).',
239
+ description: 'Submit TECHNICAL operational knowledge you discovered during this session to the Auxilo marketplace. WHEN TO USE: Call this AFTER you solve a non-trivial TECHNICAL problem that required debugging, experimentation, or combining information from multiple sources. Call it when you discover undocumented behavior, a workaround, or a subtle edge case. Do NOT call it for trivial lookups or standard documentation answers. SCOPE (hard rule — the server refuses out-of-scope submissions with CATEGORY_OUT_OF_SCOPE): technical learnings ONLY — APIs, developer tools, code, infrastructure, data pipelines, monitoring, payment/crypto technology, debugging. NEVER submit interpersonal/communication strategy, copywriting/content/marketing insights, business or negotiation strategy, personal matters, or creative-writing technique. A technical learning about a messaging/email/notification API belongs under web-interaction or code-execution; content/data pipeline tech belongs under data-processing. QUALITY GATE: Self-assess on Specificity, Actionability, Novelty, Completeness (1-5 each) and ALWAYS include your scores in quality_self_assessment — a submission WITHOUT it is held for manual review instead of publishing seamlessly. Only submit if total >= 14/20, no dimension below 3 (the server quarantines below-floor submissions for review). DEDUP: Search auxilo_knowledge first to avoid duplicates. SENSITIVITY (mandatory self-screen): never include secrets, credentials, API keys, PII, private filesystem paths, or proprietary/client business content — generalize to placeholders or omit; this is a PUBLIC marketplace. PRICING: Leave unlock_price unset to let the dynamic pricing engine calculate automatically (recommended). If setting manually: $0.05-$0.10 common techniques, $0.10-$1.00 specific solutions, $1.00-$10.00 novel discoveries, $10.00-$50.00 breakthroughs. Minimum $0.05, maximum $50.00. Free to submit — you earn 70% when others unlock. If the result is status pending_review, follow its how_to_review instructions (self-approval via `auxilo review`, the dashboard queue, or GET /account/pending).',
238
240
  inputSchema: {
239
241
  type: 'object',
240
242
  properties: {
241
243
  title: { type: 'string', description: 'Concise title (min 10 chars)' },
242
244
  body: { type: 'string', description: 'Detailed explanation — what you tried, what worked, what failed (min 50 chars)' },
243
- category: { type: 'string', enum: ['data-processing', 'web-interaction', 'code-execution', 'communication', 'storage-state', 'content-generation', 'payment-financial', 'monitoring'] },
245
+ // CI-5: technical-only taxonomy — 'communication' and 'content-generation' retired.
246
+ category: { type: 'string', enum: ['data-processing', 'web-interaction', 'code-execution', 'storage-state', 'payment-financial', 'monitoring'] },
244
247
  tags: { type: 'array', items: { type: 'string' }, description: 'Relevant keywords' },
245
248
  task_context: { type: 'string', description: 'What task were you performing?' },
246
249
  outcome: { type: 'string', enum: ['success', 'partial', 'failure', 'workaround'] },
247
250
  quality_self_assessment: {
248
251
  type: 'object',
249
- description: 'Your quality self-assessment. ALWAYS include this — without it the submission is held for manual review. Score each dimension 1-5; total MUST equal their sum (server-verified). Submissions below the floor (total < 14 or any dimension < 3) are quarantined for review rather than published.',
252
+ description: 'Your quality self-assessment. ALWAYS include this — without it the submission is held for manual review. Score each dimension 1-5; total MUST equal their sum (server-verified). Submissions below the floor (total < 14 or any dimension < 3) are quarantined for review rather than published. High scores REQUIRE a system+symptom anchor — a named external system and a concrete error/limitation/reproducible behavior an agent would search mid-task; process or workflow advice cannot score high no matter how polished (the server holds it for review as process_advice).',
250
253
  properties: {
251
254
  specificity: { type: 'integer', minimum: 1, maximum: 5, description: 'How precise and detailed? (1-5)' },
252
255
  actionability: { type: 'integer', minimum: 1, maximum: 5, description: 'Can another agent directly use this? (1-5)' },
@@ -272,7 +275,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
272
275
  type: 'object',
273
276
  properties: {
274
277
  query: { type: 'string', description: 'What you need help with' },
275
- category: { type: 'string', enum: ['data-processing', 'web-interaction', 'code-execution', 'communication', 'storage-state', 'content-generation', 'payment-financial', 'monitoring'] },
278
+ // CI-5: learnings are technical-only; retired labels match nothing post-migration.
279
+ category: { type: 'string', enum: ['data-processing', 'web-interaction', 'code-execution', 'storage-state', 'payment-financial', 'monitoring'] },
276
280
  outcome: { type: 'string', enum: ['success', 'partial', 'failure', 'workaround'] },
277
281
  related_skill: { type: 'string', description: 'Filter by Auxilo skill ID' },
278
282
  limit: { type: 'number', description: 'Max results (default 5, max 15)' },
@@ -283,7 +287,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
283
287
  },
284
288
  {
285
289
  name: 'auxilo_unlock',
286
- description: 'Unlock full learning content by ID. Price is set by the contributor (min $0.05 USDC). 70% of the amount you pay goes to the contributor who shared this knowledge. Check unlock_price_usd in search results to see the cost before unlocking.',
290
+ description: 'Unlock full learning content by ID. Price is set by the contributor (min $0.05 USDC). 70% of the amount you pay goes to the contributor who shared this knowledge. Check unlock_price_usd in search results to see the cost before unlocking. YOUR OWN learnings are $0: anything this account contributed (or that was contributed under a wallet linked to this account) comes back free, with no credit deducted and no earnings movement.',
287
291
  inputSchema: {
288
292
  type: 'object',
289
293
  properties: {
@@ -295,7 +299,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
295
299
  },
296
300
  {
297
301
  name: 'auxilo_rate',
298
- description: 'Rate a learning 1-5 after using it. WHEN TO USE: After you unlock and apply knowledge from auxilo_unlock, always come back and rate it. Your rating helps other agents find the best knowledge and deprioritizes low-quality submissions. This is how the marketplace stays useful. Free.',
302
+ description: 'Rate a learning 1-5 after using it. WHEN TO USE: After you unlock and apply knowledge from auxilo_unlock, always come back and rate it. Your rating helps other agents find the best knowledge and deprioritizes low-quality submissions. This is how the marketplace stays useful. Free. One rating slot per account per learning: re-rating REPLACES your prior score, it never counts twice (CH-6). REQUIRES: your API key (run `npx auxilo setup` if unset) and a prior unlock of this learning by your account — only verified purchasers can rate (LW-7).',
299
303
  inputSchema: {
300
304
  type: 'object',
301
305
  properties: {
@@ -390,27 +394,27 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
390
394
  },
391
395
  {
392
396
  name: 'auxilo_review',
393
- description: 'Review YOUR OWN pending-review learnings (from background extraction) so they can be approved to the public marketplace or rejected to stay private. Account-scoped: only the authenticated account\'s own pending items are ever visible or affected. ACTIONS: "list" returns the triage summary (counts + compact rows with quality score and platform screen verdicts: injection, content sensitivity, near-duplicate). "approve" / "reject" apply explicit decisions to the ids you pass (the operator must have named or confirmed these items). "approve_clean" selects every item that passed ALL platform screens AND has quality >= min_quality (default 14/20); it is DRY-RUN BY DEFAULT and returns exactly what WOULD be approved. CONSENT CONTRACT: nothing goes public without the contributor\'s explicit approval. So before executing approve_clean you MUST show the operator the dry-run list and count and get their confirmation, then call again with dry_run:false, confirm:true, and expected_count set to the dry-run count. The server also enforces a counted-confirmation gate on every bulk call. Requires your configured API key (or session_token).',
397
+ description: 'Review YOUR OWN pending-review learnings (from background extraction) so they can be approved to the public marketplace or rejected to stay private. Account-scoped: only the authenticated account\'s own pending items are ever visible or affected. ACTIONS: "list" returns the triage summary (counts incl. by_signal + compact rows with quality score, lane, a one-sentence why for flagged items, and platform screen verdicts: injection, content sensitivity, near-duplicate). "approve" / "reject" apply explicit decisions to the ids you pass (the operator must have named or confirmed these items). "approve_clean" selects every item that passed ALL platform screens AND has quality >= min_quality (default 14/20); it is DRY-RUN BY DEFAULT and returns exactly what WOULD be approved. "reject_by_signal" bulk-rejects every pending item carrying one flag signal (e.g. social_handle) — REJECT ONLY (items stay private; there is deliberately no bulk approve by class): the operator must confirm the signal AND its by_signal count from "list", and you pass that count as expected_count — the server refuses if the live selection differs. "sanitize" resubmits ONE operator-corrected item through EVERY screen with lineage (the original is retired to private, reason sanitize-resubmit; the replacement is ALWAYS held for the operator\'s explicit approval — never auto-published): only call it with a correction the operator reviewed. CONSENT CONTRACT: nothing goes public without the contributor\'s explicit approval. So before executing approve_clean you MUST show the operator the dry-run list and count and get their confirmation, then call again with dry_run:false, confirm:true, and expected_count set to the dry-run count. The server also enforces a counted-confirmation gate on every bulk call. Requires your configured API key (or session_token).',
394
398
  inputSchema: {
395
399
  type: 'object',
396
400
  properties: {
397
- action: { type: 'string', enum: ['list', 'approve', 'reject', 'approve_clean'], description: 'What to do. Start with "list".' },
401
+ action: { type: 'string', enum: ['list', 'approve', 'reject', 'approve_clean', 'reject_by_signal', 'sanitize'], description: 'What to do. Start with "list".' },
398
402
  ids: { type: 'array', items: { type: 'string' }, description: 'Learning ids for action approve/reject. These must be items the operator explicitly chose.' },
399
- reason: { type: 'string', description: 'Optional rejection reason (action reject; max 500 chars).' },
403
+ reason: { type: 'string', description: 'Optional rejection reason (actions reject / reject_by_signal; max 500 chars).' },
404
+ signal: { type: 'string', description: 'reject_by_signal only. The flag signal to reject by (a name from counts.by_signal, e.g. social_handle, person_name, injection).' },
405
+ id: { type: 'string', description: 'sanitize only. The pending/rejected learning being corrected (operator-chosen).' },
406
+ title: { type: 'string', description: 'sanitize only. Corrected title (omit to keep the original).' },
407
+ body: { type: 'string', description: 'sanitize only. Corrected body (omit to keep the original). At least one of title/body is required.' },
408
+ tags: { type: 'array', items: { type: 'string' }, description: 'sanitize only. Corrected tags (omit to keep the original).' },
400
409
  dry_run: { type: 'boolean', description: 'approve_clean only. Default TRUE: report what would be approved without changing anything. Set false only together with confirm:true and expected_count after the operator confirmed the dry-run list.' },
401
410
  confirm: { type: 'boolean', description: 'approve_clean only. Must be exactly true to execute. Never set this without the operator\'s explicit go-ahead on the dry-run output.' },
402
- expected_count: { type: 'number', description: 'approve_clean execute only. The count from the dry run, echoed back. If the live selection differs (queue changed), nothing is approved and a fresh dry run is returned.' },
411
+ expected_count: { type: 'number', description: 'approve_clean execute + reject_by_signal. The count the operator confirmed (dry-run count / by_signal count), echoed back. If the live selection differs (queue changed), nothing is mutated.' },
403
412
  min_quality: { type: 'number', description: 'approve_clean quality threshold 0-20 (default 14). 0 includes unscored items.' },
404
413
  session_token: { type: 'string', description: 'Optional JWT session token from /auth/verify. If omitted, your configured API key authenticates the account.' },
405
414
  },
406
415
  required: ['action'],
407
416
  },
408
417
  },
409
- {
410
- name: 'get_stats',
411
- description: 'Get Auxilo registry statistics — catalog size, skill types, and query volume. Free.',
412
- inputSchema: { type: 'object', properties: {} },
413
- },
414
418
  {
415
419
  name: 'get_knowledge_stats',
416
420
  description: 'Get knowledge marketplace statistics — total learnings, unlocks, contributors, and top categories. Free.',
@@ -703,12 +707,52 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
703
707
  });
704
708
  }
705
709
 
706
- return text({ error: `Unknown action: ${args.action}. Use list, approve, reject, or approve_clean.` });
707
- }
710
+ if (args.action === 'reject_by_signal') {
711
+ // SPEC3 B2 (§5.2): server-side counted bulk reject by flag class.
712
+ // The operator confirms signal + by_signal count from {action:"list"};
713
+ // the server refuses (409, nothing mutated) if the live selection
714
+ // differs. Reject-only — the safe direction (items stay private).
715
+ if (typeof args.signal !== 'string' || !args.signal) {
716
+ return text({ error: 'reject_by_signal requires "signal" (a name from counts.by_signal — run {action:"list"} first).' });
717
+ }
718
+ if (!Number.isInteger(args.expected_count)) {
719
+ return text({ error: 'reject_by_signal requires expected_count: the by_signal count the operator confirmed. This is the counted-confirmation gate.' });
720
+ }
721
+ const resp = await fetch(`${AUXILO_BASE}/account/pending/reject-by-signal`, {
722
+ method: 'POST',
723
+ headers,
724
+ body: JSON.stringify({
725
+ signal: args.signal,
726
+ expected_count: args.expected_count,
727
+ ...(args.reason && { reason: args.reason }),
728
+ }),
729
+ });
730
+ return text(await resp.json());
731
+ }
708
732
 
709
- case 'get_stats': {
710
- const resp = await fetch(`${AUXILO_BASE}/stats`, { headers: baseHeaders() });
711
- return text(await resp.json());
733
+ if (args.action === 'sanitize') {
734
+ // SPEC3 B3 (§5.3): operator-corrected resubmission through EVERY
735
+ // screen, with lineage. The replacement is ALWAYS held for the
736
+ // operator's explicit approve — this action can never publish.
737
+ if (typeof args.id !== 'string' || !args.id) {
738
+ return text({ error: 'sanitize requires "id" (the pending/rejected learning the operator chose to correct).' });
739
+ }
740
+ if (args.title === undefined && args.body === undefined) {
741
+ return text({ error: 'sanitize requires at least one of "title" / "body" (the operator-reviewed correction).' });
742
+ }
743
+ const resp = await fetch(`${AUXILO_BASE}/account/pending/${encodeURIComponent(args.id)}/sanitize`, {
744
+ method: 'POST',
745
+ headers,
746
+ body: JSON.stringify({
747
+ ...(args.title !== undefined && { title: args.title }),
748
+ ...(args.body !== undefined && { body: args.body }),
749
+ ...(args.tags !== undefined && { tags: args.tags }),
750
+ }),
751
+ });
752
+ return text(await resp.json());
753
+ }
754
+
755
+ return text({ error: `Unknown action: ${args.action}. Use list, approve, reject, approve_clean, reject_by_signal, or sanitize.` });
712
756
  }
713
757
 
714
758
  case 'get_knowledge_stats': {
@@ -736,12 +780,12 @@ if (require.main !== module) {
736
780
  return;
737
781
  }
738
782
 
739
- // ─── CLI delegation (LW-17) ────────────────────────────────────────────────────
740
- // `npx auxilo-mcp setup|status|review|disable` runs the full turnkey CLI.
783
+ // ─── CLI delegation (LW-17; init added Wave 3.4/NF-3) ─────────────────────────
784
+ // `npx auxilo-mcp setup|init|status|review|disable` runs the full turnkey CLI.
741
785
  // npx resolves PACKAGE names, not bin aliases — the documented `npx auxilo
742
786
  // setup` 404s until the `auxilo` npm package name is claimed, so the package
743
787
  // bin must handle these commands itself.
744
- if (['setup', 'status', 'review', 'disable'].includes(process.argv[2])) {
788
+ if (['setup', 'init', 'status', 'review', 'disable'].includes(process.argv[2])) {
745
789
  require('./bin/auxilo-cli.js').run();
746
790
  return; // module-level return in CommonJS stops the MCP server from starting
747
791
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auxilo-mcp",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
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",
@@ -17,6 +17,7 @@
17
17
  "scripts/runner.js",
18
18
  "scripts/capture-core.js",
19
19
  "scripts/extract-local.js",
20
+ "scripts/review-notice.js",
20
21
  "scripts/sources/",
21
22
  "scripts/hooks/auxilo-extract.sh",
22
23
  "README.md",
@@ -24,7 +25,7 @@
24
25
  ],
25
26
  "scripts": {
26
27
  "start": "node mcp-server.js",
27
- "test": "node --test --test-force-exit test/*.test.js"
28
+ "test": "node --test test/*.test.js && node tests/test-mobile-nav-overlay.js"
28
29
  },
29
30
  "keywords": [
30
31
  "mcp",
@@ -49,11 +50,14 @@
49
50
  "homepage": "https://auxilo.io",
50
51
  "license": "MIT",
51
52
  "dependencies": {
53
+ "@aws-sdk/client-s3": "^3.1090.0",
54
+ "@aws-sdk/lib-storage": "^3.1090.0",
52
55
  "@modelcontextprotocol/sdk": "^1.0.0",
53
56
  "stripe": "^17.0.0",
54
57
  "viem": "^2.46.3"
55
58
  },
56
59
  "devDependencies": {
60
+ "playwright": "1.61.1",
57
61
  "proxyquire": "^2.1.3"
58
62
  }
59
- }
63
+ }
@@ -17,12 +17,40 @@ const fs = require('fs');
17
17
  const path = require('path');
18
18
  const os = require('os');
19
19
 
20
- const CATEGORIES = ['data-processing', 'web-interaction', 'code-execution', 'communication', 'storage-state', 'content-generation', 'payment-financial', 'monitoring'];
20
+ // CI-5 (PUNCH-LIST §30, 2026-07-19): Auxilo is TECHNICAL-ONLY. The learning
21
+ // taxonomy is these six tech categories; `communication` and `content-generation`
22
+ // are RETIRED — the server 400s them (CATEGORY_OUT_OF_SCOPE) and this extractor
23
+ // must never emit them. This file ships standalone in the npm package, so the
24
+ // lists are duplicated here from lib/category-scope-migration.js (server truth);
25
+ // test/ci5-scope-enforcement.test.js pins the copies equal.
26
+ const CATEGORIES = ['data-processing', 'web-interaction', 'code-execution', 'storage-state', 'payment-financial', 'monitoring'];
27
+ const RETIRED_CATEGORIES = ['communication', 'content-generation'];
21
28
 
22
- const EXTRACTION_PROMPT = `You are extracting reusable OPERATIONAL LEARNINGS from an AI agent's session transcript, to publish to a PUBLIC knowledge marketplace read by other AI agents.
29
+ /**
30
+ * SPEC3 slice A1 gate — score-at-extraction, BUILT BUT DARK by default.
31
+ *
32
+ * ┌─ CRITICAL SEQUENCING CONSTRAINT (SPEC3-BUILDER-REVIEW-LOOP §3.1/§8) ──────┐
33
+ * │ Under a server WITHOUT the B1 extraction-channel hold, a clean /learn │
34
+ * │ submission carrying a floor-passing quality_self_assessment publishes │
35
+ * │ IMMEDIATELY (seamlessEligible). Turning this gate on against such a │
36
+ * │ server silently flips hook extraction from "everything held" to "clean │
37
+ * │ items auto-publish" — an unrecorded consent change (2026-06-10 class). │
38
+ * │ Do NOT set AUXILO_SCORE_EXTRACTION=1 until the server holds │
39
+ * │ submission_channel:'extraction' items behind standing consent (B1). │
40
+ * └───────────────────────────────────────────────────────────────────────────┘
41
+ */
42
+ function scoreExtractionEnabled(env = process.env) {
43
+ return env.AUXILO_SCORE_EXTRACTION === '1';
44
+ }
45
+
46
+ const EXTRACTION_PROMPT_BASE = `You are extracting reusable OPERATIONAL LEARNINGS from an AI agent's session transcript, to publish to a PUBLIC knowledge marketplace read by other AI agents.
23
47
 
24
48
  Extract 0 to 5 GENUINE learnings: non-obvious solutions, workarounds, API quirks, error root-causes, integration gotchas — the kind of thing that cost real debugging or combined multiple sources. SKIP trivial lookups, well-documented standard approaches, opinions, and conversation.
25
49
 
50
+ HARD SCOPE RULE — TECHNICAL LEARNINGS ONLY (the marketplace accepts nothing else): extract ONLY technical/operational learnings — APIs, developer tools, code, infrastructure, data pipelines, monitoring/observability, payment/crypto TECHNOLOGY, debugging. NEVER extract interpersonal or communication strategy, copywriting/content/marketing insights, business or negotiation strategy, personal matters, or creative-writing technique — DROP such candidates entirely, do not relabel them. A technical learning about a messaging/email/notification API belongs under "web-interaction" or "code-execution"; content/data pipeline TECH belongs under "data-processing".
51
+
52
+ SYSTEM-FACT TEST (CI-7): Extract ONLY when a system and a symptom are at the core — an error, an undocumented limitation, a reproducible behavior of an external tool/API/OS. If the candidate is advice about how to work (process, workflow, methodology, decision practice), do NOT extract it. "Odesli cannot resolve Tidal artist URLs" is a learning; "use a two-phase consultation workflow" is not, no matter how well it would score.
53
+
26
54
  MANDATORY SENSITIVITY SELF-SCREEN (the marketplace is PUBLIC): NEVER include secrets, credentials, API keys, tokens, private keys, or seed phrases; personal data (real people's names, emails, phone numbers, wallet addresses); private filesystem paths, internal hostnames, or infrastructure identifiers; proprietary, confidential, or client-specific business content. Rewrite specifics into generic placeholders (/Users/USER/..., API_KEY, "a client") or omit them. If a learning cannot be generalized without leaking private material, DROP it entirely.
27
55
 
28
56
  Output STRICT JSON ONLY — an array (possibly empty []) of objects with these keys:
@@ -31,12 +59,39 @@ Output STRICT JSON ONLY — an array (possibly empty []) of objects with these k
31
59
  "category": one of ${JSON.stringify(CATEGORIES)}
32
60
  "tags": array of lowercase keyword strings
33
61
  "task_context": one sentence describing the task
34
- "outcome": one of "success","partial","failure","workaround"
62
+ "outcome": one of "success","partial","failure","workaround"`;
63
+
64
+ /** A1: rubric addendum — appended ONLY when scoreExtractionEnabled(). */
65
+ const QUALITY_RUBRIC_ADDENDUM = `
66
+ "quality_self_assessment": an object scoring the learning honestly on four
67
+ dimensions, each an INTEGER 1-5: "specificity" (precise and detailed, not
68
+ vague), "actionability" (another agent can directly use it), "novelty"
69
+ (non-obvious; an LLM would likely get it wrong), "completeness" (context,
70
+ reproduction steps, caveats), plus "total" (the exact sum of the four).
71
+ A learning worth publishing scores at least 14/20 with no dimension below 3.
72
+ High scores REQUIRE a system+symptom anchor — a named external system and a
73
+ concrete error/limitation/behavior; process or workflow advice cannot score
74
+ high no matter how polished (CI-7 system-fact test).
75
+ If a learning honestly scores below that bar, DROP it from the array rather
76
+ than inflating the numbers.`;
77
+
78
+ const PROMPT_SUFFIX = `
35
79
  No prose, no explanation, no markdown code fences — just the raw JSON array.
36
80
 
37
81
  TRANSCRIPT:
38
82
  `;
39
83
 
84
+ /** Build the extraction prompt for the current (or injected) gate state. */
85
+ function buildExtractionPrompt(opts = {}) {
86
+ const withScore = opts.scoreExtraction !== undefined
87
+ ? Boolean(opts.scoreExtraction)
88
+ : scoreExtractionEnabled();
89
+ return EXTRACTION_PROMPT_BASE + (withScore ? QUALITY_RUBRIC_ADDENDUM : '') + PROMPT_SUFFIX;
90
+ }
91
+
92
+ /** Back-compat export: the default (gate-evaluated-at-call) prompt. */
93
+ const EXTRACTION_PROMPT = buildExtractionPrompt({ scoreExtraction: false });
94
+
40
95
  /** Resolve the `claude` binary — hook/launchd env may have a minimal PATH. */
41
96
  function resolveClaudeBin() {
42
97
  const candidates = [
@@ -63,7 +118,7 @@ function resolveClaudeBin() {
63
118
  */
64
119
  function extractWithClaudeCode(transcript) {
65
120
  const bin = resolveClaudeBin();
66
- const input = EXTRACTION_PROMPT + String(transcript).slice(0, 200000);
121
+ const input = buildExtractionPrompt() + String(transcript).slice(0, 200000);
67
122
  // Do NOT pass ANTHROPIC_API_KEY through — we want the user's logged-in Claude
68
123
  // subscription (OAuth), not an API key (which would bill someone). Delete it.
69
124
  const childEnv = { ...process.env, AUXILO_EXTRACTING: '1' };
@@ -79,8 +134,41 @@ function extractWithClaudeCode(transcript) {
79
134
  return { ok: true, out, reason: null };
80
135
  }
81
136
 
82
- /** Defensively parse a JSON array of learnings out of model output. */
83
- function parseLearnings(raw) {
137
+ /**
138
+ * A1: validate a model-emitted quality_self_assessment. Returns the normalized
139
+ * object, or null when malformed/missing — the caller then OMITS the field
140
+ * entirely (never fabricate; the server's `awaiting_quality` hold is the
141
+ * correct fallback and is deliberately not a 400 — AUD19-6 quarantines rather
142
+ * than bounces).
143
+ */
144
+ const QUALITY_DIMENSIONS = ['specificity', 'actionability', 'novelty', 'completeness'];
145
+ function validateQualityAssessment(qa) {
146
+ if (!qa || typeof qa !== 'object' || Array.isArray(qa)) return null;
147
+ const out = {};
148
+ let sum = 0;
149
+ for (const dim of QUALITY_DIMENSIONS) {
150
+ const v = qa[dim];
151
+ if (!Number.isInteger(v) || v < 1 || v > 5) return null;
152
+ out[dim] = v;
153
+ sum += v;
154
+ }
155
+ if (!Number.isInteger(qa.total) || qa.total !== sum) return null;
156
+ out.total = sum;
157
+ return out;
158
+ }
159
+
160
+ /**
161
+ * Defensively parse a JSON array of learnings out of model output.
162
+ * A1: `quality_self_assessment` is attached ONLY when (a) the score gate is on
163
+ * (opts.scoreExtraction, default = env AUXILO_SCORE_EXTRACTION) AND (b) the
164
+ * assessment validates. Gate OFF strips the field even if the model emits one,
165
+ * so a dark 0.9.3 client can never arm seamless publish (see the sequencing
166
+ * constraint at scoreExtractionEnabled).
167
+ */
168
+ function parseLearnings(raw, opts = {}) {
169
+ const withScore = opts.scoreExtraction !== undefined
170
+ ? Boolean(opts.scoreExtraction)
171
+ : scoreExtractionEnabled();
84
172
  let s = String(raw || '').trim();
85
173
  const fence = s.match(/```(?:json)?\s*([\s\S]*?)```/i);
86
174
  if (fence) s = fence[1].trim();
@@ -90,16 +178,35 @@ function parseLearnings(raw) {
90
178
  let arr;
91
179
  try { arr = JSON.parse(s.slice(start, end + 1)); } catch (_) { return []; }
92
180
  if (!Array.isArray(arr)) return [];
93
- return arr
94
- .filter(l => l && typeof l.title === 'string' && typeof l.body === 'string' && l.title.length >= 10 && l.body.length >= 50)
95
- .map(l => ({
96
- title: l.title,
97
- body: l.body,
98
- category: CATEGORIES.includes(l.category) ? l.category : 'code-execution',
99
- tags: Array.isArray(l.tags) ? l.tags.slice(0, 8).map(String) : [],
100
- task_context: typeof l.task_context === 'string' ? l.task_context : '',
101
- outcome: ['success', 'partial', 'failure', 'workaround'].includes(l.outcome) ? l.outcome : 'success',
102
- }));
181
+ const shaped = arr
182
+ .filter(l => l && typeof l.title === 'string' && typeof l.body === 'string' && l.title.length >= 10 && l.body.length >= 50);
183
+ // CI-5 post-parse scope validation (defense-in-depth against prompt drift):
184
+ // a candidate whose category is outside the tech set is DROPPED entirely.
185
+ // The old coerce-unknown-to-'code-execution' fallback is gone — coercion
186
+ // would launder a non-tech candidate (e.g. one the model labeled
187
+ // 'communication') into the catalog wearing a tech label. Category-based,
188
+ // so it applies identically in BOTH score-gate states.
189
+ const inScope = shaped.filter(l => CATEGORIES.includes(l.category));
190
+ // Gate-A F5: make the drop observable — count to stderr (never stdout; the
191
+ // hook log captures it) so a silently over-dropping prompt is diagnosable.
192
+ const dropped = shaped.length - inScope.length;
193
+ if (dropped > 0) console.error(`[extract-local] dropped ${dropped} candidate(s) outside the technical category set (CI-5 scope)`);
194
+ return inScope
195
+ .map(l => {
196
+ const out = {
197
+ title: l.title,
198
+ body: l.body,
199
+ category: l.category,
200
+ tags: Array.isArray(l.tags) ? l.tags.slice(0, 8).map(String) : [],
201
+ task_context: typeof l.task_context === 'string' ? l.task_context : '',
202
+ outcome: ['success', 'partial', 'failure', 'workaround'].includes(l.outcome) ? l.outcome : 'success',
203
+ };
204
+ if (withScore) {
205
+ const qa = validateQualityAssessment(l.quality_self_assessment);
206
+ if (qa) out.quality_self_assessment = qa;
207
+ }
208
+ return out;
209
+ });
103
210
  }
104
211
 
105
212
  /**
@@ -116,4 +223,8 @@ async function extractLocally(transcript, sourceType) {
116
223
  return { learnings: parseLearnings(out) };
117
224
  }
118
225
 
119
- module.exports = { extractLocally, parseLearnings, resolveClaudeBin, EXTRACTION_PROMPT, CATEGORIES };
226
+ module.exports = {
227
+ extractLocally, parseLearnings, resolveClaudeBin, CATEGORIES, RETIRED_CATEGORIES,
228
+ EXTRACTION_PROMPT, buildExtractionPrompt, scoreExtractionEnabled,
229
+ validateQualityAssessment, QUALITY_DIMENSIONS,
230
+ };
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * scripts/review-notice.js — SessionStart held-count notice (LW-18 layer 1b)
5
+ *
6
+ * Invoked by the Claude Code SessionStart hook (structured entry written by
7
+ * `auxilo setup` → lib/installer.js registerClaudeCodeSessionStartNotice via
8
+ * the ~/.auxilo/bin/auxilo-review-notice.sh shim). Whatever this prints to
9
+ * stdout is added to the session's context — so the contract is strict:
10
+ *
11
+ * COUNT ONLY. Never titles, never bodies, never flags. Pending bodies are
12
+ * adversarial by assumption (some are held BECAUSE injection-flagged —
13
+ * LW-18 threat model); surfacing content to a live agent at session start
14
+ * would be a confused-deputy channel. One count + two command names, that
15
+ * is all.
16
+ *
17
+ * Behavior:
18
+ * - FAIL-SILENT: every non-happy path exits 0 with NO output. A broken
19
+ * notice must never break (or noise up) a session.
20
+ * - Recursion guard: AUXILO_EXTRACTING=1 (the headless `claude -p`
21
+ * extraction child fires SessionStart too) → silent exit.
22
+ * - Suppression: at most ONE notice per 4 hours, tracked in
23
+ * ~/.auxilo/review-notice-state.json (LW-18 spec).
24
+ * - Count source: GET /account/pending/summary with the credentials from
25
+ * ~/.auxilo/credentials.json; 3.5s abort so session start is never held
26
+ * hostage by a slow network.
27
+ *
28
+ * Self-contained (fs/path/os + global fetch) — ships in RUNNER_STACK to
29
+ * ~/.auxilo/bin/scripts/ and must not require anything outside that layout
30
+ * except node builtins.
31
+ *
32
+ * @module review-notice
33
+ */
34
+
35
+ 'use strict';
36
+
37
+ const fs = require('fs');
38
+ const path = require('path');
39
+ const os = require('os');
40
+
41
+ /** ≤ 1 notice per 4h (LW-18 spec). */
42
+ const NOTICE_SUPPRESSION_MS = 4 * 60 * 60 * 1000;
43
+
44
+ /** Network budget: SessionStart hooks block session start. */
45
+ const FETCH_TIMEOUT_MS = 3500;
46
+
47
+ function auxiloDir(homeDir) {
48
+ return path.join(homeDir || os.homedir(), '.auxilo');
49
+ }
50
+
51
+ function statePath(homeDir) {
52
+ return path.join(auxiloDir(homeDir), 'review-notice-state.json');
53
+ }
54
+
55
+ /** Read suppression state; malformed/absent → {} (fail-open to notifying). */
56
+ function readState(homeDir) {
57
+ try {
58
+ const s = JSON.parse(fs.readFileSync(statePath(homeDir), 'utf-8'));
59
+ return s && typeof s === 'object' ? s : {};
60
+ } catch {
61
+ return {};
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Pure suppression decision: notify iff no prior notice, or the last one is
67
+ * older than the suppression window (bad timestamps read as "no prior").
68
+ */
69
+ function shouldNotify(state, now = Date.now(), suppressionMs = NOTICE_SUPPRESSION_MS) {
70
+ const last = state && state.last_notice_at ? Date.parse(state.last_notice_at) : NaN;
71
+ if (!Number.isFinite(last)) return true;
72
+ return now - last >= suppressionMs;
73
+ }
74
+
75
+ /** Persist the suppression stamp (best-effort — failure must not throw). */
76
+ function writeState(homeDir, now = Date.now()) {
77
+ try {
78
+ const p = statePath(homeDir);
79
+ fs.mkdirSync(path.dirname(p), { recursive: true });
80
+ fs.writeFileSync(p, JSON.stringify({ last_notice_at: new Date(now).toISOString() }, null, 2) + '\n');
81
+ } catch { /* fail-silent */ }
82
+ }
83
+
84
+ /** The one line. Count only — see the header contract. */
85
+ function renderNotice(count) {
86
+ return `Auxilo: ${count} learning(s) held for your review — run auxilo_review (MCP) or \`npx auxilo review\`.`;
87
+ }
88
+
89
+ /** Load credentials; null when absent/malformed/keyless. */
90
+ function readCredentials(homeDir) {
91
+ try {
92
+ const creds = JSON.parse(fs.readFileSync(path.join(auxiloDir(homeDir), 'credentials.json'), 'utf-8'));
93
+ return creds && typeof creds === 'object' && creds.api_key ? creds : null;
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ async function fetchPendingCount(creds, fetchImpl = fetch) {
100
+ const baseUrl = String(creds.base_url || 'https://auxilo.io').replace(/\/+$/, '');
101
+ const controller = new AbortController();
102
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
103
+ try {
104
+ const res = await fetchImpl(`${baseUrl}/account/pending/summary`, {
105
+ headers: { 'X-API-Key': creds.api_key },
106
+ signal: controller.signal,
107
+ });
108
+ if (!res.ok) return null;
109
+ const body = await res.json();
110
+ const n = body && body.pending_count;
111
+ return Number.isFinite(n) ? n : null;
112
+ } catch {
113
+ return null;
114
+ } finally {
115
+ clearTimeout(timer);
116
+ }
117
+ }
118
+
119
+ async function main() {
120
+ // Recursion guard: extraction children must never notice (or burn the
121
+ // suppression window).
122
+ if (process.env.AUXILO_EXTRACTING === '1') return;
123
+
124
+ const homeDir = os.homedir();
125
+ const creds = readCredentials(homeDir);
126
+ if (!creds) return;
127
+
128
+ if (!shouldNotify(readState(homeDir))) return;
129
+
130
+ const count = await fetchPendingCount(creds);
131
+ if (count == null || count <= 0) return;
132
+
133
+ process.stdout.write(renderNotice(count) + '\n');
134
+ writeState(homeDir);
135
+ }
136
+
137
+ module.exports = {
138
+ shouldNotify, renderNotice, readState, writeState, readCredentials,
139
+ fetchPendingCount, NOTICE_SUPPRESSION_MS, FETCH_TIMEOUT_MS,
140
+ };
141
+
142
+ if (require.main === module) {
143
+ // FAIL-SILENT: exit 0 on every path, including internal errors.
144
+ main().then(() => process.exit(0)).catch(() => process.exit(0));
145
+ }