Merge branch origin/copilot/sub-pr-2 into main

This commit is contained in:
Alexa Amundson
2025-11-28 22:56:31 -06:00
24 changed files with 1807 additions and 1 deletions

View File

@@ -0,0 +1,37 @@
name: Creator Studio Pack CI
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install Node dependencies
run: npm ci
- name: Run TypeScript tests
run: npm test
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Python dependencies
run: pip install -r requirements.txt
- name: Run pytest
run: pytest
- name: Validate YAML manifests
run: python scripts/validate_yaml.py

View File

@@ -1 +1,38 @@
# blackroad-os-pack-creator-studio # Creator Studio Pack
Creator-focused pack for BlackRoad OS with content planning, longform scripting, and shortform clip workflows.
## Repo Structure
- `pack.yaml` — pack manifest (id, roles, dependencies)
- `agents/` — agent implementations and metadata
- `workflows/` — orchestrations for campaigns and calendars
- `presets/` — reusable content pillar and brand voice profiles
- `docs/` — playbooks, pipeline map, and rights guidance
- `tests/` — TypeScript and Python tests for agents
- `.github/workflows/` — CI for linting, tests, and YAML validation
## Integration with BlackRoad OS
1. **Register the pack** in your Pack Index using `pack.yaml` so agents and workflows are discoverable.
2. **Trigger workflows** via the Operator API:
```ts
// Example: triggering a campaign via the Operator API
await client.startWorkflow("creator_campaign_launch", {
goals,
audience,
});
```
3. **Customize presets** by editing `presets/content_pillars.yaml` and `presets/brand_voice_profiles.yaml` or layering deployment-specific overrides.
4. **Observe in Prism Console (future hook):** upcoming calendars, running campaigns, and performance metrics can be surfaced once the Prism dashboard adapter is wired.
## Usage
- Use `workflows/campaign_launch.workflow.yaml` for end-to-end campaign orchestration (pillars → scripts → clips → calendar).
- Use `workflows/weekly_content_calendar.workflow.yaml` for recurring calendar refreshes informed by performance signals.
## Testing
- Node/TypeScript tests: `npm test`
- Python tests: `pytest`
- YAML validation: `python scripts/validate_yaml.py`
## Limitations
- Channel adapters (YouTube, TikTok, etc.) are not wired; outputs are adapter-ready stubs.
- Analytics hooks are placeholders; performance insights should be injected via API or data warehouse taps.

View File

@@ -0,0 +1,30 @@
id: cs-content-strategist-01
display_name: "Content Strategist // The Narrative Architect"
role_archetype: Growth Catalyst
org_layer: domain-specialist
personality:
traits: [story_driven, data_informed, playful]
leadership_style: collaborator
capabilities:
- content_pillar_design
- campaign_mapping
- editorial_calendar
identity:
ps_sha_infinity: "pssha∞:br:cs-content-strategist-01:<HASH>"
version: 1
policy_profile:
risk_band: medium
allowed_domains: [marketing, creator]
prohibited_actions:
- modify_legal_policies
- send_live_posts_without_review
suitability_profile:
ideal_context:
- "multi-channel_campaigns"
- "brand_launches"
anti_patterns:
- "churns_random_trends"
- "forgets_business_goals"
owner: "@Alexa"
home_repo: blackroad-os-pack-creator-studio
lifecycle: active

View File

