Merge branch origin/copilot/sub-pr-2 into main
This commit is contained in:
37
.github/workflows/pack-creator-studio-ci.yml
vendored
Normal file
37
.github/workflows/pack-creator-studio-ci.yml
vendored
Normal 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
|
||||||
39
README.md
39
README.md
@@ -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.
|
||||||
|
|||||||
30
agents/content_strategist.agent.yaml
Normal file
30
agents/content_strategist.agent.yaml
Normal 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
|
||||||
101
agents/content_strategist.ts
Normal file
101
agents/content_strategist.ts
Normal 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;
|
||||||
|
}
|
||||||
30
agents/scriptwriter_longform.agent.yaml
Normal file
30
agents/scriptwriter_longform.agent.yaml
Normal 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
|
||||||
46
agents/scriptwriter_longform.ts
Normal file
46
agents/scriptwriter_longform.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
28
agents/social_clip_alchemist.agent.yaml
Normal file
28
agents/social_clip_alchemist.agent.yaml
Normal 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
|
||||||
29
agents/social_clip_alchemist.py
Normal file
29
agents/social_clip_alchemist.py
Normal 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}",
|
||||||
|
}
|
||||||
23
docs/CONTENT_PIPELINE_MAP.md
Normal file
23
docs/CONTENT_PIPELINE_MAP.md
Normal 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.
|
||||||
30
docs/CREATOR_STUDIO_PLAYBOOK.md
Normal file
30
docs/CREATOR_STUDIO_PLAYBOOK.md
Normal 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.
|
||||||
7
docs/RIGHTS_AND_ATTRIBUTION_NOTES.md
Normal file
7
docs/RIGHTS_AND_ATTRIBUTION_NOTES.md
Normal 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
19
pack.yaml
Normal 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
1157
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
package.json
Normal file
16
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
presets/brand_voice_profiles.yaml
Normal file
12
presets/brand_voice_profiles.yaml
Normal 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"
|
||||||
13
presets/content_pillars.yaml
Normal file
13
presets/content_pillars.yaml
Normal 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
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pytest
|
||||||
|
pyyaml
|
||||||
53
scripts/validate_yaml.py
Normal file
53
scripts/validate_yaml.py
Normal 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
6
tests/conftest.py
Normal 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))
|
||||||
51
tests/content_strategist.test.ts
Normal file
51
tests/content_strategist.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
26
tests/test_social_clip_alchemist.py
Normal file
26
tests/test_social_clip_alchemist.py
Normal 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
15
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
19
workflows/campaign_launch.workflow.yaml
Normal file
19
workflows/campaign_launch.workflow.yaml
Normal 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
|
||||||
19
workflows/weekly_content_calendar.workflow.yaml
Normal file
19
workflows/weekly_content_calendar.workflow.yaml
Normal 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
|
||||||
Reference in New Issue
Block a user