@gcunharodrigues/wrxn 0.17.0 → 0.18.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gcunharodrigues/wrxn",
3
- "version": "0.17.0",
3
+ "version": "0.18.1",
4
4
  "description": "WRXN Kernel — installable AI operating system. Two profiles (project | workspace), pull-based updates, managed/seeded/state file classes.",
5
5
  "bin": {
6
6
  "wrxn": "bin/wrxn.cjs"
@@ -15,21 +15,38 @@ const path = require('path');
15
15
  // secretScan — replicated here because each self-contained install-only module imports no shared
16
16
  // kernel-lib (node stdlib only), exactly as it is duplicated across dream.cjs / sync.cjs. CASE-SENSITIVE:
17
17
  // the token shapes are case-specific. A coalesced sidecar must never harden a credential onto disk.
18
- const SECRET_PATTERNS = [
19
- /AKIA[0-9A-Z]{16}/, // AWS access key id
20
- /gh[pousr]_[A-Za-z0-9]{36}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_)
21
- /npm_[A-Za-z0-9]{36}/, // npm automation token
22
- /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
23
- /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/, // FULL PEM block (BEGIN…END) — MUST precede the header-only line so redaction consumes the body, not just the header
24
- /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM private-key header (fallback: a lone/truncated header with no END)
25
- /Bearer\s+[A-Za-z0-9\-._~+/]{16,}=*/i, // HTTP bearer / Authorization token (case-insensitive scheme)
18
+ // The CANONICAL secret-shape set (#39) — kept BYTE-IDENTICAL across every copy (dream / sync / harvest /
19
+ // memory-synth + this one), drift-pinned by adapter-drift-guard.test.cjs. Each self-contained module
20
+ // replicates the set (the install-only adapters import no shared module — node stdlib only), so the pin is
21
+ // what keeps the copies honest. CASE-SENSITIVE except where a shape carries its own /i flag.
22
+ const SECRET_PATTERNS_CANON = [
23
+ /AKIA[0-9A-Z]{16}/, // AWS access key id
24
+ /gh[pousr]_[A-Za-z0-9]{20,}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_); {20,} covers the 36-char + CI forms
25
+ /github_pat_[A-Za-z0-9_]{22,}/, // GitHub fine-grained PAT
26
+ /xox[baprs]-[A-Za-z0-9-]{10,}/, // Slack token
27
+ /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
28
+ /sk-proj-[A-Za-z0-9_-]{20,}/, // OpenAI project-scoped key (underscore form sk-… misses)
29
+ /AIza[0-9A-Za-z._-]{10,}/, // Google / Gemini API key
30
+ /sk_(?:live|test)_[A-Za-z0-9]{20,}/, // Stripe live/test secret key
31
+ /npm_[A-Za-z0-9]{20,}/, // npm publish / automation token
32
+ /\bey[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{3,}\.[A-Za-z0-9_-]{3,}\b/, // JWT (incl. Bearer payloads); the eyJ… header gates it
33
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/, // PEM block (FULL — must precede the header fallback so redaction eats the body)
34
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM header (fallback: a lone/truncated header with no END)
35
+ /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/, // opaque Bearer token (non-JWT)
36
+ /\b[A-Za-z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD)\b\s*[:=]\s*\S+/i, // KEY/TOKEN/SECRET/PASSWORD = value
37
+ ];
38
+ // sidecar-LOCAL extras (#38) the canonical set does not subsume — broader / hooks-layer-specific, so they
39
+ // are NOT part of the drift-pinned core. Kept so sidecar never weakens (no pre-existing match is lost).
40
+ const SIDECAR_EXTRA = [
41
+ /Bearer\s+[A-Za-z0-9\-._~+/]{16,}=*/i, // case-insensitive bearer scheme, 16-char floor (broader than the canonical Bearer)
26
42
  // password=/pwd= assignment (=, :, quoted JSON/YAML). The keyword is EITHER fully quoted ("password":)
27
43
  // OR bare (password=) — never a lone trailing quote, so a path ending in "passwd" used as a JSON key
28
44
  // (e.g. "../../etc/passwd": …) is NOT misread as an assignment (it would falsely refuse a sidecar write).
29
45
  /(?:["'](?:password|passwd|pwd)["']|(?:password|passwd|pwd))\s*[:=]\s*["']?[^\s"',;)&}{]+/i,
30
46
  /[a-z][a-z0-9+.\-]+:\/\/[^\s:/@]+:[^\s/@]+@\S+/i, // URI connection string with inline creds (scheme://user:pass@host)
31
- /eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}/, // JWT: base64url header(eyJ…).payload.signature
47
+ /eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}/, // JWT (eyJ header; shorter min than the canonical JWT)
32
48
  ];
49
+ const SECRET_PATTERNS = [...SECRET_PATTERNS_CANON, ...SIDECAR_EXTRA];
33
50
 
34
51
  function secretScan(text) {
35
52
  const s = String(text || ''); // NOT lowercased — the token shapes are case-sensitive.
@@ -421,11 +421,20 @@ function negativeFilter(text) {
421
421
  // token shapes are case-specific). Evidence quotes are audit-only (never written to a page) so they
422
422
  // stay out of scope here, like the negative filters.
423
423
  const SECRET_PATTERNS = [
424
- /AKIA[0-9A-Z]{16}/, // AWS access key id
425
- /gh[pousr]_[A-Za-z0-9]{36}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_)
426
- /npm_[A-Za-z0-9]{36}/, // npm automation token
427
- /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
428
- /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM private-key header
424
+ /AKIA[0-9A-Z]{16}/, // AWS access key id
425
+ /gh[pousr]_[A-Za-z0-9]{20,}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_); {20,} covers the 36-char + CI forms
426
+ /github_pat_[A-Za-z0-9_]{22,}/, // GitHub fine-grained PAT
427
+ /xox[baprs]-[A-Za-z0-9-]{10,}/, // Slack token
428
+ /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
429
+ /sk-proj-[A-Za-z0-9_-]{20,}/, // OpenAI project-scoped key (underscore form sk-… misses)
430
+ /AIza[0-9A-Za-z._-]{10,}/, // Google / Gemini API key
431
+ /sk_(?:live|test)_[A-Za-z0-9]{20,}/, // Stripe live/test secret key
432
+ /npm_[A-Za-z0-9]{20,}/, // npm publish / automation token
433
+ /\bey[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{3,}\.[A-Za-z0-9_-]{3,}\b/, // JWT (incl. Bearer payloads); the eyJ… header gates it
434
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/, // PEM block (FULL — must precede the header fallback so redaction eats the body)
435
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM header (fallback: a lone/truncated header with no END)
436
+ /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/, // opaque Bearer token (non-JWT)
437
+ /\b[A-Za-z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD)\b\s*[:=]\s*\S+/i, // KEY/TOKEN/SECRET/PASSWORD = value
429
438
  ];
430
439
 
431
440
  function secretScan(text) {
@@ -653,11 +653,20 @@ function isKebab(s) {
653
653
  // A merged survivor must never harden a session secret into recalled prose. Same patterns + case-sensitive
654
654
  // scope as dream/sync — replicated because each install-only adapter is self-contained (node stdlib only).
655
655
  const SECRET_PATTERNS = [
656
- /AKIA[0-9A-Z]{16}/, // AWS access key id
657
- /gh[pousr]_[A-Za-z0-9]{36}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_)
658
- /npm_[A-Za-z0-9]{36}/, // npm automation token
659
- /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
660
- /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM private-key header
656
+ /AKIA[0-9A-Z]{16}/, // AWS access key id
657
+ /gh[pousr]_[A-Za-z0-9]{20,}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_); {20,} covers the 36-char + CI forms
658
+ /github_pat_[A-Za-z0-9_]{22,}/, // GitHub fine-grained PAT
659
+ /xox[baprs]-[A-Za-z0-9-]{10,}/, // Slack token
660
+ /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
661
+ /sk-proj-[A-Za-z0-9_-]{20,}/, // OpenAI project-scoped key (underscore form sk-… misses)
662
+ /AIza[0-9A-Za-z._-]{10,}/, // Google / Gemini API key
663
+ /sk_(?:live|test)_[A-Za-z0-9]{20,}/, // Stripe live/test secret key
664
+ /npm_[A-Za-z0-9]{20,}/, // npm publish / automation token
665
+ /\bey[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{3,}\.[A-Za-z0-9_-]{3,}\b/, // JWT (incl. Bearer payloads); the eyJ… header gates it
666
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/, // PEM block (FULL — must precede the header fallback so redaction eats the body)
667
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM header (fallback: a lone/truncated header with no END)
668
+ /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/, // opaque Bearer token (non-JWT)
669
+ /\b[A-Za-z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD)\b\s*[:=]\s*\S+/i, // KEY/TOKEN/SECRET/PASSWORD = value
661
670
  ];
662
671
 
663
672
  function secretScan(text) {
@@ -24,19 +24,28 @@ const path = require('path');
24
24
  const https = require('https');
25
25
  const { spawnSync } = require('child_process');
26
26
 
27
- // ── default tiering (PRD) ──────────────────────────────────────────────────────
28
- // handoff + dream default to gemini/gemini-3.1-flash-lite, falling back to claude/claude-sonnet-4-6.
29
- // The seeded memory.config.json is this object serialized; an operator edits it and `wrxn update`
30
- // preserves it (seeded class). Claude stays the fallback so a keyless fresh install still writes a
31
- // baton via the operator's CLI auth (no GEMINI_API_KEY gemini gated claude carries it).
27
+ // ── default tiering (PRD + #59) ────────────────────────────────────────────────
28
+ // handoff stays FAST gemini-3.1-flash-lite with thinkingBudget:0 (reasoning OFF), because handoff is on the
29
+ // 180s session-start hold path. dream REASONS gemini-3.5-flash with thinkingBudget:-1 (dynamic, the model
30
+ // decides) + maxOutputTokens:8192 (sized for reasoning + the ~4.3k-char dream JSON so it can't truncate),
31
+ // because dream runs off the critical path where quality matters. Both fall back to claude/claude-sonnet-4-6.
32
+ // The seeded memory.config.json is DEFAULTS serialized; an operator edits it and `wrxn update` preserves it
33
+ // (seeded class), so EXISTING installs keep their config and only FRESH installs get these defaults (no
34
+ // migration). Claude stays the fallback so a keyless fresh install still writes a baton via the operator's CLI
35
+ // auth (no GEMINI_API_KEY → gemini gated → claude carries it). thinkingBudget: 0 off / -1 dynamic / N>0 bounded.
36
+ // DEFAULT_TASK is the conservative per-task default for any task NOT named in DEFAULTS.tasks (reasoning off,
37
+ // preserving #30's safe default); handoff + dream pin their own tiering below.
32
38
  const DEFAULT_TASK = {
33
- primary: { engine: 'gemini', model: 'gemini-3.1-flash-lite' },
39
+ primary: { engine: 'gemini', model: 'gemini-3.1-flash-lite', thinkingBudget: 0 },
34
40
  fallback: { engine: 'claude', model: 'claude-sonnet-4-6' },
35
41
  };
36
42
  const DEFAULTS = {
37
43
  tasks: {
38
44
  handoff: clone(DEFAULT_TASK),
39
- dream: clone(DEFAULT_TASK),
45
+ dream: {
46
+ primary: { engine: 'gemini', model: 'gemini-3.5-flash', thinkingBudget: -1, maxOutputTokens: 8192 },
47
+ fallback: { engine: 'claude', model: 'claude-sonnet-4-6' },
48
+ },
40
49
  },
41
50
  };
42
51
 
@@ -178,6 +187,51 @@ const THINK_MAX = 600;
178
187
  const TOOL_USE_MAX = 300;
179
188
  const TOOL_RESULT_MAX = 200;
180
189
 
190
+ // Hook-injected framework-context sentinels. SessionStart emits <wrxn-orientation> (which embeds the
191
+ // ENTIRE prior baton as the resume surface); UserPromptSubmit emits <synapse-rules>/<recall-surface>/
192
+ // <reference-candidate> each turn; <system-reminder> wraps harness notes. All land verbatim in the
193
+ // transcript. Left in, they out-mass a light session's real turns → the synth re-emits the embedded
194
+ // prior baton (fresh `wrote`, frozen content) — the content-channel rot of #62. Strip them BEFORE
195
+ // chunking. The blob builder is the ONE shared surface, so the claude and gemini paths both benefit.
196
+ const INJECTED_SENTINELS = ['wrxn-orientation', 'synapse-rules', 'recall-surface', 'reference-candidate', 'system-reminder'];
197
+ // ReDoS bound (#62): the phase-1 closed-block strip is ~quadratic on a pathological part (many opens, no
198
+ // closes). A real injected block (orientation embeds a ≤400-word baton) is a few KB; this cap sits far
199
+ // above that yet below the multi-second regex range, so the strip stays effectively linear.
200
+ const INJECTED_STRIP_MAX = 65536;
201
+
202
+ /**
203
+ * Strip hook-injected framework-context blocks from one transcript text part BEFORE chunking. Two phases:
204
+ * 1. every well-delimited `<tag>…</tag>` block, anywhere (multi-line via [\s\S]; nested/sequential safe);
205
+ * 2. a part-LEADING unclosed `<tag>…` (transcript truncated mid-block) → stripped to end-of-part.
206
+ * The phase-2 strip is ANCHORED to part-leading (`^\s*` — after optional whitespace, incl. the newline a
207
+ * phase-1 removal leaves) because real framework injections are always part-leading. A sentinel merely
208
+ * MENTIONED mid-prose is therefore NOT a leading match, so its trailing text survives (Finding A). PURE
209
+ * and FAIL-OPEN: any fault returns the input unchanged — a filter fault must NEVER break the blob. Node
210
+ * stdlib only. Mirrors the redactSecrets scrub discipline. (#62)
211
+ * @param {string} text
212
+ * @returns {string}
213
+ */
214
+ function stripInjectedContext(text) {
215
+ try {
216
+ let out = String(text || '');
217
+ // ReDoS guard (Finding B): skip a part far larger than any real injected block — it is ordinary content
218
+ // (a pasted file/log) with nothing part-leading to strip, so returning it as-is can never hang.
219
+ if (out.length > INJECTED_STRIP_MAX) return out;
220
+ // Phase 1 — remove every well-delimited block anywhere (low risk; sequential + nested blocks collapse).
221
+ for (const tag of INJECTED_SENTINELS) {
222
+ out = out.replace(new RegExp(`<${tag}>[\\s\\S]*?</${tag}>`, 'g'), '');
223
+ }
224
+ // Phase 2 — remove a part-LEADING unclosed remainder to end-of-part (after phase 1, any leading injected
225
+ // block left dangling by truncation is part-leading). Anchored so a mid-prose mention keeps its tail.
226
+ for (const tag of INJECTED_SENTINELS) {
227
+ out = out.replace(new RegExp(`^\\s*<${tag}>[\\s\\S]*$`), '');
228
+ }
229
+ return out;
230
+ } catch {
231
+ return String(text || ''); // a filter fault never breaks the blob
232
+ }
233
+ }
234
+
181
235
  /**
182
236
  * Build a bounded plain-text blob from a Claude Code transcript (JSONL string).
183
237
  * @param {string} jsonlText the raw transcript file contents
@@ -199,12 +253,12 @@ function buildTranscriptBlob(jsonlText) {
199
253
  const c = m.content;
200
254
  const parts = [];
201
255
  if (typeof c === 'string') {
202
- parts.push(c);
256
+ parts.push(stripInjectedContext(c)); // a SessionStart-injected orientation often arrives as string content
203
257
  } else if (Array.isArray(c)) {
204
258
  for (const p of c) {
205
259
  if (!p || typeof p !== 'object') continue;
206
260
  if (p.type === 'text') {
207
- parts.push(p.text || '');
261
+ parts.push(stripInjectedContext(p.text || '')); // UserPromptSubmit sentinels prepend the user's text part
208
262
  } else if (p.type === 'thinking') {
209
263
  parts.push('[thinking] ' + String(p.thinking || '').slice(0, THINK_MAX));
210
264
  } else if (p.type === 'tool_use') {
@@ -272,15 +326,17 @@ function buildClaudeSpec({ model, prompt, blob }) {
272
326
  * `x-goog-api-key` header, the task prompt as `system_instruction` and the blob as user content
273
327
  * (mirrors the proven aimem-handoff-synth call). PURE.
274
328
  *
275
- * `thinking` controls the `generationConfig.thinkingConfig` directive (#30): when true (default) the spec
276
- * carries `{ thinkingBudget: 0 }`, which DISABLES model-side thinking on thinking-default models (a verified
277
- * HTTP-200 no-op on non-thinking models) so reasoning tokens never consume the output budget and the answer
278
- * is never split into a dropped `thought` part. Pass `false` to OMIT the directive — the AC3 retry path for
279
- * forced-thinking models (gemma-class) that reject thinkingConfig with an HTTP 400.
329
+ * `thinkingBudget` is the PER-ENGINE reasoning directive (#59, generalizing #30): a NUMBER sets
330
+ * `generationConfig.thinkingConfig = { thinkingBudget }` `0` disables model-side thinking (a verified
331
+ * HTTP-200 no-op on non-thinking models), `-1` lets the model decide (dynamic), `N>0` bounds it to N thinking
332
+ * tokens. An ABSENT budget defaults to `0` (preserves #30's safe default). An explicit `null` OMITS
333
+ * thinkingConfig entirely — the AC3 retry path for forced-thinking models (gemma-class) that reject the
334
+ * directive with an HTTP 400. `maxOutputTokens` is the per-engine output cap (default GEMINI_MAX_OUTPUT_TOKENS).
280
335
  */
281
- function buildGeminiSpec({ model, prompt, blob, apiKey, thinking = true }) {
282
- const generationConfig = { temperature: 0.2, maxOutputTokens: GEMINI_MAX_OUTPUT_TOKENS };
283
- if (thinking) generationConfig.thinkingConfig = { thinkingBudget: 0 };
336
+ function buildGeminiSpec({ model, prompt, blob, apiKey, thinkingBudget = 0, maxOutputTokens = GEMINI_MAX_OUTPUT_TOKENS }) {
337
+ const generationConfig = { temperature: 0.2, maxOutputTokens };
338
+ // a NUMBER (incl. 0 and -1) sets the directive; an explicit `null` OMITS it (the AC3 retry-without path).
339
+ if (thinkingBudget !== null) generationConfig.thinkingConfig = { thinkingBudget };
284
340
  return {
285
341
  engine: 'gemini',
286
342
  method: 'POST',
@@ -450,11 +506,12 @@ async function runEngine(engine, { prompt, blob, apiKey, invoke, sleep = default
450
506
  callOnce = () => invoke(spec);
451
507
  } else if (engine.engine === 'gemini') {
452
508
  if (!apiKey) return { text: null, attempts: 0 }; // missing key fails this engine (→ fallback / null), no request, no retry.
509
+ const { thinkingBudget, maxOutputTokens } = engine; // per-engine reasoning config (#59); absent → buildGeminiSpec defaults (0 / 4096).
453
510
  callOnce = async () => {
454
- const r = await invoke(buildGeminiSpec({ model: engine.model, prompt, blob, apiKey, thinking: true }));
511
+ const r = await invoke(buildGeminiSpec({ model: engine.model, prompt, blob, apiKey, thinkingBudget, maxOutputTokens }));
455
512
  if (r && r.thinkingUnsupported) {
456
- // this model rejects the thinkingConfig directive (forced-thinking / gemma-class) — retry once without it.
457
- return invoke(buildGeminiSpec({ model: engine.model, prompt, blob, apiKey, thinking: false }));
513
+ // this model rejects the thinkingConfig directive (forced-thinking / gemma-class) — retry once without it (same cap).
514
+ return invoke(buildGeminiSpec({ model: engine.model, prompt, blob, apiKey, thinkingBudget: null, maxOutputTokens }));
458
515
  }
459
516
  return r;
460
517
  };
@@ -615,22 +672,31 @@ function stripPreamble(text) {
615
672
  // becomes the durable baton. Pattern-based (high-signal vendor token shapes, JWTs incl. Bearer
616
673
  // payloads, and `KEY/TOKEN/SECRET/PASSWORD=value` assignments); each match → `[REDACTED]`. Conservative
617
674
  // by design: it never rewrites ordinary prose, only well-known credential shapes.
618
- const REDACTIONS = [
619
- /\bAKIA[0-9A-Z]{16}\b/g, // AWS access key id
620
- /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g, // GitHub PAT / OAuth / refresh / server tokens
621
- /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, // Slack tokens
622
- /\bsk-[A-Za-z0-9]{20,}\b/g, // OpenAI-style secret keys
623
- /\bAIza[0-9A-Za-z._-]{10,}\b/g, // Google / Gemini API keys
624
- /\bey[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{3,}\.[A-Za-z0-9_-]{3,}\b/g, // JWTs (incl. Bearer payloads): the discriminating `eyJ…` header gates it
625
- /\bnpm_[A-Za-z0-9]{20,}\b/g, // npm publish / automation tokens (≥20 covers the 36-char granular form + variable-length CI tokens)
626
- /\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, // GitHub fine-grained PATs
627
- /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/g, // Stripe live/test secret keys
628
- /\bsk-proj-[A-Za-z0-9_-]{20,}\b/g, // OpenAI project-scoped keys (underscore form not caught by sk-…)
629
- /-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z ]+ )?PRIVATE KEY-----/g, // PEM private-key blocks
630
- /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/g, // opaque Bearer tokens (non-JWT)
631
- /\b[A-Za-z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD)\b\s*[:=]\s*\S+/gi, // KEY/TOKEN/SECRET = value
675
+ // The ONE canonical secret-shape set — drift-pinned byte-identical to the dream / sync / harvest copies
676
+ // by adapter-drift-guard.test.cjs. Non-global base: redaction derives a g-flagged clone below. (Dropping
677
+ // the older inline \b boundaries is deliberate and fail-safe a redactor erring broader never under-scrubs.)
678
+ const SECRET_PATTERNS = [
679
+ /AKIA[0-9A-Z]{16}/, // AWS access key id
680
+ /gh[pousr]_[A-Za-z0-9]{20,}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_); {20,} covers the 36-char + CI forms
681
+ /github_pat_[A-Za-z0-9_]{22,}/, // GitHub fine-grained PAT
682
+ /xox[baprs]-[A-Za-z0-9-]{10,}/, // Slack token
683
+ /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
684
+ /sk-proj-[A-Za-z0-9_-]{20,}/, // OpenAI project-scoped key (underscore form sk-… misses)
685
+ /AIza[0-9A-Za-z._-]{10,}/, // Google / Gemini API key
686
+ /sk_(?:live|test)_[A-Za-z0-9]{20,}/, // Stripe live/test secret key
687
+ /npm_[A-Za-z0-9]{20,}/, // npm publish / automation token
688
+ /\bey[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{3,}\.[A-Za-z0-9_-]{3,}\b/, // JWT (incl. Bearer payloads); the eyJ… header gates it
689
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/, // PEM block (FULL — must precede the header fallback so redaction eats the body)
690
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM header (fallback: a lone/truncated header with no END)
691
+ /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/, // opaque Bearer token (non-JWT)
692
+ /\b[A-Za-z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD)\b\s*[:=]\s*\S+/i, // KEY/TOKEN/SECRET/PASSWORD = value
632
693
  ];
633
694
 
695
+ // Redaction form: every shape made global so EVERY occurrence on a line is scrubbed (not just the first).
696
+ // Each clone PRESERVES its own flags (e.g. the /i assignment shape) and only ADDS g — so detection and
697
+ // redaction never diverge. String#replace resets a global regex's lastIndex per call → reuse is safe.
698
+ const REDACTIONS = SECRET_PATTERNS.map((re) => new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g'));
699
+
634
700
  /**
635
701
  * Redact common secret shapes from `text`, replacing each match with `[REDACTED]`. PURE. Ordinary prose
636
702
  * is preserved verbatim — only credential-looking substrings are scrubbed.
@@ -957,7 +1023,7 @@ async function run(args, { invoke = defaultInvoke, out = process.stdout, err = p
957
1023
  return 2;
958
1024
  }
959
1025
  const root = rootArg || findInstallRoot(path.dirname(path.resolve(file))) || process.cwd();
960
- const blob = readTranscriptBlob(file);
1026
+ const blob = redactSecrets(readTranscriptBlob(file)); // scrub BEFORE the blob egresses to the external model — parity with runHandoff:729 / runDream:866 (#63).
961
1027
  const config = loadConfig(root);
962
1028
  const apiKey = loadEnv(root).GEMINI_API_KEY;
963
1029
  const text = await synthesize({ task, prompt, blob, config, apiKey, invoke });
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "tasks": {
3
3
  "handoff": {
4
- "primary": { "engine": "gemini", "model": "gemini-3.1-flash-lite" },
4
+ "primary": { "engine": "gemini", "model": "gemini-3.1-flash-lite", "thinkingBudget": 0 },
5
5
  "fallback": { "engine": "claude", "model": "claude-sonnet-4-6" }
6
6
  },
7
7
  "dream": {
8
- "primary": { "engine": "gemini", "model": "gemini-3.1-flash-lite" },
8
+ "primary": { "engine": "gemini", "model": "gemini-3.5-flash", "thinkingBudget": -1, "maxOutputTokens": 8192 },
9
9
  "fallback": { "engine": "claude", "model": "claude-sonnet-4-6" }
10
10
  }
11
11
  }
@@ -264,11 +264,20 @@ async function driftFromDoor(root, { transport, timeoutMs } = {}) {
264
264
  // no shared kernel-lib import), exactly as findInstallRoot/flag/fail are duplicated across these files.
265
265
  // CASE-SENSITIVE: the token shapes are case-specific.
266
266
  const SECRET_PATTERNS = [
267
- /AKIA[0-9A-Z]{16}/, // AWS access key id
268
- /gh[pousr]_[A-Za-z0-9]{36}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_)
269
- /npm_[A-Za-z0-9]{36}/, // npm automation token
270
- /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
271
- /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM private-key header
267
+ /AKIA[0-9A-Z]{16}/, // AWS access key id
268
+ /gh[pousr]_[A-Za-z0-9]{20,}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_); {20,} covers the 36-char + CI forms
269
+ /github_pat_[A-Za-z0-9_]{22,}/, // GitHub fine-grained PAT
270
+ /xox[baprs]-[A-Za-z0-9-]{10,}/, // Slack token
271
+ /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
272
+ /sk-proj-[A-Za-z0-9_-]{20,}/, // OpenAI project-scoped key (underscore form sk-… misses)
273
+ /AIza[0-9A-Za-z._-]{10,}/, // Google / Gemini API key
274
+ /sk_(?:live|test)_[A-Za-z0-9]{20,}/, // Stripe live/test secret key
275
+ /npm_[A-Za-z0-9]{20,}/, // npm publish / automation token
276
+ /\bey[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{3,}\.[A-Za-z0-9_-]{3,}\b/, // JWT (incl. Bearer payloads); the eyJ… header gates it
277
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/, // PEM block (FULL — must precede the header fallback so redaction eats the body)
278
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM header (fallback: a lone/truncated header with no END)
279
+ /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/, // opaque Bearer token (non-JWT)
280
+ /\b[A-Za-z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD)\b\s*[:=]\s*\S+/i, // KEY/TOKEN/SECRET/PASSWORD = value
272
281
  ];
273
282
 
274
283
  function secretScan(text) {