ai-advisory-board 0.3.0 → 0.4.0
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/CHANGELOG.md +12 -0
- package/dist/bin/aab.js +164 -5
- package/dist/bin/aab.js.map +1 -1
- package/gui/app.js +113 -22
- package/gui/style.css +54 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# CHANGELOG — AI Advisory Board CLI
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 81661f9: Action items now capture the discussion context they were born from. When you add an action step to the board (UI, the extract route, or `aab actions add --discussion`), the server snapshots a `sourceContext` onto the item: the original board question, the suggesting member's name + title/expertise, their actual reasoning behind the step, their related key points, and the round number. Previously an action carried only its one-line title plus "Suggested by <member>".
|
|
8
|
+
|
|
9
|
+
The Skill Planner now receives this as a dedicated, self-describing `<source_context>` prompt block (authoritative statement of intent — it designs the skill for what the advisor _meant_, not just the literal title), and the skill-creator brief carries it through as `action.sourceContext`. In the web UI, action cards show a "↩ From discussion" link and the edit modal gains a read-only **Source** section (question, member, reasoning, key points) that deep-links back to the originating discussion.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- ad82fa0: fix(ui): live discussion view no longer shows false "No response" after a streaming glitch. When a live `member_response` event failed to match its typing bubble, the view left a perpetual-"No response" orphan in place and appended a misplaced duplicate of the real answer further down. `finalizeChat` now rebuilds the chat stream from the authoritative discussion record the server sends with `discussion_completed` / `discussion_gated`, so the live view matches exactly what a page reload renders.
|
|
14
|
+
|
|
3
15
|
## 0.3.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/dist/bin/aab.js
CHANGED
|
@@ -857,6 +857,13 @@ ${OUTPUT_CONTRACT}
|
|
|
857
857
|
|
|
858
858
|
<input>
|
|
859
859
|
<action>${opts.actionItemJson}</action>
|
|
860
|
+
<source_context>
|
|
861
|
+
The action title above is a one-line distillation. THIS block is the authoritative
|
|
862
|
+
statement of intent: it carries the advisor's actual reasoning and the question the
|
|
863
|
+
board was answering. When the title is terse or ambiguous, resolve it against this
|
|
864
|
+
context \u2014 design the skill for what the advisor MEANT, not just the literal title.
|
|
865
|
+
${opts.sourceContext ?? "(no discussion provenance \u2014 this action was added manually)"}
|
|
866
|
+
</source_context>
|
|
860
867
|
<linked_discussion_summary>${opts.discussionSummary ?? ""}</linked_discussion_summary>
|
|
861
868
|
<recon>
|
|
862
869
|
<pc_scan>${opts.reconResultJson}</pc_scan>
|
|
@@ -1155,6 +1162,7 @@ async function runPlanner(opts) {
|
|
|
1155
1162
|
null,
|
|
1156
1163
|
2
|
|
1157
1164
|
);
|
|
1165
|
+
const sourceContextText = opts.action.sourceContext ? buildSourceContextString(opts.action.sourceContext) : "";
|
|
1158
1166
|
const discussionSummaryText = opts.discussionSummary ? buildDiscussionSummaryString(opts.discussionSummary) : "";
|
|
1159
1167
|
const reconResultJson = JSON.stringify(summarizeRecon(opts.recon), null, 2);
|
|
1160
1168
|
let attempts = 0;
|
|
@@ -1165,6 +1173,7 @@ async function runPlanner(opts) {
|
|
|
1165
1173
|
attempts++;
|
|
1166
1174
|
const prompt2 = renderSkillPlannerPrompt({
|
|
1167
1175
|
actionItemJson,
|
|
1176
|
+
sourceContext: sourceContextText,
|
|
1168
1177
|
discussionSummary: discussionSummaryText,
|
|
1169
1178
|
reconResultJson,
|
|
1170
1179
|
wikiContextJson: JSON.stringify(opts.recon.wiki, null, 2),
|
|
@@ -1273,6 +1282,29 @@ function buildDiscussionSummaryString(summary) {
|
|
|
1273
1282
|
if (summary.actionableInsights?.length) parts.push(`insights: ${summary.actionableInsights.slice(0, 4).join(" | ")}`);
|
|
1274
1283
|
return parts.join("\n").slice(0, 4e3);
|
|
1275
1284
|
}
|
|
1285
|
+
function buildSourceContextString(sc) {
|
|
1286
|
+
if (!sc) return "";
|
|
1287
|
+
const lines = [];
|
|
1288
|
+
const who = [sc.memberName, sc.memberTitle].filter(Boolean).join(", ");
|
|
1289
|
+
if (who || typeof sc.roundNumber === "number") {
|
|
1290
|
+
const round = typeof sc.roundNumber === "number" ? ` (advisory round ${sc.roundNumber})` : "";
|
|
1291
|
+
lines.push(`This action was proposed by ${who || "an advisory board member"}${round}.`);
|
|
1292
|
+
}
|
|
1293
|
+
if (sc.discussionQuestion?.trim()) {
|
|
1294
|
+
lines.push(`The board was convened on this question: "${sc.discussionQuestion.trim()}"`);
|
|
1295
|
+
}
|
|
1296
|
+
if (sc.memberReasoning?.trim()) {
|
|
1297
|
+
lines.push(`The advisor's reasoning behind this action:
|
|
1298
|
+
${sc.memberReasoning.trim()}`);
|
|
1299
|
+
}
|
|
1300
|
+
if (sc.relatedKeyPoints?.length) {
|
|
1301
|
+
lines.push(
|
|
1302
|
+
`The advisor's related key points:
|
|
1303
|
+
${sc.relatedKeyPoints.slice(0, 6).map((p) => `- ${p}`).join("\n")}`
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
return lines.join("\n\n").slice(0, 6e3);
|
|
1307
|
+
}
|
|
1276
1308
|
function summarizeRecon(recon) {
|
|
1277
1309
|
return {
|
|
1278
1310
|
platform: recon.pc.platform,
|
|
@@ -9198,7 +9230,7 @@ function dedupeByTitle(items) {
|
|
|
9198
9230
|
}
|
|
9199
9231
|
function buildAnalysisPrompt(discussion) {
|
|
9200
9232
|
const PER_RESPONSE_CAP = 4e3;
|
|
9201
|
-
const
|
|
9233
|
+
const allResponses2 = discussion.responses.map((r) => {
|
|
9202
9234
|
const preview = r.content.length > PER_RESPONSE_CAP ? `${r.content.slice(0, PER_RESPONSE_CAP)}
|
|
9203
9235
|
|
|
9204
9236
|
[\u2026truncated for extraction\u2026]` : r.content;
|
|
@@ -9219,7 +9251,7 @@ ${preview}`;
|
|
|
9219
9251
|
"",
|
|
9220
9252
|
summaryContext,
|
|
9221
9253
|
"FULL CONVERSATION:",
|
|
9222
|
-
|
|
9254
|
+
allResponses2 || "(no responses)",
|
|
9223
9255
|
"",
|
|
9224
9256
|
"INSTRUCTIONS:",
|
|
9225
9257
|
"1. Extract SPECIFIC, ACTIONABLE items only \u2014 skip vague suggestions.",
|
|
@@ -9309,6 +9341,93 @@ function fallbackResult(discussion, processingTimeMs) {
|
|
|
9309
9341
|
};
|
|
9310
9342
|
}
|
|
9311
9343
|
|
|
9344
|
+
// src/core/actions/source-context.ts
|
|
9345
|
+
var MAX_REASONING_CHARS = 1500;
|
|
9346
|
+
var MAX_KEY_POINTS = 6;
|
|
9347
|
+
function buildSourceContext(discussion, members, opts) {
|
|
9348
|
+
const response = pickResponse(discussion, opts);
|
|
9349
|
+
const member = findMember(members, {
|
|
9350
|
+
memberId: opts.memberId ?? response?.memberId,
|
|
9351
|
+
memberName: opts.memberName ?? response?.memberName
|
|
9352
|
+
});
|
|
9353
|
+
const ctx = {};
|
|
9354
|
+
const question = discussion.question?.trim();
|
|
9355
|
+
if (question) ctx.discussionQuestion = question;
|
|
9356
|
+
const memberId = response?.memberId ?? member?.id ?? opts.memberId;
|
|
9357
|
+
if (memberId) ctx.memberId = memberId;
|
|
9358
|
+
const memberName = response?.memberName ?? member?.name ?? opts.memberName;
|
|
9359
|
+
if (memberName) ctx.memberName = memberName;
|
|
9360
|
+
const memberTitle = buildMemberTitle(member);
|
|
9361
|
+
if (memberTitle) ctx.memberTitle = memberTitle;
|
|
9362
|
+
if (response) {
|
|
9363
|
+
const reasoning = response.content?.trim();
|
|
9364
|
+
if (reasoning) {
|
|
9365
|
+
ctx.memberReasoning = reasoning.length > MAX_REASONING_CHARS ? reasoning.slice(0, MAX_REASONING_CHARS).trimEnd() + "\u2026" : reasoning;
|
|
9366
|
+
}
|
|
9367
|
+
const keyPoints = response.structuredData?.keyPoints?.filter((p) => p && p.trim());
|
|
9368
|
+
if (keyPoints && keyPoints.length) {
|
|
9369
|
+
ctx.relatedKeyPoints = keyPoints.slice(0, MAX_KEY_POINTS);
|
|
9370
|
+
}
|
|
9371
|
+
if (typeof response.roundNumber === "number") ctx.roundNumber = response.roundNumber;
|
|
9372
|
+
}
|
|
9373
|
+
return Object.keys(ctx).length > 0 ? ctx : void 0;
|
|
9374
|
+
}
|
|
9375
|
+
function allResponses(discussion) {
|
|
9376
|
+
if (discussion.responses && discussion.responses.length > 0) return discussion.responses;
|
|
9377
|
+
return (discussion.rounds ?? []).flatMap((r) => r.responses ?? []);
|
|
9378
|
+
}
|
|
9379
|
+
function pickResponse(discussion, opts) {
|
|
9380
|
+
const responses = allResponses(discussion);
|
|
9381
|
+
if (responses.length === 0) return void 0;
|
|
9382
|
+
const hasMemberMatcher = Boolean(opts.memberId || opts.memberName);
|
|
9383
|
+
const byMember = responses.filter((r) => {
|
|
9384
|
+
if (opts.memberId && r.memberId === opts.memberId) return true;
|
|
9385
|
+
if (opts.memberName && r.memberName === opts.memberName) return true;
|
|
9386
|
+
return false;
|
|
9387
|
+
});
|
|
9388
|
+
const pool = hasMemberMatcher ? byMember : responses;
|
|
9389
|
+
if (pool.length === 0) return void 0;
|
|
9390
|
+
const wantStep = normalize(opts.stepText);
|
|
9391
|
+
if (wantStep) {
|
|
9392
|
+
const exact = pool.find(
|
|
9393
|
+
(r) => (r.structuredData?.actionSteps ?? []).some((s) => stepsMatch(normalize(s), wantStep))
|
|
9394
|
+
);
|
|
9395
|
+
if (exact) return exact;
|
|
9396
|
+
}
|
|
9397
|
+
return hasMemberMatcher ? [...pool].sort(latestFirst)[0] : void 0;
|
|
9398
|
+
}
|
|
9399
|
+
function latestFirst(a, b) {
|
|
9400
|
+
if ((b.roundNumber ?? 0) !== (a.roundNumber ?? 0)) return (b.roundNumber ?? 0) - (a.roundNumber ?? 0);
|
|
9401
|
+
if ((b.turnNumber ?? 0) !== (a.turnNumber ?? 0)) return (b.turnNumber ?? 0) - (a.turnNumber ?? 0);
|
|
9402
|
+
return (b.order ?? 0) - (a.order ?? 0);
|
|
9403
|
+
}
|
|
9404
|
+
function findMember(members, by) {
|
|
9405
|
+
if (by.memberId) {
|
|
9406
|
+
const m = members.find((x) => x.id === by.memberId);
|
|
9407
|
+
if (m) return m;
|
|
9408
|
+
}
|
|
9409
|
+
if (by.memberName) return members.find((x) => x.name === by.memberName);
|
|
9410
|
+
return void 0;
|
|
9411
|
+
}
|
|
9412
|
+
function buildMemberTitle(member) {
|
|
9413
|
+
if (!member) return void 0;
|
|
9414
|
+
const title = member.title?.trim();
|
|
9415
|
+
const expertise = (member.expertise ?? []).filter((e) => e && e.trim()).slice(0, 3);
|
|
9416
|
+
if (title && expertise.length) return `${title} \xB7 ${expertise.join(", ")}`;
|
|
9417
|
+
if (title) return title;
|
|
9418
|
+
if (expertise.length) return expertise.join(", ");
|
|
9419
|
+
return void 0;
|
|
9420
|
+
}
|
|
9421
|
+
function normalize(s) {
|
|
9422
|
+
return (s ?? "").toLowerCase().replace(/\s+/g, " ").replace(/[^a-z0-9 ]/g, "").trim();
|
|
9423
|
+
}
|
|
9424
|
+
function stepsMatch(a, b) {
|
|
9425
|
+
if (!a || !b) return false;
|
|
9426
|
+
if (a === b) return true;
|
|
9427
|
+
const [short, long] = a.length <= b.length ? [a, b] : [b, a];
|
|
9428
|
+
return long.startsWith(short) && short.length >= 12;
|
|
9429
|
+
}
|
|
9430
|
+
|
|
9312
9431
|
// src/core/members/ai-enhancer.ts
|
|
9313
9432
|
init_claude_code_runner();
|
|
9314
9433
|
init_safe_json();
|
|
@@ -10956,6 +11075,7 @@ function buildSkillCreatorBrief(opts) {
|
|
|
10956
11075
|
title: opts.action.title,
|
|
10957
11076
|
description: opts.action.description,
|
|
10958
11077
|
priority: opts.action.priority,
|
|
11078
|
+
...opts.action.sourceContext ? { sourceContext: opts.action.sourceContext } : {},
|
|
10959
11079
|
...opts.action.discussionId && opts.discussionSummary ? { linkedDiscussion: { id: opts.action.discussionId, summary: opts.discussionSummary } } : {}
|
|
10960
11080
|
},
|
|
10961
11081
|
skillPlannerProposal: cp.proposal,
|
|
@@ -12180,6 +12300,24 @@ function coerceActionStatus(value, fallback = "pending") {
|
|
|
12180
12300
|
}
|
|
12181
12301
|
return fallback;
|
|
12182
12302
|
}
|
|
12303
|
+
async function resolveSourceContext(storage, args) {
|
|
12304
|
+
try {
|
|
12305
|
+
const discussion = await storage.loadDiscussionById(args.discussionId);
|
|
12306
|
+
if (!discussion) return void 0;
|
|
12307
|
+
const members = await storage.loadBoardMembers();
|
|
12308
|
+
return buildSourceContext(discussion, members, {
|
|
12309
|
+
memberId: args.memberId,
|
|
12310
|
+
memberName: args.memberName,
|
|
12311
|
+
stepText: args.title
|
|
12312
|
+
});
|
|
12313
|
+
} catch (err) {
|
|
12314
|
+
logger.debug("[actions] sourceContext resolution failed", {
|
|
12315
|
+
discussionId: args.discussionId,
|
|
12316
|
+
error: err instanceof Error ? err.message : String(err)
|
|
12317
|
+
});
|
|
12318
|
+
return void 0;
|
|
12319
|
+
}
|
|
12320
|
+
}
|
|
12183
12321
|
var DEFAULT_PORT = 3737;
|
|
12184
12322
|
async function startUiServer(opts) {
|
|
12185
12323
|
const port = opts.port ?? DEFAULT_PORT;
|
|
@@ -12308,15 +12446,24 @@ async function startUiServer(opts) {
|
|
|
12308
12446
|
const now = nowIso();
|
|
12309
12447
|
const priority = coerceActionPriority(body.priority);
|
|
12310
12448
|
const status = coerceActionStatus(body.status);
|
|
12449
|
+
const title = body.title.trim();
|
|
12450
|
+
const discussionId = typeof body.discussionId === "string" ? body.discussionId : void 0;
|
|
12451
|
+
const sourceContext = discussionId ? await resolveSourceContext(opts.storage, {
|
|
12452
|
+
discussionId,
|
|
12453
|
+
title,
|
|
12454
|
+
memberId: body.sourceMemberId,
|
|
12455
|
+
memberName: body.sourceMemberName
|
|
12456
|
+
}) : void 0;
|
|
12311
12457
|
const item = {
|
|
12312
12458
|
id: generateUUID(),
|
|
12313
|
-
discussionId
|
|
12314
|
-
title
|
|
12459
|
+
discussionId,
|
|
12460
|
+
title,
|
|
12315
12461
|
description: typeof body.description === "string" ? body.description : "",
|
|
12316
12462
|
priority,
|
|
12317
12463
|
status,
|
|
12318
12464
|
assignedTo: typeof body.assignedTo === "string" && body.assignedTo.trim() ? body.assignedTo.trim() : void 0,
|
|
12319
12465
|
dueDate: typeof body.dueDate === "string" && body.dueDate.trim() ? body.dueDate.trim() : void 0,
|
|
12466
|
+
...sourceContext ? { sourceContext } : {},
|
|
12320
12467
|
createdAt: now,
|
|
12321
12468
|
updatedAt: now
|
|
12322
12469
|
};
|
|
@@ -12379,20 +12526,28 @@ async function startUiServer(opts) {
|
|
|
12379
12526
|
const body = req.body ?? {};
|
|
12380
12527
|
if (Array.isArray(body.accept)) {
|
|
12381
12528
|
const created = [];
|
|
12529
|
+
const members = await opts.storage.loadBoardMembers();
|
|
12382
12530
|
for (const raw of body.accept) {
|
|
12383
12531
|
if (!raw || typeof raw !== "object") continue;
|
|
12384
12532
|
const cand = raw;
|
|
12385
12533
|
if (!cand.title || typeof cand.title !== "string" || !cand.title.trim()) continue;
|
|
12386
12534
|
const now = nowIso();
|
|
12535
|
+
const title = cand.title.trim().slice(0, 200);
|
|
12536
|
+
const sourceContext = buildSourceContext(discussion, members, {
|
|
12537
|
+
memberId: cand.sourceMemberId,
|
|
12538
|
+
memberName: cand.sourceMemberName,
|
|
12539
|
+
stepText: title
|
|
12540
|
+
});
|
|
12387
12541
|
const item = {
|
|
12388
12542
|
id: generateUUID(),
|
|
12389
12543
|
discussionId: discussion.id,
|
|
12390
|
-
title
|
|
12544
|
+
title,
|
|
12391
12545
|
description: typeof cand.description === "string" ? cand.description : "",
|
|
12392
12546
|
priority: coerceActionPriority(cand.priority),
|
|
12393
12547
|
status: "pending",
|
|
12394
12548
|
assignedTo: typeof cand.suggestedAssignee === "string" && cand.suggestedAssignee.trim() ? cand.suggestedAssignee.trim() : void 0,
|
|
12395
12549
|
dueDate: typeof cand.suggestedDueDate === "string" && cand.suggestedDueDate.trim() ? cand.suggestedDueDate.trim() : void 0,
|
|
12550
|
+
...sourceContext ? { sourceContext } : {},
|
|
12396
12551
|
createdAt: now,
|
|
12397
12552
|
updatedAt: now
|
|
12398
12553
|
};
|
|
@@ -16289,9 +16444,12 @@ function registerActionsCommand(program) {
|
|
|
16289
16444
|
const dueDate = opts.due ?? "";
|
|
16290
16445
|
const assignee = opts.assignee ?? "";
|
|
16291
16446
|
let discussionId;
|
|
16447
|
+
let sourceContext;
|
|
16292
16448
|
if (opts.discussion) {
|
|
16293
16449
|
const linked = await resolveDiscussion2(ctx.storage, opts.discussion);
|
|
16294
16450
|
discussionId = linked.id;
|
|
16451
|
+
const members = await ctx.storage.loadBoardMembers();
|
|
16452
|
+
sourceContext = buildSourceContext(linked, members, { stepText: finalTitle });
|
|
16295
16453
|
}
|
|
16296
16454
|
const now = nowIso();
|
|
16297
16455
|
const item = {
|
|
@@ -16303,6 +16461,7 @@ function registerActionsCommand(program) {
|
|
|
16303
16461
|
status: "pending",
|
|
16304
16462
|
assignedTo: assignee || void 0,
|
|
16305
16463
|
dueDate: dueDate || void 0,
|
|
16464
|
+
...sourceContext ? { sourceContext } : {},
|
|
16306
16465
|
createdAt: now,
|
|
16307
16466
|
updatedAt: now
|
|
16308
16467
|
};
|