@papi-ai/server 0.7.103 → 0.7.105

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.
@@ -201,6 +201,7 @@ __export(git_exports, {
201
201
  ensureLatestDevelop: () => ensureLatestDevelop,
202
202
  ensureTagAtHead: () => ensureTagAtHead,
203
203
  fetchBaseBranch: () => fetchBaseBranch,
204
+ findCollidingWork: () => findCollidingWork,
204
205
  findContributorReleasePullRequests: () => findContributorReleasePullRequests,
205
206
  findTaskCommitsOnBase: () => findTaskCommitsOnBase,
206
207
  getBaseDivergence: () => getBaseDivergence,
@@ -1493,6 +1494,23 @@ function getRemoteBranchFiles(cwd, branch, baseBranch) {
1493
1494
  return [];
1494
1495
  }
1495
1496
  }
1497
+ function findCollidingWork(cwd, baseBranch, filesLikelyTouched) {
1498
+ if (filesLikelyTouched.length === 0) return null;
1499
+ const prs = listOpenPullRequests(cwd);
1500
+ if (!prs || prs.length === 0) return null;
1501
+ const wanted = new Set(filesLikelyTouched);
1502
+ for (const pr of prs) {
1503
+ const branchFiles = getRemoteBranchFiles(cwd, pr.headRefName, baseBranch);
1504
+ const overlap = branchFiles.filter((f) => wanted.has(f));
1505
+ if (overlap.length > 0) {
1506
+ return {
1507
+ label: `PR #${pr.number} "${pr.title}" (branch \`${pr.headRefName}\`, opened by ${pr.author})`,
1508
+ files: overlap
1509
+ };
1510
+ }
1511
+ }
1512
+ return null;
1513
+ }
1496
1514
  var AUTO_WRITTEN_PATHS, GIT_NETWORK_TIMEOUT_MS, MERGE_RETRY_DELAY_MS, MERGE_MAX_RETRIES, GIT_FETCH_TIMEOUT_MS;
1497
1515
  var init_git = __esm({
1498
1516
  "src/lib/git.ts"() {
@@ -1684,7 +1702,6 @@ var init_proxy_adapter = __esm({
1684
1702
  "getBuildReportsSince",
1685
1703
  "getContextHashes",
1686
1704
  "getContextUtilisation",
1687
- "getCostSnapshots",
1688
1705
  "getCostSummary",
1689
1706
  "getCurrentNorthStar",
1690
1707
  "getCycleHealth",
@@ -2338,9 +2355,8 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
2338
2355
  getCostSummary(cycleNumber) {
2339
2356
  return this.invoke("getCostSummary", [cycleNumber]);
2340
2357
  }
2341
- getCostSnapshots() {
2342
- return this.invoke("getCostSnapshots");
2343
- }
2358
+ // task-3332: getCostSnapshots retired — see the removal note on CostSnapshot
2359
+ // in packages/shared/src/entities.ts.
2344
2360
  appendCycleMetrics(snapshot) {
2345
2361
  return this.invoke("appendCycleMetrics", [snapshot]);
2346
2362
  }
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)",
@@ -960,6 +971,7 @@ __export(git_exports, {
960
971
  ensureLatestDevelop: () => ensureLatestDevelop,
961
972
  ensureTagAtHead: () => ensureTagAtHead,
962
973
  fetchBaseBranch: () => fetchBaseBranch,
974
+ findCollidingWork: () => findCollidingWork,
963
975
  findContributorReleasePullRequests: () => findContributorReleasePullRequests,
964
976
  findTaskCommitsOnBase: () => findTaskCommitsOnBase,
965
977
  getBaseDivergence: () => getBaseDivergence,
@@ -2252,6 +2264,23 @@ function getRemoteBranchFiles(cwd, branch, baseBranch) {
2252
2264
  return [];
2253
2265
  }
2254
2266
  }
2267
+ function findCollidingWork(cwd, baseBranch, filesLikelyTouched) {
2268
+ if (filesLikelyTouched.length === 0) return null;
2269
+ const prs = listOpenPullRequests(cwd);
2270
+ if (!prs || prs.length === 0) return null;
2271
+ const wanted = new Set(filesLikelyTouched);
2272
+ for (const pr of prs) {
2273
+ const branchFiles = getRemoteBranchFiles(cwd, pr.headRefName, baseBranch);
2274
+ const overlap = branchFiles.filter((f) => wanted.has(f));
2275
+ if (overlap.length > 0) {
2276
+ return {
2277
+ label: `PR #${pr.number} "${pr.title}" (branch \`${pr.headRefName}\`, opened by ${pr.author})`,
2278
+ files: overlap
2279
+ };
2280
+ }
2281
+ }
2282
+ return null;
2283
+ }
2255
2284
  var AUTO_WRITTEN_PATHS, GIT_NETWORK_TIMEOUT_MS, MERGE_RETRY_DELAY_MS, MERGE_MAX_RETRIES, GIT_FETCH_TIMEOUT_MS;
2256
2285
  var init_git = __esm({
2257
2286
  "src/lib/git.ts"() {
@@ -2509,7 +2538,6 @@ var init_proxy_adapter = __esm({
2509
2538
  "getBuildReportsSince",
2510
2539
  "getContextHashes",
2511
2540
  "getContextUtilisation",
2512
- "getCostSnapshots",
2513
2541
  "getCostSummary",
2514
2542
  "getCurrentNorthStar",
2515
2543
  "getCycleHealth",
@@ -3163,9 +3191,8 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
3163
3191
  getCostSummary(cycleNumber) {
3164
3192
  return this.invoke("getCostSummary", [cycleNumber]);
3165
3193
  }
3166
- getCostSnapshots() {
3167
- return this.invoke("getCostSnapshots");
3168
- }
3194
+ // task-3332: getCostSnapshots retired — see the removal note on CostSnapshot
3195
+ // in packages/shared/src/entities.ts.
3169
3196
  appendCycleMetrics(snapshot) {
3170
3197
  return this.invoke("appendCycleMetrics", [snapshot]);
3171
3198
  }
@@ -7572,11 +7599,78 @@ init_dist();
7572
7599
 
7573
7600
  // src/lib/formatters.ts
7574
7601
  init_dist();
7602
+ var CONFIDENCE_RANK = { HIGH: 0, MEDIUM: 1, LOW: 2 };
7603
+ function rankDecisionsForPlan(decisions) {
7604
+ return [...decisions].sort((a, b2) => {
7605
+ const confDiff = (CONFIDENCE_RANK[a.confidence] ?? 1) - (CONFIDENCE_RANK[b2.confidence] ?? 1);
7606
+ if (confDiff !== 0) return confDiff;
7607
+ return (b2.modifiedCycle ?? b2.createdCycle ?? 0) - (a.modifiedCycle ?? a.createdCycle ?? 0);
7608
+ });
7609
+ }
7575
7610
  function formatActiveDecisionsForPlan(decisions) {
7576
7611
  if (decisions.length === 0) return "No active decisions.";
7577
- return decisions.filter((d) => !d.superseded).map((d) => `### ${d.id}: ${d.title} [Confidence: ${d.confidence}]
7612
+ const active = decisions.filter((d) => !d.superseded);
7613
+ if (active.length === 0) return "No active decisions.";
7614
+ const softBudget = Number(process.env.PAPI_AD_CONTEXT_BUDGET) || 4e4;
7615
+ const hardBudget = Math.round(softBudget * 1.3);
7616
+ const ranked = rankDecisionsForPlan(active);
7617
+ const fullBlocks = [];
7618
+ const oneLiners = [];
7619
+ let spent = 0;
7620
+ let degradedFrom = -1;
7621
+ for (let i = 0; i < ranked.length; i++) {
7622
+ const d = ranked[i];
7623
+ const fullBlock = `### ${d.id}: ${d.title} [Confidence: ${d.confidence}]
7624
+
7625
+ ${d.body}`;
7626
+ const fullCost = Buffer.byteLength(`${fullBlock}
7578
7627
 
7579
- ${d.body}`).join("\n\n");
7628
+ `, "utf-8");
7629
+ if (spent + fullCost <= softBudget) {
7630
+ fullBlocks.push(fullBlock);
7631
+ spent += fullCost;
7632
+ continue;
7633
+ }
7634
+ degradedFrom = i;
7635
+ break;
7636
+ }
7637
+ const compactedIds = [];
7638
+ const omittedIds = [];
7639
+ if (degradedFrom >= 0) {
7640
+ for (let i = degradedFrom; i < ranked.length; i++) {
7641
+ const d = ranked[i];
7642
+ const line = `- ${d.id} (${d.title}) [${d.confidence}]`;
7643
+ const cost = Buffer.byteLength(`${line}
7644
+ `, "utf-8");
7645
+ if (spent + cost <= hardBudget) {
7646
+ oneLiners.push(line);
7647
+ compactedIds.push(d.id);
7648
+ spent += cost;
7649
+ } else {
7650
+ omittedIds.push(d.id);
7651
+ }
7652
+ }
7653
+ }
7654
+ const parts = [...fullBlocks];
7655
+ if (oneLiners.length > 0) {
7656
+ parts.push(`### Other Active Decisions (compacted for budget)
7657
+
7658
+ ${oneLiners.join("\n")}`);
7659
+ }
7660
+ if (compactedIds.length > 0 || omittedIds.length > 0) {
7661
+ const noteLines = [
7662
+ `### Context Budget \u2014 Active Decisions trimmed`,
7663
+ "",
7664
+ `${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.`
7665
+ ];
7666
+ if (omittedIds.length > 0) {
7667
+ noteLines.push(
7668
+ `${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.`
7669
+ );
7670
+ }
7671
+ parts.push(noteLines.join("\n"));
7672
+ }
7673
+ return parts.join("\n\n");
7580
7674
  }
7581
7675
  function formatActiveDecisionsForReview(decisions) {
7582
7676
  if (decisions.length === 0) return "No active decisions.";
@@ -8381,6 +8475,9 @@ Why now: [justification]
8381
8475
  DEPENDS ON
8382
8476
  [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
8477
 
8478
+ RELEVANT ACTIVE DECISIONS
8479
+ [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.]
8480
+
8384
8481
  SCOPE (DO THIS)
8385
8482
  [specific deliverables \u2014 write for the simplest viable path first]
8386
8483
 
@@ -9716,6 +9813,9 @@ Why now: [justification]
9716
9813
  DEPENDS ON
9717
9814
  [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
9815
 
9816
+ RELEVANT ACTIVE DECISIONS
9817
+ [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.]
9818
+
9719
9819
  SCOPE (DO THIS)
9720
9820
  [specific deliverables \u2014 write for the simplest viable path first]
9721
9821
 
@@ -10870,6 +10970,23 @@ function idMatches(candidate, ref) {
10870
10970
  if (!candidate) return false;
10871
10971
  return candidate.toLowerCase() === ref.toLowerCase();
10872
10972
  }
10973
+ function findDecision(ref, decisions) {
10974
+ return decisions.find((d) => idMatches(d.displayId, ref) || idMatches(d.id, ref));
10975
+ }
10976
+ function resolveCurrentDecision(ref, decisions, maxDepth = 10) {
10977
+ let current = findDecision(ref, decisions);
10978
+ if (!current) return void 0;
10979
+ const visited = /* @__PURE__ */ new Set([current.id]);
10980
+ let depth = 0;
10981
+ while (current?.superseded === true && current.supersededBy && depth < maxDepth) {
10982
+ const next = findDecision(current.supersededBy, decisions);
10983
+ if (!next || visited.has(next.id)) break;
10984
+ visited.add(next.id);
10985
+ current = next;
10986
+ depth += 1;
10987
+ }
10988
+ return current;
10989
+ }
10873
10990
  function isBlockerResolved(blocker, ctx) {
10874
10991
  if (!blocker || !blocker.ref) return false;
10875
10992
  switch (blocker.type) {
@@ -10884,9 +11001,7 @@ function isBlockerResolved(blocker, ctx) {
10884
11001
  return action != null && action.completed_at != null;
10885
11002
  }
10886
11003
  case "decision-gate": {
10887
- const decision = ctx.decisions.find(
10888
- (d) => idMatches(d.displayId, blocker.ref) || idMatches(d.id, blocker.ref)
10889
- );
11004
+ const decision = resolveCurrentDecision(blocker.ref, ctx.decisions);
10890
11005
  if (decision?.superseded === true) return true;
10891
11006
  const resolvedOutcomes = /* @__PURE__ */ new Set(["validated"]);
10892
11007
  if (decision?.outcome && resolvedOutcomes.has(decision.outcome)) return true;
@@ -10902,9 +11017,7 @@ function isBlockerResolved(blocker, ctx) {
10902
11017
  }
10903
11018
  function blockerNeedsRedecision(blocker, ctx) {
10904
11019
  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
- );
11020
+ const decision = resolveCurrentDecision(blocker.ref, ctx.decisions);
10908
11021
  return decision?.resolutionState === "withdrawn";
10909
11022
  }
10910
11023
  function formatBlockerWaiting(blocker, refTitle) {
@@ -10927,9 +11040,7 @@ function resolveBlockerTitle(blocker, ctx) {
10927
11040
  (t) => idMatches(t.displayId, blocker.ref) || idMatches(t.id, blocker.ref)
10928
11041
  )?.title;
10929
11042
  case "decision-gate":
10930
- return ctx.decisions.find(
10931
- (d) => idMatches(d.displayId, blocker.ref) || idMatches(d.id, blocker.ref)
10932
- )?.title;
11043
+ return resolveCurrentDecision(blocker.ref, ctx.decisions)?.title;
10933
11044
  default:
10934
11045
  return void 0;
10935
11046
  }
@@ -17761,7 +17872,11 @@ var boardDeprioritiseTool = {
17761
17872
  },
17762
17873
  blocker_ref: {
17763
17874
  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)."
17875
+ 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)."
17876
+ },
17877
+ owner_action_name: {
17878
+ type: "string",
17879
+ 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
17880
  },
17766
17881
  defer: {
17767
17882
  type: "boolean",
@@ -17779,15 +17894,26 @@ var boardDeprioritiseTool = {
17779
17894
  required: ["task_id"],
17780
17895
  // task-2802: mirror the handler's guards so the agent sees the dependency up
17781
17896
  // 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.
17897
+ // and requires `blocker_ref` whenever `blocker_type` is set EXCEPT the
17898
+ // task-3451 owner-action create mode (blocker_type='owner-action' +
17899
+ // owner_action_name), which derives its ref from the row it creates.
17900
+ // Keyed on explicit values — an omitted action defaults to "backlog" and
17901
+ // triggers neither.
17784
17902
  allOf: [
17785
17903
  {
17786
17904
  if: { properties: { action: { enum: ["block", "cancel"] } }, required: ["action"] },
17787
17905
  then: { required: ["reason"] }
17788
17906
  },
17789
17907
  {
17790
- if: { required: ["blocker_type"] },
17908
+ if: {
17909
+ required: ["blocker_type"],
17910
+ not: {
17911
+ allOf: [
17912
+ { properties: { blocker_type: { const: "owner-action" } }, required: ["blocker_type"] },
17913
+ { required: ["owner_action_name"] }
17914
+ ]
17915
+ }
17916
+ },
17791
17917
  then: { required: ["blocker_ref"] }
17792
17918
  }
17793
17919
  ]
@@ -18092,13 +18218,18 @@ async function handleBoardDeprioritise(adapter2, args) {
18092
18218
  return errorResponse("reason is required when blocking a task \u2014 explain what external dependency or gate is blocking it.");
18093
18219
  }
18094
18220
  const blockerType = args.blocker_type;
18095
- const blockerRef = args.blocker_ref;
18221
+ let blockerRef = args.blocker_ref;
18222
+ const ownerActionName = args.owner_action_name;
18223
+ const isOwnerActionCreateMode = blockerType === "owner-action" && Boolean(ownerActionName);
18096
18224
  const validTypes = /* @__PURE__ */ new Set(["depends-on", "decision-gate", "owner-action"]);
18097
18225
  if (blockerType !== void 0 && !validTypes.has(blockerType)) {
18098
18226
  return errorResponse(`blocker_type must be one of: depends-on, decision-gate, owner-action (got "${blockerType}").`);
18099
18227
  }
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.");
18228
+ if (blockerType !== void 0 && !blockerRef && !isOwnerActionCreateMode) {
18229
+ 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).");
18230
+ }
18231
+ if (ownerActionName && blockerType !== "owner-action") {
18232
+ return errorResponse('owner_action_name only applies when blocker_type is "owner-action".');
18102
18233
  }
18103
18234
  try {
18104
18235
  const task = await adapter2.getTask(taskId);
@@ -18111,25 +18242,41 @@ async function handleBoardDeprioritise(adapter2, args) {
18111
18242
  notes: `${existingNotes}BLOCKED: ${reason}`
18112
18243
  };
18113
18244
  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) {
18245
+ if (blockerType !== void 0 && (blockerRef || isOwnerActionCreateMode)) {
18246
+ if (isOwnerActionCreateMode && adapter2.createOwnerAction && adapter2.getProjectOwnerUserId) {
18127
18247
  try {
18128
18248
  const ownerUserId = await adapter2.getProjectOwnerUserId();
18129
- if (ownerUserId) await adapter2.linkOwnerActionToTask(blockerRef, task.uuid, ownerUserId);
18249
+ if (ownerUserId) {
18250
+ const created = await adapter2.createOwnerAction(ownerUserId, {
18251
+ name: ownerActionName,
18252
+ dependency: reason,
18253
+ unlocks_task_id: task.uuid
18254
+ });
18255
+ blockerRef = created.id;
18256
+ }
18130
18257
  } catch {
18131
18258
  }
18132
18259
  }
18260
+ if (blockerRef) {
18261
+ const health = await adapter2.getCycleHealth().catch(() => null);
18262
+ const blockedCycle = health?.totalCycles ?? 0;
18263
+ updates.blocker = {
18264
+ type: blockerType,
18265
+ ref: blockerRef,
18266
+ reason,
18267
+ blockedCycle
18268
+ };
18269
+ typedSuffix = `
18270
+
18271
+ Blocker: **${blockerType}** \u2192 ${blockerRef} (auto-unblock scanned at plan/orient).`;
18272
+ if (blockerType === "owner-action" && !isOwnerActionCreateMode && adapter2.linkOwnerActionToTask && adapter2.getProjectOwnerUserId) {
18273
+ try {
18274
+ const ownerUserId = await adapter2.getProjectOwnerUserId();
18275
+ if (ownerUserId) await adapter2.linkOwnerActionToTask(blockerRef, task.uuid, ownerUserId);
18276
+ } catch {
18277
+ }
18278
+ }
18279
+ }
18133
18280
  }
18134
18281
  await adapter2.updateTask(taskId, updates);
18135
18282
  return textResponse(`Blocked **${taskId}** (${task.title}).
@@ -24777,6 +24924,41 @@ The registry stores metadata and a summary \u2014 not the body. This doc has one
24777
24924
  return `
24778
24925
  - **Durability:** committed \`${path8}\` (was untracked)`;
24779
24926
  }
24927
+ var DOC_OVERLAP_COVERAGE_THRESHOLD = 0.6;
24928
+ async function findOverlappingDocs(target, type, title, summary) {
24929
+ if (!target.searchDocs) return [];
24930
+ const newKeywords = extractKeywords(`${title} ${summary}`);
24931
+ if (newKeywords.size < 2) return [];
24932
+ let candidates;
24933
+ try {
24934
+ candidates = await target.searchDocs({ type, status: "active" });
24935
+ } catch {
24936
+ return [];
24937
+ }
24938
+ const matches = [];
24939
+ for (const doc of candidates) {
24940
+ const docKeywords = extractKeywords(`${doc.title} ${doc.summary}`);
24941
+ if (docKeywords.size < 2) continue;
24942
+ let covered = 0;
24943
+ for (const word of newKeywords) if (docKeywords.has(word)) covered++;
24944
+ const coverage = covered / newKeywords.size;
24945
+ if (coverage >= DOC_OVERLAP_COVERAGE_THRESHOLD) {
24946
+ matches.push({ path: doc.path, title: doc.title, coverage });
24947
+ }
24948
+ }
24949
+ return matches.sort((a, b2) => b2.coverage - a.coverage).slice(0, 3);
24950
+ }
24951
+ function overlapReconciliationNote(overlaps) {
24952
+ if (overlaps.length === 0) return "";
24953
+ const named = overlaps.map((o) => `\`${o.path}\` ("${o.title}")`).join(", ");
24954
+ return `
24955
+
24956
+ \u26A0\uFE0F **Possible overlap** with existing active doc(s): ${named} \u2014 verify it is real, then classify:
24957
+ - **AGREES** \u2014 restates the existing doc. Don't duplicate; nothing to register.
24958
+ - **EXTENDS** \u2014 new ground the existing doc doesn't cover. Fine to register as-is.
24959
+ - **CONTRADICTS** \u2014 cuts against the existing doc. Surface to the owner \u2014 never resolve silently (task-3219: a contradiction is not a veto).
24960
+ - **SUPERSEDES** \u2014 this doc should replace the existing one. Re-run doc_register with \`superseded_by_path\` pointing at the overlapping doc's path.`;
24961
+ }
24780
24962
  async function handleDocRegister(adapter2, args, config2) {
24781
24963
  const adapterType = config2?.adapterType ?? "unknown";
24782
24964
  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 +25017,7 @@ async function handleDocRegister(adapter2, args, config2) {
24835
25017
  );
24836
25018
  }
24837
25019
  try {
25020
+ const overlaps = supersededByPath ? [] : await findOverlappingDocs(target, type, title, summary);
24838
25021
  let supersededBy;
24839
25022
  if (supersededByPath) {
24840
25023
  const existing = await target.getDoc?.(supersededByPath);
@@ -24910,7 +25093,7 @@ ${decision.message}`;
24910
25093
  - **Visibility:** ${visibilityLabel}
24911
25094
  - **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
24912
25095
  - **Actions:** ${actions?.length ?? 0} items
24913
- - **ID:** ${entry.id}` + bodyNote + durability
25096
+ - **ID:** ${entry.id}` + bodyNote + durability + overlapReconciliationNote(overlaps)
24914
25097
  );
24915
25098
  } catch (err) {
24916
25099
  const message = err instanceof Error ? err.message : String(err);
@@ -27595,7 +27778,7 @@ function resolveAdHocCycle(cycle, latest, latestComplete) {
27595
27778
  if (cycle === "current") return latest;
27596
27779
  return latestComplete ? latest + 1 : latest;
27597
27780
  }
27598
- async function recordAdHoc(adapter2, input) {
27781
+ async function recordAdHoc(adapter2, config2, input) {
27599
27782
  const [health, phases] = await Promise.all([
27600
27783
  adapter2.getCycleHealth(),
27601
27784
  adapter2.readPhases()
@@ -27617,9 +27800,16 @@ async function recordAdHoc(adapter2, input) {
27617
27800
  if (!existing) {
27618
27801
  throw new Error(`Task "${input.taskId}" not found on the board. Check the task ID and try again.`);
27619
27802
  }
27620
- if (!existing.assigneeId) {
27803
+ if (existing.assigneeId) {
27804
+ const gate = await resolveOwnerGate(adapter2, config2);
27805
+ if (gate.enforced && (!gate.callerUserId || gate.callerUserId !== existing.assigneeId)) {
27806
+ throw new Error(
27807
+ `Task "${input.taskId}" (${existing.title}) is claimed by another member \u2014 ad-hoc work cannot be recorded against it. Have the claimer record it, or run \`task_unclaim\` to release it first.`
27808
+ );
27809
+ }
27810
+ } else {
27621
27811
  warnings.push(
27622
- `\u2139\uFE0F ${input.taskId} has no assignee \u2014 recording ad-hoc work against an unclaimed pool task. If someone else might be building it too, check for an open PR/branch on the same files before merging.`
27812
+ `\u2139\uFE0F ${input.taskId} has no assignee \u2014 recording ad-hoc work against an unclaimed pool task. ` + (input.collidingWorkNote ?? `If someone else might be building it too, check for an open PR/branch on the same files before merging.`)
27623
27813
  );
27624
27814
  }
27625
27815
  const updatePayload = {
@@ -27876,7 +28066,22 @@ async function handleAdHoc(adapter2, config2, args) {
27876
28066
  const gitUsable = !overrideNote && isGitAvailable() && isGitRepo(config2.projectRoot);
27877
28067
  const currentBranch = gitUsable ? getCurrentBranch(config2.projectRoot) : null;
27878
28068
  const baseBranch = gitUsable ? resolveBaseBranch(config2.projectRoot, config2.baseBranch) : null;
27879
- const result = await recordAdHoc(target, {
28069
+ let collidingWorkNote;
28070
+ if (taskId && gitUsable && baseBranch) {
28071
+ try {
28072
+ const existingTask = await target.getTask(taskId);
28073
+ const filesLikelyTouched = existingTask?.buildHandoff?.filesLikelyTouched ?? [];
28074
+ if (existingTask && !existingTask.assigneeId && filesLikelyTouched.length > 0) {
28075
+ const collision = findCollidingWork(config2.projectRoot, baseBranch, filesLikelyTouched);
28076
+ if (collision) {
28077
+ const fileList = collision.files.slice(0, 3).join(", ") + (collision.files.length > 3 ? ", \u2026" : "");
28078
+ collidingWorkNote = `${collision.label} already touches ${collision.files.length === 1 ? "the same file" : `${collision.files.length} of the same files`} (${fileList}) \u2014 check it before merging.`;
28079
+ }
28080
+ }
28081
+ } catch {
28082
+ }
28083
+ }
28084
+ const result = await recordAdHoc(target, config2, {
27880
28085
  title: title || "",
27881
28086
  taskId,
27882
28087
  notes: rawNotes,
@@ -27891,7 +28096,8 @@ async function handleAdHoc(adapter2, config2, args) {
27891
28096
  stage: stageArg,
27892
28097
  hold: holdArg,
27893
28098
  currentBranch,
27894
- baseBranch
28099
+ baseBranch,
28100
+ collidingWorkNote
27895
28101
  });
27896
28102
  if (!holdArg && gitUsable) {
27897
28103
  try {
@@ -28923,6 +29129,27 @@ var reviewSubmitTool = {
28923
29129
  }
28924
29130
  },
28925
29131
  required: ["verdict", "summary", "findings"]
29132
+ },
29133
+ proposal: {
29134
+ type: "object",
29135
+ description: 'task-3388, OPTIONAL (build-acceptance + verdict:"accept" only): propose a Decision or a Convention the build or the review itself settled. Same mechanism as build_execute\u2019s `proposal` (task-3273) \u2014 PAPI never mints one for you. A proposal that passes all four admission tests is QUEUED for the owner behind a decision gate, and nothing is written as an Active Decision. YOU apply the four tests (they are stated in the planning prompts); the server routes on your answers and makes no judgement about the content. Fails "arguable today" only and it is recorded as a Convention instead, riding every future build. Fails "constrains future work" and it is returned to you with the reason, unrecorded.',
29136
+ properties: {
29137
+ title: { type: "string", description: "One line stating the stance or rule." },
29138
+ body: { type: "string", description: "What was decided or settled, and why." },
29139
+ module: { type: "string", description: "Optional module scope. Only used when this lands as a Convention." },
29140
+ tests: {
29141
+ type: "object",
29142
+ description: "The four admission tests, each answered explicitly. An omitted test is NOT a failed test \u2014 the proposal is refused rather than routed.",
29143
+ properties: {
29144
+ alternativesWereReal: { type: "boolean", description: "(a) Something else could genuinely have been chosen." },
29145
+ constrainsFutureWork: { type: "boolean", description: "(b) It changes what a task nobody has written yet will do." },
29146
+ arguableToday: { type: "boolean", description: "(c) A competent person could argue the other side right now." },
29147
+ reversalCostsMore: { type: "boolean", description: "(d) Reversing it costs more than making it did." }
29148
+ },
29149
+ required: ["alternativesWereReal", "constrainsFutureWork", "arguableToday", "reversalCostsMore"]
29150
+ }
29151
+ },
29152
+ required: ["title", "body", "tests"]
28926
29153
  }
28927
29154
  },
28928
29155
  required: ["task_id", "stage", "verdict", "comments"],
@@ -29538,6 +29765,30 @@ Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
29538
29765
  }
29539
29766
  } catch {
29540
29767
  }
29768
+ let proposalNote = "";
29769
+ if (args.proposal !== void 0) {
29770
+ if (!(stage === "build-acceptance" && verdict === "accept")) {
29771
+ proposalNote = "\n\n---\n\n**Proposal not recorded.** `proposal` is only accepted on a build-acceptance `accept` \u2014 nothing has settled on a handoff-review or a non-accept verdict.\n\nThe review itself is unaffected \u2014 only the proposal was skipped.";
29772
+ } else {
29773
+ const validated = validateProposal(args.proposal);
29774
+ if ("error" in validated) {
29775
+ proposalNote = `
29776
+
29777
+ ---
29778
+
29779
+ **Proposal not recorded.** ${validated.error}
29780
+
29781
+ The review itself is unaffected \u2014 only the proposal failed.`;
29782
+ } else {
29783
+ const routing = routeProposal(validated.proposal);
29784
+ const applied = await applyProposal(adapter2, routing, validated.proposal, {
29785
+ cycleNumber: result.currentCycle,
29786
+ sourceTaskId: result.taskId
29787
+ });
29788
+ proposalNote = formatProposalOutcome(routing, validated.proposal, applied);
29789
+ }
29790
+ }
29791
+ }
29541
29792
  tracker.mark("format-response");
29542
29793
  return textResponse(
29543
29794
  `**${result.stageLabel}** recorded for ${result.taskId}.
@@ -29545,7 +29796,7 @@ Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
29545
29796
  - **Verdict:** ${result.verdict}
29546
29797
  - **Comments:** ${trimForEcho(result.comments)}
29547
29798
 
29548
- ${statusNote}${capabilityReviewSkippedNote}${autoReviewNote}${securityNote}${adConflictNote}${unblockNote}${docClosureNote}${regenNote}${mergeNote}${overlapNote}${batchSummaryNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
29799
+ ${statusNote}${capabilityReviewSkippedNote}${autoReviewNote}${securityNote}${adConflictNote}${unblockNote}${docClosureNote}${regenNote}${mergeNote}${overlapNote}${batchSummaryNote}${autoReleaseNote}${nextStepNote}${phaseNote}${proposalNote}`
29549
29800
  );
29550
29801
  } catch (err) {
29551
29802
  const message = err instanceof Error ? err.message : String(err);
@@ -30579,6 +30830,41 @@ function formatUnblockSection(candidates) {
30579
30830
  return lines.join("\n");
30580
30831
  }
30581
30832
 
30833
+ // src/lib/decision-drafts.ts
30834
+ async function findPendingDecisionDrafts(adapter2) {
30835
+ let blockedProbe;
30836
+ try {
30837
+ blockedProbe = await adapter2.queryBoard({ status: ["Blocked"], compact: true });
30838
+ } catch {
30839
+ return [];
30840
+ }
30841
+ if (blockedProbe.length === 0) return [];
30842
+ const drafts = [];
30843
+ for (const task of blockedProbe) {
30844
+ const blocker = task.blocker;
30845
+ if (!blocker || blocker.type !== "decision-gate" || blocker.ref !== PROPOSED_DECISION_REF) continue;
30846
+ const evidence = (task.notes ?? "").split("\n")[0]?.trim() ?? "";
30847
+ drafts.push({
30848
+ taskId: task.displayId ?? task.id,
30849
+ taskTitle: task.title,
30850
+ evidence
30851
+ });
30852
+ }
30853
+ return drafts;
30854
+ }
30855
+ function formatPendingDecisionDrafts(drafts) {
30856
+ if (drafts.length === 0) return "";
30857
+ const lines = ["## Decisions Awaiting Your Call"];
30858
+ lines.push(
30859
+ `${drafts.length} decision${drafts.length === 1 ? "" : "s"} proposed during a build, waiting on you \u2014 mint with \`strategy_change\` to make it a real Active Decision, or \`board_edit <task> status:Cancelled\` to dismiss it. Neither is automatic.`
30860
+ );
30861
+ lines.push("");
30862
+ for (const d of drafts) {
30863
+ lines.push(`- **${formatRef(d.taskId, d.taskTitle)}**${d.evidence ? ` \u2014 ${d.evidence}` : ""}`);
30864
+ }
30865
+ return lines.join("\n");
30866
+ }
30867
+
30582
30868
  // src/lib/deferred-gate.ts
30583
30869
  var GATE_PHRASES = [
30584
30870
  "depends on",
@@ -32121,7 +32407,8 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
32121
32407
  }
32122
32408
  let unrecordedNote2 = "";
32123
32409
  try {
32124
- const unrecorded = detectUnrecordedCommits(config2.projectRoot, config2.baseBranch);
32410
+ const { compareRef } = getBaseDivergence(config2.projectRoot, config2.baseBranch, { timeoutMs: ORIGIN_FETCH_TIMEOUT_MS });
32411
+ const unrecorded = detectUnrecordedCommits(config2.projectRoot, compareRef);
32125
32412
  if (unrecorded.length > 0) {
32126
32413
  const doneTasks = await adapter2.queryBoard({ status: ["Done"] });
32127
32414
  const adHocDoneTasks = doneTasks.filter((t) => t.cycle == null);
@@ -32250,8 +32537,14 @@ ${versionDrift}` : "";
32250
32537
  }
32251
32538
  tracker.mark("parallel-tail");
32252
32539
  const sharedContributorsPromise = adapterSupports(adapter2, "listContributors") ? adapter2.listContributors().catch(() => []) : Promise.resolve([]);
32253
- const [unblockCandidates, subAgents, teamSummaryLine, releaseHistoryLine, cohortVisibilityLine, carryForwardRefs] = await Promise.all([
32540
+ const [unblockCandidates, pendingDecisionDrafts, subAgents, teamSummaryLine, releaseHistoryLine, cohortVisibilityLine, carryForwardRefs] = await Promise.all([
32254
32541
  tracked("unblock-candidates", () => findUnblockCandidates(adapter2, currentCycle2))().catch(() => []),
32542
+ // task-3388: pending decision-proposal drafts (build_execute/review_submit's
32543
+ // `proposal` param, task-3273/3388) — mandatory reader, deliberately
32544
+ // uncapped and separate from the housekeeping-suggestion unblock list above
32545
+ // (see lib/decision-drafts.ts for why). Runs on every orient call, not
32546
+ // gated behind deep_housekeeping — same posture as unblock-candidates.
32547
+ tracked("pending-decision-drafts", () => findPendingDecisionDrafts(adapter2))().catch(() => []),
32255
32548
  // task-1866: discover project sub-agents for the orient surface (read-only, never throws).
32256
32549
  listAgents(config2.projectRoot),
32257
32550
  // task-2071 (MU-3) + task-2072 (MU-5): team summary + release-history — both
@@ -32270,6 +32563,10 @@ ${versionDrift}` : "";
32270
32563
  const unblockNote = unblockSection ? `
32271
32564
 
32272
32565
  ${unblockSection}` : "";
32566
+ const decisionDraftsSection = formatPendingDecisionDrafts(pendingDecisionDrafts);
32567
+ const decisionDraftsNote = decisionDraftsSection ? `
32568
+
32569
+ ${decisionDraftsSection}` : "";
32273
32570
  const teamSummary = [teamSummaryLine, releaseHistoryLine, cohortVisibilityLine].filter(Boolean).join("\n") || void 0;
32274
32571
  let deferredGateNote = "";
32275
32572
  if (deepHousekeeping) {
@@ -32300,7 +32597,42 @@ ${section}`;
32300
32597
  const runtimeIdentityNote = `
32301
32598
 
32302
32599
  ${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 };
32600
+ let effDeepHint = deepHint;
32601
+ let effOnboardingCoachingNote = onboardingCoachingNote;
32602
+ let effStaleSkillsNote = staleSkillsNote;
32603
+ let effResearchSignalsNote = researchSignalsNote;
32604
+ const droppedNotes = [];
32605
+ const assembleOrientOutput = () => projectOverrideLine + projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName, carryForwardRefs) + runtimeIdentityNote + unblockNote + decisionDraftsNote + deferredGateNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + effStaleSkillsNote + effResearchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + effOnboardingCoachingNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + effDeepHint + enrichmentFilesSection;
32606
+ const orientBudgetSoft = Number(process.env.PAPI_ORIENT_CONTEXT_BUDGET) || 6e4;
32607
+ let assembled = assembleOrientOutput();
32608
+ if (Buffer.byteLength(assembled, "utf-8") > orientBudgetSoft && effDeepHint) {
32609
+ effDeepHint = "";
32610
+ droppedNotes.push("deep-housekeeping tip");
32611
+ assembled = assembleOrientOutput();
32612
+ }
32613
+ if (Buffer.byteLength(assembled, "utf-8") > orientBudgetSoft && effOnboardingCoachingNote) {
32614
+ effOnboardingCoachingNote = "";
32615
+ droppedNotes.push("onboarding coaching");
32616
+ assembled = assembleOrientOutput();
32617
+ }
32618
+ if (Buffer.byteLength(assembled, "utf-8") > orientBudgetSoft && effStaleSkillsNote) {
32619
+ effStaleSkillsNote = "";
32620
+ droppedNotes.push("stale skills");
32621
+ assembled = assembleOrientOutput();
32622
+ }
32623
+ if (Buffer.byteLength(assembled, "utf-8") > orientBudgetSoft && effResearchSignalsNote) {
32624
+ effResearchSignalsNote = "";
32625
+ droppedNotes.push("research signals");
32626
+ assembled = assembleOrientOutput();
32627
+ }
32628
+ if (droppedNotes.length > 0) {
32629
+ assembled += `
32630
+
32631
+ ---
32632
+
32633
+ *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.*`;
32634
+ }
32635
+ return { ...textResponse(assembled), _cycleNumber: healthResult.cycleNumber };
32304
32636
  } catch (err) {
32305
32637
  const message = err instanceof Error ? err.message : String(err);
32306
32638
  const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
@@ -33561,7 +33893,7 @@ var decisionResolveTool = {
33561
33893
  required: ["ad_id", "action"]
33562
33894
  }
33563
33895
  };
33564
- function findDecision(decisions, adId) {
33896
+ function findDecision2(decisions, adId) {
33565
33897
  const trimmed = adId.trim();
33566
33898
  return decisions.find((d) => d.id === trimmed || d.displayId === trimmed);
33567
33899
  }
@@ -33594,7 +33926,7 @@ async function handleDecisionResolve(adapter2, config2, args) {
33594
33926
  } catch (err) {
33595
33927
  return errorResponse(`Failed to read active decisions: ${err instanceof Error ? err.message : String(err)}`);
33596
33928
  }
33597
- const target = findDecision(decisions, adId);
33929
+ const target = findDecision2(decisions, adId);
33598
33930
  if (!target) return errorResponse(`AD not found: ${adId}. Run ad_view to see available decisions.`);
33599
33931
  if (target.resolutionState !== "proposed") {
33600
33932
  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.105",
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",