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 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 allResponses = discussion.responses.map((r) => {
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
- allResponses || "(no responses)",
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: typeof body.discussionId === "string" ? body.discussionId : void 0,
12314
- title: body.title.trim(),
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: cand.title.trim().slice(0, 200),
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
  };