@papi-ai/server 0.7.46 → 0.7.48
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/backfill-cycle-metrics.js +49 -14
- package/dist/index.js +152 -52
- package/dist/prompts.js +2 -2
- package/package.json +1 -1
|
@@ -4519,31 +4519,66 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
4519
4519
|
}
|
|
4520
4520
|
|
|
4521
4521
|
// src/lib/formatters.ts
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4522
|
+
function effortWeight(size) {
|
|
4523
|
+
switch ((size || "").toUpperCase()) {
|
|
4524
|
+
case "XS":
|
|
4525
|
+
return 1;
|
|
4526
|
+
case "S":
|
|
4527
|
+
case "SMALL":
|
|
4528
|
+
return 2;
|
|
4529
|
+
case "M":
|
|
4530
|
+
case "MEDIUM":
|
|
4531
|
+
return 3;
|
|
4532
|
+
case "L":
|
|
4533
|
+
case "LARGE":
|
|
4534
|
+
return 5;
|
|
4535
|
+
case "XL":
|
|
4536
|
+
return 8;
|
|
4537
|
+
default:
|
|
4538
|
+
return 3;
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
function computeSnapshotsFromBuildReports(reports, tasks) {
|
|
4542
|
+
const reportsByCycle = /* @__PURE__ */ new Map();
|
|
4526
4543
|
for (const r of reports) {
|
|
4527
|
-
const existing =
|
|
4544
|
+
const existing = reportsByCycle.get(r.cycle) ?? [];
|
|
4528
4545
|
existing.push(r);
|
|
4529
|
-
|
|
4546
|
+
reportsByCycle.set(r.cycle, existing);
|
|
4530
4547
|
}
|
|
4548
|
+
const tasksByCycle = /* @__PURE__ */ new Map();
|
|
4549
|
+
for (const t of tasks ?? []) {
|
|
4550
|
+
if (t.cycle == null) continue;
|
|
4551
|
+
const existing = tasksByCycle.get(t.cycle) ?? [];
|
|
4552
|
+
existing.push(t);
|
|
4553
|
+
tasksByCycle.set(t.cycle, existing);
|
|
4554
|
+
}
|
|
4555
|
+
const cycleNumbers = /* @__PURE__ */ new Set([...reportsByCycle.keys(), ...tasksByCycle.keys()]);
|
|
4556
|
+
if (cycleNumbers.size === 0) return [];
|
|
4531
4557
|
const snapshots = [];
|
|
4532
|
-
for (const
|
|
4533
|
-
const
|
|
4534
|
-
const
|
|
4558
|
+
for (const sn of cycleNumbers) {
|
|
4559
|
+
const cycleReports = reportsByCycle.get(sn) ?? [];
|
|
4560
|
+
const cycleTaskRows = tasksByCycle.get(sn);
|
|
4535
4561
|
const withEffort = cycleReports.filter((r) => r.estimatedEffort && r.actualEffort);
|
|
4536
4562
|
const accurate = withEffort.filter((r) => r.estimatedEffort === r.actualEffort).length;
|
|
4537
4563
|
const matchRate = withEffort.length > 0 ? Math.round(accurate / withEffort.length * 100) : 0;
|
|
4538
|
-
let
|
|
4539
|
-
|
|
4540
|
-
|
|
4564
|
+
let completed;
|
|
4565
|
+
let total;
|
|
4566
|
+
let effortPoints;
|
|
4567
|
+
if (cycleTaskRows && cycleTaskRows.length > 0) {
|
|
4568
|
+
const done = cycleTaskRows.filter((t) => t.status === "Done");
|
|
4569
|
+
completed = done.length;
|
|
4570
|
+
total = cycleTaskRows.length;
|
|
4571
|
+
effortPoints = done.reduce((s, t) => s + effortWeight(t.complexity), 0);
|
|
4572
|
+
} else {
|
|
4573
|
+
completed = cycleReports.filter((r) => r.completed === "Yes").length;
|
|
4574
|
+
total = cycleReports.length;
|
|
4575
|
+
effortPoints = cycleReports.reduce((s, r) => s + effortWeight(r.actualEffort), 0);
|
|
4541
4576
|
}
|
|
4542
4577
|
snapshots.push({
|
|
4543
4578
|
cycle: sn,
|
|
4544
4579
|
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4545
|
-
accuracy: [{ cycle: sn, reports:
|
|
4546
|
-
velocity: [{ cycle: sn, completed, partial: 0, failed: total - completed, effortPoints }]
|
|
4580
|
+
accuracy: [{ cycle: sn, reports: cycleReports.length, matchRate, mae: 0, bias: 0 }],
|
|
4581
|
+
velocity: [{ cycle: sn, completed, partial: 0, failed: Math.max(0, total - completed), effortPoints }]
|
|
4547
4582
|
});
|
|
4548
4583
|
}
|
|
4549
4584
|
snapshots.sort((a, b) => a.cycle - b.cycle);
|
package/dist/index.js
CHANGED
|
@@ -8306,31 +8306,66 @@ function formatBoardForReview(tasks) {
|
|
|
8306
8306
|
Notes: ${t.notes}` : ""}`
|
|
8307
8307
|
).join("\n\n");
|
|
8308
8308
|
}
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8309
|
+
function effortWeight(size2) {
|
|
8310
|
+
switch ((size2 || "").toUpperCase()) {
|
|
8311
|
+
case "XS":
|
|
8312
|
+
return 1;
|
|
8313
|
+
case "S":
|
|
8314
|
+
case "SMALL":
|
|
8315
|
+
return 2;
|
|
8316
|
+
case "M":
|
|
8317
|
+
case "MEDIUM":
|
|
8318
|
+
return 3;
|
|
8319
|
+
case "L":
|
|
8320
|
+
case "LARGE":
|
|
8321
|
+
return 5;
|
|
8322
|
+
case "XL":
|
|
8323
|
+
return 8;
|
|
8324
|
+
default:
|
|
8325
|
+
return 3;
|
|
8326
|
+
}
|
|
8327
|
+
}
|
|
8328
|
+
function computeSnapshotsFromBuildReports(reports, tasks) {
|
|
8329
|
+
const reportsByCycle = /* @__PURE__ */ new Map();
|
|
8313
8330
|
for (const r of reports) {
|
|
8314
|
-
const existing =
|
|
8331
|
+
const existing = reportsByCycle.get(r.cycle) ?? [];
|
|
8315
8332
|
existing.push(r);
|
|
8316
|
-
|
|
8333
|
+
reportsByCycle.set(r.cycle, existing);
|
|
8317
8334
|
}
|
|
8335
|
+
const tasksByCycle = /* @__PURE__ */ new Map();
|
|
8336
|
+
for (const t of tasks ?? []) {
|
|
8337
|
+
if (t.cycle == null) continue;
|
|
8338
|
+
const existing = tasksByCycle.get(t.cycle) ?? [];
|
|
8339
|
+
existing.push(t);
|
|
8340
|
+
tasksByCycle.set(t.cycle, existing);
|
|
8341
|
+
}
|
|
8342
|
+
const cycleNumbers = /* @__PURE__ */ new Set([...reportsByCycle.keys(), ...tasksByCycle.keys()]);
|
|
8343
|
+
if (cycleNumbers.size === 0) return [];
|
|
8318
8344
|
const snapshots = [];
|
|
8319
|
-
for (const
|
|
8320
|
-
const
|
|
8321
|
-
const
|
|
8345
|
+
for (const sn of cycleNumbers) {
|
|
8346
|
+
const cycleReports = reportsByCycle.get(sn) ?? [];
|
|
8347
|
+
const cycleTaskRows = tasksByCycle.get(sn);
|
|
8322
8348
|
const withEffort = cycleReports.filter((r) => r.estimatedEffort && r.actualEffort);
|
|
8323
8349
|
const accurate = withEffort.filter((r) => r.estimatedEffort === r.actualEffort).length;
|
|
8324
8350
|
const matchRate = withEffort.length > 0 ? Math.round(accurate / withEffort.length * 100) : 0;
|
|
8325
|
-
let
|
|
8326
|
-
|
|
8327
|
-
|
|
8351
|
+
let completed;
|
|
8352
|
+
let total;
|
|
8353
|
+
let effortPoints;
|
|
8354
|
+
if (cycleTaskRows && cycleTaskRows.length > 0) {
|
|
8355
|
+
const done = cycleTaskRows.filter((t) => t.status === "Done");
|
|
8356
|
+
completed = done.length;
|
|
8357
|
+
total = cycleTaskRows.length;
|
|
8358
|
+
effortPoints = done.reduce((s, t) => s + effortWeight(t.complexity), 0);
|
|
8359
|
+
} else {
|
|
8360
|
+
completed = cycleReports.filter((r) => r.completed === "Yes").length;
|
|
8361
|
+
total = cycleReports.length;
|
|
8362
|
+
effortPoints = cycleReports.reduce((s, r) => s + effortWeight(r.actualEffort), 0);
|
|
8328
8363
|
}
|
|
8329
8364
|
snapshots.push({
|
|
8330
8365
|
cycle: sn,
|
|
8331
8366
|
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8332
|
-
accuracy: [{ cycle: sn, reports:
|
|
8333
|
-
velocity: [{ cycle: sn, completed, partial: 0, failed: total - completed, effortPoints }]
|
|
8367
|
+
accuracy: [{ cycle: sn, reports: cycleReports.length, matchRate, mae: 0, bias: 0 }],
|
|
8368
|
+
velocity: [{ cycle: sn, completed, partial: 0, failed: Math.max(0, total - completed), effortPoints }]
|
|
8334
8369
|
});
|
|
8335
8370
|
}
|
|
8336
8371
|
snapshots.sort((a, b2) => a.cycle - b2.cycle);
|
|
@@ -8679,7 +8714,7 @@ REFERENCE DOCS
|
|
|
8679
8714
|
[Optional \u2014 paths to docs/ files that provide background context for this task. Include only when the task originated from research or scoping work and the doc contains context the builder will need beyond what is in this handoff. Omit this section entirely for tasks that don't need supplementary context.]
|
|
8680
8715
|
|
|
8681
8716
|
PRE-BUILD VERIFICATION
|
|
8682
|
-
[List 2-5 specific file paths the builder should read BEFORE implementing to check if the functionality already exists. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. If >80% of the scope is already implemented, the builder should report "already built" instead of re-implementing. Include this section for EVERY task \u2014 it prevents wasted build slots on already-shipped code.]
|
|
8717
|
+
[List 2-5 specific file paths the builder should read BEFORE implementing to check if the functionality already exists. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. ALSO mandate a docs sweep, not just file-existence (task-2161): name any docs the builder should check via doc_search or the docs index \u2014 a design/research/status:final doc or a prior task may already cover the work. If >80% of the scope is already implemented, the builder should report "already built" instead of re-implementing. Include this section for EVERY task \u2014 it prevents wasted build slots on already-shipped code.]
|
|
8683
8718
|
|
|
8684
8719
|
FILES LIKELY TOUCHED
|
|
8685
8720
|
[files]
|
|
@@ -9274,7 +9309,7 @@ function buildHandoffsOnlyUserMessage(inputs) {
|
|
|
9274
9309
|
}
|
|
9275
9310
|
function extractDependencyChain(displayText) {
|
|
9276
9311
|
const match = displayText.match(
|
|
9277
|
-
/^##\s+Dependency Chain[^\n]*\n([\s\S]*?)(?=\n#{2,3}\s|$(?![\s\S]))/m
|
|
9312
|
+
/^##\s+Dependency Chain[^\n]*\n([\s\S]*?)(?=\n#{2,3}\s|\n-{3,}\s*(?:\n|$)|\n\s*BUILD HANDOFF\b|\n<!--\s*PAPI_STRUCTURED_OUTPUT|$(?![\s\S]))/m
|
|
9278
9313
|
);
|
|
9279
9314
|
if (!match) return void 0;
|
|
9280
9315
|
const body = match[1].trim();
|
|
@@ -10770,7 +10805,8 @@ async function resolveOwnerGate(adapter2, config2) {
|
|
|
10770
10805
|
enforced: true,
|
|
10771
10806
|
callerIsOwner: isProjectOwner(callerUserId, identity.ownerUserId),
|
|
10772
10807
|
callerUserId,
|
|
10773
|
-
ownerUserId: identity.ownerUserId
|
|
10808
|
+
ownerUserId: identity.ownerUserId,
|
|
10809
|
+
transport: "proxy"
|
|
10774
10810
|
};
|
|
10775
10811
|
} catch (err) {
|
|
10776
10812
|
return {
|
|
@@ -10778,7 +10814,8 @@ async function resolveOwnerGate(adapter2, config2) {
|
|
|
10778
10814
|
callerIsOwner: false,
|
|
10779
10815
|
callerUserId: config2.userId ?? null,
|
|
10780
10816
|
ownerUserId: null,
|
|
10781
|
-
resolutionError: err instanceof Error ? err.message : String(err)
|
|
10817
|
+
resolutionError: err instanceof Error ? err.message : String(err),
|
|
10818
|
+
transport: "proxy"
|
|
10782
10819
|
};
|
|
10783
10820
|
}
|
|
10784
10821
|
}
|
|
@@ -10796,14 +10833,16 @@ async function resolveOwnerGate(adapter2, config2) {
|
|
|
10796
10833
|
callerIsOwner: isProjectOwner(callerUserId, ownerUserId),
|
|
10797
10834
|
callerUserId,
|
|
10798
10835
|
ownerUserId,
|
|
10799
|
-
...resolutionError !== void 0 ? { resolutionError } : {}
|
|
10836
|
+
...resolutionError !== void 0 ? { resolutionError } : {},
|
|
10837
|
+
transport: "pg"
|
|
10800
10838
|
};
|
|
10801
10839
|
}
|
|
10802
10840
|
return {
|
|
10803
10841
|
enforced: false,
|
|
10804
10842
|
callerIsOwner: true,
|
|
10805
10843
|
callerUserId: config2.userId ?? null,
|
|
10806
|
-
ownerUserId: null
|
|
10844
|
+
ownerUserId: null,
|
|
10845
|
+
transport: "md"
|
|
10807
10846
|
};
|
|
10808
10847
|
}
|
|
10809
10848
|
|
|
@@ -12515,6 +12554,13 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
12515
12554
|
contextHashes
|
|
12516
12555
|
};
|
|
12517
12556
|
}
|
|
12557
|
+
var PLAN_STAGE_STEPS = ["health-check", "inbox-triage", "board-integrity", "maturity-gate", "recommendation", "dependency-chain"];
|
|
12558
|
+
async function streamPlanStageSteps(tracker, newCycleNumber) {
|
|
12559
|
+
tracker.setStreamScope({ cycle: newCycleNumber });
|
|
12560
|
+
for (const step of PLAN_STAGE_STEPS) {
|
|
12561
|
+
await tracker.recordStep(step);
|
|
12562
|
+
}
|
|
12563
|
+
}
|
|
12518
12564
|
async function applyPlan(adapter2, config2, rawLlmOutput, mode, cycleNumber, strategyReviewWarning, contextHashes, planRunMeta, tracker) {
|
|
12519
12565
|
const applyTimer = startTimer();
|
|
12520
12566
|
console.error(`[plan-perf] applyPlan: start (llm_response=${rawLlmOutput.length} chars)`);
|
|
@@ -12527,6 +12573,7 @@ async function applyPlan(adapter2, config2, rawLlmOutput, mode, cycleNumber, str
|
|
|
12527
12573
|
if (config2.adapterType === "pg") {
|
|
12528
12574
|
tracker?.mark("process_llm_output_pg");
|
|
12529
12575
|
const result = await processLlmOutput(adapter2, config2, rawLlmOutput, mode, cycleNumber, contextHashes, planRunMeta);
|
|
12576
|
+
if (tracker) await streamPlanStageSteps(tracker, cycleNumber + 1);
|
|
12530
12577
|
const applyMs2 = applyTimer();
|
|
12531
12578
|
console.error(`[plan-perf] applyPlan: total=${applyMs2}ms (pg, no git sync)`);
|
|
12532
12579
|
return { ...result, pullNote: "", strategyReviewWarning };
|
|
@@ -13253,10 +13300,6 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13253
13300
|
}
|
|
13254
13301
|
const skipHandoffs = args.skip_handoffs === true;
|
|
13255
13302
|
const result = await preparePlan(adapter2, config2, filters, focus, force, handoffsOnly, skipHandoffs, tracker);
|
|
13256
|
-
tracker.setStreamScope({ cycle: result.cycleNumber + 1 });
|
|
13257
|
-
for (const step of ["health-check", "inbox-triage", "board-integrity", "maturity-gate", "recommendation", "dependency-chain"]) {
|
|
13258
|
-
await tracker.recordStep(step);
|
|
13259
|
-
}
|
|
13260
13303
|
planPrepareCache.set(callerKey, {
|
|
13261
13304
|
contextHashes: result.contextHashes,
|
|
13262
13305
|
userMessage: result.userMessage,
|
|
@@ -18835,7 +18878,8 @@ To override, pass force=true (emits a telemetry warning).`
|
|
|
18835
18878
|
if (adapter2.getBuildReportsSince) {
|
|
18836
18879
|
try {
|
|
18837
18880
|
const cycleReports = (await adapter2.getBuildReportsSince(currentCycle)).filter((r) => r.cycle === currentCycle);
|
|
18838
|
-
|
|
18881
|
+
const cycleTasks = (await adapter2.queryBoard({ cycleSince: currentCycle, compact: true })).filter((t) => t.cycle === currentCycle);
|
|
18882
|
+
[snapshot] = computeSnapshotsFromBuildReports(cycleReports, cycleTasks);
|
|
18839
18883
|
} catch (err) {
|
|
18840
18884
|
console.error(
|
|
18841
18885
|
`[release] cycle-metrics snapshot compute failed for cycle ${currentCycle} (non-blocking): ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -19374,10 +19418,22 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
19374
19418
|
const resolutionNote = gate.resolutionError ? `
|
|
19375
19419
|
|
|
19376
19420
|
Identity resolution failed (${gate.resolutionError}) \u2014 the gate fails closed. Retry once connectivity is restored.` : "";
|
|
19421
|
+
if (gate.transport === "pg" && gate.callerUserId === null) {
|
|
19422
|
+
return errorResponse(
|
|
19423
|
+
`Release needs to know who you are, and this setup has no PAPI_USER_ID yet.
|
|
19424
|
+
|
|
19425
|
+
Add it to the papi server's env block in .mcp.json \u2014 the value is your account UUID (getpapi.ai \u2192 Settings \u2192 Account):
|
|
19426
|
+
|
|
19427
|
+
"env": { "PAPI_USER_ID": "<your-account-uuid>", ... }
|
|
19428
|
+
|
|
19429
|
+
Then reconnect your AI tool and run release again. (Direct/pg setups read identity from this variable; hosted setups resolve it from the API key automatically.)`
|
|
19430
|
+
);
|
|
19431
|
+
}
|
|
19377
19432
|
tracker.mark("contributor-role-gate");
|
|
19378
19433
|
const callerRole = adapter2.getContributorRole && gate.callerUserId ? await adapter2.getContributorRole(gate.callerUserId).catch(() => null) : null;
|
|
19379
19434
|
if (callerRole !== "editor") {
|
|
19380
|
-
const
|
|
19435
|
+
const ownerHint = gate.transport === "pg" ? `If you ARE the owner, set PAPI_USER_ID to your account UUID in .mcp.json (getpapi.ai \u2192 Settings \u2192 Account).` : `If you ARE the owner, your identity comes from your API key \u2014 check you are using YOUR key for YOUR project.`;
|
|
19436
|
+
const roleNote = callerRole === "viewer" ? `Your role on this project is "viewer", which cannot release. Ask the owner (or an editor) to release, or ask for editor access.` : `Your identity does not match this project's owner, and you do not have an editor role to open a release PR. If you are a contributor, ask the owner for editor access (or push your branch and open a PR manually). ` + ownerHint;
|
|
19381
19437
|
return errorResponse(
|
|
19382
19438
|
`Release to ${productionBaseBranch} is restricted to the project owner.
|
|
19383
19439
|
|
|
@@ -19754,7 +19810,10 @@ function triggerSurfaceHitsOnBranch(projectRoot, baseRef = "origin/main") {
|
|
|
19754
19810
|
function selectAutostashPaths(modified, untracked) {
|
|
19755
19811
|
const untrackedSet = new Set(untracked);
|
|
19756
19812
|
const isDocsPath = (p) => p === "docs" || p.startsWith("docs/");
|
|
19757
|
-
const
|
|
19813
|
+
const isUntrackedDirEntry = (p) => p.endsWith("/") && untracked.some((u) => u.startsWith(p));
|
|
19814
|
+
const trackedDirty = modified.filter(
|
|
19815
|
+
(p) => !untrackedSet.has(p) && !isUntrackedDirEntry(p)
|
|
19816
|
+
);
|
|
19758
19817
|
const preservedDocs = untracked.filter(isDocsPath);
|
|
19759
19818
|
const stashableUntracked = untracked.filter((p) => !isDocsPath(p));
|
|
19760
19819
|
return { toStash: [...trackedDirty, ...stashableUntracked], preservedDocs };
|
|
@@ -20071,6 +20130,20 @@ async function describeTask(adapter2, taskId) {
|
|
|
20071
20130
|
}
|
|
20072
20131
|
return { task };
|
|
20073
20132
|
}
|
|
20133
|
+
function shouldBlockDirtyBranchSwitch(opts) {
|
|
20134
|
+
return opts.trackedDirtyCount > 0 && opts.currentBranch !== opts.baseBranch && opts.currentBranch !== opts.featureBranch;
|
|
20135
|
+
}
|
|
20136
|
+
async function persistBranchName(adapter2, taskId, branch) {
|
|
20137
|
+
try {
|
|
20138
|
+
await adapter2.updateTask(taskId, { branchName: branch });
|
|
20139
|
+
return true;
|
|
20140
|
+
} catch (err) {
|
|
20141
|
+
console.error(
|
|
20142
|
+
`[build] branch_name persist skipped for ${taskId} (non-fatal): ` + (err instanceof Error ? err.message : String(err))
|
|
20143
|
+
);
|
|
20144
|
+
return false;
|
|
20145
|
+
}
|
|
20146
|
+
}
|
|
20074
20147
|
async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
20075
20148
|
const task = await adapter2.getTask(taskId);
|
|
20076
20149
|
if (!task) {
|
|
@@ -20192,11 +20265,11 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
20192
20265
|
branchLines.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
|
|
20193
20266
|
}
|
|
20194
20267
|
const trackedDirty = getTrackedModifiedFiles(config2.projectRoot);
|
|
20195
|
-
if (trackedDirty.length
|
|
20268
|
+
if (shouldBlockDirtyBranchSwitch({ trackedDirtyCount: trackedDirty.length, currentBranch, baseBranch, featureBranch })) {
|
|
20196
20269
|
const shown = trackedDirty.slice(0, 5).join(", ");
|
|
20197
20270
|
const more = trackedDirty.length > 5 ? ` (+${trackedDirty.length - 5} more)` : "";
|
|
20198
20271
|
throw new Error(
|
|
20199
|
-
`build_execute won't switch branches: the working tree has uncommitted changes on '${currentBranch}' (this task targets '${featureBranch}'). Another session may be working there, and switching would revert those files. Commit or stash them first,
|
|
20272
|
+
`build_execute won't switch branches: the working tree has uncommitted changes on '${currentBranch}' (this task targets '${featureBranch}'). Another session may be working there, and switching would revert those files. Commit or stash them first, run build_execute from '${baseBranch}', or check out '${featureBranch}' if this is your own in-progress work. Dirty: ${shown}${more}.`
|
|
20200
20273
|
);
|
|
20201
20274
|
}
|
|
20202
20275
|
if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
|
|
@@ -20287,6 +20360,10 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
20287
20360
|
if (task.status !== "In Progress") {
|
|
20288
20361
|
await adapter2.updateTaskStatus(taskId, "In Progress");
|
|
20289
20362
|
}
|
|
20363
|
+
const startedBranch = taskBranchMap.get(taskId);
|
|
20364
|
+
if (startedBranch) {
|
|
20365
|
+
await persistBranchName(adapter2, taskId, startedBranch);
|
|
20366
|
+
}
|
|
20290
20367
|
buildStartTimes.set(taskId, (/* @__PURE__ */ new Date()).toISOString());
|
|
20291
20368
|
if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
20292
20369
|
const startSha = getHeadCommitSha(config2.projectRoot);
|
|
@@ -20369,7 +20446,8 @@ function extractDocMeta(absolutePath, relativePath, cycleNumber) {
|
|
|
20369
20446
|
}
|
|
20370
20447
|
return { title, type, cycle, summary, tags: [] };
|
|
20371
20448
|
}
|
|
20372
|
-
function assertDeployVerification(config2, input) {
|
|
20449
|
+
function assertDeployVerification(config2, input, opts = { deployingNow: true }) {
|
|
20450
|
+
if (!opts.deployingNow) return;
|
|
20373
20451
|
if (config2.adapterType !== "pg" && config2.adapterType !== "proxy") return;
|
|
20374
20452
|
let triggerHits = [];
|
|
20375
20453
|
try {
|
|
@@ -20378,6 +20456,8 @@ function assertDeployVerification(config2, input) {
|
|
|
20378
20456
|
return;
|
|
20379
20457
|
}
|
|
20380
20458
|
if (triggerHits.length === 0) return;
|
|
20459
|
+
const mergeDeployHits = triggerHits.filter((p) => !isDeployAtReleaseSurface(p));
|
|
20460
|
+
if (mergeDeployHits.length === 0) return;
|
|
20381
20461
|
const verification = input.productionVerification;
|
|
20382
20462
|
if (!verification) {
|
|
20383
20463
|
const targetLine = config2.verifyCommand ? `
|
|
@@ -20391,25 +20471,28 @@ Run a curl against the live deploy and pass production_verification on build_exe
|
|
|
20391
20471
|
{ urls, curl_command, http_status, response_excerpt, verified_at }
|
|
20392
20472
|
`;
|
|
20393
20473
|
throw new Error(
|
|
20394
|
-
`Deploy-verification required: this branch's diff touches ${
|
|
20395
|
-
` +
|
|
20396
|
-
Trigger surface is enforced because changes here have historically shipped to prod undetected (C166, C264-C265, C271).`
|
|
20474
|
+
`Deploy-verification required: this branch's diff touches ${mergeDeployHits.length} deploy-on-merge trigger-surface file(s):
|
|
20475
|
+
` + mergeDeployHits.map((p) => ` - ${p}`).join("\n") + targetLine + `
|
|
20476
|
+
Trigger surface is enforced because changes here have historically shipped to prod undetected (C166, C264-C265, C271). Edge/data-proxy diffs (supabase/functions/**) are verified at release instead.`
|
|
20397
20477
|
);
|
|
20398
20478
|
}
|
|
20399
20479
|
if (verification.http_status < 200 || verification.http_status >= 300) {
|
|
20400
20480
|
throw new Error(
|
|
20401
20481
|
`Deploy-verification failed: production_verification reports http_status=${verification.http_status} on a trigger-surface task.
|
|
20402
|
-
Trigger files touched: ${
|
|
20482
|
+
Trigger files touched: ${mergeDeployHits.join(", ")}
|
|
20403
20483
|
Fix the deploy first, then re-run build_execute complete with a 2xx verification.`
|
|
20404
20484
|
);
|
|
20405
20485
|
}
|
|
20406
20486
|
}
|
|
20487
|
+
function isDeployAtReleaseSurface(path7) {
|
|
20488
|
+
return path7.startsWith("supabase/functions/");
|
|
20489
|
+
}
|
|
20407
20490
|
async function completeBuild(adapter2, config2, taskId, input, options = {}, clientName) {
|
|
20408
20491
|
const task = await adapter2.getTask(taskId);
|
|
20409
20492
|
if (!task) {
|
|
20410
20493
|
throw new Error(`Task "${taskId}" not found on the Cycle Board.`);
|
|
20411
20494
|
}
|
|
20412
|
-
assertDeployVerification(config2, input);
|
|
20495
|
+
assertDeployVerification(config2, input, { deployingNow: options.light === true });
|
|
20413
20496
|
const [healthResult, priorCount] = await Promise.all([
|
|
20414
20497
|
adapter2.getCycleHealth().catch(() => ({ totalCycles: 0 })),
|
|
20415
20498
|
typeof adapter2.getBuildReportCountForTask === "function" ? adapter2.getBuildReportCountForTask(taskId).catch(() => 0) : Promise.resolve(0)
|
|
@@ -21389,7 +21472,7 @@ var buildExecuteTool = {
|
|
|
21389
21472
|
},
|
|
21390
21473
|
dead_ends: {
|
|
21391
21474
|
type: "string",
|
|
21392
|
-
description: `
|
|
21475
|
+
description: `CONTRACT: send this on every complete. List approaches you tried and RULED OUT during this build, with why each failed. Example: "Tried Supabase realtime but Edge Functions can't hold persistent connections \u2014 switched to polling." Send "None" ONLY if you genuinely tried nothing that failed. Ruled-out paths are first-class intelligence \u2014 future builds and plans read them to avoid repeating blind alleys.`
|
|
21393
21476
|
},
|
|
21394
21477
|
brief_implications: {
|
|
21395
21478
|
type: "array",
|
|
@@ -21634,7 +21717,8 @@ async function handleBuildExecute(adapter2, config2, args, clientName) {
|
|
|
21634
21717
|
**PRE-BUILD VERIFICATION:** Before writing any code, read these files and check if the functionality already exists:
|
|
21635
21718
|
${verificationFiles.map((f) => `- ${f}`).join("\n")}
|
|
21636
21719
|
If >80% of the scope is already implemented, call \`build_execute\` with completed="yes" and note "already built" in surprises instead of re-implementing.` : "";
|
|
21637
|
-
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.';
|
|
21720
|
+
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.';
|
|
21721
|
+
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.";
|
|
21638
21722
|
let adSection = "";
|
|
21639
21723
|
try {
|
|
21640
21724
|
const allADs = await adapter2.getActiveDecisions();
|
|
@@ -21669,7 +21753,7 @@ ${entries}`;
|
|
|
21669
21753
|
formatModelRecommendation(result.task.buildHandoff?.effort ?? result.task.complexity)
|
|
21670
21754
|
) ?? "";
|
|
21671
21755
|
const gestaltNote = buildGestaltPreBuildDirective(caps) ?? "";
|
|
21672
|
-
return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + gestaltNote + adSection + moduleInstructions + moduleContext + dogfoodSection + verificationNote + chainInstruction + phaseNote + filesToWriteSection);
|
|
21756
|
+
return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + gestaltNote + adSection + moduleInstructions + moduleContext + dogfoodSection + verificationNote + buildDisciplineNote + chainInstruction + phaseNote + filesToWriteSection);
|
|
21673
21757
|
} catch (err) {
|
|
21674
21758
|
if (isNoHandoffError(err)) {
|
|
21675
21759
|
const lines = [
|
|
@@ -21787,9 +21871,11 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
21787
21871
|
tracker.mark("complete_format");
|
|
21788
21872
|
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.cycleNumber });
|
|
21789
21873
|
await tracker.recordStep("report_written");
|
|
21790
|
-
|
|
21791
|
-
|
|
21792
|
-
|
|
21874
|
+
if ((result.autoTriagedCount ?? 0) > 0) {
|
|
21875
|
+
await tracker.recordStep("issues_triaged", {
|
|
21876
|
+
metadata: { autoTriagedCount: result.autoTriagedCount }
|
|
21877
|
+
});
|
|
21878
|
+
}
|
|
21793
21879
|
if (resolvesLearnings && resolvesLearnings.length > 0 && adapter2.updateCycleLearningActionRef) {
|
|
21794
21880
|
for (const learningId of resolvesLearnings) {
|
|
21795
21881
|
try {
|
|
@@ -21798,9 +21884,11 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
21798
21884
|
}
|
|
21799
21885
|
}
|
|
21800
21886
|
}
|
|
21801
|
-
|
|
21802
|
-
|
|
21803
|
-
|
|
21887
|
+
if ((result.learningsLinkedCount ?? 0) > 0) {
|
|
21888
|
+
await tracker.recordStep("learnings", {
|
|
21889
|
+
metadata: { learningsLinkedCount: result.learningsLinkedCount }
|
|
21890
|
+
});
|
|
21891
|
+
}
|
|
21804
21892
|
await tracker.recordStep("moving-to-review", {
|
|
21805
21893
|
metadata: { status: result.task.status }
|
|
21806
21894
|
});
|
|
@@ -22195,7 +22283,7 @@ function collectDiagnostics(config2) {
|
|
|
22195
22283
|
}
|
|
22196
22284
|
var bugTool = {
|
|
22197
22285
|
name: "bug",
|
|
22198
|
-
description:
|
|
22286
|
+
description: `Report a bug OR submit an idea. Routing: a bug about PAPI itself (a PAPI tool/MCP error, the connector, a handoff/cycle problem) auto-submits UPSTREAM to PAPI maintainers with diagnostics \u2014 you do NOT need to set report=true for these. A bug in the user's OWN project auto-files as a Backlog task on their board. IMPORTANT: report=true is ONLY for a genuine PAPI-product defect \u2014 it is NOT the catch-all for "not about my app". A bug in the user's harness/editor/OS/git/other tooling (e.g. Claude Code, Codex, VS Code, a shell command) is NOT a PAPI bug: file it on the user's own board (report=false / default) or that tool's own tracker, never upstream. Override the routing explicitly: report=true forces upstream (PAPI-product only), report=false forces the user's own board. Set \`type\` ("bug"/"idea") and optional notify-when-fixed / contact-ok consent for upstream submissions. Does not call the Anthropic API.`,
|
|
22199
22287
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
22200
22288
|
inputSchema: {
|
|
22201
22289
|
type: "object",
|
|
@@ -22227,7 +22315,7 @@ var bugTool = {
|
|
|
22227
22315
|
},
|
|
22228
22316
|
report: {
|
|
22229
22317
|
type: "boolean",
|
|
22230
|
-
description: "Routing override. Leave UNSET to auto-route: PAPI-product bugs go upstream to maintainers, project-domain bugs go to the user's board. Set true to
|
|
22318
|
+
description: "Routing override. Leave UNSET to auto-route: PAPI-product bugs go upstream to maintainers, project-domain bugs go to the user's board. Set true ONLY for a genuine PAPI-product defect (PAPI tool/MCP/connector/handoff) \u2014 a bug in the user's harness/editor/OS/other tooling is NOT a PAPI bug and must go to their own board (report=false or default), not upstream. Set false to force a task on the user's own board (use false when an auto-detected PAPI bug is actually about the user's project, harness, or environment)."
|
|
22231
22319
|
},
|
|
22232
22320
|
type: {
|
|
22233
22321
|
type: "string",
|
|
@@ -22244,7 +22332,7 @@ var bugTool = {
|
|
|
22244
22332
|
},
|
|
22245
22333
|
project: {
|
|
22246
22334
|
type: "string",
|
|
22247
|
-
description: "Project id (UUID) or slug to file this bug under, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. Use project_switch to change the session default."
|
|
22335
|
+
description: "BOARD MODE ONLY. Project id (UUID) or slug to file this bug under, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. It CANNOT redirect an upstream (report=true) submission \u2014 those always go to PAPI maintainers. Use project_switch to change the session default."
|
|
22248
22336
|
}
|
|
22249
22337
|
},
|
|
22250
22338
|
required: ["text"]
|
|
@@ -24499,7 +24587,12 @@ ${writeNote}
|
|
|
24499
24587
|
const envVars = {
|
|
24500
24588
|
PAPI_PROJECT_DIR: projectRoot,
|
|
24501
24589
|
PAPI_ADAPTER: "pg",
|
|
24502
|
-
DATABASE_URL: process.env.DATABASE_URL || "<YOUR_DATABASE_URL>"
|
|
24590
|
+
DATABASE_URL: process.env.DATABASE_URL || "<YOUR_DATABASE_URL>",
|
|
24591
|
+
// task-2508: pg/direct setups resolve caller identity from this var (the
|
|
24592
|
+
// hosted path uses the bearer token instead). Without it the FIRST
|
|
24593
|
+
// release hits the owner-gate with a null caller — ship it in the
|
|
24594
|
+
// template so fresh pg projects are owner-resolvable out of the box.
|
|
24595
|
+
PAPI_USER_ID: process.env.PAPI_USER_ID || "<YOUR_ACCOUNT_UUID>"
|
|
24503
24596
|
};
|
|
24504
24597
|
if (rootHash) {
|
|
24505
24598
|
envVars.PAPI_RESOLUTION_METHOD = "root_hash";
|
|
@@ -24528,7 +24621,8 @@ ${writeNote}
|
|
|
24528
24621
|
"## Next Steps",
|
|
24529
24622
|
"",
|
|
24530
24623
|
...process.env.DATABASE_URL ? ["1. **Restart your MCP client** to pick up the new config."] : [`1. **Set your DATABASE_URL** \u2014 replace \`<YOUR_DATABASE_URL>\` in \`${target.displayPath}\` with your Supabase **session pooler** connection string (port **5432**, not 6543 \u2014 the transaction pooler wedges on interrupted calls).`],
|
|
24531
|
-
"2. **
|
|
24624
|
+
...process.env.PAPI_USER_ID ? [] : ["2. **Set your PAPI_USER_ID** \u2014 replace `<YOUR_ACCOUNT_UUID>` with your account UUID (getpapi.ai \u2192 Settings \u2192 Account). Release and reviews use it to recognise you as the owner."],
|
|
24625
|
+
`${process.env.PAPI_USER_ID ? "2" : "3"}. **Run \`setup\`** \u2014 this scaffolds your project with a Product Brief, Active Decisions, and CLAUDE.md.`
|
|
24532
24626
|
].join("\n");
|
|
24533
24627
|
return textResponse(output2 + formatFilesToWriteSection(collector));
|
|
24534
24628
|
}
|
|
@@ -24698,7 +24792,12 @@ async function getHealthSummary(adapter2) {
|
|
|
24698
24792
|
try {
|
|
24699
24793
|
try {
|
|
24700
24794
|
buildReports = await adapter2.getRecentBuildReports(50);
|
|
24701
|
-
|
|
24795
|
+
let metricsTasks = [];
|
|
24796
|
+
try {
|
|
24797
|
+
metricsTasks = await adapter2.queryBoard({ cycleSince: Math.max(1, cycleNumber - 6), compact: true });
|
|
24798
|
+
} catch {
|
|
24799
|
+
}
|
|
24800
|
+
snapshots = computeSnapshotsFromBuildReports(buildReports, metricsTasks);
|
|
24702
24801
|
} catch {
|
|
24703
24802
|
}
|
|
24704
24803
|
metricsSection = formatCycleMetrics(snapshots);
|
|
@@ -26039,7 +26138,7 @@ ${versionDrift}` : "";
|
|
|
26039
26138
|
let enrichmentNote = "";
|
|
26040
26139
|
const enrichmentCollector = new FileWriteCollector();
|
|
26041
26140
|
try {
|
|
26042
|
-
enrichmentNote = enrichClaudeMd(config2.projectRoot, healthResult.cycleNumber, config2.adapterType, enrichmentCollector);
|
|
26141
|
+
enrichmentNote = enrichClaudeMd(config2.projectRoot, healthResult.cycleNumber, config2.adapterType, enrichmentCollector, clientName);
|
|
26043
26142
|
} catch {
|
|
26044
26143
|
}
|
|
26045
26144
|
const enrichmentFilesSection = enrichmentCollector.isEmpty() ? "" : formatFilesToWriteSection(enrichmentCollector);
|
|
@@ -26100,7 +26199,8 @@ ${section}`;
|
|
|
26100
26199
|
}));
|
|
26101
26200
|
}
|
|
26102
26201
|
}
|
|
26103
|
-
function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector) {
|
|
26202
|
+
function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, clientName) {
|
|
26203
|
+
if (!shouldWriteClaudeMd(clientName)) return "";
|
|
26104
26204
|
if (adapterType === "proxy") {
|
|
26105
26205
|
const additions2 = [];
|
|
26106
26206
|
if (cycleNumber >= 6) additions2.push(CLAUDE_MD_TIER_1);
|
package/dist/prompts.js
CHANGED
|
@@ -66,7 +66,7 @@ REFERENCE DOCS
|
|
|
66
66
|
[Optional \u2014 paths to docs/ files that provide background context for this task. Include only when the task originated from research or scoping work and the doc contains context the builder will need beyond what is in this handoff. Omit this section entirely for tasks that don't need supplementary context.]
|
|
67
67
|
|
|
68
68
|
PRE-BUILD VERIFICATION
|
|
69
|
-
[List 2-5 specific file paths the builder should read BEFORE implementing to check if the functionality already exists. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. If >80% of the scope is already implemented, the builder should report "already built" instead of re-implementing. Include this section for EVERY task \u2014 it prevents wasted build slots on already-shipped code.]
|
|
69
|
+
[List 2-5 specific file paths the builder should read BEFORE implementing to check if the functionality already exists. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. ALSO mandate a docs sweep, not just file-existence (task-2161): name any docs the builder should check via doc_search or the docs index \u2014 a design/research/status:final doc or a prior task may already cover the work. If >80% of the scope is already implemented, the builder should report "already built" instead of re-implementing. Include this section for EVERY task \u2014 it prevents wasted build slots on already-shipped code.]
|
|
70
70
|
|
|
71
71
|
FILES LIKELY TOUCHED
|
|
72
72
|
[files]
|
|
@@ -661,7 +661,7 @@ function buildHandoffsOnlyUserMessage(inputs) {
|
|
|
661
661
|
}
|
|
662
662
|
function extractDependencyChain(displayText) {
|
|
663
663
|
const match = displayText.match(
|
|
664
|
-
/^##\s+Dependency Chain[^\n]*\n([\s\S]*?)(?=\n#{2,3}\s|$(?![\s\S]))/m
|
|
664
|
+
/^##\s+Dependency Chain[^\n]*\n([\s\S]*?)(?=\n#{2,3}\s|\n-{3,}\s*(?:\n|$)|\n\s*BUILD HANDOFF\b|\n<!--\s*PAPI_STRUCTURED_OUTPUT|$(?![\s\S]))/m
|
|
665
665
|
);
|
|
666
666
|
if (!match) return void 0;
|
|
667
667
|
const body = match[1].trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.48",
|
|
4
4
|
"description": "PAPI MCP server \u2014 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",
|