@@ -0,0 +1,101 @@
export interface BrandGoals {
primaryObjective: string;
secondaryObjectives?: string[];
kpis?: string[];
}
export interface AudienceProfile {
name: string;
motivations: string[];
painPoints: string[];
preferredChannels: string[];
}
export interface ContentPillar {
id: string;
label: string;
description: string;
primaryChannels?: string[];
}
export interface CadenceConfig {
frequency: "daily" | "weekly" | "biweekly" | "monthly";
slotsPerWeek?: number;
preferredDays?: string[];
}
export interface CalendarEntry {
pillarId: string;
title: string;
channel: string;
scheduledFor: string;
format: string;
}
export function designContentPillars(
goals: BrandGoals,
audience: AudienceProfile
): ContentPillar[] {
const basePillars: ContentPillar[] = [
{
id: "vision-roadmap",
label: "Vision & Roadmap",
description: `Connecting ${audience.name} to where the brand is headed and why it matters to their goals`,
primaryChannels: ["blog", "linkedin"],
},
{
id: "behind-the-scenes",
label: "Behind the Scenes",
description: "Transparent builds, sprints, and what the team is learning along the way",
primaryChannels: ["youtube", "newsletter"],
},
{
id: "education-deep-dives",
label: "Education / Deep Dives",
description: `How-tos and explainers that remove the pain points: ${audience.painPoints.join(", ")}`,
primaryChannels: ["blog", "youtube", "twitter"],
},
];
if (goals.primaryObjective.toLowerCase().includes("community")) {
basePillars.push({
id: "community-spotlight",
label: "Community Spotlight",
description: "Featuring collaborators, partners, and early adopters to build trust",
primaryChannels: audience.preferredChannels,
});
}
return basePillars;
}
export function generateContentCalendar(
pillars: ContentPillar[],
cadence: CadenceConfig
): CalendarEntry[] {
const slots = cadence.slotsPerWeek ?? 3;
const days = cadence.preferredDays ?? ["Mon", "Wed", "Fri"];
const calendar: CalendarEntry[] = [];
// Track how many entries are scheduled for each day to stagger times
const dayEntryCount: Record<string, number> = {};
for (let i = 0; i < slots; i += 1) {
const day = days[i % days.length];
const pillar = pillars[i % pillars.length];
const channel = pillar.primaryChannels?.[0] ?? "blog";
// Determine the hour for this entry (start at 10:00, increment by 1 for each additional entry on the same day)
const entryCount = dayEntryCount[day] ?? 0;
const hour = 10 + entryCount;
dayEntryCount[day] = entryCount + 1;
calendar.push({
pillarId: pillar.id,
title: `${pillar.label}: ${pillar.description.slice(0, 40)}...`,
channel,
scheduledFor: `${day} ${hour.toString().padStart(2, "0")}:00`,
format: channel === "youtube" ? "video" : "article",
});
}
return calendar;
}

View File

@@ -0,0 +1,30 @@
id: cs-scriptwriter-longform-01
display_name: "Scriptwriter // Longform Architect"
role_archetype: Growth Catalyst
org_layer: domain-specialist
personality:
traits: [structured, narrative_focused, empathetic]
leadership_style: collaborator
capabilities:
- longform_scriptwriting
- tone_matching
- runtime_editing
identity:
ps_sha_infinity: "pssha∞:br:cs-scriptwriter-longform-01:<HASH>"
version: 1
policy_profile:
risk_band: medium
allowed_domains: [marketing, creator]
prohibited_actions:
- clickbait-only framing
- over-promising outcomes
suitability_profile:
ideal_context:
- "storytelling-led_campaigns"
- "longform_content_systems"
anti_patterns:
- "clickbait-only framing"
- "over-promising outcomes"
owner: "@Alexa"
home_repo: blackroad-os-pack-creator-studio
lifecycle: active

View File

@@ -0,0 +1,46 @@
export interface ScriptOutline {
title: string;
keyPoints: string[];
audience: string;
}
export interface BrandVoiceProfile {
id: string;
adjectives: string[];
avoid?: string[];
emoji_usage?: string;
}
export interface ScriptDraft {
title: string;
segments: string[];
durationMinutes: number;
toneNotes: string;
}
export function draftScript(
outline: ScriptOutline,
brandVoice: BrandVoiceProfile
): ScriptDraft {
const tone = brandVoice.adjectives.join(", ");
const segments = outline.keyPoints.map((point, index) => `Segment ${index + 1}: ${point}`);
return {
title: outline.title,
segments,
durationMinutes: Math.max(segments.length * 2, 5),
toneNotes: `Voice: ${tone}. Avoid: ${(brandVoice.avoid ?? []).join(", ")}`,
};
}
export function tightenForRuntime(
draft: ScriptDraft,
targetMinutes: number
): ScriptDraft {
const maxSegments = Math.max(1, Math.floor(targetMinutes / 2));
const trimmedSegments = draft.segments.slice(0, maxSegments);
return {
...draft,
segments: trimmedSegments,
durationMinutes: targetMinutes,
};
}

View File

