@oxygen-agent/cli 1.888.6 → 1.893.0

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.
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Knowledge bootstrap — the once-per-workspace, automatic company research pass.
3
+ *
4
+ * WHAT IT IS. Exactly once in a workspace's life, OXYGEN researches the customer's
5
+ * OWN company from the domain we already resolved at org creation
6
+ * (control-DB `organizations.iconDomain`, `iconStatus = 'ok'`), then fills the typed
7
+ * company profile (`ox_context.company_profile`) and writes one cited wiki page.
8
+ * A founder who signs up should not face an empty Knowledge Graph and have to type
9
+ * their own positioning back at us; every grounded AI action downstream (message
10
+ * drafts, AI columns, agent runs) reads that profile, so an empty one degrades the
11
+ * whole product's first hour.
12
+ *
13
+ * WHY THIS IS NOT A VIOLATION OF THE PAID-ACTIONS RULE. `CLAUDE.md` says never run
14
+ * paid provider actions unless the user explicitly asks. This slice is a deliberate,
15
+ * founder-approved exception: signup IS the standing authorization, the same way an
16
+ * armed table-webhook auto-run configuration is scoped standing permission for the
17
+ * columns it queues. The exception is defensible ONLY because every one of the
18
+ * following properties holds. They are load-bearing — do not drop one for
19
+ * convenience, and if you remove one, the exception no longer stands:
20
+ *
21
+ * 1. CAPPED — `KNOWLEDGE_BOOTSTRAP_MAX_CREDITS` is a hard ceiling on the whole
22
+ * pass, with `KNOWLEDGE_BOOTSTRAP_ENRICHMENT_MAX_CREDITS` a tighter
23
+ * sub-ceiling on the provider (external-money) half.
24
+ * 2. CONSENTED — it runs only for a workspace whose own domain we already derived
25
+ * at signup; a personal-email signup is `skipped_personal_email`
26
+ * upstream and never reaches here, so we never research a stranger.
27
+ * 3. JOURNALLED — the marker below records status, timing, run id, credits and the
28
+ * domain in `knowledge_state.watermarks`; the URLs read land as
29
+ * immutable `page_sources` rows plus an `ingest` knowledge_log
30
+ * entry. Nothing about the spend is invisible.
31
+ * 4. KILL-SWITCHED — `OXYGEN_KNOWLEDGE_BOOTSTRAP_DISABLED` stops it fleet-wide at
32
+ * the worker with no deploy, matching the other OXYGEN_KNOWLEDGE_*
33
+ * switches.
34
+ * 5. REVERSIBLE — the marker is a single jsonb key; clearing it (the `--force`
35
+ * path) re-arms the pass, and the pages/profile it wrote are
36
+ * ordinary revisioned wiki writes a human can revert.
37
+ * 6. EXACTLY-ONCE — the marker is claimed with a conditional UPDATE, so N worker
38
+ * replicas polling one tenant produce one run, never N runs.
39
+ *
40
+ * CAP ARITHMETIC (prices re-read from packages/integrations/src/cogs-rates.ts —
41
+ * confirm them there rather than trusting this comment after a re-pricing):
42
+ *
43
+ * serper.search 5 cr per call (cogs-rates: serper.customerOxygen…search)
44
+ * exa.contents 5 cr per CALL (not per URL — this is the whole trick)
45
+ * AI column, medium tier 75 cr per run (pricing-sheet: AI_COLUMN_CREDITS.medium)
46
+ *
47
+ * Happy path = 1 serper.search (5) + 1 exa.contents over <= 5 URLs (5)
48
+ * + 1 medium synthesis pass (75) = 85 cr
49
+ * Enrichment sub-cap 60 cr — ~6x the 10 cr the happy path needs, so a waterfall
50
+ * retry or a second search fits, and a runaway
51
+ * per-URL fetch loop is stopped long before the
52
+ * overall ceiling.
53
+ * Hard cap 300 cr — 85 cr nominal plus room for two retried synthesis
54
+ * passes. 3% of the 10,000-credit signup grant
55
+ * (FREE_SIGNUP_GRANT_CREDITS, ./billing.ts), so the
56
+ * pass can never eat a founder's trial.
57
+ *
58
+ * The obvious implementation — firecrawl.map + 3x firecrawl.scrape — costs
59
+ * 4 x 40 = 160 cr for the SAME job, because Firecrawl bills per page. exa.contents
60
+ * is priced per call and is built for fetching text at known URLs; that is why the
61
+ * read budget is a page COUNT (`KNOWLEDGE_BOOTSTRAP_MAX_PAGES_READ`) inside one
62
+ * call rather than a call count.
63
+ *
64
+ * This module is a leaf: constants and types only, no DB and no provider imports, so
65
+ * the worker, tenant-db, CLI, and API surfaces all bind to one contract.
66
+ */
67
+ import type { HostedAiLevel } from "./hosted-ai.js";
68
+ /** Hard ceiling on managed credits ONE workspace bootstrap may spend, all phases. */
69
+ export declare const KNOWLEDGE_BOOTSTRAP_MAX_CREDITS = 300;
70
+ /**
71
+ * Tighter sub-ceiling on the provider/enrichment half (search + page fetch), i.e.
72
+ * the external-money phase. Reaching it degrades the pass to `partial` — synthesize
73
+ * from what was read — rather than failing it.
74
+ */
75
+ export declare const KNOWLEDGE_BOOTSTRAP_ENRICHMENT_MAX_CREDITS = 60;
76
+ /**
77
+ * Reasoning tier the synthesis pass runs at. `medium` ("Oxygen Balanced") is the
78
+ * deliberate middle: `low` cannot hold a company profile's structure together, and
79
+ * `high` triples the cost of a pass nobody asked for.
80
+ */
81
+ export declare const KNOWLEDGE_BOOTSTRAP_SYNTHESIS_TIER: HostedAiLevel;
82
+ /**
83
+ * Most URLs whose text the pass may read — inside ONE `exa.contents` call, which is
84
+ * priced per call. Raising this raises latency and prompt size, not provider spend;
85
+ * it is a quality/latency knob, and the credit ceilings above remain the money guard.
86
+ */
87
+ export declare const KNOWLEDGE_BOOTSTRAP_MAX_PAGES_READ = 5;
88
+ /** Fleet-wide kill switch, matching OXYGEN_KNOWLEDGE_RETRIEVAL/AUTO_SOURCES_DISABLED. */
89
+ export declare const KNOWLEDGE_BOOTSTRAP_DISABLED_ENV_VAR = "OXYGEN_KNOWLEDGE_BOOTSTRAP_DISABLED";
90
+ /**
91
+ * Is the automatic bootstrap disabled by operator env?
92
+ *
93
+ * Unset = ENABLED (this is a fail-OPEN switch, unlike the free-grant flag): the
94
+ * feature is meant to run for every eligible workspace, and the kill switch exists to
95
+ * stop it fleet-wide in an incident with no deploy. `"1"` or `"true"` disables it;
96
+ * any other value is treated as unset, exactly like the sibling knowledge switches.
97
+ */
98
+ export declare function knowledgeBootstrapDisabled(env?: Record<string, string | undefined>): boolean;
99
+ /**
100
+ * Lifecycle of the once-ever marker.
101
+ *
102
+ * queued — reserved: a surface claimed the slot but has not started work.
103
+ * running — claimed by a worker replica; the pass is in flight.
104
+ * completed — profile + wiki page written.
105
+ * partial — some evidence gathered and written, but a cap, a provider failure, or
106
+ * a thin domain stopped it short. Deliberately TERMINAL.
107
+ * skipped — never attempted for a policy reason (no domain, personal-email
108
+ * signup, kill switch, zero spendable balance). `reason` says which.
109
+ * failed — attempted and errored. Also TERMINAL.
110
+ *
111
+ * Every one of these blocks a second claim: "once per workspace, ever" means the
112
+ * marker's mere EXISTENCE is the lock, not its value. A human re-arms the pass with
113
+ * `--force` (`POST /api/cli/knowledge/bootstrap`, which calls tenant-db's
114
+ * `requeueKnowledgeBootstrap`) — the one deliberate way to spend a second time. It
115
+ * overwrites every status above EXCEPT a `running` marker that is still LIVE, i.e.
116
+ * whose `started_at` is inside {@link KNOWLEDGE_BOOTSTRAP_STUCK_AFTER_MS}; a `running`
117
+ * marker older than that window is stale and IS re-armable, which is how a wedged pass
118
+ * is recovered without hand-written SQL.
119
+ */
120
+ export declare const KNOWLEDGE_BOOTSTRAP_STATUSES: readonly ["queued", "running", "completed", "partial", "skipped", "failed"];
121
+ export type KnowledgeBootstrapStatus = (typeof KNOWLEDGE_BOOTSTRAP_STATUSES)[number];
122
+ export declare function isKnowledgeBootstrapStatus(value: unknown): value is KnowledgeBootstrapStatus;
123
+ /**
124
+ * The marker itself, stored VERBATIM at `knowledge_state.watermarks -> 'bootstrap'`.
125
+ * Field names are snake_case because this type IS the on-disk jsonb shape — keeping
126
+ * them identical means a human reading the row and an agent reading the type see the
127
+ * same thing.
128
+ */
129
+ export type KnowledgeBootstrapMarker = {
130
+ status: KnowledgeBootstrapStatus;
131
+ /** ISO timestamp the claim was won. */
132
+ started_at: string | null;
133
+ /** ISO timestamp a terminal status was written; null while `queued`/`running`. */
134
+ completed_at: string | null;
135
+ /** `ox_runs.runs` id (source_type `system`) carrying the observable run. */
136
+ run_id: string | null;
137
+ /** Managed credits actually spent, across every phase. Never exceeds the cap. */
138
+ credits_used: number;
139
+ /** The workspace domain researched (from organizations.iconDomain). */
140
+ domain: string | null;
141
+ /** Why a pass was skipped/partial/failed — a short machine-ish token, not prose. */
142
+ reason: string | null;
143
+ /** Slugs of the wiki pages this pass created or updated (the URLs it READ are in
144
+ * page_sources — see recordKnowledgeUrlSources). */
145
+ pages: string[];
146
+ /**
147
+ * The credit ceiling the REQUESTER authorized, when a surface reserved this slot
148
+ * (`POST /api/cli/knowledge/bootstrap`, mode `live`, which requires `max_credits`).
149
+ *
150
+ * Absent/null means the automatic pass, which is authorized by signup consent and
151
+ * bounded by `KNOWLEDGE_BOOTSTRAP_MAX_CREDITS` instead. It rides the marker because
152
+ * the surface that takes the authorization is not the process that spends: a
153
+ * request that says "up to 100" and a worker that spends up to 300 is not a capped
154
+ * pass, it is an uncapped one with a friendly form. Always clamped to the platform
155
+ * cap on the way in — a marker can lower the ceiling, never raise it.
156
+ */
157
+ max_credits?: number | null;
158
+ /**
159
+ * Canonical company LinkedIn URL the requester supplied, when they supplied one.
160
+ * The same hint `/setup` stores at KNOWLEDGE_BOOTSTRAP_LINKEDIN_METADATA_KEY, but
161
+ * carried per-request so a one-off `--linkedin` never mutates the workspace's
162
+ * recorded answer.
163
+ */
164
+ linkedin_url?: string | null;
165
+ };
166
+ /**
167
+ * Control-DB `organizations.metadata` key carrying the workspace's recorded consent
168
+ * to the automatic research pass. Written once at signup by the setup surface; read
169
+ * by the worker cycle before anything is spent.
170
+ *
171
+ * It lives in shared, not in either caller, so the writer and the reader cannot
172
+ * drift onto two different key names — a silent drift there would either deny every
173
+ * workspace its bootstrap forever, or (far worse) make the reader see consent that
174
+ * nobody recorded.
175
+ */
176
+ export declare const KNOWLEDGE_BOOTSTRAP_CONSENT_METADATA_KEY = "knowledge_bootstrap_consent";
177
+ /** The recorded consent, exactly as stored under the key above. */
178
+ export type KnowledgeBootstrapConsent = {
179
+ /** ISO timestamp consent was recorded. Must parse; a marker that does not is not consent. */
180
+ granted_at: string;
181
+ /** Where it came from, e.g. `signup`. Informational. */
182
+ source: string | null;
183
+ /** Clerk user who accepted, when the surface knows. Informational. */
184
+ granted_by_clerk_user_id: string | null;
185
+ };
186
+ /**
187
+ * Read consent out of an organization's control-DB metadata blob, FAIL-CLOSED.
188
+ *
189
+ * Absent, malformed, non-object, or carrying an unparseable `granted_at` all read as
190
+ * "not consented" — a workspace is never researched on the strength of a truthy-looking
191
+ * value. This is deliberately stricter than the kill switch beside it: the switch is
192
+ * fail-OPEN because an operator flips it, while consent is fail-CLOSED because its
193
+ * absence is the default state of every workspace that predates the feature, and
194
+ * spending a stranger's credits on a guess is the one outcome the exception cannot
195
+ * survive.
196
+ */
197
+ export declare function readKnowledgeBootstrapConsent(metadata: unknown): KnowledgeBootstrapConsent | null;
198
+ /**
199
+ * Control-DB `organizations.metadata` key carrying the company LinkedIn page the
200
+ * workspace typed at signup, when it typed one.
201
+ *
202
+ * It sits beside the consent key for the same reason: the setup surface writes it and
203
+ * the worker reads it, and two spellings of the key would silently drop a hint the
204
+ * customer took the trouble to give us. The stored value is the canonical form
205
+ * produced by `normalizeLinkedinCompanyUrl` (./dnc-identities.js) — normalize on the
206
+ * way in AND on the way out, never trust the raw string, and never let a personal
207
+ * `/in/` profile through: this is the COMPANY page, and researching a founder's
208
+ * personal profile as if it were the company is exactly the confidently-wrong write
209
+ * the bootstrap's domain gate exists to prevent.
210
+ *
211
+ * Optional by design. Its absence is normal and costs nothing: the research pass
212
+ * resolves the company from the domain and pays for a LinkedIn lookup only when it
213
+ * has a page worth looking up.
214
+ */
215
+ export declare const KNOWLEDGE_BOOTSTRAP_LINKEDIN_METADATA_KEY = "company_linkedin_url";
216
+ /**
217
+ * How long a `running` marker may sit before the workspace is reported as STUCK.
218
+ *
219
+ * Nothing retries it automatically. An unattended retry on a paid path is how a founder
220
+ * gets billed twice for one pass, so a crashed bootstrap stays visibly unfinished until
221
+ * a human re-arms it with `--force` (tenant-db's `requeueKnowledgeBootstrap`).
222
+ *
223
+ * This bound does two things, and the second is load-bearing: it decides when the
224
+ * worker starts SAYING a pass is stuck, AND it is the exact window after which
225
+ * `--force` may re-arm over a `running` marker. Inside the window the re-arm is
226
+ * refused, because the pass may still be spending; outside it the marker is presumed
227
+ * dead and the workspace is recoverable through the product.
228
+ */
229
+ export declare const KNOWLEDGE_BOOTSTRAP_STUCK_AFTER_MS: number;
230
+ /**
231
+ * The one wiki page a bootstrap authors. A stable slug is what makes a re-armed pass
232
+ * REVISE the note rather than litter the wiki with `company-research-2`.
233
+ *
234
+ * It lives here rather than in the worker because it is a PROMISE the preview surface
235
+ * makes before anything runs (`POST /api/cli/knowledge/bootstrap` returns it inside
236
+ * `would_write`, with a deep-link). Two copies of a promised slug is how a dry run
237
+ * starts lying about which page it is going to write.
238
+ */
239
+ export declare const KNOWLEDGE_BOOTSTRAP_RESEARCH_PAGE_SLUG = "company-research";
240
+ /**
241
+ * The typed `company_profile` sections a bootstrap may write — and the exact set the
242
+ * "never clobber a profile a human already filled" gate inspects.
243
+ *
244
+ * `gtm_stack` and `custom` are structurally excluded: research of a company's public
245
+ * web presence says nothing about which tools it has connected here, and a workspace
246
+ * that filled only those has told us nothing about its company and must still be
247
+ * bootstrapped. Same reasoning as the slug above — the preview promises this list.
248
+ */
249
+ export declare const KNOWLEDGE_BOOTSTRAP_PROFILE_SECTIONS: readonly ["company", "offering", "icp", "market"];
250
+ /**
251
+ * Mirrors the four `CONTEXT_PROFILE_SECTIONS` members above. Declared locally because
252
+ * this module is a leaf (no tenant-db import); the union stays assignable to
253
+ * `ContextProfileSection`, so the worker keeps its real type with no cast.
254
+ */
255
+ export type KnowledgeBootstrapProfileSection = "company" | "offering" | "icp" | "market";
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Knowledge bootstrap — the once-per-workspace, automatic company research pass.
3
+ *
4
+ * WHAT IT IS. Exactly once in a workspace's life, OXYGEN researches the customer's
5
+ * OWN company from the domain we already resolved at org creation
6
+ * (control-DB `organizations.iconDomain`, `iconStatus = 'ok'`), then fills the typed
7
+ * company profile (`ox_context.company_profile`) and writes one cited wiki page.
8
+ * A founder who signs up should not face an empty Knowledge Graph and have to type
9
+ * their own positioning back at us; every grounded AI action downstream (message
10
+ * drafts, AI columns, agent runs) reads that profile, so an empty one degrades the
11
+ * whole product's first hour.
12
+ *
13
+ * WHY THIS IS NOT A VIOLATION OF THE PAID-ACTIONS RULE. `CLAUDE.md` says never run
14
+ * paid provider actions unless the user explicitly asks. This slice is a deliberate,
15
+ * founder-approved exception: signup IS the standing authorization, the same way an
16
+ * armed table-webhook auto-run configuration is scoped standing permission for the
17
+ * columns it queues. The exception is defensible ONLY because every one of the
18
+ * following properties holds. They are load-bearing — do not drop one for
19
+ * convenience, and if you remove one, the exception no longer stands:
20
+ *
21
+ * 1. CAPPED — `KNOWLEDGE_BOOTSTRAP_MAX_CREDITS` is a hard ceiling on the whole
22
+ * pass, with `KNOWLEDGE_BOOTSTRAP_ENRICHMENT_MAX_CREDITS` a tighter
23
+ * sub-ceiling on the provider (external-money) half.
24
+ * 2. CONSENTED — it runs only for a workspace whose own domain we already derived
25
+ * at signup; a personal-email signup is `skipped_personal_email`
26
+ * upstream and never reaches here, so we never research a stranger.
27
+ * 3. JOURNALLED — the marker below records status, timing, run id, credits and the
28
+ * domain in `knowledge_state.watermarks`; the URLs read land as
29
+ * immutable `page_sources` rows plus an `ingest` knowledge_log
30
+ * entry. Nothing about the spend is invisible.
31
+ * 4. KILL-SWITCHED — `OXYGEN_KNOWLEDGE_BOOTSTRAP_DISABLED` stops it fleet-wide at
32
+ * the worker with no deploy, matching the other OXYGEN_KNOWLEDGE_*
33
+ * switches.
34
+ * 5. REVERSIBLE — the marker is a single jsonb key; clearing it (the `--force`
35
+ * path) re-arms the pass, and the pages/profile it wrote are
36
+ * ordinary revisioned wiki writes a human can revert.
37
+ * 6. EXACTLY-ONCE — the marker is claimed with a conditional UPDATE, so N worker
38
+ * replicas polling one tenant produce one run, never N runs.
39
+ *
40
+ * CAP ARITHMETIC (prices re-read from packages/integrations/src/cogs-rates.ts —
41
+ * confirm them there rather than trusting this comment after a re-pricing):
42
+ *
43
+ * serper.search 5 cr per call (cogs-rates: serper.customerOxygen…search)
44
+ * exa.contents 5 cr per CALL (not per URL — this is the whole trick)
45
+ * AI column, medium tier 75 cr per run (pricing-sheet: AI_COLUMN_CREDITS.medium)
46
+ *
47
+ * Happy path = 1 serper.search (5) + 1 exa.contents over <= 5 URLs (5)
48
+ * + 1 medium synthesis pass (75) = 85 cr
49
+ * Enrichment sub-cap 60 cr — ~6x the 10 cr the happy path needs, so a waterfall
50
+ * retry or a second search fits, and a runaway
51
+ * per-URL fetch loop is stopped long before the
52
+ * overall ceiling.
53
+ * Hard cap 300 cr — 85 cr nominal plus room for two retried synthesis
54
+ * passes. 3% of the 10,000-credit signup grant
55
+ * (FREE_SIGNUP_GRANT_CREDITS, ./billing.ts), so the
56
+ * pass can never eat a founder's trial.
57
+ *
58
+ * The obvious implementation — firecrawl.map + 3x firecrawl.scrape — costs
59
+ * 4 x 40 = 160 cr for the SAME job, because Firecrawl bills per page. exa.contents
60
+ * is priced per call and is built for fetching text at known URLs; that is why the
61
+ * read budget is a page COUNT (`KNOWLEDGE_BOOTSTRAP_MAX_PAGES_READ`) inside one
62
+ * call rather than a call count.
63
+ *
64
+ * This module is a leaf: constants and types only, no DB and no provider imports, so
65
+ * the worker, tenant-db, CLI, and API surfaces all bind to one contract.
66
+ */
67
+ /** Hard ceiling on managed credits ONE workspace bootstrap may spend, all phases. */
68
+ export const KNOWLEDGE_BOOTSTRAP_MAX_CREDITS = 300;
69
+ /**
70
+ * Tighter sub-ceiling on the provider/enrichment half (search + page fetch), i.e.
71
+ * the external-money phase. Reaching it degrades the pass to `partial` — synthesize
72
+ * from what was read — rather than failing it.
73
+ */
74
+ export const KNOWLEDGE_BOOTSTRAP_ENRICHMENT_MAX_CREDITS = 60;
75
+ /**
76
+ * Reasoning tier the synthesis pass runs at. `medium` ("Oxygen Balanced") is the
77
+ * deliberate middle: `low` cannot hold a company profile's structure together, and
78
+ * `high` triples the cost of a pass nobody asked for.
79
+ */
80
+ export const KNOWLEDGE_BOOTSTRAP_SYNTHESIS_TIER = "medium";
81
+ /**
82
+ * Most URLs whose text the pass may read — inside ONE `exa.contents` call, which is
83
+ * priced per call. Raising this raises latency and prompt size, not provider spend;
84
+ * it is a quality/latency knob, and the credit ceilings above remain the money guard.
85
+ */
86
+ export const KNOWLEDGE_BOOTSTRAP_MAX_PAGES_READ = 5;
87
+ /** Fleet-wide kill switch, matching OXYGEN_KNOWLEDGE_RETRIEVAL/AUTO_SOURCES_DISABLED. */
88
+ export const KNOWLEDGE_BOOTSTRAP_DISABLED_ENV_VAR = "OXYGEN_KNOWLEDGE_BOOTSTRAP_DISABLED";
89
+ /**
90
+ * Is the automatic bootstrap disabled by operator env?
91
+ *
92
+ * Unset = ENABLED (this is a fail-OPEN switch, unlike the free-grant flag): the
93
+ * feature is meant to run for every eligible workspace, and the kill switch exists to
94
+ * stop it fleet-wide in an incident with no deploy. `"1"` or `"true"` disables it;
95
+ * any other value is treated as unset, exactly like the sibling knowledge switches.
96
+ */
97
+ export function knowledgeBootstrapDisabled(env) {
98
+ const value = (env ?? process.env)[KNOWLEDGE_BOOTSTRAP_DISABLED_ENV_VAR];
99
+ return value === "1" || value === "true";
100
+ }
101
+ /**
102
+ * Lifecycle of the once-ever marker.
103
+ *
104
+ * queued — reserved: a surface claimed the slot but has not started work.
105
+ * running — claimed by a worker replica; the pass is in flight.
106
+ * completed — profile + wiki page written.
107
+ * partial — some evidence gathered and written, but a cap, a provider failure, or
108
+ * a thin domain stopped it short. Deliberately TERMINAL.
109
+ * skipped — never attempted for a policy reason (no domain, personal-email
110
+ * signup, kill switch, zero spendable balance). `reason` says which.
111
+ * failed — attempted and errored. Also TERMINAL.
112
+ *
113
+ * Every one of these blocks a second claim: "once per workspace, ever" means the
114
+ * marker's mere EXISTENCE is the lock, not its value. A human re-arms the pass with
115
+ * `--force` (`POST /api/cli/knowledge/bootstrap`, which calls tenant-db's
116
+ * `requeueKnowledgeBootstrap`) — the one deliberate way to spend a second time. It
117
+ * overwrites every status above EXCEPT a `running` marker that is still LIVE, i.e.
118
+ * whose `started_at` is inside {@link KNOWLEDGE_BOOTSTRAP_STUCK_AFTER_MS}; a `running`
119
+ * marker older than that window is stale and IS re-armable, which is how a wedged pass
120
+ * is recovered without hand-written SQL.
121
+ */
122
+ export const KNOWLEDGE_BOOTSTRAP_STATUSES = [
123
+ "queued",
124
+ "running",
125
+ "completed",
126
+ "partial",
127
+ "skipped",
128
+ "failed",
129
+ ];
130
+ export function isKnowledgeBootstrapStatus(value) {
131
+ return (typeof value === "string" &&
132
+ KNOWLEDGE_BOOTSTRAP_STATUSES.includes(value));
133
+ }
134
+ // ---------------------------------------------------------------------------
135
+ // Consent (property 2 of the exception above, made explicit)
136
+ // ---------------------------------------------------------------------------
137
+ /**
138
+ * Control-DB `organizations.metadata` key carrying the workspace's recorded consent
139
+ * to the automatic research pass. Written once at signup by the setup surface; read
140
+ * by the worker cycle before anything is spent.
141
+ *
142
+ * It lives in shared, not in either caller, so the writer and the reader cannot
143
+ * drift onto two different key names — a silent drift there would either deny every
144
+ * workspace its bootstrap forever, or (far worse) make the reader see consent that
145
+ * nobody recorded.
146
+ */
147
+ export const KNOWLEDGE_BOOTSTRAP_CONSENT_METADATA_KEY = "knowledge_bootstrap_consent";
148
+ /**
149
+ * Read consent out of an organization's control-DB metadata blob, FAIL-CLOSED.
150
+ *
151
+ * Absent, malformed, non-object, or carrying an unparseable `granted_at` all read as
152
+ * "not consented" — a workspace is never researched on the strength of a truthy-looking
153
+ * value. This is deliberately stricter than the kill switch beside it: the switch is
154
+ * fail-OPEN because an operator flips it, while consent is fail-CLOSED because its
155
+ * absence is the default state of every workspace that predates the feature, and
156
+ * spending a stranger's credits on a guess is the one outcome the exception cannot
157
+ * survive.
158
+ */
159
+ export function readKnowledgeBootstrapConsent(metadata) {
160
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
161
+ return null;
162
+ const raw = metadata[KNOWLEDGE_BOOTSTRAP_CONSENT_METADATA_KEY];
163
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
164
+ return null;
165
+ const record = raw;
166
+ const grantedAt = record.granted_at;
167
+ if (typeof grantedAt !== "string" || grantedAt.length === 0)
168
+ return null;
169
+ if (!Number.isFinite(Date.parse(grantedAt)))
170
+ return null;
171
+ return {
172
+ granted_at: grantedAt,
173
+ source: typeof record.source === "string" && record.source.length > 0 ? record.source : null,
174
+ granted_by_clerk_user_id: typeof record.granted_by_clerk_user_id === "string" && record.granted_by_clerk_user_id.length > 0
175
+ ? record.granted_by_clerk_user_id
176
+ : null,
177
+ };
178
+ }
179
+ /**
180
+ * Control-DB `organizations.metadata` key carrying the company LinkedIn page the
181
+ * workspace typed at signup, when it typed one.
182
+ *
183
+ * It sits beside the consent key for the same reason: the setup surface writes it and
184
+ * the worker reads it, and two spellings of the key would silently drop a hint the
185
+ * customer took the trouble to give us. The stored value is the canonical form
186
+ * produced by `normalizeLinkedinCompanyUrl` (./dnc-identities.js) — normalize on the
187
+ * way in AND on the way out, never trust the raw string, and never let a personal
188
+ * `/in/` profile through: this is the COMPANY page, and researching a founder's
189
+ * personal profile as if it were the company is exactly the confidently-wrong write
190
+ * the bootstrap's domain gate exists to prevent.
191
+ *
192
+ * Optional by design. Its absence is normal and costs nothing: the research pass
193
+ * resolves the company from the domain and pays for a LinkedIn lookup only when it
194
+ * has a page worth looking up.
195
+ */
196
+ export const KNOWLEDGE_BOOTSTRAP_LINKEDIN_METADATA_KEY = "company_linkedin_url";
197
+ /**
198
+ * How long a `running` marker may sit before the workspace is reported as STUCK.
199
+ *
200
+ * Nothing retries it automatically. An unattended retry on a paid path is how a founder
201
+ * gets billed twice for one pass, so a crashed bootstrap stays visibly unfinished until
202
+ * a human re-arms it with `--force` (tenant-db's `requeueKnowledgeBootstrap`).
203
+ *
204
+ * This bound does two things, and the second is load-bearing: it decides when the
205
+ * worker starts SAYING a pass is stuck, AND it is the exact window after which
206
+ * `--force` may re-arm over a `running` marker. Inside the window the re-arm is
207
+ * refused, because the pass may still be spending; outside it the marker is presumed
208
+ * dead and the workspace is recoverable through the product.
209
+ */
210
+ export const KNOWLEDGE_BOOTSTRAP_STUCK_AFTER_MS = 60 * 60 * 1000;
211
+ // ---------------------------------------------------------------------------
212
+ // Write targets (what a bootstrap actually changes)
213
+ // ---------------------------------------------------------------------------
214
+ /**
215
+ * The one wiki page a bootstrap authors. A stable slug is what makes a re-armed pass
216
+ * REVISE the note rather than litter the wiki with `company-research-2`.
217
+ *
218
+ * It lives here rather than in the worker because it is a PROMISE the preview surface
219
+ * makes before anything runs (`POST /api/cli/knowledge/bootstrap` returns it inside
220
+ * `would_write`, with a deep-link). Two copies of a promised slug is how a dry run
221
+ * starts lying about which page it is going to write.
222
+ */
223
+ export const KNOWLEDGE_BOOTSTRAP_RESEARCH_PAGE_SLUG = "company-research";
224
+ /**
225
+ * The typed `company_profile` sections a bootstrap may write — and the exact set the
226
+ * "never clobber a profile a human already filled" gate inspects.
227
+ *
228
+ * `gtm_stack` and `custom` are structurally excluded: research of a company's public
229
+ * web presence says nothing about which tools it has connected here, and a workspace
230
+ * that filled only those has told us nothing about its company and must still be
231
+ * bootstrapped. Same reasoning as the slug above — the preview promises this list.
232
+ */
233
+ export const KNOWLEDGE_BOOTSTRAP_PROFILE_SECTIONS = [
234
+ "company",
235
+ "offering",
236
+ "icp",
237
+ "market",
238
+ ];
@@ -37,4 +37,7 @@ export declare const KNOWLEDGE_GRAPH_MAX_NODES = 2000;
37
37
  export declare const KNOWLEDGE_GRAPH_DEFAULT_DEPTH = 1;
