@oxygen-agent/cli 1.1011.97 → 1.1011.288
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 +1 -1
- package/dist/admin-primary-providers-render.js +25 -5
- package/dist/column-run-notices.d.ts +16 -0
- package/dist/column-run-notices.js +57 -0
- package/dist/command-manifest.js +10 -1
- package/dist/help.d.ts +27 -1
- package/dist/help.js +112 -29
- package/dist/http-client.js +8 -0
- package/dist/index.js +348 -127
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +20 -69
- package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +6 -4
- package/node_modules/@oxygen/shared/dist/byok-connect.js +11 -4
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +127 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +286 -24
- package/node_modules/@oxygen/shared/dist/collab.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/collab.js +12 -0
- package/node_modules/@oxygen/shared/dist/column-decision.js +7 -3
- package/node_modules/@oxygen/shared/dist/column-output-fields.js +45 -3
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +20 -13
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +29 -13
- package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
- package/node_modules/@oxygen/shared/dist/credit-guidance.js +12 -1
- package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +37 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +79 -44
- package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/linkedin-url.js +9 -0
- package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/log.js +15 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.js +6 -2
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +7 -3
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +22 -1
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +3 -7
- package/node_modules/@oxygen/shared/dist/plan-limits.js +2 -14
- package/node_modules/@oxygen/shared/dist/sending-schedule.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/sending-schedule.js +41 -0
- package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +20 -0
- package/node_modules/@oxygen/shared/dist/sequence-sender-variables.d.ts +35 -0
- package/node_modules/@oxygen/shared/dist/sequence-sender-variables.js +63 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +13 -7
- package/node_modules/@oxygen/shared/dist/sequences.js +26 -0
- package/node_modules/@oxygen/shared/dist/signup-lead-payload.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/signup-lead-payload.js +5 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-series.js +1 -15
- package/node_modules/@oxygen/shared/dist/standard-webhooks.d.ts +51 -0
- package/node_modules/@oxygen/shared/dist/standard-webhooks.js +124 -0
- package/node_modules/@oxygen/shared/dist/tags.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/tags.js +13 -1
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +11 -7
- package/node_modules/@oxygen/shared/dist/ugc.js +14 -6
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/zoned-time.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/zoned-time.js +81 -0
- package/package.json +1 -1
|
@@ -224,6 +224,43 @@ const ENRICHMENT_CELL_SCHEMA = {
|
|
|
224
224
|
reused_from: { type: ["string", "null"], description: "When the reused value was enriched." },
|
|
225
225
|
},
|
|
226
226
|
};
|
|
227
|
+
/**
|
|
228
|
+
* The work-email column's personal email option (founder, 2026-09-29): the
|
|
229
|
+
* personal-email chain's own result, written into the same cell. Declared only
|
|
230
|
+
* when the column selects it — the predicate mirrors
|
|
231
|
+
* `isPersonalEmailOptionEnabled` in @oxygen/integrations, which this leaf
|
|
232
|
+
* cannot import.
|
|
233
|
+
*/
|
|
234
|
+
const PERSONAL_EMAIL_CELL_PROPERTY = {
|
|
235
|
+
type: "object",
|
|
236
|
+
description: "The personal email option's result for this row.",
|
|
237
|
+
properties: {
|
|
238
|
+
value: { type: ["string", "null"], description: "The personal (non-work) email, when one was found." },
|
|
239
|
+
source: { type: ["string", "null"], description: "Provider that produced it." },
|
|
240
|
+
confidence: { type: ["string", "null"], description: "Verifier grade of the address." },
|
|
241
|
+
status: { type: ["string", "null"], description: "Outcome of the personal-email chain." },
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
const PERSONAL_EMAIL_FIELD_LABELS = {
|
|
245
|
+
personal_email: "Personal email result",
|
|
246
|
+
"personal_email.value": "Personal email",
|
|
247
|
+
"personal_email.source": "Personal email source",
|
|
248
|
+
"personal_email.confidence": "Personal email confidence",
|
|
249
|
+
"personal_email.status": "Personal email status",
|
|
250
|
+
};
|
|
251
|
+
function enrichmentCellSchema(definition) {
|
|
252
|
+
const option = definition.personalEmail;
|
|
253
|
+
if (definition.intent !== "find_email" || !isRecord(option) || option.enabled !== true) {
|
|
254
|
+
return ENRICHMENT_CELL_SCHEMA;
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
...ENRICHMENT_CELL_SCHEMA,
|
|
258
|
+
properties: {
|
|
259
|
+
...ENRICHMENT_CELL_SCHEMA.properties,
|
|
260
|
+
personal_email: PERSONAL_EMAIL_CELL_PROPERTY,
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
227
264
|
/** The shape a `bind` cell is written with. */
|
|
228
265
|
const BIND_CELL_SCHEMA = {
|
|
229
266
|
type: "object",
|
|
@@ -406,14 +443,19 @@ function resolveByKind(column, definition, options) {
|
|
|
406
443
|
return researchContract(definition);
|
|
407
444
|
case "tool":
|
|
408
445
|
return toolContract(definition, options);
|
|
409
|
-
case "enrichment":
|
|
446
|
+
case "enrichment": {
|
|
447
|
+
const schema = enrichmentCellSchema(definition);
|
|
410
448
|
return {
|
|
411
|
-
schema
|
|
412
|
-
fields: collectJsonSchemaFields(
|
|
449
|
+
schema,
|
|
450
|
+
fields: collectJsonSchemaFields(schema).map((raw) => {
|
|
451
|
+
const label = PERSONAL_EMAIL_FIELD_LABELS[raw.segments.join(".")];
|
|
452
|
+
return toField(label ? { ...raw, label } : raw, "enrichment_envelope");
|
|
453
|
+
}),
|
|
413
454
|
// `{{work_email}}` means the email, not the envelope around it.
|
|
414
455
|
primaryField: "value",
|
|
415
456
|
serverManaged: true,
|
|
416
457
|
};
|
|
458
|
+
}
|
|
417
459
|
case "bind":
|
|
418
460
|
return {
|
|
419
461
|
schema: BIND_CELL_SCHEMA,
|
|
@@ -54,11 +54,10 @@ export declare function isDerivedCompanyEnrichmentField(field: CompanyEnrichment
|
|
|
54
54
|
*/
|
|
55
55
|
export declare const DEFAULT_COMPANY_FIND_FIELDS: readonly CompanyEnrichmentFieldKey[];
|
|
56
56
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* default bundle's per-row ceiling stays the profile chain's.
|
|
57
|
+
* The identity/firmographic set one company profile lookup answers — the scope
|
|
58
|
+
* of the `company_enrich` bundle, which since D56 (2026-09-24) fills it from
|
|
59
|
+
* `oxygen.company_profile` with no field choices. Every other field is in
|
|
60
|
+
* exactly one COMPANY_ENRICHMENT_FIELD_GROUPS entry below.
|
|
62
61
|
*
|
|
63
62
|
* Funding stage and latest round left the default on 2026-09-24 (founder, D55):
|
|
64
63
|
* the profile lane is Blitz first now, and Blitz's company profile carries no
|
|
@@ -66,7 +65,7 @@ export declare const DEFAULT_COMPANY_FIND_FIELDS: readonly CompanyEnrichmentFiel
|
|
|
66
65
|
* live in the Funding details group instead.
|
|
67
66
|
*/
|
|
68
67
|
export declare const DEFAULT_COMPANY_PRESET_FIELDS: readonly CompanyEnrichmentFieldKey[];
|
|
69
|
-
export type CompanyEnrichmentFieldGroupId = "profile_details" | "funding_details" | "job_openings" | "tech_stack" | "technology_check";
|
|
68
|
+
export type CompanyEnrichmentFieldGroupId = "profile_details" | "funding_details" | "revenue" | "job_openings" | "tech_stack" | "technology_check";
|
|
70
69
|
export type CompanyEnrichmentFieldGroup = {
|
|
71
70
|
id: CompanyEnrichmentFieldGroupId;
|
|
72
71
|
/** Picker row and catalog label, in customer words. */
|
|
@@ -89,13 +88,21 @@ export type CompanyEnrichmentFieldGroup = {
|
|
|
89
88
|
requires?: "check_technologies";
|
|
90
89
|
};
|
|
91
90
|
/**
|
|
92
|
-
* The
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
91
|
+
* The company fields beyond the default set, grouped the way a customer asks
|
|
92
|
+
* for them ("add their funding", "add their tech stack"). The groups partition
|
|
93
|
+
* exactly the non-default, non-parked fields (pinned by
|
|
94
|
+
* company-enrichment-fields.test.ts).
|
|
95
|
+
*
|
|
96
|
+
* The `company_enrich` bundle takes none of them: since D56 (2026-09-24) it is
|
|
97
|
+
* one profile lookup with no field choices, and the widen-on-reapply path that
|
|
98
|
+
* folded a group into its column is gone. A lane-priced group is instead its
|
|
99
|
+
* OWN table bundle, which runs the `find company` field waterfall for exactly
|
|
100
|
+
* the group's fields in its own paid column (@oxygen/tenant-db
|
|
101
|
+
* enrichment-column-presets.ts): `funding_details` is `--preset
|
|
102
|
+
* company_funding`, `revenue` is `company_revenue`, `job_openings` is
|
|
103
|
+
* `company_hiring` and `tech_stack` is `company_tech_stack` (2026-09-29).
|
|
104
|
+
* `profile_details` and `technology_check` have no bundle; `find company
|
|
105
|
+
* --fields` answers them for one company at a time.
|
|
99
106
|
*/
|
|
100
107
|
export declare const COMPANY_ENRICHMENT_FIELD_GROUPS: readonly CompanyEnrichmentFieldGroup[];
|
|
101
108
|
export declare function companyEnrichmentFieldGroup(id: string): CompanyEnrichmentFieldGroup | null;
|
|
@@ -440,11 +440,10 @@ export const DEFAULT_COMPANY_FIND_FIELDS = [
|
|
|
440
440
|
"hq_country",
|
|
441
441
|
];
|
|
442
442
|
/**
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
* default bundle's per-row ceiling stays the profile chain's.
|
|
443
|
+
* The identity/firmographic set one company profile lookup answers — the scope
|
|
444
|
+
* of the `company_enrich` bundle, which since D56 (2026-09-24) fills it from
|
|
445
|
+
* `oxygen.company_profile` with no field choices. Every other field is in
|
|
446
|
+
* exactly one COMPANY_ENRICHMENT_FIELD_GROUPS entry below.
|
|
448
447
|
*
|
|
449
448
|
* Funding stage and latest round left the default on 2026-09-24 (founder, D55):
|
|
450
449
|
* the profile lane is Blitz first now, and Blitz's company profile carries no
|
|
@@ -462,13 +461,21 @@ export const DEFAULT_COMPANY_PRESET_FIELDS = [
|
|
|
462
461
|
"hq_country",
|
|
463
462
|
];
|
|
464
463
|
/**
|
|
465
|
-
* The
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
464
|
+
* The company fields beyond the default set, grouped the way a customer asks
|
|
465
|
+
* for them ("add their funding", "add their tech stack"). The groups partition
|
|
466
|
+
* exactly the non-default, non-parked fields (pinned by
|
|
467
|
+
* company-enrichment-fields.test.ts).
|
|
468
|
+
*
|
|
469
|
+
* The `company_enrich` bundle takes none of them: since D56 (2026-09-24) it is
|
|
470
|
+
* one profile lookup with no field choices, and the widen-on-reapply path that
|
|
471
|
+
* folded a group into its column is gone. A lane-priced group is instead its
|
|
472
|
+
* OWN table bundle, which runs the `find company` field waterfall for exactly
|
|
473
|
+
* the group's fields in its own paid column (@oxygen/tenant-db
|
|
474
|
+
* enrichment-column-presets.ts): `funding_details` is `--preset
|
|
475
|
+
* company_funding`, `revenue` is `company_revenue`, `job_openings` is
|
|
476
|
+
* `company_hiring` and `tech_stack` is `company_tech_stack` (2026-09-29).
|
|
477
|
+
* `profile_details` and `technology_check` have no bundle; `find company
|
|
478
|
+
* --fields` answers them for one company at a time.
|
|
472
479
|
*/
|
|
473
480
|
export const COMPANY_ENRICHMENT_FIELD_GROUPS = [
|
|
474
481
|
{
|
|
@@ -482,7 +489,16 @@ export const COMPANY_ENRICHMENT_FIELD_GROUPS = [
|
|
|
482
489
|
id: "funding_details",
|
|
483
490
|
label: "Funding details",
|
|
484
491
|
description: "Funding stage, latest round, total raised, number of rounds and investors, with the full funding history kept alongside.",
|
|
485
|
-
fields: ["funding", "
|
|
492
|
+
fields: ["funding", "funding_stage", "latest_funding_round", "total_funding", "funding_rounds_count", "investors"],
|
|
493
|
+
cost: "extra",
|
|
494
|
+
},
|
|
495
|
+
// Its own group since 2026-09-29: revenue is its own bundle and chain (D50),
|
|
496
|
+
// so a customer who wants it never buys the funding lookup to get it.
|
|
497
|
+
{
|
|
498
|
+
id: "revenue",
|
|
499
|
+
label: "Revenue (estimate)",
|
|
500
|
+
description: "Estimated annual revenue, as a figure or a range.",
|
|
501
|
+
fields: ["revenue"],
|
|
486
502
|
cost: "extra",
|
|
487
503
|
},
|
|
488
504
|
{
|
|
@@ -74,7 +74,7 @@ export const SENDER_LIMITS_SET_OPERATION = {
|
|
|
74
74
|
contract: SENDER_LIMITS_PATCH_CONTRACT,
|
|
75
75
|
cli: {
|
|
76
76
|
path: ["senders", "limits", "set"],
|
|
77
|
-
summary: "Adjust per-account action limits and working hours. LinkedIn actions only run inside the account's working hours (default 07:00-22:00 every day, in the account's timezone); work due outside them is deferred to the next window, not failed. Set them with --timezone, --working-days, --working-hours-start and --working-hours-end; flags you omit keep their current value. A sequence's
|
|
77
|
+
summary: "Adjust per-account action limits and working hours. LinkedIn actions only run inside the account's working hours (default 07:00-22:00 every day, in the account's timezone); work due outside them is deferred to the next window, not failed. Set them with --timezone, --working-days, --working-hours-start and --working-hours-end; flags you omit keep their current value. A sequence's sending schedule (sequences update --schedule-*, or --linkedin-* for LinkedIn alone) can narrow the window for its LinkedIn steps, never widen it; email steps follow the sequence's email send window instead. Every limit flag shows its default and hard maximum; values above the maximum are clamped, and higher caps raise the risk of a LinkedIn restriction. Each action has its own cap (InMails per day and per rolling 30 days, profile views, profile lookups, endorsements, invitation withdrawals, posts), and every send except posts also counts toward --total-actions-per-day. Daily send caps run at half on Saturday and Sunday in the account's timezone unless --weekend-full-volume is set. A withdrawn invitation cannot be re-sent to the same person for 21 days. Own-feed posts and your own Unibox replies ignore working hours. A refused action names its reason (outside_working_hours defers it; linkedin_rate_limited names the limit and resets_at). Changing limits is free. <id> accepts a sender account id, connection id, or Unipile account id.",
|
|
78
78
|
argument: { name: "id", description: "Sender account id, connection id, or Unipile account id." },
|
|
79
79
|
},
|
|
80
80
|
mcp: {
|
|
@@ -14,8 +14,8 @@ export declare const COPILOT_SKILL_SNAPSHOTS_GENERATED: readonly [{
|
|
|
14
14
|
readonly slug: "linkedin-content-strategy";
|
|
15
15
|
readonly title: "Playbook: LinkedIn content strategy";
|
|
16
16
|
readonly sources: readonly ["oxygen-playbooks/playbooks/linkedin-content-strategy.md"];
|
|
17
|
-
readonly sha256: "
|
|
18
|
-
readonly content: "---\nname: linkedin-content-strategy\ndescription: \"Install and run the founder LinkedIn content engine: a wiki-grounded strategy page, an idea backlog, a week of drafts approved post by post, scheduled publishing, answered comments, weekly engagement read-back, outlier mining, and the hand-off of engagers to inbound-led outbound.\"\n---\n\n# Playbook: LinkedIn content strategy\n\n## Motion in one sentence\n\nA strategy page written from the wiki fixes the pillars, cadence and voice; ideas accumulate in a backlog; a week of drafts is generated from that wiki and approved one post at a time; the scheduler publishes each from the founder's own account; comments are answered through a previewed reply; engagement is read back weekly; outlier mining says which structures travelled; the lessons file back into the wiki; and the engagers become the inbound signal. Each stage reads the page the last one wrote — a posting habit without the strategy page is a treadmill, and an outlier bank without the habit is a swipe file nobody drains.\n\nRead first: `oxygen recipes show founder-posting-system --json`, `oxygen recipes show weekly-content-calendar --json`, `oxygen recipes show content-outlier-mining --json` (the stage 7 kit). Mechanics: `oxygen skills get oxygen-linkedin-marketing --json`.\n\n## Stage table\n\n| # | Stage | Owner | Install / command | Inputs | Cap | Approval | Verify | Stop |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| 0 | Context + strategy | Knowledge Graph | `oxygen knowledge resolve --purpose outbound_copy --require-ready --json` → `oxygen knowledge page upsert --slug content-strategy --type strategy --title \"Content strategy\" --status active --body \"<pillars / cadence / voice>\" --json` | `positioning`, `icp`, `offers`, `voice`, all `active` | 3–5 pillars, each naming one ICP problem | none — a `strategy` page is working knowledge, logged as a revision | `oxygen knowledge page get content-strategy --json` | a stub ICP page, or a pillar with no buyer problem |\n| 1 | Sender | Publishing | `oxygen senders list --status active --json`; `oxygen senders health <sender_id> --json` (`oxygen senders limits get` for the budgets) | one connected LinkedIn account | `posts_per_day` (default 10, max 25) | none — reads | `active`, no open checkpoint | `restricted` / `credentials_required`: reconnect, never route around it |\n| 2 | Backlog | Publishing | `oxygen publishing ideas add --text \"<angle>\" --topic <pillar> --json`; `oxygen publishing ideas list --json` | angles as they occur; structures from stage 7 | two weeks of slots ahead | none (internal write) | `oxygen publishing ideas list --limit 50 --json` | an empty backlog on drafting day: mine outliers, don't draft from taste |\n| 3 | Draft + queue | Publishing | `oxygen publishing posts draft --template personal_story --topic \"<angle>\" --max-credits <cap> --json` → `oxygen publishing drafts edit <draft_id> --text-file mon.txt --json` → `oxygen publishing drafts accept <draft_id> --publish-at <iso+offset> --sender <sender_id> --json` | a pillar + a backlog angle per slot | 3–5 posts/week; 1–5 variants (default 3) | `draft-week`: paid, cap from the preview; queueing needs none — it never sends | `oxygen publishing posts list --approval-status needs_approval --json` | a bare `--publish-at`: with no offset it is stored as UTC |\n| 4 | Review + publish | Publishing | `oxygen publishing posts review <post_id> --max-credits <cap> --json`; `oxygen publishing mentions resolve --text-file mon.txt --json` → `oxygen publishing posts approve <post_id> --json` | the exact final text, read by the founder | ≤15% promotion posts; ≤1 lead-magnet CTA/week | `review-post`: paid, advisory. `post-publish`: human, every post, never batched | `oxygen publishing posts get <post_id> --json` — attempts, provider id, deep-link | an unresolved `@<public-identifier>`; `linkedin_rate_limited` |\n| 5 | Comments | Publishing | `oxygen publishing comments list --view unanswered --json` → `oxygen publishing comments reply <comment_id> --text-file reply.txt --json` → `oxygen publishing comments approve <action_id> --content-hash <sha256> --approved --json` → `oxygen publishing comments resolve <comment_id> --json` | the unanswered queue (30-day scope) | one reply per comment, inside 48h | `comment-reply`: human approves that exact previewed text and its hash | the queue drains; handled items read `resolved` | a comment older than 48h with no reply |\n| 6 | Read back | Publishing + Posts | `oxygen publishing analytics summary --channel linkedin --range 30d --json` (winners, `leader` lead, `null_reasons`); `oxygen publishing analytics timeseries --channel linkedin --range 30d --json` (earned per day, growing or shrinking vs the previous period, follower change); `oxygen publishing analytics post <post_id> --json`; `oxygen posts get --post <post_url> --json` | published posts | metrics refresh ~30 days after publish | none — reads | reactions, comments, reshares and — on your own original posts — impressions present; reposts counted in `reposts_excluded`, not in totals | every metric zero after two days: confirm `published`, not `deferred` |\n| 7 | Mine outliers | Workflows — kit stage `linkedin-keyword-outliers` | `oxygen recipes apply content-outlier-mining --dry-run --json` → without `--dry-run`; then `oxygen blueprints apply linkedin-creator-outliers --table-ref content_outliers=<bank_table_id> --json` | 3–4 buyer-language keywords; ≤25 public `/in/` URLs | 7-day lookback; 1 page/keyword on the pilot; ceiling from preflight | `pilot-live` then `sweep-arm`: human, re-granted on reapply | `oxygen workflows tail <run_id> --json`; `oxygen tables query <bank_table_id> --json` | everything near par, or top rows are hiring posts |\n| 8 | Learn | Knowledge Graph | `oxygen knowledge synthesize --kind voice --json` → `--approved`; `oxygen knowledge log append --event decision --slug content-strategy --json` | analytics, comments, the bank, draft edits | one rule per loop | canonical voice / positioning file a proposal: `oxygen knowledge proposals approve <id> --json` | `oxygen knowledge page get content-strategy --json` | a change with no receipt behind it |\n| 9 | Hand off | Signals → inbound-led outbound | `oxygen engagement harvest --post <social_id> --source unipile --recurring --json`; `oxygen engagement status --post <social_id> --json` | the composite `social_id` from `oxygen posts get` | free; drips against the ingest budget | none — it contacts nobody | `oxygen engagement engagers --post <social_id> --json` previews the table | outreach begins here: run the inbound-led-outbound playbook |\n\n## Numeric guardrails\n\n- **Cadence and mix:** 3–5 posts/week planned; 2–3/week for four consecutive weeks is the floor before any signal is readable. At most 15% promotion posts and one lead-magnet CTA post per week. Consistency leads, reach lags.\n- **Own-feed cap `posts_per_day`** per account (default 10, max 25): own-feed publishing sits outside the outreach send quotas because it is not aimed at another member, but still refuses a sender that is not `active`. Own-post reads draw the other-API-read budget (200/day default, 1,000 max); `engagement harvest` draws the separate ingest budget (20/day, 200 max), so a big post drips over days. `oxygen engagement engagers` walks both sources up to `--max-pages` (default 5, max 20) × 100.\n- **Paid calls:** 1–5 variants per draft call (default 3), one AI call per draft and per review. Take every ceiling from the preview and `oxygen tools get <tool_id> --json`, never from prose; sweep ceilings from `oxygen blueprints preflight`.\n- **Comment SLA 48 hours**, over a rolling 30-day owned-post scope: an older thread is absent from the queue, not silent. Analytics refresh for ~30 days; reactions, comments and reshares are real; impressions come from LinkedIn's own post analytics, which it shows only to the author, so they are real for the connected member's original posts; saves and sends are not exposed.\n- **Outlier scoring:** `rank_score = 100 × √(outlier_multiple × audience_index)`, **100 is par**; a healthy sweep puts ~5–15% above 200. Defaults: `lookback_days` 7, `min_post_age_hours` 24, `min_author_followers` 1000, weights 1 / 3 / 5; 3–4 keywords (max 8), 10–25 creators (max 25).\n\n## Angle gate spec\n\nThe gate is not who gets contacted — nobody is contacted here — it is **which idea becomes a public post in the founder's name**. An item advances to draft only when all of these hold: it maps to a named pillar on `[[content-strategy]]`; it carries one concrete claim, story or artifact traceable to a wiki page, a shipped thing or a real customer outcome; it names the audience in plain words; if promotional, the week's share is still under 15%; and its voice matches the pinned voice page. The first three are enforced at drafting, because the draft call grounds on the wiki by default; voice by the free channel lint plus the paid `publishing posts review` check.\n\nNothing bypasses it, because **approval is per post and is not batchable server-side**: a draft accepted from the AI queue, a post created directly, and a row loaded by `oxygen publishing import` all land needs-approval. Borrowing from the bank has its own rule — lift the **structure**, never the text, and only from a row that cleared `rank_score` 200 with a `baseline_kind` you trust; `author_unresolved` and `maturing` rows are unscored and are not evidence.\n\n## Approval gates, mapped to who decides\n\n| Gate | What it authorises | Copilot posture |\n| --- | --- | --- |\n| kit apply (`oxygen recipes apply content-outlier-mining`) | 0-credit install of the sweep, workflow disabled | human card, once |\n| `draft-week` / `review-post` | one paid AI drafting or voice/claims call | human sets each cap from the preview |\n| `post-publish` | one public post in the founder's name, on this exact text | human, every post, never batched |\n| `comment-reply` | one public reply bound to the preview's `action_id` and `content_hash` | human approves that exact text |\n| `pilot-live` then `sweep-arm` | one live outlier cycle, then the recurring sweep | human for each; re-grant on reapply (revision-bound) |\n| amplification (`oxygen publishing amplification create`, then `enable`) | real public engagement from teammates' accounts, spending credits; LinkedIn may restrict those accounts | human twice: grant, then arm, each with `--acknowledge-risk` |\n| canonical wiki edits | voice / brand / positioning / pinned playbooks | a proposal a human approves |\n\nInstallation is never permission: the kit applies at 0 credits with its workflow disabled, and every paid run, publish, reply and armed sweep is its own gate above.\n\nReads need no approval (`oxygen publishing posts list`, `oxygen publishing analytics summary`, `oxygen senders health`, `oxygen tables query`); working wiki pages write as logged revisions.\n\n## Failure modes\n\n- **The two axes.** `--status` is pipeline position (`draft` → `scheduled` → `queued` → `published`); `--approval-status` is whether a human said yes. A queued post is both `scheduled` and `needs_approval`. Filter on the approval axis or you will report an empty queue you just filled.\n- **A bare local `--publish-at`.** With no offset it is stored as UTC; `--timezone` only changes the display. Pass an offset or `Z`.\n- **The activity URN is not the `social_id`.** `posts reactions`, `posts comments`, `engagement engagers` and `engagement harvest` all need the composite `social_id` from `oxygen posts get`. Nothing lists your own feed, so record the id when the post goes out.\n- **Mentions and content.** A verified `@<public-identifier>` goes in the post text — a structured `mentions` array is ignored and `content.mentions` rejected; approve blocks anything unresolved. `--content-json` replaces the whole content object, so a media update drops an existing `first_comment` unless you resend it.\n- **A post on a dead sender never dispatches**; fix the account instead of re-queueing. A rate limit **defers** rather than drops and names `resets_at`, so retrying in a loop only burns quota. Company-page posting is a tested-negative anti-pattern — post from the personal profile.\n- **Generic drafts are a thin wiki, not a thin prompt**; a bank where everything scores near par is a keyword naming a category. Fix the page or the phrase, never the prompt length or the weights.\n- **Outreach leaking in.** The moment the plan is to message an engager, this motion is over — hand off at stage 9. Sequences own initiation; a CRM person is created at a warm-lead gate, never because someone reacted.\n\n## Data-quality checks (after every week)\n\n1. `oxygen publishing posts list --status published --json` — every planned slot published, or one sat `needs_approval` all week? A missed slot is the failure this motion exists to prevent.\n2. `oxygen publishing comments list --view unanswered --json` — nothing older than 48h, and never `resolved` on a thread still owed an answer.\n3. `oxygen publishing analytics summary --channel linkedin --range 30d --json` — all-zero after two days means `deferred`. Then read the comments, not the counts: ICP titles, or peers and recruiters?\n4. `oxygen tables query <bank_table_id> --limit 25 --json` — mostly `author_unresolved` or `maturing` is a cold cache or short lookback; a backlog under two weeks deep means next week starts from taste.\n\n## Learning loop\n\n- **What auto-files:** a draft edit snapshots the AI's original copy, so an accepted edit records what the founder changed; `oxygen publishing drafts reject <draft_id> --reason \"...\" --json` files the reason; revisions, review findings, the metric series and the comment queue are durable.\n- **What to synthesise weekly:** which pillar produced ICP-fit comments rather than peer likes; which post *shape* travelled (a number in line one, a named enemy, a before/after); and `oxygen knowledge synthesize --kind voice --json`, a voice guide distilled from real sent copy.\n- **What to change, one rule per loop:** the pillar mix, a slot's time, a keyword on the source table (pause it, never delete it), the CTA, or the voice page. Write the change and its reason into the wiki; a voice or positioning change files a proposal.\n- **Define \"good\" first:** an inbound conversation from an ICP-fit person — a DM, a comment thread that becomes a call, a reply naming their own version of the problem. Not reach, not reactions, not a bank with more rows.\n\n## Go-live checklist\n\n1. `oxygen senders list --status active --json`, then `oxygen senders health <sender_id> --json` — one healthy account, no open checkpoint.\n2. `oxygen knowledge resolve --purpose outbound_copy --require-ready --json`, then `oxygen knowledge page upsert --slug content-strategy --type strategy --status active --body \"...\" --json`.\n3. `oxygen publishing ideas add --text \"...\" --topic <pillar> --json` until two weeks of slots exist, then draft and queue each slot (stage 3).\n4. `oxygen publishing posts list --approval-status needs_approval --json`, present the week, then `oxygen publishing posts approve <post_id> --json` one post at a time.\n5. `oxygen publishing comments list --view unanswered --json` — the 48h loop runs and a named person owns it.\n6. `oxygen recipes apply content-outlier-mining --dry-run --json`, apply, one `oxygen workflows call <workflow_id> --mode dry-run --json`, one `pilot-live` cycle, then `oxygen workflows enable <workflow_id> --approved --max-credits <cap> --json`.\n7. `oxygen budget list --json` for the org backstop, then `oxygen knowledge log append --event note --slug content-strategy --json`.\n\n## Open questions (state them, do not resolve them)\n\n- LinkedIn shows impressions only to a post's author and never exposes saves or sends. Reach is measurable for the connected member's original posts (and earned-media value once a CPM is set with `oxygen publishing analytics emv`); for anything else it stays empty. Never substitute a proxy and call it reach.\n- The cadence and mix numbers are operator estimates, not measured benchmarks. Replace them with four weeks of your own evidence.\n- The outlier kit is `beta`, and `oxygen recipes apply content-outlier-mining` installs only the keyword sweep; the creator watch applies separately against the same tables with `--table-ref`, and omitting those flags reports a table collision — the guard working.\n- Whether engagers flow automatically into outreach is not this playbook's call; stage 9 hands them over deliberately. Amplification from teammates' accounts is likewise a founder decision about the company's public name.\n\n## Resume point\n\n`oxygen recipes show founder-posting-system --json` and `oxygen knowledge page get content-strategy --json`, then `oxygen publishing posts list --json`, `oxygen publishing comments list --json` and `oxygen workflows list --json`. Start from what exists, never from scratch.\n";
|
|
17
|
+
readonly sha256: "d68f101de1686a557d4c4e125f5bb7760b1041729aa90412223c56b89370f1b1";
|
|
18
|
+
readonly content: "---\nname: linkedin-content-strategy\ndescription: \"Install and run the founder LinkedIn content engine: a wiki-grounded strategy page, an idea backlog, a week of drafts approved post by post, scheduled publishing, answered comments, weekly engagement read-back, outlier mining, and the hand-off of engagers to inbound-led outbound.\"\n---\n\n# Playbook: LinkedIn content strategy\n\n## Motion in one sentence\n\nA strategy page written from the wiki fixes the pillars, cadence and voice; ideas accumulate in a backlog; a week of drafts is generated from that wiki and approved one post at a time; the scheduler publishes each from the founder's own account; comments are answered through a previewed reply; engagement is read back weekly; outlier mining says which structures travelled; the lessons file back into the wiki; and the engagers become the inbound signal. Each stage reads the page the last one wrote — a posting habit without the strategy page is a treadmill, and an outlier bank without the habit is a swipe file nobody drains.\n\nRead first: `oxygen recipes show founder-posting-system --json`, `oxygen recipes show weekly-content-calendar --json`, `oxygen recipes show content-outlier-mining --json` (the stage 7 kit). Mechanics: `oxygen skills get oxygen-linkedin-marketing --json`.\n\n## Stage table\n\n| # | Stage | Owner | Install / command | Inputs | Cap | Approval | Verify | Stop |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| 0 | Context + strategy | Knowledge Graph | `oxygen knowledge resolve --purpose outbound_copy --require-ready --json` → `oxygen knowledge page upsert --slug content-strategy --type strategy --title \"Content strategy\" --status active --body \"<pillars / cadence / voice>\" --json` | `positioning`, `icp`, `offers`, `voice`, all `active` | 3–5 pillars, each naming one ICP problem | none — a `strategy` page is working knowledge, logged as a revision | `oxygen knowledge page get content-strategy --json` | a stub ICP page, or a pillar with no buyer problem |\n| 1 | Sender | Publishing | `oxygen senders list --status active --json`; `oxygen senders health <sender_id> --json` (`oxygen senders limits get` for the budgets) | one connected LinkedIn account | `posts_per_day` (default 10, max 25) | none — reads | `active`, no open checkpoint | `restricted` / `credentials_required`: reconnect, never route around it |\n| 2 | Backlog | Publishing | `oxygen publishing ideas add --text \"<angle>\" --topic <pillar> --json`; `oxygen publishing ideas list --json` | angles as they occur; structures from stage 7 | two weeks of slots ahead | none (internal write) | `oxygen publishing ideas list --limit 50 --json` | an empty backlog on drafting day: mine outliers, don't draft from taste |\n| 3 | Draft + queue | Publishing | `oxygen publishing posts draft --template personal_story --topic \"<angle>\" --max-credits <cap> --json` → `oxygen publishing drafts edit <draft_id> --text-file mon.txt --json` → `oxygen publishing drafts accept <draft_id> --publish-at <iso+offset> --sender <sender_id> --json` | a pillar + a backlog angle per slot | 3–5 posts/week; 1–5 variants (default 3) | `draft-week`: paid, cap from the preview; queueing needs none — it never sends | `oxygen publishing posts list --approval-status needs_approval --json` | a `--publish-at` with no offset: it is read in `--timezone`, UTC when none is given |\n| 4 | Review + publish | Publishing | `oxygen publishing posts review <post_id> --max-credits <cap> --json`; `oxygen publishing mentions resolve --text-file mon.txt --json` → `oxygen publishing posts approve <post_id> --json` | the exact final text, read by the founder | ≤15% promotion posts; ≤1 lead-magnet CTA/week | `review-post`: paid, advisory. `post-publish`: human, every post, never batched | `oxygen publishing posts get <post_id> --json` — attempts, provider id, deep-link | an unresolved `@<public-identifier>`; `linkedin_rate_limited` |\n| 5 | Comments | Publishing | `oxygen publishing comments list --view unanswered --json` → `oxygen publishing comments reply <comment_id> --text-file reply.txt --json` → `oxygen publishing comments approve <action_id> --content-hash <sha256> --approved --json` → `oxygen publishing comments resolve <comment_id> --json` | the unanswered queue (30-day scope) | one reply per comment, inside 48h | `comment-reply`: human approves that exact previewed text and its hash | the queue drains; handled items read `resolved` | a comment older than 48h with no reply |\n| 6 | Read back | Publishing + Posts | `oxygen publishing analytics summary --channel linkedin --range 30d --json` (winners, `leader` lead, `null_reasons`); `oxygen publishing analytics timeseries --channel linkedin --range 30d --json` (earned per day, growing or shrinking vs the previous period, follower change); `oxygen publishing analytics post <post_id> --json`; `oxygen posts get --post <post_url> --json` | published posts | metrics refresh ~30 days after publish | none — reads | reactions, comments, reshares and — on your own original posts — impressions present; reposts counted in `reposts_excluded`, not in totals | every metric zero after two days: confirm `published`, not `deferred` |\n| 7 | Mine outliers | Workflows — kit stage `linkedin-keyword-outliers` | `oxygen recipes apply content-outlier-mining --dry-run --json` → without `--dry-run`; then `oxygen blueprints apply linkedin-creator-outliers --table-ref content_outliers=<bank_table_id> --json` | 3–4 buyer-language keywords; ≤25 public `/in/` URLs | 7-day lookback; 1 page/keyword on the pilot; ceiling from preflight | `pilot-live` then `sweep-arm`: human, re-granted on reapply | `oxygen workflows tail <run_id> --json`; `oxygen tables query <bank_table_id> --json` | everything near par, or top rows are hiring posts |\n| 8 | Learn | Knowledge Graph | `oxygen knowledge synthesize --kind voice --json` → `--approved`; `oxygen knowledge log append --event decision --slug content-strategy --json` | analytics, comments, the bank, draft edits | one rule per loop | canonical voice / positioning file a proposal: `oxygen knowledge proposals approve <id> --json` | `oxygen knowledge page get content-strategy --json` | a change with no receipt behind it |\n| 9 | Hand off | Signals → inbound-led outbound | `oxygen engagement harvest --post <social_id> --source unipile --recurring --json`; `oxygen engagement status --post <social_id> --json` | the composite `social_id` from `oxygen posts get` | free; drips against the ingest budget | none — it contacts nobody | `oxygen engagement engagers --post <social_id> --json` previews the table | outreach begins here: run the inbound-led-outbound playbook |\n\n## Numeric guardrails\n\n- **Cadence and mix:** 3–5 posts/week planned; 2–3/week for four consecutive weeks is the floor before any signal is readable. At most 15% promotion posts and one lead-magnet CTA post per week. Consistency leads, reach lags.\n- **Own-feed cap `posts_per_day`** per account (default 10, max 25): own-feed publishing sits outside the outreach send quotas because it is not aimed at another member, but still refuses a sender that is not `active`. Own-post reads draw the other-API-read budget (200/day default, 1,000 max); `engagement harvest` draws the separate ingest budget (20/day, 200 max), so a big post drips over days. `oxygen engagement engagers` walks both sources up to `--max-pages` (default 5, max 20) × 100.\n- **Paid calls:** 1–5 variants per draft call (default 3), one AI call per draft and per review. Take every ceiling from the preview and `oxygen tools get <tool_id> --json`, never from prose; sweep ceilings from `oxygen blueprints preflight`.\n- **Comment SLA 48 hours**, over a rolling 30-day owned-post scope: an older thread is absent from the queue, not silent. Analytics refresh for ~30 days; reactions, comments and reshares are real; impressions come from LinkedIn's own post analytics, which it shows only to the author, so they are real for the connected member's original posts; saves and sends are not exposed.\n- **Outlier scoring:** `rank_score = 100 × √(outlier_multiple × audience_index)`, **100 is par**; a healthy sweep puts ~5–15% above 200. Defaults: `lookback_days` 7, `min_post_age_hours` 24, `min_author_followers` 1000, weights 1 / 3 / 5; 3–4 keywords (max 8), 10–25 creators (max 25).\n\n## Angle gate spec\n\nThe gate is not who gets contacted — nobody is contacted here — it is **which idea becomes a public post in the founder's name**. An item advances to draft only when all of these hold: it maps to a named pillar on `[[content-strategy]]`; it carries one concrete claim, story or artifact traceable to a wiki page, a shipped thing or a real customer outcome; it names the audience in plain words; if promotional, the week's share is still under 15%; and its voice matches the pinned voice page. The first three are enforced at drafting, because the draft call grounds on the wiki by default; voice by the free channel lint plus the paid `publishing posts review` check.\n\nNothing bypasses it, because **approval is per post and is not batchable server-side**: a draft accepted from the AI queue, a post created directly, and a row loaded by `oxygen publishing import` all land needs-approval. Borrowing from the bank has its own rule — lift the **structure**, never the text, and only from a row that cleared `rank_score` 200 with a `baseline_kind` you trust; `author_unresolved` and `maturing` rows are unscored and are not evidence.\n\n## Approval gates, mapped to who decides\n\n| Gate | What it authorises | Copilot posture |\n| --- | --- | --- |\n| kit apply (`oxygen recipes apply content-outlier-mining`) | 0-credit install of the sweep, workflow disabled | human card, once |\n| `draft-week` / `review-post` | one paid AI drafting or voice/claims call | human sets each cap from the preview |\n| `post-publish` | one public post in the founder's name, on this exact text | human, every post, never batched |\n| `comment-reply` | one public reply bound to the preview's `action_id` and `content_hash` | human approves that exact text |\n| `pilot-live` then `sweep-arm` | one live outlier cycle, then the recurring sweep | human for each; re-grant on reapply (revision-bound) |\n| amplification (`oxygen publishing amplification create`, then `enable`) | real public engagement from teammates' accounts, spending credits; LinkedIn may restrict those accounts | human twice: grant, then arm, each with `--acknowledge-risk` |\n| canonical wiki edits | voice / brand / positioning / pinned playbooks | a proposal a human approves |\n\nInstallation is never permission: the kit applies at 0 credits with its workflow disabled, and every paid run, publish, reply and armed sweep is its own gate above.\n\nReads need no approval (`oxygen publishing posts list`, `oxygen publishing analytics summary`, `oxygen senders health`, `oxygen tables query`); working wiki pages write as logged revisions.\n\n## Failure modes\n\n- **The two axes.** `--status` is pipeline position (`draft` → `scheduled` → `queued` → `published`); `--approval-status` is whether a human said yes. A queued post is both `scheduled` and `needs_approval`. Filter on the approval axis or you will report an empty queue you just filled.\n- **A `--publish-at` without an offset** is wall-clock time in `--timezone`: `publishing posts create` defaults that to the sender's own zone, `drafts accept` to UTC. Pass an offset or `Z` to pin the exact instant.\n- **The activity URN is not the `social_id`.** `posts reactions`, `posts comments`, `engagement engagers` and `engagement harvest` all need the composite `social_id` from `oxygen posts get`. Nothing lists your own feed, so record the id when the post goes out.\n- **Mentions and content.** A verified `@<public-identifier>` goes in the post text — a structured `mentions` array is ignored and `content.mentions` rejected; approve blocks anything unresolved. `--content-json` replaces the whole content object, so a media update drops an existing `first_comment` unless you resend it.\n- **A post on a dead sender never dispatches**; fix the account instead of re-queueing. A rate limit **defers** rather than drops and names `resets_at`, so retrying in a loop only burns quota. Company-page posting is a tested-negative anti-pattern — post from the personal profile.\n- **Generic drafts are a thin wiki, not a thin prompt**; a bank where everything scores near par is a keyword naming a category. Fix the page or the phrase, never the prompt length or the weights.\n- **Outreach leaking in.** The moment the plan is to message an engager, this motion is over — hand off at stage 9. Sequences own initiation; a CRM person is created at a warm-lead gate, never because someone reacted.\n\n## Data-quality checks (after every week)\n\n1. `oxygen publishing posts list --status published --json` — every planned slot published, or one sat `needs_approval` all week? A missed slot is the failure this motion exists to prevent.\n2. `oxygen publishing comments list --view unanswered --json` — nothing older than 48h, and never `resolved` on a thread still owed an answer.\n3. `oxygen publishing analytics summary --channel linkedin --range 30d --json` — all-zero after two days means `deferred`. Then read the comments, not the counts: ICP titles, or peers and recruiters?\n4. `oxygen tables query <bank_table_id> --limit 25 --json` — mostly `author_unresolved` or `maturing` is a cold cache or short lookback; a backlog under two weeks deep means next week starts from taste.\n\n## Learning loop\n\n- **What auto-files:** a draft edit snapshots the AI's original copy, so an accepted edit records what the founder changed; `oxygen publishing drafts reject <draft_id> --reason \"...\" --json` files the reason; revisions, review findings, the metric series and the comment queue are durable.\n- **What to synthesise weekly:** which pillar produced ICP-fit comments rather than peer likes; which post *shape* travelled (a number in line one, a named enemy, a before/after); and `oxygen knowledge synthesize --kind voice --json`, a voice guide distilled from real sent copy.\n- **What to change, one rule per loop:** the pillar mix, a slot's time, a keyword on the source table (pause it, never delete it), the CTA, or the voice page. Write the change and its reason into the wiki; a voice or positioning change files a proposal.\n- **Define \"good\" first:** an inbound conversation from an ICP-fit person — a DM, a comment thread that becomes a call, a reply naming their own version of the problem. Not reach, not reactions, not a bank with more rows.\n\n## Go-live checklist\n\n1. `oxygen senders list --status active --json`, then `oxygen senders health <sender_id> --json` — one healthy account, no open checkpoint.\n2. `oxygen knowledge resolve --purpose outbound_copy --require-ready --json`, then `oxygen knowledge page upsert --slug content-strategy --type strategy --status active --body \"...\" --json`.\n3. `oxygen publishing ideas add --text \"...\" --topic <pillar> --json` until two weeks of slots exist, then draft and queue each slot (stage 3).\n4. `oxygen publishing posts list --approval-status needs_approval --json`, present the week, then `oxygen publishing posts approve <post_id> --json` one post at a time.\n5. `oxygen publishing comments list --view unanswered --json` — the 48h loop runs and a named person owns it.\n6. `oxygen recipes apply content-outlier-mining --dry-run --json`, apply, one `oxygen workflows call <workflow_id> --mode dry-run --json`, one `pilot-live` cycle, then `oxygen workflows enable <workflow_id> --approved --max-credits <cap> --json`.\n7. `oxygen budget list --json` for the org backstop, then `oxygen knowledge log append --event note --slug content-strategy --json`.\n\n## Open questions (state them, do not resolve them)\n\n- LinkedIn shows impressions only to a post's author and never exposes saves or sends. Reach is measurable for the connected member's original posts (and earned-media value once a CPM is set with `oxygen publishing analytics emv`); for anything else it stays empty. Never substitute a proxy and call it reach.\n- The cadence and mix numbers are operator estimates, not measured benchmarks. Replace them with four weeks of your own evidence.\n- The outlier kit is `beta`, and `oxygen recipes apply content-outlier-mining` installs only the keyword sweep; the creator watch applies separately against the same tables with `--table-ref`, and omitting those flags reports a table collision — the guard working.\n- Whether engagers flow automatically into outreach is not this playbook's call; stage 9 hands them over deliberately. Amplification from teammates' accounts is likewise a founder decision about the company's public name.\n\n## Resume point\n\n`oxygen recipes show founder-posting-system --json` and `oxygen knowledge page get content-strategy --json`, then `oxygen publishing posts list --json`, `oxygen publishing comments list --json` and `oxygen workflows list --json`. Start from what exists, never from scratch.\n";
|
|
19
19
|
}, {
|
|
20
20
|
readonly slug: "inbound-led-outbound";
|
|
21
21
|
readonly title: "Playbook: Inbound-led outbound";
|
|
@@ -21,8 +21,8 @@ export const COPILOT_SKILL_SNAPSHOTS_GENERATED = [
|
|
|
21
21
|
slug: "linkedin-content-strategy",
|
|
22
22
|
title: "Playbook: LinkedIn content strategy",
|
|
23
23
|
sources: ["oxygen-playbooks/playbooks/linkedin-content-strategy.md"],
|
|
24
|
-
sha256: "
|
|
25
|
-
content: "---\nname: linkedin-content-strategy\ndescription: \"Install and run the founder LinkedIn content engine: a wiki-grounded strategy page, an idea backlog, a week of drafts approved post by post, scheduled publishing, answered comments, weekly engagement read-back, outlier mining, and the hand-off of engagers to inbound-led outbound.\"\n---\n\n# Playbook: LinkedIn content strategy\n\n## Motion in one sentence\n\nA strategy page written from the wiki fixes the pillars, cadence and voice; ideas accumulate in a backlog; a week of drafts is generated from that wiki and approved one post at a time; the scheduler publishes each from the founder's own account; comments are answered through a previewed reply; engagement is read back weekly; outlier mining says which structures travelled; the lessons file back into the wiki; and the engagers become the inbound signal. Each stage reads the page the last one wrote — a posting habit without the strategy page is a treadmill, and an outlier bank without the habit is a swipe file nobody drains.\n\nRead first: `oxygen recipes show founder-posting-system --json`, `oxygen recipes show weekly-content-calendar --json`, `oxygen recipes show content-outlier-mining --json` (the stage 7 kit). Mechanics: `oxygen skills get oxygen-linkedin-marketing --json`.\n\n## Stage table\n\n| # | Stage | Owner | Install / command | Inputs | Cap | Approval | Verify | Stop |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| 0 | Context + strategy | Knowledge Graph | `oxygen knowledge resolve --purpose outbound_copy --require-ready --json` → `oxygen knowledge page upsert --slug content-strategy --type strategy --title \"Content strategy\" --status active --body \"<pillars / cadence / voice>\" --json` | `positioning`, `icp`, `offers`, `voice`, all `active` | 3–5 pillars, each naming one ICP problem | none — a `strategy` page is working knowledge, logged as a revision | `oxygen knowledge page get content-strategy --json` | a stub ICP page, or a pillar with no buyer problem |\n| 1 | Sender | Publishing | `oxygen senders list --status active --json`; `oxygen senders health <sender_id> --json` (`oxygen senders limits get` for the budgets) | one connected LinkedIn account | `posts_per_day` (default 10, max 25) | none — reads | `active`, no open checkpoint | `restricted` / `credentials_required`: reconnect, never route around it |\n| 2 | Backlog | Publishing | `oxygen publishing ideas add --text \"<angle>\" --topic <pillar> --json`; `oxygen publishing ideas list --json` | angles as they occur; structures from stage 7 | two weeks of slots ahead | none (internal write) | `oxygen publishing ideas list --limit 50 --json` | an empty backlog on drafting day: mine outliers, don't draft from taste |\n| 3 | Draft + queue | Publishing | `oxygen publishing posts draft --template personal_story --topic \"<angle>\" --max-credits <cap> --json` → `oxygen publishing drafts edit <draft_id> --text-file mon.txt --json` → `oxygen publishing drafts accept <draft_id> --publish-at <iso+offset> --sender <sender_id> --json` | a pillar + a backlog angle per slot | 3–5 posts/week; 1–5 variants (default 3) | `draft-week`: paid, cap from the preview; queueing needs none — it never sends | `oxygen publishing posts list --approval-status needs_approval --json` | a bare `--publish-at`: with no offset it is stored as UTC |\n| 4 | Review + publish | Publishing | `oxygen publishing posts review <post_id> --max-credits <cap> --json`; `oxygen publishing mentions resolve --text-file mon.txt --json` → `oxygen publishing posts approve <post_id> --json` | the exact final text, read by the founder | ≤15% promotion posts; ≤1 lead-magnet CTA/week | `review-post`: paid, advisory. `post-publish`: human, every post, never batched | `oxygen publishing posts get <post_id> --json` — attempts, provider id, deep-link | an unresolved `@<public-identifier>`; `linkedin_rate_limited` |\n| 5 | Comments | Publishing | `oxygen publishing comments list --view unanswered --json` → `oxygen publishing comments reply <comment_id> --text-file reply.txt --json` → `oxygen publishing comments approve <action_id> --content-hash <sha256> --approved --json` → `oxygen publishing comments resolve <comment_id> --json` | the unanswered queue (30-day scope) | one reply per comment, inside 48h | `comment-reply`: human approves that exact previewed text and its hash | the queue drains; handled items read `resolved` | a comment older than 48h with no reply |\n| 6 | Read back | Publishing + Posts | `oxygen publishing analytics summary --channel linkedin --range 30d --json` (winners, `leader` lead, `null_reasons`); `oxygen publishing analytics timeseries --channel linkedin --range 30d --json` (earned per day, growing or shrinking vs the previous period, follower change); `oxygen publishing analytics post <post_id> --json`; `oxygen posts get --post <post_url> --json` | published posts | metrics refresh ~30 days after publish | none — reads | reactions, comments, reshares and — on your own original posts — impressions present; reposts counted in `reposts_excluded`, not in totals | every metric zero after two days: confirm `published`, not `deferred` |\n| 7 | Mine outliers | Workflows — kit stage `linkedin-keyword-outliers` | `oxygen recipes apply content-outlier-mining --dry-run --json` → without `--dry-run`; then `oxygen blueprints apply linkedin-creator-outliers --table-ref content_outliers=<bank_table_id> --json` | 3–4 buyer-language keywords; ≤25 public `/in/` URLs | 7-day lookback; 1 page/keyword on the pilot; ceiling from preflight | `pilot-live` then `sweep-arm`: human, re-granted on reapply | `oxygen workflows tail <run_id> --json`; `oxygen tables query <bank_table_id> --json` | everything near par, or top rows are hiring posts |\n| 8 | Learn | Knowledge Graph | `oxygen knowledge synthesize --kind voice --json` → `--approved`; `oxygen knowledge log append --event decision --slug content-strategy --json` | analytics, comments, the bank, draft edits | one rule per loop | canonical voice / positioning file a proposal: `oxygen knowledge proposals approve <id> --json` | `oxygen knowledge page get content-strategy --json` | a change with no receipt behind it |\n| 9 | Hand off | Signals → inbound-led outbound | `oxygen engagement harvest --post <social_id> --source unipile --recurring --json`; `oxygen engagement status --post <social_id> --json` | the composite `social_id` from `oxygen posts get` | free; drips against the ingest budget | none — it contacts nobody | `oxygen engagement engagers --post <social_id> --json` previews the table | outreach begins here: run the inbound-led-outbound playbook |\n\n## Numeric guardrails\n\n- **Cadence and mix:** 3–5 posts/week planned; 2–3/week for four consecutive weeks is the floor before any signal is readable. At most 15% promotion posts and one lead-magnet CTA post per week. Consistency leads, reach lags.\n- **Own-feed cap `posts_per_day`** per account (default 10, max 25): own-feed publishing sits outside the outreach send quotas because it is not aimed at another member, but still refuses a sender that is not `active`. Own-post reads draw the other-API-read budget (200/day default, 1,000 max); `engagement harvest` draws the separate ingest budget (20/day, 200 max), so a big post drips over days. `oxygen engagement engagers` walks both sources up to `--max-pages` (default 5, max 20) × 100.\n- **Paid calls:** 1–5 variants per draft call (default 3), one AI call per draft and per review. Take every ceiling from the preview and `oxygen tools get <tool_id> --json`, never from prose; sweep ceilings from `oxygen blueprints preflight`.\n- **Comment SLA 48 hours**, over a rolling 30-day owned-post scope: an older thread is absent from the queue, not silent. Analytics refresh for ~30 days; reactions, comments and reshares are real; impressions come from LinkedIn's own post analytics, which it shows only to the author, so they are real for the connected member's original posts; saves and sends are not exposed.\n- **Outlier scoring:** `rank_score = 100 × √(outlier_multiple × audience_index)`, **100 is par**; a healthy sweep puts ~5–15% above 200. Defaults: `lookback_days` 7, `min_post_age_hours` 24, `min_author_followers` 1000, weights 1 / 3 / 5; 3–4 keywords (max 8), 10–25 creators (max 25).\n\n## Angle gate spec\n\nThe gate is not who gets contacted — nobody is contacted here — it is **which idea becomes a public post in the founder's name**. An item advances to draft only when all of these hold: it maps to a named pillar on `[[content-strategy]]`; it carries one concrete claim, story or artifact traceable to a wiki page, a shipped thing or a real customer outcome; it names the audience in plain words; if promotional, the week's share is still under 15%; and its voice matches the pinned voice page. The first three are enforced at drafting, because the draft call grounds on the wiki by default; voice by the free channel lint plus the paid `publishing posts review` check.\n\nNothing bypasses it, because **approval is per post and is not batchable server-side**: a draft accepted from the AI queue, a post created directly, and a row loaded by `oxygen publishing import` all land needs-approval. Borrowing from the bank has its own rule — lift the **structure**, never the text, and only from a row that cleared `rank_score` 200 with a `baseline_kind` you trust; `author_unresolved` and `maturing` rows are unscored and are not evidence.\n\n## Approval gates, mapped to who decides\n\n| Gate | What it authorises | Copilot posture |\n| --- | --- | --- |\n| kit apply (`oxygen recipes apply content-outlier-mining`) | 0-credit install of the sweep, workflow disabled | human card, once |\n| `draft-week` / `review-post` | one paid AI drafting or voice/claims call | human sets each cap from the preview |\n| `post-publish` | one public post in the founder's name, on this exact text | human, every post, never batched |\n| `comment-reply` | one public reply bound to the preview's `action_id` and `content_hash` | human approves that exact text |\n| `pilot-live` then `sweep-arm` | one live outlier cycle, then the recurring sweep | human for each; re-grant on reapply (revision-bound) |\n| amplification (`oxygen publishing amplification create`, then `enable`) | real public engagement from teammates' accounts, spending credits; LinkedIn may restrict those accounts | human twice: grant, then arm, each with `--acknowledge-risk` |\n| canonical wiki edits | voice / brand / positioning / pinned playbooks | a proposal a human approves |\n\nInstallation is never permission: the kit applies at 0 credits with its workflow disabled, and every paid run, publish, reply and armed sweep is its own gate above.\n\nReads need no approval (`oxygen publishing posts list`, `oxygen publishing analytics summary`, `oxygen senders health`, `oxygen tables query`); working wiki pages write as logged revisions.\n\n## Failure modes\n\n- **The two axes.** `--status` is pipeline position (`draft` → `scheduled` → `queued` → `published`); `--approval-status` is whether a human said yes. A queued post is both `scheduled` and `needs_approval`. Filter on the approval axis or you will report an empty queue you just filled.\n- **A bare local `--publish-at`.** With no offset it is stored as UTC; `--timezone` only changes the display. Pass an offset or `Z`.\n- **The activity URN is not the `social_id`.** `posts reactions`, `posts comments`, `engagement engagers` and `engagement harvest` all need the composite `social_id` from `oxygen posts get`. Nothing lists your own feed, so record the id when the post goes out.\n- **Mentions and content.** A verified `@<public-identifier>` goes in the post text — a structured `mentions` array is ignored and `content.mentions` rejected; approve blocks anything unresolved. `--content-json` replaces the whole content object, so a media update drops an existing `first_comment` unless you resend it.\n- **A post on a dead sender never dispatches**; fix the account instead of re-queueing. A rate limit **defers** rather than drops and names `resets_at`, so retrying in a loop only burns quota. Company-page posting is a tested-negative anti-pattern — post from the personal profile.\n- **Generic drafts are a thin wiki, not a thin prompt**; a bank where everything scores near par is a keyword naming a category. Fix the page or the phrase, never the prompt length or the weights.\n- **Outreach leaking in.** The moment the plan is to message an engager, this motion is over — hand off at stage 9. Sequences own initiation; a CRM person is created at a warm-lead gate, never because someone reacted.\n\n## Data-quality checks (after every week)\n\n1. `oxygen publishing posts list --status published --json` — every planned slot published, or one sat `needs_approval` all week? A missed slot is the failure this motion exists to prevent.\n2. `oxygen publishing comments list --view unanswered --json` — nothing older than 48h, and never `resolved` on a thread still owed an answer.\n3. `oxygen publishing analytics summary --channel linkedin --range 30d --json` — all-zero after two days means `deferred`. Then read the comments, not the counts: ICP titles, or peers and recruiters?\n4. `oxygen tables query <bank_table_id> --limit 25 --json` — mostly `author_unresolved` or `maturing` is a cold cache or short lookback; a backlog under two weeks deep means next week starts from taste.\n\n## Learning loop\n\n- **What auto-files:** a draft edit snapshots the AI's original copy, so an accepted edit records what the founder changed; `oxygen publishing drafts reject <draft_id> --reason \"...\" --json` files the reason; revisions, review findings, the metric series and the comment queue are durable.\n- **What to synthesise weekly:** which pillar produced ICP-fit comments rather than peer likes; which post *shape* travelled (a number in line one, a named enemy, a before/after); and `oxygen knowledge synthesize --kind voice --json`, a voice guide distilled from real sent copy.\n- **What to change, one rule per loop:** the pillar mix, a slot's time, a keyword on the source table (pause it, never delete it), the CTA, or the voice page. Write the change and its reason into the wiki; a voice or positioning change files a proposal.\n- **Define \"good\" first:** an inbound conversation from an ICP-fit person — a DM, a comment thread that becomes a call, a reply naming their own version of the problem. Not reach, not reactions, not a bank with more rows.\n\n## Go-live checklist\n\n1. `oxygen senders list --status active --json`, then `oxygen senders health <sender_id> --json` — one healthy account, no open checkpoint.\n2. `oxygen knowledge resolve --purpose outbound_copy --require-ready --json`, then `oxygen knowledge page upsert --slug content-strategy --type strategy --status active --body \"...\" --json`.\n3. `oxygen publishing ideas add --text \"...\" --topic <pillar> --json` until two weeks of slots exist, then draft and queue each slot (stage 3).\n4. `oxygen publishing posts list --approval-status needs_approval --json`, present the week, then `oxygen publishing posts approve <post_id> --json` one post at a time.\n5. `oxygen publishing comments list --view unanswered --json` — the 48h loop runs and a named person owns it.\n6. `oxygen recipes apply content-outlier-mining --dry-run --json`, apply, one `oxygen workflows call <workflow_id> --mode dry-run --json`, one `pilot-live` cycle, then `oxygen workflows enable <workflow_id> --approved --max-credits <cap> --json`.\n7. `oxygen budget list --json` for the org backstop, then `oxygen knowledge log append --event note --slug content-strategy --json`.\n\n## Open questions (state them, do not resolve them)\n\n- LinkedIn shows impressions only to a post's author and never exposes saves or sends. Reach is measurable for the connected member's original posts (and earned-media value once a CPM is set with `oxygen publishing analytics emv`); for anything else it stays empty. Never substitute a proxy and call it reach.\n- The cadence and mix numbers are operator estimates, not measured benchmarks. Replace them with four weeks of your own evidence.\n- The outlier kit is `beta`, and `oxygen recipes apply content-outlier-mining` installs only the keyword sweep; the creator watch applies separately against the same tables with `--table-ref`, and omitting those flags reports a table collision — the guard working.\n- Whether engagers flow automatically into outreach is not this playbook's call; stage 9 hands them over deliberately. Amplification from teammates' accounts is likewise a founder decision about the company's public name.\n\n## Resume point\n\n`oxygen recipes show founder-posting-system --json` and `oxygen knowledge page get content-strategy --json`, then `oxygen publishing posts list --json`, `oxygen publishing comments list --json` and `oxygen workflows list --json`. Start from what exists, never from scratch.\n",
|
|
24
|
+
sha256: "d68f101de1686a557d4c4e125f5bb7760b1041729aa90412223c56b89370f1b1",
|
|
25
|
+
content: "---\nname: linkedin-content-strategy\ndescription: \"Install and run the founder LinkedIn content engine: a wiki-grounded strategy page, an idea backlog, a week of drafts approved post by post, scheduled publishing, answered comments, weekly engagement read-back, outlier mining, and the hand-off of engagers to inbound-led outbound.\"\n---\n\n# Playbook: LinkedIn content strategy\n\n## Motion in one sentence\n\nA strategy page written from the wiki fixes the pillars, cadence and voice; ideas accumulate in a backlog; a week of drafts is generated from that wiki and approved one post at a time; the scheduler publishes each from the founder's own account; comments are answered through a previewed reply; engagement is read back weekly; outlier mining says which structures travelled; the lessons file back into the wiki; and the engagers become the inbound signal. Each stage reads the page the last one wrote — a posting habit without the strategy page is a treadmill, and an outlier bank without the habit is a swipe file nobody drains.\n\nRead first: `oxygen recipes show founder-posting-system --json`, `oxygen recipes show weekly-content-calendar --json`, `oxygen recipes show content-outlier-mining --json` (the stage 7 kit). Mechanics: `oxygen skills get oxygen-linkedin-marketing --json`.\n\n## Stage table\n\n| # | Stage | Owner | Install / command | Inputs | Cap | Approval | Verify | Stop |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| 0 | Context + strategy | Knowledge Graph | `oxygen knowledge resolve --purpose outbound_copy --require-ready --json` → `oxygen knowledge page upsert --slug content-strategy --type strategy --title \"Content strategy\" --status active --body \"<pillars / cadence / voice>\" --json` | `positioning`, `icp`, `offers`, `voice`, all `active` | 3–5 pillars, each naming one ICP problem | none — a `strategy` page is working knowledge, logged as a revision | `oxygen knowledge page get content-strategy --json` | a stub ICP page, or a pillar with no buyer problem |\n| 1 | Sender | Publishing | `oxygen senders list --status active --json`; `oxygen senders health <sender_id> --json` (`oxygen senders limits get` for the budgets) | one connected LinkedIn account | `posts_per_day` (default 10, max 25) | none — reads | `active`, no open checkpoint | `restricted` / `credentials_required`: reconnect, never route around it |\n| 2 | Backlog | Publishing | `oxygen publishing ideas add --text \"<angle>\" --topic <pillar> --json`; `oxygen publishing ideas list --json` | angles as they occur; structures from stage 7 | two weeks of slots ahead | none (internal write) | `oxygen publishing ideas list --limit 50 --json` | an empty backlog on drafting day: mine outliers, don't draft from taste |\n| 3 | Draft + queue | Publishing | `oxygen publishing posts draft --template personal_story --topic \"<angle>\" --max-credits <cap> --json` → `oxygen publishing drafts edit <draft_id> --text-file mon.txt --json` → `oxygen publishing drafts accept <draft_id> --publish-at <iso+offset> --sender <sender_id> --json` | a pillar + a backlog angle per slot | 3–5 posts/week; 1–5 variants (default 3) | `draft-week`: paid, cap from the preview; queueing needs none — it never sends | `oxygen publishing posts list --approval-status needs_approval --json` | a `--publish-at` with no offset: it is read in `--timezone`, UTC when none is given |\n| 4 | Review + publish | Publishing | `oxygen publishing posts review <post_id> --max-credits <cap> --json`; `oxygen publishing mentions resolve --text-file mon.txt --json` → `oxygen publishing posts approve <post_id> --json` | the exact final text, read by the founder | ≤15% promotion posts; ≤1 lead-magnet CTA/week | `review-post`: paid, advisory. `post-publish`: human, every post, never batched | `oxygen publishing posts get <post_id> --json` — attempts, provider id, deep-link | an unresolved `@<public-identifier>`; `linkedin_rate_limited` |\n| 5 | Comments | Publishing | `oxygen publishing comments list --view unanswered --json` → `oxygen publishing comments reply <comment_id> --text-file reply.txt --json` → `oxygen publishing comments approve <action_id> --content-hash <sha256> --approved --json` → `oxygen publishing comments resolve <comment_id> --json` | the unanswered queue (30-day scope) | one reply per comment, inside 48h | `comment-reply`: human approves that exact previewed text and its hash | the queue drains; handled items read `resolved` | a comment older than 48h with no reply |\n| 6 | Read back | Publishing + Posts | `oxygen publishing analytics summary --channel linkedin --range 30d --json` (winners, `leader` lead, `null_reasons`); `oxygen publishing analytics timeseries --channel linkedin --range 30d --json` (earned per day, growing or shrinking vs the previous period, follower change); `oxygen publishing analytics post <post_id> --json`; `oxygen posts get --post <post_url> --json` | published posts | metrics refresh ~30 days after publish | none — reads | reactions, comments, reshares and — on your own original posts — impressions present; reposts counted in `reposts_excluded`, not in totals | every metric zero after two days: confirm `published`, not `deferred` |\n| 7 | Mine outliers | Workflows — kit stage `linkedin-keyword-outliers` | `oxygen recipes apply content-outlier-mining --dry-run --json` → without `--dry-run`; then `oxygen blueprints apply linkedin-creator-outliers --table-ref content_outliers=<bank_table_id> --json` | 3–4 buyer-language keywords; ≤25 public `/in/` URLs | 7-day lookback; 1 page/keyword on the pilot; ceiling from preflight | `pilot-live` then `sweep-arm`: human, re-granted on reapply | `oxygen workflows tail <run_id> --json`; `oxygen tables query <bank_table_id> --json` | everything near par, or top rows are hiring posts |\n| 8 | Learn | Knowledge Graph | `oxygen knowledge synthesize --kind voice --json` → `--approved`; `oxygen knowledge log append --event decision --slug content-strategy --json` | analytics, comments, the bank, draft edits | one rule per loop | canonical voice / positioning file a proposal: `oxygen knowledge proposals approve <id> --json` | `oxygen knowledge page get content-strategy --json` | a change with no receipt behind it |\n| 9 | Hand off | Signals → inbound-led outbound | `oxygen engagement harvest --post <social_id> --source unipile --recurring --json`; `oxygen engagement status --post <social_id> --json` | the composite `social_id` from `oxygen posts get` | free; drips against the ingest budget | none — it contacts nobody | `oxygen engagement engagers --post <social_id> --json` previews the table | outreach begins here: run the inbound-led-outbound playbook |\n\n## Numeric guardrails\n\n- **Cadence and mix:** 3–5 posts/week planned; 2–3/week for four consecutive weeks is the floor before any signal is readable. At most 15% promotion posts and one lead-magnet CTA post per week. Consistency leads, reach lags.\n- **Own-feed cap `posts_per_day`** per account (default 10, max 25): own-feed publishing sits outside the outreach send quotas because it is not aimed at another member, but still refuses a sender that is not `active`. Own-post reads draw the other-API-read budget (200/day default, 1,000 max); `engagement harvest` draws the separate ingest budget (20/day, 200 max), so a big post drips over days. `oxygen engagement engagers` walks both sources up to `--max-pages` (default 5, max 20) × 100.\n- **Paid calls:** 1–5 variants per draft call (default 3), one AI call per draft and per review. Take every ceiling from the preview and `oxygen tools get <tool_id> --json`, never from prose; sweep ceilings from `oxygen blueprints preflight`.\n- **Comment SLA 48 hours**, over a rolling 30-day owned-post scope: an older thread is absent from the queue, not silent. Analytics refresh for ~30 days; reactions, comments and reshares are real; impressions come from LinkedIn's own post analytics, which it shows only to the author, so they are real for the connected member's original posts; saves and sends are not exposed.\n- **Outlier scoring:** `rank_score = 100 × √(outlier_multiple × audience_index)`, **100 is par**; a healthy sweep puts ~5–15% above 200. Defaults: `lookback_days` 7, `min_post_age_hours` 24, `min_author_followers` 1000, weights 1 / 3 / 5; 3–4 keywords (max 8), 10–25 creators (max 25).\n\n## Angle gate spec\n\nThe gate is not who gets contacted — nobody is contacted here — it is **which idea becomes a public post in the founder's name**. An item advances to draft only when all of these hold: it maps to a named pillar on `[[content-strategy]]`; it carries one concrete claim, story or artifact traceable to a wiki page, a shipped thing or a real customer outcome; it names the audience in plain words; if promotional, the week's share is still under 15%; and its voice matches the pinned voice page. The first three are enforced at drafting, because the draft call grounds on the wiki by default; voice by the free channel lint plus the paid `publishing posts review` check.\n\nNothing bypasses it, because **approval is per post and is not batchable server-side**: a draft accepted from the AI queue, a post created directly, and a row loaded by `oxygen publishing import` all land needs-approval. Borrowing from the bank has its own rule — lift the **structure**, never the text, and only from a row that cleared `rank_score` 200 with a `baseline_kind` you trust; `author_unresolved` and `maturing` rows are unscored and are not evidence.\n\n## Approval gates, mapped to who decides\n\n| Gate | What it authorises | Copilot posture |\n| --- | --- | --- |\n| kit apply (`oxygen recipes apply content-outlier-mining`) | 0-credit install of the sweep, workflow disabled | human card, once |\n| `draft-week` / `review-post` | one paid AI drafting or voice/claims call | human sets each cap from the preview |\n| `post-publish` | one public post in the founder's name, on this exact text | human, every post, never batched |\n| `comment-reply` | one public reply bound to the preview's `action_id` and `content_hash` | human approves that exact text |\n| `pilot-live` then `sweep-arm` | one live outlier cycle, then the recurring sweep | human for each; re-grant on reapply (revision-bound) |\n| amplification (`oxygen publishing amplification create`, then `enable`) | real public engagement from teammates' accounts, spending credits; LinkedIn may restrict those accounts | human twice: grant, then arm, each with `--acknowledge-risk` |\n| canonical wiki edits | voice / brand / positioning / pinned playbooks | a proposal a human approves |\n\nInstallation is never permission: the kit applies at 0 credits with its workflow disabled, and every paid run, publish, reply and armed sweep is its own gate above.\n\nReads need no approval (`oxygen publishing posts list`, `oxygen publishing analytics summary`, `oxygen senders health`, `oxygen tables query`); working wiki pages write as logged revisions.\n\n## Failure modes\n\n- **The two axes.** `--status` is pipeline position (`draft` → `scheduled` → `queued` → `published`); `--approval-status` is whether a human said yes. A queued post is both `scheduled` and `needs_approval`. Filter on the approval axis or you will report an empty queue you just filled.\n- **A `--publish-at` without an offset** is wall-clock time in `--timezone`: `publishing posts create` defaults that to the sender's own zone, `drafts accept` to UTC. Pass an offset or `Z` to pin the exact instant.\n- **The activity URN is not the `social_id`.** `posts reactions`, `posts comments`, `engagement engagers` and `engagement harvest` all need the composite `social_id` from `oxygen posts get`. Nothing lists your own feed, so record the id when the post goes out.\n- **Mentions and content.** A verified `@<public-identifier>` goes in the post text — a structured `mentions` array is ignored and `content.mentions` rejected; approve blocks anything unresolved. `--content-json` replaces the whole content object, so a media update drops an existing `first_comment` unless you resend it.\n- **A post on a dead sender never dispatches**; fix the account instead of re-queueing. A rate limit **defers** rather than drops and names `resets_at`, so retrying in a loop only burns quota. Company-page posting is a tested-negative anti-pattern — post from the personal profile.\n- **Generic drafts are a thin wiki, not a thin prompt**; a bank where everything scores near par is a keyword naming a category. Fix the page or the phrase, never the prompt length or the weights.\n- **Outreach leaking in.** The moment the plan is to message an engager, this motion is over — hand off at stage 9. Sequences own initiation; a CRM person is created at a warm-lead gate, never because someone reacted.\n\n## Data-quality checks (after every week)\n\n1. `oxygen publishing posts list --status published --json` — every planned slot published, or one sat `needs_approval` all week? A missed slot is the failure this motion exists to prevent.\n2. `oxygen publishing comments list --view unanswered --json` — nothing older than 48h, and never `resolved` on a thread still owed an answer.\n3. `oxygen publishing analytics summary --channel linkedin --range 30d --json` — all-zero after two days means `deferred`. Then read the comments, not the counts: ICP titles, or peers and recruiters?\n4. `oxygen tables query <bank_table_id> --limit 25 --json` — mostly `author_unresolved` or `maturing` is a cold cache or short lookback; a backlog under two weeks deep means next week starts from taste.\n\n## Learning loop\n\n- **What auto-files:** a draft edit snapshots the AI's original copy, so an accepted edit records what the founder changed; `oxygen publishing drafts reject <draft_id> --reason \"...\" --json` files the reason; revisions, review findings, the metric series and the comment queue are durable.\n- **What to synthesise weekly:** which pillar produced ICP-fit comments rather than peer likes; which post *shape* travelled (a number in line one, a named enemy, a before/after); and `oxygen knowledge synthesize --kind voice --json`, a voice guide distilled from real sent copy.\n- **What to change, one rule per loop:** the pillar mix, a slot's time, a keyword on the source table (pause it, never delete it), the CTA, or the voice page. Write the change and its reason into the wiki; a voice or positioning change files a proposal.\n- **Define \"good\" first:** an inbound conversation from an ICP-fit person — a DM, a comment thread that becomes a call, a reply naming their own version of the problem. Not reach, not reactions, not a bank with more rows.\n\n## Go-live checklist\n\n1. `oxygen senders list --status active --json`, then `oxygen senders health <sender_id> --json` — one healthy account, no open checkpoint.\n2. `oxygen knowledge resolve --purpose outbound_copy --require-ready --json`, then `oxygen knowledge page upsert --slug content-strategy --type strategy --status active --body \"...\" --json`.\n3. `oxygen publishing ideas add --text \"...\" --topic <pillar> --json` until two weeks of slots exist, then draft and queue each slot (stage 3).\n4. `oxygen publishing posts list --approval-status needs_approval --json`, present the week, then `oxygen publishing posts approve <post_id> --json` one post at a time.\n5. `oxygen publishing comments list --view unanswered --json` — the 48h loop runs and a named person owns it.\n6. `oxygen recipes apply content-outlier-mining --dry-run --json`, apply, one `oxygen workflows call <workflow_id> --mode dry-run --json`, one `pilot-live` cycle, then `oxygen workflows enable <workflow_id> --approved --max-credits <cap> --json`.\n7. `oxygen budget list --json` for the org backstop, then `oxygen knowledge log append --event note --slug content-strategy --json`.\n\n## Open questions (state them, do not resolve them)\n\n- LinkedIn shows impressions only to a post's author and never exposes saves or sends. Reach is measurable for the connected member's original posts (and earned-media value once a CPM is set with `oxygen publishing analytics emv`); for anything else it stays empty. Never substitute a proxy and call it reach.\n- The cadence and mix numbers are operator estimates, not measured benchmarks. Replace them with four weeks of your own evidence.\n- The outlier kit is `beta`, and `oxygen recipes apply content-outlier-mining` installs only the keyword sweep; the creator watch applies separately against the same tables with `--table-ref`, and omitting those flags reports a table collision — the guard working.\n- Whether engagers flow automatically into outreach is not this playbook's call; stage 9 hands them over deliberately. Amplification from teammates' accounts is likewise a founder decision about the company's public name.\n\n## Resume point\n\n`oxygen recipes show founder-posting-system --json` and `oxygen knowledge page get content-strategy --json`, then `oxygen publishing posts list --json`, `oxygen publishing comments list --json` and `oxygen workflows list --json`. Start from what exists, never from scratch.\n",
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
slug: "inbound-led-outbound",
|
|
@@ -85,6 +85,17 @@ export function buildCreditGuidance(input) {
|
|
|
85
85
|
// fall-through really could outrun the balance; saying it otherwise invents
|
|
86
86
|
// a risk that does not exist.
|
|
87
87
|
if (expectedCredits !== null) {
|
|
88
|
+
if (input.worstCaseIsHardBound === true && expectedCredits === estimatedCredits) {
|
|
89
|
+
return {
|
|
90
|
+
...expectedFields,
|
|
91
|
+
estimated_credits: estimatedCredits,
|
|
92
|
+
recommended_max_credits: recommendedMaxCredits,
|
|
93
|
+
available_credits: availableCredits,
|
|
94
|
+
credit_posture: "sufficient",
|
|
95
|
+
credit_guidance: `At the fixed per-row price, the selected rows are estimated at ${formatCreditsForCopy(estimatedCredits)} credits. `
|
|
96
|
+
+ "recommended_max_credits is a safety ceiling; unused credits are not spent.",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
88
99
|
const tailWarning = estimatedCredits > availableCredits
|
|
89
100
|
? " Only a run where nearly every row falls through to the dearest provider could exceed your"
|
|
90
101
|
+ " balance — the run keeps going and stops when the balance runs out, keeping every row it"
|
|
@@ -138,7 +149,7 @@ function normalizeMultiplier(value) {
|
|
|
138
149
|
: DEFAULT_HEADROOM_MULTIPLIER;
|
|
139
150
|
}
|
|
140
151
|
function formatCreditsForCopy(value) {
|
|
141
|
-
return
|
|
152
|
+
return value.toLocaleString("en-US", { maximumFractionDigits: 3 });
|
|
142
153
|
}
|
|
143
154
|
function normalizeCreditValue(value) {
|
|
144
155
|
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
@@ -12,7 +12,21 @@ export type EnrichmentIntentKind = "profile" | "enrichment" | "verification" | "
|
|
|
12
12
|
*/
|
|
13
13
|
export type EnrichmentIntentStatus = "default" | "selectable" | "no_default" | "coming_soon" | "rejected";
|
|
14
14
|
export type EnrichmentIntentOutputField = {
|
|
15
|
+
/**
|
|
16
|
+
* The column the row's printed CLI/MCP add path creates and a run writes
|
|
17
|
+
* (the web picker names a column after the label typed in its editor).
|
|
18
|
+
* apps/web/src/lib/enrichment-catalogue-add-paths.test.ts builds each add
|
|
19
|
+
* path and fails on a key it does not write.
|
|
20
|
+
*/
|
|
15
21
|
key: string;
|
|
22
|
+
/**
|
|
23
|
+
* Set when the value lives INSIDE that column's cell: a declared output field
|
|
24
|
+
* of the column (`{{key.path}}`, a sub-column in the grid, a filter path).
|
|
25
|
+
* An enrichment column's cell is `{value, confidence, status, source,
|
|
26
|
+
* attempts}`; its flat companions (`email`, `mobile_phone_e164`, …) are
|
|
27
|
+
* filled only when the table already has them, so a promise never names one.
|
|
28
|
+
*/
|
|
29
|
+
path?: string;
|
|
16
30
|
label: string;
|
|
17
31
|
/** `company.*` / `person.*` semantic type (packages/tenant-db/src/model.ts) or null. */
|
|
18
32
|
semanticType: string | null;
|
|
@@ -45,6 +59,25 @@ export type EnrichmentIntentSurfaces = {
|
|
|
45
59
|
/** For per-field company rows: the exact `field:<key>` chains of `enrich_company`. */
|
|
46
60
|
registryInputShapes?: readonly string[];
|
|
47
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* An opt-in add-on of a row's column (founder, 2026-09-29: one email column,
|
|
64
|
+
* "with an additional select to add personal email"). Selected, the parent's
|
|
65
|
+
* column also runs another row's chain on each row and writes that answer into
|
|
66
|
+
* the same cell. The other row is `selectable`, names the parent in `optionOf`
|
|
67
|
+
* and has no add path of its own: this option is its add path.
|
|
68
|
+
*/
|
|
69
|
+
export type EnrichmentIntentOption = {
|
|
70
|
+
/** Stable id, also the column/API/MCP argument (`include_personal_email: true`). */
|
|
71
|
+
id: string;
|
|
72
|
+
/** Taxonomy row whose chain the option runs. */
|
|
73
|
+
intent: string;
|
|
74
|
+
/** The switch's customer label on the web configure step. */
|
|
75
|
+
label: string;
|
|
76
|
+
/** Flag on `columns add --capability <parent>` and `enrich-column`. */
|
|
77
|
+
cliFlag: string;
|
|
78
|
+
/** Fields the option adds to the parent's column. */
|
|
79
|
+
outputFields: readonly EnrichmentIntentOutputField[];
|
|
80
|
+
};
|
|
48
81
|
export type EnrichmentIntentTaxonomyEntry = {
|
|
49
82
|
/** Stable id: registry intent id, `enrich_company:<group>`, `people_search:<intent>`, `signals:<intent>`, or a slug. */
|
|
50
83
|
id: string;
|
|
@@ -70,6 +103,10 @@ export type EnrichmentIntentTaxonomyEntry = {
|
|
|
70
103
|
decision?: string;
|
|
71
104
|
/** Provider ids (metadata only) that document the data, for no_default / coming_soon rows. */
|
|
72
105
|
candidateProviders?: readonly string[];
|
|
106
|
+
/** Opt-in add-ons of this row's column. */
|
|
107
|
+
options?: readonly EnrichmentIntentOption[];
|
|
108
|
+
/** Set on a row reached only as another row's option: that row's id. */
|
|
109
|
+
optionOf?: string;
|
|
73
110
|
};
|
|
74
111
|
export declare const ENRICHMENT_INTENT_TAXONOMY: readonly EnrichmentIntentTaxonomyEntry[];
|
|
75
112
|
/** True when a customer-facing string leaks vendor cost structure (OXYGEN_PRODUCT.md 2026-08-10). */
|