@@ -0,0 +1,28 @@
id: cs-social-clip-alchemist-01
display_name: "Social Clip Alchemist"
role_archetype: Growth Catalyst
org_layer: domain-specialist
personality:
traits: [energetic, pattern_spotter, platform_native]
leadership_style: collaborator
capabilities:
- shortform_clip_ideation
- platform_adaptation
identity:
ps_sha_infinity: "pssha∞:br:cs-social-clip-alchemist-01:<HASH>"
version: 1
policy_profile:
risk_band: medium
allowed_domains: [marketing, creator]
prohibited_actions:
- generate medical/financial claims without disclaimer
suitability_profile:
ideal_context:
- "content_repurposing"
- "launch_moments"
anti_patterns:
- "over-promising outcomes"
- "ignoring attribution"
owner: "@Alexa"
home_repo: blackroad-os-pack-creator-studio
lifecycle: active

View File

@@ -0,0 +1,29 @@
from typing import Dict, List
def extract_clip_candidates(transcript: Dict, max_clips: int = 10) -> List[Dict]:
sentences = transcript.get("sentences", [])
clips: List[Dict] = []
for index, sentence in enumerate(sentences[:max_clips]):
clip = {
"id": f"clip-{index+1}",
"start": sentence.get("start", index * 10),
"end": sentence.get("end", (index + 1) * 10),
"summary": sentence.get("text", "").strip()[:120],
}
clips.append(clip)
return clips
def generate_social_captions(clip: Dict, brand_voice: Dict) -> Dict:
adjectives = brand_voice.get("adjectives", [])
avoid = brand_voice.get("avoid", [])
base_tone = ", ".join(adjectives) if adjectives else "steady"
caution = f" Avoid: {', '.join(avoid)}" if avoid else ""
headline = clip.get("summary", "").strip() or "Fresh drop"
return {
"twitter": f"{headline}{base_tone} take.{caution}",
"tiktok": f"Hook: {headline}. Keep it punchy.{caution}",
"linkedin": f"{headline} | Practical takeaway | {base_tone} voice.{caution}",
}

View File

@@ -0,0 +1,23 @@
# Content Pipeline Map
```
Strategy (Content Strategist)
-> Longform Drafts (Scriptwriter)
-> Clip Ideation (Social Clip Alchemist)
-> Distribution (Channel adapters)
-> Analytics + Feedback (future adapters)
-> Strategy refresh (Content Strategist)
```
## How Agents Plug In
- **Content Strategist:** designs pillars, picks channels, and assembles calendars using performance signals.
- **Scriptwriter // Longform Architect:** turns outlines into ready-to-record scripts and trims to runtime.
- **Social Clip Alchemist:** mines transcripts for moments and drafts platform-specific captions.
## Platform Touchpoints
- **blackroad-os-api:** workflows can call API endpoints for asset storage, scheduling hooks, or operator commands.
- **blackroad-os-prism-console:** future dashboard surface to visualize calendars, running campaigns, and performance metrics.
## Notes
- Keep assets traceable: every draft or clip should be associated with a source transcript and owner.
- Channel adapters are not implemented here—stubs are ready for integration without hitting real APIs.

View File

@@ -0,0 +1,30 @@
# Creator Studio Pack Playbook
The Creator Studio pack gives any BlackRoad OS deployment a starter "creator / studio OS" with connected agents, workflows, and presets. It is designed to help solo creators, boutique studios, and in-house brand teams plan campaigns, draft scripts, and repurpose content across channels.
## Core Capabilities
- **Content planning:** translate business and brand goals into structured content pillars and calendars.
- **Longform scripting:** transform outlines into ready-to-record scripts and adjust runtimes for each format.
- **Clip orchestration:** identify shareable shortform moments and propose channel-specific captions.
## Agents
- **Content Strategist (cs-content-strategist-01):** maps goals to pillars, channels, and cadence.
- **Scriptwriter // Longform Architect (cs-scriptwriter-longform-01):** drafts and tightens scripts from outlines and pillars.
- **Social Clip Alchemist (cs-social-clip-alchemist-01):** extracts clip ideas from transcripts and drafts social-ready captions.
## Example: Campaign Idea to Live Posts
1. **Define goals and audience.** Feed goals and audience profile into the **Content Strategist** to generate pillars.
2. **Draft longform scripts.** Use pillars to create an outline and ask the **Scriptwriter** to produce a longform draft.
3. **Generate clips.** Send the draft transcript to the **Social Clip Alchemist** to surface shortform clip ideas with captions.
4. **Assemble the calendar.** Run **Content Strategist** again to assemble the weekly calendar with clip insertion.
5. **Launch.** Export calendar to your scheduling system or downstream channel adapters.
## Typical Use Cases
- Solo creators who need a reliable ideation-to-publishing loop.
- Small studios collaborating on weekly drops across YouTube, newsletters, and socials.
- In-house brand teams seeking repeatable campaign launches with clear accountability.
## Integration Notes
- All workflows are expressed in YAML and can be registered directly with the BlackRoad Operator API.
- Agents reference PS-SHA∞ identifiers and roles so they can be cataloged and governed centrally.
- Presets (pillars, voice) can be overridden per deployment without editing agent code.

