@papi-ai/server 0.7.110 → 0.7.113
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/README.md +3 -0
- package/dist/index.js +273 -72
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,6 +50,9 @@ database adapter pool, so abandoned client sessions do not accumulate. Set
|
|
|
50
50
|
`PAPI_STDIO_IDLE_TIMEOUT_MS=0` to disable this behavior, or provide another
|
|
51
51
|
non-negative millisecond value in the `.mcp.json` environment block. This
|
|
52
52
|
setting applies only to stdio mode; hosted HTTP instances are not idle-killed.
|
|
53
|
+
While `plan`, `handoff_generate`, or `strategy_review` is between its prepare
|
|
54
|
+
and apply calls, the server allows an additional bounded grace period for the
|
|
55
|
+
caller to finish the review instead of treating that intentional pause as idle.
|
|
53
56
|
|
|
54
57
|
## License
|
|
55
58
|
|
package/dist/index.js
CHANGED
|
@@ -7075,7 +7075,7 @@ async function resolveMutationProject(input) {
|
|
|
7075
7075
|
if (input.workspacePath) {
|
|
7076
7076
|
if (!storedPapiDir) {
|
|
7077
7077
|
throw new ProjectResolutionError(
|
|
7078
|
-
`Project "${projectSlug}" has no stored workspace mapping. Refusing to write from "${input.workspacePath}" until the project is explicitly mapped.`
|
|
7078
|
+
`Project "${projectSlug}" has no stored workspace mapping. Refusing to write from "${input.workspacePath}" until the project is explicitly mapped. If your working directory belongs to a different project, run \`project_switch\` or pass \`project=<id>\` on the call.`
|
|
7079
7079
|
);
|
|
7080
7080
|
}
|
|
7081
7081
|
try {
|
|
@@ -7087,7 +7087,7 @@ async function resolveMutationProject(input) {
|
|
|
7087
7087
|
});
|
|
7088
7088
|
if (pathResult?.action !== "ok") {
|
|
7089
7089
|
throw new ProjectResolutionError(
|
|
7090
|
-
`Project "${projectSlug}" has not been explicitly verified for this workspace. Refusing to write until its mapping is confirmed.`
|
|
7090
|
+
`Project "${projectSlug}" has not been explicitly verified for this workspace ("${input.workspacePath}"). Refusing to write until its mapping is confirmed. If this session is bound to the wrong project, run \`project_switch\` or pass \`project=<id>\` on the call.`
|
|
7091
7091
|
);
|
|
7092
7092
|
}
|
|
7093
7093
|
} catch (err) {
|
|
@@ -7133,6 +7133,10 @@ async function resolveMutationProject(input) {
|
|
|
7133
7133
|
function formatMutationScopeReceipt(scope) {
|
|
7134
7134
|
const target = scope.targetProject ? `
|
|
7135
7135
|
Target project: ${scope.targetProject.name} (${scope.targetProject.slug}, ${scope.targetProject.id})` : "";
|
|
7136
|
+
const workspaceUnverified = scope.workspacePath.startsWith("unavailable");
|
|
7137
|
+
const noMapping = /mapping=none/.test(scope.resolutionEvidence);
|
|
7138
|
+
const unverifiedNote = workspaceUnverified && noMapping ? `
|
|
7139
|
+
\u26A0 This project was bound from your MCP config with no workspace cross-check \u2014 PAPI cannot confirm it matches your working directory. If a write ever lands on the wrong board: pass \`project=<id>\` per call, run \`project_switch\`, or fix PAPI_PROJECT_ID / the x-papi-project-id header and reconnect.` : "";
|
|
7136
7140
|
return [
|
|
7137
7141
|
"---",
|
|
7138
7142
|
"Project scope receipt",
|
|
@@ -7140,7 +7144,8 @@ Target project: ${scope.targetProject.name} (${scope.targetProject.slug}, ${scop
|
|
|
7140
7144
|
`Repository evidence: ${scope.repositoryEvidence}`,
|
|
7141
7145
|
`Workspace: ${scope.workspacePath}`,
|
|
7142
7146
|
`Resolution: ${scope.resolutionEvidence}`,
|
|
7143
|
-
target.trimStart()
|
|
7147
|
+
target.trimStart(),
|
|
7148
|
+
unverifiedNote.trimStart()
|
|
7144
7149
|
].filter(Boolean).join("\n");
|
|
7145
7150
|
}
|
|
7146
7151
|
|
|
@@ -11241,6 +11246,20 @@ function determineContextTier(cycleCount) {
|
|
|
11241
11246
|
if (cycleCount <= 20) return 2;
|
|
11242
11247
|
return 3;
|
|
11243
11248
|
}
|
|
11249
|
+
function trimContextForFirstCycle(ctx, cycleCount) {
|
|
11250
|
+
if (cycleCount !== 0 || ctx.mode === "bootstrap") return;
|
|
11251
|
+
ctx.cycleLog = "";
|
|
11252
|
+
ctx.recentBuildReports = "";
|
|
11253
|
+
const ad = ctx.activeDecisions;
|
|
11254
|
+
if (ad && ad.includes("[Confidence: ")) {
|
|
11255
|
+
const blocks = ad.split(/\n(?=### )/);
|
|
11256
|
+
const highOnly = blocks.filter(
|
|
11257
|
+
(b2) => !b2.startsWith("### ") || /\[Confidence: HIGH\]/.test(b2)
|
|
11258
|
+
);
|
|
11259
|
+
const trimmed = highOnly.join("\n").trim();
|
|
11260
|
+
ctx.activeDecisions = trimmed.length > 0 ? trimmed : ad;
|
|
11261
|
+
}
|
|
11262
|
+
}
|
|
11244
11263
|
function applyContextTier(ctx, cycleCount) {
|
|
11245
11264
|
const tier = determineContextTier(cycleCount);
|
|
11246
11265
|
const label = tier === 1 ? "Tier 1 (cycles 1-5)" : tier === 2 ? "Tier 2 (cycles 6-20)" : "Tier 3 (cycles 21+)";
|
|
@@ -11994,6 +12013,7 @@ ${lines.join("\n")}`;
|
|
|
11994
12013
|
siblingRepoWarning
|
|
11995
12014
|
};
|
|
11996
12015
|
const { label: leanTierLabel } = applyContextTier(ctx2, health.totalCycles);
|
|
12016
|
+
trimContextForFirstCycle(ctx2, health.totalCycles);
|
|
11997
12017
|
ctx2.contextTier = leanTierLabel;
|
|
11998
12018
|
console.error(`[plan-perf] context tier: ${leanTierLabel} (cycle ${health.totalCycles})`);
|
|
11999
12019
|
t = startTimer();
|
|
@@ -12191,6 +12211,7 @@ ${logLines}`);
|
|
|
12191
12211
|
siblingRepoWarning
|
|
12192
12212
|
};
|
|
12193
12213
|
const { label: fullTierLabel } = applyContextTier(ctx, health.totalCycles);
|
|
12214
|
+
trimContextForFirstCycle(ctx, health.totalCycles);
|
|
12194
12215
|
ctx.contextTier = fullTierLabel;
|
|
12195
12216
|
console.error(`[plan-perf] context tier: ${fullTierLabel} (cycle ${health.totalCycles})`);
|
|
12196
12217
|
const prevHashes = contextHashesResultFull.status === "fulfilled" ? contextHashesResultFull.value : null;
|
|
@@ -12818,15 +12839,20 @@ async function assertSingleActiveCycle(adapter2, opts = {}) {
|
|
|
12818
12839
|
for (const c of cycles) {
|
|
12819
12840
|
if (!newestByNumber.has(c.number)) newestByNumber.set(c.number, c);
|
|
12820
12841
|
}
|
|
12821
|
-
const
|
|
12822
|
-
|
|
12842
|
+
const blockingAll = [...newestByNumber.values()].filter((c) => c.status === "active" && c.number !== opts.allowNumber).filter((c) => opts.userId == null || c.userId === opts.userId);
|
|
12843
|
+
const phantoms = blockingAll.filter((c) => (c.goals?.length ?? 0) === 0);
|
|
12844
|
+
const blocking = blockingAll.filter((c) => (c.goals?.length ?? 0) > 0);
|
|
12845
|
+
const phantomNotes = phantoms.map(
|
|
12846
|
+
(c) => `Absorbed phantom active cycle ${c.number} (no goals \u2014 incomplete plan apply). Re-planning replaces it.`
|
|
12847
|
+
);
|
|
12848
|
+
if (blocking.length === 0) return phantomNotes;
|
|
12823
12849
|
if (!opts.autoComplete) {
|
|
12824
12850
|
const nums = blocking.map((c) => c.number).join(", ");
|
|
12825
12851
|
throw new Error(
|
|
12826
12852
|
`Cycle ${nums} is still active for this project \u2014 a second active cycle corrupts orient/build_execute. Run \`release\` to complete it first, or pass \`force: true\` to auto-complete it and continue.`
|
|
12827
12853
|
);
|
|
12828
12854
|
}
|
|
12829
|
-
const notes = [];
|
|
12855
|
+
const notes = [...phantomNotes];
|
|
12830
12856
|
for (const stale of blocking) {
|
|
12831
12857
|
await adapter2.createCycle({
|
|
12832
12858
|
...stale,
|
|
@@ -12861,6 +12887,18 @@ async function validateAndPrepare(adapter2, force, callerUserId, adapterType) {
|
|
|
12861
12887
|
const health = await adapter2.getCycleHealth();
|
|
12862
12888
|
cycleNumber = health.totalCycles;
|
|
12863
12889
|
mode = determineMode(health.projectCycleCount ?? health.totalCycles);
|
|
12890
|
+
try {
|
|
12891
|
+
const cyclesForPhantomCheck = await adapter2.readCycles();
|
|
12892
|
+
let callerNewest;
|
|
12893
|
+
for (const c of cyclesForPhantomCheck) {
|
|
12894
|
+
if (callerUserId && c.userId != null && c.userId !== callerUserId) continue;
|
|
12895
|
+
if (!callerNewest || c.number > callerNewest.number) callerNewest = c;
|
|
12896
|
+
}
|
|
12897
|
+
if (callerNewest && callerNewest.status === "active" && (callerNewest.goals?.length ?? 0) === 0 && callerNewest.number === cycleNumber) {
|
|
12898
|
+
cycleNumber = callerNewest.number - 1;
|
|
12899
|
+
}
|
|
12900
|
+
} catch {
|
|
12901
|
+
}
|
|
12864
12902
|
const blockingCycle = callerUserId ? await resolveCallerLatestCycle(adapter2, callerUserId) : void 0;
|
|
12865
12903
|
const latestStatus = callerUserId ? blockingCycle?.status : health.latestCycleStatus;
|
|
12866
12904
|
const blockingNumber = blockingCycle?.number ?? cycleNumber;
|
|
@@ -12951,6 +12989,11 @@ function assertApplyPayloadNonEmpty(data, cycleNumber) {
|
|
|
12951
12989
|
`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).`
|
|
12952
12990
|
);
|
|
12953
12991
|
}
|
|
12992
|
+
if (!data.cycleLogTitle || data.cycleLogTitle.trim().length === 0) {
|
|
12993
|
+
throw new Error(
|
|
12994
|
+
`Plan apply rejected: trimmed/partial apply payload \u2014 nothing persisted. The parsed plan for Cycle ${cycleNumber + 1} has no cycleLogTitle, so the cycle row would be written with empty goals \u2014 the phantom-cycle failure mode that blocks the next plan and renders nowhere on the hub. Re-run the plan and resend the COMPLETE structured output including cycleLogTitle.`
|
|
12995
|
+
);
|
|
12996
|
+
}
|
|
12954
12997
|
}
|
|
12955
12998
|
var VALID_PLAN_PRIORITIES = /* @__PURE__ */ new Set(["P0 Critical", "P1 High", "P2 Medium", "P3 Low"]);
|
|
12956
12999
|
function assertPlanPrioritiesValid(data) {
|
|
@@ -13087,7 +13130,7 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
|
|
|
13087
13130
|
decisionConflicts
|
|
13088
13131
|
};
|
|
13089
13132
|
}
|
|
13090
|
-
async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnly, skipHandoffs, tracker, density) {
|
|
13133
|
+
async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnly, skipHandoffs, tracker, density, taskIds) {
|
|
13091
13134
|
const prepareTimer = startTimer();
|
|
13092
13135
|
tracker?.mark("validate_and_prepare");
|
|
13093
13136
|
let t = startTimer();
|
|
@@ -13115,10 +13158,43 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
13115
13158
|
tracker?.mark("handoffs_only_assemble");
|
|
13116
13159
|
t = startTimer();
|
|
13117
13160
|
const targetCycle = cycleNumber + 1;
|
|
13118
|
-
const allTasks = await adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready", "In Progress"] });
|
|
13119
|
-
|
|
13120
|
-
if (
|
|
13121
|
-
|
|
13161
|
+
const allTasks = await adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"] });
|
|
13162
|
+
let preAssigned;
|
|
13163
|
+
if (taskIds && taskIds.length > 0) {
|
|
13164
|
+
const byDisplayId = new Map(allTasks.map((t2) => [t2.displayId ?? t2.id, t2]));
|
|
13165
|
+
const byId = new Map(allTasks.map((t2) => [t2.id, t2]));
|
|
13166
|
+
const BUILDABLE = /* @__PURE__ */ new Set(["Backlog", "In Cycle", "Ready"]);
|
|
13167
|
+
const resolved = [];
|
|
13168
|
+
const rejected = [];
|
|
13169
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13170
|
+
for (const raw of taskIds) {
|
|
13171
|
+
const task = byDisplayId.get(raw) ?? byId.get(raw);
|
|
13172
|
+
if (!task) {
|
|
13173
|
+
rejected.push(`${raw}: not a task in this project (or already Done/Cancelled/Deferred)`);
|
|
13174
|
+
continue;
|
|
13175
|
+
}
|
|
13176
|
+
if (seen.has(task.id)) continue;
|
|
13177
|
+
seen.add(task.id);
|
|
13178
|
+
if (!BUILDABLE.has(task.status)) {
|
|
13179
|
+
rejected.push(`${raw}: status is "${task.status}" \u2014 only Backlog / In Cycle / Ready tasks can be planned`);
|
|
13180
|
+
continue;
|
|
13181
|
+
}
|
|
13182
|
+
resolved.push(task);
|
|
13183
|
+
}
|
|
13184
|
+
if (rejected.length > 0) {
|
|
13185
|
+
throw new Error(
|
|
13186
|
+
`Cycle ${targetCycle} \u2014 nothing was applied. ${rejected.length} of ${taskIds.length} task id(s) could not be planned:
|
|
13187
|
+
` + rejected.map((r) => ` - ${r}`).join("\n") + `
|
|
13188
|
+
|
|
13189
|
+
Fix the ids and re-run. Every id must be a currently-buildable task (Backlog / In Cycle / Ready) in this project.`
|
|
13190
|
+
);
|
|
13191
|
+
}
|
|
13192
|
+
preAssigned = resolved;
|
|
13193
|
+
} else {
|
|
13194
|
+
preAssigned = allTasks.filter((task) => task.cycle === targetCycle);
|
|
13195
|
+
if (preAssigned.length === 0) {
|
|
13196
|
+
throw new Error(`No tasks assigned to Cycle ${targetCycle}. Assign tasks first (set cycle = ${targetCycle} via SQL) or run plan without handoffs_only.`);
|
|
13197
|
+
}
|
|
13122
13198
|
}
|
|
13123
13199
|
const [decisions, reports, brief] = await Promise.all([
|
|
13124
13200
|
adapter2.getActiveDecisions(),
|
|
@@ -13760,6 +13836,37 @@ function planDelivery(input) {
|
|
|
13760
13836
|
}
|
|
13761
13837
|
}
|
|
13762
13838
|
|
|
13839
|
+
// src/lib/harness-capability.ts
|
|
13840
|
+
var HARNESS_REGISTRY = {
|
|
13841
|
+
// Local stdio CLI agents — PAPI runs git on the user's machine. Confirmed in telemetry.
|
|
13842
|
+
"claude-code": { build: true, label: "Claude Code" },
|
|
13843
|
+
"opencode": { build: true, label: "opencode" },
|
|
13844
|
+
"zcode": { build: true, label: "zcode" },
|
|
13845
|
+
"codex": { build: true, label: "Codex" },
|
|
13846
|
+
"cursor": { build: true, label: "Cursor" },
|
|
13847
|
+
// Cloud harnesses with code sandboxes — their agent builds in-sandbox over HTTP+OAuth.
|
|
13848
|
+
"lovable": { build: true, label: "Lovable" },
|
|
13849
|
+
"bolt": { build: true, label: "Bolt" },
|
|
13850
|
+
"replit": { build: true, label: "Replit" },
|
|
13851
|
+
// Chat-only surfaces, no sandbox — planning only.
|
|
13852
|
+
"chatgpt": { build: false, label: "ChatGPT" },
|
|
13853
|
+
"claude.ai": { build: false, label: "Claude.ai" },
|
|
13854
|
+
"claude-desktop": { build: false, label: "Claude Desktop" }
|
|
13855
|
+
};
|
|
13856
|
+
var UNKNOWN_DEFAULT = { build: false, label: "your tool" };
|
|
13857
|
+
function detectHarness(clientName) {
|
|
13858
|
+
const raw = clientName?.trim() || null;
|
|
13859
|
+
if (!raw) {
|
|
13860
|
+
return { ...UNKNOWN_DEFAULT, raw: null, key: null, known: false };
|
|
13861
|
+
}
|
|
13862
|
+
const norm = raw.toLowerCase();
|
|
13863
|
+
const key = HARNESS_REGISTRY[norm] ? norm : Object.keys(HARNESS_REGISTRY).find((k) => norm.includes(k)) ?? null;
|
|
13864
|
+
if (!key) {
|
|
13865
|
+
return { ...UNKNOWN_DEFAULT, label: raw, raw, key: null, known: false };
|
|
13866
|
+
}
|
|
13867
|
+
return { ...HARNESS_REGISTRY[key], raw, key, known: true };
|
|
13868
|
+
}
|
|
13869
|
+
|
|
13763
13870
|
// src/services/session-guidance.ts
|
|
13764
13871
|
var DEFAULT_CALLER_KEY = "__default__";
|
|
13765
13872
|
var sessionStates = /* @__PURE__ */ new Map();
|
|
@@ -13836,7 +13943,7 @@ function markOrient(callerKey) {
|
|
|
13836
13943
|
}
|
|
13837
13944
|
function getProjectConnectionBanner(projectName, projectSlug) {
|
|
13838
13945
|
if (!projectName || !projectSlug) return null;
|
|
13839
|
-
return
|
|
13946
|
+
return `**\u26A0 Bound to: ${projectName} (${projectSlug})** \u2014 every write on this session lands on THIS project's board. If that is not the project your working directory belongs to, stop: pass \`project=<id>\` on the call to target a different project, run \`project_switch\` to rebind the session, or fix the project id in your MCP config (PAPI_PROJECT_ID for local, x-papi-project-id header for remote) and reconnect.`;
|
|
13840
13947
|
}
|
|
13841
13948
|
function detectContextDegradation(now = Date.now(), callerKey) {
|
|
13842
13949
|
const state = getState(callerKey);
|
|
@@ -14019,6 +14126,9 @@ function savePrepareContextFile(projectId, callerKey, content) {
|
|
|
14019
14126
|
|
|
14020
14127
|
// src/tools/plan.ts
|
|
14021
14128
|
var planPrepareCache = new PerCallerCache();
|
|
14129
|
+
function hasPendingPlanPrepare() {
|
|
14130
|
+
return planPrepareCache.size() > 0;
|
|
14131
|
+
}
|
|
14022
14132
|
var planTool = {
|
|
14023
14133
|
name: "plan",
|
|
14024
14134
|
description: 'Turn a backlog into one scoped cycle of work, with a written spec for every task in it. plan reads the whole board, the decisions this project has already taken, and how much it actually delivered in recent cycles, then prioritises what to do next and writes a per-task BUILD HANDOFF: scope, what is deliberately out of scope, acceptance criteria, files likely touched, security notes, and the shared branch each task belongs on. It also reports board health, so stale, blocked and drifting work is visible instead of accumulating. This is not the same as planning inside one session: the cycle, the specs and the reasoning are stored, so the next session, the next tool, or the next person picks up where this one stopped. Run once per cycle, after setup the first time, or after completing all builds AND running release for the previous cycle. NEVER call when unbuilt cycle tasks exist: 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, and generate handoffs separately via `handoff_generate`.',
|
|
@@ -14081,6 +14191,11 @@ var planTool = {
|
|
|
14081
14191
|
type: "boolean",
|
|
14082
14192
|
description: "Skip backlog analysis and task selection. Only generate BUILD HANDOFFs for tasks already assigned to the target cycle. Requires pre-assigned tasks (set cycle number on tasks first). ~30% of normal plan cost."
|
|
14083
14193
|
},
|
|
14194
|
+
task_ids: {
|
|
14195
|
+
type: "array",
|
|
14196
|
+
items: { type: "string" },
|
|
14197
|
+
description: 'Plan EXACTLY these backlog tasks into the cycle, with BUILD HANDOFFs, via the same atomic apply as a normal plan. Use when the user says "take these tasks and get them build-ready" or "plan task-12, task-15, task-19" \u2014 do NOT reach for ad_hoc (a receipt) or board_edit (strands them with no handoff). Skips backlog selection: only the named tasks are scoped. Every id must be a real, currently-buildable task (Backlog / In Cycle / Ready) in this project \u2014 unknown or ineligible ids are rejected with a per-id reason and nothing is applied. The apply assigns cycle + handoff + status together, so a failure leaves nothing half-landed.'
|
|
14198
|
+
},
|
|
14084
14199
|
skip_handoffs: {
|
|
14085
14200
|
type: "boolean",
|
|
14086
14201
|
description: "Run full planning (triage, task selection, board management) but skip BUILD HANDOFF generation. Selected tasks are assigned to the cycle without handoffs. Run `handoff_generate` after to create handoffs separately. Reduces planner cognitive load for large backlogs."
|
|
@@ -14190,7 +14305,7 @@ function formatPlanResult(result) {
|
|
|
14190
14305
|
}
|
|
14191
14306
|
return response;
|
|
14192
14307
|
}
|
|
14193
|
-
async function handlePlan(adapter2, config2, args) {
|
|
14308
|
+
async function handlePlan(adapter2, config2, args, clientName) {
|
|
14194
14309
|
const toolMode = args.mode;
|
|
14195
14310
|
const callerKey = callerKeyFromConfig(config2);
|
|
14196
14311
|
const filters = {};
|
|
@@ -14200,7 +14315,8 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
14200
14315
|
if (typeof args.priority === "string") filters.priority = args.priority;
|
|
14201
14316
|
const focus = typeof args.focus === "string" ? args.focus : void 0;
|
|
14202
14317
|
const force = args.force === true;
|
|
14203
|
-
const
|
|
14318
|
+
const taskIds = Array.isArray(args.task_ids) ? args.task_ids.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim()) : void 0;
|
|
14319
|
+
const handoffsOnly = args.handoffs_only === true || taskIds !== void 0 && taskIds.length > 0;
|
|
14204
14320
|
const density = args.density === "light" || args.density === "standard" || args.density === "deep" ? args.density : void 0;
|
|
14205
14321
|
const tracker = new ProgressTracker(toolMode === "apply" ? "apply_validate" : "prepare_validate").bindStream(adapter2, { stage: "plan" });
|
|
14206
14322
|
try {
|
|
@@ -14282,7 +14398,7 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
14282
14398
|
} catch {
|
|
14283
14399
|
}
|
|
14284
14400
|
const skipHandoffs = args.skip_handoffs === true;
|
|
14285
|
-
const result = await preparePlan(adapter2, config2, filters, focus, force, handoffsOnly, skipHandoffs, tracker, density);
|
|
14401
|
+
const result = await preparePlan(adapter2, config2, filters, focus, force, handoffsOnly, skipHandoffs, tracker, density, taskIds);
|
|
14286
14402
|
const prepareState = {
|
|
14287
14403
|
contextHashes: result.contextHashes,
|
|
14288
14404
|
userMessage: result.userMessage,
|
|
@@ -14293,7 +14409,15 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
14293
14409
|
planPrepareCache.set(callerKey, prepareState);
|
|
14294
14410
|
savePrepareSpill(adapter2.getProjectId?.(), callerKey, prepareState);
|
|
14295
14411
|
const explicit = args.dispatch === "inline" ? false : args.dispatch === "subagent" ? true : void 0;
|
|
14296
|
-
|
|
14412
|
+
let dispatch = shouldDispatch(result.contextBytes ?? 0, explicit) ? "subagent" : "inline";
|
|
14413
|
+
let autoRouteDowngraded = false;
|
|
14414
|
+
if (dispatch === "subagent" && explicit === void 0) {
|
|
14415
|
+
const harness = detectHarness(clientName);
|
|
14416
|
+
if (harness.key !== "claude-code") {
|
|
14417
|
+
dispatch = "inline";
|
|
14418
|
+
autoRouteDowngraded = true;
|
|
14419
|
+
}
|
|
14420
|
+
}
|
|
14297
14421
|
const modeLabel = result.mode === "bootstrap" ? "Bootstrap" : "Full";
|
|
14298
14422
|
const header = result.strategyReviewWarning ? `${result.strategyReviewWarning}
|
|
14299
14423
|
` : "";
|
|
@@ -14329,7 +14453,12 @@ ${result.userMessage}
|
|
|
14329
14453
|
const dispatchHeader = result.strategyReviewWarning ? `${result.strategyReviewWarning}
|
|
14330
14454
|
|
|
14331
14455
|
` : "";
|
|
14332
|
-
|
|
14456
|
+
const inlineOverride = `
|
|
14457
|
+
|
|
14458
|
+
---
|
|
14459
|
+
|
|
14460
|
+
> **No sub-agent or \`Task\` primitive in your client?** Re-call \`plan\` with \`dispatch: "inline"\` to get the planning prompt back directly, then run it yourself in a fresh context and pass the output to the apply call. The apply contract is identical either way.`;
|
|
14461
|
+
return { ...textResponse(`${dispatchHeader}${dispatchPrompt}${inlineOverride}`), _contextBytes: result.contextBytes };
|
|
14333
14462
|
}
|
|
14334
14463
|
if (contextFilePath) {
|
|
14335
14464
|
const kb = result.contextBytes !== void 0 ? ` (~${(result.contextBytes / 1024).toFixed(0)} KB)` : "";
|
|
@@ -14351,8 +14480,11 @@ The full planning brief \u2014 system prompt + all context${kb} \u2014 has been
|
|
|
14351
14480
|
);
|
|
14352
14481
|
return { ...pathResponse, _contextBytes: result.contextBytes };
|
|
14353
14482
|
}
|
|
14483
|
+
const downgradeNote = autoRouteDowngraded ? `> This context is large. It is returned inline because your client has no \`Task\` sub-agent primitive. If your client CAN run a sub-agent, re-call \`plan\` with \`dispatch: "subagent"\`.
|
|
14484
|
+
|
|
14485
|
+
` : "";
|
|
14354
14486
|
const response = textResponse(
|
|
14355
|
-
`${header}## PAPI Cycle Plan \u2014 Prepare Phase (${modeLabel} Mode, Cycle ${result.cycleNumber + 1})
|
|
14487
|
+
`${header}${downgradeNote}## PAPI Cycle Plan \u2014 Prepare Phase (${modeLabel} Mode, Cycle ${result.cycleNumber + 1})
|
|
14356
14488
|
|
|
14357
14489
|
Follow the system prompt and context below to generate a complete cycle plan.
|
|
14358
14490
|
|
|
@@ -17104,6 +17236,9 @@ function buildStrategyReviewPostDirective(cycleNumber) {
|
|
|
17104
17236
|
].join("\n");
|
|
17105
17237
|
}
|
|
17106
17238
|
var reviewPrepareCache = new PerCallerCache();
|
|
17239
|
+
function hasPendingStrategyReview() {
|
|
17240
|
+
return reviewPrepareCache.size() > 0;
|
|
17241
|
+
}
|
|
17107
17242
|
var strategyReviewTool = {
|
|
17108
17243
|
name: "strategy_review",
|
|
17109
17244
|
description: 'Run a Strategy Review \u2014 assesses project direction, velocity, and Active Decisions. Produces recommendations and potential AD updates that feed into the next plan. Offered every 5 cycles; hard-blocked at 7+ overdue cycles. Run it in your current conversation \u2014 only start a fresh one if you are genuinely under context pressure (your host just compacted, you are near the context limit, or the session is heavy with build context), not just because a review is next. First call returns a review prompt for you to execute (prepare phase). Then call again with mode "apply" and your output. Pass `force: true` to run before the cadence gate.',
|
|
@@ -17558,6 +17693,7 @@ ${result.userMessage}
|
|
|
17558
17693
|
}
|
|
17559
17694
|
|
|
17560
17695
|
// src/lib/task-readiness.ts
|
|
17696
|
+
var ADVISORY_GAP_LABELS = /* @__PURE__ */ new Set(["WHY NOW", "FILES LIKELY TOUCHED"]);
|
|
17561
17697
|
function hasMeaningfulEntry(value) {
|
|
17562
17698
|
return Array.isArray(value) && value.some((entry) => {
|
|
17563
17699
|
if (typeof entry === "string") return entry.trim().length > 0;
|
|
@@ -17577,23 +17713,29 @@ function getHandoffGaps(handoff) {
|
|
|
17577
17713
|
return gaps;
|
|
17578
17714
|
}
|
|
17579
17715
|
function getTaskReadiness(task, currentCycle2) {
|
|
17580
|
-
const
|
|
17581
|
-
const
|
|
17716
|
+
const allGaps = getHandoffGaps(task.buildHandoff);
|
|
17717
|
+
const blockingGaps = allGaps.filter((g) => !ADVISORY_GAP_LABELS.has(g));
|
|
17718
|
+
const advisoryGaps = allGaps.filter((g) => ADVISORY_GAP_LABELS.has(g));
|
|
17719
|
+
const handoffGaps = [...blockingGaps, ...advisoryGaps];
|
|
17720
|
+
const handoffComplete = blockingGaps.length === 0;
|
|
17721
|
+
const advisoryNote = advisoryGaps.length > 0 ? ` A planner should still fill ${advisoryGaps.join(", ")}, but it does not block the build.` : "";
|
|
17582
17722
|
const cycleMismatch = currentCycle2 != null && currentCycle2 > 0 && task.cycle !== currentCycle2;
|
|
17583
17723
|
if (task.status === "Backlog") {
|
|
17584
17724
|
if (!handoffComplete) {
|
|
17585
17725
|
return {
|
|
17586
17726
|
label: "Needs Planning",
|
|
17587
|
-
reason: `No complete BUILD HANDOFF (${
|
|
17727
|
+
reason: `No complete BUILD HANDOFF (${blockingGaps.join(", ")}). Run plan before claiming or building this task.`,
|
|
17588
17728
|
handoffComplete,
|
|
17589
|
-
handoffGaps
|
|
17729
|
+
handoffGaps,
|
|
17730
|
+
advisoryGaps
|
|
17590
17731
|
};
|
|
17591
17732
|
}
|
|
17592
17733
|
return {
|
|
17593
17734
|
label: "Needs Planning",
|
|
17594
17735
|
reason: "Backlog work is waiting for a planning pass to assign it to a cycle before it can be built.",
|
|
17595
17736
|
handoffComplete,
|
|
17596
|
-
handoffGaps
|
|
17737
|
+
handoffGaps,
|
|
17738
|
+
advisoryGaps
|
|
17597
17739
|
};
|
|
17598
17740
|
}
|
|
17599
17741
|
if (task.status === "In Cycle" || task.status === "Ready") {
|
|
@@ -17602,22 +17744,25 @@ function getTaskReadiness(task, currentCycle2) {
|
|
|
17602
17744
|
label: "Needs Planning",
|
|
17603
17745
|
reason: `Assigned to Cycle ${task.cycle ?? "an earlier cycle"}, not the active Cycle ${currentCycle2}. Run plan before building it.`,
|
|
17604
17746
|
handoffComplete,
|
|
17605
|
-
handoffGaps
|
|
17747
|
+
handoffGaps,
|
|
17748
|
+
advisoryGaps
|
|
17606
17749
|
};
|
|
17607
17750
|
}
|
|
17608
17751
|
if (!handoffComplete) {
|
|
17609
17752
|
return {
|
|
17610
17753
|
label: "Needs Planning",
|
|
17611
|
-
reason: `Planning has not produced a complete BUILD HANDOFF (${
|
|
17754
|
+
reason: `Planning has not produced a complete BUILD HANDOFF (${blockingGaps.join(", ")}).`,
|
|
17612
17755
|
handoffComplete,
|
|
17613
|
-
handoffGaps
|
|
17756
|
+
handoffGaps,
|
|
17757
|
+
advisoryGaps
|
|
17614
17758
|
};
|
|
17615
17759
|
}
|
|
17616
17760
|
return {
|
|
17617
17761
|
label: "Ready to Build",
|
|
17618
|
-
reason:
|
|
17762
|
+
reason: `Task is assigned to the active cycle and has a complete BUILD HANDOFF.${advisoryNote}`,
|
|
17619
17763
|
handoffComplete,
|
|
17620
|
-
handoffGaps
|
|
17764
|
+
handoffGaps,
|
|
17765
|
+
advisoryGaps
|
|
17621
17766
|
};
|
|
17622
17767
|
}
|
|
17623
17768
|
const labelByStatus = {
|
|
@@ -17633,7 +17778,8 @@ function getTaskReadiness(task, currentCycle2) {
|
|
|
17633
17778
|
label,
|
|
17634
17779
|
reason: label === "Needs Planning" ? "Task is not in a buildable lifecycle state." : `Task status is ${task.status}.`,
|
|
17635
17780
|
handoffComplete,
|
|
17636
|
-
handoffGaps
|
|
17781
|
+
handoffGaps,
|
|
17782
|
+
advisoryGaps
|
|
17637
17783
|
};
|
|
17638
17784
|
}
|
|
17639
17785
|
|
|
@@ -18244,6 +18390,20 @@ async function handleBoardView(adapter2, args) {
|
|
|
18244
18390
|
fields,
|
|
18245
18391
|
membership.isMultiMember ? assigneeColumn(membership.nameOf) : null
|
|
18246
18392
|
);
|
|
18393
|
+
if (typeof args.cycle === "number") {
|
|
18394
|
+
try {
|
|
18395
|
+
const cycleRows = await adapter2.readCycles();
|
|
18396
|
+
const row = cycleRows.find((c) => c.number === args.cycle);
|
|
18397
|
+
const listed = row?.taskIds?.length ?? 0;
|
|
18398
|
+
const carrying = result.tasks.filter((t) => t.cycle === args.cycle).length;
|
|
18399
|
+
if (row && listed > 0 && listed !== carrying) {
|
|
18400
|
+
output += `
|
|
18401
|
+
|
|
18402
|
+
> \u26A0\uFE0F Cycle ${args.cycle} membership disagrees: the cycle row lists ${listed} task(s) but ${carrying} carry cycle=${args.cycle}. This view shows the latter. Run \`board_reconcile\` to realign them.`;
|
|
18403
|
+
}
|
|
18404
|
+
} catch {
|
|
18405
|
+
}
|
|
18406
|
+
}
|
|
18247
18407
|
if (!fields) {
|
|
18248
18408
|
try {
|
|
18249
18409
|
const comments = await adapter2.getRecentTaskComments?.(30);
|
|
@@ -20963,37 +21123,6 @@ function pendingDecisionBlocker(attempts, cycle, taskId) {
|
|
|
20963
21123
|
};
|
|
20964
21124
|
}
|
|
20965
21125
|
|
|
20966
|
-
// src/lib/harness-capability.ts
|
|
20967
|
-
var HARNESS_REGISTRY = {
|
|
20968
|
-
// Local stdio CLI agents — PAPI runs git on the user's machine. Confirmed in telemetry.
|
|
20969
|
-
"claude-code": { build: true, label: "Claude Code" },
|
|
20970
|
-
"opencode": { build: true, label: "opencode" },
|
|
20971
|
-
"zcode": { build: true, label: "zcode" },
|
|
20972
|
-
"codex": { build: true, label: "Codex" },
|
|
20973
|
-
"cursor": { build: true, label: "Cursor" },
|
|
20974
|
-
// Cloud harnesses with code sandboxes — their agent builds in-sandbox over HTTP+OAuth.
|
|
20975
|
-
"lovable": { build: true, label: "Lovable" },
|
|
20976
|
-
"bolt": { build: true, label: "Bolt" },
|
|
20977
|
-
"replit": { build: true, label: "Replit" },
|
|
20978
|
-
// Chat-only surfaces, no sandbox — planning only.
|
|
20979
|
-
"chatgpt": { build: false, label: "ChatGPT" },
|
|
20980
|
-
"claude.ai": { build: false, label: "Claude.ai" },
|
|
20981
|
-
"claude-desktop": { build: false, label: "Claude Desktop" }
|
|
20982
|
-
};
|
|
20983
|
-
var UNKNOWN_DEFAULT = { build: false, label: "your tool" };
|
|
20984
|
-
function detectHarness(clientName) {
|
|
20985
|
-
const raw = clientName?.trim() || null;
|
|
20986
|
-
if (!raw) {
|
|
20987
|
-
return { ...UNKNOWN_DEFAULT, raw: null, key: null, known: false };
|
|
20988
|
-
}
|
|
20989
|
-
const norm = raw.toLowerCase();
|
|
20990
|
-
const key = HARNESS_REGISTRY[norm] ? norm : Object.keys(HARNESS_REGISTRY).find((k) => norm.includes(k)) ?? null;
|
|
20991
|
-
if (!key) {
|
|
20992
|
-
return { ...UNKNOWN_DEFAULT, label: raw, raw, key: null, known: false };
|
|
20993
|
-
}
|
|
20994
|
-
return { ...HARNESS_REGISTRY[key], raw, key, known: true };
|
|
20995
|
-
}
|
|
20996
|
-
|
|
20997
21126
|
// src/lib/harness-build-steps.ts
|
|
20998
21127
|
init_git();
|
|
20999
21128
|
function startBuildSteps(branch, base, taskId) {
|
|
@@ -25567,6 +25696,9 @@ init_git();
|
|
|
25567
25696
|
|
|
25568
25697
|
// src/tools/handoff.ts
|
|
25569
25698
|
var handoffPrepareCache = new PerCallerCache();
|
|
25699
|
+
function hasPendingHandoffGenerate() {
|
|
25700
|
+
return handoffPrepareCache.size() > 0;
|
|
25701
|
+
}
|
|
25570
25702
|
var handoffGenerateTool = {
|
|
25571
25703
|
name: "handoff_generate",
|
|
25572
25704
|
description: "Generate BUILD HANDOFFs for cycle tasks that don't have one yet. Run after `plan` (with skip_handoffs=true) or to regenerate stale handoffs. Uses the prepare/apply pattern \u2014 first call returns a prompt, second call persists results.",
|
|
@@ -31771,10 +31903,14 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
31771
31903
|
lines.push("Board is empty \u2014 run `plan` to create your next cycle.");
|
|
31772
31904
|
}
|
|
31773
31905
|
lines.push("");
|
|
31906
|
+
lines.push(WORK_DUMP_ROUTING_RULE);
|
|
31907
|
+
lines.push("");
|
|
31774
31908
|
} else if (buildInfo.noHandoffs) {
|
|
31775
31909
|
lines.push("## Tasks");
|
|
31776
31910
|
lines.push("No tasks with BUILD HANDOFFs \u2014 run `plan` to generate cycle tasks.");
|
|
31777
31911
|
lines.push("");
|
|
31912
|
+
lines.push(WORK_DUMP_ROUTING_RULE);
|
|
31913
|
+
lines.push("");
|
|
31778
31914
|
} else {
|
|
31779
31915
|
lines.push(`## Tasks with Handoffs`);
|
|
31780
31916
|
lines.push(`${buildInfo.totalHandoffs} total \u2014 ${buildInfo.inProgress.length} in progress, ${buildInfo.backlogCount} backlog, ${buildInfo.blockedCount} blocked`);
|
|
@@ -31805,6 +31941,7 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
31805
31941
|
return lines.join("\n").trimEnd();
|
|
31806
31942
|
}
|
|
31807
31943
|
var NOT_SET_UP_MESSAGE = "Setup required \u2014 this project has no PAPI state yet. That is the normal starting point, not an error. Run `setup` to generate your Product Brief and scaffold the workflow, then `plan` to create your first cycle.";
|
|
31944
|
+
var WORK_DUMP_ROUTING_RULE = '_If someone hands you a list of work (an audit, a bug list, "build all of these"), do NOT start writing code. File each item with `idea`, then run `plan` to scope a cycle \u2014 that is how work gets a spec, a branch and a review._';
|
|
31808
31945
|
function emptyHealthSummary() {
|
|
31809
31946
|
return {
|
|
31810
31947
|
cycleNumber: 0,
|
|
@@ -32160,12 +32297,34 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
|
|
|
32160
32297
|
cycleSince: currentCycle2,
|
|
32161
32298
|
compact: true
|
|
32162
32299
|
}).catch(() => []) : [];
|
|
32163
|
-
const
|
|
32164
|
-
const cycleCounts = computeCycleTaskCounts(cycleTasksAll, currentCycle2, cycleIsComplete);
|
|
32300
|
+
const cycleCounts = computeCycleTaskCounts(cycleTasksAllRaw, currentCycle2, cycleIsComplete);
|
|
32165
32301
|
const { inProgress: cycleInProgress, inReview: cycleInReview, inCycle: cycleInCycle, ready: cycleReady, backlog: cycleBacklog, total: cycleTotal, done: cycleDone } = cycleCounts;
|
|
32166
32302
|
if (!cycleIsComplete && cycleTotal === 0 && cycleDone > 0) {
|
|
32167
32303
|
buildResult.warnings.unshift(`\u26A0\uFE0F Cycle ${currentCycle2} is complete \u2014 all ${cycleDone} task${cycleDone !== 1 ? "s" : ""} Done. Release has not been run. Run \`release\` now (or \`release\` with \`skipVersion=true\` to close the cycle without a git tag).`);
|
|
32168
32304
|
}
|
|
32305
|
+
if (!cycleIsComplete && currentCycle2 > 0) {
|
|
32306
|
+
try {
|
|
32307
|
+
const cyclesForPhantom = await adapter2.readCycles();
|
|
32308
|
+
const mine = cyclesForPhantom.filter(
|
|
32309
|
+
(c) => c.number === currentCycle2 && (!callerUserId || c.userId == null || c.userId === callerUserId)
|
|
32310
|
+
);
|
|
32311
|
+
const phantom = mine.find((c) => c.status === "active" && (c.goals?.length ?? 0) === 0);
|
|
32312
|
+
if (phantom) {
|
|
32313
|
+
buildResult.warnings.unshift(
|
|
32314
|
+
`\u26A0\uFE0F Cycle ${currentCycle2} has no goals and looks like a plan that didn't finish writing. Run \`plan\` again \u2014 it will replace this incomplete cycle rather than stacking a new one on top. No data is lost: nothing has been built in it.`
|
|
32315
|
+
);
|
|
32316
|
+
}
|
|
32317
|
+
const cycleRow = mine.find((c) => c.status === "active") ?? mine[0];
|
|
32318
|
+
const taskIdMembership = cycleRow?.taskIds?.length ?? 0;
|
|
32319
|
+
const columnMembership = cycleTasksAllRaw.filter((t) => t.cycle === currentCycle2).length;
|
|
32320
|
+
if (cycleRow && taskIdMembership !== columnMembership && taskIdMembership > 0) {
|
|
32321
|
+
buildResult.warnings.unshift(
|
|
32322
|
+
`\u26A0\uFE0F Cycle ${currentCycle2} membership disagrees: the cycle row lists ${taskIdMembership} task(s) but ${columnMembership} task(s) actually carry cycle=${currentCycle2}. Some cycle tasks may not show on \`board_view cycle=${currentCycle2}\` or the hub. Run \`board_reconcile\` to realign them.`
|
|
32323
|
+
);
|
|
32324
|
+
}
|
|
32325
|
+
} catch {
|
|
32326
|
+
}
|
|
32327
|
+
}
|
|
32169
32328
|
tracker.mark("parallel-reads");
|
|
32170
32329
|
const inProgressItems = buildResult.inProgress.map(
|
|
32171
32330
|
(t) => `- **${t.id}:** ${t.title} (${t.priority} | ${t.complexity})`
|
|
@@ -35561,6 +35720,9 @@ var PAPI_TOOLS = [
|
|
|
35561
35720
|
function getQualifiedPapiTools() {
|
|
35562
35721
|
return qualifyTools(PAPI_TOOLS);
|
|
35563
35722
|
}
|
|
35723
|
+
function hasPendingStdioPrepare() {
|
|
35724
|
+
return hasPendingPlanPrepare() || hasPendingStrategyReview() || hasPendingHandoffGenerate();
|
|
35725
|
+
}
|
|
35564
35726
|
function getToolMetadata() {
|
|
35565
35727
|
return PAPI_TOOLS.map((t) => ({ name: t.name, description: t.description }));
|
|
35566
35728
|
}
|
|
@@ -35685,7 +35847,7 @@ function createServer(adapter2, config2, observation) {
|
|
|
35685
35847
|
const runHandler = async () => {
|
|
35686
35848
|
switch (name) {
|
|
35687
35849
|
case "plan":
|
|
35688
|
-
return handlePlan(scopedAdapter, scopedConfig, safeArgs);
|
|
35850
|
+
return handlePlan(scopedAdapter, scopedConfig, safeArgs, server2.getClientVersion()?.name);
|
|
35689
35851
|
case "strategy_review":
|
|
35690
35852
|
return handleStrategyReview(scopedAdapter, scopedConfig, safeArgs);
|
|
35691
35853
|
case "strategy_change":
|
|
@@ -36455,11 +36617,36 @@ function startHttpTransport(opts) {
|
|
|
36455
36617
|
}
|
|
36456
36618
|
try {
|
|
36457
36619
|
parsedBody = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
36458
|
-
observation?.setTool(extractToolName(parsedBody) ?? extractRpcMethod(parsedBody));
|
|
36459
36620
|
} catch {
|
|
36460
36621
|
sendError(res, { status: 400, body: { error: "Invalid JSON body" } });
|
|
36461
36622
|
return;
|
|
36462
36623
|
}
|
|
36624
|
+
if (Array.isArray(parsedBody)) {
|
|
36625
|
+
logEvent({
|
|
36626
|
+
level: "warn",
|
|
36627
|
+
msg: "jsonrpc_batch_rejected",
|
|
36628
|
+
ip,
|
|
36629
|
+
bearer_prefix: bearerPrefix(bearer),
|
|
36630
|
+
status: 400
|
|
36631
|
+
});
|
|
36632
|
+
if (!res.headersSent) {
|
|
36633
|
+
res.writeHead(400, {
|
|
36634
|
+
"Content-Type": "application/json",
|
|
36635
|
+
"CDN-Cache-Control": "private, no-store",
|
|
36636
|
+
"Vercel-CDN-Cache-Control": "private, no-store"
|
|
36637
|
+
});
|
|
36638
|
+
res.end(JSON.stringify({
|
|
36639
|
+
jsonrpc: "2.0",
|
|
36640
|
+
id: null,
|
|
36641
|
+
error: {
|
|
36642
|
+
code: -32600,
|
|
36643
|
+
message: "JSON-RPC batch requests are not supported. Send each tool call as its own HTTP request."
|
|
36644
|
+
}
|
|
36645
|
+
}));
|
|
36646
|
+
}
|
|
36647
|
+
return;
|
|
36648
|
+
}
|
|
36649
|
+
observation?.setTool(extractToolName(parsedBody) ?? extractRpcMethod(parsedBody));
|
|
36463
36650
|
}
|
|
36464
36651
|
logEvent({
|
|
36465
36652
|
level: "info",
|
|
@@ -36710,6 +36897,7 @@ function shouldCheckForUpdate(version, selfHostFlag) {
|
|
|
36710
36897
|
|
|
36711
36898
|
// src/lib/stdio-idle-timeout.ts
|
|
36712
36899
|
var DEFAULT_STDIO_IDLE_TIMEOUT_MS = 12e4;
|
|
36900
|
+
var DEFAULT_STDIO_PENDING_GRACE_MS = 15 * 6e4;
|
|
36713
36901
|
function parseStdioIdleTimeoutMs(raw = process.env["PAPI_STDIO_IDLE_TIMEOUT_MS"]) {
|
|
36714
36902
|
if (raw === void 0 || raw.trim() === "") return DEFAULT_STDIO_IDLE_TIMEOUT_MS;
|
|
36715
36903
|
const timeoutMs = Number(raw);
|
|
@@ -36720,10 +36908,26 @@ function parseStdioIdleTimeoutMs(raw = process.env["PAPI_STDIO_IDLE_TIMEOUT_MS"]
|
|
|
36720
36908
|
}
|
|
36721
36909
|
return timeoutMs;
|
|
36722
36910
|
}
|
|
36723
|
-
function startStdioIdleTimeout(input, timeoutMs, onTimeout) {
|
|
36911
|
+
function startStdioIdleTimeout(input, timeoutMs, onTimeout, isIdle = () => true) {
|
|
36724
36912
|
if (timeoutMs === 0) return () => void 0;
|
|
36725
36913
|
let timer2;
|
|
36726
36914
|
let stopped = false;
|
|
36915
|
+
let pendingSince;
|
|
36916
|
+
const schedule = () => {
|
|
36917
|
+
timer2 = setTimeout(() => {
|
|
36918
|
+
if (stopped) return;
|
|
36919
|
+
if (!isIdle()) {
|
|
36920
|
+
pendingSince ??= Date.now();
|
|
36921
|
+
if (Date.now() - pendingSince < DEFAULT_STDIO_PENDING_GRACE_MS) {
|
|
36922
|
+
schedule();
|
|
36923
|
+
return;
|
|
36924
|
+
}
|
|
36925
|
+
}
|
|
36926
|
+
stopped = true;
|
|
36927
|
+
input.off("data", refresh);
|
|
36928
|
+
onTimeout();
|
|
36929
|
+
}, timeoutMs);
|
|
36930
|
+
};
|
|
36727
36931
|
const stop = () => {
|
|
36728
36932
|
if (stopped) return;
|
|
36729
36933
|
stopped = true;
|
|
@@ -36733,12 +36937,8 @@ function startStdioIdleTimeout(input, timeoutMs, onTimeout) {
|
|
|
36733
36937
|
const refresh = () => {
|
|
36734
36938
|
if (stopped) return;
|
|
36735
36939
|
if (timer2 !== void 0) clearTimeout(timer2);
|
|
36736
|
-
|
|
36737
|
-
|
|
36738
|
-
stopped = true;
|
|
36739
|
-
input.off("data", refresh);
|
|
36740
|
-
onTimeout();
|
|
36741
|
-
}, timeoutMs);
|
|
36940
|
+
pendingSince = void 0;
|
|
36941
|
+
schedule();
|
|
36742
36942
|
};
|
|
36743
36943
|
input.on("data", refresh);
|
|
36744
36944
|
refresh();
|
|
@@ -36968,7 +37168,8 @@ if (isHttpMode && httpPort !== void 0) {
|
|
|
36968
37168
|
parseStdioIdleTimeoutMs(),
|
|
36969
37169
|
() => {
|
|
36970
37170
|
void gracefulShutdown("idle-timeout");
|
|
36971
|
-
}
|
|
37171
|
+
},
|
|
37172
|
+
() => !hasPendingStdioPrepare()
|
|
36972
37173
|
);
|
|
36973
37174
|
process.stderr.write(`${formatConnectionBanner({
|
|
36974
37175
|
serverName: config.serverName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.113",
|
|
4
4
|
"description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"mcpName": "io.github.getpapi/papi",
|