osborn 0.9.252-swap.0 → 0.9.252

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.
Files changed (50) hide show
  1. package/dist/bridge-llm.d.ts +22 -0
  2. package/dist/bridge-llm.js +39 -0
  3. package/dist/codex-handler.d.ts +38 -0
  4. package/dist/codex-handler.js +83 -0
  5. package/dist/codex-llm.d.ts +40 -0
  6. package/dist/codex-llm.js +144 -0
  7. package/dist/content/compaction-lens-worker.d.ts +7 -2
  8. package/dist/content/compaction-lens-worker.js +60 -7
  9. package/dist/content/compaction-lens.js +13 -2
  10. package/dist/content/content-brief.d.ts +90 -0
  11. package/dist/content/content-brief.js +165 -0
  12. package/dist/content/content-checks.d.ts +88 -0
  13. package/dist/content/content-checks.js +249 -0
  14. package/dist/content/content-ingest.d.ts +51 -0
  15. package/dist/content/content-ingest.js +107 -0
  16. package/dist/content/content-llm.d.ts +134 -0
  17. package/dist/content/content-llm.js +412 -0
  18. package/dist/content/content-manifest.d.ts +115 -0
  19. package/dist/content/content-manifest.js +59 -0
  20. package/dist/content/content-query-gate.d.ts +22 -0
  21. package/dist/content/content-query-gate.js +78 -0
  22. package/dist/content/content-redact.d.ts +22 -0
  23. package/dist/content/content-redact.js +49 -0
  24. package/dist/content/content-research.d.ts +90 -0
  25. package/dist/content/content-research.js +256 -0
  26. package/dist/content/content-run.d.ts +85 -0
  27. package/dist/content/content-run.js +321 -0
  28. package/dist/content/content-script-rules.d.ts +74 -0
  29. package/dist/content/content-script-rules.js +134 -0
  30. package/dist/content/content-script.d.ts +61 -0
  31. package/dist/content/content-script.js +107 -0
  32. package/dist/content/lens-ingest.d.ts +7 -0
  33. package/dist/content/lens-ingest.js +21 -4
  34. package/dist/content/transcript-sanitizer.js +6 -0
  35. package/dist/fast-brain.d.ts +176 -0
  36. package/dist/fast-brain.js +1887 -0
  37. package/dist/index-3-2-26-legacy.d.ts +1 -0
  38. package/dist/index-3-2-26-legacy.js +2233 -0
  39. package/dist/index.js +77 -197
  40. package/dist/leagcyprompts2.d.ts +0 -0
  41. package/dist/leagcyprompts2.js +573 -0
  42. package/dist/pipeline-direct-llm.d.ts +1 -4
  43. package/dist/pipeline-direct-llm.js +0 -30
  44. package/dist/prompts-2-25-26.d.ts +0 -0
  45. package/dist/prompts-2-25-26.js +518 -0
  46. package/dist/prompts-3-2-26.d.ts +78 -0
  47. package/dist/prompts-3-2-26.js +1319 -0
  48. package/dist/summary-index.d.ts +90 -0
  49. package/dist/summary-index.js +585 -0
  50. package/package.json +1 -1
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Bridge LLM Module - Creates LLM instances for pipelined voice sessions
3
+ *
4
+ * In pipelined mode, we use a separate LLM (Gemini or GPT-4o) as the
5
+ * "conversation manager" that handles voice I/O and routes to Claude Code.
6
+ */
7
+ import * as google from '@livekit/agents-plugin-google';
8
+ import * as openai from '@livekit/agents-plugin-openai';
9
+ export interface BridgeLLMConfig {
10
+ provider: 'gemini-pro' | 'gemini-flash' | 'gpt-4o' | 'gpt-4o-mini';
11
+ model?: string;
12
+ }
13
+ /**
14
+ * Create Bridge LLM instance for pipelined voice sessions
15
+ *
16
+ * Options:
17
+ * - gemini-pro: Gemini 2.5 Pro (smart, good reasoning)
18
+ * - gemini-flash: Gemini 2.0 Flash (faster, cheaper)
19
+ * - gpt-4o: GPT-4o (alternative if OpenAI preferred)
20
+ * - gpt-4o-mini: GPT-4o Mini (faster, cheaper)
21
+ */
22
+ export declare function createBridgeLLM(config: BridgeLLMConfig): google.LLM | openai.LLM;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Bridge LLM Module - Creates LLM instances for pipelined voice sessions
3
+ *
4
+ * In pipelined mode, we use a separate LLM (Gemini or GPT-4o) as the
5
+ * "conversation manager" that handles voice I/O and routes to Claude Code.
6
+ */
7
+ import * as google from '@livekit/agents-plugin-google';
8
+ import * as openai from '@livekit/agents-plugin-openai';
9
+ /**
10
+ * Create Bridge LLM instance for pipelined voice sessions
11
+ *
12
+ * Options:
13
+ * - gemini-pro: Gemini 2.5 Pro (smart, good reasoning)
14
+ * - gemini-flash: Gemini 2.0 Flash (faster, cheaper)
15
+ * - gpt-4o: GPT-4o (alternative if OpenAI preferred)
16
+ * - gpt-4o-mini: GPT-4o Mini (faster, cheaper)
17
+ */
18
+ export function createBridgeLLM(config) {
19
+ switch (config.provider) {
20
+ case 'gemini-pro':
21
+ return new google.LLM({
22
+ model: config.model || 'gemini-2.5-pro',
23
+ });
24
+ case 'gemini-flash':
25
+ return new google.LLM({
26
+ model: config.model || 'gemini-2.0-flash',
27
+ });
28
+ case 'gpt-4o':
29
+ return new openai.LLM({
30
+ model: config.model || 'gpt-4o',
31
+ });
32
+ case 'gpt-4o-mini':
33
+ return new openai.LLM({
34
+ model: config.model || 'gpt-4o-mini',
35
+ });
36
+ default:
37
+ throw new Error(`Unknown Bridge LLM provider: ${config.provider}`);
38
+ }
39
+ }
@@ -0,0 +1,38 @@
1
+ import { EventEmitter } from 'events';
2
+ type SandboxMode = 'workspace-write' | 'danger-full-access';
3
+ type ApprovalPolicy = 'on-failure' | 'on-request' | 'never';
4
+ interface CodexHandlerOptions {
5
+ workingDirectory?: string;
6
+ skipGitRepoCheck?: boolean;
7
+ sandboxMode?: SandboxMode;
8
+ approvalPolicy?: ApprovalPolicy;
9
+ }
10
+ /**
11
+ * Codex Handler using the official OpenAI Codex SDK
12
+ *
13
+ * Features:
14
+ * - Thread persistence (reuses same thread for context)
15
+ * - Built-in coding tools (file operations, terminal commands)
16
+ * - Hooks for observability
17
+ *
18
+ * Requirements:
19
+ * - Codex CLI must be installed (npm install -g @openai/codex or use npx)
20
+ * - Must be authenticated via: codex login (ChatGPT) or OPENAI_API_KEY env var
21
+ *
22
+ * Sandbox/Approval modes are configured via:
23
+ * - ~/.codex/config.yaml for global settings
24
+ * - CLI flags: --sandbox, --approval-policy
25
+ *
26
+ * Note: The SDK spawns the CLI binary, so authentication must be done beforehand.
27
+ */
28
+ export declare class CodexHandler extends EventEmitter {
29
+ private codex;
30
+ private thread;
31
+ private options;
32
+ constructor(options?: CodexHandlerOptions);
33
+ run(prompt: string): Promise<string>;
34
+ getThreadId(): string | null;
35
+ clearThread(): void;
36
+ }
37
+ export declare function askCodex(prompt: string, cwd?: string): Promise<string>;
38
+ export {};
@@ -0,0 +1,83 @@
1
+ import { Codex } from '@openai/codex-sdk';
2
+ import { EventEmitter } from 'events';
3
+ /**
4
+ * Codex Handler using the official OpenAI Codex SDK
5
+ *
6
+ * Features:
7
+ * - Thread persistence (reuses same thread for context)
8
+ * - Built-in coding tools (file operations, terminal commands)
9
+ * - Hooks for observability
10
+ *
11
+ * Requirements:
12
+ * - Codex CLI must be installed (npm install -g @openai/codex or use npx)
13
+ * - Must be authenticated via: codex login (ChatGPT) or OPENAI_API_KEY env var
14
+ *
15
+ * Sandbox/Approval modes are configured via:
16
+ * - ~/.codex/config.yaml for global settings
17
+ * - CLI flags: --sandbox, --approval-policy
18
+ *
19
+ * Note: The SDK spawns the CLI binary, so authentication must be done beforehand.
20
+ */
21
+ export class CodexHandler extends EventEmitter {
22
+ codex;
23
+ thread = null;
24
+ options;
25
+ constructor(options = {}) {
26
+ super();
27
+ this.options = {
28
+ workingDirectory: options.workingDirectory || process.cwd(),
29
+ skipGitRepoCheck: options.skipGitRepoCheck ?? true,
30
+ };
31
+ // Initialize Codex (inherits process env by default)
32
+ this.codex = new Codex();
33
+ }
34
+ async run(prompt) {
35
+ try {
36
+ // Create or reuse thread
37
+ if (!this.thread) {
38
+ console.log('🆕 Starting new Codex thread');
39
+ this.thread = this.codex.startThread({
40
+ workingDirectory: this.options.workingDirectory,
41
+ skipGitRepoCheck: this.options.skipGitRepoCheck,
42
+ });
43
+ }
44
+ else {
45
+ console.log('🔄 Continuing Codex thread');
46
+ }
47
+ console.log(`📁 CWD: ${this.options.workingDirectory}`);
48
+ this.emit('thinking', prompt);
49
+ // Run the prompt and get the Turn result
50
+ const turn = await this.thread.run(prompt);
51
+ // Extract the final response text
52
+ const result = turn.finalResponse || '';
53
+ this.emit('result', result);
54
+ console.log(`✅ Codex done. Length: ${result.length}`);
55
+ // Log tool usage if any
56
+ if (turn.items && turn.items.length > 0) {
57
+ for (const item of turn.items) {
58
+ this.emit('tool_use', item);
59
+ }
60
+ }
61
+ return result || 'Task completed.';
62
+ }
63
+ catch (error) {
64
+ console.error('❌ Codex SDK error:', error);
65
+ this.emit('error', error);
66
+ throw error;
67
+ }
68
+ }
69
+ getThreadId() {
70
+ // Codex threads have IDs but they may not be directly exposed
71
+ // Check the thread object for an id property
72
+ return this.thread?.id || null;
73
+ }
74
+ clearThread() {
75
+ this.thread = null;
76
+ console.log('🗑️ Codex thread cleared');
77
+ }
78
+ }
79
+ // Quick utility for one-off queries
80
+ export async function askCodex(prompt, cwd) {
81
+ const handler = new CodexHandler({ workingDirectory: cwd });
82
+ return handler.run(prompt);
83
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Codex LLM Wrapper for LiveKit Agents
3
+ *
4
+ * Wraps the Codex Agent SDK (@openai/codex-sdk) to work
5
+ * with LiveKit's AgentSession as an LLM provider.
6
+ *
7
+ * Flow: User speaks → STT → CodexLLM (Agent SDK) → TTS → User hears
8
+ */
9
+ import { llm, type APIConnectOptions } from '@livekit/agents';
10
+ import { Codex } from '@openai/codex-sdk';
11
+ import { EventEmitter } from 'events';
12
+ export interface CodexLLMOptions {
13
+ workingDirectory?: string;
14
+ skipGitRepoCheck?: boolean;
15
+ /** Event emitter for tool_use, progress updates */
16
+ eventEmitter?: EventEmitter;
17
+ }
18
+ /**
19
+ * Codex LLM - Wraps Codex Agent SDK for LiveKit
20
+ */
21
+ export declare class CodexLLM extends llm.LLM {
22
+ #private;
23
+ constructor(opts?: CodexLLMOptions);
24
+ label(): string;
25
+ get model(): string;
26
+ get events(): EventEmitter;
27
+ get thread(): ReturnType<Codex['startThread']> | null;
28
+ chat({ chatCtx, toolCtx, connOptions, }: {
29
+ chatCtx: llm.ChatContext;
30
+ toolCtx?: llm.ToolContext;
31
+ connOptions?: APIConnectOptions;
32
+ parallelToolCalls?: boolean;
33
+ toolChoice?: llm.ToolChoice;
34
+ extraKwargs?: Record<string, unknown>;
35
+ }): llm.LLMStream;
36
+ }
37
+ /**
38
+ * Create a CodexLLM instance
39
+ */
40
+ export declare function createCodexLLM(opts?: CodexLLMOptions): CodexLLM;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Codex LLM Wrapper for LiveKit Agents
3
+ *
4
+ * Wraps the Codex Agent SDK (@openai/codex-sdk) to work
5
+ * with LiveKit's AgentSession as an LLM provider.
6
+ *
7
+ * Flow: User speaks → STT → CodexLLM (Agent SDK) → TTS → User hears
8
+ */
9
+ import { llm, shortuuid, DEFAULT_API_CONNECT_OPTIONS } from '@livekit/agents';
10
+ import { Codex } from '@openai/codex-sdk';
11
+ import { EventEmitter } from 'events';
12
+ /**
13
+ * Codex LLM - Wraps Codex Agent SDK for LiveKit
14
+ */
15
+ export class CodexLLM extends llm.LLM {
16
+ #opts;
17
+ #codex;
18
+ #thread = null;
19
+ #eventEmitter;
20
+ constructor(opts = {}) {
21
+ super();
22
+ this.#opts = {
23
+ workingDirectory: opts.workingDirectory || process.cwd(),
24
+ skipGitRepoCheck: opts.skipGitRepoCheck ?? true,
25
+ };
26
+ this.#codex = new Codex();
27
+ this.#eventEmitter = opts.eventEmitter || new EventEmitter();
28
+ console.log('🟣 CodexLLM initialized (Codex Agent SDK)');
29
+ console.log(` 📁 Working dir: ${this.#opts.workingDirectory}`);
30
+ }
31
+ label() {
32
+ return 'codex.agent-sdk';
33
+ }
34
+ get model() {
35
+ return 'codex';
36
+ }
37
+ get events() {
38
+ return this.#eventEmitter;
39
+ }
40
+ get thread() {
41
+ return this.#thread;
42
+ }
43
+ chat({ chatCtx, toolCtx, connOptions = DEFAULT_API_CONNECT_OPTIONS, }) {
44
+ return new CodexLLMStream(this, {
45
+ chatCtx,
46
+ toolCtx,
47
+ connOptions,
48
+ opts: this.#opts,
49
+ codex: this.#codex,
50
+ thread: this.#thread,
51
+ onThread: (t) => { this.#thread = t; },
52
+ eventEmitter: this.#eventEmitter,
53
+ });
54
+ }
55
+ }
56
+ /**
57
+ * Codex LLM Stream - Runs Codex Agent SDK and streams results
58
+ */
59
+ class CodexLLMStream extends llm.LLMStream {
60
+ #opts;
61
+ #codex;
62
+ #thread;
63
+ #onThread;
64
+ #eventEmitter;
65
+ constructor(llmInstance, { chatCtx, toolCtx, connOptions, opts, codex, thread, onThread, eventEmitter, }) {
66
+ super(llmInstance, { chatCtx, toolCtx, connOptions });
67
+ this.#opts = opts;
68
+ this.#codex = codex;
69
+ this.#thread = thread;
70
+ this.#onThread = onThread;
71
+ this.#eventEmitter = eventEmitter;
72
+ }
73
+ async run() {
74
+ const requestId = `codex_${shortuuid()}`;
75
+ try {
76
+ // Extract user's message from chat context
77
+ // ChatContext has .items which are ChatItem[] (ChatMessage | FunctionCall | FunctionCallOutput)
78
+ const items = this.chatCtx.items;
79
+ // Find the last user message
80
+ let userText = '';
81
+ for (let i = items.length - 1; i >= 0; i--) {
82
+ const item = items[i];
83
+ if (item.type === 'message' && item.role === 'user') {
84
+ // Content is ChatContent[] = (ImageContent | AudioContent | string)[]
85
+ if (Array.isArray(item.content)) {
86
+ userText = item.content
87
+ .filter((c) => typeof c === 'string')
88
+ .join('\n');
89
+ }
90
+ break;
91
+ }
92
+ }
93
+ if (!userText.trim()) {
94
+ this.queue.put({
95
+ id: requestId,
96
+ delta: { role: 'assistant', content: "I didn't catch that. Could you repeat?" },
97
+ });
98
+ return;
99
+ }
100
+ console.log(`🎤 User (${userText.length} chars): "${userText}"`);
101
+ // Create or reuse thread
102
+ if (!this.#thread) {
103
+ console.log('🆕 Starting new Codex thread');
104
+ this.#thread = this.#codex.startThread({
105
+ workingDirectory: this.#opts.workingDirectory,
106
+ skipGitRepoCheck: this.#opts.skipGitRepoCheck,
107
+ });
108
+ this.#onThread(this.#thread);
109
+ }
110
+ else {
111
+ console.log('🔄 Continuing Codex thread');
112
+ }
113
+ // Run Codex and get result
114
+ const turn = await this.#thread.run(userText);
115
+ // Emit tool usage events
116
+ if (turn.items && turn.items.length > 0) {
117
+ for (const item of turn.items) {
118
+ console.log(`🔧 Codex: ${item.type || 'action'}`);
119
+ this.#eventEmitter.emit('tool_use', item);
120
+ }
121
+ }
122
+ // Send response to TTS
123
+ const result = turn.finalResponse || 'Done.';
124
+ this.queue.put({
125
+ id: requestId,
126
+ delta: { role: 'assistant', content: result },
127
+ });
128
+ console.log('✅ Codex response complete');
129
+ }
130
+ catch (error) {
131
+ console.error('❌ Codex Agent SDK error:', error);
132
+ this.queue.put({
133
+ id: requestId,
134
+ delta: { role: 'assistant', content: 'Sorry, I encountered an error.' },
135
+ });
136
+ }
137
+ }
138
+ }
139
+ /**
140
+ * Create a CodexLLM instance
141
+ */
142
+ export function createCodexLLM(opts) {
143
+ return new CodexLLM(opts);
144
+ }
@@ -13,10 +13,15 @@
13
13
  * copy to <path> instead of content-profile.md and leaves the HWM alone.
