privateboard 0.1.15 → 0.1.16
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 +1626 -232
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/public/adjourn-overlay.css +6 -6
- package/public/agent-build-bgm.js +292 -0
- package/public/agent-overlay.css +14 -14
- package/public/agent-profile.css +408 -87
- package/public/agent-profile.js +254 -0
- package/public/app.js +1577 -731
- package/public/home.html +26 -26
- package/public/i18n.js +1890 -21
- package/public/icons/logo2.png +0 -0
- package/public/icons/private-board-vi.html +1716 -0
- package/public/index.html +2242 -1114
- package/public/magazine.html +12 -12
- package/public/new-agent.css +29 -29
- package/public/newspaper.html +20 -20
- package/public/onboarding.css +350 -272
- package/public/onboarding.js +614 -323
- package/public/quote-cta.css +4 -4
- package/public/report.html +2008 -1673
- package/public/room-settings.css +192 -24
- package/public/room-settings.js +5 -0
- package/public/share-cover-svg-creator.js +736 -0
- package/public/themes.css +0 -34
- package/public/typing-sfx.js +176 -3
- package/public/user-settings.css +50 -27
- package/public/user-settings.js +43 -14
- package/public/voice-onboarding.css +425 -0
- package/public/voice-onboarding.js +144 -0
- package/public/voice-replay.css +31 -38
- package/public/voice-replay.js +12 -11
package/dist/cli.js
CHANGED
|
@@ -584,6 +584,122 @@ ALTER TABLE topic_recs ADD COLUMN tag TEXT;
|
|
|
584
584
|
}
|
|
585
585
|
});
|
|
586
586
|
|
|
587
|
+
// src/storage/migrations/036_negative_space.sql
|
|
588
|
+
var negative_space_default;
|
|
589
|
+
var init_negative_space = __esm({
|
|
590
|
+
"src/storage/migrations/036_negative_space.sql"() {
|
|
591
|
+
negative_space_default = `-- Negative-space memory \xB7 Layer 3.2 of the divergence stack.
|
|
592
|
+
-- At round-end the chair extracts "what this round did NOT touch
|
|
593
|
+
-- but should have" \u2014 angles raised-then-abandoned, dimensions
|
|
594
|
+
-- conspicuously absent. These get injected into the next round's
|
|
595
|
+
-- director prompts as "UNEXPLORED ANGLES" so the room has positive-
|
|
596
|
+
-- space breadcrumbs alongside the frame-break negative-space rules.
|
|
597
|
+
--
|
|
598
|
+
-- One row per angle (not one row per round) so the next-round
|
|
599
|
+
-- prompt can pull the top-N most recent regardless of round
|
|
600
|
+
-- boundaries \xB7 enables cross-round angle memory.
|
|
601
|
+
CREATE TABLE IF NOT EXISTS negative_space (
|
|
602
|
+
id TEXT PRIMARY KEY,
|
|
603
|
+
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
604
|
+
round_num INTEGER NOT NULL,
|
|
605
|
+
angle TEXT NOT NULL,
|
|
606
|
+
created_at INTEGER NOT NULL,
|
|
607
|
+
-- Soft consumption flag \xB7 when a subsequent round prompt injected
|
|
608
|
+
-- this angle and the room actually engaged with it, the chair
|
|
609
|
+
-- post-processor flips this to 1. Subsequent rounds prefer
|
|
610
|
+
-- unconsumed angles to avoid suggesting the same thing twice.
|
|
611
|
+
consumed INTEGER NOT NULL DEFAULT 0
|
|
612
|
+
);
|
|
613
|
+
CREATE INDEX IF NOT EXISTS idx_negative_space_room_round
|
|
614
|
+
ON negative_space(room_id, round_num);
|
|
615
|
+
CREATE INDEX IF NOT EXISTS idx_negative_space_room_unconsumed
|
|
616
|
+
ON negative_space(room_id, consumed);
|
|
617
|
+
`;
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// src/storage/migrations/037_topic_branches.sql
|
|
622
|
+
var topic_branches_default;
|
|
623
|
+
var init_topic_branches = __esm({
|
|
624
|
+
"src/storage/migrations/037_topic_branches.sql"() {
|
|
625
|
+
topic_branches_default = `-- Topic-tree tracking \xB7 Layer 3.1 of the divergence stack.
|
|
626
|
+
-- Each director turn gets tagged with a branch_id \xB7 the cluster of
|
|
627
|
+
-- prior turns it extended. New top-level branches are surfaced when
|
|
628
|
+
-- a turn opens a genuinely fresh angle. Drives:
|
|
629
|
+
-- \xB7 dissent-gap picker (Layer 2.1) prefers directors who have NOT
|
|
630
|
+
-- spoken on the dominant branches
|
|
631
|
+
-- \xB7 UI \xB7 optional "topic map" overlay so the user sees the room's
|
|
632
|
+
-- coverage breadth
|
|
633
|
+
-- \xB7 structured summarization \xB7 long rooms can summarize by branch
|
|
634
|
+
-- instead of by chronological round
|
|
635
|
+
CREATE TABLE IF NOT EXISTS topic_branches (
|
|
636
|
+
id TEXT PRIMARY KEY,
|
|
637
|
+
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
638
|
+
label TEXT NOT NULL, -- short noun-phrase name (\u2264 8 words)
|
|
639
|
+
parent_id TEXT, -- nullable \xB7 top-level branches have NULL
|
|
640
|
+
opened_at INTEGER NOT NULL, -- ms epoch \xB7 ordering / decay
|
|
641
|
+
-- Activity counters \xB7 maintained by the post-turn tagger so the
|
|
642
|
+
-- picker can score "this branch is hot" / "this branch is unspoken".
|
|
643
|
+
turn_count INTEGER NOT NULL DEFAULT 0,
|
|
644
|
+
last_speaker_id TEXT
|
|
645
|
+
);
|
|
646
|
+
CREATE INDEX IF NOT EXISTS idx_topic_branches_room
|
|
647
|
+
ON topic_branches(room_id, opened_at);
|
|
648
|
+
|
|
649
|
+
-- Per-message branch assignment \xB7 one row per director message.
|
|
650
|
+
-- Populated by a post-turn haiku tagger that reads the message body
|
|
651
|
+
-- + the room's current branch list and decides: "extends branch X"
|
|
652
|
+
-- or "opens new branch Y". Idempotent \xB7 re-running the tagger for
|
|
653
|
+
-- the same message_id replaces the row.
|
|
654
|
+
CREATE TABLE IF NOT EXISTS message_branches (
|
|
655
|
+
message_id TEXT PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,
|
|
656
|
+
branch_id TEXT NOT NULL REFERENCES topic_branches(id) ON DELETE CASCADE,
|
|
657
|
+
is_opener INTEGER NOT NULL DEFAULT 0, -- 1 = this message opened the branch
|
|
658
|
+
tagged_at INTEGER NOT NULL
|
|
659
|
+
);
|
|
660
|
+
CREATE INDEX IF NOT EXISTS idx_message_branches_branch
|
|
661
|
+
ON message_branches(branch_id);
|
|
662
|
+
`;
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// src/storage/migrations/038_qd_archive.sql
|
|
667
|
+
var qd_archive_default;
|
|
668
|
+
var init_qd_archive = __esm({
|
|
669
|
+
"src/storage/migrations/038_qd_archive.sql"() {
|
|
670
|
+
qd_archive_default = `-- Quality-Diversity behavioral archive \xB7 Layer 4 of the divergence
|
|
671
|
+
-- stack. Each director message gets scored on 3 behavioral
|
|
672
|
+
-- dimensions (abstraction-level / time-scale / stakeholder-scope) by
|
|
673
|
+
-- a cheap haiku post-turn. The resulting (a, t, s) triple maps to a
|
|
674
|
+
-- single cell in a 4\xD74\xD74 = 64-cell grid \xB7 the room's "MAP-Elites
|
|
675
|
+
-- archive". Picker layers can query the archive to reward
|
|
676
|
+
-- candidates whose persona is likely to fill an EMPTY cell, and the
|
|
677
|
+
-- room-end report shows coverage as a divergence KPI.
|
|
678
|
+
--
|
|
679
|
+
-- One row per (message, dimensions) so re-tagging is idempotent \xB7
|
|
680
|
+
-- the post-turn scorer always INSERT OR REPLACE.
|
|
681
|
+
CREATE TABLE IF NOT EXISTS qd_archive (
|
|
682
|
+
message_id TEXT PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,
|
|
683
|
+
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
684
|
+
-- Dimension scores in [0, 1] \xB7 float for diagnostics, INTEGER
|
|
685
|
+
-- bucket [0, 3] for the cell index. All scored by the same haiku
|
|
686
|
+
-- pass to amortize cost.
|
|
687
|
+
abstraction_score REAL NOT NULL,
|
|
688
|
+
abstraction_bucket INTEGER NOT NULL,
|
|
689
|
+
time_score REAL NOT NULL,
|
|
690
|
+
time_bucket INTEGER NOT NULL,
|
|
691
|
+
stakeholder_score REAL NOT NULL,
|
|
692
|
+
stakeholder_bucket INTEGER NOT NULL,
|
|
693
|
+
scored_at INTEGER NOT NULL
|
|
694
|
+
);
|
|
695
|
+
CREATE INDEX IF NOT EXISTS idx_qd_archive_room
|
|
696
|
+
ON qd_archive(room_id);
|
|
697
|
+
CREATE INDEX IF NOT EXISTS idx_qd_archive_room_cell
|
|
698
|
+
ON qd_archive(room_id, abstraction_bucket, time_bucket, stakeholder_bucket);
|
|
699
|
+
`;
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
587
703
|
// src/storage/db.ts
|
|
588
704
|
var db_exports = {};
|
|
589
705
|
__export(db_exports, {
|
|
@@ -676,6 +792,9 @@ var init_db = __esm({
|
|
|
676
792
|
init_room_members_removed_at();
|
|
677
793
|
init_user_topic_recs();
|
|
678
794
|
init_topic_rec_tag();
|
|
795
|
+
init_negative_space();
|
|
796
|
+
init_topic_branches();
|
|
797
|
+
init_qd_archive();
|
|
679
798
|
MIGRATIONS = [
|
|
680
799
|
{ name: "001_init.sql", sql: init_default },
|
|
681
800
|
{ name: "002_default_opus.sql", sql: default_opus_default },
|
|
@@ -711,7 +830,10 @@ var init_db = __esm({
|
|
|
711
830
|
{ name: "032_room_vote_trigger.sql", sql: room_vote_trigger_default },
|
|
712
831
|
{ name: "033_room_members_removed_at.sql", sql: room_members_removed_at_default },
|
|
713
832
|
{ name: "034_user_topic_recs.sql", sql: user_topic_recs_default },
|
|
714
|
-
{ name: "035_topic_rec_tag.sql", sql: topic_rec_tag_default }
|
|
833
|
+
{ name: "035_topic_rec_tag.sql", sql: topic_rec_tag_default },
|
|
834
|
+
{ name: "036_negative_space.sql", sql: negative_space_default },
|
|
835
|
+
{ name: "037_topic_branches.sql", sql: topic_branches_default },
|
|
836
|
+
{ name: "038_qd_archive.sql", sql: qd_archive_default }
|
|
715
837
|
];
|
|
716
838
|
_db = null;
|
|
717
839
|
}
|
|
@@ -965,6 +1087,8 @@ function parsePersonaSpec(raw) {
|
|
|
965
1087
|
const differentiationScore = typeof obj.differentiationScore === "number" && Number.isFinite(obj.differentiationScore) ? obj.differentiationScore : null;
|
|
966
1088
|
const toolAccessRaw = obj.toolAccess || {};
|
|
967
1089
|
const toolAccess = { webSearch: toolAccessRaw.webSearch !== false };
|
|
1090
|
+
const buildLog = parseBuildLog(obj.buildLog);
|
|
1091
|
+
const guessName = typeof obj.guessName === "string" && obj.guessName.trim().length > 0 ? obj.guessName.trim() : void 0;
|
|
968
1092
|
return {
|
|
969
1093
|
version: 1,
|
|
970
1094
|
generatedAt,
|
|
@@ -976,12 +1100,75 @@ function parsePersonaSpec(raw) {
|
|
|
976
1100
|
reflectionChecklist,
|
|
977
1101
|
evalSet,
|
|
978
1102
|
differentiationScore,
|
|
979
|
-
toolAccess
|
|
1103
|
+
toolAccess,
|
|
1104
|
+
...guessName ? { guessName } : {},
|
|
1105
|
+
...buildLog ? { buildLog } : {}
|
|
980
1106
|
};
|
|
981
1107
|
} catch {
|
|
982
1108
|
return null;
|
|
983
1109
|
}
|
|
984
1110
|
}
|
|
1111
|
+
function parseBuildLog(raw) {
|
|
1112
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1113
|
+
const obj = raw;
|
|
1114
|
+
const narrative = typeof obj.narrative === "string" ? obj.narrative : "";
|
|
1115
|
+
const locale = obj.locale === "en" || obj.locale === "zh" || obj.locale === "ja" || obj.locale === "es" ? obj.locale : "en";
|
|
1116
|
+
const generatedAt = typeof obj.generatedAt === "number" && Number.isFinite(obj.generatedAt) ? obj.generatedAt : 0;
|
|
1117
|
+
const events = [];
|
|
1118
|
+
if (Array.isArray(obj.events)) {
|
|
1119
|
+
for (const e of obj.events) {
|
|
1120
|
+
if (!e || typeof e !== "object") continue;
|
|
1121
|
+
const ev = e;
|
|
1122
|
+
const ts = typeof ev.ts === "number" ? ev.ts : 0;
|
|
1123
|
+
const kind = ev.kind;
|
|
1124
|
+
if (kind === "phase-start" && typeof ev.phase === "number" && typeof ev.label === "string") {
|
|
1125
|
+
events.push({ kind, ts, phase: ev.phase, label: ev.label });
|
|
1126
|
+
} else if (kind === "phase-end" && typeof ev.phase === "number" && typeof ev.durationMs === "number") {
|
|
1127
|
+
events.push({ kind, ts, phase: ev.phase, durationMs: ev.durationMs });
|
|
1128
|
+
} else if (kind === "dimension-plan" && Array.isArray(ev.dimensions)) {
|
|
1129
|
+
const dims = ev.dimensions.flatMap((d) => {
|
|
1130
|
+
if (!d || typeof d !== "object") return [];
|
|
1131
|
+
const x = d;
|
|
1132
|
+
if (typeof x.dimension !== "string" || typeof x.query !== "string") return [];
|
|
1133
|
+
return [{
|
|
1134
|
+
dimension: x.dimension,
|
|
1135
|
+
query: x.query,
|
|
1136
|
+
why: typeof x.why === "string" ? x.why : ""
|
|
1137
|
+
}];
|
|
1138
|
+
});
|
|
1139
|
+
events.push({ kind, ts, dimensions: dims });
|
|
1140
|
+
} else if (kind === "search" && typeof ev.query === "string") {
|
|
1141
|
+
events.push({
|
|
1142
|
+
kind,
|
|
1143
|
+
ts,
|
|
1144
|
+
query: ev.query,
|
|
1145
|
+
resultsCount: typeof ev.resultsCount === "number" ? ev.resultsCount : 0,
|
|
1146
|
+
pagesRead: typeof ev.pagesRead === "number" ? ev.pagesRead : 0,
|
|
1147
|
+
...typeof ev.dimension === "string" ? { dimension: ev.dimension } : {},
|
|
1148
|
+
...typeof ev.round === "number" ? { round: ev.round } : {},
|
|
1149
|
+
...ev.topup === true ? { topup: true } : {}
|
|
1150
|
+
});
|
|
1151
|
+
} else if (kind === "divergence") {
|
|
1152
|
+
events.push({
|
|
1153
|
+
kind,
|
|
1154
|
+
ts,
|
|
1155
|
+
score: typeof ev.score === "number" && Number.isFinite(ev.score) ? ev.score : null
|
|
1156
|
+
});
|
|
1157
|
+
} else if (kind === "error" && typeof ev.message === "string") {
|
|
1158
|
+
events.push({ kind, ts, message: ev.message });
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (!narrative && events.length === 0) return null;
|
|
1163
|
+
const totalTokens = typeof obj.totalTokens === "number" && Number.isFinite(obj.totalTokens) ? Math.max(0, Math.round(obj.totalTokens)) : void 0;
|
|
1164
|
+
return {
|
|
1165
|
+
narrative,
|
|
1166
|
+
locale,
|
|
1167
|
+
generatedAt,
|
|
1168
|
+
events,
|
|
1169
|
+
...totalTokens !== void 0 ? { totalTokens } : {}
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
985
1172
|
function serializeVoice(v) {
|
|
986
1173
|
if (!v) return null;
|
|
987
1174
|
const provider = VALID_VOICE_PROVIDERS.has(v.provider) ? v.provider : null;
|
|
@@ -4303,6 +4490,169 @@ var PersonaBus = class {
|
|
|
4303
4490
|
};
|
|
4304
4491
|
var personaBus = new PersonaBus();
|
|
4305
4492
|
|
|
4493
|
+
// src/orchestrator/persona-narrator.ts
|
|
4494
|
+
var NARRATOR_TIMEOUT_MS = 6e4;
|
|
4495
|
+
var NARRATOR_MAX_TOKENS = 900;
|
|
4496
|
+
function narratorCandidates() {
|
|
4497
|
+
const out = [];
|
|
4498
|
+
const utility = utilityModelFor();
|
|
4499
|
+
if (utility) out.push(utility);
|
|
4500
|
+
const flagship = effectiveDefaultModel();
|
|
4501
|
+
if (flagship && !out.includes(flagship)) out.push(flagship);
|
|
4502
|
+
for (const m of reachableModels()) {
|
|
4503
|
+
if (FLAGSHIP_TIER.has(m.modelV) && !out.includes(m.modelV)) out.push(m.modelV);
|
|
4504
|
+
}
|
|
4505
|
+
return out;
|
|
4506
|
+
}
|
|
4507
|
+
var LOCALE_HINTS = {
|
|
4508
|
+
en: "Write the summary in clear, warm English.",
|
|
4509
|
+
zh: "\u7528\u6D41\u7545\u81EA\u7136\u7684\u7B80\u4F53\u4E2D\u6587\u5199\u8FD9\u6BB5\u603B\u8FF0\u3002",
|
|
4510
|
+
ja: "\u89AA\u3057\u307F\u3084\u3059\u3044\u81EA\u7136\u306A\u65E5\u672C\u8A9E\u3067\u66F8\u3044\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
4511
|
+
es: "Escribe el resumen en espa\xF1ol claro y cercano."
|
|
4512
|
+
};
|
|
4513
|
+
function summarizeEvents(events) {
|
|
4514
|
+
const phases = [];
|
|
4515
|
+
const searchesByDim = /* @__PURE__ */ new Map();
|
|
4516
|
+
const topupSearches = [];
|
|
4517
|
+
let dimensionPlan = [];
|
|
4518
|
+
let divergenceScore = null;
|
|
4519
|
+
for (const e of events) {
|
|
4520
|
+
if (e.kind === "phase-start") {
|
|
4521
|
+
phases.push({ phase: e.phase, label: e.label });
|
|
4522
|
+
} else if (e.kind === "phase-end") {
|
|
4523
|
+
const found = phases.find((p) => p.phase === e.phase);
|
|
4524
|
+
if (found) found.durationMs = e.durationMs;
|
|
4525
|
+
} else if (e.kind === "dimension-plan") {
|
|
4526
|
+
dimensionPlan = e.dimensions;
|
|
4527
|
+
} else if (e.kind === "search") {
|
|
4528
|
+
if (e.topup) {
|
|
4529
|
+
topupSearches.push({ query: e.query, sources: e.pagesRead });
|
|
4530
|
+
} else if (e.dimension) {
|
|
4531
|
+
const cur = searchesByDim.get(e.dimension) ?? { count: 0, sources: 0, queries: [] };
|
|
4532
|
+
cur.count += 1;
|
|
4533
|
+
cur.sources += e.pagesRead;
|
|
4534
|
+
cur.queries.push(e.query);
|
|
4535
|
+
searchesByDim.set(e.dimension, cur);
|
|
4536
|
+
}
|
|
4537
|
+
} else if (e.kind === "divergence") {
|
|
4538
|
+
divergenceScore = e.score;
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
const lines = [];
|
|
4542
|
+
lines.push("Phases that ran:");
|
|
4543
|
+
for (const p of phases) {
|
|
4544
|
+
const dur = typeof p.durationMs === "number" ? ` (${Math.round(p.durationMs / 1e3)}s)` : "";
|
|
4545
|
+
lines.push(` ${p.phase}. ${p.label}${dur}`);
|
|
4546
|
+
}
|
|
4547
|
+
if (dimensionPlan.length > 0) {
|
|
4548
|
+
lines.push("");
|
|
4549
|
+
lines.push(`Research angles (picked by the planner, ${dimensionPlan.length} total):`);
|
|
4550
|
+
for (const d of dimensionPlan) {
|
|
4551
|
+
const stats = searchesByDim.get(d.dimension);
|
|
4552
|
+
const tail = stats ? ` \u2014 ${stats.sources} sources read` : "";
|
|
4553
|
+
lines.push(` \u2022 ${d.dimension}: ${d.why || d.query}${tail}`);
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
if (topupSearches.length > 0) {
|
|
4557
|
+
lines.push("");
|
|
4558
|
+
lines.push(`Top-up gap searches (${topupSearches.length}):`);
|
|
4559
|
+
for (const t of topupSearches) {
|
|
4560
|
+
lines.push(` \u2022 "${t.query}" \u2192 ${t.sources} sources`);
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
if (divergenceScore !== null) {
|
|
4564
|
+
lines.push("");
|
|
4565
|
+
lines.push(`Voice uniqueness vs generic AI baseline: ${Math.round(divergenceScore * 100)}%`);
|
|
4566
|
+
}
|
|
4567
|
+
return lines.join("\n");
|
|
4568
|
+
}
|
|
4569
|
+
var NARRATOR_SYSTEM = [
|
|
4570
|
+
"You are writing a short, pitch-style summary of how a custom AI persona was just built.",
|
|
4571
|
+
"",
|
|
4572
|
+
"Audience: the person who kicked the build. They watched a progress bar \u2014 they have no idea what the pipeline actually DID under the hood. Your job is to tell them, in plain language.",
|
|
4573
|
+
"",
|
|
4574
|
+
"Hard rules:",
|
|
4575
|
+
" 1. NEVER use jargon. Do NOT say 'multi-dimensional retrieval', 'ReAct loop', 'lexical divergence', 'parallel batch', 'synthesis', 'critique pass', 'eval set', 'differentiation probe'.",
|
|
4576
|
+
" 2. Instead, say things like: 'we looked at the question from several different angles', 'we double-checked a couple of gaps', 'we drafted the personality twice \u2014 once cold, once after reading up', 'this voice came out N% different from a generic AI'.",
|
|
4577
|
+
" 3. First-person plural ('we'). Warm tone. Short sentences. 3-5 short paragraphs.",
|
|
4578
|
+
" 4. Use specific details from the build log \u2014 name the angles by name, mention concrete sources-read counts, quote a sample query if it helps.",
|
|
4579
|
+
" 5. Stay between 200 and 400 words.",
|
|
4580
|
+
" 6. Do NOT use Markdown. Plain prose only, no headers, no bullet points.",
|
|
4581
|
+
" 7. Do NOT mention the model name, the agent's database id, or any internal terminology.",
|
|
4582
|
+
" 8. End with a single short sentence that invites the user to talk to the new director."
|
|
4583
|
+
].join("\n");
|
|
4584
|
+
async function runPersonaNarrator(state, input) {
|
|
4585
|
+
const eventsSummary = summarizeEvents(input.events);
|
|
4586
|
+
if (!eventsSummary.trim()) return "";
|
|
4587
|
+
const profileBlock = input.profileV2 ? [
|
|
4588
|
+
"Persona shape (refined):",
|
|
4589
|
+
"",
|
|
4590
|
+
"```json",
|
|
4591
|
+
JSON.stringify(
|
|
4592
|
+
{
|
|
4593
|
+
intellectualLineage: input.profileV2.intellectualLineage,
|
|
4594
|
+
contrarianTakes: input.profileV2.contrarianTakes,
|
|
4595
|
+
loadBearingConcepts: input.profileV2.loadBearingConcepts
|
|
4596
|
+
},
|
|
4597
|
+
null,
|
|
4598
|
+
2
|
|
4599
|
+
),
|
|
4600
|
+
"```"
|
|
4601
|
+
].join("\n") : "";
|
|
4602
|
+
const userBody = [
|
|
4603
|
+
`What the user asked us to build:`,
|
|
4604
|
+
``,
|
|
4605
|
+
input.description.trim(),
|
|
4606
|
+
``,
|
|
4607
|
+
input.guessName ? `Director we ended up naming: ${input.guessName}` : "",
|
|
4608
|
+
"",
|
|
4609
|
+
"Build log (structured):",
|
|
4610
|
+
"",
|
|
4611
|
+
eventsSummary,
|
|
4612
|
+
"",
|
|
4613
|
+
profileBlock,
|
|
4614
|
+
"",
|
|
4615
|
+
LOCALE_HINTS[input.locale],
|
|
4616
|
+
"",
|
|
4617
|
+
"Now write the 200-400 word pitch-style summary."
|
|
4618
|
+
].filter((s) => s !== "").join("\n");
|
|
4619
|
+
const messages = [
|
|
4620
|
+
{ role: "system", content: NARRATOR_SYSTEM },
|
|
4621
|
+
{ role: "user", content: userBody }
|
|
4622
|
+
];
|
|
4623
|
+
const candidates = narratorCandidates();
|
|
4624
|
+
for (const modelV of candidates) {
|
|
4625
|
+
if (!isModelV(modelV)) continue;
|
|
4626
|
+
if (state.controller.signal.aborted) return "";
|
|
4627
|
+
const controller = new AbortController();
|
|
4628
|
+
const onParentAbort = () => controller.abort();
|
|
4629
|
+
state.controller.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
4630
|
+
const timer = setTimeout(() => controller.abort(), NARRATOR_TIMEOUT_MS);
|
|
4631
|
+
try {
|
|
4632
|
+
const r = await callLLMWithUsage({
|
|
4633
|
+
modelV,
|
|
4634
|
+
messages,
|
|
4635
|
+
temperature: 0.7,
|
|
4636
|
+
maxTokens: NARRATOR_MAX_TOKENS,
|
|
4637
|
+
signal: controller.signal
|
|
4638
|
+
});
|
|
4639
|
+
if (r.usage) {
|
|
4640
|
+
state.totalPromptTokens += r.usage.promptTokens;
|
|
4641
|
+
state.totalOutputTokens += r.usage.completionTokens;
|
|
4642
|
+
}
|
|
4643
|
+
const out = (r.text || "").trim();
|
|
4644
|
+
if (out.length >= 80) return out;
|
|
4645
|
+
} catch (e) {
|
|
4646
|
+
process.stderr.write(`[persona-narrator] ${modelV} failed: ${e instanceof Error ? e.message : String(e)}
|
|
4647
|
+
`);
|
|
4648
|
+
} finally {
|
|
4649
|
+
clearTimeout(timer);
|
|
4650
|
+
state.controller.signal.removeEventListener("abort", onParentAbort);
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
return "";
|
|
4654
|
+
}
|
|
4655
|
+
|
|
4306
4656
|
// src/orchestrator/persona-builder.ts
|
|
4307
4657
|
var LLM_CALL_TIMEOUT_MS = 9e4;
|
|
4308
4658
|
var BUILD_WALL_CLOCK_MS = 10 * 6e4;
|
|
@@ -4341,8 +4691,12 @@ function signalWithTimeout(parent, timeoutMs) {
|
|
|
4341
4691
|
};
|
|
4342
4692
|
}
|
|
4343
4693
|
function bumpTokens(state, prompt, output) {
|
|
4344
|
-
|
|
4345
|
-
|
|
4694
|
+
const p = Math.max(0, prompt | 0);
|
|
4695
|
+
const o = Math.max(0, output | 0);
|
|
4696
|
+
state.promptTokens += p;
|
|
4697
|
+
state.outputTokens += o;
|
|
4698
|
+
state.totalPromptTokens += p;
|
|
4699
|
+
state.totalOutputTokens += o;
|
|
4346
4700
|
return state.promptTokens > PROMPT_TOKEN_CEILING || state.outputTokens > OUTPUT_TOKEN_CEILING;
|
|
4347
4701
|
}
|
|
4348
4702
|
function flagshipCandidates() {
|
|
@@ -4415,14 +4769,19 @@ function startPersonaBuild(opts) {
|
|
|
4415
4769
|
const state = {
|
|
4416
4770
|
id: jobId,
|
|
4417
4771
|
description,
|
|
4772
|
+
locale: opts.locale ?? "en",
|
|
4418
4773
|
startedAt: Date.now(),
|
|
4419
4774
|
controller: new AbortController(),
|
|
4420
4775
|
promptTokens: 0,
|
|
4421
4776
|
outputTokens: 0,
|
|
4777
|
+
totalPromptTokens: 0,
|
|
4778
|
+
totalOutputTokens: 0,
|
|
4422
4779
|
searchRounds: [],
|
|
4423
4780
|
dimensionPlan: [],
|
|
4424
4781
|
rawSourcesByDim: /* @__PURE__ */ new Map(),
|
|
4425
|
-
rawSourcesTopup: []
|
|
4782
|
+
rawSourcesTopup: [],
|
|
4783
|
+
buildEvents: [],
|
|
4784
|
+
phaseStartedAt: /* @__PURE__ */ new Map()
|
|
4426
4785
|
};
|
|
4427
4786
|
inFlightJobs.set(jobId, state);
|
|
4428
4787
|
const wallClockTimer = setTimeout(() => {
|
|
@@ -4462,18 +4821,42 @@ async function runPipeline(state) {
|
|
|
4462
4821
|
const phaseEtas = [30, 280, 30, 45, 90, 30, 60];
|
|
4463
4822
|
const totalEta = phaseEtas.reduce((a, b) => a + b, 0);
|
|
4464
4823
|
let progressBaselinePct = 0;
|
|
4824
|
+
const recordEvent = (ev) => {
|
|
4825
|
+
state.buildEvents.push(ev);
|
|
4826
|
+
};
|
|
4827
|
+
const syncBuildLogSnapshot = () => {
|
|
4828
|
+
partial.buildLog = {
|
|
4829
|
+
narrative: partial.buildLog?.narrative ?? "",
|
|
4830
|
+
locale: state.locale,
|
|
4831
|
+
generatedAt: partial.buildLog?.generatedAt ?? 0,
|
|
4832
|
+
events: state.buildEvents.slice(),
|
|
4833
|
+
totalTokens: state.totalPromptTokens + state.totalOutputTokens
|
|
4834
|
+
};
|
|
4835
|
+
};
|
|
4465
4836
|
const startPhase = (phase) => {
|
|
4466
4837
|
const i = phase - 1;
|
|
4838
|
+
const now = Date.now();
|
|
4839
|
+
state.phaseStartedAt.set(phase, now);
|
|
4467
4840
|
personaBus.emit(state.id, {
|
|
4468
4841
|
type: "persona-phase-start",
|
|
4469
4842
|
phase,
|
|
4470
4843
|
label: phaseLabels[i],
|
|
4471
4844
|
etaSec: phaseEtas[i]
|
|
4472
4845
|
});
|
|
4846
|
+
recordEvent({ kind: "phase-start", ts: now, phase, label: phaseLabels[i] });
|
|
4473
4847
|
};
|
|
4474
4848
|
const finishPhase = (phase) => {
|
|
4475
4849
|
const i = phase - 1;
|
|
4476
4850
|
progressBaselinePct = Math.round(phaseEtas.slice(0, i + 1).reduce((a, b) => a + b, 0) / totalEta * 100);
|
|
4851
|
+
const startedAt = state.phaseStartedAt.get(phase) ?? Date.now();
|
|
4852
|
+
const finishedAt = Date.now();
|
|
4853
|
+
recordEvent({
|
|
4854
|
+
kind: "phase-end",
|
|
4855
|
+
ts: finishedAt,
|
|
4856
|
+
phase,
|
|
4857
|
+
durationMs: Math.max(0, finishedAt - startedAt)
|
|
4858
|
+
});
|
|
4859
|
+
syncBuildLogSnapshot();
|
|
4477
4860
|
personaBus.emit(state.id, {
|
|
4478
4861
|
type: "persona-phase-end",
|
|
4479
4862
|
phase,
|
|
@@ -4598,10 +4981,43 @@ async function runPipeline(state) {
|
|
|
4598
4981
|
partial.evalSet = scored;
|
|
4599
4982
|
const valid = scored.map((e) => e.divergenceScore).filter((s) => s !== null);
|
|
4600
4983
|
partial.differentiationScore = valid.length > 0 ? valid.reduce((a, b) => a + b, 0) / valid.length : null;
|
|
4984
|
+
state.buildEvents.push({
|
|
4985
|
+
kind: "divergence",
|
|
4986
|
+
ts: Date.now(),
|
|
4987
|
+
score: partial.differentiationScore
|
|
4988
|
+
});
|
|
4601
4989
|
}
|
|
4602
4990
|
partial.toolAccess = { webSearch: hasWebSearchKey() };
|
|
4603
4991
|
reportProgress(7, "naming the director", 0.85);
|
|
4604
4992
|
partial.guessName = await runNamePhase(state, partial.profileV2) || void 0;
|
|
4993
|
+
reportProgress(7, "summarising the build", 0.92);
|
|
4994
|
+
try {
|
|
4995
|
+
const narrative = await runPersonaNarrator(state, {
|
|
4996
|
+
description: state.description,
|
|
4997
|
+
locale: state.locale,
|
|
4998
|
+
events: state.buildEvents,
|
|
4999
|
+
profileV2: partial.profileV2 ?? null,
|
|
5000
|
+
differentiationScore: partial.differentiationScore ?? null,
|
|
5001
|
+
guessName: partial.guessName ?? null
|
|
5002
|
+
});
|
|
5003
|
+
partial.buildLog = {
|
|
5004
|
+
narrative: narrative || "",
|
|
5005
|
+
locale: state.locale,
|
|
5006
|
+
generatedAt: Date.now(),
|
|
5007
|
+
events: state.buildEvents.slice(),
|
|
5008
|
+
totalTokens: state.totalPromptTokens + state.totalOutputTokens
|
|
5009
|
+
};
|
|
5010
|
+
} catch (e) {
|
|
5011
|
+
process.stderr.write(`[persona-builder/narrator] failed: ${e instanceof Error ? e.message : String(e)}
|
|
5012
|
+
`);
|
|
5013
|
+
partial.buildLog = {
|
|
5014
|
+
narrative: "",
|
|
5015
|
+
locale: state.locale,
|
|
5016
|
+
generatedAt: Date.now(),
|
|
5017
|
+
events: state.buildEvents.slice(),
|
|
5018
|
+
totalTokens: state.totalPromptTokens + state.totalOutputTokens
|
|
5019
|
+
};
|
|
5020
|
+
}
|
|
4605
5021
|
finishPhase(7);
|
|
4606
5022
|
{
|
|
4607
5023
|
const status = checkAbortOrCap();
|
|
@@ -4685,6 +5101,11 @@ async function runReActLoop(state, profileV1, reportProgress) {
|
|
|
4685
5101
|
type: "persona-dimension-plan",
|
|
4686
5102
|
dimensions: plan.slice()
|
|
4687
5103
|
});
|
|
5104
|
+
state.buildEvents.push({
|
|
5105
|
+
kind: "dimension-plan",
|
|
5106
|
+
ts: Date.now(),
|
|
5107
|
+
dimensions: plan.map((d) => ({ dimension: d.dimension, query: d.query, why: d.why }))
|
|
5108
|
+
});
|
|
4688
5109
|
reportProgress(2, `${plan.length} angles picked \xB7 searching in parallel`, 0.05);
|
|
4689
5110
|
let doneCount = 0;
|
|
4690
5111
|
for (let chunkStart = 0; chunkStart < plan.length; chunkStart += DIMENSION_PARALLEL_CHUNK) {
|
|
@@ -4806,6 +5227,15 @@ ${ext.text}
|
|
|
4806
5227
|
pagesRead: pageExtracts.filter((e) => e.ok).length,
|
|
4807
5228
|
phase: "topup"
|
|
4808
5229
|
});
|
|
5230
|
+
state.buildEvents.push({
|
|
5231
|
+
kind: "search",
|
|
5232
|
+
ts: Date.now(),
|
|
5233
|
+
query,
|
|
5234
|
+
resultsCount: resultsArr.length,
|
|
5235
|
+
pagesRead: pageExtracts.filter((e) => e.ok).length,
|
|
5236
|
+
round: roundNum,
|
|
5237
|
+
topup: true
|
|
5238
|
+
});
|
|
4809
5239
|
}
|
|
4810
5240
|
const merged = concatRawSources(state);
|
|
4811
5241
|
if (!merged.trim()) {
|
|
@@ -4927,6 +5357,15 @@ ${ext.text}
|
|
|
4927
5357
|
dimension: entry.dimension,
|
|
4928
5358
|
phase: "dimension"
|
|
4929
5359
|
});
|
|
5360
|
+
state.buildEvents.push({
|
|
5361
|
+
kind: "search",
|
|
5362
|
+
ts: Date.now(),
|
|
5363
|
+
query: entry.query,
|
|
5364
|
+
resultsCount: resultsArr.length,
|
|
5365
|
+
pagesRead: pageCount,
|
|
5366
|
+
dimension: entry.dimension,
|
|
5367
|
+
round: roundNum
|
|
5368
|
+
});
|
|
4930
5369
|
}
|
|
4931
5370
|
function concatRawSources(state) {
|
|
4932
5371
|
const parts = [];
|
|
@@ -5154,7 +5593,8 @@ function partialToPersona(partial) {
|
|
|
5154
5593
|
evalSet: partial.evalSet || [],
|
|
5155
5594
|
differentiationScore: partial.differentiationScore ?? null,
|
|
5156
5595
|
toolAccess: partial.toolAccess || { webSearch: false },
|
|
5157
|
-
...partial.guessName ? { guessName: partial.guessName } : {}
|
|
5596
|
+
...partial.guessName ? { guessName: partial.guessName } : {},
|
|
5597
|
+
...partial.buildLog ? { buildLog: partial.buildLog } : {}
|
|
5158
5598
|
};
|
|
5159
5599
|
}
|
|
5160
5600
|
function getPartialPersona(jobId) {
|
|
@@ -5174,7 +5614,8 @@ function getPartialPersona(jobId) {
|
|
|
5174
5614
|
evalSet: v.evalSet || [],
|
|
5175
5615
|
differentiationScore: typeof v.differentiationScore === "number" ? v.differentiationScore : null,
|
|
5176
5616
|
toolAccess: v.toolAccess || { webSearch: false },
|
|
5177
|
-
...typeof v.guessName === "string" && v.guessName ? { guessName: v.guessName } : {}
|
|
5617
|
+
...typeof v.guessName === "string" && v.guessName ? { guessName: v.guessName } : {},
|
|
5618
|
+
...v.buildLog ? { buildLog: v.buildLog } : {}
|
|
5178
5619
|
};
|
|
5179
5620
|
}
|
|
5180
5621
|
|
|
@@ -6918,7 +7359,9 @@ function agentsRouter() {
|
|
|
6918
7359
|
if (description.length > 1200) {
|
|
6919
7360
|
return c.json({ error: "description too long (max 1200 chars)" }, 400);
|
|
6920
7361
|
}
|
|
6921
|
-
const
|
|
7362
|
+
const localeRaw = typeof b.locale === "string" ? b.locale : "";
|
|
7363
|
+
const locale = localeRaw === "zh" || localeRaw === "ja" || localeRaw === "es" ? localeRaw : "en";
|
|
7364
|
+
const jobId = startPersonaBuild({ description, locale });
|
|
6922
7365
|
return c.json({ jobId });
|
|
6923
7366
|
});
|
|
6924
7367
|
r.get("/generate-persona/:jobId/stream", (c) => {
|
|
@@ -8166,15 +8609,15 @@ var SCAFFOLD_SYSTEM = [
|
|
|
8166
8609
|
"",
|
|
8167
8610
|
"7b. **Director Perspectives** (`directorPerspectives`) \xB7 MANDATORY in every brief with \u2265 2 active directors. The \"social map\" of the room \u2014 every active director gets a row, alignment / divergence groups are explicit, the chair's structural observation closes the block. Distinct from convergence (narrow), divergence (single central tension), positions (2-3 camps with quotes). views-compared is the bird's-eye view that lets a stakeholder skim WHO was in the room and where each one stood. Object shape: `{ intro, alignment: [...], divergence: [...], perspectives: [...], chairSynthesis }`. Each entry of `perspectives`: `directorId` (must match the room's member list), `stance` (\u2264 60 chars short label of the director's angle), `position` (1-2 sentences \u2264 300 chars \xB7 the director's load-bearing argument in their voice), `quote` (optional verbatim \u2264 40 words; empty when no memorable line), `lens` (data | dissent | narrative | structural | first-principle). EVERY active director gets one entry \u2014 no exceptions. Each `alignment` group: `pointOfAgreement` (\u2264 80 chars), `directorIds` (\u2265 2), `note` (\u2264 220 chars \xB7 why the convergence is structurally interesting \xB7 prefer to surface independent paths to same conclusion). Each `divergence` entry: `hinge` (\u2264 140 chars \xB7 what separates them), `sides` (2-3 named sides; each: label / directorIds / 1-sentence stance), `resolution` (\u2264 220 chars \xB7 what would settle it; empty when unresolved). `chairSynthesis` (2-4 sentences, \u2264 400 chars) is the chair's meta-observation comparing the views \u2014 moderator-neutral voice, observation not advocacy. Set the WHOLE block to `null` ONLY when there's exactly 1 active director (no comparison possible).",
|
|
8168
8611
|
"",
|
|
8169
|
-
"8. **Visuals** \xB7 0\u20134 blocks. Content-driven. **Strongly prefer
|
|
8612
|
+
"8. **Visuals** \xB7 0\u20134 blocks. Content-driven. **Strongly prefer chart sub-types over text matrices** \u2014 charts render as a real diagram readers can scan in seconds; text matrices are dense tables that take paragraphs to absorb. Only fall back to text matrix when the data shape genuinely doesn't fit any chart form.",
|
|
8170
8613
|
"",
|
|
8171
|
-
"
|
|
8172
|
-
" \xB7 `quadrant-chart` \u2014 items plotted on two real axes
|
|
8173
|
-
" \xB7 `bar-chart` \u2014 2\u20138 named items ranked by ONE quantitative dimension
|
|
8174
|
-
" \xB7 `timeline` \u2014 3\u20138 dated points telling a narrative arc
|
|
8175
|
-
" \xB7 `pie-chart` \u2014 2\u20136 slices showing a distribution
|
|
8614
|
+
" Chart sub-types (strongly preferred \xB7 all rendered as inline SVG through the `kami-chart` pipeline):",
|
|
8615
|
+
" \xB7 `quadrant-chart` \u2014 items plotted on two real axes. Effort/impact, support/strength, urgency/uncertainty. Rendered as inline SVG via the `kami-chart` pipeline.",
|
|
8616
|
+
" \xB7 `bar-chart` \u2014 2\u20138 named items ranked by ONE quantitative dimension \xB7 cost / support / size / time. Rendered as inline SVG via the `kami-chart` pipeline (spine-tokenised, no JS runtime).",
|
|
8617
|
+
" \xB7 `timeline` \u2014 3\u20138 dated points telling a narrative arc \xB7 retro / historical analogue / projected sequence. Rendered as inline SVG via the `kami-chart` pipeline.",
|
|
8618
|
+
" \xB7 `pie-chart` \u2014 2\u20136 slices showing a distribution \xB7 scenario probabilities / lens shares / vote tallies / market mix. Rendered as a DONUT (hollow centre carries the largest slice's value) via the `kami-chart` pipeline. Numbers can be percentages OR raw counts \u2014 the renderer normalises.",
|
|
8176
8619
|
"",
|
|
8177
|
-
" Text matrices (fall back when no
|
|
8620
|
+
" Text matrices (fall back when no chart form fits):",
|
|
8178
8621
|
" \xB7 `comparison-table` \u2014 \u2265 2 named options compared on shared dimensions",
|
|
8179
8622
|
" \xB7 `force-field` \u2014 drivers vs resistors of one outcome",
|
|
8180
8623
|
" \xB7 `strengths-cautions`\u2014 strengths / cautions / verdict per option",
|
|
@@ -8187,7 +8630,7 @@ var SCAFFOLD_SYSTEM = [
|
|
|
8187
8630
|
" \xB7 ONLY when the data is N-options \xD7 M-criteria with mixed cell types (text + numbers + tags) \u2192 `comparison-table`.",
|
|
8188
8631
|
" \xB7 ONLY when the room argued exactly one outcome with for/against forces \u2192 `force-field`.",
|
|
8189
8632
|
" \xB7 ONLY when N options each need a strengths/cautions/verdict triplet AND no numeric ranking \u2192 `strengths-cautions`.",
|
|
8190
|
-
" These six routes cover most rooms. Default toward picking 2\u20133 visuals (one quantitative + one categorical) \u2014 the writer is forbidden from emitting only text matrices. Beyond these, the Stage 3 writer also emits inline
|
|
8633
|
+
" These six routes cover most rooms. Default toward picking 2\u20133 visuals (one quantitative + one categorical) \u2014 the writer is forbidden from emitting only text matrices. Beyond these, the Stage 3 writer also emits inline charts (flowchart / mindmap / gantt / sequenceDiagram / stateDiagram / journey \u2014 all rendered through the `kami-chart` pipeline) where prose can't carry the structure efficiently \u2014 those don't go in the typed `visuals` list, so don't pre-allocate them here.",
|
|
8191
8634
|
"",
|
|
8192
8635
|
'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`, `expectedBenefit` (the upside if you act \u2014 stated as a concrete payoff, NOT a metric to watch). Recommendations are imperatives \u2014 "Do X" not "X should happen". The `expectedBenefit` is what gets a stakeholder to actually approve the action \u2014 it answers "if I do this, what do I get?" in one short sentence.',
|
|
8193
8636
|
"",
|
|
@@ -9174,26 +9617,33 @@ var WRITE_SYSTEM = [
|
|
|
9174
9617
|
"",
|
|
9175
9618
|
" For `quadrant-chart`:",
|
|
9176
9619
|
" ### {title}",
|
|
9177
|
-
|
|
9620
|
+
' Render a fenced ```kami-chart block \xB7 strict JSON. Two-axis plot where the top-right (Q1) quadrant is the brand\'s "preferred" zone, tinted in the spine accent. Schema:',
|
|
9178
9621
|
" ```",
|
|
9179
|
-
"
|
|
9180
|
-
"
|
|
9181
|
-
'
|
|
9182
|
-
'
|
|
9183
|
-
'
|
|
9184
|
-
'
|
|
9185
|
-
'
|
|
9186
|
-
'
|
|
9187
|
-
'
|
|
9622
|
+
" {",
|
|
9623
|
+
' "type": "quadrant-chart",',
|
|
9624
|
+
' "title": "{scaffold.title}",',
|
|
9625
|
+
' "xLabel": "{scaffold.xLabel \xB7 low\u2192high}",',
|
|
9626
|
+
' "yLabel": "{scaffold.yLabel \xB7 low\u2192high}",',
|
|
9627
|
+
' "q1": "{top-right \xB7 preferred}",',
|
|
9628
|
+
' "q2": "{top-left}",',
|
|
9629
|
+
' "q3": "{bottom-left}",',
|
|
9630
|
+
' "q4": "{bottom-right}",',
|
|
9631
|
+
' "focal": "{label of the load-bearing item \xB7 optional}",',
|
|
9632
|
+
' "items": [',
|
|
9633
|
+
' { "label": "Item A", "x": 0.72, "y": 0.84 },',
|
|
9634
|
+
' { "label": "Item B", "x": 0.35, "y": 0.66, "tier": 4 }',
|
|
9635
|
+
" ],",
|
|
9636
|
+
' "caption": "{One-sentence editorial gloss with *italic* on the operative phrase.}"',
|
|
9637
|
+
" }",
|
|
9188
9638
|
" ```",
|
|
9189
|
-
" Hard rules
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
" \xB7
|
|
9194
|
-
" \xB7
|
|
9195
|
-
" \xB7
|
|
9196
|
-
" \xB7
|
|
9639
|
+
" Hard rules:",
|
|
9640
|
+
" \xB7 Strict JSON. No trailing commas, no comments.",
|
|
9641
|
+
" \xB7 `x` and `y` are decimals strictly inside `(0, 1)` \u2014 2 decimals max. Never use 0 or 1 exactly (points clip on the frame).",
|
|
9642
|
+
" \xB7 `q1`\u2013`q4` are short quadrant labels (\u2264 24 chars). `q1` is the preferred / focal quadrant by convention \u2014 pick names accordingly.",
|
|
9643
|
+
" \xB7 `focal` is OPTIONAL. When set, that item renders solid accent + larger radius \u2014 concentrates attention on the load-bearing position. Only one focal per chart.",
|
|
9644
|
+
" \xB7 `tier` (optional, 1\u20134) downranks an item's visual weight: 1 = standard, 2 = near-focal (accent outline), 3 = standard, 4 = faint (muted outline). Use sparingly to push uninteresting positions to the background.",
|
|
9645
|
+
" \xB7 2\u201316 items. Item labels \u2264 24 chars; avoid colons / brackets.",
|
|
9646
|
+
" \xB7 NO blank lines inside the fenced block.",
|
|
9197
9647
|
"",
|
|
9198
9648
|
" For `force-field`:",
|
|
9199
9649
|
" ### {title}",
|
|
@@ -9205,62 +9655,88 @@ var WRITE_SYSTEM = [
|
|
|
9205
9655
|
"",
|
|
9206
9656
|
" For `bar-chart`:",
|
|
9207
9657
|
" ### {title}",
|
|
9208
|
-
" Render a fenced ```
|
|
9658
|
+
" Render a fenced ```kami-chart block \xB7 strict JSON. The renderer turns it into an inline SVG bar chart styled by the active spine palette:",
|
|
9209
9659
|
" ```",
|
|
9210
|
-
"
|
|
9211
|
-
'
|
|
9212
|
-
'
|
|
9213
|
-
'
|
|
9214
|
-
"
|
|
9660
|
+
" {",
|
|
9661
|
+
' "type": "bar-chart",',
|
|
9662
|
+
' "title": "{scaffold.title}",',
|
|
9663
|
+
' "yLabel": "{scaffold.yLabel}",',
|
|
9664
|
+
' "unit": "{scaffold.unit}",',
|
|
9665
|
+
' "focal": "{label of the one bar that carries the argument \xB7 optional}",',
|
|
9666
|
+
' "bars": [',
|
|
9667
|
+
' { "label": "{bar.label}", "value": {bar.value} },',
|
|
9668
|
+
' { "label": "{bar.label}", "value": {bar.value} }',
|
|
9669
|
+
" ],",
|
|
9670
|
+
' "caption": "{One-sentence editorial gloss \xB7 what the chart says, not what it plots. Use *italic markers* (e.g. *2\xD7 faster*) for the operative phrase.}"',
|
|
9671
|
+
" }",
|
|
9215
9672
|
" ```",
|
|
9216
9673
|
" Hard rules:",
|
|
9217
|
-
" \xB7
|
|
9218
|
-
" \xB7 Inside any quoted label: NO double-quote, NO `:`, NO `[`, NO `]`. Replace with ` - ` if needed.",
|
|
9219
|
-
" \xB7 `bar` values are bare numbers, in the same order as x-axis labels. Match counts (lexer fails on mismatch).",
|
|
9220
|
-
" \xB7 `title` is double-quoted. ASCII parens only \u2014 replace fullwidth `\uFF08\uFF09` with halfwidth `()`.",
|
|
9674
|
+
" \xB7 Strict JSON (double-quoted keys + string values, no trailing commas, no comments). The renderer parses and falls back to an error placeholder if JSON is malformed.",
|
|
9221
9675
|
" \xB7 2\u20138 bars. Below 2 isn't a comparison; above 8 stops being scannable.",
|
|
9222
|
-
" \xB7
|
|
9676
|
+
" \xB7 `value` is a bare number (no quotes, no unit suffix). Y-axis auto-scales to the next nice round number.",
|
|
9677
|
+
' \xB7 `unit` (e.g. "w", "%", "$M") is appended to the data label above each bar \u2014 keep it short (\u2264 4 chars).',
|
|
9678
|
+
" \xB7 `focal` is optional. When set, that bar renders in the spine accent and the others in neutral ink-mid \u2014 concentrates attention on the bar that carries the argument. When omitted, every bar gets the accent.",
|
|
9679
|
+
" \xB7 `caption` is rendered in serif italic below the chart. Wrap the operative phrase in `*\u2026*` so the renderer can italicise it in the spine accent.",
|
|
9680
|
+
" \xB7 Labels \u2264 24 chars; the renderer truncates to 18 at draw time. Avoid colons / brackets in labels (cosmetic, not strict).",
|
|
9681
|
+
" \xB7 NO blank lines inside the fenced block.",
|
|
9223
9682
|
"",
|
|
9224
9683
|
" For `timeline`:",
|
|
9225
9684
|
" ### {title}",
|
|
9226
|
-
" Render a fenced ```
|
|
9685
|
+
" Render a fenced ```kami-chart block \xB7 strict JSON. Horizontal time-axis chart where events alternate above / below the axis; the `focal` event renders in the spine accent. Schema:",
|
|
9227
9686
|
" ```",
|
|
9228
|
-
"
|
|
9229
|
-
"
|
|
9230
|
-
"
|
|
9687
|
+
" {",
|
|
9688
|
+
' "type": "timeline",',
|
|
9689
|
+
' "title": "{scaffold.title}",',
|
|
9690
|
+
' "focal": "{period string of the load-bearing milestone \xB7 optional}",',
|
|
9691
|
+
' "points": [',
|
|
9692
|
+
' { "period": "2019", "label": "First open weights ship", "description": "" },',
|
|
9693
|
+
' { "period": "2022", "label": "Parity claim", "description": "Optional one-clause expansion" }',
|
|
9694
|
+
" ],",
|
|
9695
|
+
' "caption": "{Editorial gloss \xB7 what the arc says, with *italic* on the turn.}"',
|
|
9696
|
+
" }",
|
|
9231
9697
|
" ```",
|
|
9232
9698
|
" Hard rules:",
|
|
9233
|
-
" \xB7
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
" \xB7
|
|
9237
|
-
" \xB7
|
|
9238
|
-
" \xB7 NO blank lines inside the fenced block.
|
|
9699
|
+
" \xB7 Strict JSON. 3\u20138 points. Below 3 is a stub; above 8 overflows the strip.",
|
|
9700
|
+
' \xB7 `period` is the column header (mono): "2019", "Q3 2024", "Today", "+12 mo". \u2264 16 chars.',
|
|
9701
|
+
" \xB7 `label` is the headline event (sans): \u2264 60 chars. Concrete; avoid corporate verbs.",
|
|
9702
|
+
" \xB7 `description` is optional (\u2264 40 chars after rendering \u2014 gets truncated). Empty string is fine.",
|
|
9703
|
+
" \xB7 `focal` matches the `period` string of the load-bearing event. The matched event renders in accent: filled dot, solid connector, accent-bordered card. Only one focal.",
|
|
9704
|
+
" \xB7 NO blank lines inside the fenced block.",
|
|
9239
9705
|
"",
|
|
9240
9706
|
" For `pie-chart`:",
|
|
9241
9707
|
" ### {title}",
|
|
9242
|
-
" Render a fenced ```
|
|
9708
|
+
" Render a fenced ```kami-chart block \xB7 strict JSON. Renders as a DONUT (hollow centre with the largest slice's value + label inside). The largest slice carries the spine accent; smaller slices cascade down the neutral ramp. Schema:",
|
|
9243
9709
|
" ```",
|
|
9244
|
-
"
|
|
9245
|
-
"
|
|
9246
|
-
'
|
|
9247
|
-
|
|
9710
|
+
" {",
|
|
9711
|
+
' "type": "donut",',
|
|
9712
|
+
' "title": "{scaffold.title}",',
|
|
9713
|
+
` "centerLabel": "{optional \xB7 short text under the centre value \xB7 falls back to the largest slice's label}",`,
|
|
9714
|
+
' "slices": [',
|
|
9715
|
+
' { "label": "Base case", "value": 55 },',
|
|
9716
|
+
' { "label": "Upside", "value": 25 },',
|
|
9717
|
+
' { "label": "Downside", "value": 20 }',
|
|
9718
|
+
" ],",
|
|
9719
|
+
' "caption": "{Editorial gloss with *italic* on the operative phrase.}"',
|
|
9720
|
+
" }",
|
|
9248
9721
|
" ```",
|
|
9249
9722
|
" Hard rules:",
|
|
9250
|
-
" \xB7 `
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
" \xB7
|
|
9254
|
-
" \xB7
|
|
9723
|
+
" \xB7 `type` MUST be `donut` (the old `pie-chart` is renamed at render time).",
|
|
9724
|
+
" \xB7 Strict JSON. 2\u20136 slices. Above 6 the chart stops being readable.",
|
|
9725
|
+
" \xB7 `value` is a bare number \u2014 percentages summing to ~100 OR raw counts. The renderer normalises and prints percentages in the legend.",
|
|
9726
|
+
" \xB7 The renderer SORTS slices largest-first, so the visual order may differ from the JSON order. Don't try to control colours by ordering \u2014 the accent always lands on the largest slice.",
|
|
9727
|
+
" \xB7 `centerLabel` is OPTIONAL \u2014 defaults to the largest slice's label (uppercased). Set it when the slice label is long or wants a different framing inside the donut.",
|
|
9728
|
+
" \xB7 Slice labels \u2264 24 chars. NO blank lines inside the fenced block.",
|
|
9255
9729
|
" \xB7 NO blank lines inside the fenced block. Indent body lines 4 spaces.",
|
|
9256
9730
|
"",
|
|
9257
|
-
"## Inline
|
|
9731
|
+
"## Inline charts \xB7 additional chart types beyond the typed visuals",
|
|
9732
|
+
"",
|
|
9733
|
+
"Beyond the typed visuals (`comparison-table` / `quadrant-chart` / `force-field` / `strengths-cautions` / `bar-chart` / `timeline` / `pie-chart`), the writer drops fenced chart blocks inline within body sections to surface logic, sequence, hierarchy, or state that prose can't carry as efficiently. Each must visualise something prose can't.",
|
|
9258
9734
|
"",
|
|
9259
|
-
"
|
|
9735
|
+
"All inline charts render through the `kami-chart` pipeline \xB7 strict JSON inside a ```kami-chart fence, spine-tokenised inline SVG, no JS runtime. The catalogue below covers every inline chart type \u2014 `flowchart` / `mindmap`\u2192`tree` / `gantt` / `sequenceDiagram`\u2192`swimlane` / `journey`\u2192`swimlane` / `stateDiagram-v2`\u2192`state-machine`.",
|
|
9260
9736
|
"",
|
|
9261
|
-
"**
|
|
9262
|
-
" \xB7 Typed visuals where the sub-type is
|
|
9263
|
-
" \xB7 Inline
|
|
9737
|
+
"**Chart bias \xB7 prefer charts where they fit naturally \u2014 no fixed quota.** Two sources count:",
|
|
9738
|
+
" \xB7 Typed visuals where the sub-type is a chart (`quadrant-chart` / `bar-chart` / `timeline` / `pie-chart`).",
|
|
9739
|
+
" \xB7 Inline chart blocks from the catalogue below (`flowchart` / `mindmap` / `gantt` / `sequenceDiagram` / `journey` / `stateDiagram-v2`).",
|
|
9264
9740
|
"Quality over quantity. Emit a chart whenever the section's content has structure prose can't carry efficiently. Skip when the prose alone is clear \u2014 a forced chart is worse than no chart. There is NO minimum count; a substantive strategy brief might naturally land at 4\u20136 charts, a tight philosophical brief at 0\u20131, both are correct.",
|
|
9265
9741
|
"",
|
|
9266
9742
|
"**The default lean is YES** when a section's content matches one of these shapes: branching logic, sequenced events, multi-party interactions, hierarchical structure, state transitions, 2-axis comparisons, distributions, before/after framings. The cost of an extra chart is small; the cost of a wall of text is a reader who skips. But don't manufacture structure that isn't in the material \u2014 chart only what the room actually produced.",
|
|
@@ -9283,12 +9759,12 @@ var WRITE_SYSTEM = [
|
|
|
9283
9759
|
" \xB7 `Options Analysis` / `Decision Options` \u2192 \u25C7 ONLY when there are \u2265 4 options OR each option branches into 2+ sub-considerations (5+ nodes total with branching). With 2-3 options, the typed table / decision-options block carries it \u2014 no chart.",
|
|
9284
9760
|
" \xB7 `Two Paths` \u2192 \u25C7 rarely; two parallel trajectories joined at a hinge is exactly the trivial case. Render the typed table only.",
|
|
9285
9761
|
" \xB7 `Strategic Outlook` \u2192 \u25C7 `mindmap` ONLY when the room named \u2265 4 distinct forces with internal sub-structure (4 branches \xD7 \u2265 2 children).",
|
|
9286
|
-
" \xB7 `Critical Assumptions` \u2192 \u25C7 `flowchart
|
|
9287
|
-
" \xB7 `Scenario Tree` \u2192 \u2713 `flowchart
|
|
9288
|
-
" \xB7 `Threats to Validity` \u2192 \u25C7 `flowchart
|
|
9762
|
+
" \xB7 `Critical Assumptions` \u2192 \u25C7 `flowchart` ONLY when assumptions form a multi-step dependency chain WITH branching (assumption A holds \u2192 either B or C \u2192 recommendation D), 5+ nodes. Linear 3-step dependency = prose.",
|
|
9763
|
+
" \xB7 `Scenario Tree` \u2192 \u2713 `flowchart` when there are \u2265 3 scenarios EACH with named effects/triggers as sub-nodes (root + 3 scenarios + 3-6 effect children = 7-10 nodes). Below that, the typed table is enough.",
|
|
9764
|
+
" \xB7 `Threats to Validity` \u2192 \u25C7 `flowchart` ONLY when threats compound (sample bias \u2192 selection bias \u2192 generalizability ceiling) with branching, 5+ nodes.",
|
|
9289
9765
|
" \xB7 `Recommendations` \u2192 \u2713 `gantt` for multi-phase rollouts (\u2265 2 sections AND \u2265 4 tasks). For sequenced action chains under 4 tasks, use prose / numbered list \u2014 NOT a linear flowchart.",
|
|
9290
9766
|
" \xB7 `Leading Indicators` \u2192 \u25C7 `stateDiagram-v2` when indicators map to \u2265 4 scenario states with at least one feedback loop / back-transition.",
|
|
9291
|
-
" \xB7 `Risk Register` \u2192 \u2713 `flowchart
|
|
9767
|
+
" \xB7 `Risk Register` \u2192 \u2713 `flowchart` when there are \u2265 5 risks AND multiple risks share a category cluster (root \u2192 category nodes \u2192 individual risks as leaves = \u2265 10 nodes). The typed risk table is enough on its own when the register is < 5 entries.",
|
|
9292
9768
|
" \xB7 `Risk Register` \u2192 \u2713 `quadrantChart` of severity \xD7 likelihood (always \u2014 the quadrant chart is a 2-axis plot, not subject to the flowchart-complexity floor).",
|
|
9293
9769
|
" \xB7 `New Questions This Surfaced` \u2192 \u25C7 `mindmap` when there are \u2265 4 new questions clustering into \u2265 3 themes.",
|
|
9294
9770
|
" \xB7 `Strategic Planning Assumption` \u2192 \u25C7 rarely.",
|
|
@@ -9299,130 +9775,178 @@ var WRITE_SYSTEM = [
|
|
|
9299
9775
|
"**Routing constraints** (avoid double-rendering the same content):",
|
|
9300
9776
|
" \xB7 Risk Register flowchart + Risk Register table = \u2713 both, complementary.",
|
|
9301
9777
|
" \xB7 Risk Register quadrantChart + Risk Register table = \u2713 both, complementary.",
|
|
9302
|
-
" \xB7 A single section gets at MOST one inline
|
|
9778
|
+
" \xB7 A single section gets at MOST one inline chart (plus the typed visual if any). Never stack 2+ inline charts in one section.",
|
|
9303
9779
|
" \xB7 A `gantt` and a `flowchart` covering the SAME recommendation rollout = pick one (gantt if dates matter, flowchart if branching matters).",
|
|
9304
9780
|
" \xB7 If a typed `visuals` block already covers a content shape (e.g. `bar-chart` for ranked options), don't add an inline `flowchart` for the same options.",
|
|
9305
9781
|
"",
|
|
9306
9782
|
" ### flowchart \xB7 decision tree / process branches",
|
|
9307
|
-
' Use when a section argues a decision sequence ("if X then Y else Z") or a process where order
|
|
9783
|
+
' Use when a section argues a decision sequence ("if X then Y else Z") or a short process where order matters. Render as a fenced ```kami-chart JSON with one of two LAYOUTS \u2014 the renderer owns positioning. Schemas:',
|
|
9784
|
+
" Linear chain \xB7 2\u20135 nodes top-to-bottom:",
|
|
9308
9785
|
" ```",
|
|
9309
|
-
"
|
|
9310
|
-
"
|
|
9311
|
-
"
|
|
9312
|
-
"
|
|
9313
|
-
"
|
|
9314
|
-
|
|
9786
|
+
" {",
|
|
9787
|
+
' "type": "flowchart",',
|
|
9788
|
+
' "layout": "linear-v",',
|
|
9789
|
+
' "focal": "{optional \xB7 label of the load-bearing node}",',
|
|
9790
|
+
' "nodes": [',
|
|
9791
|
+
' { "label": "Start", "kind": "start" },',
|
|
9792
|
+
' { "label": "Validate input", "kind": "step", "hint": "" },',
|
|
9793
|
+
' { "label": "Persist write", "kind": "step", "hint": "" },',
|
|
9794
|
+
' { "label": "Done", "kind": "end" }',
|
|
9795
|
+
" ],",
|
|
9796
|
+
' "caption": "{One-sentence editorial gloss with *italic* on the operative phrase.}"',
|
|
9797
|
+
" }",
|
|
9798
|
+
" ```",
|
|
9799
|
+
" Y-decision \xB7 root (decision) \u2192 2 branches \u2192 optional join:",
|
|
9800
|
+
" ```",
|
|
9801
|
+
" {",
|
|
9802
|
+
' "type": "flowchart",',
|
|
9803
|
+
' "layout": "y-decision",',
|
|
9804
|
+
' "focal": "{optional \xB7 label of the load-bearing node \u2014 usually the decision or the preferred branch}",',
|
|
9805
|
+
' "nodes": [',
|
|
9806
|
+
' { "label": "Pilot beats threshold?", "kind": "decision" },',
|
|
9807
|
+
' { "label": "Scale rollout", "kind": "outcome", "hint": "yes" },',
|
|
9808
|
+
' { "label": "Iterate pilot", "kind": "outcome", "hint": "no" },',
|
|
9809
|
+
' { "label": "Decision recorded", "kind": "end" }',
|
|
9810
|
+
" ],",
|
|
9811
|
+
' "caption": "{Editorial gloss.}"',
|
|
9812
|
+
" }",
|
|
9315
9813
|
" ```",
|
|
9316
9814
|
" Hard rules:",
|
|
9317
|
-
' \xB7
|
|
9318
|
-
|
|
9319
|
-
" \xB7
|
|
9320
|
-
" \xB7
|
|
9321
|
-
|
|
9322
|
-
" \xB7
|
|
9323
|
-
" \xB7
|
|
9324
|
-
"",
|
|
9325
|
-
"
|
|
9326
|
-
"
|
|
9815
|
+
' \xB7 `layout` MUST be `linear-v` or `y-decision`. Pick `linear-v` for a sequence ("A leads to B leads to C"). Pick `y-decision` whenever a decision branches into 2 outcomes.',
|
|
9816
|
+
' \xB7 `kind` values: `start` (pill, paper-soft) \xB7 `step` (square, ink border) \xB7 `decision` (diamond, accent border) \xB7 `outcome` (square, faint border \u2014 "deferred branch" reads as receded) \xB7 `end` (pill, paper-soft).',
|
|
9817
|
+
" \xB7 `hint` is rendered as a short uppercase mono caption inside the node (for steps / outcomes) OR as the edge label (for y-decision branches \u2014 write `yes` / `no` / `<6 chars`).",
|
|
9818
|
+
" \xB7 For `y-decision`: nodes in canonical order \u2014 [0] root, [1] left branch, [2] right branch, [3] optional join.",
|
|
9819
|
+
" \xB7 For `linear-v`: max 5 nodes. For `y-decision`: 3\u20134 nodes.",
|
|
9820
|
+
" \xB7 Labels \u2264 18 chars (renderer truncates). Avoid colons / brackets in labels.",
|
|
9821
|
+
" \xB7 `focal` matches a node's `label` exactly; that node renders in the spine accent.",
|
|
9822
|
+
" \xB7 **Complexity floor stays**: only emit a flowchart when prose can't carry the structure \u2014 a 2-node linear-v is exactly the trivial case banned above.",
|
|
9823
|
+
"",
|
|
9824
|
+
" ### mindmap \xB7 hierarchical idea tree (renders as `tree`)",
|
|
9825
|
+
' Use when a section needs to surface the *shape* of the room\'s thinking \u2014 typically in brainstorm-mode rooms (`adjacent-angles`, `worth-chasing`). Render as a fenced ```kami-chart with `type: "tree"`. The renderer draws 1 root + N branches (with right-angle connectors and chevron arrows at child tops), and optional leaves under each branch:',
|
|
9327
9826
|
" ```",
|
|
9328
|
-
"
|
|
9329
|
-
"
|
|
9330
|
-
"
|
|
9331
|
-
"
|
|
9332
|
-
"
|
|
9333
|
-
"
|
|
9334
|
-
"
|
|
9827
|
+
" {",
|
|
9828
|
+
' "type": "tree",',
|
|
9829
|
+
' "root": "Central premise",',
|
|
9830
|
+
' "focal": "{optional \xB7 branch label that carries the argument}",',
|
|
9831
|
+
' "branches": [',
|
|
9832
|
+
' { "label": "Branch A", "leaves": ["Sub A1", "Sub A2"] },',
|
|
9833
|
+
' { "label": "Branch B", "leaves": ["Sub B1", "Sub B2", "Sub B3"] },',
|
|
9834
|
+
' { "label": "Branch C", "leaves": [] }',
|
|
9835
|
+
" ],",
|
|
9836
|
+
' "caption": "{Editorial gloss with *italic* on the operative phrase.}"',
|
|
9837
|
+
" }",
|
|
9335
9838
|
" ```",
|
|
9336
9839
|
" Hard rules:",
|
|
9337
|
-
" \xB7
|
|
9338
|
-
" \xB7
|
|
9339
|
-
" \xB7
|
|
9340
|
-
|
|
9341
|
-
" \xB7
|
|
9342
|
-
" \xB7 NO blank lines inside the fenced block.",
|
|
9840
|
+
" \xB7 `type` MUST be `tree` (the old `mindmap` is renamed at render time).",
|
|
9841
|
+
" \xB7 2\u20136 branches. Each branch can have 0\u20134 leaves (no leaves is fine \u2014 the renderer just shows the branch tier).",
|
|
9842
|
+
" \xB7 `root` \u2264 22 chars, branch labels \u2264 18 chars, leaf labels \u2264 16 chars. The renderer truncates.",
|
|
9843
|
+
" \xB7 `focal` matches a branch label; that branch + its leaves render in the spine accent.",
|
|
9844
|
+
" \xB7 **Complexity floor stays**: 2-branch tree with no leaves = naive. The renderer enforces a minimum of 2 branches but the floor in the trigger map applies: \u2265 4 top-level branches OR \u2265 3 branches \xD7 \u2265 2 children each.",
|
|
9343
9845
|
"",
|
|
9344
9846
|
" ### gantt \xB7 execution timeline with dependencies",
|
|
9345
|
-
|
|
9847
|
+
' Use when a section names a multi-phase rollout with time-bound activities \u2014 typically inside Recommendations. Render as a fenced ```kami-chart with `type: "gantt"`. The renderer draws bars on a shared horizontal time axis, grouped by section:',
|
|
9346
9848
|
" ```",
|
|
9347
|
-
"
|
|
9348
|
-
"
|
|
9349
|
-
"
|
|
9350
|
-
"
|
|
9351
|
-
"
|
|
9352
|
-
"
|
|
9353
|
-
"
|
|
9354
|
-
|
|
9355
|
-
|
|
9849
|
+
" {",
|
|
9850
|
+
' "type": "gantt",',
|
|
9851
|
+
' "title": "Rollout phasing \xB7 12 months",',
|
|
9852
|
+
' "unit": "mo",',
|
|
9853
|
+
' "focal": "{optional \xB7 phase label that carries the most cost / time}",',
|
|
9854
|
+
' "phases": [',
|
|
9855
|
+
' { "section": "Foundations", "label": "Discovery", "start": 0, "end": 2 },',
|
|
9856
|
+
' { "section": "Foundations", "label": "Pilot scope", "start": 2, "end": 3 },',
|
|
9857
|
+
' { "section": "Build", "label": "Vertical 1", "start": 3, "end": 6 },',
|
|
9858
|
+
' { "section": "Build", "label": "Vertical 2", "start": 6, "end": 9 },',
|
|
9859
|
+
' { "section": "Launch", "label": "GA", "start": 9, "end": 12 }',
|
|
9860
|
+
" ],",
|
|
9861
|
+
' "caption": "{Editorial gloss \xB7 what the schedule says, with *italic* on the operative phrase.}"',
|
|
9862
|
+
" }",
|
|
9356
9863
|
" ```",
|
|
9357
9864
|
" Hard rules:",
|
|
9358
|
-
" \xB7
|
|
9359
|
-
" \xB7 `
|
|
9360
|
-
" \xB7 `
|
|
9361
|
-
" \xB7
|
|
9362
|
-
" \xB7
|
|
9363
|
-
|
|
9364
|
-
"
|
|
9365
|
-
"",
|
|
9366
|
-
|
|
9367
|
-
" Use when a section describes a multi-party negotiation, a system-call sequence, or a step-by-step protocol where the *order of who-talks-to-whom* matters. Best fit: technical workflow rooms, governance / approval-chain briefs.",
|
|
9865
|
+
" \xB7 `start` and `end` are bare numbers in the same unit (months / weeks / days \u2014 your choice). The renderer auto-scales the time axis from 0 to a nice-round max.",
|
|
9866
|
+
" \xB7 `unit` (e.g. `mo`, `w`, `d`) is a short suffix on the axis ticks and bar durations.",
|
|
9867
|
+
" \xB7 `section` is optional but recommended \u2014 phases with the same section are grouped under a section label on the left.",
|
|
9868
|
+
" \xB7 `focal` matches a phase label exactly; that bar renders in the spine accent.",
|
|
9869
|
+
" \xB7 2\u201310 phases. **Complexity floor stays**: a 2-phase gantt is a bullet list with bars; only emit when \u2265 4 tasks total AND \u2265 2 sections.",
|
|
9870
|
+
" \xB7 Phase labels \u2264 24 chars (renderer truncates). Avoid colons / brackets.",
|
|
9871
|
+
"",
|
|
9872
|
+
" ### sequenceDiagram \xB7 actor interactions over time (renders as `swimlane`)",
|
|
9873
|
+
' Use when a section describes a multi-party negotiation, a system-call sequence, or a step-by-step protocol where the *order of who-talks-to-whom* matters. Render as a fenced ```kami-chart with `type: "swimlane"`. The renderer draws N lanes stacked vertically; steps go in their assigned lane in left-to-right order; elbow connectors with chevron arrowheads link consecutive steps. Schema:',
|
|
9368
9874
|
" ```",
|
|
9369
|
-
"
|
|
9370
|
-
"
|
|
9371
|
-
"
|
|
9372
|
-
"
|
|
9373
|
-
"
|
|
9374
|
-
"
|
|
9375
|
-
"
|
|
9376
|
-
"
|
|
9875
|
+
" {",
|
|
9876
|
+
' "type": "swimlane",',
|
|
9877
|
+
' "title": "Request \u2192 response sequence",',
|
|
9878
|
+
' "lanes": ["Client", "Server", "Database"],',
|
|
9879
|
+
' "focalLane": "{optional \xB7 lane name to tint as the focal row}",',
|
|
9880
|
+
' "focal": "{optional \xB7 step label to render in accent}",',
|
|
9881
|
+
' "steps": [',
|
|
9882
|
+
' { "label": "Request", "laneIdx": 0, "kicker": "USER" },',
|
|
9883
|
+
' { "label": "Validate", "laneIdx": 1, "kicker": "AUTH" },',
|
|
9884
|
+
' { "label": "Query", "laneIdx": 2, "kicker": "STORE" },',
|
|
9885
|
+
' { "label": "Respond", "laneIdx": 0, "kicker": "USER" }',
|
|
9886
|
+
" ],",
|
|
9887
|
+
' "caption": "{Editorial gloss with *italic* on the operative phrase.}"',
|
|
9888
|
+
" }",
|
|
9377
9889
|
" ```",
|
|
9378
9890
|
" Hard rules:",
|
|
9379
|
-
" \xB7
|
|
9380
|
-
" \xB7
|
|
9381
|
-
" \xB7
|
|
9382
|
-
|
|
9383
|
-
" \xB7
|
|
9384
|
-
" \xB7
|
|
9385
|
-
"",
|
|
9386
|
-
"
|
|
9387
|
-
|
|
9891
|
+
" \xB7 `type` MUST be `swimlane` (sequence diagrams are rendered through the unified swimlane pipeline).",
|
|
9892
|
+
" \xB7 **Complexity floor \xB7 \u2265 2 lanes AND \u2265 4 steps**. A 3-step sequence is just a bullet list with arrow glyphs \u2014 drop it and use prose.",
|
|
9893
|
+
" \xB7 2\u20134 lanes; 3\u201310 steps total.",
|
|
9894
|
+
" \xB7 `laneIdx` is the 0-based index into the `lanes` array. The renderer routes elbow connectors when consecutive steps cross lanes.",
|
|
9895
|
+
" \xB7 `kicker` (optional) is a short mono caption inside the step box (the role or call type). Defaults to `STEP NN` when omitted.",
|
|
9896
|
+
" \xB7 `focal` matches a step label exactly \xB7 accent border + accent kicker. `focalLane` tints the lane background.",
|
|
9897
|
+
" \xB7 Labels \u2264 18 chars (renderer truncates).",
|
|
9898
|
+
"",
|
|
9899
|
+
" ### journey \xB7 user / stakeholder journey scoring (renders as `swimlane`)",
|
|
9900
|
+
' Use when a section maps how a stakeholder experiences a process \u2014 best for product / UX rooms scoring touchpoints, or adoption-friction analysis ("the buyer\'s journey from awareness to renewal"). Render through the SAME `swimlane` schema, with an added `score` (1\u20135) per step. The renderer draws 5 small dots under each scored step, filled up to the score. Schema (only differences from sequenceDiagram):',
|
|
9388
9901
|
" ```",
|
|
9389
|
-
"
|
|
9390
|
-
"
|
|
9391
|
-
"
|
|
9392
|
-
"
|
|
9393
|
-
"
|
|
9394
|
-
|
|
9395
|
-
"
|
|
9396
|
-
"
|
|
9397
|
-
|
|
9398
|
-
"
|
|
9902
|
+
" {",
|
|
9903
|
+
' "type": "swimlane",',
|
|
9904
|
+
' "lanes": ["Awareness", "Trial", "Adopt"],',
|
|
9905
|
+
' "steps": [',
|
|
9906
|
+
' { "label": "Hear about it", "laneIdx": 0, "kicker": "BUYER", "score": 3 },',
|
|
9907
|
+
' { "label": "Read landing", "laneIdx": 0, "kicker": "BUYER", "score": 4 },',
|
|
9908
|
+
' { "label": "Sign up", "laneIdx": 1, "kicker": "BUYER", "score": 2 },',
|
|
9909
|
+
' { "label": "First task", "laneIdx": 1, "kicker": "BUYER \xB7 IT", "score": 1 },',
|
|
9910
|
+
' { "label": "Approve", "laneIdx": 2, "kicker": "LEGAL", "score": 4 }',
|
|
9911
|
+
" ],",
|
|
9912
|
+
' "caption": "{Editorial gloss with *italic* on the worst moment.}"',
|
|
9913
|
+
" }",
|
|
9399
9914
|
" ```",
|
|
9400
|
-
" Hard rules:",
|
|
9401
|
-
" \xB7 `
|
|
9402
|
-
" \xB7
|
|
9403
|
-
|
|
9404
|
-
"
|
|
9405
|
-
"
|
|
9406
|
-
"",
|
|
9407
|
-
" ### stateDiagram \xB7 lifecycle / phase transitions",
|
|
9408
|
-
" Use when a section names a process with discrete states the subject moves between (deal lifecycle, customer onboarding, product evolution, regulatory approval phases, scenario branching with feedback loops). Reads cleaner than a flowchart when the *states themselves* are the load-bearing concept, not the conditions. Best fit: execution-plan rollouts with stage gates, market-evolution narratives, retro post-mortems where the system passed through phases.",
|
|
9915
|
+
" Hard rules (additional to swimlane):",
|
|
9916
|
+
" \xB7 `score` is OPTIONAL \xB7 1\u20135 (5 = best). Omit on steps where scoring doesn't apply. Mixing scored and unscored steps in one chart is fine.",
|
|
9917
|
+
" \xB7 `kicker` can list multiple actors with ` \xB7 ` separator (e.g. `BUYER \xB7 IT`).",
|
|
9918
|
+
" \xB7 For journey, set `focal` on the step with the *worst* score (the friction point that drives the argument). Counter-intuitive but correct \u2014 the editorial accent goes where the work is.",
|
|
9919
|
+
"",
|
|
9920
|
+
" ### stateDiagram \xB7 lifecycle / phase transitions (renders as `state-machine`)",
|
|
9921
|
+
' Use when a section names a process with discrete states the subject moves between (deal lifecycle, customer onboarding, product evolution, regulatory approval phases). Render as a fenced ```kami-chart with `type: "state-machine"`. The renderer draws 2\u20136 states left-to-right with forward arrows between consecutive states; back-transitions render as dashed arcs above. Schema:',
|
|
9409
9922
|
" ```",
|
|
9410
|
-
"
|
|
9411
|
-
"
|
|
9412
|
-
"
|
|
9413
|
-
"
|
|
9414
|
-
"
|
|
9415
|
-
"
|
|
9416
|
-
"
|
|
9923
|
+
" {",
|
|
9924
|
+
' "type": "state-machine",',
|
|
9925
|
+
' "title": "Deal lifecycle",',
|
|
9926
|
+
' "showStart": true,',
|
|
9927
|
+
' "showEnd": true,',
|
|
9928
|
+
' "focal": "{optional \xB7 state label rendered in accent}",',
|
|
9929
|
+
' "states": [',
|
|
9930
|
+
' { "label": "Discovery", "hint": "opportunity" },',
|
|
9931
|
+
' { "label": "Pilot", "hint": "hypothesis" },',
|
|
9932
|
+
' { "label": "Scale", "hint": "rolling out" },',
|
|
9933
|
+
' { "label": "Closed", "hint": "resolved", "terminal": true }',
|
|
9934
|
+
" ],",
|
|
9935
|
+
' "forwardLabels": ["validated", "win-rate \u2265 60%", "complete"],',
|
|
9936
|
+
' "backTransitions": [',
|
|
9937
|
+
' { "fromIdx": 1, "toIdx": 0, "label": "reset" }',
|
|
9938
|
+
" ],",
|
|
9939
|
+
' "caption": "{Editorial gloss with *italic* on the cycle.}"',
|
|
9940
|
+
" }",
|
|
9417
9941
|
" ```",
|
|
9418
9942
|
" Hard rules:",
|
|
9419
|
-
" \xB7
|
|
9420
|
-
" \xB7
|
|
9421
|
-
" \xB7 `
|
|
9422
|
-
|
|
9423
|
-
" \xB7
|
|
9424
|
-
" \xB7
|
|
9425
|
-
" \xB7
|
|
9943
|
+
" \xB7 `type` MUST be `state-machine`.",
|
|
9944
|
+
" \xB7 **Complexity floor \xB7 \u2265 3 states AND at least one back-transition** (a `backTransitions` entry). A pure linear `Discovery \u2192 Pilot \u2192 Scale` chain has no cycle \u2014 render as `flowchart linear-v` instead. The thing that justifies a state-machine is the back-transition.",
|
|
9945
|
+
" \xB7 `states` array: 2\u20136 entries. Each has `label` (\u2264 16 chars), optional `hint` (\u2264 14 chars \xB7 mono caption inside the box), optional `terminal: true` (draws with darker ink border).",
|
|
9946
|
+
" \xB7 `forwardLabels` is an array indexed by the source state position \xB7 entry `i` labels the arrow from state `i` to state `i+1`. Empty / missing entries leave the arrow unlabeled.",
|
|
9947
|
+
" \xB7 `backTransitions`: each entry needs `fromIdx > toIdx` (back-arcs go right-to-left). The renderer draws a dashed Q-curve above the row.",
|
|
9948
|
+
" \xB7 `showStart: true` draws a solid black dot before the first state (the entry pseudo-state). `showEnd: true` draws a double-circle terminator after the last state.",
|
|
9949
|
+
" \xB7 `focal` matches a state label exactly \xB7 that state renders in the spine accent.",
|
|
9426
9950
|
"",
|
|
9427
9951
|
" ## Recommendations",
|
|
9428
9952
|
" Skip if `recommendations` is empty. Otherwise render as a numbered list, one per recommendation, sorted by priority. Each item:",
|
|
@@ -9457,7 +9981,7 @@ var WRITE_SYSTEM = [
|
|
|
9457
9981
|
' "Channel concentration": [0.80, 0.80]',
|
|
9458
9982
|
' "Hiring bench thin": [0.50, 0.20]',
|
|
9459
9983
|
" ```",
|
|
9460
|
-
" Leave a BLANK LINE between the H2 heading and the fenced ```
|
|
9984
|
+
" Leave a BLANK LINE between the H2 heading and the fenced ```kami-chart block.",
|
|
9461
9985
|
"",
|
|
9462
9986
|
" 2. **Risk table** \u2014 markdown table with columns `Risk | Category | Severity | Likelihood | Owner | Mitigation`. One row per `RiskItem`. Sort rows: severity high before medium before low; within the same severity, likelihood high before medium before low. Render category and severity / likelihood as **bold inline tags** (`**Market**`, `**High**`). Leave a BLANK LINE between the quadrant chart and the table header row \xB7 without that gap markdown parsers concatenate the prose with the table and pipe syntax leaks.",
|
|
9463
9987
|
' When `mitigation` is the literal string `"monitor only"`, render the cell as italic: `_monitor only_` so the reader sees this row as a watch-list rather than a closeable risk.',
|
|
@@ -9647,7 +10171,7 @@ var WRITE_SYSTEM = [
|
|
|
9647
10171
|
" ### metricStrip (dashboard \xB7 the room's numbers as a row of KPI cards)",
|
|
9648
10172
|
" 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.",
|
|
9649
10173
|
" Heading from the house style (default `## By the Numbers`).",
|
|
9650
|
-
" 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 ```
|
|
10174
|
+
" 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 ```kami-chart is handled today). Format:",
|
|
9651
10175
|
" ```metric-strip",
|
|
9652
10176
|
" {",
|
|
9653
10177
|
' "intro": "Three numbers worth pricing in",',
|
|
@@ -9659,7 +10183,7 @@ var WRITE_SYSTEM = [
|
|
|
9659
10183
|
" }",
|
|
9660
10184
|
" ```",
|
|
9661
10185
|
" Hard rules:",
|
|
9662
|
-
" \xB7 The block opens with the literal three backticks + `metric-strip` and closes with three backticks on a line by itself. Just like
|
|
10186
|
+
" \xB7 The block opens with the literal three backticks + `metric-strip` and closes with three backticks on a line by itself. Just like kami-chart blocks.",
|
|
9663
10187
|
" \xB7 Body is one JSON object with `intro` (string, may be empty) and `cards` (array of 3\u20135 objects).",
|
|
9664
10188
|
' \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).',
|
|
9665
10189
|
" \xB7 Mirror the scaffold.metricStrip values 1:1. Don't invent extra cards; don't drop cards the scaffold supplied.",
|
|
@@ -9746,7 +10270,7 @@ var WRITE_SYSTEM = [
|
|
|
9746
10270
|
"\xB7 Use **bold** for claims and section markers.",
|
|
9747
10271
|
'\xB7 No "I" or "we" as the writer. The brief is the room speaking.',
|
|
9748
10272
|
'\xB7 No preamble, no closing remarks, no "in summary". Just the brief.',
|
|
9749
|
-
"\xB7 Markdown only \u2014 fenced ```
|
|
10273
|
+
"\xB7 Markdown only \u2014 fenced ```kami-chart blocks (and the other structured-fence languages like ```metric-strip / ```callout) are part of markdown for our renderer.",
|
|
9750
10274
|
"\xB7 Replace all director ids (like `dirId-a`) with display names. Never let a raw id leak into prose.",
|
|
9751
10275
|
'\xB7 Numbers everywhere \u2014 even qualitative claims get bracketed by numbers when possible ("about 2/3 of the directors", "in the next 18 months", "~30% confidence").',
|
|
9752
10276
|
'\xB7 Section headings ARE the takeaway \u2014 never use topic-style headings (e.g. "Market analysis"). Always claim-style (e.g. "China growth will slow to <5%").'
|
|
@@ -11727,7 +12251,7 @@ var SYSTEM_PROMPT = [
|
|
|
11727
12251
|
" \xB7 `convergence` Where directors aligned via independent reasoning paths. Needs \u2265 2 directors via \u2265 2 lenses.",
|
|
11728
12252
|
" \xB7 `divergence` The single hinge where directors split. Skip when the room had no real central tension.",
|
|
11729
12253
|
" \xB7 `positions` 2\u20133 named camps with a pull-quote per camp. Skip when directors didn't cluster.",
|
|
11730
|
-
" \xB7 `visuals` 0\u20134 visual exhibits. Seven sub-types \u2014 **prefer
|
|
12254
|
+
" \xB7 `visuals` 0\u20134 visual exhibits. Seven sub-types \u2014 **prefer the inline-SVG chart sub-types (4) over text matrix (3)** unless the data shape genuinely doesn't fit. SVG charts render as a real diagram in seconds; text matrices are dense and slow to absorb. Inline-SVG sub-types (rendered through the `kami-chart` pipeline, spine-tokenised): `quadrant-chart` (2-axis plot), `bar-chart` (ranked numeric \xB7 2\u20138 items), `timeline` (dated narrative \xB7 3\u20138 beats), `pie-chart` (distribution \xB7 2\u20136 slices \xB7 renders as a donut). Text-matrix sub-types: `comparison-table` (N options \xD7 M criteria), `force-field` (drivers vs resistors), `strengths-cautions` (pros/cons/verdict per option). Triggers: ranked numeric \u2192 bar-chart \xB7 chronology / historical analogue / projected sequence \u2192 timeline \xB7 distribution that sums (probability split / vote tallies / shares / mix) \u2192 pie-chart \xB7 2-axis plot \u2192 quadrant-chart \xB7 N-options-with-mixed-cells \u2192 comparison-table \xB7 for/against forces \u2192 force-field \xB7 pros/cons/verdict matrix \u2192 strengths-cautions. **Beyond `visuals`, the writer ALSO emits inline charts (flowchart / mindmap / gantt / sequenceDiagram / stateDiagram / journey, all rendered through the `kami-chart` pipeline) inside body sections where prose can't carry the structure efficiently** \u2014 these are automatic and don't need to be picked here.",
|
|
11731
12255
|
" \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.",
|
|
11732
12256
|
" \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.",
|
|
11733
12257
|
" \xB7 `why-now` Single panel: window opened by what \xB7 window closes when \xB7 the bet implied. For investment / opportunity rooms (a16z-thesis spine).",
|
|
@@ -11767,11 +12291,11 @@ var SYSTEM_PROMPT = [
|
|
|
11767
12291
|
"",
|
|
11768
12292
|
"These rules ALSO apply transitively: if the asset bundle has 0 entries for a trigger, the matching component is encouraged but not mandatory.",
|
|
11769
12293
|
"",
|
|
11770
|
-
"## Visualisation discipline \xB7 prefer
|
|
12294
|
+
"## Visualisation discipline \xB7 prefer charts where they fit naturally",
|
|
11771
12295
|
"",
|
|
11772
|
-
"Reports benefit from diagrams when content has structure prose can't carry efficiently \u2014 but no fixed quota. Pick components that auto-fire
|
|
11773
|
-
" 1. Pick `visuals` with
|
|
11774
|
-
" 2. Stage 3 writer auto-emits inline
|
|
12296
|
+
"Reports benefit from diagrams when content has structure prose can't carry efficiently \u2014 but no fixed quota. Pick components that auto-fire charts (divergence / positions / decision-options / scenario-tree / risk-register / recommendations / convergence) when their material is real in the room. Skip them when material is thin. Source of charts (either / both):",
|
|
12297
|
+
" 1. Pick `visuals` with chart sub-types (`quadrant-chart` / `bar-chart` / `timeline` / `pie-chart`) \u2014 these render as inline SVG through the `kami-chart` pipeline (spine-tokenised, no JS runtime).",
|
|
12298
|
+
" 2. Stage 3 writer auto-emits inline charts (`flowchart` / `mindmap` / `gantt` / `sequenceDiagram` / `stateDiagram` / `journey`, all rendered through the `kami-chart` pipeline) on a per-section basis when content fits \u2014 knowing this helps you avoid double-allocating the same content to a typed visual.",
|
|
11775
12299
|
"",
|
|
11776
12300
|
"**Quality over quantity.** A substantive strategy brief might naturally land at 4\u20136 charts; a tight philosophical brief at 0\u20131; both are correct. Don't manufacture chart material that isn't in the conversation \u2014 that produces hollow diagrams that distract more than they inform.",
|
|
11777
12301
|
"",
|
|
@@ -11788,7 +12312,7 @@ var SYSTEM_PROMPT = [
|
|
|
11788
12312
|
" \xB7 ANY multi-party negotiation / approval-chain / system-call sequence \u2192 the writer auto-emits an inline `sequenceDiagram`. Surfaces under whichever section names the actors.",
|
|
11789
12313
|
" \xB7 ANY lifecycle / state-machine / phase-gating story \u2192 the writer auto-emits an inline `stateDiagram-v2`. Attach to `recommendations` or a `risk-register` branch.",
|
|
11790
12314
|
"",
|
|
11791
|
-
"Truly unvisualisable rooms (purely philosophical / definitional / no concrete entities) are allowed to skip \u2014 but they're rare. **When in doubt, pick `visuals` with a
|
|
12315
|
+
"Truly unvisualisable rooms (purely philosophical / definitional / no concrete entities) are allowed to skip \u2014 but they're rare. **When in doubt, pick `visuals` with a chart sub-type.** Reports without a chart in 2026 read as 2018-era memos.",
|
|
11792
12316
|
"",
|
|
11793
12317
|
"## Picking presets",
|
|
11794
12318
|
"",
|
|
@@ -12866,10 +13390,10 @@ function stageFlagshipList() {
|
|
|
12866
13390
|
if (cheap && !out.includes(cheap)) out.push(cheap);
|
|
12867
13391
|
return out;
|
|
12868
13392
|
}
|
|
12869
|
-
var STAGE_2_RETRIES =
|
|
12870
|
-
var STAGE_2_TEMPERATURES = [0.2];
|
|
13393
|
+
var STAGE_2_RETRIES = 2;
|
|
13394
|
+
var STAGE_2_TEMPERATURES = [0.2, 0.5];
|
|
12871
13395
|
var STAGE_1_CALL_TIMEOUT_MS = 75e3;
|
|
12872
|
-
var STAGE_2_CALL_TIMEOUT_MS =
|
|
13396
|
+
var STAGE_2_CALL_TIMEOUT_MS = 24e4;
|
|
12873
13397
|
var STAGE_3_CALL_TIMEOUT_MS = 24e4;
|
|
12874
13398
|
function signalWithTimeout2(parent, timeoutMs) {
|
|
12875
13399
|
const controller = new AbortController();
|
|
@@ -13108,7 +13632,7 @@ var TPS_BY_MODEL = {
|
|
|
13108
13632
|
sonnet: 45,
|
|
13109
13633
|
// sonnet-4-6 — structured JSON output, not free prose
|
|
13110
13634
|
opus: 28
|
|
13111
|
-
// opus-4-7 — rich markdown with tables /
|
|
13635
|
+
// opus-4-7 — rich markdown with tables / inline-svg charts
|
|
13112
13636
|
};
|
|
13113
13637
|
var BASE_OVERHEAD_S = 1;
|
|
13114
13638
|
var TTFT_S_PER_KT = 0.35;
|
|
@@ -13512,7 +14036,16 @@ async function runStage1(roomId, briefId, directors, transcript, room, language,
|
|
|
13512
14036
|
modelV,
|
|
13513
14037
|
messages,
|
|
13514
14038
|
temperature: 0.2,
|
|
13515
|
-
|
|
14039
|
+
// 4400 cap (was 2200) · 9-field asset bundle JSON · max 38
|
|
14040
|
+
// entries × ~150 chars per entry ≈ 5700 chars ≈ 2500-3500
|
|
14041
|
+
// output tokens for a dense director. 2200 was right at the
|
|
14042
|
+
// edge for terse models (Gemini Flash) but Anthropic Opus /
|
|
14043
|
+
// Sonnet emit verbose JSON and overflow, producing truncated
|
|
14044
|
+
// output → parser drops to empty assets → `heuristicDirectorAssets`
|
|
14045
|
+
// takes over with weaker signal recovery. Doubling the cap
|
|
14046
|
+
// gives the verbose providers headroom without affecting
|
|
14047
|
+
// the terse ones (they don't hit the cap).
|
|
14048
|
+
maxTokens: 4400,
|
|
13516
14049
|
signal: timeout.signal
|
|
13517
14050
|
});
|
|
13518
14051
|
if (timeout.timedOut()) throw new Error(`timed out after ${STAGE_1_CALL_TIMEOUT_MS / 1e3}s`);
|
|
@@ -13873,7 +14406,18 @@ async function runStage2(args) {
|
|
|
13873
14406
|
modelV,
|
|
13874
14407
|
messages: messagesForAttempt(),
|
|
13875
14408
|
temperature: STAGE_2_TEMPERATURES[attempt] ?? 0.6,
|
|
13876
|
-
|
|
14409
|
+
// 16k cap (was 8k) · the structured scaffold JSON for a dense
|
|
14410
|
+
// room (≥3 directors with rich signals, full visuals + risk
|
|
14411
|
+
// register + perspectives) regularly lands at 10–14k output
|
|
14412
|
+
// tokens. Opus 4.6 Fast hit the 8k ceiling mid-`directorPerspectives`
|
|
14413
|
+
// and emitted truncated JSON · parseScaffold then returned
|
|
14414
|
+
// null and the pipeline dropped to deterministic fallback,
|
|
14415
|
+
// which has no visuals / divergence / risk — wiping every
|
|
14416
|
+
// chart from the brief. Gemini 3 Flash is more terse on the
|
|
14417
|
+
// same prompt so it cleared 8k on most rooms; Anthropic
|
|
14418
|
+
// models reliably overflow. 16k matches Anthropic's max
|
|
14419
|
+
// output cap for the Sonnet/Opus class.
|
|
14420
|
+
maxTokens: 16e3,
|
|
13877
14421
|
signal: timeout.signal,
|
|
13878
14422
|
onText: (_delta, full) => advanceOnBuffer(full)
|
|
13879
14423
|
});
|
|
@@ -14072,7 +14616,7 @@ async function runBentoStage(args) {
|
|
|
14072
14616
|
modelV,
|
|
14073
14617
|
messages,
|
|
14074
14618
|
temperature: STAGE_2_TEMPERATURES[attempt] ?? 0.6,
|
|
14075
|
-
maxTokens:
|
|
14619
|
+
maxTokens: 16e3,
|
|
14076
14620
|
signal: args.signal
|
|
14077
14621
|
})) {
|
|
14078
14622
|
if (chunk.type === "text") {
|
|
@@ -15285,7 +15829,7 @@ async function synthesiseTopics(state, modelV, opts) {
|
|
|
15285
15829
|
${s.description}
|
|
15286
15830
|
${s.url}`
|
|
15287
15831
|
).join("\n\n");
|
|
15288
|
-
const system = 'You recommend boardroom discussion topics to a user, based on (a) the chair\'s long-term memory of who they are + what they care about, and optionally (b) a set of currently-trending web/x.com snippets keyed off the user\'s recent interests. Produce EXACTLY
|
|
15832
|
+
const system = 'You recommend boardroom discussion topics to a user, based on (a) the chair\'s long-term memory of who they are + what they care about, and optionally (b) a set of currently-trending web/x.com snippets keyed off the user\'s recent interests. Produce EXACTLY 5 distinct topics \u2014 not 4, not 6, five. Each topic is a subject line a user could plausibly drop into the convene composer.\n\nThe 5 topics MUST span DIFFERENT dimensions/categories \u2014 don\'t return five pricing topics. Use the 10 keywords as a multi-dimensional search index; the 5 final topics should distil ACROSS those dimensions so the picker reads as a balanced board agenda, not a single-angle obsession. Each topic gets a different `tag`.\n\nVoice: tight, specific, opinionated. Avoid corporate-speak. Skew toward questions the user would actually want to debate, not generic explainers.\n\nEVERY topic MUST include a `tag` field \xB7 a SHORT CATEGORY in 1-2 lowercase words naming what bucket the topic falls into. Pick from the user\'s actual subject matter (examples: strategy / product / market / pricing / positioning / brand / hiring / fundraising / ops / infra / research / craft / ethics / personal / leadership / growth / sales / design / data / partnerships / regulation). FORBIDDEN tag values: "web", "memory", "general", "misc", "other", "topic", "recommendation" \u2014 these are meta-vocabulary that leaks the system, not real categories. If the topic spans two areas, pick the dominant one. NEVER omit the tag field.\n\nEach topic must cite either (a) at least one snippet ref (S<n>) \u2014 in which case set "source":"web" \u2014 OR (b) no snippet refs at all \u2014 in which case set "source":"memory". The `source` and `tag` fields are independent: `source` is the data provenance, `tag` is the topic category. Never use "web" or "memory" as a tag.\n\nStrict JSON output only:\n{ "topics": [ { "tag": "pricing", "subject": "...", "rationale": "one sentence on why this fits the user", "source": "web|memory", "snippetRefs": [<S indexes, omit when source=memory>] } ] }';
|
|
15289
15833
|
const user = `# Keywords distilled from chair memory
|
|
15290
15834
|
${keywords.map((k, i) => `K${i + 1}. ${k}`).join("\n")}
|
|
15291
15835
|
|
|
@@ -15295,7 +15839,7 @@ ${memorySummary}
|
|
|
15295
15839
|
# Web snippets ${hasWeb ? "(use these to ground at least some recs as source=web)" : "(none \u2014 synthesise from memory only)"}
|
|
15296
15840
|
${snippetBlock}
|
|
15297
15841
|
|
|
15298
|
-
Return EXACTLY
|
|
15842
|
+
Return EXACTLY 5 topics as JSON, each with a different tag, spanning different dimensions.`;
|
|
15299
15843
|
const raw = await callPhaseLLM2(state, modelV, [
|
|
15300
15844
|
{ role: "system", content: system },
|
|
15301
15845
|
{ role: "user", content: user }
|
|
@@ -15904,9 +16448,21 @@ async function pickRoundWrap(opts) {
|
|
|
15904
16448
|
}
|
|
15905
16449
|
async function pickNextSpeaker(opts) {
|
|
15906
16450
|
const { candidates, history, room, signal } = opts;
|
|
16451
|
+
const mode = opts.mode === "dissent-gap" ? "dissent-gap" : "lens-gap";
|
|
16452
|
+
const convergentTerms = (opts.convergentTerms || []).filter(Boolean);
|
|
15907
16453
|
if (candidates.length < 2) return { agentId: null, rationale: "", intervention: null };
|
|
15908
|
-
const roster = candidates.map((a) =>
|
|
15909
|
-
|
|
16454
|
+
const roster = candidates.map((a) => {
|
|
16455
|
+
const baseRow = `- ${a.id} \xB7 ${a.name} (${a.handle}) \xB7 ${a.roleTag}
|
|
16456
|
+
${a.bio}`;
|
|
16457
|
+
if (mode !== "dissent-gap") return baseRow;
|
|
16458
|
+
const takes = a.personaSpec?.spec?.contrarianTakes?.slice(0, 3) || [];
|
|
16459
|
+
const failures = a.personaSpec?.spec?.failureModes?.slice(0, 1) || [];
|
|
16460
|
+
const extras = [];
|
|
16461
|
+
if (takes.length > 0) extras.push(` \xB7 contrarian takes: ${takes.join(" \xB7 ")}`);
|
|
16462
|
+
if (failures.length > 0) extras.push(` \xB7 failure mode: ${failures[0]}`);
|
|
16463
|
+
return extras.length > 0 ? `${baseRow}
|
|
16464
|
+
${extras.join("\n")}` : baseRow;
|
|
16465
|
+
}).join("\n");
|
|
15910
16466
|
const transcript = history.slice(-12).filter((m) => {
|
|
15911
16467
|
if (!m.body || !m.body.trim()) return false;
|
|
15912
16468
|
const meta = m.meta;
|
|
@@ -15917,6 +16473,38 @@ async function pickNextSpeaker(opts) {
|
|
|
15917
16473
|
const who = m.authorKind === "user" ? "USER" : m.authorId || "agent";
|
|
15918
16474
|
return `[${who}] ${m.body.trim().slice(0, 600)}`;
|
|
15919
16475
|
}).join("\n\n");
|
|
16476
|
+
const decision1Block = mode === "dissent-gap" ? [
|
|
16477
|
+
"DECISION 1 \xB7 Next speaker (DISSENT-GAP MODE).",
|
|
16478
|
+
"The room is converging on a single frame \u2014 for THIS pick, the chair",
|
|
16479
|
+
"needs the director MOST LIKELY to break that frame. Score each",
|
|
16480
|
+
"candidate on:",
|
|
16481
|
+
" \xB7 Their `contrarian takes` (listed in the roster) versus the room's",
|
|
16482
|
+
" detected convergent terms (surfaced in the user message below).",
|
|
16483
|
+
" Pick whose stated contrarian moves DIRECTLY puncture the cluster.",
|
|
16484
|
+
" \xB7 Their `failure mode` is a NEGATIVE signal \u2014 a director whose",
|
|
16485
|
+
" failure mode is 'gets sucked into specifics' is exactly who you",
|
|
16486
|
+
" do NOT pick when the room is already lost in specifics.",
|
|
16487
|
+
" \xB7 Lens distance from the convergent frame \xB7 pick a lens furthest",
|
|
16488
|
+
" from the cluster's gravitational center.",
|
|
16489
|
+
" \xB7 Recency \xB7 prefer directors who haven't spoken in the last 2 turns",
|
|
16490
|
+
" when scores are comparable.",
|
|
16491
|
+
" \xB7 If NO candidate is clearly the frame-breaker (e.g. all candidates",
|
|
16492
|
+
" have already been used recently OR none have relevant contrarian",
|
|
16493
|
+
" takes), set agent_id=null and let round-robin run."
|
|
16494
|
+
].join("\n") : [
|
|
16495
|
+
"DECISION 1 \xB7 Next speaker. From the candidates below, pick which",
|
|
16496
|
+
"director should speak NEXT \u2014 the one whose lens most sharply",
|
|
16497
|
+
"addresses the unresolved tension, hidden assumption, or missing",
|
|
16498
|
+
"counter-argument in the previous turn.",
|
|
16499
|
+
" \xB7 Match LENS to the gap, not just topic relevance. If the prior",
|
|
16500
|
+
" turn made a structural claim, pick a director whose role",
|
|
16501
|
+
" pushes back from a different lens (data \u2192 narrative,",
|
|
16502
|
+
" empirical \u2192 first-principles, etc.).",
|
|
16503
|
+
" \xB7 Prefer directors who haven't been quoted yet THIS round when",
|
|
16504
|
+
" fits are comparable \u2014 diversity of voice.",
|
|
16505
|
+
" \xB7 If no candidate clearly fits better than the current head of",
|
|
16506
|
+
" queue, set agent_id=null and let round-robin run."
|
|
16507
|
+
].join("\n");
|
|
15920
16508
|
const sys = {
|
|
15921
16509
|
role: "system",
|
|
15922
16510
|
content: [
|
|
@@ -15924,18 +16512,7 @@ async function pickNextSpeaker(opts) {
|
|
|
15924
16512
|
"a reactive round; one director just finished. You make TWO",
|
|
15925
16513
|
"decisions in one pass.",
|
|
15926
16514
|
"",
|
|
15927
|
-
|
|
15928
|
-
"director should speak NEXT \u2014 the one whose lens most sharply",
|
|
15929
|
-
"addresses the unresolved tension, hidden assumption, or missing",
|
|
15930
|
-
"counter-argument in the previous turn.",
|
|
15931
|
-
" \xB7 Match LENS to the gap, not just topic relevance. If the prior",
|
|
15932
|
-
" turn made a structural claim, pick a director whose role",
|
|
15933
|
-
" pushes back from a different lens (data \u2192 narrative,",
|
|
15934
|
-
" empirical \u2192 first-principles, etc.).",
|
|
15935
|
-
" \xB7 Prefer directors who haven't been quoted yet THIS round when",
|
|
15936
|
-
" fits are comparable \u2014 diversity of voice.",
|
|
15937
|
-
" \xB7 If no candidate clearly fits better than the current head of",
|
|
15938
|
-
" queue, set agent_id=null and let round-robin run.",
|
|
16515
|
+
decision1Block,
|
|
15939
16516
|
"",
|
|
15940
16517
|
"DECISION 2 \xB7 Intervention (optional \xB7 default: null). Read the",
|
|
15941
16518
|
"prior 2\u20133 turns. Drop a 1-sentence chair note ONLY if a substantive",
|
|
@@ -15987,6 +16564,11 @@ async function pickNextSpeaker(opts) {
|
|
|
15987
16564
|
// only language signal was "recent transcript" — which a
|
|
15988
16565
|
// single English chair drift could pollute.
|
|
15989
16566
|
...room?.subject ? [`Room subject: ${room.subject}`, ``] : [],
|
|
16567
|
+
...mode === "dissent-gap" && convergentTerms.length > 0 ? [
|
|
16568
|
+
`Detected convergent terms (room is over-investing here \xB7 the dissent pick should puncture these):`,
|
|
16569
|
+
...convergentTerms.map((t) => ` \xB7 "${t}"`),
|
|
16570
|
+
``
|
|
16571
|
+
] : [],
|
|
15990
16572
|
`Candidates (queued, in current order):`,
|
|
15991
16573
|
roster,
|
|
15992
16574
|
``,
|
|
@@ -16421,6 +17003,74 @@ function renderPersonaFewShotBlock(speaker, deliveryMode) {
|
|
|
16421
17003
|
}
|
|
16422
17004
|
return lines.join("\n");
|
|
16423
17005
|
}
|
|
17006
|
+
function renderFrameBreakGuidance(terms) {
|
|
17007
|
+
if (!terms || terms.length === 0) return "";
|
|
17008
|
+
const bullets = terms.map((t) => ` \xB7 "${t}"`).join("\n");
|
|
17009
|
+
return [
|
|
17010
|
+
"",
|
|
17011
|
+
`\u2500\u2500\u2500 FRAME-BREAK GUIDANCE \xB7 WHAT THE ROOM HAS ALREADY OVER-INVESTED IN \u2500\u2500\u2500`,
|
|
17012
|
+
`The following noun phrases have been the recurring fixation in the last several turns. The room's value is breadth, not depth-in-one-frame. For THIS turn:`,
|
|
17013
|
+
bullets,
|
|
17014
|
+
`Treat these as a soft no-go. You may touch them ONLY (a) as a counter-example ("unlike X, \u2026") OR (b) to point out an assumption inside them the room hasn't questioned. Do NOT extend / refine / give a sub-angle on them. Find an entry point that lives OUTSIDE this cluster \u2014 a different stakeholder, a different time scale, a different mechanism, a different domain analogy.`
|
|
17015
|
+
].join("\n");
|
|
17016
|
+
}
|
|
17017
|
+
function renderFrameBreakerRole(role) {
|
|
17018
|
+
if (!role || !role.convergentFrame) return "";
|
|
17019
|
+
return [
|
|
17020
|
+
"",
|
|
17021
|
+
`\u2500\u2500\u2500 YOUR EXTRA JOB THIS TURN \xB7 BREAK THE ROOM'S FRAME \u2500\u2500\u2500`,
|
|
17022
|
+
`The room is converging on "${role.convergentFrame}". For THIS turn, you have been designated the frame-breaker. Your turn MUST do at least ONE of:`,
|
|
17023
|
+
` (a) Propose a concrete scenario / population / domain where "${role.convergentFrame}" simply does NOT apply, and show what the product / decision would look like there instead.`,
|
|
17024
|
+
` (b) Introduce a constraint dimension the room has not yet considered (a different time scale \xB7 a different stakeholder type \xB7 a different technical layer \xB7 a different cultural / regulatory context \xB7 a different physical / material constraint \xB7 a different value system) and show how it changes what matters.`,
|
|
17025
|
+
`Do NOT execute this as a literal labelled item ("frame-breaker move: \u2026"). Weave the move into your normal turn \u2014 the user sees the angle land naturally, not the assignment.`
|
|
17026
|
+
].join("\n");
|
|
17027
|
+
}
|
|
17028
|
+
function renderUnexploredAngles(angles) {
|
|
17029
|
+
if (!angles || angles.length === 0) return "";
|
|
17030
|
+
const bullets = angles.map((a) => ` \xB7 ${a}`).join("\n");
|
|
17031
|
+
return [
|
|
17032
|
+
"",
|
|
17033
|
+
`\u2500\u2500\u2500 UNEXPLORED ANGLES \xB7 WHERE THE ROOM HASN'T LOOKED YET \u2500\u2500\u2500`,
|
|
17034
|
+
`The chair noted these angles were raised-then-abandoned or notably absent in recent rounds:`,
|
|
17035
|
+
bullets,
|
|
17036
|
+
`Pick ONE as a possible entry point for your turn \u2014 or generate a fresh one of your own. The room is starved for breadth, not depth; one bullet on a genuinely new angle is worth three bullets refining a familiar one.`
|
|
17037
|
+
].join("\n");
|
|
17038
|
+
}
|
|
17039
|
+
function renderPersonaLensReminder(speaker) {
|
|
17040
|
+
const spec = speaker.personaSpec;
|
|
17041
|
+
if (spec && spec.spec) {
|
|
17042
|
+
const concepts = (spec.spec.loadBearingConcepts || []).slice(0, 3);
|
|
17043
|
+
const takes = (spec.spec.contrarianTakes || []).slice(0, 2);
|
|
17044
|
+
const failure = (spec.spec.failureModes || [])[0] || "";
|
|
17045
|
+
if (concepts.length === 0 && takes.length === 0) return "";
|
|
17046
|
+
const lines = [
|
|
17047
|
+
"",
|
|
17048
|
+
`\u2500\u2500\u2500 YOUR LENS \xB7 LAST-MINUTE REMINDER \u2500\u2500\u2500`,
|
|
17049
|
+
`Before you generate, re-anchor on who YOU are at this table. The conversation above has its own gravity; do NOT let it pull you off your signature angle.`
|
|
17050
|
+
];
|
|
17051
|
+
if (concepts.length > 0) {
|
|
17052
|
+
lines.push(`Your load-bearing concepts (the moves you naturally make): ${concepts.join(" \xB7 ")}.`);
|
|
17053
|
+
}
|
|
17054
|
+
if (takes.length > 0) {
|
|
17055
|
+
lines.push(`Your contrarian takes (push back from these when the room is converging): ${takes.join(" \xB7 ")}.`);
|
|
17056
|
+
}
|
|
17057
|
+
if (failure) {
|
|
17058
|
+
lines.push(`Your most common failure mode (avoid it this turn): ${failure}.`);
|
|
17059
|
+
}
|
|
17060
|
+
lines.push(`Speak from this. Not from the room's average frame.`);
|
|
17061
|
+
return lines.join("\n");
|
|
17062
|
+
}
|
|
17063
|
+
const instr = (speaker.instruction || "").trim();
|
|
17064
|
+
if (!instr) return "";
|
|
17065
|
+
const slice = instr.length > 280 ? instr.slice(0, 280) + "\u2026" : instr;
|
|
17066
|
+
return [
|
|
17067
|
+
"",
|
|
17068
|
+
`\u2500\u2500\u2500 YOUR LENS \xB7 LAST-MINUTE REMINDER \u2500\u2500\u2500`,
|
|
17069
|
+
`Before you generate, re-anchor on who YOU are at this table \u2014 the conversation above has its own gravity, do not let it pull you off your signature angle.`,
|
|
17070
|
+
`Recall your director frame: ${slice}`,
|
|
17071
|
+
`Speak from this lens, not from the room's average frame.`
|
|
17072
|
+
].join("\n");
|
|
17073
|
+
}
|
|
16424
17074
|
function renderPersonaReflectionBlock(speaker) {
|
|
16425
17075
|
const spec = speaker.personaSpec;
|
|
16426
17076
|
if (!spec || spec.reflectionChecklist.length === 0) return "";
|
|
@@ -16495,7 +17145,15 @@ var TONE_GUIDANCE = {
|
|
|
16495
17145
|
"## Optional \xB7 one synthesis turn per round",
|
|
16496
17146
|
"Once per round, ONE director may do a synthesis turn instead of pure generation: pick 2\u20133 of the room's ideas and propose 1\u20132 combinations. Use sparingly \u2014 80%+ of turns should be pure generation. The room's value is in the pile of ideas, not the polish on any one.",
|
|
16497
17147
|
"",
|
|
16498
|
-
|
|
17148
|
+
"PERSONA OVERRIDE \xB7 your director instruction reads like Socrates / First Principles / Value Investor \u2014 the demand-definition / demand-mechanism / demand-base-rate DNA. For THIS room, the **rigor / forensic mode** of that DNA is paused (no slow definition-check ladders, no methodical mechanism decomposition, no insistence on base-rate citation before moving) \u2014 that's too slow for brainstorm cadence. But the **contrarian / curiosity / lens-distinctness** half of your DNA is *AMPLIFIED*, not paused. Your persona's signature angles (your loadBearingConcepts, your contrarianTakes, your failureModes) are exactly what produces the unexpected ideas the room needs.",
|
|
17149
|
+
"",
|
|
17150
|
+
"DIVERGENCE DISCIPLINE (this is the anti-convergence rule, read it twice) \xB7",
|
|
17151
|
+
" \xB7 The room's gravitational pull, after 2-3 rounds, is toward whatever phrase has been said the most. Resist this consciously. If a core word from the prior 2 turns is about to appear in your turn, **stop and find a different entry point**. Saying it a second time = you got pulled. Saying it a third = the room has collapsed.",
|
|
17152
|
+
" \xB7 Yes-and is allowed for ONE of your bullets at most. The other 3-5 bullets must each open a direction the prior turn(s) did NOT touch. Yes-and is the local move; the room's job is global coverage.",
|
|
17153
|
+
` \xB7 If the most recent turn lived in domain X (e.g. "audit", "trust", "workflow"), your turn's center of gravity must be at LEAST one domain away (e.g. if peers are on audit, you go to physical environment, or cultural ritual, or time scale, or hidden user, or material constraint \u2014 not "audit but for X").`,
|
|
17154
|
+
' \xB7 Half-baked + concrete > polished + abstract. "What if the AI sat on a literal chair and rolled with the team" beats "AI as a collaborative presence layer" every time. Wild specificity > safe generality.',
|
|
17155
|
+
` \xB7 Your contrarian DNA: if your persona would normally push back against the room's emerging consensus, DO IT \u2014 but in brainstorm style: not as a 5-sentence rebuttal but as ONE counter-idea bullet that opens a fresh angle ("contra all the productivity talk \u2014 what if the assistant's job is to slow people down at exactly 3 inflection points a day?").`,
|
|
17156
|
+
` \xB7 TURN_DIRECTIVE ("introduce a new variable / constraint / analogy / counter-example") here means **at least one bullet of every turn must satisfy it**. Yes-and bullets don't count toward this floor.`
|
|
16499
17157
|
].join("\n"),
|
|
16500
17158
|
constructive: [
|
|
16501
17159
|
"CONSTRUCTIVE \xB7 sympathetic interrogator. You want the user to win, but only via an idea that can actually survive scrutiny.",
|
|
@@ -16836,14 +17494,35 @@ Name: ${prefs.name}
|
|
|
16836
17494
|
`\xB7 When the user's most recent input is already in the room (visible above as a [${prefs.name || "You"}] turn), you may acknowledge it ONCE in the opening sweep \u2014 never again. On any later turn, do NOT open with "Since you asked \u2026" / "As you requested \u2026" / "\u65E2\u7136\u4F60\u8981\u6C42\u4E86 \u2026" / "\u6309\u4F60\u8BF4\u7684 \u2026" / "\u65E2\u7136\u4F60\u63D0\u51FA \u2026" / "\u4F60\u65E2\u7136\u8BA9\u6211 \u2026" or any rephrasing. The user's direction is absorbed context now; engage with the discussion, don't re-preface every turn \u2014 that loops. If you've already spoken once on this user input, your next turn must move PAST that acknowledgment.`,
|
|
16837
17495
|
`\xB7 If you genuinely have NOTHING substantive to add this turn \u2014 the room has exhausted your angle, every point you'd make has already been made \u2014 return an EMPTY response (no text at all). Do NOT narrate your silence. Never output "\uFF08\u6C89\u9ED8\uFF09", "(silent)", "\u6211\u6CA1\u6709\u66F4\u591A\u8981\u8865\u5145\u7684", "I have nothing to add", "pass this round", "skip this turn", "abstain", or any variant. Those bubbles read as "the director gave up" and pollute the transcript; the system handles silent turns gracefully and moves the queue on. Return empty OR find one genuinely fresh angle (a different lens, a sharper edge case, a counter-frame, a missing trade-off) \u2014 never the meta-narration in between.`,
|
|
16838
17496
|
`\xB7 The TONE and INTENSITY blocks above are the room's working agreement \u2014 they OVERRIDE ${toneOverrideTarget} The user explicitly opted into this register; staying in role is the helpful behaviour, not breaking it for trained politeness or trained adversariness.`,
|
|
16839
|
-
// Persona reflection checklist ·
|
|
16840
|
-
//
|
|
16841
|
-
//
|
|
16842
|
-
//
|
|
16843
|
-
// failure modes specific to THIS director (e.g. "Am I
|
|
16844
|
-
// repeating @another_director's mechanism point?" for a
|
|
16845
|
-
// Historian).
|
|
17497
|
+
// Persona reflection checklist · catches failure modes
|
|
17498
|
+
// specific to THIS director (e.g. "Am I repeating
|
|
17499
|
+
// @another_director's mechanism point?" for a Historian).
|
|
17500
|
+
// Empty for Signal-mode / seed directors.
|
|
16846
17501
|
renderPersonaReflectionBlock(speaker),
|
|
17502
|
+
// Frame-break guidance (Layer 1.4) · "the room is converging
|
|
17503
|
+
// on X / Y / Z — don't extend them this turn." Soft no-go
|
|
17504
|
+
// list seen by ALL directors. Empty on opening rounds, short
|
|
17505
|
+
// rooms, and diverse rooms where no fixation detected.
|
|
17506
|
+
renderFrameBreakGuidance(opts.frameBreakTerms),
|
|
17507
|
+
// Unexplored angles (Layer 3.2) · positive companion · "here
|
|
17508
|
+
// are angles the room hasn't gone to yet; pick one or generate
|
|
17509
|
+
// your own." Empty when no negative-space record exists.
|
|
17510
|
+
renderUnexploredAngles(opts.unexploredAngles),
|
|
17511
|
+
// Frame-breaker role (Layer 2.2) · single designated
|
|
17512
|
+
// director per round; addendum tells them to do one of two
|
|
17513
|
+
// structural moves to break the room's frame. Empty for
|
|
17514
|
+
// non-frame-breakers and non-reactive rounds.
|
|
17515
|
+
renderFrameBreakerRole(opts.frameBreakerRole),
|
|
17516
|
+
// Persona-lens reminder · re-anchors the director on their
|
|
17517
|
+
// signature angle (top 3 loadBearingConcepts + top 2
|
|
17518
|
+
// contrarianTakes + worst failure mode) at the very tail of
|
|
17519
|
+
// the system prompt. The base persona instruction at the TOP
|
|
17520
|
+
// of the prompt has decayed against transformer attention by
|
|
17521
|
+
// this point; this reminder fights the conversational-mean
|
|
17522
|
+
// gravity that pulls directors into homogeneous voice by
|
|
17523
|
+
// round 3-4. See renderPersonaLensReminder above for the
|
|
17524
|
+
// composition rules.
|
|
17525
|
+
renderPersonaLensReminder(speaker),
|
|
16847
17526
|
// Target-language LANGUAGE LOCK · TRULY the last block in the
|
|
16848
17527
|
// system prompt so it's the freshest signal in the LLM's
|
|
16849
17528
|
// attention. Written in the room's working language (Chinese
|
|
@@ -17315,6 +17994,65 @@ function parseRoundEndOutput(text) {
|
|
|
17315
17994
|
return { ping, points, modeShift };
|
|
17316
17995
|
}
|
|
17317
17996
|
|
|
17997
|
+
// src/orchestrator/negative-space-extract.ts
|
|
17998
|
+
function renderTranscriptForRound(roundMessages) {
|
|
17999
|
+
const lines = [];
|
|
18000
|
+
for (const m of roundMessages) {
|
|
18001
|
+
if (m.authorKind !== "agent") continue;
|
|
18002
|
+
const kind = m.meta?.kind;
|
|
18003
|
+
if (kind && (kind === "round-open" || kind === "settings" || kind === "round-prompt")) continue;
|
|
18004
|
+
const body = (m.body || "").trim();
|
|
18005
|
+
if (!body) continue;
|
|
18006
|
+
lines.push(body.length > 500 ? body.slice(0, 500) + "\u2026" : body);
|
|
18007
|
+
}
|
|
18008
|
+
return lines.join("\n---\n");
|
|
18009
|
+
}
|
|
18010
|
+
async function extractNegativeSpace(opts) {
|
|
18011
|
+
const turns = (opts.roundMessages || []).filter(
|
|
18012
|
+
(m) => m.authorKind === "agent" && !m.meta?.kind
|
|
18013
|
+
);
|
|
18014
|
+
if (turns.length < 2) return [];
|
|
18015
|
+
const transcript = renderTranscriptForRound(turns);
|
|
18016
|
+
if (!transcript) return [];
|
|
18017
|
+
const modelV = utilityModelFor();
|
|
18018
|
+
if (!modelV) return [];
|
|
18019
|
+
const prompt = `You are protecting the divergence of a multi-director brainstorm. The room's subject is: "${opts.roomSubject}". A round of director turns just ended. Read those turns below and identify 1-3 ANGLES the round did NOT touch but plausibly SHOULD have, given the room's subject. An "angle" is a short noun phrase \u2264 8 words \u2014 a stakeholder type, a time horizon, a domain analogy, a technical layer, a cultural / regulatory context, a material constraint, a hidden user, a counter-population.
|
|
18020
|
+
|
|
18021
|
+
RULES
|
|
18022
|
+
\xB7 Each angle is a NEW direction the room could explore next, not a critique of what was said.
|
|
18023
|
+
\xB7 Each angle is a CONCRETE noun phrase, not a question. ("informal-economy workers" yes; "what about workers?" no.)
|
|
18024
|
+
\xB7 Each angle is genuinely fresh \u2014 NOT a paraphrase of what the round already discussed.
|
|
18025
|
+
\xB7 Match the language of the transcript.
|
|
18026
|
+
\xB7 Return ONLY a newline-separated list (max 3 lines). No bullets, no numbering, no preamble.
|
|
18027
|
+
\xB7 If the round was already genuinely diverse and no obvious angle is missing, return the literal token NONE.
|
|
18028
|
+
|
|
18029
|
+
Round transcript:
|
|
18030
|
+
${transcript}
|
|
18031
|
+
|
|
18032
|
+
Unexplored angles (newline-separated, or NONE):`;
|
|
18033
|
+
let body;
|
|
18034
|
+
try {
|
|
18035
|
+
body = await callLLM({
|
|
18036
|
+
modelV,
|
|
18037
|
+
carrier: null,
|
|
18038
|
+
messages: [{ role: "user", content: prompt }],
|
|
18039
|
+
temperature: 0.4,
|
|
18040
|
+
maxTokens: 120
|
|
18041
|
+
});
|
|
18042
|
+
} catch (e) {
|
|
18043
|
+
process.stderr.write(
|
|
18044
|
+
`[negative-space] extract failed: ${e instanceof Error ? e.message : String(e)}
|
|
18045
|
+
`
|
|
18046
|
+
);
|
|
18047
|
+
return [];
|
|
18048
|
+
}
|
|
18049
|
+
const txt = (body || "").trim();
|
|
18050
|
+
if (!txt) return [];
|
|
18051
|
+
if (/^none\b/i.test(txt) || /^无$/.test(txt)) return [];
|
|
18052
|
+
const lines = txt.split(/\n/).map((l) => l.trim().replace(/^[-·•*\d.)\s]+/, "").replace(/[。.]+$/, "")).filter((l) => l.length > 0 && l.length < 200);
|
|
18053
|
+
return lines.slice(0, 3);
|
|
18054
|
+
}
|
|
18055
|
+
|
|
17318
18056
|
// src/storage/summaries.ts
|
|
17319
18057
|
init_db();
|
|
17320
18058
|
function rowFrom(r) {
|
|
@@ -17419,7 +18157,13 @@ async function generateL1ForRound(roomId, roundNum) {
|
|
|
17419
18157
|
const keyPointsBlock = keyPoints.length ? "\n\nChair's key points from this round:\n" + keyPoints.map((kp, i) => `${i + 1}. ${kp.body}`).join("\n") : "";
|
|
17420
18158
|
const modelV = utilityModelFor();
|
|
17421
18159
|
if (!modelV) return;
|
|
17422
|
-
const prompt = `Summarise round ${roundNum} of a multi-director boardroom discussion in
|
|
18160
|
+
const prompt = `Summarise round ${roundNum} of a multi-director boardroom discussion in 4-7 short sentences. Your PRIMARY job is to **protect divergence** \u2014 preserve angles the room raised but did not develop, so future rounds know what's still open. Capture, in this order:
|
|
18161
|
+
(a) **New variables / lenses** any director introduced (even if the room ignored them) \u2014 list each by name. These are the room's unspent fuel.
|
|
18162
|
+
(b) **What was waved past** \u2014 angles mentioned in one sentence then abandoned. Name them explicitly.
|
|
18163
|
+
(c) **What was notably ABSENT** \u2014 if the round all discussed X, name 1-2 obvious dimensions (time scale, stakeholder type, geographic context, technical layer, cultural angle) that the round did NOT touch but should have.
|
|
18164
|
+
(d) **Tensions** \u2014 where directors took different positions (not "they all agreed on X"; that erases the divergence we want to preserve).
|
|
18165
|
+
(e) Only AFTER (a)-(d), 1-2 sentences on the substantive claims that emerged.
|
|
18166
|
+
Do NOT restate the original question. Do NOT add commentary. Do NOT use phrases like "directors converged" or "consensus emerged" \u2014 those distill out exactly the divergence we want to keep. Plain prose, third person, present tense.
|
|
17423
18167
|
|
|
17424
18168
|
--- Round ${roundNum} transcript ---
|
|
17425
18169
|
${transcript}` + keyPointsBlock;
|
|
@@ -17429,8 +18173,8 @@ ${transcript}` + keyPointsBlock;
|
|
|
17429
18173
|
messages: [
|
|
17430
18174
|
{ role: "user", content: prompt }
|
|
17431
18175
|
],
|
|
17432
|
-
temperature: 0.
|
|
17433
|
-
maxTokens:
|
|
18176
|
+
temperature: 0.3,
|
|
18177
|
+
maxTokens: 650
|
|
17434
18178
|
});
|
|
17435
18179
|
const trimmed = body.trim();
|
|
17436
18180
|
if (!trimmed) return;
|
|
@@ -17452,7 +18196,14 @@ async function foldL1IntoL2(roomId, newRoundNum, newL1Body) {
|
|
|
17452
18196
|
${existing.body}
|
|
17453
18197
|
|
|
17454
18198
|
` : "";
|
|
17455
|
-
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
|
|
18199
|
+
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 6-10 sentences covering rounds ${startRound} through ${endRound}.
|
|
18200
|
+
|
|
18201
|
+
Your PRIMARY job is to **protect divergence over time** \u2014 long rooms drift toward a single thread, and your narrative is what the next 10 rounds will use to remember the room. Maintain:
|
|
18202
|
+
(a) **A running list of "still-open angles"** \u2014 variables / lenses that were raised in any round and not deeply developed. Carry these forward across folds; do NOT prune them when a new round dominates a different track.
|
|
18203
|
+
(b) **Each director's distinctive lens** \u2014 what makes their contribution NOT interchangeable with the others. Avoid "all directors agreed on X" framings.
|
|
18204
|
+
(c) **Major pivots** \u2014 points where the room genuinely shifted direction (versus just deepened an existing track).
|
|
18205
|
+
(d) **Tensions that remain unresolved** \u2014 disagreements that haven't been settled. Preserve them; do NOT smooth them into consensus.
|
|
18206
|
+
Drop: minor exchanges, micro-clarifications, repeated points. Plain prose, third person. Do NOT use phrases like "directors converged" or "consensus emerged" \u2014 they erase exactly the divergence we want to preserve.
|
|
17456
18207
|
|
|
17457
18208
|
${previousBlock}New round ${newRoundNum} (just demoted from L1):
|
|
17458
18209
|
${newL1Body}`;
|
|
@@ -17460,8 +18211,8 @@ ${newL1Body}`;
|
|
|
17460
18211
|
modelV,
|
|
17461
18212
|
carrier: null,
|
|
17462
18213
|
messages: [{ role: "user", content: prompt }],
|
|
17463
|
-
temperature: 0.
|
|
17464
|
-
maxTokens:
|
|
18214
|
+
temperature: 0.3,
|
|
18215
|
+
maxTokens: 900
|
|
17465
18216
|
});
|
|
17466
18217
|
const trimmed = body.trim();
|
|
17467
18218
|
if (!trimmed) return;
|
|
@@ -17497,9 +18248,53 @@ function hashOf(s) {
|
|
|
17497
18248
|
return (h >>> 0).toString(16);
|
|
17498
18249
|
}
|
|
17499
18250
|
|
|
18251
|
+
// src/storage/negative-space.ts
|
|
18252
|
+
init_db();
|
|
18253
|
+
function mapRow11(r) {
|
|
18254
|
+
return {
|
|
18255
|
+
id: r.id,
|
|
18256
|
+
roomId: r.room_id,
|
|
18257
|
+
roundNum: r.round_num,
|
|
18258
|
+
angle: r.angle,
|
|
18259
|
+
createdAt: r.created_at,
|
|
18260
|
+
consumed: r.consumed === 1
|
|
18261
|
+
};
|
|
18262
|
+
}
|
|
18263
|
+
function insertNegativeSpaceAngles(roomId, roundNum, angles) {
|
|
18264
|
+
if (angles.length === 0) return;
|
|
18265
|
+
const db = getDb();
|
|
18266
|
+
const stmt = db.prepare(
|
|
18267
|
+
"INSERT INTO negative_space (id, room_id, round_num, angle, created_at, consumed) VALUES (?, ?, ?, ?, ?, 0)"
|
|
18268
|
+
);
|
|
18269
|
+
const now = Date.now();
|
|
18270
|
+
const tx = db.transaction((rows) => {
|
|
18271
|
+
for (const r of rows) {
|
|
18272
|
+
stmt.run(r.id, roomId, roundNum, r.angle, now);
|
|
18273
|
+
}
|
|
18274
|
+
});
|
|
18275
|
+
tx(
|
|
18276
|
+
angles.map((a) => a.trim()).filter((a) => a.length > 0 && a.length < 280).map((a) => ({ id: newId(), angle: a }))
|
|
18277
|
+
);
|
|
18278
|
+
}
|
|
18279
|
+
function getRecentUnexploredAngles(roomId, limit = 5) {
|
|
18280
|
+
const rows = getDb().prepare(
|
|
18281
|
+
"SELECT id, room_id, round_num, angle, created_at, consumed FROM negative_space WHERE room_id = ? AND consumed = 0 ORDER BY created_at DESC LIMIT ?"
|
|
18282
|
+
).all(roomId, Math.max(1, Math.floor(limit)));
|
|
18283
|
+
return rows.map(mapRow11);
|
|
18284
|
+
}
|
|
18285
|
+
function markAnglesConsumed(angleIds) {
|
|
18286
|
+
if (angleIds.length === 0) return;
|
|
18287
|
+
const db = getDb();
|
|
18288
|
+
const stmt = db.prepare("UPDATE negative_space SET consumed = 1 WHERE id = ?");
|
|
18289
|
+
const tx = db.transaction((ids) => {
|
|
18290
|
+
for (const id of ids) stmt.run(id);
|
|
18291
|
+
});
|
|
18292
|
+
tx(angleIds);
|
|
18293
|
+
}
|
|
18294
|
+
|
|
17500
18295
|
// src/storage/config-events.ts
|
|
17501
18296
|
init_db();
|
|
17502
|
-
function
|
|
18297
|
+
function mapRow12(row) {
|
|
17503
18298
|
return {
|
|
17504
18299
|
id: row.id,
|
|
17505
18300
|
roomId: row.room_id,
|
|
@@ -17513,7 +18308,7 @@ function listConfigEvents(roomId) {
|
|
|
17513
18308
|
const rows = getDb().prepare(
|
|
17514
18309
|
"SELECT id, room_id, kind, payload, actor_kind, created_at FROM config_events WHERE room_id = ? ORDER BY created_at ASC"
|
|
17515
18310
|
).all(roomId);
|
|
17516
|
-
return rows.map(
|
|
18311
|
+
return rows.map(mapRow12);
|
|
17517
18312
|
}
|
|
17518
18313
|
function insertConfigEvent(e) {
|
|
17519
18314
|
const id = newId();
|
|
@@ -17631,6 +18426,382 @@ ${lines.join("\n")}`);
|
|
|
17631
18426
|
].join("\n");
|
|
17632
18427
|
}
|
|
17633
18428
|
|
|
18429
|
+
// src/orchestrator/frame-break.ts
|
|
18430
|
+
function renderForExtraction(messages, maxBodyChars = 360) {
|
|
18431
|
+
const lines = [];
|
|
18432
|
+
for (const m of messages) {
|
|
18433
|
+
if (m.authorKind !== "agent") continue;
|
|
18434
|
+
const meta = m.meta || {};
|
|
18435
|
+
if (meta.kind === "round-open" || meta.kind === "settings" || meta.kind === "round-prompt") continue;
|
|
18436
|
+
const body = (m.body || "").trim();
|
|
18437
|
+
if (!body) continue;
|
|
18438
|
+
lines.push(body.length > maxBodyChars ? body.slice(0, maxBodyChars) + "\u2026" : body);
|
|
18439
|
+
}
|
|
18440
|
+
return lines.join("\n---\n");
|
|
18441
|
+
}
|
|
18442
|
+
async function extractDominantTerms(opts) {
|
|
18443
|
+
const window = opts.windowSize ?? 15;
|
|
18444
|
+
const recent = (opts.messages || []).slice(-window);
|
|
18445
|
+
const directorTurns = recent.filter(
|
|
18446
|
+
(m) => m.authorKind === "agent" && !m.meta?.kind
|
|
18447
|
+
);
|
|
18448
|
+
if (directorTurns.length < 4) return [];
|
|
18449
|
+
const modelV = utilityModelFor();
|
|
18450
|
+
if (!modelV) return [];
|
|
18451
|
+
const transcript = renderForExtraction(directorTurns);
|
|
18452
|
+
if (!transcript.trim()) return [];
|
|
18453
|
+
const prompt = `You are inspecting a multi-director brainstorm for SIGNS OF CONVERGENCE. Read the recent director turns below and identify the noun phrases / concepts that have become the room's recurring fixation \u2014 terms mentioned by multiple directors across multiple turns that are now functioning as the room's gravitational center.
|
|
18454
|
+
|
|
18455
|
+
RULES
|
|
18456
|
+
\xB7 Return ONLY a comma-separated list (no preamble, no JSON, no explanation).
|
|
18457
|
+
\xB7 3-5 terms maximum. Each term \u2264 4 words.
|
|
18458
|
+
\xB7 Prefer the highest-content noun phrases (e.g. "audit responsibility", "compliance burden") over generic words ("AI", "tool", "user").
|
|
18459
|
+
\xB7 If the conversation is genuinely diverse and no single fixation has emerged, return the literal token NONE (no list).
|
|
18460
|
+
\xB7 Match the language of the transcript (Chinese in, Chinese terms out).
|
|
18461
|
+
|
|
18462
|
+
Recent director turns:
|
|
18463
|
+
${transcript}
|
|
18464
|
+
|
|
18465
|
+
Recurring fixation terms (comma-separated, or NONE):`;
|
|
18466
|
+
let body;
|
|
18467
|
+
try {
|
|
18468
|
+
body = await callLLM({
|
|
18469
|
+
modelV,
|
|
18470
|
+
carrier: null,
|
|
18471
|
+
messages: [{ role: "user", content: prompt }],
|
|
18472
|
+
temperature: 0.1,
|
|
18473
|
+
maxTokens: 80
|
|
18474
|
+
});
|
|
18475
|
+
} catch (e) {
|
|
18476
|
+
process.stderr.write(
|
|
18477
|
+
`[frame-break] extract failed: ${e instanceof Error ? e.message : String(e)}
|
|
18478
|
+
`
|
|
18479
|
+
);
|
|
18480
|
+
return [];
|
|
18481
|
+
}
|
|
18482
|
+
const txt = (body || "").trim();
|
|
18483
|
+
if (!txt) return [];
|
|
18484
|
+
if (/^none\b/i.test(txt) || /^无$/.test(txt)) return [];
|
|
18485
|
+
const raw = txt.split(/[,,、]/);
|
|
18486
|
+
const out = [];
|
|
18487
|
+
for (const r of raw) {
|
|
18488
|
+
const trimmed = r.trim().replace(/^["'"'"']+|["'"'"'.。]+$/g, "");
|
|
18489
|
+
if (!trimmed) continue;
|
|
18490
|
+
if (trimmed.length > 60) continue;
|
|
18491
|
+
if (/[\n::]/.test(trimmed)) continue;
|
|
18492
|
+
out.push(trimmed);
|
|
18493
|
+
if (out.length >= 5) break;
|
|
18494
|
+
}
|
|
18495
|
+
return out;
|
|
18496
|
+
}
|
|
18497
|
+
|
|
18498
|
+
// src/storage/qd-archive.ts
|
|
18499
|
+
init_db();
|
|
18500
|
+
var QD_BUCKETS_PER_DIM = 4;
|
|
18501
|
+
var QD_TOTAL_CELLS = QD_BUCKETS_PER_DIM ** 3;
|
|
18502
|
+
function mapRow13(r) {
|
|
18503
|
+
return {
|
|
18504
|
+
messageId: r.message_id,
|
|
18505
|
+
roomId: r.room_id,
|
|
18506
|
+
abstractionBucket: r.abstraction_bucket,
|
|
18507
|
+
timeBucket: r.time_bucket,
|
|
18508
|
+
stakeholderBucket: r.stakeholder_bucket,
|
|
18509
|
+
abstractionScore: r.abstraction_score,
|
|
18510
|
+
timeScore: r.time_score,
|
|
18511
|
+
stakeholderScore: r.stakeholder_score,
|
|
18512
|
+
scoredAt: r.scored_at
|
|
18513
|
+
};
|
|
18514
|
+
}
|
|
18515
|
+
function bucketize(score) {
|
|
18516
|
+
const s = Math.max(0, Math.min(1, score));
|
|
18517
|
+
const b = Math.floor(s * QD_BUCKETS_PER_DIM);
|
|
18518
|
+
return b === QD_BUCKETS_PER_DIM ? QD_BUCKETS_PER_DIM - 1 : b;
|
|
18519
|
+
}
|
|
18520
|
+
function upsertQDScore(opts) {
|
|
18521
|
+
const now = Date.now();
|
|
18522
|
+
const ab = bucketize(opts.scores.abstractionScore);
|
|
18523
|
+
const tb = bucketize(opts.scores.timeScore);
|
|
18524
|
+
const sb = bucketize(opts.scores.stakeholderScore);
|
|
18525
|
+
getDb().prepare(
|
|
18526
|
+
"INSERT OR REPLACE INTO qd_archive (message_id, room_id, abstraction_score, abstraction_bucket, time_score, time_bucket, stakeholder_score, stakeholder_bucket, scored_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
18527
|
+
).run(
|
|
18528
|
+
opts.messageId,
|
|
18529
|
+
opts.roomId,
|
|
18530
|
+
opts.scores.abstractionScore,
|
|
18531
|
+
ab,
|
|
18532
|
+
opts.scores.timeScore,
|
|
18533
|
+
tb,
|
|
18534
|
+
opts.scores.stakeholderScore,
|
|
18535
|
+
sb,
|
|
18536
|
+
now
|
|
18537
|
+
);
|
|
18538
|
+
return {
|
|
18539
|
+
messageId: opts.messageId,
|
|
18540
|
+
roomId: opts.roomId,
|
|
18541
|
+
abstractionBucket: ab,
|
|
18542
|
+
timeBucket: tb,
|
|
18543
|
+
stakeholderBucket: sb,
|
|
18544
|
+
abstractionScore: opts.scores.abstractionScore,
|
|
18545
|
+
timeScore: opts.scores.timeScore,
|
|
18546
|
+
stakeholderScore: opts.scores.stakeholderScore,
|
|
18547
|
+
scoredAt: now
|
|
18548
|
+
};
|
|
18549
|
+
}
|
|
18550
|
+
function listQDForRoom(roomId) {
|
|
18551
|
+
const rows = getDb().prepare(
|
|
18552
|
+
"SELECT message_id, room_id, abstraction_score, abstraction_bucket, time_score, time_bucket, stakeholder_score, stakeholder_bucket, scored_at FROM qd_archive WHERE room_id = ?"
|
|
18553
|
+
).all(roomId);
|
|
18554
|
+
return rows.map(mapRow13);
|
|
18555
|
+
}
|
|
18556
|
+
function filledCellsForRoom(roomId) {
|
|
18557
|
+
const rows = getDb().prepare(
|
|
18558
|
+
"SELECT DISTINCT abstraction_bucket AS ab, time_bucket AS tb, stakeholder_bucket AS sb FROM qd_archive WHERE room_id = ?"
|
|
18559
|
+
).all(roomId);
|
|
18560
|
+
const set = /* @__PURE__ */ new Set();
|
|
18561
|
+
for (const r of rows) {
|
|
18562
|
+
set.add(packCell(r.ab, r.tb, r.sb));
|
|
18563
|
+
}
|
|
18564
|
+
return set;
|
|
18565
|
+
}
|
|
18566
|
+
function packCell(a, t, s) {
|
|
18567
|
+
return a * QD_BUCKETS_PER_DIM * QD_BUCKETS_PER_DIM + t * QD_BUCKETS_PER_DIM + s;
|
|
18568
|
+
}
|
|
18569
|
+
function coverageForRoom(roomId) {
|
|
18570
|
+
const filled = filledCellsForRoom(roomId).size;
|
|
18571
|
+
return {
|
|
18572
|
+
filled,
|
|
18573
|
+
total: QD_TOTAL_CELLS,
|
|
18574
|
+
pct: filled / QD_TOTAL_CELLS
|
|
18575
|
+
};
|
|
18576
|
+
}
|
|
18577
|
+
|
|
18578
|
+
// src/orchestrator/qd-scorer.ts
|
|
18579
|
+
function parseScore(line) {
|
|
18580
|
+
const m = line.match(/(\d+\.\d+|\d+)/);
|
|
18581
|
+
if (!m) return null;
|
|
18582
|
+
let n = parseFloat(m[1]);
|
|
18583
|
+
if (!Number.isFinite(n)) return null;
|
|
18584
|
+
if (n > 1 && n <= 10) n = n / 10;
|
|
18585
|
+
if (n > 1 && n <= 100) n = n / 100;
|
|
18586
|
+
return Math.max(0, Math.min(1, n));
|
|
18587
|
+
}
|
|
18588
|
+
async function scoreAndArchive(opts) {
|
|
18589
|
+
const body = (opts.body || "").trim();
|
|
18590
|
+
if (body.length < 40) return null;
|
|
18591
|
+
const modelV = utilityModelFor();
|
|
18592
|
+
if (!modelV) return null;
|
|
18593
|
+
const prompt = `Rate the director turn below on THREE behavioral dimensions. Each rating is a single floating-point number 0.00-1.00.
|
|
18594
|
+
|
|
18595
|
+
Dimension A \xB7 Abstraction level
|
|
18596
|
+
\xB7 0.00 = concrete example (specific named user, specific product, specific scenario)
|
|
18597
|
+
\xB7 0.33 = case / use-case (representative pattern, named domain)
|
|
18598
|
+
\xB7 0.66 = mechanism / structural argument (how-it-works, conditions)
|
|
18599
|
+
\xB7 1.00 = abstract principle (timeless / cross-domain / first-principles)
|
|
18600
|
+
|
|
18601
|
+
Dimension B \xB7 Time scale
|
|
18602
|
+
\xB7 0.00 = this quarter / immediate (months)
|
|
18603
|
+
\xB7 0.33 = product cycle (1-3 years)
|
|
18604
|
+
\xB7 0.66 = strategic / generational (5-20 years)
|
|
18605
|
+
\xB7 1.00 = civilizational / long-horizon (50+ years, structural)
|
|
18606
|
+
|
|
18607
|
+
Dimension C \xB7 Stakeholder scope
|
|
18608
|
+
\xB7 0.00 = individual user / single role
|
|
18609
|
+
\xB7 0.33 = team / org
|
|
18610
|
+
\xB7 0.66 = industry / market
|
|
18611
|
+
\xB7 1.00 = society / civilization
|
|
18612
|
+
|
|
18613
|
+
OUTPUT \xB7 exactly three lines, one float per line, in order A B C. No labels, no JSON, no commentary.
|
|
18614
|
+
|
|
18615
|
+
Turn:
|
|
18616
|
+
${body.length > 1200 ? body.slice(0, 1200) + "\u2026" : body}
|
|
18617
|
+
|
|
18618
|
+
Three scores (one per line, A then B then C):`;
|
|
18619
|
+
let raw;
|
|
18620
|
+
try {
|
|
18621
|
+
raw = await callLLM({
|
|
18622
|
+
modelV,
|
|
18623
|
+
carrier: null,
|
|
18624
|
+
messages: [{ role: "user", content: prompt }],
|
|
18625
|
+
temperature: 0,
|
|
18626
|
+
maxTokens: 30
|
|
18627
|
+
});
|
|
18628
|
+
} catch (e) {
|
|
18629
|
+
process.stderr.write(
|
|
18630
|
+
`[qd-scorer] LLM call failed: ${e instanceof Error ? e.message : String(e)}
|
|
18631
|
+
`
|
|
18632
|
+
);
|
|
18633
|
+
return null;
|
|
18634
|
+
}
|
|
18635
|
+
const lines = (raw || "").split(/\n/).map((l) => l.trim()).filter(Boolean);
|
|
18636
|
+
if (lines.length < 3) return null;
|
|
18637
|
+
const a = parseScore(lines[0]);
|
|
18638
|
+
const t = parseScore(lines[1]);
|
|
18639
|
+
const s = parseScore(lines[2]);
|
|
18640
|
+
if (a === null || t === null || s === null) return null;
|
|
18641
|
+
const scores = { abstractionScore: a, timeScore: t, stakeholderScore: s };
|
|
18642
|
+
try {
|
|
18643
|
+
upsertQDScore({ messageId: opts.messageId, roomId: opts.roomId, scores });
|
|
18644
|
+
} catch (e) {
|
|
18645
|
+
process.stderr.write(
|
|
18646
|
+
`[qd-scorer] persist failed: ${e instanceof Error ? e.message : String(e)}
|
|
18647
|
+
`
|
|
18648
|
+
);
|
|
18649
|
+
return null;
|
|
18650
|
+
}
|
|
18651
|
+
return scores;
|
|
18652
|
+
}
|
|
18653
|
+
|
|
18654
|
+
// src/storage/topic-branches.ts
|
|
18655
|
+
init_db();
|
|
18656
|
+
function mapBranch(r) {
|
|
18657
|
+
return {
|
|
18658
|
+
id: r.id,
|
|
18659
|
+
roomId: r.room_id,
|
|
18660
|
+
label: r.label,
|
|
18661
|
+
parentId: r.parent_id,
|
|
18662
|
+
openedAt: r.opened_at,
|
|
18663
|
+
turnCount: r.turn_count,
|
|
18664
|
+
lastSpeakerId: r.last_speaker_id
|
|
18665
|
+
};
|
|
18666
|
+
}
|
|
18667
|
+
function listBranchesForRoom(roomId) {
|
|
18668
|
+
const rows = getDb().prepare(
|
|
18669
|
+
"SELECT id, room_id, label, parent_id, opened_at, turn_count, last_speaker_id FROM topic_branches WHERE room_id = ? ORDER BY opened_at ASC"
|
|
18670
|
+
).all(roomId);
|
|
18671
|
+
return rows.map(mapBranch);
|
|
18672
|
+
}
|
|
18673
|
+
function createBranch(opts) {
|
|
18674
|
+
const now = Date.now();
|
|
18675
|
+
const id = newId();
|
|
18676
|
+
const label = opts.label.trim().slice(0, 80);
|
|
18677
|
+
const parentId = opts.parentId || null;
|
|
18678
|
+
const speakerId = opts.openerSpeakerId || null;
|
|
18679
|
+
getDb().prepare(
|
|
18680
|
+
"INSERT INTO topic_branches (id, room_id, label, parent_id, opened_at, turn_count, last_speaker_id) VALUES (?, ?, ?, ?, ?, 1, ?)"
|
|
18681
|
+
).run(id, opts.roomId, label, parentId, now, speakerId);
|
|
18682
|
+
return {
|
|
18683
|
+
id,
|
|
18684
|
+
roomId: opts.roomId,
|
|
18685
|
+
label,
|
|
18686
|
+
parentId,
|
|
18687
|
+
openedAt: now,
|
|
18688
|
+
turnCount: 1,
|
|
18689
|
+
lastSpeakerId: speakerId
|
|
18690
|
+
};
|
|
18691
|
+
}
|
|
18692
|
+
function tagMessageWithBranch(opts) {
|
|
18693
|
+
const now = Date.now();
|
|
18694
|
+
const db = getDb();
|
|
18695
|
+
db.prepare(
|
|
18696
|
+
"INSERT OR REPLACE INTO message_branches (message_id, branch_id, is_opener, tagged_at) VALUES (?, ?, ?, ?)"
|
|
18697
|
+
).run(opts.messageId, opts.branchId, opts.isOpener ? 1 : 0, now);
|
|
18698
|
+
if (!opts.isOpener) {
|
|
18699
|
+
db.prepare(
|
|
18700
|
+
"UPDATE topic_branches SET turn_count = turn_count + 1, last_speaker_id = COALESCE(?, last_speaker_id) WHERE id = ?"
|
|
18701
|
+
).run(opts.speakerId || null, opts.branchId);
|
|
18702
|
+
} else if (opts.speakerId) {
|
|
18703
|
+
db.prepare("UPDATE topic_branches SET last_speaker_id = ? WHERE id = ?").run(opts.speakerId, opts.branchId);
|
|
18704
|
+
}
|
|
18705
|
+
}
|
|
18706
|
+
function speakersOnBranches(roomId, branchIds) {
|
|
18707
|
+
if (branchIds.length === 0) return /* @__PURE__ */ new Set();
|
|
18708
|
+
const placeholders = branchIds.map(() => "?").join(",");
|
|
18709
|
+
const rows = getDb().prepare(
|
|
18710
|
+
`SELECT DISTINCT m.author_id AS author_id
|
|
18711
|
+
FROM messages m
|
|
18712
|
+
JOIN message_branches mb ON mb.message_id = m.id
|
|
18713
|
+
WHERE m.room_id = ?
|
|
18714
|
+
AND mb.branch_id IN (${placeholders})
|
|
18715
|
+
AND m.author_kind = 'agent'
|
|
18716
|
+
AND m.author_id IS NOT NULL`
|
|
18717
|
+
).all(roomId, ...branchIds);
|
|
18718
|
+
return new Set(rows.map((r) => r.author_id));
|
|
18719
|
+
}
|
|
18720
|
+
function dominantBranches(roomId, limit = 3) {
|
|
18721
|
+
const rows = getDb().prepare(
|
|
18722
|
+
"SELECT id, room_id, label, parent_id, opened_at, turn_count, last_speaker_id FROM topic_branches WHERE room_id = ? ORDER BY turn_count DESC, opened_at DESC LIMIT ?"
|
|
18723
|
+
).all(roomId, Math.max(1, Math.floor(limit)));
|
|
18724
|
+
return rows.map(mapBranch);
|
|
18725
|
+
}
|
|
18726
|
+
|
|
18727
|
+
// src/orchestrator/topic-tagger.ts
|
|
18728
|
+
async function tagMessageBranch(opts) {
|
|
18729
|
+
const body = (opts.body || "").trim();
|
|
18730
|
+
if (!body) return null;
|
|
18731
|
+
const modelV = utilityModelFor();
|
|
18732
|
+
if (!modelV) return null;
|
|
18733
|
+
const existing = listBranchesForRoom(opts.roomId);
|
|
18734
|
+
const branchList = existing.length === 0 ? "(none yet \u2014 this is the first branch)" : existing.map((b, i) => `${i + 1}. id=${b.id} \xB7 "${b.label}" \xB7 ${b.turnCount} turn(s)`).join("\n");
|
|
18735
|
+
const prompt = `You are tagging a director's turn in a multi-director brainstorm with the topic branch it belongs to. Branches are short noun-phrase angles the room has been exploring (e.g. "audit responsibility", "informal-economy workers", "ritualised handoff").
|
|
18736
|
+
|
|
18737
|
+
Room subject: "${opts.roomSubject}"
|
|
18738
|
+
|
|
18739
|
+
Existing branches in this room:
|
|
18740
|
+
${branchList}
|
|
18741
|
+
|
|
18742
|
+
Director's turn to tag (verbatim):
|
|
18743
|
+
${body.length > 1200 ? body.slice(0, 1200) + "\u2026" : body}
|
|
18744
|
+
|
|
18745
|
+
Decide ONE of:
|
|
18746
|
+
(A) This turn primarily EXTENDS existing branch X. Output: EXTEND <branch-id>
|
|
18747
|
+
(B) This turn primarily OPENS a NEW branch. Output: NEW <short-label-\u2264-8-words>
|
|
18748
|
+
|
|
18749
|
+
Rules:
|
|
18750
|
+
\xB7 A turn that mostly adds detail / sub-angle to an existing branch is EXTEND.
|
|
18751
|
+
\xB7 A turn that introduces a genuinely fresh lens / domain / stakeholder is NEW.
|
|
18752
|
+
\xB7 When the turn could plausibly go either way, prefer NEW \xB7 the room benefits from more branches.
|
|
18753
|
+
\xB7 The label should be a CONCRETE noun phrase, not a question or full sentence.
|
|
18754
|
+
\xB7 Match the language of the turn for new-branch labels.
|
|
18755
|
+
\xB7 Output ONLY the directive \xB7 no explanation, no JSON, no preamble.`;
|
|
18756
|
+
let raw;
|
|
18757
|
+
try {
|
|
18758
|
+
raw = await callLLM({
|
|
18759
|
+
modelV,
|
|
18760
|
+
carrier: null,
|
|
18761
|
+
messages: [{ role: "user", content: prompt }],
|
|
18762
|
+
temperature: 0.1,
|
|
18763
|
+
maxTokens: 40
|
|
18764
|
+
});
|
|
18765
|
+
} catch (e) {
|
|
18766
|
+
process.stderr.write(
|
|
18767
|
+
`[topic-tagger] failed: ${e instanceof Error ? e.message : String(e)}
|
|
18768
|
+
`
|
|
18769
|
+
);
|
|
18770
|
+
return null;
|
|
18771
|
+
}
|
|
18772
|
+
const txt = (raw || "").trim();
|
|
18773
|
+
if (!txt) return null;
|
|
18774
|
+
const extendMatch = txt.match(/^EXTEND\s+(\S+)/i);
|
|
18775
|
+
if (extendMatch) {
|
|
18776
|
+
const branchId = extendMatch[1];
|
|
18777
|
+
if (existing.some((b) => b.id === branchId)) {
|
|
18778
|
+
tagMessageWithBranch({
|
|
18779
|
+
messageId: opts.messageId,
|
|
18780
|
+
branchId,
|
|
18781
|
+
isOpener: false,
|
|
18782
|
+
speakerId: opts.speakerId
|
|
18783
|
+
});
|
|
18784
|
+
return branchId;
|
|
18785
|
+
}
|
|
18786
|
+
}
|
|
18787
|
+
const newMatch = txt.match(/^NEW\s+(.+)$/i);
|
|
18788
|
+
let label = newMatch ? newMatch[1].trim() : txt.length < 80 ? txt : "";
|
|
18789
|
+
label = label.replace(/^["'`]+|["'`]+$/g, "").replace(/[。.!?]+$/, "").trim();
|
|
18790
|
+
if (!label || label.length > 80) return null;
|
|
18791
|
+
const branch = createBranch({
|
|
18792
|
+
roomId: opts.roomId,
|
|
18793
|
+
label,
|
|
18794
|
+
openerSpeakerId: opts.speakerId
|
|
18795
|
+
});
|
|
18796
|
+
tagMessageWithBranch({
|
|
18797
|
+
messageId: opts.messageId,
|
|
18798
|
+
branchId: branch.id,
|
|
18799
|
+
isOpener: true,
|
|
18800
|
+
speakerId: opts.speakerId
|
|
18801
|
+
});
|
|
18802
|
+
return branch.id;
|
|
18803
|
+
}
|
|
18804
|
+
|
|
17634
18805
|
// src/voice/sentence-splitter.ts
|
|
17635
18806
|
var END_RE = /[。!?!?;;::\n]|[.](?=\s|$)/;
|
|
17636
18807
|
var SentenceChunker = class {
|
|
@@ -18035,6 +19206,9 @@ function ensureState(roomId) {
|
|
|
18035
19206
|
pendingRoundEnd: false,
|
|
18036
19207
|
savedOnPause: null,
|
|
18037
19208
|
pendingChairPick: null,
|
|
19209
|
+
pendingFrameBreakTerms: null,
|
|
19210
|
+
pendingFrameBreakerRole: null,
|
|
19211
|
+
lastFrameBreakerAgentId: null,
|
|
18038
19212
|
billingHaltedThisTurn: false,
|
|
18039
19213
|
voiceWaiters: /* @__PURE__ */ new Map()
|
|
18040
19214
|
};
|
|
@@ -18278,11 +19452,18 @@ function tickRoom(roomId, opts) {
|
|
|
18278
19452
|
if (plan.length === 0) return;
|
|
18279
19453
|
const state = ensureState(roomId);
|
|
18280
19454
|
if (state.inflight) state.inflight.abort();
|
|
19455
|
+
for (const [, waiter] of state.voiceWaiters) {
|
|
19456
|
+
waiter();
|
|
19457
|
+
}
|
|
19458
|
+
state.voiceWaiters.clear();
|
|
18281
19459
|
state.queue = plan.map((a) => ({ agentId: a.id, status: "queued" }));
|
|
18282
19460
|
state.roundNum = opts.roundNum;
|
|
18283
19461
|
state.speakersThisTurn = 0;
|
|
18284
19462
|
state.savedOnPause = null;
|
|
18285
19463
|
state.pendingChairPick = null;
|
|
19464
|
+
state.pendingFrameBreakTerms = null;
|
|
19465
|
+
state.pendingFrameBreakerRole = null;
|
|
19466
|
+
state.lastFrameBreakerAgentId = null;
|
|
18286
19467
|
state.billingHaltedThisTurn = false;
|
|
18287
19468
|
state.maxSpeakersThisTurn = plan.length;
|
|
18288
19469
|
emitQueueUpdate(roomId, state);
|
|
@@ -18345,11 +19526,77 @@ async function pumpQueue(roomId) {
|
|
|
18345
19526
|
try {
|
|
18346
19527
|
emitChairPending(roomId, "next-speaker");
|
|
18347
19528
|
const pickRoom = getRoom(roomId);
|
|
19529
|
+
let convergentTerms = [];
|
|
19530
|
+
try {
|
|
19531
|
+
convergentTerms = await extractDominantTerms({ messages: recent });
|
|
19532
|
+
} catch {
|
|
19533
|
+
}
|
|
19534
|
+
try {
|
|
19535
|
+
const branches = dominantBranches(roomId, 3);
|
|
19536
|
+
if (branches.length > 0) {
|
|
19537
|
+
const seen = new Set(convergentTerms.map((t) => t.toLowerCase()));
|
|
19538
|
+
for (const b of branches) {
|
|
19539
|
+
if (b.turnCount >= 2 && !seen.has(b.label.toLowerCase())) {
|
|
19540
|
+
convergentTerms.push(b.label);
|
|
19541
|
+
seen.add(b.label.toLowerCase());
|
|
19542
|
+
}
|
|
19543
|
+
}
|
|
19544
|
+
}
|
|
19545
|
+
} catch {
|
|
19546
|
+
}
|
|
19547
|
+
const useDissentMode = convergentTerms.length > 0;
|
|
19548
|
+
if (useDissentMode) {
|
|
19549
|
+
state.pendingFrameBreakTerms = convergentTerms.slice();
|
|
19550
|
+
rlog(roomId, "divergence-detect", {
|
|
19551
|
+
terms: convergentTerms,
|
|
19552
|
+
pickerMode: "dissent-gap"
|
|
19553
|
+
});
|
|
19554
|
+
}
|
|
19555
|
+
let pickerCandidates = candidates;
|
|
19556
|
+
if (useDissentMode) {
|
|
19557
|
+
try {
|
|
19558
|
+
const branches = dominantBranches(roomId, 3);
|
|
19559
|
+
const dominantBranchIds = branches.map((b) => b.id);
|
|
19560
|
+
if (dominantBranchIds.length > 0) {
|
|
19561
|
+
const exposed = speakersOnBranches(roomId, dominantBranchIds);
|
|
19562
|
+
const underexposed = candidates.filter((c) => !exposed.has(c.id));
|
|
19563
|
+
const overexposed = candidates.filter((c) => exposed.has(c.id));
|
|
19564
|
+
if (underexposed.length > 0 && overexposed.length > 0) {
|
|
19565
|
+
pickerCandidates = [...underexposed, ...overexposed];
|
|
19566
|
+
}
|
|
19567
|
+
}
|
|
19568
|
+
} catch {
|
|
19569
|
+
}
|
|
19570
|
+
}
|
|
18348
19571
|
const pick = await pickNextSpeaker({
|
|
18349
|
-
candidates,
|
|
19572
|
+
candidates: pickerCandidates,
|
|
18350
19573
|
history: recent,
|
|
18351
|
-
room: pickRoom ?? void 0
|
|
19574
|
+
room: pickRoom ?? void 0,
|
|
19575
|
+
mode: useDissentMode ? "dissent-gap" : "lens-gap",
|
|
19576
|
+
convergentTerms: useDissentMode ? convergentTerms : void 0
|
|
18352
19577
|
});
|
|
19578
|
+
if (useDissentMode && convergentTerms.length > 0) {
|
|
19579
|
+
const lastBreaker = state.lastFrameBreakerAgentId;
|
|
19580
|
+
const chosenId = pick.agentId ?? state.queue[0]?.agentId;
|
|
19581
|
+
let breakerId = null;
|
|
19582
|
+
if (chosenId && chosenId !== lastBreaker) {
|
|
19583
|
+
breakerId = chosenId;
|
|
19584
|
+
} else {
|
|
19585
|
+
const alt = candidates.find((c) => c.id !== lastBreaker && c.id !== chosenId);
|
|
19586
|
+
if (alt) breakerId = alt.id;
|
|
19587
|
+
}
|
|
19588
|
+
if (breakerId) {
|
|
19589
|
+
const frameLabel = (convergentTerms[0] || "").slice(0, 60);
|
|
19590
|
+
state.pendingFrameBreakerRole = {
|
|
19591
|
+
agentId: breakerId,
|
|
19592
|
+
convergentFrame: frameLabel
|
|
19593
|
+
};
|
|
19594
|
+
rlog(roomId, "frame-breaker-assign", {
|
|
19595
|
+
agent: getAgent(breakerId)?.name ?? breakerId,
|
|
19596
|
+
frame: frameLabel
|
|
19597
|
+
});
|
|
19598
|
+
}
|
|
19599
|
+
}
|
|
18353
19600
|
const stillSameQueue = state.queue.length === queueSnapshot.length && state.queue.every((q, i) => q.agentId === queueSnapshot[i].agentId);
|
|
18354
19601
|
if (stillSameQueue) {
|
|
18355
19602
|
if (pick.agentId && pick.agentId !== state.queue[0].agentId) {
|
|
@@ -18707,6 +19954,46 @@ async function streamSpeakerTurn(args) {
|
|
|
18707
19954
|
});
|
|
18708
19955
|
}
|
|
18709
19956
|
}
|
|
19957
|
+
const tStateForTurn = ensureState(roomId);
|
|
19958
|
+
let frameBreakTerms;
|
|
19959
|
+
if (tStateForTurn.pendingFrameBreakTerms && tStateForTurn.pendingFrameBreakTerms.length > 0) {
|
|
19960
|
+
frameBreakTerms = tStateForTurn.pendingFrameBreakTerms.slice();
|
|
19961
|
+
} else if (roundNum > 1 && history.length >= 4) {
|
|
19962
|
+
try {
|
|
19963
|
+
frameBreakTerms = await extractDominantTerms({ messages: history });
|
|
19964
|
+
if (frameBreakTerms.length > 0) {
|
|
19965
|
+
rlog(roomId, "frame-break-extract", {
|
|
19966
|
+
round: roundNum,
|
|
19967
|
+
speaker: speaker.name,
|
|
19968
|
+
terms: frameBreakTerms,
|
|
19969
|
+
via: "stream-fallback"
|
|
19970
|
+
});
|
|
19971
|
+
tStateForTurn.pendingFrameBreakTerms = frameBreakTerms.slice();
|
|
19972
|
+
}
|
|
19973
|
+
} catch {
|
|
19974
|
+
}
|
|
19975
|
+
}
|
|
19976
|
+
let frameBreakerRole;
|
|
19977
|
+
if (tStateForTurn.pendingFrameBreakerRole && tStateForTurn.pendingFrameBreakerRole.agentId === speaker.id) {
|
|
19978
|
+
frameBreakerRole = { convergentFrame: tStateForTurn.pendingFrameBreakerRole.convergentFrame };
|
|
19979
|
+
tStateForTurn.lastFrameBreakerAgentId = speaker.id;
|
|
19980
|
+
tStateForTurn.pendingFrameBreakerRole = null;
|
|
19981
|
+
}
|
|
19982
|
+
let unexploredAngles;
|
|
19983
|
+
if (roundNum > 1) {
|
|
19984
|
+
try {
|
|
19985
|
+
const rows = getRecentUnexploredAngles(roomId, 3);
|
|
19986
|
+
if (rows.length > 0) {
|
|
19987
|
+
unexploredAngles = rows.map((r) => r.angle);
|
|
19988
|
+
markAnglesConsumed(rows.map((r) => r.id));
|
|
19989
|
+
}
|
|
19990
|
+
} catch (e) {
|
|
19991
|
+
process.stderr.write(
|
|
19992
|
+
`[room] unexplored-angles read failed: ${e instanceof Error ? e.message : String(e)}
|
|
19993
|
+
`
|
|
19994
|
+
);
|
|
19995
|
+
}
|
|
19996
|
+
}
|
|
18710
19997
|
const llmMessages = buildDirectorMessages({
|
|
18711
19998
|
speaker,
|
|
18712
19999
|
cast,
|
|
@@ -18719,6 +20006,9 @@ async function streamSpeakerTurn(args) {
|
|
|
18719
20006
|
chairBrief: chairBriefForTurn ?? void 0,
|
|
18720
20007
|
summaryPreamble,
|
|
18721
20008
|
priorContext,
|
|
20009
|
+
frameBreakTerms,
|
|
20010
|
+
frameBreakerRole,
|
|
20011
|
+
unexploredAngles,
|
|
18722
20012
|
deliveryMode: room.deliveryMode
|
|
18723
20013
|
});
|
|
18724
20014
|
const placeholderMeta = {
|
|
@@ -18763,10 +20053,23 @@ async function streamSpeakerTurn(args) {
|
|
|
18763
20053
|
process.stderr.write(`[voice-debug] room=${roomId} deliveryMode="${room.deliveryMode}" voiceMode=${voiceMode}
|
|
18764
20054
|
`);
|
|
18765
20055
|
const voiceChunker = voiceMode ? new SentenceChunker({ maxChars: 120 }) : null;
|
|
18766
|
-
const
|
|
20056
|
+
const initialVoiceProfile = voiceMode ? voiceProfileForAgent(speaker) : null;
|
|
18767
20057
|
let voiceSeq = 0;
|
|
20058
|
+
function currentVoiceProfile() {
|
|
20059
|
+
if (!voiceMode) return null;
|
|
20060
|
+
try {
|
|
20061
|
+
const fresh = getAgent(speaker.id);
|
|
20062
|
+
if (fresh) return voiceProfileForAgent(fresh);
|
|
20063
|
+
} catch (e) {
|
|
20064
|
+
process.stderr.write(`[tts] currentVoiceProfile read failed: ${e instanceof Error ? e.message : String(e)}
|
|
20065
|
+
`);
|
|
20066
|
+
}
|
|
20067
|
+
return initialVoiceProfile;
|
|
20068
|
+
}
|
|
18768
20069
|
async function emitVoiceText(text) {
|
|
18769
|
-
if (!voiceMode || !
|
|
20070
|
+
if (!voiceMode || !text.trim()) return;
|
|
20071
|
+
const voiceProfile = currentVoiceProfile();
|
|
20072
|
+
if (!voiceProfile) return;
|
|
18770
20073
|
process.stderr.write(`[tts] emitVoiceText called: provider=${voiceProfile.provider} voiceId=${voiceProfile.voiceId} textLen=${text.length} text="${text.slice(0, 50)}"
|
|
18771
20074
|
`);
|
|
18772
20075
|
let chunkCount = 0;
|
|
@@ -18789,8 +20092,10 @@ async function streamSpeakerTurn(args) {
|
|
|
18789
20092
|
process.stderr.write(`[tts] emitVoiceText done: ${chunkCount} chunks emitted
|
|
18790
20093
|
`);
|
|
18791
20094
|
} catch (e) {
|
|
18792
|
-
process.stderr.write(
|
|
18793
|
-
`)
|
|
20095
|
+
process.stderr.write(
|
|
20096
|
+
`[tts] ERROR room=${roomId} agent=${speaker.name} provider=${voiceProfile.provider} voiceId=${voiceProfile.voiceId} \xB7 ${e instanceof Error ? e.stack || e.message : String(e)}
|
|
20097
|
+
`
|
|
20098
|
+
);
|
|
18794
20099
|
}
|
|
18795
20100
|
}
|
|
18796
20101
|
try {
|
|
@@ -18941,6 +20246,38 @@ async function streamSpeakerTurn(args) {
|
|
|
18941
20246
|
messageId: placeholder.id,
|
|
18942
20247
|
finishReason
|
|
18943
20248
|
});
|
|
20249
|
+
if (buf.trim().length >= 40) {
|
|
20250
|
+
void (async () => {
|
|
20251
|
+
try {
|
|
20252
|
+
await tagMessageBranch({
|
|
20253
|
+
roomId,
|
|
20254
|
+
messageId: placeholder.id,
|
|
20255
|
+
speakerId: speaker.id,
|
|
20256
|
+
body: buf,
|
|
20257
|
+
roomSubject: room.subject || ""
|
|
20258
|
+
});
|
|
20259
|
+
} catch (e) {
|
|
20260
|
+
process.stderr.write(
|
|
20261
|
+
`[room] topic-tag failed: ${e instanceof Error ? e.message : String(e)}
|
|
20262
|
+
`
|
|
20263
|
+
);
|
|
20264
|
+
}
|
|
20265
|
+
})();
|
|
20266
|
+
void (async () => {
|
|
20267
|
+
try {
|
|
20268
|
+
await scoreAndArchive({
|
|
20269
|
+
roomId,
|
|
20270
|
+
messageId: placeholder.id,
|
|
20271
|
+
body: buf
|
|
20272
|
+
});
|
|
20273
|
+
} catch (e) {
|
|
20274
|
+
process.stderr.write(
|
|
20275
|
+
`[room] qd-score failed: ${e instanceof Error ? e.message : String(e)}
|
|
20276
|
+
`
|
|
20277
|
+
);
|
|
20278
|
+
}
|
|
20279
|
+
})();
|
|
20280
|
+
}
|
|
18944
20281
|
}
|
|
18945
20282
|
return placeholder.id;
|
|
18946
20283
|
}
|
|
@@ -19747,6 +21084,27 @@ async function runChairRoundEnd(roomId, roundNum) {
|
|
|
19747
21084
|
}
|
|
19748
21085
|
});
|
|
19749
21086
|
void runRoundEndSummarization(roomId, roundNum);
|
|
21087
|
+
void (async () => {
|
|
21088
|
+
try {
|
|
21089
|
+
const room = getRoom(roomId);
|
|
21090
|
+
if (!room) return;
|
|
21091
|
+
const allMsgs = listMessages(roomId);
|
|
21092
|
+
const roundMsgs = allMsgs.filter((m) => m.roundNum === roundNum);
|
|
21093
|
+
if (roundMsgs.length === 0) return;
|
|
21094
|
+
const angles = await extractNegativeSpace({
|
|
21095
|
+
roundMessages: roundMsgs,
|
|
21096
|
+
roomSubject: room.subject || ""
|
|
21097
|
+
});
|
|
21098
|
+
if (angles.length > 0) {
|
|
21099
|
+
insertNegativeSpaceAngles(roomId, roundNum, angles);
|
|
21100
|
+
}
|
|
21101
|
+
} catch (e) {
|
|
21102
|
+
process.stderr.write(
|
|
21103
|
+
`[chair] negative-space extract failed: ${e instanceof Error ? e.message : String(e)}
|
|
21104
|
+
`
|
|
21105
|
+
);
|
|
21106
|
+
}
|
|
21107
|
+
})();
|
|
19750
21108
|
}
|
|
19751
21109
|
async function emitChairAnnouncementVoice(roomId, messageId, body) {
|
|
19752
21110
|
const room = getRoom(roomId);
|
|
@@ -20026,7 +21384,7 @@ function announceRoundOpen(roomId, roundNum, opening) {
|
|
|
20026
21384
|
function announceAdjournNoBrief(roomId) {
|
|
20027
21385
|
const chair = getChairAgent();
|
|
20028
21386
|
if (!chair) return;
|
|
20029
|
-
const body = "
|
|
21387
|
+
const body = "Session adjourned without a report. If you'd like one later, use *Generate report* in the room header.";
|
|
20030
21388
|
const m = insertMessage({
|
|
20031
21389
|
roomId,
|
|
20032
21390
|
authorKind: "agent",
|
|
@@ -21378,6 +22736,42 @@ function roomsRouter() {
|
|
|
21378
22736
|
if (!brief) return c.json({ error: "brief not yet generated" }, 404);
|
|
21379
22737
|
return c.json({ ...brief, isGenerating: isBriefGenerating(brief.id) });
|
|
21380
22738
|
});
|
|
22739
|
+
r.get("/:id/diversity", (c) => {
|
|
22740
|
+
const id = c.req.param("id");
|
|
22741
|
+
if (!getRoom(id)) return c.json({ error: "not found" }, 404);
|
|
22742
|
+
const branches = listBranchesForRoom(id).slice().sort((a, b) => b.turnCount - a.turnCount || a.openedAt - b.openedAt).map((b) => ({
|
|
22743
|
+
id: b.id,
|
|
22744
|
+
label: b.label,
|
|
22745
|
+
parentId: b.parentId,
|
|
22746
|
+
openedAt: b.openedAt,
|
|
22747
|
+
turnCount: b.turnCount,
|
|
22748
|
+
lastSpeakerId: b.lastSpeakerId
|
|
22749
|
+
}));
|
|
22750
|
+
const coverage = coverageForRoom(id);
|
|
22751
|
+
const qdRows = listQDForRoom(id);
|
|
22752
|
+
const buckets = {
|
|
22753
|
+
abstraction: [0, 0, 0, 0],
|
|
22754
|
+
time: [0, 0, 0, 0],
|
|
22755
|
+
stakeholder: [0, 0, 0, 0]
|
|
22756
|
+
};
|
|
22757
|
+
for (const r2 of qdRows) {
|
|
22758
|
+
buckets.abstraction[r2.abstractionBucket] += 1;
|
|
22759
|
+
buckets.time[r2.timeBucket] += 1;
|
|
22760
|
+
buckets.stakeholder[r2.stakeholderBucket] += 1;
|
|
22761
|
+
}
|
|
22762
|
+
const unexplored = getRecentUnexploredAngles(id, 5).map((a) => ({
|
|
22763
|
+
id: a.id,
|
|
22764
|
+
angle: a.angle,
|
|
22765
|
+
roundNum: a.roundNum
|
|
22766
|
+
}));
|
|
22767
|
+
return c.json({
|
|
22768
|
+
branches,
|
|
22769
|
+
coverage,
|
|
22770
|
+
buckets,
|
|
22771
|
+
messagesScored: qdRows.length,
|
|
22772
|
+
unexplored
|
|
22773
|
+
});
|
|
22774
|
+
});
|
|
21381
22775
|
r.get("/:id/briefs", (c) => {
|
|
21382
22776
|
const id = c.req.param("id");
|
|
21383
22777
|
if (!getRoom(id)) return c.json({ error: "not found" }, 404);
|
|
@@ -21739,7 +23133,7 @@ function voicesRouter() {
|
|
|
21739
23133
|
init_paths();
|
|
21740
23134
|
|
|
21741
23135
|
// src/version.ts
|
|
21742
|
-
var VERSION = "0.1.
|
|
23136
|
+
var VERSION = "0.1.16";
|
|
21743
23137
|
|
|
21744
23138
|
// src/server.ts
|
|
21745
23139
|
function createApp() {
|