From 6d2ee432cd5fa1514d4c46d34d8403aec0a05209 Mon Sep 17 00:00:00 2001 From: Dean Zaka Hidayat Date: Mon, 13 Apr 2026 09:10:31 +0700 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20add=20scaffolding=20=E2=80=94=20REA?= =?UTF-8?q?DME,=20scripts,=20CI,=20docs,=20templates=20(#26176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename all artifact files from ULIDs to human-readable names (strategist.json, company-context.json, daily-standup.json, etc.). Add repo infrastructure: - README with fork-to-activate CTA and full layout - .gitignore, LICENSE (MIT), tsconfig.json - _template.json for agents/, projects/, automations/ - scripts/assemble.mjs (builds dist/workspace.json + cortex.tsk) - .github/workflows/publish-bundle.yml (attach .tsk to GH Release) - .github/ISSUE_TEMPLATE/fork-feedback.md - 6 docs: GENESIS-101, AGENT-GUIDE, PROJECT-GUIDE, AUTOMATION-GUIDE, APP-KIT-SPEC, FORK-AND-CUSTOMIZE - Update validate/summary to skip _template files Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/ISSUE_TEMPLATE/fork-feedback.md | 22 + .github/workflows/ci.yml | 28 + .github/workflows/publish-bundle.yml | 35 + .gitignore | 4 + LICENSE | 21 + README.md | 167 + agents/_template.json | 22 + ...WA8NVMYWZ85XHAVP0RG4.json => builder.json} | 0 ...2WA8N4VKKY6WB2XX4CB0A.json => critic.json} | 0 ...2WA8N1S8JDRDP55D87NX5.json => editor.json} | 0 ...NDTKXX5K4NFZE32YC.json => researcher.json} | 0 ...NPMP922SAHDKHQS8Q.json => strategist.json} | 0 apps/{default.json => cortex.json} | 0 automations/_template.json | 29 + ...MDTGDSPPQBS5XZ.json => daily-standup.json} | 0 ...CDQT43A2K1Q.json => decision-council.json} | 0 ...GFKFJKTR2CXMSJQ.json => inbox-triage.json} | 0 ...0GAAYZ8G9R.json => incident-response.json} | 0 ...8NFENB9AWMFR.json => monday-planning.json} | 0 ...V626F46CM2WP36.json => weekly-review.json} | 0 cortex.tsk | 7835 +++++++++++++++++ docs/AGENT-GUIDE.md | 84 + docs/APP-KIT-SPEC.md | 62 + docs/AUTOMATION-GUIDE.md | 72 + docs/FORK-AND-CUSTOMIZE.md | 96 + docs/GENESIS-101.md | 57 + docs/PROJECT-GUIDE.md | 61 + docs/contributing.md | 48 + docs/repository-structure.md | 53 + package.json | 21 + projects/_template.json | 24 + ...eH5BoVPriUiE.json => company-context.json} | 0 ...Ss5trhjYStYSydc.json => decision-log.json} | 0 ...bQz8RQWuJ.json => library-frameworks.json} | 0 ...Rn72cNtBr.json => library-references.json} | 0 ...bfwFLYwHY.json => playbook-fundraise.json} | 0 ...iT9XYgNKD1Dz.json => playbook-hiring.json} | 0 ...7iQavPKCJNJ6.json => playbook-launch.json} | 0 ...BknjP4GhuFG.json => playbook-pricing.json} | 0 ...qqNQFFP8SR1.json => playbook-support.json} | 0 .../{nNzQx9qpAitsKEMa.json => welcome.json} | 0 scripts/assemble.mjs | 75 + scripts/summary.mjs | 59 + scripts/validate.mjs | 163 + tsconfig.json | 15 + 45 files changed, 9053 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/fork-feedback.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish-bundle.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 agents/_template.json rename agents/{01KP22WA8NVMYWZ85XHAVP0RG4.json => builder.json} (100%) rename agents/{01KP22WA8N4VKKY6WB2XX4CB0A.json => critic.json} (100%) rename agents/{01KP22WA8N1S8JDRDP55D87NX5.json => editor.json} (100%) rename agents/{01KP22WA8NDTKXX5K4NFZE32YC.json => researcher.json} (100%) rename agents/{01KP22WA8NPMP922SAHDKHQS8Q.json => strategist.json} (100%) rename apps/{default.json => cortex.json} (100%) create mode 100644 automations/_template.json rename automations/{01KP22WA8NVFMDTGDSPPQBS5XZ.json => daily-standup.json} (100%) rename automations/{01KP22WA8NMWA2MCDQT43A2K1Q.json => decision-council.json} (100%) rename automations/{01KP22WA8N4GFKFJKTR2CXMSJQ.json => inbox-triage.json} (100%) rename automations/{01KP22WA8N2MNZ830GAAYZ8G9R.json => incident-response.json} (100%) rename automations/{01KP22WA8N1NF18NFENB9AWMFR.json => monday-planning.json} (100%) rename automations/{01KP22WA8NHPV626F46CM2WP36.json => weekly-review.json} (100%) create mode 100644 cortex.tsk create mode 100644 docs/AGENT-GUIDE.md create mode 100644 docs/APP-KIT-SPEC.md create mode 100644 docs/AUTOMATION-GUIDE.md create mode 100644 docs/FORK-AND-CUSTOMIZE.md create mode 100644 docs/GENESIS-101.md create mode 100644 docs/PROJECT-GUIDE.md create mode 100644 docs/contributing.md create mode 100644 docs/repository-structure.md create mode 100644 package.json create mode 100644 projects/_template.json rename projects/{zWQheH5BoVPriUiE.json => company-context.json} (100%) rename projects/{aSs5trhjYStYSydc.json => decision-log.json} (100%) rename projects/{7GBCCnibQz8RQWuJ.json => library-frameworks.json} (100%) rename projects/{BFsoG1yRn72cNtBr.json => library-references.json} (100%) rename projects/{P4vqDyabfwFLYwHY.json => playbook-fundraise.json} (100%) rename projects/{opHBiT9XYgNKD1Dz.json => playbook-hiring.json} (100%) rename projects/{YoDL7iQavPKCJNJ6.json => playbook-launch.json} (100%) rename projects/{Lz5wgBknjP4GhuFG.json => playbook-pricing.json} (100%) rename projects/{u5uSqqqNQFFP8SR1.json => playbook-support.json} (100%) rename projects/{nNzQx9qpAitsKEMa.json => welcome.json} (100%) create mode 100644 scripts/assemble.mjs create mode 100644 scripts/summary.mjs create mode 100644 scripts/validate.mjs create mode 100644 tsconfig.json diff --git a/.github/ISSUE_TEMPLATE/fork-feedback.md b/.github/ISSUE_TEMPLATE/fork-feedback.md new file mode 100644 index 0000000..b1923c1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/fork-feedback.md @@ -0,0 +1,22 @@ +--- +name: Fork Feedback +about: Tell us about your fork — what you changed, what worked, what didn't +title: "[Fork] " +labels: feedback +assignees: '' +--- + +**Your fork URL** (optional): + + +**What did you change?** + + + +**What worked well?** + + +**What was confusing or broken?** + + +**What would you add to the template?** diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6dd9f81 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: + +jobs: + validate-bundle: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: Run CI checks + run: npm run ci \ No newline at end of file diff --git a/.github/workflows/publish-bundle.yml b/.github/workflows/publish-bundle.yml new file mode 100644 index 0000000..d6e1217 --- /dev/null +++ b/.github/workflows/publish-bundle.yml @@ -0,0 +1,35 @@ +name: Publish Bundle + +on: + release: + types: [published] + +permissions: + contents: write + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install dependencies + run: npm install + + - name: Validate + run: npm run validate + + - name: Assemble bundle + run: npm run assemble + + - name: Upload cortex.tsk to release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload "${{ github.event.release.tag_name }}" cortex.tsk --clobber diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..815a0d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist/ +node_modules/ +.env +*.tgz diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f47d655 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Taskade Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6afa887 --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# Cortex + +**The default starter brain for Taskade Genesis.** + +Fork this repo. Import into Taskade. Get a fully wired AI workspace in 60 seconds — 5 agents, 10 projects, 6 automations, 1 app — all connected and running. + +> Cortex is not a sample app. It is a working brain. You don't learn it. You use it. Then you make it yours. + +--- + +## What's Inside + +### Intelligence — 5 Agents + +| Agent | Role | Emoji | +|-------|------|-------| +| **Strategist** | Think in tradeoffs, answer in options | ♟ | +| **Editor** | Cut 30%, keep the nerve | ✂ | +| **Researcher** | Find primary sources, cite everything | 🔍 | +| **Critic** | Argue against whatever you just said | ⚖ | +| **Builder** | Spec it, scope it, ship it | 🛠 | + +### Memory — 10 Projects + +- **Company Context** — who you are, what you sell, how you talk +- **Decision Log** — every major call, reasoning, outcome +- **5 Playbooks** — Hiring, Launch, Pricing, Fundraise, Support +- **2 Libraries** — Mental Models & Frameworks, References +- **Welcome** — onboarding guide to Cortex itself + +### Reflexes — 6 Automations + +- **Daily Standup** — morning cron that pulls tasks and summarizes blockers +- **Decision Council** — webhook triggers 3 agents to debate and synthesize +- **Weekly Review** — Friday evening scores the week and plans the next +- **Inbox Triage** — classifies incoming messages and routes to the right agent +- **Incident Response** — assembles a team and creates a war room +- **Monday Planning** — reviews goals and seeds the week's priorities + +### Interface — 1 Genesis App + +A multi-route React SPA with dashboard, council, journal, and library views — all wired to the agents and projects above. + +--- + +## Quick Start + +### Option A: Fork and Import (recommended) + +1. **Fork** this repo +2. Go to your Taskade workspace +3. **Import** → select `cortex.tsk` (or point at your fork's URL) +4. All 5 agents, 10 projects, 6 automations, and the app appear — wired and running + +### Option B: Clone and Customize + +```bash +git clone https://github.com/taskade/cortex.git +cd cortex +npm install +``` + +Edit any JSON file under `agents/`, `projects/`, `automations/`, or `apps/`. + +```bash +npm run validate # Check structural integrity +npm run summary # Print artifact counts and IDs +npm run assemble # Build cortex.tsk bundle +``` + +Then import the generated `cortex.tsk` into Taskade. + +--- + +## Repo Layout + +``` +cortex/ +├── manifest.json Bundle metadata +├── cortex.tsk One-click import bundle (generated) +│ +├── agents/ Intelligence layer +│ ├── _template.json Blank template — copy to create your own +│ ├── strategist.json +│ ├── editor.json +│ ├── researcher.json +│ ├── critic.json +│ └── builder.json +│ +├── projects/ Memory layer +│ ├── _template.json +│ ├── company-context.json +│ ├── decision-log.json +│ ├── playbook-*.json (5 playbooks) +│ ├── library-*.json (2 libraries) +│ └── welcome.json +│ +├── automations/ Reflexes layer +│ ├── _template.json +│ ├── daily-standup.json +│ ├── decision-council.json +│ ├── weekly-review.json +│ ├── inbox-triage.json +│ ├── incident-response.json +│ └── monday-planning.json +│ +├── apps/ +│ └── cortex.json Genesis app (React SPA) +│ +├── docs/ Guides +│ ├── GENESIS-101.md +│ ├── AGENT-GUIDE.md +│ ├── PROJECT-GUIDE.md +│ ├── AUTOMATION-GUIDE.md +│ ├── APP-KIT-SPEC.md +│ └── FORK-AND-CUSTOMIZE.md +│ +└── scripts/ Tooling + ├── validate.mjs + ├── summary.mjs + └── assemble.mjs +``` + +--- + +## Customization + +Every JSON file is a standalone artifact. Swap any of them: + +- **Replace an agent** — copy `agents/_template.json`, write your persona prompt, delete the old one +- **Add a project** — copy `projects/_template.json`, structure your content, save +- **Modify a flow** — edit trigger/action pairs in `automations/*.json` +- **Redesign the app** — edit the FileSystemTree in `apps/cortex.json` + +See [docs/FORK-AND-CUSTOMIZE.md](docs/FORK-AND-CUSTOMIZE.md) for a step-by-step guide. + +--- + +## Scripts + +| Command | What it does | +|---------|-------------| +| `npm run validate` | Structural integrity checks on all JSON artifacts | +| `npm run summary` | Print artifact counts and IDs | +| `npm run assemble` | Build `dist/workspace.json` + `cortex.tsk` | +| `npm run ci` | Run summary + validate (used in GitHub Actions) | + +--- + +## Documentation + +- [GENESIS-101](docs/GENESIS-101.md) — What is Genesis? The 4 DNA layers +- [AGENT-GUIDE](docs/AGENT-GUIDE.md) — How to write a good agent prompt +- [PROJECT-GUIDE](docs/PROJECT-GUIDE.md) — How to structure memory projects +- [AUTOMATION-GUIDE](docs/AUTOMATION-GUIDE.md) — Triggers, actions, piece library +- [APP-KIT-SPEC](docs/APP-KIT-SPEC.md) — FileSystemTree, Parade engine, SSE +- [FORK-AND-CUSTOMIZE](docs/FORK-AND-CUSTOMIZE.md) — Step-by-step customization + +--- + +## License + +[MIT](LICENSE) + +--- + +Built by [Taskade](https://taskade.com). Fork it. Make it yours. Ship it. diff --git a/agents/_template.json b/agents/_template.json new file mode 100644 index 0000000..0c7e18b --- /dev/null +++ b/agents/_template.json @@ -0,0 +1,22 @@ +{ + "version": "1", + "name": "My Agent", + "description": "Describe your agent's role, rules, format, and voice. Be specific — vague prompts produce vague agents.", + "persona": "my-persona", + "tone": "direct", + "language": "en-US", + "llm": { "type": "anthropic", "name": "anthropic/claude-sonnet-4.6" }, + "introduction": "One sentence the agent says when the conversation starts.", + "avatar": { "type": "emoji", "data": { "value": "🤖" } }, + "commands": [ + { + "id": "my_command", + "name": "My Command", + "prompt": "Describe what the agent should do when this command is invoked.", + "mode": "default" + } + ], + "conversationStarters": [ + { "id": "starter-1", "text": "Try me with this prompt." } + ] +} diff --git a/agents/01KP22WA8NVMYWZ85XHAVP0RG4.json b/agents/builder.json similarity index 100% rename from agents/01KP22WA8NVMYWZ85XHAVP0RG4.json rename to agents/builder.json diff --git a/agents/01KP22WA8N4VKKY6WB2XX4CB0A.json b/agents/critic.json similarity index 100% rename from agents/01KP22WA8N4VKKY6WB2XX4CB0A.json rename to agents/critic.json diff --git a/agents/01KP22WA8N1S8JDRDP55D87NX5.json b/agents/editor.json similarity index 100% rename from agents/01KP22WA8N1S8JDRDP55D87NX5.json rename to agents/editor.json diff --git a/agents/01KP22WA8NDTKXX5K4NFZE32YC.json b/agents/researcher.json similarity index 100% rename from agents/01KP22WA8NDTKXX5K4NFZE32YC.json rename to agents/researcher.json diff --git a/agents/01KP22WA8NPMP922SAHDKHQS8Q.json b/agents/strategist.json similarity index 100% rename from agents/01KP22WA8NPMP922SAHDKHQS8Q.json rename to agents/strategist.json diff --git a/apps/default.json b/apps/cortex.json similarity index 100% rename from apps/default.json rename to apps/cortex.json diff --git a/automations/_template.json b/automations/_template.json new file mode 100644 index 0000000..fdcf5f1 --- /dev/null +++ b/automations/_template.json @@ -0,0 +1,29 @@ +{ + "version": "2", + "flowTitle": "My Flow", + "trigger": { + "valid": true, + "type": "WEBHOOK", + "settings": { + "input": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Input for message" + } + }, + "additionalProperties": false + } + } + }, + "actions": [ + { + "valid": true, + "type": "taskade_ai_prompt", + "settings": { + "prompt": "Process the input: {{trigger.message}}" + } + } + ] +} diff --git a/automations/01KP22WA8NVFMDTGDSPPQBS5XZ.json b/automations/daily-standup.json similarity index 100% rename from automations/01KP22WA8NVFMDTGDSPPQBS5XZ.json rename to automations/daily-standup.json diff --git a/automations/01KP22WA8NMWA2MCDQT43A2K1Q.json b/automations/decision-council.json similarity index 100% rename from automations/01KP22WA8NMWA2MCDQT43A2K1Q.json rename to automations/decision-council.json diff --git a/automations/01KP22WA8N4GFKFJKTR2CXMSJQ.json b/automations/inbox-triage.json similarity index 100% rename from automations/01KP22WA8N4GFKFJKTR2CXMSJQ.json rename to automations/inbox-triage.json diff --git a/automations/01KP22WA8N2MNZ830GAAYZ8G9R.json b/automations/incident-response.json similarity index 100% rename from automations/01KP22WA8N2MNZ830GAAYZ8G9R.json rename to automations/incident-response.json diff --git a/automations/01KP22WA8N1NF18NFENB9AWMFR.json b/automations/monday-planning.json similarity index 100% rename from automations/01KP22WA8N1NF18NFENB9AWMFR.json rename to automations/monday-planning.json diff --git a/automations/01KP22WA8NHPV626F46CM2WP36.json b/automations/weekly-review.json similarity index 100% rename from automations/01KP22WA8NHPV626F46CM2WP36.json rename to automations/weekly-review.json diff --git a/cortex.tsk b/cortex.tsk new file mode 100644 index 0000000..55aa194 --- /dev/null +++ b/cortex.tsk @@ -0,0 +1,7835 @@ +{ + "version": "1.0", + "sourceEnvironment": "development", + "exportedAt": "2026-04-13T00:53:26.716Z", + "spaceId": "zn8t049t1cyr6yrh", + "spaceMetadata": { + "name": "Cortex", + "color": "#4b0082", + "emoji": "", + "visibility": "collaborator" + }, + "name": "Cortex", + "agents": [ + { + "id": "builder", + "data": { + "version": "1", + "name": "Builder", + "commands": [ + { + "id": "spec_it", + "mode": "default", + "name": "Spec It", + "prompt": "The user has an idea. Turn it into an executable spec: MVP definition, in/out scope table, sequenced steps with owners and dates, dependency graph, definition of done, the one killer risk.", + "isBackgroundJob": false + }, + { + "id": "scope_down", + "mode": "default", + "name": "Scope It Down", + "prompt": "The user has a plan that is too big. Cut features until the MVP is the smallest version that proves the point. Return what you cut, what you kept, what to ship in week 1 vs weeks 2-4.", + "isBackgroundJob": false + }, + { + "id": "sequence", + "mode": "default", + "name": "Sequence the Work", + "prompt": "The user has a list of things to build. Sequence them so the riskiest part is validated first, dependencies resolve correctly, and the work is shippable at each checkpoint.", + "isBackgroundJob": false + } + ], + "description": "You are a builder. The user has an idea, feature, or vague direction. Your job is to turn it into an executable spec: scoped, sequenced, and ready to ship. Convert abstract desires into concrete deliverables. Scope the MVP. Sequence so the risky part is done first. Name every dependency. Refuse to make scope bigger than needed. FORMAT: MVP definition (one sentence), scope (in/out table), sequence (numbered steps), dependencies, definition of done, risk. VOICE: practical, use verbs and dates.", + "avatar": { + "data": { + "value": "🛠" + }, + "type": "emoji" + }, + "variables": { + "zWQheH5BoVPriUiE": { + "id": "zWQheH5BoVPriUiE", + "rank": "V", + "type": "string" + } + }, + "addToKnowledgeProjectVariables": [ + { + "id": "zWQheH5BoVPriUiE", + "type": "agent-template-variable-input-type" + } + ] + } + }, + { + "id": "critic", + "data": { + "version": "1", + "name": "Critic", + "commands": [ + { + "id": "argue_against", + "mode": "default", + "name": "Argue Against This", + "prompt": "The user has presented an argument. Restate it, name the hidden assumption, present the strongest counter-case, identify the one move that would neutralize the strongest objection. Steelman the opposition.", + "isBackgroundJob": false + }, + { + "id": "red_team", + "mode": "default", + "name": "Red Team the Plan", + "prompt": "The user has a plan. Pretend you are a hostile competitor trying to destroy it. Identify 3 specific attacks, why they would work, and what to build now to prevent them.", + "isBackgroundJob": false + }, + { + "id": "pre_mortem", + "mode": "default", + "name": "Pre-mortem", + "prompt": "Assume it is six months from now and the decision failed catastrophically. Write the post-mortem: what happened, why, what the warning signs were, what they should have done instead.", + "isBackgroundJob": false + } + ], + "description": "You are the devil's advocate. The user will present an argument, plan, or belief. Your job is to construct the strongest possible case AGAINST it — not a strawman, but the argument a hostile expert would make. Steelman the opposition. Name the hidden assumption, then attack it. Use specific counterexamples. After the counter-case, note the ONE move that would address the strongest objection. VOICE: adversarial but intellectually honest.", + "avatar": { + "data": { + "value": "⚖" + }, + "type": "emoji" + }, + "variables": { + "zWQheH5BoVPriUiE": { + "id": "zWQheH5BoVPriUiE", + "rank": "V", + "type": "string" + } + }, + "addToKnowledgeProjectVariables": [ + { + "id": "zWQheH5BoVPriUiE", + "type": "agent-template-variable-input-type" + } + ] + } + }, + { + "id": "editor", + "data": { + "version": "1", + "name": "Editor", + "commands": [ + { + "id": "cut_30", + "mode": "default", + "name": "Cut 30%", + "prompt": "The user has pasted writing. Cut a minimum of 30% by word count. Return the edited version, then explain in 3 lines what you removed and why. Preserve every concrete noun, number, name, and date.", + "isBackgroundJob": false + }, + { + "id": "find_nerve", + "mode": "default", + "name": "Find the Nerve", + "prompt": "The user has pasted a draft that feels flat. Identify the ONE sentence that carries real stake. Then rewrite the draft so every other sentence serves that one.", + "isBackgroundJob": false + }, + { + "id": "headline", + "mode": "default", + "name": "Write the Headline", + "prompt": "The user has pasted a doc, post, or pitch. Write 5 headlines, each under 10 words, each making a different claim about what the piece argues.", + "isBackgroundJob": false + } + ], + "description": "You are a ruthless editor. Your job is to cut. The user will paste writing. You delete 30% of it minimum, and the result must be stronger. Strip hedges, throat-clearing, adverbs, redundant sentences. Preserve every concrete noun, number, name, date, and the one sentence that carries the emotional stake. Return the edited version first, then a 3-line explanation of what you cut and why. VOICE: direct, no apology.", + "avatar": { + "data": { + "value": "✂" + }, + "type": "emoji" + }, + "variables": { + "zWQheH5BoVPriUiE": { + "id": "zWQheH5BoVPriUiE", + "rank": "V", + "type": "string" + } + }, + "addToKnowledgeProjectVariables": [ + { + "id": "zWQheH5BoVPriUiE", + "type": "agent-template-variable-input-type" + } + ] + } + }, + { + "id": "researcher", + "data": { + "version": "1", + "name": "Researcher", + "commands": [ + { + "id": "investigate", + "mode": "default", + "name": "Investigate a Topic", + "prompt": "The user has given a topic. Research it. Return findings in bullet form with inline citations. Follow with a numbered source list and an uncertainties section.", + "isBackgroundJob": false + }, + { + "id": "verify_claim", + "mode": "default", + "name": "Verify a Claim", + "prompt": "The user has presented a specific claim. Find its primary source. If verified, cite it. If not, say so explicitly. If contradicted, present the contradiction.", + "isBackgroundJob": false + }, + { + "id": "deep_dive", + "mode": "default", + "name": "Deep Dive", + "prompt": "The user has given a topic requiring sustained investigation. Plan and execute a multi-step research pass. End with a \"what I still don't know\" section.", + "isBackgroundJob": false + } + ], + "description": "You are a research analyst. Your job is to find primary sources, distinguish them from secondary commentary, and present findings with citations. You refuse to assert facts without sources. When sources disagree, present both with the disagreement named. Refuse to fabricate. FORMAT: findings (bullets), sources (numbered with URLs), uncertainties (what you could not verify). VOICE: careful, precise, non-dramatic.", + "avatar": { + "data": { + "value": "🔍" + }, + "type": "emoji" + }, + "variables": { + "zWQheH5BoVPriUiE": { + "id": "zWQheH5BoVPriUiE", + "rank": "V", + "type": "string" + } + }, + "addToKnowledgeProjectVariables": [ + { + "id": "zWQheH5BoVPriUiE", + "type": "agent-template-variable-input-type" + } + ] + } + }, + { + "id": "strategist", + "data": { + "version": "1", + "name": "Strategist", + "commands": [ + { + "id": "counsel", + "mode": "default", + "name": "Counsel on a Decision", + "prompt": "The user has presented a decision. Restate the decision, name the implicit frame, present 3+ options with upside/cost/hidden assumption, name the tie-breaker information, and name the one move if forced to act today.", + "isBackgroundJob": false + }, + { + "id": "stress_test", + "mode": "default", + "name": "Stress Test the Plan", + "prompt": "The user has presented a plan. Identify the three most likely ways it fails. For each, name the early warning signal and the pivot.", + "isBackgroundJob": false + }, + { + "id": "tradeoff_map", + "mode": "default", + "name": "Map the Tradeoffs", + "prompt": "The user has options to compare. Build a comparison table across cost, speed, reversibility, optionality, team strain. End with the condition under which the loser would win.", + "isBackgroundJob": false + } + ], + "description": "You are a strategic advisor. Your job is to surface tradeoffs the user hasn't named. When a user asks \"should I do X?\", you do NOT answer yes or no. You identify at least three options, the explicit and hidden costs of each, and the conditions under which each wins. You refuse false binaries. You speak in options, not conclusions. TONE: direct, analytical, short sentences. Never start with \"Great question.\"", + "avatar": { + "data": { + "value": "♟" + }, + "type": "emoji" + }, + "variables": { + "zWQheH5BoVPriUiE": { + "id": "zWQheH5BoVPriUiE", + "rank": "V", + "type": "string" + } + }, + "addToKnowledgeProjectVariables": [ + { + "id": "zWQheH5BoVPriUiE", + "type": "agent-template-variable-input-type" + } + ] + } + } + ], + "projects": [ + { + "id": "company-context", + "data": { + "root": { + "type": "root", + "children": [ + { + "type": "text", + "id": "43eac57e-ef23-4833-9412-a3a7295f1b7e", + "text": { + "ops": [ + { + "insert": "Company Context" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h1", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "a3ba0811-9747-48c9-8b9c-83450ec800a3", + "text": { + "ops": [ + { + "insert": "Mission" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "15d90cb1-4f20-4412-b386-7fdc9052839f", + "text": { + "ops": [ + { + "insert": "Define your company's mission here. What problem do you solve? For whom?", + "attributes": { + "italic": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "ce99aa06-5cdf-4e78-be16-bb45a9fb8247", + "text": { + "ops": [ + { + "insert": "One-liner:", + "attributes": { + "bold": true + } + }, + { + "insert": " [Your company] helps [target audience] to [outcome] by [method]." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "7815d4bb-ac8b-4cdd-9572-e5a2ca701c86", + "text": { + "ops": [ + { + "insert": "North star metric:", + "attributes": { + "bold": true + } + }, + { + "insert": " [The single number that measures success]" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "4127ba58-53df-4dd0-920e-189a4df98bc9", + "text": { + "ops": [ + { + "insert": "Customers" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "03fa8805-20d5-4c52-abbb-a7901ce32c26", + "text": { + "ops": [ + { + "insert": "Primary persona:", + "attributes": { + "bold": true + } + }, + { + "insert": " [Who buys? Job title, company size, pain points]" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "e08109d3-984f-4f46-9c7e-ae251086296f", + "text": { + "ops": [ + { + "insert": "Secondary persona:", + "attributes": { + "bold": true + } + }, + { + "insert": " [Who else uses the product?]" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "5bb434d3-7029-4c52-b129-0f8ddf914b53", + "text": { + "ops": [ + { + "insert": "Anti-persona:", + "attributes": { + "bold": true + } + }, + { + "insert": " [Who is NOT your customer? Why?]" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "819b8422-a338-4a24-b2ac-b38f3add5142", + "text": { + "ops": [ + { + "insert": "Product" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "262f55f4-11fd-49c1-8aaf-4c2dc789b207", + "text": { + "ops": [ + { + "insert": "Core value prop:", + "attributes": { + "bold": true + } + }, + { + "insert": " [What do you do better than anyone else?]" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "1e40aeb3-39c8-48ce-8229-c613c190a07b", + "text": { + "ops": [ + { + "insert": "Key features:", + "attributes": { + "bold": true + } + }, + { + "insert": " [Top 3 features that matter]" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "bc9fcaca-ff43-4127-91d5-d1720eefbe74", + "text": { + "ops": [ + { + "insert": "Tech stack:", + "attributes": { + "bold": true + } + }, + { + "insert": " [What powers the product]" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "28313cf5-dd0b-4150-9c79-dd591672963a", + "text": { + "ops": [ + { + "insert": "Competitors" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "61a4c45e-d2ad-4f90-8523-9cbdeeb94525", + "text": { + "ops": [ + { + "insert": "Competitor Their Strength Our Advantage" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "c8a8dee8-2633-4b89-b55c-7e3b6ee46c0f", + "text": { + "ops": [ + { + "insert": "Competitor A", + "attributes": { + "italic": true + } + }, + { + "insert": " " + }, + { + "insert": "Their moat", + "attributes": { + "italic": true + } + }, + { + "insert": " " + }, + { + "insert": "Why we win", + "attributes": { + "italic": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "342c801e-8221-4abc-83a7-1efa2dc84799", + "text": { + "ops": [ + { + "insert": "Competitor B", + "attributes": { + "italic": true + } + }, + { + "insert": " " + }, + { + "insert": "Their moat", + "attributes": { + "italic": true + } + }, + { + "insert": " " + }, + { + "insert": "Why we win", + "attributes": { + "italic": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "1d3f5a9c-a343-40b5-b77e-9a01a8215e3a", + "text": { + "ops": [ + { + "insert": "Voice & Tone" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "4b491d3c-0d88-47d8-95ec-2d0a31d0bdd2", + "text": { + "ops": [ + { + "insert": "We sound like:", + "attributes": { + "bold": true + } + }, + { + "insert": " [Direct, warm, technical, casual?]" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "5f9194d4-d68f-46c8-9189-ff468411024a", + "text": { + "ops": [ + { + "insert": "We never sound like:", + "attributes": { + "bold": true + } + }, + { + "insert": " [Corporate jargon, overly casual?]" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "3123cf18-d03e-4102-b63f-7fcf5a5f161c", + "text": { + "ops": [ + { + "insert": "Writing rules:", + "attributes": { + "bold": true + } + }, + { + "insert": " [Max sentence length? Avoid passive voice?]" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "569b44dc-4298-4874-9be0-b212c9af0e72", + "text": { + "ops": [ + { + "insert": "Non-Goals" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "b4827331-4ec2-419b-bb5a-eecd1ed43ef5", + "text": { + "ops": [ + { + "insert": "Things we explicitly do NOT do" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "b9a22e2b-66b6-4852-9f9f-6f6af9e859ea", + "text": { + "ops": [ + { + "insert": "Markets we do NOT serve" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "5b054f02-1a1d-479d-85e5-1385885c5fa2", + "text": { + "ops": [ + { + "insert": "Features we will NOT build this year" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + } + ] + } + ] + }, + "preferences": { + "avatar": { + "type": "emoji", + "value": "📋" + } + } + } + }, + { + "id": "decision-log", + "data": { + "root": { + "type": "root", + "children": [ + { + "type": "text", + "id": "486c6a76-0ea5-4ade-9041-49025b41198a", + "text": { + "ops": [ + { + "insert": "Decision Log" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h1", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "083f4472-95f1-41d5-8a97-5dbf038e15b8", + "text": { + "ops": [ + { + "insert": "Track every major decision, its reasoning, and outcome." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "5b6fdb01-3937-40a4-acf3-6d3c7b70887a", + "text": { + "ops": [ + { + "insert": "Template" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "87c40d42-4f6b-472e-8c4f-55238b5345e9", + "text": { + "ops": [ + { + "insert": "[Date] — [Decision Title]", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "3863da13-d2ae-4a45-8fa1-f4691576c863", + "text": { + "ops": [ + { + "insert": "Context:", + "attributes": { + "bold": true + } + }, + { + "insert": " What prompted this decision?" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "5e5d12c7-27ef-4bbf-b6c3-e506e88d1267", + "text": { + "ops": [ + { + "insert": "Options considered:", + "attributes": { + "bold": true + } + }, + { + "insert": " What alternatives did we evaluate?" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "176a481f-1bc2-47b0-b493-891def9ba121", + "text": { + "ops": [ + { + "insert": "Decision:", + "attributes": { + "bold": true + } + }, + { + "insert": " What did we choose?" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "a924415d-04a3-4002-bf03-8d79f1596908", + "text": { + "ops": [ + { + "insert": "Reasoning:", + "attributes": { + "bold": true + } + }, + { + "insert": " Why this option over others?" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "c1272a5e-f7ea-4a4c-9dd0-316877ee4108", + "text": { + "ops": [ + { + "insert": "Owner:", + "attributes": { + "bold": true + } + }, + { + "insert": " Who is responsible for execution?" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "bc31621b-bb48-4c97-92e9-7c6bfccc7661", + "text": { + "ops": [ + { + "insert": "Review date:", + "attributes": { + "bold": true + } + }, + { + "insert": " When do we evaluate if this was right?" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "2f567cf1-5752-46c8-a4a6-bff77e9e1871", + "text": { + "ops": [ + { + "insert": "Outcome:", + "attributes": { + "bold": true + } + }, + { + "insert": " [Fill in after review date]" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "a650614d-f9c8-4741-9e4a-af31e360cf9c", + "text": { + "ops": [ + { + "insert": "Decisions" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "fce06a12-e481-4482-a5e8-bfcd9c154543", + "text": { + "ops": [ + { + "insert": "2026-01-15 — Pricing Model Change", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "5d2935d4-9073-4c13-bb3d-da387ba2932f", + "text": { + "ops": [ + { + "insert": "Context:", + "attributes": { + "bold": true + } + }, + { + "insert": " Free tier conversion was 2%, below target" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "e0fc995c-2d61-4ec4-8aee-82882b37123a", + "text": { + "ops": [ + { + "insert": "Options:", + "attributes": { + "bold": true + } + }, + { + "insert": " (1) Remove free tier (2) Add usage limits (3) Add feature gates" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "13595f2f-b845-41fc-9388-a23a22756de3", + "text": { + "ops": [ + { + "insert": "Decision:", + "attributes": { + "bold": true + } + }, + { + "insert": " Feature gates on AI agent count" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "4b57c9df-8e57-4c53-b437-228222dd37ec", + "text": { + "ops": [ + { + "insert": "Reasoning:", + "attributes": { + "bold": true + } + }, + { + "insert": " Preserves top-of-funnel while gating the sticky feature" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "40c81065-2349-4abc-9f4e-67f043eb24e3", + "text": { + "ops": [ + { + "insert": "Owner:", + "attributes": { + "bold": true + } + }, + { + "insert": " Product lead" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "3555159a-0532-4e04-b6b5-159827c44f36", + "text": { + "ops": [ + { + "insert": "Review date:", + "attributes": { + "bold": true + } + }, + { + "insert": " 2026-04-15" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "64a75ebe-b237-47b6-9e54-f2271937f6c0", + "text": { + "ops": [ + { + "insert": "Outcome:", + "attributes": { + "bold": true + } + }, + { + "insert": " " + }, + { + "insert": "Pending review", + "attributes": { + "italic": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + } + ] + } + ] + }, + "preferences": { + "avatar": { + "type": "emoji", + "value": "💭" + } + } + } + }, + { + "id": "library-frameworks", + "data": { + "root": { + "type": "root", + "children": [ + { + "type": "text", + "id": "0d74dede-b2d8-4a7b-a2b5-f34e0feb6fcd", + "text": { + "ops": [ + { + "insert": "Library — Mental Models & Frameworks" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h1", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "38c93c60-4bda-46fe-abd1-c0d4b0445457", + "text": { + "ops": [ + { + "insert": "Decision Making" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "a0e25cb4-b9e2-4629-a8ff-20d5677c6d51", + "text": { + "ops": [ + { + "insert": "Fear Filter", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "96032316-56f5-4d49-ab79-9c2bdbf9c3ff", + "text": { + "ops": [ + { + "insert": "Before any decision, ask:" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "3618432c-1e51-46d2-b607-2139958dd4dc", + "text": { + "ops": [ + { + "insert": "Am I avoiding this because it's wrong, or because it's scary?" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "be908ef2-7cd5-4b90-98aa-43dd141ef2b9", + "text": { + "ops": [ + { + "insert": "What's the worst realistic outcome?" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "ef286b7d-0d41-4e15-97e4-bc1da890dbea", + "text": { + "ops": [ + { + "insert": "Would I regret NOT doing this in 10 years?" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "20ff3b18-7645-4d45-9b12-ca3dc474fae6", + "text": { + "ops": [ + { + "insert": "Regret Minimization", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "9ad080ab-cbb4-4e8e-a4d5-c62b096a8eeb", + "text": { + "ops": [ + { + "insert": "\"When I'm 80, will I regret not trying this?\" — Bezos" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "c8a5d781-5640-4915-8c4e-c8ab11ed4e15", + "text": { + "ops": [ + { + "insert": "10x Test", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "343e3518-a18d-44e1-957a-e55be752f8d6", + "text": { + "ops": [ + { + "insert": "\"Is this 10x better than the alternative, or just 10% better?\"" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "38f6985f-fc14-4ea0-b38a-b9c2960b2828", + "text": { + "ops": [ + { + "insert": "10% better = incremental, competitors catch up" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "b9e18904-35d7-4c82-872c-7fd22a15e4c3", + "text": { + "ops": [ + { + "insert": "10x better = category shift, defensible" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "47618114-3a17-4452-b14f-e0caedeffd28", + "text": { + "ops": [ + { + "insert": "Strategy" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "e9cd58a2-ae52-4546-a241-9a731af8f8b9", + "text": { + "ops": [ + { + "insert": "Jobs To Be Done", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "345fae2c-e00d-443b-b24e-6ea6129ac681", + "text": { + "ops": [ + { + "insert": "People don't buy products. They hire them to do a job." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "266cc72a-0611-4a88-a41f-150e66634999", + "text": { + "ops": [ + { + "insert": "Functional job:", + "attributes": { + "bold": true + } + }, + { + "insert": " What task does it accomplish?" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "f188630b-d77c-4256-b14d-f4f84393167b", + "text": { + "ops": [ + { + "insert": "Emotional job:", + "attributes": { + "bold": true + } + }, + { + "insert": " How does it make them feel?" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "eccbe3f3-9d8a-49e0-bea8-ccb1eda9ede5", + "text": { + "ops": [ + { + "insert": "Social job:", + "attributes": { + "bold": true + } + }, + { + "insert": " How does it make them look to others?" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "347ae9f0-4317-42ff-9dd7-ae56664f8666", + "text": { + "ops": [ + { + "insert": "Eisenhower Matrix", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "1c87c2da-ade0-4af4-903b-f71445bdc479", + "text": { + "ops": [ + { + "insert": "Urgent Not Urgent" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "0634cf08-bf0e-4149-85d0-07bd130df95c", + "text": { + "ops": [ + { + "insert": "Important", + "attributes": { + "bold": true + } + }, + { + "insert": " DO first SCHEDULE" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "16e70b38-f2bf-47cb-9e86-396be5ba287f", + "text": { + "ops": [ + { + "insert": "Not Important", + "attributes": { + "bold": true + } + }, + { + "insert": " DELEGATE DELETE" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "ba24a439-7282-49b6-9e63-e18d3ea9885d", + "text": { + "ops": [ + { + "insert": "Communication" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "460f6570-7b92-4719-848e-e1ffd3cb4ee5", + "text": { + "ops": [ + { + "insert": "Minto Pyramid", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "d526bbf3-9dd4-4893-9f96-92a6c3baaf1c", + "text": { + "ops": [ + { + "insert": "Lead with the answer" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "0f0c9db6-e552-465a-891e-98ba57342f39", + "text": { + "ops": [ + { + "insert": "Group supporting arguments" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "3ac8bc3f-908d-4f20-8dc6-4a48f596d44f", + "text": { + "ops": [ + { + "insert": "Order logically within each group" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "8cfc5fae-6523-48fe-823e-9eb97278d4d2", + "text": { + "ops": [ + { + "insert": "Support with data at the bottom" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + } + ] + } + ] + }, + "preferences": { + "avatar": { + "type": "emoji", + "value": "✒️" + } + } + } + }, + { + "id": "library-references", + "data": { + "root": { + "type": "root", + "children": [ + { + "type": "text", + "id": "f1159fde-8dbf-43b1-ad49-43f8e1e159ac", + "text": { + "ops": [ + { + "insert": "Library — References" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h1", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "7b445805-f8d1-4efc-907d-e267e645407b", + "text": { + "ops": [ + { + "insert": "Essential Reading" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "2bdf7def-a070-43e2-95a4-0a76517447ab", + "text": { + "ops": [ + { + "insert": "\"The Mom Test\" — Rob Fitzpatrick (customer interviews)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "2c994a35-c3eb-4365-8bbd-f01a89a260dd", + "text": { + "ops": [ + { + "insert": "\"Obviously Awesome\" — April Dunford (positioning)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "36b0f2fe-470b-4e5e-ae48-d4ce786d59b6", + "text": { + "ops": [ + { + "insert": "\"Working Backwards\" — Colin Bryar (Amazon's process)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "82b4aa47-7fdf-4ab1-9eec-737c5f3693e0", + "text": { + "ops": [ + { + "insert": "\"High Output Management\" — Andy Grove (management)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "f6de14eb-3094-4896-938b-387a39e473d6", + "text": { + "ops": [ + { + "insert": "\"The Hard Thing About Hard Things\" — Ben Horowitz (startup reality)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "fc9653a3-b693-4d0b-93bd-22d1fb862df2", + "text": { + "ops": [ + { + "insert": "Canonical Blog Posts" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "9dfea986-bd37-4ec3-bc99-df0c0ab61a40", + "text": { + "ops": [ + { + "insert": "Paul Graham: \"Do Things That Don't Scale\" — " + }, + { + "insert": "http://paulgraham.com/ds.html", + "attributes": { + "link": "http://paulgraham.com/ds.html" + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "5b4b955b-8427-411e-8d28-973716486537", + "text": { + "ops": [ + { + "insert": "Marc Andreessen: \"Product-Market Fit\" — pmarchive.com" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "14bfdaaa-de68-47fc-a4cc-cf9b5ff1fea2", + "text": { + "ops": [ + { + "insert": "Brian Chesky: \"7-Star Experience\" — Masters of Scale" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "a572164a-6867-4f46-92a6-a58416b06f4d", + "text": { + "ops": [ + { + "insert": "Tools & Resources" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "2c94f614-8c94-4f41-b4ca-64c15104aeaf", + "text": { + "ops": [ + { + "insert": "Category Tool Use Case" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "e73e6f54-1215-448c-ab52-5090f058ee4e", + "text": { + "ops": [ + { + "insert": "Analytics Mixpanel Product analytics" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "9a414cb1-c511-4097-9229-5a2d157d0906", + "text": { + "ops": [ + { + "insert": "Support Zendesk Ticket management" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "f730022d-efe7-4aa2-a8a3-a52fb39a928f", + "text": { + "ops": [ + { + "insert": "Design Figma UI/UX design" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "e2294c09-3182-436c-bf21-0ae528f387ae", + "text": { + "ops": [ + { + "insert": "Docs Notion/Taskade Internal docs" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "40a6aa88-49d8-442c-a3e7-d350898c4bd0", + "text": { + "ops": [ + { + "insert": "CI/CD GitHub Actions Deployment" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "8b7aa663-65c8-41dd-b0b7-67dbe8977c60", + "text": { + "ops": [ + { + "insert": "Key Metrics Definitions" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "609244e4-6d49-4328-9422-a13812add216", + "text": { + "ops": [ + { + "insert": "MRR:", + "attributes": { + "bold": true + } + }, + { + "insert": " Monthly Recurring Revenue (excluding one-time charges)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "733fb126-ee6c-4757-9f0b-6639f9fc0a0f", + "text": { + "ops": [ + { + "insert": "Churn Rate:", + "attributes": { + "bold": true + } + }, + { + "insert": " % of customers lost per month" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "51be63c3-6ba8-4596-a3e3-29b2ab9eca42", + "text": { + "ops": [ + { + "insert": "CAC:", + "attributes": { + "bold": true + } + }, + { + "insert": " Cost to Acquire a Customer (total sales+marketing / new customers)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "5d51f717-bde7-4783-ab7c-c6384c2912f1", + "text": { + "ops": [ + { + "insert": "LTV:", + "attributes": { + "bold": true + } + }, + { + "insert": " Lifetime Value (average revenue per customer x average lifespan)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "048cfc26-e66f-4caf-b6b5-76a22de0077d", + "text": { + "ops": [ + { + "insert": "NPS:", + "attributes": { + "bold": true + } + }, + { + "insert": " Net Promoter Score (-100 to 100)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + } + ] + } + ] + }, + "preferences": { + "avatar": { + "type": "emoji", + "value": "📚" + } + } + } + }, + { + "id": "playbook-fundraise", + "data": { + "root": { + "type": "root", + "children": [ + { + "type": "text", + "id": "e8e75fcf-84bd-4024-aaa5-26e52c96c786", + "text": { + "ops": [ + { + "insert": "Playbook — Fundraise" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h1", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "6e963fce-8107-489b-8a93-68abf83f5cf6", + "text": { + "ops": [ + { + "insert": "Investor Pipeline" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "f23e5a60-801a-418a-addb-a4a335769762", + "text": { + "ops": [ + { + "insert": "Stage Count Next Step" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "6f4c2764-15ba-4d9f-8a9b-0bdb78da28ff", + "text": { + "ops": [ + { + "insert": "Target list 0 Research + warm intros" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "36ac7c5b-8d5a-448d-a0f4-2ffeda43aa44", + "text": { + "ops": [ + { + "insert": "Intro sent 0 Follow up in 3 days" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "b520db84-2c12-418b-9f61-97190f8690e9", + "text": { + "ops": [ + { + "insert": "First meeting 0 Send deck + data room" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "29ead92c-8dcb-46a9-9fd7-20352f3bb879", + "text": { + "ops": [ + { + "insert": "Partner meeting 0 Prep deep-dive answers" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "15513956-4617-4727-960c-ea847835332b", + "text": { + "ops": [ + { + "insert": "Term sheet 0 Negotiate + legal review" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "6b2118c1-4f9a-423e-ad6e-e126c6b531ba", + "text": { + "ops": [ + { + "insert": "Due Diligence Checklist" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "5ea68f75-75d9-4761-a61f-d28703adcd49", + "text": { + "ops": [ + { + "insert": "Financial model (3-year projections)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "071a4006-f875-49f5-82b0-e2e4f3879de0", + "text": { + "ops": [ + { + "insert": "Cap table (current + post-round)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "b4343441-4ebb-44fb-84c8-964094c6850c", + "text": { + "ops": [ + { + "insert": "Key metrics dashboard (MRR, churn, CAC, LTV)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "4aacf737-e7ca-422a-a2fd-26db6dfdbba9", + "text": { + "ops": [ + { + "insert": "Customer references (3-5 happy customers)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "a4b0d498-cd73-4d1a-90b9-bfc2dcb99592", + "text": { + "ops": [ + { + "insert": "Technical architecture overview" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "bee54e7f-33e1-42a2-8bf1-fbe6336cefb6", + "text": { + "ops": [ + { + "insert": "Team bios + org chart" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "29fa8125-3747-40cc-a085-013d1e23d2d3", + "text": { + "ops": [ + { + "insert": "Legal: incorporation docs, IP assignments, contracts" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "368281cd-c636-4f47-a6c6-afcbd1a83a5b", + "text": { + "ops": [ + { + "insert": "Pitch Deck Structure" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "446523fe-bb87-4c91-888d-acc310241a8c", + "text": { + "ops": [ + { + "insert": "Problem (1 slide)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "b62b85eb-8b63-4ed9-b0ed-cd4fdd29b5a7", + "text": { + "ops": [ + { + "insert": "Solution (1 slide)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "ef9accee-0a61-4dc3-b2de-2da03e55a295", + "text": { + "ops": [ + { + "insert": "Demo / product (2 slides)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "cae9c6e6-cc55-49c4-bf09-d0cfb692a65a", + "text": { + "ops": [ + { + "insert": "Market size (1 slide)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "fcc9d080-1f8f-48e4-bbcc-5d7f76d213da", + "text": { + "ops": [ + { + "insert": "Business model (1 slide)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "2ec50faa-ce3d-4f50-92ac-b85192c45c71", + "text": { + "ops": [ + { + "insert": "Traction (1 slide)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "20a68a83-1130-48a5-82fa-c68cb0e5e0ab", + "text": { + "ops": [ + { + "insert": "Team (1 slide)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "801b0024-3690-44a4-8c89-f7e2ab3ece66", + "text": { + "ops": [ + { + "insert": "Ask (1 slide)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + } + ] + } + ] + }, + "preferences": { + "avatar": { + "type": "emoji", + "value": "💌" + } + } + } + }, + { + "id": "playbook-hiring", + "data": { + "root": { + "type": "root", + "children": [ + { + "type": "text", + "id": "3595fb78-6c5b-47ad-a8ab-87d99a01a739", + "text": { + "ops": [ + { + "insert": "Playbook — Hiring" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h1", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "4c5475a0-c268-4f31-a9f6-3efc23e1bc5e", + "text": { + "ops": [ + { + "insert": "Process Overview" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "57974de6-fd64-4234-bedf-46b0e6423a71", + "text": { + "ops": [ + { + "insert": "Define role → 2. Source candidates → 3. Screen → 4. Interview → 5. Decide → 6. Offer → 7. Onboard" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "fed1c71e-ca4a-4c66-aec8-b27d8da9e4e4", + "text": { + "ops": [ + { + "insert": "Role Definition Template" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "1d7dabcc-a84f-4498-81c3-f2e478a523d4", + "text": { + "ops": [ + { + "insert": "Title:", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "a9dac8f3-1230-4598-9046-c3bd3cc5b755", + "text": { + "ops": [ + { + "insert": "Reports to:", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "3079e010-acbc-47fa-875b-a3202d08538b", + "text": { + "ops": [ + { + "insert": "Why now:", + "attributes": { + "bold": true + } + }, + { + "insert": " What changed that makes this role necessary?" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "2cccab49-a6f7-472c-86e6-39455c045fc6", + "text": { + "ops": [ + { + "insert": "Success in 90 days:", + "attributes": { + "bold": true + } + }, + { + "insert": " What will this person have shipped?" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "12b0b161-0441-4b53-a2b6-6a0cd27b172c", + "text": { + "ops": [ + { + "insert": "Must-haves:", + "attributes": { + "bold": true + } + }, + { + "insert": " [3 non-negotiable skills]" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "d0b789be-4bfd-467d-b48f-cbc0a2882d3c", + "text": { + "ops": [ + { + "insert": "Nice-to-haves:", + "attributes": { + "bold": true + } + }, + { + "insert": " [3 bonus skills]" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "9ca4094f-0f81-4040-84fc-5e8556054fa8", + "text": { + "ops": [ + { + "insert": "Anti-patterns:", + "attributes": { + "bold": true + } + }, + { + "insert": " [3 red flags in candidates]" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "8d0c4595-d5d2-45cd-9d95-bd8e30037df8", + "text": { + "ops": [ + { + "insert": "Interview Structure" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "23cf944f-c862-4831-84ba-ba2f3d3503ef", + "text": { + "ops": [ + { + "insert": "Round Duration Focus Interviewer" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "f5b463f2-7970-4d02-b013-167309e37f9f", + "text": { + "ops": [ + { + "insert": "Screen 30 min Culture + motivation Hiring manager" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "2d7ffed1-6b41-45c4-a27d-e3dbd79ee84b", + "text": { + "ops": [ + { + "insert": "Technical 60 min Skills + problem solving Team lead" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "a9f483b9-80e2-48a5-8e76-e76b4097d7e9", + "text": { + "ops": [ + { + "insert": "System design 45 min Architecture thinking Senior engineer" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "8ffda003-4cd4-4924-a903-77598309c083", + "text": { + "ops": [ + { + "insert": "Final 30 min Values + questions Founder" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "0f25489a-3d6d-402b-9891-3fb7b879a37d", + "text": { + "ops": [ + { + "insert": "Evaluation Criteria" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "94c28278-3fb3-4fa7-86d6-2eb5bf5b689c", + "text": { + "ops": [ + { + "insert": "Can they do the job? (skills)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "a7e55fcb-1b60-4207-8578-766c5775eb67", + "text": { + "ops": [ + { + "insert": "Will they do the job? (motivation)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "7a84748b-48ea-4695-af1a-3a5b89b2e1f3", + "text": { + "ops": [ + { + "insert": "Can we work with them? (culture)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "85cdc1c3-2302-47fd-9f1a-bdb688650496", + "text": { + "ops": [ + { + "insert": "Will they raise the bar? (growth potential)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + } + ] + } + ] + }, + "preferences": { + "avatar": { + "type": "emoji", + "value": "📓" + } + } + } + }, + { + "id": "playbook-launch", + "data": { + "root": { + "type": "root", + "children": [ + { + "type": "text", + "id": "91c4061d-68ca-417b-8ce8-78edfe3bb2ed", + "text": { + "ops": [ + { + "insert": "Playbook — Launch" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h1", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "ddba096e-0de8-4513-b2b3-23046fc572e7", + "text": { + "ops": [ + { + "insert": "Pre-Launch Checklist (T-14 days)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "8b332eaf-d07c-4732-a857-24373e0f6a29", + "text": { + "ops": [ + { + "insert": "Feature complete and QA'd" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "4e9a50ed-89fa-4759-a7a4-622add2dccfe", + "text": { + "ops": [ + { + "insert": "Staging tested by 3+ team members" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "a4414c9f-0680-42a2-8695-aee90c350cd9", + "text": { + "ops": [ + { + "insert": "Docs/help articles updated" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "71247c3e-f64a-4d6a-93c0-deed4c864215", + "text": { + "ops": [ + { + "insert": "Changelog entry drafted" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "a266c927-15c1-4192-8ad1-9808ed86085c", + "text": { + "ops": [ + { + "insert": "Social media posts scheduled" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "af628126-efb0-42f5-ad73-7b945a7bf149", + "text": { + "ops": [ + { + "insert": "Email announcement drafted" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "bd93efde-ecc7-41c5-bf64-cf75f80b779a", + "text": { + "ops": [ + { + "insert": "Internal team briefed" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "e77dfafa-e829-4636-a1b0-8d4828c3c4b8", + "text": { + "ops": [ + { + "insert": "Launch Day Checklist (T-0)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "837fa6fe-280b-4555-8334-04065a626fc7", + "text": { + "ops": [ + { + "insert": "Deploy to production" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "9775484e-8922-477e-b415-c40b7db3833a", + "text": { + "ops": [ + { + "insert": "Smoke test core flows" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "7717b4c2-7182-4928-9f9f-ecc39ab084f5", + "text": { + "ops": [ + { + "insert": "Publish changelog" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "7f2febff-078e-4bfe-88e6-ac5c665b5996", + "text": { + "ops": [ + { + "insert": "Send email announcement" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "66cdb344-33aa-4c1b-9ea8-19e5cc74703f", + "text": { + "ops": [ + { + "insert": "Post on social media" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "285ade4d-104e-431a-86d9-0205d38943b2", + "text": { + "ops": [ + { + "insert": "Monitor error rates (Sentry)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "8fb322eb-1edd-45ee-af0a-61e1255d69b1", + "text": { + "ops": [ + { + "insert": "Monitor support inbox" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "19b95767-a820-4855-ab39-79f2d935e7a7", + "text": { + "ops": [ + { + "insert": "Post-Launch (T+7)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "a0b69212-4659-43fd-a6a7-8db1e667141d", + "text": { + "ops": [ + { + "insert": "Review metrics vs. targets" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "e39c453c-2050-43d1-ad78-b1e491eab5e5", + "text": { + "ops": [ + { + "insert": "Collect user feedback" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "a7a98bb3-d83c-4016-84f2-f80ab97cf038", + "text": { + "ops": [ + { + "insert": "Triage bugs filed" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "e1adfaf4-3982-46e5-9358-26682b031bf8", + "text": { + "ops": [ + { + "insert": "Write retrospective" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "c996678c-88f4-4d4b-9bf5-552fd697952d", + "text": { + "ops": [ + { + "insert": "Plan iteration based on feedback" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + } + ] + } + ] + }, + "preferences": { + "avatar": { + "type": "emoji", + "value": "💭" + } + } + } + }, + { + "id": "playbook-pricing", + "data": { + "root": { + "type": "root", + "children": [ + { + "type": "text", + "id": "2610cd92-ea78-4f62-91d8-df7dd49b2300", + "text": { + "ops": [ + { + "insert": "Playbook — Pricing" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h1", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "8477955f-18f5-4515-b855-7cde27cd02c1", + "text": { + "ops": [ + { + "insert": "Pricing Change Framework" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "cc494fab-4e99-4390-b42c-8abeb56047ae", + "text": { + "ops": [ + { + "insert": "Before Changing Price", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "7342f5be-7d5b-488c-bf3e-9a70f37ee657", + "text": { + "ops": [ + { + "insert": "What metric moved?", + "attributes": { + "bold": true + } + }, + { + "insert": " (conversion, revenue, churn)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "8d8cd6d2-0492-42be-99bd-e6384aa9ca5a", + "text": { + "ops": [ + { + "insert": "Is this a perception problem or a value problem?", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "383e4b9a-d8ef-4325-ba1c-43ce84c47c6f", + "text": { + "ops": [ + { + "insert": "What do churned users say?", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "ea89c8c0-71fa-425d-ada6-26db94fba3d1", + "text": { + "ops": [ + { + "insert": "What do power users say they'd pay more for?", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "d7814df8-1c13-4998-9847-37bf4ea49fa0", + "text": { + "ops": [ + { + "insert": "Price Change Decision Matrix", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "d5cfe0ca-3247-4fa6-a9a2-8186608d59ba", + "text": { + "ops": [ + { + "insert": "Signal Action Risk" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "a0c4e880-9678-4a4c-b5c4-ec56dbef7246", + "text": { + "ops": [ + { + "insert": "High conversion, low revenue Raise price Churn spike" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "521603be-2aa5-4191-adec-0aacc736b9da", + "text": { + "ops": [ + { + "insert": "Low conversion, high willingness Simplify tiers Complexity" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "265c3995-b044-4398-b9c7-10d55f63f0e3", + "text": { + "ops": [ + { + "insert": "Feature requests cluster Add tier Over-segmentation" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "69e13549-9139-481a-b091-7d6e8209702f", + "text": { + "ops": [ + { + "insert": "Competitor undercuts Add value, don't cut price Feature bloat" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "bab6ad9e-d1e0-4fc9-821a-514f7b15cd5d", + "text": { + "ops": [ + { + "insert": "Implementation Checklist", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "39d1ab22-2b9e-4f9d-b229-cd7a785f507a", + "text": { + "ops": [ + { + "insert": "Grandfather existing users? (Y/N, for how long?)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "c6325b65-7bca-4ed8-880f-ff7578dd8a7f", + "text": { + "ops": [ + { + "insert": "A/B test or hard switch?" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "43af96f6-f8c8-446d-a9fc-387987ad839b", + "text": { + "ops": [ + { + "insert": "Billing system changes needed?" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "632d22ac-a924-4814-a40b-3d592dbc6ca7", + "text": { + "ops": [ + { + "insert": "Comms plan for existing users" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "bf1f25d6-610b-4c4d-a85b-05eccef3babf", + "text": { + "ops": [ + { + "insert": "FAQ for support team" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "1ccc426c-ccec-4d08-880c-2a528063c4ad", + "text": { + "ops": [ + { + "insert": "Monitor churn for 30 days post-change" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "checkbox", + "children": "checkbox" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + } + ] + } + ] + }, + "preferences": { + "avatar": { + "type": "emoji", + "value": "📕" + } + } + } + }, + { + "id": "playbook-support", + "data": { + "root": { + "type": "root", + "children": [ + { + "type": "text", + "id": "1b03e65e-3422-4aae-9794-2435ac3a6ac1", + "text": { + "ops": [ + { + "insert": "Playbook — Support" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h1", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "52c68b7a-b501-443e-9c2b-6e5eb1a7245b", + "text": { + "ops": [ + { + "insert": "Escalation Tree" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "78d39141-400a-4921-9e8d-386caf37e1d9", + "text": { + "ops": [ + { + "insert": "Severity Response Time Owner Channel" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "61424ebf-1a4e-496c-8ed5-ba0ad442dec9", + "text": { + "ops": [ + { + "insert": "P0 — Service down 15 min On-call engineer Slack #incidents" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "d4a232df-eaa9-4941-9782-bdeca2082a6b", + "text": { + "ops": [ + { + "insert": "P1 — Major feature broken 1 hour Support lead Zendesk" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "dbdaeb2d-fb20-4ae5-8bc0-67180688a5a7", + "text": { + "ops": [ + { + "insert": "P2 — Minor bug 24 hours Support team Zendesk" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "7f57ee46-2655-4848-bdc8-8c86456db90c", + "text": { + "ops": [ + { + "insert": "P3 — Feature request 72 hours Product Feedback board" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "fada303a-6b6d-4a66-b74f-6064b7d57ddc", + "text": { + "ops": [ + { + "insert": "Response Library" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "d2ed7df8-764b-4331-81a4-2f60a0072a69", + "text": { + "ops": [ + { + "insert": "Account Issues", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "c585d8b8-0b26-458a-b25d-22337610ab94", + "text": { + "ops": [ + { + "insert": "Password reset:", + "attributes": { + "bold": true + } + }, + { + "insert": " \"You can reset your password at [link]. If you're still locked out, reply here and I'll help.\"" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "d770f20a-8f6c-4c0f-9b60-a951579a5fa2", + "text": { + "ops": [ + { + "insert": "Billing question:", + "attributes": { + "bold": true + } + }, + { + "insert": " \"I've pulled up your account. [Describe what you see]. Would you like me to [specific action]?\"" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "3c7f5b64-b7e0-4468-a433-baba625d073a", + "text": { + "ops": [ + { + "insert": "Technical Issues", + "attributes": { + "bold": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "f458fa5a-819f-4876-b606-7c89e7c23d24", + "text": { + "ops": [ + { + "insert": "Bug report:", + "attributes": { + "bold": true + } + }, + { + "insert": " \"Thanks for reporting this. I've reproduced it and filed [ticket #]. The team will have a fix in [timeline].\"" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "5b76c504-26e8-48c4-bb6d-c1aa07ebe368", + "text": { + "ops": [ + { + "insert": "Feature request:", + "attributes": { + "bold": true + } + }, + { + "insert": " \"That's a great idea. I've added it to our feedback board at [link]. Other users have asked for this too.\"" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "c40bfd75-0234-4b16-9155-386e693321fa", + "text": { + "ops": [ + { + "insert": "Support Metrics" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "d4060f38-910c-4fbb-8dcb-52732761c594", + "text": { + "ops": [ + { + "insert": "First response time: target < 2 hours" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "d3127294-7afa-4a45-9a5b-29e856654c87", + "text": { + "ops": [ + { + "insert": "Resolution time: target < 24 hours" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "2a9a650a-771e-416b-8c91-3471a8feb4ff", + "text": { + "ops": [ + { + "insert": "CSAT: target > 4.5/5" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "23ea3bcc-d46f-4262-bed7-56b4fd9c09f4", + "text": { + "ops": [ + { + "insert": "Ticket volume: track weekly trend" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + } + ] + } + ] + }, + "preferences": { + "avatar": { + "type": "emoji", + "value": "⚕️" + } + } + } + }, + { + "id": "welcome", + "data": { + "root": { + "type": "root", + "children": [ + { + "type": "text", + "id": "8b0e4e9d-7f07-4dba-aa93-5c65d6270051", + "text": { + "ops": [ + { + "insert": "Welcome to Cortex" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h1", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "ece8440d-4286-4528-bad2-35660159f409", + "text": { + "ops": [ + { + "insert": "Cortex is your AI-powered starter brain. It comes pre-loaded with everything you need to run a company, make decisions, and ship products — powered by 5 AI agents, 10 projects, 6 automations, and a dashboard app." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "17248ede-21c1-42d3-9578-641693b243e5", + "text": { + "ops": [ + { + "insert": "Your 5 Agents" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "abd892f3-cd9e-445d-946d-3217ab46bc33", + "text": { + "ops": [ + { + "insert": "♟ " + }, + { + "insert": "Strategist", + "attributes": { + "bold": true + } + }, + { + "insert": " — Think in tradeoffs, answer in options. Use for major decisions." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "189c8383-d8ad-4725-9750-bd27f134b518", + "text": { + "ops": [ + { + "insert": "✂ " + }, + { + "insert": "Editor", + "attributes": { + "bold": true + } + }, + { + "insert": " — Cut 30%, keep the nerve. Paste any writing and get it tightened." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "c2bd7a96-35e1-48b7-a257-33d77b0c06c7", + "text": { + "ops": [ + { + "insert": "🔍 " + }, + { + "insert": "Researcher", + "attributes": { + "bold": true + } + }, + { + "insert": " — Find primary sources, cite everything. Verify claims and investigate topics." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "5d31999a-9249-4f2c-b122-cc827fa3f8b7", + "text": { + "ops": [ + { + "insert": "⚖ " + }, + { + "insert": "Critic", + "attributes": { + "bold": true + } + }, + { + "insert": " — Argue against whatever you just said. Stress-test plans and assumptions." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "e41eb1f4-6130-4db3-be95-43f0241ec85b", + "text": { + "ops": [ + { + "insert": "🛠 " + }, + { + "insert": "Builder", + "attributes": { + "bold": true + } + }, + { + "insert": " — Spec it, scope it, ship it. Turn ideas into executable specs." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "e48460e5-dee5-48af-a1e9-14fb1835b605", + "text": { + "ops": [ + { + "insert": "Your 10 Projects (Memory)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "60e75d1d-eeb2-4e8b-b617-430ebb1e6423", + "text": { + "ops": [ + { + "insert": "Company Context", + "attributes": { + "bold": true + } + }, + { + "insert": " — Who you are, what you sell, how you talk. Every agent reads this." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "dc9b9c6a-121e-4478-b4c3-dfeb9d3c6e59", + "text": { + "ops": [ + { + "insert": "Decision Log", + "attributes": { + "bold": true + } + }, + { + "insert": " — Every major call, reasoning, and outcome. Your institutional memory." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "edc3275f-f9a1-47f1-9400-14cb17c5f5e6", + "text": { + "ops": [ + { + "insert": "5 Playbooks", + "attributes": { + "bold": true + } + }, + { + "insert": " — Hiring, Launch, Pricing, Fundraise, Support. Step-by-step processes." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "820f253d-5b41-4986-bd55-c2246adb30eb", + "text": { + "ops": [ + { + "insert": "2 Libraries", + "attributes": { + "bold": true + } + }, + { + "insert": " — Frameworks (mental models) and References (links, books, resources)." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "3446ce53-81d6-4a2e-ad49-a6605d86b31b", + "text": { + "ops": [ + { + "insert": "This Welcome Guide", + "attributes": { + "bold": true + } + }, + { + "insert": " — You're reading it now." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "f5e44e74-3c13-40da-ab76-2379886fec46", + "text": { + "ops": [ + { + "insert": "Your 6 Automations (Reflexes)" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "66c7ebc5-853a-4608-8354-fee4ad776aaf", + "text": { + "ops": [ + { + "insert": "Daily Standup", + "attributes": { + "bold": true + } + }, + { + "insert": " — Every weekday at 9am, Builder reviews tasks and suggests priorities." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "f6bc2b30-5921-4e0f-9228-0d024725bf1c", + "text": { + "ops": [ + { + "insert": "Decision Council", + "attributes": { + "bold": true + } + }, + { + "insert": " — Trigger it to have Strategist, Critic, and Researcher debate a decision." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "5dff91ec-c017-466b-a462-87d9d7f2cd8b", + "text": { + "ops": [ + { + "insert": "Weekly Review", + "attributes": { + "bold": true + } + }, + { + "insert": " — Every Friday, score the week and plan the next one." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "0ac7ba72-36b5-4664-b373-69bfb1bc9148", + "text": { + "ops": [ + { + "insert": "Inbox Triage", + "attributes": { + "bold": true + } + }, + { + "insert": " — Classify incoming items and route to the right agent." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "262d5f56-1616-495e-a1b2-26239625e394", + "text": { + "ops": [ + { + "insert": "Incident Response", + "attributes": { + "bold": true + } + }, + { + "insert": " — Assemble a response team and create a war room." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "7c775130-98d2-45dd-baf8-eb0ea1b2e6d8", + "text": { + "ops": [ + { + "insert": "Monday Planning", + "attributes": { + "bold": true + } + }, + { + "insert": " — Every Monday, review goals and seed the week." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "bullet", + "children": "bullet" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "f4a1545e-08a0-4f2e-a622-4dc0fc244037", + "text": { + "ops": [ + { + "insert": "Getting Started" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "57a03f38-87f0-4442-a5a1-138120f14190", + "text": { + "ops": [ + { + "insert": "Open " + }, + { + "insert": "Company Context", + "attributes": { + "bold": true + } + }, + { + "insert": " and fill in your company's details" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "3137a9e3-400d-4d15-96f4-04211a3e4205", + "text": { + "ops": [ + { + "insert": "Try the " + }, + { + "insert": "Strategist", + "attributes": { + "bold": true + } + }, + { + "insert": " agent — ask it to counsel you on a real decision" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "65e74018-db77-4b6d-99e2-a5d18bcc0726", + "text": { + "ops": [ + { + "insert": "Trigger the " + }, + { + "insert": "Decision Council", + "attributes": { + "bold": true + } + }, + { + "insert": " automation with an example decision" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "f2c9e52d-7de3-4f6d-b266-91da7163b60c", + "text": { + "ops": [ + { + "insert": "Open the " + }, + { + "insert": "Dashboard", + "attributes": { + "bold": true + } + }, + { + "insert": " app to see everything connected" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "number", + "children": "number" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + }, + { + "type": "text", + "id": "ea56a8d9-d5c2-4564-8f68-d0d122f3a073", + "text": { + "ops": [ + { + "insert": "Customize It" + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "h2", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [ + { + "type": "text", + "id": "1980dc45-cb72-492a-855b-a771ee352a3f", + "text": { + "ops": [ + { + "insert": "Cortex is a starting point, not a final product. Fork it, rename agents, rewrite playbooks, add your own automations. The structure stays the same — Memory + Intelligence + Reflexes + Interface — but the content becomes yours." + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + }, + { + "type": "text", + "id": "e5b20b93-5d00-49ea-b0fd-9ae5321312da", + "text": { + "ops": [ + { + "insert": "Built with Taskade Genesis. Fork this at github.com/taskade/cortex.", + "attributes": { + "italic": true + } + }, + { + "insert": "\n", + "attributes": { + "paragraph": true + } + } + ] + }, + "format": { + "node": "text", + "children": "text" + }, + "collapsed": false, + "completed": false, + "children": [] + } + ] + } + ] + } + ] + }, + "preferences": { + "avatar": { + "type": "emoji", + "value": "🧩" + } + } + } + } + ], + "automations": [ + { + "id": "daily-standup", + "data": { + "version": "2", + "flowTitle": "Daily Standup", + "trigger": { + "valid": false, + "displayName": "Every Weekday at 9AM", + "type": "PIECE_TRIGGER", + "settings": { + "pieceName": "@taskade/automade-internalpiece-schedule", + "pieceVersion": "BLANK", + "triggerName": "schedule.daily", + "input": {} + } + }, + "actions": [ + { + "name": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "valid": true, + "displayName": "Weekday Filter (Mon–Fri)", + "type": "BRANCH", + "settings": {}, + "paths": [ + { + "name": "b2c3d4e5-f678-90ab-cdef-123456789012", + "valid": true, + "displayName": "Is Weekday", + "type": "BRANCH_PATH", + "settings": { + "ruleset": { + "type": "CUSTOM", + "input": { + "type": "or", + "value": [ + { + "type": "and", + "value": [ + { + "condition": "text_is_not_empty", + "id": "7fb4f314-774d-4a21-a95d-bc276be645be", + "data": "{{ trigger[\"triggerTime\"] }}", + "comparator": "" + } + ] + } + ] + } + } + }, + "actions": [] + } + ] + }, + { + "name": "c3d4e5f6-7890-abcd-ef12-345678901234", + "valid": false, + "displayName": "Builder — Sequence the Work", + "type": "PIECE", + "settings": { + "pieceName": "@taskade/automade-internalpiece-openai", + "pieceVersion": "BLANK", + "actionName": "agent.run_command", + "input": { + "input": "Review open tasks across the workspace and suggest what should ship today. Prioritize by urgency, dependencies, and impact. Format as a clear, actionable standup list.", + "agentId": { + "type": "flow-template-variable-input-type", + "id": "01KP22WA8NVMYWZ85XHAVP0RG4" + } + } + } + }, + { + "name": "d4e5f678-90ab-cdef-1234-567890abcdef", + "valid": false, + "displayName": "Append Standup to Decision Log", + "type": "PIECE", + "settings": { + "pieceName": "@taskade/automade-internalpiece-taskade", + "pieceVersion": "BLANK", + "actionName": "task.create", + "input": { + "content": "Daily Standup — {{ trigger[\"triggerTime\"] }}\n\n{{ c3d4e5f6-7890-abcd-ef12-345678901234[\"result\"] }}", + "position": "beforeend", + "projectId": { + "type": "flow-template-variable-input-type", + "id": "aSs5trhjYStYSydc" + } + } + } + } + ], + "variables": { + "01KP22WA8NVMYWZ85XHAVP0RG4": { + "id": "01KP22WA8NVMYWZ85XHAVP0RG4", + "rank": "V", + "type": "string", + "format": "taskade-space-agent-id" + }, + "aSs5trhjYStYSydc": { + "id": "aSs5trhjYStYSydc", + "rank": "k", + "type": "string", + "format": "taskade-project-id" + } + } + } + }, + { + "id": "decision-council", + "data": { + "version": "2", + "flowTitle": "Decision Council", + "trigger": { + "valid": true, + "type": "WEBHOOK", + "settings": { + "input": { + "type": "object", + "properties": { + "context": { + "type": "string", + "description": "Input for context" + }, + "decision": { + "type": "string", + "description": "Input for decision" + } + }, + "additionalProperties": false + } + } + }, + "actions": [ + { + "name": "a1a1a1a1-0001-0001-0001-000000000001", + "valid": false, + "displayName": "Strategist — Counsel on a Decision", + "type": "PIECE", + "settings": { + "pieceName": "@taskade/automade-internalpiece-openai", + "pieceVersion": "BLANK", + "actionName": "agent.run_command", + "input": { + "input": "Decision: {{ trigger[\"body\"][\"decision\"] }}\n\nContext: {{ trigger[\"body\"][\"context\"] }}", + "agentId": { + "type": "flow-template-variable-input-type", + "id": "01KP22WA8NPMP922SAHDKHQS8Q" + } + } + } + }, + { + "name": "a2a2a2a2-0002-0002-0002-000000000002", + "valid": false, + "displayName": "Critic — Argue Against This", + "type": "PIECE", + "settings": { + "pieceName": "@taskade/automade-internalpiece-openai", + "pieceVersion": "BLANK", + "actionName": "agent.run_command", + "input": { + "input": "Decision: {{ trigger[\"body\"][\"decision\"] }}\n\nContext: {{ trigger[\"body\"][\"context\"] }}", + "agentId": { + "type": "flow-template-variable-input-type", + "id": "01KP22WA8N4VKKY6WB2XX4CB0A" + } + } + } + }, + { + "name": "a3a3a3a3-0003-0003-0003-000000000003", + "valid": false, + "displayName": "Researcher — Verify a Claim", + "type": "PIECE", + "settings": { + "pieceName": "@taskade/automade-internalpiece-openai", + "pieceVersion": "BLANK", + "actionName": "agent.run_command", + "input": { + "input": "Decision: {{ trigger[\"body\"][\"decision\"] }}\n\nContext: {{ trigger[\"body\"][\"context\"] }}", + "agentId": { + "type": "flow-template-variable-input-type", + "id": "01KP22WA8NDTKXX5K4NFZE32YC" + } + } + } + }, + { + "name": "a4a4a4a4-0004-0004-0004-000000000004", + "valid": false, + "displayName": "Builder — Spec It", + "type": "PIECE", + "settings": { + "pieceName": "@taskade/automade-internalpiece-openai", + "pieceVersion": "BLANK", + "actionName": "agent.run_command", + "input": { + "input": "Decision: {{ trigger[\"body\"][\"decision\"] }}\n\nContext: {{ trigger[\"body\"][\"context\"] }}", + "agentId": { + "type": "flow-template-variable-input-type", + "id": "01KP22WA8NVMYWZ85XHAVP0RG4" + } + } + } + }, + { + "name": "a5a5a5a5-0005-0005-0005-000000000005", + "valid": false, + "displayName": "Append Council Verdict to Decision Log", + "type": "PIECE", + "settings": { + "pieceName": "@taskade/automade-internalpiece-taskade", + "pieceVersion": "BLANK", + "actionName": "task.create", + "input": { + "content": "Decision Council — {{ trigger[\"body\"][\"decision\"] }}\n\n---\n\n## 🧭 Strategist: Counsel on a Decision\n{{ a1a1a1a1-0001-0001-0001-000000000001[\"result\"] }}\n\n---\n\n## ⚔️ Critic: Argue Against This\n{{ a2a2a2a2-0002-0002-0002-000000000002[\"result\"] }}\n\n---\n\n## 🔬 Researcher: Verify a Claim\n{{ a3a3a3a3-0003-0003-0003-000000000003[\"result\"] }}\n\n---\n\n## 🔧 Builder: Spec It\n{{ a4a4a4a4-0004-0004-0004-000000000004[\"result\"] }}", + "position": "beforeend", + "projectId": { + "type": "flow-template-variable-input-type", + "id": "aSs5trhjYStYSydc" + } + } + } + } + ], + "variables": { + "01KP22WA8NPMP922SAHDKHQS8Q": { + "id": "01KP22WA8NPMP922SAHDKHQS8Q", + "rank": "V", + "type": "string", + "format": "taskade-space-agent-id" + }, + "01KP22WA8N4VKKY6WB2XX4CB0A": { + "id": "01KP22WA8N4VKKY6WB2XX4CB0A", + "rank": "k", + "type": "string", + "format": "taskade-space-agent-id" + }, + "01KP22WA8NDTKXX5K4NFZE32YC": { + "id": "01KP22WA8NDTKXX5K4NFZE32YC", + "rank": "s", + "type": "string", + "format": "taskade-space-agent-id" + }, + "01KP22WA8NVMYWZ85XHAVP0RG4": { + "id": "01KP22WA8NVMYWZ85XHAVP0RG4", + "rank": "w", + "type": "string", + "format": "taskade-space-agent-id" + }, + "aSs5trhjYStYSydc": { + "id": "aSs5trhjYStYSydc", + "rank": "y", + "type": "string", + "format": "taskade-project-id" + } + } + } + }, + { + "id": "inbox-triage", + "data": { + "version": "2", + "flowTitle": "Inbox Triage", + "trigger": { + "valid": true, + "type": "WEBHOOK", + "settings": { + "input": { + "type": "object", + "properties": { + "sender": { + "type": "string", + "description": "Input for sender" + }, + "source": { + "type": "string", + "description": "Input for source" + }, + "message": { + "type": "string", + "description": "Input for message" + } + }, + "additionalProperties": false + } + } + }, + "actions": [ + { + "name": "c1c1c1c1-0001-0001-0001-000000000001", + "valid": false, + "displayName": "Researcher — Classify Message", + "type": "PIECE", + "settings": { + "pieceName": "@taskade/automade-internalpiece-openai", + "pieceVersion": "BLANK", + "actionName": "agent.run_command", + "input": { + "input": "Classify the following incoming message into exactly ONE of these four categories:\n\n- SUPPORT TICKET — a user reporting a bug, issue, or needing help\n- SALES LEAD — a potential customer asking about pricing, features, or partnerships\n- INVESTOR INTRO — someone expressing interest in investing or requesting a meeting for funding\n- NOISE — spam, irrelevant, automated, or low-signal message\n\nRespond with:\nCATEGORY: [one of the four above]\nCONFIDENCE: [High / Medium / Low]\nREASON: [one sentence max]\n\nMessage:\n{{ trigger[\"body\"][\"message\"] }}\n\nSender: {{ trigger[\"body\"][\"sender\"] }}\nSource: {{ trigger[\"body\"][\"source\"] }}", + "agentId": { + "type": "flow-template-variable-input-type", + "id": "01KP22WA8NDTKXX5K4NFZE32YC" + } + } + } + }, + { + "name": "c2c2c2c2-0002-0002-0002-000000000002", + "valid": false, + "displayName": "Append to Company Context", + "type": "PIECE", + "settings": { + "pieceName": "@taskade/automade-internalpiece-taskade", + "pieceVersion": "BLANK", + "actionName": "task.create", + "input": { + "content": "Inbox Triage — {{ trigger[\"body\"][\"sender\"] }} via {{ trigger[\"body\"][\"source\"] }}\n\n{{ c1c1c1c1-0001-0001-0001-000000000001[\"result\"] }}\n\n---\n\nOriginal message:\n{{ trigger[\"body\"][\"message\"] }}", + "position": "beforeend", + "projectId": { + "type": "flow-template-variable-input-type", + "id": "zWQheH5BoVPriUiE" + } + } + } + } + ], + "variables": { + "01KP22WA8NDTKXX5K4NFZE32YC": { + "id": "01KP22WA8NDTKXX5K4NFZE32YC", + "rank": "V", + "type": "string", + "format": "taskade-space-agent-id" + }, + "zWQheH5BoVPriUiE": { + "id": "zWQheH5BoVPriUiE", + "rank": "k", + "type": "string", + "format": "taskade-project-id" + } + } + } + }, + { + "id": "incident-response", + "data": { + "version": "2", + "flowTitle": "Incident Response", + "trigger": { + "valid": true, + "type": "WEBHOOK", + "settings": { + "input": { + "type": "object", + "properties": { + "owner": { + "type": "string", + "description": "Input for owner" + }, + "summary": { + "type": "string", + "description": "Input for summary" + }, + "severity": { + "type": "string", + "anyOf": [ + { + "const": "P0", + "title": "P0 — Critical (system down)" + }, + { + "const": "P1", + "title": "P1 — High (major feature broken)" + }, + { + "const": "P2", + "title": "P2 — Medium (degraded performance)" + }, + { + "const": "P3", + "title": "P3 — Low (minor issue)" + } + ] + }, + "affected_systems": { + "type": "string", + "description": "Input for affected_systems" + } + }, + "additionalProperties": false + } + } + }, + "actions": [ + { + "name": "d1d1d1d1-0001-0001-0001-000000000001", + "valid": false, + "displayName": "Builder — Spec the Response Plan", + "type": "PIECE", + "settings": { + "pieceName": "@taskade/automade-internalpiece-openai", + "pieceVersion": "BLANK", + "actionName": "agent.run_command", + "input": { + "input": "We have an active incident. Spec the next 4 hours of response.\n\nSEVERITY: {{ trigger[\"body\"][\"severity\"] }}\nSUMMARY: {{ trigger[\"body\"][\"summary\"] }}\nAFFECTED SYSTEMS: {{ trigger[\"body\"][\"affected_systems\"] }}\nOWNER: {{ trigger[\"body\"][\"owner\"] }}\n\nDeliver:\n1. Immediate actions (first 15 min)\n2. Investigation sequence (next 45 min)\n3. Mitigation steps (next 60 min)\n4. Communication cadence (who, what, when)\n5. Definition of resolved\n6. Top 2 risks that could make this worse\n\nBe precise. Use verbs. No hedging.", + "agentId": { + "type": "flow-template-variable-input-type", + "id": "01KP22WA8NVMYWZ85XHAVP0RG4" + } + } + } + }, + { + "name": "d2d2d2d2-0002-0002-0002-000000000002", + "valid": false, + "displayName": "Append War Room to Decision Log", + "type": "PIECE", + "settings": { + "pieceName": "@taskade/automade-internalpiece-taskade", + "pieceVersion": "BLANK", + "actionName": "task.create", + "input": { + "content": "🚨 WAR ROOM — {{ trigger[\"body\"][\"severity\"] }}\n\nSummary: {{ trigger[\"body\"][\"summary\"] }}\nAffected: {{ trigger[\"body\"][\"affected_systems\"] }}\nOwner: {{ trigger[\"body\"][\"owner\"] }}\n\n---\n\n## Response Plan\n\n{{ d1d1d1d1-0001-0001-0001-000000000001[\"result\"] }}", + "position": "beforeend", + "projectId": { + "type": "flow-template-variable-input-type", + "id": "aSs5trhjYStYSydc" + } + } + } + } + ], + "variables": { + "01KP22WA8NVMYWZ85XHAVP0RG4": { + "id": "01KP22WA8NVMYWZ85XHAVP0RG4", + "rank": "V", + "type": "string", + "format": "taskade-space-agent-id" + }, + "aSs5trhjYStYSydc": { + "id": "aSs5trhjYStYSydc", + "rank": "k", + "type": "string", + "format": "taskade-project-id" + } + } + } + }, + { + "id": "monday-planning", + "data": { + "version": "2", + "flowTitle": "Monday Planning", + "trigger": { + "valid": false, + "displayName": "Every Monday at 8AM", + "type": "PIECE_TRIGGER", + "settings": { + "pieceName": "@taskade/automade-internalpiece-schedule", + "pieceVersion": "BLANK", + "triggerName": "schedule.weekly", + "input": {} + } + }, + "actions": [ + { + "name": "e1e1e1e1-0001-0001-0001-000000000001", + "valid": false, + "displayName": "Strategist — Map the Tradeoffs", + "type": "PIECE", + "settings": { + "pieceName": "@taskade/automade-internalpiece-openai", + "pieceVersion": "BLANK", + "actionName": "agent.run_command", + "input": { + "input": "It's Monday morning. Identify the ONE thing that matters most this week. Surface the tradeoffs in choosing it over everything else on the table. Structure your output:\n\n1. THE ONE THING — One sentence, no hedging\n2. WHY THIS WEEK — What makes right now the right moment\n3. WHAT WE'RE NOT DOING — The top 3 things we're explicitly deferring and why\n4. THE TRADEOFF — The real cost of this focus\n5. THE SIGNAL TO WATCH — One metric or signal that tells us if we're right\n6. RISK — What could invalidate this by Friday", + "agentId": { + "type": "flow-template-variable-input-type", + "id": "01KP22WA8NPMP922SAHDKHQS8Q" + } + } + } + }, + { + "name": "e2e2e2e2-0002-0002-0002-000000000002", + "valid": false, + "displayName": "Append Weekly Priorities to Company Context", + "type": "PIECE", + "settings": { + "pieceName": "@taskade/automade-internalpiece-taskade", + "pieceVersion": "BLANK", + "actionName": "task.create", + "input": { + "content": "📅 Monday Planning — {{ trigger[\"triggerTime\"] }}\n\n{{ e1e1e1e1-0001-0001-0001-000000000001[\"result\"] }}", + "position": "beforeend", + "projectId": { + "type": "flow-template-variable-input-type", + "id": "zWQheH5BoVPriUiE" + } + } + } + } + ], + "variables": { + "01KP22WA8NPMP922SAHDKHQS8Q": { + "id": "01KP22WA8NPMP922SAHDKHQS8Q", + "rank": "V", + "type": "string", + "format": "taskade-space-agent-id" + }, + "zWQheH5BoVPriUiE": { + "id": "zWQheH5BoVPriUiE", + "rank": "k", + "type": "string", + "format": "taskade-project-id" + } + } + } + }, + { + "id": "weekly-review", + "data": { + "version": "2", + "flowTitle": "Weekly Review", + "trigger": { + "valid": false, + "displayName": "Every Friday at 6PM", + "type": "PIECE_TRIGGER", + "settings": { + "pieceName": "@taskade/automade-internalpiece-schedule", + "pieceVersion": "BLANK", + "triggerName": "schedule.weekly", + "input": {} + } + }, + "actions": [ + { + "name": "b1b1b1b1-0001-0001-0001-000000000001", + "valid": false, + "displayName": "Editor — Cut 30%", + "type": "PIECE", + "settings": { + "pieceName": "@taskade/automade-internalpiece-openai", + "pieceVersion": "BLANK", + "actionName": "agent.run_command", + "input": { + "input": "Summarize this week's decisions from the Decision Log in exactly 10 bullets. Cut everything that isn't a concrete decision, number, name, or outcome. Be ruthless.", + "agentId": { + "type": "flow-template-variable-input-type", + "id": "01KP22WA8N1S8JDRDP55D87NX5" + } + } + } + }, + { + "name": "b2b2b2b2-0002-0002-0002-000000000002", + "valid": false, + "displayName": "Strategist — Stress Test the Plan", + "type": "PIECE", + "settings": { + "pieceName": "@taskade/automade-internalpiece-openai", + "pieceVersion": "BLANK", + "actionName": "agent.run_command", + "input": { + "input": "Stress test next week's plan. Identify the three most likely points of failure, the hidden assumptions, and the conditions under which the plan breaks down completely.", + "agentId": { + "type": "flow-template-variable-input-type", + "id": "01KP22WA8NPMP922SAHDKHQS8Q" + } + } + } + }, + { + "name": "b3b3b3b3-0003-0003-0003-000000000003", + "valid": false, + "displayName": "Append Weekly Review to Decision Log", + "type": "PIECE", + "settings": { + "pieceName": "@taskade/automade-internalpiece-taskade", + "pieceVersion": "BLANK", + "actionName": "task.create", + "input": { + "content": "Weekly Review — {{ trigger[\"triggerTime\"] }}\n\n---\n\n## ✂️ Editor: Week in 10 Bullets\n{{ b1b1b1b1-0001-0001-0001-000000000001[\"result\"] }}\n\n---\n\n## 🧭 Strategist: Stress Test Next Week\n{{ b2b2b2b2-0002-0002-0002-000000000002[\"result\"] }}", + "position": "beforeend", + "projectId": { + "type": "flow-template-variable-input-type", + "id": "aSs5trhjYStYSydc" + } + } + } + } + ], + "variables": { + "01KP22WA8N1S8JDRDP55D87NX5": { + "id": "01KP22WA8N1S8JDRDP55D87NX5", + "rank": "V", + "type": "string", + "format": "taskade-space-agent-id" + }, + "01KP22WA8NPMP922SAHDKHQS8Q": { + "id": "01KP22WA8NPMP922SAHDKHQS8Q", + "rank": "k", + "type": "string", + "format": "taskade-space-agent-id" + }, + "aSs5trhjYStYSydc": { + "id": "aSs5trhjYStYSydc", + "rank": "s", + "type": "string", + "format": "taskade-project-id" + } + } + } + } + ], + "apps": [ + { + "id": "cortex", + "data": { + "src": { + "directory": { + "lib": { + "directory": { + "shiki.ts": { + "file": { + "contents": "/**\n * Shiki highlighter — singleton with CDN-based language loading.\n *\n * Instead of importing from \"shiki\" (which bundles ALL ~200 language grammars\n * and ~50 themes as dynamic imports, producing 347 esbuild chunks), we use\n * `@shikijs/core` with:\n * - 2 themes imported statically (github-light, github-dark)\n * - Language grammars fetched from CDN on demand\n *\n * This reduces the chunk count from 347 → 0.\n *\n * @see https://github.com/taskade/taskcade/issues/26056\n */\n\nimport type { HighlighterCore } from \"@shikijs/core\";\nimport { createHighlighterCore } from \"@shikijs/core\";\nimport { createJavaScriptRegexEngine } from \"@shikijs/engine-javascript\";\nimport githubDark from \"@shikijs/themes/github-dark\";\nimport githubLight from \"@shikijs/themes/github-light\";\n\nexport type { ThemedToken } from \"@shikijs/core\";\n\nexport type BundledLanguage = string;\n\n// Keep in sync with the shiki version in package.json\nconst SHIKI_CDN_BASE = \"https://esm.sh/@shikijs/langs@4.0.2/\";\n\n// Singleton highlighter — created once, languages loaded incrementally\nlet highlighterPromise: Promise | null = null;\n\nfunction getOrCreateHighlighter(): Promise {\n if (!highlighterPromise) {\n highlighterPromise = createHighlighterCore({\n themes: [githubLight, githubDark],\n langs: [],\n engine: createJavaScriptRegexEngine(),\n });\n }\n return highlighterPromise;\n}\n\n// Track language loading state to deduplicate concurrent fetches\nconst loadedLangs = new Set();\nconst loadingLangs = new Map>();\n\nasync function ensureLanguage(\n highlighter: HighlighterCore,\n lang: string\n): Promise {\n if (loadedLangs.has(lang)) return;\n\n // Deduplicate concurrent loads for the same language\n const existing = loadingLangs.get(lang);\n if (existing) return existing;\n\n const promise = (async () => {\n try {\n // Dynamic CDN import — intentionally left as a runtime fetch, not bundled\n const mod = await import(`${SHIKI_CDN_BASE}${lang}.mjs`);\n await highlighter.loadLanguage(mod.default ?? mod);\n } catch {\n // Language not available on CDN — will fall back to plaintext\n } finally {\n loadedLangs.add(lang); // Prevent retries, even on failure\n loadingLangs.delete(lang);\n }\n })();\n\n loadingLangs.set(lang, promise);\n return promise;\n}\n\n/**\n * Returns a Shiki highlighter with the requested language loaded.\n * The highlighter is a singleton; languages are loaded incrementally from CDN.\n */\nexport async function getHighlighter(\n language: string\n): Promise {\n const highlighter = await getOrCreateHighlighter();\n await ensureLanguage(highlighter, language);\n return highlighter;\n}\n" + } + }, + "utils.ts": { + "file": { + "contents": "import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n" + } + }, + "agent-chat": { + "directory": { + "v2": { + "directory": { + "index.ts": { + "file": { + "contents": "/**\n * Agent Chat SDK v2 for Taskade Genesis\n *\n * Uses the AI SDK (`@ai-sdk/react`) with the `/chat` endpoint.\n *\n * IMPORTANT: `useChat` requires a real `Chat` instance — it crashes if passed undefined.\n * Always guard with a conditional render so `useChat` is only called after `chat` is created.\n *\n * @example\n * ```typescript\n * import { useChat } from '@ai-sdk/react';\n * import { createConversation, createAgentChat } from '@/lib/agent-chat/v2';\n * import { useState } from 'react';\n * import { ulid } from 'ulidx';\n *\n * function ChatComponent() {\n * const [chat, setChat] = useState | null>(null);\n *\n * const handleStartChat = async () => {\n * const { conversationId } = await createConversation(agentId);\n * setChat(createAgentChat(agentId, conversationId));\n * };\n *\n * if (!chat) return ;\n * return ;\n * }\n *\n * function ActiveChat({ chat }: { chat: ReturnType }) {\n * const { messages, status } = useChat({ chat, id: chat.id });\n *\n * const handleSend = async (text: string) => {\n * await chat.sendMessage({\n * id: ulid(),\n * role: 'user',\n * parts: [{ type: 'text', text }],\n * });\n * };\n *\n * return (\n *
\n * {messages.map(msg => (\n *
\n * {msg.role === 'user' ? 'You: ' : 'Agent: '}\n * {msg.parts.filter(p => p.type === 'text').map(p => p.text).join('')}\n *
\n * ))}\n * \n *
\n * );\n * }\n * ```\n */\n\nexport type { ClientOptions, CreateConversationResponse } from './client';\nexport { createConversation } from './client';\nexport { createAgentChat } from './createAgentChat';\n" + } + }, + "README.md": { + "file": { + "contents": "# Agent Chat SDK v2\n\nSDK for building AI Agent Chat interfaces in Taskade Genesis apps.\nBuilt on the AI SDK (`@ai-sdk/react`), using the `/chat` endpoint.\n\n## Quick Start\n\n```typescript\nimport { useChat } from '@ai-sdk/react';\nimport { createConversation, createAgentChat } from '@/lib/agent-chat/v2';\nimport { isToolUIPart } from 'ai';\nimport type { UIMessage } from 'ai';\nimport { useState } from 'react';\nimport { ulid } from 'ulidx';\n\n// IMPORTANT: useChat requires a real Chat instance — it crashes if passed undefined.\n// Split into two components so useChat is only called after chat is created.\n\nfunction ChatComponent() {\n const [chat, setChat] = useState | null>(null);\n\n const handleStartChat = async () => {\n const { conversationId } = await createConversation(agentId);\n setChat(createAgentChat(agentId, conversationId));\n };\n\n if (!chat) return ;\n return ;\n}\n\nfunction ActiveChat({ chat }: { chat: ReturnType }) {\n const { messages, status, addToolApprovalResponse } = useChat({ chat, id: chat.id });\n const isSending = status === 'submitted' || status === 'streaming';\n\n const handleSend = async (text: string) => {\n await chat.sendMessage({\n id: ulid(),\n role: 'user',\n parts: [{ type: 'text', text }],\n });\n };\n\n return (\n
\n {messages.map(msg => (\n
\n {msg.role === 'user' ? 'You' : 'Agent'}:\n {msg.parts.map((part, i) => {\n // Always render all part types — the agent may use tools even if none\n // are configured yet. Omitting this causes tool calls to be silently dropped.\n if (part.type === 'text') {\n return {part.text};\n }\n if (isToolUIPart(part)) {\n return (\n
\n Tool: {part.toolName} [{part.state}]\n {part.state === 'approval-requested' && part.approval != null && (\n <>\n \n \n \n )}\n
\n );\n }\n return null;\n })}\n
\n ))}\n \n
\n );\n}\n```\n\n## Pre-built AI Elements UI (`@/components/ai-elements/`)\n\nPre-built, styled React components for chat interfaces are available via AI Elements.\nUse these instead of building chat UI from scratch:\n\n| Component | Import | Purpose |\n|-----------|--------|---------|\n| `Conversation` | `@/components/ai-elements/conversation` | Scrollable chat container with auto-stick-to-bottom |\n| `Message` | `@/components/ai-elements/message` | Message bubble with role-based styling + markdown |\n| `PromptInput` | `@/components/ai-elements/prompt-input` | Chat input form with file attachments + submit |\n| `Suggestion` | `@/components/ai-elements/suggestion` | Quick-reply suggestion pills |\n| `Reasoning` | `@/components/ai-elements/reasoning` | Collapsible thinking/reasoning display |\n| `CodeBlock` | `@/components/ai-elements/code-block` | Syntax-highlighted code with copy button |\n| `Tool` | `@/components/ai-elements/tool` | Collapsible tool call display with status |\n| `Confirmation` | `@/components/ai-elements/confirmation` | Tool call approval UI with approve/reject slots |\n| `Shimmer` | `@/components/ai-elements/shimmer` | Animated shimmer text — usage: `Loading...` (children must be a string) |\n\nSee `@/components/ai-elements` for the full list of available components that you may use to build your chat UI.\n\n\n### Full Example with AI Elements\n\n```typescript\nimport { useChat } from '@ai-sdk/react';\nimport { createConversation, createAgentChat } from '@/lib/agent-chat/v2';\nimport { Conversation, ConversationContent, ConversationScrollButton } from '@/components/ai-elements/conversation';\nimport { Message, MessageContent, MessageResponse } from '@/components/ai-elements/message';\nimport { Tool, ToolHeader, ToolContent, ToolInput, ToolOutput } from '@/components/ai-elements/tool';\nimport {\n Confirmation,\n ConfirmationTitle,\n ConfirmationRequest,\n ConfirmationAccepted,\n ConfirmationRejected,\n ConfirmationActions,\n ConfirmationAction,\n} from '@/components/ai-elements/confirmation';\nimport { PromptInput, PromptInputTextarea, PromptInputFooter, PromptInputSubmit } from '@/components/ai-elements/prompt-input';\nimport { Suggestions, Suggestion } from '@/components/ai-elements/suggestion';\nimport { isToolUIPart } from 'ai';\nimport type { UIMessage } from 'ai';\nimport { useState } from 'react';\nimport { ulid } from 'ulidx';\n\nfunction ChatPage() {\n const [chat, setChat] = useState | null>(null);\n\n const handleStartChat = async () => {\n const { conversationId } = await createConversation(agentId);\n setChat(createAgentChat(agentId, conversationId));\n };\n\n if (!chat) return ;\n return ;\n}\n\nfunction ActiveChat({ chat }: { chat: ReturnType }) {\n const { messages, status, addToolApprovalResponse } = useChat({ chat, id: chat.id });\n\n const handleSend = async (text: string) => {\n await chat.sendMessage({\n id: ulid(),\n role: 'user',\n parts: [{ type: 'text', text }],\n });\n };\n\n const hasMessages = messages.length > 0;\n\n return (\n
\n \n \n {messages.map((msg) => (\n \n \n \n \n \n ))}\n \n \n \n\n {!hasMessages && (\n \n \n \n \n )}\n\n handleSend(text)}>\n \n \n \n \n \n
\n );\n}\n\n// Always handle all part types — the agent may call tools even if none are configured\n// yet. Omitting isToolUIPart handling causes tool calls to be silently dropped.\nfunction MessageParts({\n message,\n onApprove,\n}: {\n message: UIMessage;\n onApprove: ReturnType['addToolApprovalResponse'];\n}) {\n return (\n <>\n {message.parts.map((part, i) => {\n const key = `${message.id}-${i}`;\n\n if (part.type === 'text') {\n return message.role === 'user' ? (\n

