@papi-ai/server 0.7.60 → 0.7.62

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.
@@ -88,6 +88,13 @@ Your output is **not done** if ANY of these is true (these are falsifiable — c
88
88
  - [ ] Guidance, state, or a next action explained in a sentence of helper text where a visual cue
89
89
  (position, size, colour, icon, a signpost, motion-on-change) would carry it — prose used as a
90
90
  crutch for missing visual steering.
91
+ - [ ] A label, its value, and its explanation each taking their own line ("The Stacked Caption").
92
+ Anywhere the three exist, they share ONE line.
93
+ - [ ] A new datum added as a sibling row beside a row it could extend ("The Siblinged Row").
94
+ Extend the existing row — inline suffix, extra cell, chip on the same baseline — never stack
95
+ a twin row underneath.
96
+ - [ ] An eyebrow label or an explanatory subtitle above/below a heading ("The Eyebrow Crutch").
97
+ Labels carry themselves; if a heading needs a subtitle to be understood, the heading is wrong.
91
98
  - [ ] More than ~3 sections fully visible above the fold.
92
99
  - [ ] Identical-sized cards in a uniform row (flat hierarchy — needs a dominant 2×+ cell).
93
100
  - [ ] Monospace on labels, status, timestamps, or nav.
@@ -108,6 +108,12 @@ Run through every named anti-pattern and flag matches. Common universal ones:
108
108
  - Guidance, state, or a next action carried by a sentence of helper text where a visual cue
109
109
  (position, size, colour, an icon, a signpost, motion-on-change) would do — prose as a crutch for
110
110
  missing visual steering. Prefer showing over telling.
111
+ - A label, its value, and its explanation each taking their own line ("The Stacked Caption") —
112
+ anywhere the three exist, they share ONE line.
113
+ - A new datum added as a sibling row beside a row it could extend ("The Siblinged Row") — extend
114
+ the existing row (inline suffix, extra cell, chip on the same baseline), never stack a twin row.
115
+ - An eyebrow label or explanatory subtitle above/below a heading ("The Eyebrow Crutch") — labels
116
+ carry themselves; a heading that needs a subtitle to be understood is the wrong heading.
111
117
  - Identical-sized cards in a uniform row (flat hierarchy).
112
118
  - Monospace on labels / status / timestamps / nav.
113
119
  - Header, label, and value at the same size+weight.
@@ -1189,7 +1189,12 @@ var init_proxy_adapter = __esm({
1189
1189
  "listContributorReleasePrs",
1190
1190
  "claimReview",
1191
1191
  "getSiblingAds",
1192
- "getSiblingRepoTasks"
1192
+ "getSiblingRepoTasks",
1193
+ // task-2828 (C339): attributed-intelligence analytics reader — pg-only this cycle.
1194
+ // Hosted forwarding needs a SECURITY DEFINER RPC + edge handler (like task-2394 did
1195
+ // for getModuleEstimationStats); until then keep it here so hosted degrades to a
1196
+ // safe `undefined` rather than forwarding into a 403. Wire under task-2390.
1197
+ "getModelOutcomeStats"
1193
1198
  // task-2394 (C329) — Batch A wired: findPendingDocActionsForTask,
1194
1199
  // getModuleEstimationStats and getDecisionScorePatterns now have edge case handlers
1195
1200
  // (each backed by a SECURITY DEFINER RPC, migration 20260714140000) plus
package/dist/index.js CHANGED
@@ -1306,7 +1306,12 @@ var init_proxy_adapter = __esm({
1306
1306
  "listContributorReleasePrs",
1307
1307
  "claimReview",
1308
1308
  "getSiblingAds",
1309
- "getSiblingRepoTasks"
1309
+ "getSiblingRepoTasks",
1310
+ // task-2828 (C339): attributed-intelligence analytics reader — pg-only this cycle.
1311
+ // Hosted forwarding needs a SECURITY DEFINER RPC + edge handler (like task-2394 did
1312
+ // for getModuleEstimationStats); until then keep it here so hosted degrades to a
1313
+ // safe `undefined` rather than forwarding into a 403. Wire under task-2390.
1314
+ "getModelOutcomeStats"
1310
1315
  // task-2394 (C329) — Batch A wired: findPendingDocActionsForTask,
1311
1316
  // getModuleEstimationStats and getDecisionScorePatterns now have edge case handlers
1312
1317
  // (each backed by a SECURITY DEFINER RPC, migration 20260714140000) plus
@@ -5054,7 +5059,7 @@ var init_setup = __esm({
5054
5059
 
5055
5060
  // src/index.ts
5056
5061
  import { readFileSync as readFileSync16 } from "fs";
5057
- import { dirname as dirname6, join as join24 } from "path";
5062
+ import { dirname as dirname6, join as join24, basename as basename2 } from "path";
5058
5063
  import { fileURLToPath as fileURLToPath4 } from "url";
5059
5064
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5060
5065
  import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
@@ -10946,7 +10951,7 @@ function computeCarryForwardStaleness(log2, doneTaskIds) {
10946
10951
  if (stale.length === 0) return void 0;
10947
10952
  const lines = stale.map(([id, count]) => `- **${id}** \u2014 deferred ${count} consecutive cycle(s)`);
10948
10953
  return [
10949
- `\u26A0\uFE0F ${stale.length} task(s) have been in carry-forward for 3+ consecutive cycles. The planner must resolve each \u2014 either escalate to P1 High or recommend cancellation with a closure reason. Deferring again without justification is not acceptable.`,
10954
+ `\u26A0\uFE0F ${stale.length} task(s) have been in carry-forward for 3+ consecutive cycles. The planner must resolve each \u2014 either escalate it to P1 High, or move it to Deferred with a reason. Do NOT cancel a stale carry-forward task: cancelling removes it from the owner's field of view, and only the owner may cancel. Deferring again without a fresh justification is not acceptable.`,
10950
10955
  "",
10951
10956
  ...lines
10952
10957
  ].join("\n");
@@ -11096,6 +11101,15 @@ async function resolveOwnerGate(adapter2, config2) {
11096
11101
 
11097
11102
  // src/services/plan.ts
11098
11103
  var PLAN_BUILD_REPORT_BUDGET = { maxReports: 12, fieldBudget: 280 };
11104
+ function leadChainWithRecommended(chain, recommendedTaskId) {
11105
+ const rec = recommendedTaskId?.trim();
11106
+ const existing = (chain ?? "").trim();
11107
+ if (!rec) return existing || void 0;
11108
+ const firstMentioned = existing.match(/task-\d+/)?.[0];
11109
+ if (firstMentioned === rec) return existing || void 0;
11110
+ const lead = `Build order: ${rec} first.`;
11111
+ return existing ? `${lead} ${existing}` : lead;
11112
+ }
11099
11113
  async function resolvePlanScope(adapter2, config2) {
11100
11114
  const gate = await resolveOwnerGate(adapter2, config2);
11101
11115
  return { callerUserId: gate.callerUserId, ownerUserId: gate.ownerUserId, callerIsOwner: gate.callerIsOwner };
@@ -12044,7 +12058,12 @@ ${cleanContent}`;
12044
12058
  // project owner when unset, so owner-operated plans stay correct).
12045
12059
  ...options.ownerUserId ? { userId: options.ownerUserId } : {},
12046
12060
  // task-2483 (C319): persist the plan's Dependency Chain build-order markdown.
12047
- ...data.dependencyChain ? { dependencyChain: data.dependencyChain } : {}
12061
+ // task-2823 (C338): seed it to lead with recommendedTaskId when the plan omits a
12062
+ // chain or it doesn't lead with the planner's pick, so the hub next-move agrees.
12063
+ ...(() => {
12064
+ const chain = leadChainWithRecommended(data.dependencyChain, data.recommendedTaskId);
12065
+ return chain ? { dependencyChain: chain } : {};
12066
+ })()
12048
12067
  };
12049
12068
  const earlyCreateWarnings = [];
12050
12069
  try {
@@ -12477,7 +12496,13 @@ ${cleanContent}`;
12477
12496
  ...options.ownerUserId ? { userId: options.ownerUserId } : {},
12478
12497
  // task-2483 (C319): persist the plan's Dependency Chain build-order markdown.
12479
12498
  // This full write patches the early minimal row created in Phase 1.
12480
- ...data.dependencyChain ? { dependencyChain: data.dependencyChain } : {}
12499
+ // task-2823 (C338): seed it to lead with recommendedTaskId (mapped from any
12500
+ // new-task tempId) so the hub next-move matches the planner's explicit pick.
12501
+ ...(() => {
12502
+ const recId = data.recommendedTaskId ? newTaskIdMap.get(data.recommendedTaskId) ?? data.recommendedTaskId : null;
12503
+ const chain = leadChainWithRecommended(data.dependencyChain, recId);
12504
+ return chain ? { dependencyChain: chain } : {};
12505
+ })()
12481
12506
  };
12482
12507
  await adapter2.createCycle(cycle);
12483
12508
  } catch (err) {
@@ -12740,6 +12765,7 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
12740
12765
  const contextBytes2 = Buffer.byteLength(userMessage2, "utf-8");
12741
12766
  console.error(`[plan-perf] contextBytes=${contextBytes2} (handoffs-only)`);
12742
12767
  const planSystemPrompt2 = await getPrompt("plan-system");
12768
+ await recordPlanGenerationActive(tracker, incomingCycle);
12743
12769
  return {
12744
12770
  mode: "full",
12745
12771
  // apply phase treats it the same
@@ -12800,6 +12826,7 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
12800
12826
  } catch {
12801
12827
  }
12802
12828
  const planSystemPrompt = await getPrompt("plan-system");
12829
+ await recordPlanGenerationActive(tracker, incomingCycle);
12803
12830
  return {
12804
12831
  mode,
12805
12832
  cycleNumber,
@@ -12823,6 +12850,15 @@ async function streamPlanStageSteps(tracker, newCycleNumber) {
12823
12850
  await tracker.recordStep(step);
12824
12851
  }
12825
12852
  }
12853
+ async function recordPlanGenerationActive(tracker, incomingCycleNumber) {
12854
+ if (!tracker) return;
12855
+ await tracker.recordStep("recommendation", {
12856
+ cycle: incomingCycleNumber,
12857
+ stage: "plan",
12858
+ status: "active",
12859
+ metadata: { phase: "generating" }
12860
+ });
12861
+ }
12826
12862
  async function applyPlan(adapter2, config2, rawLlmOutput, mode, cycleNumber, strategyReviewWarning, contextHashes, planRunMeta, tracker) {
12827
12863
  const applyTimer = startTimer();
12828
12864
  console.error(`[plan-perf] applyPlan: start (llm_response=${rawLlmOutput.length} chars)`);
@@ -13053,7 +13089,6 @@ function defaultHint(tool) {
13053
13089
  function buildSubagentDispatchPrompt(input) {
13054
13090
  const {
13055
13091
  tool,
13056
- applyMode,
13057
13092
  cycleNumber = 0,
13058
13093
  strategyReviewWarning = "",
13059
13094
  taskId,
@@ -13065,7 +13100,7 @@ function buildSubagentDispatchPrompt(input) {
13065
13100
  const isReview = tool === "review_submit";
13066
13101
  let applyNote;
13067
13102
  if (tool === "plan") {
13068
- applyNote = `\`plan\` again with mode="apply", llm_response=<sub-agent output>, plan_mode="${applyMode ?? "full"}", cycle_number=${cycleNumber + 1}, strategy_review_warning=${JSON.stringify(strategyReviewWarning)}`;
13103
+ applyNote = `\`plan\` again with mode="apply", llm_response=<sub-agent output>, cycle_number=${cycleNumber + 1}, strategy_review_warning=${JSON.stringify(strategyReviewWarning)}`;
13069
13104
  } else if (tool === "strategy_review") {
13070
13105
  applyNote = `\`strategy_review\` again with mode="apply", llm_response=<sub-agent output>, cycle_number=${cycleNumber}`;
13071
13106
  } else if (tool === "zoom_out") {
@@ -13129,6 +13164,105 @@ Do NOT post-process the sub-agent reply. The apply path expects the full structu
13129
13164
  step2Note
13130
13165
  ].join("\n");
13131
13166
  }
13167
+ var REVIEW_PRESETS = {
13168
+ // Minimal gate: does it work, and is every write actually read (AD-74)?
13169
+ gate: ["correctness", "wiring"],
13170
+ // Everything.
13171
+ full: ["correctness", "security", "wiring", "test-quality", "scope-drift"],
13172
+ // Security first, plus correctness and a lighter wiring check.
13173
+ "security-focused": ["security", "correctness", "wiring"]
13174
+ };
13175
+ var LENS_INSTRUCTIONS = {
13176
+ correctness: "CORRECTNESS lens. Does the change do what the build report claims, without bugs? Hunt logic errors, off-by-one/boundary mistakes, unhandled error branches, null/undefined hazards, race conditions, and wrong assumptions about inputs. Ignore style, security, and scope \u2014 other lenses own those.",
13177
+ security: "SECURITY lens. Any auth/data/secret/injection risk introduced? Check for missing project_id scoping on multi-tenant SQL, RLS bypass via service role, unescaped input, leaked credentials/tokens in logs or responses, and newly-public routes (PUBLIC_PATHS). Ignore correctness/style unless it is the vector for a security issue.",
13178
+ wiring: "WIRING lens (AD-74 \u2014 producer\u2192store\u2192consumer). A write with no reader is NOT done. For every new value produced (state, DB column, event, field), confirm something actually CONSUMES it \u2014 grep for the reader. localStorage is a cache, never a store. Flag decorative writes, dead affordances, and green gates that guard a component nothing imports. Ignore correctness/security details \u2014 focus on whether the data path is actually connected end-to-end.",
13179
+ "test-quality": "TEST-QUALITY lens. Are the tests present and meaningful? A test that cannot fail is worse than none. Check that assertions test TRANSITIONS not snapshots, that a probe would actually go RED if the code broke, and that new behaviour is covered. Flag tests that assert on mocks instead of real behaviour, and missing coverage on the changed paths.",
13180
+ "scope-drift": "SCOPE-DRIFT lens. Does the diff match the BUILD HANDOFF scope? Flag unrelated changes, files touched outside the stated scope, new dependencies/abstractions not asked for, and \u2014 conversely \u2014 acceptance criteria from the handoff that were skipped. Ignore whether the in-scope code is correct; only judge whether it is the RIGHT set of changes."
13181
+ };
13182
+ var SYNTHESIS_CONTRACT_SHAPE = '{"verdict":"pass|warn|fail","summary":"<one-line consolidated assessment>","findings":[{"severity":"error|warning|info","file":"<path>","line":<number>,"message":"<specific issue>"}]}';
13183
+ var LEG_CONTRACT_SHAPE = '{"lens":"<lens-name>","verdict":"pass|warn|fail","findings":[{"severity":"error|warning|info","file":"<path>","line":<number>,"message":"<specific issue>"}]}';
13184
+ function legId(lens) {
13185
+ return `leg-${lens}`;
13186
+ }
13187
+ function buildFanoutReviewDispatchPrompt(input) {
13188
+ const { taskId, preset, systemPrompt, userMessage, contextBytes } = input;
13189
+ const lenses = REVIEW_PRESETS[preset];
13190
+ const n = lenses.length;
13191
+ const sizeNote = contextBytes !== void 0 ? ` \xB7 ~${Math.round(contextBytes / 1024)} KB context` : "";
13192
+ const sharedContext = `<review_rubric>
13193
+ ${systemPrompt}
13194
+ </review_rubric>
13195
+
13196
+ <build_under_review>
13197
+ ${userMessage}
13198
+ </build_under_review>`;
13199
+ const legBlocks = lenses.map((lens, i) => {
13200
+ const legPrompt = `You are lens ${i + 1} of ${n} in a PAPI multi-lens build review. Review the completed build below through ONE lens only.
13201
+
13202
+ LENS \u2014 ${LENS_INSTRUCTIONS[lens]}
13203
+
13204
+ CONTRACT:
13205
+ - The build report says what was intended; the diff is what actually changed.
13206
+ - Report ONLY findings your lens (${lens}) is responsible for. Do not stray into other lenses.
13207
+ - Return ONLY a single JSON object \u2014 no preamble, no commentary, no fences \u2014 in exactly this shape:
13208
+ ${LEG_CONTRACT_SHAPE.replace("<lens-name>", lens)}
13209
+ - "pass" = no blocking issues for this lens; "warn" = minor/non-blocking; "fail" = a blocking issue this lens found. \`file\`/\`line\` are optional. Empty \`findings\` is valid when your lens is clean.
13210
+
13211
+ ${sharedContext}`;
13212
+ return [
13213
+ `#### Lens ${i + 1}/${n} \u2014 ${lens} \`[${legId(lens)}]\``,
13214
+ "",
13215
+ `<<<BEGIN_LENS_PROMPT:${lens}>>>`,
13216
+ legPrompt,
13217
+ `<<<END_LENS_PROMPT:${lens}>>>`
13218
+ ].join("\n");
13219
+ });
13220
+ const dependsOn = `[${lenses.map(legId).join(", ")}]`;
13221
+ const synthesisPrompt = `You are the SYNTHESIS step of a PAPI multi-lens build review.
13222
+
13223
+ depends_on = ${dependsOn}
13224
+
13225
+ You receive the JSON outputs of all ${n} specialist lens legs below. Your job:
13226
+ - Merge every leg's findings into ONE consolidated review.
13227
+ - Dedupe: collapse findings that multiple lenses raised about the same file/line/issue into a single entry (keep the highest severity).
13228
+ - Prioritize: order findings most-severe first (error \u2192 warning \u2192 info).
13229
+ - Decide ONE overall verdict: "fail" if any leg found a blocking issue, "warn" if only minor nits remain, "pass" if all legs were clean.
13230
+ - Return ONLY a single JSON object \u2014 no preamble, no commentary, no fences \u2014 in exactly this shape (the review_submit auto_review contract):
13231
+ ${SYNTHESIS_CONTRACT_SHAPE}
13232
+
13233
+ <lens_leg_outputs>
13234
+ Paste every lens leg's JSON output here, one per line or concatenated:
13235
+ <<<PASTE_ALL_LEG_OUTPUTS_HERE>>>
13236
+ </lens_leg_outputs>`;
13237
+ const applyNote = `\`review_submit\` with task_id="${taskId}", stage="build-acceptance", verdict=<your decision: accept / request-changes / reject>, comments=<your reasoning>, and auto_review set to the synthesis JSON object verbatim. The synthesized verdict is a RECOMMENDATION \u2014 you make the final call.`;
13238
+ return [
13239
+ `## PAPI review_submit \u2014 Multi-Lens Fan-Out Review (${taskId} \xB7 preset: ${preset} \xB7 ${n} lens${n === 1 ? "" : "es"} + synthesis${sizeNote})`,
13240
+ "",
13241
+ "This build review fans out across specialist lenses: each lens below reviews the SAME diff in parallel, then a synthesis step dedupes their findings into one prioritized verdict. **PAPI emits this prompt \u2014 YOU (the host) run the Task calls. PAPI never executes the agents itself.**",
13242
+ "",
13243
+ "---",
13244
+ "",
13245
+ `### Step 1 \u2014 Dispatch ${n} specialist lens${n === 1 ? "" : "es"} IN PARALLEL`,
13246
+ "",
13247
+ `Make ${n} \`Task\` call${n === 1 ? "" : "s"} in a SINGLE message so they run concurrently. Each uses \`subagent_type: general-purpose\`. For each lens, pass the prompt block between its BEGIN/END markers (trimmed of the markers). Each leg returns ONLY its own tagged JSON.`,
13248
+ "",
13249
+ legBlocks.join("\n\n"),
13250
+ "",
13251
+ "### Step 2 \u2014 Synthesis (`depends_on` = ALL lens legs)",
13252
+ "",
13253
+ `Wait until ALL ${n} lens legs above have returned. The synthesis step below declares \`depends_on = ${dependsOn}\` \u2014 do NOT run it before every leg is back. Make ONE more \`Task\` call (\`subagent_type: general-purpose\`) with the block below, pasting every leg's JSON output where marked.`,
13254
+ "",
13255
+ "<<<BEGIN_SYNTHESIS_PROMPT>>>",
13256
+ synthesisPrompt,
13257
+ "<<<END_SYNTHESIS_PROMPT>>>",
13258
+ "",
13259
+ "### Step 3 \u2014 Apply the synthesized verdict",
13260
+ "",
13261
+ `When synthesis returns its JSON, surface the consolidated findings, decide your verdict, then call ${applyNote}`,
13262
+ "",
13263
+ "Do NOT post-process the synthesis JSON \u2014 the `auto_review` apply path expects it exactly as emitted."
13264
+ ].join("\n");
13265
+ }
13132
13266
 
13133
13267
  // src/lib/resolve-llm-response.ts
13134
13268
  import { readFile as readFile3, stat } from "fs/promises";
@@ -21049,6 +21183,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
21049
21183
  completed: capitalizeCompleted(input.completed),
21050
21184
  actualEffort: input.effort,
21051
21185
  estimatedEffort: input.estimatedEffort,
21186
+ model: input.model,
21052
21187
  surprises: input.surprises,
21053
21188
  discoveredIssues: input.discoveredIssues,
21054
21189
  architectureNotes: input.architectureNotes,
@@ -21985,6 +22120,11 @@ var buildExecuteTool = {
21985
22120
  enum: ["XS", "S", "M", "L", "XL"],
21986
22121
  description: "Estimated effort from the BUILD HANDOFF. Required for complete."
21987
22122
  },
22123
+ model: {
22124
+ type: "string",
22125
+ maxLength: 120,
22126
+ description: "The model/agent that executed this build, self-reported, e.g. 'claude-opus-4' / 'gpt-5' \u2014 optional; powers attributed-intelligence analytics. PAPI never selects a model (AD-58) \u2014 report the model YOU are running as."
22127
+ },
21988
22128
  surprises: {
21989
22129
  type: "string",
21990
22130
  maxLength: 800,
@@ -21993,7 +22133,7 @@ var buildExecuteTool = {
21993
22133
  discovered_issues: {
21994
22134
  type: "string",
21995
22135
  maxLength: 800,
21996
- description: `Problems found DURING this build that are OUTSIDE this task's scope. Include severity (P0-P3). Good: "P2: Auth middleware doesn't validate token expiry \u2014 affects all protected routes." Bad: "Had to install a dependency." Only real bugs or gaps that need their own task. Use "None" if none. Required for complete. TIP: When submitting a follow-up idea for a discovered issue, include "learning:<uuid>" in the idea notes to link it to this cycle learning entry \u2014 use the UUID returned in the build completion output.`
22136
+ description: `A REAL bug or gap OUTSIDE this task's scope that needs its own task. Include severity (P0-P3). INCLUSION TEST \u2014 file it only if BOTH are true: (1) the bug would exist even if this task had never run, and (2) it is outside this task's scope. If either is false, it is NOT a discovered_issue. NEVER file a trivial in-file cleanup you noticed while editing (an em-dash tidy, a rename, a lint nit, a stray console.log) \u2014 those are not bugs, just clean them up or leave them. Good: "P2: Auth middleware doesn't validate token expiry \u2014 affects all protected routes." Bad: "Two em dashes remain in the file" / "Had to install a dependency." Use "None" if none. Required for complete. TIP: When submitting a follow-up idea for a discovered issue, include "learning:<uuid>" in the idea notes to link it to this cycle learning entry \u2014 use the UUID returned in the build completion output.`
21997
22137
  },
21998
22138
  architecture_notes: {
21999
22139
  type: "string",
@@ -22279,7 +22419,7 @@ async function handleBuildExecute(adapter2, config2, args, clientName) {
22279
22419
  **PRE-BUILD VERIFICATION:** Before writing any code, read these files and check if the functionality already exists:
22280
22420
  ${verificationFiles.map((f) => `- ${f}`).join("\n")}
22281
22421
  If >80% of the scope is already implemented, call \`build_execute\` with completed="yes" and note "already built" in surprises instead of re-implementing.` : "";
22282
- const chainInstruction = '\n\n---\n\n**IMPORTANT:** After implementing this task, immediately call `build_execute` again with report fields (`completed`, `effort`, `estimated_effort`, `surprises`, `discovered_issues`, `architecture_notes`) to complete the build. Do not wait for user confirmation.\n\n**Build Report Quality Bar:**\n- **surprises**: What was DIFFERENT from expected \u2014 wrong assumptions, scope changes, missing infrastructure. NOT implementation mechanics ("used X library").\n- **discovered_issues**: Bugs/gaps OUTSIDE this task\'s scope, with severity (P0-P3). NOT "had to install a dependency".\n- **architecture_notes**: Patterns/decisions that AFFECT FUTURE WORK. NOT "used React hooks".\n- If nothing meaningful to report for a field, use "None" \u2014 empty signal is better than noise.\n- **dead_ends**: approaches you tried and RULED OUT (with why). Ruled-out paths are first-class intelligence \u2014 send "None" only if nothing you tried failed.';
22422
+ const chainInstruction = '\n\n---\n\n**IMPORTANT:** After implementing this task, immediately call `build_execute` again with report fields (`completed`, `effort`, `estimated_effort`, `surprises`, `discovered_issues`, `architecture_notes`) to complete the build. Do not wait for user confirmation.\n\n**Build Report Quality Bar:**\n- **surprises**: What was DIFFERENT from expected \u2014 wrong assumptions, scope changes, missing infrastructure. NOT implementation mechanics ("used X library").\n- **discovered_issues**: REAL bugs/gaps OUTSIDE this task\'s scope, with severity (P0-P3). Inclusion test: file it only if the bug would exist even if this task had never run AND it is outside this task\'s scope. NEVER a trivial in-file cleanup you noticed while editing (em-dash tidy, rename, lint nit) \u2014 those are not discovered_issues. NOT "had to install a dependency".\n- **architecture_notes**: Patterns/decisions that AFFECT FUTURE WORK. NOT "used React hooks".\n- If nothing meaningful to report for a field, use "None" \u2014 empty signal is better than noise.\n- **dead_ends**: approaches you tried and RULED OUT (with why). Ruled-out paths are first-class intelligence \u2014 send "None" only if nothing you tried failed.';
22283
22423
  const buildDisciplineNote = "\n\n---\n\n**BUILD DISCIPLINE:**\n- **Read before you claim.** Before asserting how something works \u2014 or that it is already done \u2014 read the actual code/state. Don't rely on memory or earlier context.\n- **Sweep existing material first.** Check the docs (`doc_search` or your docs index) and prior tasks, not just whether a file exists \u2014 the work may already be covered.\n- **Ranged reads.** For large or unfamiliar files, read the relevant range rather than the whole file.";
22284
22424
  let adSection = "";
22285
22425
  try {
@@ -22307,6 +22447,27 @@ ${entries}`;
22307
22447
  }
22308
22448
  } catch {
22309
22449
  }
22450
+ let openIssuesSection = "";
22451
+ try {
22452
+ if (adapter2.getCycleLearnings) {
22453
+ const issues = (await adapter2.getCycleLearnings({ category: "issue", limit: 20 })).filter((l) => !l.resolvedAt && l.id);
22454
+ const taskIds = new Set([result.task.id, result.task.displayId].filter(Boolean));
22455
+ const moduleTag = result.task.module?.trim().toLowerCase();
22456
+ const score = (l) => (taskIds.has(l.taskId) ? 2 : 0) + (moduleTag && l.tags.some((t) => t.toLowerCase() === moduleTag) ? 1 : 0);
22457
+ const top = issues.sort((a, b2) => score(b2) - score(a)).slice(0, 8);
22458
+ if (top.length > 0) {
22459
+ const rows = top.map((l) => `- \`${l.id}\` \xB7 ${l.severity ?? "P3"} \xB7 ${l.summary}`).join("\n");
22460
+ openIssuesSection = `
22461
+
22462
+ ---
22463
+
22464
+ **OPEN DISCOVERED ISSUES** (${top.length} shown):
22465
+ ${rows}
22466
+ If this build fixes any of these, pass their UUIDs in \`fixed_issues\` on complete \u2014 that stamps them FIXED on the caught\u2192fixed ledger. Do not fix out-of-scope issues just to clear the list.`;
22467
+ }
22468
+ }
22469
+ } catch {
22470
+ }
22310
22471
  const moduleInstructions = getModuleInstructions(result.task.module);
22311
22472
  const moduleContext = await getModuleContext(adapter2, result.task);
22312
22473
  const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
@@ -22315,7 +22476,7 @@ ${entries}`;
22315
22476
  formatModelRecommendation(result.task.buildHandoff?.effort ?? result.task.complexity)
22316
22477
  ) ?? "";
22317
22478
  const gestaltNote = buildGestaltPreBuildDirective(caps) ?? "";
22318
- return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + gestaltNote + adSection + moduleInstructions + moduleContext + dogfoodSection + verificationNote + buildDisciplineNote + chainInstruction + phaseNote + filesToWriteSection);
22479
+ return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + gestaltNote + adSection + moduleInstructions + moduleContext + dogfoodSection + openIssuesSection + verificationNote + buildDisciplineNote + chainInstruction + phaseNote + filesToWriteSection);
22319
22480
  } catch (err) {
22320
22481
  if (isNoHandoffError(err)) {
22321
22482
  const lines = [
@@ -22355,6 +22516,7 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
22355
22516
  const completed = args.completed;
22356
22517
  const effort = args.effort;
22357
22518
  const estimatedEffort = args.estimated_effort;
22519
+ const model = typeof args.model === "string" && args.model.trim() !== "" ? args.model.trim() : void 0;
22358
22520
  const surprises = args.surprises;
22359
22521
  const discoveredIssues = args.discovered_issues;
22360
22522
  const architectureNotes = args.architecture_notes;
@@ -22410,6 +22572,7 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
22410
22572
  completed,
22411
22573
  effort: parsedEffort,
22412
22574
  estimatedEffort: parsedEstimatedEffort,
22575
+ model,
22413
22576
  surprises,
22414
22577
  discoveredIssues,
22415
22578
  architectureNotes,
@@ -22487,9 +22650,14 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
22487
22650
  batchRollupNote = "";
22488
22651
  }
22489
22652
  }
22490
- const fixedNote = fixedResolvedCount > 0 ? `
22653
+ let fixedNote = "";
22654
+ if (fixedResolvedCount > 0) {
22655
+ fixedNote = `
22491
22656
 
22492
- \u2705 Marked ${fixedResolvedCount} discovered issue(s) FIXED \u2014 resolved_at stamped, now counted as fixed on the hub's caught\u2192fixed ledger.` : "";
22657
+ \u2705 Marked ${fixedResolvedCount} discovered issue(s) FIXED \u2014 resolved_at stamped, now counted as fixed on the hub's caught\u2192fixed ledger.`;
22658
+ } else if (discoveredIssues && discoveredIssues.trim() !== "" && !/^none\b/i.test(discoveredIssues.trim())) {
22659
+ fixedNote = "\n\n\u2139\uFE0F This build filed discovered issues but passed no `fixed_issues`. When a future build fixes one, pass its UUID in `fixed_issues` so it counts as FIXED (not just auto-cleared) on the hub ledger.";
22660
+ }
22493
22661
  return textResponse(formatCompleteResult(result) + fixedNote + docsNote + batchRollupNote);
22494
22662
  } catch (err) {
22495
22663
  const message = err instanceof Error ? err.message : String(err);
@@ -22957,7 +23125,11 @@ async function handleBug(adapter2, config2, args) {
22957
23125
  type,
22958
23126
  description: text,
22959
23127
  diagnostics,
22960
- status: "open",
23128
+ // Canonical triage vocabulary (submitted|investigating|fixed|wont_fix).
23129
+ // Was 'open' — a third vocabulary that made the owner triage console
23130
+ // render new upstream reports as un-triaged. Adapters pass status through,
23131
+ // so emitting canonical here keeps hosted + pg writes consistent.
23132
+ status: "submitted",
22961
23133
  notifyRequested,
22962
23134
  contactOk
22963
23135
  });
@@ -24058,7 +24230,7 @@ var REVIEW_RUBRIC = [
24058
24230
  "- Quality: tests present and meaningful, no obvious debt or dead code.",
24059
24231
  "Be specific and cite file/line where you can. Recommend fail only for blocking issues."
24060
24232
  ].join("\n");
24061
- async function buildReviewDispatch(adapter2, config2, taskId) {
24233
+ async function assembleReviewContext(adapter2, config2, taskId) {
24062
24234
  const task = await adapter2.getTask(taskId);
24063
24235
  if (!task) {
24064
24236
  return { ok: false, error: `Task ${taskId} not found \u2014 cannot assemble review context.` };
@@ -24091,13 +24263,13 @@ ${report}
24091
24263
 
24092
24264
  ${diffBlock}`;
24093
24265
  const contextBytes = Buffer.byteLength(userMessage, "utf-8");
24094
- const prompt2 = buildSubagentDispatchPrompt({
24095
- tool: "review_submit",
24096
- taskId,
24097
- systemPrompt: REVIEW_RUBRIC,
24098
- userMessage,
24099
- contextBytes
24100
- });
24266
+ return { ok: true, userMessage, contextBytes };
24267
+ }
24268
+ async function buildReviewDispatch(adapter2, config2, taskId, preset) {
24269
+ const ctx = await assembleReviewContext(adapter2, config2, taskId);
24270
+ if (!ctx.ok) return ctx;
24271
+ const { userMessage, contextBytes } = ctx;
24272
+ const prompt2 = preset ? buildFanoutReviewDispatchPrompt({ taskId, preset, systemPrompt: REVIEW_RUBRIC, userMessage, contextBytes }) : buildSubagentDispatchPrompt({ tool: "review_submit", taskId, systemPrompt: REVIEW_RUBRIC, userMessage, contextBytes });
24101
24273
  return { ok: true, prompt: prompt2, contextBytes };
24102
24274
  }
24103
24275
  var reviewListTool = {
@@ -24149,6 +24321,11 @@ var reviewSubmitTool = {
24149
24321
  enum: ["inline", "subagent"],
24150
24322
  description: `task-1864: set "subagent" (build-acceptance only) to offload code review to a fresh sub-agent. Returns a Task() invocation prompt that feeds the build report + branch diff to the sub-agent, which returns structured auto_review findings. Verdict is NOT required on this call \u2014 you call review_submit again with the human verdict + the sub-agent's auto_review. Default "inline" (record the verdict directly).`
24151
24323
  },
24324
+ review_preset: {
24325
+ type: "string",
24326
+ enum: ["gate", "full", "security-focused"],
24327
+ description: 'task-2824 (opt-in): upgrade the sub-agent dispatch to a MULTI-LENS fan-out. Requires dispatch:"subagent" (build-acceptance only). Emits N specialist lens legs (correctness/security/wiring/test-quality/scope-drift) that review the same diff in parallel, plus a synthesis leg that dedupes their findings into ONE prioritized auto_review verdict. "gate" = correctness+wiring, "full" = all 5, "security-focused" = security+correctness+wiring. Omit for the existing single-lens review (unchanged default).'
24328
+ },
24152
24329
  reviewer_confirmed: {
24153
24330
  type: "boolean",
24154
24331
  description: "Set to true to confirm you have reviewed the build (read the build report or the pending list via review_list) before submitting an accept verdict. Required to accept a build-acceptance review unless review_list was called in the same session within the last 15 minutes. Defense-in-depth against SUP-2026-010 (Codex prematurely accepted a task because review_list was missing from its tool surface)."
@@ -24363,11 +24540,24 @@ async function handleReviewSubmit(adapter2, config2, args) {
24363
24540
  caps = {};
24364
24541
  }
24365
24542
  const explicitDispatch = args.dispatch === "subagent";
24543
+ const rawPreset = args.review_preset;
24544
+ const reviewPreset = typeof rawPreset === "string" && rawPreset in REVIEW_PRESETS ? rawPreset : void 0;
24545
+ if (rawPreset !== void 0 && !explicitDispatch) {
24546
+ return errorResponse('review_preset requires dispatch:"subagent" (multi-lens fan-out is only available on an explicit sub-agent dispatch).');
24547
+ }
24548
+ if (typeof rawPreset === "string" && rawPreset.length > 0 && !reviewPreset) {
24549
+ return errorResponse(`review_preset "${rawPreset}" is not valid. Use "gate", "full", or "security-focused".`);
24550
+ }
24366
24551
  const autoDispatchOptIn = args.dispatch !== "inline" && process.env.PAPI_AUTO_DISPATCH !== "false" && isCapabilityEnabled(caps, "prReviewer");
24367
24552
  const autoDispatchEligible = !verdict && autoDispatchOptIn;
24368
24553
  const capabilityAutoReviewEligible = verdict === "accept" && !autoReview && autoDispatchOptIn;
24369
24554
  if ((explicitDispatch || autoDispatchEligible || capabilityAutoReviewEligible) && stage === "build-acceptance" && taskId) {
24370
- const dispatch = await buildReviewDispatch(adapter2, config2, taskId);
24555
+ const dispatch = await buildReviewDispatch(
24556
+ adapter2,
24557
+ config2,
24558
+ taskId,
24559
+ explicitDispatch ? reviewPreset : void 0
24560
+ );
24371
24561
  if (!dispatch.ok) {
24372
24562
  if (explicitDispatch) return errorResponse(dispatch.error);
24373
24563
  } else if (explicitDispatch || capabilityAutoReviewEligible || dispatch.contextBytes > REVIEW_DISPATCH_THRESHOLD) {
@@ -30050,4 +30240,10 @@ if (isHttpMode && httpPort !== void 0) {
30050
30240
  }
30051
30241
  const transport = new StdioServerTransport();
30052
30242
  await server.connect(transport);
30243
+ const projectName = basename2(config.projectRoot);
30244
+ const projectIdShort = config.projectId ? ` (${config.projectId.slice(0, 8)})` : process.env.PAPI_PROJECT_ID ? ` (${process.env.PAPI_PROJECT_ID.slice(0, 8)})` : "";
30245
+ process.stderr.write(
30246
+ `[papi] Connected \u2014 project: ${projectName}${projectIdShort}, adapter: ${config.adapterType}, v${pkgVersion}
30247
+ `
30248
+ );
30053
30249
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.60",
3
+ "version": "0.7.62",
4
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",