@papi-ai/server 0.7.63 → 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 +17 -10
- package/dist/index.js +219 -79
- package/dist/prompts.js +20 -5
- package/package.json +1 -1
|
@@ -1210,12 +1210,12 @@ var init_proxy_adapter = __esm({
|
|
|
1210
1210
|
"listContributorReleasePrs",
|
|
1211
1211
|
"claimReview",
|
|
1212
1212
|
"getSiblingAds",
|
|
1213
|
-
"getSiblingRepoTasks"
|
|
1214
|
-
// task-2828 (C339): attributed-intelligence analytics reader — pg-only
|
|
1215
|
-
//
|
|
1216
|
-
//
|
|
1217
|
-
//
|
|
1218
|
-
|
|
1213
|
+
"getSiblingRepoTasks"
|
|
1214
|
+
// task-2828 (C339): attributed-intelligence analytics reader — pg-only that cycle.
|
|
1215
|
+
// task-2864 (C343): WIRED. getModelOutcomeStats now has an edge case handler (raw
|
|
1216
|
+
// SQL via postgres.js mirroring the pg query + inlined computeModelOutcomes bucketing)
|
|
1217
|
+
// + an ALLOWED_METHODS entry, so it forwards. countPlanRunsForCycle wired alongside it
|
|
1218
|
+
// (task-2860's auto-release guard reader). Both REMOVED from NO_FORWARD.
|
|
1219
1219
|
// task-2394 (C329) — Batch A wired: findPendingDocActionsForTask,
|
|
1220
1220
|
// getModuleEstimationStats and getDecisionScorePatterns now have edge case handlers
|
|
1221
1221
|
// (each backed by a SECURITY DEFINER RPC, migration 20260714140000) plus
|
|
@@ -1348,7 +1348,8 @@ var init_proxy_adapter = __esm({
|
|
|
1348
1348
|
if (response.status === 401) {
|
|
1349
1349
|
throw new Error(
|
|
1350
1350
|
`Auth: Invalid API key \u2014 PAPI_DATA_API_KEY was rejected by the proxy.
|
|
1351
|
-
|
|
1351
|
+
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.
|
|
1352
|
+
Moving off a local install? Switch to the remote MCP: https://getpapi.ai/docs/install
|
|
1352
1353
|
(${response.status} on ${method}: ${message})`
|
|
1353
1354
|
);
|
|
1354
1355
|
}
|
|
@@ -2220,6 +2221,9 @@ var SECTION_HEADERS = [
|
|
|
2220
2221
|
"FILES LIKELY TOUCHED",
|
|
2221
2222
|
"EFFORT"
|
|
2222
2223
|
];
|
|
2224
|
+
function normaliseHeaderLine(line) {
|
|
2225
|
+
return line.trim().replace(/^#{1,6}\s*/, "").replace(/^-\s+/, "").replace(/^\*\*(.*?)\*\*$/, "$1").replace(/:\s*$/, "").trim();
|
|
2226
|
+
}
|
|
2223
2227
|
function splitSections(text) {
|
|
2224
2228
|
const sections = /* @__PURE__ */ new Map();
|
|
2225
2229
|
const lines = text.split("\n");
|
|
@@ -2232,8 +2236,8 @@ function splitSections(text) {
|
|
|
2232
2236
|
}
|
|
2233
2237
|
};
|
|
2234
2238
|
for (const line of lines) {
|
|
2235
|
-
const
|
|
2236
|
-
const matched = SECTION_HEADERS.find((h) =>
|
|
2239
|
+
const normalised = normaliseHeaderLine(line);
|
|
2240
|
+
const matched = SECTION_HEADERS.find((h) => normalised === h);
|
|
2237
2241
|
if (matched) {
|
|
2238
2242
|
flush();
|
|
2239
2243
|
currentSection = matched;
|
|
@@ -2254,7 +2258,10 @@ function parseChecklist(text) {
|
|
|
2254
2258
|
return text.split("\n").map((l) => l.replace(/^\s*\[[ x]]\s*/, "").trim()).filter((l) => l.length > 0);
|
|
2255
2259
|
}
|
|
2256
2260
|
function parseBuildHandoff(markdown) {
|
|
2257
|
-
if (!markdown.
|
|
2261
|
+
if (typeof markdown !== "string" || !markdown.trim()) return null;
|
|
2262
|
+
if (!markdown.includes("BUILD HANDOFF") && splitSections(markdown).size === 0) {
|
|
2263
|
+
return null;
|
|
2264
|
+
}
|
|
2258
2265
|
const taskIdMatch = markdown.match(/BUILD HANDOFF\s*—\s*(task-\d+)/);
|
|
2259
2266
|
const taskTitleMatch = markdown.match(/^Task:\s*(.+)$/m);
|
|
2260
2267
|
const cycleMatch = markdown.match(/^Cycle:\s*(\d+)$/m);
|
package/dist/index.js
CHANGED
|
@@ -1327,12 +1327,12 @@ var init_proxy_adapter = __esm({
|
|
|
1327
1327
|
"listContributorReleasePrs",
|
|
1328
1328
|
"claimReview",
|
|
1329
1329
|
"getSiblingAds",
|
|
1330
|
-
"getSiblingRepoTasks"
|
|
1331
|
-
// task-2828 (C339): attributed-intelligence analytics reader — pg-only
|
|
1332
|
-
//
|
|
1333
|
-
//
|
|
1334
|
-
//
|
|
1335
|
-
|
|
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.
|
|
1336
1336
|
// task-2394 (C329) — Batch A wired: findPendingDocActionsForTask,
|
|
1337
1337
|
// getModuleEstimationStats and getDecisionScorePatterns now have edge case handlers
|
|
1338
1338
|
// (each backed by a SECURITY DEFINER RPC, migration 20260714140000) plus
|
|
@@ -1465,7 +1465,8 @@ var init_proxy_adapter = __esm({
|
|
|
1465
1465
|
if (response.status === 401) {
|
|
1466
1466
|
throw new Error(
|
|
1467
1467
|
`Auth: Invalid API key \u2014 PAPI_DATA_API_KEY was rejected by the proxy.
|
|
1468
|
-
|
|
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
|
|
1469
1470
|
(${response.status} on ${method}: ${message})`
|
|
1470
1471
|
);
|
|
1471
1472
|
}
|
|
@@ -4333,7 +4334,7 @@ __export(doctor_exports, {
|
|
|
4333
4334
|
__testing: () => __testing,
|
|
4334
4335
|
runDoctor: () => runDoctor
|
|
4335
4336
|
});
|
|
4336
|
-
import { existsSync as existsSync11, readFileSync as
|
|
4337
|
+
import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
|
|
4337
4338
|
import { homedir as homedir4 } from "os";
|
|
4338
4339
|
import { join as join20 } from "path";
|
|
4339
4340
|
function redact(name, value) {
|
|
@@ -4353,7 +4354,7 @@ function findMcpJson() {
|
|
|
4353
4354
|
for (const path7 of candidates) {
|
|
4354
4355
|
if (!existsSync11(path7)) continue;
|
|
4355
4356
|
try {
|
|
4356
|
-
const raw =
|
|
4357
|
+
const raw = readFileSync13(path7, "utf-8");
|
|
4357
4358
|
const parsed = JSON.parse(raw);
|
|
4358
4359
|
const papiEntry = parsed.papi ?? parsed.mcpServers?.papi;
|
|
4359
4360
|
if (!papiEntry) continue;
|
|
@@ -4631,7 +4632,7 @@ __export(reset_exports, {
|
|
|
4631
4632
|
removePapiEntry: () => removePapiEntry,
|
|
4632
4633
|
runReset: () => runReset
|
|
4633
4634
|
});
|
|
4634
|
-
import { existsSync as existsSync12, readFileSync as
|
|
4635
|
+
import { existsSync as existsSync12, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
|
|
4635
4636
|
import { homedir as homedir5 } from "os";
|
|
4636
4637
|
import { join as join21 } from "path";
|
|
4637
4638
|
import { createInterface } from "readline/promises";
|
|
@@ -4641,7 +4642,7 @@ function findResetTarget() {
|
|
|
4641
4642
|
let raw;
|
|
4642
4643
|
let parsed;
|
|
4643
4644
|
try {
|
|
4644
|
-
raw =
|
|
4645
|
+
raw = readFileSync14(path7, "utf-8");
|
|
4645
4646
|
parsed = JSON.parse(raw);
|
|
4646
4647
|
} catch {
|
|
4647
4648
|
continue;
|
|
@@ -4742,7 +4743,7 @@ __export(audit_exports, {
|
|
|
4742
4743
|
__testing: () => __testing2,
|
|
4743
4744
|
runAudit: () => runAudit
|
|
4744
4745
|
});
|
|
4745
|
-
import { existsSync as existsSync13, readFileSync as
|
|
4746
|
+
import { existsSync as existsSync13, readFileSync as readFileSync15, readdirSync as readdirSync7 } from "fs";
|
|
4746
4747
|
import { homedir as homedir6 } from "os";
|
|
4747
4748
|
import { join as join22 } from "path";
|
|
4748
4749
|
function safeListDirs(dir) {
|
|
@@ -4763,7 +4764,7 @@ function readMcp(projectPath) {
|
|
|
4763
4764
|
const path7 = join22(projectPath, ".mcp.json");
|
|
4764
4765
|
if (!existsSync13(path7)) return { servers: [] };
|
|
4765
4766
|
try {
|
|
4766
|
-
const parsed = JSON.parse(
|
|
4767
|
+
const parsed = JSON.parse(readFileSync15(path7, "utf-8"));
|
|
4767
4768
|
const mcpServers = parsed.mcpServers ?? {};
|
|
4768
4769
|
const servers = Object.keys(mcpServers);
|
|
4769
4770
|
if (parsed.papi && !servers.includes("papi")) servers.push("papi");
|
|
@@ -4819,7 +4820,7 @@ function readGlobalSkills() {
|
|
|
4819
4820
|
function readGlobalMcpServers() {
|
|
4820
4821
|
if (!existsSync13(GLOBAL_CLAUDE_JSON)) return [];
|
|
4821
4822
|
try {
|
|
4822
|
-
const parsed = JSON.parse(
|
|
4823
|
+
const parsed = JSON.parse(readFileSync15(GLOBAL_CLAUDE_JSON, "utf-8"));
|
|
4823
4824
|
const servers = parsed.mcpServers ?? {};
|
|
4824
4825
|
return Object.keys(servers).sort((a, b2) => a.localeCompare(b2));
|
|
4825
4826
|
} catch {
|
|
@@ -4985,7 +4986,7 @@ var setup_exports = {};
|
|
|
4985
4986
|
__export(setup_exports, {
|
|
4986
4987
|
runSetup: () => runSetup
|
|
4987
4988
|
});
|
|
4988
|
-
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";
|
|
4989
4990
|
import { join as join23 } from "path";
|
|
4990
4991
|
function baseUrl() {
|
|
4991
4992
|
const fromEnv = process.env["PAPI_HOST"] ?? process.env["PAPI_BASE_URL"];
|
|
@@ -5022,7 +5023,7 @@ function writeMcpJson(opts) {
|
|
|
5022
5023
|
let parsed = {};
|
|
5023
5024
|
if (existsSync14(path7)) {
|
|
5024
5025
|
try {
|
|
5025
|
-
parsed = JSON.parse(
|
|
5026
|
+
parsed = JSON.parse(readFileSync16(path7, "utf-8"));
|
|
5026
5027
|
} catch {
|
|
5027
5028
|
throw new Error(`.mcp.json at ${path7} is not valid JSON. Fix it or remove it before re-running setup.`);
|
|
5028
5029
|
}
|
|
@@ -5045,7 +5046,7 @@ function writeMcpJson(opts) {
|
|
|
5045
5046
|
parsed.mcpServers = mcpServers;
|
|
5046
5047
|
writeFileSync7(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5047
5048
|
try {
|
|
5048
|
-
const mode =
|
|
5049
|
+
const mode = statSync8(path7).mode & 511;
|
|
5049
5050
|
if (mode !== 384) chmodSync2(path7, 384);
|
|
5050
5051
|
} catch {
|
|
5051
5052
|
}
|
|
@@ -5153,7 +5154,7 @@ var init_setup = __esm({
|
|
|
5153
5154
|
});
|
|
5154
5155
|
|
|
5155
5156
|
// src/index.ts
|
|
5156
|
-
import { readFileSync as
|
|
5157
|
+
import { readFileSync as readFileSync17 } from "fs";
|
|
5157
5158
|
import { dirname as dirname6, join as join24, basename as basename2 } from "path";
|
|
5158
5159
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
5159
5160
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -5199,7 +5200,6 @@ function loadConfig() {
|
|
|
5199
5200
|
"\nPAPI is running, but no project is configured here.\n" + HELP_FOOTER + "\n"
|
|
5200
5201
|
);
|
|
5201
5202
|
}
|
|
5202
|
-
const anthropicApiKey = process.env.PAPI_API_KEY ?? "";
|
|
5203
5203
|
const autoCommit2 = process.env.PAPI_AUTO_COMMIT !== "false";
|
|
5204
5204
|
const baseBranch = process.env.PAPI_BASE_BRANCH ?? "main";
|
|
5205
5205
|
const autoPR = process.env.PAPI_AUTO_PR !== "false";
|
|
@@ -5273,7 +5273,6 @@ Already have an account? Make sure PAPI_USER_ID is set in your .mcp.json env con
|
|
|
5273
5273
|
return {
|
|
5274
5274
|
projectRoot,
|
|
5275
5275
|
papiDir: path.join(projectRoot, ".papi"),
|
|
5276
|
-
anthropicApiKey,
|
|
5277
5276
|
autoCommit: autoCommit2,
|
|
5278
5277
|
baseBranch,
|
|
5279
5278
|
autoPR,
|
|
@@ -5564,6 +5563,9 @@ var SECTION_HEADERS = [
|
|
|
5564
5563
|
"FILES LIKELY TOUCHED",
|
|
5565
5564
|
"EFFORT"
|
|
5566
5565
|
];
|
|
5566
|
+
function normaliseHeaderLine(line) {
|
|
5567
|
+
return line.trim().replace(/^#{1,6}\s*/, "").replace(/^-\s+/, "").replace(/^\*\*(.*?)\*\*$/, "$1").replace(/:\s*$/, "").trim();
|
|
5568
|
+
}
|
|
5567
5569
|
function splitSections(text) {
|
|
5568
5570
|
const sections = /* @__PURE__ */ new Map();
|
|
5569
5571
|
const lines = text.split("\n");
|
|
@@ -5576,8 +5578,8 @@ function splitSections(text) {
|
|
|
5576
5578
|
}
|
|
5577
5579
|
};
|
|
5578
5580
|
for (const line of lines) {
|
|
5579
|
-
const
|
|
5580
|
-
const matched = SECTION_HEADERS.find((h) =>
|
|
5581
|
+
const normalised = normaliseHeaderLine(line);
|
|
5582
|
+
const matched = SECTION_HEADERS.find((h) => normalised === h);
|
|
5581
5583
|
if (matched) {
|
|
5582
5584
|
flush();
|
|
5583
5585
|
currentSection = matched;
|
|
@@ -5598,7 +5600,10 @@ function parseChecklist(text) {
|
|
|
5598
5600
|
return text.split("\n").map((l) => l.replace(/^\s*\[[ x]]\s*/, "").trim()).filter((l) => l.length > 0);
|
|
5599
5601
|
}
|
|
5600
5602
|
function parseBuildHandoff(markdown) {
|
|
5601
|
-
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
|
+
}
|
|
5602
5607
|
const taskIdMatch = markdown.match(/BUILD HANDOFF\s*—\s*(task-\d+)/);
|
|
5603
5608
|
const taskTitleMatch = markdown.match(/^Task:\s*(.+)$/m);
|
|
5604
5609
|
const cycleMatch = markdown.match(/^Cycle:\s*(\d+)$/m);
|
|
@@ -5633,6 +5638,28 @@ function parseBuildHandoff(markdown) {
|
|
|
5633
5638
|
effort
|
|
5634
5639
|
};
|
|
5635
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
|
+
}
|
|
5636
5663
|
function ensureArray(value) {
|
|
5637
5664
|
if (Array.isArray(value)) return value;
|
|
5638
5665
|
if (typeof value === "string") {
|
|
@@ -8166,7 +8193,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
8166
8193
|
}
|
|
8167
8194
|
|
|
8168
8195
|
// src/server.ts
|
|
8169
|
-
import { readFileSync as
|
|
8196
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
8170
8197
|
import { access as access4, readdir as readdir4, readFile as readFile9 } from "fs/promises";
|
|
8171
8198
|
import { join as join19, dirname as dirname5 } from "path";
|
|
8172
8199
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
@@ -9007,6 +9034,7 @@ Everything in Part 1 (natural language) is **display-only**. Part 2 (structured
|
|
|
9007
9034
|
- Updated or created Active Decisions in Part 1? \u2192 Put them in \`activeDecisions\` array (with id and full body including ### heading)
|
|
9008
9035
|
- Found board corrections (wrong priority, missing fields, stale status) in Part 1? \u2192 Put them in \`boardCorrections\` array
|
|
9009
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.
|
|
9010
9038
|
|
|
9011
9039
|
**Example with populated fields (DO NOT copy literally \u2014 adapt to your actual analysis):**
|
|
9012
9040
|
\`\`\`json
|
|
@@ -9047,13 +9075,20 @@ This is Cycle 0 \u2014 the first planning cycle for a brand-new project.
|
|
|
9047
9075
|
- Do NOT assume web-app patterns (routes, pages, components) unless the brief explicitly describes a web application
|
|
9048
9076
|
- All tasks: status Backlog, priority P1-P2, reviewed true, phase "Phase 1"
|
|
9049
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
|
+
|
|
9050
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.
|
|
9051
9086
|
|
|
9052
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.
|
|
9053
9088
|
|
|
9054
9089
|
### Structured output for Bootstrap:
|
|
9055
9090
|
In the JSON block, you MUST include:
|
|
9056
|
-
- "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).
|
|
9057
9092
|
- "productBrief": the full Product Brief markdown content. **If null, the brief stays as the template.**
|
|
9058
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.**
|
|
9059
9094
|
- "recommendedTaskId": null (the handler will use the first new task)
|
|
@@ -9597,10 +9632,17 @@ function coerceToString(value) {
|
|
|
9597
9632
|
return JSON.stringify(value, null, 2);
|
|
9598
9633
|
}
|
|
9599
9634
|
function coerceStructuredOutput(parsed) {
|
|
9600
|
-
const cycleHandoffs = Array.isArray(parsed.cycleHandoffs) ? parsed.cycleHandoffs.map((h) =>
|
|
9601
|
-
taskId:
|
|
9602
|
-
buildHandoff
|
|
9603
|
-
|
|
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
|
+
}) : [];
|
|
9604
9646
|
const newTasks = Array.isArray(parsed.newTasks) ? parsed.newTasks.map((t) => ({
|
|
9605
9647
|
// task-2242: stable join key (optional — undefined falls back to index).
|
|
9606
9648
|
tempId: t.tempId !== void 0 && t.tempId !== null ? coerceToString(t.tempId) : void 0,
|
|
@@ -10710,9 +10752,12 @@ async function applyHandoffs(adapter2, rawLlmOutput, cycleNumber, force = false)
|
|
|
10710
10752
|
skipped++;
|
|
10711
10753
|
continue;
|
|
10712
10754
|
}
|
|
10713
|
-
const parsed = parseBuildHandoff(handoff.buildHandoff);
|
|
10755
|
+
const parsed = parseBuildHandoff(handoff.buildHandoff) ?? (handoff.structuredFields ? coerceBuildHandoff(handoff.structuredFields, handoff.taskId) : null);
|
|
10714
10756
|
if (!parsed) {
|
|
10715
|
-
|
|
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
|
+
);
|
|
10716
10761
|
continue;
|
|
10717
10762
|
}
|
|
10718
10763
|
const invalidFields = validateHandoffScope(parsed);
|
|
@@ -10720,7 +10765,6 @@ async function applyHandoffs(adapter2, rawLlmOutput, cycleNumber, force = false)
|
|
|
10720
10765
|
warnings.push(
|
|
10721
10766
|
`Rejected handoff for ${handoff.taskId}: missing or empty ${invalidFields.join(", ")}. Handoffs without explicit scope produce ambiguous builds.`
|
|
10722
10767
|
);
|
|
10723
|
-
skipped++;
|
|
10724
10768
|
continue;
|
|
10725
10769
|
}
|
|
10726
10770
|
if (!parsed.createdAt) {
|
|
@@ -11195,6 +11239,20 @@ async function resolveOwnerGate(adapter2, config2) {
|
|
|
11195
11239
|
}
|
|
11196
11240
|
|
|
11197
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
|
+
}
|
|
11198
11256
|
var PLAN_BUILD_REPORT_BUDGET = { maxReports: 12, fieldBudget: 280 };
|
|
11199
11257
|
function leadChainWithRecommended(chain, recommendedTaskId) {
|
|
11200
11258
|
const rec = recommendedTaskId?.trim();
|
|
@@ -12133,10 +12191,13 @@ ${cleanContent}`;
|
|
|
12133
12191
|
} catch {
|
|
12134
12192
|
}
|
|
12135
12193
|
const handoffs = (data.cycleHandoffs ?? []).map((h) => {
|
|
12136
|
-
const parsed = parseBuildHandoff(h.buildHandoff);
|
|
12194
|
+
const parsed = parseBuildHandoff(h.buildHandoff) ?? (h.structuredFields ? coerceBuildHandoff(h.structuredFields, h.taskId) : null);
|
|
12137
12195
|
if (parsed && !parsed.createdAt) {
|
|
12138
12196
|
parsed.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12139
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
|
+
}
|
|
12140
12201
|
return { taskId: h.taskId, handoff: parsed };
|
|
12141
12202
|
}).filter((h) => h.handoff != null);
|
|
12142
12203
|
const cycleTaskIds = data.cycleTaskIds?.length ? data.cycleTaskIds : (data.cycleHandoffs ?? []).map((h) => h.taskId);
|
|
@@ -12223,7 +12284,7 @@ ${cleanContent}`;
|
|
|
12223
12284
|
title: t.title,
|
|
12224
12285
|
status: t.status || "Backlog",
|
|
12225
12286
|
priority: t.priority || "P1 High",
|
|
12226
|
-
complexity: t.complexity
|
|
12287
|
+
complexity: normalizeComplexity(t.complexity),
|
|
12227
12288
|
module: t.module || "Core",
|
|
12228
12289
|
epic: t.epic || "Platform",
|
|
12229
12290
|
phase: t.phase || "Phase 1",
|
|
@@ -12371,7 +12432,7 @@ ${cleanContent}`;
|
|
|
12371
12432
|
title: task.title,
|
|
12372
12433
|
status: task.status || "Backlog",
|
|
12373
12434
|
priority: task.priority || "P1 High",
|
|
12374
|
-
complexity: task.complexity
|
|
12435
|
+
complexity: normalizeComplexity(task.complexity),
|
|
12375
12436
|
module: task.module || "Core",
|
|
12376
12437
|
epic: task.epic || "Platform",
|
|
12377
12438
|
phase: task.phase || "Phase 1",
|
|
@@ -13383,6 +13444,13 @@ async function resolveLlmResponse(inlineResponse, filePath) {
|
|
|
13383
13444
|
}
|
|
13384
13445
|
const resolvedPath = filePath.trim();
|
|
13385
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
|
+
}
|
|
13386
13454
|
return {
|
|
13387
13455
|
ok: false,
|
|
13388
13456
|
error: `llm_response_file must be an absolute path, got: ${resolvedPath}`
|
|
@@ -13629,7 +13697,7 @@ var planTool = {
|
|
|
13629
13697
|
},
|
|
13630
13698
|
llm_response_file: {
|
|
13631
13699
|
type: "string",
|
|
13632
|
-
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.'
|
|
13633
13701
|
},
|
|
13634
13702
|
cycle_number: {
|
|
13635
13703
|
type: "number",
|
|
@@ -13704,7 +13772,7 @@ function formatPlanResult(result) {
|
|
|
13704
13772
|
return textResponse(
|
|
13705
13773
|
`${pullLine}**${modeLabel} Mode \u2014 ${cycleLabel}**
|
|
13706
13774
|
|
|
13707
|
-
\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).`
|
|
13708
13776
|
);
|
|
13709
13777
|
}
|
|
13710
13778
|
const lines = [];
|
|
@@ -16059,7 +16127,7 @@ var strategyReviewTool = {
|
|
|
16059
16127
|
},
|
|
16060
16128
|
llm_response_file: {
|
|
16061
16129
|
type: "string",
|
|
16062
|
-
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.'
|
|
16063
16131
|
},
|
|
16064
16132
|
cycle_number: {
|
|
16065
16133
|
type: "number",
|
|
@@ -18034,7 +18102,9 @@ async function applySetupOutputs(adapter2, config2, input, collector, briefText,
|
|
|
18034
18102
|
}
|
|
18035
18103
|
if (adapter2.upsertActiveDecision) {
|
|
18036
18104
|
const title = ad.title || ad.body.split("\n")[0].replace(/^#+\s*/, "").slice(0, 120);
|
|
18037
|
-
|
|
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);
|
|
18038
18108
|
} else {
|
|
18039
18109
|
await adapter2.updateActiveDecision(ad.id, ad.body, 0);
|
|
18040
18110
|
}
|
|
@@ -18814,10 +18884,14 @@ ${[created, skipped].filter(Boolean).join(", ")}.${idea}`;
|
|
|
18814
18884
|
\u26A0\uFE0F **Setup warnings (non-blocking):**
|
|
18815
18885
|
${result.warnings.map((w) => `- ${w}`).join("\n")}` : "";
|
|
18816
18886
|
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
18817
|
-
|
|
18818
|
-
|
|
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).` : `
|
|
18819
18891
|
|
|
18820
|
-
**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}
|
|
18821
18895
|
|
|
18822
18896
|
Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-written brief.
|
|
18823
18897
|
|
|
@@ -23234,7 +23308,7 @@ var bugTool = {
|
|
|
23234
23308
|
severity: {
|
|
23235
23309
|
type: "string",
|
|
23236
23310
|
enum: ["critical", "major", "minor"],
|
|
23237
|
-
description: 'Bug severity (default: "major"). Critical = P1,
|
|
23311
|
+
description: 'Bug severity (default: "major"). Critical = P0, Major = P1, Minor = P2.'
|
|
23238
23312
|
},
|
|
23239
23313
|
notes: {
|
|
23240
23314
|
type: "string",
|
|
@@ -27135,6 +27209,7 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
27135
27209
|
// Skill proposals — one-time codebase scan on first orient per project.
|
|
27136
27210
|
// PERF: hasToolMilestone (LIMIT 1 EXISTS) replaces readToolMetrics (5000-row pull).
|
|
27137
27211
|
tracked("skill-scan", async () => {
|
|
27212
|
+
if (!hasLocalWorkspace()) return "";
|
|
27138
27213
|
const alreadyScanned = adapter2.hasToolMilestone ? await adapter2.hasToolMilestone("milestone:skill_scan_completed") : (await adapter2.readToolMetrics()).some((m) => m.tool === "milestone:skill_scan_completed");
|
|
27139
27214
|
if (alreadyScanned) return "";
|
|
27140
27215
|
const proposals = scanForSkillSignals(config2.projectRoot);
|
|
@@ -28029,11 +28104,11 @@ var handoffGenerateTool = {
|
|
|
28029
28104
|
},
|
|
28030
28105
|
llm_response: {
|
|
28031
28106
|
type: "string",
|
|
28032
|
-
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.'
|
|
28033
28108
|
},
|
|
28034
28109
|
llm_response_file: {
|
|
28035
28110
|
type: "string",
|
|
28036
|
-
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.'
|
|
28037
28112
|
},
|
|
28038
28113
|
cycle_number: {
|
|
28039
28114
|
type: "number",
|
|
@@ -28130,10 +28205,12 @@ ${result.userMessage}
|
|
|
28130
28205
|
}
|
|
28131
28206
|
}
|
|
28132
28207
|
|
|
28208
|
+
// src/tools/scope-brief.ts
|
|
28209
|
+
import { readFileSync as readFileSync11, statSync as statSync7 } from "fs";
|
|
28210
|
+
|
|
28133
28211
|
// src/services/scope-brief.ts
|
|
28134
28212
|
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
28135
28213
|
import { join as join17, dirname as dirname4 } from "path";
|
|
28136
|
-
import Anthropic from "@anthropic-ai/sdk";
|
|
28137
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.
|
|
28138
28215
|
|
|
28139
28216
|
A scope document must:
|
|
@@ -28164,7 +28241,18 @@ Return the scope document in Markdown. Use this exact structure:
|
|
|
28164
28241
|
<Any unknowns that affect scope, or "None">
|
|
28165
28242
|
|
|
28166
28243
|
Return ONLY the markdown document. No preamble, no commentary.`;
|
|
28167
|
-
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) {
|
|
28168
28256
|
const tasks = await adapter2.queryBoard({});
|
|
28169
28257
|
const task = tasks.find((t) => t.id === input.taskId || t.displayId === input.taskId);
|
|
28170
28258
|
if (!task) {
|
|
@@ -28173,17 +28261,9 @@ async function runScopeBrief(adapter2, input) {
|
|
|
28173
28261
|
if (task.scopeClass !== "brief") {
|
|
28174
28262
|
throw new Error(`Task ${input.taskId} is not brief-class (scopeClass=${task.scopeClass ?? "task"}). Only brief-class tasks can be scoped.`);
|
|
28175
28263
|
}
|
|
28176
|
-
const
|
|
28177
|
-
const client = new Anthropic({ apiKey: input.apiKey });
|
|
28178
|
-
const response = await client.messages.create({
|
|
28179
|
-
model: "claude-sonnet-4-6",
|
|
28180
|
-
max_tokens: 2048,
|
|
28181
|
-
system: SCOPE_BRIEF_SYSTEM,
|
|
28182
|
-
messages: [{ role: "user", content: taskContext }]
|
|
28183
|
-
});
|
|
28184
|
-
const docContent = response.content[0].type === "text" ? response.content[0].text.trim() : "";
|
|
28264
|
+
const docContent = input.llmMarkdown.trim();
|
|
28185
28265
|
if (!docContent) {
|
|
28186
|
-
throw new Error("
|
|
28266
|
+
throw new Error("Scope document is empty \u2014 pass the markdown you produced from the scope prompt as llm_response.");
|
|
28187
28267
|
}
|
|
28188
28268
|
const slug = input.taskId.replace(/[^a-z0-9-]/g, "-").toLowerCase();
|
|
28189
28269
|
const relPath = `docs/scopes/${slug}.md`;
|
|
@@ -28268,7 +28348,7 @@ function buildSummary(task, taskCount) {
|
|
|
28268
28348
|
// src/tools/scope-brief.ts
|
|
28269
28349
|
var scopeBriefTool = {
|
|
28270
28350
|
name: "scope_brief",
|
|
28271
|
-
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.',
|
|
28272
28352
|
annotations: { title: "Scope Brief", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
28273
28353
|
inputSchema: {
|
|
28274
28354
|
type: "object",
|
|
@@ -28276,42 +28356,102 @@ var scopeBriefTool = {
|
|
|
28276
28356
|
task_id: {
|
|
28277
28357
|
type: "string",
|
|
28278
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.'
|
|
28279
28372
|
}
|
|
28280
28373
|
},
|
|
28281
28374
|
required: ["task_id"]
|
|
28282
28375
|
}
|
|
28283
28376
|
};
|
|
28377
|
+
var MAX_RESPONSE_FILE_BYTES = 500 * 1024;
|
|
28284
28378
|
async function handleScopeBrief(adapter2, config2, args) {
|
|
28285
28379
|
const taskId = args.task_id;
|
|
28286
28380
|
if (!taskId) {
|
|
28287
28381
|
return errorResponse("task_id is required.");
|
|
28288
28382
|
}
|
|
28289
|
-
const
|
|
28290
|
-
if (!apiKey) {
|
|
28291
|
-
return errorResponse("ANTHROPIC_API_KEY is not set \u2014 scope_brief requires LLM access.");
|
|
28292
|
-
}
|
|
28383
|
+
const mode = args.mode ?? "prepare";
|
|
28293
28384
|
const health = await adapter2.getCycleHealth().catch(() => ({ totalCycles: 0 }));
|
|
28294
|
-
|
|
28295
|
-
|
|
28296
|
-
taskId
|
|
28297
|
-
|
|
28298
|
-
|
|
28299
|
-
|
|
28300
|
-
|
|
28301
|
-
|
|
28302
|
-
|
|
28303
|
-
|
|
28304
|
-
|
|
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}\`
|
|
28305
28426
|
- **Sub-tasks defined:** ${result.taskCount}
|
|
28306
28427
|
- **Doc registry ID:** ${result.docId}
|
|
28307
28428
|
- **Source task:** ${taskId} marked as decomposed
|
|
28308
28429
|
|
|
28309
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
|
|
28310
|
-
|
|
28311
|
-
|
|
28312
|
-
|
|
28313
|
-
|
|
28431
|
+
);
|
|
28432
|
+
} catch (err) {
|
|
28433
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
28434
|
+
return errorResponse(`scope_brief apply failed: ${message}`);
|
|
28435
|
+
}
|
|
28314
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
|
+
}
|
|
28453
|
+
}
|
|
28454
|
+
return null;
|
|
28315
28455
|
}
|
|
28316
28456
|
|
|
28317
28457
|
// src/tools/ad-view.ts
|
|
@@ -29537,7 +29677,7 @@ function createServer(adapter2, config2) {
|
|
|
29537
29677
|
const __pkgDir = dirname5(__pkgFilename);
|
|
29538
29678
|
let serverVersion = "unknown";
|
|
29539
29679
|
try {
|
|
29540
|
-
const pkg = JSON.parse(
|
|
29680
|
+
const pkg = JSON.parse(readFileSync12(join19(__pkgDir, "..", "package.json"), "utf-8"));
|
|
29541
29681
|
serverVersion = pkg.version ?? "unknown";
|
|
29542
29682
|
} catch {
|
|
29543
29683
|
}
|
|
@@ -30283,7 +30423,7 @@ async function dispatchRequest(args) {
|
|
|
30283
30423
|
var __dirname = dirname6(fileURLToPath4(import.meta.url));
|
|
30284
30424
|
var pkgVersion = "unknown";
|
|
30285
30425
|
try {
|
|
30286
|
-
const pkg = JSON.parse(
|
|
30426
|
+
const pkg = JSON.parse(readFileSync17(join24(__dirname, "..", "package.json"), "utf-8"));
|
|
30287
30427
|
pkgVersion = pkg.version;
|
|
30288
30428
|
} catch {
|
|
30289
30429
|
}
|
package/dist/prompts.js
CHANGED
|
@@ -130,6 +130,7 @@ Everything in Part 1 (natural language) is **display-only**. Part 2 (structured
|
|
|
130
130
|
- Updated or created Active Decisions in Part 1? \u2192 Put them in \`activeDecisions\` array (with id and full body including ### heading)
|
|
131
131
|
- Found board corrections (wrong priority, missing fields, stale status) in Part 1? \u2192 Put them in \`boardCorrections\` array
|
|
132
132
|
- Generated BUILD HANDOFFs in Part 1? \u2192 Put them in \`cycleHandoffs\` array
|
|
133
|
+
- **\`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.
|
|
133
134
|
|
|
134
135
|
**Example with populated fields (DO NOT copy literally \u2014 adapt to your actual analysis):**
|
|
135
136
|
\`\`\`json
|
|
@@ -170,13 +171,20 @@ This is Cycle 0 \u2014 the first planning cycle for a brand-new project.
|
|
|
170
171
|
- Do NOT assume web-app patterns (routes, pages, components) unless the brief explicitly describes a web application
|
|
171
172
|
- All tasks: status Backlog, priority P1-P2, reviewed true, phase "Phase 1"
|
|
172
173
|
|
|
174
|
+
**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:
|
|
175
|
+
\`Why: <one plain-language sentence, \u2264160 chars, on why THIS task earns its place in the first cycle>\`
|
|
176
|
+
Then a blank line, then any other notes. Rules:
|
|
177
|
+
- 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.
|
|
178
|
+
- Plain language for a semi-technical builder \u2014 no jargon, no raw IDs, not a restatement of the title.
|
|
179
|
+
- This is a hard requirement: the dashboard's first-cycle experience reads and renders this \`Why:\` line per task.
|
|
180
|
+
|
|
173
181
|
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.
|
|
174
182
|
|
|
175
183
|
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.
|
|
176
184
|
|
|
177
185
|
### Structured output for Bootstrap:
|
|
178
186
|
In the JSON block, you MUST include:
|
|
179
|
-
- "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.**
|
|
187
|
+
- "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).
|
|
180
188
|
- "productBrief": the full Product Brief markdown content. **If null, the brief stays as the template.**
|
|
181
189
|
- "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.**
|
|
182
190
|
- "recommendedTaskId": null (the handler will use the first new task)
|
|
@@ -720,10 +728,17 @@ function coerceToString(value) {
|
|
|
720
728
|
return JSON.stringify(value, null, 2);
|
|
721
729
|
}
|
|
722
730
|
function coerceStructuredOutput(parsed) {
|
|
723
|
-
const cycleHandoffs = Array.isArray(parsed.cycleHandoffs) ? parsed.cycleHandoffs.map((h) =>
|
|
724
|
-
taskId:
|
|
725
|
-
buildHandoff
|
|
726
|
-
|
|
731
|
+
const cycleHandoffs = Array.isArray(parsed.cycleHandoffs) ? parsed.cycleHandoffs.map((h) => {
|
|
732
|
+
const { taskId: _t, buildHandoff: _b, ...rest } = h;
|
|
733
|
+
if (typeof h.buildHandoff === "object" && h.buildHandoff !== null && !Array.isArray(h.buildHandoff)) {
|
|
734
|
+
Object.assign(rest, h.buildHandoff);
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
taskId: coerceToString(h.taskId),
|
|
738
|
+
buildHandoff: typeof h.buildHandoff === "string" ? h.buildHandoff : "",
|
|
739
|
+
...Object.keys(rest).length > 0 ? { structuredFields: rest } : {}
|
|
740
|
+
};
|
|
741
|
+
}) : [];
|
|
727
742
|
const newTasks = Array.isArray(parsed.newTasks) ? parsed.newTasks.map((t) => ({
|
|
728
743
|
// task-2242: stable join key (optional — undefined falls back to index).
|
|
729
744
|
tempId: t.tempId !== void 0 && t.tempId !== null ? coerceToString(t.tempId) : void 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.64",
|
|
4
4
|
"description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"mcpName": "io.github.getpapi/papi",
|