@papi-ai/server 0.7.66 → 0.7.69
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/index.js +1061 -148
- package/dist/prompts.js +65 -21
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4340,9 +4340,9 @@ __export(doctor_exports, {
|
|
|
4340
4340
|
__testing: () => __testing,
|
|
4341
4341
|
runDoctor: () => runDoctor
|
|
4342
4342
|
});
|
|
4343
|
-
import { existsSync as
|
|
4343
|
+
import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
|
|
4344
4344
|
import { homedir as homedir4 } from "os";
|
|
4345
|
-
import { join as
|
|
4345
|
+
import { join as join21 } from "path";
|
|
4346
4346
|
function redact(name, value) {
|
|
4347
4347
|
if (!value) return "(empty)";
|
|
4348
4348
|
if (SECRET_VARS.has(name)) {
|
|
@@ -4353,14 +4353,14 @@ function redact(name, value) {
|
|
|
4353
4353
|
}
|
|
4354
4354
|
function findMcpJson() {
|
|
4355
4355
|
const candidates = [
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4356
|
+
join21(process.cwd(), ".mcp.json"),
|
|
4357
|
+
join21(homedir4(), ".claude", ".mcp.json"),
|
|
4358
|
+
join21(homedir4(), ".mcp.json")
|
|
4359
4359
|
];
|
|
4360
4360
|
for (const path7 of candidates) {
|
|
4361
|
-
if (!
|
|
4361
|
+
if (!existsSync12(path7)) continue;
|
|
4362
4362
|
try {
|
|
4363
|
-
const raw =
|
|
4363
|
+
const raw = readFileSync14(path7, "utf-8");
|
|
4364
4364
|
const parsed = JSON.parse(raw);
|
|
4365
4365
|
const papiEntry = parsed.papi ?? parsed.mcpServers?.papi;
|
|
4366
4366
|
if (!papiEntry) continue;
|
|
@@ -4638,17 +4638,17 @@ __export(reset_exports, {
|
|
|
4638
4638
|
removePapiEntry: () => removePapiEntry,
|
|
4639
4639
|
runReset: () => runReset
|
|
4640
4640
|
});
|
|
4641
|
-
import { existsSync as
|
|
4641
|
+
import { existsSync as existsSync13, readFileSync as readFileSync15, writeFileSync as writeFileSync7 } from "fs";
|
|
4642
4642
|
import { homedir as homedir5 } from "os";
|
|
4643
|
-
import { join as
|
|
4643
|
+
import { join as join22 } from "path";
|
|
4644
4644
|
import { createInterface } from "readline/promises";
|
|
4645
4645
|
function findResetTarget() {
|
|
4646
4646
|
for (const path7 of CANDIDATE_PATHS()) {
|
|
4647
|
-
if (!
|
|
4647
|
+
if (!existsSync13(path7)) continue;
|
|
4648
4648
|
let raw;
|
|
4649
4649
|
let parsed;
|
|
4650
4650
|
try {
|
|
4651
|
-
raw =
|
|
4651
|
+
raw = readFileSync15(path7, "utf-8");
|
|
4652
4652
|
parsed = JSON.parse(raw);
|
|
4653
4653
|
} catch {
|
|
4654
4654
|
continue;
|
|
@@ -4719,7 +4719,7 @@ async function runReset(args = []) {
|
|
|
4719
4719
|
}
|
|
4720
4720
|
}
|
|
4721
4721
|
try {
|
|
4722
|
-
|
|
4722
|
+
writeFileSync7(target.path, removePapiEntry(target), "utf-8");
|
|
4723
4723
|
process.stdout.write(`
|
|
4724
4724
|
\u2713 Removed papi entry from ${target.path}
|
|
4725
4725
|
`);
|
|
@@ -4736,9 +4736,9 @@ var init_reset = __esm({
|
|
|
4736
4736
|
"src/cli/reset.ts"() {
|
|
4737
4737
|
"use strict";
|
|
4738
4738
|
CANDIDATE_PATHS = () => [
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4739
|
+
join22(process.cwd(), ".mcp.json"),
|
|
4740
|
+
join22(homedir5(), ".claude", ".mcp.json"),
|
|
4741
|
+
join22(homedir5(), ".mcp.json")
|
|
4742
4742
|
];
|
|
4743
4743
|
}
|
|
4744
4744
|
});
|
|
@@ -4749,9 +4749,9 @@ __export(audit_exports, {
|
|
|
4749
4749
|
__testing: () => __testing2,
|
|
4750
4750
|
runAudit: () => runAudit
|
|
4751
4751
|
});
|
|
4752
|
-
import { existsSync as
|
|
4752
|
+
import { existsSync as existsSync14, readFileSync as readFileSync16, readdirSync as readdirSync7 } from "fs";
|
|
4753
4753
|
import { homedir as homedir6 } from "os";
|
|
4754
|
-
import { join as
|
|
4754
|
+
import { join as join23 } from "path";
|
|
4755
4755
|
function safeListDirs(dir) {
|
|
4756
4756
|
try {
|
|
4757
4757
|
return readdirSync7(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b2) => a.localeCompare(b2));
|
|
@@ -4767,10 +4767,10 @@ function safeListFiles(dir, ext) {
|
|
|
4767
4767
|
}
|
|
4768
4768
|
}
|
|
4769
4769
|
function readMcp(projectPath) {
|
|
4770
|
-
const path7 =
|
|
4771
|
-
if (!
|
|
4770
|
+
const path7 = join23(projectPath, ".mcp.json");
|
|
4771
|
+
if (!existsSync14(path7)) return { servers: [] };
|
|
4772
4772
|
try {
|
|
4773
|
-
const parsed = JSON.parse(
|
|
4773
|
+
const parsed = JSON.parse(readFileSync16(path7, "utf-8"));
|
|
4774
4774
|
const mcpServers = parsed.mcpServers ?? {};
|
|
4775
4775
|
const servers = Object.keys(mcpServers);
|
|
4776
4776
|
if (parsed.papi && !servers.includes("papi")) servers.push("papi");
|
|
@@ -4802,18 +4802,18 @@ function auditProjectSync(projectPath, name) {
|
|
|
4802
4802
|
path: projectPath,
|
|
4803
4803
|
papiProjectId,
|
|
4804
4804
|
mcpServers: servers,
|
|
4805
|
-
skills: safeListDirs(
|
|
4806
|
-
agentSkills: safeListDirs(
|
|
4807
|
-
agents: safeListFiles(
|
|
4808
|
-
hooks: safeListFiles(
|
|
4805
|
+
skills: safeListDirs(join23(projectPath, ".claude", "skills")),
|
|
4806
|
+
agentSkills: safeListDirs(join23(projectPath, ".agents", "skills")),
|
|
4807
|
+
agents: safeListFiles(join23(projectPath, ".claude", "agents"), ".md"),
|
|
4808
|
+
hooks: safeListFiles(join23(projectPath, ".claude", "hooks"), ".sh")
|
|
4809
4809
|
};
|
|
4810
4810
|
}
|
|
4811
4811
|
function discoverProjects() {
|
|
4812
4812
|
const out = [];
|
|
4813
4813
|
for (const root of PROJECT_ROOTS) {
|
|
4814
4814
|
for (const name of safeListDirs(root)) {
|
|
4815
|
-
const path7 =
|
|
4816
|
-
if (
|
|
4815
|
+
const path7 = join23(root, name);
|
|
4816
|
+
if (existsSync14(join23(path7, ".mcp.json")) || existsSync14(join23(path7, ".claude"))) {
|
|
4817
4817
|
out.push({ name, path: path7 });
|
|
4818
4818
|
}
|
|
4819
4819
|
}
|
|
@@ -4824,9 +4824,9 @@ function readGlobalSkills() {
|
|
|
4824
4824
|
return safeListDirs(GLOBAL_SKILLS_DIR);
|
|
4825
4825
|
}
|
|
4826
4826
|
function readGlobalMcpServers() {
|
|
4827
|
-
if (!
|
|
4827
|
+
if (!existsSync14(GLOBAL_CLAUDE_JSON)) return [];
|
|
4828
4828
|
try {
|
|
4829
|
-
const parsed = JSON.parse(
|
|
4829
|
+
const parsed = JSON.parse(readFileSync16(GLOBAL_CLAUDE_JSON, "utf-8"));
|
|
4830
4830
|
const servers = parsed.mcpServers ?? {};
|
|
4831
4831
|
return Object.keys(servers).sort((a, b2) => a.localeCompare(b2));
|
|
4832
4832
|
} catch {
|
|
@@ -4978,9 +4978,9 @@ var PROJECT_ROOTS, GLOBAL_SKILLS_DIR, GLOBAL_CLAUDE_JSON, IDLE_WINDOW_DAYS, GLOB
|
|
|
4978
4978
|
var init_audit = __esm({
|
|
4979
4979
|
"src/cli/audit.ts"() {
|
|
4980
4980
|
"use strict";
|
|
4981
|
-
PROJECT_ROOTS = [
|
|
4982
|
-
GLOBAL_SKILLS_DIR =
|
|
4983
|
-
GLOBAL_CLAUDE_JSON =
|
|
4981
|
+
PROJECT_ROOTS = [join23(homedir6(), "Ai-App-Projects"), join23(homedir6(), "android-projects")];
|
|
4982
|
+
GLOBAL_SKILLS_DIR = join23(homedir6(), ".claude", "skills");
|
|
4983
|
+
GLOBAL_CLAUDE_JSON = join23(homedir6(), ".claude.json");
|
|
4984
4984
|
IDLE_WINDOW_DAYS = 30;
|
|
4985
4985
|
GLOBALIZE_THRESHOLD = 3;
|
|
4986
4986
|
__testing2 = { readMcp, computeFlags, formatReport: formatReport2, discoverProjects, auditProjectSync };
|
|
@@ -4992,8 +4992,8 @@ var setup_exports = {};
|
|
|
4992
4992
|
__export(setup_exports, {
|
|
4993
4993
|
runSetup: () => runSetup
|
|
4994
4994
|
});
|
|
4995
|
-
import { existsSync as
|
|
4996
|
-
import { join as
|
|
4995
|
+
import { existsSync as existsSync15, readFileSync as readFileSync17, writeFileSync as writeFileSync8, chmodSync as chmodSync2, statSync as statSync8 } from "fs";
|
|
4996
|
+
import { join as join24 } from "path";
|
|
4997
4997
|
function baseUrl() {
|
|
4998
4998
|
const fromEnv = process.env["PAPI_HOST"] ?? process.env["PAPI_BASE_URL"];
|
|
4999
4999
|
if (fromEnv) return fromEnv.replace(/\/$/, "");
|
|
@@ -5025,11 +5025,11 @@ function sleep(ms) {
|
|
|
5025
5025
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5026
5026
|
}
|
|
5027
5027
|
function writeMcpJson(opts) {
|
|
5028
|
-
const path7 =
|
|
5028
|
+
const path7 = join24(process.cwd(), ".mcp.json");
|
|
5029
5029
|
let parsed = {};
|
|
5030
|
-
if (
|
|
5030
|
+
if (existsSync15(path7)) {
|
|
5031
5031
|
try {
|
|
5032
|
-
parsed = JSON.parse(
|
|
5032
|
+
parsed = JSON.parse(readFileSync17(path7, "utf-8"));
|
|
5033
5033
|
} catch {
|
|
5034
5034
|
throw new Error(`.mcp.json at ${path7} is not valid JSON. Fix it or remove it before re-running setup.`);
|
|
5035
5035
|
}
|
|
@@ -5050,7 +5050,7 @@ function writeMcpJson(opts) {
|
|
|
5050
5050
|
}
|
|
5051
5051
|
mcpServers.papi = papiEntry;
|
|
5052
5052
|
parsed.mcpServers = mcpServers;
|
|
5053
|
-
|
|
5053
|
+
writeFileSync8(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5054
5054
|
try {
|
|
5055
5055
|
const mode = statSync8(path7).mode & 511;
|
|
5056
5056
|
if (mode !== 384) chmodSync2(path7, 384);
|
|
@@ -5160,8 +5160,8 @@ var init_setup = __esm({
|
|
|
5160
5160
|
});
|
|
5161
5161
|
|
|
5162
5162
|
// src/index.ts
|
|
5163
|
-
import { readFileSync as
|
|
5164
|
-
import { dirname as dirname6, join as
|
|
5163
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
5164
|
+
import { dirname as dirname6, join as join25, basename as basename2 } from "path";
|
|
5165
5165
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
5166
5166
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5167
5167
|
import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -5210,6 +5210,7 @@ function loadConfig() {
|
|
|
5210
5210
|
const baseBranch = process.env.PAPI_BASE_BRANCH ?? "main";
|
|
5211
5211
|
const autoPR = process.env.PAPI_AUTO_PR !== "false";
|
|
5212
5212
|
const lightMode = process.env.PAPI_LIGHT_MODE === "true";
|
|
5213
|
+
const autoWorktree = process.env.PAPI_AUTO_WORKTREE === "true";
|
|
5213
5214
|
const projectOwner = process.env.PAPI_OWNER ?? "Cathal";
|
|
5214
5215
|
const skipProjectSpecificRules = process.env.PAPI_SKIP_PROJECT_RULES === "true";
|
|
5215
5216
|
const userId = process.env.PAPI_USER_ID || void 0;
|
|
@@ -5283,8 +5284,13 @@ Already have an account? Make sure PAPI_USER_ID is set in your .mcp.json env con
|
|
|
5283
5284
|
baseBranch,
|
|
5284
5285
|
autoPR,
|
|
5285
5286
|
adapterType,
|
|
5287
|
+
// Local stdio boot: the server runs on the user's machine, so the client
|
|
5288
|
+
// shares its filesystem. The HTTP transport overrides this to false per
|
|
5289
|
+
// request (task-2906).
|
|
5290
|
+
localFilesystem: true,
|
|
5286
5291
|
papiEndpoint,
|
|
5287
5292
|
lightMode,
|
|
5293
|
+
autoWorktree,
|
|
5288
5294
|
projectOwner,
|
|
5289
5295
|
skipProjectSpecificRules,
|
|
5290
5296
|
userId,
|
|
@@ -8199,9 +8205,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
8199
8205
|
}
|
|
8200
8206
|
|
|
8201
8207
|
// src/server.ts
|
|
8202
|
-
import { readFileSync as
|
|
8208
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
8203
8209
|
import { access as access4, readdir as readdir4, readFile as readFile9 } from "fs/promises";
|
|
8204
|
-
import { join as
|
|
8210
|
+
import { join as join20, dirname as dirname5 } from "path";
|
|
8205
8211
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
8206
8212
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8207
8213
|
import {
|
|
@@ -9475,11 +9481,16 @@ function buildPlanUserMessage(ctx) {
|
|
|
9475
9481
|
""
|
|
9476
9482
|
);
|
|
9477
9483
|
}
|
|
9478
|
-
|
|
9479
|
-
|
|
9484
|
+
const enrichment = [];
|
|
9485
|
+
const addEnrichment = (label, ...lines) => {
|
|
9486
|
+
enrichment.push({ label, block: lines.join("\n") });
|
|
9487
|
+
};
|
|
9488
|
+
if (ctx.strategyRecommendations) {
|
|
9489
|
+
addEnrichment("Strategy Recommendations (Pending)", "### Strategy Recommendations (Pending)", "", ctx.strategyRecommendations, "");
|
|
9480
9490
|
}
|
|
9481
9491
|
if (ctx.learningPatterns) {
|
|
9482
|
-
|
|
9492
|
+
addEnrichment(
|
|
9493
|
+
"Recurring Learning Patterns",
|
|
9483
9494
|
"### Recurring Learning Patterns (cross-cycle)",
|
|
9484
9495
|
"",
|
|
9485
9496
|
"Tags from cycle learnings (surprises, issues, dead-ends, estimation misses) that keep recurring. Treat a module/theme that recurs across many cycles as a signal: size its tasks up and write a sharper pre-mortem.",
|
|
@@ -9489,7 +9500,8 @@ function buildPlanUserMessage(ctx) {
|
|
|
9489
9500
|
);
|
|
9490
9501
|
}
|
|
9491
9502
|
if (ctx.decisionScorePatterns) {
|
|
9492
|
-
|
|
9503
|
+
addEnrichment(
|
|
9504
|
+
"Decision Risk Scores",
|
|
9493
9505
|
"### Decision Risk Scores (high-risk + movement)",
|
|
9494
9506
|
"",
|
|
9495
9507
|
"Per-decision risk scores (effort+risk+reversibility+scaleCost+lockIn, /25; lower = safer). Decisions flagged \u26A0 high-risk (>15/25) or whose score moved since the last scoring. When a task depends on or advances a \u26A0 high-risk or \u2191 riskier decision, name that decision in the BUILD HANDOFF pre-mortem and lean toward the larger estimate. Do NOT re-score decisions here \u2014 this is read-only input.",
|
|
@@ -9498,38 +9510,39 @@ function buildPlanUserMessage(ctx) {
|
|
|
9498
9510
|
""
|
|
9499
9511
|
);
|
|
9500
9512
|
}
|
|
9513
|
+
if (ctx.estimationCalibration) {
|
|
9514
|
+
addEnrichment("Estimation Calibration", "### Estimation Calibration (Historical)", "", ctx.estimationCalibration, "");
|
|
9515
|
+
}
|
|
9501
9516
|
if (ctx.reviewPatterns) {
|
|
9502
|
-
|
|
9517
|
+
addEnrichment("Review Patterns", "### Review Patterns", "", ctx.reviewPatterns, "");
|
|
9503
9518
|
}
|
|
9504
9519
|
if (ctx.methodologyMetrics) {
|
|
9505
|
-
|
|
9506
|
-
}
|
|
9507
|
-
if (ctx.estimationCalibration) {
|
|
9508
|
-
parts.push("### Estimation Calibration (Historical)", "", ctx.estimationCalibration, "");
|
|
9520
|
+
addEnrichment("Methodology Trends", "### Methodology Trends", "", ctx.methodologyMetrics, "");
|
|
9509
9521
|
}
|
|
9510
9522
|
if (ctx.horizonContext) {
|
|
9511
|
-
|
|
9523
|
+
addEnrichment("Forward Horizon", "### Forward Horizon", "", ctx.horizonContext, "");
|
|
9512
9524
|
}
|
|
9513
|
-
if (ctx.
|
|
9514
|
-
|
|
9515
|
-
}
|
|
9516
|
-
if (ctx.dogfoodEntries) {
|
|
9517
|
-
parts.push("### Dogfood Observations (Recent)", "", ctx.dogfoodEntries, "");
|
|
9525
|
+
if (ctx.buildPatterns) {
|
|
9526
|
+
addEnrichment("Build Patterns", "### Build Patterns", "", ctx.buildPatterns, "");
|
|
9518
9527
|
}
|
|
9519
9528
|
if (ctx.taskComments) {
|
|
9520
|
-
|
|
9529
|
+
addEnrichment("Task Discussion Threads", "### Task Discussion Threads", "", ctx.taskComments, "");
|
|
9521
9530
|
}
|
|
9522
9531
|
if (ctx.recentReviews) {
|
|
9523
|
-
|
|
9532
|
+
addEnrichment("Human Reviews", "### Human Reviews", "", ctx.recentReviews, "");
|
|
9533
|
+
}
|
|
9534
|
+
if (ctx.dogfoodEntries) {
|
|
9535
|
+
addEnrichment("Dogfood Observations", "### Dogfood Observations (Recent)", "", ctx.dogfoodEntries, "");
|
|
9524
9536
|
}
|
|
9525
9537
|
if (ctx.discoveryCanvas) {
|
|
9526
|
-
|
|
9538
|
+
addEnrichment("Discovery Canvas", "### Discovery Canvas", "", ctx.discoveryCanvas, "");
|
|
9527
9539
|
}
|
|
9528
9540
|
if (ctx.registeredDocs) {
|
|
9529
|
-
|
|
9541
|
+
addEnrichment("Relevant Research Docs", "### Relevant Research Docs", "", ctx.registeredDocs, "");
|
|
9530
9542
|
}
|
|
9531
9543
|
if (ctx.unactionedDocs) {
|
|
9532
|
-
|
|
9544
|
+
addEnrichment(
|
|
9545
|
+
"Unactioned Research",
|
|
9533
9546
|
"### Unactioned Research (soft-warn)",
|
|
9534
9547
|
"",
|
|
9535
9548
|
"These registered docs still have pending actions. Before adding net-new tasks, consider whether one of these unactioned items should be promoted into this cycle via `doc_action_promote`. Surface this in your plan output so the user can decide.",
|
|
@@ -9539,7 +9552,31 @@ function buildPlanUserMessage(ctx) {
|
|
|
9539
9552
|
);
|
|
9540
9553
|
}
|
|
9541
9554
|
if (ctx.carryForwardStaleness) {
|
|
9542
|
-
|
|
9555
|
+
addEnrichment("Carry-Forward Staleness", "### Carry-Forward Staleness", "", ctx.carryForwardStaleness, "");
|
|
9556
|
+
}
|
|
9557
|
+
const contextBudget = Number(process.env.PAPI_PLAN_CONTEXT_BUDGET) || 9e4;
|
|
9558
|
+
const coreBytes = Buffer.byteLength(parts.join("\n"), "utf-8");
|
|
9559
|
+
let enrichmentSpent = 0;
|
|
9560
|
+
const elidedLabels = [];
|
|
9561
|
+
for (const { label, block } of enrichment) {
|
|
9562
|
+
const cost = Buffer.byteLength(`
|
|
9563
|
+
${block}`, "utf-8");
|
|
9564
|
+
if (coreBytes + enrichmentSpent + cost > contextBudget) {
|
|
9565
|
+
elidedLabels.push(label);
|
|
9566
|
+
continue;
|
|
9567
|
+
}
|
|
9568
|
+
parts.push(block);
|
|
9569
|
+
enrichmentSpent += cost;
|
|
9570
|
+
}
|
|
9571
|
+
if (elidedLabels.length > 0) {
|
|
9572
|
+
parts.push(
|
|
9573
|
+
"### Context Budget \u2014 enrichment sections trimmed",
|
|
9574
|
+
"",
|
|
9575
|
+
`${elidedLabels.length} non-essential context section(s) were omitted to keep this plan payload under ${Math.round(contextBudget / 1024)} KB \u2014 a mature board otherwise overflows the client's tool-result ceiling and the plan output is spilled to a file. Trimmed (lowest-value first): ${elidedLabels.join(", ")}.`,
|
|
9576
|
+
"",
|
|
9577
|
+
"These are cross-cycle analytics/enrichment, NOT task-selection inputs \u2014 the Board, Active Decisions, Product Brief, Full Notes, and Recent Build Reports above are complete and untrimmed. Plan normally. If you specifically need one of the trimmed sections, raise `PAPI_PLAN_CONTEXT_BUDGET` in the MCP server env and re-run.",
|
|
9578
|
+
""
|
|
9579
|
+
);
|
|
9543
9580
|
}
|
|
9544
9581
|
}
|
|
9545
9582
|
return parts.join("\n");
|
|
@@ -9928,6 +9965,19 @@ function buildReviewUserMessage(ctx) {
|
|
|
9928
9965
|
} else {
|
|
9929
9966
|
parts.push("(Showing build reports and cycle log entries since last strategy review.)");
|
|
9930
9967
|
}
|
|
9968
|
+
if (ctx.earnedPushback) {
|
|
9969
|
+
parts.push(
|
|
9970
|
+
"",
|
|
9971
|
+
"---",
|
|
9972
|
+
"",
|
|
9973
|
+
"## \u26A0\uFE0F EARNED ADVERSARIAL PUSHBACK",
|
|
9974
|
+
"",
|
|
9975
|
+
"This section appears ONLY because concrete signals crossed a threshold this window. Address each item in your review with a clearly-labelled pushback callout: verify the claim against the actual build reports / code / decisions before accepting it, and push back proportionally to the stakes. If verification shows a signal is benign, say so and move on. Do NOT manufacture additional pushback beyond what these signals support.",
|
|
9976
|
+
"",
|
|
9977
|
+
ctx.earnedPushback,
|
|
9978
|
+
""
|
|
9979
|
+
);
|
|
9980
|
+
}
|
|
9931
9981
|
parts.push(
|
|
9932
9982
|
"",
|
|
9933
9983
|
"---",
|
|
@@ -12794,11 +12844,24 @@ Run \`strategy_review\` first, or pass \`force: true\` to bypass this gate.`
|
|
|
12794
12844
|
}
|
|
12795
12845
|
return { mode, cycleNumber, strategyReviewWarning };
|
|
12796
12846
|
}
|
|
12847
|
+
function assertApplyPayloadNonEmpty(data, cycleNumber) {
|
|
12848
|
+
const handoffCount = data.cycleHandoffs?.length ?? 0;
|
|
12849
|
+
const taskIdCount = data.cycleTaskIds?.length ?? 0;
|
|
12850
|
+
const newTaskCount = data.newTasks?.length ?? 0;
|
|
12851
|
+
if (handoffCount + taskIdCount + newTaskCount === 0) {
|
|
12852
|
+
throw new Error(
|
|
12853
|
+
`Plan apply rejected: trimmed/partial apply payload \u2014 nothing persisted. The parsed plan for Cycle ${cycleNumber + 1} assigns NO tasks to the cycle (cycleHandoffs, cycleTaskIds and newTasks are all empty). Writing it would create a cycle row with zero task membership and no handoffs \u2014 the phantom-empty-cycle failure mode that renders an empty Team view. The most likely cause is a truncated apply JSON: the prepare payload overflowed the client tool-result ceiling (see task-2905/2906). Re-run the plan and resend the COMPLETE structured output \u2014 ensure the JSON after the <!-- PAPI_STRUCTURED_OUTPUT --> marker includes the full cycleHandoffs array (or cycleTaskIds when using skip_handoffs).`
|
|
12854
|
+
);
|
|
12855
|
+
}
|
|
12856
|
+
}
|
|
12797
12857
|
async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber, contextHashes, planRunMeta) {
|
|
12798
12858
|
const applyStartMs = Date.now();
|
|
12799
12859
|
const applyScope = await resolvePlanScope(adapter2, config2);
|
|
12800
12860
|
await assertSingleActiveCycle(adapter2, { allowNumber: cycleNumber + 1, userId: applyScope.callerUserId ?? void 0 });
|
|
12801
12861
|
const { displayText, data } = parseStructuredOutput(rawOutput);
|
|
12862
|
+
if (data) {
|
|
12863
|
+
assertApplyPayloadNonEmpty(data, cycleNumber);
|
|
12864
|
+
}
|
|
12802
12865
|
let resolvedDisplayText = displayText;
|
|
12803
12866
|
let autoCommitNote = "";
|
|
12804
12867
|
let priorityLockNote = "";
|
|
@@ -13276,7 +13339,15 @@ function buildSubagentDispatchPrompt(input) {
|
|
|
13276
13339
|
- "pass" = no blocking issues; "warn" = minor/non-blocking nits; "fail" = blocking issues that should send the build back. \`file\`/\`line\` are optional per finding. Empty \`findings\` is valid when clean.` : `- Read the system prompt and context exactly as the main agent would.
|
|
13277
13340
|
- Produce the full structured output the system prompt requires (Part 1 markdown + Part 2 JSON after \`<!-- PAPI_STRUCTURED_OUTPUT -->\`, or whatever the system prompt specifies).
|
|
13278
13341
|
- Return ONLY that output \u2014 no preamble, no commentary, no closing summary. The dispatching agent will pass your reply verbatim to the apply call.`;
|
|
13279
|
-
const contextBlock = isReview ?
|
|
13342
|
+
const contextBlock = input.contextFilePath ? isReview ? `The review rubric and the full build-under-review context have been written to a local file:
|
|
13343
|
+
|
|
13344
|
+
${input.contextFilePath}
|
|
13345
|
+
|
|
13346
|
+
Read that file IN FULL (it contains the rubric and the build report + diff) before you start. Do not skip it \u2014 it is your only source of context.` : `The system prompt and full planning context have been written to a local file:
|
|
13347
|
+
|
|
13348
|
+
${input.contextFilePath}
|
|
13349
|
+
|
|
13350
|
+
Read that file IN FULL before you produce anything \u2014 it contains the system prompt and all context you need. Do not skip it; it is your only source of context.` : isReview ? `<review_rubric>
|
|
13280
13351
|
${systemPrompt}
|
|
13281
13352
|
</review_rubric>
|
|
13282
13353
|
|
|
@@ -13682,6 +13753,15 @@ function clearPrepareSpill(projectId, callerKey) {
|
|
|
13682
13753
|
} catch {
|
|
13683
13754
|
}
|
|
13684
13755
|
}
|
|
13756
|
+
function contextPath(projectId, callerKey) {
|
|
13757
|
+
const id = createHash2("sha256").update(`${projectId ?? "no-project"}|${callerKey ?? DEFAULT_CALLER_KEY3}`).digest("hex").slice(0, 16);
|
|
13758
|
+
return join3(tmpdir(), `papi-plan-context-${id}.md`);
|
|
13759
|
+
}
|
|
13760
|
+
function savePrepareContextFile(projectId, callerKey, content) {
|
|
13761
|
+
const path7 = contextPath(projectId, callerKey);
|
|
13762
|
+
writeFileSync2(path7, content, { mode: 384 });
|
|
13763
|
+
return path7;
|
|
13764
|
+
}
|
|
13685
13765
|
|
|
13686
13766
|
// src/tools/plan.ts
|
|
13687
13767
|
var planPrepareCache = new PerCallerCache();
|
|
@@ -13690,6 +13770,7 @@ var planTool = {
|
|
|
13690
13770
|
description: 'Run once per cycle to select tasks and generate BUILD HANDOFFs. Call after setup (first time) or after completing all builds AND running release for the previous cycle. Returns prioritised task recommendations with detailed implementation specs. NEVER call when unbuilt cycle tasks exist \u2014 build and release first. First call returns a planning prompt for you to execute (prepare phase). Then call again with mode "apply" and your output to write results. Use skip_handoffs=true for large backlogs \u2014 handoffs are then generated separately via `handoff_generate`.',
|
|
13691
13771
|
annotations: { title: "Plan Cycle", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
13692
13772
|
inputSchema: {
|
|
13773
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
13693
13774
|
type: "object",
|
|
13694
13775
|
properties: {
|
|
13695
13776
|
mode: {
|
|
@@ -13760,7 +13841,16 @@ var planTool = {
|
|
|
13760
13841
|
description: 'Apply-mode confirmation guard for planner-initiated cancellations (task-1727). When false (default), any boardCorrection with status="Cancelled" is SKIPPED and listed in the response so the user can confirm before destructive writes. Set true to apply the cancellations. board_edit-direct cancellations are unaffected \u2014 this guard is plan-only.'
|
|
13761
13842
|
}
|
|
13762
13843
|
},
|
|
13763
|
-
required: []
|
|
13844
|
+
required: [],
|
|
13845
|
+
// task-2802: mirror resolveLlmResponse — llm_response and llm_response_file are
|
|
13846
|
+
// mutually exclusive, and an apply call must carry exactly one of them.
|
|
13847
|
+
not: { required: ["llm_response", "llm_response_file"] },
|
|
13848
|
+
allOf: [
|
|
13849
|
+
{
|
|
13850
|
+
if: { properties: { mode: { const: "apply" } }, required: ["mode"] },
|
|
13851
|
+
then: { anyOf: [{ required: ["llm_response"] }, { required: ["llm_response_file"] }] }
|
|
13852
|
+
}
|
|
13853
|
+
]
|
|
13764
13854
|
}
|
|
13765
13855
|
};
|
|
13766
13856
|
function formatPlanResult(result) {
|
|
@@ -13929,6 +14019,27 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13929
14019
|
} else {
|
|
13930
14020
|
dispatch = "inline";
|
|
13931
14021
|
}
|
|
14022
|
+
const modeLabel = result.mode === "bootstrap" ? "Bootstrap" : "Full";
|
|
14023
|
+
const header = result.strategyReviewWarning ? `${result.strategyReviewWarning}
|
|
14024
|
+
` : "";
|
|
14025
|
+
let contextFilePath;
|
|
14026
|
+
if (config2.localFilesystem) {
|
|
14027
|
+
const contextDoc = `### System Prompt
|
|
14028
|
+
|
|
14029
|
+
${result.systemPrompt}
|
|
14030
|
+
|
|
14031
|
+
---
|
|
14032
|
+
|
|
14033
|
+
### Context
|
|
14034
|
+
|
|
14035
|
+
${result.userMessage}
|
|
14036
|
+
`;
|
|
14037
|
+
try {
|
|
14038
|
+
contextFilePath = savePrepareContextFile(adapter2.getProjectId?.(), callerKey, contextDoc);
|
|
14039
|
+
} catch {
|
|
14040
|
+
contextFilePath = void 0;
|
|
14041
|
+
}
|
|
14042
|
+
}
|
|
13932
14043
|
if (dispatch === "subagent") {
|
|
13933
14044
|
const dispatchPrompt = buildSubagentDispatchPrompt({
|
|
13934
14045
|
tool: "plan",
|
|
@@ -13937,16 +14048,34 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13937
14048
|
strategyReviewWarning: result.strategyReviewWarning,
|
|
13938
14049
|
systemPrompt: result.systemPrompt,
|
|
13939
14050
|
userMessage: result.userMessage,
|
|
13940
|
-
contextBytes: result.contextBytes
|
|
14051
|
+
contextBytes: result.contextBytes,
|
|
14052
|
+
contextFilePath
|
|
13941
14053
|
});
|
|
13942
|
-
const
|
|
14054
|
+
const dispatchHeader = result.strategyReviewWarning ? `${result.strategyReviewWarning}
|
|
13943
14055
|
|
|
13944
14056
|
` : "";
|
|
13945
|
-
return { ...textResponse(`${
|
|
14057
|
+
return { ...textResponse(`${dispatchHeader}${dispatchPrompt}`), _contextBytes: result.contextBytes };
|
|
14058
|
+
}
|
|
14059
|
+
if (contextFilePath) {
|
|
14060
|
+
const kb = result.contextBytes !== void 0 ? ` (~${(result.contextBytes / 1024).toFixed(0)} KB)` : "";
|
|
14061
|
+
const pathResponse = textResponse(
|
|
14062
|
+
`${header}## PAPI Cycle Plan \u2014 Prepare Phase (${modeLabel} Mode, Cycle ${result.cycleNumber + 1})
|
|
14063
|
+
|
|
14064
|
+
The full planning brief \u2014 system prompt + all context${kb} \u2014 has been written to a local file to keep it off the tool-result channel (it overflows the client ceiling on a mature board):
|
|
14065
|
+
|
|
14066
|
+
\`${contextFilePath}\`
|
|
14067
|
+
|
|
14068
|
+
**Do this:**
|
|
14069
|
+
1. **Read that file in full** \u2014 it is the system prompt and the entire planning context.
|
|
14070
|
+
2. Produce the complete plan output in TWO parts: Part 1 markdown with BUILD HANDOFF blocks, then after \`<!-- PAPI_STRUCTURED_OUTPUT -->\` a Part 2 JSON block.
|
|
14071
|
+
3. **Write your output to a local file**, then call \`plan\` again with:
|
|
14072
|
+
- \`mode\`: "apply"
|
|
14073
|
+
- \`llm_response_file\`: the absolute path to YOUR output file
|
|
14074
|
+
- \`cycle_number\`: ${result.cycleNumber + 1}
|
|
14075
|
+
- \`strategy_review_warning\`: "${result.strategyReviewWarning.replace(/"/g, '\\"')}"`
|
|
14076
|
+
);
|
|
14077
|
+
return { ...pathResponse, _contextBytes: result.contextBytes };
|
|
13946
14078
|
}
|
|
13947
|
-
const modeLabel = result.mode === "bootstrap" ? "Bootstrap" : "Full";
|
|
13948
|
-
const header = result.strategyReviewWarning ? `${result.strategyReviewWarning}
|
|
13949
|
-
` : "";
|
|
13950
14079
|
const response = textResponse(
|
|
13951
14080
|
`${header}## PAPI Cycle Plan \u2014 Prepare Phase (${modeLabel} Mode, Cycle ${result.cycleNumber + 1})
|
|
13952
14081
|
|
|
@@ -14564,6 +14693,55 @@ function generateValueReport(snapshots) {
|
|
|
14564
14693
|
return lines.join("\n");
|
|
14565
14694
|
}
|
|
14566
14695
|
|
|
14696
|
+
// src/lib/earned-pushback.ts
|
|
14697
|
+
var SIGNAL_MIN_COUNT = 3;
|
|
14698
|
+
var EFFORT_RANK = { XS: 0, S: 1, M: 2, L: 3, XL: 4 };
|
|
14699
|
+
function rankEffort(size2) {
|
|
14700
|
+
if (!size2) return void 0;
|
|
14701
|
+
return EFFORT_RANK[size2];
|
|
14702
|
+
}
|
|
14703
|
+
function computeEarnedPushback(inputs) {
|
|
14704
|
+
const { reports, log: log2, doneTaskIds } = inputs;
|
|
14705
|
+
const drifted = reports.filter(
|
|
14706
|
+
(r) => r.scopeAccuracy && r.scopeAccuracy !== "accurate" || !!r.scopeDriftSignal
|
|
14707
|
+
);
|
|
14708
|
+
const crept = reports.filter((r) => {
|
|
14709
|
+
const actual = rankEffort(r.actualEffort);
|
|
14710
|
+
const estimated = rankEffort(r.estimatedEffort);
|
|
14711
|
+
return actual !== void 0 && estimated !== void 0 && actual > estimated;
|
|
14712
|
+
});
|
|
14713
|
+
const staleness = computeCarryForwardStaleness(log2, doneTaskIds);
|
|
14714
|
+
const driftFires = drifted.length >= SIGNAL_MIN_COUNT;
|
|
14715
|
+
const creepFires = crept.length >= SIGNAL_MIN_COUNT;
|
|
14716
|
+
const deferralFires = staleness !== void 0;
|
|
14717
|
+
if (!driftFires && !creepFires && !deferralFires) return void 0;
|
|
14718
|
+
const sections = [
|
|
14719
|
+
"The following signals crossed a threshold this window and warrant adversarial scrutiny. For each, verify the claim against the actual reports/code before accepting it, and push back proportionally to the stakes \u2014 do NOT wave it through, and do NOT invent pushback beyond what these signals support."
|
|
14720
|
+
];
|
|
14721
|
+
if (driftFires) {
|
|
14722
|
+
const named = drifted.slice(0, 8).map((r) => {
|
|
14723
|
+
const why = r.scopeDriftSignal ? `files diverged from handoff` : `scope ${r.scopeAccuracy}`;
|
|
14724
|
+
return ` - **${r.displayId ?? r.taskId}** (${r.taskName}) \u2014 ${why}`;
|
|
14725
|
+
}).join("\n");
|
|
14726
|
+
sections.push(
|
|
14727
|
+
`**Scope drift (${drifted.length} build${drifted.length === 1 ? "" : "s"}):** handoff scope did not match what was actually built. Verify whether the plan or the execution was wrong, and whether an Active Decision or planning heuristic needs to change.
|
|
14728
|
+
${named}`
|
|
14729
|
+
);
|
|
14730
|
+
}
|
|
14731
|
+
if (creepFires) {
|
|
14732
|
+
const named = crept.slice(0, 8).map((r) => ` - **${r.displayId ?? r.taskId}** (${r.taskName}) \u2014 estimated ${r.estimatedEffort}, actual ${r.actualEffort}`).join("\n");
|
|
14733
|
+
sections.push(
|
|
14734
|
+
`**Scope creep (${crept.length} task${crept.length === 1 ? "" : "s"}):** actual effort exceeded the estimate. Check whether these were under-scoped at plan time or grew mid-build, and whether cycle sizing is drifting.
|
|
14735
|
+
${named}`
|
|
14736
|
+
);
|
|
14737
|
+
}
|
|
14738
|
+
if (deferralFires) {
|
|
14739
|
+
sections.push(`**Repeat deferral (3+ consecutive cycles):**
|
|
14740
|
+
${staleness}`);
|
|
14741
|
+
}
|
|
14742
|
+
return sections.join("\n\n");
|
|
14743
|
+
}
|
|
14744
|
+
|
|
14567
14745
|
// src/services/strategy.ts
|
|
14568
14746
|
var STRATEGY_DUPE_COVERAGE_THRESHOLD = 0.6;
|
|
14569
14747
|
function taskStatusLabel(task) {
|
|
@@ -15071,10 +15249,10 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
|
|
|
15071
15249
|
try {
|
|
15072
15250
|
const comments = await adapter2.getRecentTaskComments?.(50);
|
|
15073
15251
|
if (comments && comments.length > 0) {
|
|
15074
|
-
const
|
|
15252
|
+
const doneTaskIds2 = new Set(recentDoneTasks.map((t) => t.id));
|
|
15075
15253
|
const inReviewTasks = activeTasks.filter((t) => t.status === "In Review");
|
|
15076
15254
|
const reviewWindowTaskIds = /* @__PURE__ */ new Set([
|
|
15077
|
-
...
|
|
15255
|
+
...doneTaskIds2,
|
|
15078
15256
|
...inReviewTasks.map((t) => t.id)
|
|
15079
15257
|
]);
|
|
15080
15258
|
const filtered = comments.filter((c) => reviewWindowTaskIds.has(c.taskId));
|
|
@@ -15104,7 +15282,7 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
|
|
|
15104
15282
|
try {
|
|
15105
15283
|
if (docsWithPendingActions && docsWithPendingActions.length > 0) {
|
|
15106
15284
|
const STALE_THRESHOLD = 20;
|
|
15107
|
-
const
|
|
15285
|
+
const doneTaskIds2 = new Set(recentDoneTasks.map((t) => t.displayId ?? t.id));
|
|
15108
15286
|
const completed = [];
|
|
15109
15287
|
const deferred = [];
|
|
15110
15288
|
const stale = [];
|
|
@@ -15114,7 +15292,7 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
|
|
|
15114
15292
|
const ageInCycles = cycleNumber - (doc.cycleCreated ?? cycleNumber);
|
|
15115
15293
|
for (const action of pendingActions) {
|
|
15116
15294
|
const line = ` - **${doc.title}** (C${doc.cycleCreated ?? "?"}): ${action.description}${action.linkedTaskId ? ` [\u2192${action.linkedTaskId}]` : ""}`;
|
|
15117
|
-
if (action.linkedTaskId &&
|
|
15295
|
+
if (action.linkedTaskId && doneTaskIds2.has(action.linkedTaskId)) {
|
|
15118
15296
|
completed.push(line);
|
|
15119
15297
|
} else if (ageInCycles > STALE_THRESHOLD) {
|
|
15120
15298
|
stale.push(line);
|
|
@@ -15161,6 +15339,10 @@ ${lines.join("\n")}`;
|
|
|
15161
15339
|
{ label: "taskComments", hasData: taskCommentsText !== void 0 },
|
|
15162
15340
|
{ label: "docActionStaleness", hasData: docActionStalenessText !== void 0 }
|
|
15163
15341
|
]);
|
|
15342
|
+
const doneTaskIds = new Set(
|
|
15343
|
+
recentDoneTasks.map((t) => t.displayId ?? t.id).filter((id) => !!id)
|
|
15344
|
+
);
|
|
15345
|
+
const earnedPushback = computeEarnedPushback({ reports, log: recentLog, doneTaskIds });
|
|
15164
15346
|
const context = {
|
|
15165
15347
|
sessionNumber: cycleNumber,
|
|
15166
15348
|
lastReviewCycle: lastReviewCycleNum,
|
|
@@ -15169,6 +15351,7 @@ ${lines.join("\n")}`;
|
|
|
15169
15351
|
allBuildReports: buildReportsText,
|
|
15170
15352
|
sessionLog: formatCycleLog(recentLog),
|
|
15171
15353
|
board: smartBoard,
|
|
15354
|
+
earnedPushback,
|
|
15172
15355
|
humanReviews: formatReviews(reviews),
|
|
15173
15356
|
buildPatterns: buildPatternsText,
|
|
15174
15357
|
reviewPatterns: reviewPatternsText,
|
|
@@ -16644,6 +16827,7 @@ var boardViewTool = {
|
|
|
16644
16827
|
description: 'View the Board. To find a SPECIFIC task or subset, FILTER FIRST \u2014 do not dump the whole board: pass task_id for one task (full detail), query="<text>" for a title/notes substring match, or cycle=<n> for one cycle. Combine with status/phase. By default shows active tasks only (excludes Done/Cancelled), sorted by priority, limited to 50; titles are truncated in the table (single-task lookup shows the full title). Use status="all" to see everything, mode="summary" for counts only. Does not call the Anthropic API.',
|
|
16645
16828
|
annotations: { title: "View Board", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
16646
16829
|
inputSchema: {
|
|
16830
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
16647
16831
|
type: "object",
|
|
16648
16832
|
properties: {
|
|
16649
16833
|
task_id: {
|
|
@@ -16668,10 +16852,14 @@ var boardViewTool = {
|
|
|
16668
16852
|
},
|
|
16669
16853
|
limit: {
|
|
16670
16854
|
type: "number",
|
|
16855
|
+
minimum: 1,
|
|
16856
|
+
default: 50,
|
|
16671
16857
|
description: "Max tasks to return (default: 50)."
|
|
16672
16858
|
},
|
|
16673
16859
|
offset: {
|
|
16674
16860
|
type: "number",
|
|
16861
|
+
minimum: 0,
|
|
16862
|
+
default: 0,
|
|
16675
16863
|
description: "Skip first N tasks for pagination (default: 0)."
|
|
16676
16864
|
},
|
|
16677
16865
|
mode: {
|
|
@@ -16688,6 +16876,7 @@ var boardDeprioritiseTool = {
|
|
|
16688
16876
|
description: `Remove a task from the current cycle. Four actions: "backlog" (not now, maybe later \u2014 preserves handoff), "defer" (valid but premature \u2014 hidden from planner), "block" (waiting on external dependency \u2014 visible on board but skipped by planner), "cancel" (don't want this \u2014 permanently closed with reason). When a user rejects a task, ALWAYS ask which action they want. Does not call the Anthropic API.`,
|
|
16689
16877
|
annotations: { title: "Deprioritise Task", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
16690
16878
|
inputSchema: {
|
|
16879
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
16691
16880
|
type: "object",
|
|
16692
16881
|
properties: {
|
|
16693
16882
|
task_id: {
|
|
@@ -16725,7 +16914,21 @@ var boardDeprioritiseTool = {
|
|
|
16725
16914
|
description: 'Optional new phase (only applies to "backlog" and "defer" actions).'
|
|
16726
16915
|
}
|
|
16727
16916
|
},
|
|
16728
|
-
required: ["task_id"]
|
|
16917
|
+
required: ["task_id"],
|
|
16918
|
+
// task-2802: mirror the handler's guards so the agent sees the dependency up
|
|
16919
|
+
// front. handleBoardDeprioritise requires `reason` for both block and cancel,
|
|
16920
|
+
// and requires `blocker_ref` whenever `blocker_type` is set. Keyed on explicit
|
|
16921
|
+
// values — an omitted action defaults to "backlog" and triggers neither.
|
|
16922
|
+
allOf: [
|
|
16923
|
+
{
|
|
16924
|
+
if: { properties: { action: { enum: ["block", "cancel"] } }, required: ["action"] },
|
|
16925
|
+
then: { required: ["reason"] }
|
|
16926
|
+
},
|
|
16927
|
+
{
|
|
16928
|
+
if: { required: ["blocker_type"] },
|
|
16929
|
+
then: { required: ["blocker_ref"] }
|
|
16930
|
+
}
|
|
16931
|
+
]
|
|
16729
16932
|
}
|
|
16730
16933
|
};
|
|
16731
16934
|
var boardArchiveTool = {
|
|
@@ -16752,7 +16955,15 @@ var boardEditTool = {
|
|
|
16752
16955
|
description: "Edit fields on an existing task. Supports title, priority, complexity, module, epic, phase, notes (with notes_mode for append/replace/clear), status, maturity, and cycle (number or null). Pass task_id plus any fields to update. Does not call the Anthropic API.",
|
|
16753
16956
|
annotations: { title: "Edit Task", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
16754
16957
|
inputSchema: {
|
|
16958
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
16755
16959
|
type: "object",
|
|
16960
|
+
// task-2802: shared effort-size enum referenced by estimated_effort/actual_effort.
|
|
16961
|
+
$defs: {
|
|
16962
|
+
effortSize: {
|
|
16963
|
+
type: "string",
|
|
16964
|
+
enum: ["XS", "S", "M", "L", "XL"]
|
|
16965
|
+
}
|
|
16966
|
+
},
|
|
16756
16967
|
properties: {
|
|
16757
16968
|
task_id: {
|
|
16758
16969
|
type: "string",
|
|
@@ -16808,13 +17019,11 @@ var boardEditTool = {
|
|
|
16808
17019
|
description: "Cycle assignment. Pass a cycle number to assign, or null to remove from any cycle. Validated against existing cycles. Replaces the prior workaround of editing cycle_tasks.cycle directly via SQL."
|
|
16809
17020
|
},
|
|
16810
17021
|
estimated_effort: {
|
|
16811
|
-
|
|
16812
|
-
enum: ["XS", "S", "M", "L", "XL"],
|
|
17022
|
+
$ref: "#/$defs/effortSize",
|
|
16813
17023
|
description: "task-2182: correct the estimated effort on this task's LATEST build report (fixes a mis-recorded estimate). Updates the build_reports row, not the task \u2014 feeds the next estimation-accuracy recompute."
|
|
16814
17024
|
},
|
|
16815
17025
|
actual_effort: {
|
|
16816
|
-
|
|
16817
|
-
enum: ["XS", "S", "M", "L", "XL"],
|
|
17026
|
+
$ref: "#/$defs/effortSize",
|
|
16818
17027
|
description: "task-2182: correct the actual effort on this task's LATEST build report (fixes a mis-recorded actual)."
|
|
16819
17028
|
}
|
|
16820
17029
|
},
|
|
@@ -17246,6 +17455,9 @@ var PROJECT_BUNDLE_REL = join6(".agents", "skills", "papi-cycle");
|
|
|
17246
17455
|
function bundleDestRel(rel) {
|
|
17247
17456
|
return rel === "AGENTS.md" ? "AGENTS.md" : join6(PROJECT_BUNDLE_REL, rel);
|
|
17248
17457
|
}
|
|
17458
|
+
function claudeSkillDestRel(rel) {
|
|
17459
|
+
return rel === "AGENTS.md" ? void 0 : join6(".claude", "skills", rel);
|
|
17460
|
+
}
|
|
17249
17461
|
function resolveBundleDir() {
|
|
17250
17462
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
17251
17463
|
for (let i = 0; i < 5; i++) {
|
|
@@ -17274,10 +17486,13 @@ function readBundleFiles(bundleDir = resolveBundleDir()) {
|
|
|
17274
17486
|
function planBundleInstall(projectRoot, projectName, opts = {}) {
|
|
17275
17487
|
const out = {};
|
|
17276
17488
|
for (const f of readBundleFiles()) {
|
|
17277
|
-
const dest = join6(projectRoot, bundleDestRel(f.rel));
|
|
17278
|
-
if (opts.skipExisting && existsSync4(dest) && statSync4(dest).isFile()) continue;
|
|
17279
17489
|
const content = f.rel === "AGENTS.md" ? f.content.replace(/\{\{project_name\}\}/g, projectName) : f.content;
|
|
17280
|
-
|
|
17490
|
+
for (const rel of [bundleDestRel(f.rel), claudeSkillDestRel(f.rel)]) {
|
|
17491
|
+
if (!rel) continue;
|
|
17492
|
+
const dest = join6(projectRoot, rel);
|
|
17493
|
+
if (opts.skipExisting && existsSync4(dest) && statSync4(dest).isFile()) continue;
|
|
17494
|
+
out[dest] = content;
|
|
17495
|
+
}
|
|
17281
17496
|
}
|
|
17282
17497
|
return out;
|
|
17283
17498
|
}
|
|
@@ -17565,7 +17780,7 @@ var CLAUDE_MD_ENRICHMENT_SENTINEL_T1 = "<!-- PAPI_ENRICHMENT_TIER_1 -->";
|
|
|
17565
17780
|
var CLAUDE_MD_ENRICHMENT_SENTINEL_T2 = "<!-- PAPI_ENRICHMENT_TIER_2 -->";
|
|
17566
17781
|
var CLAUDE_MD_STUB = `# {{project_name}}
|
|
17567
17782
|
|
|
17568
|
-
This project is managed with **PAPI**. The agent harness \u2014 session workflow, the plan \u2192 build \u2192 review cycle, branching, and conventions \u2014 lives in **\`AGENTS.md\`** (always loaded) plus lazy-loaded phase skills under \`.agents/skills/papi-cycle
|
|
17783
|
+
This project is managed with **PAPI**. The agent harness \u2014 session workflow, the plan \u2192 build \u2192 review cycle, branching, and conventions \u2014 lives in **\`AGENTS.md\`** (always loaded) plus lazy-loaded phase skills under \`.agents/skills/papi-cycle/\` (Claude Code loads the same phase skills natively from \`.claude/skills/papi-*\`).
|
|
17569
17784
|
|
|
17570
17785
|
**At session start: read \`AGENTS.md\`, then run \`orient\`.** Phase mechanics (planning, building, strategy, ideas) load on demand from the skills bundle. Add project-specific notes below this line \u2014 they will not be overwritten.
|
|
17571
17786
|
|
|
@@ -20618,6 +20833,32 @@ async function ownsLocalWorkspace(adapter2, cwd) {
|
|
|
20618
20833
|
return storedOwner === remoteOwner;
|
|
20619
20834
|
}
|
|
20620
20835
|
|
|
20836
|
+
// src/lib/worktree-collision.ts
|
|
20837
|
+
var WORKTREE_DIR = ".claude/worktrees";
|
|
20838
|
+
function detectWorktreeCollision(input) {
|
|
20839
|
+
const target = input.targetBranch.trim();
|
|
20840
|
+
if (!target) return null;
|
|
20841
|
+
const conflicts = input.inProgress.filter((b2) => {
|
|
20842
|
+
const branch = b2.branch?.trim();
|
|
20843
|
+
return !!branch && branch !== target && b2.taskId !== input.taskId;
|
|
20844
|
+
});
|
|
20845
|
+
if (conflicts.length === 0) return null;
|
|
20846
|
+
const worktreePath = `${WORKTREE_DIR}/${input.taskId}`;
|
|
20847
|
+
const worktreeCommand = `git worktree add ${worktreePath} ${target}`;
|
|
20848
|
+
const others = conflicts.map((c) => `${c.taskId} (on '${c.branch.trim()}')`).join(", ");
|
|
20849
|
+
const isAre = conflicts.length === 1 ? "is" : "are";
|
|
20850
|
+
const warning = `\u26A0\uFE0F Concurrent build detected: ${others} ${isAre} In Progress on a different branch, but this build targets '${target}'. Switching HEAD on the shared checkout would revert that session's working tree (the task-2346 data-loss incident). Isolate this build into its own worktree instead of switching branches here.`;
|
|
20851
|
+
const setupHint = `Then run \`npm run worktree:setup\` inside ${worktreePath} \u2014 node_modules only exists in the main worktree, so the symlink step is required before tsup/builds work there (task-1419).`;
|
|
20852
|
+
return {
|
|
20853
|
+
warning,
|
|
20854
|
+
worktreeCommand,
|
|
20855
|
+
setupHint,
|
|
20856
|
+
worktreePath,
|
|
20857
|
+
auto: input.autoWorktree,
|
|
20858
|
+
conflicts
|
|
20859
|
+
};
|
|
20860
|
+
}
|
|
20861
|
+
|
|
20621
20862
|
// src/services/build.ts
|
|
20622
20863
|
init_git();
|
|
20623
20864
|
|
|
@@ -21221,7 +21462,46 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
21221
21462
|
}
|
|
21222
21463
|
taskBranchMap.set(taskId, featureBranch);
|
|
21223
21464
|
const currentBranch = getCurrentBranch(config2.projectRoot);
|
|
21224
|
-
|
|
21465
|
+
let isolatedIntoWorktree = false;
|
|
21466
|
+
if (currentBranch !== featureBranch) {
|
|
21467
|
+
const otherInProgress = allTasks.filter((t) => t.status === "In Progress" && t.id !== taskId && t.displayId !== taskId).map((t) => ({
|
|
21468
|
+
taskId: t.displayId || t.id,
|
|
21469
|
+
branch: (t.branchName ?? taskBranchMap.get(t.id) ?? taskBranchMap.get(t.displayId) ?? "").trim()
|
|
21470
|
+
})).filter((t) => t.branch.length > 0);
|
|
21471
|
+
const collision = detectWorktreeCollision({
|
|
21472
|
+
taskId,
|
|
21473
|
+
targetBranch: featureBranch,
|
|
21474
|
+
inProgress: otherInProgress,
|
|
21475
|
+
autoWorktree: config2.autoWorktree
|
|
21476
|
+
});
|
|
21477
|
+
if (collision) {
|
|
21478
|
+
branchLines.push(collision.warning);
|
|
21479
|
+
if (collision.auto) {
|
|
21480
|
+
try {
|
|
21481
|
+
const { execFileSync: execFileSync7 } = await import("child_process");
|
|
21482
|
+
const baseForWorktree = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
21483
|
+
const wtArgs = branchExists(config2.projectRoot, featureBranch) ? ["worktree", "add", collision.worktreePath, featureBranch] : ["worktree", "add", "-b", featureBranch, collision.worktreePath, baseForWorktree];
|
|
21484
|
+
execFileSync7("git", wtArgs, { cwd: config2.projectRoot, encoding: "utf-8" });
|
|
21485
|
+
isolatedIntoWorktree = true;
|
|
21486
|
+
branchLines.push(
|
|
21487
|
+
`Auto-isolated (PAPI_AUTO_WORKTREE): created worktree at ${collision.worktreePath} on '${featureBranch}'. The shared checkout stays on '${currentBranch}' \u2014 continue this build from inside the worktree.`
|
|
21488
|
+
);
|
|
21489
|
+
branchLines.push(collision.setupHint);
|
|
21490
|
+
} catch (err) {
|
|
21491
|
+
branchLines.push(
|
|
21492
|
+
`Auto-worktree attempt failed (${err instanceof Error ? err.message : String(err)}) \u2014 isolate manually instead:`
|
|
21493
|
+
);
|
|
21494
|
+
branchLines.push(` ${collision.worktreeCommand}`);
|
|
21495
|
+
branchLines.push(collision.setupHint);
|
|
21496
|
+
}
|
|
21497
|
+
} else {
|
|
21498
|
+
branchLines.push(` ${collision.worktreeCommand}`);
|
|
21499
|
+
branchLines.push(collision.setupHint);
|
|
21500
|
+
}
|
|
21501
|
+
}
|
|
21502
|
+
}
|
|
21503
|
+
if (isolatedIntoWorktree) {
|
|
21504
|
+
} else if (currentBranch === featureBranch) {
|
|
21225
21505
|
branchLines.push(`Already on branch '${featureBranch}'.`);
|
|
21226
21506
|
if (useSharedBranch) {
|
|
21227
21507
|
branchLines.push(`Reusing shared cycle branch for ${task.complexity} ${task.module} task.`);
|
|
@@ -22435,6 +22715,97 @@ async function handleDocReorder(adapter2, args) {
|
|
|
22435
22715
|
|
|
22436
22716
|
// src/tools/build.ts
|
|
22437
22717
|
init_git();
|
|
22718
|
+
|
|
22719
|
+
// src/lib/build-checkpoint.ts
|
|
22720
|
+
import { createHash as createHash4 } from "crypto";
|
|
22721
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
22722
|
+
import { join as join13 } from "path";
|
|
22723
|
+
var BUILD_CHECKPOINT_VERSION = 1;
|
|
22724
|
+
function cwdHash(cwd) {
|
|
22725
|
+
return createHash4("sha256").update(realpathOrSelf(cwd)).digest("hex").slice(0, 12);
|
|
22726
|
+
}
|
|
22727
|
+
function safeTaskId(taskId) {
|
|
22728
|
+
return taskId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
22729
|
+
}
|
|
22730
|
+
function checkpointDir(cwd) {
|
|
22731
|
+
return join13(cwd, ".papi", "state");
|
|
22732
|
+
}
|
|
22733
|
+
function checkpointPath(cwd, taskId) {
|
|
22734
|
+
return join13(checkpointDir(cwd), `build-${safeTaskId(taskId)}.${cwdHash(cwd)}.json`);
|
|
22735
|
+
}
|
|
22736
|
+
function writeBuildCheckpoint(input) {
|
|
22737
|
+
try {
|
|
22738
|
+
const dir = checkpointDir(input.cwd);
|
|
22739
|
+
if (!existsSync9(dir)) {
|
|
22740
|
+
mkdirSync3(dir, { recursive: true });
|
|
22741
|
+
}
|
|
22742
|
+
const checkpoint = {
|
|
22743
|
+
version: BUILD_CHECKPOINT_VERSION,
|
|
22744
|
+
taskId: input.taskId,
|
|
22745
|
+
branch: input.branch,
|
|
22746
|
+
step: input.step,
|
|
22747
|
+
lastCommitSha: input.lastCommitSha,
|
|
22748
|
+
modifiedFiles: input.modifiedFiles,
|
|
22749
|
+
cwd: realpathOrSelf(input.cwd),
|
|
22750
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22751
|
+
};
|
|
22752
|
+
writeFileSync4(checkpointPath(input.cwd, input.taskId), JSON.stringify(checkpoint, null, 2) + "\n", "utf-8");
|
|
22753
|
+
} catch {
|
|
22754
|
+
}
|
|
22755
|
+
}
|
|
22756
|
+
function readBuildCheckpoint(key) {
|
|
22757
|
+
try {
|
|
22758
|
+
const path7 = checkpointPath(key.cwd, key.taskId);
|
|
22759
|
+
if (!existsSync9(path7)) return null;
|
|
22760
|
+
const parsed = JSON.parse(readFileSync8(path7, "utf-8"));
|
|
22761
|
+
if (!parsed || parsed.version !== BUILD_CHECKPOINT_VERSION || parsed.taskId !== key.taskId) {
|
|
22762
|
+
return null;
|
|
22763
|
+
}
|
|
22764
|
+
return {
|
|
22765
|
+
version: BUILD_CHECKPOINT_VERSION,
|
|
22766
|
+
taskId: parsed.taskId,
|
|
22767
|
+
branch: parsed.branch ?? null,
|
|
22768
|
+
step: "branch_ready",
|
|
22769
|
+
lastCommitSha: parsed.lastCommitSha ?? null,
|
|
22770
|
+
modifiedFiles: Array.isArray(parsed.modifiedFiles) ? parsed.modifiedFiles : [],
|
|
22771
|
+
cwd: typeof parsed.cwd === "string" ? parsed.cwd : realpathOrSelf(key.cwd),
|
|
22772
|
+
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : ""
|
|
22773
|
+
};
|
|
22774
|
+
} catch {
|
|
22775
|
+
return null;
|
|
22776
|
+
}
|
|
22777
|
+
}
|
|
22778
|
+
function clearBuildCheckpoint(key) {
|
|
22779
|
+
try {
|
|
22780
|
+
const path7 = checkpointPath(key.cwd, key.taskId);
|
|
22781
|
+
if (existsSync9(path7)) {
|
|
22782
|
+
unlinkSync3(path7);
|
|
22783
|
+
}
|
|
22784
|
+
} catch {
|
|
22785
|
+
}
|
|
22786
|
+
}
|
|
22787
|
+
function formatResumeNote(cp) {
|
|
22788
|
+
const files = cp.modifiedFiles.filter((f) => f && f.trim());
|
|
22789
|
+
const fileList = files.length > 0 ? files.slice(0, 12).map((f) => ` - ${f}`).join("\n") + (files.length > 12 ? `
|
|
22790
|
+
- \u2026+${files.length - 12} more` : "") : " _(none recorded)_";
|
|
22791
|
+
const lines = [
|
|
22792
|
+
"> **\u21BB Resuming from checkpoint** \u2014 this task is already In Progress; you are NOT starting clean.",
|
|
22793
|
+
`> - Branch: \`${cp.branch ?? "unknown"}\``,
|
|
22794
|
+
"> - Last step: branch ready",
|
|
22795
|
+
`> - Last commit: ${cp.lastCommitSha ? cp.lastCommitSha.slice(0, 8) : "none"}`,
|
|
22796
|
+
`> - Checkpoint saved: ${cp.updatedAt || "unknown"}`,
|
|
22797
|
+
">",
|
|
22798
|
+
"> Modified files at last checkpoint:",
|
|
22799
|
+
...fileList.split("\n").map((l) => `> ${l}`),
|
|
22800
|
+
">",
|
|
22801
|
+
"> Review the existing branch and changes above before writing new code \u2014 do not re-do work already committed.",
|
|
22802
|
+
"",
|
|
22803
|
+
""
|
|
22804
|
+
];
|
|
22805
|
+
return lines.join("\n");
|
|
22806
|
+
}
|
|
22807
|
+
|
|
22808
|
+
// src/tools/build.ts
|
|
22438
22809
|
var buildListTool = {
|
|
22439
22810
|
name: "build_list",
|
|
22440
22811
|
description: "List cycle tasks that have BUILD HANDOFFs ready for execution. Shows task ID, title, status, priority, and complexity. In Progress tasks appear first, then Backlog. Does not call the Anthropic API.",
|
|
@@ -22470,7 +22841,17 @@ var buildExecuteTool = {
|
|
|
22470
22841
|
description: "Start or complete a build task. Call with just task_id to start (returns BUILD HANDOFF, creates feature branch, marks In Progress). After implementing the task, you MUST call build_execute again with all report fields (completed, effort, estimated_effort, surprises, discovered_issues, architecture_notes) to finish \u2014 do not wait for user confirmation between start and complete. Never call on tasks that are already In Review or Done. Does not call the Anthropic API. Set light=true to skip branch/PR creation (commits to current branch). Set PAPI_LIGHT_MODE=true in env to default all builds to light mode.",
|
|
22471
22842
|
annotations: { title: "Run Build", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
22472
22843
|
inputSchema: {
|
|
22844
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
22473
22845
|
type: "object",
|
|
22846
|
+
// task-2802: JSON Schema 2020-12. Shared effort-size enum lives in $defs and is
|
|
22847
|
+
// referenced from effort/estimated_effort so the two stay in lockstep; the
|
|
22848
|
+
// start-vs-complete contract is expressed as a conditional (see allOf below).
|
|
22849
|
+
$defs: {
|
|
22850
|
+
effortSize: {
|
|
22851
|
+
type: "string",
|
|
22852
|
+
enum: ["XS", "S", "M", "L", "XL"]
|
|
22853
|
+
}
|
|
22854
|
+
},
|
|
22474
22855
|
properties: {
|
|
22475
22856
|
task_id: {
|
|
22476
22857
|
type: "string",
|
|
@@ -22490,13 +22871,11 @@ var buildExecuteTool = {
|
|
|
22490
22871
|
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.`
|
|
22491
22872
|
},
|
|
22492
22873
|
effort: {
|
|
22493
|
-
|
|
22494
|
-
enum: ["XS", "S", "M", "L", "XL"],
|
|
22874
|
+
$ref: "#/$defs/effortSize",
|
|
22495
22875
|
description: "Actual effort: XS, S, M, L, or XL. Required for complete."
|
|
22496
22876
|
},
|
|
22497
22877
|
estimated_effort: {
|
|
22498
|
-
|
|
22499
|
-
enum: ["XS", "S", "M", "L", "XL"],
|
|
22878
|
+
$ref: "#/$defs/effortSize",
|
|
22500
22879
|
description: "Estimated effort from the BUILD HANDOFF. Required for complete."
|
|
22501
22880
|
},
|
|
22502
22881
|
model: {
|
|
@@ -22540,6 +22919,8 @@ var buildExecuteTool = {
|
|
|
22540
22919
|
},
|
|
22541
22920
|
corrections_count: {
|
|
22542
22921
|
type: "integer",
|
|
22922
|
+
minimum: 0,
|
|
22923
|
+
default: 0,
|
|
22543
22924
|
description: "Number of times the user corrected or redirected the build during implementation. Captures informal pushback that bypasses review_submit. Default 0."
|
|
22544
22925
|
},
|
|
22545
22926
|
dead_ends: {
|
|
@@ -22590,7 +22971,29 @@ var buildExecuteTool = {
|
|
|
22590
22971
|
}
|
|
22591
22972
|
}
|
|
22592
22973
|
},
|
|
22593
|
-
required: ["task_id"]
|
|
22974
|
+
required: ["task_id"],
|
|
22975
|
+
// task-2802: a COMPLETE call is any call carrying report data (mirrors the
|
|
22976
|
+
// handler's isCompleteCall at build.ts). When any of the six report fields is
|
|
22977
|
+
// present, all six are required — exactly what completeBuild enforces, so this
|
|
22978
|
+
// never rejects a call the handler would accept; it just steers the agent to
|
|
22979
|
+
// send the full report in one shot instead of round-tripping on a missing field.
|
|
22980
|
+
allOf: [
|
|
22981
|
+
{
|
|
22982
|
+
if: {
|
|
22983
|
+
anyOf: [
|
|
22984
|
+
{ required: ["completed"] },
|
|
22985
|
+
{ required: ["effort"] },
|
|
22986
|
+
{ required: ["estimated_effort"] },
|
|
22987
|
+
{ required: ["surprises"] },
|
|
22988
|
+
{ required: ["discovered_issues"] },
|
|
22989
|
+
{ required: ["architecture_notes"] }
|
|
22990
|
+
]
|
|
22991
|
+
},
|
|
22992
|
+
then: {
|
|
22993
|
+
required: ["completed", "effort", "estimated_effort", "surprises", "discovered_issues", "architecture_notes"]
|
|
22994
|
+
}
|
|
22995
|
+
}
|
|
22996
|
+
]
|
|
22594
22997
|
}
|
|
22595
22998
|
};
|
|
22596
22999
|
var buildCancelTool = {
|
|
@@ -22772,10 +23175,23 @@ async function handleBuildExecute(adapter2, config2, args, clientName) {
|
|
|
22772
23175
|
if (scopeTask) {
|
|
22773
23176
|
tracker.setStreamScope({ taskId: scopeTask.displayId ?? scopeTask.id, cycle: scopeTask.cycle ?? null });
|
|
22774
23177
|
}
|
|
23178
|
+
let resumeNote = "";
|
|
23179
|
+
if (scopeTask?.status === "In Progress") {
|
|
23180
|
+
const existing = readBuildCheckpoint({ cwd: config2.projectRoot, taskId });
|
|
23181
|
+
if (existing) resumeNote = formatResumeNote(existing);
|
|
23182
|
+
}
|
|
22775
23183
|
await tracker.recordStep("started");
|
|
22776
23184
|
const result = await startBuild(adapter2, config2, taskId, { light }, clientName);
|
|
22777
23185
|
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.task.cycle ?? null });
|
|
22778
23186
|
await tracker.recordStep("branch_ready");
|
|
23187
|
+
writeBuildCheckpoint({
|
|
23188
|
+
cwd: config2.projectRoot,
|
|
23189
|
+
taskId,
|
|
23190
|
+
branch: getCurrentBranch(config2.projectRoot),
|
|
23191
|
+
step: "branch_ready",
|
|
23192
|
+
lastCommitSha: getHeadCommitSha(config2.projectRoot),
|
|
23193
|
+
modifiedFiles: getModifiedFiles(config2.projectRoot)
|
|
23194
|
+
});
|
|
22779
23195
|
tracker.mark("start_decorate_handoff");
|
|
22780
23196
|
const branchInfo = result.branchLines.length > 0 ? result.branchLines.map((l) => `> ${l}`).join("\n") + "\n\n" : "";
|
|
22781
23197
|
const phaseNote = result.phaseChanges.length > 0 ? "\n\n" + result.phaseChanges.map((c) => `Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`).join("\n") : "";
|
|
@@ -22856,7 +23272,7 @@ If this build fixes any of these, pass their UUIDs in \`fixed_issues\` on comple
|
|
|
22856
23272
|
) ?? "";
|
|
22857
23273
|
const gestaltNote = buildGestaltPreBuildDirective(caps) ?? "";
|
|
22858
23274
|
const buildDisciplineSection = buildPapiMetaFramingDirective(caps, buildDisciplineNote) ?? "";
|
|
22859
|
-
return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + gestaltNote + adSection + moduleInstructions + moduleContext + dogfoodSection + openIssuesSection + verificationNote + buildDisciplineSection + chainInstruction + phaseNote + filesToWriteSection);
|
|
23275
|
+
return textResponse(resumeNote + header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + gestaltNote + adSection + moduleInstructions + moduleContext + dogfoodSection + openIssuesSection + verificationNote + buildDisciplineSection + chainInstruction + phaseNote + filesToWriteSection);
|
|
22860
23276
|
} catch (err) {
|
|
22861
23277
|
if (isNoHandoffError(err)) {
|
|
22862
23278
|
const lines = [
|
|
@@ -22994,6 +23410,7 @@ Your report was NOT discarded and the task is NOT yet Done \u2014 re-send with \
|
|
|
22994
23410
|
preview
|
|
22995
23411
|
}, { light }, clientName);
|
|
22996
23412
|
tracker.mark("complete_format");
|
|
23413
|
+
clearBuildCheckpoint({ cwd: config2.projectRoot, taskId });
|
|
22997
23414
|
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.cycleNumber });
|
|
22998
23415
|
await tracker.recordStep("report_written");
|
|
22999
23416
|
if ((result.autoTriagedCount ?? 0) > 0) {
|
|
@@ -23351,12 +23768,460 @@ Re-submit with \`notes: "... Reference: <path>"\` to link one, or ignore if none
|
|
|
23351
23768
|
return textResponse(`${result.message}${overrideNote}`);
|
|
23352
23769
|
}
|
|
23353
23770
|
|
|
23771
|
+
// src/services/import.ts
|
|
23772
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
23773
|
+
var IMPLEMENTED_SOURCES = ["csv", "markdown", "linear"];
|
|
23774
|
+
var MAX_TITLE_LEN = 200;
|
|
23775
|
+
var MAX_NOTES_LEN = 2e3;
|
|
23776
|
+
function sanitiseText(value, maxLen, keepNewlines = false) {
|
|
23777
|
+
let str;
|
|
23778
|
+
if (typeof value === "string") str = value;
|
|
23779
|
+
else if (typeof value === "number" || typeof value === "boolean") str = String(value);
|
|
23780
|
+
else if (value == null) str = "";
|
|
23781
|
+
else {
|
|
23782
|
+
try {
|
|
23783
|
+
str = String(value);
|
|
23784
|
+
} catch {
|
|
23785
|
+
str = "";
|
|
23786
|
+
}
|
|
23787
|
+
}
|
|
23788
|
+
if (keepNewlines) {
|
|
23789
|
+
str = str.replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/g, " ");
|
|
23790
|
+
str = str.replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n");
|
|
23791
|
+
} else {
|
|
23792
|
+
str = str.replace(/[\x00-\x1F\x7F-\x9F]/g, " ");
|
|
23793
|
+
str = str.replace(/\s+/g, " ");
|
|
23794
|
+
}
|
|
23795
|
+
str = str.trim();
|
|
23796
|
+
if (str.length > maxLen) str = str.slice(0, maxLen).trim();
|
|
23797
|
+
return str;
|
|
23798
|
+
}
|
|
23799
|
+
function mapPriorityString(value) {
|
|
23800
|
+
const v = String(value ?? "").trim().toLowerCase();
|
|
23801
|
+
if (/\b(p0|urgent|critical|highest|blocker)\b/.test(v)) return "P0 Critical";
|
|
23802
|
+
if (/\b(p1|high)\b/.test(v)) return "P1 High";
|
|
23803
|
+
if (/\b(p3|low|lowest|minor)\b/.test(v)) return "P3 Low";
|
|
23804
|
+
return "P2 Medium";
|
|
23805
|
+
}
|
|
23806
|
+
function mapPriorityNumber(value) {
|
|
23807
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
23808
|
+
switch (n) {
|
|
23809
|
+
case 1:
|
|
23810
|
+
return "P0 Critical";
|
|
23811
|
+
case 2:
|
|
23812
|
+
return "P1 High";
|
|
23813
|
+
case 4:
|
|
23814
|
+
return "P3 Low";
|
|
23815
|
+
case 3:
|
|
23816
|
+
case 0:
|
|
23817
|
+
default:
|
|
23818
|
+
return "P2 Medium";
|
|
23819
|
+
}
|
|
23820
|
+
}
|
|
23821
|
+
function mapStatusString(value) {
|
|
23822
|
+
const v = String(value ?? "").trim().toLowerCase();
|
|
23823
|
+
if (/\b(done|complete|completed|closed|merged|shipped|resolved)\b/.test(v)) return "Done";
|
|
23824
|
+
if (/\b(cancel|cancelled|canceled|wont.?fix|duplicate|abandoned)\b/.test(v)) return "Cancelled";
|
|
23825
|
+
if (/\b(in.?review|review|qa|verifying)\b/.test(v)) return "In Review";
|
|
23826
|
+
if (/\b(in.?progress|started|doing|active|wip)\b/.test(v)) return "In Progress";
|
|
23827
|
+
if (/\b(blocked|waiting|on.?hold)\b/.test(v)) return "Blocked";
|
|
23828
|
+
return "Backlog";
|
|
23829
|
+
}
|
|
23830
|
+
function estimateToComplexity(value) {
|
|
23831
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
23832
|
+
if (!Number.isFinite(n) || n <= 0) return "Small";
|
|
23833
|
+
if (n <= 1) return "XS";
|
|
23834
|
+
if (n <= 2) return "Small";
|
|
23835
|
+
if (n <= 3) return "Medium";
|
|
23836
|
+
if (n <= 5) return "Large";
|
|
23837
|
+
return "XL";
|
|
23838
|
+
}
|
|
23839
|
+
function mapComplexityField(value) {
|
|
23840
|
+
const v = String(value ?? "").trim();
|
|
23841
|
+
if (v !== "" && /^\d+(\.\d+)?$/.test(v)) return estimateToComplexity(v);
|
|
23842
|
+
return normalizeComplexity(v);
|
|
23843
|
+
}
|
|
23844
|
+
function parseCsv(text) {
|
|
23845
|
+
const rows = [];
|
|
23846
|
+
let field = "";
|
|
23847
|
+
let row = [];
|
|
23848
|
+
let inQuotes = false;
|
|
23849
|
+
for (let i = 0; i < text.length; i++) {
|
|
23850
|
+
const c = text[i];
|
|
23851
|
+
if (inQuotes) {
|
|
23852
|
+
if (c === '"') {
|
|
23853
|
+
if (text[i + 1] === '"') {
|
|
23854
|
+
field += '"';
|
|
23855
|
+
i++;
|
|
23856
|
+
} else {
|
|
23857
|
+
inQuotes = false;
|
|
23858
|
+
}
|
|
23859
|
+
} else {
|
|
23860
|
+
field += c;
|
|
23861
|
+
}
|
|
23862
|
+
continue;
|
|
23863
|
+
}
|
|
23864
|
+
if (c === '"') {
|
|
23865
|
+
inQuotes = true;
|
|
23866
|
+
} else if (c === ",") {
|
|
23867
|
+
row.push(field);
|
|
23868
|
+
field = "";
|
|
23869
|
+
} else if (c === "\n" || c === "\r") {
|
|
23870
|
+
if (c === "\r" && text[i + 1] === "\n") i++;
|
|
23871
|
+
row.push(field);
|
|
23872
|
+
field = "";
|
|
23873
|
+
if (row.some((f) => f.trim() !== "")) rows.push(row);
|
|
23874
|
+
row = [];
|
|
23875
|
+
} else {
|
|
23876
|
+
field += c;
|
|
23877
|
+
}
|
|
23878
|
+
}
|
|
23879
|
+
if (field !== "" || row.length > 0) {
|
|
23880
|
+
row.push(field);
|
|
23881
|
+
if (row.some((f) => f.trim() !== "")) rows.push(row);
|
|
23882
|
+
}
|
|
23883
|
+
return rows;
|
|
23884
|
+
}
|
|
23885
|
+
function findColumn(header, candidates) {
|
|
23886
|
+
const lower = header.map((h) => h.trim().toLowerCase());
|
|
23887
|
+
for (const cand of candidates) {
|
|
23888
|
+
const idx = lower.indexOf(cand);
|
|
23889
|
+
if (idx !== -1) return idx;
|
|
23890
|
+
}
|
|
23891
|
+
return -1;
|
|
23892
|
+
}
|
|
23893
|
+
var csvNormaliser = ({ raw }) => {
|
|
23894
|
+
if (!raw || !raw.trim()) return [];
|
|
23895
|
+
const rows = parseCsv(raw);
|
|
23896
|
+
if (rows.length < 2) return [];
|
|
23897
|
+
const header = rows[0];
|
|
23898
|
+
const titleCol = findColumn(header, ["title", "name", "task", "summary", "subject"]);
|
|
23899
|
+
if (titleCol === -1) {
|
|
23900
|
+
throw new Error(
|
|
23901
|
+
"CSV import: no recognisable title column. Expected a header row with one of: title, name, task, summary, subject."
|
|
23902
|
+
);
|
|
23903
|
+
}
|
|
23904
|
+
const notesCol = findColumn(header, ["notes", "description", "details", "body"]);
|
|
23905
|
+
const statusCol = findColumn(header, ["status", "state"]);
|
|
23906
|
+
const priorityCol = findColumn(header, ["priority", "importance"]);
|
|
23907
|
+
const complexityCol = findColumn(header, ["complexity", "estimate", "size", "points", "effort"]);
|
|
23908
|
+
const idCol = findColumn(header, ["id", "key", "identifier", "ref"]);
|
|
23909
|
+
const out = [];
|
|
23910
|
+
for (let r = 1; r < rows.length; r++) {
|
|
23911
|
+
const cells = rows[r];
|
|
23912
|
+
const title = sanitiseText(cells[titleCol], MAX_TITLE_LEN);
|
|
23913
|
+
if (!title) continue;
|
|
23914
|
+
out.push({
|
|
23915
|
+
title,
|
|
23916
|
+
notes: notesCol === -1 ? "" : sanitiseText(cells[notesCol], MAX_NOTES_LEN, true),
|
|
23917
|
+
status: statusCol === -1 ? "Backlog" : mapStatusString(cells[statusCol]),
|
|
23918
|
+
priority: priorityCol === -1 ? "P2 Medium" : mapPriorityString(cells[priorityCol]),
|
|
23919
|
+
complexity: complexityCol === -1 ? "Small" : mapComplexityField(cells[complexityCol]),
|
|
23920
|
+
sourceId: idCol === -1 ? void 0 : sanitiseText(cells[idCol], 80) || void 0
|
|
23921
|
+
});
|
|
23922
|
+
}
|
|
23923
|
+
return out;
|
|
23924
|
+
};
|
|
23925
|
+
var markdownNormaliser = ({ raw }) => {
|
|
23926
|
+
if (!raw || !raw.trim()) return [];
|
|
23927
|
+
const lines = raw.split(/\r?\n/);
|
|
23928
|
+
const tableHeaderIdx = lines.findIndex(
|
|
23929
|
+
(l, i) => /\|/.test(l) && lines[i + 1] !== void 0 && /^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i + 1]) && /-/.test(lines[i + 1])
|
|
23930
|
+
);
|
|
23931
|
+
if (tableHeaderIdx !== -1) {
|
|
23932
|
+
const splitRow = (l) => l.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim());
|
|
23933
|
+
const header = splitRow(lines[tableHeaderIdx]);
|
|
23934
|
+
const titleCol = findColumn(header, ["title", "name", "task", "summary", "subject"]);
|
|
23935
|
+
if (titleCol === -1) {
|
|
23936
|
+
throw new Error(
|
|
23937
|
+
"Markdown table import: no recognisable title column. Expected a header cell of: title, name, task, summary, subject."
|
|
23938
|
+
);
|
|
23939
|
+
}
|
|
23940
|
+
const notesCol = findColumn(header, ["notes", "description", "details", "body"]);
|
|
23941
|
+
const statusCol = findColumn(header, ["status", "state"]);
|
|
23942
|
+
const priorityCol = findColumn(header, ["priority", "importance"]);
|
|
23943
|
+
const complexityCol = findColumn(header, ["complexity", "estimate", "size", "points", "effort"]);
|
|
23944
|
+
const idCol = findColumn(header, ["id", "key", "identifier", "ref"]);
|
|
23945
|
+
const out2 = [];
|
|
23946
|
+
for (let i = tableHeaderIdx + 2; i < lines.length; i++) {
|
|
23947
|
+
const l = lines[i];
|
|
23948
|
+
if (!/\|/.test(l) || l.trim() === "") break;
|
|
23949
|
+
const cells = splitRow(l);
|
|
23950
|
+
const title = sanitiseText(cells[titleCol], MAX_TITLE_LEN);
|
|
23951
|
+
if (!title) continue;
|
|
23952
|
+
out2.push({
|
|
23953
|
+
title,
|
|
23954
|
+
notes: notesCol === -1 ? "" : sanitiseText(cells[notesCol], MAX_NOTES_LEN, true),
|
|
23955
|
+
status: statusCol === -1 ? "Backlog" : mapStatusString(cells[statusCol]),
|
|
23956
|
+
priority: priorityCol === -1 ? "P2 Medium" : mapPriorityString(cells[priorityCol]),
|
|
23957
|
+
complexity: complexityCol === -1 ? "Small" : mapComplexityField(cells[complexityCol]),
|
|
23958
|
+
sourceId: idCol === -1 ? void 0 : sanitiseText(cells[idCol], 80) || void 0
|
|
23959
|
+
});
|
|
23960
|
+
}
|
|
23961
|
+
return out2;
|
|
23962
|
+
}
|
|
23963
|
+
const out = [];
|
|
23964
|
+
const checklistRe = /^\s*(?:[-*+]|\d+\.)\s+(?:\[( |x|X)\]\s+)?(.*\S)\s*$/;
|
|
23965
|
+
for (const line of lines) {
|
|
23966
|
+
const m = line.match(checklistRe);
|
|
23967
|
+
if (!m) continue;
|
|
23968
|
+
const checked = m[1];
|
|
23969
|
+
const title = sanitiseText(m[2], MAX_TITLE_LEN);
|
|
23970
|
+
if (!title) continue;
|
|
23971
|
+
out.push({
|
|
23972
|
+
title,
|
|
23973
|
+
notes: "",
|
|
23974
|
+
status: checked && checked.toLowerCase() === "x" ? "Done" : "Backlog",
|
|
23975
|
+
priority: "P2 Medium",
|
|
23976
|
+
complexity: "Small"
|
|
23977
|
+
});
|
|
23978
|
+
}
|
|
23979
|
+
return out;
|
|
23980
|
+
};
|
|
23981
|
+
function isRecord(v) {
|
|
23982
|
+
return typeof v === "object" && v !== null;
|
|
23983
|
+
}
|
|
23984
|
+
var linearNormaliser = ({ rows }) => {
|
|
23985
|
+
if (!rows || rows.length === 0) return [];
|
|
23986
|
+
const out = [];
|
|
23987
|
+
for (const row of rows) {
|
|
23988
|
+
if (!isRecord(row)) continue;
|
|
23989
|
+
const issue = row;
|
|
23990
|
+
const title = sanitiseText(issue.title, MAX_TITLE_LEN);
|
|
23991
|
+
if (!title) continue;
|
|
23992
|
+
const stateName = isRecord(issue.state) ? issue.state.name ?? issue.state.type : issue.state;
|
|
23993
|
+
const identifier = sanitiseText(issue.identifier, 80) || sanitiseText(issue.id, 80) || void 0;
|
|
23994
|
+
const url = sanitiseText(issue.url, 300);
|
|
23995
|
+
const description = sanitiseText(issue.description, MAX_NOTES_LEN, true);
|
|
23996
|
+
const notesParts = [];
|
|
23997
|
+
if (description) notesParts.push(description);
|
|
23998
|
+
if (url) notesParts.push(`Reference: ${url}`);
|
|
23999
|
+
out.push({
|
|
24000
|
+
title,
|
|
24001
|
+
notes: sanitiseText(notesParts.join("\n"), MAX_NOTES_LEN, true),
|
|
24002
|
+
status: mapStatusString(stateName),
|
|
24003
|
+
priority: mapPriorityNumber(issue.priority),
|
|
24004
|
+
complexity: issue.estimate == null ? "Small" : estimateToComplexity(issue.estimate),
|
|
24005
|
+
sourceId: identifier
|
|
24006
|
+
});
|
|
24007
|
+
}
|
|
24008
|
+
return out;
|
|
24009
|
+
};
|
|
24010
|
+
function notImplemented(source) {
|
|
24011
|
+
return () => {
|
|
24012
|
+
throw new Error(
|
|
24013
|
+
`Import source "${source}" is not implemented yet. Shipped sources: ${IMPLEMENTED_SOURCES.join(", ")}. To add "${source}", implement a normaliser in packages/server/src/services/import.ts (see the "EXTENDING THE IMPORTER" header comment) \u2014 the host reads "${source}" via its own MCP tool and passes rows here; PAPI never connects to it directly.`
|
|
24014
|
+
);
|
|
24015
|
+
};
|
|
24016
|
+
}
|
|
24017
|
+
var SOURCE_NORMALISERS = {
|
|
24018
|
+
csv: csvNormaliser,
|
|
24019
|
+
markdown: markdownNormaliser,
|
|
24020
|
+
linear: linearNormaliser,
|
|
24021
|
+
// Extension points — build these by replacing notImplemented() with a real normaliser.
|
|
24022
|
+
trello: notImplemented("trello"),
|
|
24023
|
+
todoist: notImplemented("todoist"),
|
|
24024
|
+
notion: notImplemented("notion")
|
|
24025
|
+
};
|
|
24026
|
+
function importMarker(source, sourceId) {
|
|
24027
|
+
return `[papi-import:${source}:${sourceId}]`;
|
|
24028
|
+
}
|
|
24029
|
+
function dedupTitleKey(title) {
|
|
24030
|
+
return title.toLowerCase().replace(/[^a-z0-9\s]/g, "").replace(/\s+/g, " ").trim();
|
|
24031
|
+
}
|
|
24032
|
+
async function importBacklog(adapter2, input) {
|
|
24033
|
+
const normaliser = SOURCE_NORMALISERS[input.source];
|
|
24034
|
+
if (!normaliser) {
|
|
24035
|
+
throw new Error(
|
|
24036
|
+
`Unknown import source "${input.source}". Valid sources: ${Object.keys(SOURCE_NORMALISERS).join(", ")}.`
|
|
24037
|
+
);
|
|
24038
|
+
}
|
|
24039
|
+
const normalised = normaliser({ raw: input.raw, rows: input.rows });
|
|
24040
|
+
const existing = await adapter2.queryBoard({});
|
|
24041
|
+
const existingTitleKeys = /* @__PURE__ */ new Set();
|
|
24042
|
+
const existingMarkers = /* @__PURE__ */ new Set();
|
|
24043
|
+
for (const t of existing) {
|
|
24044
|
+
existingTitleKeys.add(dedupTitleKey(t.title));
|
|
24045
|
+
const notes = t.notes ?? "";
|
|
24046
|
+
const markerMatches = notes.match(/\[papi-import:[^\]]+\]/g);
|
|
24047
|
+
if (markerMatches) for (const mk of markerMatches) existingMarkers.add(mk);
|
|
24048
|
+
}
|
|
24049
|
+
const health = await adapter2.getCycleHealth();
|
|
24050
|
+
warnIfEmpty("getCycleHealth (import)", health);
|
|
24051
|
+
const createdCycle = health.totalCycles;
|
|
24052
|
+
const targetModule = input.module && input.module.trim() || "Core";
|
|
24053
|
+
const createdTasks = [];
|
|
24054
|
+
const skippedTitles = [];
|
|
24055
|
+
const seenThisBatch = /* @__PURE__ */ new Set();
|
|
24056
|
+
for (const task of normalised) {
|
|
24057
|
+
const titleKey = dedupTitleKey(task.title);
|
|
24058
|
+
const marker = task.sourceId ? importMarker(input.source, task.sourceId) : null;
|
|
24059
|
+
const isDup = marker !== null && existingMarkers.has(marker) || existingTitleKeys.has(titleKey) || seenThisBatch.has(marker ?? titleKey);
|
|
24060
|
+
if (isDup) {
|
|
24061
|
+
skippedTitles.push(task.title);
|
|
24062
|
+
continue;
|
|
24063
|
+
}
|
|
24064
|
+
seenThisBatch.add(marker ?? titleKey);
|
|
24065
|
+
if (input.dryRun) {
|
|
24066
|
+
createdTasks.push({ id: "(dry-run)", title: task.title });
|
|
24067
|
+
continue;
|
|
24068
|
+
}
|
|
24069
|
+
const notes = marker ? `${task.notes ? `${task.notes}
|
|
24070
|
+
|
|
24071
|
+
` : ""}${marker}`.trim() : task.notes;
|
|
24072
|
+
const created = await adapter2.createTask({
|
|
24073
|
+
uuid: randomUUID13(),
|
|
24074
|
+
displayId: "",
|
|
24075
|
+
title: task.title,
|
|
24076
|
+
status: task.status,
|
|
24077
|
+
priority: task.priority,
|
|
24078
|
+
complexity: task.complexity,
|
|
24079
|
+
module: targetModule,
|
|
24080
|
+
epic: "Platform",
|
|
24081
|
+
phase: "Unscoped",
|
|
24082
|
+
owner: "TBD",
|
|
24083
|
+
reviewed: false,
|
|
24084
|
+
createdCycle,
|
|
24085
|
+
notes,
|
|
24086
|
+
taskType: "task",
|
|
24087
|
+
maturity: "raw",
|
|
24088
|
+
source: `import:${input.source}`
|
|
24089
|
+
});
|
|
24090
|
+
createdTasks.push({ id: created.id, title: created.title });
|
|
24091
|
+
existingTitleKeys.add(titleKey);
|
|
24092
|
+
if (marker) existingMarkers.add(marker);
|
|
24093
|
+
}
|
|
24094
|
+
return {
|
|
24095
|
+
source: input.source,
|
|
24096
|
+
normalised: normalised.length,
|
|
24097
|
+
imported: input.dryRun ? 0 : createdTasks.length,
|
|
24098
|
+
skipped: skippedTitles.length,
|
|
24099
|
+
skippedTitles,
|
|
24100
|
+
createdTasks,
|
|
24101
|
+
dryRun: input.dryRun === true
|
|
24102
|
+
};
|
|
24103
|
+
}
|
|
24104
|
+
|
|
24105
|
+
// src/tools/import.ts
|
|
24106
|
+
var ALL_SOURCES = ["csv", "markdown", "linear", "trello", "todoist", "notion"];
|
|
24107
|
+
var backlogImportTool = {
|
|
24108
|
+
name: "backlog_import",
|
|
24109
|
+
annotations: { title: "Import Backlog", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
24110
|
+
description: `Import an existing backlog from another tool into PAPI as Backlog tasks, removing the "start from an empty board" wall. PAPI never connects to the source tool \u2014 YOU (the host) read the export and pass it here. Flat-file sources (csv, markdown) take a \`raw\` string; structured sources (linear) take a \`rows\` array you read via that tool's MCP. Import is one-way and idempotent: re-running the same import does NOT create duplicates. Shipped sources: ${IMPLEMENTED_SOURCES.join(", ")}. trello/todoist/notion are reserved but not implemented yet. Does not call the Anthropic API.`,
|
|
24111
|
+
inputSchema: {
|
|
24112
|
+
type: "object",
|
|
24113
|
+
properties: {
|
|
24114
|
+
source: {
|
|
24115
|
+
type: "string",
|
|
24116
|
+
enum: ALL_SOURCES,
|
|
24117
|
+
description: "The tool the backlog came from. csv/markdown read a `raw` string; linear reads a `rows` array of issues. trello/todoist/notion are reserved extension points (not implemented yet)."
|
|
24118
|
+
},
|
|
24119
|
+
raw: {
|
|
24120
|
+
type: "string",
|
|
24121
|
+
description: "Raw export text for flat-file sources. CSV: a header row + data rows (title/name column required; optional notes/description, status/state, priority, complexity/estimate, id). Markdown: a checklist (`- [ ] Task`) or a GitHub-style table."
|
|
24122
|
+
},
|
|
24123
|
+
rows: {
|
|
24124
|
+
type: "array",
|
|
24125
|
+
description: "Pre-read rows for structured sources. For linear: an array of issues you read via the Linear MCP, each like { identifier, title, description, state: { name }, priority (0-4), estimate, url }.",
|
|
24126
|
+
items: { type: "object" }
|
|
24127
|
+
},
|
|
24128
|
+
module: {
|
|
24129
|
+
type: "string",
|
|
24130
|
+
description: 'Module to file imported tasks under (default "Core").'
|
|
24131
|
+
},
|
|
24132
|
+
dry_run: {
|
|
24133
|
+
type: "boolean",
|
|
24134
|
+
description: "When true, normalise and dedup-check without writing anything. Use this to preview what would be imported. Default: false."
|
|
24135
|
+
},
|
|
24136
|
+
project: {
|
|
24137
|
+
type: "string",
|
|
24138
|
+
description: "Project id (UUID) or slug to import into, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise."
|
|
24139
|
+
}
|
|
24140
|
+
},
|
|
24141
|
+
required: ["source"]
|
|
24142
|
+
}
|
|
24143
|
+
};
|
|
24144
|
+
function diagnostic(lastStep, error, hint) {
|
|
24145
|
+
return errorResponse(JSON.stringify({ tool: "backlog_import", lastStep, error, hint }, null, 2));
|
|
24146
|
+
}
|
|
24147
|
+
function formatResult(result, overrideNote) {
|
|
24148
|
+
const lines = [];
|
|
24149
|
+
const verb = result.dryRun ? "Would import" : "Imported";
|
|
24150
|
+
lines.push(
|
|
24151
|
+
`${verb} ${result.dryRun ? result.normalised - result.skipped : result.imported} task(s) from ${result.source}${overrideNote}.`
|
|
24152
|
+
);
|
|
24153
|
+
lines.push(` normalised: ${result.normalised} \xB7 ${result.dryRun ? "would-write" : "written"}: ${result.dryRun ? result.normalised - result.skipped : result.imported} \xB7 skipped (duplicates): ${result.skipped}`);
|
|
24154
|
+
if (result.createdTasks.length > 0 && !result.dryRun) {
|
|
24155
|
+
const preview = result.createdTasks.slice(0, 10).map((t) => ` - ${t.id}: ${t.title}`);
|
|
24156
|
+
lines.push("Created:");
|
|
24157
|
+
lines.push(...preview);
|
|
24158
|
+
if (result.createdTasks.length > 10) lines.push(` \u2026and ${result.createdTasks.length - 10} more`);
|
|
24159
|
+
}
|
|
24160
|
+
if (result.skipped > 0) {
|
|
24161
|
+
const dupPreview = result.skippedTitles.slice(0, 5).map((t) => ` - ${t}`);
|
|
24162
|
+
lines.push(`Skipped ${result.skipped} duplicate(s) (already on the board or repeated in this import):`);
|
|
24163
|
+
lines.push(...dupPreview);
|
|
24164
|
+
if (result.skippedTitles.length > 5) lines.push(` \u2026and ${result.skippedTitles.length - 5} more`);
|
|
24165
|
+
}
|
|
24166
|
+
if (!result.dryRun && result.imported > 0) {
|
|
24167
|
+
lines.push("\nRun `plan` to triage and scope the imported backlog into a cycle.");
|
|
24168
|
+
}
|
|
24169
|
+
return lines.join("\n");
|
|
24170
|
+
}
|
|
24171
|
+
async function handleBacklogImport(adapter2, args) {
|
|
24172
|
+
const source = args.source?.trim();
|
|
24173
|
+
if (!source) {
|
|
24174
|
+
return errorResponse(`source is required \u2014 one of: ${ALL_SOURCES.join(", ")}.`);
|
|
24175
|
+
}
|
|
24176
|
+
if (!ALL_SOURCES.includes(source)) {
|
|
24177
|
+
return errorResponse(`Unknown source "${source}". Valid: ${ALL_SOURCES.join(", ")}.`);
|
|
24178
|
+
}
|
|
24179
|
+
const raw = args.raw;
|
|
24180
|
+
const rawRows = args.rows;
|
|
24181
|
+
const rows = Array.isArray(rawRows) ? rawRows : void 0;
|
|
24182
|
+
const isFlatFile = source === "csv" || source === "markdown";
|
|
24183
|
+
if (isFlatFile && (!raw || !raw.trim())) {
|
|
24184
|
+
return errorResponse(`Source "${source}" needs a \`raw\` string (the exported ${source} text).`);
|
|
24185
|
+
}
|
|
24186
|
+
if (!isFlatFile && IMPLEMENTED_SOURCES.includes(source) && (!rows || rows.length === 0)) {
|
|
24187
|
+
return errorResponse(`Source "${source}" needs a \`rows\` array of issues you read via that tool's MCP.`);
|
|
24188
|
+
}
|
|
24189
|
+
let target = adapter2;
|
|
24190
|
+
let overrideNote = "";
|
|
24191
|
+
try {
|
|
24192
|
+
({ adapter: target, overrideNote } = await resolvePerCallProjectAdapter(adapter2, args));
|
|
24193
|
+
} catch (err) {
|
|
24194
|
+
if (err instanceof ProjectResolutionError) return errorResponse(err.message);
|
|
24195
|
+
throw err;
|
|
24196
|
+
}
|
|
24197
|
+
try {
|
|
24198
|
+
const result = await importBacklog(target, {
|
|
24199
|
+
source,
|
|
24200
|
+
raw,
|
|
24201
|
+
rows,
|
|
24202
|
+
module: args.module,
|
|
24203
|
+
dryRun: args.dry_run === true
|
|
24204
|
+
});
|
|
24205
|
+
return textResponse(formatResult(result, overrideNote));
|
|
24206
|
+
} catch (err) {
|
|
24207
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24208
|
+
if (/not implemented yet|no recognisable title column/i.test(message)) {
|
|
24209
|
+
return errorResponse(message);
|
|
24210
|
+
}
|
|
24211
|
+
return diagnostic(
|
|
24212
|
+
"importBacklog",
|
|
24213
|
+
message,
|
|
24214
|
+
"Check the export shape matches the source (csv/markdown need `raw`; linear needs `rows`). Re-run with dry_run:true to preview."
|
|
24215
|
+
);
|
|
24216
|
+
}
|
|
24217
|
+
}
|
|
24218
|
+
|
|
23354
24219
|
// src/tools/bug.ts
|
|
23355
24220
|
import os from "os";
|
|
23356
24221
|
init_git();
|
|
23357
24222
|
|
|
23358
24223
|
// src/services/bug.ts
|
|
23359
|
-
import { randomUUID as
|
|
24224
|
+
import { randomUUID as randomUUID14 } from "crypto";
|
|
23360
24225
|
function resolveCurrentPhase2(phases) {
|
|
23361
24226
|
if (phases.length === 0) return "Unscoped";
|
|
23362
24227
|
const inProgress = phases.find((p) => p.status === "In Progress");
|
|
@@ -23382,7 +24247,7 @@ async function captureBug(adapter2, input) {
|
|
|
23382
24247
|
warnIfEmpty("getCycleHealth (bug)", health);
|
|
23383
24248
|
const phase = input.phase || resolveCurrentPhase2(phases);
|
|
23384
24249
|
return adapter2.createTask({
|
|
23385
|
-
uuid:
|
|
24250
|
+
uuid: randomUUID14(),
|
|
23386
24251
|
displayId: "",
|
|
23387
24252
|
title: input.text,
|
|
23388
24253
|
status: "Backlog",
|
|
@@ -23636,7 +24501,7 @@ ${lines.join("\n")}`
|
|
|
23636
24501
|
init_git();
|
|
23637
24502
|
|
|
23638
24503
|
// src/services/ad-hoc.ts
|
|
23639
|
-
import { randomUUID as
|
|
24504
|
+
import { randomUUID as randomUUID15 } from "crypto";
|
|
23640
24505
|
function resolveAdHocCycle(cycle, latest, latestComplete) {
|
|
23641
24506
|
if (cycle === void 0) return null;
|
|
23642
24507
|
if (typeof cycle === "number") return cycle;
|
|
@@ -23676,7 +24541,7 @@ async function recordAdHoc(adapter2, input) {
|
|
|
23676
24541
|
} else {
|
|
23677
24542
|
const phase = resolveCurrentPhase(phases);
|
|
23678
24543
|
task = await adapter2.createTask({
|
|
23679
|
-
uuid:
|
|
24544
|
+
uuid: randomUUID15(),
|
|
23680
24545
|
displayId: "",
|
|
23681
24546
|
title: input.title,
|
|
23682
24547
|
status: landInReview ? "In Review" : "Done",
|
|
@@ -23695,7 +24560,7 @@ async function recordAdHoc(adapter2, input) {
|
|
|
23695
24560
|
});
|
|
23696
24561
|
}
|
|
23697
24562
|
const report = {
|
|
23698
|
-
uuid:
|
|
24563
|
+
uuid: randomUUID15(),
|
|
23699
24564
|
createdAt: now.toISOString(),
|
|
23700
24565
|
taskId: task.id,
|
|
23701
24566
|
taskName: task.title ?? task.displayId ?? "untitled",
|
|
@@ -23849,13 +24714,38 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
23849
24714
|
const promoNote = result.task.cycle != null ? ` Promoted into Cycle ${result.task.cycle} as injected work${result.task.status === "In Review" ? " (In Review \u2014 will release with the cycle)" : ""}.` : "";
|
|
23850
24715
|
if (holdArg) {
|
|
23851
24716
|
const branch = `feat/${result.task.id}`;
|
|
24717
|
+
let collisionBlock = "";
|
|
24718
|
+
try {
|
|
24719
|
+
const board = await adapter2.queryBoard({ status: ["In Progress"] });
|
|
24720
|
+
const otherInProgress = board.filter((t) => t.id !== result.task.id && t.displayId !== result.task.id).map((t) => ({ taskId: t.displayId || t.id, branch: (t.branchName ?? "").trim() })).filter((t) => t.branch.length > 0);
|
|
24721
|
+
const collision = detectWorktreeCollision({
|
|
24722
|
+
taskId: result.task.id,
|
|
24723
|
+
targetBranch: branch,
|
|
24724
|
+
inProgress: otherInProgress,
|
|
24725
|
+
// Held work never runs git server-side — treat as suggest-only.
|
|
24726
|
+
autoWorktree: false
|
|
24727
|
+
});
|
|
24728
|
+
if (collision) {
|
|
24729
|
+
collisionBlock = `
|
|
24730
|
+
> ${collision.warning}
|
|
24731
|
+
> Instead of \`git switch -c\`, isolate into a worktree:
|
|
24732
|
+
> \`\`\`
|
|
24733
|
+
> git worktree add -b ${branch} ${collision.worktreePath}
|
|
24734
|
+
> cd ${collision.worktreePath} && npm run worktree:setup
|
|
24735
|
+
> \`\`\`
|
|
24736
|
+
> ${collision.setupHint}
|
|
24737
|
+
|
|
24738
|
+
`;
|
|
24739
|
+
}
|
|
24740
|
+
} catch {
|
|
24741
|
+
}
|
|
23852
24742
|
return textResponse(
|
|
23853
24743
|
`**${result.task.id}:** "${result.task.title}" held for review (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule}).${truncateWarning}${promoNote} Build report attached.
|
|
23854
24744
|
|
|
23855
24745
|
## Held for the next cycle \u2014 branch + commit, do NOT merge
|
|
23856
24746
|
The task is recorded **In Review** and pinned to **Cycle ${result.task.cycle}**, so the planner won't re-plan it and it rides that cycle's review \u2192 release bundled with planned work.
|
|
23857
24747
|
|
|
23858
|
-
1. Create a branch and commit your code there (never \`main\`):
|
|
24748
|
+
` + collisionBlock + `1. Create a branch and commit your code there (never \`main\`):
|
|
23859
24749
|
\`\`\`
|
|
23860
24750
|
git switch -c ${branch}
|
|
23861
24751
|
git add -- <your changed files>
|
|
@@ -23877,13 +24767,13 @@ _To correct: board_edit ${result.task.id} with updated fields._`
|
|
|
23877
24767
|
init_git();
|
|
23878
24768
|
|
|
23879
24769
|
// src/services/reconcile.ts
|
|
23880
|
-
import { readFileSync as
|
|
23881
|
-
import { join as
|
|
24770
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
24771
|
+
import { join as join14 } from "path";
|
|
23882
24772
|
function loadDocsIndex(projectRoot) {
|
|
23883
24773
|
if (!hasLocalWorkspace()) return "";
|
|
23884
24774
|
try {
|
|
23885
|
-
const indexPath =
|
|
23886
|
-
const raw =
|
|
24775
|
+
const indexPath = join14(projectRoot, "docs", "INDEX.md");
|
|
24776
|
+
const raw = readFileSync9(indexPath, "utf8");
|
|
23887
24777
|
const rows = raw.split("\n").filter((l) => l.startsWith("| ["));
|
|
23888
24778
|
if (rows.length === 0) return "";
|
|
23889
24779
|
const entries = rows.map((row) => {
|
|
@@ -24458,12 +25348,12 @@ Produce your analysis and structured output above. Present Part 1 to the user an
|
|
|
24458
25348
|
}
|
|
24459
25349
|
|
|
24460
25350
|
// src/tools/review.ts
|
|
24461
|
-
import { existsSync as
|
|
24462
|
-
import { join as
|
|
25351
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
25352
|
+
import { join as join15 } from "path";
|
|
24463
25353
|
init_git();
|
|
24464
25354
|
|
|
24465
25355
|
// src/services/review.ts
|
|
24466
|
-
import { randomUUID as
|
|
25356
|
+
import { randomUUID as randomUUID16 } from "crypto";
|
|
24467
25357
|
function isValidVerdict(stage, verdict) {
|
|
24468
25358
|
if (stage === "handoff-review") {
|
|
24469
25359
|
return verdict === "approve" || verdict === "request-changes" || verdict === "reject";
|
|
@@ -24520,7 +25410,7 @@ async function submitReview(adapter2, input) {
|
|
|
24520
25410
|
}
|
|
24521
25411
|
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
24522
25412
|
const review = {
|
|
24523
|
-
uuid:
|
|
25413
|
+
uuid: randomUUID16(),
|
|
24524
25414
|
taskId: input.taskId,
|
|
24525
25415
|
stage: input.stage,
|
|
24526
25416
|
reviewer: input.reviewer,
|
|
@@ -24688,11 +25578,11 @@ ${task.buildReport}` : "### Build Report\n(none recorded)";
|
|
|
24688
25578
|
${diff}
|
|
24689
25579
|
\`\`\`` : "### Branch diff vs base\n(no diff resolved \u2014 not a git repo, no base ref, or no committed changes)";
|
|
24690
25580
|
let projectContext = "";
|
|
24691
|
-
const ctxPath =
|
|
24692
|
-
if (
|
|
25581
|
+
const ctxPath = join15(config2.projectRoot, ".agents", "papi-context.md");
|
|
25582
|
+
if (existsSync10(ctxPath)) {
|
|
24693
25583
|
try {
|
|
24694
25584
|
projectContext = `### Project context (.agents/papi-context.md)
|
|
24695
|
-
${
|
|
25585
|
+
${readFileSync10(ctxPath, "utf-8")}
|
|
24696
25586
|
|
|
24697
25587
|
`;
|
|
24698
25588
|
} catch {
|
|
@@ -24735,6 +25625,7 @@ var reviewSubmitTool = {
|
|
|
24735
25625
|
description: "Record a review verdict on a completed build (build-acceptance) or task plan (handoff-review). ALWAYS ask the human for their verdict before calling \u2014 never auto-submit without human input. Accept moves the task to Done, request-changes sends it back for rework, reject discards the build. Updates task status based on the verdict. On handoff-review with suggested changes, returns a prompt to revise the BUILD HANDOFF.\n\nDO NOT use this tool as a substitute for review_list. If you need to see what is pending review, call review_list first. If review_list is unavailable in your tool set, STOP and tell the human their MCP integration is incomplete rather than guessing at the next pending task. (SUP-2026-010.)",
|
|
24736
25626
|
annotations: { title: "Submit Review", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
24737
25627
|
inputSchema: {
|
|
25628
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
24738
25629
|
type: "object",
|
|
24739
25630
|
properties: {
|
|
24740
25631
|
task_id: {
|
|
@@ -24801,7 +25692,21 @@ var reviewSubmitTool = {
|
|
|
24801
25692
|
required: ["verdict", "summary", "findings"]
|
|
24802
25693
|
}
|
|
24803
25694
|
},
|
|
24804
|
-
required: ["task_id", "stage", "verdict", "comments"]
|
|
25695
|
+
required: ["task_id", "stage", "verdict", "comments"],
|
|
25696
|
+
// task-2802: mirror isValidVerdict — the legal verdict set is stage-dependent.
|
|
25697
|
+
// handoff-review takes approve/request-changes/reject; build-acceptance takes
|
|
25698
|
+
// accept/request-changes/reject. The flat enum above lists the union; these
|
|
25699
|
+
// conditionals narrow it per stage so the agent can't pair an impossible verdict.
|
|
25700
|
+
allOf: [
|
|
25701
|
+
{
|
|
25702
|
+
if: { properties: { stage: { const: "handoff-review" } }, required: ["stage"] },
|
|
25703
|
+
then: { properties: { verdict: { enum: ["approve", "request-changes", "reject"] } } }
|
|
25704
|
+
},
|
|
25705
|
+
{
|
|
25706
|
+
if: { properties: { stage: { const: "build-acceptance" } }, required: ["stage"] },
|
|
25707
|
+
then: { properties: { verdict: { enum: ["accept", "request-changes", "reject"] } } }
|
|
25708
|
+
}
|
|
25709
|
+
]
|
|
24805
25710
|
}
|
|
24806
25711
|
};
|
|
24807
25712
|
function formatReviewList(pendingBuilds) {
|
|
@@ -24852,8 +25757,8 @@ function mergeAfterAccept(config2, taskId) {
|
|
|
24852
25757
|
};
|
|
24853
25758
|
}
|
|
24854
25759
|
const details = [];
|
|
24855
|
-
const papiDir =
|
|
24856
|
-
if (
|
|
25760
|
+
const papiDir = join15(config2.projectRoot, ".papi");
|
|
25761
|
+
if (existsSync10(papiDir)) {
|
|
24857
25762
|
try {
|
|
24858
25763
|
const commitResult = stageDirAndCommit(
|
|
24859
25764
|
config2.projectRoot,
|
|
@@ -25420,7 +26325,7 @@ async function handleReviewClaim(adapter2, config2, args) {
|
|
|
25420
26325
|
}
|
|
25421
26326
|
|
|
25422
26327
|
// src/tools/init.ts
|
|
25423
|
-
import { randomUUID as
|
|
26328
|
+
import { randomUUID as randomUUID17 } from "crypto";
|
|
25424
26329
|
import { access as access3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
25425
26330
|
import path5 from "path";
|
|
25426
26331
|
var initTool = {
|
|
@@ -25853,7 +26758,7 @@ ${writeNote}
|
|
|
25853
26758
|
);
|
|
25854
26759
|
}
|
|
25855
26760
|
if (isDatabaseUser) {
|
|
25856
|
-
const projectId =
|
|
26761
|
+
const projectId = randomUUID17();
|
|
25857
26762
|
const envVars = {
|
|
25858
26763
|
PAPI_PROJECT_DIR: projectRoot,
|
|
25859
26764
|
PAPI_ADAPTER: "pg",
|
|
@@ -26435,7 +27340,7 @@ function formatDeferredGateSection(sweep) {
|
|
|
26435
27340
|
|
|
26436
27341
|
// src/tools/agent-list.ts
|
|
26437
27342
|
import { readdir as readdir2, readFile as readFile7 } from "fs/promises";
|
|
26438
|
-
import { join as
|
|
27343
|
+
import { join as join16 } from "path";
|
|
26439
27344
|
var NO_AGENTS_HINT = "No project sub-agents found in `.claude/agents/`. Add a `*.md` file with `name` + `description` frontmatter \u2014 see the 1926-Census marketing sub-agent for a reference implementation.";
|
|
26440
27345
|
function parseAgentFrontmatter(content) {
|
|
26441
27346
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
@@ -26447,7 +27352,7 @@ function parseAgentFrontmatter(content) {
|
|
|
26447
27352
|
return { name: nameMatch?.[1].trim(), description };
|
|
26448
27353
|
}
|
|
26449
27354
|
async function listAgents(projectRoot) {
|
|
26450
|
-
const agentsDir =
|
|
27355
|
+
const agentsDir = join16(projectRoot, ".claude", "agents");
|
|
26451
27356
|
let files;
|
|
26452
27357
|
try {
|
|
26453
27358
|
files = await readdir2(agentsDir);
|
|
@@ -26458,7 +27363,7 @@ async function listAgents(projectRoot) {
|
|
|
26458
27363
|
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
26459
27364
|
let content;
|
|
26460
27365
|
try {
|
|
26461
|
-
content = await readFile7(
|
|
27366
|
+
content = await readFile7(join16(agentsDir, file), "utf-8");
|
|
26462
27367
|
} catch {
|
|
26463
27368
|
continue;
|
|
26464
27369
|
}
|
|
@@ -26466,7 +27371,7 @@ async function listAgents(projectRoot) {
|
|
|
26466
27371
|
agents.push({
|
|
26467
27372
|
name: meta?.name ?? file.replace(/\.md$/, ""),
|
|
26468
27373
|
description: meta?.description ?? "",
|
|
26469
|
-
path:
|
|
27374
|
+
path: join16(".claude", "agents", file)
|
|
26470
27375
|
});
|
|
26471
27376
|
}
|
|
26472
27377
|
agents.sort((a, b2) => a.name.localeCompare(b2.name));
|
|
@@ -26607,8 +27512,8 @@ async function verifyProject(adapter2) {
|
|
|
26607
27512
|
// src/tools/orient.ts
|
|
26608
27513
|
import { execFile as execFile2 } from "child_process";
|
|
26609
27514
|
import { promisify as promisify2 } from "util";
|
|
26610
|
-
import { readFileSync as
|
|
26611
|
-
import { join as
|
|
27515
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync5, existsSync as existsSync11 } from "fs";
|
|
27516
|
+
import { join as join17 } from "path";
|
|
26612
27517
|
var execFileAsync2 = promisify2(execFile2);
|
|
26613
27518
|
var GIT_DEPENDENT_ENVS = /* @__PURE__ */ new Set(["hosted", "api"]);
|
|
26614
27519
|
var VALID_ENVS = /* @__PURE__ */ new Set(["local-cli", "hosted", "api", "unknown"]);
|
|
@@ -26656,6 +27561,7 @@ var orientTool = {
|
|
|
26656
27561
|
description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files. PAPI detects build capability from the connecting harness (clientInfo); pass `environment` only to override that detection for git-dependent recommendations (build_execute, release, review_submit).",
|
|
26657
27562
|
annotations: { title: "Orient Session", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
26658
27563
|
inputSchema: {
|
|
27564
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
26659
27565
|
type: "object",
|
|
26660
27566
|
properties: {
|
|
26661
27567
|
environment: {
|
|
@@ -26967,8 +27873,8 @@ async function getLatestGitTag(projectRoot) {
|
|
|
26967
27873
|
}
|
|
26968
27874
|
async function checkNpmVersionDrift() {
|
|
26969
27875
|
try {
|
|
26970
|
-
const pkgPath =
|
|
26971
|
-
const pkg = JSON.parse(
|
|
27876
|
+
const pkgPath = join17(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
27877
|
+
const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
26972
27878
|
const localVersion = pkg.version;
|
|
26973
27879
|
const packageName = pkg.name;
|
|
26974
27880
|
const { stdout } = await execFileAsync2("npm", ["view", packageName, "version"], {
|
|
@@ -27642,9 +28548,9 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
|
|
|
27642
28548
|
|
|
27643
28549
|
\u{1F4DD} **CLAUDE.md enriched** \u2014 added ${tierNames2.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
|
|
27644
28550
|
}
|
|
27645
|
-
const claudeMdPath =
|
|
27646
|
-
if (!
|
|
27647
|
-
const content =
|
|
28551
|
+
const claudeMdPath = join17(projectRoot, "CLAUDE.md");
|
|
28552
|
+
if (!existsSync11(claudeMdPath)) return "";
|
|
28553
|
+
const content = readFileSync11(claudeMdPath, "utf-8");
|
|
27648
28554
|
const additions = [];
|
|
27649
28555
|
if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
|
|
27650
28556
|
additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_1));
|
|
@@ -27653,7 +28559,7 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
|
|
|
27653
28559
|
additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_2));
|
|
27654
28560
|
}
|
|
27655
28561
|
if (additions.length === 0) return "";
|
|
27656
|
-
|
|
28562
|
+
writeFileSync5(claudeMdPath, content + additions.join(""), "utf-8");
|
|
27657
28563
|
const tierNames = [];
|
|
27658
28564
|
if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1))) tierNames.push("Established (batch building, strategy reviews, AD lifecycle)");
|
|
27659
28565
|
if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T2))) tierNames.push("Mature (idea pipeline, doc registry, advanced patterns)");
|
|
@@ -28376,11 +29282,11 @@ ${result.userMessage}
|
|
|
28376
29282
|
}
|
|
28377
29283
|
|
|
28378
29284
|
// src/tools/scope-brief.ts
|
|
28379
|
-
import { readFileSync as
|
|
29285
|
+
import { readFileSync as readFileSync12, statSync as statSync7 } from "fs";
|
|
28380
29286
|
|
|
28381
29287
|
// src/services/scope-brief.ts
|
|
28382
|
-
import { writeFileSync as
|
|
28383
|
-
import { join as
|
|
29288
|
+
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
29289
|
+
import { join as join18, dirname as dirname4 } from "path";
|
|
28384
29290
|
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.
|
|
28385
29291
|
|
|
28386
29292
|
A scope document must:
|
|
@@ -28437,14 +29343,14 @@ async function applyScopeBrief(adapter2, input) {
|
|
|
28437
29343
|
}
|
|
28438
29344
|
const slug = input.taskId.replace(/[^a-z0-9-]/g, "-").toLowerCase();
|
|
28439
29345
|
const relPath = `docs/scopes/${slug}.md`;
|
|
28440
|
-
const absPath =
|
|
29346
|
+
const absPath = join18(input.projectRoot, relPath);
|
|
28441
29347
|
const docBody = addFrontmatter(docContent, task, input.cycleNumber);
|
|
28442
29348
|
const collector = new FileWriteCollector();
|
|
28443
29349
|
if (input.adapterType === "proxy") {
|
|
28444
29350
|
collector.add({ path: relPath, content: docBody, mode: "overwrite" });
|
|
28445
29351
|
} else {
|
|
28446
|
-
|
|
28447
|
-
|
|
29352
|
+
mkdirSync4(dirname4(absPath), { recursive: true });
|
|
29353
|
+
writeFileSync6(absPath, docBody, "utf-8");
|
|
28448
29354
|
}
|
|
28449
29355
|
const taskCount = countSubTasks(docContent);
|
|
28450
29356
|
const summary = buildSummary(task, taskCount);
|
|
@@ -28615,7 +29521,7 @@ function readLlmResponse(args) {
|
|
|
28615
29521
|
if (statSync7(filePath).size > MAX_RESPONSE_FILE_BYTES) {
|
|
28616
29522
|
throw new Error(`llm_response_file exceeds ${MAX_RESPONSE_FILE_BYTES} bytes`);
|
|
28617
29523
|
}
|
|
28618
|
-
const body =
|
|
29524
|
+
const body = readFileSync12(filePath, "utf-8");
|
|
28619
29525
|
return body.trim() ? body : null;
|
|
28620
29526
|
} catch (err) {
|
|
28621
29527
|
throw new Error(`could not read llm_response_file: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -29324,19 +30230,19 @@ Its build reports, comments, and history moved with it; the cycle assignment was
|
|
|
29324
30230
|
|
|
29325
30231
|
// src/services/harness-inventory.ts
|
|
29326
30232
|
import { readdir as readdir3, readFile as readFile8, stat as stat3 } from "fs/promises";
|
|
29327
|
-
import { join as
|
|
29328
|
-
import { createHash as
|
|
30233
|
+
import { join as join19 } from "path";
|
|
30234
|
+
import { createHash as createHash5 } from "crypto";
|
|
29329
30235
|
var RECOMMENDED_HOOKS = ["stop-release-check.sh", "claude-md-size-guard.sh"];
|
|
29330
30236
|
async function computeFingerprint(root) {
|
|
29331
30237
|
const parts = [];
|
|
29332
30238
|
for (const sub of [".claude/skills", ".claude/agents", ".claude/hooks"]) {
|
|
29333
|
-
const dir =
|
|
30239
|
+
const dir = join19(root, sub);
|
|
29334
30240
|
try {
|
|
29335
30241
|
const names = (await readdir3(dir)).sort((a, b2) => a.localeCompare(b2));
|
|
29336
30242
|
for (const name of names) {
|
|
29337
30243
|
let mtime = "";
|
|
29338
30244
|
try {
|
|
29339
|
-
mtime = String(Math.floor((await stat3(
|
|
30245
|
+
mtime = String(Math.floor((await stat3(join19(dir, name))).mtimeMs));
|
|
29340
30246
|
} catch {
|
|
29341
30247
|
}
|
|
29342
30248
|
parts.push(`${sub}/${name}:${mtime}`);
|
|
@@ -29351,11 +30257,11 @@ async function computeFingerprint(root) {
|
|
|
29351
30257
|
} catch {
|
|
29352
30258
|
parts.push("manifest:none");
|
|
29353
30259
|
}
|
|
29354
|
-
return
|
|
30260
|
+
return createHash5("sha256").update(parts.join("|")).digest("hex").slice(0, 16);
|
|
29355
30261
|
}
|
|
29356
30262
|
async function readSkillDescription(skillDir) {
|
|
29357
30263
|
try {
|
|
29358
|
-
const content = await readFile8(
|
|
30264
|
+
const content = await readFile8(join19(skillDir, "SKILL.md"), "utf-8");
|
|
29359
30265
|
const fm = content.match(/^---\n([\s\S]*?)\n---/);
|
|
29360
30266
|
if (!fm) return void 0;
|
|
29361
30267
|
const desc = fm[1].match(/^description:\s*[>|]?\s*\n?([\s\S]*?)(?=\n\w+:|\n---|$)/m);
|
|
@@ -29375,17 +30281,17 @@ async function scanInventory(root, toolDefs) {
|
|
|
29375
30281
|
version = loadManifest().packageVersion;
|
|
29376
30282
|
} catch {
|
|
29377
30283
|
}
|
|
29378
|
-
const skillsDir =
|
|
30284
|
+
const skillsDir = join19(root, ".claude", "skills");
|
|
29379
30285
|
try {
|
|
29380
30286
|
const dirents = await readdir3(skillsDir, { withFileTypes: true });
|
|
29381
30287
|
for (const d of dirents.filter((e) => e.isDirectory())) {
|
|
29382
30288
|
entries.push({
|
|
29383
30289
|
kind: "skill",
|
|
29384
30290
|
name: d.name,
|
|
29385
|
-
description: await readSkillDescription(
|
|
30291
|
+
description: await readSkillDescription(join19(skillsDir, d.name)),
|
|
29386
30292
|
version,
|
|
29387
30293
|
status: stale.has(d.name) ? "stale_fork" : "ok",
|
|
29388
|
-
path:
|
|
30294
|
+
path: join19(".claude", "skills", d.name)
|
|
29389
30295
|
});
|
|
29390
30296
|
}
|
|
29391
30297
|
} catch {
|
|
@@ -29401,9 +30307,9 @@ async function scanInventory(root, toolDefs) {
|
|
|
29401
30307
|
}
|
|
29402
30308
|
const present = /* @__PURE__ */ new Set();
|
|
29403
30309
|
try {
|
|
29404
|
-
for (const f of (await readdir3(
|
|
30310
|
+
for (const f of (await readdir3(join19(root, ".claude", "hooks"))).filter((n) => n.endsWith(".sh"))) {
|
|
29405
30311
|
present.add(f);
|
|
29406
|
-
entries.push({ kind: "hook", name: f, status: "ok", path:
|
|
30312
|
+
entries.push({ kind: "hook", name: f, status: "ok", path: join19(".claude", "hooks", f) });
|
|
29407
30313
|
}
|
|
29408
30314
|
} catch {
|
|
29409
30315
|
}
|
|
@@ -29801,6 +30707,7 @@ var PAPI_TOOLS = [
|
|
|
29801
30707
|
buildExecuteTool,
|
|
29802
30708
|
buildCancelTool,
|
|
29803
30709
|
ideaTool,
|
|
30710
|
+
backlogImportTool,
|
|
29804
30711
|
bugTool,
|
|
29805
30712
|
bugListTool,
|
|
29806
30713
|
adHocTool,
|
|
@@ -29846,7 +30753,7 @@ function createServer(adapter2, config2) {
|
|
|
29846
30753
|
const __pkgDir = dirname5(__pkgFilename);
|
|
29847
30754
|
let serverVersion = "unknown";
|
|
29848
30755
|
try {
|
|
29849
|
-
const pkg = JSON.parse(
|
|
30756
|
+
const pkg = JSON.parse(readFileSync13(join20(__pkgDir, "..", "package.json"), "utf-8"));
|
|
29850
30757
|
serverVersion = pkg.version ?? "unknown";
|
|
29851
30758
|
} catch {
|
|
29852
30759
|
}
|
|
@@ -29864,7 +30771,7 @@ function createServer(adapter2, config2) {
|
|
|
29864
30771
|
}
|
|
29865
30772
|
const __filename = fileURLToPath3(import.meta.url);
|
|
29866
30773
|
const __dirname2 = dirname5(__filename);
|
|
29867
|
-
const skillsDir =
|
|
30774
|
+
const skillsDir = join20(__dirname2, "..", "skills");
|
|
29868
30775
|
function parseSkillFrontmatter(content) {
|
|
29869
30776
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
29870
30777
|
if (!match) return null;
|
|
@@ -29882,7 +30789,7 @@ function createServer(adapter2, config2) {
|
|
|
29882
30789
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
29883
30790
|
const prompts = [];
|
|
29884
30791
|
for (const file of mdFiles) {
|
|
29885
|
-
const content = await readFile9(
|
|
30792
|
+
const content = await readFile9(join20(skillsDir, file), "utf-8");
|
|
29886
30793
|
const meta = parseSkillFrontmatter(content);
|
|
29887
30794
|
if (meta) {
|
|
29888
30795
|
prompts.push({ name: meta.name, description: meta.description });
|
|
@@ -29898,7 +30805,7 @@ function createServer(adapter2, config2) {
|
|
|
29898
30805
|
try {
|
|
29899
30806
|
const files = await readdir4(skillsDir);
|
|
29900
30807
|
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
29901
|
-
const content = await readFile9(
|
|
30808
|
+
const content = await readFile9(join20(skillsDir, file), "utf-8");
|
|
29902
30809
|
const meta = parseSkillFrontmatter(content);
|
|
29903
30810
|
if (meta?.name === name) {
|
|
29904
30811
|
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
@@ -29983,6 +30890,8 @@ function createServer(adapter2, config2) {
|
|
|
29983
30890
|
return handleBuildCancel(adapter2, safeArgs);
|
|
29984
30891
|
case "idea":
|
|
29985
30892
|
return handleIdea(adapter2, config2, safeArgs);
|
|
30893
|
+
case "backlog_import":
|
|
30894
|
+
return handleBacklogImport(adapter2, safeArgs);
|
|
29986
30895
|
case "bug":
|
|
29987
30896
|
return handleBug(adapter2, config2, safeArgs);
|
|
29988
30897
|
case "bug_list":
|
|
@@ -30559,7 +31468,11 @@ async function dispatchRequest(args) {
|
|
|
30559
31468
|
const requestConfig = {
|
|
30560
31469
|
...baseConfig,
|
|
30561
31470
|
adapterType: "proxy",
|
|
30562
|
-
projectId: effectiveProjectId
|
|
31471
|
+
projectId: effectiveProjectId,
|
|
31472
|
+
// Remote transport: the client cannot read files this server writes, so
|
|
31473
|
+
// prepare must keep returning inline blobs (2905's budget bounds them),
|
|
31474
|
+
// never a server-side file path (task-2906).
|
|
31475
|
+
localFilesystem: false
|
|
30563
31476
|
};
|
|
30564
31477
|
const server2 = createServer(adapter2, requestConfig);
|
|
30565
31478
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
@@ -30598,7 +31511,7 @@ async function dispatchRequest(args) {
|
|
|
30598
31511
|
var __dirname = dirname6(fileURLToPath4(import.meta.url));
|
|
30599
31512
|
var pkgVersion = "unknown";
|
|
30600
31513
|
try {
|
|
30601
|
-
const pkg = JSON.parse(
|
|
31514
|
+
const pkg = JSON.parse(readFileSync18(join25(__dirname, "..", "package.json"), "utf-8"));
|
|
30602
31515
|
pkgVersion = pkg.version;
|
|
30603
31516
|
} catch {
|
|
30604
31517
|
}
|