14
14
  * node compaction-lens-worker.js --library <sessionId> [--cwd <dir>] [--dry] [--out-dir <dir>]
15
15
  * LIBRARY step only (manual / dry run). Takes the per-project lock itself.
16
- * Per-compaction runs do TWO independent, fail-open steps under one lock:
16
+ * node compaction-lens-worker.js --content <sessionId> [--cwd <dir>] [--dry] [--research-refresh]
17
+ * CONTENT step (Stage A) only: manual / the per-project "Refresh research"
18
+ * backend. --dry = no ingest (still spends, capped). Takes the per-project lock.
19
+ * Per-compaction runs do THREE independent, fail-open steps under one lock:
17
20
  * 1. the content lens (content-profile.md + HWM), unchanged;
18
21
  * 2. the library step (lens-library.ts): one period-map page for the period
19
- * that just ended + INDEX.md. A failure in either never stops the other.
22
+ * that just ended + INDEX.md;
23
+ * 3. the content step (content-run.ts, Stage A): research → briefs → scripts →
24
+ * checks → draft ingest for that period. A failure in any never stops the others.
20
25
  * Fully self-contained: top-level try/catch, redacted log in the project dir,
21
26
  * refreshes + releases the per-project lock, always exits.
22
27
  */
@@ -13,10 +13,15 @@
13
13
  * copy to <path> instead of content-profile.md and leaves the HWM alone.
