@papi-ai/server 0.7.103 → 0.7.104

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/index.js CHANGED
@@ -401,6 +401,7 @@ function parseBuildHandoff(markdown) {
401
401
  taskTitle,
402
402
  cycle,
403
403
  whyNow,
404
+ relevantDecisions: parseBulletsOnly(sections.get("RELEVANT ACTIVE DECISIONS") ?? ""),
404
405
  scope: parseBulletList(sections.get("SCOPE (DO THIS)") ?? ""),
405
406
  scopeBoundary: parseBulletList(sections.get("SCOPE BOUNDARY (DO NOT DO THIS)") ?? ""),
406
407
  acceptanceCriteria: parseChecklist(sections.get("ACCEPTANCE CRITERIA") ?? ""),
@@ -424,6 +425,7 @@ function coerceBuildHandoff(fields, taskId) {
424
425
  taskTitle: str(fields.taskTitle),
425
426
  cycle: typeof fields.cycle === "number" ? fields.cycle : 0,
426
427
  whyNow: str(fields.whyNow),
428
+ relevantDecisions: ensureArray(fields.relevantDecisions),
427
429
  scope,
428
430
  scopeBoundary,
429
431
  // task-3223: the structured path is where an object criterion (text + an
@@ -471,6 +473,14 @@ function serializeBuildHandoff(raw) {
471
473
  lines.push(`Task: ${handoff.taskTitle}`);
472
474
  lines.push(`Cycle: ${handoff.cycle}`);
473
475
  lines.push(`Why now: ${handoff.whyNow}`);
476
+ const relevantDecisions = ensureArray(handoff.relevantDecisions);
477
+ if (relevantDecisions.length > 0) {
478
+ lines.push("");
479
+ lines.push("RELEVANT ACTIVE DECISIONS");
480
+ for (const item of relevantDecisions) {
481
+ lines.push(`- ${item}`);
482
+ }
483
+ }
474
484
  lines.push("");
475
485
  lines.push("SCOPE (DO THIS)");
476
486
  for (const item of ensureArray(handoff.scope)) {
@@ -912,6 +922,7 @@ var init_dist = __esm({
912
922
  CHECK_VALUE_MAX = 200;
913
923
  VALID_EFFORT_SIZES = /* @__PURE__ */ new Set(["XS", "S", "M", "L", "XL"]);
914
924
  SECTION_HEADERS = [
925
+ "RELEVANT ACTIVE DECISIONS",
915
926
  "SCOPE (DO THIS)",
916
927
  "WHY NOT SIMPLER",
917
928
  "SCOPE BOUNDARY (DO NOT DO THIS)",
@@ -7572,11 +7583,78 @@ init_dist();
7572
7583
 
7573
7584
  // src/lib/formatters.ts
7574
7585
  init_dist();
7586
+ var CONFIDENCE_RANK = { HIGH: 0, MEDIUM: 1, LOW: 2 };
7587
+ function rankDecisionsForPlan(decisions) {
7588
+ return [...decisions].sort((a, b2) => {
7589
+ const confDiff = (CONFIDENCE_RANK[a.confidence] ?? 1) - (CONFIDENCE_RANK[b2.confidence] ?? 1);
7590
+ if (confDiff !== 0) return confDiff;
7591
+ return (b2.modifiedCycle ?? b2.createdCycle ?? 0) - (a.modifiedCycle ?? a.createdCycle ?? 0);
7592
+ });
7593
+ }
7575
7594
  function formatActiveDecisionsForPlan(decisions) {
7576
7595
  if (decisions.length === 0) return "No active decisions.";
7577
- return decisions.filter((d) => !d.superseded).map((d) => `### ${d.id}: ${d.title} [Confidence: ${d.confidence}]
7596
+ const active = decisions.filter((d) => !d.superseded);
7597
+ if (active.length === 0) return "No active decisions.";
7598
+ const softBudget = Number(process.env.PAPI_AD_CONTEXT_BUDGET) || 4e4;
7599
+ const hardBudget = Math.round(softBudget * 1.3);
7600
+ const ranked = rankDecisionsForPlan(active);
7601
+ const fullBlocks = [];
7602
+ const oneLiners = [];
7603
+ let spent = 0;
7604
+ let degradedFrom = -1;
7605
+ for (let i = 0; i < ranked.length; i++) {
7606
+ const d = ranked[i];
7607
+ const fullBlock = `### ${d.id}: ${d.title} [Confidence: ${d.confidence}]
7608
+
7609
+ ${d.body}`;
7610
+ const fullCost = Buffer.byteLength(`${fullBlock}
7578
7611
 
7579
- ${d.body}`).join("\n\n");
7612
+ `, "utf-8");
7613
+ if (spent + fullCost <= softBudget) {
7614
+ fullBlocks.push(fullBlock);
7615
+ spent += fullCost;
7616
+ continue;
7617
+ }
7618
+ degradedFrom = i;
7619
+ break;
7620
+ }
7621
+ const compactedIds = [];
7622
+ const omittedIds = [];
7623
+ if (degradedFrom >= 0) {
7624
+ for (let i = degradedFrom; i < ranked.length; i++) {
7625
+ const d = ranked[i];
7626
+ const line = `- ${d.id} (${d.title}) [${d.confidence}]`;
7627
+ const cost = Buffer.byteLength(`${line}
7628
+ `, "utf-8");
7629
+ if (spent + cost <= hardBudget) {
7630
+ oneLiners.push(line);
7631
+ compactedIds.push(d.id);
7632
+ spent += cost;
7633
+ } else {
7634
+ omittedIds.push(d.id);
7635
+ }
7636
+ }
7637
+ }
7638
+ const parts = [...fullBlocks];
7639
+ if (oneLiners.length > 0) {
7640
+ parts.push(`### Other Active Decisions (compacted for budget)
7641
+
7642
+ ${oneLiners.join("\n")}`);
7643
+ }
7644
+ if (compactedIds.length > 0 || omittedIds.length > 0) {
7645
+ const noteLines = [
7646
+ `### Context Budget \u2014 Active Decisions trimmed`,
7647
+ "",
7648
+ `${compactedIds.length + omittedIds.length} of ${ranked.length} Active Decisions were compacted to one-liners to keep this plan payload under ~${Math.round(softBudget / 1024)} KB (lowest confidence/oldest first). Full bodies above are complete and untrimmed.`
7649
+ ];
7650
+ if (omittedIds.length > 0) {
7651
+ noteLines.push(
7652
+ `${omittedIds.length} AD one-liner(s) were additionally omitted from the list above for exceeding the hard budget, but are not forgotten: ${omittedIds.join(", ")}. Raise PAPI_AD_CONTEXT_BUDGET in the MCP server env to see their one-liners, or run ad_view for full detail on any specific one.`
7653
+ );
7654
+ }
7655
+ parts.push(noteLines.join("\n"));
7656
+ }
7657
+ return parts.join("\n\n");
7580
7658
  }
7581
7659
  function formatActiveDecisionsForReview(decisions) {
7582
7660
  if (decisions.length === 0) return "No active decisions.";
@@ -8381,6 +8459,9 @@ Why now: [justification]
8381
8459
  DEPENDS ON
8382
8460
  [Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
8383
8461
 
8462
+ RELEVANT ACTIVE DECISIONS
8463
+ [Optional \u2014 self-check which Active Decisions actually bind the FILES LIKELY TOUCHED and module below, out of the full AD list already in your context. List the top 3-6 as "AD-N (title) \u2014 one-line reason this AD binds the files/approach". Rank by blast radius (an AD that would be VIOLATED by a naive implementation outranks one that's merely thematically adjacent). This is a judgement call \u2014 no server-side scoring feeds this. Omit this section entirely when nothing in the AD list is genuinely relevant to what this task touches; do not force a match.]
8464
+
8384
8465
  SCOPE (DO THIS)
8385
8466
  [specific deliverables \u2014 write for the simplest viable path first]
8386
8467
 
@@ -9716,6 +9797,9 @@ Why now: [justification]
9716
9797
  DEPENDS ON
9717
9798
  [Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
9718
9799
 
9800
+ RELEVANT ACTIVE DECISIONS
9801
+ [Optional \u2014 self-check which Active Decisions actually bind the FILES LIKELY TOUCHED and module below, out of the full AD list already in your context. List the top 3-6 as "AD-N (title) \u2014 one-line reason this AD binds the files/approach". Rank by blast radius (an AD that would be VIOLATED by a naive implementation outranks one that's merely thematically adjacent). This is a judgement call \u2014 no server-side scoring feeds this. Omit this section entirely when nothing in the AD list is genuinely relevant to what this task touches; do not force a match.]
9802
+
9719
9803
  SCOPE (DO THIS)
9720
9804
  [specific deliverables \u2014 write for the simplest viable path first]
9721
9805
 
@@ -10870,6 +10954,23 @@ function idMatches(candidate, ref) {
10870
10954
  if (!candidate) return false;
10871
10955
  return candidate.toLowerCase() === ref.toLowerCase();
10872
10956
  }
10957
+ function findDecision(ref, decisions) {
10958
+ return decisions.find((d) => idMatches(d.displayId, ref) || idMatches(d.id, ref));
10959
+ }
10960
+ function resolveCurrentDecision(ref, decisions, maxDepth = 10) {
10961
+ let current = findDecision(ref, decisions);
10962
+ if (!current) return void 0;
10963
+ const visited = /* @__PURE__ */ new Set([current.id]);
10964
+ let depth = 0;
10965
+ while (current?.superseded === true && current.supersededBy && depth < maxDepth) {
10966
+ const next = findDecision(current.supersededBy, decisions);
10967
+ if (!next || visited.has(next.id)) break;
10968
+ visited.add(next.id);
10969
+ current = next;
10970
+ depth += 1;
10971
+ }
10972
+ return current;
10973
+ }
10873
10974
  function isBlockerResolved(blocker, ctx) {
10874
10975
  if (!blocker || !blocker.ref) return false;
10875
10976
  switch (blocker.type) {
@@ -10884,9 +10985,7 @@ function isBlockerResolved(blocker, ctx) {
10884
10985
  return action != null && action.completed_at != null;
10885
10986
  }
10886
10987
  case "decision-gate": {
10887
- const decision = ctx.decisions.find(
10888
- (d) => idMatches(d.displayId, blocker.ref) || idMatches(d.id, blocker.ref)
10889
- );
10988
+ const decision = resolveCurrentDecision(blocker.ref, ctx.decisions);
10890
10989
  if (decision?.superseded === true) return true;
10891
10990
  const resolvedOutcomes = /* @__PURE__ */ new Set(["validated"]);
10892
10991
  if (decision?.outcome && resolvedOutcomes.has(decision.outcome)) return true;
@@ -10902,9 +11001,7 @@ function isBlockerResolved(blocker, ctx) {
10902
11001
  }
10903
11002
  function blockerNeedsRedecision(blocker, ctx) {
10904
11003
  if (!blocker || blocker.type !== "decision-gate" || !blocker.ref) return false;
10905
- const decision = ctx.decisions.find(
10906
- (d) => idMatches(d.displayId, blocker.ref) || idMatches(d.id, blocker.ref)
10907
- );
11004
+ const decision = resolveCurrentDecision(blocker.ref, ctx.decisions);
10908
11005
  return decision?.resolutionState === "withdrawn";
10909
11006
  }
10910
11007
  function formatBlockerWaiting(blocker, refTitle) {
@@ -10927,9 +11024,7 @@ function resolveBlockerTitle(blocker, ctx) {
10927
11024
  (t) => idMatches(t.displayId, blocker.ref) || idMatches(t.id, blocker.ref)
10928
11025
  )?.title;
10929
11026
  case "decision-gate":
10930
- return ctx.decisions.find(
10931
- (d) => idMatches(d.displayId, blocker.ref) || idMatches(d.id, blocker.ref)
10932
- )?.title;
11027
+ return resolveCurrentDecision(blocker.ref, ctx.decisions)?.title;
10933
11028
  default:
10934
11029
  return void 0;
10935
11030
  }
@@ -17761,7 +17856,11 @@ var boardDeprioritiseTool = {
17761
17856
  },
17762
17857
  blocker_ref: {
17763
17858
  type: "string",
17764
- description: "Required when blocker_type is set. The identifier being waited on: a task display-id (depends-on), an AD/decision id (decision-gate), or an owner_action id (owner-action)."
17859
+ description: "Required when blocker_type is set, EXCEPT owner-action with owner_action_name (that mode creates the owner_action and derives the ref itself). The identifier being waited on: a task display-id (depends-on), an AD/decision id (decision-gate), or an existing owner_action id (owner-action, link mode)."
17860
+ },
17861
+ owner_action_name: {
17862
+ type: "string",
17863
+ description: 'Optional, blocker_type="owner-action" only. When set, CREATES a new owner action with this name and unlocks_task_id set to this task in the same insert, instead of linking to an existing one \u2014 collapses the old create-then-link two-step into one call. Omit blocker_ref in this mode; it is derived from the created row.'
17765
17864
  },
17766
17865
  defer: {
17767
17866
  type: "boolean",
@@ -17779,15 +17878,26 @@ var boardDeprioritiseTool = {
17779
17878
  required: ["task_id"],
17780
17879
  // task-2802: mirror the handler's guards so the agent sees the dependency up
17781
17880
  // front. handleBoardDeprioritise requires `reason` for both block and cancel,
17782
- // and requires `blocker_ref` whenever `blocker_type` is set. Keyed on explicit
17783
- // values an omitted action defaults to "backlog" and triggers neither.
17881
+ // and requires `blocker_ref` whenever `blocker_type` is set EXCEPT the
17882
+ // task-3451 owner-action create mode (blocker_type='owner-action' +
17883
+ // owner_action_name), which derives its ref from the row it creates.
17884
+ // Keyed on explicit values — an omitted action defaults to "backlog" and
17885
+ // triggers neither.
17784
17886
  allOf: [
17785
17887
  {
17786
17888
  if: { properties: { action: { enum: ["block", "cancel"] } }, required: ["action"] },
17787
17889
  then: { required: ["reason"] }
17788
17890
  },
17789
17891
  {
17790
- if: { required: ["blocker_type"] },
17892
+ if: {
17893
+ required: ["blocker_type"],
17894
+ not: {
17895
+ allOf: [
17896
+ { properties: { blocker_type: { const: "owner-action" } }, required: ["blocker_type"] },
17897
+ { required: ["owner_action_name"] }
17898
+ ]
17899
+ }
17900
+ },
17791
17901
  then: { required: ["blocker_ref"] }
17792
17902
  }
17793
17903
  ]
@@ -18092,13 +18202,18 @@ async function handleBoardDeprioritise(adapter2, args) {
18092
18202
  return errorResponse("reason is required when blocking a task \u2014 explain what external dependency or gate is blocking it.");
18093
18203
  }
18094
18204
  const blockerType = args.blocker_type;
18095
- const blockerRef = args.blocker_ref;
18205
+ let blockerRef = args.blocker_ref;
18206
+ const ownerActionName = args.owner_action_name;
18207
+ const isOwnerActionCreateMode = blockerType === "owner-action" && Boolean(ownerActionName);
18096
18208
  const validTypes = /* @__PURE__ */ new Set(["depends-on", "decision-gate", "owner-action"]);
18097
18209
  if (blockerType !== void 0 && !validTypes.has(blockerType)) {
18098
18210
  return errorResponse(`blocker_type must be one of: depends-on, decision-gate, owner-action (got "${blockerType}").`);
18099
18211
  }
18100
- if (blockerType !== void 0 && !blockerRef) {
18101
- return errorResponse("blocker_ref is required when blocker_type is set \u2014 provide the task display-id, decision id, or owner_action id being waited on.");
18212
+ if (blockerType !== void 0 && !blockerRef && !isOwnerActionCreateMode) {
18213
+ return errorResponse("blocker_ref is required when blocker_type is set \u2014 provide the task display-id, decision id, or owner_action id being waited on (or owner_action_name to create a new owner action).");
18214
+ }
18215
+ if (ownerActionName && blockerType !== "owner-action") {
18216
+ return errorResponse('owner_action_name only applies when blocker_type is "owner-action".');
18102
18217
  }
18103
18218
  try {
18104
18219
  const task = await adapter2.getTask(taskId);
@@ -18111,25 +18226,41 @@ async function handleBoardDeprioritise(adapter2, args) {
18111
18226
  notes: `${existingNotes}BLOCKED: ${reason}`
18112
18227
  };
18113
18228
  let typedSuffix = "";
18114
- if (blockerType !== void 0 && blockerRef) {
18115
- const health = await adapter2.getCycleHealth().catch(() => null);
18116
- const blockedCycle = health?.totalCycles ?? 0;
18117
- updates.blocker = {
18118
- type: blockerType,
18119
- ref: blockerRef,
18120
- reason,
18121
- blockedCycle
18122
- };
18123
- typedSuffix = `
18124
-
18125
- Blocker: **${blockerType}** \u2192 ${blockerRef} (auto-unblock scanned at plan/orient).`;
18126
- if (blockerType === "owner-action" && adapter2.linkOwnerActionToTask && adapter2.getProjectOwnerUserId) {
18229
+ if (blockerType !== void 0 && (blockerRef || isOwnerActionCreateMode)) {
18230
+ if (isOwnerActionCreateMode && adapter2.createOwnerAction && adapter2.getProjectOwnerUserId) {
18127
18231
  try {
18128
18232
  const ownerUserId = await adapter2.getProjectOwnerUserId();
18129
- if (ownerUserId) await adapter2.linkOwnerActionToTask(blockerRef, task.uuid, ownerUserId);
18233
+ if (ownerUserId) {
18234
+ const created = await adapter2.createOwnerAction(ownerUserId, {
18235
+ name: ownerActionName,
18236
+ dependency: reason,
18237
+ unlocks_task_id: task.uuid
18238
+ });
18239
+ blockerRef = created.id;
18240
+ }
18130
18241
  } catch {
18131
18242
  }
18132
18243
  }
18244
+ if (blockerRef) {
18245
+ const health = await adapter2.getCycleHealth().catch(() => null);
18246
+ const blockedCycle = health?.totalCycles ?? 0;
18247
+ updates.blocker = {
18248
+ type: blockerType,
18249
+ ref: blockerRef,
18250
+ reason,
18251
+ blockedCycle
18252
+ };
18253
+ typedSuffix = `
18254
+
18255
+ Blocker: **${blockerType}** \u2192 ${blockerRef} (auto-unblock scanned at plan/orient).`;
18256
+ if (blockerType === "owner-action" && !isOwnerActionCreateMode && adapter2.linkOwnerActionToTask && adapter2.getProjectOwnerUserId) {
18257
+ try {
18258
+ const ownerUserId = await adapter2.getProjectOwnerUserId();
18259
+ if (ownerUserId) await adapter2.linkOwnerActionToTask(blockerRef, task.uuid, ownerUserId);
18260
+ } catch {
18261
+ }
18262
+ }
18263
+ }
18133
18264
  }
18134
18265
  await adapter2.updateTask(taskId, updates);
18135
18266
  return textResponse(`Blocked **${taskId}** (${task.title}).
@@ -24777,6 +24908,41 @@ The registry stores metadata and a summary \u2014 not the body. This doc has one
24777
24908
  return `
24778
24909
  - **Durability:** committed \`${path8}\` (was untracked)`;
24779
24910
  }
24911
+ var DOC_OVERLAP_COVERAGE_THRESHOLD = 0.6;
24912
+ async function findOverlappingDocs(target, type, title, summary) {
24913
+ if (!target.searchDocs) return [];
24914
+ const newKeywords = extractKeywords(`${title} ${summary}`);
24915
+ if (newKeywords.size < 2) return [];
24916
+ let candidates;
24917
+ try {
24918
+ candidates = await target.searchDocs({ type, status: "active" });
24919
+ } catch {
24920
+ return [];
24921
+ }
24922
+ const matches = [];
24923
+ for (const doc of candidates) {
24924
+ const docKeywords = extractKeywords(`${doc.title} ${doc.summary}`);
24925
+ if (docKeywords.size < 2) continue;
24926
+ let covered = 0;
24927
+ for (const word of newKeywords) if (docKeywords.has(word)) covered++;
24928
+ const coverage = covered / newKeywords.size;
24929
+ if (coverage >= DOC_OVERLAP_COVERAGE_THRESHOLD) {
24930
+ matches.push({ path: doc.path, title: doc.title, coverage });
24931
+ }
24932
+ }
24933
+ return matches.sort((a, b2) => b2.coverage - a.coverage).slice(0, 3);
24934
+ }
24935
+ function overlapReconciliationNote(overlaps) {
24936
+ if (overlaps.length === 0) return "";
24937
+ const named = overlaps.map((o) => `\`${o.path}\` ("${o.title}")`).join(", ");
24938
+ return `
24939
+
24940
+ \u26A0\uFE0F **Possible overlap** with existing active doc(s): ${named} \u2014 verify it is real, then classify:
24941
+ - **AGREES** \u2014 restates the existing doc. Don't duplicate; nothing to register.
24942
+ - **EXTENDS** \u2014 new ground the existing doc doesn't cover. Fine to register as-is.
24943
+ - **CONTRADICTS** \u2014 cuts against the existing doc. Surface to the owner \u2014 never resolve silently (task-3219: a contradiction is not a veto).
24944
+ - **SUPERSEDES** \u2014 this doc should replace the existing one. Re-run doc_register with \`superseded_by_path\` pointing at the overlapping doc's path.`;
24945
+ }
24780
24946
  async function handleDocRegister(adapter2, args, config2) {
24781
24947
  const adapterType = config2?.adapterType ?? "unknown";
24782
24948
  const continueHint = "doc_register is advisory \u2014 your build/plan/review flow is unaffected. Fix the input (or wait for the registry to recover) and re-run doc_register; or just continue without it.";
@@ -24835,6 +25001,7 @@ async function handleDocRegister(adapter2, args, config2) {
24835
25001
  );
24836
25002
  }
24837
25003
  try {
25004
+ const overlaps = supersededByPath ? [] : await findOverlappingDocs(target, type, title, summary);
24838
25005
  let supersededBy;
24839
25006
  if (supersededByPath) {
24840
25007
  const existing = await target.getDoc?.(supersededByPath);
@@ -24910,7 +25077,7 @@ ${decision.message}`;
24910
25077
  - **Visibility:** ${visibilityLabel}
24911
25078
  - **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
24912
25079
  - **Actions:** ${actions?.length ?? 0} items
24913
- - **ID:** ${entry.id}` + bodyNote + durability
25080
+ - **ID:** ${entry.id}` + bodyNote + durability + overlapReconciliationNote(overlaps)
24914
25081
  );
24915
25082
  } catch (err) {
24916
25083
  const message = err instanceof Error ? err.message : String(err);
@@ -32300,7 +32467,42 @@ ${section}`;
32300
32467
  const runtimeIdentityNote = `
32301
32468
 
32302
32469
  ${formatRuntimeIdentity(getRuntimeIdentity(config2, serverVersion))}`;
32303
- return { ...textResponse(projectOverrideLine + projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName, carryForwardRefs) + runtimeIdentityNote + unblockNote + deferredGateNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + onboardingCoachingNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection), _cycleNumber: healthResult.cycleNumber };
32470
+ let effDeepHint = deepHint;
32471
+ let effOnboardingCoachingNote = onboardingCoachingNote;
32472
+ let effStaleSkillsNote = staleSkillsNote;
32473
+ let effResearchSignalsNote = researchSignalsNote;
32474
+ const droppedNotes = [];
32475
+ const assembleOrientOutput = () => projectOverrideLine + projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName, carryForwardRefs) + runtimeIdentityNote + unblockNote + deferredGateNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + effStaleSkillsNote + effResearchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + effOnboardingCoachingNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + effDeepHint + enrichmentFilesSection;
32476
+ const orientBudgetSoft = Number(process.env.PAPI_ORIENT_CONTEXT_BUDGET) || 6e4;
32477
+ let assembled = assembleOrientOutput();
32478
+ if (Buffer.byteLength(assembled, "utf-8") > orientBudgetSoft && effDeepHint) {
32479
+ effDeepHint = "";
32480
+ droppedNotes.push("deep-housekeeping tip");
32481
+ assembled = assembleOrientOutput();
32482
+ }
32483
+ if (Buffer.byteLength(assembled, "utf-8") > orientBudgetSoft && effOnboardingCoachingNote) {
32484
+ effOnboardingCoachingNote = "";
32485
+ droppedNotes.push("onboarding coaching");
32486
+ assembled = assembleOrientOutput();
32487
+ }
32488
+ if (Buffer.byteLength(assembled, "utf-8") > orientBudgetSoft && effStaleSkillsNote) {
32489
+ effStaleSkillsNote = "";
32490
+ droppedNotes.push("stale skills");
32491
+ assembled = assembleOrientOutput();
32492
+ }
32493
+ if (Buffer.byteLength(assembled, "utf-8") > orientBudgetSoft && effResearchSignalsNote) {
32494
+ effResearchSignalsNote = "";
32495
+ droppedNotes.push("research signals");
32496
+ assembled = assembleOrientOutput();
32497
+ }
32498
+ if (droppedNotes.length > 0) {
32499
+ assembled += `
32500
+
32501
+ ---
32502
+
32503
+ *Context budget: ${droppedNotes.length} advisory note(s) omitted to keep this orient payload under ~${Math.round(orientBudgetSoft / 1024)} KB \u2014 ${droppedNotes.join(", ")}. Raise \`PAPI_ORIENT_CONTEXT_BUDGET\` in the MCP server env, or re-run without \`deep_housekeeping\`/\`full\` for a smaller payload by default.*`;
32504
+ }
32505
+ return { ...textResponse(assembled), _cycleNumber: healthResult.cycleNumber };
32304
32506
  } catch (err) {
32305
32507
  const message = err instanceof Error ? err.message : String(err);
32306
32508
  const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
@@ -33561,7 +33763,7 @@ var decisionResolveTool = {
33561
33763
  required: ["ad_id", "action"]
33562
33764
  }
33563
33765
  };
33564
- function findDecision(decisions, adId) {
33766
+ function findDecision2(decisions, adId) {
33565
33767
  const trimmed = adId.trim();
33566
33768
  return decisions.find((d) => d.id === trimmed || d.displayId === trimmed);
33567
33769
  }
@@ -33594,7 +33796,7 @@ async function handleDecisionResolve(adapter2, config2, args) {
33594
33796
  } catch (err) {
33595
33797
  return errorResponse(`Failed to read active decisions: ${err instanceof Error ? err.message : String(err)}`);
33596
33798
  }
33597
- const target = findDecision(decisions, adId);
33799
+ const target = findDecision2(decisions, adId);
33598
33800
  if (!target) return errorResponse(`AD not found: ${adId}. Run ad_view to see available decisions.`);
33599
33801
  if (target.resolutionState !== "proposed") {
33600
33802
  return errorResponse(
package/dist/prompts.js CHANGED
@@ -78,6 +78,9 @@ Why now: [justification]
78
78
  DEPENDS ON
79
79
  [Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
80
80
 
81
+ RELEVANT ACTIVE DECISIONS
82
+ [Optional \u2014 self-check which Active Decisions actually bind the FILES LIKELY TOUCHED and module below, out of the full AD list already in your context. List the top 3-6 as "AD-N (title) \u2014 one-line reason this AD binds the files/approach". Rank by blast radius (an AD that would be VIOLATED by a naive implementation outranks one that's merely thematically adjacent). This is a judgement call \u2014 no server-side scoring feeds this. Omit this section entirely when nothing in the AD list is genuinely relevant to what this task touches; do not force a match.]
83
+
81
84
  SCOPE (DO THIS)
82
85
  [specific deliverables \u2014 write for the simplest viable path first]
83
86
 
@@ -1413,6 +1416,9 @@ Why now: [justification]
1413
1416
  DEPENDS ON
1414
1417
  [Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
1415
1418
 
1419
+ RELEVANT ACTIVE DECISIONS
1420
+ [Optional \u2014 self-check which Active Decisions actually bind the FILES LIKELY TOUCHED and module below, out of the full AD list already in your context. List the top 3-6 as "AD-N (title) \u2014 one-line reason this AD binds the files/approach". Rank by blast radius (an AD that would be VIOLATED by a naive implementation outranks one that's merely thematically adjacent). This is a judgement call \u2014 no server-side scoring feeds this. Omit this section entirely when nothing in the AD list is genuinely relevant to what this task touches; do not force a match.]
1421
+
1416
1422
  SCOPE (DO THIS)
1417
1423
  [specific deliverables \u2014 write for the simplest viable path first]
1418
1424
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.103",
4
- "description": "PAPI MCP server \u2014 AI-powered sprint planning, build execution, and strategy review for software projects",
3
+ "version": "0.7.104",
4
+ "description": "PAPI MCP server AI-powered sprint planning, build execution, and strategy review for software projects",
5
5
  "license": "Elastic-2.0",
6
6
  "mcpName": "io.github.getpapi/papi",
7
7
  "type": "module",