@papi-ai/server 0.7.52 → 0.7.53

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.
@@ -25,6 +25,7 @@ __export(git_exports, {
25
25
  detectUnrecordedCommits: () => detectUnrecordedCommits,
26
26
  ensureLatestDevelop: () => ensureLatestDevelop,
27
27
  ensureTagAtHead: () => ensureTagAtHead,
28
+ findTaskCommitsOnBase: () => findTaskCommitsOnBase,
28
29
  getBranchDiff: () => getBranchDiff,
29
30
  getCommitsSinceTag: () => getCommitsSinceTag,
30
31
  getCurrentBranch: () => getCurrentBranch,
@@ -718,11 +719,29 @@ function escapeRegexLiteral(s) {
718
719
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
719
720
  }
720
721
  function detectMergedInProgress(cwd, preferredBase, tasks) {
721
- if (!isGitAvailable() || !isGitRepo(cwd)) return [];
722
722
  const inProgress = tasks.filter((t) => t.status === "In Progress");
723
723
  if (inProgress.length === 0) return [];
724
+ const landed = findTaskCommitsOnBase(cwd, preferredBase, inProgress.map((t) => t.displayId));
725
+ const hits = [];
726
+ for (const task of inProgress) {
727
+ const hit = landed.get(task.displayId);
728
+ if (!hit) continue;
729
+ hits.push({
730
+ displayId: task.displayId,
731
+ title: task.title,
732
+ commit: hit.commit,
733
+ subject: hit.subject,
734
+ pr: hit.pr
735
+ });
736
+ }
737
+ return hits;
738
+ }
739
+ function findTaskCommitsOnBase(cwd, preferredBase, displayIds) {
740
+ const out = /* @__PURE__ */ new Map();
741
+ if (displayIds.length === 0) return out;
742
+ if (!isGitAvailable() || !isGitRepo(cwd)) return out;
724
743
  const base = resolveDefaultBranch(cwd, preferredBase);
725
- if (!branchExists(cwd, base)) return [];
744
+ if (!branchExists(cwd, base)) return out;
726
745
  let raw;
727
746
  try {
728
747
  raw = execFileSync(
@@ -731,28 +750,25 @@ function detectMergedInProgress(cwd, preferredBase, tasks) {
731
750
  { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
732
751
  );
733
752
  } catch {
734
- return [];
753
+ return out;
735
754
  }
736
755
  const commits = raw.split("\n").map((line) => {
737
756
  const idx = line.indexOf("");
738
757
  if (idx === -1) return null;
739
758
  return { hash: line.slice(0, idx).trim(), subject: line.slice(idx + 1).trim() };
740
759
  }).filter((c) => c !== null && c.hash !== "");
741
- const hits = [];
742
- for (const task of inProgress) {
743
- const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(task.displayId)}([^\\w-]|$)`);
760
+ for (const displayId of displayIds) {
761
+ const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(displayId)}([^\\w-]|$)`);
744
762
  const hit = commits.find((c) => re.test(c.subject));
745
763
  if (!hit) continue;
746
764
  const prMatch = hit.subject.match(/#(\d+)/);
747
- hits.push({
748
- displayId: task.displayId,
749
- title: task.title,
765
+ out.set(displayId, {
750
766
  commit: hit.hash,
751
767
  subject: hit.subject,
752
768
  pr: prMatch ? `#${prMatch[1]}` : null
753
769
  });
754
770
  }
755
- return hits;
771
+ return out;
756
772
  }
757
773
  function detectUnrecordedCommits(cwd, baseBranch) {
758
774
  if (!isGitAvailable() || !isGitRepo(cwd)) return [];
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ __export(git_exports, {
26
26
  detectUnrecordedCommits: () => detectUnrecordedCommits,
27
27
  ensureLatestDevelop: () => ensureLatestDevelop,
28
28
  ensureTagAtHead: () => ensureTagAtHead,
29
+ findTaskCommitsOnBase: () => findTaskCommitsOnBase,
29
30
  getBranchDiff: () => getBranchDiff,
30
31
  getCommitsSinceTag: () => getCommitsSinceTag,
31
32
  getCurrentBranch: () => getCurrentBranch,
@@ -719,11 +720,29 @@ function escapeRegexLiteral(s) {
719
720
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
720
721
  }
721
722
  function detectMergedInProgress(cwd, preferredBase, tasks) {
722
- if (!isGitAvailable() || !isGitRepo(cwd)) return [];
723
723
  const inProgress = tasks.filter((t) => t.status === "In Progress");
724
724
  if (inProgress.length === 0) return [];
725
+ const landed = findTaskCommitsOnBase(cwd, preferredBase, inProgress.map((t) => t.displayId));
726
+ const hits = [];
727
+ for (const task of inProgress) {
728
+ const hit = landed.get(task.displayId);
729
+ if (!hit) continue;
730
+ hits.push({
731
+ displayId: task.displayId,
732
+ title: task.title,
733
+ commit: hit.commit,
734
+ subject: hit.subject,
735
+ pr: hit.pr
736
+ });
737
+ }
738
+ return hits;
739
+ }
740
+ function findTaskCommitsOnBase(cwd, preferredBase, displayIds) {
741
+ const out = /* @__PURE__ */ new Map();
742
+ if (displayIds.length === 0) return out;
743
+ if (!isGitAvailable() || !isGitRepo(cwd)) return out;
725
744
  const base = resolveDefaultBranch(cwd, preferredBase);
726
- if (!branchExists(cwd, base)) return [];
745
+ if (!branchExists(cwd, base)) return out;
727
746
  let raw;
728
747
  try {
729
748
  raw = execFileSync(
@@ -732,28 +751,25 @@ function detectMergedInProgress(cwd, preferredBase, tasks) {
732
751
  { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
733
752
  );
734
753
  } catch {
735
- return [];
754
+ return out;
736
755
  }
737
756
  const commits = raw.split("\n").map((line) => {
738
757
  const idx = line.indexOf("");
739
758
  if (idx === -1) return null;
740
759
  return { hash: line.slice(0, idx).trim(), subject: line.slice(idx + 1).trim() };
741
760
  }).filter((c) => c !== null && c.hash !== "");
742
- const hits = [];
743
- for (const task of inProgress) {
744
- const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(task.displayId)}([^\\w-]|$)`);
761
+ for (const displayId of displayIds) {
762
+ const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(displayId)}([^\\w-]|$)`);
745
763
  const hit = commits.find((c) => re.test(c.subject));
746
764
  if (!hit) continue;
747
765
  const prMatch = hit.subject.match(/#(\d+)/);
748
- hits.push({
749
- displayId: task.displayId,
750
- title: task.title,
766
+ out.set(displayId, {
751
767
  commit: hit.hash,
752
768
  subject: hit.subject,
753
769
  pr: prMatch ? `#${prMatch[1]}` : null
754
770
  });
755
771
  }
756
- return hits;
772
+ return out;
757
773
  }
758
774
  function detectUnrecordedCommits(cwd, baseBranch) {
759
775
  if (!isGitAvailable() || !isGitRepo(cwd)) return [];
@@ -19087,15 +19103,35 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
19087
19103
  if (adapter2 && resolvedCycleNum > 0) {
19088
19104
  try {
19089
19105
  const stampedAt = (/* @__PURE__ */ new Date()).toISOString();
19106
+ const resolvedBase = resolveBaseBranch(config2.projectRoot, branch);
19090
19107
  const doneCycleTasks = (await adapter2.queryBoard()).filter(
19091
19108
  (t) => t.cycle === resolvedCycleNum && t.status === "Done"
19092
19109
  );
19110
+ const canVerify = isGitAvailable() && isGitRepo(config2.projectRoot);
19111
+ const landed = canVerify ? findTaskCommitsOnBase(
19112
+ config2.projectRoot,
19113
+ resolvedBase,
19114
+ doneCycleTasks.map((t) => t.displayId)
19115
+ ) : /* @__PURE__ */ new Map();
19116
+ const unverified = [];
19093
19117
  for (const t of doneCycleTasks) {
19118
+ const hit = landed.get(t.displayId);
19119
+ if (canVerify && !hit) {
19120
+ unverified.push(
19121
+ `${t.displayId}${t.branchName ? ` (branch \`${t.branchName}\`)` : " (no branch recorded)"}`
19122
+ );
19123
+ continue;
19124
+ }
19094
19125
  try {
19095
19126
  await adapter2.updateTask(t.id, { mergedAt: stampedAt });
19096
19127
  } catch {
19097
19128
  }
19098
19129
  }
19130
+ if (unverified.length > 0) {
19131
+ warnings.push(
19132
+ `\u26A0\uFE0F DATA-INTEGRITY \u2014 ${unverified.length} task(s) are marked **Done** in cycle ${resolvedCycleNum} but their work is NOT on ${resolvedBase}: ${unverified.join("; ")}. They were NOT stamped as merged. Held ad-hoc work is the usual cause (\`ad_hoc\` + \`board_edit\` leaves the branch unmerged by design). Merge the branch(es), or move the task(s) to the next cycle \u2014 do not leave Done unbacked by git.`
19133
+ );
19134
+ }
19099
19135
  } catch {
19100
19136
  }
19101
19137
  }
@@ -19155,6 +19191,44 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
19155
19191
  };
19156
19192
  }
19157
19193
 
19194
+ // src/services/release-bookkeeping.ts
19195
+ async function beginRelease(tracker, cycleNum) {
19196
+ tracker.setStreamScope({ cycle: cycleNum ?? null });
19197
+ await tracker.recordStep("release_starting");
19198
+ }
19199
+ async function recordReadinessVerified(tracker) {
19200
+ await tracker.recordStep("readiness_verified");
19201
+ }
19202
+ async function recordQualityGate(tracker, decision, caps) {
19203
+ await tracker.recordStep("quality_gate", {
19204
+ status: decision.stepStatus,
19205
+ capabilityKey: "releaseGate",
19206
+ capabilityEnabled: isCapabilityEnabled(caps, "releaseGate"),
19207
+ metadata: decision.metadata
19208
+ });
19209
+ }
19210
+ async function completeRelease(tracker, opts) {
19211
+ tracker.setStreamScope({ cycle: opts.cycleClosed ?? null });
19212
+ await tracker.recordStep("cycle_complete", { metadata: { version: opts.version } });
19213
+ for (const m of opts.branchMerges ?? []) {
19214
+ await tracker.recordStep("branch_merged", {
19215
+ metadata: { branch: m.branch, prUrl: m.prUrl ?? null }
19216
+ });
19217
+ }
19218
+ await tracker.recordStep("changelog", {
19219
+ capabilityKey: "changelog",
19220
+ capabilityEnabled: isCapabilityEnabled(opts.caps, "changelog"),
19221
+ status: opts.changelogEmitted ? "complete" : "active"
19222
+ });
19223
+ if (opts.deployHookEmitted) {
19224
+ await tracker.recordStep("deploy_hook", {
19225
+ capabilityKey: "deployHook",
19226
+ capabilityEnabled: isCapabilityEnabled(opts.caps, "deployHook")
19227
+ });
19228
+ }
19229
+ await tracker.recordStep("released", { metadata: { version: opts.version } });
19230
+ }
19231
+
19158
19232
  // src/tools/release.ts
19159
19233
  init_git();
19160
19234
 
@@ -19444,9 +19518,16 @@ async function handleRelease(adapter2, config2, args) {
19444
19518
  }
19445
19519
  const tracker = new ProgressTracker("validate-args").bindStream(adapter2, { stage: "release" });
19446
19520
  try {
19447
- await tracker.recordStep("release_starting");
19448
19521
  tracker.mark("owner-identity-guard");
19449
19522
  const gate = await resolveOwnerGate(adapter2, config2);
19523
+ let cycleToClose = null;
19524
+ try {
19525
+ const resolved = await resolveCycleToClose(adapter2, version, gate.callerUserId);
19526
+ cycleToClose = resolved > 0 ? resolved : null;
19527
+ } catch {
19528
+ cycleToClose = null;
19529
+ }
19530
+ await beginRelease(tracker, cycleToClose);
19450
19531
  const productionBaseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
19451
19532
  if (branch === productionBaseBranch) {
19452
19533
  if (gate.enforced && !gate.callerIsOwner) {
@@ -19511,6 +19592,13 @@ Next: run \`plan\` to start your next cycle.`
19511
19592
  }
19512
19593
  }
19513
19594
  }
19595
+ let caps = {};
19596
+ try {
19597
+ const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
19598
+ caps = info?.capabilities ?? {};
19599
+ } catch {
19600
+ caps = {};
19601
+ }
19514
19602
  if (isHostedTransport()) {
19515
19603
  tracker.mark("hosted-close-cycle");
19516
19604
  let closed;
@@ -19523,6 +19611,16 @@ Next: run \`plan\` to start your next cycle.`
19523
19611
  const message = err instanceof Error ? err.message : String(err);
19524
19612
  return errorResponse(message);
19525
19613
  }
19614
+ await recordReadinessVerified(tracker);
19615
+ const hostedGate = evaluateReleaseGate(caps, config2.gateCommand, gateResult);
19616
+ await recordQualityGate(tracker, hostedGate, caps);
19617
+ await completeRelease(tracker, {
19618
+ cycleClosed: closed.resolvedCycleNum > 0 ? closed.resolvedCycleNum : null,
19619
+ version,
19620
+ caps,
19621
+ branchMerges: [],
19622
+ changelogEmitted: false
19623
+ });
19526
19624
  const tagAnnotation = `Release ${version}`;
19527
19625
  const cyclePart = closed.resolvedCycleNum > 0 ? `Cycle ${closed.resolvedCycleNum}` : "The cycle";
19528
19626
  const warningsBlock = closed.warnings.length > 0 ? `
@@ -19583,22 +19681,10 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
19583
19681
  console.error(`[release] gh CLI not available \u2014 ${pendingGrouped.length} shared branch(es) will be squash-merged via git: ${pendingGrouped.join(", ")}`);
19584
19682
  }
19585
19683
  }
19586
- await tracker.recordStep("readiness_verified");
19587
- let caps = {};
19588
- try {
19589
- const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
19590
- caps = info?.capabilities ?? {};
19591
- } catch {
19592
- caps = {};
19593
- }
19684
+ await recordReadinessVerified(tracker);
19594
19685
  tracker.mark("quality-gate");
19595
19686
  const gateDecision = evaluateReleaseGate(caps, config2.gateCommand, gateResult);
19596
- await tracker.recordStep("quality_gate", {
19597
- status: gateDecision.stepStatus,
19598
- capabilityKey: "releaseGate",
19599
- capabilityEnabled: isCapabilityEnabled(caps, "releaseGate"),
19600
- metadata: gateDecision.metadata
19601
- });
19687
+ await recordQualityGate(tracker, gateDecision, caps);
19602
19688
  if (gateDecision.action === "directive") {
19603
19689
  return textResponse(gateDecision.message);
19604
19690
  }
@@ -19611,13 +19697,6 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
19611
19697
  skipVersion: skipVersion ?? false,
19612
19698
  callerUserId: gate.callerUserId
19613
19699
  });
19614
- tracker.setStreamScope({ cycle: result.cycleClosed ?? null });
19615
- await tracker.recordStep("cycle_complete", { metadata: { version: result.version } });
19616
- for (const m of result.groupedBranchMerges ?? []) {
19617
- await tracker.recordStep("branch_merged", {
19618
- metadata: { branch: m.branch, prUrl: m.prUrl ?? null }
19619
- });
19620
- }
19621
19700
  const lines = [
19622
19701
  `## Release ${result.version}${skipVersion ? " (skip version)" : ""}`,
19623
19702
  "",
@@ -19672,21 +19751,16 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
19672
19751
  buildCycleUpdateCurationDirective(result.version, result.cycleClosed ?? 0)
19673
19752
  );
19674
19753
  if (cycleUpdateDirective) lines.push(cycleUpdateDirective);
19675
- await tracker.recordStep("changelog", {
19676
- capabilityKey: "changelog",
19677
- capabilityEnabled: isCapabilityEnabled(caps, "changelog"),
19678
- status: cycleUpdateDirective ? "complete" : "active"
19679
- });
19680
19754
  const deployDirective = buildDeployHookDirective(caps, config2.deployCommand);
19681
- if (deployDirective) {
19682
- lines.push(deployDirective);
19683
- await tracker.recordStep("deploy_hook", {
19684
- capabilityKey: "deployHook",
19685
- capabilityEnabled: isCapabilityEnabled(caps, "deployHook"),
19686
- status: "complete",
19687
- metadata: { configured: true }
19688
- });
19689
- }
19755
+ if (deployDirective) lines.push(deployDirective);
19756
+ await completeRelease(tracker, {
19757
+ cycleClosed: result.cycleClosed ?? null,
19758
+ version: result.version,
19759
+ caps,
19760
+ branchMerges: result.groupedBranchMerges ?? [],
19761
+ changelogEmitted: Boolean(cycleUpdateDirective),
19762
+ deployHookEmitted: Boolean(deployDirective)
19763
+ });
19690
19764
  const publishEnabled = isCapabilityEnabled(caps, "publishDirective");
19691
19765
  if (publishEnabled) {
19692
19766
  try {
@@ -19714,7 +19788,6 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
19714
19788
  const verifyDirective = buildVerifyHealthCheckDirective(caps);
19715
19789
  if (verifyDirective) lines.push(verifyDirective);
19716
19790
  lines.push("", `Next: cycle released! Run \`plan\` to start your next cycle, or \`idea "<what's next>"\` first if your backlog is thin.`);
19717
- await tracker.recordStep("released", { metadata: { version: result.version } });
19718
19791
  const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
19719
19792
  return textResponse(lines.join("\n") + filesToWriteSection);
19720
19793
  } catch (err) {
@@ -19874,32 +19947,94 @@ function capitalizeCompleted(value) {
19874
19947
  };
19875
19948
  return map[value] ?? "No";
19876
19949
  }
19950
+ function stripAnnotations(text) {
19951
+ let out = "";
19952
+ let i = 0;
19953
+ while (i < text.length) {
19954
+ const ch = text[i];
19955
+ if (ch === ")") {
19956
+ i++;
19957
+ continue;
19958
+ }
19959
+ if (ch === "(") {
19960
+ let depth = 0;
19961
+ let j = i;
19962
+ for (; j < text.length; j++) {
19963
+ if (text[j] === "(") depth++;
19964
+ else if (text[j] === ")") {
19965
+ depth--;
19966
+ if (depth === 0) {
19967
+ j++;
19968
+ break;
19969
+ }
19970
+ }
19971
+ }
19972
+ if (depth !== 0) {
19973
+ break;
19974
+ }
19975
+ const inner = text.slice(i + 1, j - 1);
19976
+ const prev = i > 0 ? text[i - 1] : "/";
19977
+ const next = text[j] ?? "";
19978
+ const isRouteGroup = (prev === "/" || i === 0) && !/\s/.test(inner) && next === "/";
19979
+ if (isRouteGroup) {
19980
+ out += text.slice(i, j);
19981
+ i = j;
19982
+ continue;
19983
+ }
19984
+ i = j;
19985
+ continue;
19986
+ }
19987
+ out += ch;
19988
+ i++;
19989
+ }
19990
+ return out;
19991
+ }
19877
19992
  function sanitisePredictedFiles(raw) {
19878
19993
  const out = [];
19879
19994
  for (const entry of raw) {
19880
19995
  if (typeof entry !== "string") continue;
19881
- for (const segment of entry.split(";")) {
19882
- const altMatches = segment.match(/\(\s*or\s+`([^`]+)`\s*\)/gi) ?? [];
19883
- for (const alt of altMatches) {
19884
- const inner = alt.match(/`([^`]+)`/);
19885
- if (inner) out.push(inner[1].trim());
19886
- }
19887
- const cleaned = segment.replace(/`/g, "").replace(/\s*\(.*$/, "").trim();
19888
- if (cleaned) out.push(cleaned);
19996
+ let working = entry;
19997
+ const altMatches = working.match(/\(\s*or\s+`([^`]+)`\s*\)/gi) ?? [];
19998
+ for (const alt of altMatches) {
19999
+ const inner = alt.match(/`([^`]+)`/);
20000
+ if (inner) out.push(inner[1].trim());
20001
+ working = working.replace(alt, " ");
20002
+ }
20003
+ const cleanedEntry = stripAnnotations(working.replace(/`/g, ""));
20004
+ for (const segment of cleanedEntry.split(/[;,]/)) {
20005
+ const cleaned = segment.trim();
20006
+ if (!cleaned) continue;
20007
+ if (/\s/.test(cleaned)) continue;
20008
+ if (!/[A-Za-z0-9]/.test(cleaned)) continue;
20009
+ out.push(cleaned);
19889
20010
  }
19890
20011
  }
19891
20012
  return Array.from(new Set(out));
19892
20013
  }
20014
+ function globToRegExp(entry) {
20015
+ const source = entry.split(/(\*\*|\*)/).map((part) => {
20016
+ if (part === "**") return ".*";
20017
+ if (part === "*") return "[^/]*";
20018
+ return part.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
20019
+ }).join("");
20020
+ return new RegExp(`^${source}$`, "i");
20021
+ }
19893
20022
  function pathBasename(p) {
19894
20023
  const parts = p.replace(/\\/g, "/").replace(/\/+$/, "").split("/");
19895
20024
  return parts[parts.length - 1] ?? p;
19896
20025
  }
19897
20026
  function isPathInPredictedScope(changedPath, predicted) {
19898
- const changedLower = changedPath.replace(/\\/g, "/").toLowerCase();
20027
+ const changed = changedPath.replace(/\\/g, "/");
20028
+ const changedLower = changed.toLowerCase();
19899
20029
  const changedBase = pathBasename(changedPath);
19900
20030
  for (const raw of predicted) {
19901
20031
  const entry = raw.replace(/\\/g, "/").replace(/\/+$/, "").trim();
19902
20032
  if (!entry) continue;
20033
+ if (entry.includes("*")) {
20034
+ if (globToRegExp(entry).test(changed)) return true;
20035
+ if (!entry.includes("/") && globToRegExp(entry).test(changedBase)) return true;
20036
+ continue;
20037
+ }
19903
20038
  if (pathBasename(entry) === changedBase) return true;
19904
20039
  const entryLower = entry.toLowerCase();
19905
20040
  if (changedLower === entryLower || changedLower.startsWith(`${entryLower}/`)) {
@@ -19943,23 +20078,27 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
19943
20078
  };
19944
20079
  const cleanedPredicted = sanitisePredictedFiles(predictedFiles);
19945
20080
  const scoped = modified.filter((p) => isPathInPredictedScope(p, cleanedPredicted));
19946
- if (scoped.length === 0) {
20081
+ const untracked = getUntrackedFiles(cwd);
20082
+ const scopedSet = new Set(scoped);
20083
+ const untrackedInScope = untracked.filter(
20084
+ (p) => !scopedSet.has(p) && isPathInPredictedScope(p, cleanedPredicted)
20085
+ );
20086
+ if (scoped.length === 0 && untrackedInScope.length === 0) {
19947
20087
  const modSample = modified.slice(0, 5).join(", ");
19948
20088
  const predSample = cleanedPredicted.slice(0, 5).join(", ");
19949
20089
  return `Auto-commit: refused \u2014 none of the ${modified.length} modified file(s) intersect FILES LIKELY TOUCHED. Modified: ${modSample}. Expected: ${predSample}. Stage the intended files manually (\`git add <paths>\`) then re-run, or set PAPI_AUTO_COMMIT=false.`;
19950
20090
  }
19951
- const untracked = getUntrackedFiles(cwd);
19952
20091
  const scopedDirs = [...new Set(scoped.map(dirname7).filter((d) => d.length > 0))];
19953
20092
  const isUnderScopedDir = (p) => scopedDirs.some((d) => p === d || p.startsWith(`${d}/`) || p.startsWith(`${d}\\`));
19954
- const scopedSet = new Set(scoped);
20093
+ const inScopeSet = /* @__PURE__ */ new Set([...scoped, ...untrackedInScope]);
19955
20094
  const adjacentUntracked = untracked.filter(
19956
- (p) => !scopedSet.has(p) && isUnderScopedDir(p)
20095
+ (p) => !inScopeSet.has(p) && isUnderScopedDir(p)
19957
20096
  );
19958
- const toStage = [...scoped, ...adjacentUntracked];
20097
+ const toStage = [...scoped, ...untrackedInScope, ...adjacentUntracked];
19959
20098
  const toStageSet = new Set(toStage);
19960
20099
  const droppedUntracked = untracked.filter((p) => !toStageSet.has(p));
19961
20100
  const droppedModified = modified.filter((p) => !scopedSet.has(p));
19962
- let line = safeRun(() => stagePathsAndCommit(cwd, toStage, message)) + ` (scoped to ${scoped.length}/${modified.length} files via FILES LIKELY TOUCHED` + (adjacentUntracked.length > 0 ? ` + ${adjacentUntracked.length} untracked under scoped dir(s)` : "") + `).`;
20101
+ let line = safeRun(() => stagePathsAndCommit(cwd, toStage, message)) + ` (scoped to ${scoped.length}/${modified.length} files via FILES LIKELY TOUCHED` + (untrackedInScope.length > 0 ? ` + ${untrackedInScope.length} new file(s) named in the handoff` : "") + (adjacentUntracked.length > 0 ? ` + ${adjacentUntracked.length} untracked under scoped dir(s)` : "") + `).`;
19963
20102
  if (droppedModified.length > 0) {
19964
20103
  const sample = droppedModified.slice(0, 10).join(", ");
19965
20104
  const more = droppedModified.length > 10 ? ` (+${droppedModified.length - 10} more)` : "";
@@ -24035,11 +24174,38 @@ Merge or squash those PRs first, then run \`release\` manually.`;
24035
24174
  } catch {
24036
24175
  }
24037
24176
  const version = `v0.${result.currentCycle}.0`;
24038
- const releaseResult = await createRelease(config2, baseBranch, version, adapter2, result.currentCycle);
24039
- await tracker.recordStep("auto_release_triggered", { metadata: { version: releaseResult.version } });
24040
- const pushInfo = releaseResult.pushNotes.join(" ");
24041
- const groupedMergeNote = releaseResult.groupedBranchMerges?.length ? "\n" + releaseResult.groupedBranchMerges.map((r) => `- Merged shared branch \`${r.branch}\` via PR: ${r.prUrl ?? "n/a"}`).join("\n") : "";
24042
- autoReleaseNote = `
24177
+ const autoGate = evaluateReleaseGate(caps, config2.gateCommand, void 0);
24178
+ if (autoGate.action !== "proceed") {
24179
+ autoReleaseNote = `
24180
+
24181
+ ---
24182
+
24183
+ \u26A0\uFE0F **Auto-release skipped** \u2014 a release quality gate is configured (\`${config2.gateCommand}\`), and it cannot be run from inside \`review_submit\`.
24184
+
24185
+ Run \`release\` manually: PAPI will hand you the gate command, then release once you report it green.`;
24186
+ } else {
24187
+ const releaseTracker = new ProgressTracker("auto-release").bindStream(adapter2, { stage: "release" });
24188
+ await beginRelease(releaseTracker, result.currentCycle);
24189
+ const releaseResult = await createRelease(config2, baseBranch, version, adapter2, result.currentCycle);
24190
+ await recordReadinessVerified(releaseTracker);
24191
+ await recordQualityGate(releaseTracker, autoGate, caps);
24192
+ await tracker.recordStep("auto_release_triggered", { metadata: { version: releaseResult.version } });
24193
+ const autoChangelogDirective = buildChangelogDirective(
24194
+ caps,
24195
+ buildCycleUpdateCurationDirective(releaseResult.version, releaseResult.cycleClosed ?? 0)
24196
+ );
24197
+ const autoDeployDirective = buildDeployHookDirective(caps, config2.deployCommand);
24198
+ await completeRelease(releaseTracker, {
24199
+ cycleClosed: releaseResult.cycleClosed ?? null,
24200
+ version: releaseResult.version,
24201
+ caps,
24202
+ branchMerges: releaseResult.groupedBranchMerges ?? [],
24203
+ changelogEmitted: Boolean(autoChangelogDirective),
24204
+ deployHookEmitted: Boolean(autoDeployDirective)
24205
+ });
24206
+ const pushInfo = releaseResult.pushNotes.join(" ");
24207
+ const groupedMergeNote = releaseResult.groupedBranchMerges?.length ? "\n" + releaseResult.groupedBranchMerges.map((r) => `- Merged shared branch \`${r.branch}\` via PR: ${r.prUrl ?? "n/a"}`).join("\n") : "";
24208
+ autoReleaseNote = `
24043
24209
 
24044
24210
  ---
24045
24211
 
@@ -24049,9 +24215,15 @@ Merge or squash those PRs first, then run \`release\` manually.`;
24049
24215
  - ${releaseResult.commitNote}
24050
24216
  - ${releaseResult.tagMessage}
24051
24217
  - ${pushInfo}` + groupedMergeNote + (releaseResult.warnings?.length ? `
24052
- - Warnings: ${releaseResult.warnings.join(", ")}` : "") + `
24218
+ - Warnings: ${releaseResult.warnings.join(", ")}` : "") + // task-2598 (C328): the auto path previously swallowed the curated
24219
+ // cycle-update directive that the manual path emits, so an auto-released
24220
+ // cycle never prompted the Discord post. Same directive, same gate.
24221
+ (autoChangelogDirective ? `
24222
+ ${autoChangelogDirective}` : "") + (autoDeployDirective ? `
24223
+ ${autoDeployDirective}` : "") + `
24053
24224
 
24054
24225
  Run \`plan\` to create Cycle ${result.currentCycle + 1}.`;
24226
+ }
24055
24227
  }
24056
24228
  }
24057
24229
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.52",
3
+ "version": "0.7.53",
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",