14
14
  * node compaction-lens-worker.js --library <sessionId> [--cwd <dir>] [--dry] [--out-dir <dir>]
15
15
  * LIBRARY step only (manual / dry run). Takes the per-project lock itself.
16
- * Per-compaction runs do TWO independent, fail-open steps under one lock:
16
+ * node compaction-lens-worker.js --content <sessionId> [--cwd <dir>] [--dry] [--research-refresh]
17
+ * CONTENT step (Stage A) only: manual / the per-project "Refresh research"
18
+ * backend. --dry = no ingest (still spends, capped). Takes the per-project lock.
19
+ * Per-compaction runs do THREE independent, fail-open steps under one lock:
17
20
  * 1. the content lens (content-profile.md + HWM), unchanged;
18
21
  * 2. the library step (lens-library.ts): one period-map page for the period
19
- * that just ended + INDEX.md. A failure in either never stops the other.
22
+ * that just ended + INDEX.md;
23
+ * 3. the content step (content-run.ts, Stage A): research → briefs → scripts →
24
+ * checks → draft ingest for that period. A failure in any never stops the others.
20
25
  * Fully self-contained: top-level try/catch, redacted log in the project dir,
21
26
  * refreshes + releases the per-project lock, always exits.
22
27
  */
@@ -27,15 +32,36 @@ import { resolveSessionDb } from './lens-db.js';
27
32
  import { acquireLock, refuseOutPath, releaseLock, resolveProjectDir, touchLock } from './lens-paths.js';