38
38
  export declare const KNOWLEDGE_GRAPH_MAX_DEPTH = 3;
39
39
  export declare const KNOWLEDGE_SYNC_MAX_PAGE_BATCH = 200;
40
+ export declare const KNOWLEDGE_STUB_MARKER = "Stub \u2014 fill me.";
41
+ export declare const KNOWLEDGE_BOOTSTRAP_COMMAND = "oxygen knowledge bootstrap";
42
+ export declare const KNOWLEDGE_BOOTSTRAP_AVAILABLE = true;
40
43
  export declare const RESERVED_KNOWLEDGE_FRONTMATTER_KEYS: readonly ["oxygen_page", "id", "slug", "type", "title", "status", "tags", "canonical", "summary", "revision", "updated_at", "web_url"];
@@ -123,6 +123,25 @@ export const KNOWLEDGE_GRAPH_MAX_NODES = 2_000;
123
123
  export const KNOWLEDGE_GRAPH_DEFAULT_DEPTH = 1;
124
124
  export const KNOWLEDGE_GRAPH_MAX_DEPTH = 3;
125
125
  export const KNOWLEDGE_SYNC_MAX_PAGE_BATCH = 200;
126
+ // The domain-grounded scaffold-fill command. KNOWLEDGE_BOOTSTRAP_AVAILABLE is the
127
+ // single gate: while it is false, `oxygen knowledge lint` reports researchable stubs
128
+ // as FACTS (which stubs are still empty, and whether the workspace has a domain a
129
+ // research pass could use) but never names a command that would 404 — the repo's
130
+ // "no placeholder commands" rule. The bootstrap slice flips this one constant and
131
+ // every surface lights up unchanged. NOT dead code: do not delete it in a cleanup pass.
132
+ // The literal line every seeded stub body opens with. It is load-bearing in TWO places,
133
+ // which is why it lives here rather than being inlined:
134
+ // - retrieval (knowledge-retrieval.ts) refuses any page still carrying it, so
135
+ // scaffolding text can never reach a customer's cold email;
136
+ // - lint (knowledge-lint.ts) counts such a page as an unfilled stub.
137
+ // Both used to key off `revision = 1` instead, which a status-only flip defeats:
138
+ // `page upsert <slug> --status active` bumps the revision without touching the body, so
139
+ // the page became simultaneously RETRIEVABLE and invisible to lint — the one state a
140
+ // draft stub exists to prevent. The marker survives that flip; the revision does not.
141
+ // Changing this string means changing every seeded body in knowledge-seed-content.ts.
142
+ export const KNOWLEDGE_STUB_MARKER = "Stub — fill me.";
143
+ export const KNOWLEDGE_BOOTSTRAP_COMMAND = "oxygen knowledge bootstrap";
144
+ export const KNOWLEDGE_BOOTSTRAP_AVAILABLE = true;
126
145
  // Frontmatter keys the deterministic renderer owns; user `data` keys colliding
