@papi-ai/server 0.7.72 → 0.7.74
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 +317 -44
- 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 });
|
|
@@ -11214,6 +11253,55 @@ function formatBlockerWaiting(blocker) {
|
|
|
11214
11253
|
}
|
|
11215
11254
|
}
|
|
11216
11255
|
|
|
11256
|
+
// src/lib/tool-telemetry.ts
|
|
11257
|
+
var INSTRUMENTED_TOOLS = /* @__PURE__ */ new Set([
|
|
11258
|
+
"plan",
|
|
11259
|
+
"orient",
|
|
11260
|
+
"build_execute",
|
|
11261
|
+
"review_submit",
|
|
11262
|
+
"release",
|
|
11263
|
+
"strategy_review"
|
|
11264
|
+
]);
|
|
11265
|
+
var WRAPPER_INSTRUMENTED_TOOLS = new Set(
|
|
11266
|
+
[...INSTRUMENTED_TOOLS].filter((t) => t !== "plan")
|
|
11267
|
+
);
|
|
11268
|
+
var PLAN_PREPARE_TOOL_NAME = "plan:prepare";
|
|
11269
|
+
function measureResultBytes(content) {
|
|
11270
|
+
let total = 0;
|
|
11271
|
+
for (const part of content) {
|
|
11272
|
+
if (typeof part.text === "string") total += Buffer.byteLength(part.text, "utf-8");
|
|
11273
|
+
}
|
|
11274
|
+
return total;
|
|
11275
|
+
}
|
|
11276
|
+
function recordToolRun(adapter2, sample) {
|
|
11277
|
+
if (sample.toolName !== PLAN_PREPARE_TOOL_NAME && !WRAPPER_INSTRUMENTED_TOOLS.has(sample.toolName)) return;
|
|
11278
|
+
if (typeof adapter2.insertToolRun !== "function") return;
|
|
11279
|
+
try {
|
|
11280
|
+
const promise = adapter2.insertToolRun({
|
|
11281
|
+
toolName: sample.toolName,
|
|
11282
|
+
contextBytes: sample.contextBytes,
|
|
11283
|
+
durationMs: sample.durationMs,
|
|
11284
|
+
cycleNumber: sample.cycleNumber ?? null,
|
|
11285
|
+
source: "mcp-server"
|
|
11286
|
+
});
|
|
11287
|
+
if (promise && typeof promise.catch === "function") {
|
|
11288
|
+
promise.catch((err) => {
|
|
11289
|
+
console.error(`[telemetry] insertToolRun failed (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
|
|
11290
|
+
});
|
|
11291
|
+
}
|
|
11292
|
+
} catch (err) {
|
|
11293
|
+
console.error(`[telemetry] insertToolRun threw (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
|
|
11294
|
+
}
|
|
11295
|
+
}
|
|
11296
|
+
function recordPlanPrepareRun(adapter2, sample) {
|
|
11297
|
+
recordToolRun(adapter2, {
|
|
11298
|
+
toolName: PLAN_PREPARE_TOOL_NAME,
|
|
11299
|
+
contextBytes: sample.contextBytes,
|
|
11300
|
+
durationMs: sample.durationMs,
|
|
11301
|
+
cycleNumber: sample.cycleNumber ?? null
|
|
11302
|
+
});
|
|
11303
|
+
}
|
|
11304
|
+
|
|
11217
11305
|
// src/lib/visibility-inheritance.ts
|
|
11218
11306
|
var TIER_RANK = {
|
|
11219
11307
|
public: 0,
|
|
@@ -13007,6 +13095,7 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
13007
13095
|
console.error(`[plan-perf] contextBytes=${contextBytes2} (handoffs-only)`);
|
|
13008
13096
|
const planSystemPrompt2 = await getPrompt("plan-system");
|
|
13009
13097
|
await recordPlanGenerationActive(tracker, incomingCycle);
|
|
13098
|
+
recordPlanPrepareRun(adapter2, { contextBytes: contextBytes2, durationMs: totalMs2, cycleNumber: incomingCycle });
|
|
13010
13099
|
return {
|
|
13011
13100
|
mode: "full",
|
|
13012
13101
|
// apply phase treats it the same
|
|
@@ -13068,6 +13157,7 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
13068
13157
|
}
|
|
13069
13158
|
const planSystemPrompt = await getPrompt("plan-system");
|
|
13070
13159
|
await recordPlanGenerationActive(tracker, incomingCycle);
|
|
13160
|
+
recordPlanPrepareRun(adapter2, { contextBytes, durationMs: totalMs, cycleNumber: incomingCycle });
|
|
13071
13161
|
return {
|
|
13072
13162
|
mode,
|
|
13073
13163
|
cycleNumber,
|
|
@@ -19746,6 +19836,91 @@ var DB_ONLY_START_NOTICE = "No git repo detected \u2014 running this cycle in yo
|
|
|
19746
19836
|
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.";
|
|
19747
19837
|
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).";
|
|
19748
19838
|
|
|
19839
|
+
// src/lib/build-decision.ts
|
|
19840
|
+
var BUILD_DECISION_ANSWERS = [
|
|
19841
|
+
"retry-differently",
|
|
19842
|
+
"skip",
|
|
19843
|
+
"stop"
|
|
19844
|
+
];
|
|
19845
|
+
function loopThreshold(env = process.env) {
|
|
19846
|
+
const raw = Number(env.PAPI_BUILD_LOOP_THRESHOLD);
|
|
19847
|
+
return Number.isFinite(raw) && raw >= 2 ? Math.floor(raw) : 3;
|
|
19848
|
+
}
|
|
19849
|
+
function readDecision(task) {
|
|
19850
|
+
const b2 = task.blocker;
|
|
19851
|
+
if (!b2 || b2.type !== "decision-gate") return null;
|
|
19852
|
+
return b2.decision ?? null;
|
|
19853
|
+
}
|
|
19854
|
+
function isDecisionPending(task) {
|
|
19855
|
+
const d = readDecision(task);
|
|
19856
|
+
return d != null && d.answer == null;
|
|
19857
|
+
}
|
|
19858
|
+
async function countFailedAttempts(adapter2, taskId) {
|
|
19859
|
+
if (typeof adapter2.getFailedBuildAttemptsForTask !== "function") return null;
|
|
19860
|
+
try {
|
|
19861
|
+
return await adapter2.getFailedBuildAttemptsForTask(taskId);
|
|
19862
|
+
} catch {
|
|
19863
|
+
return null;
|
|
19864
|
+
}
|
|
19865
|
+
}
|
|
19866
|
+
function decisionRequiredResponse(taskId, decision) {
|
|
19867
|
+
return [
|
|
19868
|
+
`DECISION REQUIRED \u2014 ${taskId} has failed ${decision.attempts} times.`,
|
|
19869
|
+
"",
|
|
19870
|
+
decision.reason,
|
|
19871
|
+
"",
|
|
19872
|
+
"PAPI has stopped rather than starting another attempt that looks like the last one.",
|
|
19873
|
+
"Answer by re-running build_execute with a `decision`:",
|
|
19874
|
+
"",
|
|
19875
|
+
' decision: { answer: "retry-differently", guidance: "<what to do differently>" }',
|
|
19876
|
+
" Clears the gate and puts your guidance verbatim into the next handoff.",
|
|
19877
|
+
' decision: { answer: "skip" }',
|
|
19878
|
+
" Leaves the task Blocked and moves on \u2014 it stays on the board, honestly stopped.",
|
|
19879
|
+
' decision: { answer: "stop" }',
|
|
19880
|
+
" Same as skip, and records that this task should not be retried this cycle.",
|
|
19881
|
+
"",
|
|
19882
|
+
JSON.stringify(
|
|
19883
|
+
{
|
|
19884
|
+
tool: "build_execute",
|
|
19885
|
+
lastStep: "loop-detection-gate",
|
|
19886
|
+
error: "decision_required",
|
|
19887
|
+
hint: "Re-run build_execute with a decision.answer of retry-differently, skip, or stop.",
|
|
19888
|
+
options: BUILD_DECISION_ANSWERS,
|
|
19889
|
+
attempts: decision.attempts
|
|
19890
|
+
},
|
|
19891
|
+
null,
|
|
19892
|
+
2
|
|
19893
|
+
)
|
|
19894
|
+
].join("\n");
|
|
19895
|
+
}
|
|
19896
|
+
function guidanceBlock(decision) {
|
|
19897
|
+
if (!decision.guidance) return "";
|
|
19898
|
+
return [
|
|
19899
|
+
"",
|
|
19900
|
+
"\u2500\u2500 GUIDANCE FROM THE LAST FAILURE (task-2933) \u2500\u2500",
|
|
19901
|
+
`This task already failed ${decision.attempts} times. The owner answered "retry differently"`,
|
|
19902
|
+
"and left this note. Treat it as direction from a human, not as a new instruction set:",
|
|
19903
|
+
"",
|
|
19904
|
+
decision.guidance.split("\n").map((l) => ` > ${l}`).join("\n"),
|
|
19905
|
+
"",
|
|
19906
|
+
"Do something materially different from the previous attempts.",
|
|
19907
|
+
""
|
|
19908
|
+
].join("\n");
|
|
19909
|
+
}
|
|
19910
|
+
function pendingDecisionBlocker(attempts, cycle, taskId) {
|
|
19911
|
+
return {
|
|
19912
|
+
type: "decision-gate",
|
|
19913
|
+
ref: taskId,
|
|
19914
|
+
reason: `${attempts} failed build attempts \u2014 a decision is required before another retry.`,
|
|
19915
|
+
blockedCycle: cycle,
|
|
19916
|
+
decision: {
|
|
19917
|
+
reason: `This task has ${attempts} build reports recorded as not completed. Retrying unchanged is unlikely to end differently.`,
|
|
19918
|
+
attempts,
|
|
19919
|
+
raisedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
19920
|
+
}
|
|
19921
|
+
};
|
|
19922
|
+
}
|
|
19923
|
+
|
|
19749
19924
|
// src/lib/harness-capability.ts
|
|
19750
19925
|
var HARNESS_REGISTRY = {
|
|
19751
19926
|
// Local stdio CLI agents — PAPI runs git on the user's machine. Confirmed in telemetry.
|
|
@@ -21686,6 +21861,41 @@ async function persistBranchName(adapter2, taskId, branch) {
|
|
|
21686
21861
|
return false;
|
|
21687
21862
|
}
|
|
21688
21863
|
}
|
|
21864
|
+
async function resolveLoopDecision(adapter2, task, taskId, answer) {
|
|
21865
|
+
const existing = readDecision(task);
|
|
21866
|
+
if (answer && existing) {
|
|
21867
|
+
if (answer.answer === "retry-differently") {
|
|
21868
|
+
const resolved = { ...existing, answer: answer.answer, guidance: answer.guidance };
|
|
21869
|
+
await safeUpdateBlocker(adapter2, taskId, {
|
|
21870
|
+
...pendingDecisionBlocker(existing.attempts, task.cycle ?? 0, taskId),
|
|
21871
|
+
decision: resolved
|
|
21872
|
+
});
|
|
21873
|
+
return { guidance: guidanceBlock(resolved) };
|
|
21874
|
+
}
|
|
21875
|
+
await safeUpdateBlocker(adapter2, taskId, {
|
|
21876
|
+
...pendingDecisionBlocker(existing.attempts, task.cycle ?? 0, taskId),
|
|
21877
|
+
decision: { ...existing, answer: answer.answer }
|
|
21878
|
+
});
|
|
21879
|
+
throw new Error(
|
|
21880
|
+
`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.`
|
|
21881
|
+
);
|
|
21882
|
+
}
|
|
21883
|
+
if (isDecisionPending(task)) {
|
|
21884
|
+
throw new Error(decisionRequiredResponse(taskId, readDecision(task)));
|
|
21885
|
+
}
|
|
21886
|
+
const attempts = await countFailedAttempts(adapter2, taskId);
|
|
21887
|
+
if (attempts == null) return {};
|
|
21888
|
+
if (attempts < loopThreshold()) return {};
|
|
21889
|
+
const blocker = pendingDecisionBlocker(attempts, task.cycle ?? 0, taskId);
|
|
21890
|
+
await safeUpdateBlocker(adapter2, taskId, blocker);
|
|
21891
|
+
throw new Error(decisionRequiredResponse(taskId, blocker.decision));
|
|
21892
|
+
}
|
|
21893
|
+
async function safeUpdateBlocker(adapter2, taskId, blocker) {
|
|
21894
|
+
try {
|
|
21895
|
+
await adapter2.updateTask(taskId, { blocker });
|
|
21896
|
+
} catch {
|
|
21897
|
+
}
|
|
21898
|
+
}
|
|
21689
21899
|
async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
21690
21900
|
const task = await adapter2.getTask(taskId);
|
|
21691
21901
|
if (!task) {
|
|
@@ -21702,6 +21912,8 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
21702
21912
|
if (task.status === "Done" || task.status === "Archived") {
|
|
21703
21913
|
throw new Error(`Task "${taskId}" (${task.title}) is already ${task.status}. Cannot execute a completed task.`);
|
|
21704
21914
|
}
|
|
21915
|
+
const loopDecision = await resolveLoopDecision(adapter2, task, taskId, options.decision);
|
|
21916
|
+
const injectedGuidance = loopDecision.guidance;
|
|
21705
21917
|
if (task.status === "In Review") {
|
|
21706
21918
|
throw new Error(
|
|
21707
21919
|
`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.`
|
|
@@ -21987,7 +22199,10 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
21987
22199
|
});
|
|
21988
22200
|
return {
|
|
21989
22201
|
task,
|
|
21990
|
-
|
|
22202
|
+
// task-2933: a `retry-differently` answer prepends its guidance here, so it
|
|
22203
|
+
// sits ABOVE the BUILD HANDOFF in the start output — the builder reads the
|
|
22204
|
+
// reason the last attempts failed before it reads what to build.
|
|
22205
|
+
branchLines: injectedGuidance ? [injectedGuidance, ...branchLines] : branchLines,
|
|
21991
22206
|
phaseChanges,
|
|
21992
22207
|
filesToWrite: collector.isEmpty() ? void 0 : collector
|
|
21993
22208
|
};
|
|
@@ -22622,9 +22837,10 @@ import { join as join13, relative } from "path";
|
|
|
22622
22837
|
import { homedir as homedir3 } from "os";
|
|
22623
22838
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
22624
22839
|
import { docDeletionBlockMessage } from "@papi-ai/shared";
|
|
22840
|
+
init_git();
|
|
22625
22841
|
var docRegisterTool = {
|
|
22626
22842
|
name: "doc_register",
|
|
22627
|
-
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.",
|
|
22843
|
+
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.",
|
|
22628
22844
|
annotations: { title: "Register Doc", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
22629
22845
|
inputSchema: {
|
|
22630
22846
|
type: "object",
|
|
@@ -22725,6 +22941,42 @@ Diagnostic JSON:
|
|
|
22725
22941
|
${JSON.stringify(payload, null, 2)}`
|
|
22726
22942
|
);
|
|
22727
22943
|
}
|
|
22944
|
+
function ensureDocDurable(path7, projectRoot) {
|
|
22945
|
+
if (!hasLocalWorkspace() || !projectRoot) return "";
|
|
22946
|
+
const warn = (reason, fix) => `
|
|
22947
|
+
|
|
22948
|
+
\u26A0\uFE0F **Registered, but NOT durable.** ${reason}
|
|
22949
|
+
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.
|
|
22950
|
+
**Fix:** ${fix}`;
|
|
22951
|
+
if (!existsSync9(join13(projectRoot, path7))) {
|
|
22952
|
+
return warn(
|
|
22953
|
+
`No file exists at \`${path7}\`.`,
|
|
22954
|
+
`write the doc body to that path, then re-run doc_register.`
|
|
22955
|
+
);
|
|
22956
|
+
}
|
|
22957
|
+
if (!isGitAvailable() || !isGitRepo(projectRoot)) {
|
|
22958
|
+
return warn(
|
|
22959
|
+
"This project is not a git repository (or git is unavailable), so the body cannot be committed.",
|
|
22960
|
+
`back the file up outside the working tree, or run \`git init\` and commit \`${path7}\`.`
|
|
22961
|
+
);
|
|
22962
|
+
}
|
|
22963
|
+
if (isPathTracked(projectRoot, path7)) return "";
|
|
22964
|
+
if (isPathIgnored(projectRoot, path7)) {
|
|
22965
|
+
return warn(
|
|
22966
|
+
`\`${path7}\` is excluded by .gitignore, so it cannot be committed (docs/private/ is ignored by design).`,
|
|
22967
|
+
`keep a copy outside the working tree, or move the doc to a tracked folder if it is not owner-only.`
|
|
22968
|
+
);
|
|
22969
|
+
}
|
|
22970
|
+
const result = commitSinglePath(projectRoot, path7, `docs: register ${path7}`);
|
|
22971
|
+
if (!result.committed) {
|
|
22972
|
+
return warn(
|
|
22973
|
+
`Committing \`${path7}\` failed: ${result.message}`,
|
|
22974
|
+
`run \`git add ${path7} && git commit -m "docs: register ${path7}"\` yourself.`
|
|
22975
|
+
);
|
|
22976
|
+
}
|
|
22977
|
+
return `
|
|
22978
|
+
- **Durability:** committed \`${path7}\` (was untracked)`;
|
|
22979
|
+
}
|
|
22728
22980
|
async function handleDocRegister(adapter2, args, config2) {
|
|
22729
22981
|
const adapterType = config2?.adapterType ?? "unknown";
|
|
22730
22982
|
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.";
|
|
@@ -22781,6 +23033,12 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
22781
23033
|
visibility
|
|
22782
23034
|
});
|
|
22783
23035
|
const visibilityLabel = entry.visibility === "contributors" ? "team member" : entry.visibility ?? "private";
|
|
23036
|
+
let durability = "";
|
|
23037
|
+
try {
|
|
23038
|
+
durability = ensureDocDurable(entry.path, config2?.projectRoot);
|
|
23039
|
+
} catch {
|
|
23040
|
+
durability = "";
|
|
23041
|
+
}
|
|
22784
23042
|
return textResponse(
|
|
22785
23043
|
`**Registered:** ${entry.title}
|
|
22786
23044
|
- **Path:** ${entry.path}
|
|
@@ -22788,7 +23046,7 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
22788
23046
|
- **Visibility:** ${visibilityLabel}
|
|
22789
23047
|
- **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
|
|
22790
23048
|
- **Actions:** ${actions?.length ?? 0} items
|
|
22791
|
-
- **ID:** ${entry.id}`
|
|
23049
|
+
- **ID:** ${entry.id}` + durability
|
|
22792
23050
|
);
|
|
22793
23051
|
} catch (err) {
|
|
22794
23052
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -23155,6 +23413,22 @@ var buildExecuteTool = {
|
|
|
23155
23413
|
type: "boolean",
|
|
23156
23414
|
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."
|
|
23157
23415
|
},
|
|
23416
|
+
decision: {
|
|
23417
|
+
type: "object",
|
|
23418
|
+
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.",
|
|
23419
|
+
properties: {
|
|
23420
|
+
answer: {
|
|
23421
|
+
type: "string",
|
|
23422
|
+
enum: ["retry-differently", "skip", "stop"],
|
|
23423
|
+
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.'
|
|
23424
|
+
},
|
|
23425
|
+
guidance: {
|
|
23426
|
+
type: "string",
|
|
23427
|
+
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.'
|
|
23428
|
+
}
|
|
23429
|
+
},
|
|
23430
|
+
required: ["answer"]
|
|
23431
|
+
},
|
|
23158
23432
|
completed: {
|
|
23159
23433
|
type: "string",
|
|
23160
23434
|
enum: ["yes", "no", "partial"],
|
|
@@ -23499,7 +23773,19 @@ async function handleBuildExecute(adapter2, config2, args, clientName) {
|
|
|
23499
23773
|
if (existing) resumeNote = formatResumeNote(existing);
|
|
23500
23774
|
}
|
|
23501
23775
|
await tracker.recordStep("started");
|
|
23502
|
-
const
|
|
23776
|
+
const decision = args.decision;
|
|
23777
|
+
const result = await startBuild(
|
|
23778
|
+
adapter2,
|
|
23779
|
+
config2,
|
|
23780
|
+
taskId,
|
|
23781
|
+
{
|
|
23782
|
+
light,
|
|
23783
|
+
// Only pass a decision the schema recognises — an unknown answer must not
|
|
23784
|
+
// reach the resolver and silently clear a gate.
|
|
23785
|
+
decision: decision?.answer && BUILD_DECISION_ANSWERS.includes(decision.answer) ? { answer: decision.answer, guidance: decision.guidance } : void 0
|
|
23786
|
+
},
|
|
23787
|
+
clientName
|
|
23788
|
+
);
|
|
23503
23789
|
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.task.cycle ?? null });
|
|
23504
23790
|
await tracker.recordStep("branch_ready");
|
|
23505
23791
|
tracker.mark("start_decorate_handoff");
|
|
@@ -28095,6 +28381,20 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
28095
28381
|
lines.push(`**In Review:** ${health.inReviewSummary}`);
|
|
28096
28382
|
lines.push("");
|
|
28097
28383
|
}
|
|
28384
|
+
if (buildInfo.pendingDecisions.length > 0) {
|
|
28385
|
+
const n = buildInfo.pendingDecisions.length;
|
|
28386
|
+
lines.push("## Decision required");
|
|
28387
|
+
lines.push(
|
|
28388
|
+
`${n} task${n === 1 ? " has" : "s have"} stopped after repeated failed builds. PAPI will not retry until you answer.`
|
|
28389
|
+
);
|
|
28390
|
+
for (const d of buildInfo.pendingDecisions) {
|
|
28391
|
+
lines.push(`- **${d.id}:** ${d.title} \u2014 ${d.attempts} failed attempts`);
|
|
28392
|
+
}
|
|
28393
|
+
lines.push(
|
|
28394
|
+
'Answer with `build_execute <task> decision:{answer:"retry-differently", guidance:"\u2026"}` to pick it back up, or `"skip"` / `"stop"` to leave it parked.'
|
|
28395
|
+
);
|
|
28396
|
+
lines.push("");
|
|
28397
|
+
}
|
|
28098
28398
|
if (buildInfo.isEmpty) {
|
|
28099
28399
|
lines.push("## Tasks");
|
|
28100
28400
|
if (buildInfo.currentCycle === 0) {
|
|
@@ -28363,6 +28663,19 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
28363
28663
|
inReview: healthResult.inReviewSummary,
|
|
28364
28664
|
backlogCount: buildResult.backlog.length,
|
|
28365
28665
|
blockedCount: buildResult.blocked.length,
|
|
28666
|
+
// task-2933: computed here where the raw tasks are in scope. Best-effort —
|
|
28667
|
+
// a malformed blocker must never break orient.
|
|
28668
|
+
pendingDecisions: allTasks.filter((t) => {
|
|
28669
|
+
try {
|
|
28670
|
+
return isDecisionPending(t);
|
|
28671
|
+
} catch {
|
|
28672
|
+
return false;
|
|
28673
|
+
}
|
|
28674
|
+
}).map((t) => ({
|
|
28675
|
+
id: t.displayId ?? t.id,
|
|
28676
|
+
title: t.title,
|
|
28677
|
+
attempts: readDecision(t)?.attempts ?? 0
|
|
28678
|
+
})),
|
|
28366
28679
|
totalHandoffs: buildResult.sorted.length + buildResult.blocked.length,
|
|
28367
28680
|
currentCycle,
|
|
28368
28681
|
isEmpty: buildResult.isEmpty,
|
|
@@ -30062,46 +30375,6 @@ ${d.body}`;
|
|
|
30062
30375
|
${formatted}`, meta));
|
|
30063
30376
|
}
|
|
30064
30377
|
|
|
30065
|
-
// src/lib/tool-telemetry.ts
|
|
30066
|
-
var INSTRUMENTED_TOOLS = /* @__PURE__ */ new Set([
|
|
30067
|
-
"plan",
|
|
30068
|
-
"orient",
|
|
30069
|
-
"build_execute",
|
|
30070
|
-
"review_submit",
|
|
30071
|
-
"release",
|
|
30072
|
-
"strategy_review"
|
|
30073
|
-
]);
|
|
30074
|
-
var WRAPPER_INSTRUMENTED_TOOLS = new Set(
|
|
30075
|
-
[...INSTRUMENTED_TOOLS].filter((t) => t !== "plan")
|
|
30076
|
-
);
|
|
30077
|
-
function measureResultBytes(content) {
|
|
30078
|
-
let total = 0;
|
|
30079
|
-
for (const part of content) {
|
|
30080
|
-
if (typeof part.text === "string") total += Buffer.byteLength(part.text, "utf-8");
|
|
30081
|
-
}
|
|
30082
|
-
return total;
|
|
30083
|
-
}
|
|
30084
|
-
function recordToolRun(adapter2, sample) {
|
|
30085
|
-
if (!WRAPPER_INSTRUMENTED_TOOLS.has(sample.toolName)) return;
|
|
30086
|
-
if (typeof adapter2.insertToolRun !== "function") return;
|
|
30087
|
-
try {
|
|
30088
|
-
const promise = adapter2.insertToolRun({
|
|
30089
|
-
toolName: sample.toolName,
|
|
30090
|
-
contextBytes: sample.contextBytes,
|
|
30091
|
-
durationMs: sample.durationMs,
|
|
30092
|
-
cycleNumber: sample.cycleNumber ?? null,
|
|
30093
|
-
source: "mcp-server"
|
|
30094
|
-
});
|
|
30095
|
-
if (promise && typeof promise.catch === "function") {
|
|
30096
|
-
promise.catch((err) => {
|
|
30097
|
-
console.error(`[telemetry] insertToolRun failed (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
|
|
30098
|
-
});
|
|
30099
|
-
}
|
|
30100
|
-
} catch (err) {
|
|
30101
|
-
console.error(`[telemetry] insertToolRun threw (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
|
|
30102
|
-
}
|
|
30103
|
-
}
|
|
30104
|
-
|
|
30105
30378
|
// src/tools/learning-action.ts
|
|
30106
30379
|
var learningActionTool = {
|
|
30107
30380
|
name: "learning_action",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.74",
|
|
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",
|