28
33
  import { logLine } from './lens-profile.js';
29
34
  import { runLibraryStep } from './lens-library.js';
30
- // ≤2 big windows + reduce at ≤6 min/call, then the library step (1 call ≤6 min + model meta).
31
- const INCREMENTAL_TIMEOUT_MS = 35 * 60_000;
35
+ import { runContentStep } from './content-run.js';
36
+ // ≤2 big windows + reduce at ≤6 min/call, then the library step (1 call ≤6 min + model meta),
37
+ // then the content step (Stage A: ~10-18 small calls, ≤4 min each, capped in $).
38
+ const INCREMENTAL_TIMEOUT_MS = 45 * 60_000;
32
39
  const BACKFILL_TIMEOUT_MS = 3 * 60 * 60_000;
33
40
  const LIBRARY_TIMEOUT_MS = 15 * 60_000;
41
+ const CONTENT_TIMEOUT_MS = 20 * 60_000;
34
42
  function argVal(argv, flag) {
35
43
  const i = argv.indexOf(flag);
36
44
  return i >= 0 ? argv[i + 1] : undefined;
37
45
  }
38
46
  function parseJob(argv) {
47
+ if (argv[0] === '--content') {
48
+ const sessionId = argv[1];
49
+ if (!sessionId || sessionId.startsWith('--'))
50
+ throw new Error('usage: --content <sessionId> [--cwd <dir>] [--dry] [--research-refresh]');
51
+ const cwd = argVal(argv, '--cwd');
52
+ const db = resolveSessionDb(sessionId, null, cwd);
53
+ const projectDir = db ? dirname(dirname(dirname(db))) : null;
54
+ if (!projectDir || !acquireLock(projectDir))
55
+ throw new Error(`content: project dir ${projectDir ?? '(none)'} missing or locked`);
56
+ return {
57
+ opts: { sessionId, cwd, mode: 'incremental' },
58
+ kind: 'content',
59
+ content: { dry: argv.includes('--dry'), researchRefresh: argv.includes('--research-refresh') },
60
+ lockDir: projectDir,
61
+ logDir: projectDir,
62
+ timeoutMs: CONTENT_TIMEOUT_MS,
63
+ };
64
+ }
39
65
  if (argv[0] === '--library') {
40
66
  const sessionId = argv[1];
41
67
  if (!sessionId || sessionId.startsWith('--'))
@@ -135,7 +161,7 @@ async function main() {
135
161
  const t0 = Date.now();
136
162
  let lensErr = null;
137
163
  // Step 1 — content lens (skipped by the --library entry).
138
- if (job.kind !== 'library') {
164
+ if (job.kind !== 'library' && job.kind !== 'content') {
139
165
  try {
140
166
  const r = await runCompactionLens({
141
167
  ...job.opts,
@@ -155,7 +181,8 @@ async function main() {
155
181
  }
156
182
  // Step 2 — library page for the period that just ended (per-compaction + --library only; never backfill).
157
183
  // runLibraryStep never throws; any failure is logged and the worker still exits cleanly.
158
- if (job.opts.mode === 'incremental') {
184
+ let libPeriod = null;
185
+ if (job.opts.mode === 'incremental' && job.kind !== 'content') {
159
186
  if (lockDir)
160
187
  touchLock(lockDir);
161
188
  const lib = await runLibraryStep({
@@ -169,12 +196,38 @@ async function main() {
169
196
  logLine(logDir, `session ${sid}: library status=${lib.status} cost=$${lib.costUsd.toFixed(4)}${lib.page ? ` page=${lib.page}` : ''}`);
170
197
  if (job.kind === 'library')
171
198
  process.stdout.write(`${JSON.stringify({ status: lib.status, page: lib.page, index: lib.index, period: lib.period, cost: lib.costUsd, errors: lib.errors })}\n`);
199
+ if (lib.status === 'written')
200
+ libPeriod = lib.period;
201
+ }
202
+ // Step 3 — content plan (Stage A) for the period (per-compaction + --content; never backfill / --library).
203
+ // runContentStep never throws; the try is belt and braces so nothing here can fail the worker.
204
+ if (job.opts.mode === 'incremental' && job.kind !== 'library') {
205
+ try {
206
+ if (lockDir)
207
+ touchLock(lockDir);
208
+ const c = await runContentStep({
209
+ sessionId: job.opts.sessionId,
210
+ projectDir: lockDir,
211
+ cwd: job.opts.cwd,
212
+ period: libPeriod,
213
+ dry: job.content?.dry,
214
+ researchRefresh: job.content?.researchRefresh,
215
+ keepAlive: () => lockDir && touchLock(lockDir),
216
+ log: m => logLine(logDir, `session ${sid} [content]: ${m}`),
217
+ });
218
+ logLine(logDir, `session ${sid}: content status=${c.status} pieces=${c.pieces.length} cost=$${c.costUsd.toFixed(4)}${c.capHit ? ` cap=${c.capHit}` : ''}`);
219
+ if (job.kind === 'content')
220
+ process.stdout.write(`${JSON.stringify(c)}\n`);
221
+ }
222
+ catch (err) {
223
+ logLine(logDir, `content step error (ignored): ${err instanceof Error ? err.message : String(err)}`);
224
+ }
172
225
  }
173
226
  return lensErr ? 1 : 0;
174
227
  }
175
228
  catch (err) {
176
229
  logLine(logDir, `worker error: ${err instanceof Error ? err.stack || err.message : String(err)}`);
177
- if (err instanceof Error && /^(backfill|library):/.test(err.message))
230
+ if (err instanceof Error && /^(backfill|library|content):/.test(err.message))
178
231
  process.stderr.write(`${err.message}\n`);
179
232
  return 1;
180
233
  }
@@ -14,12 +14,14 @@
14
14
  * 7. HN gap check (fail-open), optional recording, client-redact the entry,
15
15
  * APPEND to <project>/content-profile.md (or a scratch file); advance HWM
16
16
  */
17
- import { dirname } from 'node:path';
17
+ import { basename, dirname } from 'node:path';
18
18
  import { loadConversationRows, packWindows, resolveSessionDb } from './lens-db.js';
19
19
  import { buildRecordIndex, explainQuote, parseJsonObject, readAngles, readCapabilities, splitByEvidence, verifyItems, } from './lens-quotes.js';
20
20
  import { Budget, chat, getModelInfo, mapPrompt, reducePrompt, windowTokensFor } from './lens-model.js';
21
21
  import { DENYLIST_FILE, hasBillingQuote, isBillingAngle, isBillingCapability, loadClientRedactor } from './lens-redact.js';
22
22
  import { gapCheck } from './lens-hn.js';
23
+ import { makeScrubber } from './content-redact.js';
24
+ import { gateQueries, privateSegments } from './content-query-gate.js';
23
25
  import { alignRecording, findSessionRecording } from './lens-audio.js';
24
26
  import { appendProfile, formatEntry, writeScratch } from './lens-profile.js';
25
27
  import { findTranscriptBySessionId, isProjectDir, readHwm, refuseOutPath, resolveProjectDir, writeHwm } from './lens-paths.js';
@@ -197,8 +199,17 @@ export async function runCompactionLens(opts) {
197
199
  bumpHwm(covered);
198
200
  return { ...r, status: 'nothing-survived' };
199
201
  }
202
+ // Outbound HN queries are model output built from session text: scrub, then the same
203
+ // deterministic gate research uses (project basename / cwd segments never leave the machine).
204
+ // gapCheck([]) makes no fetch and returns its neutral "no queries" result.
205
+ const sc = makeScrubber(projectDir);
206
+ const privateTerms = privateSegments([opts.cwd, process.env.OSBORN_CWD, basename(projectDir)]);
200
207
  for (const a of fin.c.angles) {
201
- r.angles.push({ title: a.title, why: a.why, quotes: a.quotes, gap: await gapCheck(a.queries) });
208
+ const scrubbed = (a.queries ?? []).map(q => sc.scrub(String(q ?? '')).replace(/\[[^\]]*\]/g, ' ').replace(/\s+/g, ' ').trim()).filter(q => q.length >= 3);
209
+ const queries = gateQueries(scrubbed, privateTerms);
210
+ if ((a.queries ?? []).length > queries.length)
211
+ log(`gap check "${a.title.slice(0, 60)}": ${(a.queries ?? []).length - queries.length} query(ies) held back by the outbound gate`);
212
+ r.angles.push({ title: a.title, why: a.why, quotes: a.quotes, gap: await gapCheck(queries) });
202
213
  }
203
214
  r.capabilities = fin.c.capabilities.map(c => ({
204
215
  name: c.name, did: c.did, evidence: c.evidence, proof: c.proof, quotes: c.quotes,
@@ -0,0 +1,90 @@
1
+ /**
2
+ * content-brief.ts — Stage A planning: which pieces a period yields, and the
3
+ * brief for each (rules §2-§4, §8a).
4
+ *
5
+ * planPieces() one OpenRouter call over the period's library page →
6
+ * ≤1 highlight + ≤3 how-to candidates, each with a subtopic,
7
+ * 1-2 search queries and the row range it is built from.
8
+ * buildBriefs() one call over the candidates + their research digests →
9
+ * per piece: VIEWER (from research), situation, tier, format
10
+ * (from the catalog), the research problems it speaks to.
11
+ * Deterministic validation after each call: unknown tiers/formats, rows outside
12
+ * the period, or a missing viewer fall back to safe defaults rather than failing.
13
+ * Throws CapError (cap) or Error (model) — content-run records either.
14
+ */
15
+ import { type ContentLlmOptions } from './content-llm.js';
16
+ import { type Tier } from './content-script-rules.js';
17
+ import type { TopicResearch } from './content-research.js';
18
+ export declare const OWNER_FILE = "content-owner.json";
19
+ export interface PieceCandidate {
20
+ id: string;
21
+ kind: Tier;
22
+ subtopic: string;
23
+ story: string;
24
+ queries: string[];
25
+ fromRow: number;
26
+ toRow: number;
27
+ why: string;
28
+ }
29
+ export interface Owner {
30
+ name: string;
31
+ handle?: string;
32
+ }
33
+ export interface Stake {
34
+ /** What is measured, e.g. "facts recalled after compaction". */
35
+ quantity: string;
36
+ before: string;
37
+ after: string;
38
+ }
39
+ export interface Brief {
40
+ id: string;
41
+ tier: Tier;
42
+ subtopic: string;
43
+ story: string;
44
+ fromRow: number;
45
+ toRow: number;
46
+ /** WHO is consuming it (rules §4), picked from the research. */
47
+ viewer: string;
48
+ /** What that viewer is living through. */
49
+ situation: string;
50
+ /** Format id from FORMAT_CATALOG. */
51
+ format: string;
52
+ angle: string;
53
+ /** Research problems (§8a) the piece speaks to. */
54
+ problems: string[];
55
+ /** WHAT-WORKS rule 1: the before -> after quantity or verdict the piece shows. Null = no provable stake (not scripted). */
56
+ stake: Stake | null;
57
+ researchKey: string;
58
+ owner: Owner | null;
59
+ autoGenerated: true;
60
+ }
61
+ /** Developer who owns the pieces (rules §6): env first, then <project>/content-owner.json. Null when unknown. */
62
+ export declare function loadOwner(projectDir: string | null): Owner | null;
63
+ export declare const PLAN_SYSTEM: string;
64
+ export declare function planPrompt(page: string, maxHighlights: number, maxHowtos: number, range: {
65
+ from: number;
66
+ to: number;
67
+ }): string;
68
+ export declare function validateCandidates(raw: unknown, range: {
69
+ from: number;
70
+ to: number;
71
+ }, maxPieces: number): PieceCandidate[];
72
+ /** Plan this period's pieces. One model call. */
73
+ export declare function planPieces(page: string, range: {
74
+ from: number;
75
+ to: number;
76
+ }, maxPieces: number, llm: ContentLlmOptions): Promise<{
77
+ candidates: PieceCandidate[];
78
+ costUsd: number;
79
+ }>;
80
+ export declare const BRIEF_SYSTEM: string;
81
+ export declare function catalogText(tier: Tier): string;
82
+ export declare function briefPrompt(cands: PieceCandidate[], research: Map<string, TopicResearch>): string;
83
+ /** A stake needs all three parts, and before must differ from after. */
84
+ export declare function validStake(v: unknown): Stake | null;
85
+ export declare function validateBriefs(raw: unknown, cands: PieceCandidate[], research: Map<string, TopicResearch>, owner: Owner | null): Brief[];
86
+ /** Build all briefs for a period in one model call. */
87
+ export declare function buildBriefs(cands: PieceCandidate[], research: Map<string, TopicResearch>, owner: Owner | null, llm: ContentLlmOptions): Promise<{
88
+ briefs: Brief[];
89
+ costUsd: number;
90
+ }>;