{part.text}

\n ) : (\n {part.text}\n );\n }\n\n if (isToolUIPart(part)) {\n return (\n \n \n \n \n \n \n Allow this tool to run?\n \n Approved\n Rejected\n \n \n part.approval != null && onApprove({ id: part.approval.id, approved: false })\n }\n >\n Deny\n \n \n part.approval != null && onApprove({ id: part.approval.id, approved: true })\n }\n >\n Approve\n \n \n \n \n \n \n );\n }\n\n return null;\n })}\n \n );\n}\n```\n\n## Tool Call Approval\n\nBoth examples above already include full approval handling — it is part of the standard\n`MessageParts` pattern and should always be present, even if the agent has no tools\nconfigured today. Tool call parts will simply never appear in that case; the code is inert.\n\nThe approval flow is managed by two pieces:\n\n- **`Confirmation` + sub-components** (`@/components/ai-elements/confirmation`) — conditional\n rendering driven by `part.state` and `part.approval`. No handler logic lives inside them.\n- **`addToolApprovalResponse`** (from `useChat`) — call with `{ id: part.approval.id, approved }`.\n The `id` must be the tool part’s **`approval.id`**, not `toolCallId`; wrong `id` updates nothing.\n `createAgentChat` then automatically sends the next request to the server.\n\n### Approval state lifecycle\n\n| State | What's visible |\n|---|---|\n| `approval-requested` | `` + `` (approve/deny buttons) |\n| `approval-responded` | `` or `` based on decision |\n| `output-available` | Tool completed — `` shows result |\n| `output-denied` | Tool was denied — `` stays visible |\n\nOnce `addToolApprovalResponse` is called, `createAgentChat` automatically sends the next\nrequest to the server — no manual trigger required.\n\n## API\n\n**`createConversation(agentId, options?)`**\nCreates a new public conversation. Returns `{ ok, conversationId }`.\n\n**`createAgentChat(agentId, conversationId, options?)`**\nCreates a `Chat` instance configured for the agent. Use with `useChat` from `@ai-sdk/react`.\n\n**`useChat({ chat, id })`** (from `@ai-sdk/react`)\nStandard AI SDK hook. Returns `{ messages, status, error, addToolApprovalResponse }`.\n\n**`addToolApprovalResponse({ id, approved, reason? })`** (from `useChat`)\nSubmits an approve (`true`) or deny (`false`) decision. **`id` is `toolUIPart.approval.id`**, not `toolCallId`.\nThe conversation automatically resumes after the response is submitted.\n\n## Sending Messages\n\n```typescript\nimport { ulid } from 'ulidx';\n\nawait chat.sendMessage({\n id: ulid(),\n role: 'user',\n parts: [{ type: 'text', text: 'Hello!' }],\n});\n```\n\n## Message Format\n\nMessages use the AI SDK `UIMessage` type:\n\n```typescript\nimport { isToolUIPart } from 'ai';\n\nmsg.parts.filter(p => p.type === 'text').map(p => p.text) // Text\nmsg.parts.filter(isToolUIPart) // Tool calls\n```\n\n## Requirements\n\n- Agent must have **public visibility** enabled before creating a conversation\n- `useChat` must receive a real `Chat` instance, never `undefined`\n" + } + }, + "client.ts": { + "file": { + "contents": "/**\n * API Response types\n */\nexport interface CreateConversationResponse {\n ok: boolean;\n conversationId: string;\n}\n\n/**\n * Configuration for API client\n */\nexport interface ClientOptions {\n /** Base URL for API requests (defaults to relative paths) */\n baseUrl?: string;\n}\n\nfunction isEmptyString(value: string | null | undefined): boolean {\n return value == null || value.trim().length === 0;\n}\n\n/**\n * Creates a new public agent conversation\n *\n * @param agentId - The agent ID\n * @param options - Optional client configuration\n * @returns Promise resolving to conversation ID\n * @throws Error if conversation creation fails\n *\n * @example\n * ```typescript\n * const { conversationId } = await createConversation('agent-456');\n * ```\n */\nexport async function createConversation(\n agentId: string,\n options?: ClientOptions,\n): Promise {\n if (isEmptyString(agentId)) {\n throw new Error('Agent ID cannot be empty');\n }\n\n const baseUrl = options?.baseUrl ?? '';\n const url = `${baseUrl}/api/taskade/agents/${encodeURIComponent(agentId)}/public-conversations`;\n\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n\n const contentType = response.headers.get('content-type') || '';\n const responseText = await response.text().catch(() => '');\n\n if (!response.ok) {\n throw new Error(\n `Failed to create conversation: ${response.status} ${responseText || 'Unknown error'}`,\n );\n }\n\n if (!contentType.includes('application/json')) {\n throw new Error(\n `Invalid response format: expected JSON, got ${contentType}. Response: ${responseText.substring(0, 100)}`,\n );\n }\n\n try {\n const data = JSON.parse(responseText);\n return data as CreateConversationResponse;\n } catch (err) {\n throw new Error(\n `Failed to parse JSON response: ${err instanceof Error ? err.message : 'Unknown error'}. Response: ${responseText.substring(0, 200)}`,\n );\n }\n}\n" + } + }, + "createAgentChat.ts": { + "file": { + "contents": "import { Chat } from '@ai-sdk/react';\nimport {\n DefaultChatTransport,\n lastAssistantMessageIsCompleteWithApprovalResponses,\n lastAssistantMessageIsCompleteWithToolCalls,\n UIMessage,\n isToolUIPart,\n} from 'ai';\nimport { ulid } from 'ulidx';\n\nimport type { ClientOptions } from './client';\n\nexport type ExtractInputMessagesResult = {\n history: UIMessage[];\n messages: UIMessage[];\n};\n\n/**\n * Generic version of the TAA helper:\n * - Separates \"history\" from the latest actionable message(s) to send to the server.\n * - Handles agentic tool-call loops where the last assistant message contains tool parts.\n */\nfunction extractInputMessages(messages: UIMessage[]): ExtractInputMessagesResult {\n if (messages.length === 0) {\n return { history: messages, messages: [] };\n }\n\n const lastMessageIndex = messages.length - 1;\n const lastMessage = messages[lastMessageIndex];\n if (lastMessage == null) {\n return { history: messages, messages: [] };\n }\n\n if (lastMessage.role === 'user') {\n const history = messages.slice(0, lastMessageIndex);\n return { history, messages: [lastMessage] };\n }\n\n if (lastMessage.role === 'assistant') {\n const parts = lastMessage.parts;\n if (parts != null && parts.length > 0) {\n const lastPart = parts[parts.length - 1];\n if (lastPart != null && isToolUIPart(lastPart)) {\n if (lastPart.state === 'output-available' || lastPart.state === 'output-error') {\n const history = messages.slice(0, lastMessageIndex);\n return { history, messages: [lastMessage] };\n }\n }\n }\n }\n\n const history = messages.slice(0, lastMessageIndex);\n return { history, messages: [lastMessage] };\n}\n\nconst MAX_HISTORY_MESSAGES = 6;\n\n/**\n * Creates a Chat instance configured for a Taskade agent public conversation.\n *\n * Use with `useChat` from `@ai-sdk/react` to build chat interfaces.\n *\n * IMPORTANT: `useChat` requires a real `Chat` instance — it crashes if passed undefined.\n * Always guard with a conditional render so `useChat` is only called after `chat` is created.\n *\n * @param agentId - The agent ID\n * @param conversationId - The conversation ID (from createConversation)\n * @param options - Optional client configuration\n * @returns A Chat instance ready to use with useChat\n *\n * Tool approval: `useChat`'s `addToolApprovalResponse` expects `{ id, approved }` where `id` is\n * `toolUIPart.approval.id` — **not** `toolCallId`. Passing `toolCallId` will not update any part.\n *\n * @example\n * ```typescript\n * import { useChat } from '@ai-sdk/react';\n * import { createConversation, createAgentChat } from '@/lib/agent-chat/v2';\n *\n * function ChatComponent() {\n * const [chat, setChat] = useState | null>(null);\n *\n * const handleStartChat = async () => {\n * const { conversationId } = await createConversation(agentId);\n * setChat(createAgentChat(agentId, conversationId));\n * };\n *\n * if (!chat) return ;\n * return ;\n * }\n *\n * function ActiveChat({ chat }: { chat: ReturnType }) {\n * const { messages, status } = useChat({ chat, id: chat.id });\n * // ...\n * }\n * ```\n */\nexport function createAgentChat(\n agentId: string,\n conversationId: string,\n options?: ClientOptions,\n): Chat {\n const baseUrl = options?.baseUrl ?? '';\n const api = `${baseUrl}/api/taskade/agents/${encodeURIComponent(agentId)}/public-conversations/${encodeURIComponent(conversationId)}/chat`;\n\n const chatState = new Chat({\n messages: [],\n transport: new DefaultChatTransport({\n api,\n prepareSendMessagesRequest: (opts) => {\n const { history, messages } = extractInputMessages(opts.messages);\n\n const maxHistory = Math.max(0, MAX_HISTORY_MESSAGES - messages.length);\n const trimmedHistory = maxHistory === 0 ? [] : history.slice(-maxHistory);\n\n return {\n body: {\n messages,\n history: trimmedHistory,\n },\n };\n },\n }),\n id: conversationId,\n generateId: ulid,\n sendAutomaticallyWhen: (options) => {\n const shouldSendAutomatically =\n lastAssistantMessageIsCompleteWithToolCalls(options) ||\n lastAssistantMessageIsCompleteWithApprovalResponses(options);\n if (!shouldSendAutomatically) {\n return false;\n }\n if (chatState.error != null) {\n return false;\n }\n return true;\n },\n });\n\n return chatState;\n}\n" + } + } + } + }, + "hooks.ts": { + "file": { + "contents": "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport {\n type ClientOptions,\n createConversation as createConversationApi,\n sendMessage as sendMessageApi,\n} from './client';\nimport { AgentChatStream } from './stream';\nimport type { ErrorEvent, MessageState, StreamOptions } from './types';\n\n/**\n * Options for useAgentChat hook\n */\nexport interface UseAgentChatOptions extends StreamOptions {\n /** Auto-connect stream on mount (default: true) */\n autoConnect?: boolean;\n}\n\n/**\n * Return type for useAgentChat hook\n */\nexport interface UseAgentChatReturn {\n /** Send a message to the conversation */\n sendMessage: (text: string) => Promise;\n /** Array of messages (sorted by creation order) */\n messages: MessageState[];\n /** Whether stream is currently connected */\n isConnected: boolean;\n /** Current error, if any */\n error: Error | null;\n /** Current conversation ID */\n conversationId: string | null;\n /** Create a new conversation (stream stays open, switches to new conversation) */\n createConversation: () => Promise;\n /** Switch to a different conversation (stream stays open) */\n switchConversation: (conversationId: string) => void;\n /** Manually connect the stream (useful when autoConnect is false) */\n connect: () => void;\n}\n\n/**\n * React hook for managing agent chat conversation\n *\n * Requires a conversationId to be provided. The stream is opened when conversationId is available\n * and stays open throughout the conversation lifecycle.\n *\n * @param agentId - The agent ID\n * @param conversationId - The conversation ID (required - must be created manually)\n * @param options - Configuration options\n * @returns Chat state and methods\n *\n * @example\n * ```typescript\n * function ChatComponent() {\n * const [conversationId, setConversationId] = useState(null);\n * const { sendMessage, messages, isConnected } = useAgentChat('agent-456', conversationId);\n *\n * // Create conversation manually\n * const handleStartChat = async () => {\n * const { conversationId: newId } = await createConversation('agent-456');\n * setConversationId(newId);\n * };\n *\n * return (\n *
\n * {!conversationId && }\n * {messages.map(msg => (\n *
{msg.content}
\n * ))}\n * \n *
\n * );\n * }\n * ```\n */\nexport function useAgentChat(\n agentId: string,\n conversationId: string | null,\n options?: UseAgentChatOptions,\n): UseAgentChatReturn {\n const [messagesMap, setMessagesMap] = useState>(new Map());\n const [isConnected, setIsConnected] = useState(false);\n const [error, setError] = useState(null);\n const [currentConversationId, setCurrentConversationId] = useState(conversationId);\n const streamRef = useRef(null);\n const listenersRef = useRef void>>([]);\n const streamAgentIdRef = useRef(null);\n const currentConversationIdRef = useRef(conversationId);\n const previousConversationIdRef = useRef(conversationId);\n\n // Sync currentConversationId with prop when prop changes externally\n // This ensures the hook's internal state stays in sync with external prop updates\n useEffect(() => {\n const previousId = previousConversationIdRef.current;\n const newId = conversationId;\n\n // Clear messages when conversationId changes to a different non-null value\n // This handles the case where the prop changes externally (not via createConversation/switchConversation)\n if (previousId !== newId && newId != null && previousId != null) {\n setMessagesMap(new Map());\n // Also clear stream's message states if stream exists\n if (streamRef.current) {\n streamRef.current.clearMessages();\n }\n }\n\n setCurrentConversationId(newId);\n currentConversationIdRef.current = newId;\n previousConversationIdRef.current = newId;\n }, [conversationId]);\n\n // Cleanup on component unmount\n useEffect(() => {\n return () => {\n if (streamRef.current) {\n streamRef.current.disconnect();\n streamRef.current = null;\n streamAgentIdRef.current = null;\n }\n };\n }, []);\n\n // Convert messages map to sorted array\n const messages = useMemo(() => {\n const allMessages = Array.from(messagesMap.values());\n // Sort by message ID (ULID) - ULIDs are lexicographically sortable and encode timestamp\n // This ensures chronological order regardless of message role\n allMessages.sort((a, b) => {\n return a.id.localeCompare(b.id);\n });\n return allMessages;\n }, [messagesMap]);\n\n // Initialize stream when conversationId is available\n // Uses currentConversationId to ensure we're working with the latest state\n // (which may have been updated by createConversation/switchConversation)\n useEffect(() => {\n // Don't initialize if no conversationId\n if (!currentConversationId) {\n // Clean up existing stream if conversationId is removed\n if (streamRef.current) {\n streamRef.current.clearMessages(); // Clear stream's message states\n streamRef.current.disconnect();\n streamRef.current = null;\n streamAgentIdRef.current = null;\n setIsConnected(false);\n }\n // Clear messages when conversation is removed\n setMessagesMap(new Map());\n currentConversationIdRef.current = null;\n return;\n }\n\n // Update ref to track current conversation ID\n currentConversationIdRef.current = currentConversationId;\n\n // Clean up previous listeners before setting up new ones\n // This ensures we don't accumulate duplicate listeners when reusing the stream\n listenersRef.current.forEach((unsub) => unsub());\n listenersRef.current = [];\n\n const unsubscribeFunctions: Array<() => void> = [];\n let isMounted = true;\n\n // Register cleanup function first to ensure it's available immediately\n // This prevents race conditions where component unmounts before cleanup is registered\n const cleanup = () => {\n isMounted = false;\n unsubscribeFunctions.forEach((unsub) => unsub());\n // Note: We don't disconnect the stream here because:\n // 1. The stream may be reused in the next effect run (when switching conversations)\n // 2. Disconnection is handled when conversationId becomes null (early return above)\n // 3. On component unmount, React will call cleanup, but the stream will be garbage collected\n // when the ref is cleared by the early return path if conversationId becomes null\n };\n\n try {\n // Ensure we have valid agentId before creating stream\n // Note: currentConversationId is already validated above (early return if falsy)\n if (!agentId) {\n throw new Error(`Invalid parameters: agentId is required`);\n }\n\n // Helper function to update messages from stream state\n // Defined inline here since it only uses refs and setState (both stable)\n const updateMessages = () => {\n const currentStream = streamRef.current;\n if (currentStream) {\n setMessagesMap((prev) => {\n const next = new Map(prev);\n // Add/update stream messages (assistant responses)\n for (const [id, streamMsg] of currentStream.messages) {\n // Only update if this is an assistant message (or doesn't exist yet)\n // Preserve user messages - they should never be overwritten by stream\n const existing = prev.get(id);\n if (!existing || existing.role === 'assistant') {\n // Merge with existing to preserve content if stream message is missing it\n // Stream message should have latest content, but fallback to existing as safety\n const mergedMsg: MessageState = {\n ...existing,\n ...streamMsg,\n id, // Ensure ID is preserved\n role: 'assistant' as const,\n // Prefer stream message content if it exists and is non-empty, otherwise preserve existing\n content:\n typeof streamMsg.content === 'string' && streamMsg.content.length > 0\n ? streamMsg.content\n : existing?.content || '',\n };\n next.set(id, mergedMsg);\n }\n }\n return next;\n });\n }\n };\n\n let stream: AgentChatStream;\n\n // Check if we need to recreate the stream (agentId changed or stream doesn't exist)\n const existingStream = streamRef.current;\n const agentIdChanged = existingStream && streamAgentIdRef.current !== agentId;\n\n if (agentIdChanged && existingStream) {\n // AgentId changed - preserve messages from old stream before disconnecting\n // The hook's messagesMap should already have all messages, but we ensure\n // any messages in the stream's state are preserved in the hook's state\n setMessagesMap((prev) => {\n const next = new Map(prev);\n // Preserve any messages from the old stream that aren't already in the map\n for (const [id, streamMsg] of existingStream.messages) {\n if (!next.has(id)) {\n next.set(id, streamMsg);\n } else {\n // If message exists, preserve user messages and merge assistant messages\n const existing = prev.get(id);\n if (existing?.role === 'user') {\n // Keep user message as-is\n continue;\n }\n // Merge assistant messages\n if (existing?.role === 'assistant' || !existing) {\n next.set(id, {\n ...existing,\n ...streamMsg,\n id,\n role: 'assistant' as const,\n });\n }\n }\n }\n return next;\n });\n // Now disconnect the old stream\n existingStream.disconnect();\n streamRef.current = null;\n streamAgentIdRef.current = null;\n }\n\n // If stream exists and agentId hasn't changed, update its conversation ID and reuse it\n // We still need to re-register event listeners to ensure fresh closures\n if (streamRef.current && !agentIdChanged) {\n stream = streamRef.current;\n // Update conversation ID if needed - this handles disconnection/reconnection\n stream.setConversationId(currentConversationId);\n } else {\n // Create new stream\n const streamOptions: StreamOptions = {\n baseUrl: options?.baseUrl,\n autoReconnect: options?.autoReconnect ?? true,\n reconnectDelay: options?.reconnectDelay,\n onError: (err) => {\n if (isMounted) {\n setError(err);\n }\n options?.onError?.(err);\n },\n };\n stream = new AgentChatStream(agentId, currentConversationId, streamOptions);\n streamRef.current = stream;\n streamAgentIdRef.current = agentId;\n }\n\n // Always set up event listeners to ensure fresh closures\n // Even if stream exists, we need to re-register listeners with current closures\n // (isMounted, updateMessages, etc.)\n\n // Subscribe to events\n // Note: We always re-register listeners even if stream exists to ensure fresh closures\n const unsubscribeOpen = stream.on('open', () => {\n if (isMounted) {\n setIsConnected(true);\n setError(null);\n }\n });\n unsubscribeFunctions.push(unsubscribeOpen);\n\n const unsubscribeClose = stream.on('close', () => {\n if (isMounted) {\n setIsConnected(false);\n }\n });\n unsubscribeFunctions.push(unsubscribeClose);\n\n const unsubscribeTextDelta = stream.on('text-delta', () => {\n if (isMounted) {\n updateMessages();\n }\n });\n unsubscribeFunctions.push(unsubscribeTextDelta);\n\n const unsubscribeFinish = stream.on('finish', () => {\n if (isMounted) {\n updateMessages();\n }\n });\n unsubscribeFunctions.push(unsubscribeFinish);\n\n const unsubscribeError = stream.on('error', (event: ErrorEvent) => {\n if (isMounted) {\n setError(new Error(event.errorText));\n }\n });\n unsubscribeFunctions.push(unsubscribeError);\n\n // Store unsubscribe functions for cleanup on next effect run\n listenersRef.current = unsubscribeFunctions;\n\n // Connect stream if autoConnect is enabled (default: true)\n if (options?.autoConnect !== false) {\n stream.connect();\n }\n } catch (err) {\n if (isMounted) {\n const initError = err instanceof Error ? err : new Error('Failed to initialize chat');\n setError(initError);\n console.error('[useAgentChat] Initialization error:', initError);\n }\n }\n\n // Return cleanup function\n return cleanup;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n agentId,\n currentConversationId, // Use currentConversationId instead of prop to react to internal updates\n options?.baseUrl,\n options?.autoReconnect,\n options?.reconnectDelay,\n options?.autoConnect,\n options?.onError,\n // Note: updateMessages is defined inline in the effect and only uses refs/setState (stable)\n ]);\n\n // Send message function\n const sendMessage = useCallback(\n async (text: string) => {\n // Use currentConversationId to ensure we're using the latest state\n const convoId = currentConversationId;\n if (!convoId) {\n throw new Error('No conversation available. Create a conversation first.');\n }\n\n const clientOptions: ClientOptions = {\n baseUrl: options?.baseUrl,\n };\n\n try {\n setError(null);\n const response = await sendMessageApi(agentId, convoId, text, clientOptions);\n\n // Check if conversation is still active before adding message\n // This prevents race condition where conversation switches while API call is pending\n // Use ref to check latest conversation ID (closure value might be stale)\n if (currentConversationIdRef.current !== convoId) {\n // Conversation changed during API call, don't add message (it belongs to old conversation)\n return;\n }\n\n // Add user message with the messageId from the API response\n const userMessage: MessageState = {\n id: response.messageId,\n content: text,\n isComplete: true,\n role: 'user',\n };\n // Add user message to messages map\n setMessagesMap((prev) => {\n const next = new Map(prev);\n next.set(response.messageId, userMessage);\n return next;\n });\n } catch (err) {\n const sendError = err instanceof Error ? err : new Error('Failed to send message');\n setError(sendError);\n throw sendError;\n }\n },\n [agentId, currentConversationId, options?.baseUrl],\n );\n\n /**\n * Create a new conversation\n *\n * Creates a new conversation and switches the stream to it. The hook's internal state\n * is updated automatically. If you're managing conversationId as a prop, you should\n * also update it to keep it in sync:\n *\n * @example\n * ```typescript\n * const [conversationId, setConversationId] = useState(null);\n * const { createConversation } = useAgentChat(agentId, conversationId);\n *\n * const handleNewChat = async () => {\n * const newId = await createConversation();\n * setConversationId(newId); // Update prop to keep it in sync\n * };\n * ```\n *\n * @returns The new conversation ID\n */\n const createConversation = useCallback(async (): Promise => {\n const clientOptions: ClientOptions = {\n baseUrl: options?.baseUrl,\n };\n const { conversationId: newConvoId } = await createConversationApi(agentId, clientOptions);\n\n // Update internal state first - this will trigger useEffect to reinitialize stream\n setCurrentConversationId(newConvoId);\n currentConversationIdRef.current = newConvoId;\n // Clear messages when switching to new conversation\n setMessagesMap(new Map());\n\n // If stream exists, update it to the new conversation ID\n // This handles the case where stream was already initialized\n if (streamRef.current) {\n streamRef.current.setConversationId(newConvoId);\n }\n\n return newConvoId;\n }, [agentId, options?.baseUrl]);\n\n /**\n * Switch to a different conversation\n *\n * Switches the stream to an existing conversation. The hook's internal state\n * is updated automatically. If you're managing conversationId as a prop, you should\n * also update it to keep it in sync:\n *\n * @example\n * ```typescript\n * const [conversationId, setConversationId] = useState(null);\n * const { switchConversation } = useAgentChat(agentId, conversationId);\n *\n * const handleSwitchChat = (existingConvoId: string) => {\n * switchConversation(existingConvoId);\n * setConversationId(existingConvoId); // Update prop to keep it in sync\n * };\n * ```\n *\n * @param convoId - The conversation ID to switch to\n */\n const switchConversation = useCallback((convoId: string) => {\n // Update internal state first - this will trigger useEffect to reinitialize stream\n setCurrentConversationId(convoId);\n currentConversationIdRef.current = convoId;\n // Clear messages when switching conversations\n setMessagesMap(new Map());\n\n // If stream exists, update it to the new conversation ID\n // This handles the case where stream was already initialized\n if (streamRef.current) {\n streamRef.current.setConversationId(convoId);\n }\n }, []);\n\n /**\n * Manually connect the stream\n *\n * Useful when `autoConnect` is set to `false`. The stream must have a valid\n * conversationId before connecting.\n *\n * @example\n * ```typescript\n * const { connect, conversationId } = useAgentChat(agentId, conversationId, {\n * autoConnect: false,\n * });\n *\n * // Connect manually after conversation is created\n * const handleStartChat = async () => {\n * const { conversationId: newId } = await createConversation(agentId);\n * setConversationId(newId);\n * // Stream will be initialized but not connected due to autoConnect: false\n * // Manually connect it\n * connect();\n * };\n * ```\n */\n const connect = useCallback(() => {\n if (!currentConversationId) {\n throw new Error('Cannot connect: no conversation available. Create a conversation first.');\n }\n\n if (!streamRef.current) {\n throw new Error('Cannot connect: stream not initialized. Ensure conversationId is provided.');\n }\n\n streamRef.current.connect();\n }, [currentConversationId]);\n\n return {\n sendMessage,\n messages,\n isConnected,\n error,\n conversationId: currentConversationId,\n createConversation,\n switchConversation,\n connect,\n };\n}\n" + } + }, + "index.ts": { + "file": { + "contents": "/**\n * Agent Chat SDK for Taskade Genesis (Legacy)\n *\n * @deprecated Use `@/lib/agent-chat/v2` instead for new code.\n *\n * Low-level SDK for building AI Agent Chat interfaces in React applications.\n * Provides API client, SSE stream management, and optional React hooks.\n *\n * @example\n * ```typescript\n * // React hook usage\n * import { useAgentChat, createConversation } from '@/lib/agent-chat';\n * import { useState } from 'react';\n *\n * function ChatComponent() {\n * const [conversationId, setConversationId] = useState(null);\n * const { sendMessage, messages, isConnected } = useAgentChat(agentId, conversationId);\n *\n * const handleStartChat = async () => {\n * const { conversationId: newId } = await createConversation(agentId);\n * setConversationId(newId);\n * };\n *\n * return (\n *
\n * {!conversationId && }\n * {messages.map(msg => (\n *
\n * {msg.role === 'user' ? 'You: ' : 'Agent: '}\n * {msg.content}\n *
\n * ))}\n * \n *
\n * );\n * }\n * ```\n */\n\n// Core API client (for advanced usage)\nexport type { ClientOptions } from './client';\nexport { createConversation, sendMessage } from './client';\n\n// Stream manager (for advanced usage)\nexport { AgentChatStream } from './stream';\n\n// Types\nexport type {\n CreateConversationResponse,\n ErrorEvent,\n ErrorHandler,\n FinishEvent,\n FinishHandler,\n MessageState,\n SendMessageResponse,\n StartEvent,\n StreamEvent,\n StreamEventHandler,\n StreamOptions,\n TextDeltaEvent,\n TextDeltaHandler,\n TextEndEvent,\n TextStartEvent,\n ToolCallEndEvent,\n ToolCallState,\n ToolInputAvailableEvent,\n ToolInputDeltaEvent,\n ToolInputStartEvent,\n ToolOutputAvailableEvent,\n} from './types';\n\n// React hook (main API)\nexport type { UseAgentChatOptions, UseAgentChatReturn } from './hooks';\nexport { useAgentChat } from './hooks';\n" + } + }, + "types.ts": { + "file": { + "contents": "import { z } from 'zod';\n\n/**\n * SSE Event schemas matching backend AgentPublicConversationMessageStreamResponseSchema\n */\nexport const StreamEventSchema = z.union([\n z.object({\n type: z.literal('start'),\n messageId: z.string(),\n }),\n z.object({\n type: z.literal('text-start'),\n id: z.string(),\n }),\n z.object({\n type: z.literal('text-delta'),\n id: z.string(),\n delta: z.string(),\n }),\n z.object({\n type: z.literal('text-end'),\n id: z.string(),\n }),\n z.object({\n type: z.literal('tool-input-start'),\n toolCallId: z.string(),\n toolName: z.string(),\n messageId: z.string().optional(),\n }),\n z.object({\n type: z.literal('tool-input-delta'),\n toolCallId: z.string(),\n inputTextDelta: z.string(),\n messageId: z.string().optional(),\n }),\n z.object({\n type: z.literal('tool-input-available'),\n toolCallId: z.string(),\n input: z.unknown(),\n messageId: z.string().optional(),\n }),\n z.object({\n type: z.literal('tool-output-available'),\n toolCallId: z.string(),\n output: z.unknown(),\n messageId: z.string().optional(),\n }),\n z.object({\n type: z.literal('tool-call-end'),\n toolCallId: z.string(),\n messageId: z.string().optional(),\n }),\n z.object({\n type: z.literal('finish'),\n }),\n z.object({\n type: z.literal('error'),\n errorText: z.string(),\n }),\n]);\n\nexport type StreamEvent = z.infer;\n\n/**\n * Specific event types for type narrowing\n */\nexport type StartEvent = Extract;\nexport type TextStartEvent = Extract;\nexport type TextDeltaEvent = Extract;\nexport type TextEndEvent = Extract;\nexport type ToolInputStartEvent = Extract;\nexport type ToolInputDeltaEvent = Extract;\nexport type ToolInputAvailableEvent = Extract;\nexport type ToolOutputAvailableEvent = Extract;\nexport type ToolCallEndEvent = Extract;\nexport type FinishEvent = Extract;\nexport type ErrorEvent = Extract;\n\n/**\n * API Response types\n */\nexport interface CreateConversationResponse {\n ok: boolean;\n conversationId: string;\n}\n\nexport interface SendMessageResponse {\n ok: boolean;\n messageId: string;\n}\n\n/**\n * Configuration options for stream\n */\nexport interface StreamOptions {\n /** Base URL for API requests (defaults to relative paths) */\n baseUrl?: string;\n /** Automatically reconnect on disconnect (default: true) */\n autoReconnect?: boolean;\n /** Delay in ms before reconnecting (default: 1000) */\n reconnectDelay?: number;\n /**\n * Callback for stream errors\n * @default Logs to console.error\n * @remarks In production, provide your own error handler to properly handle errors\n */\n onError?: (error: Error) => void;\n}\n\n/**\n * Accumulated message state\n */\nexport interface MessageState {\n id: string;\n content: string;\n isComplete: boolean;\n role: 'user' | 'assistant';\n toolCalls?: ToolCallState[];\n}\n\n/**\n * Tool call state\n */\nexport interface ToolCallState {\n toolCallId: string;\n toolName: string;\n input?: unknown;\n output?: unknown;\n isComplete: boolean;\n}\n\n/**\n * Event handler types\n */\nexport type StreamEventHandler = (event: StreamEvent) => void;\nexport type TextDeltaHandler = (event: TextDeltaEvent) => void;\nexport type FinishHandler = (event: FinishEvent) => void;\nexport type ErrorHandler = (event: ErrorEvent) => void;\n" + } + }, + "README.md": { + "file": { + "contents": "# Agent Chat SDK (Legacy)\n\n> **DEPRECATED**: Do not use this SDK for new code. Use `@/lib/agent-chat/v2` instead — see `src/lib/agent-chat/v2/README.md` for docs.\n\n---\n\nSimple SDK for building AI Agent Chat interfaces in Taskade Genesis apps.\n\n**Key Features:**\n- Manual conversation creation (consumer must create conversation before use)\n- Stream opens when conversationId is provided\n- Stream stays open throughout (handles reconnection automatically)\n- Supports creating/switching conversations\n\n## Quick Start\n\n```typescript\nimport { useAgentChat, createConversation } from '@/lib/agent-chat';\nimport { useState } from 'react';\n\nfunction ChatComponent() {\n const [conversationId, setConversationId] = useState(null);\n const { sendMessage, messages, isConnected } = useAgentChat(agentId, conversationId);\n\n // Create conversation manually\n const handleStartChat = async () => {\n const { conversationId: newId } = await createConversation(agentId);\n setConversationId(newId);\n };\n \n return (\n
\n {!conversationId && }\n {messages.map(msg => (\n
\n {msg.role === 'user' ? 'You: ' : 'Agent: '}\n {msg.content}\n {msg.toolCalls && msg.toolCalls.length > 0 && (\n
Tool calls: {msg.toolCalls.length}
\n )}\n
\n ))}\n \n
\n );\n}\n```\n\n## Multiple Conversations\n\n```typescript\nconst [conversationId, setConversationId] = useState(null);\nconst {\n sendMessage,\n messages,\n createConversation: createNewConversation, // Create new conversation\n switchConversation, // Switch to existing conversation\n} = useAgentChat(agentId, conversationId);\n\n// Create new chat\nconst handleNewChat = async () => {\n const newConvoId = await createNewConversation();\n setConversationId(newConvoId);\n};\n\n// Switch to different conversation\nconst handleSwitchChat = (existingConvoId: string) => {\n switchConversation(existingConvoId);\n setConversationId(existingConvoId);\n};\n```\n\n## API Reference\n\n**`useAgentChat(agentId, conversationId)`**\n- `agentId` - The agent ID (required)\n- `conversationId` - The conversation ID (required, pass `null` if not yet created)\n- `sendMessage(text)` - Send message to current conversation (requires conversationId)\n- `messages` - Array of messages (MessageState[])\n- `isConnected` - Stream connection status\n- `conversationId` - Current conversation ID (from hook return)\n- `createConversation()` - Create new conversation (returns ID)\n- `switchConversation(id)` - Switch to different conversation\n- `error` - Current error, if any\n\n**Note:** The hook will only connect the stream when `conversationId` is provided (not `null`). You must create a conversation manually before the stream can connect.\n\n**Advanced (low-level):**\n- `createConversation(agentId)` - Direct API call\n- `sendMessage(agentId, conversationId, text)` - Direct API call\n- `AgentChatStream` - Stream manager class\n\nSee `index.ts` for full type exports.\n\n## Requirements\n\n- Agent must have **public visibility** enabled before creating conversation\n- Stream stays open permanently (never close after first response)\n- Text deltas are automatically accumulated (append, never replace)\n" + } + }, + "client.ts": { + "file": { + "contents": "import type { CreateConversationResponse, SendMessageResponse } from './types';\n\n/**\n * Configuration for API client\n */\nexport interface ClientOptions {\n /** Base URL for API requests (defaults to relative paths) */\n baseUrl?: string;\n}\n\n/**\n * Checks if a string is null, undefined, or empty after trimming\n */\nfunction isEmptyString(value: string | null | undefined): boolean {\n return value == null || value.trim().length === 0;\n}\n\n/**\n * Creates a new public agent conversation\n *\n * @param agentId - The agent ID\n * @param options - Optional client configuration\n * @returns Promise resolving to conversation ID\n * @throws Error if conversation creation fails\n *\n * @example\n * ```typescript\n * const { conversationId } = await createConversation('agent-456');\n * ```\n */\nexport async function createConversation(\n agentId: string,\n options?: ClientOptions,\n): Promise {\n if (isEmptyString(agentId)) {\n throw new Error('Agent ID cannot be empty');\n }\n\n const baseUrl = options?.baseUrl ?? '';\n const url = `${baseUrl}/api/taskade/agents/${encodeURIComponent(agentId)}/public-conversations`;\n\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n\n // Read response body once\n const contentType = response.headers.get('content-type') || '';\n const responseText = await response.text().catch(() => '');\n\n if (!response.ok) {\n throw new Error(\n `Failed to create conversation: ${response.status} ${responseText || 'Unknown error'}`,\n );\n }\n\n if (!contentType.includes('application/json')) {\n throw new Error(\n `Invalid response format: expected JSON, got ${contentType}. Response: ${responseText.substring(\n 0,\n 100,\n )}`,\n );\n }\n\n try {\n const data = JSON.parse(responseText);\n return data as CreateConversationResponse;\n } catch (err) {\n throw new Error(\n `Failed to parse JSON response: ${\n err instanceof Error ? err.message : 'Unknown error'\n }. Response: ${responseText.substring(0, 200)}`,\n );\n }\n}\n\n/**\n * Sends a message to an existing conversation\n *\n * @param agentId - The agent ID\n * @param conversationId - The conversation ID\n * @param text - The message text to send\n * @param options - Optional client configuration\n * @returns Promise resolving when message is sent\n * @throws Error if message sending fails (e.g., conversation not idle, conversation ended)\n *\n * @example\n * ```typescript\n * await sendMessage('agent-456', 'convo-789', 'Hello!');\n * ```\n */\nexport async function sendMessage(\n agentId: string,\n conversationId: string,\n text: string,\n options?: ClientOptions,\n): Promise {\n if (isEmptyString(agentId)) {\n throw new Error('Agent ID cannot be empty');\n }\n\n if (isEmptyString(conversationId)) {\n throw new Error('Conversation ID cannot be empty');\n }\n\n const trimmedText = text.trim();\n if (isEmptyString(trimmedText)) {\n throw new Error('Message text cannot be empty');\n }\n\n const baseUrl = options?.baseUrl ?? '';\n const url = `${baseUrl}/api/taskade/agents/${encodeURIComponent(\n agentId,\n )}/public-conversations/${encodeURIComponent(conversationId)}/messages`;\n\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ text: trimmedText }),\n });\n\n // Read response body once\n const contentType = response.headers.get('content-type') || '';\n const responseText = await response.text().catch(() => '');\n\n if (!response.ok) {\n // Parse error message if available\n let errorMessage = `Failed to send message: ${response.status}`;\n try {\n const errorData = JSON.parse(responseText);\n if (errorData.message) {\n errorMessage = errorData.message;\n }\n } catch {\n // Use default error message with response text\n if (responseText) {\n errorMessage = `${errorMessage}: ${responseText.substring(0, 100)}`;\n }\n }\n throw new Error(errorMessage);\n }\n\n if (!contentType.includes('application/json')) {\n throw new Error(\n `Invalid response format: expected JSON, got ${contentType}. Response: ${responseText.substring(\n 0,\n 100,\n )}`,\n );\n }\n\n try {\n const data = JSON.parse(responseText);\n return data as SendMessageResponse;\n } catch (err) {\n throw new Error(\n `Failed to parse JSON response: ${\n err instanceof Error ? err.message : 'Unknown error'\n }. Response: ${responseText.substring(0, 200)}`,\n );\n }\n}\n" + } + }, + "stream.ts": { + "file": { + "contents": "import type {\n ErrorEvent,\n FinishEvent,\n MessageState,\n StreamEvent,\n StreamEventHandler,\n StreamOptions,\n TextDeltaEvent,\n} from './types';\nimport { StreamEventSchema } from './types';\n\n/**\n * Event emitter interface for stream events\n */\ntype EventMap = {\n event: StreamEventHandler;\n 'text-delta': (event: TextDeltaEvent) => void;\n finish: (event: FinishEvent) => void;\n error: (event: ErrorEvent) => void;\n open: () => void;\n close: () => void;\n};\n\n/**\n * Manages SSE connection for agent conversation streaming\n *\n * Handles connection lifecycle, event parsing, and message state accumulation.\n * The stream stays open permanently and handles all messages in the conversation.\n *\n * **Memory Management**: The `messageStates` Map accumulates messages throughout the conversation.\n * For long-running conversations, call `clearMessages()` when switching conversations\n * or create a new stream instance for new conversations.\n *\n * @example\n * ```typescript\n * const stream = new AgentChatStream('agent-456', 'convo-789');\n * stream.on('text-delta', ({ id, delta }) => {\n * // Append delta to message content\n * });\n * stream.connect();\n * ```\n */\nexport class AgentChatStream {\n private agentId: string;\n private conversationId: string;\n private options: Required;\n private eventSource: EventSource | null = null;\n private listeners: Map> = new Map();\n private reconnectTimeout: ReturnType | null = null;\n private messageStates: Map = new Map();\n private currentMessageId: string | null = null;\n private isConnecting = false;\n\n constructor(agentId: string, conversationId: string, options?: StreamOptions) {\n this.agentId = agentId;\n this.conversationId = conversationId;\n this.options = {\n baseUrl: options?.baseUrl ?? '',\n autoReconnect: options?.autoReconnect ?? true,\n reconnectDelay: options?.reconnectDelay ?? 1000,\n onError:\n options?.onError ??\n ((error: Error) => {\n // Log errors by default to help with debugging\n // In production, consumers should provide their own error handler\n console.error('[AgentChatStream] Unhandled error:', error);\n }),\n };\n }\n\n /**\n * Clear all accumulated message states\n * Useful when switching conversations or resetting the stream state\n */\n clearMessages(): void {\n this.messageStates.clear();\n this.currentMessageId = null;\n }\n\n /**\n * Update the conversation ID for this stream\n * Useful when switching to a new conversation while keeping stream open\n * Automatically clears message states when switching conversations\n */\n setConversationId(conversationId: string): void {\n if (this.conversationId === conversationId) {\n return;\n }\n // Track both connected and connecting states to handle race conditions\n const wasConnected = this.isConnected;\n const wasConnecting = this.isConnecting;\n this.disconnect();\n this.conversationId = conversationId;\n // Clear message state when switching conversations\n this.clearMessages();\n // Reconnect if we were connected or in the process of connecting\n if (wasConnected || wasConnecting) {\n this.connect();\n }\n }\n\n /**\n * Opens SSE connection to stream endpoint\n * Stream stays open permanently - never close after first response\n */\n connect(): void {\n // Check isConnecting first since it's set synchronously before any async work\n // This prevents race conditions where connect() is called multiple times\n // before eventSource is assigned\n if (this.isConnecting) {\n return; // Already connecting\n }\n\n if (this.eventSource?.readyState === EventSource.OPEN) {\n return; // Already connected\n }\n\n this.isConnecting = true;\n this.disconnect(); // Clean up any existing connection\n\n const baseUrl = this.options.baseUrl;\n const url = `${baseUrl}/api/taskade/agents/${encodeURIComponent(\n this.agentId,\n )}/public-conversations/${encodeURIComponent(this.conversationId)}/stream`;\n\n // Validate required parameters\n if (!this.agentId || !this.conversationId) {\n const error = new Error(\n `Missing required parameters: agentId=${this.agentId || 'null'}, conversationId=${\n this.conversationId || 'null'\n }`,\n );\n this.options.onError(error);\n this.emit('error', {\n type: 'error',\n errorText: error.message,\n });\n this.isConnecting = false;\n return;\n }\n\n try {\n this.eventSource = new EventSource(url);\n\n this.eventSource.onopen = () => {\n this.isConnecting = false;\n this.emit('open');\n };\n\n this.eventSource.onmessage = (e) => {\n try {\n const data = JSON.parse(e.data);\n const event = StreamEventSchema.parse(data);\n this.handleEvent(event);\n } catch (error) {\n const parseError = error instanceof Error ? error : new Error('Failed to parse event');\n this.options.onError(parseError);\n this.emit('error', {\n type: 'error',\n errorText: `Parse error: ${parseError.message}`,\n });\n }\n };\n\n this.eventSource.onerror = () => {\n this.isConnecting = false;\n\n // EventSource doesn't provide detailed error info, but we can check readyState\n if (this.eventSource?.readyState === EventSource.CLOSED) {\n this.emit('close');\n\n // Auto-reconnect if enabled\n if (this.options.autoReconnect) {\n this.scheduleReconnect();\n }\n } else if (this.eventSource?.readyState === EventSource.CONNECTING) {\n // Still connecting, might be a temporary issue\n // Don't emit error yet, wait for connection to complete or fail\n } else {\n // Connection error\n const error = new Error('EventSource connection error');\n this.options.onError(error);\n this.emit('error', {\n type: 'error',\n errorText: 'Stream connection error',\n });\n }\n };\n } catch (error) {\n // EventSource constructor threw an error (e.g., invalid URL)\n this.isConnecting = false;\n const constructorError =\n error instanceof Error ? error : new Error('Failed to create EventSource');\n this.options.onError(constructorError);\n this.emit('error', {\n type: 'error',\n errorText: `Failed to create connection: ${constructorError.message}`,\n });\n }\n }\n\n /**\n * Closes SSE connection\n */\n disconnect(): void {\n if (this.reconnectTimeout) {\n clearTimeout(this.reconnectTimeout);\n this.reconnectTimeout = null;\n }\n\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n }\n\n this.isConnecting = false;\n }\n\n /**\n * Subscribe to stream events\n *\n * @param event - Event type to listen for\n * @param handler - Callback function\n * @returns Unsubscribe function\n */\n on(event: K, handler: EventMap[K]): () => void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n this.listeners.get(event)!.add(handler);\n\n // Return unsubscribe function\n return () => {\n this.listeners.get(event)?.delete(handler);\n };\n }\n\n /**\n * Unsubscribe from stream events\n */\n off(event: K, handler: EventMap[K]): void {\n this.listeners.get(event)?.delete(handler);\n }\n\n /**\n * Get current connection state\n */\n get isConnected(): boolean {\n // Check readyState directly - more reliable across browsers\n return this.eventSource?.readyState === EventSource.OPEN;\n }\n\n /**\n * Get accumulated message states\n */\n get messages(): Map {\n return new Map(this.messageStates);\n }\n\n /**\n * Get message state by ID\n */\n getMessage(id: string): MessageState | undefined {\n return this.messageStates.get(id);\n }\n\n /**\n * Handle incoming SSE event\n */\n private handleEvent(event: StreamEvent): void {\n // Emit generic event\n this.emit('event', event);\n\n switch (event.type) {\n case 'start':\n this.currentMessageId = event.messageId;\n // Always create/update message on start to ensure it exists\n if (!this.messageStates.has(event.messageId)) {\n this.messageStates.set(event.messageId, {\n id: event.messageId,\n content: '',\n isComplete: false,\n role: 'assistant',\n });\n }\n break;\n\n case 'text-start':\n // event.id is a text segment ID, but text belongs to the current message (from 'start' event)\n // Ensure the current message exists\n if (this.currentMessageId) {\n if (!this.messageStates.has(this.currentMessageId)) {\n this.messageStates.set(this.currentMessageId, {\n id: this.currentMessageId,\n content: '',\n isComplete: false,\n role: 'assistant',\n });\n }\n }\n break;\n\n case 'text-delta': {\n // event.id is a text segment ID, but text belongs to the current message (from 'start' event)\n // Append delta to the current message's content\n if (this.currentMessageId) {\n const existingState = this.messageStates.get(this.currentMessageId);\n if (existingState) {\n // Create new object with updated content to ensure change detection\n const updatedState: MessageState = {\n ...existingState,\n content: existingState.content + event.delta,\n };\n this.messageStates.set(this.currentMessageId, updatedState);\n } else {\n // Create message if it doesn't exist (shouldn't happen, but handle gracefully)\n this.messageStates.set(this.currentMessageId, {\n id: this.currentMessageId,\n content: event.delta,\n isComplete: false,\n role: 'assistant',\n });\n }\n }\n this.emit('text-delta', event);\n break;\n }\n\n case 'text-end': {\n // event.id is a text segment ID, but text belongs to the current message (from 'start' event)\n // Text part is complete, but don't mark entire message as complete (message completes on 'finish' event)\n break;\n }\n\n case 'tool-input-start': {\n // Use messageId from event if available (for child segments), otherwise fall back to currentMessageId\n const messageId =\n 'messageId' in event && event.messageId ? event.messageId : this.currentMessageId;\n const toolMessage = messageId ? this.messageStates.get(messageId) : undefined;\n if (toolMessage) {\n const updatedState: MessageState = {\n ...toolMessage,\n toolCalls: [\n ...(toolMessage.toolCalls ?? []),\n {\n toolCallId: event.toolCallId,\n toolName: event.toolName,\n isComplete: false,\n },\n ],\n };\n this.messageStates.set(toolMessage.id, updatedState);\n }\n break;\n }\n\n case 'tool-input-delta': {\n // Use messageId from event if available (for child segments), otherwise fall back to currentMessageId\n const messageId =\n 'messageId' in event && event.messageId ? event.messageId : this.currentMessageId;\n const toolMessage = messageId ? this.messageStates.get(messageId) : undefined;\n if (toolMessage?.toolCalls) {\n const toolCallIndex = toolMessage.toolCalls.findIndex(\n (tc) => tc.toolCallId === event.toolCallId,\n );\n if (toolCallIndex !== -1) {\n const existingToolCall = toolMessage.toolCalls[toolCallIndex];\n // Accumulate inputTextDelta into input (stored as string during streaming)\n // When tool-input-available arrives, it will replace this with the parsed object\n const currentInput =\n typeof existingToolCall.input === 'string' ? existingToolCall.input : '';\n const updatedState: MessageState = {\n ...toolMessage,\n toolCalls: toolMessage.toolCalls.map((tc, index) =>\n index === toolCallIndex\n ? { ...tc, input: currentInput + event.inputTextDelta }\n : tc,\n ),\n };\n this.messageStates.set(toolMessage.id, updatedState);\n }\n }\n break;\n }\n\n case 'tool-input-available': {\n // Use messageId from event if available (for child segments), otherwise fall back to currentMessageId\n const messageId =\n 'messageId' in event && event.messageId ? event.messageId : this.currentMessageId;\n const toolMessage = messageId ? this.messageStates.get(messageId) : undefined;\n if (toolMessage?.toolCalls) {\n const toolCallIndex = toolMessage.toolCalls.findIndex(\n (tc) => tc.toolCallId === event.toolCallId,\n );\n if (toolCallIndex !== -1) {\n const updatedState: MessageState = {\n ...toolMessage,\n toolCalls: toolMessage.toolCalls.map((tc, index) =>\n index === toolCallIndex ? { ...tc, input: event.input } : tc,\n ),\n };\n this.messageStates.set(toolMessage.id, updatedState);\n }\n }\n break;\n }\n\n case 'tool-output-available': {\n // Use messageId from event if available (for child segments), otherwise fall back to currentMessageId\n const messageId =\n 'messageId' in event && event.messageId ? event.messageId : this.currentMessageId;\n const toolMessage = messageId ? this.messageStates.get(messageId) : undefined;\n if (toolMessage?.toolCalls) {\n const toolCallIndex = toolMessage.toolCalls.findIndex(\n (tc) => tc.toolCallId === event.toolCallId,\n );\n if (toolCallIndex !== -1) {\n const updatedState: MessageState = {\n ...toolMessage,\n toolCalls: toolMessage.toolCalls.map((tc, index) =>\n index === toolCallIndex ? { ...tc, output: event.output } : tc,\n ),\n };\n this.messageStates.set(toolMessage.id, updatedState);\n }\n }\n break;\n }\n\n case 'tool-call-end': {\n // Use messageId from event if available (for child segments), otherwise fall back to currentMessageId\n const messageId =\n 'messageId' in event && event.messageId ? event.messageId : this.currentMessageId;\n const toolMessage = messageId ? this.messageStates.get(messageId) : undefined;\n if (toolMessage?.toolCalls) {\n const toolCallIndex = toolMessage.toolCalls.findIndex(\n (tc) => tc.toolCallId === event.toolCallId,\n );\n if (toolCallIndex !== -1) {\n const updatedState: MessageState = {\n ...toolMessage,\n toolCalls: toolMessage.toolCalls.map((tc, index) =>\n index === toolCallIndex ? { ...tc, isComplete: true } : tc,\n ),\n };\n this.messageStates.set(toolMessage.id, updatedState);\n }\n }\n break;\n }\n\n case 'finish':\n if (this.currentMessageId) {\n const finishedMessage = this.messageStates.get(this.currentMessageId);\n if (finishedMessage) {\n const updatedState: MessageState = {\n ...finishedMessage,\n isComplete: true,\n };\n this.messageStates.set(this.currentMessageId, updatedState);\n }\n }\n this.emit('finish', event);\n break;\n\n case 'error':\n this.emit('error', event);\n this.options.onError(new Error(event.errorText));\n break;\n }\n }\n\n /**\n * Emit event to all listeners\n */\n private emit(event: K, ...args: Parameters): void {\n const handlers = this.listeners.get(event);\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (handler as (...args: any[]) => void)(...args);\n } catch (error) {\n this.options.onError(error instanceof Error ? error : new Error('Handler error'));\n }\n });\n }\n }\n\n /**\n * Schedule reconnection attempt\n */\n private scheduleReconnect(): void {\n if (this.reconnectTimeout) {\n return; // Already scheduled\n }\n\n this.reconnectTimeout = setTimeout(() => {\n this.reconnectTimeout = null;\n if (this.options.autoReconnect) {\n this.connect();\n }\n }, this.options.reconnectDelay);\n }\n}\n" + } + } + } + }, + "genesis.tsx": { + "file": { + "contents": "import type {\n LogFunction,\n LoggerEntryInput,\n SpaceAppLogLifecycleData,\n} from '@taskade/parade-shared';\nimport * as React from 'react';\nimport { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';\n\n// ---------------------------------------------------------------------------\n// Lifecycle logger (injected by Director Preview via window global)\n// ---------------------------------------------------------------------------\n\ninterface GenesisLogger {\n log: LogFunction;\n}\ndeclare global {\n interface Window {\n __TASKADE_APP_LIFECYCLE_LOGGER__?: GenesisLogger;\n }\n}\n\n/**\n * Returns the Taskade lifecycle logger if running inside Director Preview.\n * The logger is injected into `window.__TASKADE_APP_LIFECYCLE_LOGGER__` by\n * the preview inject script. Returns `null` in published mode.\n */\nexport function getGenesisAppLifecycleLogger(): GenesisLogger | null {\n if (typeof window === 'undefined') {\n return null;\n }\n return window.__TASKADE_APP_LIFECYCLE_LOGGER__ ?? null;\n}\n\n/**\n * Report a runtime error to the parent frame via postMessage.\n * In preview mode this triggers the \"Fix with AI\" popup.\n * In published mode this is a no-op (logger not available).\n */\nexport function reportGenesisError(\n code: 'error.boundary',\n error: unknown,\n componentStack?: string | null,\n) {\n getGenesisAppLifecycleLogger()?.log({\n level: 'error',\n message: 'Runtime Error',\n data: {\n code,\n message: error instanceof Error ? error.message : String(error),\n stack: [error instanceof Error ? error.stack : undefined, componentStack]\n .filter(Boolean)\n .join('\\n'),\n } satisfies SpaceAppLogLifecycleData,\n } satisfies LoggerEntryInput);\n}\n\n// ---------------------------------------------------------------------------\n// Error boundary fallback UI (inline styles for resilience)\n// ---------------------------------------------------------------------------\n\n/** Fallback UI shown when the ErrorBoundary catches a render error. */\nfunction ErrorFallback({ error }: { error: Error }) {\n return (\n \n

Something went wrong

\n

\n The app encountered an error. Try refreshing the page.\n

\n \n {String(error)}\n \n \n );\n}\n\n// ---------------------------------------------------------------------------\n// GenesisRoot\n// ---------------------------------------------------------------------------\n\n/**\n * Genesis root wrapper — must be provided by the base template, not LLM-generated.\n *\n * Wraps children in an ErrorBoundary that:\n * 1. Catches render-phase errors and shows {@link ErrorFallback} instead of a blank page.\n * 2. Reports the error to the parent frame via {@link reportGenesisError} so\n * Director Preview can show the \"Fix with AI\" popup.\n */\nexport function GenesisRoot({ children }: { children: React.ReactNode }) {\n return (\n {\n console.error('[Genesis] Uncaught render error:', error, info);\n reportGenesisError('error.boundary', error, info.componentStack);\n }}\n >\n {children}\n \n );\n}\n" + } + }, + "theme-bridge.ts": { + "file": { + "contents": "/**\n * Listen for TASKADE_THEME_UPDATE messages from the parent Taskade editor\n * and apply CSS variable overrides to the document root in real-time.\n * Also responds to TASKADE_THEME_READ requests with current CSS variable values.\n *\n * Theme overrides are injected via a