@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.
- package/dist/backfill-cycle-metrics.js +26 -10
- package/dist/index.js +240 -68
- package/package.json +1 -1
|
@@ -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
|
|
742
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
743
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
19683
|
-
|
|
19684
|
-
|
|
19685
|
-
|
|
19686
|
-
|
|
19687
|
-
|
|
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
|
-
|
|
19882
|
-
|
|
19883
|
-
|
|
19884
|
-
|
|
19885
|
-
|
|
19886
|
-
|
|
19887
|
-
|
|
19888
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
20093
|
+
const inScopeSet = /* @__PURE__ */ new Set([...scoped, ...untrackedInScope]);
|
|
19955
20094
|
const adjacentUntracked = untracked.filter(
|
|
19956
|
-
(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
|
|
24039
|
-
|
|
24040
|
-
|
|
24041
|
-
|
|
24042
|
-
|
|
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.
|
|
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",
|