@papi-ai/server 0.7.73 → 0.7.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backfill-cycle-metrics.js +39 -0
- package/dist/index.js +375 -28
- package/dist/prompts.js +4 -4
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ __export(git_exports, {
|
|
|
14
14
|
AUTO_WRITTEN_PATHS: () => AUTO_WRITTEN_PATHS,
|
|
15
15
|
branchExists: () => branchExists,
|
|
16
16
|
checkoutBranch: () => checkoutBranch,
|
|
17
|
+
commitSinglePath: () => commitSinglePath,
|
|
17
18
|
commitStagedOnly: () => commitStagedOnly,
|
|
18
19
|
createAndCheckoutBranch: () => createAndCheckoutBranch,
|
|
19
20
|
createPullRequest: () => createPullRequest,
|
|
@@ -59,6 +60,8 @@ __export(git_exports, {
|
|
|
59
60
|
isGhAvailable: () => isGhAvailable,
|
|
60
61
|
isGitAvailable: () => isGitAvailable,
|
|
61
62
|
isGitRepo: () => isGitRepo,
|
|
63
|
+
isPathIgnored: () => isPathIgnored,
|
|
64
|
+
isPathTracked: () => isPathTracked,
|
|
62
65
|
listGroupedCycleBranches: () => listGroupedCycleBranches,
|
|
63
66
|
listOpenPullRequests: () => listOpenPullRequests,
|
|
64
67
|
listOrphanFeatBranches: () => listOrphanFeatBranches,
|
|
@@ -96,6 +99,42 @@ function isGitRepo(cwd) {
|
|
|
96
99
|
return false;
|
|
97
100
|
}
|
|
98
101
|
}
|
|
102
|
+
function isPathTracked(cwd, path3) {
|
|
103
|
+
try {
|
|
104
|
+
execFileSync("git", ["ls-files", "--error-unmatch", "--", path3], {
|
|
105
|
+
cwd,
|
|
106
|
+
stdio: "ignore"
|
|
107
|
+
});
|
|
108
|
+
return true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function isPathIgnored(cwd, path3) {
|
|
114
|
+
const r = spawnSync("git", ["check-ignore", "-q", "--", path3], { cwd, stdio: "ignore" });
|
|
115
|
+
return r.status === 0;
|
|
116
|
+
}
|
|
117
|
+
function commitSinglePath(cwd, path3, message) {
|
|
118
|
+
const add = spawnSync("git", ["add", "--", path3], { cwd, encoding: "utf-8" });
|
|
119
|
+
if (add.status !== 0) {
|
|
120
|
+
return { committed: false, message: (add.stderr || "git add failed").trim() };
|
|
121
|
+
}
|
|
122
|
+
const pending = spawnSync("git", ["diff", "--cached", "--name-only", "--", path3], {
|
|
123
|
+
cwd,
|
|
124
|
+
encoding: "utf-8"
|
|
125
|
+
});
|
|
126
|
+
if (pending.status !== 0 || !(pending.stdout ?? "").trim()) {
|
|
127
|
+
return { committed: false, message: "No changes to commit." };
|
|
128
|
+
}
|
|
129
|
+
const commit = spawnSync("git", ["commit", "-m", message, "--", path3], {
|
|
130
|
+
cwd,
|
|
131
|
+
encoding: "utf-8"
|
|
132
|
+
});
|
|
133
|
+
if (commit.status !== 0) {
|
|
134
|
+
return { committed: false, message: (commit.stderr || "git commit failed").trim() };
|
|
135
|
+
}
|
|
136
|
+
return { committed: true, message };
|
|
137
|
+
}
|
|
99
138
|
function stageDirAndCommit(cwd, dir, message) {
|
|
100
139
|
try {
|
|
101
140
|
execFileSync("git", ["check-ignore", "-q", dir], { cwd });
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ __export(git_exports, {
|
|
|
15
15
|
AUTO_WRITTEN_PATHS: () => AUTO_WRITTEN_PATHS,
|
|
16
16
|
branchExists: () => branchExists,
|
|
17
17
|
checkoutBranch: () => checkoutBranch,
|
|
18
|
+
commitSinglePath: () => commitSinglePath,
|
|
18
19
|
commitStagedOnly: () => commitStagedOnly,
|
|
19
20
|
createAndCheckoutBranch: () => createAndCheckoutBranch,
|
|
20
21
|
createPullRequest: () => createPullRequest,
|
|
@@ -60,6 +61,8 @@ __export(git_exports, {
|
|
|
60
61
|
isGhAvailable: () => isGhAvailable,
|
|
61
62
|
isGitAvailable: () => isGitAvailable,
|
|
62
63
|
isGitRepo: () => isGitRepo,
|
|
64
|
+
isPathIgnored: () => isPathIgnored,
|
|
65
|
+
isPathTracked: () => isPathTracked,
|
|
63
66
|
listGroupedCycleBranches: () => listGroupedCycleBranches,
|
|
64
67
|
listOpenPullRequests: () => listOpenPullRequests,
|
|
65
68
|
listOrphanFeatBranches: () => listOrphanFeatBranches,
|
|
@@ -97,6 +100,42 @@ function isGitRepo(cwd) {
|
|
|
97
100
|
return false;
|
|
98
101
|
}
|
|
99
102
|
}
|
|
103
|
+
function isPathTracked(cwd, path7) {
|
|
104
|
+
try {
|
|
105
|
+
execFileSync("git", ["ls-files", "--error-unmatch", "--", path7], {
|
|
106
|
+
cwd,
|
|
107
|
+
stdio: "ignore"
|
|
108
|
+
});
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function isPathIgnored(cwd, path7) {
|
|
115
|
+
const r = spawnSync("git", ["check-ignore", "-q", "--", path7], { cwd, stdio: "ignore" });
|
|
116
|
+
return r.status === 0;
|
|
117
|
+
}
|
|
118
|
+
function commitSinglePath(cwd, path7, message) {
|
|
119
|
+
const add = spawnSync("git", ["add", "--", path7], { cwd, encoding: "utf-8" });
|
|
120
|
+
if (add.status !== 0) {
|
|
121
|
+
return { committed: false, message: (add.stderr || "git add failed").trim() };
|
|
122
|
+
}
|
|
123
|
+
const pending = spawnSync("git", ["diff", "--cached", "--name-only", "--", path7], {
|
|
124
|
+
cwd,
|
|
125
|
+
encoding: "utf-8"
|
|
126
|
+
});
|
|
127
|
+
if (pending.status !== 0 || !(pending.stdout ?? "").trim()) {
|
|
128
|
+
return { committed: false, message: "No changes to commit." };
|
|
129
|
+
}
|
|
130
|
+
const commit = spawnSync("git", ["commit", "-m", message, "--", path7], {
|
|
131
|
+
cwd,
|
|
132
|
+
encoding: "utf-8"
|
|
133
|
+
});
|
|
134
|
+
if (commit.status !== 0) {
|
|
135
|
+
return { committed: false, message: (commit.stderr || "git commit failed").trim() };
|
|
136
|
+
}
|
|
137
|
+
return { committed: true, message };
|
|
138
|
+
}
|
|
100
139
|
function stageDirAndCommit(cwd, dir, message) {
|
|
101
140
|
try {
|
|
102
141
|
execFileSync("git", ["check-ignore", "-q", dir], { cwd });
|
|
@@ -5002,7 +5041,7 @@ var setup_exports = {};
|
|
|
5002
5041
|
__export(setup_exports, {
|
|
5003
5042
|
runSetup: () => runSetup
|
|
5004
5043
|
});
|
|
5005
|
-
import { existsSync as existsSync15, readFileSync as readFileSync17, writeFileSync as writeFileSync8, chmodSync as chmodSync2, statSync as
|
|
5044
|
+
import { existsSync as existsSync15, readFileSync as readFileSync17, writeFileSync as writeFileSync8, chmodSync as chmodSync2, statSync as statSync9 } from "fs";
|
|
5006
5045
|
import { join as join24 } from "path";
|
|
5007
5046
|
function baseUrl() {
|
|
5008
5047
|
const fromEnv = process.env["PAPI_HOST"] ?? process.env["PAPI_BASE_URL"];
|
|
@@ -5062,7 +5101,7 @@ function writeMcpJson(opts) {
|
|
|
5062
5101
|
parsed.mcpServers = mcpServers;
|
|
5063
5102
|
writeFileSync8(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5064
5103
|
try {
|
|
5065
|
-
const mode =
|
|
5104
|
+
const mode = statSync9(path7).mode & 511;
|
|
5066
5105
|
if (mode !== 384) chmodSync2(path7, 384);
|
|
5067
5106
|
} catch {
|
|
5068
5107
|
}
|
|
@@ -5172,7 +5211,7 @@ var init_setup = __esm({
|
|
|
5172
5211
|
// src/index.ts
|
|
5173
5212
|
import { readFileSync as readFileSync18 } from "fs";
|
|
5174
5213
|
import { dirname as dirname6, join as join25, basename as basename2 } from "path";
|
|
5175
|
-
import { fileURLToPath as
|
|
5214
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
5176
5215
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5177
5216
|
import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5178
5217
|
import {
|
|
@@ -8218,7 +8257,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
8218
8257
|
import { readFileSync as readFileSync13 } from "fs";
|
|
8219
8258
|
import { access as access4, readdir as readdir4, readFile as readFile9 } from "fs/promises";
|
|
8220
8259
|
import { join as join20, dirname as dirname5 } from "path";
|
|
8221
|
-
import { fileURLToPath as
|
|
8260
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
8222
8261
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8223
8262
|
import {
|
|
8224
8263
|
CallToolRequestSchema,
|
|
@@ -8245,6 +8284,8 @@ var UNIVERSAL_FRAME = `PAPI gives this project a structured plan \u2192 build \u
|
|
|
8245
8284
|
|
|
8246
8285
|
5. VERIFY BEFORE DONE. Test the change and confirm it works before reporting a task complete. Report failures honestly, with the output.
|
|
8247
8286
|
|
|
8287
|
+
6. NAME THE PROJECT WHEN YOU KNOW IT. If you know which repo this session is working in, pass \`project="<slug>"\` on the call rather than relying on whatever the connection defaults to. An account with more than one project cannot be resolved from a connection with no project bound \u2014 PAPI will stop and ask which one rather than guess, and answering costs a round trip. If PAPI asks, put the list to the user, then re-call with their choice; run \`project_switch\` to make it stick.
|
|
8288
|
+
|
|
8248
8289
|
PAPI reads and writes all project state through these tools \u2014 they are the source of truth, not local files.`;
|
|
8249
8290
|
|
|
8250
8291
|
// src/lib/response.ts
|
|
@@ -8995,7 +9036,7 @@ PRE-BUILD VERIFICATION
|
|
|
8995
9036
|
[List 2-5 specific file paths the builder should read BEFORE implementing to check if the functionality already exists. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. ALSO mandate a docs sweep, not just file-existence (task-2161): name any docs the builder should check via doc_search or the docs index \u2014 a design/research/status:final doc or a prior task may already cover the work. If >80% of the scope is already implemented, the builder should report "already built" instead of re-implementing. Include this section for EVERY task \u2014 it prevents wasted build slots on already-shipped code.]
|
|
8996
9037
|
|
|
8997
9038
|
FILES LIKELY TOUCHED
|
|
8998
|
-
[files \u2014 real paths only. TEST FILE LOCATIONS (task-2606): PAPI tests are NOT co-located next to source in a src/**/__tests__/ folder. Use the actual layout: server tests \u2192 packages/server/tests/<name>.test.ts; adapter-pg tests \u2192 packages/adapter-pg/src/__tests__/<name>.test.ts; dashboard/root tests \u2192 tests/<area>/<name>.test.ts. Do NOT invent packages/server/src/**/__tests__/ paths \u2014 they do not exist.]
|
|
9039
|
+
[files \u2014 real paths only. TEST FILE LOCATIONS (task-2606): PAPI tests are NOT co-located next to source in a src/**/__tests__/ folder. Use the actual layout: server tests \u2192 packages/server/tests/<name>.test.ts; adapter-pg tests \u2192 packages/adapter-pg/src/__tests__/<name>.test.ts; dashboard/root tests \u2192 tests/<area>/<name>.test.ts. Do NOT invent packages/server/src/**/__tests__/ paths \u2014 they do not exist. MCP RESTART RULE (task-2978): whenever FILES LIKELY TOUCHED includes any \`packages/server/**\` path, append this line verbatim to the handoff \u2014 "MCP RESTART REQUIRED: a mid-session \`npm run mcp-build\` writes a dist the already-booted MCP process never loads, so verifying this change through PAPI's own tools before an explicit MCP reconnect is INVALID. Implement \u2192 mcp-build \u2192 reconnect \u2192 only then verify." This is one line inside this section, not a new section.]
|
|
8999
9040
|
|
|
9000
9041
|
EFFORT
|
|
9001
9042
|
[XS/S/M/L/XL]
|
|
@@ -9009,7 +9050,7 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
9009
9050
|
{
|
|
9010
9051
|
"cycleLogTitle": "string \u2014 short descriptive title WITHOUT 'Cycle N' prefix. Should capture the cycle theme in 3-5 words (e.g. 'MCP Quality + Product Readiness' not 'Cycle 5 \u2014 Board Triage \u2014 Bug Fix'). This is the canonical theme label for the cycle.",
|
|
9011
9052
|
"cycleLogContent": "string \u2014 5-10 line cycle log body in markdown, NO heading (the ### heading is generated automatically)",
|
|
9012
|
-
"cycleLogCarryForward": "string or null \u2014 carry-forward
|
|
9053
|
+
"cycleLogCarryForward": "string or null \u2014 carry-forward for the next cycle, in TWO NAMED SECTIONS, product signal FIRST. Section 1 begins with the literal label 'WHAT SHIPS FOR USERS:' and names, concretely, what a person using this project can now see or do, and how to check it \u2014 never a deploy step. Section 2 begins with the literal label 'RELEASE MECHANICS:' and carries everything operational: deploy, publish, migrations, gate order, branch counts, spot-checks. The mechanics are load-bearing and must NOT be dropped or shortened \u2014 they go second, not away. orient parses these two labels and renders the product half first, so the labels are a contract, not decoration. If a cycle genuinely ships nothing user-visible, say so in one line under section 1 rather than omitting the label.",
|
|
9013
9054
|
"cycleLogNotes": "string or null \u2014 1-3 lines of cycle-level observations: estimation accuracy, recurring blockers, velocity trends, dependency signals. Omit if no noteworthy observations.",
|
|
9014
9055
|
"nextMode": "Full",
|
|
9015
9056
|
"boardHealth": "string \u2014 e.g. 5 tasks (3 backlog, 2 done)",
|
|
@@ -9873,7 +9914,7 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
9873
9914
|
\`\`\`json
|
|
9874
9915
|
{
|
|
9875
9916
|
"sessionLogTitle": "string \u2014 Strategy Review title WITHOUT 'Cycle N' prefix (e.g. 'Strategy Review' not 'Cycle 5 \u2014 Strategy Review')",
|
|
9876
|
-
"sessionLogContent": "string \u2014 5-10 line cycle log body summarizing the review, NO heading (the ### heading is generated automatically)",
|
|
9917
|
+
"sessionLogContent": "string \u2014 5-10 line cycle log body summarizing the review, NO heading (the ### heading is generated automatically). LEAD WITH TANGIBLE PRODUCT INSIGHT: open with what was learned about the product, its users, or the market \u2014 a specific finding a reader can act on. Board hygiene, cadence, cycle counts, branch state and other housekeeping are legitimate content but they TRAIL; they must never be the opening line. If the review's most important finding genuinely is an operational one, say why it matters to the product in the same breath rather than reporting it as admin.",
|
|
9877
9918
|
"velocityAssessment": "string \u2014 2-3 sentence velocity summary",
|
|
9878
9919
|
"strategicRecommendations": "string \u2014 key recommendations in markdown",
|
|
9879
9920
|
"activeDecisionUpdates": [
|
|
@@ -10237,7 +10278,7 @@ REFERENCE DOCS
|
|
|
10237
10278
|
[Optional \u2014 paths to docs/ files with background context. Omit if not needed.]
|
|
10238
10279
|
|
|
10239
10280
|
FILES LIKELY TOUCHED
|
|
10240
|
-
[files \u2014 real paths only. TEST FILE LOCATIONS (task-2606): PAPI tests are NOT co-located next to source in a src/**/__tests__/ folder. Use the actual layout: server tests \u2192 packages/server/tests/<name>.test.ts; adapter-pg tests \u2192 packages/adapter-pg/src/__tests__/<name>.test.ts; dashboard/root tests \u2192 tests/<area>/<name>.test.ts. Do NOT invent packages/server/src/**/__tests__/ paths \u2014 they do not exist.]
|
|
10281
|
+
[files \u2014 real paths only. TEST FILE LOCATIONS (task-2606): PAPI tests are NOT co-located next to source in a src/**/__tests__/ folder. Use the actual layout: server tests \u2192 packages/server/tests/<name>.test.ts; adapter-pg tests \u2192 packages/adapter-pg/src/__tests__/<name>.test.ts; dashboard/root tests \u2192 tests/<area>/<name>.test.ts. Do NOT invent packages/server/src/**/__tests__/ paths \u2014 they do not exist. MCP RESTART RULE (task-2978): whenever FILES LIKELY TOUCHED includes any \`packages/server/**\` path, append this line verbatim to the handoff \u2014 "MCP RESTART REQUIRED: a mid-session \`npm run mcp-build\` writes a dist the already-booted MCP process never loads, so verifying this change through PAPI's own tools before an explicit MCP reconnect is INVALID. Implement \u2192 mcp-build \u2192 reconnect \u2192 only then verify." This is one line inside this section, not a new section.]
|
|
10241
10282
|
|
|
10242
10283
|
EFFORT
|
|
10243
10284
|
[XS/S/M/L/XL]`;
|
|
@@ -10602,9 +10643,9 @@ async function getPrompt(name) {
|
|
|
10602
10643
|
if (!endpoint) {
|
|
10603
10644
|
return LOCAL_PROMPTS[name];
|
|
10604
10645
|
}
|
|
10605
|
-
const
|
|
10606
|
-
if (
|
|
10607
|
-
return
|
|
10646
|
+
const cached2 = cache.get(name);
|
|
10647
|
+
if (cached2 && Date.now() - cached2.fetchedAt < CACHE_TTL_MS) {
|
|
10648
|
+
return cached2.content;
|
|
10608
10649
|
}
|
|
10609
10650
|
try {
|
|
10610
10651
|
const url = endpoint.endsWith("/") ? `${endpoint}${name}` : `${endpoint}/${name}`;
|
|
@@ -10617,19 +10658,19 @@ async function getPrompt(name) {
|
|
|
10617
10658
|
}
|
|
10618
10659
|
const response = await fetch(url, { headers, signal: AbortSignal.timeout(1e4) });
|
|
10619
10660
|
if (!response.ok) {
|
|
10620
|
-
if (
|
|
10661
|
+
if (cached2) return cached2.content;
|
|
10621
10662
|
return LOCAL_PROMPTS[name];
|
|
10622
10663
|
}
|
|
10623
10664
|
const data = await response.json();
|
|
10624
10665
|
const content = data.content;
|
|
10625
10666
|
if (typeof content !== "string" || content.length === 0) {
|
|
10626
|
-
if (
|
|
10667
|
+
if (cached2) return cached2.content;
|
|
10627
10668
|
return LOCAL_PROMPTS[name];
|
|
10628
10669
|
}
|
|
10629
10670
|
cache.set(name, { content, fetchedAt: Date.now() });
|
|
10630
10671
|
return content;
|
|
10631
10672
|
} catch {
|
|
10632
|
-
if (
|
|
10673
|
+
if (cached2) return cached2.content;
|
|
10633
10674
|
return LOCAL_PROMPTS[name];
|
|
10634
10675
|
}
|
|
10635
10676
|
}
|
|
@@ -19797,6 +19838,91 @@ var DB_ONLY_START_NOTICE = "No git repo detected \u2014 running this cycle in yo
|
|
|
19797
19838
|
var DB_ONLY_COMPLETE_NOTICE = "No git repo \u2014 build recorded in your project database only (no commit or PR). Run `git init` (and add a remote) to enable git-backed commits and PR review.";
|
|
19798
19839
|
var DB_ONLY_RELEASE_NOTICE = "No git repo detected \u2014 this release closed the cycle in your project database only. No tag, branch merge, or CHANGELOG was created. Run `git init` (and add a remote) to enable git-backed releases (tags, merges and changelog).";
|
|
19799
19840
|
|
|
19841
|
+
// src/lib/build-decision.ts
|
|
19842
|
+
var BUILD_DECISION_ANSWERS = [
|
|
19843
|
+
"retry-differently",
|
|
19844
|
+
"skip",
|
|
19845
|
+
"stop"
|
|
19846
|
+
];
|
|
19847
|
+
function loopThreshold(env = process.env) {
|
|
19848
|
+
const raw = Number(env.PAPI_BUILD_LOOP_THRESHOLD);
|
|
19849
|
+
return Number.isFinite(raw) && raw >= 2 ? Math.floor(raw) : 3;
|
|
19850
|
+
}
|
|
19851
|
+
function readDecision(task) {
|
|
19852
|
+
const b2 = task.blocker;
|
|
19853
|
+
if (!b2 || b2.type !== "decision-gate") return null;
|
|
19854
|
+
return b2.decision ?? null;
|
|
19855
|
+
}
|
|
19856
|
+
function isDecisionPending(task) {
|
|
19857
|
+
const d = readDecision(task);
|
|
19858
|
+
return d != null && d.answer == null;
|
|
19859
|
+
}
|
|
19860
|
+
async function countFailedAttempts(adapter2, taskId) {
|
|
19861
|
+
if (typeof adapter2.getFailedBuildAttemptsForTask !== "function") return null;
|
|
19862
|
+
try {
|
|
19863
|
+
return await adapter2.getFailedBuildAttemptsForTask(taskId);
|
|
19864
|
+
} catch {
|
|
19865
|
+
return null;
|
|
19866
|
+
}
|
|
19867
|
+
}
|
|
19868
|
+
function decisionRequiredResponse(taskId, decision) {
|
|
19869
|
+
return [
|
|
19870
|
+
`DECISION REQUIRED \u2014 ${taskId} has failed ${decision.attempts} times.`,
|
|
19871
|
+
"",
|
|
19872
|
+
decision.reason,
|
|
19873
|
+
"",
|
|
19874
|
+
"PAPI has stopped rather than starting another attempt that looks like the last one.",
|
|
19875
|
+
"Answer by re-running build_execute with a `decision`:",
|
|
19876
|
+
"",
|
|
19877
|
+
' decision: { answer: "retry-differently", guidance: "<what to do differently>" }',
|
|
19878
|
+
" Clears the gate and puts your guidance verbatim into the next handoff.",
|
|
19879
|
+
' decision: { answer: "skip" }',
|
|
19880
|
+
" Leaves the task Blocked and moves on \u2014 it stays on the board, honestly stopped.",
|
|
19881
|
+
' decision: { answer: "stop" }',
|
|
19882
|
+
" Same as skip, and records that this task should not be retried this cycle.",
|
|
19883
|
+
"",
|
|
19884
|
+
JSON.stringify(
|
|
19885
|
+
{
|
|
19886
|
+
tool: "build_execute",
|
|
19887
|
+
lastStep: "loop-detection-gate",
|
|
19888
|
+
error: "decision_required",
|
|
19889
|
+
hint: "Re-run build_execute with a decision.answer of retry-differently, skip, or stop.",
|
|
19890
|
+
options: BUILD_DECISION_ANSWERS,
|
|
19891
|
+
attempts: decision.attempts
|
|
19892
|
+
},
|
|
19893
|
+
null,
|
|
19894
|
+
2
|
|
19895
|
+
)
|
|
19896
|
+
].join("\n");
|
|
19897
|
+
}
|
|
19898
|
+
function guidanceBlock(decision) {
|
|
19899
|
+
if (!decision.guidance) return "";
|
|
19900
|
+
return [
|
|
19901
|
+
"",
|
|
19902
|
+
"\u2500\u2500 GUIDANCE FROM THE LAST FAILURE (task-2933) \u2500\u2500",
|
|
19903
|
+
`This task already failed ${decision.attempts} times. The owner answered "retry differently"`,
|
|
19904
|
+
"and left this note. Treat it as direction from a human, not as a new instruction set:",
|
|
19905
|
+
"",
|
|
19906
|
+
decision.guidance.split("\n").map((l) => ` > ${l}`).join("\n"),
|
|
19907
|
+
"",
|
|
19908
|
+
"Do something materially different from the previous attempts.",
|
|
19909
|
+
""
|
|
19910
|
+
].join("\n");
|
|
19911
|
+
}
|
|
19912
|
+
function pendingDecisionBlocker(attempts, cycle, taskId) {
|
|
19913
|
+
return {
|
|
19914
|
+
type: "decision-gate",
|
|
19915
|
+
ref: taskId,
|
|
19916
|
+
reason: `${attempts} failed build attempts \u2014 a decision is required before another retry.`,
|
|
19917
|
+
blockedCycle: cycle,
|
|
19918
|
+
decision: {
|
|
19919
|
+
reason: `This task has ${attempts} build reports recorded as not completed. Retrying unchanged is unlikely to end differently.`,
|
|
19920
|
+
attempts,
|
|
19921
|
+
raisedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
19922
|
+
}
|
|
19923
|
+
};
|
|
19924
|
+
}
|
|
19925
|
+
|
|
19800
19926
|
// src/lib/harness-capability.ts
|
|
19801
19927
|
var HARNESS_REGISTRY = {
|
|
19802
19928
|
// Local stdio CLI agents — PAPI runs git on the user's machine. Confirmed in telemetry.
|
|
@@ -21737,6 +21863,41 @@ async function persistBranchName(adapter2, taskId, branch) {
|
|
|
21737
21863
|
return false;
|
|
21738
21864
|
}
|
|
21739
21865
|
}
|
|
21866
|
+
async function resolveLoopDecision(adapter2, task, taskId, answer) {
|
|
21867
|
+
const existing = readDecision(task);
|
|
21868
|
+
if (answer && existing) {
|
|
21869
|
+
if (answer.answer === "retry-differently") {
|
|
21870
|
+
const resolved = { ...existing, answer: answer.answer, guidance: answer.guidance };
|
|
21871
|
+
await safeUpdateBlocker(adapter2, taskId, {
|
|
21872
|
+
...pendingDecisionBlocker(existing.attempts, task.cycle ?? 0, taskId),
|
|
21873
|
+
decision: resolved
|
|
21874
|
+
});
|
|
21875
|
+
return { guidance: guidanceBlock(resolved) };
|
|
21876
|
+
}
|
|
21877
|
+
await safeUpdateBlocker(adapter2, taskId, {
|
|
21878
|
+
...pendingDecisionBlocker(existing.attempts, task.cycle ?? 0, taskId),
|
|
21879
|
+
decision: { ...existing, answer: answer.answer }
|
|
21880
|
+
});
|
|
21881
|
+
throw new Error(
|
|
21882
|
+
`Task "${taskId}" recorded as "${answer.answer}" after ${existing.attempts} failed attempts. It stays Blocked on the board rather than pretending to be in flight. Re-run build_execute with decision.answer "retry-differently" and guidance when you want it picked back up.`
|
|
21883
|
+
);
|
|
21884
|
+
}
|
|
21885
|
+
if (isDecisionPending(task)) {
|
|
21886
|
+
throw new Error(decisionRequiredResponse(taskId, readDecision(task)));
|
|
21887
|
+
}
|
|
21888
|
+
const attempts = await countFailedAttempts(adapter2, taskId);
|
|
21889
|
+
if (attempts == null) return {};
|
|
21890
|
+
if (attempts < loopThreshold()) return {};
|
|
21891
|
+
const blocker = pendingDecisionBlocker(attempts, task.cycle ?? 0, taskId);
|
|
21892
|
+
await safeUpdateBlocker(adapter2, taskId, blocker);
|
|
21893
|
+
throw new Error(decisionRequiredResponse(taskId, blocker.decision));
|
|
21894
|
+
}
|
|
21895
|
+
async function safeUpdateBlocker(adapter2, taskId, blocker) {
|
|
21896
|
+
try {
|
|
21897
|
+
await adapter2.updateTask(taskId, { blocker });
|
|
21898
|
+
} catch {
|
|
21899
|
+
}
|
|
21900
|
+
}
|
|
21740
21901
|
async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
21741
21902
|
const task = await adapter2.getTask(taskId);
|
|
21742
21903
|
if (!task) {
|
|
@@ -21753,6 +21914,8 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
21753
21914
|
if (task.status === "Done" || task.status === "Archived") {
|
|
21754
21915
|
throw new Error(`Task "${taskId}" (${task.title}) is already ${task.status}. Cannot execute a completed task.`);
|
|
21755
21916
|
}
|
|
21917
|
+
const loopDecision = await resolveLoopDecision(adapter2, task, taskId, options.decision);
|
|
21918
|
+
const injectedGuidance = loopDecision.guidance;
|
|
21756
21919
|
if (task.status === "In Review") {
|
|
21757
21920
|
throw new Error(
|
|
21758
21921
|
`Task "${taskId}" (${task.title}) is already In Review \u2014 it has been built and is awaiting sign-off. Run \`review_submit\` instead of re-building. If the build genuinely needs rework, use \`review_submit\` with verdict \`request-changes\` first.`
|
|
@@ -22038,7 +22201,10 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
22038
22201
|
});
|
|
22039
22202
|
return {
|
|
22040
22203
|
task,
|
|
22041
|
-
|
|
22204
|
+
// task-2933: a `retry-differently` answer prepends its guidance here, so it
|
|
22205
|
+
// sits ABOVE the BUILD HANDOFF in the start output — the builder reads the
|
|
22206
|
+
// reason the last attempts failed before it reads what to build.
|
|
22207
|
+
branchLines: injectedGuidance ? [injectedGuidance, ...branchLines] : branchLines,
|
|
22042
22208
|
phaseChanges,
|
|
22043
22209
|
filesToWrite: collector.isEmpty() ? void 0 : collector
|
|
22044
22210
|
};
|
|
@@ -22673,9 +22839,10 @@ import { join as join13, relative } from "path";
|
|
|
22673
22839
|
import { homedir as homedir3 } from "os";
|
|
22674
22840
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
22675
22841
|
import { docDeletionBlockMessage } from "@papi-ai/shared";
|
|
22842
|
+
init_git();
|
|
22676
22843
|
var docRegisterTool = {
|
|
22677
22844
|
name: "doc_register",
|
|
22678
|
-
description: "Register or update a document in the doc registry. Called after finalising a research/planning doc, or when build_execute detects unregistered docs. Stores metadata and structured summary \u2014 not full content. Re-registering an existing doc updates its summary, tags, actions, type, and status (upsert). Visibility and owner are not changed on re-register.",
|
|
22845
|
+
description: "Register or update a document in the doc registry. Called after finalising a research/planning doc, or when build_execute detects unregistered docs. Stores metadata and structured summary \u2014 not full content; the body stays a file, so an untracked doc is committed at registration to make it durable (or you get a loud warning if it cannot be). Re-registering an existing doc updates its summary, tags, actions, type, and status (upsert). Visibility and owner are not changed on re-register.",
|
|
22679
22846
|
annotations: { title: "Register Doc", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
22680
22847
|
inputSchema: {
|
|
22681
22848
|
type: "object",
|
|
@@ -22776,6 +22943,42 @@ Diagnostic JSON:
|
|
|
22776
22943
|
${JSON.stringify(payload, null, 2)}`
|
|
22777
22944
|
);
|
|
22778
22945
|
}
|
|
22946
|
+
function ensureDocDurable(path7, projectRoot) {
|
|
22947
|
+
if (!hasLocalWorkspace() || !projectRoot) return "";
|
|
22948
|
+
const warn = (reason, fix) => `
|
|
22949
|
+
|
|
22950
|
+
\u26A0\uFE0F **Registered, but NOT durable.** ${reason}
|
|
22951
|
+
The registry stores metadata and a summary \u2014 not the body. This doc has one copy, on disk, and a branch switch or stash can take it.
|
|
22952
|
+
**Fix:** ${fix}`;
|
|
22953
|
+
if (!existsSync9(join13(projectRoot, path7))) {
|
|
22954
|
+
return warn(
|
|
22955
|
+
`No file exists at \`${path7}\`.`,
|
|
22956
|
+
`write the doc body to that path, then re-run doc_register.`
|
|
22957
|
+
);
|
|
22958
|
+
}
|
|
22959
|
+
if (!isGitAvailable() || !isGitRepo(projectRoot)) {
|
|
22960
|
+
return warn(
|
|
22961
|
+
"This project is not a git repository (or git is unavailable), so the body cannot be committed.",
|
|
22962
|
+
`back the file up outside the working tree, or run \`git init\` and commit \`${path7}\`.`
|
|
22963
|
+
);
|
|
22964
|
+
}
|
|
22965
|
+
if (isPathTracked(projectRoot, path7)) return "";
|
|
22966
|
+
if (isPathIgnored(projectRoot, path7)) {
|
|
22967
|
+
return warn(
|
|
22968
|
+
`\`${path7}\` is excluded by .gitignore, so it cannot be committed (docs/private/ is ignored by design).`,
|
|
22969
|
+
`keep a copy outside the working tree, or move the doc to a tracked folder if it is not owner-only.`
|
|
22970
|
+
);
|
|
22971
|
+
}
|
|
22972
|
+
const result = commitSinglePath(projectRoot, path7, `docs: register ${path7}`);
|
|
22973
|
+
if (!result.committed) {
|
|
22974
|
+
return warn(
|
|
22975
|
+
`Committing \`${path7}\` failed: ${result.message}`,
|
|
22976
|
+
`run \`git add ${path7} && git commit -m "docs: register ${path7}"\` yourself.`
|
|
22977
|
+
);
|
|
22978
|
+
}
|
|
22979
|
+
return `
|
|
22980
|
+
- **Durability:** committed \`${path7}\` (was untracked)`;
|
|
22981
|
+
}
|
|
22779
22982
|
async function handleDocRegister(adapter2, args, config2) {
|
|
22780
22983
|
const adapterType = config2?.adapterType ?? "unknown";
|
|
22781
22984
|
const continueHint = "doc_register is advisory \u2014 your build/plan/review flow is unaffected. Fix the input (or wait for the registry to recover) and re-run doc_register; or just continue without it.";
|
|
@@ -22832,6 +23035,12 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
22832
23035
|
visibility
|
|
22833
23036
|
});
|
|
22834
23037
|
const visibilityLabel = entry.visibility === "contributors" ? "team member" : entry.visibility ?? "private";
|
|
23038
|
+
let durability = "";
|
|
23039
|
+
try {
|
|
23040
|
+
durability = ensureDocDurable(entry.path, config2?.projectRoot);
|
|
23041
|
+
} catch {
|
|
23042
|
+
durability = "";
|
|
23043
|
+
}
|
|
22835
23044
|
return textResponse(
|
|
22836
23045
|
`**Registered:** ${entry.title}
|
|
22837
23046
|
- **Path:** ${entry.path}
|
|
@@ -22839,7 +23048,7 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
22839
23048
|
- **Visibility:** ${visibilityLabel}
|
|
22840
23049
|
- **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
|
|
22841
23050
|
- **Actions:** ${actions?.length ?? 0} items
|
|
22842
|
-
- **ID:** ${entry.id}`
|
|
23051
|
+
- **ID:** ${entry.id}` + durability
|
|
22843
23052
|
);
|
|
22844
23053
|
} catch (err) {
|
|
22845
23054
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -23206,6 +23415,22 @@ var buildExecuteTool = {
|
|
|
23206
23415
|
type: "boolean",
|
|
23207
23416
|
description: "Light-ceremony mode for XS/S tasks. Skips feature branch creation and PR. Work stays on current branch. Build report is still captured. Default false."
|
|
23208
23417
|
},
|
|
23418
|
+
decision: {
|
|
23419
|
+
type: "object",
|
|
23420
|
+
description: "task-2933: answer a loop-detection gate. After N failed build attempts (default 3, PAPI_BUILD_LOOP_THRESHOLD) build_execute REFUSES to start and asks for a decision. Re-run with this to answer. Only meaningful on a gated task.",
|
|
23421
|
+
properties: {
|
|
23422
|
+
answer: {
|
|
23423
|
+
type: "string",
|
|
23424
|
+
enum: ["retry-differently", "skip", "stop"],
|
|
23425
|
+
description: '"retry-differently" clears the gate and injects `guidance` verbatim into the next handoff. "skip"/"stop" leave the task Blocked on the board rather than pretending it is in flight.'
|
|
23426
|
+
},
|
|
23427
|
+
guidance: {
|
|
23428
|
+
type: "string",
|
|
23429
|
+
description: 'What to do differently. Required in spirit for "retry-differently" \u2014 it is the whole point of the retry. Rendered to the next builder as quoted human direction.'
|
|
23430
|
+
}
|
|
23431
|
+
},
|
|
23432
|
+
required: ["answer"]
|
|
23433
|
+
},
|
|
23209
23434
|
completed: {
|
|
23210
23435
|
type: "string",
|
|
23211
23436
|
enum: ["yes", "no", "partial"],
|
|
@@ -23550,7 +23775,19 @@ async function handleBuildExecute(adapter2, config2, args, clientName) {
|
|
|
23550
23775
|
if (existing) resumeNote = formatResumeNote(existing);
|
|
23551
23776
|
}
|
|
23552
23777
|
await tracker.recordStep("started");
|
|
23553
|
-
const
|
|
23778
|
+
const decision = args.decision;
|
|
23779
|
+
const result = await startBuild(
|
|
23780
|
+
adapter2,
|
|
23781
|
+
config2,
|
|
23782
|
+
taskId,
|
|
23783
|
+
{
|
|
23784
|
+
light,
|
|
23785
|
+
// Only pass a decision the schema recognises — an unknown answer must not
|
|
23786
|
+
// reach the resolver and silently clear a gate.
|
|
23787
|
+
decision: decision?.answer && BUILD_DECISION_ANSWERS.includes(decision.answer) ? { answer: decision.answer, guidance: decision.guidance } : void 0
|
|
23788
|
+
},
|
|
23789
|
+
clientName
|
|
23790
|
+
);
|
|
23554
23791
|
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.task.cycle ?? null });
|
|
23555
23792
|
await tracker.recordStep("branch_ready");
|
|
23556
23793
|
tracker.mark("start_decorate_handoff");
|
|
@@ -24048,7 +24285,7 @@ var ideaTool = {
|
|
|
24048
24285
|
},
|
|
24049
24286
|
project: {
|
|
24050
24287
|
type: "string",
|
|
24051
|
-
description: "Project id (UUID) or slug to write this idea to, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. Use project_switch to change the session default."
|
|
24288
|
+
description: "Project id (UUID) or slug to write this idea to, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. Use project_switch to change the session default. PASS THIS WHENEVER YOU KNOW WHICH REPO THE SESSION IS IN: on a multi-project account a connection with no project bound cannot be resolved, and PAPI will stop and ask rather than guess."
|
|
24052
24289
|
}
|
|
24053
24290
|
},
|
|
24054
24291
|
required: ["text"]
|
|
@@ -24495,7 +24732,7 @@ var backlogImportTool = {
|
|
|
24495
24732
|
},
|
|
24496
24733
|
project: {
|
|
24497
24734
|
type: "string",
|
|
24498
|
-
description: "Project id (UUID) or slug to import into, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise."
|
|
24735
|
+
description: "Project id (UUID) or slug to import into, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. PASS THIS WHENEVER YOU KNOW WHICH REPO THE SESSION IS IN: on a multi-project account a connection with no project bound cannot be resolved, and PAPI will stop and ask rather than guess."
|
|
24499
24736
|
}
|
|
24500
24737
|
},
|
|
24501
24738
|
required: ["source"]
|
|
@@ -24703,7 +24940,7 @@ var bugTool = {
|
|
|
24703
24940
|
},
|
|
24704
24941
|
project: {
|
|
24705
24942
|
type: "string",
|
|
24706
|
-
description: "BOARD MODE ONLY. Project id (UUID) or slug to file this bug under, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. It CANNOT redirect an upstream (report=true) submission \u2014 those always go to PAPI maintainers. Use project_switch to change the session default."
|
|
24943
|
+
description: "BOARD MODE ONLY. Project id (UUID) or slug to file this bug under, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. It CANNOT redirect an upstream (report=true) submission \u2014 those always go to PAPI maintainers. Use project_switch to change the session default. PASS THIS WHENEVER YOU KNOW WHICH REPO THE SESSION IS IN: on a multi-project account a connection with no project bound cannot be resolved, and PAPI will stop and ask rather than guess."
|
|
24707
24944
|
}
|
|
24708
24945
|
},
|
|
24709
24946
|
required: ["text"]
|
|
@@ -27606,6 +27843,50 @@ function formatUnblockSection(candidates) {
|
|
|
27606
27843
|
return lines.join("\n");
|
|
27607
27844
|
}
|
|
27608
27845
|
|
|
27846
|
+
// src/lib/carry-forward-shape.ts
|
|
27847
|
+
var PRODUCT_MARKER = /WHAT SHIPS FOR USERS\b[^\n:]*:/i;
|
|
27848
|
+
var MECHANICS_MARKER = /RELEASE MECHANICS\b[^\n:]*:/i;
|
|
27849
|
+
function splitCarryForward(raw) {
|
|
27850
|
+
const productMatch = PRODUCT_MARKER.exec(raw);
|
|
27851
|
+
const mechanicsMatch = MECHANICS_MARKER.exec(raw);
|
|
27852
|
+
if (!productMatch && !mechanicsMatch) {
|
|
27853
|
+
return { product: raw.trim(), mechanics: "", split: false };
|
|
27854
|
+
}
|
|
27855
|
+
if (!productMatch && mechanicsMatch) {
|
|
27856
|
+
const head = raw.slice(0, mechanicsMatch.index).trim();
|
|
27857
|
+
const tail = raw.slice(mechanicsMatch.index + mechanicsMatch[0].length).trim();
|
|
27858
|
+
return { product: head, mechanics: tail, split: true };
|
|
27859
|
+
}
|
|
27860
|
+
const productStart = productMatch.index + productMatch[0].length;
|
|
27861
|
+
if (!mechanicsMatch) {
|
|
27862
|
+
return { product: raw.slice(productStart).trim(), mechanics: "", split: true };
|
|
27863
|
+
}
|
|
27864
|
+
if (mechanicsMatch.index < productMatch.index) {
|
|
27865
|
+
return {
|
|
27866
|
+
product: raw.slice(productStart).trim(),
|
|
27867
|
+
mechanics: raw.slice(mechanicsMatch.index + mechanicsMatch[0].length, productMatch.index).trim(),
|
|
27868
|
+
split: true
|
|
27869
|
+
};
|
|
27870
|
+
}
|
|
27871
|
+
return {
|
|
27872
|
+
product: raw.slice(productStart, mechanicsMatch.index).trim(),
|
|
27873
|
+
mechanics: raw.slice(mechanicsMatch.index + mechanicsMatch[0].length).trim(),
|
|
27874
|
+
split: true
|
|
27875
|
+
};
|
|
27876
|
+
}
|
|
27877
|
+
function renderCarryForward(raw, decorate = (t) => t) {
|
|
27878
|
+
const { product, mechanics, split } = splitCarryForward(raw);
|
|
27879
|
+
if (!split) return [decorate(product)];
|
|
27880
|
+
const lines = [];
|
|
27881
|
+
if (product) lines.push(decorate(product));
|
|
27882
|
+
if (mechanics) {
|
|
27883
|
+
if (product) lines.push("");
|
|
27884
|
+
lines.push("**Release mechanics** \u2014 needed at release, not now:");
|
|
27885
|
+
lines.push(decorate(mechanics));
|
|
27886
|
+
}
|
|
27887
|
+
return lines;
|
|
27888
|
+
}
|
|
27889
|
+
|
|
27609
27890
|
// src/lib/deferred-gate.ts
|
|
27610
27891
|
var GATE_PHRASES = [
|
|
27611
27892
|
"depends on",
|
|
@@ -28146,6 +28427,20 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
28146
28427
|
lines.push(`**In Review:** ${health.inReviewSummary}`);
|
|
28147
28428
|
lines.push("");
|
|
28148
28429
|
}
|
|
28430
|
+
if (buildInfo.pendingDecisions.length > 0) {
|
|
28431
|
+
const n = buildInfo.pendingDecisions.length;
|
|
28432
|
+
lines.push("## Decision required");
|
|
28433
|
+
lines.push(
|
|
28434
|
+
`${n} task${n === 1 ? " has" : "s have"} stopped after repeated failed builds. PAPI will not retry until you answer.`
|
|
28435
|
+
);
|
|
28436
|
+
for (const d of buildInfo.pendingDecisions) {
|
|
28437
|
+
lines.push(`- **${d.id}:** ${d.title} \u2014 ${d.attempts} failed attempts`);
|
|
28438
|
+
}
|
|
28439
|
+
lines.push(
|
|
28440
|
+
'Answer with `build_execute <task> decision:{answer:"retry-differently", guidance:"\u2026"}` to pick it back up, or `"skip"` / `"stop"` to leave it parked.'
|
|
28441
|
+
);
|
|
28442
|
+
lines.push("");
|
|
28443
|
+
}
|
|
28149
28444
|
if (buildInfo.isEmpty) {
|
|
28150
28445
|
lines.push("## Tasks");
|
|
28151
28446
|
if (buildInfo.currentCycle === 0) {
|
|
@@ -28171,7 +28466,7 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
28171
28466
|
const hasCarryForward = health.carryForward !== "None found" && !health.carryForward.startsWith("No carry-forward");
|
|
28172
28467
|
if (hasCarryForward) {
|
|
28173
28468
|
lines.push("## Carry-Forward");
|
|
28174
|
-
lines.push(
|
|
28469
|
+
lines.push(...renderCarryForward(health.carryForward, (t) => annotateTaskRefs(t, taskRefs)));
|
|
28175
28470
|
lines.push("");
|
|
28176
28471
|
}
|
|
28177
28472
|
const hasMetrics = health.metricsSection !== "Could not read methodology metrics." && !health.metricsSection.includes("undefined");
|
|
@@ -28414,6 +28709,19 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
28414
28709
|
inReview: healthResult.inReviewSummary,
|
|
28415
28710
|
backlogCount: buildResult.backlog.length,
|
|
28416
28711
|
blockedCount: buildResult.blocked.length,
|
|
28712
|
+
// task-2933: computed here where the raw tasks are in scope. Best-effort —
|
|
28713
|
+
// a malformed blocker must never break orient.
|
|
28714
|
+
pendingDecisions: allTasks.filter((t) => {
|
|
28715
|
+
try {
|
|
28716
|
+
return isDecisionPending(t);
|
|
28717
|
+
} catch {
|
|
28718
|
+
return false;
|
|
28719
|
+
}
|
|
28720
|
+
}).map((t) => ({
|
|
28721
|
+
id: t.displayId ?? t.id,
|
|
28722
|
+
title: t.title,
|
|
28723
|
+
attempts: readDecision(t)?.attempts ?? 0
|
|
28724
|
+
})),
|
|
28417
28725
|
totalHandoffs: buildResult.sorted.length + buildResult.blocked.length,
|
|
28418
28726
|
currentCycle,
|
|
28419
28727
|
isEmpty: buildResult.isEmpty,
|
|
@@ -30113,6 +30421,36 @@ ${d.body}`;
|
|
|
30113
30421
|
${formatted}`, meta));
|
|
30114
30422
|
}
|
|
30115
30423
|
|
|
30424
|
+
// src/lib/dist-staleness.ts
|
|
30425
|
+
import { statSync as statSync8 } from "fs";
|
|
30426
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
30427
|
+
var BOOT_MS = Date.now();
|
|
30428
|
+
var DEFAULT_SKEW_MS = 2e3;
|
|
30429
|
+
var STAT_TTL_MS = 1e4;
|
|
30430
|
+
function isDistStale({ bootMs, distMtimeMs, skewMs = DEFAULT_SKEW_MS }) {
|
|
30431
|
+
if (distMtimeMs === null || !Number.isFinite(distMtimeMs)) return false;
|
|
30432
|
+
return distMtimeMs > bootMs + skewMs;
|
|
30433
|
+
}
|
|
30434
|
+
var cached = null;
|
|
30435
|
+
function checkDistStaleness(now = Date.now()) {
|
|
30436
|
+
if (cached && now - cached.at < STAT_TTL_MS) return cached.stale;
|
|
30437
|
+
let distMtimeMs = null;
|
|
30438
|
+
try {
|
|
30439
|
+
distMtimeMs = statSync8(fileURLToPath3(import.meta.url)).mtimeMs;
|
|
30440
|
+
} catch {
|
|
30441
|
+
distMtimeMs = null;
|
|
30442
|
+
}
|
|
30443
|
+
const stale = isDistStale({ bootMs: BOOT_MS, distMtimeMs });
|
|
30444
|
+
cached = { at: now, stale };
|
|
30445
|
+
return stale;
|
|
30446
|
+
}
|
|
30447
|
+
function stalenessWarning(now = Date.now()) {
|
|
30448
|
+
const age = Math.round((now - BOOT_MS) / 1e3);
|
|
30449
|
+
return `\u26A0\uFE0F This PAPI MCP server process booted ${age}s ago and packages/server has been REBUILT since. It is still running the code it booted with, so anything you verify through these tools right now reflects the OLD bundle. Reconnect / restart the PAPI MCP server before trusting this output.
|
|
30450
|
+
|
|
30451
|
+
`;
|
|
30452
|
+
}
|
|
30453
|
+
|
|
30116
30454
|
// src/tools/learning-action.ts
|
|
30117
30455
|
var learningActionTool = {
|
|
30118
30456
|
name: "learning_action",
|
|
@@ -31084,9 +31422,9 @@ async function checkMeter(adapter2, toolName, cacheKey) {
|
|
|
31084
31422
|
if (!adapter2.getMeteredUsage) return { blocked: false };
|
|
31085
31423
|
try {
|
|
31086
31424
|
let usage;
|
|
31087
|
-
const
|
|
31088
|
-
if (
|
|
31089
|
-
usage =
|
|
31425
|
+
const cached2 = usageCache.get(cacheKey);
|
|
31426
|
+
if (cached2 && Date.now() - cached2.at < CACHE_TTL_MS3) {
|
|
31427
|
+
usage = cached2.usage;
|
|
31090
31428
|
} else {
|
|
31091
31429
|
usage = await adapter2.getMeteredUsage();
|
|
31092
31430
|
usageCache.set(cacheKey, { usage, at: Date.now() });
|
|
@@ -31275,7 +31613,7 @@ function getToolMetadata() {
|
|
|
31275
31613
|
return PAPI_TOOLS.map((t) => ({ name: t.name, description: t.description }));
|
|
31276
31614
|
}
|
|
31277
31615
|
function createServer(adapter2, config2) {
|
|
31278
|
-
const __pkgFilename =
|
|
31616
|
+
const __pkgFilename = fileURLToPath4(import.meta.url);
|
|
31279
31617
|
const __pkgDir = dirname5(__pkgFilename);
|
|
31280
31618
|
let serverVersion = "unknown";
|
|
31281
31619
|
try {
|
|
@@ -31295,7 +31633,7 @@ function createServer(adapter2, config2) {
|
|
|
31295
31633
|
"\n\u26A0 PAPI is running in md mode \u2014 your cycles are not visible on the hosted dashboard.\n Configure DATABASE_URL or sign up at https://getpapi.ai/setup to enable observability.\n\n"
|
|
31296
31634
|
);
|
|
31297
31635
|
}
|
|
31298
|
-
const __filename =
|
|
31636
|
+
const __filename = fileURLToPath4(import.meta.url);
|
|
31299
31637
|
const __dirname2 = dirname5(__filename);
|
|
31300
31638
|
const skillsDir = join20(__dirname2, "..", "skills");
|
|
31301
31639
|
function parseSkillFrontmatter(content) {
|
|
@@ -31597,6 +31935,15 @@ ${usageLine(decision.usage)}`;
|
|
|
31597
31935
|
}
|
|
31598
31936
|
const footer = formatMetricsFooter(elapsed, usage, contextBytes);
|
|
31599
31937
|
result.content.push({ type: "text", text: footer });
|
|
31938
|
+
try {
|
|
31939
|
+
if (checkDistStaleness() && result.content.length > 0) {
|
|
31940
|
+
const first = result.content[0];
|
|
31941
|
+
if (first && typeof first.text === "string") {
|
|
31942
|
+
first.text = stalenessWarning() + first.text;
|
|
31943
|
+
}
|
|
31944
|
+
}
|
|
31945
|
+
} catch {
|
|
31946
|
+
}
|
|
31600
31947
|
return result;
|
|
31601
31948
|
});
|
|
31602
31949
|
return server2;
|
|
@@ -32042,7 +32389,7 @@ async function dispatchRequest(args) {
|
|
|
32042
32389
|
}
|
|
32043
32390
|
|
|
32044
32391
|
// src/index.ts
|
|
32045
|
-
var __dirname = dirname6(
|
|
32392
|
+
var __dirname = dirname6(fileURLToPath5(import.meta.url));
|
|
32046
32393
|
var pkgVersion = "unknown";
|
|
32047
32394
|
try {
|
|
32048
32395
|
const pkg = JSON.parse(readFileSync18(join25(__dirname, "..", "package.json"), "utf-8"));
|
package/dist/prompts.js
CHANGED
|
@@ -69,7 +69,7 @@ PRE-BUILD VERIFICATION
|
|
|
69
69
|
[List 2-5 specific file paths the builder should read BEFORE implementing to check if the functionality already exists. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. ALSO mandate a docs sweep, not just file-existence (task-2161): name any docs the builder should check via doc_search or the docs index \u2014 a design/research/status:final doc or a prior task may already cover the work. If >80% of the scope is already implemented, the builder should report "already built" instead of re-implementing. Include this section for EVERY task \u2014 it prevents wasted build slots on already-shipped code.]
|
|
70
70
|
|
|
71
71
|
FILES LIKELY TOUCHED
|
|
72
|
-
[files \u2014 real paths only. TEST FILE LOCATIONS (task-2606): PAPI tests are NOT co-located next to source in a src/**/__tests__/ folder. Use the actual layout: server tests \u2192 packages/server/tests/<name>.test.ts; adapter-pg tests \u2192 packages/adapter-pg/src/__tests__/<name>.test.ts; dashboard/root tests \u2192 tests/<area>/<name>.test.ts. Do NOT invent packages/server/src/**/__tests__/ paths \u2014 they do not exist.]
|
|
72
|
+
[files \u2014 real paths only. TEST FILE LOCATIONS (task-2606): PAPI tests are NOT co-located next to source in a src/**/__tests__/ folder. Use the actual layout: server tests \u2192 packages/server/tests/<name>.test.ts; adapter-pg tests \u2192 packages/adapter-pg/src/__tests__/<name>.test.ts; dashboard/root tests \u2192 tests/<area>/<name>.test.ts. Do NOT invent packages/server/src/**/__tests__/ paths \u2014 they do not exist. MCP RESTART RULE (task-2978): whenever FILES LIKELY TOUCHED includes any \`packages/server/**\` path, append this line verbatim to the handoff \u2014 "MCP RESTART REQUIRED: a mid-session \`npm run mcp-build\` writes a dist the already-booted MCP process never loads, so verifying this change through PAPI's own tools before an explicit MCP reconnect is INVALID. Implement \u2192 mcp-build \u2192 reconnect \u2192 only then verify." This is one line inside this section, not a new section.]
|
|
73
73
|
|
|
74
74
|
EFFORT
|
|
75
75
|
[XS/S/M/L/XL]
|
|
@@ -83,7 +83,7 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
83
83
|
{
|
|
84
84
|
"cycleLogTitle": "string \u2014 short descriptive title WITHOUT 'Cycle N' prefix. Should capture the cycle theme in 3-5 words (e.g. 'MCP Quality + Product Readiness' not 'Cycle 5 \u2014 Board Triage \u2014 Bug Fix'). This is the canonical theme label for the cycle.",
|
|
85
85
|
"cycleLogContent": "string \u2014 5-10 line cycle log body in markdown, NO heading (the ### heading is generated automatically)",
|
|
86
|
-
"cycleLogCarryForward": "string or null \u2014 carry-forward
|
|
86
|
+
"cycleLogCarryForward": "string or null \u2014 carry-forward for the next cycle, in TWO NAMED SECTIONS, product signal FIRST. Section 1 begins with the literal label 'WHAT SHIPS FOR USERS:' and names, concretely, what a person using this project can now see or do, and how to check it \u2014 never a deploy step. Section 2 begins with the literal label 'RELEASE MECHANICS:' and carries everything operational: deploy, publish, migrations, gate order, branch counts, spot-checks. The mechanics are load-bearing and must NOT be dropped or shortened \u2014 they go second, not away. orient parses these two labels and renders the product half first, so the labels are a contract, not decoration. If a cycle genuinely ships nothing user-visible, say so in one line under section 1 rather than omitting the label.",
|
|
87
87
|
"cycleLogNotes": "string or null \u2014 1-3 lines of cycle-level observations: estimation accuracy, recurring blockers, velocity trends, dependency signals. Omit if no noteworthy observations.",
|
|
88
88
|
"nextMode": "Full",
|
|
89
89
|
"boardHealth": "string \u2014 e.g. 5 tasks (3 backlog, 2 done)",
|
|
@@ -947,7 +947,7 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
947
947
|
\`\`\`json
|
|
948
948
|
{
|
|
949
949
|
"sessionLogTitle": "string \u2014 Strategy Review title WITHOUT 'Cycle N' prefix (e.g. 'Strategy Review' not 'Cycle 5 \u2014 Strategy Review')",
|
|
950
|
-
"sessionLogContent": "string \u2014 5-10 line cycle log body summarizing the review, NO heading (the ### heading is generated automatically)",
|
|
950
|
+
"sessionLogContent": "string \u2014 5-10 line cycle log body summarizing the review, NO heading (the ### heading is generated automatically). LEAD WITH TANGIBLE PRODUCT INSIGHT: open with what was learned about the product, its users, or the market \u2014 a specific finding a reader can act on. Board hygiene, cadence, cycle counts, branch state and other housekeeping are legitimate content but they TRAIL; they must never be the opening line. If the review's most important finding genuinely is an operational one, say why it matters to the product in the same breath rather than reporting it as admin.",
|
|
951
951
|
"velocityAssessment": "string \u2014 2-3 sentence velocity summary",
|
|
952
952
|
"strategicRecommendations": "string \u2014 key recommendations in markdown",
|
|
953
953
|
"activeDecisionUpdates": [
|
|
@@ -1311,7 +1311,7 @@ REFERENCE DOCS
|
|
|
1311
1311
|
[Optional \u2014 paths to docs/ files with background context. Omit if not needed.]
|
|
1312
1312
|
|
|
1313
1313
|
FILES LIKELY TOUCHED
|
|
1314
|
-
[files \u2014 real paths only. TEST FILE LOCATIONS (task-2606): PAPI tests are NOT co-located next to source in a src/**/__tests__/ folder. Use the actual layout: server tests \u2192 packages/server/tests/<name>.test.ts; adapter-pg tests \u2192 packages/adapter-pg/src/__tests__/<name>.test.ts; dashboard/root tests \u2192 tests/<area>/<name>.test.ts. Do NOT invent packages/server/src/**/__tests__/ paths \u2014 they do not exist.]
|
|
1314
|
+
[files \u2014 real paths only. TEST FILE LOCATIONS (task-2606): PAPI tests are NOT co-located next to source in a src/**/__tests__/ folder. Use the actual layout: server tests \u2192 packages/server/tests/<name>.test.ts; adapter-pg tests \u2192 packages/adapter-pg/src/__tests__/<name>.test.ts; dashboard/root tests \u2192 tests/<area>/<name>.test.ts. Do NOT invent packages/server/src/**/__tests__/ paths \u2014 they do not exist. MCP RESTART RULE (task-2978): whenever FILES LIKELY TOUCHED includes any \`packages/server/**\` path, append this line verbatim to the handoff \u2014 "MCP RESTART REQUIRED: a mid-session \`npm run mcp-build\` writes a dist the already-booted MCP process never loads, so verifying this change through PAPI's own tools before an explicit MCP reconnect is INVALID. Implement \u2192 mcp-build \u2192 reconnect \u2192 only then verify." This is one line inside this section, not a new section.]
|
|
1315
1315
|
|
|
1316
1316
|
EFFORT
|
|
1317
1317
|
[XS/S/M/L/XL]`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.75",
|
|
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",
|