View File

@@ -0,0 +1,7 @@
# Rights and Attribution Notes
- Track the source of every asset (transcripts, images, music) and keep links to licenses or contributor handles.
- Prefer public-domain or explicitly licensed resources; store the license reference next to the asset metadata.
- Attribute collaborators and editors when publishing clips or longform derivatives.
- Avoid making legal commitments in generated copy; route anything sensitive to a human reviewer.
- Include disclaimers when claims relate to regulated topics (health, finance) and avoid overpromising outcomes.

19
pack.yaml Normal file
View File

@@ -0,0 +1,19 @@
id: creator-studio
name: Creator Studio Pack
version: 0.1.0
description: >-
Content strategy, scriptwriting, and multi-platform publishing workflows
for creators, studios, and media teams.
required_roles:
- Growth Catalyst
- Brand Steward
- Producer
depends_on:
- blackroad-os-api >=0.1.0
- blackroad-os-core >=0.1.0
tags:
- creator
- media
- content
- campaigns
- social

1157
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

16
package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "blackroad-os-pack-creator-studio",
"version": "0.1.0",
"type": "commonjs",
"scripts": {
"test": "npm run test:ts",
"test:ts": "mocha -r ts-node/register tests/**/*.test.ts"
},
"devDependencies": {
"@types/mocha": "^10.0.6",
"@types/node": "^22.7.4",
"mocha": "^10.7.3",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
}
}

View File

@@ -0,0 +1,12 @@
profiles:
- id: blackroad-default
adjectives: ["calm", "clever", "precise"]
avoid: ["buzzword salad", "empty hype"]
emoji_usage: "light"
- id: lab-notes
adjectives: ["curious", "technical", "experimental"]
emoji_usage: "minimal"
- id: hype-crew
adjectives: ["energetic", "optimistic", "direct"]
avoid: ["over-promising", "unverified claims"]
emoji_usage: "medium"

View File

@@ -0,0 +1,13 @@
pillars:
- id: vision-roadmap
label: "Vision & Roadmap"
description: "Long-horizon thinking, where BlackRoad OS is going."
- id: behind-the-scenes
label: "Behind the Scenes"
description: "Build-in-public updates on infra, agents, and design."
- id: education-deep-dives
label: "Education / Deep Dives"
description: "Explainers on orchestration, PS-SHA∞, SIG, etc."
- id: community-spotlight
label: "Community Spotlight"
description: "Highlighting collaborators, partners, and superusers."

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
pytest
pyyaml

53
scripts/validate_yaml.py Normal file
View File

@@ -0,0 +1,53 @@
import sys
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parent.parent
def load_yaml(path: Path):
with path.open("r", encoding="utf-8") as handle:
return yaml.safe_load(handle)
def validate_pack_manifest() -> list[str]:
pack_path = ROOT / "pack.yaml"
data = load_yaml(pack_path)
required_fields = {"id", "name", "version", "description", "required_roles", "depends_on", "tags"}
missing = required_fields - set(data.keys())
errors: list[str] = []
if missing:
errors.append(f"pack.yaml missing fields: {', '.join(sorted(missing))}")
if not isinstance(data.get("depends_on", []), list):
errors.append("pack.yaml depends_on must be a list")
return errors
def validate_directory(glob_pattern: str) -> list[str]:
errors: list[str] = []
for path in ROOT.glob(glob_pattern):
try:
load_yaml(path)
except yaml.YAMLError as exc: # noqa: PERF203
errors.append(f"Failed to parse {path.relative_to(ROOT)}: {exc}")
return errors
def main() -> int:
issues = []
issues.extend(validate_pack_manifest())
issues.extend(validate_directory("workflows/*.workflow.yaml"))
issues.extend(validate_directory("presets/*.yaml"))
if issues:
for issue in issues:
print(issue)
return 1
print("YAML validation passed")
return 0
if __name__ == "__main__":
sys.exit(main())

6
tests/conftest.py Normal file
View File

@@ -0,0 +1,6 @@
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))