127
146
  // with these are skipped at render time (the stored jsonb keeps them).
128
147
  export const RESERVED_KNOWLEDGE_FRONTMATTER_KEYS = [
@@ -1,7 +1,9 @@
1
1
  import type { KnowledgePageType, KnowledgePageStatus } from "./knowledge-constants.js";
2
2
  /** Monotonic scaffold cursor stored in knowledge_state.scaffold_version. Bump to ship a
3
- * new wave of starter pages (tag them with a higher minScaffoldVersion). */
4
- export declare const KNOWLEDGE_SCAFFOLD_VERSION = 1;
3
+ * new wave of starter pages (tag them with a higher minScaffoldVersion).
4
+ * v1: the 9-page core. v2: twelve GTM stubs (voice/brand/messaging/objections, three
5
+ * motion playbooks + qualification, and the personas/customers/research/metrics hubs). */
6
+ export declare const KNOWLEDGE_SCAFFOLD_VERSION = 2;
5
7
  export type KnowledgeScaffoldPage = {
6
8
  /** Immutable slug; must be valid kebab grammar and NOT a reserved slug. */
7
9
  slug: string;
@@ -14,11 +16,26 @@ export type KnowledgeScaffoldPage = {
14
16
  body: string;
15
17
  /** The first scaffold version that includes this page — seeded when current < this <= target. */
16
18
  minScaffoldVersion: number;
19
+ /** True when a research pass over the company's PUBLIC web presence could draft a real
20
+ * first version of this page (what they sell, who they say they sell to, how they
21
+ * sound, who bought). Internal-only pages (playbooks, qualification, metrics, research
22
+ * notes) are never researchable — the public web does not know them. Consumed by
23
+ * `oxygen knowledge lint`'s researchable check. */
24
+ researchable?: boolean;
17
25
  };
18
26
  /**
19
- * The 9-page starter pack. Two active nav pages first (start-here + the conventions schema
20
- * page), then the seven draft stub hubs (tag `seed`). `positioning` seeds as a NON-canonical
21
- * draft so it writes freely — pinning canonical positioning stays a separate human-approved
22
- * act. All at minScaffoldVersion 1 (the first wave).
27
+ * The 21-page starter pack. Two active nav pages first (start-here + the conventions
28
+ * schema page), then nineteen draft stub hubs (tag `seed`) — seven from wave 1 and
29
+ * twelve from wave 2. `positioning` seeds as a NON-canonical draft so it writes freely;
30
+ * pinning canonical positioning stays a separate human-approved act. Wave 2 adds ZERO
31
+ * active pages, per the binding rule at the top of this file.
23
32
  */
24
33
  export declare const KNOWLEDGE_SEED_PAGES: readonly KnowledgeScaffoldPage[];
34
+ /**
35
+ * Seed slugs a domain-grounded research pass could genuinely draft from the company's
36
+ * PUBLIC web presence — what they sell, who they say they sell to, how they sound, who
37
+ * bought. Derived from the manifest's `researchable` flag (never a second hand-kept list),
38
+ * and consumed by `oxygen knowledge lint`'s researchable check, which reports the ones
39
+ * still sitting unfilled at revision 1 on a workspace that has a company domain.
40
+ */
41
+ export declare const KNOWLEDGE_RESEARCHABLE_SEED_SLUGS: readonly string[];