@papi-ai/server 0.7.62 → 0.7.64
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 +38 -10
- package/dist/index.js +526 -145
- package/dist/prompts.js +20 -5
- package/package.json +1 -1
- package/skills/papi-cycle/AGENTS.md +1 -0
package/dist/index.js
CHANGED
|
@@ -61,6 +61,7 @@ __export(git_exports, {
|
|
|
61
61
|
isGitAvailable: () => isGitAvailable,
|
|
62
62
|
isGitRepo: () => isGitRepo,
|
|
63
63
|
listGroupedCycleBranches: () => listGroupedCycleBranches,
|
|
64
|
+
listOpenPullRequests: () => listOpenPullRequests,
|
|
64
65
|
listOrphanFeatBranches: () => listOrphanFeatBranches,
|
|
65
66
|
mergePullRequest: () => mergePullRequest,
|
|
66
67
|
normalizeGitUrl: () => normalizeGitUrl,
|
|
@@ -347,6 +348,26 @@ function isGhAvailable() {
|
|
|
347
348
|
return false;
|
|
348
349
|
}
|
|
349
350
|
}
|
|
351
|
+
function listOpenPullRequests(cwd) {
|
|
352
|
+
if (!isGhAvailable()) return null;
|
|
353
|
+
try {
|
|
354
|
+
const out = execFileSync(
|
|
355
|
+
"gh",
|
|
356
|
+
["pr", "list", "--state", "open", "--limit", "100", "--json", "number,title,author,headRefName,createdAt"],
|
|
357
|
+
{ cwd, encoding: "utf-8" }
|
|
358
|
+
);
|
|
359
|
+
const raw = JSON.parse(out);
|
|
360
|
+
return raw.map((p) => ({
|
|
361
|
+
number: p.number,
|
|
362
|
+
title: p.title,
|
|
363
|
+
author: p.author?.login ?? "unknown",
|
|
364
|
+
headRefName: p.headRefName,
|
|
365
|
+
createdAt: p.createdAt
|
|
366
|
+
}));
|
|
367
|
+
} catch {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
350
371
|
function getOriginRepoSlug(cwd) {
|
|
351
372
|
try {
|
|
352
373
|
const url = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
@@ -1306,12 +1327,12 @@ var init_proxy_adapter = __esm({
|
|
|
1306
1327
|
"listContributorReleasePrs",
|
|
1307
1328
|
"claimReview",
|
|
1308
1329
|
"getSiblingAds",
|
|
1309
|
-
"getSiblingRepoTasks"
|
|
1310
|
-
// task-2828 (C339): attributed-intelligence analytics reader — pg-only
|
|
1311
|
-
//
|
|
1312
|
-
//
|
|
1313
|
-
//
|
|
1314
|
-
|
|
1330
|
+
"getSiblingRepoTasks"
|
|
1331
|
+
// task-2828 (C339): attributed-intelligence analytics reader — pg-only that cycle.
|
|
1332
|
+
// task-2864 (C343): WIRED. getModelOutcomeStats now has an edge case handler (raw
|
|
1333
|
+
// SQL via postgres.js mirroring the pg query + inlined computeModelOutcomes bucketing)
|
|
1334
|
+
// + an ALLOWED_METHODS entry, so it forwards. countPlanRunsForCycle wired alongside it
|
|
1335
|
+
// (task-2860's auto-release guard reader). Both REMOVED from NO_FORWARD.
|
|
1315
1336
|
// task-2394 (C329) — Batch A wired: findPendingDocActionsForTask,
|
|
1316
1337
|
// getModuleEstimationStats and getDecisionScorePatterns now have edge case handlers
|
|
1317
1338
|
// (each backed by a SECURITY DEFINER RPC, migration 20260714140000) plus
|
|
@@ -1444,7 +1465,8 @@ var init_proxy_adapter = __esm({
|
|
|
1444
1465
|
if (response.status === 401) {
|
|
1445
1466
|
throw new Error(
|
|
1446
1467
|
`Auth: Invalid API key \u2014 PAPI_DATA_API_KEY was rejected by the proxy.
|
|
1447
|
-
|
|
1468
|
+
This usually means the key was revoked or replaced. Mint a fresh key in the Connect panel on your PAPI dashboard (https://getpapi.ai/hub), then update PAPI_DATA_API_KEY in your .mcp.json.
|
|
1469
|
+
Moving off a local install? Switch to the remote MCP: https://getpapi.ai/docs/install
|
|
1448
1470
|
(${response.status} on ${method}: ${message})`
|
|
1449
1471
|
);
|
|
1450
1472
|
}
|
|
@@ -2040,6 +2062,76 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
2040
2062
|
}
|
|
2041
2063
|
});
|
|
2042
2064
|
|
|
2065
|
+
// src/lib/reap-orphans.ts
|
|
2066
|
+
var reap_orphans_exports = {};
|
|
2067
|
+
__export(reap_orphans_exports, {
|
|
2068
|
+
formatReapSummary: () => formatReapSummary,
|
|
2069
|
+
isPapiServerCommand: () => isPapiServerCommand,
|
|
2070
|
+
listProcesses: () => listProcesses,
|
|
2071
|
+
reapOrphans: () => reapOrphans,
|
|
2072
|
+
selectReapableOrphans: () => selectReapableOrphans
|
|
2073
|
+
});
|
|
2074
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
2075
|
+
function isPapiServerCommand(command) {
|
|
2076
|
+
return /@papi-ai[/\\]server/.test(command);
|
|
2077
|
+
}
|
|
2078
|
+
function selectReapableOrphans(procs, selfPid) {
|
|
2079
|
+
return procs.filter(
|
|
2080
|
+
(p) => p.pid !== selfPid && p.ppid === 1 && isPapiServerCommand(p.command)
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
function listProcesses() {
|
|
2084
|
+
if (process.platform === "win32") return null;
|
|
2085
|
+
try {
|
|
2086
|
+
const out = execFileSync6("ps", ["-A", "-o", "pid=,ppid=,command="], { encoding: "utf-8" });
|
|
2087
|
+
const procs = [];
|
|
2088
|
+
for (const line of out.split("\n")) {
|
|
2089
|
+
const trimmed = line.trim();
|
|
2090
|
+
if (!trimmed) continue;
|
|
2091
|
+
const m = /^(\d+)\s+(\d+)\s+(.*)$/.exec(trimmed);
|
|
2092
|
+
if (!m) continue;
|
|
2093
|
+
procs.push({ pid: Number(m[1]), ppid: Number(m[2]), command: m[3] });
|
|
2094
|
+
}
|
|
2095
|
+
return procs;
|
|
2096
|
+
} catch {
|
|
2097
|
+
return null;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
function reapOrphans(opts = {}) {
|
|
2101
|
+
const procs = listProcesses();
|
|
2102
|
+
if (procs === null) return { unsupported: true, candidates: [], reaped: [] };
|
|
2103
|
+
const orphans = selectReapableOrphans(procs, process.pid);
|
|
2104
|
+
const candidates = orphans.map((p) => p.pid);
|
|
2105
|
+
const reaped = [];
|
|
2106
|
+
if (!opts.dryRun) {
|
|
2107
|
+
for (const pid of candidates) {
|
|
2108
|
+
try {
|
|
2109
|
+
process.kill(pid, "SIGTERM");
|
|
2110
|
+
reaped.push(pid);
|
|
2111
|
+
} catch {
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
return { unsupported: false, candidates, reaped };
|
|
2116
|
+
}
|
|
2117
|
+
function formatReapSummary(result, dryRun) {
|
|
2118
|
+
if (result.unsupported) {
|
|
2119
|
+
return "Orphan reaper: unsupported on this platform \u2014 skipped (no processes touched).";
|
|
2120
|
+
}
|
|
2121
|
+
if (result.candidates.length === 0) {
|
|
2122
|
+
return "Orphan reaper: no parentless @papi-ai/server processes found.";
|
|
2123
|
+
}
|
|
2124
|
+
if (dryRun) {
|
|
2125
|
+
return `Orphan reaper: ${result.candidates.length} parentless PAPI server process(es) found: ${result.candidates.join(", ")} (run with --reap-orphans to terminate).`;
|
|
2126
|
+
}
|
|
2127
|
+
return `Orphan reaper: terminated ${result.reaped.length} parentless PAPI server process(es): ${result.reaped.join(", ")}.`;
|
|
2128
|
+
}
|
|
2129
|
+
var init_reap_orphans = __esm({
|
|
2130
|
+
"src/lib/reap-orphans.ts"() {
|
|
2131
|
+
"use strict";
|
|
2132
|
+
}
|
|
2133
|
+
});
|
|
2134
|
+
|
|
2043
2135
|
// ../../node_modules/postgres/src/query.js
|
|
2044
2136
|
function cachedError(xs) {
|
|
2045
2137
|
if (originCache.has(xs))
|
|
@@ -4242,7 +4334,7 @@ __export(doctor_exports, {
|
|
|
4242
4334
|
__testing: () => __testing,
|
|
4243
4335
|
runDoctor: () => runDoctor
|
|
4244
4336
|
});
|
|
4245
|
-
import { existsSync as existsSync11, readFileSync as
|
|
4337
|
+
import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
|
|
4246
4338
|
import { homedir as homedir4 } from "os";
|
|
4247
4339
|
import { join as join20 } from "path";
|
|
4248
4340
|
function redact(name, value) {
|
|
@@ -4262,7 +4354,7 @@ function findMcpJson() {
|
|
|
4262
4354
|
for (const path7 of candidates) {
|
|
4263
4355
|
if (!existsSync11(path7)) continue;
|
|
4264
4356
|
try {
|
|
4265
|
-
const raw =
|
|
4357
|
+
const raw = readFileSync13(path7, "utf-8");
|
|
4266
4358
|
const parsed = JSON.parse(raw);
|
|
4267
4359
|
const papiEntry = parsed.papi ?? parsed.mcpServers?.papi;
|
|
4268
4360
|
if (!papiEntry) continue;
|
|
@@ -4506,12 +4598,16 @@ async function runDoctor(cliArgs2 = []) {
|
|
|
4506
4598
|
const fixPool = cliArgs2.includes("--fix-pool") || cliArgs2.includes("--terminate-wedged");
|
|
4507
4599
|
const pool = await diagnosePool({ fix: fixPool });
|
|
4508
4600
|
process.stdout.write("\n" + formatPoolReport(pool) + "\n");
|
|
4601
|
+
const reap = cliArgs2.includes("--reap-orphans");
|
|
4602
|
+
const reapResult = reapOrphans({ dryRun: !reap });
|
|
4603
|
+
process.stdout.write("\n" + formatReapSummary(reapResult, !reap) + "\n");
|
|
4509
4604
|
return 0;
|
|
4510
4605
|
}
|
|
4511
4606
|
var SECRET_VARS, WEDGED_IDLE_TX_SECONDS, WEDGED_ACTIVE_SECONDS, __testing;
|
|
4512
4607
|
var init_doctor = __esm({
|
|
4513
4608
|
"src/cli/doctor.ts"() {
|
|
4514
4609
|
"use strict";
|
|
4610
|
+
init_reap_orphans();
|
|
4515
4611
|
SECRET_VARS = /* @__PURE__ */ new Set(["PAPI_DATA_API_KEY", "DATABASE_URL", "PAPI_ENDPOINT"]);
|
|
4516
4612
|
WEDGED_IDLE_TX_SECONDS = 300;
|
|
4517
4613
|
WEDGED_ACTIVE_SECONDS = 300;
|
|
@@ -4536,7 +4632,7 @@ __export(reset_exports, {
|
|
|
4536
4632
|
removePapiEntry: () => removePapiEntry,
|
|
4537
4633
|
runReset: () => runReset
|
|
4538
4634
|
});
|
|
4539
|
-
import { existsSync as existsSync12, readFileSync as
|
|
4635
|
+
import { existsSync as existsSync12, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
|
|
4540
4636
|
import { homedir as homedir5 } from "os";
|
|
4541
4637
|
import { join as join21 } from "path";
|
|
4542
4638
|
import { createInterface } from "readline/promises";
|
|
@@ -4546,7 +4642,7 @@ function findResetTarget() {
|
|
|
4546
4642
|
let raw;
|
|
4547
4643
|
let parsed;
|
|
4548
4644
|
try {
|
|
4549
|
-
raw =
|
|
4645
|
+
raw = readFileSync14(path7, "utf-8");
|
|
4550
4646
|
parsed = JSON.parse(raw);
|
|
4551
4647
|
} catch {
|
|
4552
4648
|
continue;
|
|
@@ -4647,7 +4743,7 @@ __export(audit_exports, {
|
|
|
4647
4743
|
__testing: () => __testing2,
|
|
4648
4744
|
runAudit: () => runAudit
|
|
4649
4745
|
});
|
|
4650
|
-
import { existsSync as existsSync13, readFileSync as
|
|
4746
|
+
import { existsSync as existsSync13, readFileSync as readFileSync15, readdirSync as readdirSync7 } from "fs";
|
|
4651
4747
|
import { homedir as homedir6 } from "os";
|
|
4652
4748
|
import { join as join22 } from "path";
|
|
4653
4749
|
function safeListDirs(dir) {
|
|
@@ -4668,7 +4764,7 @@ function readMcp(projectPath) {
|
|
|
4668
4764
|
const path7 = join22(projectPath, ".mcp.json");
|
|
4669
4765
|
if (!existsSync13(path7)) return { servers: [] };
|
|
4670
4766
|
try {
|
|
4671
|
-
const parsed = JSON.parse(
|
|
4767
|
+
const parsed = JSON.parse(readFileSync15(path7, "utf-8"));
|
|
4672
4768
|
const mcpServers = parsed.mcpServers ?? {};
|
|
4673
4769
|
const servers = Object.keys(mcpServers);
|
|
4674
4770
|
if (parsed.papi && !servers.includes("papi")) servers.push("papi");
|
|
@@ -4724,7 +4820,7 @@ function readGlobalSkills() {
|
|
|
4724
4820
|
function readGlobalMcpServers() {
|
|
4725
4821
|
if (!existsSync13(GLOBAL_CLAUDE_JSON)) return [];
|
|
4726
4822
|
try {
|
|
4727
|
-
const parsed = JSON.parse(
|
|
4823
|
+
const parsed = JSON.parse(readFileSync15(GLOBAL_CLAUDE_JSON, "utf-8"));
|
|
4728
4824
|
const servers = parsed.mcpServers ?? {};
|
|
4729
4825
|
return Object.keys(servers).sort((a, b2) => a.localeCompare(b2));
|
|
4730
4826
|
} catch {
|
|
@@ -4890,7 +4986,7 @@ var setup_exports = {};
|
|
|
4890
4986
|
__export(setup_exports, {
|
|
4891
4987
|
runSetup: () => runSetup
|
|
4892
4988
|
});
|
|
4893
|
-
import { existsSync as existsSync14, readFileSync as
|
|
4989
|
+
import { existsSync as existsSync14, readFileSync as readFileSync16, writeFileSync as writeFileSync7, chmodSync as chmodSync2, statSync as statSync8 } from "fs";
|
|
4894
4990
|
import { join as join23 } from "path";
|
|
4895
4991
|
function baseUrl() {
|
|
4896
4992
|
const fromEnv = process.env["PAPI_HOST"] ?? process.env["PAPI_BASE_URL"];
|
|
@@ -4927,7 +5023,7 @@ function writeMcpJson(opts) {
|
|
|
4927
5023
|
let parsed = {};
|
|
4928
5024
|
if (existsSync14(path7)) {
|
|
4929
5025
|
try {
|
|
4930
|
-
parsed = JSON.parse(
|
|
5026
|
+
parsed = JSON.parse(readFileSync16(path7, "utf-8"));
|
|
4931
5027
|
} catch {
|
|
4932
5028
|
throw new Error(`.mcp.json at ${path7} is not valid JSON. Fix it or remove it before re-running setup.`);
|
|
4933
5029
|
}
|
|
@@ -4950,7 +5046,7 @@ function writeMcpJson(opts) {
|
|
|
4950
5046
|
parsed.mcpServers = mcpServers;
|
|
4951
5047
|
writeFileSync7(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
4952
5048
|
try {
|
|
4953
|
-
const mode =
|
|
5049
|
+
const mode = statSync8(path7).mode & 511;
|
|
4954
5050
|
if (mode !== 384) chmodSync2(path7, 384);
|
|
4955
5051
|
} catch {
|
|
4956
5052
|
}
|
|
@@ -5058,7 +5154,7 @@ var init_setup = __esm({
|
|
|
5058
5154
|
});
|
|
5059
5155
|
|
|
5060
5156
|
// src/index.ts
|
|
5061
|
-
import { readFileSync as
|
|
5157
|
+
import { readFileSync as readFileSync17 } from "fs";
|
|
5062
5158
|
import { dirname as dirname6, join as join24, basename as basename2 } from "path";
|
|
5063
5159
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
5064
5160
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -5104,7 +5200,6 @@ function loadConfig() {
|
|
|
5104
5200
|
"\nPAPI is running, but no project is configured here.\n" + HELP_FOOTER + "\n"
|
|
5105
5201
|
);
|
|
5106
5202
|
}
|
|
5107
|
-
const anthropicApiKey = process.env.PAPI_API_KEY ?? "";
|
|
5108
5203
|
const autoCommit2 = process.env.PAPI_AUTO_COMMIT !== "false";
|
|
5109
5204
|
const baseBranch = process.env.PAPI_BASE_BRANCH ?? "main";
|
|
5110
5205
|
const autoPR = process.env.PAPI_AUTO_PR !== "false";
|
|
@@ -5178,7 +5273,6 @@ Already have an account? Make sure PAPI_USER_ID is set in your .mcp.json env con
|
|
|
5178
5273
|
return {
|
|
5179
5274
|
projectRoot,
|
|
5180
5275
|
papiDir: path.join(projectRoot, ".papi"),
|
|
5181
|
-
anthropicApiKey,
|
|
5182
5276
|
autoCommit: autoCommit2,
|
|
5183
5277
|
baseBranch,
|
|
5184
5278
|
autoPR,
|
|
@@ -5469,6 +5563,9 @@ var SECTION_HEADERS = [
|
|
|
5469
5563
|
"FILES LIKELY TOUCHED",
|
|
5470
5564
|
"EFFORT"
|
|
5471
5565
|
];
|
|
5566
|
+
function normaliseHeaderLine(line) {
|
|
5567
|
+
return line.trim().replace(/^#{1,6}\s*/, "").replace(/^-\s+/, "").replace(/^\*\*(.*?)\*\*$/, "$1").replace(/:\s*$/, "").trim();
|
|
5568
|
+
}
|
|
5472
5569
|
function splitSections(text) {
|
|
5473
5570
|
const sections = /* @__PURE__ */ new Map();
|
|
5474
5571
|
const lines = text.split("\n");
|
|
@@ -5481,8 +5578,8 @@ function splitSections(text) {
|
|
|
5481
5578
|
}
|
|
5482
5579
|
};
|
|
5483
5580
|
for (const line of lines) {
|
|
5484
|
-
const
|
|
5485
|
-
const matched = SECTION_HEADERS.find((h) =>
|
|
5581
|
+
const normalised = normaliseHeaderLine(line);
|
|
5582
|
+
const matched = SECTION_HEADERS.find((h) => normalised === h);
|
|
5486
5583
|
if (matched) {
|
|
5487
5584
|
flush();
|
|
5488
5585
|
currentSection = matched;
|
|
@@ -5503,7 +5600,10 @@ function parseChecklist(text) {
|
|
|
5503
5600
|
return text.split("\n").map((l) => l.replace(/^\s*\[[ x]]\s*/, "").trim()).filter((l) => l.length > 0);
|
|
5504
5601
|
}
|
|
5505
5602
|
function parseBuildHandoff(markdown) {
|
|
5506
|
-
if (!markdown.
|
|
5603
|
+
if (typeof markdown !== "string" || !markdown.trim()) return null;
|
|
5604
|
+
if (!markdown.includes("BUILD HANDOFF") && splitSections(markdown).size === 0) {
|
|
5605
|
+
return null;
|
|
5606
|
+
}
|
|
5507
5607
|
const taskIdMatch = markdown.match(/BUILD HANDOFF\s*—\s*(task-\d+)/);
|
|
5508
5608
|
const taskTitleMatch = markdown.match(/^Task:\s*(.+)$/m);
|
|
5509
5609
|
const cycleMatch = markdown.match(/^Cycle:\s*(\d+)$/m);
|
|
@@ -5538,6 +5638,28 @@ function parseBuildHandoff(markdown) {
|
|
|
5538
5638
|
effort
|
|
5539
5639
|
};
|
|
5540
5640
|
}
|
|
5641
|
+
function coerceBuildHandoff(fields, taskId) {
|
|
5642
|
+
const scope = ensureArray(fields.scope);
|
|
5643
|
+
const scopeBoundary = ensureArray(fields.scopeBoundary);
|
|
5644
|
+
if (scope.length === 0 && scopeBoundary.length === 0) return null;
|
|
5645
|
+
const effortRaw = typeof fields.effort === "string" ? fields.effort.trim().toUpperCase() : "";
|
|
5646
|
+
const effort = VALID_EFFORT_SIZES.has(effortRaw) ? effortRaw : "M";
|
|
5647
|
+
const str = (v) => typeof v === "string" ? v.trim() : "";
|
|
5648
|
+
return {
|
|
5649
|
+
uuid: str(fields.uuid) || randomUUID2(),
|
|
5650
|
+
taskId: str(fields.taskId) || taskId || "",
|
|
5651
|
+
taskTitle: str(fields.taskTitle),
|
|
5652
|
+
cycle: typeof fields.cycle === "number" ? fields.cycle : 0,
|
|
5653
|
+
whyNow: str(fields.whyNow),
|
|
5654
|
+
scope,
|
|
5655
|
+
scopeBoundary,
|
|
5656
|
+
acceptanceCriteria: ensureArray(fields.acceptanceCriteria),
|
|
5657
|
+
securityConsiderations: str(fields.securityConsiderations),
|
|
5658
|
+
verificationFiles: ensureArray(fields.verificationFiles),
|
|
5659
|
+
filesLikelyTouched: ensureArray(fields.filesLikelyTouched),
|
|
5660
|
+
effort
|
|
5661
|
+
};
|
|
5662
|
+
}
|
|
5541
5663
|
function ensureArray(value) {
|
|
5542
5664
|
if (Array.isArray(value)) return value;
|
|
5543
5665
|
if (typeof value === "string") {
|
|
@@ -8071,7 +8193,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
8071
8193
|
}
|
|
8072
8194
|
|
|
8073
8195
|
// src/server.ts
|
|
8074
|
-
import { readFileSync as
|
|
8196
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
8075
8197
|
import { access as access4, readdir as readdir4, readFile as readFile9 } from "fs/promises";
|
|
8076
8198
|
import { join as join19, dirname as dirname5 } from "path";
|
|
8077
8199
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
@@ -8912,6 +9034,7 @@ Everything in Part 1 (natural language) is **display-only**. Part 2 (structured
|
|
|
8912
9034
|
- Updated or created Active Decisions in Part 1? \u2192 Put them in \`activeDecisions\` array (with id and full body including ### heading)
|
|
8913
9035
|
- Found board corrections (wrong priority, missing fields, stale status) in Part 1? \u2192 Put them in \`boardCorrections\` array
|
|
8914
9036
|
- Generated BUILD HANDOFFs in Part 1? \u2192 Put them in \`cycleHandoffs\` array
|
|
9037
|
+
- **\`complexity\` uses the LONG forms only** \u2014 "XS", "Small", "Medium", "Large", "XL". Do NOT reuse the handoff EFFORT short-forms (S/M/L) for task complexity.
|
|
8915
9038
|
|
|
8916
9039
|
**Example with populated fields (DO NOT copy literally \u2014 adapt to your actual analysis):**
|
|
8917
9040
|
\`\`\`json
|
|
@@ -8952,13 +9075,20 @@ This is Cycle 0 \u2014 the first planning cycle for a brand-new project.
|
|
|
8952
9075
|
- Do NOT assume web-app patterns (routes, pages, components) unless the brief explicitly describes a web application
|
|
8953
9076
|
- All tasks: status Backlog, priority P1-P2, reviewed true, phase "Phase 1"
|
|
8954
9077
|
|
|
9078
|
+
**REASONING LINE (task-2868 \u2014 the first cycle must SHOW PAPI's intelligence, not read as a generic list).** For EVERY task, the \`notes\` field MUST OPEN with a single line in the exact form:
|
|
9079
|
+
\`Why: <one plain-language sentence, \u2264160 chars, on why THIS task earns its place in the first cycle>\`
|
|
9080
|
+
Then a blank line, then any other notes. Rules:
|
|
9081
|
+
- Task 1's \`Why:\` must also justify its POSITION \u2014 say why it is the opening slice (the thinnest thing that puts the core loop in front of the user). This one line is the cycle's sequencing rationale; the dashboard surfaces it above the first task.
|
|
9082
|
+
- Plain language for a semi-technical builder \u2014 no jargon, no raw IDs, not a restatement of the title.
|
|
9083
|
+
- This is a hard requirement: the dashboard's first-cycle experience reads and renders this \`Why:\` line per task.
|
|
9084
|
+
|
|
8955
9085
|
4. **First Active Decision** \u2014 If the description implies a clear architectural choice, create AD-1 with Confidence: MEDIUM. If no clear choice, skip this.
|
|
8956
9086
|
|
|
8957
9087
|
5. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for EVERY task created in step 3 (all 3-5 tasks). Include each in the \`cycleHandoffs\` array. **tempId join (REQUIRED \u2014 mismatches silently scramble handoffs):** give EVERY \`newTasks\` entry a unique \`tempId\` (\`"new-1"\`, \`"new-2"\`, \u2026), and set each \`cycleHandoffs\` \`taskId\` to the EXACT \`tempId\` of the newTask it belongs to. Do NOT rely on array order, and do NOT reuse a tempId. The builder needs handoffs to run \`build_execute\` \u2014 without them, tasks must be completed via \`ad_hoc\`, which breaks the normal flow.
|
|
8958
9088
|
|
|
8959
9089
|
### Structured output for Bootstrap:
|
|
8960
9090
|
In the JSON block, you MUST include:
|
|
8961
|
-
- "newTasks": array of task objects with ALL fields: title, status, priority, complexity, module, epic, phase, owner, notes. **This is how tasks get created on the board. If this array is empty, NO tasks will exist.**
|
|
9091
|
+
- "newTasks": array of task objects with ALL fields: title, status, priority, complexity, module, epic, phase, owner, notes. **This is how tasks get created on the board. If this array is empty, NO tasks will exist.** Every \`notes\` value MUST open with the \`Why: <sentence>\` reasoning line described in step 3 (the dashboard's first-cycle experience renders it).
|
|
8962
9092
|
- "productBrief": the full Product Brief markdown content. **If null, the brief stays as the template.**
|
|
8963
9093
|
- "activeDecisions": array of {id, body} objects. **If you created AD-1 in Part 1 but this array is empty, the AD will NOT be saved.**
|
|
8964
9094
|
- "recommendedTaskId": null (the handler will use the first new task)
|
|
@@ -9502,10 +9632,17 @@ function coerceToString(value) {
|
|
|
9502
9632
|
return JSON.stringify(value, null, 2);
|
|
9503
9633
|
}
|
|
9504
9634
|
function coerceStructuredOutput(parsed) {
|
|
9505
|
-
const cycleHandoffs = Array.isArray(parsed.cycleHandoffs) ? parsed.cycleHandoffs.map((h) =>
|
|
9506
|
-
taskId:
|
|
9507
|
-
buildHandoff
|
|
9508
|
-
|
|
9635
|
+
const cycleHandoffs = Array.isArray(parsed.cycleHandoffs) ? parsed.cycleHandoffs.map((h) => {
|
|
9636
|
+
const { taskId: _t, buildHandoff: _b, ...rest } = h;
|
|
9637
|
+
if (typeof h.buildHandoff === "object" && h.buildHandoff !== null && !Array.isArray(h.buildHandoff)) {
|
|
9638
|
+
Object.assign(rest, h.buildHandoff);
|
|
9639
|
+
}
|
|
9640
|
+
return {
|
|
9641
|
+
taskId: coerceToString(h.taskId),
|
|
9642
|
+
buildHandoff: typeof h.buildHandoff === "string" ? h.buildHandoff : "",
|
|
9643
|
+
...Object.keys(rest).length > 0 ? { structuredFields: rest } : {}
|
|
9644
|
+
};
|
|
9645
|
+
}) : [];
|
|
9509
9646
|
const newTasks = Array.isArray(parsed.newTasks) ? parsed.newTasks.map((t) => ({
|
|
9510
9647
|
// task-2242: stable join key (optional — undefined falls back to index).
|
|
9511
9648
|
tempId: t.tempId !== void 0 && t.tempId !== null ? coerceToString(t.tempId) : void 0,
|
|
@@ -10615,9 +10752,12 @@ async function applyHandoffs(adapter2, rawLlmOutput, cycleNumber, force = false)
|
|
|
10615
10752
|
skipped++;
|
|
10616
10753
|
continue;
|
|
10617
10754
|
}
|
|
10618
|
-
const parsed = parseBuildHandoff(handoff.buildHandoff);
|
|
10755
|
+
const parsed = parseBuildHandoff(handoff.buildHandoff) ?? (handoff.structuredFields ? coerceBuildHandoff(handoff.structuredFields, handoff.taskId) : null);
|
|
10619
10756
|
if (!parsed) {
|
|
10620
|
-
|
|
10757
|
+
const received = (handoff.buildHandoff ?? "").slice(0, 80);
|
|
10758
|
+
warnings.push(
|
|
10759
|
+
`Failed to parse handoff for ${handoff.taskId}. Received: "${received}${(handoff.buildHandoff ?? "").length > 80 ? "\u2026" : ""}". Send EITHER the BUILD HANDOFF markdown template in \`buildHandoff\` (section headers on their own lines: SCOPE (DO THIS) / SCOPE BOUNDARY (DO NOT DO THIS) / ACCEPTANCE CRITERIA / EFFORT, bullets as "- item") OR structured fields alongside taskId: {scope: string[], scopeBoundary: string[], acceptanceCriteria: string[], effort: "XS|S|M|L|XL"}.`
|
|
10760
|
+
);
|
|
10621
10761
|
continue;
|
|
10622
10762
|
}
|
|
10623
10763
|
const invalidFields = validateHandoffScope(parsed);
|
|
@@ -10625,7 +10765,6 @@ async function applyHandoffs(adapter2, rawLlmOutput, cycleNumber, force = false)
|
|
|
10625
10765
|
warnings.push(
|
|
10626
10766
|
`Rejected handoff for ${handoff.taskId}: missing or empty ${invalidFields.join(", ")}. Handoffs without explicit scope produce ambiguous builds.`
|
|
10627
10767
|
);
|
|
10628
|
-
skipped++;
|
|
10629
10768
|
continue;
|
|
10630
10769
|
}
|
|
10631
10770
|
if (!parsed.createdAt) {
|
|
@@ -11100,6 +11239,20 @@ async function resolveOwnerGate(adapter2, config2) {
|
|
|
11100
11239
|
}
|
|
11101
11240
|
|
|
11102
11241
|
// src/services/plan.ts
|
|
11242
|
+
var COMPLEXITY_ALIASES = {
|
|
11243
|
+
"XS": "XS",
|
|
11244
|
+
"S": "Small",
|
|
11245
|
+
"SMALL": "Small",
|
|
11246
|
+
"M": "Medium",
|
|
11247
|
+
"MEDIUM": "Medium",
|
|
11248
|
+
"L": "Large",
|
|
11249
|
+
"LARGE": "Large",
|
|
11250
|
+
"XL": "XL"
|
|
11251
|
+
};
|
|
11252
|
+
function normalizeComplexity(value) {
|
|
11253
|
+
const key = (value ?? "").trim().toUpperCase();
|
|
11254
|
+
return COMPLEXITY_ALIASES[key] ?? "Small";
|
|
11255
|
+
}
|
|
11103
11256
|
var PLAN_BUILD_REPORT_BUDGET = { maxReports: 12, fieldBudget: 280 };
|
|
11104
11257
|
function leadChainWithRecommended(chain, recommendedTaskId) {
|
|
11105
11258
|
const rec = recommendedTaskId?.trim();
|
|
@@ -12038,10 +12191,13 @@ ${cleanContent}`;
|
|
|
12038
12191
|
} catch {
|
|
12039
12192
|
}
|
|
12040
12193
|
const handoffs = (data.cycleHandoffs ?? []).map((h) => {
|
|
12041
|
-
const parsed = parseBuildHandoff(h.buildHandoff);
|
|
12194
|
+
const parsed = parseBuildHandoff(h.buildHandoff) ?? (h.structuredFields ? coerceBuildHandoff(h.structuredFields, h.taskId) : null);
|
|
12042
12195
|
if (parsed && !parsed.createdAt) {
|
|
12043
12196
|
parsed.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12044
12197
|
}
|
|
12198
|
+
if (!parsed) {
|
|
12199
|
+
console.error(`[plan] dropping unparseable handoff for ${h.taskId} (not the BUILD HANDOFF template and no structured scope fields)`);
|
|
12200
|
+
}
|
|
12045
12201
|
return { taskId: h.taskId, handoff: parsed };
|
|
12046
12202
|
}).filter((h) => h.handoff != null);
|
|
12047
12203
|
const cycleTaskIds = data.cycleTaskIds?.length ? data.cycleTaskIds : (data.cycleHandoffs ?? []).map((h) => h.taskId);
|
|
@@ -12128,7 +12284,7 @@ ${cleanContent}`;
|
|
|
12128
12284
|
title: t.title,
|
|
12129
12285
|
status: t.status || "Backlog",
|
|
12130
12286
|
priority: t.priority || "P1 High",
|
|
12131
|
-
complexity: t.complexity
|
|
12287
|
+
complexity: normalizeComplexity(t.complexity),
|
|
12132
12288
|
module: t.module || "Core",
|
|
12133
12289
|
epic: t.epic || "Platform",
|
|
12134
12290
|
phase: t.phase || "Phase 1",
|
|
@@ -12276,7 +12432,7 @@ ${cleanContent}`;
|
|
|
12276
12432
|
title: task.title,
|
|
12277
12433
|
status: task.status || "Backlog",
|
|
12278
12434
|
priority: task.priority || "P1 High",
|
|
12279
|
-
complexity: task.complexity
|
|
12435
|
+
complexity: normalizeComplexity(task.complexity),
|
|
12280
12436
|
module: task.module || "Core",
|
|
12281
12437
|
epic: task.epic || "Platform",
|
|
12282
12438
|
phase: task.phase || "Phase 1",
|
|
@@ -13288,6 +13444,13 @@ async function resolveLlmResponse(inlineResponse, filePath) {
|
|
|
13288
13444
|
}
|
|
13289
13445
|
const resolvedPath = filePath.trim();
|
|
13290
13446
|
if (!isAbsolute(resolvedPath)) {
|
|
13447
|
+
const looksWindows = /^[A-Za-z]:[\\/]/.test(resolvedPath) || resolvedPath.startsWith("\\\\");
|
|
13448
|
+
if (looksWindows && process.platform !== "win32") {
|
|
13449
|
+
return {
|
|
13450
|
+
ok: false,
|
|
13451
|
+
error: `llm_response_file points at a path on YOUR machine (${resolvedPath}), but this PAPI server runs remotely and cannot read your filesystem. llm_response_file only works with a locally installed (stdio) server. On the hosted connection, pass the content inline via llm_response.`
|
|
13452
|
+
};
|
|
13453
|
+
}
|
|
13291
13454
|
return {
|
|
13292
13455
|
ok: false,
|
|
13293
13456
|
error: `llm_response_file must be an absolute path, got: ${resolvedPath}`
|
|
@@ -13534,7 +13697,7 @@ var planTool = {
|
|
|
13534
13697
|
},
|
|
13535
13698
|
llm_response_file: {
|
|
13536
13699
|
type: "string",
|
|
13537
|
-
description: 'Absolute path to a file containing the plan output (mode "apply" only). Use this when the response is too large to pass as a string parameter (some hosts cap inputs around 50KB). The file must be absolute, exist, and be \u2264500KB. Mutually exclusive with llm_response.'
|
|
13700
|
+
description: 'Absolute path to a file containing the plan output (mode "apply" only). LOCAL stdio servers only \u2014 on the hosted connection the server cannot read files on your machine; pass llm_response inline instead. Use this when the response is too large to pass as a string parameter (some hosts cap inputs around 50KB). The file must be absolute, exist, and be \u2264500KB. Mutually exclusive with llm_response.'
|
|
13538
13701
|
},
|
|
13539
13702
|
cycle_number: {
|
|
13540
13703
|
type: "number",
|
|
@@ -13609,7 +13772,7 @@ function formatPlanResult(result) {
|
|
|
13609
13772
|
return textResponse(
|
|
13610
13773
|
`${pullLine}**${modeLabel} Mode \u2014 ${cycleLabel}**
|
|
13611
13774
|
|
|
13612
|
-
\u26A0\uFE0F **Persistence failed:** Structured output could not be parsed. Cycle log, board corrections, handoffs, and Active Decisions were NOT saved.
|
|
13775
|
+
\u26A0\uFE0F **Persistence failed:** Structured output could not be parsed. Cycle log, board corrections, handoffs, and Active Decisions were NOT saved. Your llm_response must contain the literal marker \`<!-- PAPI_STRUCTURED_OUTPUT -->\` followed by the JSON inside a \`\`\`json code fence \u2014 resend with that exact envelope (do not strip the marker or the fence).`
|
|
13613
13776
|
);
|
|
13614
13777
|
}
|
|
13615
13778
|
const lines = [];
|
|
@@ -15940,6 +16103,11 @@ Confidence: ${input.confidence}. Captured mid-conversation via strategy_change c
|
|
|
15940
16103
|
}
|
|
15941
16104
|
|
|
15942
16105
|
// src/tools/strategy.ts
|
|
16106
|
+
function toDateLabel(value) {
|
|
16107
|
+
if (typeof value === "string") return value.slice(0, 10);
|
|
16108
|
+
const d = value instanceof Date ? value : new Date(value);
|
|
16109
|
+
return Number.isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10);
|
|
16110
|
+
}
|
|
15943
16111
|
var reviewPrepareCache = new PerCallerCache();
|
|
15944
16112
|
var strategyReviewTool = {
|
|
15945
16113
|
name: "strategy_review",
|
|
@@ -15959,7 +16127,7 @@ var strategyReviewTool = {
|
|
|
15959
16127
|
},
|
|
15960
16128
|
llm_response_file: {
|
|
15961
16129
|
type: "string",
|
|
15962
|
-
description: 'Absolute path to a file containing the review output (mode "apply" only). Use this when the response is too large to pass as a string parameter (some hosts cap inputs around 50KB). The file must be absolute, exist, and be \u2264500KB. Mutually exclusive with llm_response.'
|
|
16130
|
+
description: 'Absolute path to a file containing the review output (mode "apply" only). LOCAL stdio servers only \u2014 on the hosted connection the server cannot read files on your machine; pass llm_response inline instead. Use this when the response is too large to pass as a string parameter (some hosts cap inputs around 50KB). The file must be absolute, exist, and be \u2264500KB. Mutually exclusive with llm_response.'
|
|
15963
16131
|
},
|
|
15964
16132
|
cycle_number: {
|
|
15965
16133
|
type: "number",
|
|
@@ -16241,7 +16409,7 @@ This topic will surface in the next \`strategy_review\`.`
|
|
|
16241
16409
|
const lines = topics.map((t, i) => {
|
|
16242
16410
|
const cycleSuffix = t.sourceCycle != null ? ` (Cycle ${t.sourceCycle})` : "";
|
|
16243
16411
|
return `${i + 1}. ${t.topic}
|
|
16244
|
-
_source: ${t.source}${cycleSuffix} \xB7 queued ${t.createdAt
|
|
16412
|
+
_source: ${t.source}${cycleSuffix} \xB7 queued ${toDateLabel(t.createdAt)}_`;
|
|
16245
16413
|
});
|
|
16246
16414
|
return textResponse(
|
|
16247
16415
|
`**Pending Agenda (${topics.length})** \u2014 surfaces at next strategy review
|
|
@@ -17934,7 +18102,9 @@ async function applySetupOutputs(adapter2, config2, input, collector, briefText,
|
|
|
17934
18102
|
}
|
|
17935
18103
|
if (adapter2.upsertActiveDecision) {
|
|
17936
18104
|
const title = ad.title || ad.body.split("\n")[0].replace(/^#+\s*/, "").slice(0, 120);
|
|
17937
|
-
|
|
18105
|
+
const confidenceMatch = ad.body.match(/\[Confidence:\s*(HIGH|MEDIUM|LOW)\]/i);
|
|
18106
|
+
const confidence = confidenceMatch ? confidenceMatch[1].toUpperCase() : ad.confidence || "MEDIUM";
|
|
18107
|
+
await adapter2.upsertActiveDecision(ad.id, ad.body, title, confidence, 0);
|
|
17938
18108
|
} else {
|
|
17939
18109
|
await adapter2.updateActiveDecision(ad.id, ad.body, 0);
|
|
17940
18110
|
}
|
|
@@ -18714,10 +18884,14 @@ ${[created, skipped].filter(Boolean).join(", ")}.${idea}`;
|
|
|
18714
18884
|
\u26A0\uFE0F **Setup warnings (non-blocking):**
|
|
18715
18885
|
${result.warnings.map((w) => `- ${w}`).join("\n")}` : "";
|
|
18716
18886
|
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
18717
|
-
|
|
18718
|
-
|
|
18887
|
+
const isHosted = !!result.filesToWrite && !result.filesToWrite.isEmpty();
|
|
18888
|
+
const filesNote = isHosted ? `
|
|
18889
|
+
|
|
18890
|
+
**Important:** This is a remote PAPI connection, so the server could not write to your project directory. Setup prepared your files (${harnessFiles}, .claude/settings.json, docs/) and returned them in the scaffolding section below \u2014 **write each one to disk, then commit** before running \`build_execute\` (it requires a clean working directory).` : `
|
|
18719
18891
|
|
|
18720
|
-
**Important:** Setup created/modified files (${harnessFiles}, .claude/settings.json, docs/). Commit these changes before running \`build_execute\` \u2014 it requires a clean working directory
|
|
18892
|
+
**Important:** Setup created/modified files (${harnessFiles}, .claude/settings.json, docs/). Commit these changes before running \`build_execute\` \u2014 it requires a clean working directory.`;
|
|
18893
|
+
return textResponse(
|
|
18894
|
+
`${prefix}Product Brief generated and saved.${briefRegenNote}${adNote}${northStarNote}${taskNote}${constraintsHint}${editorNote}${gitignoreNote}${warningsNote}${filesNote}
|
|
18721
18895
|
|
|
18722
18896
|
Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-written brief.
|
|
18723
18897
|
|
|
@@ -19727,6 +19901,61 @@ async function completeRelease(tracker, opts) {
|
|
|
19727
19901
|
}
|
|
19728
19902
|
await tracker.recordStep("released", { metadata: { version: opts.version } });
|
|
19729
19903
|
}
|
|
19904
|
+
var OPEN_PR_STALE_DAYS = 21;
|
|
19905
|
+
function taskIdFromBranch(headRefName) {
|
|
19906
|
+
const m = /^feat\/(task-\d+)\b/.exec(headRefName);
|
|
19907
|
+
return m ? m[1] : null;
|
|
19908
|
+
}
|
|
19909
|
+
function classifyOpenPr(pr, cycleNum, inReviewCycleTaskIds, nowMs) {
|
|
19910
|
+
const head = pr.headRefName;
|
|
19911
|
+
const ageDays = (nowMs - Date.parse(pr.createdAt)) / 864e5;
|
|
19912
|
+
if (new RegExp(`^feat/cycle-${cycleNum}-`).test(head)) {
|
|
19913
|
+
return { pr, bucket: "cycle-branch", action: `merge into this release \u2014 \`${head}\` is this cycle's branch and still open` };
|
|
19914
|
+
}
|
|
19915
|
+
const taskId = taskIdFromBranch(head);
|
|
19916
|
+
if (taskId && inReviewCycleTaskIds.has(taskId)) {
|
|
19917
|
+
return { pr, bucket: "held-adhoc-this-cycle", action: `MERGE before closing \u2014 ${taskId} is In Review pinned to Cycle ${cycleNum}` };
|
|
19918
|
+
}
|
|
19919
|
+
if (taskId) {
|
|
19920
|
+
return { pr, bucket: "held-adhoc-other", action: "held adhoc not pinned to this cycle \u2014 defer, or merge if ready" };
|
|
19921
|
+
}
|
|
19922
|
+
if (Number.isFinite(ageDays) && ageDays > OPEN_PR_STALE_DAYS) {
|
|
19923
|
+
return { pr, bucket: "stale", action: `review/close \u2014 open ${Math.round(ageDays)}d with no cycle link` };
|
|
19924
|
+
}
|
|
19925
|
+
return { pr, bucket: "external-other", action: "review manually \u2014 merge, defer, or close" };
|
|
19926
|
+
}
|
|
19927
|
+
var OPEN_PR_BUCKET_ORDER = [
|
|
19928
|
+
"held-adhoc-this-cycle",
|
|
19929
|
+
"cycle-branch",
|
|
19930
|
+
"external-other",
|
|
19931
|
+
"stale",
|
|
19932
|
+
"held-adhoc-other"
|
|
19933
|
+
];
|
|
19934
|
+
function buildOpenPrSweepLines(openPrs, cycleNum, inReviewCycleTaskIds, nowMs) {
|
|
19935
|
+
const lines = [];
|
|
19936
|
+
if (openPrs === null) {
|
|
19937
|
+
lines.push("", "**Open-PR sweep:** skipped \u2014 `gh` unavailable. Run `gh pr list` yourself to check for held/external PRs before considering the cycle closed.");
|
|
19938
|
+
} else if (openPrs.length > 0) {
|
|
19939
|
+
const classified = openPrs.map((pr) => classifyOpenPr(pr, cycleNum, inReviewCycleTaskIds, nowMs)).sort((a, b2) => OPEN_PR_BUCKET_ORDER.indexOf(a.bucket) - OPEN_PR_BUCKET_ORDER.indexOf(b2.bucket));
|
|
19940
|
+
lines.push("", `**Open-PR sweep \u2014 ${openPrs.length} open PR(s). Resolve each before considering Cycle ${cycleNum} closed:**`);
|
|
19941
|
+
for (const c of classified) {
|
|
19942
|
+
lines.push(`- #${c.pr.number} \`${c.pr.headRefName}\` by ${c.pr.author} \u2014 [${c.bucket}] ${c.action}`);
|
|
19943
|
+
}
|
|
19944
|
+
}
|
|
19945
|
+
if (inReviewCycleTaskIds.size > 0) {
|
|
19946
|
+
const prByTask = /* @__PURE__ */ new Map();
|
|
19947
|
+
for (const p of openPrs ?? []) {
|
|
19948
|
+
const id = taskIdFromBranch(p.headRefName);
|
|
19949
|
+
if (id) prByTask.set(id, p.number);
|
|
19950
|
+
}
|
|
19951
|
+
const rows = [...inReviewCycleTaskIds].sort().map((id) => {
|
|
19952
|
+
const prNum = prByTask.get(id);
|
|
19953
|
+
return ` - ${id}${prNum ? ` (PR #${prNum})` : " (no open PR found)"}`;
|
|
19954
|
+
});
|
|
19955
|
+
lines.push("", `\u26A0\uFE0F **${inReviewCycleTaskIds.size} task(s) still In Review, pinned to Cycle ${cycleNum} \u2014 their work is NOT merged. Accept/merge or defer before closing:**`, ...rows);
|
|
19956
|
+
}
|
|
19957
|
+
return lines;
|
|
19958
|
+
}
|
|
19730
19959
|
|
|
19731
19960
|
// src/tools/release.ts
|
|
19732
19961
|
init_git();
|
|
@@ -20217,6 +20446,20 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
20217
20446
|
if (result.warnings?.length) {
|
|
20218
20447
|
lines.push("", "\u26A0\uFE0F Warnings: " + result.warnings.join("; "));
|
|
20219
20448
|
}
|
|
20449
|
+
try {
|
|
20450
|
+
const openPrs = listOpenPullRequests(config2.projectRoot);
|
|
20451
|
+
const closedCycle = result.cycleClosed ?? 0;
|
|
20452
|
+
let inReviewIds = /* @__PURE__ */ new Set();
|
|
20453
|
+
if (closedCycle > 0) {
|
|
20454
|
+
const board = await adapter2.queryBoard();
|
|
20455
|
+
inReviewIds = new Set(
|
|
20456
|
+
board.filter((t) => t.status === "In Review" && t.cycle === closedCycle).map((t) => t.id)
|
|
20457
|
+
);
|
|
20458
|
+
}
|
|
20459
|
+
const sweep = buildOpenPrSweepLines(openPrs, closedCycle, inReviewIds, Date.now());
|
|
20460
|
+
if (sweep.length > 0) lines.push(...sweep);
|
|
20461
|
+
} catch {
|
|
20462
|
+
}
|
|
20220
20463
|
tracker.mark("surface-discovered-issues");
|
|
20221
20464
|
try {
|
|
20222
20465
|
const closedCycle = result.cycleClosed ?? 0;
|
|
@@ -20958,8 +21201,8 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
20958
21201
|
} else {
|
|
20959
21202
|
const stashLabel = `papi-autostash/${taskId}-${Math.floor(Date.now() / 1e3)}`;
|
|
20960
21203
|
try {
|
|
20961
|
-
const { execFileSync:
|
|
20962
|
-
|
|
21204
|
+
const { execFileSync: execFileSync7 } = await import("child_process");
|
|
21205
|
+
execFileSync7("git", ["stash", "push", "-u", "-m", stashLabel, "--", ...toStash], {
|
|
20963
21206
|
cwd: config2.projectRoot,
|
|
20964
21207
|
encoding: "utf-8"
|
|
20965
21208
|
});
|
|
@@ -20985,8 +21228,8 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
20985
21228
|
if (hasRemote(config2.projectRoot) && !featureBranchExistsLocally) {
|
|
20986
21229
|
if (featureBranchOnOrigin) {
|
|
20987
21230
|
try {
|
|
20988
|
-
const { execFileSync:
|
|
20989
|
-
|
|
21231
|
+
const { execFileSync: execFileSync7 } = await import("child_process");
|
|
21232
|
+
execFileSync7("git", ["fetch", "origin", `${featureBranch}:${featureBranch}`], {
|
|
20990
21233
|
cwd: config2.projectRoot,
|
|
20991
21234
|
encoding: "utf-8",
|
|
20992
21235
|
timeout: 6e4
|
|
@@ -22110,6 +22353,10 @@ var buildExecuteTool = {
|
|
|
22110
22353
|
enum: ["yes", "no", "partial"],
|
|
22111
22354
|
description: "Whether the build was completed. Required for complete."
|
|
22112
22355
|
},
|
|
22356
|
+
acceptance_confirmed: {
|
|
22357
|
+
type: "boolean",
|
|
22358
|
+
description: `task-2833: set true to assert every acceptance criterion in the task's BUILD HANDOFF was met. Required to record a completed:"yes" build when the handoff lists acceptance criteria \u2014 without it, build_execute returns the criteria checklist and does NOT mark the task Done (the report is not discarded; re-send with acceptance_confirmed:true). Tasks with no acceptance criteria, and completed:"partial"/"no", are unaffected.`
|
|
22359
|
+
},
|
|
22113
22360
|
effort: {
|
|
22114
22361
|
type: "string",
|
|
22115
22362
|
enum: ["XS", "S", "M", "L", "XL"],
|
|
@@ -22565,6 +22812,25 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
22565
22812
|
if (!parsedEstimatedEffort) {
|
|
22566
22813
|
return errorResponse(`Invalid estimated_effort value "${estimatedEffort}". Must be one of: XS, S, M, L, XL.`);
|
|
22567
22814
|
}
|
|
22815
|
+
const acceptanceConfirmed = args.acceptance_confirmed === true;
|
|
22816
|
+
if (completed === "yes" && !acceptanceConfirmed) {
|
|
22817
|
+
const gateInfo = adapter2.getProjectInfo ? await adapter2.getProjectInfo().catch(() => null) : null;
|
|
22818
|
+
const gateCaps = gateInfo?.capabilities ?? {};
|
|
22819
|
+
const gateTask = isCapabilityEnabled(gateCaps, "acceptanceGate") ? await adapter2.getTask(taskId).catch(() => null) : null;
|
|
22820
|
+
const criteria = (gateTask?.buildHandoff?.acceptanceCriteria ?? []).filter((c) => c && c.trim());
|
|
22821
|
+
if (criteria.length > 0) {
|
|
22822
|
+
const checklist = criteria.map((c) => ` - [ ] ${c}`).join("\n");
|
|
22823
|
+
return textResponse(
|
|
22824
|
+
`**Acceptance criteria not yet confirmed for ${taskId}.**
|
|
22825
|
+
|
|
22826
|
+
This task's BUILD HANDOFF lists ${criteria.length} acceptance criteri${criteria.length === 1 ? "on" : "a"}. Confirm each was met, then re-call \`build_execute\` complete with the SAME report fields plus \`acceptance_confirmed: true\`:
|
|
22827
|
+
|
|
22828
|
+
${checklist}
|
|
22829
|
+
|
|
22830
|
+
Your report was NOT discarded and the task is NOT yet Done \u2014 re-send with \`acceptance_confirmed: true\` to record completion. If a criterion was NOT met, the task is not complete: finish it, or report \`completed: "partial"\` with what remains in \`surprises\`.`
|
|
22831
|
+
);
|
|
22832
|
+
}
|
|
22833
|
+
}
|
|
22568
22834
|
const tracker = new ProgressTracker("complete_validate").bindStream(adapter2, { stage: "build", taskId });
|
|
22569
22835
|
try {
|
|
22570
22836
|
tracker.mark("complete_build");
|
|
@@ -23042,7 +23308,7 @@ var bugTool = {
|
|
|
23042
23308
|
severity: {
|
|
23043
23309
|
type: "string",
|
|
23044
23310
|
enum: ["critical", "major", "minor"],
|
|
23045
|
-
description: 'Bug severity (default: "major"). Critical = P1,
|
|
23311
|
+
description: 'Bug severity (default: "major"). Critical = P0, Major = P1, Minor = P2.'
|
|
23046
23312
|
},
|
|
23047
23313
|
notes: {
|
|
23048
23314
|
type: "string",
|
|
@@ -24222,6 +24488,13 @@ Re-run build_execute complete with a production_verification field, then re-subm
|
|
|
24222
24488
|
|
|
24223
24489
|
// src/tools/review.ts
|
|
24224
24490
|
var REVIEW_DISPATCH_THRESHOLD = 50 * 1024;
|
|
24491
|
+
var REVIEW_DISPATCH_CEILING = 40 * 1024;
|
|
24492
|
+
var REVIEW_ECHO_CAP = 4e3;
|
|
24493
|
+
function trimForEcho(text) {
|
|
24494
|
+
if (text.length <= REVIEW_ECHO_CAP) return text;
|
|
24495
|
+
return `${text.slice(0, REVIEW_ECHO_CAP)}
|
|
24496
|
+
\u2026[trimmed ${text.length - REVIEW_ECHO_CAP} chars]`;
|
|
24497
|
+
}
|
|
24225
24498
|
var REVIEW_RUBRIC = [
|
|
24226
24499
|
"You are reviewing a completed PAPI build for acceptance. Judge:",
|
|
24227
24500
|
"- Correctness: does the change do what the build report claims, without obvious bugs?",
|
|
@@ -24551,6 +24824,7 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
24551
24824
|
const autoDispatchOptIn = args.dispatch !== "inline" && process.env.PAPI_AUTO_DISPATCH !== "false" && isCapabilityEnabled(caps, "prReviewer");
|
|
24552
24825
|
const autoDispatchEligible = !verdict && autoDispatchOptIn;
|
|
24553
24826
|
const capabilityAutoReviewEligible = verdict === "accept" && !autoReview && autoDispatchOptIn;
|
|
24827
|
+
let capabilityReviewSkippedNote = "";
|
|
24554
24828
|
if ((explicitDispatch || autoDispatchEligible || capabilityAutoReviewEligible) && stage === "build-acceptance" && taskId) {
|
|
24555
24829
|
const dispatch = await buildReviewDispatch(
|
|
24556
24830
|
adapter2,
|
|
@@ -24560,8 +24834,16 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
24560
24834
|
);
|
|
24561
24835
|
if (!dispatch.ok) {
|
|
24562
24836
|
if (explicitDispatch) return errorResponse(dispatch.error);
|
|
24563
|
-
} else if (explicitDispatch ||
|
|
24837
|
+
} else if (explicitDispatch || autoDispatchEligible && dispatch.contextBytes > REVIEW_DISPATCH_THRESHOLD) {
|
|
24564
24838
|
return textResponse(dispatch.prompt);
|
|
24839
|
+
} else if (capabilityAutoReviewEligible) {
|
|
24840
|
+
if (dispatch.contextBytes <= REVIEW_DISPATCH_CEILING) {
|
|
24841
|
+
return textResponse(dispatch.prompt);
|
|
24842
|
+
}
|
|
24843
|
+
const kb = (dispatch.contextBytes / 1024).toFixed(0);
|
|
24844
|
+
capabilityReviewSkippedNote = `
|
|
24845
|
+
|
|
24846
|
+
> \u26A0\uFE0F pr-reviewer auto-review skipped \u2014 the diff/build-report (~${kb} KB) exceeds the ${REVIEW_DISPATCH_CEILING / 1024} KB inline-dispatch ceiling, which would overflow the response and drop the accept (task-2854). Verdict recorded directly. To review the diff explicitly, run \`review_submit ${taskId} build-acceptance accept dispatch:"subagent"\`.`;
|
|
24565
24847
|
}
|
|
24566
24848
|
}
|
|
24567
24849
|
if (explicitDispatch && stage !== "build-acceptance") {
|
|
@@ -24721,36 +25003,51 @@ ${overlap}`;
|
|
|
24721
25003
|
|
|
24722
25004
|
\u2705 Verdict recorded. All cycle tasks are Done, but **auto-release is owner-only** \u2014 your identity does not match this project's owner, so no release was cut. Push your branch and open a PR for the owner to run \`release\`.${resolutionNote}`;
|
|
24723
25005
|
} else if (cycleTasks.length > 0 && cycleTasks.every((t) => t.status === "Done")) {
|
|
24724
|
-
|
|
24725
|
-
|
|
24726
|
-
|
|
25006
|
+
let planRunCount = null;
|
|
25007
|
+
if (typeof adapter2.countPlanRunsForCycle === "function") {
|
|
25008
|
+
try {
|
|
25009
|
+
planRunCount = await adapter2.countPlanRunsForCycle(result.currentCycle);
|
|
25010
|
+
} catch {
|
|
25011
|
+
planRunCount = null;
|
|
25012
|
+
}
|
|
25013
|
+
}
|
|
25014
|
+
if (planRunCount === 0) {
|
|
24727
25015
|
autoReleaseNote = `
|
|
24728
25016
|
|
|
24729
25017
|
---
|
|
24730
25018
|
|
|
25019
|
+
\u26A0\uFE0F **Auto-release skipped** \u2014 Cycle ${result.currentCycle} has **no plan run** (100% injected/adhoc work), so it was never planned. Auto-release only fires for planned cycles, to avoid silently shipping a cycle nobody opened (C340 incident). All tasks are Done \u2014 run \`release\` explicitly to close and ship this cycle.`;
|
|
25020
|
+
} else {
|
|
25021
|
+
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
25022
|
+
const unmergedCycleBranches = isGitAvailable() && isGitRepo(config2.projectRoot) ? listGroupedCycleBranches(config2.projectRoot, result.currentCycle, baseBranch) : [];
|
|
25023
|
+
if (unmergedCycleBranches.length > 0) {
|
|
25024
|
+
autoReleaseNote = `
|
|
25025
|
+
|
|
25026
|
+
---
|
|
25027
|
+
|
|
24731
25028
|
\u26A0\uFE0F **Auto-release skipped** \u2014 all tasks are Done but ${unmergedCycleBranches.length} cycle branch(es) not yet merged: \`${unmergedCycleBranches.join("`, `")}\`.
|
|
24732
25029
|
|
|
24733
25030
|
Merge or squash those PRs first, then run \`release\` manually.`;
|
|
24734
|
-
|
|
24735
|
-
|
|
24736
|
-
|
|
24737
|
-
|
|
24738
|
-
|
|
24739
|
-
|
|
24740
|
-
|
|
24741
|
-
|
|
24742
|
-
|
|
24743
|
-
|
|
24744
|
-
|
|
24745
|
-
|
|
24746
|
-
|
|
24747
|
-
|
|
24748
|
-
|
|
25031
|
+
} else {
|
|
25032
|
+
try {
|
|
25033
|
+
const allReviews = await adapter2.getRecentReviews(200);
|
|
25034
|
+
const cycleReviews = allReviews.filter(
|
|
25035
|
+
(r) => r.cycle === result.currentCycle && r.stage === "build-acceptance"
|
|
25036
|
+
);
|
|
25037
|
+
const reviewsWithAutoReview = cycleReviews.filter((r) => r.autoReview);
|
|
25038
|
+
if (reviewsWithAutoReview.length > 0) {
|
|
25039
|
+
const verdictCounts = { pass: 0, warn: 0, fail: 0 };
|
|
25040
|
+
const findingsBySeverity = { error: 0, warning: 0, info: 0 };
|
|
25041
|
+
for (const r of reviewsWithAutoReview) {
|
|
25042
|
+
if (r.autoReview) {
|
|
25043
|
+
verdictCounts[r.autoReview.verdict] = (verdictCounts[r.autoReview.verdict] ?? 0) + 1;
|
|
25044
|
+
for (const f of r.autoReview.findings) {
|
|
25045
|
+
findingsBySeverity[f.severity] = (findingsBySeverity[f.severity] ?? 0) + 1;
|
|
25046
|
+
}
|
|
24749
25047
|
}
|
|
24750
25048
|
}
|
|
24751
|
-
|
|
24752
|
-
|
|
24753
|
-
batchSummaryNote = `
|
|
25049
|
+
const totalFindings = findingsBySeverity.error + findingsBySeverity.warning + findingsBySeverity.info;
|
|
25050
|
+
batchSummaryNote = `
|
|
24754
25051
|
|
|
24755
25052
|
---
|
|
24756
25053
|
|
|
@@ -24758,42 +25055,42 @@ Merge or squash those PRs first, then run \`release\` manually.`;
|
|
|
24758
25055
|
|
|
24759
25056
|
- Verdicts: ${verdictCounts.pass} pass, ${verdictCounts.warn} warn, ${verdictCounts.fail} fail
|
|
24760
25057
|
` + (totalFindings > 0 ? `- Findings: ${findingsBySeverity.error} error${findingsBySeverity.error !== 1 ? "s" : ""}, ${findingsBySeverity.warning} warning${findingsBySeverity.warning !== 1 ? "s" : ""}, ${findingsBySeverity.info} info` : "- No findings logged");
|
|
25058
|
+
}
|
|
25059
|
+
} catch {
|
|
24761
25060
|
}
|
|
24762
|
-
|
|
24763
|
-
|
|
24764
|
-
|
|
24765
|
-
|
|
24766
|
-
if (autoGate.action !== "proceed") {
|
|
24767
|
-
autoReleaseNote = `
|
|
25061
|
+
const version = `v0.${result.currentCycle}.0`;
|
|
25062
|
+
const autoGate = evaluateReleaseGate(caps, config2.gateCommand, void 0);
|
|
25063
|
+
if (autoGate.action !== "proceed") {
|
|
25064
|
+
autoReleaseNote = `
|
|
24768
25065
|
|
|
24769
25066
|
---
|
|
24770
25067
|
|
|
24771
25068
|
\u26A0\uFE0F **Auto-release skipped** \u2014 a release quality gate is configured (\`${config2.gateCommand}\`), and it cannot be run from inside \`review_submit\`.
|
|
24772
25069
|
|
|
24773
25070
|
Run \`release\` manually: PAPI will hand you the gate command, then release once you report it green.`;
|
|
24774
|
-
|
|
24775
|
-
|
|
24776
|
-
|
|
24777
|
-
|
|
24778
|
-
|
|
24779
|
-
|
|
24780
|
-
|
|
24781
|
-
|
|
24782
|
-
|
|
24783
|
-
|
|
24784
|
-
|
|
24785
|
-
|
|
24786
|
-
|
|
24787
|
-
|
|
24788
|
-
|
|
24789
|
-
|
|
24790
|
-
|
|
24791
|
-
|
|
24792
|
-
|
|
24793
|
-
|
|
24794
|
-
|
|
24795
|
-
|
|
24796
|
-
|
|
25071
|
+
} else {
|
|
25072
|
+
const releaseTracker = new ProgressTracker("auto-release").bindStream(adapter2, { stage: "release" });
|
|
25073
|
+
await beginRelease(releaseTracker, result.currentCycle);
|
|
25074
|
+
const releaseResult = await createRelease(config2, baseBranch, version, adapter2, result.currentCycle);
|
|
25075
|
+
await recordReadinessVerified(releaseTracker);
|
|
25076
|
+
await recordQualityGate(releaseTracker, autoGate, caps);
|
|
25077
|
+
await tracker.recordStep("auto_release_triggered", { metadata: { version: releaseResult.version } });
|
|
25078
|
+
const autoChangelogDirective = buildChangelogDirective(
|
|
25079
|
+
caps,
|
|
25080
|
+
buildCycleUpdateCurationDirective(releaseResult.version, releaseResult.cycleClosed ?? 0)
|
|
25081
|
+
);
|
|
25082
|
+
const autoDeployDirective = buildDeployHookDirective(caps, config2.deployCommand);
|
|
25083
|
+
await completeRelease(releaseTracker, {
|
|
25084
|
+
cycleClosed: releaseResult.cycleClosed ?? null,
|
|
25085
|
+
version: releaseResult.version,
|
|
25086
|
+
caps,
|
|
25087
|
+
branchMerges: releaseResult.groupedBranchMerges ?? [],
|
|
25088
|
+
changelogEmitted: Boolean(autoChangelogDirective),
|
|
25089
|
+
deployHookEmitted: Boolean(autoDeployDirective)
|
|
25090
|
+
});
|
|
25091
|
+
const pushInfo = releaseResult.pushNotes.join(" ");
|
|
25092
|
+
const groupedMergeNote = releaseResult.groupedBranchMerges?.length ? "\n" + releaseResult.groupedBranchMerges.map((r) => `- Merged shared branch \`${r.branch}\` via PR: ${r.prUrl ?? "n/a"}`).join("\n") : "";
|
|
25093
|
+
autoReleaseNote = `
|
|
24797
25094
|
|
|
24798
25095
|
---
|
|
24799
25096
|
|
|
@@ -24804,13 +25101,14 @@ Run \`release\` manually: PAPI will hand you the gate command, then release once
|
|
|
24804
25101
|
- ${releaseResult.tagMessage}
|
|
24805
25102
|
- ${pushInfo}` + groupedMergeNote + (releaseResult.warnings?.length ? `
|
|
24806
25103
|
- Warnings: ${releaseResult.warnings.join(", ")}` : "") + // task-2598 (C328): the auto path previously swallowed the curated
|
|
24807
|
-
|
|
24808
|
-
|
|
24809
|
-
|
|
25104
|
+
// cycle-update directive that the manual path emits, so an auto-released
|
|
25105
|
+
// cycle never prompted the Discord post. Same directive, same gate.
|
|
25106
|
+
(autoChangelogDirective ? `
|
|
24810
25107
|
${autoChangelogDirective}` : "") + (autoDeployDirective ? `
|
|
24811
25108
|
${autoDeployDirective}` : "") + `
|
|
24812
25109
|
|
|
24813
25110
|
Run \`plan\` to create Cycle ${result.currentCycle + 1}.`;
|
|
25111
|
+
}
|
|
24814
25112
|
}
|
|
24815
25113
|
}
|
|
24816
25114
|
}
|
|
@@ -24870,9 +25168,9 @@ Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
|
|
|
24870
25168
|
`**${result.stageLabel}** recorded for ${result.taskId}.
|
|
24871
25169
|
|
|
24872
25170
|
- **Verdict:** ${result.verdict}
|
|
24873
|
-
- **Comments:** ${result.comments}
|
|
25171
|
+
- **Comments:** ${trimForEcho(result.comments)}
|
|
24874
25172
|
|
|
24875
|
-
${statusNote}${autoReviewNote}${securityNote}${unblockNote}${docClosureNote}${regenNote}${mergeNote}${overlapNote}${batchSummaryNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
|
|
25173
|
+
${statusNote}${capabilityReviewSkippedNote}${autoReviewNote}${securityNote}${unblockNote}${docClosureNote}${regenNote}${mergeNote}${overlapNote}${batchSummaryNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
|
|
24876
25174
|
);
|
|
24877
25175
|
} catch (err) {
|
|
24878
25176
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -26911,6 +27209,7 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
26911
27209
|
// Skill proposals — one-time codebase scan on first orient per project.
|
|
26912
27210
|
// PERF: hasToolMilestone (LIMIT 1 EXISTS) replaces readToolMetrics (5000-row pull).
|
|
26913
27211
|
tracked("skill-scan", async () => {
|
|
27212
|
+
if (!hasLocalWorkspace()) return "";
|
|
26914
27213
|
const alreadyScanned = adapter2.hasToolMilestone ? await adapter2.hasToolMilestone("milestone:skill_scan_completed") : (await adapter2.readToolMetrics()).some((m) => m.tool === "milestone:skill_scan_completed");
|
|
26915
27214
|
if (alreadyScanned) return "";
|
|
26916
27215
|
const proposals = scanForSkillSignals(config2.projectRoot);
|
|
@@ -27805,11 +28104,11 @@ var handoffGenerateTool = {
|
|
|
27805
28104
|
},
|
|
27806
28105
|
llm_response: {
|
|
27807
28106
|
type: "string",
|
|
27808
|
-
description: 'Your raw output from executing the handoff prompt (mode "apply" only). Must include both Part 1 (markdown) and Part 2 (structured JSON after <!-- PAPI_STRUCTURED_OUTPUT
|
|
28107
|
+
description: 'Your raw output from executing the handoff prompt (mode "apply" only). Must include both Part 1 (markdown) and Part 2 (structured JSON after <!-- PAPI_STRUCTURED_OUTPUT -->, inside a ```json fence). Each cycleHandoffs entry needs EITHER a buildHandoff string in the BUILD HANDOFF markdown template (section headers on their own lines: SCOPE (DO THIS) / SCOPE BOUNDARY (DO NOT DO THIS) / ACCEPTANCE CRITERIA / EFFORT) OR structured fields alongside taskId: {scope: string[], scopeBoundary: string[], acceptanceCriteria: string[], effort: "XS|S|M|L|XL"}. Mutually exclusive with llm_response_file.'
|
|
27809
28108
|
},
|
|
27810
28109
|
llm_response_file: {
|
|
27811
28110
|
type: "string",
|
|
27812
|
-
description: 'Absolute path to a file containing the handoff output (mode "apply" only). Use instead of llm_response when the output exceeds tool parameter size limits. Mutually exclusive with llm_response.'
|
|
28111
|
+
description: 'Absolute path to a file containing the handoff output (mode "apply" only). LOCAL stdio servers only \u2014 on the hosted connection the server cannot read files on your machine; pass llm_response inline instead. Use instead of llm_response when the output exceeds tool parameter size limits. Mutually exclusive with llm_response.'
|
|
27813
28112
|
},
|
|
27814
28113
|
cycle_number: {
|
|
27815
28114
|
type: "number",
|
|
@@ -27906,10 +28205,12 @@ ${result.userMessage}
|
|
|
27906
28205
|
}
|
|
27907
28206
|
}
|
|
27908
28207
|
|
|
28208
|
+
// src/tools/scope-brief.ts
|
|
28209
|
+
import { readFileSync as readFileSync11, statSync as statSync7 } from "fs";
|
|
28210
|
+
|
|
27909
28211
|
// src/services/scope-brief.ts
|
|
27910
28212
|
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
27911
28213
|
import { join as join17, dirname as dirname4 } from "path";
|
|
27912
|
-
import Anthropic from "@anthropic-ai/sdk";
|
|
27913
28214
|
var SCOPE_BRIEF_SYSTEM = `You are a technical scoping tool. You receive a brief-class task (too large to build directly) and decompose it into a structured scope document.
|
|
27914
28215
|
|
|
27915
28216
|
A scope document must:
|
|
@@ -27940,7 +28241,18 @@ Return the scope document in Markdown. Use this exact structure:
|
|
|
27940
28241
|
<Any unknowns that affect scope, or "None">
|
|
27941
28242
|
|
|
27942
28243
|
Return ONLY the markdown document. No preamble, no commentary.`;
|
|
27943
|
-
async function
|
|
28244
|
+
async function buildScopeBriefPrompt(adapter2, taskId) {
|
|
28245
|
+
const tasks = await adapter2.queryBoard({});
|
|
28246
|
+
const task = tasks.find((t) => t.id === taskId || t.displayId === taskId);
|
|
28247
|
+
if (!task) {
|
|
28248
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
28249
|
+
}
|
|
28250
|
+
if (task.scopeClass !== "brief") {
|
|
28251
|
+
throw new Error(`Task ${taskId} is not brief-class (scopeClass=${task.scopeClass ?? "task"}). Only brief-class tasks can be scoped.`);
|
|
28252
|
+
}
|
|
28253
|
+
return { task, systemPrompt: SCOPE_BRIEF_SYSTEM, userContext: buildTaskContext(task) };
|
|
28254
|
+
}
|
|
28255
|
+
async function applyScopeBrief(adapter2, input) {
|
|
27944
28256
|
const tasks = await adapter2.queryBoard({});
|
|
27945
28257
|
const task = tasks.find((t) => t.id === input.taskId || t.displayId === input.taskId);
|
|
27946
28258
|
if (!task) {
|
|
@@ -27949,17 +28261,9 @@ async function runScopeBrief(adapter2, input) {
|
|
|
27949
28261
|
if (task.scopeClass !== "brief") {
|
|
27950
28262
|
throw new Error(`Task ${input.taskId} is not brief-class (scopeClass=${task.scopeClass ?? "task"}). Only brief-class tasks can be scoped.`);
|
|
27951
28263
|
}
|
|
27952
|
-
const
|
|
27953
|
-
const client = new Anthropic({ apiKey: input.apiKey });
|
|
27954
|
-
const response = await client.messages.create({
|
|
27955
|
-
model: "claude-sonnet-4-6",
|
|
27956
|
-
max_tokens: 2048,
|
|
27957
|
-
system: SCOPE_BRIEF_SYSTEM,
|
|
27958
|
-
messages: [{ role: "user", content: taskContext }]
|
|
27959
|
-
});
|
|
27960
|
-
const docContent = response.content[0].type === "text" ? response.content[0].text.trim() : "";
|
|
28264
|
+
const docContent = input.llmMarkdown.trim();
|
|
27961
28265
|
if (!docContent) {
|
|
27962
|
-
throw new Error("
|
|
28266
|
+
throw new Error("Scope document is empty \u2014 pass the markdown you produced from the scope prompt as llm_response.");
|
|
27963
28267
|
}
|
|
27964
28268
|
const slug = input.taskId.replace(/[^a-z0-9-]/g, "-").toLowerCase();
|
|
27965
28269
|
const relPath = `docs/scopes/${slug}.md`;
|
|
@@ -28044,7 +28348,7 @@ function buildSummary(task, taskCount) {
|
|
|
28044
28348
|
// src/tools/scope-brief.ts
|
|
28045
28349
|
var scopeBriefTool = {
|
|
28046
28350
|
name: "scope_brief",
|
|
28047
|
-
description:
|
|
28351
|
+
description: 'Decompose a brief-class task (Large/XL, too large to build directly) into a structured scope document. Two phases, like plan/strategy \u2014 PAPI never calls a model itself (AD-58): first call (mode "prepare") returns a scoping prompt for YOU to run in your own AI workspace; then call again (mode "apply") with your markdown output in llm_response to write docs/scopes/<task-id>.md, register it, and mark the source task decomposed. Use before planning a cycle that includes brief-class tasks.',
|
|
28048
28352
|
annotations: { title: "Scope Brief", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
28049
28353
|
inputSchema: {
|
|
28050
28354
|
type: "object",
|
|
@@ -28052,42 +28356,102 @@ var scopeBriefTool = {
|
|
|
28052
28356
|
task_id: {
|
|
28053
28357
|
type: "string",
|
|
28054
28358
|
description: 'ID of the brief-class task to decompose (e.g. "task-042").'
|
|
28359
|
+
},
|
|
28360
|
+
mode: {
|
|
28361
|
+
type: "string",
|
|
28362
|
+
enum: ["prepare", "apply"],
|
|
28363
|
+
description: '"prepare" (default) returns the scoping prompt for you to run. "apply" persists the scope document you produced.'
|
|
28364
|
+
},
|
|
28365
|
+
llm_response: {
|
|
28366
|
+
type: "string",
|
|
28367
|
+
description: 'Your markdown scope document from running the prepare prompt (mode "apply" only).'
|
|
28368
|
+
},
|
|
28369
|
+
llm_response_file: {
|
|
28370
|
+
type: "string",
|
|
28371
|
+
description: 'Absolute path to a file holding your markdown scope document (mode "apply" only). LOCAL stdio servers only \u2014 the hosted server cannot read your machine. Mutually exclusive with llm_response; must be absolute, exist, and be \u2264500KB.'
|
|
28055
28372
|
}
|
|
28056
28373
|
},
|
|
28057
28374
|
required: ["task_id"]
|
|
28058
28375
|
}
|
|
28059
28376
|
};
|
|
28377
|
+
var MAX_RESPONSE_FILE_BYTES = 500 * 1024;
|
|
28060
28378
|
async function handleScopeBrief(adapter2, config2, args) {
|
|
28061
28379
|
const taskId = args.task_id;
|
|
28062
28380
|
if (!taskId) {
|
|
28063
28381
|
return errorResponse("task_id is required.");
|
|
28064
28382
|
}
|
|
28065
|
-
const
|
|
28066
|
-
if (!apiKey) {
|
|
28067
|
-
return errorResponse("ANTHROPIC_API_KEY is not set \u2014 scope_brief requires LLM access.");
|
|
28068
|
-
}
|
|
28383
|
+
const mode = args.mode ?? "prepare";
|
|
28069
28384
|
const health = await adapter2.getCycleHealth().catch(() => ({ totalCycles: 0 }));
|
|
28070
|
-
|
|
28071
|
-
|
|
28072
|
-
taskId
|
|
28073
|
-
|
|
28074
|
-
|
|
28075
|
-
|
|
28076
|
-
|
|
28077
|
-
|
|
28078
|
-
|
|
28079
|
-
|
|
28080
|
-
|
|
28385
|
+
if (mode === "prepare") {
|
|
28386
|
+
try {
|
|
28387
|
+
const { task, systemPrompt, userContext } = await buildScopeBriefPrompt(adapter2, taskId);
|
|
28388
|
+
return textResponse(
|
|
28389
|
+
`**Scope Brief \u2014 prepare** for ${task.id}: ${task.title}
|
|
28390
|
+
|
|
28391
|
+
Run the scoping pass below in YOUR AI workspace (PAPI does not call a model \u2014 AD-58), then call \`scope_brief\` again with \`mode: "apply"\`, the same \`task_id\`, and your markdown document in \`llm_response\`.
|
|
28392
|
+
|
|
28393
|
+
---
|
|
28394
|
+
|
|
28395
|
+
## Scoping instructions
|
|
28396
|
+
|
|
28397
|
+
${systemPrompt}
|
|
28398
|
+
|
|
28399
|
+
---
|
|
28400
|
+
|
|
28401
|
+
## Task to decompose
|
|
28402
|
+
|
|
28403
|
+
${userContext}`
|
|
28404
|
+
);
|
|
28405
|
+
} catch (err) {
|
|
28406
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
28407
|
+
return errorResponse(`scope_brief prepare failed: ${message}`);
|
|
28408
|
+
}
|
|
28409
|
+
}
|
|
28410
|
+
if (mode === "apply") {
|
|
28411
|
+
const llmMarkdown = readLlmResponse(args);
|
|
28412
|
+
if (!llmMarkdown) {
|
|
28413
|
+
return errorResponse('mode "apply" requires your scope document \u2014 pass it in llm_response (or llm_response_file on a local server).');
|
|
28414
|
+
}
|
|
28415
|
+
try {
|
|
28416
|
+
const result = await applyScopeBrief(adapter2, {
|
|
28417
|
+
taskId,
|
|
28418
|
+
llmMarkdown,
|
|
28419
|
+
projectRoot: config2.projectRoot,
|
|
28420
|
+
cycleNumber: health.totalCycles,
|
|
28421
|
+
adapterType: config2.adapterType
|
|
28422
|
+
});
|
|
28423
|
+
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
28424
|
+
return textResponse(
|
|
28425
|
+
`**Scope document created:** \`${result.docPath}\`
|
|
28081
28426
|
- **Sub-tasks defined:** ${result.taskCount}
|
|
28082
28427
|
- **Doc registry ID:** ${result.docId}
|
|
28083
28428
|
- **Source task:** ${taskId} marked as decomposed
|
|
28084
28429
|
|
|
28085
28430
|
**Next step:** Review \`${result.docPath}\` and submit each sub-task via \`idea\` with \`Reference: ${result.docPath}\` in notes. The planner will pick them up in the next cycle.` + filesToWriteSection
|
|
28086
|
-
|
|
28087
|
-
|
|
28088
|
-
|
|
28089
|
-
|
|
28431
|
+
);
|
|
28432
|
+
} catch (err) {
|
|
28433
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
28434
|
+
return errorResponse(`scope_brief apply failed: ${message}`);
|
|
28435
|
+
}
|
|
28436
|
+
}
|
|
28437
|
+
return errorResponse(`Unknown mode "${mode}" \u2014 use "prepare" or "apply".`);
|
|
28438
|
+
}
|
|
28439
|
+
function readLlmResponse(args) {
|
|
28440
|
+
const inline = args.llm_response;
|
|
28441
|
+
if (inline && inline.trim()) return inline;
|
|
28442
|
+
const filePath = args.llm_response_file;
|
|
28443
|
+
if (filePath && filePath.trim()) {
|
|
28444
|
+
try {
|
|
28445
|
+
if (statSync7(filePath).size > MAX_RESPONSE_FILE_BYTES) {
|
|
28446
|
+
throw new Error(`llm_response_file exceeds ${MAX_RESPONSE_FILE_BYTES} bytes`);
|
|
28447
|
+
}
|
|
28448
|
+
const body = readFileSync11(filePath, "utf-8");
|
|
28449
|
+
return body.trim() ? body : null;
|
|
28450
|
+
} catch (err) {
|
|
28451
|
+
throw new Error(`could not read llm_response_file: ${err instanceof Error ? err.message : String(err)}`);
|
|
28452
|
+
}
|
|
28090
28453
|
}
|
|
28454
|
+
return null;
|
|
28091
28455
|
}
|
|
28092
28456
|
|
|
28093
28457
|
// src/tools/ad-view.ts
|
|
@@ -29313,7 +29677,7 @@ function createServer(adapter2, config2) {
|
|
|
29313
29677
|
const __pkgDir = dirname5(__pkgFilename);
|
|
29314
29678
|
let serverVersion = "unknown";
|
|
29315
29679
|
try {
|
|
29316
|
-
const pkg = JSON.parse(
|
|
29680
|
+
const pkg = JSON.parse(readFileSync12(join19(__pkgDir, "..", "package.json"), "utf-8"));
|
|
29317
29681
|
serverVersion = pkg.version ?? "unknown";
|
|
29318
29682
|
} catch {
|
|
29319
29683
|
}
|
|
@@ -29738,6 +30102,13 @@ function startHttpTransport(opts) {
|
|
|
29738
30102
|
const ip = clientIp(req);
|
|
29739
30103
|
const origin = req.headers.origin;
|
|
29740
30104
|
const cors = corsHeaders(origin);
|
|
30105
|
+
const pathname = (() => {
|
|
30106
|
+
try {
|
|
30107
|
+
return new URL(req.url ?? "/", "http://internal").pathname;
|
|
30108
|
+
} catch {
|
|
30109
|
+
return req.url ?? "/";
|
|
30110
|
+
}
|
|
30111
|
+
})();
|
|
29741
30112
|
if (req.method === "OPTIONS") {
|
|
29742
30113
|
if (Object.keys(cors).length === 0) {
|
|
29743
30114
|
sendError(res, { status: 403, body: { error: "Origin not allowed" } });
|
|
@@ -29752,12 +30123,12 @@ function startHttpTransport(opts) {
|
|
|
29752
30123
|
sendError(res, { status: 400, body: { error: "HTTPS required" } });
|
|
29753
30124
|
return;
|
|
29754
30125
|
}
|
|
29755
|
-
if (req.method === "GET" &&
|
|
30126
|
+
if (req.method === "GET" && pathname === "/healthz") {
|
|
29756
30127
|
res.writeHead(200, { "Content-Type": "text/plain", ...cors });
|
|
29757
30128
|
res.end("ok");
|
|
29758
30129
|
return;
|
|
29759
30130
|
}
|
|
29760
|
-
if (req.method === "GET" &&
|
|
30131
|
+
if (req.method === "GET" && pathname === "/.well-known/oauth-protected-resource") {
|
|
29761
30132
|
res.writeHead(200, {
|
|
29762
30133
|
"Content-Type": "application/json",
|
|
29763
30134
|
"Cache-Control": "public, max-age=3600",
|
|
@@ -29773,7 +30144,7 @@ function startHttpTransport(opts) {
|
|
|
29773
30144
|
);
|
|
29774
30145
|
return;
|
|
29775
30146
|
}
|
|
29776
|
-
if (req.method === "GET" &&
|
|
30147
|
+
if (req.method === "GET" && pathname === "/.well-known/glama.json") {
|
|
29777
30148
|
res.writeHead(200, {
|
|
29778
30149
|
"Content-Type": "application/json",
|
|
29779
30150
|
"Cache-Control": "public, max-age=3600",
|
|
@@ -29787,7 +30158,7 @@ function startHttpTransport(opts) {
|
|
|
29787
30158
|
);
|
|
29788
30159
|
return;
|
|
29789
30160
|
}
|
|
29790
|
-
if (req.method === "GET" &&
|
|
30161
|
+
if (req.method === "GET" && pathname === "/.well-known/oauth-authorization-server") {
|
|
29791
30162
|
res.writeHead(302, {
|
|
29792
30163
|
Location: `${DASHBOARD_ORIGIN}/.well-known/oauth-authorization-server`,
|
|
29793
30164
|
"Cache-Control": "public, max-age=3600",
|
|
@@ -29796,7 +30167,7 @@ function startHttpTransport(opts) {
|
|
|
29796
30167
|
res.end();
|
|
29797
30168
|
return;
|
|
29798
30169
|
}
|
|
29799
|
-
if (
|
|
30170
|
+
if (pathname !== "/mcp" && pathname !== "/sse") {
|
|
29800
30171
|
sendError(res, { status: 404, body: { error: "Not found" } }, cors);
|
|
29801
30172
|
return;
|
|
29802
30173
|
}
|
|
@@ -30052,7 +30423,7 @@ async function dispatchRequest(args) {
|
|
|
30052
30423
|
var __dirname = dirname6(fileURLToPath4(import.meta.url));
|
|
30053
30424
|
var pkgVersion = "unknown";
|
|
30054
30425
|
try {
|
|
30055
|
-
const pkg = JSON.parse(
|
|
30426
|
+
const pkg = JSON.parse(readFileSync17(join24(__dirname, "..", "package.json"), "utf-8"));
|
|
30056
30427
|
pkgVersion = pkg.version;
|
|
30057
30428
|
} catch {
|
|
30058
30429
|
}
|
|
@@ -30076,6 +30447,7 @@ Options:
|
|
|
30076
30447
|
--yes, -y Skip confirmation prompts (reset only)
|
|
30077
30448
|
--idle-mcp Flag PAPI-idle projects in audit (needs DATABASE_URL; read-only)
|
|
30078
30449
|
--fix-pool Terminate this role's confirmed-wedged DB backends (doctor only; guarded)
|
|
30450
|
+
--reap-orphans Terminate parentless @papi-ai/server processes (doctor only; guarded)
|
|
30079
30451
|
|
|
30080
30452
|
Getting started:
|
|
30081
30453
|
1. Run "npx @papi-ai/server setup" in any project folder
|
|
@@ -30238,6 +30610,15 @@ if (isHttpMode && httpPort !== void 0) {
|
|
|
30238
30610
|
process.stderr.write("[papi] Fatal: stdio mode requires an MCP server instance.\n");
|
|
30239
30611
|
process.exit(1);
|
|
30240
30612
|
}
|
|
30613
|
+
try {
|
|
30614
|
+
const { reapOrphans: reapOrphans2 } = await Promise.resolve().then(() => (init_reap_orphans(), reap_orphans_exports));
|
|
30615
|
+
const swept = reapOrphans2({});
|
|
30616
|
+
if (swept.reaped.length > 0) {
|
|
30617
|
+
process.stderr.write(`[papi] Reaped ${swept.reaped.length} orphaned server process(es): ${swept.reaped.join(", ")}
|
|
30618
|
+
`);
|
|
30619
|
+
}
|
|
30620
|
+
} catch {
|
|
30621
|
+
}
|
|
30241
30622
|
const transport = new StdioServerTransport();
|
|
30242
30623
|
await server.connect(transport);
|
|
30243
30624
|
const projectName = basename2(config.projectRoot);
|