auxilo-mcp 0.9.7 → 0.9.9
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 +6 -3
- package/bin/auxilo-cli.js +52 -10
- package/lib/extraction-index.js +6 -1
- package/lib/review.js +46 -8
- package/lib/similarity.js +34 -3
- package/mcp-server.js +110 -10
- package/package.json +1 -1
- package/scripts/extract-local.js +34 -7
- package/scripts/runner.js +34 -8
- package/scripts/sources/codex-cli.js +268 -0
package/README.md
CHANGED
|
@@ -45,13 +45,16 @@ Extraction defaults to seamless: a draft that passes every screen (secrets, sens
|
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
47
|
npx auxilo review --list # show all three lanes and each exception reason
|
|
48
|
-
npx auxilo review --approve-ready #
|
|
49
|
-
npx auxilo review
|
|
48
|
+
npx auxilo review --approve-ready # approve public-destined ready_to_publish rows
|
|
49
|
+
npx auxilo review --keep-private # keep Needs your eyes owner-only at $0 recall
|
|
50
|
+
npx auxilo review # approve, reject, keep private, view, or skip
|
|
50
51
|
npx auxilo status # clients, hooks, queue depth, consent state
|
|
51
52
|
npx auxilo disable # kill switch: extraction stops immediately
|
|
52
53
|
```
|
|
53
54
|
|
|
54
|
-
Every bulk
|
|
55
|
+
Every bulk decision 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. Private-destined rows are excluded from approval: keep one private for owner-only $0 recall, or sanitize and promote a corrected replacement through public review. A private-destined row can never go public through Approve.
|
|
56
|
+
|
|
57
|
+
Private extraction is opt-in: set `AUXILO_CAPTURE_VISIBILITY=private`, or set `"capture_visibility": "private"` in `~/.auxilo/credentials.json`. It can retain reusable non-technical candidates in your owner-only lane while preserving the same mandatory local sensitivity scrub and dedup pipeline. With the setting absent, extraction remains public-destined and technical-only.
|
|
55
58
|
|
|
56
59
|
Extraction off? Your agent can still contribute in-session: tell it to submit a learning with the `auxilo_contribute` tool.
|
|
57
60
|
|
package/bin/auxilo-cli.js
CHANGED
|
@@ -551,8 +551,9 @@ function fit(s, width) {
|
|
|
551
551
|
/** Short lane/flag codes for a summary row, e.g. 'ready' or 'inj+sens'. */
|
|
552
552
|
function shortFlags(row) {
|
|
553
553
|
const resolved = review.reviewLane(row);
|
|
554
|
-
|
|
555
|
-
if (resolved.lane === '
|
|
554
|
+
const visibility = row.visibility === 'private' ? ['priv'] : [];
|
|
555
|
+
if (resolved.lane === 'ready_to_publish') return visibility.concat('ready').join('+');
|
|
556
|
+
if (resolved.lane === 'needs_score') return visibility.concat('score').join('+');
|
|
556
557
|
const map = {
|
|
557
558
|
injection: 'inj',
|
|
558
559
|
content_sensitivity: 'sens',
|
|
@@ -560,7 +561,7 @@ function shortFlags(row) {
|
|
|
560
561
|
process_advice: 'advice',
|
|
561
562
|
account_vocab: 'vocab',
|
|
562
563
|
};
|
|
563
|
-
return (row.flags || []).map((f) => map[f] || f).join('+') || 'flagged';
|
|
564
|
+
return visibility.concat((row.flags || []).map((f) => map[f] || f)).join('+') || 'flagged';
|
|
564
565
|
}
|
|
565
566
|
|
|
566
567
|
/** One compact triage line (shared by the table and rapid mode). */
|
|
@@ -660,7 +661,7 @@ async function runBulk({ apiKey, baseUrl, rows, decision, reason }) {
|
|
|
660
661
|
baseUrl,
|
|
661
662
|
decisions,
|
|
662
663
|
onChunk: ({ chunkIndex, chunkCount, response }) => {
|
|
663
|
-
console.log(` chunk ${chunkIndex + 1}/${chunkCount}: approved ${response.approved || 0}, rejected ${response.rejected || 0}, already done ${response.idempotent || 0}, failed ${response.failed || 0}`);
|
|
664
|
+
console.log(` chunk ${chunkIndex + 1}/${chunkCount}: approved ${response.approved || 0}, kept private ${response.kept_private || 0}, rejected ${response.rejected || 0}, already done ${response.idempotent || 0}, failed ${response.failed || 0}`);
|
|
664
665
|
},
|
|
665
666
|
});
|
|
666
667
|
for (const r of totals.results) {
|
|
@@ -720,6 +721,9 @@ async function cmdReview(flags) {
|
|
|
720
721
|
const minQuality = parseMinQuality(flags);
|
|
721
722
|
const sel = review.selectForBulkApprove(rows, { mode: 'ready', minQuality });
|
|
722
723
|
console.log(`approve-ready selection: ${sel.selected.length} of ${rows.length} pending (threshold: quality >= ${minQuality}).`);
|
|
724
|
+
if (sel.excluded_private.length) {
|
|
725
|
+
console.log(` excluded ${sel.excluded_private.length} private-destined item(s); use [p]/--keep-private to keep them owner-only, or sanitize-promote them for public review.`);
|
|
726
|
+
}
|
|
723
727
|
if (minQuality < review.DEFAULT_QUALITY_THRESHOLD) {
|
|
724
728
|
const approvableCount = Number.isFinite(summary.approvable_count)
|
|
725
729
|
? summary.approvable_count
|
|
@@ -742,6 +746,9 @@ async function cmdReview(flags) {
|
|
|
742
746
|
if (flags.all) {
|
|
743
747
|
const includeFlagged = flags['include-flagged'] === true;
|
|
744
748
|
const sel = review.selectForBulkApprove(rows, { mode: 'all', includeFlagged });
|
|
749
|
+
if (sel.excluded_private.length) {
|
|
750
|
+
console.log(`Excluded ${sel.excluded_private.length} private-destined item(s); use [p]/--keep-private to keep them owner-only, or sanitize-promote them for public review.`);
|
|
751
|
+
}
|
|
745
752
|
if (includeFlagged && sel.selected.some((r) => review.reviewLane(r).lane === 'needs_your_eyes')) {
|
|
746
753
|
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.');
|
|
747
754
|
} else if (sel.excluded_flagged.length) {
|
|
@@ -755,6 +762,30 @@ async function cmdReview(flags) {
|
|
|
755
762
|
return;
|
|
756
763
|
}
|
|
757
764
|
|
|
765
|
+
// ── --keep-private: finalize one explicit server lane as owner-only.
|
|
766
|
+
// Defaults to needs_your_eyes. Because this never publishes, --yes may
|
|
767
|
+
// bypass its counted confirmation; all approve paths retain the rail. ───
|
|
768
|
+
if (flags['keep-private']) {
|
|
769
|
+
let sel;
|
|
770
|
+
try {
|
|
771
|
+
sel = review.selectForKeepPrivate(rows, { lane: flags.lane });
|
|
772
|
+
} catch (err) {
|
|
773
|
+
console.error(err.message);
|
|
774
|
+
process.exit(1);
|
|
775
|
+
}
|
|
776
|
+
console.log(`keep-private selection: ${sel.selected.length} of ${rows.length} pending (lane: ${sel.lane}).`);
|
|
777
|
+
for (const lane of ['ready_to_publish', 'needs_score', 'needs_your_eyes']) {
|
|
778
|
+
const excluded = sel.excluded_by_lane[lane];
|
|
779
|
+
if (excluded.length) console.log(` excluded ${excluded.length} ${lane} item(s).`);
|
|
780
|
+
}
|
|
781
|
+
if (sel.selected.length === 0) { console.log('Nothing qualifies. No changes made.'); return; }
|
|
782
|
+
const ok = flags.yes || await confirmByTypedCount(sel.selected, 'KEEP PRIVATE (owner-only, $0 recall)');
|
|
783
|
+
if (!ok) return;
|
|
784
|
+
const totals = await runBulk({ apiKey, baseUrl, rows: sel.selected, decision: 'keep_private' });
|
|
785
|
+
console.log(`\nDone. Kept private ${totals.kept_private} of ${sel.selected.length} (already done earlier: ${totals.idempotent}, failed: ${totals.failed}).`);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
758
789
|
// ── --all-reject: bulk reject the whole batch (incident escape hatch).
|
|
759
790
|
// Now batched through the bulk endpoint. --yes keeps its scripted-incident
|
|
760
791
|
// bypass because rejection is the SAFE direction (nothing goes public). ──
|
|
@@ -784,16 +815,16 @@ async function cmdReview(flags) {
|
|
|
784
815
|
process.exit(1);
|
|
785
816
|
}
|
|
786
817
|
|
|
787
|
-
console.log('Rapid review. Per item: [y] approve (goes
|
|
818
|
+
console.log('Rapid review. Per item: [y] approve (goes public) · [n] reject · [p] keep private (owner-only, $0 recall) · [v] view · [s] skip · [q] quit\n');
|
|
788
819
|
|
|
789
|
-
let approved = 0, rejected = 0, skipped = 0;
|
|
820
|
+
let approved = 0, keptPrivate = 0, rejected = 0, skipped = 0;
|
|
790
821
|
const ordered = rows; // quality-desc server order; the summary above groups lanes
|
|
791
822
|
for (let i = 0; i < ordered.length; i++) {
|
|
792
823
|
const row = ordered[i];
|
|
793
824
|
console.log(triageLine(row, i + 1, ordered.length));
|
|
794
825
|
let choice = '';
|
|
795
826
|
for (;;) {
|
|
796
|
-
choice = (await ask(' [y/n/v/s/q]? ')).toLowerCase().slice(0, 1);
|
|
827
|
+
choice = (await ask(' [y/n/p/v/s/q]? ')).toLowerCase().slice(0, 1);
|
|
797
828
|
if (!choice && readlineEnded) choice = 'q';
|
|
798
829
|
if (choice === 'v') {
|
|
799
830
|
const full = bodies.get(row.id);
|
|
@@ -806,7 +837,7 @@ async function cmdReview(flags) {
|
|
|
806
837
|
console.log(' ------------------------------------------------------------');
|
|
807
838
|
continue;
|
|
808
839
|
}
|
|
809
|
-
if (['y', 'n', 's', 'q'].includes(choice)) break;
|
|
840
|
+
if (['y', 'n', 'p', 's', 'q'].includes(choice)) break;
|
|
810
841
|
}
|
|
811
842
|
if (choice === 'q') { console.log(' Stopping. Remaining items left pending.'); break; }
|
|
812
843
|
if (choice === 's') { skipped += 1; continue; }
|
|
@@ -814,6 +845,9 @@ async function cmdReview(flags) {
|
|
|
814
845
|
if (choice === 'y') {
|
|
815
846
|
await review.submitDecision({ apiKey, baseUrl, id: row.id, decision: 'approve' });
|
|
816
847
|
approved += 1; console.log(' ✓ approved, now live');
|
|
848
|
+
} else if (choice === 'p') {
|
|
849
|
+
await review.submitDecision({ apiKey, baseUrl, id: row.id, decision: 'keep_private' });
|
|
850
|
+
keptPrivate += 1; console.log(' ✓ kept private (owner-only)');
|
|
817
851
|
} else {
|
|
818
852
|
await review.submitDecision({ apiKey, baseUrl, id: row.id, decision: 'reject' });
|
|
819
853
|
rejected += 1; console.log(' ✗ rejected, stays private');
|
|
@@ -823,7 +857,7 @@ async function cmdReview(flags) {
|
|
|
823
857
|
}
|
|
824
858
|
}
|
|
825
859
|
|
|
826
|
-
console.log(`\nReview complete: approved ${approved}, rejected ${rejected}, skipped ${skipped} of ${ordered.length}.`);
|
|
860
|
+
console.log(`\nReview complete: approved ${approved}, kept private ${keptPrivate}, rejected ${rejected}, skipped ${skipped} of ${ordered.length}.`);
|
|
827
861
|
}
|
|
828
862
|
|
|
829
863
|
// ─── Entry point ────────────────────────────────────────────────────────────
|
|
@@ -856,10 +890,16 @@ Flags:
|
|
|
856
890
|
--min-quality N quality threshold for --approve-ready (0-20)
|
|
857
891
|
--all approve everything except needs_your_eyes items
|
|
858
892
|
--include-flagged include needs_your_eyes with --all
|
|
893
|
+
--keep-private keep one lane owner-only (default: needs_your_eyes)
|
|
894
|
+
--lane <lane> lane for --keep-private: ready_to_publish, needs_score,
|
|
895
|
+
or needs_your_eyes
|
|
859
896
|
--all-reject reject the whole batch [--yes for scripted incident use]
|
|
897
|
+
--yes bypass count only for --keep-private or --all-reject
|
|
860
898
|
--base-url <url>
|
|
861
899
|
|
|
862
|
-
Every
|
|
900
|
+
Every bulk path prints the exact list and requires typing its count. Approval
|
|
901
|
+
never accepts --yes. Private-destined rows are excluded from approval; keep
|
|
902
|
+
them private or sanitize-promote a corrected replacement.`,
|
|
863
903
|
disable: `Usage: auxilo disable [--base-url <url>]
|
|
864
904
|
|
|
865
905
|
Disable background extraction locally and optionally revoke server consent.`,
|
|
@@ -902,6 +942,8 @@ Commands:
|
|
|
902
942
|
(add --include-flagged to include that lane).
|
|
903
943
|
Same typed-count confirmation.
|
|
904
944
|
--min-quality N quality threshold for --approve-ready (0-20)
|
|
945
|
+
--keep-private keep needs_your_eyes owner-only by default
|
|
946
|
+
--lane <lane> choose a server lane for --keep-private
|
|
905
947
|
--all-reject reject the whole batch [--yes for scripted
|
|
906
948
|
incident response; rejects stay private]
|
|
907
949
|
--base-url <url>
|
package/lib/extraction-index.js
CHANGED
|
@@ -405,6 +405,7 @@ function filterIndexedNearDuplicates(candidates, indexState, opts = {}) {
|
|
|
405
405
|
if (!indexState || !indexState.usable) {
|
|
406
406
|
return { kept: input.slice(), dropped: [], disabled: true };
|
|
407
407
|
}
|
|
408
|
+
const comparisonAccountId = opts.contributorAccountId || '__local_index_owner';
|
|
408
409
|
const localRows = indexState.rows
|
|
409
410
|
.filter((row) => typeof row.body === 'string' && row.body.trim())
|
|
410
411
|
.map((row, index) => ({
|
|
@@ -413,6 +414,8 @@ function filterIndexedNearDuplicates(candidates, indexState, opts = {}) {
|
|
|
413
414
|
body: row.body,
|
|
414
415
|
category: row.category,
|
|
415
416
|
status: row.status || null,
|
|
417
|
+
visibility: row.visibility,
|
|
418
|
+
contributor_account_id: row.contributor_account_id || comparisonAccountId,
|
|
416
419
|
}));
|
|
417
420
|
if (!localRows.length) return { kept: input.slice(), dropped: [], disabled: false };
|
|
418
421
|
|
|
@@ -420,7 +423,9 @@ function filterIndexedNearDuplicates(candidates, indexState, opts = {}) {
|
|
|
420
423
|
const kept = [];
|
|
421
424
|
const dropped = [];
|
|
422
425
|
for (const candidate of input) {
|
|
423
|
-
const result = findNearDuplicate(candidate, localRows
|
|
426
|
+
const result = findNearDuplicate(candidate, localRows, {
|
|
427
|
+
contributorAccountId: comparisonAccountId,
|
|
428
|
+
});
|
|
424
429
|
if (result.verdict === 'flag') {
|
|
425
430
|
const matchedRow = localRows.find((row) => row.id === result.match.id);
|
|
426
431
|
dropped.push({
|
package/lib/review.js
CHANGED
|
@@ -59,12 +59,12 @@ async function fetchPending(opts = {}) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* POST /account/pending/:id/approve|reject — submit a self-review decision.
|
|
62
|
+
* POST /account/pending/:id/approve|reject|keep-private — submit a self-review decision.
|
|
63
63
|
*
|
|
64
64
|
* @param {object} opts
|
|
65
65
|
* @param {string} opts.apiKey
|
|
66
66
|
* @param {string} opts.id
|
|
67
|
-
* @param {'approve'|'reject'} opts.decision
|
|
67
|
+
* @param {'approve'|'reject'|'keep_private'} opts.decision
|
|
68
68
|
* @param {string} [opts.reason] only sent on reject
|
|
69
69
|
* @param {string} [opts.baseUrl]
|
|
70
70
|
* @param {Function} [opts.fetchImpl]
|
|
@@ -74,8 +74,8 @@ async function submitDecision(opts = {}) {
|
|
|
74
74
|
const { apiKey, id, decision, reason } = opts;
|
|
75
75
|
if (!apiKey) throw new Error('submitDecision: apiKey is required');
|
|
76
76
|
if (!id) throw new Error('submitDecision: id is required');
|
|
77
|
-
if (decision !== 'approve' && decision !== 'reject') {
|
|
78
|
-
throw new Error('submitDecision: decision must be "approve" or "
|
|
77
|
+
if (decision !== 'approve' && decision !== 'reject' && decision !== 'keep_private') {
|
|
78
|
+
throw new Error('submitDecision: decision must be "approve", "reject", or "keep_private"');
|
|
79
79
|
}
|
|
80
80
|
const baseUrl = normBase(opts.baseUrl);
|
|
81
81
|
const fetchImpl = opts.fetchImpl || fetch;
|
|
@@ -89,10 +89,14 @@ async function submitDecision(opts = {}) {
|
|
|
89
89
|
init.body = JSON.stringify(reason ? { reason } : {});
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
const
|
|
92
|
+
const routeDecision = decision === 'keep_private' ? 'keep-private' : decision;
|
|
93
|
+
const res = await fetchImpl(`${baseUrl}/account/pending/${encodeURIComponent(id)}/${routeDecision}`, init);
|
|
93
94
|
let body = {};
|
|
94
95
|
try { body = await res.json(); } catch { /* non-JSON error body */ }
|
|
95
96
|
if (!res.ok) {
|
|
97
|
+
if (decision === 'keep_private' && res.status === 400 && body.code === 'bad_decision') {
|
|
98
|
+
throw new Error('server predates the private tier — update the server');
|
|
99
|
+
}
|
|
96
100
|
throw new Error(`${decision} failed for ${id} (HTTP ${res.status}): ${body.error || 'unknown error'}`);
|
|
97
101
|
}
|
|
98
102
|
return body;
|
|
@@ -210,15 +214,16 @@ function chunkDecisions(list, size = BULK_CHUNK) {
|
|
|
210
214
|
* stay applied; the endpoint is idempotent per id, so re-running is safe).
|
|
211
215
|
*
|
|
212
216
|
* @param {object} opts { apiKey, decisions, baseUrl?, fetchImpl?, onChunk? }
|
|
213
|
-
* @returns {Promise<{approved:number, rejected:number, idempotent:number, failed:number, results:Array<object>}>}
|
|
217
|
+
* @returns {Promise<{approved:number, kept_private:number, rejected:number, idempotent:number, failed:number, results:Array<object>}>}
|
|
214
218
|
*/
|
|
215
219
|
async function submitBulkChunked(opts = {}) {
|
|
216
220
|
const { decisions } = opts;
|
|
217
|
-
const totals = { approved: 0, rejected: 0, idempotent: 0, failed: 0, results: [] };
|
|
221
|
+
const totals = { approved: 0, kept_private: 0, rejected: 0, idempotent: 0, failed: 0, results: [] };
|
|
218
222
|
const chunks = chunkDecisions(decisions || []);
|
|
219
223
|
for (let i = 0; i < chunks.length; i++) {
|
|
220
224
|
const resp = await submitBulk({ ...opts, decisions: chunks[i] });
|
|
221
225
|
totals.approved += resp.approved || 0;
|
|
226
|
+
totals.kept_private += resp.kept_private || 0;
|
|
222
227
|
totals.rejected += resp.rejected || 0;
|
|
223
228
|
totals.idempotent += resp.idempotent || 0;
|
|
224
229
|
totals.failed += resp.failed || 0;
|
|
@@ -270,7 +275,7 @@ function reviewLane(row) {
|
|
|
270
275
|
*
|
|
271
276
|
* @param {Array<object>} rows
|
|
272
277
|
* @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}}
|
|
278
|
+
* @returns {{selected:Array<object>, included_beyond_verdict:Array<object>, excluded_private:Array<object>, excluded_flagged:Array<object>, excluded_low_quality:Array<object>, excluded_unscored:Array<object>, min_quality:number|null, version_skew:boolean}}
|
|
274
279
|
*/
|
|
275
280
|
function selectForBulkApprove(rows, opts = {}) {
|
|
276
281
|
const mode = opts.mode === 'all' ? 'all' : 'ready';
|
|
@@ -281,6 +286,7 @@ function selectForBulkApprove(rows, opts = {}) {
|
|
|
281
286
|
|
|
282
287
|
const selected = [];
|
|
283
288
|
const included_beyond_verdict = [];
|
|
289
|
+
const excluded_private = [];
|
|
284
290
|
const excluded_flagged = [];
|
|
285
291
|
const excluded_low_quality = [];
|
|
286
292
|
const excluded_unscored = [];
|
|
@@ -288,6 +294,10 @@ function selectForBulkApprove(rows, opts = {}) {
|
|
|
288
294
|
|
|
289
295
|
for (const row of rows || []) {
|
|
290
296
|
if (!row || !row.id) continue;
|
|
297
|
+
if (row.visibility === 'private') {
|
|
298
|
+
excluded_private.push(row);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
291
301
|
const resolved = reviewLane(row);
|
|
292
302
|
version_skew = version_skew || resolved.version_skew;
|
|
293
303
|
|
|
@@ -320,6 +330,7 @@ function selectForBulkApprove(rows, opts = {}) {
|
|
|
320
330
|
return {
|
|
321
331
|
selected,
|
|
322
332
|
included_beyond_verdict,
|
|
333
|
+
excluded_private,
|
|
323
334
|
excluded_flagged,
|
|
324
335
|
excluded_low_quality,
|
|
325
336
|
excluded_unscored,
|
|
@@ -328,6 +339,32 @@ function selectForBulkApprove(rows, opts = {}) {
|
|
|
328
339
|
};
|
|
329
340
|
}
|
|
330
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Select one server lane to finalize as owner-only private. The default is
|
|
344
|
+
* needs_your_eyes; another lane requires an explicit --lane value.
|
|
345
|
+
*/
|
|
346
|
+
function selectForKeepPrivate(rows, opts = {}) {
|
|
347
|
+
const lane = opts.lane || LANE_NEEDS_EYES;
|
|
348
|
+
if (!REVIEW_LANES.has(lane)) {
|
|
349
|
+
throw new Error(`Invalid --lane "${lane}" (expected ${Array.from(REVIEW_LANES).join(', ')})`);
|
|
350
|
+
}
|
|
351
|
+
const selected = [];
|
|
352
|
+
const excluded_by_lane = {
|
|
353
|
+
[LANE_READY]: [],
|
|
354
|
+
[LANE_NEEDS_SCORE]: [],
|
|
355
|
+
[LANE_NEEDS_EYES]: [],
|
|
356
|
+
};
|
|
357
|
+
let version_skew = false;
|
|
358
|
+
for (const row of rows || []) {
|
|
359
|
+
if (!row || !row.id) continue;
|
|
360
|
+
const resolved = reviewLane(row);
|
|
361
|
+
version_skew = version_skew || resolved.version_skew;
|
|
362
|
+
if (resolved.lane === lane) selected.push(row);
|
|
363
|
+
else excluded_by_lane[resolved.lane].push(row);
|
|
364
|
+
}
|
|
365
|
+
return { selected, excluded_by_lane, lane, version_skew };
|
|
366
|
+
}
|
|
367
|
+
|
|
331
368
|
module.exports = {
|
|
332
369
|
DEFAULT_BASE_URL,
|
|
333
370
|
BULK_CHUNK,
|
|
@@ -342,4 +379,5 @@ module.exports = {
|
|
|
342
379
|
qualityClears,
|
|
343
380
|
reviewLane,
|
|
344
381
|
selectForBulkApprove,
|
|
382
|
+
selectForKeepPrivate,
|
|
345
383
|
};
|
package/lib/similarity.js
CHANGED
|
@@ -204,13 +204,39 @@ function similarityScore(a, b) {
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
207
|
+
* SPEC3-G1 comparison-set doctrine. Public predecessors collide for every
|
|
208
|
+
* caller; any non-public predecessor (pending, rejected, retracted, or
|
|
209
|
+
* approved-private) is visible to duplicate screening only for its owner.
|
|
210
|
+
* Missing visibility is the zero-migration public default.
|
|
211
|
+
*/
|
|
212
|
+
function isPublicComparisonPredecessor(learning) {
|
|
213
|
+
return !!learning &&
|
|
214
|
+
(!learning.status || learning.status === 'approved') &&
|
|
215
|
+
learning.visibility !== 'private';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isComparisonEligible(learning, contributorAccountId) {
|
|
219
|
+
if (isPublicComparisonPredecessor(learning)) return true;
|
|
220
|
+
return !!contributorAccountId &&
|
|
221
|
+
learning &&
|
|
222
|
+
learning.contributor_account_id === contributorAccountId;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function comparisonCatalog(catalog, contributorAccountId) {
|
|
226
|
+
return (catalog || []).filter((learning) =>
|
|
227
|
+
isComparisonEligible(learning, contributorAccountId));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Scan the caller-safe comparison catalog for a near-verbatim match.
|
|
232
|
+
* Public predecessors compare for everyone. Non-public predecessors compare
|
|
233
|
+
* only for their contributor account.
|
|
209
234
|
*
|
|
210
235
|
* @param {{title:string, body:string, category:string}} candidate
|
|
211
236
|
* @param {Array<object>} catalog existing learnings
|
|
212
237
|
* @param {{flagThreshold?:number, tfCosineThreshold?:number,
|
|
213
|
-
* compositeThreshold?:number, excludeId?:string
|
|
238
|
+
* compositeThreshold?:number, excludeId?:string,
|
|
239
|
+
* contributorAccountId?:string}} [opts]
|
|
214
240
|
* @returns {{verdict:'flag'|'clean', match: null|object}}
|
|
215
241
|
*/
|
|
216
242
|
function findNearDuplicate(candidate, catalog, opts = {}) {
|
|
@@ -223,6 +249,7 @@ function findNearDuplicate(candidate, catalog, opts = {}) {
|
|
|
223
249
|
for (const existing of catalog || []) {
|
|
224
250
|
if (!existing) continue;
|
|
225
251
|
if (opts.excludeId && existing.id === opts.excludeId) continue;
|
|
252
|
+
if (!isComparisonEligible(existing, opts.contributorAccountId)) continue;
|
|
226
253
|
|
|
227
254
|
const channels = scorePrepared(candidatePrepared, prepareLearning(existing));
|
|
228
255
|
const shingleFlagged = channels.d1 >= flagAt;
|
|
@@ -232,6 +259,7 @@ function findNearDuplicate(candidate, catalog, opts = {}) {
|
|
|
232
259
|
best = {
|
|
233
260
|
id: existing.id,
|
|
234
261
|
status: existing.status || null,
|
|
262
|
+
visibility: existing.visibility === 'private' ? 'private' : 'public',
|
|
235
263
|
category: existing.category || null,
|
|
236
264
|
similarity: channels.composite,
|
|
237
265
|
channel: shingleFlagged ? 'shingle' : 'near_verbatim',
|
|
@@ -251,6 +279,9 @@ function findNearDuplicate(candidate, catalog, opts = {}) {
|
|
|
251
279
|
module.exports = {
|
|
252
280
|
similarityScore,
|
|
253
281
|
findNearDuplicate,
|
|
282
|
+
isPublicComparisonPredecessor,
|
|
283
|
+
isComparisonEligible,
|
|
284
|
+
comparisonCatalog,
|
|
254
285
|
tokenize,
|
|
255
286
|
tokenSet,
|
|
256
287
|
jaccard,
|
package/mcp-server.js
CHANGED
|
@@ -134,10 +134,46 @@ function planApproveClean(summary, opts = {}) {
|
|
|
134
134
|
};
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// Private-tier counterpart to planApproveClean. Selection is delegated to the
|
|
138
|
+
// same lib/review.js helper as the CLI; the default is needs_your_eyes.
|
|
139
|
+
function planKeepPrivate(summary, opts = {}) {
|
|
140
|
+
const sel = reviewLib.selectForKeepPrivate((summary && summary.items) || [], {
|
|
141
|
+
lane: opts.lane,
|
|
142
|
+
});
|
|
143
|
+
const brief = (r) => ({
|
|
144
|
+
id: r.id,
|
|
145
|
+
title: r.title,
|
|
146
|
+
category: r.category,
|
|
147
|
+
quality: r.quality,
|
|
148
|
+
visibility: r.visibility,
|
|
149
|
+
lane: reviewLib.reviewLane(r).lane,
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
dry_run: true,
|
|
153
|
+
lane: sel.lane,
|
|
154
|
+
pending_count: summary ? summary.pending_count : 0,
|
|
155
|
+
would_keep_private_count: sel.selected.length,
|
|
156
|
+
would_keep_private: sel.selected.map(brief),
|
|
157
|
+
excluded_by_lane: Object.fromEntries(
|
|
158
|
+
Object.entries(sel.excluded_by_lane).map(([lane, rows]) => [lane, rows.map(brief)])
|
|
159
|
+
),
|
|
160
|
+
next_step: sel.selected.length > 0
|
|
161
|
+
? `Show the operator this list and count (${sel.selected.length}). After confirmation, call auxilo_review again with {action:"keep_private", dry_run:false, confirm:true, expected_count:${sel.selected.length}}. Each item stays yours; owner-only recall at $0; never published.`
|
|
162
|
+
: 'Nothing is in the selected lane. No follow-up call needed.',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function keepPrivateDecisions(plan) {
|
|
167
|
+
return (plan && plan.would_keep_private || []).map((row) => ({
|
|
168
|
+
id: row.id,
|
|
169
|
+
decision: 'keep_private',
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
|
|
137
173
|
// Review-seamless: POST one confirmed decision batch through the counted bulk
|
|
138
174
|
// endpoint, chunked at the server cap. Used by approve/reject/approve_clean.
|
|
139
175
|
async function postBulkChunks(headers, decisions) {
|
|
140
|
-
const totals = { approved: 0, rejected: 0, idempotent: 0, failed: 0, results: [] };
|
|
176
|
+
const totals = { approved: 0, kept_private: 0, rejected: 0, idempotent: 0, failed: 0, results: [] };
|
|
141
177
|
for (const chunk of reviewLib.chunkDecisions(decisions)) {
|
|
142
178
|
const resp = await fetch(`${AUXILO_BASE}/account/pending/bulk`, {
|
|
143
179
|
method: 'POST',
|
|
@@ -151,6 +187,7 @@ async function postBulkChunks(headers, decisions) {
|
|
|
151
187
|
return totals;
|
|
152
188
|
}
|
|
153
189
|
totals.approved += data.approved || 0;
|
|
190
|
+
totals.kept_private += data.kept_private || 0;
|
|
154
191
|
totals.rejected += data.rejected || 0;
|
|
155
192
|
totals.idempotent += data.idempotent || 0;
|
|
156
193
|
totals.failed += data.failed || 0;
|
|
@@ -160,7 +197,7 @@ async function postBulkChunks(headers, decisions) {
|
|
|
160
197
|
}
|
|
161
198
|
|
|
162
199
|
const server = new Server(
|
|
163
|
-
{ name: 'auxilo', version: '0.9.
|
|
200
|
+
{ name: 'auxilo', version: '0.9.9' },
|
|
164
201
|
{
|
|
165
202
|
capabilities: { tools: {} },
|
|
166
203
|
instructions: `You are connected to Auxilo, a knowledge marketplace where AI agents buy and sell operational learnings.
|
|
@@ -394,21 +431,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
394
431
|
},
|
|
395
432
|
{
|
|
396
433
|
name: 'auxilo_review',
|
|
397
|
-
description: 'Review YOUR OWN pending-review learnings (from background extraction) so they can be approved to the public marketplace
|
|
434
|
+
description: 'Review YOUR OWN pending-review learnings (from background extraction) so they can be approved to the public marketplace, rejected, or kept 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). "keep_private" finalizes one id or the selected lane (needs_your_eyes by default): it stays yours; owner-only recall at $0; never published. Bulk keep_private is DRY-RUN BY DEFAULT and uses the same selection helper as the CLI. "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. Before executing approve_clean or bulk keep_private, show the operator the dry-run list and count, 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).',
|
|
398
435
|
inputSchema: {
|
|
399
436
|
type: 'object',
|
|
400
437
|
properties: {
|
|
401
|
-
action: { type: 'string', enum: ['list', 'approve', 'reject', 'approve_clean', 'reject_by_signal', 'sanitize'], description: 'What to do. Start with "list".' },
|
|
438
|
+
action: { type: 'string', enum: ['list', 'approve', 'reject', 'keep_private', 'approve_clean', 'reject_by_signal', 'sanitize'], description: 'What to do. Start with "list".' },
|
|
402
439
|
ids: { type: 'array', items: { type: 'string' }, description: 'Learning ids for action approve/reject. These must be items the operator explicitly chose.' },
|
|
403
440
|
reason: { type: 'string', description: 'Optional rejection reason (actions reject / reject_by_signal; max 500 chars).' },
|
|
404
441
|
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
|
|
442
|
+
id: { type: 'string', description: 'keep_private single-item or sanitize. The operator-chosen learning id.' },
|
|
406
443
|
title: { type: 'string', description: 'sanitize only. Corrected title (omit to keep the original).' },
|
|
407
444
|
body: { type: 'string', description: 'sanitize only. Corrected body (omit to keep the original). At least one of title/body is required.' },
|
|
408
445
|
tags: { type: 'array', items: { type: 'string' }, description: 'sanitize only. Corrected tags (omit to keep the original).' },
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
446
|
+
lane: { type: 'string', enum: ['ready_to_publish', 'needs_score', 'needs_your_eyes'], description: 'keep_private bulk only. Lane to select; defaults to needs_your_eyes.' },
|
|
447
|
+
dry_run: { type: 'boolean', description: 'approve_clean or bulk keep_private. Default TRUE: report exactly what would change. Set false only with confirm:true and expected_count after operator confirmation.' },
|
|
448
|
+
confirm: { type: 'boolean', description: 'approve_clean or bulk keep_private. Must be exactly true to execute after operator confirmation.' },
|
|
449
|
+
expected_count: { type: 'number', description: 'approve_clean, bulk keep_private, or reject_by_signal. The confirmed selection count. If the live selection differs, nothing is mutated.' },
|
|
412
450
|
min_quality: { type: 'number', description: 'approve_clean quality threshold 0-20 (default 14). 0 includes unscored items.' },
|
|
413
451
|
session_token: { type: 'string', description: 'Optional JWT session token from /auth/verify. If omitted, your configured API key authenticates the account.' },
|
|
414
452
|
},
|
|
@@ -665,6 +703,58 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
665
703
|
return text({ action: args.action, submitted: decisions.length, ...totals });
|
|
666
704
|
}
|
|
667
705
|
|
|
706
|
+
if (args.action === 'keep_private') {
|
|
707
|
+
// A named id is an explicit single-item decision. It uses the
|
|
708
|
+
// existing single route and therefore surfaces server 4xx truth.
|
|
709
|
+
if (typeof args.id === 'string' && args.id) {
|
|
710
|
+
const resp = await fetch(
|
|
711
|
+
`${AUXILO_BASE}/account/pending/${encodeURIComponent(args.id)}/keep-private`,
|
|
712
|
+
{ method: 'POST', headers }
|
|
713
|
+
);
|
|
714
|
+
const data = await resp.json();
|
|
715
|
+
return text(data);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const summaryResp = await fetch(`${AUXILO_BASE}/account/pending/summary`, { headers });
|
|
719
|
+
const summary = await summaryResp.json();
|
|
720
|
+
if (!summaryResp.ok) return text(summary);
|
|
721
|
+
|
|
722
|
+
let plan;
|
|
723
|
+
try {
|
|
724
|
+
plan = planKeepPrivate(summary, args);
|
|
725
|
+
} catch (err) {
|
|
726
|
+
return text({ error: err.message });
|
|
727
|
+
}
|
|
728
|
+
if (args.dry_run !== false || args.confirm !== true) {
|
|
729
|
+
return text(plan);
|
|
730
|
+
}
|
|
731
|
+
if (!Number.isInteger(args.expected_count)) {
|
|
732
|
+
return text({
|
|
733
|
+
error: 'expected_count is required to execute keep_private: echo the would_keep_private_count from the dry run.',
|
|
734
|
+
...plan,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
if (args.expected_count !== plan.would_keep_private_count) {
|
|
738
|
+
return text({
|
|
739
|
+
error: `Selection changed since the dry run (expected ${args.expected_count}, now ${plan.would_keep_private_count}). Nothing was changed. Re-run the dry run and re-confirm.`,
|
|
740
|
+
...plan,
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
const decisions = keepPrivateDecisions(plan);
|
|
744
|
+
if (decisions.length === 0) {
|
|
745
|
+
return text({ action: 'keep_private', kept_private: 0, message: 'Nothing is in the selected lane.' });
|
|
746
|
+
}
|
|
747
|
+
const totals = await postBulkChunks(headers, decisions);
|
|
748
|
+
return text({
|
|
749
|
+
action: 'keep_private',
|
|
750
|
+
executed: true,
|
|
751
|
+
lane: plan.lane,
|
|
752
|
+
submitted: decisions.length,
|
|
753
|
+
...totals,
|
|
754
|
+
note: 'These items stay yours; owner-only recall at $0; never published.',
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
668
758
|
if (args.action === 'approve_clean') {
|
|
669
759
|
const summaryResp = await fetch(`${AUXILO_BASE}/account/pending/summary`, { headers });
|
|
670
760
|
const summary = await summaryResp.json();
|
|
@@ -752,7 +842,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
752
842
|
return text(await resp.json());
|
|
753
843
|
}
|
|
754
844
|
|
|
755
|
-
return text({ error: `Unknown action: ${args.action}. Use list, approve, reject, approve_clean, reject_by_signal, or sanitize.` });
|
|
845
|
+
return text({ error: `Unknown action: ${args.action}. Use list, approve, reject, keep_private, approve_clean, reject_by_signal, or sanitize.` });
|
|
756
846
|
}
|
|
757
847
|
|
|
758
848
|
case 'get_knowledge_stats': {
|
|
@@ -775,7 +865,17 @@ function text(obj) {
|
|
|
775
865
|
// LW-3(a): export the pure helpers so they can be unit-tested without starting
|
|
776
866
|
// the stdio transport. When this file is required (not run directly), stop here
|
|
777
867
|
// before the CLI dispatch and MCP startup below.
|
|
778
|
-
module.exports = {
|
|
868
|
+
module.exports = {
|
|
869
|
+
fenceUnlockResult,
|
|
870
|
+
UNTRUSTED_CONTENT_ADVISORY,
|
|
871
|
+
baseHeaders,
|
|
872
|
+
planApproveClean,
|
|
873
|
+
planKeepPrivate,
|
|
874
|
+
keepPrivateDecisions,
|
|
875
|
+
unlockPaymentRequired,
|
|
876
|
+
shapeWithdrawStatus,
|
|
877
|
+
verifyWalletRequestBody,
|
|
878
|
+
};
|
|
779
879
|
if (require.main !== module) {
|
|
780
880
|
return;
|
|
781
881
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auxilo-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
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",
|
package/scripts/extract-local.js
CHANGED
|
@@ -32,6 +32,7 @@ const {
|
|
|
32
32
|
// lists are duplicated here from lib/category-scope-migration.js (server truth);
|
|
33
33
|
// test/ci5-scope-enforcement.test.js pins the copies equal.
|
|
34
34
|
const CATEGORIES = ['data-processing', 'web-interaction', 'code-execution', 'storage-state', 'payment-financial', 'monitoring'];
|
|
35
|
+
const PRIVATE_CATEGORIES = [...CATEGORIES, 'non-technical'];
|
|
35
36
|
const RETIRED_CATEGORIES = ['communication', 'content-generation'];
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -76,6 +77,23 @@ Output STRICT JSON ONLY — an object with:
|
|
|
76
77
|
"matched_title": "<exact matched title>"}
|
|
77
78
|
Scope/quality/sensitivity skips are not dedup_drops.`;
|
|
78
79
|
|
|
80
|
+
const PUBLIC_SCOPE_BLOCK = `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".
|
|
81
|
+
|
|
82
|
+
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.`;
|
|
83
|
+
|
|
84
|
+
const PRIVATE_SCOPE_BLOCK = `PRIVATE CAPTURE SCOPE — OWNER-ONLY: extract reusable technical OR non-technical operational learnings. Non-technical process, workflow, communication, content, business, or creative learnings may use category "non-technical"; do not drop a genuine reusable candidate solely because it is non-technical. This private lane is never published unless the owner later sanitizes and promotes an item through public review. The mandatory sensitivity screen still applies without exception.`;
|
|
85
|
+
|
|
86
|
+
function promptBaseForVisibility(captureVisibility) {
|
|
87
|
+
if (captureVisibility !== 'private') return EXTRACTION_PROMPT_BASE;
|
|
88
|
+
return EXTRACTION_PROMPT_BASE
|
|
89
|
+
.replace(
|
|
90
|
+
"to publish to a PUBLIC knowledge marketplace read by other AI agents.",
|
|
91
|
+
"for the owner's private, owner-only knowledge lane."
|
|
92
|
+
)
|
|
93
|
+
.replace(PUBLIC_SCOPE_BLOCK, PRIVATE_SCOPE_BLOCK)
|
|
94
|
+
.replace(JSON.stringify(CATEGORIES), JSON.stringify(PRIVATE_CATEGORIES));
|
|
95
|
+
}
|
|
96
|
+
|
|
79
97
|
/** A1: rubric addendum — appended ONLY when scoreExtractionEnabled(). */
|
|
80
98
|
const QUALITY_RUBRIC_ADDENDUM = `
|
|
81
99
|
"quality_self_assessment": an object scoring the learning honestly on four
|
|
@@ -105,7 +123,7 @@ function buildExtractionPrompt(opts = {}) {
|
|
|
105
123
|
const memory = typeof opts.previousLessonsSection === 'string'
|
|
106
124
|
? opts.previousLessonsSection
|
|
107
125
|
: '';
|
|
108
|
-
return
|
|
126
|
+
return promptBaseForVisibility(opts.captureVisibility) +
|
|
109
127
|
(withScore ? QUALITY_RUBRIC_ADDENDUM : '') +
|
|
110
128
|
(memory ? `\n\n${memory}` : '') +
|
|
111
129
|
PROMPT_SUFFIX;
|
|
@@ -144,6 +162,7 @@ function extractWithClaudeCode(transcript, opts = {}) {
|
|
|
144
162
|
? opts.prompt
|
|
145
163
|
: buildExtractionPrompt({
|
|
146
164
|
previousLessonsSection: opts.previousLessonsSection,
|
|
165
|
+
captureVisibility: opts.captureVisibility,
|
|
147
166
|
...(opts.scoreExtraction !== undefined && { scoreExtraction: opts.scoreExtraction }),
|
|
148
167
|
});
|
|
149
168
|
const input = prompt + String(transcript).slice(0, 200000);
|
|
@@ -226,11 +245,15 @@ function normalizeLearningArray(arr, opts = {}) {
|
|
|
226
245
|
// would launder a non-tech candidate (e.g. one the model labeled
|
|
227
246
|
// 'communication') into the catalog wearing a tech label. Category-based,
|
|
228
247
|
// so it applies identically in BOTH score-gate states.
|
|
229
|
-
const
|
|
248
|
+
const allowedCategories = opts.captureVisibility === 'private' ? PRIVATE_CATEGORIES : CATEGORIES;
|
|
249
|
+
const inScope = shaped.filter(l => allowedCategories.includes(l.category));
|
|
230
250
|
// Gate-A F5: make the drop observable — count to stderr (never stdout; the
|
|
231
251
|
// hook log captures it) so a silently over-dropping prompt is diagnosable.
|
|
232
252
|
const dropped = shaped.length - inScope.length;
|
|
233
|
-
if (dropped > 0)
|
|
253
|
+
if (dropped > 0) {
|
|
254
|
+
const scope = opts.captureVisibility === 'private' ? 'private category set' : 'technical category set (CI-5 scope)';
|
|
255
|
+
console.error(`[extract-local] dropped ${dropped} candidate(s) outside the ${scope}`);
|
|
256
|
+
}
|
|
234
257
|
return inScope
|
|
235
258
|
.map(l => {
|
|
236
259
|
const out = {
|
|
@@ -535,11 +558,14 @@ async function runAnchoredJudge(candidates, indexState, opts = {}) {
|
|
|
535
558
|
|
|
536
559
|
/**
|
|
537
560
|
* Extract learnings locally. Returns { learnings: [...] } or { learnings: [], skipped }.
|
|
538
|
-
*
|
|
539
|
-
* proactive auxilo_contribute
|
|
561
|
+
* Claude Code and Codex rollout captures use the existing client-local Claude
|
|
562
|
+
* extractor; other clients rely on the agent's proactive auxilo_contribute
|
|
563
|
+
* (MCP) call.
|
|
540
564
|
*/
|
|
565
|
+
const EXTRACTABLE_SOURCES = new Set(['claude-code', 'codex-cli']);
|
|
566
|
+
|
|
541
567
|
async function extractLocally(transcript, sourceType, opts = {}) {
|
|
542
|
-
if (sourceType && sourceType
|
|
568
|
+
if (sourceType && !EXTRACTABLE_SOURCES.has(sourceType)) {
|
|
543
569
|
return { learnings: [], skipped: `local extraction not implemented for "${sourceType}" — agent contributes via auxilo_contribute` };
|
|
544
570
|
}
|
|
545
571
|
|
|
@@ -570,6 +596,7 @@ async function extractLocally(transcript, sourceType, opts = {}) {
|
|
|
570
596
|
};
|
|
571
597
|
const prompt = buildExtractionPrompt({
|
|
572
598
|
previousLessonsSection: promptMemory.section,
|
|
599
|
+
captureVisibility: opts.captureVisibility,
|
|
573
600
|
...(opts.scoreExtraction !== undefined && { scoreExtraction: opts.scoreExtraction }),
|
|
574
601
|
});
|
|
575
602
|
const invokeModel = typeof opts.invokeModel === 'function'
|
|
@@ -639,7 +666,7 @@ async function extractLocally(transcript, sourceType, opts = {}) {
|
|
|
639
666
|
|
|
640
667
|
module.exports = {
|
|
641
668
|
extractLocally, parseLearnings, parseExtractionOutput, resolveClaudeBin,
|
|
642
|
-
CATEGORIES, RETIRED_CATEGORIES,
|
|
669
|
+
CATEGORIES, PRIVATE_CATEGORIES, RETIRED_CATEGORIES,
|
|
643
670
|
EXTRACTION_PROMPT, buildExtractionPrompt, scoreExtractionEnabled,
|
|
644
671
|
validateQualityAssessment, QUALITY_DIMENSIONS,
|
|
645
672
|
buildAnchoredJudgePrompt, parseJudgeDecisions, runAnchoredJudge,
|
package/scripts/runner.js
CHANGED
|
@@ -59,6 +59,13 @@ const CREDS_PATH = path.join(AUXILO_DIR, 'credentials.json');
|
|
|
59
59
|
* 2. credentials.json .base_url
|
|
60
60
|
* 3. http://localhost:49152 (dev default)
|
|
61
61
|
*/
|
|
62
|
+
function resolveCaptureVisibility(env = {}, fileCreds = {}) {
|
|
63
|
+
const value = Object.prototype.hasOwnProperty.call(env, 'AUXILO_CAPTURE_VISIBILITY')
|
|
64
|
+
? env.AUXILO_CAPTURE_VISIBILITY
|
|
65
|
+
: fileCreds.capture_visibility;
|
|
66
|
+
return value === 'private' ? 'private' : 'public';
|
|
67
|
+
}
|
|
68
|
+
|
|
62
69
|
function loadCredentials() {
|
|
63
70
|
let fileCreds = {};
|
|
64
71
|
try {
|
|
@@ -70,10 +77,16 @@ function loadCredentials() {
|
|
|
70
77
|
apiKey: process.env.AUXILO_API_KEY || fileCreds.api_key || null,
|
|
71
78
|
baseUrl: process.env.AUXILO_BASE_URL || fileCreds.base_url || 'http://localhost:49152',
|
|
72
79
|
accountLabel: fileCreds.label || fileCreds.account_id || null,
|
|
80
|
+
captureVisibility: resolveCaptureVisibility(process.env, fileCreds),
|
|
73
81
|
};
|
|
74
82
|
}
|
|
75
83
|
|
|
76
|
-
const {
|
|
84
|
+
const {
|
|
85
|
+
apiKey: API_KEY,
|
|
86
|
+
baseUrl: BASE_URL,
|
|
87
|
+
accountLabel: ACCOUNT_LABEL,
|
|
88
|
+
captureVisibility: CAPTURE_VISIBILITY,
|
|
89
|
+
} = loadCredentials();
|
|
77
90
|
|
|
78
91
|
// Parsing contract with jobs/daily-digest.js readLogRows(): digest-relevant log
|
|
79
92
|
// lines must carry `account=` (builder attribution) and, on publish lines,
|
|
@@ -290,7 +303,7 @@ function listPendingFiles() {
|
|
|
290
303
|
*
|
|
291
304
|
* @param {Array<object>} learnings
|
|
292
305
|
* @param {string} sourceType
|
|
293
|
-
* @param {object} [opts] { fetchImpl, baseUrl, apiKey, indexPath, now } —
|
|
306
|
+
* @param {object} [opts] { fetchImpl, baseUrl, apiKey, captureVisibility, indexPath, now } —
|
|
294
307
|
* injectable for tests
|
|
295
308
|
* @returns {Promise<{published:number, held:number, rejected:number}>}
|
|
296
309
|
*/
|
|
@@ -298,6 +311,7 @@ async function submitLearnings(learnings, sourceType, opts = {}) {
|
|
|
298
311
|
const fetchImpl = opts.fetchImpl || fetch;
|
|
299
312
|
const baseUrl = opts.baseUrl || BASE_URL;
|
|
300
313
|
const apiKey = opts.apiKey !== undefined ? opts.apiKey : API_KEY;
|
|
314
|
+
const captureVisibility = opts.captureVisibility || CAPTURE_VISIBILITY;
|
|
301
315
|
|
|
302
316
|
let published = 0;
|
|
303
317
|
let held = 0;
|
|
@@ -320,6 +334,7 @@ async function submitLearnings(learnings, sourceType, opts = {}) {
|
|
|
320
334
|
outcome: l.outcome,
|
|
321
335
|
contributor_agent: `auxilo-hook/${sourceType}`,
|
|
322
336
|
submission_channel: 'extraction',
|
|
337
|
+
...(captureVisibility === 'private' && { visibility: 'private' }),
|
|
323
338
|
...(l.quality_self_assessment && { quality_self_assessment: l.quality_self_assessment }),
|
|
324
339
|
}),
|
|
325
340
|
});
|
|
@@ -341,7 +356,7 @@ async function submitLearnings(learnings, sourceType, opts = {}) {
|
|
|
341
356
|
return { published, held, rejected };
|
|
342
357
|
}
|
|
343
358
|
|
|
344
|
-
async function postExtract(transcript, sessionId, sourceType, _scrubReport) {
|
|
359
|
+
async function postExtract(transcript, sessionId, sourceType, _scrubReport, opts = {}) {
|
|
345
360
|
// CLIENT-SIDE extraction (2026-07-02). Server /extract is deprecated (410) — Auxilo
|
|
346
361
|
// does not pay to extract. The local model (via `claude -p`) extracts + self-screens
|
|
347
362
|
// the already-client-scrubbed transcript, and we submit finished learnings to /learn.
|
|
@@ -365,8 +380,9 @@ async function postExtract(transcript, sessionId, sourceType, _scrubReport) {
|
|
|
365
380
|
judge_prompt_tokens: judgePromptTokens = 0,
|
|
366
381
|
judge_completion_tokens: judgeCompletionTokens = 0,
|
|
367
382
|
} = await extractLocally(transcript, sourceType, {
|
|
368
|
-
baseUrl: BASE_URL,
|
|
369
|
-
apiKey: API_KEY,
|
|
383
|
+
baseUrl: opts.baseUrl || BASE_URL,
|
|
384
|
+
apiKey: opts.apiKey !== undefined ? opts.apiKey : API_KEY,
|
|
385
|
+
captureVisibility: opts.captureVisibility || CAPTURE_VISIBILITY,
|
|
370
386
|
log,
|
|
371
387
|
auditLog: auditDropLog,
|
|
372
388
|
}));
|
|
@@ -378,7 +394,10 @@ async function postExtract(transcript, sessionId, sourceType, _scrubReport) {
|
|
|
378
394
|
return { learnings_published: 0, learnings_held: 0, learnings_rejected: 0, extraction_id: 'client-skip' };
|
|
379
395
|
}
|
|
380
396
|
|
|
381
|
-
const { published, held, rejected } = await submitLearnings(learnings, sourceType
|
|
397
|
+
const { published, held, rejected } = await submitLearnings(learnings, sourceType, {
|
|
398
|
+
...opts,
|
|
399
|
+
captureVisibility: opts.captureVisibility || CAPTURE_VISIBILITY,
|
|
400
|
+
});
|
|
382
401
|
// NOTE: token-free prose — the digest-parsed `published=`/`held=`/`rejected=`
|
|
383
402
|
// tokens live ONLY on the per-run caller log lines. This line previously
|
|
384
403
|
// carried `published=` too and the digest double-counted every extraction
|
|
@@ -924,7 +943,8 @@ async function main() {
|
|
|
924
943
|
try {
|
|
925
944
|
const payload = JSON.parse(fs.readFileSync(qf, 'utf-8'));
|
|
926
945
|
const result = await postExtract(
|
|
927
|
-
payload.transcript, payload.sessionId, payload.source, payload.scrubReport
|
|
946
|
+
payload.transcript, payload.sessionId, payload.source, payload.scrubReport,
|
|
947
|
+
{ captureVisibility: payload.capture_visibility || CAPTURE_VISIBILITY }
|
|
928
948
|
);
|
|
929
949
|
log(`[runner] ✓ Flushed ${path.basename(qf)}: published=${result.learnings_published || 0} held=${result.learnings_held || 0} rejected=${result.learnings_rejected || 0} ${DIGEST_ACCOUNT}`);
|
|
930
950
|
flushHeld += result.learnings_held || 0;
|
|
@@ -953,6 +973,7 @@ async function main() {
|
|
|
953
973
|
let totalOversize = 0; // N1: oversize-cap skips (subset of totalSkipped)
|
|
954
974
|
let totalFailed = 0;
|
|
955
975
|
let totalHeld = 0;
|
|
976
|
+
const refusedBySource = new Map();
|
|
956
977
|
|
|
957
978
|
for (const source of sources) {
|
|
958
979
|
log(`[runner] Discovering sessions from ${source.label} (${source.type})...`);
|
|
@@ -1001,7 +1022,7 @@ async function main() {
|
|
|
1001
1022
|
|
|
1002
1023
|
// UC-1 format-probe refusal: null = skip silently (not a failure).
|
|
1003
1024
|
if (!transcriptData || typeof transcriptData.transcript !== 'string') {
|
|
1004
|
-
|
|
1025
|
+
refusedBySource.set(source.type, (refusedBySource.get(source.type) || 0) + 1);
|
|
1005
1026
|
totalSkipped++;
|
|
1006
1027
|
ledgerMark(ledger, source.type, sessionRef.sessionId, 'probe-refused', sessionRef.mtime);
|
|
1007
1028
|
continue;
|
|
@@ -1059,6 +1080,7 @@ async function main() {
|
|
|
1059
1080
|
scrubReport: report,
|
|
1060
1081
|
mtime: sessionRef.mtime,
|
|
1061
1082
|
queuedAt: new Date().toISOString(),
|
|
1083
|
+
...(CAPTURE_VISIBILITY === 'private' && { capture_visibility: 'private' }),
|
|
1062
1084
|
});
|
|
1063
1085
|
|
|
1064
1086
|
try {
|
|
@@ -1077,6 +1099,9 @@ async function main() {
|
|
|
1077
1099
|
}
|
|
1078
1100
|
}
|
|
1079
1101
|
|
|
1102
|
+
for (const [sourceType, count] of refusedBySource) {
|
|
1103
|
+
log(`[runner] ${sourceType}: ${count} refused (non-user/format)`);
|
|
1104
|
+
}
|
|
1080
1105
|
saveLedger(ledger);
|
|
1081
1106
|
log(`[runner] Summary: ${totalDiscovered} discovered, ${totalProcessed} processed, ${totalSkipped} skipped (${totalOversize} oversize), ${totalFailed} failed`);
|
|
1082
1107
|
if (totalOversize > 0) {
|
|
@@ -1093,6 +1118,7 @@ module.exports = {
|
|
|
1093
1118
|
loadLedger, saveLedger, ledgerHighWater, ledgerHas, ledgerMark,
|
|
1094
1119
|
installHooks, installSweeper, installDigest, printStatus, scrubAndVerify, enumerateActiveSources,
|
|
1095
1120
|
loadSources, SOURCES, sweeperManifest, submitLearnings, notifyHeld,
|
|
1121
|
+
resolveCaptureVisibility, postExtract,
|
|
1096
1122
|
KILL_SWITCH_PATH, PENDING_DIR, LEDGER_PATH,
|
|
1097
1123
|
};
|
|
1098
1124
|
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/sources/codex-cli.js — Codex CLI + Desktop Transcript Source (UC-6)
|
|
3
|
+
*
|
|
4
|
+
* Best-effort UC-3 poll adapter.
|
|
5
|
+
* UC-3 disclaimer: format community-reverse-engineered; verified against live operator install 2026-07-26 (247 rollouts).
|
|
6
|
+
* Shape drift fails silent instead of producing a guessed transcript.
|
|
7
|
+
*
|
|
8
|
+
* Upstream context:
|
|
9
|
+
* openai/codex#21639 — Codex Desktop hooks regression
|
|
10
|
+
* openai/codex#24948 — rollout files can grow to multi-GB size
|
|
11
|
+
* openai/codex#21660 — rollout files may be created with 0644 permissions
|
|
12
|
+
*
|
|
13
|
+
* Desktop embeds the CLI and shares its ~/.codex rollout store, so one source
|
|
14
|
+
* id deliberately covers both clients. Privacy-sensitive base instructions,
|
|
15
|
+
* world state (including AGENTS.md), reasoning, and duplicate event messages
|
|
16
|
+
* are never normalized into transcript output.
|
|
17
|
+
*
|
|
18
|
+
* @module sources/codex-cli
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const os = require('os');
|
|
26
|
+
const { TranscriptSource } = require('./source.interface');
|
|
27
|
+
|
|
28
|
+
const DEFAULT_QUIESCENCE_MS = 30 * 60 * 1000;
|
|
29
|
+
const UUID_AT_END_RE = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
|
|
30
|
+
|
|
31
|
+
function resolveQuiescenceMs(env = process.env) {
|
|
32
|
+
const raw = env && env.AUXILO_CODEX_QUIESCENCE_MS;
|
|
33
|
+
if (raw === undefined || raw === null || raw === '') return DEFAULT_QUIESCENCE_MS;
|
|
34
|
+
const value = Number(raw);
|
|
35
|
+
return Number.isInteger(value) && value > 0 ? value : DEFAULT_QUIESCENCE_MS;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function rolloutSessionId(filePath) {
|
|
39
|
+
const name = path.basename(filePath);
|
|
40
|
+
const match = name.match(UUID_AT_END_RE);
|
|
41
|
+
return match ? match[1] : path.basename(name, path.extname(name));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function listRollouts(root, recursive) {
|
|
45
|
+
const found = [];
|
|
46
|
+
let entries;
|
|
47
|
+
try {
|
|
48
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
49
|
+
} catch {
|
|
50
|
+
return found;
|
|
51
|
+
}
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const filePath = path.join(root, entry.name);
|
|
54
|
+
if (recursive && entry.isDirectory()) {
|
|
55
|
+
found.push(...listRollouts(filePath, true));
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (entry.isFile() && /^rollout-.*\.jsonl$/i.test(entry.name)) found.push(filePath);
|
|
59
|
+
}
|
|
60
|
+
return found;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function textContentItems(content) {
|
|
64
|
+
if (!Array.isArray(content)) return [];
|
|
65
|
+
return content
|
|
66
|
+
.filter((item) =>
|
|
67
|
+
item &&
|
|
68
|
+
typeof item === 'object' &&
|
|
69
|
+
['input_text', 'output_text', 'text'].includes(item.type) &&
|
|
70
|
+
typeof item.text === 'string'
|
|
71
|
+
)
|
|
72
|
+
.map((item) => item.text);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function stringifyValue(value) {
|
|
76
|
+
if (value === undefined || value === null) return '';
|
|
77
|
+
if (typeof value === 'string') return value;
|
|
78
|
+
try { return JSON.stringify(value) ?? ''; } catch { return String(value); }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function oneLine(value) {
|
|
82
|
+
return stringifyValue(value).replace(/\s+/g, ' ').trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toolOutputItems(payload) {
|
|
86
|
+
const output = payload.output !== undefined ? payload.output : payload.content;
|
|
87
|
+
if (typeof output === 'string') return [output];
|
|
88
|
+
if (Array.isArray(output)) {
|
|
89
|
+
return output.flatMap((item) => {
|
|
90
|
+
if (typeof item === 'string') return [item];
|
|
91
|
+
if (!item || typeof item !== 'object') return [];
|
|
92
|
+
if (typeof item.text === 'string') return [item.text];
|
|
93
|
+
if (typeof item.output === 'string') return [item.output];
|
|
94
|
+
if (typeof item.content === 'string') return [item.content];
|
|
95
|
+
return [];
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (output && typeof output === 'object') {
|
|
99
|
+
if (typeof output.text === 'string') return [output.text];
|
|
100
|
+
if (typeof output.output === 'string') return [output.output];
|
|
101
|
+
}
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
class CodexCliSource extends TranscriptSource {
|
|
106
|
+
static id = 'codex-cli';
|
|
107
|
+
static displayName = 'Codex (CLI + Desktop)';
|
|
108
|
+
static version = '1.0.0';
|
|
109
|
+
|
|
110
|
+
constructor(config = {}) {
|
|
111
|
+
super(config);
|
|
112
|
+
const homeDir = config.homeDir || os.homedir();
|
|
113
|
+
this.codexDir = config.codexDir || path.join(homeDir, '.codex');
|
|
114
|
+
this.sessionsDir = path.join(this.codexDir, 'sessions');
|
|
115
|
+
this.archivedSessionsDir = path.join(this.codexDir, 'archived_sessions');
|
|
116
|
+
this.env = config.env || process.env;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async detect() {
|
|
120
|
+
try {
|
|
121
|
+
return fs.statSync(this.sessionsDir).isDirectory();
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async discoverSessions({ since } = {}) {
|
|
128
|
+
const parsedSince = since ? Date.parse(since) : 0;
|
|
129
|
+
const sinceMs = Number.isFinite(parsedSince) ? parsedSince : 0;
|
|
130
|
+
const quiescentBefore = Date.now() - resolveQuiescenceMs(this.env);
|
|
131
|
+
const candidates = [
|
|
132
|
+
...listRollouts(this.sessionsDir, true),
|
|
133
|
+
...listRollouts(this.archivedSessionsDir, false),
|
|
134
|
+
];
|
|
135
|
+
const sessions = [];
|
|
136
|
+
|
|
137
|
+
for (const filePath of candidates) {
|
|
138
|
+
try {
|
|
139
|
+
const stat = fs.statSync(filePath);
|
|
140
|
+
if (!stat.isFile()) continue;
|
|
141
|
+
if (stat.mtimeMs <= sinceMs || stat.mtimeMs >= quiescentBefore) continue;
|
|
142
|
+
sessions.push({
|
|
143
|
+
sessionId: rolloutSessionId(filePath),
|
|
144
|
+
path: filePath,
|
|
145
|
+
mtime: stat.mtime.toISOString(),
|
|
146
|
+
bytes: stat.size,
|
|
147
|
+
});
|
|
148
|
+
} catch {
|
|
149
|
+
// A rollout can disappear or become unreadable while the sweep walks.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return sessions.sort((a, b) =>
|
|
154
|
+
Date.parse(a.mtime) - Date.parse(b.mtime) || a.path.localeCompare(b.path)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async readSession(sessionRef) {
|
|
159
|
+
try {
|
|
160
|
+
return await this._readSession(sessionRef);
|
|
161
|
+
} catch {
|
|
162
|
+
// Gate-A F-A: the adapter contract is never-throw. A newly observed
|
|
163
|
+
// optional field or other shape drift refuses the whole rollout rather
|
|
164
|
+
// than escaping into the runner as a failed session.
|
|
165
|
+
return this._refuse(sessionRef && sessionRef.path, 'unexpected normalization error');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_readSession(sessionRef) {
|
|
170
|
+
const filePath = sessionRef && sessionRef.path;
|
|
171
|
+
let raw;
|
|
172
|
+
try {
|
|
173
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
174
|
+
} catch {
|
|
175
|
+
return this._refuse(filePath, 'unreadable rollout');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const records = [];
|
|
179
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
180
|
+
if (!line.trim()) continue;
|
|
181
|
+
try {
|
|
182
|
+
const record = JSON.parse(line);
|
|
183
|
+
if (record && typeof record === 'object') records.push(record);
|
|
184
|
+
} catch {
|
|
185
|
+
// Individual malformed records are ignored; the format probe below
|
|
186
|
+
// still requires a valid first record and at least two valid records.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (records.length === 0 || records[0].type !== 'session_meta') {
|
|
191
|
+
return this._refuse(filePath, 'first parseable record is not session_meta');
|
|
192
|
+
}
|
|
193
|
+
const sessionMeta = records[0].payload;
|
|
194
|
+
if (!sessionMeta || typeof sessionMeta !== 'object') {
|
|
195
|
+
return this._refuse(filePath, 'session_meta payload missing');
|
|
196
|
+
}
|
|
197
|
+
if (sessionMeta.thread_source !== 'user') {
|
|
198
|
+
return this._refuse(filePath, 'non-user thread');
|
|
199
|
+
}
|
|
200
|
+
if (records.length < 2) {
|
|
201
|
+
return this._refuse(filePath, 'fewer than two parseable records');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const turns = [];
|
|
205
|
+
for (const record of records) {
|
|
206
|
+
if (record.type !== 'response_item') continue;
|
|
207
|
+
const payload = record.payload;
|
|
208
|
+
if (!payload || typeof payload !== 'object') continue;
|
|
209
|
+
|
|
210
|
+
if (payload.type === 'message') {
|
|
211
|
+
const role = payload.role === 'user'
|
|
212
|
+
? 'User'
|
|
213
|
+
: payload.role === 'assistant'
|
|
214
|
+
? 'Assistant'
|
|
215
|
+
: null;
|
|
216
|
+
if (!role) continue;
|
|
217
|
+
const text = textContentItems(payload.content).join('\n').trim();
|
|
218
|
+
if (text) turns.push(`${role}: ${text}`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (payload.type === 'custom_tool_call') {
|
|
223
|
+
const name = oneLine(payload.name || payload.tool_name || payload.tool || 'unknown');
|
|
224
|
+
const args = oneLine(payload.arguments).slice(0, 500);
|
|
225
|
+
turns.push(`Tool: ${name}${args ? ` ${args}` : ''}`);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (payload.type === 'custom_tool_call_output') {
|
|
230
|
+
const items = toolOutputItems(payload)
|
|
231
|
+
.map((item) => String(item).slice(0, 2000))
|
|
232
|
+
.filter(Boolean);
|
|
233
|
+
if (items.length > 0) turns.push(`Tool result: ${items.join('\n')}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
transcript: turns.join('\n\n'),
|
|
239
|
+
metadata: {
|
|
240
|
+
sessionId: sessionRef.sessionId,
|
|
241
|
+
source: 'codex-cli',
|
|
242
|
+
mtime: sessionRef.mtime,
|
|
243
|
+
bytes: sessionRef.bytes,
|
|
244
|
+
originator: sessionMeta.originator,
|
|
245
|
+
cwd: sessionMeta.cwd,
|
|
246
|
+
model_provider: sessionMeta.model_provider,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_refuse(filePath, reason) {
|
|
252
|
+
// Gate-A F-B: sweeps summarize refusals once in runner.js. Per-file paths
|
|
253
|
+
// are available only under an explicit local debug opt-in.
|
|
254
|
+
if (this.env.AUXILO_CODEX_DEBUG === '1') {
|
|
255
|
+
process.stderr.write(`[codex-cli] format probe refused ${filePath || '(unknown)'} (${reason}) — skipping\n`);
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async registerSessionEndHook(cb) { return null; }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = {
|
|
264
|
+
CodexCliSource,
|
|
265
|
+
DEFAULT_QUIESCENCE_MS,
|
|
266
|
+
resolveQuiescenceMs,
|
|
267
|
+
rolloutSessionId,
|
|
268
|
+
};
|