@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.
@@ -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
- branchLines,
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 result = await startBuild(adapter2, config2, taskId, { light }, clientName);
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.72",
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",