weftens 0.1.1 → 0.1.3

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/src/registry.js CHANGED
@@ -1,160 +1,163 @@
1
- // The Weftens agent registry — the single source of truth for which agents exist and
2
- // how a request finds one.
3
- //
4
- // Routing is deterministic and explainable (see agent-router.js): a request matches an
5
- // agent by its declared routeHints, and you can always see which hints matched. No
6
- // model call is needed to route — the model only runs when an agent is invoked to
7
- // produce, so the routing layer is verifiable with no API key.
8
- //
9
- // Fields:
10
- // tier 1 = lead that owns sub-agents (richy, pio, content); 2 = leaf. invoke()
11
- // branches on this.
12
- // core path to a deterministic agent-cores package, or absent.
13
- // coreBin, how to shell that core's CLI (it does parse+analyze+format). Present only
14
- // coreInputFlag where wired; absent -> invoke reports the core isn't wired yet.
15
- // leads tier-1 content only: the pipeline it delegates to.
16
- // wire tier-1 richy/pio only: they answer through the existing orchestrator, not here.
17
- // ownedBy content-team seats owned by the Content lead — not dispatched by the door.
18
- // seatFile the .md seat used as the system prompt when a seat produces via the model.
19
- // routeHints keyword/phrase signals for the router.
20
- //
21
- // Note: seo-technical+seo-content and amazon-ppc+paid-search map to single real job
22
- // postings, so they are merge candidates — but the merge is deferred until the roster
23
- // actually needs consolidating. Merging early only adds structure nothing reads yet.
24
-
25
- // One home: the agent seats live in this repo (weftens/agents). Override with
26
- // WEFTENS_AGENTS_DIR. No external directory and no fallback — one canonical location.
27
- import { join, dirname } from "node:path";
28
- import { fileURLToPath } from "node:url";
29
-
30
- const AGENTS_DIR = process.env.WEFTENS_AGENTS_DIR || join(dirname(fileURLToPath(import.meta.url)), "..", "agents");
31
- const seat = (name) => join(AGENTS_DIR, `${name}.md`);
32
-
33
- export const REGISTRY = [
34
- // ---- Tier 1: lead agents (own sub-agents / their own engines) ----
35
- {
36
- name: "richy", tier: 1, team: "weftens", wire: "representationCheck", seatFile: seat("richy"),
37
- routeHints: ["represent", "representation", "reputation", "online", "website", "site",
38
- "review", "listing", "search", "seo", "visibility", "audit", "appear",
39
- "how do we look", "what do people see", "brand", "knowledge panel", "ai answer"],
40
- },
41
- {
42
- name: "pio", tier: 1, team: "weftens", wire: "operationsSummary", seatFile: seat("pio"),
43
- routeHints: ["event", "attendee", "participant", "check-in", "checkin", "roster",
44
- "grant", "funding", "funder", "program", "people", "follow-up", "followup", "donor",
45
- "prospect", "attendance", "registration", "operation", "who should i meet", "dossier",
46
- // Bare "people" is too generic to dispatch on (it's a weak hint), but these PHRASES are
47
- // unambiguous asks that a customer actually types. Without them, "we need people ops"
48
- // matched only the weak "people" and fell through to no-route.
49
- "people ops", "people operations", "ops help", "back office", "development ops",
50
- "who funds", "funding pipeline", "grant research", "prospect research"],
51
- },
52
- {
53
- name: "content", tier: 1, team: "content", leads: "content-org", seatFile: null,
54
- routeHints: ["make content", "content", "script", "reel", "video", "caption", "post",
55
- "series", "episode", "produce", "storyboard", "shot list"],
56
- },
57
-
58
- // ---- Tier 2: marketing/SEO leaves with deterministic agent-cores ----
59
- {
60
- name: "seo-technical", tier: 2, team: "seo", core: "agent-cores/seo-technical", seatFile: seat("seo-technical"),
61
- routeHints: ["crawl", "index", "indexing", "render", "schema", "structured data",
62
- "robots", "sitemap", "canonical", "core web vitals", "technical seo", "rich result"],
63
- },
64
- {
65
- name: "seo-content", tier: 2, team: "seo", core: "agent-cores/seo-content", seatFile: seat("seo-content"),
66
- routeHints: ["keyword", "topic", "entity", "content strategy", "brief",
67
- "serp", "rankings", "answer surface", "citation", "gsc", "search console"],
68
- },
69
- {
70
- // paid-search is the one core whose CLI takes a flag (--search-terms <csv>) rather
71
- // than a positional file, so it overrides the positional default in invoke.
72
- name: "paid-search", tier: 2, team: "marketing", core: "agent-cores/paid-search",
73
- coreInputFlag: "--search-terms", seatFile: seat("paid-search"),
74
- routeHints: ["google ads", "paid search", "ppc", "search terms", "negatives",
75
- "bid", "budget", "impression share", "quality score", "change sheet"],
76
- },
77
- {
78
- name: "amazon-ppc", tier: 2, team: "marketing", core: "agent-cores/amazon-ppc", seatFile: seat("amazon-ppc"),
79
- routeHints: ["amazon", "amazon ads", "sponsored products", "acos", "tacos",
80
- "harvest", "negate", "amazon search term", "sponsored brand"],
81
- },
82
- {
83
- name: "meta-ads", tier: 2, team: "marketing", core: "agent-cores/meta-ads", seatFile: seat("meta-ads"),
84
- routeHints: ["meta", "facebook", "instagram", "meta ads", "facebook ads", "instagram ads",
85
- "ad set", "adset", "creative fatigue", "frequency", "roas", "ads manager"],
86
- },
87
- {
88
- name: "supply-verify", tier: 2, team: "marketing", core: "agent-cores/supply-verify", seatFile: seat("supply-verify"),
89
- routeHints: ["placement", "mfa", "made for advertising", "ivt", "invalid traffic",
90
- "supply path", "log-level", "programmatic", "ad spend waste", "exclusion"],
91
- },
92
- {
93
- name: "content-ops", tier: 2, team: "content", core: "agent-cores/content-ops", seatFile: seat("content-ops"),
94
- routeHints: ["content inventory", "cms export", "keep update retire", "migration",
95
- "content audit", "tagging", "metadata cleanup", "template conform"],
96
- },
97
-
98
- // ---- Tier 2: marketing/PR leaves (no deterministic core — model-only) ----
99
- {
100
- name: "social-voice", tier: 2, team: "marketing", seatFile: seat("social-voice"),
101
- routeHints: ["social", "instagram", "tiktok", "linkedin", "reply", "engagement",
102
- "calendar", "per-platform", "voice-locked", "community management"],
103
- },
104
- {
105
- name: "claims-content", tier: 2, team: "content", seatFile: seat("claims-content"),
106
- routeHints: ["health", "pharma", "finance", "supplement", "regulated", "claim",
107
- "medical review", "legal review", "compliance", "substantiation"],
108
- },
109
- {
110
- name: "growth-marketing", tier: 2, team: "marketing", seatFile: seat("growth-marketing"),
111
- // "cro"/"funnel"/"activation" are the words a practitioner uses; the words a CUSTOMER
112
- // uses for the same problem are "converts badly", "signups", "checkout". Without those
113
- // the seat was unreachable by anyone describing their own problem in their own words.
114
- routeHints: ["growth", "acquisition", "channel", "funnel", "cro", "lifecycle",
115
- "campaign design", "experiment", "retention", "activation",
116
- "convert", "converts", "conversion", "converting", "signup", "sign-up",
117
- "checkout", "landing page", "drop off", "drop-off", "abandon", "onboarding"],
118
- },
119
- {
120
- name: "security-writer", tier: 2, team: "security", seatFile: seat("security-writer"),
121
- routeHints: ["security content", "technical writing", "security explainer",
122
- "threat", "vulnerability writeup", "security training", "devsecops doc"],
123
- },
124
-
125
- // ---- Tier 2: evaluation leaves (agents whose job is judgment — normal pool) ----
126
- {
127
- name: "hilbert", tier: 2, team: "eval", seatFile: seat("hilbert"),
128
- routeHints: ["headline", "tagline", "name", "slogan", "will this survive",
129
- "memetic", "does this stop someone", "positioning line", "pitch line", "repeated"],
130
- },
131
- {
132
- name: "lippmann", tier: 2, team: "eval", seatFile: seat("lippmann"),
133
- routeHints: ["perception", "what do people believe", "public sources", "reputation read",
134
- "how is x seen", "sentiment", "frame provenance", "before committing"],
135
- },
136
- {
137
- name: "agent2060", tier: 2, team: "eval", seatFile: seat("agent2060"),
138
- routeHints: ["foresight", "future", "durable", "will this last", "trend", "horizon",
139
- "bet", "does this survive", "strategic call", "category"],
140
- },
141
-
142
- // ---- Tier 2: content-team seats, owned by the Content lead (not dispatched by the door) ----
143
- ...["content-strategy", "ideator", "researcher", "writer", "script-editor",
144
- "showrunner", "video-editor", "feed-optimizer", "disclosure-qc", "growth-analyst"]
145
- .map((name) => ({
146
- name, tier: 2, team: "content", seatFile: seat(name), ownedBy: "content",
147
- routeHints: [name.replace(/-/g, " ")],
148
- })),
149
- ];
150
-
151
- export function agentByName(name) {
152
- const key = String(name).toLowerCase().trim();
153
- return REGISTRY.find((a) => a.name === key) ?? null;
154
- }
155
-
156
- // Agents the front door auto-dispatches over — everything except the content-team
157
- // seats the Content lead owns (the door routes to `content`, which owns those).
158
- export function dispatchableAgents() {
159
- return REGISTRY.filter((a) => !a.ownedBy);
160
- }
1
+ // The Weftens agent registry — the single source of truth for which agents exist and
2
+ // how a request finds one.
3
+ //
4
+ // Routing is deterministic and explainable (see agent-router.js): a request matches an
5
+ // agent by its declared routeHints, and you can always see which hints matched. No
6
+ // model call is needed to route — the model only runs when an agent is invoked to
7
+ // produce, so the routing layer is verifiable with no API key.
8
+ //
9
+ // Fields:
10
+ // tier 1 = lead that owns sub-agents (richy, pio, content); 2 = leaf. invoke()
11
+ // branches on this.
12
+ // core path to a deterministic agent-cores package, or absent.
13
+ // coreBin, how to shell that core's CLI (it does parse+analyze+format). Present only
14
+ // coreInputFlag where wired; absent -> invoke reports the core isn't wired yet.
15
+ // leads tier-1 content only: the pipeline it delegates to.
16
+ // wire tier-1 richy/pio only: they answer through the existing orchestrator, not here.
17
+ // ownedBy content-team seats owned by the Content lead — not dispatched by the door.
18
+ // seatFile the .md seat used as the system prompt when a seat produces via the model.
19
+ // routeHints keyword/phrase signals for the router.
20
+ //
21
+ // Note: seo-technical+seo-content and amazon-ppc+paid-search map to single real job
22
+ // postings, so they are merge candidates — but the merge is deferred until the roster
23
+ // actually needs consolidating. Merging early only adds structure nothing reads yet.
24
+
25
+ // One home: the agent seats live in this repo (weftens/agents). Override with
26
+ // WEFTENS_AGENTS_DIR. No external directory and no fallback — one canonical location.
27
+ import { join, dirname } from "node:path";
28
+ import { fileURLToPath } from "node:url";
29
+
30
+ const AGENTS_DIR = process.env.WEFTENS_AGENTS_DIR || join(dirname(fileURLToPath(import.meta.url)), "..", "agents");
31
+ const seat = (name) => join(AGENTS_DIR, `${name}.md`);
32
+
33
+ export const REGISTRY = [
34
+ // ---- Tier 1: lead agents (own sub-agents / their own engines) ----
35
+ {
36
+ name: "richy", tier: 1, team: "weftens", wire: "representationCheck", seatFile: seat("richy"),
37
+ routeHints: ["represent", "representation", "reputation", "online", "website", "site",
38
+ "review", "listing", "search", "seo", "visibility", "audit", "appear",
39
+ "how do we look", "what do people see", "brand", "knowledge panel", "ai answer"],
40
+ },
41
+ {
42
+ name: "pio", tier: 1, team: "weftens", wire: "operationsSummary", seatFile: seat("pio"),
43
+ routeHints: ["event", "attendee", "participant", "check-in", "checkin", "roster",
44
+ "grant", "funding", "funder", "program", "people", "follow-up", "followup", "donor",
45
+ "prospect", "attendance", "registration", "operation", "who should i meet", "dossier",
46
+ // Bare "people" is too generic to dispatch on (it's a weak hint), but these PHRASES are
47
+ // unambiguous asks that a customer actually types. Without them, "we need people ops"
48
+ // matched only the weak "people" and fell through to no-route.
49
+ "people ops", "people operations", "ops help", "back office", "development ops",
50
+ "who funds", "funding pipeline", "grant research", "prospect research"],
51
+ },
52
+ {
53
+ name: "content", tier: 1, team: "content", leads: "content-org", seatFile: null,
54
+ routeHints: ["make content", "content", "script", "reel", "video", "caption", "post",
55
+ "series", "episode", "produce", "storyboard", "shot list"],
56
+ },
57
+
58
+ // ---- Tier 2: marketing/SEO leaves with deterministic agent-cores ----
59
+ {
60
+ name: "seo-technical", tier: 2, team: "seo", core: "agent-cores/seo-technical", seatFile: seat("seo-technical"),
61
+ routeHints: ["crawl", "index", "indexing", "render", "schema", "structured data",
62
+ "robots", "sitemap", "canonical", "core web vitals", "technical seo", "rich result"],
63
+ },
64
+ {
65
+ name: "seo-content", tier: 2, team: "seo", core: "agent-cores/seo-content", seatFile: seat("seo-content"),
66
+ routeHints: ["keyword", "topic", "entity", "content strategy", "brief",
67
+ "serp", "rankings", "answer surface", "citation", "gsc", "search console"],
68
+ },
69
+ {
70
+ // paid-search is the one core whose CLI takes a flag (--search-terms <csv>) rather
71
+ // than a positional file, so it overrides the positional default in invoke.
72
+ name: "paid-search", tier: 2, team: "marketing", core: "agent-cores/paid-search",
73
+ coreInputFlag: "--search-terms", seatFile: seat("paid-search"),
74
+ routeHints: ["google ads", "paid search", "ppc", "search terms", "negatives",
75
+ "bid", "budget", "impression share", "quality score", "change sheet"],
76
+ },
77
+ {
78
+ name: "amazon-ppc", tier: 2, team: "marketing", core: "agent-cores/amazon-ppc", seatFile: seat("amazon-ppc"),
79
+ routeHints: ["amazon", "amazon ads", "sponsored products", "acos", "tacos",
80
+ "harvest", "negate", "amazon search term", "sponsored brand"],
81
+ },
82
+ {
83
+ name: "meta-ads", tier: 2, team: "marketing", core: "agent-cores/meta-ads", seatFile: seat("meta-ads"),
84
+ // No bare "instagram": the platform name is not an intent, and social-voice (organic)
85
+ // wants the same word. A permanent tie routes by scoring accident, so both sides carry
86
+ // the phrase that says which job is being asked for.
87
+ routeHints: ["meta", "facebook", "meta ads", "facebook ads", "instagram ads",
88
+ "ad set", "adset", "creative fatigue", "frequency", "roas", "ads manager"],
89
+ },
90
+ {
91
+ name: "supply-verify", tier: 2, team: "marketing", core: "agent-cores/supply-verify", seatFile: seat("supply-verify"),
92
+ routeHints: ["placement", "mfa", "made for advertising", "ivt", "invalid traffic",
93
+ "supply path", "log-level", "programmatic", "ad spend waste", "exclusion"],
94
+ },
95
+ {
96
+ name: "content-ops", tier: 2, team: "content", core: "agent-cores/content-ops", seatFile: seat("content-ops"),
97
+ routeHints: ["content inventory", "cms export", "keep update retire", "migration",
98
+ "content audit", "tagging", "metadata cleanup", "template conform"],
99
+ },
100
+
101
+ // ---- Tier 2: marketing/PR leaves (no deterministic core — model-only) ----
102
+ {
103
+ name: "social-voice", tier: 2, team: "marketing", seatFile: seat("social-voice"),
104
+ routeHints: ["social", "instagram post", "post on instagram", "tiktok", "linkedin", "reply", "engagement",
105
+ "calendar", "per-platform", "voice-locked", "community management"],
106
+ },
107
+ {
108
+ name: "claims-content", tier: 2, team: "content", seatFile: seat("claims-content"),
109
+ routeHints: ["health", "pharma", "finance", "supplement", "regulated", "claim",
110
+ "medical review", "legal review", "compliance", "substantiation"],
111
+ },
112
+ {
113
+ name: "growth-marketing", tier: 2, team: "marketing", seatFile: seat("growth-marketing"),
114
+ // "cro"/"funnel"/"activation" are the words a practitioner uses; the words a CUSTOMER
115
+ // uses for the same problem are "converts badly", "signups", "checkout". Without those
116
+ // the seat was unreachable by anyone describing their own problem in their own words.
117
+ routeHints: ["growth", "acquisition", "channel", "funnel", "cro", "lifecycle",
118
+ "campaign design", "experiment", "retention", "activation",
119
+ "convert", "converts", "conversion", "converting", "signup", "sign-up",
120
+ "checkout", "landing page", "drop off", "drop-off", "abandon", "onboarding"],
121
+ },
122
+ {
123
+ name: "security-writer", tier: 2, team: "security", seatFile: seat("security-writer"),
124
+ routeHints: ["security content", "technical writing", "security explainer",
125
+ "threat", "vulnerability writeup", "security training", "devsecops doc"],
126
+ },
127
+
128
+ // ---- Tier 2: evaluation leaves (agents whose job is judgment — normal pool) ----
129
+ {
130
+ name: "hilbert", tier: 2, team: "eval", seatFile: seat("hilbert"),
131
+ routeHints: ["headline", "tagline", "name", "slogan", "will this survive",
132
+ "memetic", "does this stop someone", "positioning line", "pitch line", "repeated"],
133
+ },
134
+ {
135
+ name: "lippmann", tier: 2, team: "eval", seatFile: seat("lippmann"),
136
+ routeHints: ["perception", "what do people believe", "public sources", "reputation read",
137
+ "how is x seen", "sentiment", "frame provenance", "before committing"],
138
+ },
139
+ {
140
+ name: "agent2060", tier: 2, team: "eval", seatFile: seat("agent2060"),
141
+ routeHints: ["foresight", "future", "durable", "will this last", "trend", "horizon",
142
+ "bet", "does this survive", "strategic call", "category"],
143
+ },
144
+
145
+ // ---- Tier 2: content-team seats, owned by the Content lead (not dispatched by the door) ----
146
+ ...["content-strategy", "ideator", "researcher", "writer", "script-editor",
147
+ "showrunner", "video-editor", "feed-optimizer", "disclosure-qc", "growth-analyst"]
148
+ .map((name) => ({
149
+ name, tier: 2, team: "content", seatFile: seat(name), ownedBy: "content",
150
+ routeHints: [name.replace(/-/g, " ")],
151
+ })),
152
+ ];
153
+
154
+ export function agentByName(name) {
155
+ const key = String(name).toLowerCase().trim();
156
+ return REGISTRY.find((a) => a.name === key) ?? null;
157
+ }
158
+
159
+ // Agents the front door auto-dispatches over — everything except the content-team
160
+ // seats the Content lead owns (the door routes to `content`, which owns those).
161
+ export function dispatchableAgents() {
162
+ return REGISTRY.filter((a) => !a.ownedBy);
163
+ }
@@ -0,0 +1,50 @@
1
+ // Building the specialist clients (Richy, PIO) from the environment.
2
+ //
3
+ // This lived in bin/weftens.js, which meant the CLI could reach live specialist services
4
+ // and the SDK could not — the same call did different things depending on which door you
5
+ // came through. It belongs in src/ so both surfaces share one implementation.
6
+ //
7
+ // A specialist is live only when its FULL env group is set. A partial group is a hard
8
+ // error, not a silent fixture fallback: an operator who typo'd one variable would
9
+ // otherwise get sample data while believing they were looking at their own organization.
10
+
11
+ import { RichyClient, FixtureRichyClient } from "./richy-client.js";
12
+ import { PioClient, FixturePioClient } from "./pio-client.js";
13
+
14
+ function envGroup(label, names, env) {
15
+ const set = names.filter((n) => env[n]);
16
+ if (set.length === 0) return null;
17
+ if (set.length < names.length) {
18
+ const missing = names.filter((n) => !env[n]).join(", ");
19
+ throw new Error(
20
+ `${label} is partially configured — set ${missing} (or unset ${set.join(", ")} to run with sample data)`
21
+ );
22
+ }
23
+ return Object.fromEntries(names.map((n) => [n, env[n]]));
24
+ }
25
+
26
+ /**
27
+ * @param opts { env?, onFixture? } — onFixture is called with the names of the specialists
28
+ * falling back to sample data, so a CLI can warn on stderr and a library can stay quiet.
29
+ * @returns { richy, pio } clients — real when configured, clearly-flagged fixtures otherwise.
30
+ */
31
+ export function buildSpecialistClients({ env = process.env, onFixture } = {}) {
32
+ const richyEnv = envGroup("Richy", ["WEFTENS_RICHY_URL", "WEFTENS_RICHY_KEY"], env);
33
+ const pioEnv = envGroup("PIO", ["WEFTENS_PIO_URL", "WEFTENS_PIO_MEMBER", "WEFTENS_PIO_PASSWORD"], env);
34
+
35
+ const richy = richyEnv
36
+ ? new RichyClient({ baseUrl: richyEnv.WEFTENS_RICHY_URL, key: richyEnv.WEFTENS_RICHY_KEY })
37
+ : new FixtureRichyClient();
38
+ const pio = pioEnv
39
+ ? new PioClient({
40
+ baseUrl: pioEnv.WEFTENS_PIO_URL,
41
+ member: pioEnv.WEFTENS_PIO_MEMBER,
42
+ password: pioEnv.WEFTENS_PIO_PASSWORD,
43
+ tenant: env.WEFTENS_PIO_TENANT ?? null,
44
+ })
45
+ : new FixturePioClient();
46
+
47
+ const fixtures = [!richyEnv && "Richy", !pioEnv && "PIO"].filter(Boolean);
48
+ if (fixtures.length > 0 && typeof onFixture === "function") onFixture(fixtures);
49
+ return { richy, pio };
50
+ }
@@ -1,6 +0,0 @@
1
- {"ts":"2026-07-19T17:03:20.481Z","tool":"edit_file","source":"raw","status":"pass","counts":{"total":8,"pass":8,"warn":0,"fail":0},"topRules":[],"findings":[],"pathPreview":"C:\\Users\\teche\\weftens\\src\\registry.js","durationMs":3}
2
- {"ts":"2026-07-19T17:03:27.408Z","tool":"edit_file","source":"raw","status":"pass","counts":{"total":8,"pass":8,"warn":0,"fail":0},"topRules":[],"findings":[],"pathPreview":"C:\\Users\\teche\\weftens\\src\\invoke.js","durationMs":2}
3
- {"ts":"2026-07-19T17:03:35.845Z","tool":"edit_file","source":"raw","status":"pass","counts":{"total":8,"pass":8,"warn":0,"fail":0},"topRules":[],"findings":[],"pathPreview":"C:\\Users\\teche\\weftens\\src\\invoke.js","durationMs":2}
4
- {"ts":"2026-07-19T17:03:39.916Z","tool":"edit_file","source":"raw","status":"pass","counts":{"total":8,"pass":8,"warn":0,"fail":0},"topRules":[],"findings":[],"pathPreview":"C:\\Users\\teche\\weftens\\src\\invoke.js","durationMs":2}
5
- {"ts":"2026-07-19T17:03:56.374Z","tool":"write_file","source":"raw","status":"pass","counts":{"total":7,"pass":7,"warn":0,"fail":0},"topRules":[],"findings":[],"pathPreview":"C:\\Users\\teche\\weftens\\index.js","durationMs":2}
6
- {"ts":"2026-07-19T17:04:01.490Z","tool":"edit_file","source":"raw","status":"pass","counts":{"total":8,"pass":8,"warn":0,"fail":0},"topRules":[],"findings":[],"pathPreview":"C:\\Users\\teche\\weftens\\package.json","durationMs":2}
@@ -1,47 +0,0 @@
1
- # Format briefs
2
-
3
- The five formats the org produces. Each is a **brief the seats consume** — angle patterns, structure,
4
- and the disclosure stance. `RISK` in `config.mjs` sets which auto-post vs hold. Audience across all:
5
- **older Facebook users**; brand voice: *declare what's real*; disclosure is a **per-piece call, never an
6
- always-rule**; the scam guardrail is **theatre** (instant reveal, captures nothing, no real person harmed).
7
-
8
- ---
9
-
10
- ## traveler · auto
11
- The recurring time-traveler character dropped into a different time+place each episode. **The moat** —
12
- people subscribe to a *who*, not a vibe. Needs character-consistent clips (Veo/Seedance hero lane).
13
- - **Hook (3s):** the character mid-scene in a vivid era ("It's Tokyo, 1985, and…").
14
- - **Structure:** cold open in-scene → one wistful observation that ties past to now → button line.
15
- - **Disclosure:** "Imagined with AI" when it fits; sometimes the era-craft *is* the point. Kayla's call.
16
- - **Continuity:** same face, voice, and one signature line across episodes.
17
-
18
- ## scam-reveal · hold
19
- Nostalgia/relatable hook → theatrical "attempted hack" → **"YOU COULD HAVE BEEN HACKED"** → one defensive tell.
20
- - **Hook (3s):** a familiar scenario (a text from "your bank," a too-good deal).
21
- - **Structure:** set the trap → the reveal beat → one concrete thing the viewer can do tomorrow.
22
- - **HARD LINE:** theatre only. Instant reveal, page/piece captures NOTHING, no real non-consenting person
23
- is hacked or harmed. QC blocks anything that crosses this.
24
- - **Disclosure:** the reveal is the disclosure; label as AI where useful.
25
-
26
- ## is-this-real · hold
27
- Teach older viewers to spot AI fakes / deepfakes / cloned voices. **Most on-brand** — the content *is* the
28
- "declare what's real" demo.
29
- - **Hook (3s):** "Is this real? Watch closely." (a clip with one tell).
30
- - **Structure:** show the fake → reveal the tell(s) → "here's how to check next time."
31
- - **Disclosure:** always clarify what was AI vs real in-piece — here it's the whole point.
32
- - **QC:** claim accuracy is critical; never mislabel a real thing as fake or vice-versa.
33
-
34
- ## nostalgia · auto
35
- Straight "remember when" archival-feeling reach. Pure audience fuel; highest raw engagement with older FB.
36
- - **Hook (3s):** a specific sensory memory ("If you remember the sound of a rotary phone…").
37
- - **Structure:** montage of era details → one warm line of meaning → gentle prompt to comment.
38
- - **Disclosure:** label as AI-imagined where the footage is fabricated (Meta throttles undisclosed
39
- fake-real-events — proactive labeling protects reach).
40
- - **Bulk lane** (Wan) is fine; no recurring face needed.
41
-
42
- ## ai-for-seniors · hold
43
- Calm, plain-language explainers ("what's an agent?", "is the AI listening to me?"). Empowerment, no fear.
44
- - **Hook (3s):** a real question they've wondered ("Is your phone spying on you? Sort of — here's the truth").
45
- - **Structure:** the worry → the plain answer → one practical takeaway.
46
- - **Disclosure:** AI-made, said plainly; builds the brand credibility the flywheel cashes in.
47
- - **QC:** no false/overblown claims about AI; accuracy is the whole value.
@@ -1,78 +0,0 @@
1
- // content-org — central config. Change tiers/models here; nothing else needs editing.
2
-
3
- export const MODELS = {
4
- haiku: "claude-haiku-4-5", // $1/$5 per 1M — routine seats
5
- sonnet: "claude-sonnet-5", // $3/$15 — writing-quality seats
6
- opus: "claude-opus-4-8", // $5/$25 — only if a real test shows a lift
7
- };
8
-
9
- // Value tier (locked 2026-07-08): pay for quality only where it moves reach/moat.
10
- // Sonnet on the seats whose prose the viewer feels (Strategy board, Script, Editor);
11
- // Haiku on the structured hand-offs the viewer never sees.
12
- export const SEAT_MODEL = {
13
- showrunner: "haiku",
14
- strategy: "sonnet",
15
- ideator: "haiku",
16
- research: "haiku",
17
- script: "sonnet",
18
- editor: "sonnet",
19
- cut: "sonnet", // Video Editor — the cut decides completion rate; worth Sonnet
20
- feed: "haiku",
21
- qc: "haiku",
22
- growth: "haiku", // Growth/Analyst — reads the deterministic metrics.mjs summary; structured hand-off, viewer never sees it
23
- };
24
-
25
- // Clip lanes — pick per piece. Prices are RANGES (sources disagree; price the exact
26
- // tier on fal.ai at commit). No flat-unlimited *automated* lane exists (verified 2026-07-08).
27
- export const CLIP_LANES = {
28
- manual: { label: "manual flat sub", usdPerSec: 0, note: "hand-generate; ~$0 marginal; cheapest" },
29
- bulk: { label: "Wan 2.6 (bulk B-roll)", usdPerSec: 0.06, audio: true },
30
- hero: { label: "Veo 3.1 (hero / recurring face)", usdPerSec: 0.05, audio: true, note: "value king: audio + consistency" },
31
- // Kling deliberately omitted from defaults — priciest, no consistency edge over Veo/Seedance.
32
- };
33
-
34
- // Approx token cost per finished piece by model mix (for cost.mjs). ~per-piece USD of the
35
- // 8 text-seat calls; Batch API halves it. Rough, tune against real usage.
36
- export const TEXT_COST_PER_PIECE = {
37
- valueTier: 0.20, // Sonnet(strategy/script/editor) + Haiku(rest)
38
- valueTierBatch: 0.10, // with Batch API (50% off)
39
- allHaiku: 0.05,
40
- allOpus: 0.55,
41
- };
42
-
43
- export const VOICE = {
44
- elevenlabs: { label: "ElevenLabs Starter", usdPerMonth: 5, perPieceUsd: 0.03 },
45
- kokoro: { label: "Kokoro (OSS, local)", usdPerMonth: 0, perPieceUsd: 0 },
46
- veoNative: { label: "Veo native audio (no separate voice)", usdPerMonth: 0, perPieceUsd: 0 },
47
- };
48
-
49
- // Client-facing price tiers (DRAFT — Kayla to confirm numbers). reels = pieces/month.
50
- export const CLIENT_TIERS = {
51
- starter: {
52
- name: "Starter", priceUsd: 399, reels: 8,
53
- clip: "bulk", script: "sonnet", voice: "elevenlabs",
54
- recurringCharacter: 0, managedPosting: false, complianceQC: "basic", revisions: 1,
55
- },
56
- growth: {
57
- name: "Growth", priceUsd: 899, reels: 20,
58
- clip: "hero+bulk", script: "sonnet", voice: "elevenlabs",
59
- recurringCharacter: 1, managedPosting: true, complianceQC: "disclosed+claim-clean",
60
- analytics: "monthly", revisions: 2,
61
- },
62
- signature: {
63
- name: "Signature", priceUsd: 1999, reels: 40,
64
- clip: "hero", script: "sonnet+opus-hero", voice: "elevenlabs",
65
- recurringCharacter: 2, managedPosting: true, complianceQC: "senior/finance/health compliance pass",
66
- analytics: "full+strategy", revisions: "generous",
67
- },
68
- alacarte: { name: "À la carte", priceUsd: 100, reels: 1, note: "$75–150/Reel for one-offs & tests" },
69
- };
70
-
71
- // Format → risk tier (tiered approval). Sensitive formats HOLD for Kayla.
72
- export const RISK = {
73
- "traveler": "auto",
74
- "nostalgia": "auto",
75
- "scam-reveal": "hold",
76
- "is-this-real": "hold",
77
- "ai-for-seniors": "hold",
78
- };
@@ -1,36 +0,0 @@
1
- // content-org — COGS + margin calculator. `node cost.mjs [piecesPerMonth]`
2
- // No dependencies. Prices are estimates from config.mjs — tune against real usage.
3
- import { CLIENT_TIERS, TEXT_COST_PER_PIECE, CLIP_LANES, VOICE } from "./config.mjs";
4
-
5
- const usd = (n) => "$" + n.toFixed(2);
6
- const REEL_SECONDS = 30; // ~30s Reel
7
-
8
- // per-finished-Reel COGS given a clip lane + text tier + voice
9
- function cogsPerReel({ clip = "hero", textTier = "valueTierBatch", voice = "elevenlabs" } = {}) {
10
- const text = TEXT_COST_PER_PIECE[textTier] ?? TEXT_COST_PER_PIECE.valueTierBatch;
11
- const clipUsd = (CLIP_LANES[clip]?.usdPerSec ?? 0) * REEL_SECONDS;
12
- const voiceUsd = VOICE[voice]?.perPieceUsd ?? 0;
13
- return text + clipUsd + voiceUsd;
14
- }
15
-
16
- console.log("=== per-Reel COGS by clip lane (value text tier + Batch, ElevenLabs voice) ===");
17
- for (const lane of Object.keys(CLIP_LANES)) {
18
- console.log(` ${lane.padEnd(7)} ${usd(cogsPerReel({ clip: lane }))} (${CLIP_LANES[lane].label})`);
19
- }
20
-
21
- console.log("\n=== client tiers (DRAFT prices) ===");
22
- for (const key of ["starter", "growth", "signature"]) {
23
- const t = CLIENT_TIERS[key];
24
- const lane = key === "starter" ? "bulk" : "hero";
25
- const cogs = cogsPerReel({ clip: lane }) * t.reels;
26
- const voiceSub = VOICE.elevenlabs.usdPerMonth; // one sub covers all clients
27
- const margin = t.priceUsd - cogs;
28
- const marginPct = ((margin / t.priceUsd) * 100).toFixed(0);
29
- console.log(` ${t.name.padEnd(10)} ${usd(t.priceUsd)}/mo ${String(t.reels).padStart(2)} Reels COGS ${usd(cogs)} → margin ${usd(margin)} (${marginPct}%)`);
30
- }
31
- console.log(` (+ shared fixed: ElevenLabs sub ${usd(VOICE.elevenlabs.usdPerMonth)}/mo across all clients; tooling floor ~$130/mo automated)`);
32
-
33
- console.log("\n=== YOUR OWN pages ===");
34
- const own = Number(process.argv[2] || "90");
35
- const ownCogs = cogsPerReel({ clip: "hero" }) * own;
36
- console.log(` ${own} Reels/mo automated (Veo hero): clips+text+voice ≈ ${usd(ownCogs)} | manual clips ≈ ${usd(cogsPerReel({ clip: "manual" }) * own)}`);
@@ -1,64 +0,0 @@
1
- // Content lead — the single callable entry the Weftens front door hands to when a
2
- // request is about making content. The door calls this; this runs the 10-seat
3
- // pipeline and returns one result. The door never addresses the individual seats
4
- // (ideator, writer, qc, …) — the Content lead owns them.
5
- //
6
- // It shells the existing orchestrator rather than importing a run() function: the
7
- // orchestrator's full-run body is argv/global-driven, and wrapping it by shell keeps
8
- // its 27 passing tests untouched (no refactor risk). The orchestrator writes packet
9
- // JSON into content-org/inbox/<date>/; we run it, read those packets back, and return
10
- // them as the result.
11
- //
12
- // Defaults to dry-run. A real content run depends on Phase-0 decisions that are
13
- // Kayla's and still open (local vs cloud, cadence, keys) — so this wiring proves the
14
- // pipeline is callable as one node WITHOUT producing real published content. Pass
15
- // { dry: false } only once those decisions are made and a key is configured.
16
-
17
- import { execFile } from "node:child_process";
18
- import { promisify } from "node:util";
19
- import { readFile, readdir, rm } from "node:fs/promises";
20
- import { fileURLToPath } from "node:url";
21
- import { dirname, join } from "node:path";
22
-
23
- const execFileP = promisify(execFile);
24
- const HERE = dirname(fileURLToPath(import.meta.url));
25
-
26
- /**
27
- * Run the content pipeline as one node.
28
- * @param opts { format?, count?, client?, tier?, dry? } // dry defaults true (safe)
29
- * @returns { dry, format, count, artifacts, result, log }
30
- * artifacts packet file paths produced
31
- * result the parsed packets (dry-run packets are stub-shaped, clearly slugged dryrun-N)
32
- */
33
- export async function runContent(opts = {}) {
34
- const { format = "scam-reveal", count = 1, client = null, tier = null, dry = true } = opts;
35
-
36
- const args = ["orchestrator.mjs", format, String(count)];
37
- if (client) args.push(`--client=${client}`);
38
- if (tier) args.push(`--tier=${tier}`);
39
- if (dry) args.push("--dry-run");
40
-
41
- const { stdout } = await execFileP("node", args, { cwd: HERE, maxBuffer: 8_000_000 });
42
-
43
- // The orchestrator writes to inbox/<date>/; recover <date> from its own output line
44
- // ("Packets in inbox/2026-07-19/.") rather than recomputing a clock here.
45
- const m = stdout.match(/Packets in (inbox[\\/][^\s/]+)/);
46
- const outRel = m ? m[1].replace(/[\\/]$/, "") : null;
47
- const artifacts = [];
48
- const result = [];
49
- if (outRel) {
50
- const outDir = join(HERE, outRel);
51
- for (const name of (await readdir(outDir)).filter((n) => n.endsWith(".json"))) {
52
- const file = join(outDir, name);
53
- artifacts.push(file);
54
- result.push(JSON.parse(await readFile(file, "utf8")));
55
- }
56
- // A dry run's stub packets are throwaway — remove them so a verification run does
57
- // not litter content-org/inbox. Real runs keep their packets.
58
- if (dry) {
59
- await Promise.all(artifacts.map((f) => rm(f, { force: true })));
60
- }
61
- }
62
-
63
- return { dry, format, count, artifacts, result, log: stdout.trim() };
64
- }