privateboard 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2060 -183
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/public/agent-overlay.js +3 -3
- package/public/agent-profile.css +5 -4
- package/public/agent-profile.js +18 -2
- package/public/app.js +513 -26
- package/public/avatar-skill.js +6 -9
- package/public/home.html +1750 -0
- package/public/{prototype-dashboard.html → index.html} +129 -116
- package/public/onboarding.js +4 -4
- package/public/quote-cta.css +225 -0
- package/public/quote-cta.js +355 -0
- package/public/report/spines/a16z-thesis.css +33 -1
- package/public/report/spines/anthropic-essay.css +54 -1
- package/public/report/spines/boardroom-dark.css +18 -2
- package/public/report/spines/gartner-note.css +47 -0
- package/public/report/spines/mckinsey-deck.css +38 -1
- package/public/report/spines/openai-paper.css +37 -1
- package/public/report.html +361 -6
- package/public/room-settings.css +6 -4
- package/public/room-settings.js +8 -5
- package/public/user-settings.css +18 -0
- package/public/user-settings.js +31 -2
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
9
11
|
|
|
10
12
|
// src/utils/paths.ts
|
|
11
13
|
import { existsSync, mkdirSync } from "fs";
|
|
@@ -48,30 +50,73 @@ function publicDir() {
|
|
|
48
50
|
}
|
|
49
51
|
return candidates[0];
|
|
50
52
|
}
|
|
53
|
+
var init_paths = __esm({
|
|
54
|
+
"src/utils/paths.ts"() {
|
|
55
|
+
"use strict";
|
|
56
|
+
}
|
|
57
|
+
});
|
|
51
58
|
|
|
52
59
|
// src/storage/migrations/001_init.sql
|
|
53
|
-
var init_default
|
|
60
|
+
var init_default;
|
|
61
|
+
var init_init = __esm({
|
|
62
|
+
"src/storage/migrations/001_init.sql"() {
|
|
63
|
+
init_default = "-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- Boardroom \xB7 initial schema\n-- All tables for the v1 MVP. memory_* and knowledge_* are deferred\n-- (post-MVP) and will land in a later migration.\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n-- User preferences \xB7 single-row table.\nCREATE TABLE prefs (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n name TEXT NOT NULL DEFAULT 'You',\n intro TEXT NOT NULL DEFAULT '',\n avatar_seed TEXT,\n theme TEXT NOT NULL DEFAULT 'regent',\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\nINSERT INTO prefs (id, name, intro, theme, created_at, updated_at)\nVALUES (1, 'You', '', 'regent',\n CAST(strftime('%s','now') AS INTEGER) * 1000,\n CAST(strftime('%s','now') AS INTEGER) * 1000);\n\n-- LLM provider API keys \xB7 stored AES-GCM encrypted.\nCREATE TABLE provider_keys (\n provider TEXT PRIMARY KEY,\n key_blob BLOB NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\n-- Directors / Agents.\nCREATE TABLE agents (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n handle TEXT UNIQUE NOT NULL,\n role_tag TEXT NOT NULL DEFAULT '', -- e.g. 'skeptic', 'physicist'\n bio TEXT NOT NULL DEFAULT '',\n cover_quote TEXT,\n instruction TEXT NOT NULL,\n model_v TEXT NOT NULL, -- 'sonnet-4-6' | 'gpt-5' | ...\n avatar_path TEXT NOT NULL,\n is_pinned INTEGER NOT NULL DEFAULT 0,\n is_seed INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\n-- Rooms (boardroom sessions).\nCREATE TABLE rooms (\n id TEXT PRIMARY KEY,\n number INTEGER NOT NULL UNIQUE,\n name TEXT NOT NULL,\n subject TEXT NOT NULL,\n mode TEXT NOT NULL DEFAULT 'discovery', -- discovery|constructive|adversarial\n status TEXT NOT NULL DEFAULT 'live', -- live|adjourned\n brief_style TEXT,\n created_at INTEGER NOT NULL,\n adjourned_at INTEGER\n);\n\n-- Room \u2194 Agent membership (M:N).\nCREATE TABLE room_members (\n room_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n position INTEGER NOT NULL, -- speaking order in round-robin\n joined_at INTEGER NOT NULL,\n PRIMARY KEY (room_id, agent_id),\n FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE,\n FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE\n);\n\nCREATE INDEX idx_room_members_room ON room_members(room_id, position);\n\n-- Messages (user, agents, system).\nCREATE TABLE messages (\n id TEXT PRIMARY KEY,\n room_id TEXT NOT NULL,\n author_kind TEXT NOT NULL, -- 'agent' | 'user' | 'system'\n author_id TEXT, -- agent.id or NULL\n reply_to_id TEXT,\n body TEXT NOT NULL,\n meta_json TEXT, -- JSON: mentions[], speakerStatus, ...\n round_num INTEGER NOT NULL DEFAULT 1,\n created_at INTEGER NOT NULL,\n FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE,\n FOREIGN KEY (reply_to_id) REFERENCES messages(id) ON DELETE SET NULL\n);\n\nCREATE INDEX idx_messages_room ON messages(room_id, created_at);\n\n-- Configuration / lifecycle events (room-opened, room-adjourned, member-add, ...).\nCREATE TABLE config_events (\n id TEXT PRIMARY KEY,\n room_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n payload TEXT, -- JSON\n actor_kind TEXT NOT NULL, -- 'user' | 'system'\n created_at INTEGER NOT NULL,\n FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE\n);\n\n-- Briefs \xB7 adjourn product.\nCREATE TABLE briefs (\n id TEXT PRIMARY KEY,\n room_id TEXT NOT NULL UNIQUE,\n style TEXT NOT NULL,\n title TEXT NOT NULL,\n body_md TEXT NOT NULL,\n body_json TEXT,\n created_at INTEGER NOT NULL,\n FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE\n);\n";
|
|
64
|
+
}
|
|
65
|
+
});
|
|
54
66
|
|
|
55
67
|
// src/storage/migrations/002_default_opus.sql
|
|
56
|
-
var default_opus_default
|
|
68
|
+
var default_opus_default;
|
|
69
|
+
var init_default_opus = __esm({
|
|
70
|
+
"src/storage/migrations/002_default_opus.sql"() {
|
|
71
|
+
default_opus_default = "-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- Default seeded directors switch to Opus 4.7.\n-- Only agents that are still on the prior default (`sonnet-4-6`) AND were\n-- originally seeded by Boardroom (is_seed = 1) get bumped \u2014 any agent the\n-- user has explicitly customized is left alone.\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nUPDATE agents\nSET model_v = 'opus-4-7',\n updated_at = CAST(strftime('%s','now') AS INTEGER) * 1000\nWHERE is_seed = 1 AND model_v = 'sonnet-4-6';\n";
|
|
72
|
+
}
|
|
73
|
+
});
|
|
57
74
|
|
|
58
75
|
// src/storage/migrations/003_paused_at.sql
|
|
59
|
-
var paused_at_default
|
|
76
|
+
var paused_at_default;
|
|
77
|
+
var init_paused_at = __esm({
|
|
78
|
+
"src/storage/migrations/003_paused_at.sql"() {
|
|
79
|
+
paused_at_default = "-- Add paused_at column for the paused room status.\nALTER TABLE rooms ADD COLUMN paused_at INTEGER;\n";
|
|
80
|
+
}
|
|
81
|
+
});
|
|
60
82
|
|
|
61
83
|
// src/storage/migrations/004_room_intensity.sql
|
|
62
|
-
var room_intensity_default
|
|
84
|
+
var room_intensity_default;
|
|
85
|
+
var init_room_intensity = __esm({
|
|
86
|
+
"src/storage/migrations/004_room_intensity.sql"() {
|
|
87
|
+
room_intensity_default = "-- Add intensity column for the calm | sharp | brutal slider on convene.\n-- 'sharp' is the prototype default.\nALTER TABLE rooms ADD COLUMN intensity TEXT NOT NULL DEFAULT 'sharp';\n";
|
|
88
|
+
}
|
|
89
|
+
});
|
|
63
90
|
|
|
64
91
|
// src/storage/migrations/005_chair.sql
|
|
65
|
-
var chair_default
|
|
92
|
+
var chair_default;
|
|
93
|
+
var init_chair = __esm({
|
|
94
|
+
"src/storage/migrations/005_chair.sql"() {
|
|
95
|
+
chair_default = "-- Adds the Chair role: a special always-on agent that opens with a\n-- clarification, ends each round with key-points + a vote prompt,\n-- announces config changes, and writes the closing brief.\n--\n-- The chair is modeled as a regular agent with role_kind = 'moderator'\n-- so we can reuse the message + streaming pipeline.\n\nALTER TABLE agents ADD COLUMN role_kind TEXT NOT NULL DEFAULT 'director';\n\n-- Key points: the chair generates 3 of these at the end of each round.\n-- Users vote up/down; voted points feed back into the next director\n-- system prompt as user-interest signals.\nCREATE TABLE key_points (\n id TEXT PRIMARY KEY,\n room_id TEXT NOT NULL,\n message_id TEXT, -- round-end chair message that introduced these\n round_num INTEGER NOT NULL,\n body TEXT NOT NULL,\n vote TEXT, -- 'up' | 'down' | NULL\n position INTEGER NOT NULL, -- 0/1/2 ordering\n created_at INTEGER NOT NULL,\n voted_at INTEGER,\n FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE\n);\nCREATE INDEX idx_key_points_room ON key_points(room_id);\nCREATE INDEX idx_key_points_round ON key_points(room_id, round_num);\n\n-- Soft pause flag: set when the chair finishes a round-end message.\n-- The orchestrator won't dispatch the next director until the user\n-- clicks Continue (or Adjourn).\nALTER TABLE rooms ADD COLUMN awaiting_continue INTEGER NOT NULL DEFAULT 0;\n";
|
|
96
|
+
}
|
|
97
|
+
});
|
|
66
98
|
|
|
67
99
|
// src/storage/migrations/006_awaiting_clarify.sql
|
|
68
|
-
var awaiting_clarify_default
|
|
100
|
+
var awaiting_clarify_default;
|
|
101
|
+
var init_awaiting_clarify = __esm({
|
|
102
|
+
"src/storage/migrations/006_awaiting_clarify.sql"() {
|
|
103
|
+
awaiting_clarify_default = "-- Soft pause for the chair's opening clarification phase. While set, the\n-- orchestrator routes user replies through the chair instead of the\n-- director queue, so the chair can ask multiple follow-up questions\n-- before releasing the directors. Capped at 3 chair turns by the\n-- orchestrator to prevent unbounded loops.\nALTER TABLE rooms ADD COLUMN awaiting_clarify INTEGER NOT NULL DEFAULT 0;\n";
|
|
104
|
+
}
|
|
105
|
+
});
|
|
69
106
|
|
|
70
107
|
// src/storage/migrations/007_agent_tokens.sql
|
|
71
|
-
var agent_tokens_default
|
|
108
|
+
var agent_tokens_default;
|
|
109
|
+
var init_agent_tokens = __esm({
|
|
110
|
+
"src/storage/migrations/007_agent_tokens.sql"() {
|
|
111
|
+
agent_tokens_default = "-- Track cumulative tokens consumed by each agent across every LLM\n-- turn they've been the speaker for. Counted from the Vercel AI SDK's\n-- usage report when each director / chair stream finishes \u2014 see\n-- src/orchestrator/room.ts. Rooms-joined and rounds-spoken are\n-- derived on read (room_members count, distinct round_num count).\nALTER TABLE agents ADD COLUMN tokens_consumed INTEGER NOT NULL DEFAULT 0;\n";
|
|
112
|
+
}
|
|
113
|
+
});
|
|
72
114
|
|
|
73
115
|
// src/storage/migrations/008_agent_memories.sql
|
|
74
|
-
var agent_memories_default
|
|
116
|
+
var agent_memories_default;
|
|
117
|
+
var init_agent_memories = __esm({
|
|
118
|
+
"src/storage/migrations/008_agent_memories.sql"() {
|
|
119
|
+
agent_memories_default = `-- Long-term agent memory \xB7 per-agent notes about the USER that
|
|
75
120
|
-- accumulate across rooms. Each agent (directors + chair) keeps an
|
|
76
121
|
-- independent set so multi-perspective lenses stay distinct (Skeptic
|
|
77
122
|
-- vs Empath remember different things). Read back into every prompt
|
|
@@ -116,55 +161,146 @@ CREATE INDEX IF NOT EXISTS idx_agent_memories_room ON agent_memories(source_room
|
|
|
116
161
|
-- Default 0 (writes by default, per the v1 product decision).
|
|
117
162
|
ALTER TABLE rooms ADD COLUMN incognito INTEGER NOT NULL DEFAULT 0;
|
|
118
163
|
`;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
119
166
|
|
|
120
167
|
// src/storage/migrations/009_skills.sql
|
|
121
|
-
var skills_default
|
|
168
|
+
var skills_default;
|
|
169
|
+
var init_skills = __esm({
|
|
170
|
+
"src/storage/migrations/009_skills.sql"() {
|
|
171
|
+
skills_default = "-- Per-agent skill catalog. Skills are installed by uploading a Skill.md\n-- file with YAML frontmatter (name, slug, description, when_to_use,\n-- ability deltas, tips) plus a free markdown body. The body is only\n-- injected into the agent's Pass-2 system prompt when the Pass-1 router\n-- picks the skill for that turn (Claude Code-style progressive\n-- disclosure). Per-agent storage in v1 \u2014 no shared library.\nCREATE TABLE IF NOT EXISTS agent_skills (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n slug TEXT NOT NULL,\n name TEXT NOT NULL,\n version TEXT NOT NULL DEFAULT '1.0',\n description TEXT NOT NULL,\n when_to_use TEXT NOT NULL,\n body_md TEXT NOT NULL,\n ability_json TEXT NOT NULL DEFAULT '{}',\n tips_json TEXT NOT NULL DEFAULT '[]',\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n UNIQUE (agent_id, slug),\n FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE\n);\nCREATE INDEX IF NOT EXISTS idx_agent_skills_agent ON agent_skills (agent_id);\n";
|
|
172
|
+
}
|
|
173
|
+
});
|
|
122
174
|
|
|
123
175
|
// src/storage/migrations/010_briefs_multi.sql
|
|
124
|
-
var briefs_multi_default
|
|
176
|
+
var briefs_multi_default;
|
|
177
|
+
var init_briefs_multi = __esm({
|
|
178
|
+
"src/storage/migrations/010_briefs_multi.sql"() {
|
|
179
|
+
briefs_multi_default = "-- Multiple briefs per room. Previously the briefs table had `room_id\n-- NOT NULL UNIQUE`, enforcing a single deliverable per room with a\n-- regenerate-overwrites-original UX. The new model preserves history:\n-- the first brief is generated on adjourn; each \"Add a perspective\"\n-- regeneration appends a new row. The user can switch between briefs\n-- via a tab strip in the brief card and the report viewer.\n--\n-- Also adds an optional `supplement` column to record the user's input\n-- when a brief was a regeneration (the first brief has supplement = NULL).\n--\n-- SQLite can't DROP a UNIQUE constraint in place, so we rebuild the\n-- table. The migration runner already wraps each migration in a\n-- transaction, so no explicit BEGIN/COMMIT here.\n\nCREATE TABLE briefs_new (\n id TEXT PRIMARY KEY,\n room_id TEXT NOT NULL,\n style TEXT NOT NULL,\n title TEXT NOT NULL,\n body_md TEXT NOT NULL,\n body_json TEXT,\n supplement TEXT,\n created_at INTEGER NOT NULL,\n FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE\n);\n\nINSERT INTO briefs_new (id, room_id, style, title, body_md, body_json, supplement, created_at)\nSELECT id, room_id, style, title, body_md, body_json, NULL, created_at FROM briefs;\n\nDROP TABLE briefs;\nALTER TABLE briefs_new RENAME TO briefs;\n\n-- Fast lookup of the latest brief for a room (newest first).\nCREATE INDEX idx_briefs_room_created ON briefs(room_id, created_at DESC);\n";
|
|
180
|
+
}
|
|
181
|
+
});
|
|
125
182
|
|
|
126
183
|
// src/storage/migrations/011_agent_ability.sql
|
|
127
|
-
var agent_ability_default
|
|
184
|
+
var agent_ability_default;
|
|
185
|
+
var init_agent_ability = __esm({
|
|
186
|
+
"src/storage/migrations/011_agent_ability.sql"() {
|
|
187
|
+
agent_ability_default = '-- Per-agent base ability profile (radar chart).\n-- Stored as JSON: {"dissent": N, "pattern_recall": N, "rigor": N,\n-- "empathy": N, "narrative": N, "decisiveness": N} where each N is in\n-- 0..10. NULL means "no profile set" \u2014 radar falls back to flat 5/all\n-- (legacy behaviour). New AI-generated directors carry a profile\n-- inferred from their description so the radar reflects personality.\nALTER TABLE agents ADD COLUMN ability_json TEXT;\n';
|
|
188
|
+
}
|
|
189
|
+
});
|
|
128
190
|
|
|
129
191
|
// src/storage/migrations/012_brief_composer.sql
|
|
130
|
-
var brief_composer_default
|
|
192
|
+
var brief_composer_default;
|
|
193
|
+
var init_brief_composer = __esm({
|
|
194
|
+
"src/storage/migrations/012_brief_composer.sql"() {
|
|
195
|
+
brief_composer_default = "-- Report composer \xB7 capture the chair's component picks alongside each\n-- brief. Today every report is a fixed McKinsey-style 12-section deck;\n-- the new flow has a Stage 1.5 composer that picks (a) a style spine\n-- and (b) a subset of components based on the room's subject and the\n-- per-director signals.\n--\n-- Columns:\n-- spine Renderer key. v1 ships with `boardroom-dark`\n-- only; later releases add `a16z-thesis`,\n-- `anthropic-essay`, `gartner-note`,\n-- `mckinsey-deck`, `openai-paper`. Default keeps\n-- legacy briefs renderable.\n-- components_json JSON array of { kind, order } objects. Empty\n-- array means \"legacy / all 12 components\" \u2014 the\n-- renderer falls back to today's static layout.\n-- composer_rationale The composer's one-line explanation, surfaced\n-- on hover of the SPINE tag in the UI. NULL when\n-- the composer was bypassed.\n-- subject_type Coarse classification the composer assigned\n-- (e.g. `investment-judgement`, `philosophical`,\n-- `option-comparison`). Used by analytics + by\n-- future \"regenerate as\" presets. NULL for legacy.\n\nALTER TABLE briefs ADD COLUMN spine TEXT NOT NULL DEFAULT 'boardroom-dark';\nALTER TABLE briefs ADD COLUMN components_json TEXT NOT NULL DEFAULT '[]';\nALTER TABLE briefs ADD COLUMN composer_rationale TEXT;\nALTER TABLE briefs ADD COLUMN subject_type TEXT;\n";
|
|
196
|
+
}
|
|
197
|
+
});
|
|
131
198
|
|
|
132
199
|
// src/storage/migrations/013_retired_token_usage.sql
|
|
133
|
-
var retired_token_usage_default
|
|
200
|
+
var retired_token_usage_default;
|
|
201
|
+
var init_retired_token_usage = __esm({
|
|
202
|
+
"src/storage/migrations/013_retired_token_usage.sql"() {
|
|
203
|
+
retired_token_usage_default = '-- Token usage rollup for deleted custom agents.\n--\n-- When a user deletes a custom agent, the row is removed from the\n-- `agents` table \u2014 but the LLM calls that agent already made were real:\n-- the user already paid for those tokens, and the Usage panel\'s totals\n-- become a lie if they silently disappear.\n--\n-- Before each `DELETE FROM agents`, the storage layer transfers the\n-- agent\'s `tokens_consumed` into this rollup table, keyed by model.\n-- Per-agent identity is intentionally lost (deletion = "make it go\n-- away"), but the model rollup and the grand total stay honest.\n--\n-- Columns:\n-- model_v The model the retired agents used. Multiple deleted\n-- agents that shared a model collapse into one row.\n-- tokens Cumulative tokens billed against retired agents on\n-- this model.\n-- agents Number of agents folded into this row (used by the\n-- UI to surface "+ N retired agents \xB7 X tokens").\n-- updated_at Last time a deletion landed here.\n\nCREATE TABLE retired_token_usage (\n model_v TEXT PRIMARY KEY,\n tokens INTEGER NOT NULL DEFAULT 0,\n agents INTEGER NOT NULL DEFAULT 0,\n updated_at INTEGER NOT NULL\n);\n';
|
|
204
|
+
}
|
|
205
|
+
});
|
|
134
206
|
|
|
135
207
|
// src/storage/migrations/014_agent_web_search.sql
|
|
136
|
-
var agent_web_search_default
|
|
208
|
+
var agent_web_search_default;
|
|
209
|
+
var init_agent_web_search = __esm({
|
|
210
|
+
"src/storage/migrations/014_agent_web_search.sql"() {
|
|
211
|
+
agent_web_search_default = "-- Per-agent web-search toggle.\n--\n-- Default 1 (enabled). The actual gate is the user-supplied Brave\n-- Search API key in `provider_keys` \u2014 without that key, no agent\n-- can search regardless of this flag. With the key set, every agent\n-- is search-capable by default; the user can switch any individual\n-- director off via the agent profile page (e.g. to keep one\n-- director on first-principles only).\n\nALTER TABLE agents ADD COLUMN web_search_enabled INTEGER NOT NULL DEFAULT 1;\n";
|
|
212
|
+
}
|
|
213
|
+
});
|
|
137
214
|
|
|
138
215
|
// src/storage/migrations/015_web_search_default_off.sql
|
|
139
|
-
var web_search_default_off_default
|
|
216
|
+
var web_search_default_off_default;
|
|
217
|
+
var init_web_search_default_off = __esm({
|
|
218
|
+
"src/storage/migrations/015_web_search_default_off.sql"() {
|
|
219
|
+
web_search_default_off_default = "-- Reset every agent's Web Search flag to OFF.\n--\n-- Migration 014 introduced the column with `DEFAULT 1` because the\n-- gating model at that point was \"global key gates everything; per-\n-- agent toggle is a fine-tune\". After the first round of UX feedback\n-- we flipped the philosophy: the default experience should be no\n-- search, with the user opting in TWICE \u2014 once globally (configuring\n-- the Brave key in Preferences) and once per-agent (flipping the\n-- toggle on the director's profile).\n--\n-- This migration only resets the row values. The column DEFAULT in\n-- the schema stays at 1 because changing it requires a SQLite full\n-- table rebuild; the application layer (`insertAgent`) explicitly\n-- writes 0 for newly created agents going forward.\n\nUPDATE agents SET web_search_enabled = 0;\n";
|
|
220
|
+
}
|
|
221
|
+
});
|
|
140
222
|
|
|
141
223
|
// src/storage/migrations/016_prefs_default_model.sql
|
|
142
|
-
var prefs_default_model_default
|
|
224
|
+
var prefs_default_model_default;
|
|
225
|
+
var init_prefs_default_model = __esm({
|
|
226
|
+
"src/storage/migrations/016_prefs_default_model.sql"() {
|
|
227
|
+
prefs_default_model_default = "-- Global default model \xB7 the modelV new agents inherit at create time\n-- and the runtime fallback when an agent's stored modelV becomes\n-- unreachable (user revoked the underlying API key). NULL on legacy\n-- rows; the availability layer's `defaultModelFor()` helper picks a\n-- sensible value at first use and the prefs row gets back-filled.\nALTER TABLE prefs ADD COLUMN default_model_v TEXT;\n";
|
|
228
|
+
}
|
|
229
|
+
});
|
|
143
230
|
|
|
144
231
|
// src/storage/migrations/017_agent_carrier_pref.sql
|
|
145
|
-
var agent_carrier_pref_default
|
|
232
|
+
var agent_carrier_pref_default;
|
|
233
|
+
var init_agent_carrier_pref = __esm({
|
|
234
|
+
"src/storage/migrations/017_agent_carrier_pref.sql"() {
|
|
235
|
+
agent_carrier_pref_default = "-- Per-agent carrier preference.\n--\n-- The same modelV (e.g. \"gpt-5-5\") may be reachable via multiple\n-- carriers when the user has more than one provider key configured \u2014\n-- e.g. OpenRouter (`google/gpt-5.5`) vs OpenAI direct (`gpt-5.5`).\n-- The adapter's default precedence rules pick one carrier; this column\n-- lets each agent override that pick. NULL means \"use the default\n-- precedence\" (the prior behavior). Values: 'openrouter' | 'anthropic'\n-- | 'openai' | 'google' | 'xai'. Validation lives in the application\n-- layer; SQLite isn't asked to enforce the enum.\nALTER TABLE agents ADD COLUMN carrier_pref TEXT;\n";
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// src/storage/migrations/018_brief_house_style.sql
|
|
240
|
+
var brief_house_style_default;
|
|
241
|
+
var init_brief_house_style = __esm({
|
|
242
|
+
"src/storage/migrations/018_brief_house_style.sql"() {
|
|
243
|
+
brief_house_style_default = '-- House style preset \xB7 captures the editorial / structural register the\n-- composer picked for this brief, alongside the existing spine (visual\n-- CSS) and components_json (per-component picks). The same components\n-- can render under different house styles with different section\n-- vocabulary and voice \u2014 `sequoia-memo` and `stanford-research` share\n-- the recommendations component but rename it to "## What Would Make\n-- This Work" vs "## Future Work" and shift voice from declarative to\n-- hedged.\n--\n-- Default keeps legacy briefs renderable (`boardroom-default` is the\n-- existing behaviour: default English labels, neutral voice).\nALTER TABLE briefs ADD COLUMN house_style TEXT NOT NULL DEFAULT \'boardroom-default\';\n';
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// src/storage/migrations/019_room_summaries.sql
|
|
248
|
+
var room_summaries_default;
|
|
249
|
+
var init_room_summaries = __esm({
|
|
250
|
+
"src/storage/migrations/019_room_summaries.sql"() {
|
|
251
|
+
room_summaries_default = '-- Hierarchical summarization \xB7 per-room cached summaries used by the\n-- director-context assembler to stay under context-window pressure on\n-- long rooms without dropping continuity.\n--\n-- Tier model:\n-- level=1 \xB7 per-round narrative, generated once when the round drops\n-- out of the L0 (verbatim) window. round_num identifies which\n-- round this summary represents.\n-- level=2 \xB7 rolling consolidated summary of all rounds older than the\n-- L1 window. start_round + end_round describe the range it\n-- covers; regenerated when a new L1 row gets folded in.\n--\n-- A row\'s `body` is plain narrative text (not key-points, not JSON);\n-- the assembler concatenates it directly into the system prompt under\n-- a "// EARLIER IN THIS ROOM" header.\n--\n-- `source_hash` lets future invalidation logic detect whether the\n-- inputs changed (e.g. a new message inserted into an old round); for\n-- v1 we only generate forward, so the hash is informational.\nCREATE TABLE IF NOT EXISTS room_summaries (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n room_id TEXT NOT NULL,\n level INTEGER NOT NULL CHECK (level IN (1, 2)),\n round_num INTEGER, -- L1: the round this summary represents; L2: NULL\n start_round INTEGER, -- L2: oldest round covered; L1: same as round_num\n end_round INTEGER, -- L2: newest round covered; L1: same as round_num\n body TEXT NOT NULL,\n model_v TEXT, -- which utility model generated this row\n source_hash TEXT, -- hash of the input chunk(s); v2 invalidation hook\n created_at INTEGER NOT NULL,\n FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE\n);\n\n-- One L1 row per (room, round). One L2 row per room at any moment.\nCREATE UNIQUE INDEX IF NOT EXISTS idx_room_summaries_l1 ON room_summaries(room_id, level, round_num) WHERE level = 1;\nCREATE UNIQUE INDEX IF NOT EXISTS idx_room_summaries_l2 ON room_summaries(room_id, level) WHERE level = 2;\n\n-- Read path: assembler queries by room + level.\nCREATE INDEX IF NOT EXISTS idx_room_summaries_room_level ON room_summaries(room_id, level);\n';
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// src/storage/migrations/020_room_followup.sql
|
|
256
|
+
var room_followup_default;
|
|
257
|
+
var init_room_followup = __esm({
|
|
258
|
+
"src/storage/migrations/020_room_followup.sql"() {
|
|
259
|
+
room_followup_default = `-- Follow-up rooms \xB7 "Convene a follow-up" feature
|
|
260
|
+
--
|
|
261
|
+
-- A room can be started as a continuation of a prior adjourned room.
|
|
262
|
+
-- Two reference columns are added:
|
|
263
|
+
--
|
|
264
|
+
-- parent_room_id \xB7 the prior room being followed up. Both rooms
|
|
265
|
+
-- remain independent (own messages, own briefs);
|
|
266
|
+
-- the link is purely a reference for navigation
|
|
267
|
+
-- and for context injection at director-prompt
|
|
268
|
+
-- build time.
|
|
269
|
+
-- parent_brief_id \xB7 which specific brief the follow-up is scoped
|
|
270
|
+
-- to. A parent room can carry multiple briefs
|
|
271
|
+
-- (regenerations); the orchestrator uses this id
|
|
272
|
+
-- to load exactly that brief's markdown + signal
|
|
273
|
+
-- attribution into the new room's director system
|
|
274
|
+
-- prompts.
|
|
275
|
+
--
|
|
276
|
+
-- Both nullable so existing standalone rooms keep working unchanged.
|
|
277
|
+
-- No FK constraints \u2014 parent rooms can be deleted independently
|
|
278
|
+
-- (the follow-up degrades gracefully into "orphan follow-up" with
|
|
279
|
+
-- the link nulled at read time if the parent disappears).
|
|
280
|
+
|
|
281
|
+
ALTER TABLE rooms ADD COLUMN parent_room_id TEXT NULL;
|
|
282
|
+
ALTER TABLE rooms ADD COLUMN parent_brief_id TEXT NULL;
|
|
283
|
+
|
|
284
|
+
-- Persist Stage-1 per-director signals on the brief row so a
|
|
285
|
+
-- follow-up room can re-use the prior session's named-by-lens
|
|
286
|
+
-- observations without re-running the haiku extract pass. The
|
|
287
|
+
-- field carries a JSON array of { directorId, directorName,
|
|
288
|
+
-- signals: [{ text, lens, sources }] } \u2014 same shape the brief
|
|
289
|
+
-- orchestrator's Stage 2 receives. NULL on legacy briefs (the
|
|
290
|
+
-- follow-up reader falls back to brief markdown alone).
|
|
291
|
+
ALTER TABLE briefs ADD COLUMN signals_json TEXT NULL;
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
146
295
|
|
|
147
296
|
// src/storage/db.ts
|
|
148
|
-
var
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
{ name: "007_agent_tokens.sql", sql: agent_tokens_default },
|
|
156
|
-
{ name: "008_agent_memories.sql", sql: agent_memories_default },
|
|
157
|
-
{ name: "009_skills.sql", sql: skills_default },
|
|
158
|
-
{ name: "010_briefs_multi.sql", sql: briefs_multi_default },
|
|
159
|
-
{ name: "011_agent_ability.sql", sql: agent_ability_default },
|
|
160
|
-
{ name: "012_brief_composer.sql", sql: brief_composer_default },
|
|
161
|
-
{ name: "013_retired_token_usage.sql", sql: retired_token_usage_default },
|
|
162
|
-
{ name: "014_agent_web_search.sql", sql: agent_web_search_default },
|
|
163
|
-
{ name: "015_web_search_default_off.sql", sql: web_search_default_off_default },
|
|
164
|
-
{ name: "016_prefs_default_model.sql", sql: prefs_default_model_default },
|
|
165
|
-
{ name: "017_agent_carrier_pref.sql", sql: agent_carrier_pref_default }
|
|
166
|
-
];
|
|
167
|
-
var _db = null;
|
|
297
|
+
var db_exports = {};
|
|
298
|
+
__export(db_exports, {
|
|
299
|
+
closeDb: () => closeDb,
|
|
300
|
+
getDb: () => getDb,
|
|
301
|
+
runMigrations: () => runMigrations
|
|
302
|
+
});
|
|
303
|
+
import Database from "better-sqlite3";
|
|
168
304
|
function getDb() {
|
|
169
305
|
if (_db) return _db;
|
|
170
306
|
const file = statePath();
|
|
@@ -175,6 +311,16 @@ function getDb() {
|
|
|
175
311
|
_db = db;
|
|
176
312
|
return db;
|
|
177
313
|
}
|
|
314
|
+
function closeDb() {
|
|
315
|
+
if (_db) {
|
|
316
|
+
try {
|
|
317
|
+
_db.pragma("wal_checkpoint(TRUNCATE)");
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
_db.close();
|
|
321
|
+
_db = null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
178
324
|
function runMigrations() {
|
|
179
325
|
const db = getDb();
|
|
180
326
|
db.exec(`
|
|
@@ -199,8 +345,63 @@ function runMigrations() {
|
|
|
199
345
|
}
|
|
200
346
|
return { applied };
|
|
201
347
|
}
|
|
348
|
+
var MIGRATIONS, _db;
|
|
349
|
+
var init_db = __esm({
|
|
350
|
+
"src/storage/db.ts"() {
|
|
351
|
+
"use strict";
|
|
352
|
+
init_paths();
|
|
353
|
+
init_init();
|
|
354
|
+
init_default_opus();
|
|
355
|
+
init_paused_at();
|
|
356
|
+
init_room_intensity();
|
|
357
|
+
init_chair();
|
|
358
|
+
init_awaiting_clarify();
|
|
359
|
+
init_agent_tokens();
|
|
360
|
+
init_agent_memories();
|
|
361
|
+
init_skills();
|
|
362
|
+
init_briefs_multi();
|
|
363
|
+
init_agent_ability();
|
|
364
|
+
init_brief_composer();
|
|
365
|
+
init_retired_token_usage();
|
|
366
|
+
init_agent_web_search();
|
|
367
|
+
init_web_search_default_off();
|
|
368
|
+
init_prefs_default_model();
|
|
369
|
+
init_agent_carrier_pref();
|
|
370
|
+
init_brief_house_style();
|
|
371
|
+
init_room_summaries();
|
|
372
|
+
init_room_followup();
|
|
373
|
+
MIGRATIONS = [
|
|
374
|
+
{ name: "001_init.sql", sql: init_default },
|
|
375
|
+
{ name: "002_default_opus.sql", sql: default_opus_default },
|
|
376
|
+
{ name: "003_paused_at.sql", sql: paused_at_default },
|
|
377
|
+
{ name: "004_room_intensity.sql", sql: room_intensity_default },
|
|
378
|
+
{ name: "005_chair.sql", sql: chair_default },
|
|
379
|
+
{ name: "006_awaiting_clarify.sql", sql: awaiting_clarify_default },
|
|
380
|
+
{ name: "007_agent_tokens.sql", sql: agent_tokens_default },
|
|
381
|
+
{ name: "008_agent_memories.sql", sql: agent_memories_default },
|
|
382
|
+
{ name: "009_skills.sql", sql: skills_default },
|
|
383
|
+
{ name: "010_briefs_multi.sql", sql: briefs_multi_default },
|
|
384
|
+
{ name: "011_agent_ability.sql", sql: agent_ability_default },
|
|
385
|
+
{ name: "012_brief_composer.sql", sql: brief_composer_default },
|
|
386
|
+
{ name: "013_retired_token_usage.sql", sql: retired_token_usage_default },
|
|
387
|
+
{ name: "014_agent_web_search.sql", sql: agent_web_search_default },
|
|
388
|
+
{ name: "015_web_search_default_off.sql", sql: web_search_default_off_default },
|
|
389
|
+
{ name: "016_prefs_default_model.sql", sql: prefs_default_model_default },
|
|
390
|
+
{ name: "017_agent_carrier_pref.sql", sql: agent_carrier_pref_default },
|
|
391
|
+
{ name: "018_brief_house_style.sql", sql: brief_house_style_default },
|
|
392
|
+
{ name: "019_room_summaries.sql", sql: room_summaries_default },
|
|
393
|
+
{ name: "020_room_followup.sql", sql: room_followup_default }
|
|
394
|
+
];
|
|
395
|
+
_db = null;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// src/cli.ts
|
|
400
|
+
import { Command } from "commander";
|
|
401
|
+
import open from "open";
|
|
202
402
|
|
|
203
403
|
// src/storage/agents.ts
|
|
404
|
+
init_db();
|
|
204
405
|
var VALID_CARRIER_PREFS = /* @__PURE__ */ new Set([
|
|
205
406
|
"openrouter",
|
|
206
407
|
"anthropic",
|
|
@@ -448,6 +649,9 @@ function updateAgent(id, patch) {
|
|
|
448
649
|
return getAgent(id);
|
|
449
650
|
}
|
|
450
651
|
|
|
652
|
+
// src/seed/run.ts
|
|
653
|
+
init_db();
|
|
654
|
+
|
|
451
655
|
// src/seed/chair.ts
|
|
452
656
|
var CHAIR_ID = "chair";
|
|
453
657
|
var CHAIR_HANDLE = "/chair";
|
|
@@ -1125,9 +1329,12 @@ function getModel(v) {
|
|
|
1125
1329
|
return m;
|
|
1126
1330
|
}
|
|
1127
1331
|
function isModelV(v) {
|
|
1128
|
-
return Object.
|
|
1332
|
+
return Object.hasOwn(MODELS, v);
|
|
1129
1333
|
}
|
|
1130
1334
|
|
|
1335
|
+
// src/storage/memories.ts
|
|
1336
|
+
init_db();
|
|
1337
|
+
|
|
1131
1338
|
// src/utils/id.ts
|
|
1132
1339
|
import { randomBytes } from "crypto";
|
|
1133
1340
|
var ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
@@ -1228,6 +1435,7 @@ function isMemoryKind(v) {
|
|
|
1228
1435
|
}
|
|
1229
1436
|
|
|
1230
1437
|
// src/storage/skills.ts
|
|
1438
|
+
init_db();
|
|
1231
1439
|
var SELECT_COLS3 = "id, agent_id, slug, name, version, description, when_to_use, body_md, ability_json, tips_json, created_at, updated_at";
|
|
1232
1440
|
function safeParseObject(s) {
|
|
1233
1441
|
try {
|
|
@@ -1476,6 +1684,7 @@ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
|
1476
1684
|
import { APICallError, streamText } from "ai";
|
|
1477
1685
|
|
|
1478
1686
|
// src/storage/keys.ts
|
|
1687
|
+
init_db();
|
|
1479
1688
|
import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, scryptSync } from "crypto";
|
|
1480
1689
|
import { userInfo } from "os";
|
|
1481
1690
|
var SALT = "boardroom.v1.salt";
|
|
@@ -2945,6 +3154,7 @@ import { writeFile } from "fs/promises";
|
|
|
2945
3154
|
import { join as join2 } from "path";
|
|
2946
3155
|
|
|
2947
3156
|
// src/storage/prefs.ts
|
|
3157
|
+
init_db();
|
|
2948
3158
|
function mapRow4(row) {
|
|
2949
3159
|
return {
|
|
2950
3160
|
name: row.name,
|
|
@@ -3190,6 +3400,481 @@ function utilityModelFor(fallback = null) {
|
|
|
3190
3400
|
return any ?? null;
|
|
3191
3401
|
}
|
|
3192
3402
|
|
|
3403
|
+
// src/ai/prompts/house-styles.ts
|
|
3404
|
+
var HOUSE_STYLES = [
|
|
3405
|
+
{
|
|
3406
|
+
id: "boardroom-default",
|
|
3407
|
+
label: "Boardroom (default)",
|
|
3408
|
+
spine: "boardroom-dark",
|
|
3409
|
+
voice: {
|
|
3410
|
+
en: "Plain analytical voice. State the call, show the reasoning, name the trade-offs. No house-style flourishes.",
|
|
3411
|
+
zh: "\u4E2D\u7ACB\u5206\u6790\u53E3\u543B\u3002\u5148\u7ED9\u5224\u65AD\uFF0C\u518D\u9648\u8FF0\u7406\u636E\uFF0C\u660E\u786E\u6743\u8861\u3002\u4E0D\u5E26\u7279\u5B9A\u6D41\u6D3E\u7684\u4FEE\u8F9E\u3002"
|
|
3412
|
+
},
|
|
3413
|
+
labels: {},
|
|
3414
|
+
fits: ["other", "retro", "philosophical", "operational"],
|
|
3415
|
+
pitch: "Default \xB7 neutral analyst voice with the legacy section vocabulary."
|
|
3416
|
+
},
|
|
3417
|
+
{
|
|
3418
|
+
id: "sequoia-memo",
|
|
3419
|
+
label: "Sequoia memo",
|
|
3420
|
+
spine: "a16z-thesis",
|
|
3421
|
+
voice: {
|
|
3422
|
+
en: 'Investment-memo register. Present-tense, declarative, no hedging. Lead each section with the load-bearing claim, then the evidence. Address "we" as a reader; the writer is the partnership weighing the call. Trim qualifiers \u2014 "strong", "clear", "compelling" only when earned. Concrete numbers, named precedents, no marketing adjectives.',
|
|
3423
|
+
zh: '\u6295\u8D44\u5907\u5FD8\u5F55\u5F0F\u8BED\u6C14\u3002\u73B0\u5728\u65F6\u9648\u8FF0\u53E5\uFF0C\u4E0D\u542B hedge\u3002\u6BCF\u6BB5\u5148\u7ED9\u5173\u952E\u5224\u65AD\uFF0C\u518D\u5F15\u8BC1\u636E\u3002\u5BF9\u8BFB\u8005\u7528 "\u6211\u4EEC"\uFF0C\u5199\u4F5C\u4E3B\u4F53\u662F\u5408\u4F19\u56E2\u961F\u3002\u5254\u9664\u4FEE\u9970\u8BCD \u2014\u2014 "\u5F3A""\u6E05\u6670""\u4EE4\u4EBA\u4FE1\u670D" \u53EA\u5728\u786E\u5B9E\u7AD9\u5F97\u4F4F\u65F6\u4F7F\u7528\u3002\u7ED9\u51FA\u5177\u4F53\u6570\u5B57\u3001\u547D\u540D\u5148\u4F8B\uFF0C\u4E0D\u7528\u8425\u9500\u8154\u5F62\u5BB9\u8BCD\u3002'
|
|
3424
|
+
},
|
|
3425
|
+
labels: {
|
|
3426
|
+
"bottom-line": [
|
|
3427
|
+
{ en: "The Memo", zh: "\u5907\u5FD8\u8981\u70B9" },
|
|
3428
|
+
{ en: "Where We Land", zh: "\u6211\u4EEC\u7684\u5224\u65AD" }
|
|
3429
|
+
],
|
|
3430
|
+
"thesis": [
|
|
3431
|
+
{ en: "Why We'd Back It", zh: "\u4E3A\u4EC0\u4E48\u6211\u4EEC\u4F1A\u652F\u6301" },
|
|
3432
|
+
{ en: "The Bet We'd Take", zh: "\u6211\u4EEC\u613F\u610F\u4E0B\u7684\u6CE8" }
|
|
3433
|
+
],
|
|
3434
|
+
"working-hypothesis": { en: "The Working View", zh: "\u5F53\u524D\u5224\u65AD" },
|
|
3435
|
+
"frame-shift": [
|
|
3436
|
+
{ en: "How the Question Sharpened", zh: "\u95EE\u9898\u5982\u4F55\u88AB\u78E8\u950B\u5229" },
|
|
3437
|
+
{ en: "What the Room Got Clearer About", zh: "\u5168\u573A\u9010\u6E10\u660E\u786E\u7684\u4E8B" }
|
|
3438
|
+
],
|
|
3439
|
+
"headline-findings": [
|
|
3440
|
+
{ en: "The Pillars", zh: "\u652F\u67F1" },
|
|
3441
|
+
{ en: "Why We Like It", zh: "\u6211\u4EEC\u559C\u6B22\u8FD9\u4E8B\u7684\u7406\u7531" }
|
|
3442
|
+
],
|
|
3443
|
+
"big-ideas": [
|
|
3444
|
+
{ en: "Three Reasons We're In", zh: "\u6211\u4EEC\u5165\u5C40\u7684\u4E09\u4E2A\u7406\u7531" },
|
|
3445
|
+
{ en: "Three Things That Stand Out", zh: "\u6700\u663E\u773C\u7684\u4E09\u4EF6\u4E8B" }
|
|
3446
|
+
],
|
|
3447
|
+
"convergence": { en: "Where the Partners Aligned", zh: "\u5408\u4F19\u4EBA\u5171\u8BC6\u6240\u5728" },
|
|
3448
|
+
"divergence": { en: "The Open Disagreement", zh: "\u6211\u4EEC\u5F53\u524D\u7684\u5206\u6B67" },
|
|
3449
|
+
"why-now": [
|
|
3450
|
+
{ en: "Why Now", zh: "\u4E3A\u4EC0\u4E48\u662F\u73B0\u5728" },
|
|
3451
|
+
{ en: "Why the Window Is Open", zh: "\u4E3A\u4EC0\u4E48\u8FD9\u6247\u7A97\u6237\u73B0\u5728\u5F00\u7740" }
|
|
3452
|
+
],
|
|
3453
|
+
"the-bet": [
|
|
3454
|
+
{ en: "Conditions to Back It", zh: "\u652F\u6301\u8FD9\u7B14\u4E0B\u6CE8\u7684\u524D\u63D0" },
|
|
3455
|
+
{ en: "What Has to Be True", zh: "\u4EC0\u4E48\u5FC5\u987B\u6210\u7ACB" }
|
|
3456
|
+
],
|
|
3457
|
+
"recommendations": [
|
|
3458
|
+
{ en: "What Would Make This Work", zh: "\u8BA9\u5B83\u6210\u7ACB\u9700\u8981\u505A\u7684\u4E8B" },
|
|
3459
|
+
{ en: "How We'd Move", zh: "\u6211\u4EEC\u4F1A\u600E\u4E48\u505A" }
|
|
3460
|
+
],
|
|
3461
|
+
"considerations": { en: "Things We'd Stress-Test", zh: "\u9700\u8981\u538B\u529B\u6D4B\u8BD5\u7684\u70B9" },
|
|
3462
|
+
"pre-mortem": [
|
|
3463
|
+
{ en: "Where It Could Break", zh: "\u53EF\u80FD\u5D29\u76D8\u4E4B\u5904" },
|
|
3464
|
+
{ en: "How This Goes Wrong", zh: "\u8FD9\u4E8B\u4F1A\u600E\u4E48\u641E\u7838" }
|
|
3465
|
+
],
|
|
3466
|
+
"critical-assumptions": { en: "What We're Assuming", zh: "\u6211\u4EEC\u7684\u5047\u8BBE" },
|
|
3467
|
+
"threats-to-validity": [
|
|
3468
|
+
{ en: "What Could Be Wrong With This Read", zh: "\u8FD9\u5957\u5224\u65AD\u53EF\u80FD\u9519\u5728\u54EA" },
|
|
3469
|
+
{ en: "Where Our Analysis Could Mislead Us", zh: "\u5206\u6790\u53EF\u80FD\u628A\u6211\u4EEC\u5E26\u504F\u7684\u5730\u65B9" }
|
|
3470
|
+
],
|
|
3471
|
+
"metric-strip": [
|
|
3472
|
+
{ en: "By the Numbers", zh: "\u7528\u6570\u5B57\u8BF4\u8BDD" },
|
|
3473
|
+
{ en: "The Underwrite", zh: "\u652F\u6491\u6570\u636E" },
|
|
3474
|
+
{ en: "Three Numbers Worth Pricing In", zh: "\u503C\u5F97\u5B9A\u4EF7\u7684\u4E09\u4E2A\u6570\u5B57" }
|
|
3475
|
+
],
|
|
3476
|
+
"new-questions": [
|
|
3477
|
+
{ en: "What's Still Unclear", zh: "\u4ECD\u672A\u660E\u786E\u7684\u95EE\u9898" },
|
|
3478
|
+
{ en: "What We'd Want to Know Next", zh: "\u4E0B\u4E00\u6B65\u60F3\u641E\u6E05\u695A\u7684\u4E8B" }
|
|
3479
|
+
],
|
|
3480
|
+
"open-questions": { en: "Outstanding Items", zh: "\u5F85\u529E\u4E8B\u9879" },
|
|
3481
|
+
"scenario-tree": { en: "How This Could Play Out", zh: "\u53EF\u80FD\u7684\u6F14\u5316\u8DEF\u5F84" },
|
|
3482
|
+
"leading-indicators": { en: "What We'll Be Watching", zh: "\u6211\u4EEC\u4F1A\u76EF\u7684\u4FE1\u53F7" },
|
|
3483
|
+
"planning-assumption": { en: "The Bet Behind the Bet", zh: "\u80CC\u540E\u7684\u4E0B\u6CE8\u5047\u8BBE" },
|
|
3484
|
+
"two-paths": { en: "Two Routes", zh: "\u4E24\u6761\u8DEF\u7EBF" },
|
|
3485
|
+
"positions": { en: "Camps Around the Table", zh: "\u684C\u4E0A\u7684\u51E0\u6D3E\u7ACB\u573A" }
|
|
3486
|
+
},
|
|
3487
|
+
fits: ["investment-judgement", "market-forecast"],
|
|
3488
|
+
pitch: 'Investment memo \xB7 declarative, present-tense, partner voice. For "should we back this" rooms.'
|
|
3489
|
+
},
|
|
3490
|
+
{
|
|
3491
|
+
id: "a16z-thesis",
|
|
3492
|
+
label: "a16z thesis",
|
|
3493
|
+
spine: "a16z-thesis",
|
|
3494
|
+
voice: {
|
|
3495
|
+
en: 'Contrarian thesis-essay register. Lead with the counter-consensus claim \u2014 sharp, opinionated, willing to be wrong loudly. Optimistic-but-rigorous: every bold claim earns its place with one named evidence point. Use "the consensus says X \u2014 we think Y because Z" framings. Avoid corporate hedges; embrace strong verbs ("breaks", "unlocks", "compounds"). One pull-quote per section is plenty.',
|
|
3496
|
+
zh: '\u9006\u5171\u8BC6\u8BBA\u6587\u4F53\u3002\u5148\u629B\u51FA\u53CD\u5171\u8BC6\u7684\u5224\u65AD \u2014\u2014 \u950B\u5229\u3001\u6709\u7ACB\u573A\u3001\u6562\u4E8E\u516C\u5F00\u627F\u62C5\u9519\u3002\u4E50\u89C2\u4F46\u4E25\u8C28\uFF1A\u6BCF\u4E2A\u5927\u80C6\u4E3B\u5F20\u90FD\u6709\u4E00\u4E2A\u5177\u540D\u8BC1\u636E\u70B9\u6491\u4F4F\u3002\u591A\u7528 "\u5171\u8BC6\u8BA4\u4E3A X \u2014\u2014 \u6211\u4EEC\u8BA4\u4E3A Y\uFF0C\u56E0\u4E3A Z" \u8FD9\u7C7B\u6846\u67B6\u3002\u4E0D\u8981\u516C\u53F8\u5F0F hedging\uFF1B\u591A\u7528\u5F3A\u52A8\u8BCD\uFF08"\u6253\u7834""\u89E3\u9501""\u590D\u5229\u7D2F\u79EF"\uFF09\u3002\u6BCF\u8282\u4E00\u53E5\u62C9\u5F97\u51FA\u6765\u7684\u91D1\u53E5\u5373\u53EF\u3002'
|
|
3497
|
+
},
|
|
3498
|
+
labels: {
|
|
3499
|
+
"thesis": [
|
|
3500
|
+
{ en: "The Thesis", zh: "\u6838\u5FC3\u8BBA\u70B9" },
|
|
3501
|
+
{ en: "The Counter-Consensus Read", zh: "\u53CD\u5171\u8BC6\u5224\u65AD" }
|
|
3502
|
+
],
|
|
3503
|
+
"bottom-line": [
|
|
3504
|
+
{ en: "The Read", zh: "\u5224\u65AD" },
|
|
3505
|
+
{ en: "What We Think Is True", zh: "\u6211\u4EEC\u8BA4\u4E3A\u6210\u7ACB\u7684\u4E8B" }
|
|
3506
|
+
],
|
|
3507
|
+
"frame-shift": [
|
|
3508
|
+
{ en: "How the Question Moved", zh: "\u95EE\u9898\u5982\u4F55\u8F6C\u79FB" },
|
|
3509
|
+
{ en: "The Frame the Consensus Is Missing", zh: "\u5171\u8BC6\u770B\u6F0F\u7684\u89C6\u89D2" }
|
|
3510
|
+
],
|
|
3511
|
+
"headline-findings": [
|
|
3512
|
+
{ en: "Three Pillars", zh: "\u4E09\u4E2A\u652F\u6491" },
|
|
3513
|
+
{ en: "Why This Compounds", zh: "\u4E3A\u4EC0\u4E48\u8FD9\u4E8B\u4F1A\u590D\u5229" }
|
|
3514
|
+
],
|
|
3515
|
+
"big-ideas": [
|
|
3516
|
+
{ en: "Three Big Ideas", zh: "\u4E09\u4E2A\u5927\u60F3\u6CD5" },
|
|
3517
|
+
{ en: "Three Things the Consensus Misses", zh: "\u5171\u8BC6\u5FFD\u89C6\u7684\u4E09\u4EF6\u4E8B" }
|
|
3518
|
+
],
|
|
3519
|
+
"why-now": [
|
|
3520
|
+
{ en: "Why Now", zh: "\u4E3A\u4EC0\u4E48\u662F\u73B0\u5728" },
|
|
3521
|
+
{ en: "The Window That Just Opened", zh: "\u521A\u521A\u6253\u5F00\u7684\u7A97\u53E3" }
|
|
3522
|
+
],
|
|
3523
|
+
"the-bet": [
|
|
3524
|
+
{ en: "What We'd Bet", zh: "\u6211\u4EEC\u4F1A\u4E0B\u7684\u6CE8" },
|
|
3525
|
+
{ en: "The Bet on the Table", zh: "\u684C\u4E0A\u7684\u8FD9\u7B14\u4E0B\u6CE8" }
|
|
3526
|
+
],
|
|
3527
|
+
"recommendations": [
|
|
3528
|
+
{ en: "How to Play It", zh: "\u600E\u4E48\u6253\u8FD9\u5C40" },
|
|
3529
|
+
{ en: "Where to Push", zh: "\u5728\u54EA\u53D1\u529B" }
|
|
3530
|
+
],
|
|
3531
|
+
"scenario-tree": { en: "Where the World Could Land", zh: "\u4E16\u754C\u53EF\u80FD\u843D\u5230\u54EA\u51E0\u79CD\u72B6\u6001" },
|
|
3532
|
+
"leading-indicators": [
|
|
3533
|
+
{ en: "What We're Watching", zh: "\u6211\u4EEC\u76EF\u7740\u4EC0\u4E48" },
|
|
3534
|
+
{ en: "The Signals That Tell Us We're Right", zh: "\u8BC1\u660E\u6211\u4EEC\u5BF9\u4E86\u7684\u4FE1\u53F7" }
|
|
3535
|
+
],
|
|
3536
|
+
"pre-mortem": [
|
|
3537
|
+
{ en: "How This Goes Wrong", zh: "\u8FD9\u4E8B\u4F1A\u600E\u4E48\u641E\u7838" },
|
|
3538
|
+
{ en: "The Failure Modes We're Underwriting", zh: "\u6211\u4EEC\u5728\u66FF\u8C01\u515C\u5E95\u7684\u5931\u8D25\u6A21\u5F0F" }
|
|
3539
|
+
],
|
|
3540
|
+
"convergence": { en: "What We Agreed On", zh: "\u6211\u4EEC\u8FBE\u6210\u7684\u5171\u8BC6" },
|
|
3541
|
+
"divergence": { en: "The Open Question Inside", zh: "\u6211\u4EEC\u4E4B\u95F4\u7684\u5206\u6B67" },
|
|
3542
|
+
"threats-to-validity": [
|
|
3543
|
+
{ en: "Where We Could Be Wrong", zh: "\u6211\u4EEC\u53EF\u80FD\u9519\u5728\u54EA" },
|
|
3544
|
+
{ en: "What Would Break the Thesis", zh: "\u4EC0\u4E48\u4F1A\u51FB\u7A7F\u8FD9\u5957\u8BBA\u70B9" }
|
|
3545
|
+
],
|
|
3546
|
+
"metric-strip": [
|
|
3547
|
+
{ en: "The Numbers", zh: "\u6570\u5B57" },
|
|
3548
|
+
{ en: "Why the Math Works", zh: "\u4E3A\u4EC0\u4E48\u6570\u7B97\u5F97\u8FC7\u6765" },
|
|
3549
|
+
{ en: "What the Data Says", zh: "\u6570\u636E\u600E\u4E48\u8BF4\u7684" }
|
|
3550
|
+
],
|
|
3551
|
+
"new-questions": [
|
|
3552
|
+
{ en: "What This Opens Up", zh: "\u7531\u6B64\u6253\u5F00\u7684\u95EE\u9898" },
|
|
3553
|
+
{ en: "The Next Set of Questions", zh: "\u4E0B\u4E00\u7EC4\u95EE\u9898" }
|
|
3554
|
+
],
|
|
3555
|
+
"open-questions": { en: "Still on the Table", zh: "\u5C1A\u5728\u684C\u4E0A" },
|
|
3556
|
+
"two-paths": { en: "Platform vs Vertical", zh: "\u5E73\u53F0\u8DEF vs \u7EB5\u6DF1\u8DEF" },
|
|
3557
|
+
"critical-assumptions": { en: "What Has to Be True", zh: "\u5FC5\u987B\u6210\u7ACB\u7684\u524D\u63D0" },
|
|
3558
|
+
"considerations": { en: "Things to Watch", zh: "\u503C\u5F97\u7559\u610F\u7684\u70B9" },
|
|
3559
|
+
"positions": { en: "Where the Camps Sit", zh: "\u51E0\u6D3E\u7684\u4F4D\u7F6E" },
|
|
3560
|
+
"planning-assumption": { en: "The Forecast", zh: "\u9884\u5224" }
|
|
3561
|
+
},
|
|
3562
|
+
fits: ["investment-judgement", "market-forecast", "strategic-decision"],
|
|
3563
|
+
pitch: "a16z-style thesis \xB7 contrarian, claim-forward, optimistic-but-rigorous. For market opportunity / big-idea rooms."
|
|
3564
|
+
},
|
|
3565
|
+
{
|
|
3566
|
+
id: "stanford-research",
|
|
3567
|
+
label: "Stanford research note",
|
|
3568
|
+
spine: "openai-paper",
|
|
3569
|
+
voice: {
|
|
3570
|
+
en: `Hedged scholarly register. Frame conclusions as working hypotheses, name uncertainty explicitly, surface threats to validity before stating recommendations. Third-person where natural; "the analysis suggests" beats "we conclude". Each finding cites which director / lens generated it. Counter-evidence and limitations get equal footprint to findings \u2014 they're not afterthoughts. No imperatives in the action section; substitute with "considerations" framing.`,
|
|
3571
|
+
zh: '\u514B\u5236\u7684\u5B66\u672F\u53E3\u543B\u3002\u628A\u7ED3\u8BBA\u6846\u5B9A\u4E3A\u5DE5\u4F5C\u5047\u8BBE\uFF0C\u660E\u786E\u70B9\u540D\u4E0D\u786E\u5B9A\u6027\uFF0C\u5148\u8BB2\u5BF9\u7ED3\u8BBA\u6709\u6548\u6027\u7684\u5A01\u80C1\uFF0C\u518D\u7ED9\u5EFA\u8BAE\u3002\u80FD\u7528\u7B2C\u4E09\u4EBA\u79F0\u5C31\u7528 \u2014\u2014 "\u5206\u6790\u663E\u793A" \u4F18\u4E8E "\u6211\u4EEC\u8BA4\u4E3A"\u3002\u6BCF\u6761\u53D1\u73B0\u6CE8\u660E\u7531\u54EA\u4F4D\u8463\u4E8B / \u89C6\u89D2\u4EA7\u751F\u3002\u5BF9\u7ACB\u8BC1\u636E\u548C\u5C40\u9650\u4E0E\u53D1\u73B0\u7B49\u91CF\u5448\u73B0\uFF0C\u4E0D\u5F53\u4F5C\u9644\u5F55\u3002\u884C\u52A8\u6BB5\u4E0D\u7528\u7948\u4F7F\u53E5\uFF0C\u6539\u4E3A "\u503C\u5F97\u8003\u8651" \u5F0F\u8868\u8FBE\u3002'
|
|
3572
|
+
},
|
|
3573
|
+
labels: {
|
|
3574
|
+
"working-hypothesis": [
|
|
3575
|
+
{ en: "Working Hypothesis", zh: "\u5DE5\u4F5C\u5047\u8BBE" },
|
|
3576
|
+
{ en: "Tentative Position", zh: "\u521D\u6B65\u7ACB\u573A" }
|
|
3577
|
+
],
|
|
3578
|
+
"bottom-line": [
|
|
3579
|
+
{ en: "Abstract", zh: "\u6458\u8981" },
|
|
3580
|
+
{ en: "Summary of Findings", zh: "\u7814\u7A76\u7EFC\u8FF0" }
|
|
3581
|
+
],
|
|
3582
|
+
"thesis": { en: "Central Claim", zh: "\u6838\u5FC3\u4E3B\u5F20" },
|
|
3583
|
+
"frame-shift": [
|
|
3584
|
+
{ en: "How the Question Was Reframed", zh: "\u95EE\u9898\u5982\u4F55\u88AB\u91CD\u65B0\u5B9A\u4E49" },
|
|
3585
|
+
{ en: "Reframing the Research Question", zh: "\u5BF9\u7814\u7A76\u95EE\u9898\u7684\u91CD\u65B0\u6846\u5B9A" }
|
|
3586
|
+
],
|
|
3587
|
+
"headline-findings": [
|
|
3588
|
+
{ en: "Findings", zh: "\u7814\u7A76\u53D1\u73B0" },
|
|
3589
|
+
{ en: "Principal Findings", zh: "\u4E3B\u8981\u53D1\u73B0" },
|
|
3590
|
+
{ en: "What the Analysis Suggests", zh: "\u5206\u6790\u6240\u63ED\u793A\u7684\u5185\u5BB9" }
|
|
3591
|
+
],
|
|
3592
|
+
"big-ideas": [
|
|
3593
|
+
{ en: "Three Observations", zh: "\u4E09\u70B9\u89C2\u5BDF" },
|
|
3594
|
+
{ en: "Three Patterns Worth Naming", zh: "\u4E09\u4E2A\u503C\u5F97\u547D\u540D\u7684\u6A21\u5F0F" }
|
|
3595
|
+
],
|
|
3596
|
+
"convergence": { en: "Independent Convergence", zh: "\u72EC\u7ACB\u8DEF\u5F84\u4E0A\u7684\u8D8B\u540C" },
|
|
3597
|
+
"divergence": { en: "Where Reasonable Lenses Disagree", zh: "\u7406\u6027\u89C6\u89D2\u4E0B\u7684\u5206\u6B67" },
|
|
3598
|
+
"positions": { en: "Schools of Thought", zh: "\u51E0\u79CD\u601D\u60F3\u6D41\u6D3E" },
|
|
3599
|
+
// critical-assumptions remains "Threats to Validity" — it shipped
|
|
3600
|
+
// before the dedicated `threats-to-validity` component existed; we
|
|
3601
|
+
// keep the legacy mapping intact so old briefs render unchanged,
|
|
3602
|
+
// and the new component below gets the canonical labelling.
|
|
3603
|
+
"critical-assumptions": { en: "Critical Assumptions", zh: "\u5173\u952E\u5047\u8BBE" },
|
|
3604
|
+
"threats-to-validity": [
|
|
3605
|
+
{ en: "Threats to Validity", zh: "\u5BF9\u7ED3\u8BBA\u6709\u6548\u6027\u7684\u5A01\u80C1" },
|
|
3606
|
+
{ en: "Where the Analysis Could Mislead", zh: "\u5206\u6790\u53EF\u80FD\u8BEF\u5BFC\u4E4B\u5904" },
|
|
3607
|
+
{ en: "Internal & External Validity Concerns", zh: "\u5185\u5916\u90E8\u6548\u5EA6\u7684\u62C5\u5FE7" }
|
|
3608
|
+
],
|
|
3609
|
+
"metric-strip": [
|
|
3610
|
+
{ en: "Quantitative Reads", zh: "\u5B9A\u91CF\u7ED3\u679C" },
|
|
3611
|
+
{ en: "Key Metrics", zh: "\u5173\u952E\u6307\u6807" },
|
|
3612
|
+
{ en: "Empirical Anchors", zh: "\u5B9E\u8BC1\u951A\u70B9" }
|
|
3613
|
+
],
|
|
3614
|
+
"pre-mortem": [
|
|
3615
|
+
{ en: "Failure Modes Considered", zh: "\u5DF2\u8003\u8651\u7684\u5931\u8D25\u6A21\u5F0F" },
|
|
3616
|
+
{ en: "How the Argument Could Fail", zh: "\u8BBA\u70B9\u53EF\u80FD\u5982\u4F55\u5931\u8D25" }
|
|
3617
|
+
],
|
|
3618
|
+
"considerations": [
|
|
3619
|
+
{ en: "Things Worth Considering", zh: "\u503C\u5F97\u8003\u8651\u7684\u4E8B\u9879" },
|
|
3620
|
+
{ en: "Practical Implications", zh: "\u5B9E\u8DF5\u5C42\u9762\u7684\u542B\u4E49" }
|
|
3621
|
+
],
|
|
3622
|
+
"recommendations": [
|
|
3623
|
+
{ en: "Implications", zh: "\u7531\u6B64\u5F15\u51FA\u7684\u5F71\u54CD" },
|
|
3624
|
+
{ en: "Practical Implications", zh: "\u5B9E\u8DF5\u5C42\u9762\u7684\u5F71\u54CD" }
|
|
3625
|
+
],
|
|
3626
|
+
"the-bet": { en: "Conditions for the Claim", zh: "\u652F\u6491\u8BE5\u4E3B\u5F20\u7684\u6761\u4EF6" },
|
|
3627
|
+
"scenario-tree": { en: "Plausible Futures", zh: "\u53EF\u80FD\u7684\u672A\u6765\u60C5\u666F" },
|
|
3628
|
+
"leading-indicators": { en: "Empirical Signals to Track", zh: "\u53EF\u8FFD\u8E2A\u7684\u7ECF\u9A8C\u4FE1\u53F7" },
|
|
3629
|
+
"new-questions": [
|
|
3630
|
+
{ en: "Future Work", zh: "\u540E\u7EED\u5DE5\u4F5C" },
|
|
3631
|
+
{ en: "Open Lines of Inquiry", zh: "\u5C1A\u5F00\u653E\u7684\u7814\u7A76\u65B9\u5411" }
|
|
3632
|
+
],
|
|
3633
|
+
"open-questions": { en: "Limitations", zh: "\u7814\u7A76\u5C40\u9650" },
|
|
3634
|
+
"why-now": { en: "Temporal Window", zh: "\u65F6\u95F4\u7A97\u53E3" },
|
|
3635
|
+
"planning-assumption": { en: "Forecasting Assumption", zh: "\u9884\u6D4B\u5047\u8BBE" },
|
|
3636
|
+
"two-paths": { en: "Two Lines of Inquiry", zh: "\u4E24\u6761\u7814\u7A76\u8DEF\u5F84" }
|
|
3637
|
+
},
|
|
3638
|
+
fits: ["philosophical", "market-forecast", "other"],
|
|
3639
|
+
pitch: "Stanford research note \xB7 hedged, hypothesis-driven, threats-to-validity surfaced. For open-ended / philosophical rooms."
|
|
3640
|
+
},
|
|
3641
|
+
{
|
|
3642
|
+
id: "bcg-strategy",
|
|
3643
|
+
label: "BCG strategy memo",
|
|
3644
|
+
spine: "mckinsey-deck",
|
|
3645
|
+
voice: {
|
|
3646
|
+
en: 'Structured strategy-consulting register. Pyramid principle in every section: lead claim, then 3 supporting points, then evidence. MECE wherever possible \u2014 list items must be mutually exclusive and collectively exhaustive. Imperative voice in actions ("Do X", "Stop Y"). Each insight earns its inclusion by being load-bearing for the recommendations. Avoid academic hedging; this is a memo to a CEO with 20 minutes.',
|
|
3647
|
+
zh: '\u7ED3\u6784\u5316\u6218\u7565\u54A8\u8BE2\u8BED\u6C14\u3002\u6BCF\u8282\u90FD\u7528\u91D1\u5B57\u5854\u539F\u7406\uFF1A\u4E3B\u5F20\u5728\u524D\uFF0C3 \u4E2A\u652F\u6491\u70B9\u5C45\u4E2D\uFF0C\u8BC1\u636E\u5728\u540E\u3002\u80FD MECE \u5C31 MECE \u2014\u2014 \u5217\u8868\u9879\u4E4B\u95F4\u4E92\u65A5\u3001\u5408\u5728\u4E00\u8D77\u7A77\u5C3D\u3002\u884C\u52A8\u6BB5\u7528\u7948\u4F7F\u53E5\uFF08"\u505A X""\u505C Y"\uFF09\u3002\u6BCF\u6761\u6D1E\u5BDF\u90FD\u5F97\u5BF9\u6700\u7EC8\u5EFA\u8BAE\u6709\u627F\u91CD\u4F5C\u7528\uFF0C\u5426\u5219\u5220\u6389\u3002\u5C11\u5B66\u672F hedging\uFF0C\u8FD9\u662F\u7ED9 CEO \u7684\u4E8C\u5341\u5206\u949F\u5907\u5FD8\u5F55\u3002'
|
|
3648
|
+
},
|
|
3649
|
+
labels: {
|
|
3650
|
+
"bottom-line": [
|
|
3651
|
+
{ en: "The Strategic Imperative", zh: "\u6218\u7565\u8981\u52A1" },
|
|
3652
|
+
{ en: "The Call to the Board", zh: "\u5BF9\u8463\u4E8B\u4F1A\u7684\u5224\u65AD" }
|
|
3653
|
+
],
|
|
3654
|
+
"thesis": [
|
|
3655
|
+
{ en: "The Strategic Call", zh: "\u6218\u7565\u5224\u65AD" },
|
|
3656
|
+
{ en: "The Position We'd Hold", zh: "\u6211\u4EEC\u4F1A\u6301\u7684\u7ACB\u573A" }
|
|
3657
|
+
],
|
|
3658
|
+
"working-hypothesis": { en: "Working Strategic View", zh: "\u5F53\u524D\u6218\u7565\u89C2\u70B9" },
|
|
3659
|
+
"frame-shift": { en: "Reframing the Question", zh: "\u5BF9\u95EE\u9898\u7684\u91CD\u65B0\u5B9A\u4E49" },
|
|
3660
|
+
"strategic-outlook": [
|
|
3661
|
+
{ en: "The Operating Environment", zh: "\u7ECF\u8425\u73AF\u5883" },
|
|
3662
|
+
{ en: "The Context We're Operating In", zh: "\u6211\u4EEC\u6240\u5904\u7684\u683C\u5C40" }
|
|
3663
|
+
],
|
|
3664
|
+
"headline-findings": [
|
|
3665
|
+
{ en: "Three Strategic Insights", zh: "\u4E09\u5927\u6218\u7565\u6D1E\u5BDF" },
|
|
3666
|
+
{ en: "Three Findings That Drive the Call", zh: "\u652F\u6491\u5224\u65AD\u7684\u4E09\u9879\u53D1\u73B0" }
|
|
3667
|
+
],
|
|
3668
|
+
"big-ideas": [
|
|
3669
|
+
{ en: "Three Strategic Themes", zh: "\u4E09\u4E2A\u6218\u7565\u4E3B\u9898" },
|
|
3670
|
+
{ en: "Three Themes That Recurred", zh: "\u53CD\u590D\u51FA\u73B0\u7684\u4E09\u4E2A\u4E3B\u9898" }
|
|
3671
|
+
],
|
|
3672
|
+
"convergence": { en: "Where the Analysis Aligns", zh: "\u5206\u6790\u7684\u5171\u8BC6\u6240\u5728" },
|
|
3673
|
+
"divergence": { en: "The Strategic Tension", zh: "\u6218\u7565\u5C42\u9762\u7684\u5F20\u529B" },
|
|
3674
|
+
"positions": { en: "Strategic Camps", zh: "\u6218\u7565\u9635\u8425" },
|
|
3675
|
+
"critical-assumptions": { en: "Critical Assumptions", zh: "\u5173\u952E\u5047\u8BBE" },
|
|
3676
|
+
"threats-to-validity": [
|
|
3677
|
+
{ en: "Where the Analysis Could Be Wrong", zh: "\u5206\u6790\u53EF\u80FD\u9519\u5728\u54EA\u91CC" },
|
|
3678
|
+
{ en: "Holes in This Read", zh: "\u8FD9\u5957\u5224\u65AD\u7684\u6F0F\u6D1E" }
|
|
3679
|
+
],
|
|
3680
|
+
"metric-strip": [
|
|
3681
|
+
{ en: "Strategic Indicators", zh: "\u6218\u7565\u6307\u6807" },
|
|
3682
|
+
{ en: "The Diagnostic at a Glance", zh: "\u8BCA\u65AD\u901F\u89C8" },
|
|
3683
|
+
{ en: "Numbers That Drive the Call", zh: "\u9A71\u52A8\u5224\u65AD\u7684\u6570\u5B57" }
|
|
3684
|
+
],
|
|
3685
|
+
"scenario-tree": { en: "Strategic Scenarios", zh: "\u6218\u7565\u60C5\u666F" },
|
|
3686
|
+
"leading-indicators": { en: "Indicators to Monitor", zh: "\u9700\u76D1\u6D4B\u7684\u5148\u884C\u6307\u6807" },
|
|
3687
|
+
"recommendations": [
|
|
3688
|
+
{ en: "What to Do", zh: "\u5E94\u5F53\u91C7\u53D6\u7684\u884C\u52A8" },
|
|
3689
|
+
{ en: "The Move", zh: "\u52A8\u4F5C" },
|
|
3690
|
+
{ en: "Strategic Imperatives", zh: "\u6218\u7565\u8981\u52A1" }
|
|
3691
|
+
],
|
|
3692
|
+
"the-bet": { en: "Conditions for Commitment", zh: "\u505A\u51FA\u627F\u8BFA\u7684\u524D\u63D0\u6761\u4EF6" },
|
|
3693
|
+
"considerations": { en: "Trade-offs to Weigh", zh: "\u9700\u8981\u6743\u8861\u7684\u53D6\u820D" },
|
|
3694
|
+
"pre-mortem": [
|
|
3695
|
+
{ en: "Risks to Manage", zh: "\u9700\u7BA1\u7406\u7684\u98CE\u9669" },
|
|
3696
|
+
{ en: "Where Execution Could Fail", zh: "\u6267\u884C\u53EF\u80FD\u5D29\u76D8\u4E4B\u5904" }
|
|
3697
|
+
],
|
|
3698
|
+
"two-paths": { en: "Strategic Options", zh: "\u6218\u7565\u9009\u9879\u5BF9\u7167" },
|
|
3699
|
+
"why-now": { en: "Why the Window Is Open", zh: "\u4E3A\u4F55\u7A97\u53E3\u73B0\u5728\u6253\u5F00" },
|
|
3700
|
+
"new-questions": [
|
|
3701
|
+
{ en: "Questions for the Next Phase", zh: "\u4E0B\u4E00\u9636\u6BB5\u9700\u8981\u56DE\u7B54\u7684\u95EE\u9898" },
|
|
3702
|
+
{ en: "What the Next Diagnostic Should Probe", zh: "\u4E0B\u4E00\u6B21\u8BCA\u65AD\u8BE5\u6DF1\u5165\u7684\u95EE\u9898" }
|
|
3703
|
+
],
|
|
3704
|
+
"open-questions": { en: "Open Items", zh: "\u5F85\u89E3\u51B3\u4E8B\u9879" },
|
|
3705
|
+
"planning-assumption": { en: "Strategic Planning Assumption", zh: "\u6218\u7565\u89C4\u5212\u5047\u8BBE" }
|
|
3706
|
+
},
|
|
3707
|
+
fits: ["strategic-decision", "operational", "option-comparison"],
|
|
3708
|
+
pitch: 'BCG / strategy-consulting memo \xB7 MECE, pyramid principle, imperative actions. For "what should we do" rooms.'
|
|
3709
|
+
},
|
|
3710
|
+
{
|
|
3711
|
+
id: "first-round-essay",
|
|
3712
|
+
label: "First Round Review essay",
|
|
3713
|
+
spine: "anthropic-essay",
|
|
3714
|
+
voice: {
|
|
3715
|
+
en: 'Operator-essay register, narrative voice. Use first-person plural ("we found", "we learned") as if the room is the author. Reference specific moments from the conversation rather than abstracted findings \u2014 "when Socrates pressed on the definition of engagement, the room realized\u2026". Warm, reflective, willing to admit confusion. Imperative voice is too sharp here; default to "things worth thinking about" / "considerations". Each section can open with a small story before the claim.',
|
|
3716
|
+
zh: '\u64CD\u76D8\u8005\u5199\u968F\u7B14\u7684\u8BED\u6C14\uFF0C\u53D9\u4E8B\u53E3\u543B\u3002\u7B2C\u4E00\u4EBA\u79F0\u590D\u6570\uFF08"\u6211\u4EEC\u53D1\u73B0""\u6211\u4EEC\u610F\u8BC6\u5230"\uFF09\uFF0C\u628A\u4F1A\u8BAE\u672C\u8EAB\u5F53\u6210\u4F5C\u8005\u3002\u5F15\u7528\u5BF9\u8BDD\u4E2D\u7684\u5177\u4F53\u7247\u6BB5\uFF0C\u800C\u4E0D\u662F\u62BD\u8C61\u63D0\u70BC \u2014\u2014 "\u5F53 Socrates \u8FFD\u95EE engagement \u7684\u5B9A\u4E49\u65F6\uFF0C\u5168\u573A\u624D\u610F\u8BC6\u5230\u2026\u2026"\u3002\u6E29\u6696\u3001\u6709\u53CD\u601D\u611F\u3001\u613F\u610F\u627F\u8BA4\u56F0\u60D1\u3002\u8FD9\u91CC\u7948\u4F7F\u53E5\u592A\u950B\u5229\uFF0C\u9ED8\u8BA4\u6539\u7528 "\u503C\u5F97\u601D\u8003\u7684\u4E8B""\u53EF\u80FD\u7684\u8003\u91CF" \u8FD9\u7C7B\u8868\u8FBE\u3002\u6BCF\u8282\u53EF\u4EE5\u5148\u8BB2\u4E00\u4E2A\u5C0F\u6545\u4E8B\u518D\u4E0A\u5224\u65AD\u3002'
|
|
3717
|
+
},
|
|
3718
|
+
labels: {
|
|
3719
|
+
"working-hypothesis": [
|
|
3720
|
+
{ en: "What We've Come to Believe", zh: "\u6211\u4EEC\u9010\u6E10\u76F8\u4FE1\u7684\u4E8B" },
|
|
3721
|
+
{ en: "Where We Are with This", zh: "\u6211\u4EEC\u76EE\u524D\u5BF9\u5B83\u7684\u7406\u89E3" }
|
|
3722
|
+
],
|
|
3723
|
+
"bottom-line": [
|
|
3724
|
+
{ en: "Where We Landed", zh: "\u6211\u4EEC\u6700\u540E\u843D\u5230\u54EA\u91CC" },
|
|
3725
|
+
{ en: "What We Took Away", zh: "\u6211\u4EEC\u5E26\u8D70\u7684\u4E1C\u897F" }
|
|
3726
|
+
],
|
|
3727
|
+
"thesis": { en: "The Idea That Stuck", zh: "\u7ACB\u4F4F\u4E86\u7684\u90A3\u4E2A\u60F3\u6CD5" },
|
|
3728
|
+
"frame-shift": { en: "How the Question Changed Us", zh: "\u95EE\u9898\u5982\u4F55\u53CD\u8FC7\u6765\u6539\u9020\u4E86\u6211\u4EEC" },
|
|
3729
|
+
"headline-findings": [
|
|
3730
|
+
{ en: "Three Things We Saw", zh: "\u6211\u4EEC\u770B\u5230\u7684\u4E09\u4EF6\u4E8B" },
|
|
3731
|
+
{ en: "Three Things That Kept Coming Back", zh: "\u53CD\u590D\u56DE\u5230\u684C\u4E0A\u7684\u4E09\u4EF6\u4E8B" }
|
|
3732
|
+
],
|
|
3733
|
+
"big-ideas": [
|
|
3734
|
+
{ en: "Three Things We Noticed", zh: "\u6211\u4EEC\u6CE8\u610F\u5230\u7684\u4E09\u4EF6\u4E8B" },
|
|
3735
|
+
{ en: "Three Patterns Worth Naming", zh: "\u503C\u5F97\u8BF4\u51FA\u6765\u7684\u4E09\u4E2A\u6A21\u5F0F" }
|
|
3736
|
+
],
|
|
3737
|
+
"convergence": { en: "Where We Agreed", zh: "\u6211\u4EEC\u5F7C\u6B64\u540C\u610F\u7684\u5730\u65B9" },
|
|
3738
|
+
"divergence": { en: "Where We Couldn't Agree", zh: "\u6211\u4EEC\u5F7C\u6B64\u65E0\u6CD5\u8BF4\u670D\u7684\u5730\u65B9" },
|
|
3739
|
+
"positions": { en: "How the Room Split", zh: "\u623F\u95F4\u5206\u6210\u4E86\u54EA\u51E0\u6D3E" },
|
|
3740
|
+
"critical-assumptions": { en: "What We're Quietly Assuming", zh: "\u6211\u4EEC\u6697\u81EA\u5047\u8BBE\u7684\u4E8B" },
|
|
3741
|
+
"threats-to-validity": [
|
|
3742
|
+
{ en: "Where We Might Be Fooling Ourselves", zh: "\u6211\u4EEC\u53EF\u80FD\u5728\u81EA\u6B3A\u7684\u5730\u65B9" },
|
|
3743
|
+
{ en: "What Could Be Wrong With This Take", zh: "\u8FD9\u5957\u770B\u6CD5\u53EF\u80FD\u9519\u5728\u54EA" }
|
|
3744
|
+
],
|
|
3745
|
+
"metric-strip": [
|
|
3746
|
+
{ en: "A Few Numbers That Came Up", zh: "\u51E0\u4E2A\u6D6E\u4E0A\u6765\u7684\u6570\u5B57" },
|
|
3747
|
+
{ en: "What the Numbers Said", zh: "\u6570\u5B57\u544A\u8BC9\u6211\u4EEC\u7684\u4E8B" }
|
|
3748
|
+
],
|
|
3749
|
+
"considerations": [
|
|
3750
|
+
{ en: "Things Worth Thinking About", zh: "\u503C\u5F97\u60F3\u60F3\u7684\u4E8B" },
|
|
3751
|
+
{ en: "Questions We'd Sit With", zh: "\u6211\u4EEC\u4F1A\u966A\u7740\u7684\u95EE\u9898" }
|
|
3752
|
+
],
|
|
3753
|
+
"recommendations": [
|
|
3754
|
+
{ en: "What We'd Do With This", zh: "\u6211\u4EEC\u4F1A\u600E\u4E48\u7528\u8FD9\u4E2A" },
|
|
3755
|
+
{ en: "How We'd Carry This Out", zh: "\u6211\u4EEC\u4F1A\u600E\u4E48\u628A\u8FD9\u4E8B\u843D\u4E0B\u6765" }
|
|
3756
|
+
],
|
|
3757
|
+
"the-bet": { en: "What We'd Need to Believe to Move", zh: "\u8981\u884C\u52A8\u9700\u8981\u76F8\u4FE1\u4EC0\u4E48" },
|
|
3758
|
+
"pre-mortem": [
|
|
3759
|
+
{ en: "Where We'd Get Stuck", zh: "\u6211\u4EEC\u5927\u6982\u4F1A\u5361\u5728\u54EA" },
|
|
3760
|
+
{ en: "Where Things Tend to Fall Apart", zh: "\u901A\u5E38\u4F1A\u5D29\u5728\u54EA" }
|
|
3761
|
+
],
|
|
3762
|
+
"new-questions": [
|
|
3763
|
+
{ en: "Questions We Walked Out With", zh: "\u8D70\u51FA\u4F1A\u8BAE\u5BA4\u65F6\u5E26\u7740\u7684\u65B0\u95EE\u9898" },
|
|
3764
|
+
{ en: "What We're Going Home With", zh: "\u6211\u4EEC\u6253\u5305\u5E26\u56DE\u5BB6\u7684\u95EE\u9898" }
|
|
3765
|
+
],
|
|
3766
|
+
"open-questions": { en: "Still Unresolved", zh: "\u4ECD\u672A\u89E3\u51B3" },
|
|
3767
|
+
"why-now": { en: "Why This Mattered Today", zh: "\u4E3A\u4EC0\u4E48\u8FD9\u4E8B\u6B64\u523B\u8981\u7D27" },
|
|
3768
|
+
"scenario-tree": { en: "How This Might Unfold", zh: "\u8FD9\u4E8B\u53EF\u80FD\u600E\u4E48\u6F14" },
|
|
3769
|
+
"leading-indicators": { en: "What We'd Watch For", zh: "\u6211\u4EEC\u4F1A\u7559\u610F\u4EC0\u4E48" },
|
|
3770
|
+
"two-paths": { en: "Two Roads", zh: "\u4E24\u6761\u8DEF" },
|
|
3771
|
+
"planning-assumption": { en: "What We Think Will Hold", zh: "\u6211\u4EEC\u8BA4\u4E3A\u4F1A\u6210\u7ACB\u7684\u5224\u65AD" }
|
|
3772
|
+
},
|
|
3773
|
+
fits: ["philosophical", "retro", "operational"],
|
|
3774
|
+
pitch: "First Round-style operator essay \xB7 first-person plural, narrative, references specific moments. For retro / philosophical rooms."
|
|
3775
|
+
},
|
|
3776
|
+
{
|
|
3777
|
+
id: "gartner-research",
|
|
3778
|
+
label: "Gartner research note",
|
|
3779
|
+
spine: "gartner-note",
|
|
3780
|
+
voice: {
|
|
3781
|
+
en: 'Risk-conscious analyst register. Every claim carries a confidence band; every assumption carries a falsifier; every recommendation carries a horizon. Probability-aware: "by 2027, 60% probability that \u2026, unless \u2026". Watch-list orientation \u2014 the brief tells the reader what to monitor as much as what to conclude. Avoid the sales-deck adjectives ("transformative", "game-changing"); the voice is the analyst writing for a procurement committee, not the vendor selling to it.',
|
|
3782
|
+
zh: '\u98CE\u9669\u610F\u8BC6\u5F3A\u7684\u5206\u6790\u5E08\u53E3\u543B\u3002\u6BCF\u4E2A\u5224\u65AD\u90FD\u6709\u7F6E\u4FE1\u5EA6\u5E26\uFF1B\u6BCF\u4E2A\u5047\u8BBE\u90FD\u6709\u53EF\u8BC1\u4F2A\u89E6\u53D1\u6761\u4EF6\uFF1B\u6BCF\u6761\u5EFA\u8BAE\u90FD\u6709\u65F6\u95F4\u7A97\u3002\u5E26\u6982\u7387\u611F\uFF1A"\u5230 2027 \u5E74\uFF0C60% \u6982\u7387 X \u53D1\u751F\uFF0C\u9664\u975E Y"\u3002"\u76D1\u6D4B\u6E05\u5355" \u5FC3\u6001 \u2014\u2014 \u62A5\u544A\u544A\u8BC9\u8BFB\u8005\u8981\u6301\u7EED\u76EF\u4EC0\u4E48\uFF0C\u8DDF\u544A\u8BC9\u4ED6\u4EEC\u7ED3\u8BBA\u4E00\u6837\u91CD\u8981\u3002\u907F\u514D\u63A8\u9500 PPT \u5F62\u5BB9\u8BCD\uFF08"\u98A0\u8986\u6027\u7684""\u6539\u53D8\u6E38\u620F\u89C4\u5219"\uFF09\uFF0C\u8BED\u6C14\u662F\u5206\u6790\u5E08\u5199\u7ED9\u91C7\u8D2D\u59D4\u5458\u4F1A\u7684\uFF0C\u4E0D\u662F\u4F9B\u5E94\u5546\u5199\u7ED9\u5BA2\u6237\u7684\u3002'
|
|
3783
|
+
},
|
|
3784
|
+
labels: {
|
|
3785
|
+
"bottom-line": [
|
|
3786
|
+
{ en: "The Read", zh: "\u5224\u65AD\u8981\u70B9" },
|
|
3787
|
+
{ en: "Analyst Read", zh: "\u5206\u6790\u5E08\u5224\u65AD" }
|
|
3788
|
+
],
|
|
3789
|
+
"thesis": { en: "Analyst Position", zh: "\u5206\u6790\u5E08\u7ACB\u573A" },
|
|
3790
|
+
"working-hypothesis": { en: "Current Working View", zh: "\u5F53\u524D\u5DE5\u4F5C\u5224\u65AD" },
|
|
3791
|
+
"strategic-outlook": { en: "Strategic Outlook", zh: "\u6218\u7565\u5C55\u671B" },
|
|
3792
|
+
"frame-shift": { en: "Reframing the Decision", zh: "\u5BF9\u51B3\u7B56\u6846\u67B6\u7684\u91CD\u7F6E" },
|
|
3793
|
+
"headline-findings": [
|
|
3794
|
+
{ en: "Key Findings", zh: "\u5173\u952E\u53D1\u73B0" },
|
|
3795
|
+
{ en: "Findings That Drive the Read", zh: "\u9A71\u52A8\u5224\u65AD\u7684\u53D1\u73B0" }
|
|
3796
|
+
],
|
|
3797
|
+
"big-ideas": [
|
|
3798
|
+
{ en: "Three Themes", zh: "\u4E09\u4E2A\u4E3B\u9898" },
|
|
3799
|
+
{ en: "Three Patterns the Data Supports", zh: "\u6570\u636E\u652F\u6301\u7684\u4E09\u4E2A\u6A21\u5F0F" }
|
|
3800
|
+
],
|
|
3801
|
+
"critical-assumptions": { en: "Critical Assumptions", zh: "\u5173\u952E\u5047\u8BBE" },
|
|
3802
|
+
"threats-to-validity": [
|
|
3803
|
+
{ en: "Validity Concerns", zh: "\u5BF9\u7ED3\u8BBA\u6548\u5EA6\u7684\u62C5\u5FE7" },
|
|
3804
|
+
{ en: "Threats to the Read", zh: "\u5BF9\u5224\u65AD\u7684\u5A01\u80C1" },
|
|
3805
|
+
{ en: "Where the Analysis Could Be Wrong", zh: "\u5206\u6790\u53EF\u80FD\u9519\u5728\u54EA" }
|
|
3806
|
+
],
|
|
3807
|
+
"metric-strip": [
|
|
3808
|
+
{ en: "Indicator Dashboard", zh: "\u6307\u6807\u770B\u677F" },
|
|
3809
|
+
{ en: "Key Metrics at a Glance", zh: "\u5173\u952E\u6307\u6807\u4E00\u89C8" },
|
|
3810
|
+
{ en: "Quantitative Read", zh: "\u5B9A\u91CF\u5224\u8BFB" }
|
|
3811
|
+
],
|
|
3812
|
+
"scenario-tree": { en: "Scenario Tree", zh: "\u60C5\u666F\u6811" },
|
|
3813
|
+
"leading-indicators": { en: "Leading Indicators", zh: "\u5148\u884C\u6307\u6807" },
|
|
3814
|
+
"convergence": { en: "Areas of Analyst Agreement", zh: "\u5206\u6790\u5171\u8BC6\u6240\u5728" },
|
|
3815
|
+
"divergence": { en: "Analyst Disagreement", zh: "\u5206\u6790\u5E08\u5206\u6B67" },
|
|
3816
|
+
"positions": { en: "Vendor / Camp Positions", zh: "\u5382\u5546 / \u9635\u8425\u7ACB\u573A" },
|
|
3817
|
+
"pre-mortem": [
|
|
3818
|
+
{ en: "Failure Modes", zh: "\u5931\u8D25\u6A21\u5F0F" },
|
|
3819
|
+
{ en: "Downside Scenarios", zh: "\u4E0B\u884C\u60C5\u666F" }
|
|
3820
|
+
],
|
|
3821
|
+
"recommendations": [
|
|
3822
|
+
{ en: "Strategic Imperatives", zh: "\u6218\u7565\u8981\u52A1" },
|
|
3823
|
+
{ en: "Recommended Actions", zh: "\u5EFA\u8BAE\u884C\u52A8" },
|
|
3824
|
+
{ en: "Action Items for Decision-Makers", zh: "\u51B3\u7B56\u8005\u7684\u884C\u52A8\u6E05\u5355" }
|
|
3825
|
+
],
|
|
3826
|
+
"the-bet": { en: "Conditions to Commit", zh: "\u505A\u51FA\u627F\u8BFA\u7684\u524D\u63D0" },
|
|
3827
|
+
"considerations": { en: "Decision Considerations", zh: "\u51B3\u7B56\u8003\u91CF" },
|
|
3828
|
+
"planning-assumption": { en: "Strategic Planning Assumption", zh: "\u6218\u7565\u89C4\u5212\u5047\u8BBE" },
|
|
3829
|
+
"new-questions": [
|
|
3830
|
+
{ en: "Emerging Questions", zh: "\u65B0\u6D6E\u73B0\u7684\u95EE\u9898" },
|
|
3831
|
+
{ en: "Questions to Track", zh: "\u9700\u8981\u8FFD\u8E2A\u7684\u95EE\u9898" }
|
|
3832
|
+
],
|
|
3833
|
+
"open-questions": { en: "Outstanding Items", zh: "\u5C1A\u672A\u89E3\u51B3\u7684\u4E8B\u9879" },
|
|
3834
|
+
"why-now": { en: "Decision Window", zh: "\u51B3\u7B56\u7A97\u53E3" },
|
|
3835
|
+
"two-paths": { en: "Option A vs Option B", zh: "\u9009\u9879 A \u5BF9\u7167 \u9009\u9879 B" }
|
|
3836
|
+
},
|
|
3837
|
+
fits: ["strategic-decision", "market-forecast", "option-comparison"],
|
|
3838
|
+
pitch: "Gartner research note \xB7 probabilistic, watch-list-oriented, every claim carries a confidence + falsifier. For uncertainty-heavy decisions."
|
|
3839
|
+
}
|
|
3840
|
+
];
|
|
3841
|
+
var HOUSE_STYLE_BY_ID = new Map(HOUSE_STYLES.map((s) => [s.id, s]));
|
|
3842
|
+
function resolveHouseStyle(id) {
|
|
3843
|
+
if (id) {
|
|
3844
|
+
const found = HOUSE_STYLE_BY_ID.get(id);
|
|
3845
|
+
if (found) return found;
|
|
3846
|
+
}
|
|
3847
|
+
return HOUSE_STYLE_BY_ID.get("boardroom-default");
|
|
3848
|
+
}
|
|
3849
|
+
function pickIndex(seed, kind, n) {
|
|
3850
|
+
if (n <= 1) return 0;
|
|
3851
|
+
const s = (seed === void 0 ? "" : String(seed)) + "::" + kind;
|
|
3852
|
+
let h = 5381;
|
|
3853
|
+
for (let i = 0; i < s.length; i++) {
|
|
3854
|
+
h = (h << 5) + h ^ s.charCodeAt(i);
|
|
3855
|
+
}
|
|
3856
|
+
return Math.abs(h | 0) % n;
|
|
3857
|
+
}
|
|
3858
|
+
function houseStyleLabel(style, kind, lang, seed) {
|
|
3859
|
+
const entry = style.labels[kind];
|
|
3860
|
+
if (!entry) return null;
|
|
3861
|
+
const variants = Array.isArray(entry) ? entry : [entry];
|
|
3862
|
+
if (variants.length === 0) return null;
|
|
3863
|
+
const idx = pickIndex(seed, kind, variants.length);
|
|
3864
|
+
const picked = variants[idx];
|
|
3865
|
+
return lang === "zh" ? picked.zh : picked.en;
|
|
3866
|
+
}
|
|
3867
|
+
function formatHouseStyleCatalog() {
|
|
3868
|
+
return HOUSE_STYLES.map((s) => {
|
|
3869
|
+
const fitsLine = s.fits.length ? ` fits: ${s.fits.join(", ")}` : "";
|
|
3870
|
+
return [
|
|
3871
|
+
` \xB7 \`${s.id}\` \xB7 ${s.label}`,
|
|
3872
|
+
` ${s.pitch}`,
|
|
3873
|
+
fitsLine
|
|
3874
|
+
].filter((l) => l.trim()).join("\n");
|
|
3875
|
+
}).join("\n");
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3193
3878
|
// src/ai/prompts/brief-stages.ts
|
|
3194
3879
|
var EVIDENCE_LENSES = [
|
|
3195
3880
|
"data",
|
|
@@ -3299,11 +3984,14 @@ var SCAFFOLD_SYSTEM = [
|
|
|
3299
3984
|
'7. **Positions** \xB7 2\u20133 named camps. Short evocative label ("The Skeptics", "The Long-Horizon Camp"), one-sentence collective claim, director ids, supporting signal refs. A director appears in only one camp.',
|
|
3300
3985
|
"",
|
|
3301
3986
|
"8. **Visuals** \xB7 0\u20134 blocks. Content-driven. Pick from:",
|
|
3302
|
-
" \xB7 `comparison-table`
|
|
3303
|
-
" \xB7 `quadrant-chart`
|
|
3304
|
-
" \xB7 `force-field`
|
|
3305
|
-
" \xB7 `strengths-cautions
|
|
3306
|
-
"
|
|
3987
|
+
" \xB7 `comparison-table` \u2014 \u2265 2 named options compared on shared dimensions (text matrix)",
|
|
3988
|
+
" \xB7 `quadrant-chart` \u2014 items plotted on two real axes (mermaid quadrantChart)",
|
|
3989
|
+
" \xB7 `force-field` \u2014 drivers vs resistors of one outcome (text two-column)",
|
|
3990
|
+
" \xB7 `strengths-cautions`\u2014 strengths / cautions / verdict per option (text matrix)",
|
|
3991
|
+
" \xB7 `bar-chart` \u2014 2\u20138 named items ranked by ONE quantitative dimension (mermaid xychart-beta \xB7 cost / support / size / time)",
|
|
3992
|
+
" \xB7 `timeline` \u2014 3\u20138 dated points telling a narrative arc (mermaid timeline \xB7 retro / historical analogue / projected sequence)",
|
|
3993
|
+
" \xB7 `pie-chart` \u2014 2\u20136 slices showing a distribution (mermaid pie \xB7 scenario probabilities / lens shares / vote tallies / market mix). Numbers can be percentages OR raw counts \u2014 mermaid normalises.",
|
|
3994
|
+
" Strong rule: if the discussion contained ANY ranked numeric measure across items \u2192 bar-chart. ANY chronological sequence \u2265 3 events \u2192 timeline. ANY distribution that sums (probability split, votes, lens count, market share) \u2192 pie-chart. These three are massively higher information density than the equivalent prose.",
|
|
3307
3995
|
"",
|
|
3308
3996
|
'9. **Recommendations** \xB7 3\u20135 concrete actions, each with: `priority` (P0/P1/P2), `action` (imperative), `rationale`, `ownerType`, `horizon` (e.g. "next 30 days"), `successMetric` (observable proof of execution), `riskIfSkipped`. Recommendations are imperatives \u2014 "Do X" not "X should happen".',
|
|
3309
3997
|
"",
|
|
@@ -3372,7 +4060,10 @@ var SCAFFOLD_SYSTEM = [
|
|
|
3372
4060
|
' { "type": "comparison-table", "title": "...", "rowLabel": "Option", "columns": ["Speed", "Risk", "Cost"], "rows": [ { "name": "Option A", "cells": ["Fast", "High", "Low"] } ] },',
|
|
3373
4061
|
' { "type": "quadrant-chart", "title": "...", "xLabel": "Effort", "yLabel": "Impact", "q1": "Quick wins", "q2": "Major projects", "q3": "Fill-ins", "q4": "Thankless tasks", "items": [ { "label": "Idea A", "x": 0.7, "y": 0.8 } ] },',
|
|
3374
4062
|
' { "type": "force-field", "title": "...", "drivers": ["..."], "resistors": ["..."] },',
|
|
3375
|
-
' { "type": "strengths-cautions", "title": "...", "rows": [ { "option": "Option A", "strengths": ["..."], "cautions": ["..."], "verdict": "recommended" } ] }',
|
|
4063
|
+
' { "type": "strengths-cautions", "title": "...", "rows": [ { "option": "Option A", "strengths": ["..."], "cautions": ["..."], "verdict": "recommended" } ] },',
|
|
4064
|
+
' { "type": "bar-chart", "title": "Estimated time-to-ship", "yLabel": "Months to ship", "unit": "mo", "bars": [ { "label": "Option A", "value": 6 }, { "label": "Option B", "value": 14 } ] },',
|
|
4065
|
+
' { "type": "timeline", "title": "How the platform reached parity", "points": [ { "period": "2019", "label": "First open weights ship", "description": "..." }, { "period": "2022", "label": "...", "description": "" } ] },',
|
|
4066
|
+
' { "type": "pie-chart", "title": "Scenario probability split", "slices": [ { "label": "Base", "value": 55 }, { "label": "Upside", "value": 25 }, { "label": "Downside", "value": 20 } ] }',
|
|
3376
4067
|
" ],",
|
|
3377
4068
|
' "recommendations": [',
|
|
3378
4069
|
' { "priority": "P0", "action": "Imperative concrete action.", "rationale": "Why this works.", "ownerType": "platform team", "horizon": "next 30 days", "successMetric": "Observable proof.", "riskIfSkipped": "What goes wrong.", "criticalDependency": "What MUST be true for this to work \u2014 the load-bearing pre-condition. Forces stress-testing." }',
|
|
@@ -3414,7 +4105,16 @@ var SCAFFOLD_SYSTEM = [
|
|
|
3414
4105
|
" },",
|
|
3415
4106
|
' "leadingIndicators": [',
|
|
3416
4107
|
' { "signal": "What to watch.", "threshold": "Threshold or pattern that flips the read.", "cadence": "weekly", "flipsTo": "Which scenario this confirms or which assumption it falsifies." }',
|
|
3417
|
-
" ]",
|
|
4108
|
+
" ],",
|
|
4109
|
+
' "threatsToValidity": [',
|
|
4110
|
+
` { "category": "Selection bias", "threat": "1-2 sentences naming WHAT about the analysis could mislead.", "observable": "What you'd see if this threat is realized.", "severity": "high", "mitigation": "What would address it (or null)." }`,
|
|
4111
|
+
" ],",
|
|
4112
|
+
' "metricStrip": {',
|
|
4113
|
+
` "intro": "Single sentence framing the strip ('Three numbers worth pricing in' / 'By the numbers').",`,
|
|
4114
|
+
' "cards": [',
|
|
4115
|
+
` { "label": "\u2264 60 chars \xB7 what this number measures.", "value": "\u2264 24 chars \xB7 the number-like reading ('\u2264 8%', '18 mo').", "qualifier": "Optional \xB7 \u2264 80 chars context ('of total ARR').", "trend": "up | down | flat | null", "attribution": "Optional \xB7 which director / lens (\u2264 80 chars)." }`,
|
|
4116
|
+
" ]",
|
|
4117
|
+
" }",
|
|
3418
4118
|
"}",
|
|
3419
4119
|
"```",
|
|
3420
4120
|
"",
|
|
@@ -3446,7 +4146,7 @@ var SCAFFOLD_SYSTEM = [
|
|
|
3446
4146
|
" \xB7 `the-bet` \u2192 fill `theBet: { ifBacked, conditions[3-5], killCriteria }`. Leave others.",
|
|
3447
4147
|
" \xB7 `considerations` \u2192 fill `considerations` with the SAME shape as `recommendations` (3-5 items, P0/P1/P2, owner, horizon, success metric, risk-if-skipped). Voice should be hedged in the prose (we'll worry about voice at write time; the data shape is identical).",
|
|
3448
4148
|
"",
|
|
3449
|
-
"Optional kinds (`frame-shift`, `convergence`, `divergence`, `positions`, `visuals`, `two-paths`, `why-now`, `pre-mortem`, `new-questions`, `planning-assumption`, `open-questions`, `strategic-outlook`, `critical-assumptions`, `scenario-tree`, `leading-indicators`): when listed in the picked set, fill them as the spec above describes. When NOT listed, set them to the empty value (`[]` for arrays, `null` for nullable objects, `{shifted:false, original:'', reframed:'', trigger:''}` for frameShift).",
|
|
4149
|
+
"Optional kinds (`frame-shift`, `convergence`, `divergence`, `positions`, `visuals`, `two-paths`, `why-now`, `pre-mortem`, `new-questions`, `planning-assumption`, `open-questions`, `strategic-outlook`, `critical-assumptions`, `scenario-tree`, `leading-indicators`, `threats-to-validity`, `metric-strip`): when listed in the picked set, fill them as the spec above describes. When NOT listed, set them to the empty value (`[]` for arrays, `null` for nullable objects, `{shifted:false, original:'', reframed:'', trigger:''}` for frameShift).",
|
|
3450
4150
|
"",
|
|
3451
4151
|
"## Substitute schemas (when picked)",
|
|
3452
4152
|
"",
|
|
@@ -3542,6 +4242,37 @@ var SCAFFOLD_SYSTEM = [
|
|
|
3542
4242
|
"```",
|
|
3543
4243
|
"Sum of `probability` across branches must be 95\u2013105 (drift to 100 \xB1 5 allowed).",
|
|
3544
4244
|
"",
|
|
4245
|
+
"`threatsToValidity` (3\u20135 ways the *analysis itself* could be wrong \xB7 Stanford-grade self-criticism):",
|
|
4246
|
+
"```json",
|
|
4247
|
+
"[",
|
|
4248
|
+
" {",
|
|
4249
|
+
' "category": "\u2264 50 chars \xB7 concrete category name (e.g. \\"Selection bias\\", \\"Generalizability ceiling\\", \\"Construct validity\\", \\"Confounding factor\\", \\"Sample of N=1\\", \\"Lens blind spot\\", \\"Survivorship\\", \\"Anchoring on the loudest director\\"). Pick a NAMED category \u2014 not a free-form essay.",',
|
|
4250
|
+
' "threat": "1-2 sentences (\u2264 280 chars) naming WHAT about the *analysis itself* could mislead. Distinct from pre-mortem (how the recommended action could fail) and from critical-assumptions (the assumptions the brief rests on).",',
|
|
4251
|
+
' "observable": "What you would see if this threat is realized (\u2264 200 chars). Without an observable, a threat is just a hedge \u2014 it must be falsifiable.",',
|
|
4252
|
+
' "severity": "low | medium | high",',
|
|
4253
|
+
' "mitigation": "What would address or defuse this threat (\u2264 200 chars). Set null when the room had no concrete mitigation."',
|
|
4254
|
+
" }",
|
|
4255
|
+
"]",
|
|
4256
|
+
"```",
|
|
4257
|
+
'Threats name limits of the analysis, not limits of the conclusion. "The recommendation might fail if X" is pre-mortem material; "our analysis only consulted Western strategy directors so the conclusion may not generalize" is a threat to validity. Pick at most 5; below 3 reads as token effort, above 5 turns into noise.',
|
|
4258
|
+
"",
|
|
4259
|
+
"`metricStrip` (3\u20135 dashboard-style KPI cards \xB7 the room's quantitative reads side-by-side):",
|
|
4260
|
+
"```json",
|
|
4261
|
+
"{",
|
|
4262
|
+
' "intro": "Single sentence framing the strip \xB7 \u2264 200 chars (e.g. \\"Three numbers worth pricing in\\", \\"By the numbers\\"). Empty string is fine when the section heading already does the framing.",',
|
|
4263
|
+
' "cards": [',
|
|
4264
|
+
" {",
|
|
4265
|
+
' "label": "\u2264 60 chars \xB7 what this number measures (e.g. \\"API revenue at risk\\", \\"Window before parity\\", \\"Convergence rate among directors\\"). Concrete, scannable.",',
|
|
4266
|
+
' "value": "\u2264 24 chars \xB7 the number-like reading. Strings, not numbers \u2014 preserves ranges (\\"\u2264 8%\\", \\"18\u201324 mo\\"), inequalities (\\"> 100\xD7\\"), and CJK units (\\"\u2248 \u4E09\u4E2A\u5B63\u5EA6\\"). The eye-catch.",',
|
|
4267
|
+
' "qualifier": "Optional \xB7 \u2264 80 chars context (\\"of total ARR\\", \\"if no leak\\", \\"in the base case\\"). Set null when the value stands alone.",',
|
|
4268
|
+
' "trend": "up | down | flat | null \xB7 directional read. Null when the value is a level, not a direction.",',
|
|
4269
|
+
' "attribution": "Optional \xB7 which director / lens generated the number (\\"First Principles \xB7 data\\"). \u2264 80 chars. Null acceptable but PREFER providing one \u2014 it makes the multi-director sourcing visible the way Headline Findings does."',
|
|
4270
|
+
" }",
|
|
4271
|
+
" ]",
|
|
4272
|
+
"}",
|
|
4273
|
+
"```",
|
|
4274
|
+
"Pick metric-strip whenever the room produced \u2265 3 quantitative claims worth surfacing as a row of cards (percentages, time windows, ratios, counts, ranges). Each card holds ONE number \u2014 never bury two numbers in one value. Distinct from `leadingIndicators` (which is a forward-looking watch-list with thresholds + cadence) \u2014 metric-strip carries the room's READS as numbers right now.",
|
|
4275
|
+
"",
|
|
3545
4276
|
"`leadingIndicators` (3\u20135 monitoring signals):",
|
|
3546
4277
|
"```json",
|
|
3547
4278
|
"[",
|
|
@@ -3580,7 +4311,11 @@ function pickedBlock(picked) {
|
|
|
3580
4311
|
"strategic-outlook",
|
|
3581
4312
|
"critical-assumptions",
|
|
3582
4313
|
"scenario-tree",
|
|
3583
|
-
"leading-indicators"
|
|
4314
|
+
"leading-indicators",
|
|
4315
|
+
// Stanford-research self-criticism block
|
|
4316
|
+
"threats-to-validity",
|
|
4317
|
+
// Dashboard-style indicator strip
|
|
4318
|
+
"metric-strip"
|
|
3584
4319
|
]);
|
|
3585
4320
|
const set = new Set(picked.filter((k) => allKnown.has(k)));
|
|
3586
4321
|
if (!set.size) return "";
|
|
@@ -3740,6 +4475,57 @@ var WRITE_SYSTEM = [
|
|
|
3740
4475
|
" ### {title}",
|
|
3741
4476
|
" A markdown table with columns `Option | Strengths | Cautions | Verdict`. Each row's Strengths/Cautions cells are bullet-separated (\xB7 between items). Verdict markers: `recommended` \u2192 **Recommended** \xB7 `caution` \u2192 \u26A0 **Caution required** \xB7 `not-recommended` \u2192 **Not recommended**.",
|
|
3742
4477
|
"",
|
|
4478
|
+
" For `bar-chart`:",
|
|
4479
|
+
" ### {title}",
|
|
4480
|
+
" Render a fenced ```mermaid block with `xychart-beta` (mermaid 10+ stable). Strict shape so the lexer doesn't reject:",
|
|
4481
|
+
" ```",
|
|
4482
|
+
" xychart-beta",
|
|
4483
|
+
' title "{title}"',
|
|
4484
|
+
' x-axis ["{bar.label}", "{bar.label}", ...]',
|
|
4485
|
+
' y-axis "{yLabel}"',
|
|
4486
|
+
" bar [{bar.value}, {bar.value}, ...]",
|
|
4487
|
+
" ```",
|
|
4488
|
+
" Hard rules:",
|
|
4489
|
+
" \xB7 `x-axis` is a literal JSON-style array of DOUBLE-QUOTED labels, comma-separated. No bare strings. CJK is fine inside the quotes.",
|
|
4490
|
+
" \xB7 Inside any quoted label: NO double-quote, NO `:`, NO `[`, NO `]`. Replace with ` - ` if needed.",
|
|
4491
|
+
" \xB7 `bar` values are bare numbers, in the same order as x-axis labels. Match counts (lexer fails on mismatch).",
|
|
4492
|
+
" \xB7 `title` is double-quoted. ASCII parens only \u2014 replace fullwidth `\uFF08\uFF09` with halfwidth `()`.",
|
|
4493
|
+
" \xB7 2\u20138 bars. Below 2 isn't a comparison; above 8 stops being scannable.",
|
|
4494
|
+
" \xB7 NO blank lines inside the fenced block. Indent body lines 4 spaces.",
|
|
4495
|
+
"",
|
|
4496
|
+
" For `timeline`:",
|
|
4497
|
+
" ### {title}",
|
|
4498
|
+
" Render a fenced ```mermaid block with `timeline`. Strict shape:",
|
|
4499
|
+
" ```",
|
|
4500
|
+
" timeline",
|
|
4501
|
+
" title {title}",
|
|
4502
|
+
" {period} : {label} : {description}",
|
|
4503
|
+
" ```",
|
|
4504
|
+
" Hard rules:",
|
|
4505
|
+
" \xB7 `title` is plain text on its own line \u2014 NO quotes (mermaid timeline syntax differs from xychart). One line. ASCII parens only. NO `:` inside the title.",
|
|
4506
|
+
" \xB7 One point per line: `{period} : {label} : {description}` \u2014 colons are the field separators, so labels / descriptions cannot contain `:`. Replace with ` \u2014 ` if needed.",
|
|
4507
|
+
' \xB7 Period (e.g. "2019", "Q3 2024", "Today") is the column header rendered above the dot.',
|
|
4508
|
+
" \xB7 Description is optional \xB7 when scaffold.description is empty, use the 2-segment form: `{period} : {label}` (no trailing colon, no empty third segment \u2014 mermaid 11.0+ rejects empty fields).",
|
|
4509
|
+
" \xB7 3\u20138 points. Below 3 reads as a stub; above 8 the strip overflows.",
|
|
4510
|
+
" \xB7 NO blank lines inside the fenced block. Indent body lines 4 spaces.",
|
|
4511
|
+
"",
|
|
4512
|
+
" For `pie-chart`:",
|
|
4513
|
+
" ### {title}",
|
|
4514
|
+
" Render a fenced ```mermaid block with `pie showData` so each slice's number is printed in the legend:",
|
|
4515
|
+
" ```",
|
|
4516
|
+
" pie showData",
|
|
4517
|
+
" title {title}",
|
|
4518
|
+
' "{slice.label}" : {slice.value}',
|
|
4519
|
+
' "{slice.label}" : {slice.value}',
|
|
4520
|
+
" ```",
|
|
4521
|
+
" Hard rules:",
|
|
4522
|
+
" \xB7 `title` is plain text \u2014 NO quotes. ASCII parens only. NO `:` inside the title.",
|
|
4523
|
+
' \xB7 Each slice is `"{label}" : {number}` \u2014 label DOUBLE-QUOTED, value bare number. The literal colon between them is required.',
|
|
4524
|
+
' \xB7 Labels: NO `"`, NO `:`, NO `[`, NO `]` inside. Replace with ` - ` if needed.',
|
|
4525
|
+
" \xB7 Values can be percentages summing to ~100 OR raw counts \u2014 mermaid normalises. Keep 2 decimals max.",
|
|
4526
|
+
" \xB7 2\u20136 slices. Pies with > 6 slices stop being readable.",
|
|
4527
|
+
" \xB7 NO blank lines inside the fenced block. Indent body lines 4 spaces.",
|
|
4528
|
+
"",
|
|
3743
4529
|
" ## Recommendations",
|
|
3744
4530
|
" Skip if `recommendations` is empty. Otherwise render as a numbered list, one per recommendation, sorted by priority. Each item:",
|
|
3745
4531
|
" 1. **`P0`** **{action}**",
|
|
@@ -3869,6 +4655,37 @@ var WRITE_SYSTEM = [
|
|
|
3869
4655
|
" | {signal} | {threshold} | {cadence} | {flipsTo} |",
|
|
3870
4656
|
" Each row is one indicator. Keep cell content tight \u2014 the value is in seeing all 3-5 indicators side-by-side as a watch-list, not in prose elaboration.",
|
|
3871
4657
|
"",
|
|
4658
|
+
" ### threatsToValidity (Stanford-style \xB7 how the analysis itself could be wrong)",
|
|
4659
|
+
" When `scaffold.threatsToValidity` is non-empty AND was picked, render it AFTER critical-assumptions (or AFTER findings when no critical-assumptions). This section is the room's intellectual honesty made structural \u2014 not an appendix caveat.",
|
|
4660
|
+
" ## Threats to Validity",
|
|
4661
|
+
' Open with one sentence framing the section: "The analysis below could be wrong in named ways. These are the threats \u2014 each is concrete, observable, and (where possible) mitigable."',
|
|
4662
|
+
" Then a markdown table with columns `Category | Threat | Observable | Severity | Mitigation`. One row per item, sorted by severity (high \u2192 medium \u2192 low). Render severity as **`High`** / **`Medium`** / **`Low`** (literal backticked, bolded). The `Mitigation` cell is the literal string `\u2014` when scaffold.mitigation is null.",
|
|
4663
|
+
" | Category | Threat | Observable | Severity | Mitigation |",
|
|
4664
|
+
" | --- | --- | --- | --- | --- |",
|
|
4665
|
+
" | {category} | {threat} | {observable} | **`Severity`** | {mitigation or \u2014} |",
|
|
4666
|
+
" Don't pad the section with prose \u2014 the table IS the section. The voice register from the picked house style applies, but the table structure stays identical across styles. Threats here name the limits of the *analysis*, not the limits of the *recommendation* (that's pre-mortem).",
|
|
4667
|
+
"",
|
|
4668
|
+
" ### metricStrip (dashboard \xB7 the room's numbers as a row of KPI cards)",
|
|
4669
|
+
" When `scaffold.metricStrip` is non-null AND was picked, render it as the report's first quantitative beat \u2014 natural slot is RIGHT AFTER the anchor (Bottom Line / Thesis / Working Hypothesis), so a reader skimming the top of the report sees the headline judgement followed immediately by the numbers behind it. Acceptable alternative slot: right before Recommendations, when the numbers frame the action rather than the judgement.",
|
|
4670
|
+
" Heading from the house style (default `## By the Numbers`).",
|
|
4671
|
+
" Then emit a fenced code block with language tag `metric-strip` whose body is STRICT JSON. The report renderer detects this block and emits the styled card grid (mirrors how ```mermaid is handled today). Format:",
|
|
4672
|
+
" ```metric-strip",
|
|
4673
|
+
" {",
|
|
4674
|
+
' "intro": "Three numbers worth pricing in",',
|
|
4675
|
+
' "cards": [',
|
|
4676
|
+
' { "label": "API revenue at risk", "value": "\u2264 8%", "qualifier": "of total ARR", "attribution": "First Principles \xB7 data" },',
|
|
4677
|
+
' { "label": "Window before parity", "value": "18 mo", "trend": "down", "qualifier": "unless training data leaks" },',
|
|
4678
|
+
' { "label": "Convergence rate", "value": "2 of 3", "qualifier": "directors at high confidence" }',
|
|
4679
|
+
" ]",
|
|
4680
|
+
" }",
|
|
4681
|
+
" ```",
|
|
4682
|
+
" Hard rules:",
|
|
4683
|
+
" \xB7 The block opens with the literal three backticks + `metric-strip` and closes with three backticks on a line by itself. Just like mermaid blocks.",
|
|
4684
|
+
" \xB7 Body is one JSON object with `intro` (string, may be empty) and `cards` (array of 3\u20135 objects).",
|
|
4685
|
+
' \xB7 Each card object: `label` (required string), `value` (required string), `qualifier` (optional string \xB7 omit key OR set null when empty), `trend` (optional \xB7 one of `"up"` / `"down"` / `"flat"` \xB7 omit key when null), `attribution` (optional string).',
|
|
4686
|
+
" \xB7 Mirror the scaffold.metricStrip values 1:1. Don't invent extra cards; don't drop cards the scaffold supplied.",
|
|
4687
|
+
" \xB7 Don't pad the section with surrounding prose. The cards carry the section.",
|
|
4688
|
+
"",
|
|
3872
4689
|
" ### twoPaths (multi-perspective / comparison alternative)",
|
|
3873
4690
|
" When `scaffold.twoPaths` is non-null AND was picked, render in place of (or alongside) `## Options Analysis`:",
|
|
3874
4691
|
" ## Two Paths",
|
|
@@ -3913,8 +4730,76 @@ function renderSignalRef(ref, perDirectorSignals) {
|
|
|
3913
4730
|
}
|
|
3914
4731
|
return `[${ref}] (missing)`;
|
|
3915
4732
|
}
|
|
4733
|
+
var DEFAULT_KIND_LABELS = {
|
|
4734
|
+
"bottom-line": "Bottom Line",
|
|
4735
|
+
"thesis": "The Thesis",
|
|
4736
|
+
"working-hypothesis": "A working hypothesis",
|
|
4737
|
+
"frame-shift": "Frame Shift",
|
|
4738
|
+
"strategic-outlook": "Strategic Outlook",
|
|
4739
|
+
"headline-findings": "Headline Findings",
|
|
4740
|
+
"big-ideas": "Three Big Ideas",
|
|
4741
|
+
"critical-assumptions": "Critical Assumptions",
|
|
4742
|
+
"convergence": "Where We Converged",
|
|
4743
|
+
"divergence": "Where We Diverged",
|
|
4744
|
+
"positions": "Positions",
|
|
4745
|
+
"visuals": "Options Analysis",
|
|
4746
|
+
"two-paths": "Two Paths",
|
|
4747
|
+
"why-now": "Why Now",
|
|
4748
|
+
"recommendations": "Recommendations",
|
|
4749
|
+
"the-bet": "The Bet",
|
|
4750
|
+
"considerations": "Considerations",
|
|
4751
|
+
"scenario-tree": "Scenario Tree",
|
|
4752
|
+
"leading-indicators": "Leading Indicators",
|
|
4753
|
+
"threats-to-validity": "Threats to Validity",
|
|
4754
|
+
"metric-strip": "By the Numbers",
|
|
4755
|
+
"pre-mortem": "Pre-mortem",
|
|
4756
|
+
"new-questions": "New Questions This Surfaced",
|
|
4757
|
+
"planning-assumption": "Strategic Planning Assumption",
|
|
4758
|
+
"open-questions": "Open Questions"
|
|
4759
|
+
};
|
|
4760
|
+
function buildHouseStyleAddendum(styleId, language, seed) {
|
|
4761
|
+
const style = resolveHouseStyle(styleId);
|
|
4762
|
+
if (style.id === "boardroom-default") return "";
|
|
4763
|
+
const overrideLines = [];
|
|
4764
|
+
for (const kind of Object.keys(DEFAULT_KIND_LABELS)) {
|
|
4765
|
+
const override = houseStyleLabel(style, kind, language, seed);
|
|
4766
|
+
if (!override) continue;
|
|
4767
|
+
const def = DEFAULT_KIND_LABELS[kind];
|
|
4768
|
+
if (!def) continue;
|
|
4769
|
+
if (override.trim() === def.trim()) continue;
|
|
4770
|
+
overrideLines.push(` \xB7 component=\`${kind}\` \xB7 default \`## ${def}\` \u2192 use \`## ${override}\``);
|
|
4771
|
+
}
|
|
4772
|
+
const voice = language === "zh" ? style.voice.zh : style.voice.en;
|
|
4773
|
+
const lines = [
|
|
4774
|
+
"",
|
|
4775
|
+
"## House style \u2014 applies to THIS brief",
|
|
4776
|
+
"",
|
|
4777
|
+
`Picked: \`${style.id}\` \xB7 ${style.label}`,
|
|
4778
|
+
"",
|
|
4779
|
+
"### Voice register",
|
|
4780
|
+
"",
|
|
4781
|
+
voice,
|
|
4782
|
+
""
|
|
4783
|
+
];
|
|
4784
|
+
if (overrideLines.length > 0) {
|
|
4785
|
+
lines.push(
|
|
4786
|
+
"### Section-heading overrides",
|
|
4787
|
+
"",
|
|
4788
|
+
"When you render the section for one of the component kinds below, use the H2 on the right INSTEAD of the default heading specified earlier in this prompt. The section's body rules (structure, fields, formatting) stay identical \u2014 only the heading text changes. Components not listed here keep their default headings. Do NOT add or drop sections based on this list \u2014 it's purely a rename.",
|
|
4789
|
+
"",
|
|
4790
|
+
...overrideLines,
|
|
4791
|
+
""
|
|
4792
|
+
);
|
|
4793
|
+
}
|
|
4794
|
+
lines.push(
|
|
4795
|
+
"### Override on heading style",
|
|
4796
|
+
"",
|
|
4797
|
+
'The default rule "section headings ARE the takeaway, claim-style only" is RELAXED for house-styled briefs. Use the override label verbatim \u2014 house-style headings are deliberately editorial (e.g. "The Pillars", "Why Now", "Limitations") rather than claim-style. The claim-style discipline still applies to H3 sub-headings inside the section.'
|
|
4798
|
+
);
|
|
4799
|
+
return lines.join("\n");
|
|
4800
|
+
}
|
|
3916
4801
|
function buildWriteMessages(opts) {
|
|
3917
|
-
const { room, members, scaffold, perDirectorSignals, language, picked } = opts;
|
|
4802
|
+
const { room, members, scaffold, perDirectorSignals, language, picked, houseStyle, briefId } = opts;
|
|
3918
4803
|
const directorNameById = new Map(members.map((a) => [a.id, a.name]));
|
|
3919
4804
|
const nameOf = (id) => directorNameById.get(id) || id;
|
|
3920
4805
|
const memberList = members.map((a) => `${a.id} \xB7 ${a.name} (${a.handle}) \u2014 ${a.roleTag}`).join("\n \xB7 ");
|
|
@@ -4142,6 +5027,28 @@ function buildWriteMessages(opts) {
|
|
|
4142
5027
|
` Flips to: ${it.flipsTo}`
|
|
4143
5028
|
].join("\n")
|
|
4144
5029
|
).join("\n\n") : " (no leading indicators \u2014 composer did not pick this component)";
|
|
5030
|
+
const threatsToValidityBlock = scaffold.threatsToValidity && scaffold.threatsToValidity.length ? scaffold.threatsToValidity.map(
|
|
5031
|
+
(t, i) => [
|
|
5032
|
+
` Threat ${i + 1}: ${t.category}`,
|
|
5033
|
+
` Threat: ${t.threat}`,
|
|
5034
|
+
` Observable: ${t.observable}`,
|
|
5035
|
+
` Severity: ${t.severity}`,
|
|
5036
|
+
` Mitigation: ${t.mitigation || "(none)"}`
|
|
5037
|
+
].join("\n")
|
|
5038
|
+
).join("\n\n") : " (no threats-to-validity \u2014 composer did not pick this component)";
|
|
5039
|
+
const metricStripBlock = scaffold.metricStrip && scaffold.metricStrip.cards.length ? [
|
|
5040
|
+
` Intro: ${scaffold.metricStrip.intro || "(none \u2014 the section heading is the framing)"}`,
|
|
5041
|
+
``,
|
|
5042
|
+
...scaffold.metricStrip.cards.map(
|
|
5043
|
+
(c, i) => [
|
|
5044
|
+
` Card ${i + 1}: ${c.label}`,
|
|
5045
|
+
` Value: ${c.value}`,
|
|
5046
|
+
` Qualifier: ${c.qualifier || "(none \u2014 omit the .metric-qualifier div)"}`,
|
|
5047
|
+
` Trend: ${c.trend || "(none \u2014 omit the data-trend attribute)"}`,
|
|
5048
|
+
` Attribution: ${c.attribution || "(none \u2014 omit the .metric-attribution div)"}`
|
|
5049
|
+
].join("\n")
|
|
5050
|
+
)
|
|
5051
|
+
].join("\n") : " (no metric-strip \u2014 composer did not pick this component)";
|
|
4145
5052
|
const pickedNote = picked && picked.length ? [
|
|
4146
5053
|
``,
|
|
4147
5054
|
`\u2500\u2500\u2500 COMPOSER PICKED COMPONENTS \u2500\u2500\u2500`,
|
|
@@ -4151,10 +5058,16 @@ function buildWriteMessages(opts) {
|
|
|
4151
5058
|
``,
|
|
4152
5059
|
`\u2500\u2500\u2500 END PICKED \u2500\u2500\u2500`
|
|
4153
5060
|
].join("\n") : "";
|
|
5061
|
+
const houseStyleAddendum = buildHouseStyleAddendum(houseStyle, language, briefId);
|
|
4154
5062
|
return [
|
|
4155
5063
|
{
|
|
4156
5064
|
role: "system",
|
|
4157
|
-
content: [
|
|
5065
|
+
content: [
|
|
5066
|
+
WRITE_SYSTEM,
|
|
5067
|
+
"",
|
|
5068
|
+
languageInstruction(language),
|
|
5069
|
+
houseStyleAddendum
|
|
5070
|
+
].filter((s) => s).join("\n")
|
|
4158
5071
|
},
|
|
4159
5072
|
{
|
|
4160
5073
|
role: "user",
|
|
@@ -4238,6 +5151,12 @@ function buildWriteMessages(opts) {
|
|
|
4238
5151
|
`## Leading Indicators (Gartner-density)`,
|
|
4239
5152
|
leadingIndicatorsBlock,
|
|
4240
5153
|
``,
|
|
5154
|
+
`## Threats to Validity (Stanford-style self-criticism)`,
|
|
5155
|
+
threatsToValidityBlock,
|
|
5156
|
+
``,
|
|
5157
|
+
`## Metric Strip (dashboard \xB7 KPI cards)`,
|
|
5158
|
+
metricStripBlock,
|
|
5159
|
+
``,
|
|
4241
5160
|
`\u2500\u2500\u2500 END SCAFFOLD \u2500\u2500\u2500`,
|
|
4242
5161
|
pickedNote,
|
|
4243
5162
|
``,
|
|
@@ -4251,7 +5170,7 @@ function buildWriteMessages(opts) {
|
|
|
4251
5170
|
`\u2500\u2500\u2500 END SUPPLEMENT \u2500\u2500\u2500`,
|
|
4252
5171
|
``
|
|
4253
5172
|
] : [],
|
|
4254
|
-
`Write the final report now. Markdown only. Start with the H2 title \u2014 no preamble. Replace director ids with display names from the directors list above. Follow the section order: Bottom Line / Thesis / Working Hypothesis (anchor) \u2192 Strategic Outlook (when picked) \u2192 Frame Shift \u2192 Headline Findings (or Big Ideas) \u2192 Where We Converged \u2192 Where We Diverged \u2192 Positions \u2192 Options Analysis / Two Paths \u2192 Critical Assumptions (when picked) \u2192 Scenario Tree (when picked) \u2192 Why Now (when picked) \u2192 Recommendations / The Bet / Considerations (action) \u2192 Leading Indicators (when picked) \u2192 Pre-mortem \u2192 New Questions This Surfaced \u2192 Strategic Planning Assumption \u2192 Open Questions.`
|
|
5173
|
+
`Write the final report now. Markdown only (the metricStrip block is the only embedded HTML \u2014 every other section is markdown). Start with the H2 title \u2014 no preamble. Replace director ids with display names from the directors list above. Follow the section order: Bottom Line / Thesis / Working Hypothesis (anchor) \u2192 Metric Strip (when picked) \u2192 Strategic Outlook (when picked) \u2192 Frame Shift \u2192 Headline Findings (or Big Ideas) \u2192 Where We Converged \u2192 Where We Diverged \u2192 Positions \u2192 Options Analysis / Two Paths \u2192 Critical Assumptions (when picked) \u2192 Threats to Validity (when picked) \u2192 Scenario Tree (when picked) \u2192 Why Now (when picked) \u2192 Recommendations / The Bet / Considerations (action) \u2192 Leading Indicators (when picked) \u2192 Pre-mortem \u2192 New Questions This Surfaced \u2192 Strategic Planning Assumption \u2192 Open Questions.`
|
|
4255
5174
|
].join("\n")
|
|
4256
5175
|
}
|
|
4257
5176
|
];
|
|
@@ -4529,6 +5448,56 @@ function parseVisual(raw) {
|
|
|
4529
5448
|
if (!rows.length) return null;
|
|
4530
5449
|
return { type: "strengths-cautions", title, rows };
|
|
4531
5450
|
}
|
|
5451
|
+
if (type === "bar-chart") {
|
|
5452
|
+
const yLabel = typeof o.yLabel === "string" ? o.yLabel.trim() : "Value";
|
|
5453
|
+
const unit = typeof o.unit === "string" ? o.unit.trim() : "";
|
|
5454
|
+
const barsRaw = Array.isArray(o.bars) ? o.bars : [];
|
|
5455
|
+
const bars = [];
|
|
5456
|
+
for (const b of barsRaw) {
|
|
5457
|
+
if (!b || typeof b !== "object") continue;
|
|
5458
|
+
const bo = b;
|
|
5459
|
+
const label = typeof bo.label === "string" ? bo.label.trim() : "";
|
|
5460
|
+
const valueRaw = bo.value;
|
|
5461
|
+
const value = typeof valueRaw === "number" && Number.isFinite(valueRaw) ? valueRaw : null;
|
|
5462
|
+
if (!label || value === null) continue;
|
|
5463
|
+
bars.push({ label, value });
|
|
5464
|
+
if (bars.length >= 8) break;
|
|
5465
|
+
}
|
|
5466
|
+
if (bars.length < 2) return null;
|
|
5467
|
+
return { type: "bar-chart", title, yLabel, unit, bars };
|
|
5468
|
+
}
|
|
5469
|
+
if (type === "timeline") {
|
|
5470
|
+
const pointsRaw = Array.isArray(o.points) ? o.points : [];
|
|
5471
|
+
const points = [];
|
|
5472
|
+
for (const p of pointsRaw) {
|
|
5473
|
+
if (!p || typeof p !== "object") continue;
|
|
5474
|
+
const po = p;
|
|
5475
|
+
const period = typeof po.period === "string" ? po.period.trim() : "";
|
|
5476
|
+
const label = typeof po.label === "string" ? po.label.trim() : "";
|
|
5477
|
+
const description = typeof po.description === "string" ? po.description.trim() : "";
|
|
5478
|
+
if (!period || !label) continue;
|
|
5479
|
+
points.push({ period, label, description });
|
|
5480
|
+
if (points.length >= 8) break;
|
|
5481
|
+
}
|
|
5482
|
+
if (points.length < 3) return null;
|
|
5483
|
+
return { type: "timeline", title, points };
|
|
5484
|
+
}
|
|
5485
|
+
if (type === "pie-chart") {
|
|
5486
|
+
const slicesRaw = Array.isArray(o.slices) ? o.slices : [];
|
|
5487
|
+
const slices = [];
|
|
5488
|
+
for (const s of slicesRaw) {
|
|
5489
|
+
if (!s || typeof s !== "object") continue;
|
|
5490
|
+
const so = s;
|
|
5491
|
+
const label = typeof so.label === "string" ? so.label.trim() : "";
|
|
5492
|
+
const valueRaw = so.value;
|
|
5493
|
+
const value = typeof valueRaw === "number" && Number.isFinite(valueRaw) && valueRaw >= 0 ? valueRaw : null;
|
|
5494
|
+
if (!label || value === null) continue;
|
|
5495
|
+
slices.push({ label, value });
|
|
5496
|
+
if (slices.length >= 6) break;
|
|
5497
|
+
}
|
|
5498
|
+
if (slices.length < 2) return null;
|
|
5499
|
+
return { type: "pie-chart", title, slices };
|
|
5500
|
+
}
|
|
4532
5501
|
return null;
|
|
4533
5502
|
}
|
|
4534
5503
|
function parseVisuals(raw) {
|
|
@@ -4726,6 +5695,155 @@ function parseTwoPathPanel(raw) {
|
|
|
4726
5695
|
if (!label || !body) return null;
|
|
4727
5696
|
return { label, body };
|
|
4728
5697
|
}
|
|
5698
|
+
function parseStrategicOutlook(raw) {
|
|
5699
|
+
if (!raw || typeof raw !== "object") return null;
|
|
5700
|
+
const o = raw;
|
|
5701
|
+
const context = typeof o.context === "string" ? o.context.trim() : "";
|
|
5702
|
+
const implication = typeof o.implication === "string" ? o.implication.trim() : "";
|
|
5703
|
+
if (!context || !implication) return null;
|
|
5704
|
+
return { context, implication };
|
|
5705
|
+
}
|
|
5706
|
+
function parseCriticalAssumption(raw) {
|
|
5707
|
+
if (!raw || typeof raw !== "object") return null;
|
|
5708
|
+
const o = raw;
|
|
5709
|
+
const statement = typeof o.statement === "string" ? o.statement.trim() : "";
|
|
5710
|
+
const falsifier = typeof o.falsifier === "string" ? o.falsifier.trim() : "";
|
|
5711
|
+
if (!statement || !falsifier) return null;
|
|
5712
|
+
const horizon = typeof o.horizon === "string" ? o.horizon.trim() : "";
|
|
5713
|
+
const attribution = typeof o.attribution === "string" ? o.attribution.trim() : "";
|
|
5714
|
+
return {
|
|
5715
|
+
statement,
|
|
5716
|
+
confidence: parseConfidence(o.confidence),
|
|
5717
|
+
falsifier,
|
|
5718
|
+
horizon,
|
|
5719
|
+
attribution
|
|
5720
|
+
};
|
|
5721
|
+
}
|
|
5722
|
+
function parseCriticalAssumptions(raw) {
|
|
5723
|
+
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
5724
|
+
const out = [];
|
|
5725
|
+
for (const entry of raw) {
|
|
5726
|
+
const a = parseCriticalAssumption(entry);
|
|
5727
|
+
if (a) out.push(a);
|
|
5728
|
+
if (out.length >= 6) break;
|
|
5729
|
+
}
|
|
5730
|
+
return out.length ? out : null;
|
|
5731
|
+
}
|
|
5732
|
+
function parseScenarioBranch(raw) {
|
|
5733
|
+
if (!raw || typeof raw !== "object") return null;
|
|
5734
|
+
const o = raw;
|
|
5735
|
+
const label = typeof o.label === "string" ? o.label.trim() : "";
|
|
5736
|
+
const trigger = typeof o.trigger === "string" ? o.trigger.trim() : "";
|
|
5737
|
+
const decisionImplication = typeof o.decisionImplication === "string" ? o.decisionImplication.trim() : "";
|
|
5738
|
+
if (!label || !trigger) return null;
|
|
5739
|
+
const probabilityRaw = o.probability;
|
|
5740
|
+
const probability = typeof probabilityRaw === "number" && Number.isFinite(probabilityRaw) ? Math.max(0, Math.min(100, Math.round(probabilityRaw))) : 0;
|
|
5741
|
+
const effectsRaw = Array.isArray(o.effects) ? o.effects : [];
|
|
5742
|
+
const effects = [];
|
|
5743
|
+
for (const e of effectsRaw) {
|
|
5744
|
+
if (typeof e !== "string") continue;
|
|
5745
|
+
const t = e.trim();
|
|
5746
|
+
if (t) effects.push(t);
|
|
5747
|
+
if (effects.length >= 4) break;
|
|
5748
|
+
}
|
|
5749
|
+
return { label, probability, trigger, effects, decisionImplication };
|
|
5750
|
+
}
|
|
5751
|
+
function parseScenarioTree(raw) {
|
|
5752
|
+
if (!raw || typeof raw !== "object") return null;
|
|
5753
|
+
const o = raw;
|
|
5754
|
+
const intro = typeof o.intro === "string" ? o.intro.trim() : "";
|
|
5755
|
+
const branchesRaw = Array.isArray(o.branches) ? o.branches : [];
|
|
5756
|
+
const branches = [];
|
|
5757
|
+
for (const b of branchesRaw) {
|
|
5758
|
+
const parsed = parseScenarioBranch(b);
|
|
5759
|
+
if (parsed) branches.push(parsed);
|
|
5760
|
+
if (branches.length >= 4) break;
|
|
5761
|
+
}
|
|
5762
|
+
if (branches.length < 2) return null;
|
|
5763
|
+
return { intro, branches };
|
|
5764
|
+
}
|
|
5765
|
+
function parseLeadingIndicator(raw) {
|
|
5766
|
+
if (!raw || typeof raw !== "object") return null;
|
|
5767
|
+
const o = raw;
|
|
5768
|
+
const signal = typeof o.signal === "string" ? o.signal.trim() : "";
|
|
5769
|
+
const threshold = typeof o.threshold === "string" ? o.threshold.trim() : "";
|
|
5770
|
+
const flipsTo = typeof o.flipsTo === "string" ? o.flipsTo.trim() : "";
|
|
5771
|
+
if (!signal || !threshold || !flipsTo) return null;
|
|
5772
|
+
const cadence = typeof o.cadence === "string" ? o.cadence.trim() : "";
|
|
5773
|
+
return { signal, threshold, cadence, flipsTo };
|
|
5774
|
+
}
|
|
5775
|
+
function parseLeadingIndicators(raw) {
|
|
5776
|
+
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
5777
|
+
const out = [];
|
|
5778
|
+
for (const entry of raw) {
|
|
5779
|
+
const parsed = parseLeadingIndicator(entry);
|
|
5780
|
+
if (parsed) out.push(parsed);
|
|
5781
|
+
if (out.length >= 5) break;
|
|
5782
|
+
}
|
|
5783
|
+
return out.length ? out : null;
|
|
5784
|
+
}
|
|
5785
|
+
function parseThreatSeverity(raw) {
|
|
5786
|
+
if (raw === "high" || raw === "medium" || raw === "low") return raw;
|
|
5787
|
+
return "medium";
|
|
5788
|
+
}
|
|
5789
|
+
function parseThreatToValidity(raw) {
|
|
5790
|
+
if (!raw || typeof raw !== "object") return null;
|
|
5791
|
+
const o = raw;
|
|
5792
|
+
const category = typeof o.category === "string" ? o.category.trim() : "";
|
|
5793
|
+
const threat = typeof o.threat === "string" ? o.threat.trim() : "";
|
|
5794
|
+
const observable = typeof o.observable === "string" ? o.observable.trim() : "";
|
|
5795
|
+
if (!category || !threat || !observable) return null;
|
|
5796
|
+
const severity = parseThreatSeverity(o.severity);
|
|
5797
|
+
const mitRaw = typeof o.mitigation === "string" ? o.mitigation.trim() : "";
|
|
5798
|
+
const mitigation = mitRaw.length > 0 ? mitRaw : null;
|
|
5799
|
+
return { category, threat, observable, severity, mitigation };
|
|
5800
|
+
}
|
|
5801
|
+
function parseThreatsToValidity(raw) {
|
|
5802
|
+
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
5803
|
+
const out = [];
|
|
5804
|
+
for (const entry of raw) {
|
|
5805
|
+
const parsed = parseThreatToValidity(entry);
|
|
5806
|
+
if (parsed) out.push(parsed);
|
|
5807
|
+
if (out.length >= 5) break;
|
|
5808
|
+
}
|
|
5809
|
+
return out.length ? out : null;
|
|
5810
|
+
}
|
|
5811
|
+
function parseMetricTrend(raw) {
|
|
5812
|
+
if (raw === "up" || raw === "down" || raw === "flat") return raw;
|
|
5813
|
+
return null;
|
|
5814
|
+
}
|
|
5815
|
+
function parseMetricCard(raw) {
|
|
5816
|
+
if (!raw || typeof raw !== "object") return null;
|
|
5817
|
+
const o = raw;
|
|
5818
|
+
const label = typeof o.label === "string" ? o.label.trim() : "";
|
|
5819
|
+
const value = typeof o.value === "string" ? o.value.trim() : "";
|
|
5820
|
+
if (!label || !value) return null;
|
|
5821
|
+
const qualifierRaw = typeof o.qualifier === "string" ? o.qualifier.trim() : "";
|
|
5822
|
+
const qualifier = qualifierRaw.length > 0 ? qualifierRaw : null;
|
|
5823
|
+
const attributionRaw = typeof o.attribution === "string" ? o.attribution.trim() : "";
|
|
5824
|
+
const attribution = attributionRaw.length > 0 ? attributionRaw : null;
|
|
5825
|
+
return {
|
|
5826
|
+
label,
|
|
5827
|
+
value,
|
|
5828
|
+
qualifier,
|
|
5829
|
+
trend: parseMetricTrend(o.trend),
|
|
5830
|
+
attribution
|
|
5831
|
+
};
|
|
5832
|
+
}
|
|
5833
|
+
function parseMetricStrip(raw) {
|
|
5834
|
+
if (!raw || typeof raw !== "object") return null;
|
|
5835
|
+
const o = raw;
|
|
5836
|
+
const intro = typeof o.intro === "string" ? o.intro.trim() : "";
|
|
5837
|
+
const cardsRaw = Array.isArray(o.cards) ? o.cards : [];
|
|
5838
|
+
const cards = [];
|
|
5839
|
+
for (const entry of cardsRaw) {
|
|
5840
|
+
const parsed = parseMetricCard(entry);
|
|
5841
|
+
if (parsed) cards.push(parsed);
|
|
5842
|
+
if (cards.length >= 5) break;
|
|
5843
|
+
}
|
|
5844
|
+
if (cards.length < 3) return null;
|
|
5845
|
+
return { intro, cards };
|
|
5846
|
+
}
|
|
4729
5847
|
function parseScaffold(raw, fallbackTitle, fallbackOriginalQuestion) {
|
|
4730
5848
|
const parsed = extractJson3(raw);
|
|
4731
5849
|
if (!parsed) return null;
|
|
@@ -4769,6 +5887,12 @@ function parseScaffold(raw, fallbackTitle, fallbackOriginalQuestion) {
|
|
|
4769
5887
|
preMortem: parsePreMortem(parsed.preMortem),
|
|
4770
5888
|
newQuestions: parseNewQuestions(parsed.newQuestions),
|
|
4771
5889
|
planningAssumption: parsePlanningAssumption(parsed.planningAssumption),
|
|
5890
|
+
strategicOutlook: parseStrategicOutlook(parsed.strategicOutlook),
|
|
5891
|
+
criticalAssumptions: parseCriticalAssumptions(parsed.criticalAssumptions),
|
|
5892
|
+
scenarioTree: parseScenarioTree(parsed.scenarioTree),
|
|
5893
|
+
leadingIndicators: parseLeadingIndicators(parsed.leadingIndicators),
|
|
5894
|
+
threatsToValidity: parseThreatsToValidity(parsed.threatsToValidity),
|
|
5895
|
+
metricStrip: parseMetricStrip(parsed.metricStrip),
|
|
4772
5896
|
openQuestions: parseOpenQuestions(parsed.openQuestions)
|
|
4773
5897
|
};
|
|
4774
5898
|
}
|
|
@@ -4805,7 +5929,23 @@ var COMPONENT_KINDS = [
|
|
|
4805
5929
|
"strategic-outlook",
|
|
4806
5930
|
"critical-assumptions",
|
|
4807
5931
|
"scenario-tree",
|
|
4808
|
-
"leading-indicators"
|
|
5932
|
+
"leading-indicators",
|
|
5933
|
+
// Stanford-research self-criticism · names how the analysis itself
|
|
5934
|
+
// could be wrong (selection bias, generalizability ceiling, lens
|
|
5935
|
+
// blind spots). Distinct from `pre-mortem` (how the recommendation
|
|
5936
|
+
// could fail) and `critical-assumptions` (the foundations the brief
|
|
5937
|
+
// rests on). Highest fit for stanford-research / gartner-research
|
|
5938
|
+
// styles, but a useful add to any brief that wants to surface
|
|
5939
|
+
// intellectual honesty as a load-bearing section instead of an
|
|
5940
|
+
// appendix caveat.
|
|
5941
|
+
"threats-to-validity",
|
|
5942
|
+
// Dashboard-style indicator strip · 3-5 KPI cards (label / value /
|
|
5943
|
+
// qualifier / trend / attribution). The "by the numbers" beat right
|
|
5944
|
+
// after the anchor, before a reader has to swim through the
|
|
5945
|
+
// findings prose. Pick whenever the room produced ≥ 3 quantitative
|
|
5946
|
+
// claims worth surfacing side-by-side. Genuinely visual — emits
|
|
5947
|
+
// raw HTML that each spine styles into a card grid.
|
|
5948
|
+
"metric-strip"
|
|
4809
5949
|
];
|
|
4810
5950
|
var ANCHORS = ["bottom-line", "thesis", "working-hypothesis"];
|
|
4811
5951
|
var FINDINGS = ["headline-findings", "big-ideas"];
|
|
@@ -4820,20 +5960,20 @@ var SPINES = [
|
|
|
4820
5960
|
];
|
|
4821
5961
|
var DEFAULT_PRESET = [
|
|
4822
5962
|
{ kind: "bottom-line", order: 1 },
|
|
4823
|
-
{ kind: "
|
|
4824
|
-
{ kind: "
|
|
4825
|
-
{ kind: "
|
|
4826
|
-
{ kind: "
|
|
4827
|
-
{ kind: "
|
|
4828
|
-
{ kind: "
|
|
4829
|
-
{ kind: "
|
|
4830
|
-
{ kind: "
|
|
4831
|
-
{ kind: "
|
|
4832
|
-
{ kind: "
|
|
5963
|
+
{ kind: "metric-strip", order: 2 },
|
|
5964
|
+
{ kind: "frame-shift", order: 3 },
|
|
5965
|
+
{ kind: "headline-findings", order: 4 },
|
|
5966
|
+
{ kind: "convergence", order: 5 },
|
|
5967
|
+
{ kind: "divergence", order: 6 },
|
|
5968
|
+
{ kind: "positions", order: 7 },
|
|
5969
|
+
{ kind: "visuals", order: 8 },
|
|
5970
|
+
{ kind: "recommendations", order: 9 },
|
|
5971
|
+
{ kind: "pre-mortem", order: 10 },
|
|
5972
|
+
{ kind: "new-questions", order: 11 },
|
|
4833
5973
|
{ kind: "open-questions", order: 12 }
|
|
4834
5974
|
];
|
|
4835
5975
|
var SYSTEM_PROMPT = [
|
|
4836
|
-
"You are the Boardroom report composer. Pick the spine and the components that will produce the most useful brief for THIS specific room.",
|
|
5976
|
+
"You are the Boardroom report composer. Pick (a) the house style, (b) the spine, and (c) the components that will produce the most useful brief for THIS specific room. Pick deliberately \u2014 the same room under different house styles reads as a different document. Avoid defaulting to `boardroom-default` unless the room genuinely doesn't fit any of the named registers.",
|
|
4837
5977
|
"",
|
|
4838
5978
|
"## What you must output",
|
|
4839
5979
|
"",
|
|
@@ -4841,16 +5981,27 @@ var SYSTEM_PROMPT = [
|
|
|
4841
5981
|
"",
|
|
4842
5982
|
"```json",
|
|
4843
5983
|
"{",
|
|
4844
|
-
' "
|
|
5984
|
+
' "house_style": "sequoia-memo",',
|
|
5985
|
+
' "spine": "a16z-thesis",',
|
|
4845
5986
|
' "subject_type": "investment-judgement",',
|
|
4846
5987
|
' "components": [',
|
|
4847
|
-
' { "kind": "
|
|
4848
|
-
' { "kind": "
|
|
5988
|
+
' { "kind": "thesis", "order": 1 },',
|
|
5989
|
+
' { "kind": "why-now", "order": 2 }',
|
|
4849
5990
|
" ],",
|
|
4850
|
-
' "rationale": "\u2264 120 chars \xB7 why this spine +
|
|
5991
|
+
' "rationale": "\u2264 120 chars \xB7 why this house-style + spine + components fit the room"',
|
|
4851
5992
|
"}",
|
|
4852
5993
|
"```",
|
|
4853
5994
|
"",
|
|
5995
|
+
"## House-style catalog (7 presets)",
|
|
5996
|
+
"",
|
|
5997
|
+
formatHouseStyleCatalog(),
|
|
5998
|
+
"",
|
|
5999
|
+
"Picking discipline:",
|
|
6000
|
+
" \xB7 The house style is the most consequential pick. It changes section vocabulary, voice register, and the kind of register the prose should adopt \u2014 investment-memo declarative vs scholarly hedged vs operator-essay narrative. Two reports under different house styles must FEEL different even when components overlap.",
|
|
6001
|
+
" \xB7 Vary across rooms. If two consecutive rooms could plausibly fit the same house style, prefer the one that fits more sharply rather than always defaulting to the safest pick. The point is variety; redundancy across briefs is a failure mode.",
|
|
6002
|
+
" \xB7 Match the spine to the house style by default (each style declares a default spine in its catalog entry above). Override only when the visual register obviously fits a different spine.",
|
|
6003
|
+
"",
|
|
6004
|
+
"",
|
|
4854
6005
|
"## Component catalogue (19 kinds)",
|
|
4855
6006
|
"",
|
|
4856
6007
|
"Anchor (pick EXACTLY one):",
|
|
@@ -4872,7 +6023,8 @@ var SYSTEM_PROMPT = [
|
|
|
4872
6023
|
" \xB7 `convergence` Where directors aligned via independent reasoning paths. Needs \u2265 2 directors via \u2265 2 lenses.",
|
|
4873
6024
|
" \xB7 `divergence` The single hinge where directors split. Skip when the room had no real central tension.",
|
|
4874
6025
|
" \xB7 `positions` 2\u20133 named camps with a pull-quote per camp. Skip when directors didn't cluster.",
|
|
4875
|
-
" \xB7 `visuals` 0\u20134 exhibits
|
|
6026
|
+
" \xB7 `visuals` 0\u20134 visual exhibits. Seven sub-types now: comparison-table / quadrant-chart / force-field / strengths-cautions (the original four \xB7 text matrices + 2-axis plot) PLUS bar-chart (mermaid xychart-beta \xB7 2\u20138 ranked items on one quantity), timeline (mermaid timeline \xB7 3\u20138 dated narrative beats), pie-chart (mermaid pie showData \xB7 2\u20136 distribution slices). Pick liberally \u2014 visuals carry information faster than prose. Triggers: any ranked numeric comparison \u2192 bar-chart; any chronology / historical analogue \u2192 timeline; any distribution that sums (probability split / votes / lens shares / market mix) \u2192 pie-chart; any 2-axis plot \u2192 quadrant-chart; any options matrix \u2192 comparison-table or strengths-cautions; any drivers-vs-resistors framing \u2192 force-field.",
|
|
6027
|
+
" \xB7 `metric-strip` 3\u20135 KPI / indicator cards \xB7 the room's quantitative reads as a dashboard row. Pick whenever \u2265 3 numbers (percentages, time windows, ratios, counts, ranges) showed up worth surfacing side-by-side. Massively higher information density than the same numbers buried in prose \u2014 strongly favoured for investment / market-forecast / strategic-decision briefs.",
|
|
4876
6028
|
" \xB7 `two-paths` Side-by-side trajectory comparison (Path A vs Path B). Pick when the room argued two distinct futures or routes \u2014 punchier than a comparison-table.",
|
|
4877
6029
|
" \xB7 `why-now` Single panel: window opened by what \xB7 window closes when \xB7 the bet implied. For investment / opportunity rooms (a16z-thesis spine).",
|
|
4878
6030
|
" \xB7 `pre-mortem` 2\u20133 failure modes with leading indicators + mitigations. Pair with `recommendations` or `the-bet` when the call is high-stakes.",
|
|
@@ -4885,6 +6037,7 @@ var SYSTEM_PROMPT = [
|
|
|
4885
6037
|
" \xB7 `critical-assumptions` 4\u20136 load-bearing assumptions, each with confidence + falsifier + time horizon. Use when the brief's logic rests on assumptions that could shift \u2014 surfacing them lets the reader stress-test the conclusion.",
|
|
4886
6038
|
" \xB7 `scenario-tree` 2\u20134 named futures (typically Base / Upside / Downside) with probabilities, triggers, effects, decision implications. Use whenever the room argued multiple futures \u2014 richer than `two-paths` because it adds probability + trigger + decision implication per branch.",
|
|
4887
6039
|
" \xB7 `leading-indicators` 3\u20135 signals to monitor with thresholds + cadence + scenario each indicator confirms. Use when the brief tells the reader to wait-and-watch, or when scenarios diverge based on observable signals.",
|
|
6040
|
+
" \xB7 `threats-to-validity` 3\u20135 ways the *analysis itself* could be wrong (selection bias, generalizability ceiling, sample of N, lens blind spot, confounding). Each names a category, the threat in 1-2 sentences, an observable that would prove it realized, severity, and an optional mitigation. Distinct from `pre-mortem` (how the *recommendation* could fail) and from `critical-assumptions` (the foundations the brief rests on). Pull in for stanford-research / gartner-research / any brief where the room had a real moment of intellectual honesty \u2014 surfacing how we could be wrong is a load-bearing section, not a caveat.",
|
|
4888
6041
|
"",
|
|
4889
6042
|
"## Composition rules \xB7 violations are rejected",
|
|
4890
6043
|
"",
|
|
@@ -4894,8 +6047,21 @@ var SYSTEM_PROMPT = [
|
|
|
4894
6047
|
"\xB7 Total components: 5\u201312. Below 5 = thin; above 12 = noise. The new range allows for the Gartner-density blocks when warranted.",
|
|
4895
6048
|
"\xB7 Drop a component if the conversation didn't produce material for it. Empty sections are worse than missing ones.",
|
|
4896
6049
|
"\xB7 Spine \u2260 template. Pick the spine whose voice fits the topic, then pick components independently.",
|
|
4897
|
-
"
|
|
4898
|
-
"
|
|
6050
|
+
"",
|
|
6051
|
+
"## Visualisation discipline \xB7 pick at least one per brief unless genuinely impossible",
|
|
6052
|
+
"",
|
|
6053
|
+
"Reports without any visual component (no `visuals`, no `metric-strip`, no `two-paths`, no `scenario-tree`, no `leading-indicators`) read as walls of text and lose the reader. The floor is: every brief should carry **at least one** component from the visual set. The bar is low \u2014 even a single quadrant-chart, a 3-card metric-strip, or a 2-column two-paths table satisfies this.",
|
|
6054
|
+
" \xB7 If the room produced \u2265 3 quantitative claims (percentages, time windows, ratios) \u2192 `metric-strip` is the strongest pick.",
|
|
6055
|
+
" \xB7 If the room compared \u2265 2 named options/paths \u2192 `visuals` (quadrant-chart or strengths-cautions) is the strongest.",
|
|
6056
|
+
" \xB7 If the room argued multiple futures \u2192 `scenario-tree` (probabilities visible) beats prose every time.",
|
|
6057
|
+
" \xB7 If the brief is a watch-and-wait stance \u2192 `leading-indicators` carries the structure.",
|
|
6058
|
+
" \xB7 Truly unvisualisable rooms (philosophical / definitional / pure narrative retro) are allowed to skip \u2014 but they're rare. When in doubt, pick a visual.",
|
|
6059
|
+
"",
|
|
6060
|
+
"## Picking presets",
|
|
6061
|
+
"",
|
|
6062
|
+
"\xB7 If unsure \u2192 `boardroom-dark` spine and the safe set: `bottom-line` + `frame-shift` + `headline-findings` + `convergence` (or `divergence`) + `visuals` (or `metric-strip` when the room had numbers) + `recommendations` + `new-questions` + `open-questions`. Note the visual is in the safe set now \u2014 earlier presets that omitted visualisation produced flat reports.",
|
|
6063
|
+
"\xB7 Strategic / market / forecast / impact-analysis subjects \u2192 `boardroom-dark` (or `gartner-note`) spine and the dense set: `bottom-line` + `metric-strip` + `strategic-outlook` + `headline-findings` + `critical-assumptions` + `scenario-tree` + `recommendations` + `leading-indicators` + `pre-mortem` + `new-questions`. ~10 components \u2014 feels like a research dashboard.",
|
|
6064
|
+
"\xB7 Investment / market opportunity rooms \u2192 `a16z-thesis` spine and: `thesis` + `metric-strip` + `why-now` + `big-ideas` + `the-bet` + `pre-mortem` + `scenario-tree` + `new-questions`. The metric-strip carries the underwriting numbers.",
|
|
4899
6065
|
"",
|
|
4900
6066
|
"## Spine catalogue",
|
|
4901
6067
|
"",
|
|
@@ -4972,6 +6138,7 @@ var SPINE_SET = new Set(SPINES);
|
|
|
4972
6138
|
var ANCHOR_SET = new Set(ANCHORS);
|
|
4973
6139
|
var FINDINGS_SET = new Set(FINDINGS);
|
|
4974
6140
|
var ACTION_SET = new Set(ACTIONS);
|
|
6141
|
+
var HOUSE_STYLE_SET = new Set(HOUSE_STYLES.map((s) => s.id));
|
|
4975
6142
|
var ALLOWED_SUBJECT_TYPES = /* @__PURE__ */ new Set([
|
|
4976
6143
|
"investment-judgement",
|
|
4977
6144
|
"option-comparison",
|
|
@@ -4985,8 +6152,11 @@ var ALLOWED_SUBJECT_TYPES = /* @__PURE__ */ new Set([
|
|
|
4985
6152
|
function parseComposerOutput(raw) {
|
|
4986
6153
|
const parsed = extractJson3(raw);
|
|
4987
6154
|
if (!parsed) return null;
|
|
6155
|
+
const houseStyleRaw = typeof parsed.house_style === "string" ? parsed.house_style : typeof parsed.houseStyle === "string" ? parsed.houseStyle : "";
|
|
6156
|
+
const houseStyleTrim = houseStyleRaw.trim();
|
|
6157
|
+
const houseStyle = HOUSE_STYLE_SET.has(houseStyleTrim) ? houseStyleTrim : "boardroom-default";
|
|
4988
6158
|
const spineRaw = typeof parsed.spine === "string" ? parsed.spine.trim() : "";
|
|
4989
|
-
const spine = SPINE_SET.has(spineRaw) ? spineRaw :
|
|
6159
|
+
const spine = SPINE_SET.has(spineRaw) ? spineRaw : resolveHouseStyle(houseStyle).spine;
|
|
4990
6160
|
if (!Array.isArray(parsed.components)) return null;
|
|
4991
6161
|
const seen = /* @__PURE__ */ new Set();
|
|
4992
6162
|
const picks = [];
|
|
@@ -5017,6 +6187,7 @@ function parseComposerOutput(raw) {
|
|
|
5017
6187
|
components: picks,
|
|
5018
6188
|
rationale,
|
|
5019
6189
|
subjectType,
|
|
6190
|
+
houseStyle,
|
|
5020
6191
|
fromComposer: true
|
|
5021
6192
|
};
|
|
5022
6193
|
}
|
|
@@ -5043,6 +6214,7 @@ function defaultComposition(reason) {
|
|
|
5043
6214
|
components: DEFAULT_PRESET.map((p) => ({ ...p })),
|
|
5044
6215
|
rationale: reason,
|
|
5045
6216
|
subjectType: null,
|
|
6217
|
+
houseStyle: "boardroom-default",
|
|
5046
6218
|
fromComposer: false
|
|
5047
6219
|
};
|
|
5048
6220
|
}
|
|
@@ -5094,6 +6266,7 @@ function extractBriefTitle(bodyMd, fallback) {
|
|
|
5094
6266
|
}
|
|
5095
6267
|
|
|
5096
6268
|
// src/storage/messages.ts
|
|
6269
|
+
init_db();
|
|
5097
6270
|
var COLS = "id, room_id, author_kind, author_id, reply_to_id, body, meta_json, round_num, created_at";
|
|
5098
6271
|
function mapRow5(row) {
|
|
5099
6272
|
return {
|
|
@@ -5159,9 +6332,31 @@ function deleteMessage(id) {
|
|
|
5159
6332
|
const r = getDb().prepare("DELETE FROM messages WHERE id = ?").run(id);
|
|
5160
6333
|
return r.changes > 0;
|
|
5161
6334
|
}
|
|
6335
|
+
function cleanupOrphanedStreams() {
|
|
6336
|
+
const db = getDb();
|
|
6337
|
+
const del = db.prepare(
|
|
6338
|
+
`DELETE FROM messages
|
|
6339
|
+
WHERE json_valid(meta_json)
|
|
6340
|
+
AND json_extract(meta_json, '$.streaming') = 1
|
|
6341
|
+
AND (body IS NULL OR trim(body) = '')`
|
|
6342
|
+
).run();
|
|
6343
|
+
const upd = db.prepare(
|
|
6344
|
+
`UPDATE messages
|
|
6345
|
+
SET meta_json = json_set(
|
|
6346
|
+
meta_json,
|
|
6347
|
+
'$.streaming', 0,
|
|
6348
|
+
'$.speakerStatus', 'final',
|
|
6349
|
+
'$.error', 'orphaned \xB7 server restarted mid-stream'
|
|
6350
|
+
)
|
|
6351
|
+
WHERE json_valid(meta_json)
|
|
6352
|
+
AND json_extract(meta_json, '$.streaming') = 1`
|
|
6353
|
+
).run();
|
|
6354
|
+
return { fixed: upd.changes, deleted: del.changes };
|
|
6355
|
+
}
|
|
5162
6356
|
|
|
5163
6357
|
// src/storage/rooms.ts
|
|
5164
|
-
|
|
6358
|
+
init_db();
|
|
6359
|
+
var ROOM_COLS = "id, number, name, subject, mode, intensity, status, brief_style, awaiting_continue, awaiting_clarify, created_at, paused_at, adjourned_at, incognito, parent_room_id, parent_brief_id";
|
|
5165
6360
|
function mapRow6(row) {
|
|
5166
6361
|
return {
|
|
5167
6362
|
id: row.id,
|
|
@@ -5177,7 +6372,9 @@ function mapRow6(row) {
|
|
|
5177
6372
|
createdAt: row.created_at,
|
|
5178
6373
|
pausedAt: row.paused_at,
|
|
5179
6374
|
adjournedAt: row.adjourned_at,
|
|
5180
|
-
incognito: row.incognito === 1
|
|
6375
|
+
incognito: row.incognito === 1,
|
|
6376
|
+
parentRoomId: row.parent_room_id,
|
|
6377
|
+
parentBriefId: row.parent_brief_id
|
|
5181
6378
|
};
|
|
5182
6379
|
}
|
|
5183
6380
|
function mapMember(row) {
|
|
@@ -5225,14 +6422,16 @@ function createRoom(input) {
|
|
|
5225
6422
|
const intensity = input.intensity ?? "sharp";
|
|
5226
6423
|
const briefStyle = input.briefStyle ?? "auto";
|
|
5227
6424
|
const insertRoom = db.prepare(
|
|
5228
|
-
"INSERT INTO rooms (id, number, name, subject, mode, intensity, brief_style, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'live', ?)"
|
|
6425
|
+
"INSERT INTO rooms (id, number, name, subject, mode, intensity, brief_style, status, created_at, parent_room_id, parent_brief_id) VALUES (?, ?, ?, ?, ?, ?, ?, 'live', ?, ?, ?)"
|
|
5229
6426
|
);
|
|
5230
6427
|
const insertMember = db.prepare(
|
|
5231
6428
|
"INSERT INTO room_members (room_id, agent_id, position, joined_at) VALUES (?, ?, ?, ?)"
|
|
5232
6429
|
);
|
|
5233
6430
|
const chair = getChairAgent();
|
|
6431
|
+
const parentRoomId = input.parentRoomId && input.parentRoomId.trim() ? input.parentRoomId.trim() : null;
|
|
6432
|
+
const parentBriefId = input.parentBriefId && input.parentBriefId.trim() ? input.parentBriefId.trim() : null;
|
|
5234
6433
|
const tx = db.transaction(() => {
|
|
5235
|
-
insertRoom.run(id, number, input.name, input.subject, mode, intensity, briefStyle, now);
|
|
6434
|
+
insertRoom.run(id, number, input.name, input.subject, mode, intensity, briefStyle, now, parentRoomId, parentBriefId);
|
|
5236
6435
|
if (chair) insertMember.run(id, chair.id, -1, now);
|
|
5237
6436
|
input.agentIds.forEach((agentId, idx) => {
|
|
5238
6437
|
if (chair && agentId === chair.id) return;
|
|
@@ -5308,9 +6507,55 @@ function deleteRoom(roomId) {
|
|
|
5308
6507
|
const result = getDb().prepare("DELETE FROM rooms WHERE id = ?").run(roomId);
|
|
5309
6508
|
return result.changes > 0;
|
|
5310
6509
|
}
|
|
6510
|
+
function recoverStuckClarifyRooms() {
|
|
6511
|
+
const r = getDb().prepare(
|
|
6512
|
+
`UPDATE rooms
|
|
6513
|
+
SET awaiting_clarify = 0
|
|
6514
|
+
WHERE awaiting_clarify = 1
|
|
6515
|
+
AND id NOT IN (
|
|
6516
|
+
SELECT DISTINCT m.room_id
|
|
6517
|
+
FROM messages m
|
|
6518
|
+
JOIN agents a ON a.id = m.author_id
|
|
6519
|
+
WHERE a.role_kind = 'moderator'
|
|
6520
|
+
AND m.author_kind = 'agent'
|
|
6521
|
+
)`
|
|
6522
|
+
).run();
|
|
6523
|
+
return r.changes;
|
|
6524
|
+
}
|
|
5311
6525
|
|
|
5312
6526
|
// src/storage/briefs.ts
|
|
5313
|
-
|
|
6527
|
+
init_db();
|
|
6528
|
+
var COLS2 = "id, room_id, style, title, body_md, body_json, supplement, spine, components_json, composer_rationale, subject_type, house_style, signals_json, created_at";
|
|
6529
|
+
function parseSignals(json) {
|
|
6530
|
+
if (!json) return null;
|
|
6531
|
+
try {
|
|
6532
|
+
const parsed = JSON.parse(json);
|
|
6533
|
+
if (!Array.isArray(parsed)) return null;
|
|
6534
|
+
const out = [];
|
|
6535
|
+
for (const entry of parsed) {
|
|
6536
|
+
if (!entry || typeof entry !== "object") continue;
|
|
6537
|
+
const e = entry;
|
|
6538
|
+
const directorId = typeof e.directorId === "string" ? e.directorId : "";
|
|
6539
|
+
const directorName = typeof e.directorName === "string" ? e.directorName : "";
|
|
6540
|
+
if (!directorId || !directorName) continue;
|
|
6541
|
+
const sigArr = Array.isArray(e.signals) ? e.signals : [];
|
|
6542
|
+
const signals = [];
|
|
6543
|
+
for (const s of sigArr) {
|
|
6544
|
+
if (!s || typeof s !== "object") continue;
|
|
6545
|
+
const so = s;
|
|
6546
|
+
const text = typeof so.text === "string" ? so.text : "";
|
|
6547
|
+
const lens = typeof so.lens === "string" ? so.lens : "";
|
|
6548
|
+
const sources = Array.isArray(so.sources) ? so.sources.filter((n) => typeof n === "number" && Number.isFinite(n)) : [];
|
|
6549
|
+
if (!text || !lens) continue;
|
|
6550
|
+
signals.push({ text, lens, sources });
|
|
6551
|
+
}
|
|
6552
|
+
out.push({ directorId, directorName, signals });
|
|
6553
|
+
}
|
|
6554
|
+
return out;
|
|
6555
|
+
} catch {
|
|
6556
|
+
return null;
|
|
6557
|
+
}
|
|
6558
|
+
}
|
|
5314
6559
|
function parseComponents(json) {
|
|
5315
6560
|
if (!json) return [];
|
|
5316
6561
|
try {
|
|
@@ -5342,6 +6587,8 @@ function mapRow7(row) {
|
|
|
5342
6587
|
components: parseComponents(row.components_json),
|
|
5343
6588
|
composerRationale: row.composer_rationale,
|
|
5344
6589
|
subjectType: row.subject_type,
|
|
6590
|
+
houseStyle: row.house_style || "boardroom-default",
|
|
6591
|
+
signals: parseSignals(row.signals_json),
|
|
5345
6592
|
createdAt: row.created_at
|
|
5346
6593
|
};
|
|
5347
6594
|
}
|
|
@@ -5361,7 +6608,7 @@ function listAllBriefs() {
|
|
|
5361
6608
|
const rows = getDb().prepare(
|
|
5362
6609
|
`SELECT b.id, b.room_id, b.style, b.title, b.body_md, b.body_json,
|
|
5363
6610
|
b.supplement, b.spine, b.components_json,
|
|
5364
|
-
b.composer_rationale, b.subject_type, b.created_at,
|
|
6611
|
+
b.composer_rationale, b.subject_type, b.house_style, b.signals_json, b.created_at,
|
|
5365
6612
|
r.name AS room_name, r.subject AS room_subject,
|
|
5366
6613
|
r.number AS room_number, r.status AS room_status
|
|
5367
6614
|
FROM briefs b
|
|
@@ -5386,8 +6633,9 @@ function insertBrief(b) {
|
|
|
5386
6633
|
const components = JSON.stringify(b.components ?? []);
|
|
5387
6634
|
const composerRationale = b.composerRationale && b.composerRationale.trim() ? b.composerRationale.trim() : null;
|
|
5388
6635
|
const subjectType = b.subjectType && b.subjectType.trim() ? b.subjectType.trim() : null;
|
|
6636
|
+
const houseStyle = b.houseStyle && b.houseStyle.trim() ? b.houseStyle.trim() : "boardroom-default";
|
|
5389
6637
|
db.prepare(
|
|
5390
|
-
`INSERT INTO briefs (${COLS2}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
6638
|
+
`INSERT INTO briefs (${COLS2}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
5391
6639
|
).run(
|
|
5392
6640
|
id,
|
|
5393
6641
|
b.roomId,
|
|
@@ -5400,10 +6648,17 @@ function insertBrief(b) {
|
|
|
5400
6648
|
components,
|
|
5401
6649
|
composerRationale,
|
|
5402
6650
|
subjectType,
|
|
6651
|
+
houseStyle,
|
|
6652
|
+
null,
|
|
6653
|
+
// signals_json · filled later by updateBriefSignals
|
|
5403
6654
|
now
|
|
5404
6655
|
);
|
|
5405
6656
|
return getBrief(id);
|
|
5406
6657
|
}
|
|
6658
|
+
function updateBriefSignals(id, signals) {
|
|
6659
|
+
const json = JSON.stringify(signals);
|
|
6660
|
+
getDb().prepare("UPDATE briefs SET signals_json = ? WHERE id = ?").run(json, id);
|
|
6661
|
+
}
|
|
5407
6662
|
function updateBriefCompose(id, fields) {
|
|
5408
6663
|
const sets = [];
|
|
5409
6664
|
const vals = [];
|
|
@@ -5423,6 +6678,10 @@ function updateBriefCompose(id, fields) {
|
|
|
5423
6678
|
sets.push("subject_type = ?");
|
|
5424
6679
|
vals.push(fields.subjectType);
|
|
5425
6680
|
}
|
|
6681
|
+
if (fields.houseStyle !== void 0) {
|
|
6682
|
+
sets.push("house_style = ?");
|
|
6683
|
+
vals.push(fields.houseStyle);
|
|
6684
|
+
}
|
|
5426
6685
|
if (!sets.length) return;
|
|
5427
6686
|
vals.push(id);
|
|
5428
6687
|
getDb().prepare(`UPDATE briefs SET ${sets.join(", ")} WHERE id = ?`).run(...vals);
|
|
@@ -5439,6 +6698,9 @@ function deleteBrief(id) {
|
|
|
5439
6698
|
return r.changes > 0;
|
|
5440
6699
|
}
|
|
5441
6700
|
|
|
6701
|
+
// src/orchestrator/brief.ts
|
|
6702
|
+
init_paths();
|
|
6703
|
+
|
|
5442
6704
|
// src/utils/tokens.ts
|
|
5443
6705
|
function estimateTokens(text) {
|
|
5444
6706
|
if (!text) return 0;
|
|
@@ -5724,8 +6986,27 @@ async function runPipeline(args) {
|
|
|
5724
6986
|
}
|
|
5725
6987
|
);
|
|
5726
6988
|
emitStage(roomId, briefId, "extract", "done");
|
|
5727
|
-
|
|
5728
|
-
|
|
6989
|
+
try {
|
|
6990
|
+
updateBriefSignals(
|
|
6991
|
+
briefId,
|
|
6992
|
+
perDirectorSignals.map((d) => ({
|
|
6993
|
+
directorId: d.directorId,
|
|
6994
|
+
directorName: d.directorName,
|
|
6995
|
+
signals: d.signals.map((s) => ({
|
|
6996
|
+
text: s.text,
|
|
6997
|
+
lens: s.lens,
|
|
6998
|
+
sources: Array.isArray(s.sources) ? s.sources : []
|
|
6999
|
+
}))
|
|
7000
|
+
}))
|
|
7001
|
+
);
|
|
7002
|
+
} catch (e) {
|
|
7003
|
+
process.stderr.write(
|
|
7004
|
+
`[brief.stage1] persist signals failed: ${e instanceof Error ? e.message : String(e)}
|
|
7005
|
+
`
|
|
7006
|
+
);
|
|
7007
|
+
}
|
|
7008
|
+
const stage1ActualSec = (Date.now() - stage1StartedAt) / 1e3;
|
|
7009
|
+
const stage1PredictedMid = (stage1Eta.lo + stage1Eta.hi) / 2;
|
|
5729
7010
|
let calibration = stage1PredictedMid > 0.5 ? stage1ActualSec / stage1PredictedMid : 1;
|
|
5730
7011
|
calibration = Math.max(0.5, Math.min(3, calibration));
|
|
5731
7012
|
emitStage(roomId, briefId, "compose", "active", void 0, void 0, { lo: 1, hi: 4 });
|
|
@@ -5741,7 +7022,8 @@ async function runPipeline(args) {
|
|
|
5741
7022
|
spine: composition.spine,
|
|
5742
7023
|
components: composition.components,
|
|
5743
7024
|
composerRationale: composition.rationale || null,
|
|
5744
|
-
subjectType: composition.subjectType
|
|
7025
|
+
subjectType: composition.subjectType,
|
|
7026
|
+
houseStyle: composition.houseStyle
|
|
5745
7027
|
});
|
|
5746
7028
|
roomBus.emit(roomId, {
|
|
5747
7029
|
type: "config-event",
|
|
@@ -5752,6 +7034,7 @@ async function runPipeline(args) {
|
|
|
5752
7034
|
components: composition.components,
|
|
5753
7035
|
rationale: composition.rationale,
|
|
5754
7036
|
subjectType: composition.subjectType,
|
|
7037
|
+
houseStyle: composition.houseStyle,
|
|
5755
7038
|
fromComposer: composition.fromComposer
|
|
5756
7039
|
},
|
|
5757
7040
|
createdAt: Date.now()
|
|
@@ -5794,7 +7077,8 @@ async function runPipeline(args) {
|
|
|
5794
7077
|
perDirectorSignals,
|
|
5795
7078
|
language,
|
|
5796
7079
|
supplement,
|
|
5797
|
-
picked: pickedKinds
|
|
7080
|
+
picked: pickedKinds,
|
|
7081
|
+
houseStyle: composition.houseStyle
|
|
5798
7082
|
});
|
|
5799
7083
|
buf = r3.body;
|
|
5800
7084
|
stage3Model = r3.model;
|
|
@@ -5983,8 +7267,8 @@ async function runStage2(args) {
|
|
|
5983
7267
|
return null;
|
|
5984
7268
|
}
|
|
5985
7269
|
async function runStage3Streaming(args) {
|
|
5986
|
-
const { roomId, briefId, chairId, room, members, scaffold, perDirectorSignals, language, supplement, picked } = args;
|
|
5987
|
-
const messages = buildWriteMessages({ room, members, scaffold, perDirectorSignals, language, supplement, picked });
|
|
7270
|
+
const { roomId, briefId, chairId, room, members, scaffold, perDirectorSignals, language, supplement, picked, houseStyle } = args;
|
|
7271
|
+
const messages = buildWriteMessages({ room, members, scaffold, perDirectorSignals, language, supplement, picked, houseStyle, briefId });
|
|
5988
7272
|
let lastError = "no model attempted";
|
|
5989
7273
|
for (const modelV of stageFlagshipList()) {
|
|
5990
7274
|
if (!isModelV(modelV)) continue;
|
|
@@ -6061,6 +7345,7 @@ function buildMethodologyFooter(args) {
|
|
|
6061
7345
|
}
|
|
6062
7346
|
|
|
6063
7347
|
// src/routes/briefs.ts
|
|
7348
|
+
init_paths();
|
|
6064
7349
|
function briefsRouter() {
|
|
6065
7350
|
const r = new Hono3();
|
|
6066
7351
|
r.get("/", (c) => {
|
|
@@ -6346,6 +7631,7 @@ function formatSearchResults(query, results) {
|
|
|
6346
7631
|
}
|
|
6347
7632
|
|
|
6348
7633
|
// src/storage/key_points.ts
|
|
7634
|
+
init_db();
|
|
6349
7635
|
var COLS3 = "id, room_id, message_id, round_num, body, vote, position, created_at, voted_at";
|
|
6350
7636
|
function mapRow8(r) {
|
|
6351
7637
|
return {
|
|
@@ -6943,6 +8229,50 @@ function renderActiveSkillsBlock(used) {
|
|
|
6943
8229
|
}
|
|
6944
8230
|
|
|
6945
8231
|
// src/orchestrator/prompt.ts
|
|
8232
|
+
function buildFollowUpPriorContext(opts) {
|
|
8233
|
+
const { parentRoomNumber, parentRoomSubject, parentBrief, parentSignals, language } = opts;
|
|
8234
|
+
const isZh = language === "zh";
|
|
8235
|
+
const parts = [
|
|
8236
|
+
"",
|
|
8237
|
+
isZh ? `\u2500\u2500\u2500 \u4E0A\u4E00\u573A\u5EF6\u7EED \xB7 Room #${parentRoomNumber} \u2500\u2500\u2500` : `\u2500\u2500\u2500 CONTINUING FROM ROOM #${parentRoomNumber} \u2500\u2500\u2500`,
|
|
8238
|
+
"",
|
|
8239
|
+
isZh ? `\u672C\u623F\u95F4\u662F\u4E0A\u4E00\u573A\u4F1A\u8BAE\u7684\u5EF6\u7EED\u3002\u4E0A\u4E00\u573A\u7684\u4E3B\u9898\u662F\uFF1A\u300C${parentRoomSubject}\u300D\u3002` : `This room is a follow-up to a prior session. The prior subject was: "${parentRoomSubject}".`,
|
|
8240
|
+
isZh ? `\u4E0B\u65B9\u662F\u4E0A\u4E00\u573A\u5DF2\u7ECF\u6210\u578B\u7684\u5224\u65AD \u2014\u2014 \u628A\u5B83\u5F53\u4F5C"\u5DF2\u843D\u5B9A\u7684\u5171\u8BC6"\uFF0C**\u4E0D\u8981\u91CD\u8FF0**\uFF0C\u76F4\u63A5\u5728\u5B83\u4E4B\u4E0A\u63A8\u8FDB\u3002\u65B0\u95EE\u9898\u5982\u679C\u4E0E\u8FD9\u4EFD\u5224\u65AD\u51B2\u7A81\uFF0C**\u663E\u5F0F\u6307\u51FA\u51B2\u7A81**\uFF0C\u4E0D\u8981\u7ED5\u5F00\u3002` : `Below is the prior session's *settled judgement* \u2014 treat it as established context. **Do not restate it**; build on it. If the new question contradicts a prior finding, **name the contradiction explicitly** rather than working around it.`,
|
|
8241
|
+
""
|
|
8242
|
+
];
|
|
8243
|
+
if (parentBrief) {
|
|
8244
|
+
parts.push(`## ${parentBrief.title}`);
|
|
8245
|
+
parts.push("");
|
|
8246
|
+
parts.push(parentBrief.bodyMd.trim());
|
|
8247
|
+
parts.push("");
|
|
8248
|
+
} else {
|
|
8249
|
+
parts.push(
|
|
8250
|
+
isZh ? `\uFF08\u4E0A\u4E00\u573A\u6CA1\u6709\u5F52\u6863\u62A5\u544A \u2014\u2014 \u4EC5\u6709\u4E0B\u65B9\u7684\u5173\u952E\u89C2\u5BDF\u53EF\u4F9B\u53C2\u8003\u3002\uFF09` : `(no brief was filed in the prior session \u2014 rely on the per-director signals below.)`
|
|
8251
|
+
);
|
|
8252
|
+
parts.push("");
|
|
8253
|
+
}
|
|
8254
|
+
const usableSignals = (parentSignals ?? []).filter((d) => d.signals.length > 0);
|
|
8255
|
+
if (usableSignals.length > 0) {
|
|
8256
|
+
parts.push(
|
|
8257
|
+
isZh ? `\u2500\u2500\u2500 \u4E0A\u4E00\u573A\u5404 director \u7684\u5173\u952E\u89C2\u5BDF \u2500\u2500\u2500` : `\u2500\u2500\u2500 PRIOR DIRECTOR SIGNALS \u2500\u2500\u2500`
|
|
8258
|
+
);
|
|
8259
|
+
parts.push(
|
|
8260
|
+
isZh ? `\u4E0B\u65B9\u662F\u4E0A\u4E00\u573A\u6BCF\u4F4D director \u81EA\u5DF1\u63D0\u70BC\u7684 load-bearing \u89C2\u5BDF\uFF0C\u6309 lens \u6807\u6CE8\u3002\u5F15\u7528\u4E0A\u4E00\u573A\u5224\u65AD\u65F6**\u6309\u5F52\u5C5E\u5F15\u7528**\uFF08"\u4E0A\u4E00\u573A Socrates \u7528 definitional lens \u63D0\u51FA X\uFF0C\u56E0\u6B64 ..."\uFF09\u2014\u2014 \u8FD9\u662F follow-up \u623F\u95F4\u533A\u522B\u4E8E\u666E\u901A\u65B0\u623F\u95F4\u7684\u5173\u952E\u7EAA\u5F8B\u3002` : `Each director's load-bearing observations from the prior session, lens-tagged. Reference by **attribution** when leaning on a prior point ("Socrates via definitional lens flagged X \u2014 so ...") \u2014 this is the discipline that makes a follow-up feel like a continuation rather than a re-open.`
|
|
8261
|
+
);
|
|
8262
|
+
parts.push("");
|
|
8263
|
+
for (const d of usableSignals) {
|
|
8264
|
+
for (const s of d.signals) {
|
|
8265
|
+
parts.push(` [${d.directorName} \xB7 ${s.lens}] "${s.text}"`);
|
|
8266
|
+
}
|
|
8267
|
+
}
|
|
8268
|
+
parts.push("");
|
|
8269
|
+
}
|
|
8270
|
+
parts.push(
|
|
8271
|
+
isZh ? `\u2500\u2500\u2500 \u4E0A\u4E00\u573A\u4E0A\u4E0B\u6587\u7ED3\u675F \xB7 \u65B0\u95EE\u9898\u4E0E\u5BF9\u8BDD\u5728\u4E0B\u65B9 \u2500\u2500\u2500` : `\u2500\u2500\u2500 END OF PRIOR CONTEXT \xB7 NEW QUESTION + DIALOGUE BELOW \u2500\u2500\u2500`,
|
|
8272
|
+
""
|
|
8273
|
+
);
|
|
8274
|
+
return parts.join("\n");
|
|
8275
|
+
}
|
|
6946
8276
|
function renderLongTermMemoryBlock(agentId, userName) {
|
|
6947
8277
|
const memories = memoriesForContext(agentId);
|
|
6948
8278
|
if (memories.length === 0) return "";
|
|
@@ -6977,14 +8307,23 @@ var TONE_GUIDANCE = {
|
|
|
6977
8307
|
"Attack moves: name a SPECIFIC risk the user hasn't named, demand evidence or boundary conditions, expose the trade-off being hidden. Sharp but professional.",
|
|
6978
8308
|
'Attack the argument, not the person. Forbidden: emotional put-downs, nitpicking word choice while ignoring the substantive claim, soft-pedalling ("maybe this could be a problem" \u2014 pick a side).'
|
|
6979
8309
|
].join("\n"),
|
|
6980
|
-
|
|
6981
|
-
"
|
|
6982
|
-
|
|
6983
|
-
`You may
|
|
6984
|
-
|
|
6985
|
-
|
|
8310
|
+
research: [
|
|
8311
|
+
"RESEARCH DISCUSSION \xB7 collaborative inquiry. The room's job is to mine the materials in front of it (the user's brief, web-search results, prior turns) for what's actually there \u2014 not to take sides.",
|
|
8312
|
+
`Each turn MUST: (1) cite a SPECIFIC piece of material \u2014 a quote, a datapoint, a stated claim, a result \u2014 never riff from thin air; (2) explicitly tag it as OBSERVATION (what the source says), INFERENCE (what you reasonably conclude from it), or SPECULATION (what you'd want to test); (3) extract the insight your lens makes salient that another director would miss; (4) on reactive rounds, connect your finding to another director's: "X plus Y suggests Z".`,
|
|
8313
|
+
`You may also flag knowledge gaps: "the materials don't tell us whether\u2026". Naming a gap is as valuable as a finding \u2014 it tells the user where to look next.`,
|
|
8314
|
+
"Forbidden: ungrounded opinion / intuition with no source citation; restating the topic; jumping to recommendations before the room has established what's known; conflating inference with observation. If you lack material to ground a claim, say so explicitly."
|
|
8315
|
+
].join("\n"),
|
|
8316
|
+
critique: [
|
|
8317
|
+
"CRITIQUE \xB7 review board. The user has put a deliverable on the table \u2014 a deck, a draft, a plan, a proposed decision. Your job is to find what's wrong with it, systematically. The artifact stands; you don't redesign it. You audit it.",
|
|
8318
|
+
`Each turn MUST: (1) name the dimension you're auditing this turn (logic, evidence, scope, risk, communication, implementability \u2014 pick what your lens is sharpest on); (2) surface 2\u20133 specific flaws, EACH labelled BLOCKER \xB7 MAJOR \xB7 MINOR; (3) for each flaw, point at the specific load-bearing piece ("the X claim in \xA72", "the assumption that Y"), state the mechanism for why it fails (not taste \u2014 mechanism), and indicate the direction a fix would lie.`,
|
|
8319
|
+
`At least one BLOCKER or MAJOR per turn is mandatory. Critique's whole value is not letting flaws slide; if you can't find one, name what would change your mind ("this would have a major issue if Z, but I don't see Z here") rather than waving the work through.`,
|
|
8320
|
+
'Forbidden: redesigning or reframing the work (you audit as-is, not as-could-be); vague "feels off" / "not quite right" without a mechanism; praise-only turns; attacking the author rather than the work. Severity labels are required, not optional.'
|
|
6986
8321
|
].join("\n")
|
|
6987
8322
|
};
|
|
8323
|
+
function normalizeTone(raw) {
|
|
8324
|
+
if (raw === "no-mercy") return "debate";
|
|
8325
|
+
return raw;
|
|
8326
|
+
}
|
|
6988
8327
|
var OPENING_BLOCK = [
|
|
6989
8328
|
"OPENING ROUND. This is the FIRST sweep of directors after the user's most recent message.",
|
|
6990
8329
|
"All other directors are responding to the same prompt IN PARALLEL \u2014 you do NOT see what they are writing right now, and they do not see you. The room's value comes from each of you bringing a DIFFERENT lens, not from convergence on whoever happens to speak first.",
|
|
@@ -7008,7 +8347,7 @@ var INTENSITY_GUIDANCE = {
|
|
|
7008
8347
|
].join("\n")
|
|
7009
8348
|
};
|
|
7010
8349
|
function buildDirectorMessages(opts) {
|
|
7011
|
-
const { speaker, cast, room, prefs, history, keyPoints, activeSkills, sharedMaterials, chairBrief } = opts;
|
|
8350
|
+
const { speaker, cast, room, prefs, history, keyPoints, activeSkills, sharedMaterials, chairBrief, summaryPreamble, priorContext } = opts;
|
|
7012
8351
|
const activeSkillsBlock = renderActiveSkillsBlock(activeSkills ?? []);
|
|
7013
8352
|
const chairBriefBlock = chairBrief && chairBrief.trim() ? [
|
|
7014
8353
|
``,
|
|
@@ -7026,7 +8365,7 @@ ABOUT THE USER:
|
|
|
7026
8365
|
Name: ${prefs.name}
|
|
7027
8366
|
`;
|
|
7028
8367
|
const memoryBlock = renderLongTermMemoryBlock(speaker.id, prefs.name || "the user");
|
|
7029
|
-
const tone = (room.mode || "constructive").toLowerCase();
|
|
8368
|
+
const tone = normalizeTone((room.mode || "constructive").toLowerCase());
|
|
7030
8369
|
const toneLine = TONE_GUIDANCE[tone] ?? TONE_GUIDANCE.constructive;
|
|
7031
8370
|
const intensity = (room.intensity || "sharp").toLowerCase();
|
|
7032
8371
|
const intensityLine = INTENSITY_GUIDANCE[intensity] ?? INTENSITY_GUIDANCE.sharp;
|
|
@@ -7071,6 +8410,7 @@ Name: ${prefs.name}
|
|
|
7071
8410
|
youSection,
|
|
7072
8411
|
...memoryBlock ? [memoryBlock] : [],
|
|
7073
8412
|
...interestLines,
|
|
8413
|
+
...priorContext && priorContext.trim() ? [priorContext] : [],
|
|
7074
8414
|
`\u2500\u2500\u2500 TONE \xB7 ${tone.toUpperCase()} \u2500\u2500\u2500`,
|
|
7075
8415
|
toneLine,
|
|
7076
8416
|
``,
|
|
@@ -7101,6 +8441,9 @@ Name: ${prefs.name}
|
|
|
7101
8441
|
].join("\n")
|
|
7102
8442
|
};
|
|
7103
8443
|
const out = [system];
|
|
8444
|
+
if (summaryPreamble && summaryPreamble.trim()) {
|
|
8445
|
+
out.push({ role: "system", content: summaryPreamble.trim() });
|
|
8446
|
+
}
|
|
7104
8447
|
for (const m of history) {
|
|
7105
8448
|
if (!m.body) continue;
|
|
7106
8449
|
if (m.authorKind === "system") {
|
|
@@ -7439,6 +8782,188 @@ function parseRoundEndOutput(text) {
|
|
|
7439
8782
|
return { ping, points };
|
|
7440
8783
|
}
|
|
7441
8784
|
|
|
8785
|
+
// src/storage/summaries.ts
|
|
8786
|
+
init_db();
|
|
8787
|
+
function rowFrom(r) {
|
|
8788
|
+
return {
|
|
8789
|
+
id: r.id,
|
|
8790
|
+
roomId: r.room_id,
|
|
8791
|
+
level: r.level,
|
|
8792
|
+
roundNum: r.round_num,
|
|
8793
|
+
startRound: r.start_round,
|
|
8794
|
+
endRound: r.end_round,
|
|
8795
|
+
body: r.body,
|
|
8796
|
+
modelV: r.model_v,
|
|
8797
|
+
sourceHash: r.source_hash,
|
|
8798
|
+
createdAt: r.created_at
|
|
8799
|
+
};
|
|
8800
|
+
}
|
|
8801
|
+
function listL1Summaries(roomId) {
|
|
8802
|
+
return getDb().prepare(
|
|
8803
|
+
`SELECT * FROM room_summaries
|
|
8804
|
+
WHERE room_id = ? AND level = 1
|
|
8805
|
+
ORDER BY round_num ASC`
|
|
8806
|
+
).all(roomId).map((r) => rowFrom(r));
|
|
8807
|
+
}
|
|
8808
|
+
function getL1Summary(roomId, roundNum) {
|
|
8809
|
+
const r = getDb().prepare(
|
|
8810
|
+
`SELECT * FROM room_summaries
|
|
8811
|
+
WHERE room_id = ? AND level = 1 AND round_num = ?
|
|
8812
|
+
LIMIT 1`
|
|
8813
|
+
).get(roomId, roundNum);
|
|
8814
|
+
return r ? rowFrom(r) : null;
|
|
8815
|
+
}
|
|
8816
|
+
function getL2Summary(roomId) {
|
|
8817
|
+
const r = getDb().prepare(
|
|
8818
|
+
`SELECT * FROM room_summaries
|
|
8819
|
+
WHERE room_id = ? AND level = 2
|
|
8820
|
+
LIMIT 1`
|
|
8821
|
+
).get(roomId);
|
|
8822
|
+
return r ? rowFrom(r) : null;
|
|
8823
|
+
}
|
|
8824
|
+
function upsertL1Summary(args) {
|
|
8825
|
+
const now = Date.now();
|
|
8826
|
+
getDb().prepare(
|
|
8827
|
+
`INSERT INTO room_summaries (room_id, level, round_num, start_round, end_round, body, model_v, source_hash, created_at)
|
|
8828
|
+
VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?)
|
|
8829
|
+
ON CONFLICT (room_id, level, round_num) WHERE level = 1
|
|
8830
|
+
DO UPDATE SET body = excluded.body, model_v = excluded.model_v, source_hash = excluded.source_hash, created_at = excluded.created_at`
|
|
8831
|
+
).run(args.roomId, args.roundNum, args.roundNum, args.roundNum, args.body, args.modelV, args.sourceHash, now);
|
|
8832
|
+
return getL1Summary(args.roomId, args.roundNum);
|
|
8833
|
+
}
|
|
8834
|
+
function upsertL2Summary(args) {
|
|
8835
|
+
const now = Date.now();
|
|
8836
|
+
const db = getDb();
|
|
8837
|
+
db.prepare("DELETE FROM room_summaries WHERE room_id = ? AND level = 2").run(args.roomId);
|
|
8838
|
+
db.prepare(
|
|
8839
|
+
`INSERT INTO room_summaries (room_id, level, round_num, start_round, end_round, body, model_v, source_hash, created_at)
|
|
8840
|
+
VALUES (?, 2, NULL, ?, ?, ?, ?, ?, ?)`
|
|
8841
|
+
).run(args.roomId, args.startRound, args.endRound, args.body, args.modelV, args.sourceHash, now);
|
|
8842
|
+
return getL2Summary(args.roomId);
|
|
8843
|
+
}
|
|
8844
|
+
function deleteL1Summary(roomId, roundNum) {
|
|
8845
|
+
getDb().prepare("DELETE FROM room_summaries WHERE room_id = ? AND level = 1 AND round_num = ?").run(roomId, roundNum);
|
|
8846
|
+
}
|
|
8847
|
+
|
|
8848
|
+
// src/orchestrator/summarize.ts
|
|
8849
|
+
var L0_KEEP = 5;
|
|
8850
|
+
var L1_BACK = 10;
|
|
8851
|
+
async function runRoundEndSummarization(roomId, roundJustEnded) {
|
|
8852
|
+
const l1Target = roundJustEnded - L0_KEEP + 1;
|
|
8853
|
+
if (l1Target < 1) return;
|
|
8854
|
+
if (!getL1Summary(roomId, l1Target)) {
|
|
8855
|
+
try {
|
|
8856
|
+
await generateL1ForRound(roomId, l1Target);
|
|
8857
|
+
} catch (e) {
|
|
8858
|
+
process.stderr.write(
|
|
8859
|
+
`[summarize] L1 round ${l1Target} room=${roomId} failed: ${e instanceof Error ? e.message : String(e)}
|
|
8860
|
+
`
|
|
8861
|
+
);
|
|
8862
|
+
}
|
|
8863
|
+
}
|
|
8864
|
+
const l2Target = roundJustEnded - L1_BACK + 1;
|
|
8865
|
+
if (l2Target < 1) return;
|
|
8866
|
+
const l1ForFolding = getL1Summary(roomId, l2Target);
|
|
8867
|
+
if (l1ForFolding) {
|
|
8868
|
+
try {
|
|
8869
|
+
await foldL1IntoL2(roomId, l1ForFolding.roundNum, l1ForFolding.body);
|
|
8870
|
+
deleteL1Summary(roomId, l1ForFolding.roundNum);
|
|
8871
|
+
} catch (e) {
|
|
8872
|
+
process.stderr.write(
|
|
8873
|
+
`[summarize] L2 fold round ${l2Target} room=${roomId} failed: ${e instanceof Error ? e.message : String(e)}
|
|
8874
|
+
`
|
|
8875
|
+
);
|
|
8876
|
+
}
|
|
8877
|
+
}
|
|
8878
|
+
}
|
|
8879
|
+
async function generateL1ForRound(roomId, roundNum) {
|
|
8880
|
+
const all = listMessages(roomId);
|
|
8881
|
+
const inRound = all.filter((m) => m.roundNum === roundNum);
|
|
8882
|
+
if (inRound.length === 0) return;
|
|
8883
|
+
const transcript = renderTranscript(inRound);
|
|
8884
|
+
if (!transcript.trim()) return;
|
|
8885
|
+
const keyPoints = listKeyPointsForRoom(roomId).filter((kp) => kp.roundNum === roundNum);
|
|
8886
|
+
const keyPointsBlock = keyPoints.length ? "\n\nChair's key points from this round:\n" + keyPoints.map((kp, i) => `${i + 1}. ${kp.body}`).join("\n") : "";
|
|
8887
|
+
const modelV = utilityModelFor();
|
|
8888
|
+
if (!modelV) return;
|
|
8889
|
+
const prompt = `Summarise round ${roundNum} of a multi-director boardroom discussion in 2-4 short sentences. Capture: (a) what the directors actually said (their distinct positions, not just topics), (b) where they pushed back on each other, (c) any concrete claim or recommendation that emerged. Do NOT restate the original question. Do NOT add commentary. Plain prose, third person, present tense.
|
|
8890
|
+
|
|
8891
|
+
--- Round ${roundNum} transcript ---
|
|
8892
|
+
${transcript}` + keyPointsBlock;
|
|
8893
|
+
const body = await callLLM({
|
|
8894
|
+
modelV,
|
|
8895
|
+
carrier: null,
|
|
8896
|
+
messages: [
|
|
8897
|
+
{ role: "user", content: prompt }
|
|
8898
|
+
],
|
|
8899
|
+
temperature: 0.2,
|
|
8900
|
+
maxTokens: 400
|
|
8901
|
+
});
|
|
8902
|
+
const trimmed = body.trim();
|
|
8903
|
+
if (!trimmed) return;
|
|
8904
|
+
upsertL1Summary({
|
|
8905
|
+
roomId,
|
|
8906
|
+
roundNum,
|
|
8907
|
+
body: trimmed,
|
|
8908
|
+
modelV,
|
|
8909
|
+
sourceHash: hashOf(transcript + keyPointsBlock)
|
|
8910
|
+
});
|
|
8911
|
+
}
|
|
8912
|
+
async function foldL1IntoL2(roomId, newRoundNum, newL1Body) {
|
|
8913
|
+
const existing = getL2Summary(roomId);
|
|
8914
|
+
const startRound = existing?.startRound ?? newRoundNum;
|
|
8915
|
+
const endRound = newRoundNum;
|
|
8916
|
+
const modelV = utilityModelFor();
|
|
8917
|
+
if (!modelV) return;
|
|
8918
|
+
const previousBlock = existing?.body ? `Previous narrative covering rounds ${existing.startRound}-${existing.endRound}:
|
|
8919
|
+
${existing.body}
|
|
8920
|
+
|
|
8921
|
+
` : "";
|
|
8922
|
+
const prompt = `You maintain a rolling consolidated narrative of an ongoing multi-director boardroom discussion. A new round just dropped out of the recent window and needs to be folded in. Produce a single narrative of 4-7 sentences covering rounds ${startRound} through ${endRound}. Preserve: each director's evolving position, where the discussion sharpened, decisions / commitments that emerged. Drop: minor exchanges, repeated points already captured. Plain prose, third person.
|
|
8923
|
+
|
|
8924
|
+
${previousBlock}New round ${newRoundNum} (just demoted from L1):
|
|
8925
|
+
${newL1Body}`;
|
|
8926
|
+
const body = await callLLM({
|
|
8927
|
+
modelV,
|
|
8928
|
+
carrier: null,
|
|
8929
|
+
messages: [{ role: "user", content: prompt }],
|
|
8930
|
+
temperature: 0.25,
|
|
8931
|
+
maxTokens: 600
|
|
8932
|
+
});
|
|
8933
|
+
const trimmed = body.trim();
|
|
8934
|
+
if (!trimmed) return;
|
|
8935
|
+
upsertL2Summary({
|
|
8936
|
+
roomId,
|
|
8937
|
+
startRound,
|
|
8938
|
+
endRound,
|
|
8939
|
+
body: trimmed,
|
|
8940
|
+
modelV,
|
|
8941
|
+
sourceHash: hashOf((existing?.body ?? "") + newL1Body)
|
|
8942
|
+
});
|
|
8943
|
+
}
|
|
8944
|
+
function renderTranscript(messages) {
|
|
8945
|
+
const prefs = getPrefs();
|
|
8946
|
+
const lines = [];
|
|
8947
|
+
for (const m of messages) {
|
|
8948
|
+
if (m.authorKind === "system") continue;
|
|
8949
|
+
if (m.authorKind === "agent") {
|
|
8950
|
+
const meta = m.meta || {};
|
|
8951
|
+
if (meta.kind === "round-open" || meta.kind === "settings" || meta.kind === "round-prompt") continue;
|
|
8952
|
+
const a = m.authorId ? getAgent(m.authorId) : null;
|
|
8953
|
+
const name = a ? a.name : "Director";
|
|
8954
|
+
lines.push(`${name}: ${m.body.trim()}`);
|
|
8955
|
+
} else if (m.authorKind === "user") {
|
|
8956
|
+
lines.push(`${prefs.name || "User"}: ${m.body.trim()}`);
|
|
8957
|
+
}
|
|
8958
|
+
}
|
|
8959
|
+
return lines.join("\n\n");
|
|
8960
|
+
}
|
|
8961
|
+
function hashOf(s) {
|
|
8962
|
+
let h = 5381;
|
|
8963
|
+
for (let i = 0; i < s.length; i++) h = (h << 5) + h + s.charCodeAt(i) | 0;
|
|
8964
|
+
return (h >>> 0).toString(16);
|
|
8965
|
+
}
|
|
8966
|
+
|
|
7442
8967
|
// src/skills/url-fetch.ts
|
|
7443
8968
|
var FETCH_TIMEOUT_MS = 6e3;
|
|
7444
8969
|
var MAX_RETRIES = 2;
|
|
@@ -8211,6 +9736,7 @@ async function runChairRoundEnd(roomId, roundNum) {
|
|
|
8211
9736
|
});
|
|
8212
9737
|
}
|
|
8213
9738
|
});
|
|
9739
|
+
void runRoundEndSummarization(roomId, roundNum);
|
|
8214
9740
|
}
|
|
8215
9741
|
function announceRoundPrompt(roomId, roundNum, recommendation) {
|
|
8216
9742
|
const chair = getChairAgent();
|
|
@@ -8284,6 +9810,34 @@ function announceIntervention(roomId, body, rationale) {
|
|
|
8284
9810
|
});
|
|
8285
9811
|
roomBus.emit(roomId, { type: "message-final", messageId: m.id });
|
|
8286
9812
|
}
|
|
9813
|
+
function announceResearchHint(roomId, lang = "en") {
|
|
9814
|
+
const chair = getChairAgent();
|
|
9815
|
+
if (!chair) return;
|
|
9816
|
+
const body = lang === "zh" ? "\u8FD9\u662F\u4E00\u95F4 **research room** \u2014\u2014 \u6211\u4F1A\u8BA9\u6BCF\u4F4D director \u9ED8\u8BA4\u7528 web search \u53BB\u5916\u90E8\u6750\u6599\u91CC\u6316\u4E8B\u5B9E\u3002\n\n\u76EE\u524D\u4F60\u8FD8\u6CA1\u914D\u7F6E **Brave Search API key**\uFF0C\u623F\u95F4\u4F1A\u7167\u5E38\u8FD0\u884C\uFF08directors \u4F1A\u4ECE\u5DF2\u6709\u4E0A\u4E0B\u6587+\u4ED6\u4EEC\u7684\u8BAD\u7EC3\u77E5\u8BC6\u91CC\u8BA8\u8BBA\uFF09\uFF0C\u4F46\u63A5\u4E0D\u4E0A\u5916\u90E8 fact-finding\uFF0C\u6548\u679C\u4F1A\u5DEE\u4E0D\u5C11\u3002\n\n\u5EFA\u8BAE\u5728 **Preference \u2192 API Key \u2192 Brave Search** \u91CC\u914D\u4E00\u4E2A\uFF08\u7EA6 $5 / 1000 \u6B21\u67E5\u8BE2\uFF0C\u9690\u79C1\u53CB\u597D\uFF09\uFF0C\u7136\u540E\u8FD9\u95F4\u623F\u5C31\u80FD\u7528\u4E86\u3002" : "This is a **research room** \u2014 I'll have every director default to web search for outside fact-finding.\n\nYou haven't configured a **Brave Search API key** yet, so the room will run on directors' training knowledge + the conversation context only \u2014 workable, but materially less useful for a research session.\n\nOpen **Preference \u2192 API Key \u2192 Brave Search** to configure one (\u2248 $5 per 1,000 queries, privacy-respecting). The room will use it from the next turn onward.";
|
|
9817
|
+
const m = insertMessage({
|
|
9818
|
+
roomId,
|
|
9819
|
+
authorKind: "agent",
|
|
9820
|
+
authorId: chair.id,
|
|
9821
|
+
body,
|
|
9822
|
+
meta: {
|
|
9823
|
+
kind: "research-hint",
|
|
9824
|
+
speakerStatus: "final",
|
|
9825
|
+
streaming: false
|
|
9826
|
+
}
|
|
9827
|
+
});
|
|
9828
|
+
roomBus.emit(roomId, {
|
|
9829
|
+
type: "message-appended",
|
|
9830
|
+
messageId: m.id,
|
|
9831
|
+
authorKind: "agent",
|
|
9832
|
+
authorId: chair.id,
|
|
9833
|
+
replyToId: null,
|
|
9834
|
+
body: m.body,
|
|
9835
|
+
meta: m.meta,
|
|
9836
|
+
roundNum: m.roundNum,
|
|
9837
|
+
createdAt: m.createdAt
|
|
9838
|
+
});
|
|
9839
|
+
roomBus.emit(roomId, { type: "message-final", messageId: m.id });
|
|
9840
|
+
}
|
|
8287
9841
|
function announceBillingNotice(roomId, opts) {
|
|
8288
9842
|
const chair = getChairAgent();
|
|
8289
9843
|
if (!chair) return;
|
|
@@ -8599,6 +10153,7 @@ function parseExtractionOutput(raw) {
|
|
|
8599
10153
|
}
|
|
8600
10154
|
|
|
8601
10155
|
// src/storage/config-events.ts
|
|
10156
|
+
init_db();
|
|
8602
10157
|
function mapRow9(row) {
|
|
8603
10158
|
return {
|
|
8604
10159
|
id: row.id,
|
|
@@ -8674,6 +10229,63 @@ function extractProviderHint(message) {
|
|
|
8674
10229
|
return null;
|
|
8675
10230
|
}
|
|
8676
10231
|
|
|
10232
|
+
// src/orchestrator/context.ts
|
|
10233
|
+
function buildDirectorContext(roomId) {
|
|
10234
|
+
const room = getRoom(roomId);
|
|
10235
|
+
const allMessages = listMessages(roomId);
|
|
10236
|
+
if (allMessages.length === 0) {
|
|
10237
|
+
return { historyMessages: [], summaryPreamble: "", currentRound: 0 };
|
|
10238
|
+
}
|
|
10239
|
+
const currentRound = Math.max(...allMessages.map((m) => m.roundNum ?? 0), 0);
|
|
10240
|
+
const l0Cutoff = Math.max(1, currentRound - L0_KEEP + 1);
|
|
10241
|
+
const anchorIds = /* @__PURE__ */ new Set();
|
|
10242
|
+
for (const m of allMessages) {
|
|
10243
|
+
const round = m.roundNum ?? 0;
|
|
10244
|
+
if (round >= l0Cutoff) continue;
|
|
10245
|
+
if (m.authorKind === "user") {
|
|
10246
|
+
anchorIds.add(m.id);
|
|
10247
|
+
continue;
|
|
10248
|
+
}
|
|
10249
|
+
if (m.authorKind === "agent") {
|
|
10250
|
+
const meta = m.meta || {};
|
|
10251
|
+
if (meta.kind === "convening") anchorIds.add(m.id);
|
|
10252
|
+
}
|
|
10253
|
+
}
|
|
10254
|
+
const anchors = allMessages.filter((m) => anchorIds.has(m.id));
|
|
10255
|
+
const l0 = allMessages.filter((m) => (m.roundNum ?? 0) >= l0Cutoff);
|
|
10256
|
+
const historyMessages = [...anchors, ...l0].sort((a, b) => a.createdAt - b.createdAt);
|
|
10257
|
+
const summaryPreamble = buildSummaryPreamble(roomId, room?.subject ?? "", currentRound, l0Cutoff);
|
|
10258
|
+
return { historyMessages, summaryPreamble, currentRound };
|
|
10259
|
+
}
|
|
10260
|
+
function buildSummaryPreamble(roomId, subject, currentRound, l0Cutoff) {
|
|
10261
|
+
const sections = [];
|
|
10262
|
+
if (subject.trim()) {
|
|
10263
|
+
sections.push(`// ROOM SUBJECT (anchor \xB7 the original question)
|
|
10264
|
+
${subject.trim()}`);
|
|
10265
|
+
}
|
|
10266
|
+
const l2 = getL2Summary(roomId);
|
|
10267
|
+
if (l2 && l2.body.trim()) {
|
|
10268
|
+
sections.push(
|
|
10269
|
+
`// EARLIER IN THIS ROOM \xB7 rounds ${l2.startRound}-${l2.endRound} (consolidated)
|
|
10270
|
+
${l2.body.trim()}`
|
|
10271
|
+
);
|
|
10272
|
+
}
|
|
10273
|
+
const l1Rows = listL1Summaries(roomId).filter((row) => (row.roundNum ?? 0) < l0Cutoff);
|
|
10274
|
+
if (l1Rows.length > 0) {
|
|
10275
|
+
const lines = l1Rows.map((row) => `\xB7 round ${row.roundNum}: ${row.body.trim()}`);
|
|
10276
|
+
sections.push(`// RECENT ROUNDS \xB7 per-round summaries
|
|
10277
|
+
${lines.join("\n")}`);
|
|
10278
|
+
}
|
|
10279
|
+
if (sections.length === 0) return "";
|
|
10280
|
+
return [
|
|
10281
|
+
"",
|
|
10282
|
+
"\u2550\u2550\u2550 context \xB7 earlier in this room \u2550\u2550\u2550",
|
|
10283
|
+
sections.join("\n\n"),
|
|
10284
|
+
"\u2550\u2550\u2550 end context \xB7 live transcript follows \u2550\u2550\u2550",
|
|
10285
|
+
""
|
|
10286
|
+
].join("\n");
|
|
10287
|
+
}
|
|
10288
|
+
|
|
8677
10289
|
// src/orchestrator/room.ts
|
|
8678
10290
|
var _state = /* @__PURE__ */ new Map();
|
|
8679
10291
|
function rlog(roomId, label, fields) {
|
|
@@ -8817,7 +10429,6 @@ function abortRoom(roomId) {
|
|
|
8817
10429
|
};
|
|
8818
10430
|
s.queue = [];
|
|
8819
10431
|
const wasSpeaking = s.inflight !== null;
|
|
8820
|
-
s.speakersThisTurn = 0;
|
|
8821
10432
|
if (s.inflight) {
|
|
8822
10433
|
s.inflight.abort();
|
|
8823
10434
|
s.inflight = null;
|
|
@@ -8874,13 +10485,15 @@ function resumeRoom(roomId) {
|
|
|
8874
10485
|
createdAt: Date.now()
|
|
8875
10486
|
});
|
|
8876
10487
|
}
|
|
10488
|
+
const nextRound = nextUserRoundNum(roomId);
|
|
8877
10489
|
rlog(roomId, "resume", {
|
|
8878
10490
|
mode: "fallback-replan",
|
|
8879
10491
|
snapshot: snap ? "empty" : "missing",
|
|
8880
|
-
|
|
10492
|
+
fromRound: s.roundNum,
|
|
10493
|
+
toRound: nextRound,
|
|
8881
10494
|
clearedAwaitingContinue: !!(room && room.awaitingContinue)
|
|
8882
10495
|
});
|
|
8883
|
-
tickRoom(roomId, { roundNum:
|
|
10496
|
+
tickRoom(roomId, { roundNum: nextRound, kind: "continue" });
|
|
8884
10497
|
}
|
|
8885
10498
|
function emitQueueUpdate(roomId, s) {
|
|
8886
10499
|
const update = {
|
|
@@ -9207,10 +10820,13 @@ async function streamSpeakerTurn(args) {
|
|
|
9207
10820
|
const memberRows = listRoomMembers(roomId);
|
|
9208
10821
|
const cast = memberRows.map((m) => getAgent(m.agentId)).filter((a) => a !== null && a.roleKind === "director");
|
|
9209
10822
|
const prefs = getPrefs();
|
|
9210
|
-
const
|
|
10823
|
+
const directorCtx = buildDirectorContext(roomId);
|
|
10824
|
+
const history = directorCtx.historyMessages;
|
|
10825
|
+
const summaryPreamble = directorCtx.summaryPreamble;
|
|
9211
10826
|
const keyPoints = listKeyPointsForRoom(roomId);
|
|
9212
10827
|
const installedSkills = listSkillsForAgent(speaker.id);
|
|
9213
|
-
const
|
|
10828
|
+
const isResearchMode = (room.mode || "").toLowerCase() === "research";
|
|
10829
|
+
const braveAvailable = hasBraveKey() && (speaker.webSearchEnabled || isResearchMode);
|
|
9214
10830
|
let activeSkills = [];
|
|
9215
10831
|
let pickerReason = "";
|
|
9216
10832
|
let webSearchQuery = null;
|
|
@@ -9267,6 +10883,31 @@ async function streamSpeakerTurn(args) {
|
|
|
9267
10883
|
chairBriefForTurn = turnState.pendingChairPick.rationale;
|
|
9268
10884
|
turnState.pendingChairPick = null;
|
|
9269
10885
|
}
|
|
10886
|
+
let priorContext;
|
|
10887
|
+
if (room.parentRoomId) {
|
|
10888
|
+
try {
|
|
10889
|
+
const parentRoom = getRoom(room.parentRoomId);
|
|
10890
|
+
if (parentRoom) {
|
|
10891
|
+
const parentBrief = room.parentBriefId ? getBrief(room.parentBriefId) : null;
|
|
10892
|
+
const langGuess = /[一-鿿]/.test(parentRoom.subject || "") ? "zh" : "en";
|
|
10893
|
+
const block = buildFollowUpPriorContext({
|
|
10894
|
+
parentRoomNumber: parentRoom.number,
|
|
10895
|
+
parentRoomSubject: parentRoom.subject,
|
|
10896
|
+
parentBrief: parentBrief ? { title: parentBrief.title, bodyMd: parentBrief.bodyMd } : null,
|
|
10897
|
+
parentSignals: parentBrief && parentBrief.signals ? parentBrief.signals.map((d) => ({
|
|
10898
|
+
directorName: d.directorName,
|
|
10899
|
+
signals: d.signals.map((s) => ({ text: s.text, lens: s.lens }))
|
|
10900
|
+
})) : null,
|
|
10901
|
+
language: langGuess
|
|
10902
|
+
});
|
|
10903
|
+
if (block.trim()) priorContext = block;
|
|
10904
|
+
}
|
|
10905
|
+
} catch (e) {
|
|
10906
|
+
rlog(roomId, "follow-up-context-error", {
|
|
10907
|
+
error: e instanceof Error ? e.message : String(e)
|
|
10908
|
+
});
|
|
10909
|
+
}
|
|
10910
|
+
}
|
|
9270
10911
|
const llmMessages = buildDirectorMessages({
|
|
9271
10912
|
speaker,
|
|
9272
10913
|
cast,
|
|
@@ -9276,7 +10917,9 @@ async function streamSpeakerTurn(args) {
|
|
|
9276
10917
|
keyPoints,
|
|
9277
10918
|
activeSkills,
|
|
9278
10919
|
sharedMaterials: sharedMaterialsBlock,
|
|
9279
|
-
chairBrief: chairBriefForTurn ?? void 0
|
|
10920
|
+
chairBrief: chairBriefForTurn ?? void 0,
|
|
10921
|
+
summaryPreamble,
|
|
10922
|
+
priorContext
|
|
9280
10923
|
});
|
|
9281
10924
|
const placeholderMeta = {
|
|
9282
10925
|
speakerStatus: "streaming",
|
|
@@ -9316,85 +10959,106 @@ async function streamSpeakerTurn(args) {
|
|
|
9316
10959
|
let buf = "";
|
|
9317
10960
|
let finishReason;
|
|
9318
10961
|
let errored = false;
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
type: "message-token",
|
|
9348
|
-
messageId: placeholder.id,
|
|
9349
|
-
delta: chunk.delta
|
|
9350
|
-
});
|
|
9351
|
-
} else if (chunk.type === "usage") {
|
|
9352
|
-
incrementAgentTokens(speaker.id, chunk.totalTokens);
|
|
9353
|
-
rlog(roomId, "speaker-usage", {
|
|
9354
|
-
agent: speaker.name,
|
|
9355
|
-
modelV: speaker.modelV,
|
|
9356
|
-
promptTokens: chunk.promptTokens,
|
|
9357
|
-
completionTokens: chunk.completionTokens,
|
|
9358
|
-
totalTokens: chunk.totalTokens
|
|
9359
|
-
});
|
|
9360
|
-
} else if (chunk.type === "done") {
|
|
9361
|
-
finishReason = chunk.finishReason;
|
|
9362
|
-
} else if (chunk.type === "error") {
|
|
9363
|
-
errored = true;
|
|
9364
|
-
process.stderr.write(
|
|
9365
|
-
`[stream-error] room=${roomId} agent=${speaker.name} modelV=${speaker.modelV} \xB7 ${chunk.message}
|
|
9366
|
-
`
|
|
9367
|
-
);
|
|
9368
|
-
if (isBillingError(chunk.message)) {
|
|
9369
|
-
const turnState2 = ensureState(roomId);
|
|
9370
|
-
deleteMessage(placeholder.id);
|
|
10962
|
+
try {
|
|
10963
|
+
for await (const chunk of callLLMStream({
|
|
10964
|
+
modelV: speaker.modelV,
|
|
10965
|
+
// Per-agent carrier override · adapter falls back to default
|
|
10966
|
+
// precedence when null, when the agent set a carrier whose key
|
|
10967
|
+
// was later removed, or when the chosen carrier doesn't actually
|
|
10968
|
+
// host the agent's modelV.
|
|
10969
|
+
carrier: speaker.carrierPref ?? null,
|
|
10970
|
+
messages: llmMessages,
|
|
10971
|
+
temperature: 0.65,
|
|
10972
|
+
// 4000 (was 800 → 2000). Gemini 3 Pro's thinking trace routinely
|
|
10973
|
+
// burns 2-3k tokens on a single director turn; with the cap at
|
|
10974
|
+
// 2000, reasoning ate the whole budget and the visible reply got
|
|
10975
|
+
// truncated mid-sentence. 4000 leaves ~1-2k of visible headroom
|
|
10976
|
+
// even for the heaviest reasoners. If a model STILL truncates,
|
|
10977
|
+
// the next move is to cap reasoning explicitly via providerOptions
|
|
10978
|
+
// (`reasoning.max_tokens`) instead of just enlarging the total cap.
|
|
10979
|
+
maxTokens: 4e3,
|
|
10980
|
+
signal
|
|
10981
|
+
})) {
|
|
10982
|
+
if (signal.aborted) break;
|
|
10983
|
+
if (chunk.type === "text") {
|
|
10984
|
+
buf += chunk.delta;
|
|
10985
|
+
updateMessageBody(placeholder.id, buf, {
|
|
10986
|
+
...placeholderMeta,
|
|
10987
|
+
speakerStatus: "streaming",
|
|
10988
|
+
streaming: true
|
|
10989
|
+
});
|
|
9371
10990
|
roomBus.emit(roomId, {
|
|
9372
|
-
type: "message-
|
|
10991
|
+
type: "message-token",
|
|
9373
10992
|
messageId: placeholder.id,
|
|
9374
|
-
|
|
10993
|
+
delta: chunk.delta
|
|
9375
10994
|
});
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
10995
|
+
} else if (chunk.type === "usage") {
|
|
10996
|
+
incrementAgentTokens(speaker.id, chunk.totalTokens);
|
|
10997
|
+
rlog(roomId, "speaker-usage", {
|
|
10998
|
+
agent: speaker.name,
|
|
10999
|
+
modelV: speaker.modelV,
|
|
11000
|
+
promptTokens: chunk.promptTokens,
|
|
11001
|
+
completionTokens: chunk.completionTokens,
|
|
11002
|
+
totalTokens: chunk.totalTokens
|
|
11003
|
+
});
|
|
11004
|
+
} else if (chunk.type === "done") {
|
|
11005
|
+
finishReason = chunk.finishReason;
|
|
11006
|
+
} else if (chunk.type === "error") {
|
|
11007
|
+
errored = true;
|
|
11008
|
+
process.stderr.write(
|
|
11009
|
+
`[stream-error] room=${roomId} agent=${speaker.name} modelV=${speaker.modelV} \xB7 ${chunk.message}
|
|
11010
|
+
`
|
|
11011
|
+
);
|
|
11012
|
+
if (isBillingError(chunk.message)) {
|
|
11013
|
+
const turnState2 = ensureState(roomId);
|
|
11014
|
+
deleteMessage(placeholder.id);
|
|
11015
|
+
roomBus.emit(roomId, {
|
|
11016
|
+
type: "message-removed",
|
|
11017
|
+
messageId: placeholder.id,
|
|
11018
|
+
reason: "billing"
|
|
9381
11019
|
});
|
|
9382
|
-
turnState2.billingHaltedThisTurn
|
|
11020
|
+
if (!turnState2.billingHaltedThisTurn) {
|
|
11021
|
+
announceBillingNotice(roomId, {
|
|
11022
|
+
providerHint: extractProviderHint(chunk.message),
|
|
11023
|
+
rawError: chunk.message,
|
|
11024
|
+
agentName: speaker.name
|
|
11025
|
+
});
|
|
11026
|
+
turnState2.billingHaltedThisTurn = true;
|
|
11027
|
+
}
|
|
11028
|
+
return;
|
|
9383
11029
|
}
|
|
9384
|
-
|
|
11030
|
+
roomBus.emit(roomId, {
|
|
11031
|
+
type: "message-error",
|
|
11032
|
+
messageId: placeholder.id,
|
|
11033
|
+
message: chunk.message
|
|
11034
|
+
});
|
|
11035
|
+
updateMessageBody(placeholder.id, buf || `[error: ${chunk.message}]`, {
|
|
11036
|
+
...placeholderMeta,
|
|
11037
|
+
speakerStatus: "final",
|
|
11038
|
+
streaming: false,
|
|
11039
|
+
error: chunk.message
|
|
11040
|
+
});
|
|
9385
11041
|
}
|
|
9386
|
-
roomBus.emit(roomId, {
|
|
9387
|
-
type: "message-error",
|
|
9388
|
-
messageId: placeholder.id,
|
|
9389
|
-
message: chunk.message
|
|
9390
|
-
});
|
|
9391
|
-
updateMessageBody(placeholder.id, buf || `[error: ${chunk.message}]`, {
|
|
9392
|
-
...placeholderMeta,
|
|
9393
|
-
speakerStatus: "final",
|
|
9394
|
-
streaming: false,
|
|
9395
|
-
error: chunk.message
|
|
9396
|
-
});
|
|
9397
11042
|
}
|
|
11043
|
+
} catch (e) {
|
|
11044
|
+
errored = true;
|
|
11045
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
11046
|
+
process.stderr.write(
|
|
11047
|
+
`[stream-throw] room=${roomId} agent=${speaker.name} modelV=${speaker.modelV} \xB7 ${msg}
|
|
11048
|
+
`
|
|
11049
|
+
);
|
|
11050
|
+
updateMessageBody(placeholder.id, buf || `[error: ${msg}]`, {
|
|
11051
|
+
...placeholderMeta,
|
|
11052
|
+
speakerStatus: "final",
|
|
11053
|
+
streaming: false,
|
|
11054
|
+
error: msg
|
|
11055
|
+
});
|
|
11056
|
+
roomBus.emit(roomId, {
|
|
11057
|
+
type: "message-error",
|
|
11058
|
+
messageId: placeholder.id,
|
|
11059
|
+
message: msg
|
|
11060
|
+
});
|
|
11061
|
+
throw e;
|
|
9398
11062
|
}
|
|
9399
11063
|
if (signal.aborted) {
|
|
9400
11064
|
finishReason = finishReason ?? "aborted";
|
|
@@ -9759,6 +11423,24 @@ function roomsRouter() {
|
|
|
9759
11423
|
const b = body ?? {};
|
|
9760
11424
|
const subject = typeof b.subject === "string" ? b.subject.trim() : "";
|
|
9761
11425
|
if (!subject) return c.json({ error: "subject is required" }, 400);
|
|
11426
|
+
let parentRoomId = null;
|
|
11427
|
+
let parentBriefId = null;
|
|
11428
|
+
if (typeof b.parentRoomId === "string" && b.parentRoomId.trim()) {
|
|
11429
|
+
const candidate = b.parentRoomId.trim();
|
|
11430
|
+
const parent = getRoom(candidate);
|
|
11431
|
+
if (parent && parent.status === "adjourned") {
|
|
11432
|
+
parentRoomId = candidate;
|
|
11433
|
+
if (typeof b.parentBriefId === "string" && b.parentBriefId.trim()) {
|
|
11434
|
+
const briefCandidate = b.parentBriefId.trim();
|
|
11435
|
+
const parentBrief = getBriefByRoom(candidate);
|
|
11436
|
+
const briefByRoom = parentBrief && parentBrief.id === briefCandidate ? parentBrief : null;
|
|
11437
|
+
parentBriefId = briefByRoom ? briefByRoom.id : null;
|
|
11438
|
+
} else {
|
|
11439
|
+
const latest = getBriefByRoom(candidate);
|
|
11440
|
+
if (latest) parentBriefId = latest.id;
|
|
11441
|
+
}
|
|
11442
|
+
}
|
|
11443
|
+
}
|
|
9762
11444
|
const autoPick = b.autoPick === true;
|
|
9763
11445
|
const agentIds = Array.isArray(b.agentIds) ? b.agentIds.filter((x) => typeof x === "string") : [];
|
|
9764
11446
|
if (!autoPick && agentIds.length === 0) {
|
|
@@ -9768,7 +11450,7 @@ function roomsRouter() {
|
|
|
9768
11450
|
if (!getAgent(id)) return c.json({ error: `unknown agent: ${id}` }, 400);
|
|
9769
11451
|
}
|
|
9770
11452
|
const name = typeof b.name === "string" && b.name.trim() ? b.name.trim().slice(0, 80) : subject.slice(0, 60);
|
|
9771
|
-
const ALLOWED_MODES = /* @__PURE__ */ new Set(["brainstorm", "constructive", "debate", "
|
|
11453
|
+
const ALLOWED_MODES = /* @__PURE__ */ new Set(["brainstorm", "constructive", "research", "debate", "critique"]);
|
|
9772
11454
|
const ALLOWED_INTENSITY = /* @__PURE__ */ new Set(["calm", "sharp", "brutal"]);
|
|
9773
11455
|
const ALLOWED_STYLES = /* @__PURE__ */ new Set(["auto", "mckinsey", "gartner", "a16z", "anthropic", "8bit"]);
|
|
9774
11456
|
const STYLE_ALIAS = { mck: "mckinsey" };
|
|
@@ -9779,7 +11461,16 @@ function roomsRouter() {
|
|
|
9779
11461
|
const rawStyle = typeof b.briefStyle === "string" ? b.briefStyle.trim() : "";
|
|
9780
11462
|
const styleResolved = STYLE_ALIAS[rawStyle] ?? rawStyle;
|
|
9781
11463
|
const briefStyle = ALLOWED_STYLES.has(styleResolved) ? styleResolved : "auto";
|
|
9782
|
-
const { room, members } = createRoom({
|
|
11464
|
+
const { room, members } = createRoom({
|
|
11465
|
+
name,
|
|
11466
|
+
subject,
|
|
11467
|
+
mode,
|
|
11468
|
+
intensity,
|
|
11469
|
+
briefStyle,
|
|
11470
|
+
agentIds,
|
|
11471
|
+
parentRoomId,
|
|
11472
|
+
parentBriefId
|
|
11473
|
+
});
|
|
9783
11474
|
insertConfigEvent({
|
|
9784
11475
|
roomId: room.id,
|
|
9785
11476
|
kind: "room-opened",
|
|
@@ -9805,6 +11496,10 @@ function roomsRouter() {
|
|
|
9805
11496
|
});
|
|
9806
11497
|
roomBus.emit(room.id, { type: "message-final", messageId: opening.id });
|
|
9807
11498
|
setAwaitingClarify(room.id, true);
|
|
11499
|
+
if (mode === "research" && !hasBraveKey()) {
|
|
11500
|
+
const langGuess = /[一-鿿]/.test(subject) ? "zh" : "en";
|
|
11501
|
+
announceResearchHint(room.id, langGuess);
|
|
11502
|
+
}
|
|
9808
11503
|
void (async () => {
|
|
9809
11504
|
try {
|
|
9810
11505
|
if (autoPick) {
|
|
@@ -9829,6 +11524,22 @@ function roomsRouter() {
|
|
|
9829
11524
|
const snap = getRoomQueueSnapshot(id);
|
|
9830
11525
|
return c.json({ ...state, events, queue: snap.queue, round: snap.round });
|
|
9831
11526
|
});
|
|
11527
|
+
r.get("/:id/export.md", (c) => {
|
|
11528
|
+
const id = c.req.param("id");
|
|
11529
|
+
const room = getRoom(id);
|
|
11530
|
+
if (!room) return c.text("not found", 404);
|
|
11531
|
+
const memberRows = listRoomMembers(id);
|
|
11532
|
+
const members = memberRows.map((m) => getAgent(m.agentId)).filter((a) => a !== null);
|
|
11533
|
+
const messages = listMessages(id);
|
|
11534
|
+
const briefs = listBriefsForRoom(id).slice().sort((a, b) => a.createdAt - b.createdAt);
|
|
11535
|
+
const md = buildRoomExportMarkdown({ room, members, messages, briefs });
|
|
11536
|
+
const filename = roomExportFilename(room);
|
|
11537
|
+
return c.body(md, 200, {
|
|
11538
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
11539
|
+
"Content-Disposition": `attachment; filename="${filename}"`,
|
|
11540
|
+
"Cache-Control": "no-store"
|
|
11541
|
+
});
|
|
11542
|
+
});
|
|
9832
11543
|
r.get("/:id/stream", (c) => {
|
|
9833
11544
|
const id = c.req.param("id");
|
|
9834
11545
|
if (!getRoom(id)) return c.json({ error: "not found" }, 404);
|
|
@@ -10002,6 +11713,42 @@ function roomsRouter() {
|
|
|
10002
11713
|
resumeRoom(id);
|
|
10003
11714
|
return c.json({ room: getRoom(id) });
|
|
10004
11715
|
});
|
|
11716
|
+
r.post("/:id/paused-input", async (c) => {
|
|
11717
|
+
const id = c.req.param("id");
|
|
11718
|
+
const room = getRoom(id);
|
|
11719
|
+
if (!room) return c.json({ error: "not found" }, 404);
|
|
11720
|
+
if (room.status !== "paused") return c.json({ error: "room is not paused" }, 409);
|
|
11721
|
+
let body;
|
|
11722
|
+
try {
|
|
11723
|
+
body = await c.req.json();
|
|
11724
|
+
} catch {
|
|
11725
|
+
return c.json({ error: "invalid JSON body" }, 400);
|
|
11726
|
+
}
|
|
11727
|
+
const b = body ?? {};
|
|
11728
|
+
const text = typeof b.body === "string" ? b.body.trim() : "";
|
|
11729
|
+
if (!text) return c.json({ error: "body is required" }, 400);
|
|
11730
|
+
const roundNum = Math.max(1, getCurrentRound(id));
|
|
11731
|
+
const msg = insertMessage({
|
|
11732
|
+
roomId: id,
|
|
11733
|
+
authorKind: "user",
|
|
11734
|
+
body: text,
|
|
11735
|
+
meta: {},
|
|
11736
|
+
roundNum
|
|
11737
|
+
});
|
|
11738
|
+
roomBus.emit(id, {
|
|
11739
|
+
type: "message-appended",
|
|
11740
|
+
messageId: msg.id,
|
|
11741
|
+
authorKind: "user",
|
|
11742
|
+
authorId: null,
|
|
11743
|
+
replyToId: null,
|
|
11744
|
+
body: msg.body,
|
|
11745
|
+
meta: msg.meta,
|
|
11746
|
+
roundNum: msg.roundNum,
|
|
11747
|
+
createdAt: msg.createdAt
|
|
11748
|
+
});
|
|
11749
|
+
roomBus.emit(id, { type: "message-final", messageId: msg.id });
|
|
11750
|
+
return c.json(msg);
|
|
11751
|
+
});
|
|
10005
11752
|
r.patch("/:id", async (c) => {
|
|
10006
11753
|
const id = c.req.param("id");
|
|
10007
11754
|
const room = getRoom(id);
|
|
@@ -10014,7 +11761,7 @@ function roomsRouter() {
|
|
|
10014
11761
|
return c.json({ error: "invalid JSON body" }, 400);
|
|
10015
11762
|
}
|
|
10016
11763
|
const b = body ?? {};
|
|
10017
|
-
const ALLOWED_MODES = /* @__PURE__ */ new Set(["brainstorm", "constructive", "debate", "
|
|
11764
|
+
const ALLOWED_MODES = /* @__PURE__ */ new Set(["brainstorm", "constructive", "research", "debate", "critique"]);
|
|
10018
11765
|
const ALLOWED_INTENSITY = /* @__PURE__ */ new Set(["calm", "sharp", "brutal"]);
|
|
10019
11766
|
const ALLOWED_STYLES = /* @__PURE__ */ new Set(["auto", "mckinsey", "gartner", "a16z", "anthropic", "8bit"]);
|
|
10020
11767
|
const STYLE_ALIAS = { mck: "mckinsey" };
|
|
@@ -10314,6 +12061,84 @@ function roomsRouter() {
|
|
|
10314
12061
|
});
|
|
10315
12062
|
return r;
|
|
10316
12063
|
}
|
|
12064
|
+
function roomExportFilename(room) {
|
|
12065
|
+
const num = String(room.number).padStart(3, "0");
|
|
12066
|
+
const d = new Date(room.createdAt);
|
|
12067
|
+
const date = isNaN(d.getTime()) ? "unknown" : d.toISOString().slice(0, 10);
|
|
12068
|
+
return `boardroom-${num}-${date}.md`;
|
|
12069
|
+
}
|
|
12070
|
+
function buildRoomExportMarkdown(opts) {
|
|
12071
|
+
const { room, members, messages, briefs } = opts;
|
|
12072
|
+
if (!room) return "";
|
|
12073
|
+
const fmtFull = (ts) => {
|
|
12074
|
+
if (!ts) return "\u2014";
|
|
12075
|
+
const d = new Date(ts);
|
|
12076
|
+
if (isNaN(d.getTime())) return "\u2014";
|
|
12077
|
+
return d.toISOString().replace("T", " ").slice(0, 19) + " UTC";
|
|
12078
|
+
};
|
|
12079
|
+
const fmtTime = (ts) => {
|
|
12080
|
+
const d = new Date(ts);
|
|
12081
|
+
if (isNaN(d.getTime())) return "\u2014";
|
|
12082
|
+
return d.toISOString().slice(11, 19) + "Z";
|
|
12083
|
+
};
|
|
12084
|
+
const directors = members.filter((a) => !!a && a.roleKind === "director");
|
|
12085
|
+
const directorLine = directors.length > 0 ? directors.map((a) => a.name).join(" \xB7 ") : "(no directors)";
|
|
12086
|
+
const nameById = /* @__PURE__ */ new Map();
|
|
12087
|
+
for (const a of members) {
|
|
12088
|
+
if (a) nameById.set(a.id, { name: a.name, handle: a.handle, roleKind: a.roleKind });
|
|
12089
|
+
}
|
|
12090
|
+
const headerLines = [
|
|
12091
|
+
`# Room #${room.number} \xB7 ${room.subject}`,
|
|
12092
|
+
``,
|
|
12093
|
+
`\xB7 **Status** \xB7 ${room.status}`,
|
|
12094
|
+
`\xB7 **Mode** \xB7 ${room.mode}`,
|
|
12095
|
+
`\xB7 **Intensity** \xB7 ${room.intensity}`,
|
|
12096
|
+
`\xB7 **Convened** \xB7 ${fmtFull(room.createdAt)}`
|
|
12097
|
+
];
|
|
12098
|
+
if (room.adjournedAt) headerLines.push(`\xB7 **Adjourned** \xB7 ${fmtFull(room.adjournedAt)}`);
|
|
12099
|
+
headerLines.push(`\xB7 **Directors** \xB7 ${directorLine}`);
|
|
12100
|
+
headerLines.push(``, `---`, ``);
|
|
12101
|
+
const transcriptLines = [`## Transcript`, ``];
|
|
12102
|
+
let lastRound = -1;
|
|
12103
|
+
for (const m of messages) {
|
|
12104
|
+
if (m.authorKind === "system") continue;
|
|
12105
|
+
if (!m.body || !m.body.trim()) continue;
|
|
12106
|
+
if (m.roundNum !== lastRound && m.roundNum > 0) {
|
|
12107
|
+
transcriptLines.push(`### Round ${m.roundNum}`, ``);
|
|
12108
|
+
lastRound = m.roundNum;
|
|
12109
|
+
}
|
|
12110
|
+
const t = fmtTime(m.createdAt);
|
|
12111
|
+
if (m.authorKind === "user") {
|
|
12112
|
+
transcriptLines.push(`**You** \xB7 ${t}`, ``);
|
|
12113
|
+
const quoted = m.body.trim().split("\n").map((line) => `> ${line}`).join("\n");
|
|
12114
|
+
transcriptLines.push(quoted, ``);
|
|
12115
|
+
continue;
|
|
12116
|
+
}
|
|
12117
|
+
const meta = m.authorId ? nameById.get(m.authorId) : null;
|
|
12118
|
+
const speakerName = meta ? meta.name : "(unknown)";
|
|
12119
|
+
const handlePart = meta && meta.handle ? ` \xB7 _${meta.handle}_` : "";
|
|
12120
|
+
const rolePart = meta && meta.roleKind === "moderator" ? " \xB7 Chair" : "";
|
|
12121
|
+
transcriptLines.push(`**${speakerName}**${handlePart}${rolePart} \xB7 ${t}`, ``);
|
|
12122
|
+
transcriptLines.push(m.body.trim(), ``);
|
|
12123
|
+
}
|
|
12124
|
+
const briefsLines = [`---`, ``, `## Filed Reports`, ``];
|
|
12125
|
+
if (briefs.length === 0) {
|
|
12126
|
+
briefsLines.push(`_No reports filed in this room._`, ``);
|
|
12127
|
+
} else {
|
|
12128
|
+
briefs.forEach((b, i) => {
|
|
12129
|
+
const ts = fmtFull(b.createdAt);
|
|
12130
|
+
const styleParts = [];
|
|
12131
|
+
if (b.houseStyle && b.houseStyle !== "boardroom-default") styleParts.push(b.houseStyle);
|
|
12132
|
+
if (b.spine && b.spine !== "boardroom-dark") styleParts.push(b.spine);
|
|
12133
|
+
const stylePart = styleParts.length > 0 ? ` \xB7 ${styleParts.join(" / ")}` : "";
|
|
12134
|
+
const supplementPart = b.supplement ? ` \xB7 supplement: "${b.supplement}"` : "";
|
|
12135
|
+
briefsLines.push(`_Brief #${i + 1} \xB7 ${ts}${stylePart}${supplementPart}_`, ``);
|
|
12136
|
+
briefsLines.push(b.bodyMd.trim(), ``);
|
|
12137
|
+
if (i < briefs.length - 1) briefsLines.push(`---`, ``);
|
|
12138
|
+
});
|
|
12139
|
+
}
|
|
12140
|
+
return [...headerLines, ...transcriptLines, ...briefsLines].join("\n") + "\n";
|
|
12141
|
+
}
|
|
10317
12142
|
|
|
10318
12143
|
// src/routes/usage.ts
|
|
10319
12144
|
import { Hono as Hono8 } from "hono";
|
|
@@ -10357,6 +12182,7 @@ function usageRouter() {
|
|
|
10357
12182
|
}
|
|
10358
12183
|
|
|
10359
12184
|
// src/server.ts
|
|
12185
|
+
init_paths();
|
|
10360
12186
|
function createApp() {
|
|
10361
12187
|
const app = new Hono9();
|
|
10362
12188
|
const dir = publicDir();
|
|
@@ -10380,8 +12206,19 @@ Build the package or check that public/ is bundled alongside dist/.`
|
|
|
10380
12206
|
});
|
|
10381
12207
|
app.get(
|
|
10382
12208
|
"/api/health",
|
|
10383
|
-
(c) => c.json({ ok: true, version: "0.1.
|
|
12209
|
+
(c) => c.json({ ok: true, version: "0.1.2", time: (/* @__PURE__ */ new Date()).toISOString() })
|
|
10384
12210
|
);
|
|
12211
|
+
app.get("/api/system/migrations", async (c) => {
|
|
12212
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), db_exports));
|
|
12213
|
+
try {
|
|
12214
|
+
const rows = getDb2().prepare("SELECT name, applied_at FROM _migrations ORDER BY applied_at ASC, name ASC").all();
|
|
12215
|
+
return c.json({
|
|
12216
|
+
migrations: rows.map((r) => ({ name: r.name, appliedAt: r.applied_at }))
|
|
12217
|
+
});
|
|
12218
|
+
} catch {
|
|
12219
|
+
return c.json({ migrations: [] });
|
|
12220
|
+
}
|
|
12221
|
+
});
|
|
10385
12222
|
app.route("/api/prefs", prefsRouter());
|
|
10386
12223
|
app.route("/api/agents", agentsRouter());
|
|
10387
12224
|
app.route("/api/keys", keysRouter());
|
|
@@ -10393,8 +12230,7 @@ Build the package or check that public/ is bundled alongside dist/.`
|
|
|
10393
12230
|
app.use(
|
|
10394
12231
|
"/*",
|
|
10395
12232
|
serveStatic({
|
|
10396
|
-
root: dir
|
|
10397
|
-
rewriteRequestPath: (path) => path === "/" ? "/prototype-dashboard.html" : path
|
|
12233
|
+
root: dir
|
|
10398
12234
|
})
|
|
10399
12235
|
);
|
|
10400
12236
|
return app;
|
|
@@ -10415,6 +12251,10 @@ async function startServer(opts) {
|
|
|
10415
12251
|
};
|
|
10416
12252
|
}
|
|
10417
12253
|
|
|
12254
|
+
// src/cli.ts
|
|
12255
|
+
init_db();
|
|
12256
|
+
init_paths();
|
|
12257
|
+
|
|
10418
12258
|
// src/utils/port.ts
|
|
10419
12259
|
import { createServer } from "net";
|
|
10420
12260
|
async function findFreePort(base = 3030, maxTries = 20) {
|
|
@@ -10433,7 +12273,7 @@ function isPortFree(port) {
|
|
|
10433
12273
|
}
|
|
10434
12274
|
|
|
10435
12275
|
// src/cli.ts
|
|
10436
|
-
var VERSION = "0.1.
|
|
12276
|
+
var VERSION = "0.1.2";
|
|
10437
12277
|
async function main() {
|
|
10438
12278
|
const program = new Command().name("privateboard").description("PrivateBoard \xB7 your private board meeting, on call. Local-first, multi-agent thinking.").version(VERSION).option("-p, --port <n>", "port to listen on (default: auto-detect from 3030)").option("--host <h>", "host to bind", "127.0.0.1").option("--no-open", "don't open the browser automatically");
|
|
10439
12279
|
program.parse();
|
|
@@ -10447,6 +12287,28 @@ async function main() {
|
|
|
10447
12287
|
reconcile = { switched: r.switched.length, cleared: r.cleared.length };
|
|
10448
12288
|
} catch (e) {
|
|
10449
12289
|
process.stderr.write(`[boot] reconcile failed: ${e instanceof Error ? e.message : String(e)}
|
|
12290
|
+
`);
|
|
12291
|
+
}
|
|
12292
|
+
try {
|
|
12293
|
+
const orphans = cleanupOrphanedStreams();
|
|
12294
|
+
if (orphans.fixed + orphans.deleted > 0) {
|
|
12295
|
+
process.stderr.write(
|
|
12296
|
+
`[boot] cleaned ${orphans.fixed} stuck stream(s), dropped ${orphans.deleted} empty placeholder(s)
|
|
12297
|
+
`
|
|
12298
|
+
);
|
|
12299
|
+
}
|
|
12300
|
+
} catch (e) {
|
|
12301
|
+
process.stderr.write(`[boot] orphan cleanup failed: ${e instanceof Error ? e.message : String(e)}
|
|
12302
|
+
`);
|
|
12303
|
+
}
|
|
12304
|
+
try {
|
|
12305
|
+
const fixed = recoverStuckClarifyRooms();
|
|
12306
|
+
if (fixed > 0) {
|
|
12307
|
+
process.stderr.write(`[boot] unstuck ${fixed} room(s) frozen in chair-clarify
|
|
12308
|
+
`);
|
|
12309
|
+
}
|
|
12310
|
+
} catch (e) {
|
|
12311
|
+
process.stderr.write(`[boot] clarify recovery failed: ${e instanceof Error ? e.message : String(e)}
|
|
10450
12312
|
`);
|
|
10451
12313
|
}
|
|
10452
12314
|
const portArg = opts.port ? Number.parseInt(opts.port, 10) : void 0;
|
|
@@ -10481,7 +12343,10 @@ async function main() {
|
|
|
10481
12343
|
open(server.url).catch(() => {
|
|
10482
12344
|
});
|
|
10483
12345
|
}
|
|
12346
|
+
let shuttingDown = false;
|
|
10484
12347
|
const shutdown = async (signal) => {
|
|
12348
|
+
if (shuttingDown) return;
|
|
12349
|
+
shuttingDown = true;
|
|
10485
12350
|
process.stdout.write(`
|
|
10486
12351
|
\u25B8 ${signal} received \xB7 shutting down
|
|
10487
12352
|
`);
|
|
@@ -10490,10 +12355,22 @@ async function main() {
|
|
|
10490
12355
|
} catch (e) {
|
|
10491
12356
|
console.error(" ! error closing server", e);
|
|
10492
12357
|
}
|
|
12358
|
+
try {
|
|
12359
|
+
closeDb();
|
|
12360
|
+
} catch (e) {
|
|
12361
|
+
console.error(" ! error closing db", e);
|
|
12362
|
+
}
|
|
10493
12363
|
process.exit(0);
|
|
10494
12364
|
};
|
|
10495
12365
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
10496
12366
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
12367
|
+
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
12368
|
+
process.on("exit", () => {
|
|
12369
|
+
try {
|
|
12370
|
+
closeDb();
|
|
12371
|
+
} catch {
|
|
12372
|
+
}
|
|
12373
|
+
});
|
|
10497
12374
|
}
|
|
10498
12375
|
main().catch((err2) => {
|
|
10499
12376
|
console.error("privateboard failed to start:", err2);
|