@papi-ai/server 0.7.108 → 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 +11 -0
- package/dist/index.js +325 -77
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,17 @@ PAPI collects anonymous usage data (tool name, duration, project UUID — no cod
|
|
|
43
43
|
|
|
44
44
|
PAPI is configured with `PAPI_PROJECT_ID` and `PAPI_DATA_API_KEY` — both are generated by the onboarding wizard at [getpapi.ai](https://getpapi.ai/) and pasted into your `.mcp.json`. If those env vars aren't set, PAPI will fall back to local file storage (md mode) and emit a stderr warning that your cycles aren't visible on the dashboard. To get on the dashboard, sign up at [getpapi.ai](https://getpapi.ai/) and use the config it gives you.
|
|
45
45
|
|
|
46
|
+
### Stdio idle shutdown
|
|
47
|
+
|
|
48
|
+
The stdio server exits cleanly after two minutes without input, including its
|
|
49
|
+
database adapter pool, so abandoned client sessions do not accumulate. Set
|
|
50
|
+
`PAPI_STDIO_IDLE_TIMEOUT_MS=0` to disable this behavior, or provide another
|
|
51
|
+
non-negative millisecond value in the `.mcp.json` environment block. This
|
|
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.
|
|
56
|
+
|
|
46
57
|
## License
|
|
47
58
|
|
|
48
59
|
[Elastic License 2.0](https://www.elastic.co/licensing/elastic-license) — free to use, self-host, and modify. Commercial hosting requires a license.
|
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
|
|
|
@@ -7558,9 +7563,10 @@ function resolveServerName(serverName) {
|
|
|
7558
7563
|
const candidate = serverName?.trim();
|
|
7559
7564
|
return candidate && SAFE_SERVER_NAME.test(candidate) ? candidate : DEFAULT_SERVER_NAME;
|
|
7560
7565
|
}
|
|
7561
|
-
function formatConnectionBanner(identity) {
|
|
7566
|
+
function formatConnectionBanner(identity, health = "connected") {
|
|
7562
7567
|
const projectIdShort = identity.projectId ? ` (${identity.projectId.slice(0, 8)})` : "";
|
|
7563
|
-
|
|
7568
|
+
const lead = health === "degraded" ? "Degraded \u2014 data may be stale" : health === "offline" ? "Offline \u2014 no database connection" : "Connected";
|
|
7569
|
+
return `[papi] ${lead} \u2014 server: ${resolveServerName(identity.serverName)}, project: ${identity.projectName}${projectIdShort}, adapter: ${identity.adapterType}, v${identity.pkgVersion}`;
|
|
7564
7570
|
}
|
|
7565
7571
|
|
|
7566
7572
|
// src/server.ts
|
|
@@ -11034,7 +11040,7 @@ function isBlockerResolved(blocker, ctx) {
|
|
|
11034
11040
|
if (decision?.outcome && resolvedOutcomes.has(decision.outcome)) return true;
|
|
11035
11041
|
if (decision?.resolutionState === "resolved") return true;
|
|
11036
11042
|
const hasRecentEvent = ctx.decisionEvents.some(
|
|
11037
|
-
(e) => idMatches(e.decisionId, blocker.ref) && e.cycle >= blocker.blockedCycle && !PROPOSAL_EVENT_TYPES.includes(e.eventType)
|
|
11043
|
+
(e) => idMatches(e.decisionId, blocker.ref) && e.cycle >= blocker.blockedCycle && !PROPOSAL_EVENT_TYPES.includes(e.eventType) && e.source !== "build_complete"
|
|
11038
11044
|
);
|
|
11039
11045
|
return hasRecentEvent;
|
|
11040
11046
|
}
|
|
@@ -11240,6 +11246,20 @@ function determineContextTier(cycleCount) {
|
|
|
11240
11246
|
if (cycleCount <= 20) return 2;
|
|
11241
11247
|
return 3;
|
|
11242
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
|
+
}
|
|
11243
11263
|
function applyContextTier(ctx, cycleCount) {
|
|
11244
11264
|
const tier = determineContextTier(cycleCount);
|
|
11245
11265
|
const label = tier === 1 ? "Tier 1 (cycles 1-5)" : tier === 2 ? "Tier 2 (cycles 6-20)" : "Tier 3 (cycles 21+)";
|
|
@@ -11993,6 +12013,7 @@ ${lines.join("\n")}`;
|
|
|
11993
12013
|
siblingRepoWarning
|
|
11994
12014
|
};
|
|
11995
12015
|
const { label: leanTierLabel } = applyContextTier(ctx2, health.totalCycles);
|
|
12016
|
+
trimContextForFirstCycle(ctx2, health.totalCycles);
|
|
11996
12017
|
ctx2.contextTier = leanTierLabel;
|
|
11997
12018
|
console.error(`[plan-perf] context tier: ${leanTierLabel} (cycle ${health.totalCycles})`);
|
|
11998
12019
|
t = startTimer();
|
|
@@ -12190,6 +12211,7 @@ ${logLines}`);
|
|
|
12190
12211
|
siblingRepoWarning
|
|
12191
12212
|
};
|
|
12192
12213
|
const { label: fullTierLabel } = applyContextTier(ctx, health.totalCycles);
|
|
12214
|
+
trimContextForFirstCycle(ctx, health.totalCycles);
|
|
12193
12215
|
ctx.contextTier = fullTierLabel;
|
|
12194
12216
|
console.error(`[plan-perf] context tier: ${fullTierLabel} (cycle ${health.totalCycles})`);
|
|
12195
12217
|
const prevHashes = contextHashesResultFull.status === "fulfilled" ? contextHashesResultFull.value : null;
|
|
@@ -12817,15 +12839,20 @@ async function assertSingleActiveCycle(adapter2, opts = {}) {
|
|
|
12817
12839
|
for (const c of cycles) {
|
|
12818
12840
|
if (!newestByNumber.has(c.number)) newestByNumber.set(c.number, c);
|
|
12819
12841
|
}
|
|
12820
|
-
const
|
|
12821
|
-
|
|
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;
|
|
12822
12849
|
if (!opts.autoComplete) {
|
|
12823
12850
|
const nums = blocking.map((c) => c.number).join(", ");
|
|
12824
12851
|
throw new Error(
|
|
12825
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.`
|
|
12826
12853
|
);
|
|
12827
12854
|
}
|
|
12828
|
-
const notes = [];
|
|
12855
|
+
const notes = [...phantomNotes];
|
|
12829
12856
|
for (const stale of blocking) {
|
|
12830
12857
|
await adapter2.createCycle({
|
|
12831
12858
|
...stale,
|
|
@@ -12860,6 +12887,18 @@ async function validateAndPrepare(adapter2, force, callerUserId, adapterType) {
|
|
|
12860
12887
|
const health = await adapter2.getCycleHealth();
|
|
12861
12888
|
cycleNumber = health.totalCycles;
|
|
12862
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
|
+
}
|
|
12863
12902
|
const blockingCycle = callerUserId ? await resolveCallerLatestCycle(adapter2, callerUserId) : void 0;
|
|
12864
12903
|
const latestStatus = callerUserId ? blockingCycle?.status : health.latestCycleStatus;
|
|
12865
12904
|
const blockingNumber = blockingCycle?.number ?? cycleNumber;
|
|
@@ -12950,6 +12989,11 @@ function assertApplyPayloadNonEmpty(data, cycleNumber) {
|
|
|
12950
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).`
|
|
12951
12990
|
);
|
|
12952
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
|
+
}
|
|
12953
12997
|
}
|
|
12954
12998
|
var VALID_PLAN_PRIORITIES = /* @__PURE__ */ new Set(["P0 Critical", "P1 High", "P2 Medium", "P3 Low"]);
|
|
12955
12999
|
function assertPlanPrioritiesValid(data) {
|
|
@@ -13086,7 +13130,7 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
|
|
|
13086
13130
|
decisionConflicts
|
|
13087
13131
|
};
|
|
13088
13132
|
}
|
|
13089
|
-
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) {
|
|
13090
13134
|
const prepareTimer = startTimer();
|
|
13091
13135
|
tracker?.mark("validate_and_prepare");
|
|
13092
13136
|
let t = startTimer();
|
|
@@ -13114,10 +13158,43 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
13114
13158
|
tracker?.mark("handoffs_only_assemble");
|
|
13115
13159
|
t = startTimer();
|
|
13116
13160
|
const targetCycle = cycleNumber + 1;
|
|
13117
|
-
const allTasks = await adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready", "In Progress"] });
|
|
13118
|
-
|
|
13119
|
-
if (
|
|
13120
|
-
|
|
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
|
+
}
|
|
13121
13198
|
}
|
|
13122
13199
|
const [decisions, reports, brief] = await Promise.all([
|
|
13123
13200
|
adapter2.getActiveDecisions(),
|
|
@@ -13759,6 +13836,37 @@ function planDelivery(input) {
|
|
|
13759
13836
|
}
|
|
13760
13837
|
}
|
|
13761
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
|
+
|
|
13762
13870
|
// src/services/session-guidance.ts
|
|
13763
13871
|
var DEFAULT_CALLER_KEY = "__default__";
|
|
13764
13872
|
var sessionStates = /* @__PURE__ */ new Map();
|
|
@@ -13835,7 +13943,7 @@ function markOrient(callerKey) {
|
|
|
13835
13943
|
}
|
|
13836
13944
|
function getProjectConnectionBanner(projectName, projectSlug) {
|
|
13837
13945
|
if (!projectName || !projectSlug) return null;
|
|
13838
|
-
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.`;
|
|
13839
13947
|
}
|
|
13840
13948
|
function detectContextDegradation(now = Date.now(), callerKey) {
|
|
13841
13949
|
const state = getState(callerKey);
|
|
@@ -14018,6 +14126,9 @@ function savePrepareContextFile(projectId, callerKey, content) {
|
|
|
14018
14126
|
|
|
14019
14127
|
// src/tools/plan.ts
|
|
14020
14128
|
var planPrepareCache = new PerCallerCache();
|
|
14129
|
+
function hasPendingPlanPrepare() {
|
|
14130
|
+
return planPrepareCache.size() > 0;
|
|
14131
|
+
}
|
|
14021
14132
|
var planTool = {
|
|
14022
14133
|
name: "plan",
|
|
14023
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`.',
|
|
@@ -14080,6 +14191,11 @@ var planTool = {
|
|
|
14080
14191
|
type: "boolean",
|
|
14081
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."
|
|
14082
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
|
+
},
|
|
14083
14199
|
skip_handoffs: {
|
|
14084
14200
|
type: "boolean",
|
|
14085
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."
|
|
@@ -14189,7 +14305,7 @@ function formatPlanResult(result) {
|
|
|
14189
14305
|
}
|
|
14190
14306
|
return response;
|
|
14191
14307
|
}
|
|
14192
|
-
async function handlePlan(adapter2, config2, args) {
|
|
14308
|
+
async function handlePlan(adapter2, config2, args, clientName) {
|
|
14193
14309
|
const toolMode = args.mode;
|
|
14194
14310
|
const callerKey = callerKeyFromConfig(config2);
|
|
14195
14311
|
const filters = {};
|
|
@@ -14199,7 +14315,8 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
14199
14315
|
if (typeof args.priority === "string") filters.priority = args.priority;
|
|
14200
14316
|
const focus = typeof args.focus === "string" ? args.focus : void 0;
|
|
14201
14317
|
const force = args.force === true;
|
|
14202
|
-
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;
|
|
14203
14320
|
const density = args.density === "light" || args.density === "standard" || args.density === "deep" ? args.density : void 0;
|
|
14204
14321
|
const tracker = new ProgressTracker(toolMode === "apply" ? "apply_validate" : "prepare_validate").bindStream(adapter2, { stage: "plan" });
|
|
14205
14322
|
try {
|
|
@@ -14281,7 +14398,7 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
14281
14398
|
} catch {
|
|
14282
14399
|
}
|
|
14283
14400
|
const skipHandoffs = args.skip_handoffs === true;
|
|
14284
|
-
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);
|
|
14285
14402
|
const prepareState = {
|
|
14286
14403
|
contextHashes: result.contextHashes,
|
|
14287
14404
|
userMessage: result.userMessage,
|
|
@@ -14292,7 +14409,15 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
14292
14409
|
planPrepareCache.set(callerKey, prepareState);
|
|
14293
14410
|
savePrepareSpill(adapter2.getProjectId?.(), callerKey, prepareState);
|
|
14294
14411
|
const explicit = args.dispatch === "inline" ? false : args.dispatch === "subagent" ? true : void 0;
|
|
14295
|
-
|
|
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
|
+
}
|
|
14296
14421
|
const modeLabel = result.mode === "bootstrap" ? "Bootstrap" : "Full";
|
|
14297
14422
|
const header = result.strategyReviewWarning ? `${result.strategyReviewWarning}
|
|
14298
14423
|
` : "";
|
|
@@ -14328,7 +14453,12 @@ ${result.userMessage}
|
|
|
14328
14453
|
const dispatchHeader = result.strategyReviewWarning ? `${result.strategyReviewWarning}
|
|
14329
14454
|
|
|
14330
14455
|
` : "";
|
|
14331
|
-
|
|
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 };
|
|
14332
14462
|
}
|
|
14333
14463
|
if (contextFilePath) {
|
|
14334
14464
|
const kb = result.contextBytes !== void 0 ? ` (~${(result.contextBytes / 1024).toFixed(0)} KB)` : "";
|
|
@@ -14350,8 +14480,11 @@ The full planning brief \u2014 system prompt + all context${kb} \u2014 has been
|
|
|
14350
14480
|
);
|
|
14351
14481
|
return { ...pathResponse, _contextBytes: result.contextBytes };
|
|
14352
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
|
+
` : "";
|
|
14353
14486
|
const response = textResponse(
|
|
14354
|
-
`${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})
|
|
14355
14488
|
|
|
14356
14489
|
Follow the system prompt and context below to generate a complete cycle plan.
|
|
14357
14490
|
|
|
@@ -17103,6 +17236,9 @@ function buildStrategyReviewPostDirective(cycleNumber) {
|
|
|
17103
17236
|
].join("\n");
|
|
17104
17237
|
}
|
|
17105
17238
|
var reviewPrepareCache = new PerCallerCache();
|
|
17239
|
+
function hasPendingStrategyReview() {
|
|
17240
|
+
return reviewPrepareCache.size() > 0;
|
|
17241
|
+
}
|
|
17106
17242
|
var strategyReviewTool = {
|
|
17107
17243
|
name: "strategy_review",
|
|
17108
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.',
|
|
@@ -17557,6 +17693,7 @@ ${result.userMessage}
|
|
|
17557
17693
|
}
|
|
17558
17694
|
|
|
17559
17695
|
// src/lib/task-readiness.ts
|
|
17696
|
+
var ADVISORY_GAP_LABELS = /* @__PURE__ */ new Set(["WHY NOW", "FILES LIKELY TOUCHED"]);
|
|
17560
17697
|
function hasMeaningfulEntry(value) {
|
|
17561
17698
|
return Array.isArray(value) && value.some((entry) => {
|
|
17562
17699
|
if (typeof entry === "string") return entry.trim().length > 0;
|
|
@@ -17576,23 +17713,29 @@ function getHandoffGaps(handoff) {
|
|
|
17576
17713
|
return gaps;
|
|
17577
17714
|
}
|
|
17578
17715
|
function getTaskReadiness(task, currentCycle2) {
|
|
17579
|
-
const
|
|
17580
|
-
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.` : "";
|
|
17581
17722
|
const cycleMismatch = currentCycle2 != null && currentCycle2 > 0 && task.cycle !== currentCycle2;
|
|
17582
17723
|
if (task.status === "Backlog") {
|
|
17583
17724
|
if (!handoffComplete) {
|
|
17584
17725
|
return {
|
|
17585
17726
|
label: "Needs Planning",
|
|
17586
|
-
reason: `No complete BUILD HANDOFF (${
|
|
17727
|
+
reason: `No complete BUILD HANDOFF (${blockingGaps.join(", ")}). Run plan before claiming or building this task.`,
|
|
17587
17728
|
handoffComplete,
|
|
17588
|
-
handoffGaps
|
|
17729
|
+
handoffGaps,
|
|
17730
|
+
advisoryGaps
|
|
17589
17731
|
};
|
|
17590
17732
|
}
|
|
17591
17733
|
return {
|
|
17592
17734
|
label: "Needs Planning",
|
|
17593
17735
|
reason: "Backlog work is waiting for a planning pass to assign it to a cycle before it can be built.",
|
|
17594
17736
|
handoffComplete,
|
|
17595
|
-
handoffGaps
|
|
17737
|
+
handoffGaps,
|
|
17738
|
+
advisoryGaps
|
|
17596
17739
|
};
|
|
17597
17740
|
}
|
|
17598
17741
|
if (task.status === "In Cycle" || task.status === "Ready") {
|
|
@@ -17601,22 +17744,25 @@ function getTaskReadiness(task, currentCycle2) {
|
|
|
17601
17744
|
label: "Needs Planning",
|
|
17602
17745
|
reason: `Assigned to Cycle ${task.cycle ?? "an earlier cycle"}, not the active Cycle ${currentCycle2}. Run plan before building it.`,
|
|
17603
17746
|
handoffComplete,
|
|
17604
|
-
handoffGaps
|
|
17747
|
+
handoffGaps,
|
|
17748
|
+
advisoryGaps
|
|
17605
17749
|
};
|
|
17606
17750
|
}
|
|
17607
17751
|
if (!handoffComplete) {
|
|
17608
17752
|
return {
|
|
17609
17753
|
label: "Needs Planning",
|
|
17610
|
-
reason: `Planning has not produced a complete BUILD HANDOFF (${
|
|
17754
|
+
reason: `Planning has not produced a complete BUILD HANDOFF (${blockingGaps.join(", ")}).`,
|
|
17611
17755
|
handoffComplete,
|
|
17612
|
-
handoffGaps
|
|
17756
|
+
handoffGaps,
|
|
17757
|
+
advisoryGaps
|
|
17613
17758
|
};
|
|
17614
17759
|
}
|
|
17615
17760
|
return {
|
|
17616
17761
|
label: "Ready to Build",
|
|
17617
|
-
reason:
|
|
17762
|
+
reason: `Task is assigned to the active cycle and has a complete BUILD HANDOFF.${advisoryNote}`,
|
|
17618
17763
|
handoffComplete,
|
|
17619
|
-
handoffGaps
|
|
17764
|
+
handoffGaps,
|
|
17765
|
+
advisoryGaps
|
|
17620
17766
|
};
|
|
17621
17767
|
}
|
|
17622
17768
|
const labelByStatus = {
|
|
@@ -17632,7 +17778,8 @@ function getTaskReadiness(task, currentCycle2) {
|
|
|
17632
17778
|
label,
|
|
17633
17779
|
reason: label === "Needs Planning" ? "Task is not in a buildable lifecycle state." : `Task status is ${task.status}.`,
|
|
17634
17780
|
handoffComplete,
|
|
17635
|
-
handoffGaps
|
|
17781
|
+
handoffGaps,
|
|
17782
|
+
advisoryGaps
|
|
17636
17783
|
};
|
|
17637
17784
|
}
|
|
17638
17785
|
|
|
@@ -18243,6 +18390,20 @@ async function handleBoardView(adapter2, args) {
|
|
|
18243
18390
|
fields,
|
|
18244
18391
|
membership.isMultiMember ? assigneeColumn(membership.nameOf) : null
|
|
18245
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
|
+
}
|
|
18246
18407
|
if (!fields) {
|
|
18247
18408
|
try {
|
|
18248
18409
|
const comments = await adapter2.getRecentTaskComments?.(30);
|
|
@@ -20962,37 +21123,6 @@ function pendingDecisionBlocker(attempts, cycle, taskId) {
|
|
|
20962
21123
|
};
|
|
20963
21124
|
}
|
|
20964
21125
|
|
|
20965
|
-
// src/lib/harness-capability.ts
|
|
20966
|
-
var HARNESS_REGISTRY = {
|
|
20967
|
-
// Local stdio CLI agents — PAPI runs git on the user's machine. Confirmed in telemetry.
|
|
20968
|
-
"claude-code": { build: true, label: "Claude Code" },
|
|
20969
|
-
"opencode": { build: true, label: "opencode" },
|
|
20970
|
-
"zcode": { build: true, label: "zcode" },
|
|
20971
|
-
"codex": { build: true, label: "Codex" },
|
|
20972
|
-
"cursor": { build: true, label: "Cursor" },
|
|
20973
|
-
// Cloud harnesses with code sandboxes — their agent builds in-sandbox over HTTP+OAuth.
|
|
20974
|
-
"lovable": { build: true, label: "Lovable" },
|
|
20975
|
-
"bolt": { build: true, label: "Bolt" },
|
|
20976
|
-
"replit": { build: true, label: "Replit" },
|
|
20977
|
-
// Chat-only surfaces, no sandbox — planning only.
|
|
20978
|
-
"chatgpt": { build: false, label: "ChatGPT" },
|
|
20979
|
-
"claude.ai": { build: false, label: "Claude.ai" },
|
|
20980
|
-
"claude-desktop": { build: false, label: "Claude Desktop" }
|
|
20981
|
-
};
|
|
20982
|
-
var UNKNOWN_DEFAULT = { build: false, label: "your tool" };
|
|
20983
|
-
function detectHarness(clientName) {
|
|
20984
|
-
const raw = clientName?.trim() || null;
|
|
20985
|
-
if (!raw) {
|
|
20986
|
-
return { ...UNKNOWN_DEFAULT, raw: null, key: null, known: false };
|
|
20987
|
-
}
|
|
20988
|
-
const norm = raw.toLowerCase();
|
|
20989
|
-
const key = HARNESS_REGISTRY[norm] ? norm : Object.keys(HARNESS_REGISTRY).find((k) => norm.includes(k)) ?? null;
|
|
20990
|
-
if (!key) {
|
|
20991
|
-
return { ...UNKNOWN_DEFAULT, label: raw, raw, key: null, known: false };
|
|
20992
|
-
}
|
|
20993
|
-
return { ...HARNESS_REGISTRY[key], raw, key, known: true };
|
|
20994
|
-
}
|
|
20995
|
-
|
|
20996
21126
|
// src/lib/harness-build-steps.ts
|
|
20997
21127
|
init_git();
|
|
20998
21128
|
function startBuildSteps(branch, base, taskId) {
|
|
@@ -25566,6 +25696,9 @@ init_git();
|
|
|
25566
25696
|
|
|
25567
25697
|
// src/tools/handoff.ts
|
|
25568
25698
|
var handoffPrepareCache = new PerCallerCache();
|
|
25699
|
+
function hasPendingHandoffGenerate() {
|
|
25700
|
+
return handoffPrepareCache.size() > 0;
|
|
25701
|
+
}
|
|
25569
25702
|
var handoffGenerateTool = {
|
|
25570
25703
|
name: "handoff_generate",
|
|
25571
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.",
|
|
@@ -31770,10 +31903,14 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
31770
31903
|
lines.push("Board is empty \u2014 run `plan` to create your next cycle.");
|
|
31771
31904
|
}
|
|
31772
31905
|
lines.push("");
|
|
31906
|
+
lines.push(WORK_DUMP_ROUTING_RULE);
|
|
31907
|
+
lines.push("");
|
|
31773
31908
|
} else if (buildInfo.noHandoffs) {
|
|
31774
31909
|
lines.push("## Tasks");
|
|
31775
31910
|
lines.push("No tasks with BUILD HANDOFFs \u2014 run `plan` to generate cycle tasks.");
|
|
31776
31911
|
lines.push("");
|
|
31912
|
+
lines.push(WORK_DUMP_ROUTING_RULE);
|
|
31913
|
+
lines.push("");
|
|
31777
31914
|
} else {
|
|
31778
31915
|
lines.push(`## Tasks with Handoffs`);
|
|
31779
31916
|
lines.push(`${buildInfo.totalHandoffs} total \u2014 ${buildInfo.inProgress.length} in progress, ${buildInfo.backlogCount} backlog, ${buildInfo.blockedCount} blocked`);
|
|
@@ -31804,6 +31941,7 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
31804
31941
|
return lines.join("\n").trimEnd();
|
|
31805
31942
|
}
|
|
31806
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._';
|
|
31807
31945
|
function emptyHealthSummary() {
|
|
31808
31946
|
return {
|
|
31809
31947
|
cycleNumber: 0,
|
|
@@ -32159,12 +32297,34 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
|
|
|
32159
32297
|
cycleSince: currentCycle2,
|
|
32160
32298
|
compact: true
|
|
32161
32299
|
}).catch(() => []) : [];
|
|
32162
|
-
const
|
|
32163
|
-
const cycleCounts = computeCycleTaskCounts(cycleTasksAll, currentCycle2, cycleIsComplete);
|
|
32300
|
+
const cycleCounts = computeCycleTaskCounts(cycleTasksAllRaw, currentCycle2, cycleIsComplete);
|
|
32164
32301
|
const { inProgress: cycleInProgress, inReview: cycleInReview, inCycle: cycleInCycle, ready: cycleReady, backlog: cycleBacklog, total: cycleTotal, done: cycleDone } = cycleCounts;
|
|
32165
32302
|
if (!cycleIsComplete && cycleTotal === 0 && cycleDone > 0) {
|
|
32166
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).`);
|
|
32167
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
|
+
}
|
|
32168
32328
|
tracker.mark("parallel-reads");
|
|
32169
32329
|
const inProgressItems = buildResult.inProgress.map(
|
|
32170
32330
|
(t) => `- **${t.id}:** ${t.title} (${t.priority} | ${t.complexity})`
|
|
@@ -32781,23 +32941,23 @@ ${formatRuntimeIdentity(getRuntimeIdentity(config2, serverVersion))}`;
|
|
|
32781
32941
|
}
|
|
32782
32942
|
}
|
|
32783
32943
|
function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, clientName) {
|
|
32784
|
-
|
|
32944
|
+
const targetFile = shouldWriteClaudeMd(clientName) ? "CLAUDE.md" : "AGENTS.md";
|
|
32785
32945
|
if (adapterType === "proxy") {
|
|
32786
32946
|
const additions2 = [];
|
|
32787
32947
|
if (cycleNumber >= 6) additions2.push(CLAUDE_MD_TIER_1);
|
|
32788
32948
|
if (cycleNumber >= 21) additions2.push(CLAUDE_MD_TIER_2);
|
|
32789
32949
|
if (additions2.length === 0) return "";
|
|
32790
|
-
collector.add({ path:
|
|
32950
|
+
collector.add({ path: targetFile, content: additions2.join(""), mode: "append" });
|
|
32791
32951
|
const tierNames2 = [];
|
|
32792
32952
|
if (cycleNumber >= 6) tierNames2.push("Established (batch building, strategy reviews, AD lifecycle)");
|
|
32793
32953
|
if (cycleNumber >= 21) tierNames2.push("Mature (idea pipeline, doc registry, advanced patterns)");
|
|
32794
32954
|
return `
|
|
32795
32955
|
|
|
32796
|
-
\u{1F4DD}
|
|
32956
|
+
\u{1F4DD} **${targetFile} enriched** \u2014 added ${tierNames2.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
|
|
32797
32957
|
}
|
|
32798
|
-
const
|
|
32799
|
-
if (!existsSync11(
|
|
32800
|
-
const content = readFileSync13(
|
|
32958
|
+
const targetPath = join18(projectRoot, targetFile);
|
|
32959
|
+
if (!existsSync11(targetPath)) return "";
|
|
32960
|
+
const content = readFileSync13(targetPath, "utf-8");
|
|
32801
32961
|
const additions = [];
|
|
32802
32962
|
if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
|
|
32803
32963
|
additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_1));
|
|
@@ -32806,13 +32966,13 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
|
|
|
32806
32966
|
additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_2));
|
|
32807
32967
|
}
|
|
32808
32968
|
if (additions.length === 0) return "";
|
|
32809
|
-
writeFileSync7(
|
|
32969
|
+
writeFileSync7(targetPath, content + additions.join(""), "utf-8");
|
|
32810
32970
|
const tierNames = [];
|
|
32811
32971
|
if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1))) tierNames.push("Established (batch building, strategy reviews, AD lifecycle)");
|
|
32812
32972
|
if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T2))) tierNames.push("Mature (idea pipeline, doc registry, advanced patterns)");
|
|
32813
32973
|
return `
|
|
32814
32974
|
|
|
32815
|
-
\u{1F4DD}
|
|
32975
|
+
\u{1F4DD} **${targetFile} enriched** \u2014 added ${tierNames.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
|
|
32816
32976
|
}
|
|
32817
32977
|
|
|
32818
32978
|
// src/tools/hierarchy.ts
|
|
@@ -35560,6 +35720,9 @@ var PAPI_TOOLS = [
|
|
|
35560
35720
|
function getQualifiedPapiTools() {
|
|
35561
35721
|
return qualifyTools(PAPI_TOOLS);
|
|
35562
35722
|
}
|
|
35723
|
+
function hasPendingStdioPrepare() {
|
|
35724
|
+
return hasPendingPlanPrepare() || hasPendingStrategyReview() || hasPendingHandoffGenerate();
|
|
35725
|
+
}
|
|
35563
35726
|
function getToolMetadata() {
|
|
35564
35727
|
return PAPI_TOOLS.map((t) => ({ name: t.name, description: t.description }));
|
|
35565
35728
|
}
|
|
@@ -35684,7 +35847,7 @@ function createServer(adapter2, config2, observation) {
|
|
|
35684
35847
|
const runHandler = async () => {
|
|
35685
35848
|
switch (name) {
|
|
35686
35849
|
case "plan":
|
|
35687
|
-
return handlePlan(scopedAdapter, scopedConfig, safeArgs);
|
|
35850
|
+
return handlePlan(scopedAdapter, scopedConfig, safeArgs, server2.getClientVersion()?.name);
|
|
35688
35851
|
case "strategy_review":
|
|
35689
35852
|
return handleStrategyReview(scopedAdapter, scopedConfig, safeArgs);
|
|
35690
35853
|
case "strategy_change":
|
|
@@ -36454,11 +36617,36 @@ function startHttpTransport(opts) {
|
|
|
36454
36617
|
}
|
|
36455
36618
|
try {
|
|
36456
36619
|
parsedBody = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
36457
|
-
observation?.setTool(extractToolName(parsedBody) ?? extractRpcMethod(parsedBody));
|
|
36458
36620
|
} catch {
|
|
36459
36621
|
sendError(res, { status: 400, body: { error: "Invalid JSON body" } });
|
|
36460
36622
|
return;
|
|
36461
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));
|
|
36462
36650
|
}
|
|
36463
36651
|
logEvent({
|
|
36464
36652
|
level: "info",
|
|
@@ -36707,6 +36895,56 @@ function shouldCheckForUpdate(version, selfHostFlag) {
|
|
|
36707
36895
|
return version !== "unknown" && !isSelfHostedDeployment(selfHostFlag);
|
|
36708
36896
|
}
|
|
36709
36897
|
|
|
36898
|
+
// src/lib/stdio-idle-timeout.ts
|
|
36899
|
+
var DEFAULT_STDIO_IDLE_TIMEOUT_MS = 12e4;
|
|
36900
|
+
var DEFAULT_STDIO_PENDING_GRACE_MS = 15 * 6e4;
|
|
36901
|
+
function parseStdioIdleTimeoutMs(raw = process.env["PAPI_STDIO_IDLE_TIMEOUT_MS"]) {
|
|
36902
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_STDIO_IDLE_TIMEOUT_MS;
|
|
36903
|
+
const timeoutMs = Number(raw);
|
|
36904
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 0) {
|
|
36905
|
+
throw new Error(
|
|
36906
|
+
`PAPI_STDIO_IDLE_TIMEOUT_MS must be a non-negative integer in milliseconds; received ${JSON.stringify(raw)}`
|
|
36907
|
+
);
|
|
36908
|
+
}
|
|
36909
|
+
return timeoutMs;
|
|
36910
|
+
}
|
|
36911
|
+
function startStdioIdleTimeout(input, timeoutMs, onTimeout, isIdle = () => true) {
|
|
36912
|
+
if (timeoutMs === 0) return () => void 0;
|
|
36913
|
+
let timer2;
|
|
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
|
+
};
|
|
36931
|
+
const stop = () => {
|
|
36932
|
+
if (stopped) return;
|
|
36933
|
+
stopped = true;
|
|
36934
|
+
if (timer2 !== void 0) clearTimeout(timer2);
|
|
36935
|
+
input.off("data", refresh);
|
|
36936
|
+
};
|
|
36937
|
+
const refresh = () => {
|
|
36938
|
+
if (stopped) return;
|
|
36939
|
+
if (timer2 !== void 0) clearTimeout(timer2);
|
|
36940
|
+
pendingSince = void 0;
|
|
36941
|
+
schedule();
|
|
36942
|
+
};
|
|
36943
|
+
input.on("data", refresh);
|
|
36944
|
+
refresh();
|
|
36945
|
+
return stop;
|
|
36946
|
+
}
|
|
36947
|
+
|
|
36710
36948
|
// src/index.ts
|
|
36711
36949
|
init_dist();
|
|
36712
36950
|
var __dirname = dirname7(fileURLToPath5(import.meta.url));
|
|
@@ -36808,7 +37046,7 @@ async function gracefulShutdown(signal) {
|
|
|
36808
37046
|
} catch (err) {
|
|
36809
37047
|
console.error(`[papi] Adapter close failed during ${signal}: ${err instanceof Error ? err.message : String(err)}`);
|
|
36810
37048
|
}
|
|
36811
|
-
process.exit(signal === "SIGTERM" || signal === "SIGINT" ? 0 : 1);
|
|
37049
|
+
process.exit(signal === "SIGTERM" || signal === "SIGINT" || signal === "idle-timeout" ? 0 : 1);
|
|
36812
37050
|
}
|
|
36813
37051
|
process.on("SIGTERM", () => {
|
|
36814
37052
|
void gracefulShutdown("SIGTERM");
|
|
@@ -36922,13 +37160,23 @@ if (isHttpMode && httpPort !== void 0) {
|
|
|
36922
37160
|
} catch {
|
|
36923
37161
|
}
|
|
36924
37162
|
const transport = new StdioServerTransport();
|
|
37163
|
+
let stopStdioIdleTimeout = () => void 0;
|
|
37164
|
+
transport.onclose = () => stopStdioIdleTimeout();
|
|
36925
37165
|
await server.connect(transport);
|
|
37166
|
+
stopStdioIdleTimeout = startStdioIdleTimeout(
|
|
37167
|
+
process.stdin,
|
|
37168
|
+
parseStdioIdleTimeoutMs(),
|
|
37169
|
+
() => {
|
|
37170
|
+
void gracefulShutdown("idle-timeout");
|
|
37171
|
+
},
|
|
37172
|
+
() => !hasPendingStdioPrepare()
|
|
37173
|
+
);
|
|
36926
37174
|
process.stderr.write(`${formatConnectionBanner({
|
|
36927
37175
|
serverName: config.serverName,
|
|
36928
37176
|
projectName: basename2(config.projectRoot),
|
|
36929
37177
|
projectId: config.projectId ?? process.env.PAPI_PROJECT_ID,
|
|
36930
37178
|
adapterType: config.adapterType,
|
|
36931
37179
|
pkgVersion
|
|
36932
|
-
})}
|
|
37180
|
+
}, getConnectionStatus())}
|
|
36933
37181
|
`);
|
|
36934
37182
|
}
|
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",
|