View File

@@ -0,0 +1,51 @@
import { strict as assert } from "assert";
import {
AudienceProfile,
BrandGoals,
CadenceConfig,
designContentPillars,
generateContentCalendar,
} from "../agents/content_strategist";
describe("content strategist", () => {
const goals: BrandGoals = {
primaryObjective: "Grow community and brand trust",
secondaryObjectives: ["newsletter signups"],
};
const audience: AudienceProfile = {
name: "Builder-operators",
motivations: ["stay ahead", "ship faster"],
painPoints: ["context switching", "content fatigue"],
preferredChannels: ["youtube", "twitter"],
};
const cadence: CadenceConfig = {
frequency: "weekly",
slotsPerWeek: 3,
preferredDays: ["Mon", "Wed", "Fri"],
};
it("designs content pillars from goals and audience", () => {
const pillars = designContentPillars(goals, audience);
assert.ok(pillars.length > 0, "at least one pillar is produced");
pillars.forEach((pillar) => {
assert.ok(pillar.id, "pillar has an id");
assert.ok(pillar.label, "pillar has a label");
assert.ok(pillar.description, "pillar has a description");
});
});
it("generates a calendar that references pillars", () => {
const pillars = designContentPillars(goals, audience);
const calendar = generateContentCalendar(pillars, cadence);
assert.equal(calendar.length, cadence.slotsPerWeek);
calendar.forEach((entry) => {
const pillarMatch = pillars.find((pillar) => pillar.id === entry.pillarId);
assert.ok(pillarMatch, "calendar entry references a valid pillar");
assert.ok(entry.title.length > 0, "entry has a title");
assert.ok(entry.channel.length > 0, "entry has a channel");
assert.ok(entry.scheduledFor.length > 0, "entry has a schedule slot");
});
});
});

View File

@@ -0,0 +1,26 @@
from agents.social_clip_alchemist import extract_clip_candidates, generate_social_captions
def test_extract_clip_candidates_limits_maximum():
transcript = {
"sentences": [
{"start": 0, "end": 5, "text": "Welcome to the lab."},
{"start": 5, "end": 12, "text": "Today we explore orchestration."},
{"start": 12, "end": 18, "text": "Agents collaborate on content."},
]
}
clips = extract_clip_candidates(transcript, max_clips=2)
assert len(clips) <= 2
for clip in clips:
assert "start" in clip and "end" in clip
assert "summary" in clip and len(clip["summary"]) > 0
def test_generate_social_captions_returns_variants():
clip = {"summary": "Agents orchestrate content drops"}
brand_voice = {"adjectives": ["calm", "clever"], "avoid": ["hype"]}
captions = generate_social_captions(clip, brand_voice)
assert set(captions.keys()) == {"twitter", "tiktok", "linkedin"}
assert "calm, clever" in captions["twitter"]
assert "Hook" in captions["tiktok"]
assert "Practical takeaway" in captions["linkedin"]

15
tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2019",
"module": "CommonJS",
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"outDir": "dist"
},
"include": ["agents/**/*.ts", "tests/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,19 @@
id: creator_campaign_launch
description: Launch a content campaign across channels.
steps:
- id: define_pillars
agent: cs-content-strategist-01
input: { goals: goals, audience: audience }
output: pillars
- id: draft_longform
agent: cs-scriptwriter-longform-01
input: { outline_from: pillars }
output: script_drafts
- id: clip_shortform
agent: cs-social-clip-alchemist-01
input: { script_drafts: script_drafts }
output: clip_ideas
- id: assemble_calendar
agent: cs-content-strategist-01
input: { pillars: pillars, clip_ideas: clip_ideas }
output: content_calendar

View File

@@ -0,0 +1,19 @@
id: weekly_content_calendar_refresh
description: Refresh the content calendar weekly with performance insights.
steps:
- id: collect_metrics
agent: cs-content-strategist-01
input: { performance_window: "last_7_days" }
output: performance_insights
- id: adjust_calendar
agent: cs-content-strategist-01
input: { current_calendar: calendar, insights: performance_insights }
output: updated_calendar
- id: propose_experiments
agent: cs-social-clip-alchemist-01
input: { updated_calendar: updated_calendar }
output: experiment_candidates
- id: finalize_week
agent: cs-scriptwriter-longform-01
input: { calendar: updated_calendar, experiments: experiment_candidates }
output: finalized_week_plan