@papi-ai/server 0.7.84 → 0.7.85
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 +80 -9
- package/dist/index.js +562 -77
- package/dist/prompts.js +7 -3
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -73,11 +73,20 @@ function resolveDeploymentProfile(env) {
|
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
+
function isNoneLeadProse(trimmed) {
|
|
77
|
+
const m = NONE_LEAD_RE.exec(trimmed);
|
|
78
|
+
if (!m) return false;
|
|
79
|
+
if (/\bP[0-3]\s*[:—–-]/.test(trimmed)) return false;
|
|
80
|
+
const rest = (m.groups?.rest ?? "").replace(/^[-•]\s*/, "").trim();
|
|
81
|
+
if (CONTRAST_LEAD_RE.test(rest)) return false;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
76
84
|
function splitFindings(raw, kind) {
|
|
77
85
|
if (!raw) return [];
|
|
78
86
|
const trimmed = raw.trim();
|
|
79
87
|
if (trimmed.length === 0 || NONE_RE.test(trimmed)) return [];
|
|
80
88
|
if (kind !== "issue" && AD_HOC_PLACEHOLDER_RE.test(trimmed)) return [];
|
|
89
|
+
if (isNoneLeadProse(trimmed)) return [];
|
|
81
90
|
const chunks = trimmed.split(/\n+/).flatMap((line) => line.split(/(?=\bP[0-3]\s*[:—-])/)).map((s) => s.trim()).filter((s) => s.length > 0 && !NONE_RE.test(s));
|
|
82
91
|
const items = [];
|
|
83
92
|
for (const chunk of chunks) {
|
|
@@ -760,7 +769,7 @@ function parsePhaseBlock(block) {
|
|
|
760
769
|
if (isNaN(order)) return null;
|
|
761
770
|
return { id, slug, label, description, status, order };
|
|
762
771
|
}
|
|
763
|
-
var CAPABILITY_REGISTRY, CAPABILITY_KEYS, ASSIGNABLE_CONTRIBUTOR_ROLES, SENSITIVE_CHANGELOG_PATTERNS, CONTRIBUTOR_PRICING_URL, HOSTED_APP_URL, HOSTED_MCP_URL, HOSTED_SUPABASE_URL, SEVERITY_ORDER, NONE_RE, AD_HOC_PLACEHOLDER_RE, MIN_REASON_LENGTH, LOW_SEVERITIES, WAB_WINDOW_DAYS, WAB_DEFAULT_WEEKS, WAB_WEEK_MS, PRODUCT_MARKER, MECHANICS_MARKER, CHECK_VALUE_MAX, VALID_EFFORT_SIZES, SECTION_HEADERS, VALID_EFFORT_SIZES2, EFFORT_SCALE, NONE_PATTERN, NONE_PATTERN2, VALID_STATUSES, PHASES_START, PHASES_END;
|
|
772
|
+
var CAPABILITY_REGISTRY, CAPABILITY_KEYS, ASSIGNABLE_CONTRIBUTOR_ROLES, SENSITIVE_CHANGELOG_PATTERNS, CONTRIBUTOR_PRICING_URL, HOSTED_APP_URL, HOSTED_MCP_URL, HOSTED_SUPABASE_URL, SEVERITY_ORDER, NONE_RE, AD_HOC_PLACEHOLDER_RE, NONE_LEAD_RE, CONTRAST_LEAD_RE, MIN_REASON_LENGTH, LOW_SEVERITIES, WAB_WINDOW_DAYS, WAB_DEFAULT_WEEKS, WAB_WEEK_MS, PRODUCT_MARKER, MECHANICS_MARKER, CHECK_VALUE_MAX, VALID_EFFORT_SIZES, SECTION_HEADERS, VALID_EFFORT_SIZES2, EFFORT_SCALE, NONE_PATTERN, NONE_PATTERN2, VALID_STATUSES, PHASES_START, PHASES_END;
|
|
764
773
|
var init_dist = __esm({
|
|
765
774
|
"../shared/dist/index.js"() {
|
|
766
775
|
"use strict";
|
|
@@ -800,7 +809,21 @@ var init_dist = __esm({
|
|
|
800
809
|
{ key: "autoBranch", label: "Auto branch", description: "Creates a feature branch per task/cycle at build start.", step: "build" },
|
|
801
810
|
{ key: "autoCommit", label: "Auto commit", description: "Commits your work automatically when a build completes.", step: "build" },
|
|
802
811
|
{ key: "autoPush", label: "Auto push & PR", description: "Pushes the branch and opens a pull request on build complete.", step: "build" },
|
|
803
|
-
{ key: "papiMetaFraming", label: "PAPI self-referential framing", description: "Includes PAPI-meta build-discipline framing in handoff output.", step: "build" }
|
|
812
|
+
{ key: "papiMetaFraming", label: "PAPI self-referential framing", description: "Includes PAPI-meta build-discipline framing in handoff output.", step: "build" },
|
|
813
|
+
// Editor-equal release. Two legitimate team shapes need opposite answers, so
|
|
814
|
+
// this is a PROJECT policy rather than a global rule:
|
|
815
|
+
//
|
|
816
|
+
// OFF (default) — an editor's release opens a CONTRIBUTOR PR and PAPI records
|
|
817
|
+
// the release when GitHub merges it. Right for an open//multi-contributor
|
|
818
|
+
// product where the project owner is the last gate before production.
|
|
819
|
+
// ON — an editor releases on exactly the owner's path. Right for a small team
|
|
820
|
+
// of trusted peers who each run whole cycles end to end, where routing every
|
|
821
|
+
// close back through one person is the bottleneck, not the safeguard.
|
|
822
|
+
//
|
|
823
|
+
// Defaults OFF so existing projects are byte-identical, and because widening who
|
|
824
|
+
// can ship to production must be a deliberate choice, never a silent upgrade.
|
|
825
|
+
// A VIEWER is denied either way — this moves the editor line only.
|
|
826
|
+
{ key: "editorRelease", label: "Editors release like the owner", description: "Lets editors run a full release instead of opening a contributor PR.", step: "release", defaultEnabled: false }
|
|
804
827
|
];
|
|
805
828
|
CAPABILITY_KEYS = CAPABILITY_REGISTRY.map((c) => c.key);
|
|
806
829
|
ASSIGNABLE_CONTRIBUTOR_ROLES = ["editor", "viewer"];
|
|
@@ -852,6 +875,8 @@ var init_dist = __esm({
|
|
|
852
875
|
};
|
|
853
876
|
NONE_RE = /^(none|n\/a|no(ne)? (found|discovered))\.?$/i;
|
|
854
877
|
AD_HOC_PLACEHOLDER_RE = /^none\s*[—–-]\s*ad[-\s]?hoc work\.?$/i;
|
|
878
|
+
NONE_LEAD_RE = /^(none|n\/a|no(ne)? (found|discovered))\s*[.,:;—–-]\s*(?<rest>.*)$/is;
|
|
879
|
+
CONTRAST_LEAD_RE = /^(but|however|although|though|except|aside from|other than|besides)\b/i;
|
|
855
880
|
MIN_REASON_LENGTH = 12;
|
|
856
881
|
LOW_SEVERITIES = /* @__PURE__ */ new Set(["P2", "P3"]);
|
|
857
882
|
WAB_WINDOW_DAYS = 7;
|
|
@@ -907,6 +932,7 @@ __export(git_exports, {
|
|
|
907
932
|
detectUnrecordedCommits: () => detectUnrecordedCommits,
|
|
908
933
|
ensureLatestDevelop: () => ensureLatestDevelop,
|
|
909
934
|
ensureTagAtHead: () => ensureTagAtHead,
|
|
935
|
+
findContributorReleasePullRequests: () => findContributorReleasePullRequests,
|
|
910
936
|
findTaskCommitsOnBase: () => findTaskCommitsOnBase,
|
|
911
937
|
getBranchDiff: () => getBranchDiff,
|
|
912
938
|
getCommitFiles: () => getCommitFiles,
|
|
@@ -924,6 +950,7 @@ __export(git_exports, {
|
|
|
924
950
|
getOriginRepoSlug: () => getOriginRepoSlug,
|
|
925
951
|
getOriginUrl: () => getOriginUrl,
|
|
926
952
|
getPathsDifferingFrom: () => getPathsDifferingFrom,
|
|
953
|
+
getPullRequestState: () => getPullRequestState,
|
|
927
954
|
getPullRequestUrl: () => getPullRequestUrl,
|
|
928
955
|
getRemoteBranchFiles: () => getRemoteBranchFiles,
|
|
929
956
|
getRootCommitHash: () => getRootCommitHash,
|
|
@@ -1887,6 +1914,56 @@ function getPullRequestUrl(cwd, branch) {
|
|
|
1887
1914
|
return null;
|
|
1888
1915
|
}
|
|
1889
1916
|
}
|
|
1917
|
+
function getPullRequestState(cwd, prUrl) {
|
|
1918
|
+
try {
|
|
1919
|
+
const output = execFileSync(
|
|
1920
|
+
"gh",
|
|
1921
|
+
["pr", "view", prUrl, "--json", "url,state,mergedAt"],
|
|
1922
|
+
{ cwd, encoding: "utf-8" }
|
|
1923
|
+
).trim();
|
|
1924
|
+
if (!output) return null;
|
|
1925
|
+
const parsed = JSON.parse(output);
|
|
1926
|
+
if (!parsed.url || !["OPEN", "CLOSED", "MERGED"].includes(parsed.state ?? "")) return null;
|
|
1927
|
+
return {
|
|
1928
|
+
url: parsed.url,
|
|
1929
|
+
state: parsed.state,
|
|
1930
|
+
mergedAt: parsed.mergedAt ?? null
|
|
1931
|
+
};
|
|
1932
|
+
} catch {
|
|
1933
|
+
return null;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
function findContributorReleasePullRequests(cwd, cycle) {
|
|
1937
|
+
if (!Number.isInteger(cycle) || cycle <= 0) return [];
|
|
1938
|
+
try {
|
|
1939
|
+
const output = execFileSync(
|
|
1940
|
+
"gh",
|
|
1941
|
+
[
|
|
1942
|
+
"pr",
|
|
1943
|
+
"list",
|
|
1944
|
+
"--state",
|
|
1945
|
+
"all",
|
|
1946
|
+
"--limit",
|
|
1947
|
+
"20",
|
|
1948
|
+
"--search",
|
|
1949
|
+
`"Contributor release PR for cycle ${cycle}" in:body`,
|
|
1950
|
+
"--json",
|
|
1951
|
+
"url,state,mergedAt,headRefName"
|
|
1952
|
+
],
|
|
1953
|
+
{ cwd, encoding: "utf-8" }
|
|
1954
|
+
).trim();
|
|
1955
|
+
if (!output) return [];
|
|
1956
|
+
const parsed = JSON.parse(output);
|
|
1957
|
+
return parsed.flatMap((pr) => pr.url && pr.headRefName && ["OPEN", "CLOSED", "MERGED"].includes(pr.state ?? "") ? [{
|
|
1958
|
+
url: pr.url,
|
|
1959
|
+
state: pr.state,
|
|
1960
|
+
mergedAt: pr.mergedAt ?? null,
|
|
1961
|
+
branch: pr.headRefName
|
|
1962
|
+
}] : []);
|
|
1963
|
+
} catch {
|
|
1964
|
+
return [];
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1890
1967
|
function squashMergePullRequest(cwd, branch) {
|
|
1891
1968
|
const repo = getOriginRepoSlug(cwd);
|
|
1892
1969
|
const baseArgs = ["pr", "merge", branch, "--squash", "--delete-branch"];
|
|
@@ -2320,6 +2397,10 @@ var init_proxy_adapter = __esm({
|
|
|
2320
2397
|
"dismissRecommendation",
|
|
2321
2398
|
"findPendingDocActionsForTask",
|
|
2322
2399
|
"getActiveDecisions",
|
|
2400
|
+
"getContributorRole",
|
|
2401
|
+
"listContributorReleasePrs",
|
|
2402
|
+
"recordContributorReleasePr",
|
|
2403
|
+
"setContributorReleasePrStatus",
|
|
2323
2404
|
"getActiveStage",
|
|
2324
2405
|
"getBuildReportCountForTask",
|
|
2325
2406
|
"getBuildReportsSince",
|
|
@@ -2459,7 +2540,7 @@ var init_proxy_adapter = __esm({
|
|
|
2459
2540
|
"applyActiveDecisionUpdates",
|
|
2460
2541
|
// getToolCallCount + updatePhaseStatus are ABSENT: they now have edge handlers and
|
|
2461
2542
|
// are served through the forwarder (getToolCallCount = SUP-2026-026 handler #1).
|
|
2462
|
-
|
|
2543
|
+
// getContributorRole is edge-wired and binds identity from the bearer.
|
|
2463
2544
|
// task-3018 (C356): storeDocBody + getDocBodyUsage are now WIRED — edge case
|
|
2464
2545
|
// handlers + ALLOWED_METHODS/WRITE_METHODS entries exist, so they forward and
|
|
2465
2546
|
// hosted callers get body storage. Removed from this list, as task-3017 required.
|
|
@@ -2467,9 +2548,6 @@ var init_proxy_adapter = __esm({
|
|
|
2467
2548
|
// of the owner-action queue. Six readers were wired C329 (task-2412) but the
|
|
2468
2549
|
// producer stayed here, so the hosted Owner Action Queue was structurally empty
|
|
2469
2550
|
// (AD-74 inverted). Now forwards; the edge binds user_id to the bearer ([C]).
|
|
2470
|
-
"recordContributorReleasePr",
|
|
2471
|
-
"setContributorReleasePrStatus",
|
|
2472
|
-
"listContributorReleasePrs",
|
|
2473
2551
|
"claimReview",
|
|
2474
2552
|
"getSiblingAds",
|
|
2475
2553
|
"getSiblingRepoTasks",
|
|
@@ -3185,8 +3263,8 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
3185
3263
|
getCycleLearningPatterns() {
|
|
3186
3264
|
return this.invoke("getCycleLearningPatterns", []);
|
|
3187
3265
|
}
|
|
3188
|
-
updateCycleLearningActionRef(learningId, taskDisplayId) {
|
|
3189
|
-
return this.invoke("updateCycleLearningActionRef", [learningId, taskDisplayId]);
|
|
3266
|
+
updateCycleLearningActionRef(learningId, taskDisplayId, opts) {
|
|
3267
|
+
return this.invoke("updateCycleLearningActionRef", [learningId, taskDisplayId, opts ?? {}]);
|
|
3190
3268
|
}
|
|
3191
3269
|
// --- Strategy Review Drafts ---
|
|
3192
3270
|
savePendingReviewResponse(cycleNumber, rawResponse) {
|
|
@@ -3292,7 +3370,11 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
3292
3370
|
*/
|
|
3293
3371
|
async getMeteredUsage() {
|
|
3294
3372
|
const body = await this.postRoute("metering", {});
|
|
3295
|
-
return {
|
|
3373
|
+
return {
|
|
3374
|
+
tier: body.tier ?? "free",
|
|
3375
|
+
monthlyToolCalls: body.monthlyToolCalls ?? 0,
|
|
3376
|
+
projectLimitOverride: body.projectLimitOverride ?? null
|
|
3377
|
+
};
|
|
3296
3378
|
}
|
|
3297
3379
|
async listUserProjects() {
|
|
3298
3380
|
const body = await this.postRoute("project-list", {});
|
|
@@ -6404,7 +6486,7 @@ async function runSetup() {
|
|
|
6404
6486
|
log(`Project: ${result.project_slug}`);
|
|
6405
6487
|
}
|
|
6406
6488
|
log("");
|
|
6407
|
-
log(`Done. Open
|
|
6489
|
+
log(`Done. Open your AI client in this folder and say "run setup" to scaffold your first plan.`);
|
|
6408
6490
|
return 0;
|
|
6409
6491
|
}
|
|
6410
6492
|
var DEFAULT_BASE_URL, MAX_POLL_SECONDS, NETWORK_RETRY_LIMIT;
|
|
@@ -6512,7 +6594,7 @@ function loadConfig() {
|
|
|
6512
6594
|
${conflicting} is set in your environment, but PAPI_DATA_API_KEY is also set \u2014
|
|
6513
6595
|
you have a hosted PAPI account, so the database URL is being ignored anyway.
|
|
6514
6596
|
|
|
6515
|
-
Fix: \`unset ${conflicting}\` (and PAPI_ADAPTER if also set) before starting
|
|
6597
|
+
Fix: \`unset ${conflicting}\` (and PAPI_ADAPTER if also set) before starting your AI client,
|
|
6516
6598
|
or remove it from your shell rc file (~/.zshrc, ~/.bashrc).
|
|
6517
6599
|
|
|
6518
6600
|
If you intentionally self-host, set PAPI_SELF_HOST=1 in your environment to bypass this guard.`
|
|
@@ -6957,7 +7039,7 @@ Fix the PAPI_PROJECT_ID in your .mcp.json (or unset it and let PAPI resolve the
|
|
|
6957
7039
|
Get started in 3 steps:
|
|
6958
7040
|
1. Sign up at ${dashboardUrl ? `${dashboardUrl}/login` : "your configured PAPI dashboard"}
|
|
6959
7041
|
2. Complete the onboarding wizard \u2014 it generates your .mcp.json config
|
|
6960
|
-
3. Download the config, place it in your project root, and restart
|
|
7042
|
+
3. Download the config, place it in your project root, and restart your AI client
|
|
6961
7043
|
|
|
6962
7044
|
Already have an account? Make sure PAPI_DATA_API_KEY is set in your .mcp.json env config.`
|
|
6963
7045
|
);
|
|
@@ -7883,7 +7965,11 @@ The guard above says what to reject. This says what to propose. Rejecting is not
|
|
|
7883
7965
|
|
|
7884
7966
|
**Copy variants are NEVER decisions.** A tagline, a headline, a value proposition, a piece of marketing or product wording: swapping one for another constrains no future work, so it fails test (b) outright. Wording changes as often as the market teaches you something, and routing every edit through decision ceremony is what buries the handful of decisions that genuinely constrain the project. Edit the wording where the wording lives. If two variants are being compared against a measurement, that is an experiment, not a decision.
|
|
7885
7967
|
|
|
7886
|
-
**A decision decaying into a convention is the healthy path, not a failure.** When a live AD stops being arguable \u2014 the alternatives are no longer on the table and nobody would re-litigate it \u2014
|
|
7968
|
+
**A decision decaying into a convention is the healthy path, not a failure.** When a live AD stops being arguable \u2014 the alternatives are no longer on the table and nobody would re-litigate it \u2014 propose demoting it during housekeeping with \`action: "demote"\`. An AD registry where most entries are settled is one nobody reads.
|
|
7969
|
+
|
|
7970
|
+
DEMOTION IS THE THIRD TRANSITION, and it is not either of the other two. \`supersede\` replaces a stance with a NEW one; \`delete\` removes something that was never a decision. \`demote\` is for a decision that was RIGHT and still holds \u2014 it keeps riding every build handoff as a convention and simply stops occupying decision surface. Apply test (c) to each live AD: could a competent person argue the other side TODAY, on current evidence? If no, propose \`demote\` and give the one-line reason it is no longer contested.
|
|
7971
|
+
|
|
7972
|
+
PROPOSE, NEVER ASSUME. A demotion is a judgement about whether something is still arguable, and the owner is the one who knows. Surface candidates with reasoning and let the human confirm on apply, exactly as board corrections and cancellations are gated. Do not demote in bulk to tidy the registry.`;
|
|
7887
7973
|
var AD_CONFLICT_SURFACING_RULES = `**A contradiction is NOT a veto \u2014 surface it, never silently shelve it.**
|
|
7888
7974
|
|
|
7889
7975
|
Active Decisions are *active*: they can be superseded, modified, or abandoned. You do NOT have authority to kill a piece of work simply because it cuts against one. That is the user's call, and they can only make it if you show it to them.
|
|
@@ -8870,7 +8956,7 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
8870
8956
|
"activeDecisionUpdates": [
|
|
8871
8957
|
{
|
|
8872
8958
|
"id": "string \u2014 AD-N (existing) or new AD-N (for new decisions)",
|
|
8873
|
-
"action": "confidence_change | modify | resolve | supersede | new | delete",
|
|
8959
|
+
"action": "confidence_change | modify | resolve | supersede | new | delete | demote",
|
|
8874
8960
|
"body": "string \u2014 full AD block including ### heading, confidence tag, and body text (empty string for delete)",
|
|
8875
8961
|
"evidenceRef": "string (optional) \u2014 for DELIBERATE decisions (modify / resolve / delete = validate/modify/invalidate), a pointer to the evidence that justified the change: a doc path (docs/research/foo.md), a build-report id, or a metric name. Builds the decision->outcome ledger. Omit if no concrete evidence.",
|
|
8876
8962
|
"metricDelta": "object (optional) \u2014 { "metric": string, "before": number, "after": number, "delta": number } \u2014 which metric moved and by how much. Use a REAL metric name from cycle_metrics_snapshots where possible (e.g. scope_accuracy, velocity, est_actual_drift). Omit if no metric moved."
|
|
@@ -9122,7 +9208,7 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
9122
9208
|
"activeDecisionUpdates": [
|
|
9123
9209
|
{
|
|
9124
9210
|
"id": "string \u2014 AD-N (existing) or new AD-N (for new decisions)",
|
|
9125
|
-
"action": "confidence_change | modify | resolve | supersede | new | delete",
|
|
9211
|
+
"action": "confidence_change | modify | resolve | supersede | new | delete | demote",
|
|
9126
9212
|
"body": "string \u2014 full AD block including ### heading, confidence tag, and body text (empty string for delete)",
|
|
9127
9213
|
"evidenceRef": "string (optional) \u2014 for DELIBERATE changes (modify / resolve / delete), a pointer to the justifying evidence: a doc path, a build-report id, or a metric name. Builds the decision->outcome ledger. Omit if none.",
|
|
9128
9214
|
"metricDelta": "object (optional) \u2014 { "metric": string, "before": number, "after": number, "delta": number } \u2014 which metric moved. Prefer a real metric name from cycle_metrics_snapshots. Omit if none."
|
|
@@ -9633,13 +9719,20 @@ async function getPrompt(name) {
|
|
|
9633
9719
|
}
|
|
9634
9720
|
|
|
9635
9721
|
// src/lib/foundation.ts
|
|
9636
|
-
var cache2 =
|
|
9722
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
9637
9723
|
var CACHE_TTL_MS2 = 5 * 60 * 1e3;
|
|
9724
|
+
var UNKNOWN_PROJECT_KEY = "\0unknown-project";
|
|
9725
|
+
function cacheKeyFor(adapter2) {
|
|
9726
|
+
const id = adapter2.getProjectId?.();
|
|
9727
|
+
return id && id.length > 0 ? id : UNKNOWN_PROJECT_KEY;
|
|
9728
|
+
}
|
|
9638
9729
|
var MAX_TOKENS = 800;
|
|
9639
9730
|
var MAX_CHARS = MAX_TOKENS * 4;
|
|
9640
9731
|
async function buildProjectFoundation(adapter2) {
|
|
9641
9732
|
const now = Date.now();
|
|
9642
|
-
|
|
9733
|
+
const cacheKey = cacheKeyFor(adapter2);
|
|
9734
|
+
const hit = cache2.get(cacheKey);
|
|
9735
|
+
if (hit && hit.expiresAt > now) return hit.content;
|
|
9643
9736
|
const [briefRaw, decisions, northStar, horizons, stages] = await Promise.all([
|
|
9644
9737
|
safe(() => adapter2.readProductBrief(), ""),
|
|
9645
9738
|
safe(() => adapter2.getActiveDecisions(), []),
|
|
@@ -9661,7 +9754,7 @@ async function buildProjectFoundation(adapter2) {
|
|
|
9661
9754
|
const userType = extractUserType(briefRaw);
|
|
9662
9755
|
if (userType) lines.push(`Primary user: ${userType}`);
|
|
9663
9756
|
if (lines.length === 0) {
|
|
9664
|
-
cache2
|
|
9757
|
+
cache2.set(cacheKey, { content: "", expiresAt: now + CACHE_TTL_MS2 });
|
|
9665
9758
|
return "";
|
|
9666
9759
|
}
|
|
9667
9760
|
const body = lines.join("\n");
|
|
@@ -9674,7 +9767,7 @@ ${body}
|
|
|
9674
9767
|
[/project_foundation]
|
|
9675
9768
|
|
|
9676
9769
|
` : block;
|
|
9677
|
-
cache2
|
|
9770
|
+
cache2.set(cacheKey, { content: final, expiresAt: now + CACHE_TTL_MS2 });
|
|
9678
9771
|
return final;
|
|
9679
9772
|
}
|
|
9680
9773
|
async function safe(fn, fallback) {
|
|
@@ -10191,7 +10284,7 @@ function titleMap(entities) {
|
|
|
10191
10284
|
const map = /* @__PURE__ */ new Map();
|
|
10192
10285
|
for (const e of entities) {
|
|
10193
10286
|
if (!e.title) continue;
|
|
10194
|
-
for (const key of [e.id, e.displayId]) {
|
|
10287
|
+
for (const key of [e.id, e.displayId, e.uuid]) {
|
|
10195
10288
|
if (!key) continue;
|
|
10196
10289
|
map.set(key, e.title);
|
|
10197
10290
|
map.set(key.toLowerCase(), e.title);
|
|
@@ -10199,6 +10292,14 @@ function titleMap(entities) {
|
|
|
10199
10292
|
}
|
|
10200
10293
|
return map;
|
|
10201
10294
|
}
|
|
10295
|
+
function formatEntityRef(value, entities) {
|
|
10296
|
+
const needle = value.toLowerCase();
|
|
10297
|
+
const match = entities.find(
|
|
10298
|
+
(e) => e.uuid?.toLowerCase() === needle || e.id?.toLowerCase() === needle || e.displayId?.toLowerCase() === needle
|
|
10299
|
+
);
|
|
10300
|
+
if (!match) return formatRef(value, lookupTitle(value, titleMap(entities)));
|
|
10301
|
+
return formatRef(match.displayId || match.id || value, match.title);
|
|
10302
|
+
}
|
|
10202
10303
|
|
|
10203
10304
|
// src/lib/blocker.ts
|
|
10204
10305
|
function idMatches(candidate, ref) {
|
|
@@ -10363,6 +10464,31 @@ function isProjectOwner(callerUserId, ownerUserId) {
|
|
|
10363
10464
|
if (caller.length === 0 || owner.length === 0) return false;
|
|
10364
10465
|
return caller === owner;
|
|
10365
10466
|
}
|
|
10467
|
+
var CYCLE_ROLES = ["owner", "editor"];
|
|
10468
|
+
async function resolveCycleGate(adapter2, gate) {
|
|
10469
|
+
if (!gate.enforced) return { allowed: true, role: null };
|
|
10470
|
+
if (gate.callerIsOwner) return { allowed: true, role: "owner" };
|
|
10471
|
+
if (!gate.callerUserId) {
|
|
10472
|
+
return {
|
|
10473
|
+
allowed: false,
|
|
10474
|
+
role: null,
|
|
10475
|
+
resolutionError: gate.resolutionError ?? "no caller identity could be resolved"
|
|
10476
|
+
};
|
|
10477
|
+
}
|
|
10478
|
+
if (!adapterSupports(adapter2, "getContributorRole")) {
|
|
10479
|
+
return { allowed: false, role: null, resolutionError: "role lookup is unavailable on this transport" };
|
|
10480
|
+
}
|
|
10481
|
+
try {
|
|
10482
|
+
const role = await adapter2.getContributorRole(gate.callerUserId);
|
|
10483
|
+
return { allowed: role != null && CYCLE_ROLES.includes(role), role: role ?? null };
|
|
10484
|
+
} catch (err) {
|
|
10485
|
+
return {
|
|
10486
|
+
allowed: false,
|
|
10487
|
+
role: null,
|
|
10488
|
+
resolutionError: err instanceof Error ? err.message : String(err)
|
|
10489
|
+
};
|
|
10490
|
+
}
|
|
10491
|
+
}
|
|
10366
10492
|
async function resolveOwnerGate(adapter2, config2) {
|
|
10367
10493
|
if (adapterSupports(adapter2, "getOwnerIdentity")) {
|
|
10368
10494
|
try {
|
|
@@ -10428,6 +10554,48 @@ function normalizeComplexity(value) {
|
|
|
10428
10554
|
const key = (value ?? "").trim().toUpperCase();
|
|
10429
10555
|
return COMPLEXITY_ALIASES[key] ?? "Small";
|
|
10430
10556
|
}
|
|
10557
|
+
var PRIORITY_ALIASES = {
|
|
10558
|
+
"P0": "P0 Critical",
|
|
10559
|
+
"P1": "P1 High",
|
|
10560
|
+
"P2": "P2 Medium",
|
|
10561
|
+
"P3": "P3 Low",
|
|
10562
|
+
"CRITICAL": "P0 Critical",
|
|
10563
|
+
"URGENT": "P0 Critical",
|
|
10564
|
+
"HIGH": "P1 High",
|
|
10565
|
+
"MEDIUM": "P2 Medium",
|
|
10566
|
+
"MED": "P2 Medium",
|
|
10567
|
+
"LOW": "P3 Low",
|
|
10568
|
+
"P0 CRITICAL": "P0 Critical",
|
|
10569
|
+
"P1 HIGH": "P1 High",
|
|
10570
|
+
"P2 MEDIUM": "P2 Medium",
|
|
10571
|
+
"P3 LOW": "P3 Low"
|
|
10572
|
+
};
|
|
10573
|
+
function resolvePriority(value) {
|
|
10574
|
+
const key = (value ?? "").trim().toUpperCase();
|
|
10575
|
+
if (!key) return "P1 High";
|
|
10576
|
+
return PRIORITY_ALIASES[key] ?? null;
|
|
10577
|
+
}
|
|
10578
|
+
function normalizePriority(value) {
|
|
10579
|
+
return resolvePriority(value) ?? "P1 High";
|
|
10580
|
+
}
|
|
10581
|
+
function validateNewTaskVocab(tasks) {
|
|
10582
|
+
const problems = [];
|
|
10583
|
+
for (const t of tasks) {
|
|
10584
|
+
const name = t.title?.trim() || "(untitled task)";
|
|
10585
|
+
if (resolvePriority(t.priority) === null) {
|
|
10586
|
+
problems.push(
|
|
10587
|
+
`"${name}": priority "${t.priority}" is not one of P0 Critical / P1 High / P2 Medium / P3 Low`
|
|
10588
|
+
);
|
|
10589
|
+
}
|
|
10590
|
+
const cx = (t.complexity ?? "").trim();
|
|
10591
|
+
if (cx && !COMPLEXITY_ALIASES[cx.toUpperCase()]) {
|
|
10592
|
+
problems.push(
|
|
10593
|
+
`"${name}": complexity "${t.complexity}" is not one of XS / Small / Medium / Large / XL`
|
|
10594
|
+
);
|
|
10595
|
+
}
|
|
10596
|
+
}
|
|
10597
|
+
return problems;
|
|
10598
|
+
}
|
|
10431
10599
|
var PLAN_BUILD_REPORT_BUDGET = { maxReports: 12, fieldBudget: 280 };
|
|
10432
10600
|
function leadChainWithRecommended(chain, recommendedTaskId) {
|
|
10433
10601
|
const rec = recommendedTaskId?.trim();
|
|
@@ -10444,10 +10612,7 @@ async function resolvePlanScope(adapter2, config2) {
|
|
|
10444
10612
|
}
|
|
10445
10613
|
function filterToPersonalBacklog(tasks, scope) {
|
|
10446
10614
|
if (!scope.callerUserId) return tasks;
|
|
10447
|
-
|
|
10448
|
-
return tasks.filter((t) => !t.assigneeId || t.assigneeId === scope.callerUserId);
|
|
10449
|
-
}
|
|
10450
|
-
return tasks.filter((t) => t.assigneeId === scope.callerUserId);
|
|
10615
|
+
return tasks.filter((t) => !t.assigneeId || t.assigneeId === scope.callerUserId);
|
|
10451
10616
|
}
|
|
10452
10617
|
function determineContextTier(cycleCount) {
|
|
10453
10618
|
if (cycleCount <= 5) return 1;
|
|
@@ -11411,6 +11576,14 @@ function applyCancellationGuard(corrections, confirmCancellations) {
|
|
|
11411
11576
|
async function transactionalWriteBack(adapter2, cycleNumber, data, contextHashes, options = {}) {
|
|
11412
11577
|
const writeBackTimer = startTimer();
|
|
11413
11578
|
const newCycleNumber = cycleNumber + 1;
|
|
11579
|
+
const vocabProblems = validateNewTaskVocab(data.newTasks ?? []);
|
|
11580
|
+
if (vocabProblems.length > 0) {
|
|
11581
|
+
throw new Error(
|
|
11582
|
+
`plan apply refused before writing: ${vocabProblems.length} task(s) use values the board does not accept.
|
|
11583
|
+
` + vocabProblems.map((p) => ` - ${p}`).join("\n") + `
|
|
11584
|
+
Nothing was written \u2014 no cycle was created and no tasks were changed. Correct these values in the plan output and re-run apply.`
|
|
11585
|
+
);
|
|
11586
|
+
}
|
|
11414
11587
|
const skippedCancellations = [];
|
|
11415
11588
|
const cleanTitle = data.cycleLogTitle.replace(/^(?:Cycle|Session)\s+\d+\s*—\s*/i, "").trim();
|
|
11416
11589
|
const cleanContent = data.cycleLogContent.replace(/^#{1,3}\s+(?:Cycle|Session)\s+\d+\s*—[^\n]*\n*/i, "").trim();
|
|
@@ -11517,7 +11690,7 @@ ${cleanContent}`;
|
|
|
11517
11690
|
// task-2242: stable handoff join key
|
|
11518
11691
|
title: t.title,
|
|
11519
11692
|
status: t.status || "Backlog",
|
|
11520
|
-
priority: t.priority
|
|
11693
|
+
priority: normalizePriority(t.priority),
|
|
11521
11694
|
complexity: normalizeComplexity(t.complexity),
|
|
11522
11695
|
module: t.module || "Core",
|
|
11523
11696
|
epic: t.epic || "Platform",
|
|
@@ -11665,7 +11838,7 @@ ${cleanContent}`;
|
|
|
11665
11838
|
displayId: "",
|
|
11666
11839
|
title: task.title,
|
|
11667
11840
|
status: task.status || "Backlog",
|
|
11668
|
-
priority: task.priority
|
|
11841
|
+
priority: normalizePriority(task.priority),
|
|
11669
11842
|
complexity: normalizeComplexity(task.complexity),
|
|
11670
11843
|
module: task.module || "Core",
|
|
11671
11844
|
epic: task.epic || "Platform",
|
|
@@ -11967,7 +12140,7 @@ async function validateAndPrepare(adapter2, force, callerUserId, adapterType) {
|
|
|
11967
12140
|
try {
|
|
11968
12141
|
const health = await adapter2.getCycleHealth();
|
|
11969
12142
|
cycleNumber = health.totalCycles;
|
|
11970
|
-
mode = determineMode(health.totalCycles);
|
|
12143
|
+
mode = determineMode(health.projectCycleCount ?? health.totalCycles);
|
|
11971
12144
|
const blockingCycle = callerUserId ? await resolveCallerLatestCycle(adapter2, callerUserId) : void 0;
|
|
11972
12145
|
const latestStatus = callerUserId ? blockingCycle?.status : health.latestCycleStatus;
|
|
11973
12146
|
const blockingNumber = blockingCycle?.number ?? cycleNumber;
|
|
@@ -14977,6 +15150,29 @@ function asDecisionBatchApplier(adapter2) {
|
|
|
14977
15150
|
const candidate = adapter2;
|
|
14978
15151
|
return typeof candidate.applyActiveDecisionUpdates === "function" ? candidate : void 0;
|
|
14979
15152
|
}
|
|
15153
|
+
async function demoteDecisionToConvention(ad, adapter2, cycleNumber, warnings) {
|
|
15154
|
+
if (!adapter2.createConvention) {
|
|
15155
|
+
warnings.push(
|
|
15156
|
+
`AD ${ad.id}: demotion skipped \u2014 this adapter cannot create conventions, and retiring the decision without writing the rule would lose the stance entirely. Left live.`
|
|
15157
|
+
);
|
|
15158
|
+
return false;
|
|
15159
|
+
}
|
|
15160
|
+
const rule = ad.body.trim();
|
|
15161
|
+
if (!rule) {
|
|
15162
|
+
warnings.push(`AD ${ad.id}: demotion skipped \u2014 empty body, nothing to carry into a convention. Left live.`);
|
|
15163
|
+
return false;
|
|
15164
|
+
}
|
|
15165
|
+
try {
|
|
15166
|
+
await adapter2.createConvention({ rule });
|
|
15167
|
+
} catch (err) {
|
|
15168
|
+
warnings.push(
|
|
15169
|
+
`AD ${ad.id}: demotion skipped \u2014 writing the convention failed (${err instanceof Error ? err.message : String(err)}). The decision is left live rather than retired with nothing to replace it.`
|
|
15170
|
+
);
|
|
15171
|
+
return false;
|
|
15172
|
+
}
|
|
15173
|
+
await adapter2.updateActiveDecision(ad.id, ad.body, cycleNumber, "demote");
|
|
15174
|
+
return true;
|
|
15175
|
+
}
|
|
14980
15176
|
function routeDecisionUpdate(ad, adapter2, cycleNumber, warnings) {
|
|
14981
15177
|
const action = ad.action;
|
|
14982
15178
|
let route;
|
|
@@ -15723,7 +15919,10 @@ ${cleanContent}`;
|
|
|
15723
15919
|
});
|
|
15724
15920
|
if (data.activeDecisionUpdates && data.activeDecisionUpdates.length > 0) {
|
|
15725
15921
|
for (const ad of data.activeDecisionUpdates) {
|
|
15726
|
-
if (ad.action === "
|
|
15922
|
+
if (ad.action === "demote") {
|
|
15923
|
+
const demoted = await demoteDecisionToConvention(ad, adapter2, cycleNumber, evidenceWarnings);
|
|
15924
|
+
if (!demoted) continue;
|
|
15925
|
+
} else if (ad.action === "delete" && adapter2.deleteActiveDecision) {
|
|
15727
15926
|
await adapter2.deleteActiveDecision(ad.id);
|
|
15728
15927
|
} else if (ad.action === "new" && adapter2.upsertActiveDecision) {
|
|
15729
15928
|
const titleMatch = ad.body.match(/^###\s+\S+:\s*([^\n[]+?)(?:\s*\[|$)/m);
|
|
@@ -17237,11 +17436,13 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
17237
17436
|
} else {
|
|
17238
17437
|
const health = await target.getCycleHealth().catch(() => null);
|
|
17239
17438
|
const activeCycle = health?.totalCycles ?? null;
|
|
17240
|
-
const
|
|
17241
|
-
const stamp = activeCycle != null ? `[C${activeCycle} ${
|
|
17439
|
+
const stampedAt = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
|
|
17440
|
+
const stamp = activeCycle != null ? `[C${activeCycle} ${stampedAt} UTC]` : `[${stampedAt} UTC]`;
|
|
17242
17441
|
const entry = `${stamp} ${trimmed}`;
|
|
17243
17442
|
updates.notes = existing.trim().length > 0 ? `${entry}
|
|
17244
17443
|
|
|
17444
|
+
---
|
|
17445
|
+
|
|
17245
17446
|
${existing}` : entry;
|
|
17246
17447
|
}
|
|
17247
17448
|
}
|
|
@@ -20071,6 +20272,47 @@ To override, pass force=true (emits a telemetry warning).`
|
|
|
20071
20272
|
}
|
|
20072
20273
|
return { resolvedCycleNum, warnings, force, skipVersion };
|
|
20073
20274
|
}
|
|
20275
|
+
async function reconcileContributorReleasePrs(config2, adapter2, callerUserId, cycle) {
|
|
20276
|
+
if (!callerUserId || !adapter2?.listContributorReleasePrs || !adapter2.setContributorReleasePrStatus) {
|
|
20277
|
+
return { merged: [], open: [] };
|
|
20278
|
+
}
|
|
20279
|
+
const records = await adapter2.listContributorReleasePrs();
|
|
20280
|
+
let own = records.filter((record) => record.contributorUserId === callerUserId);
|
|
20281
|
+
if (own.length === 0 && cycle && adapter2.recordContributorReleasePr) {
|
|
20282
|
+
const discovered = findContributorReleasePullRequests(config2.projectRoot, cycle);
|
|
20283
|
+
for (const pr of discovered) {
|
|
20284
|
+
await adapter2.recordContributorReleasePr({
|
|
20285
|
+
prUrl: pr.url,
|
|
20286
|
+
branch: pr.branch,
|
|
20287
|
+
cycle,
|
|
20288
|
+
contributorUserId: callerUserId
|
|
20289
|
+
});
|
|
20290
|
+
}
|
|
20291
|
+
own = discovered.map((pr, index) => ({
|
|
20292
|
+
id: `discovered-${index}`,
|
|
20293
|
+
contributorUserId: callerUserId,
|
|
20294
|
+
prUrl: pr.url,
|
|
20295
|
+
branch: pr.branch,
|
|
20296
|
+
cycle,
|
|
20297
|
+
status: "open",
|
|
20298
|
+
createdAt: pr.mergedAt ?? "",
|
|
20299
|
+
updatedAt: pr.mergedAt ?? ""
|
|
20300
|
+
}));
|
|
20301
|
+
}
|
|
20302
|
+
const merged = [];
|
|
20303
|
+
const open = [];
|
|
20304
|
+
for (const record of own) {
|
|
20305
|
+
const discoveredState = findContributorReleasePullRequests(config2.projectRoot, record.cycle ?? 0).find((pr) => pr.url === record.prUrl);
|
|
20306
|
+
const state = discoveredState ?? getPullRequestState(config2.projectRoot, record.prUrl);
|
|
20307
|
+
if (state?.state === "MERGED") {
|
|
20308
|
+
await adapter2.setContributorReleasePrStatus(record.prUrl, "merged");
|
|
20309
|
+
merged.push({ prUrl: record.prUrl, branch: record.branch, cycle: record.cycle });
|
|
20310
|
+
} else if (state?.state === "OPEN") {
|
|
20311
|
+
open.push({ prUrl: record.prUrl, branch: record.branch, cycle: record.cycle });
|
|
20312
|
+
}
|
|
20313
|
+
}
|
|
20314
|
+
return { merged, open };
|
|
20315
|
+
}
|
|
20074
20316
|
async function contributorAutoPrRelease(config2, adapter2, version, productionBaseBranch, options) {
|
|
20075
20317
|
if (!isGhAvailable()) {
|
|
20076
20318
|
throw new Error(
|
|
@@ -20589,8 +20831,18 @@ async function postReleaseToX(version, cycleClosed) {
|
|
|
20589
20831
|
}
|
|
20590
20832
|
function buildHostedReleaseOutput(params) {
|
|
20591
20833
|
const { version, branch, cyclePart, warningsBlock, skipVersion } = params;
|
|
20834
|
+
const handoff = buildHostedGitHandoff({ version, branch, skipVersion });
|
|
20835
|
+
return `## Release ${version}${skipVersion ? " (skip version)" : ""} \u2014 cycle closed in the DB
|
|
20836
|
+
|
|
20837
|
+
${cyclePart} is now marked **complete** in PAPI's database, so \`orient\` will no longer flag "Release has not been run."
|
|
20838
|
+
` + warningsBlock + `
|
|
20839
|
+
${handoff}
|
|
20840
|
+
|
|
20841
|
+
Next: cycle closed! Run \`plan\` to start your next cycle, or \`idea "<what's next>"\` first if your backlog is thin.`;
|
|
20842
|
+
}
|
|
20843
|
+
function buildHostedGitHandoff(params) {
|
|
20844
|
+
const { version, branch, skipVersion } = params;
|
|
20592
20845
|
const tagAnnotation = `Release ${version}`;
|
|
20593
|
-
const titleSuffix = skipVersion ? " (skip version)" : "";
|
|
20594
20846
|
const gitHalf = skipVersion ? "the git half of the release (the CHANGELOG.md commit and the branch push) must run on your own machine" : "the git half of the release (tag, push, CHANGELOG.md) must run on your own machine";
|
|
20595
20847
|
const commands = skipVersion ? `git checkout ${branch}
|
|
20596
20848
|
git pull
|
|
@@ -20607,17 +20859,13 @@ git -c user.name="Your Name" -c user.email="you@example.com" tag -a ${version} -
|
|
|
20607
20859
|
\`\`\`
|
|
20608
20860
|
|
|
20609
20861
|
`;
|
|
20610
|
-
return
|
|
20611
|
-
|
|
20612
|
-
${cyclePart} is now marked **complete** in PAPI's database, so \`orient\` will no longer flag "Release has not been run."
|
|
20613
|
-
` + warningsBlock + `
|
|
20614
|
-
The hosted remote MCP transport (mcp.getpapi.ai) has no checkout of your project and no git binary, so ${gitHalf}.
|
|
20862
|
+
return `The hosted remote MCP transport (mcp.getpapi.ai) has no checkout of your project and no git binary, so ${gitHalf}.
|
|
20615
20863
|
|
|
20616
20864
|
Finish the release locally:
|
|
20617
20865
|
\`\`\`
|
|
20618
20866
|
` + commands + `\`\`\`
|
|
20619
20867
|
|
|
20620
|
-
` + identityBlock
|
|
20868
|
+
` + identityBlock.trimEnd();
|
|
20621
20869
|
}
|
|
20622
20870
|
var releaseTool = {
|
|
20623
20871
|
name: "release",
|
|
@@ -20705,7 +20953,16 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
20705
20953
|
await beginRelease(tracker, cycleToClose);
|
|
20706
20954
|
const productionBaseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
20707
20955
|
if (branch === productionBaseBranch) {
|
|
20708
|
-
|
|
20956
|
+
let releaseCaps = {};
|
|
20957
|
+
try {
|
|
20958
|
+
const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
|
|
20959
|
+
releaseCaps = info?.capabilities ?? {};
|
|
20960
|
+
} catch {
|
|
20961
|
+
releaseCaps = {};
|
|
20962
|
+
}
|
|
20963
|
+
const editorsReleaseLikeOwner = isCapabilityEnabled(releaseCaps, "editorRelease");
|
|
20964
|
+
const cycleGate = editorsReleaseLikeOwner ? await resolveCycleGate(adapter2, gate) : { allowed: gate.callerIsOwner, role: gate.callerIsOwner ? "owner" : null };
|
|
20965
|
+
if (gate.enforced && !cycleGate.allowed) {
|
|
20709
20966
|
const resolutionNote = gate.resolutionError ? `
|
|
20710
20967
|
|
|
20711
20968
|
Identity resolution failed (${gate.resolutionError}) \u2014 the gate fails closed. Retry once connectivity is restored.` : "";
|
|
@@ -20726,13 +20983,62 @@ Then reconnect your AI tool and run release again. (Direct/pg setups read identi
|
|
|
20726
20983
|
const ownerHint = gate.transport === "pg" ? `If you ARE the owner, set PAPI_USER_ID to your account UUID in .mcp.json (getpapi.ai \u2192 Settings \u2192 Account).` : `If you ARE the owner, your identity comes from your API key \u2014 check you are using YOUR key for YOUR project.`;
|
|
20727
20984
|
const roleNote = callerRole === "viewer" ? `Your role on this project is "viewer", which cannot release. Ask the owner (or an editor) to release, or ask for editor access.` : `Your identity does not match this project's owner, and you do not have an editor role to open a release PR. If you are a contributor, ask the owner for editor access (or push your branch and open a PR manually). ` + ownerHint;
|
|
20728
20985
|
return errorResponse(
|
|
20729
|
-
`Release to ${productionBaseBranch}
|
|
20986
|
+
`Release to ${productionBaseBranch} needs an owner or editor role.
|
|
20730
20987
|
|
|
20731
20988
|
` + roleNote + `
|
|
20732
20989
|
|
|
20733
20990
|
Releasing to a non-base branch (e.g. dev) is unaffected.` + resolutionNote
|
|
20734
20991
|
);
|
|
20735
20992
|
}
|
|
20993
|
+
tracker.mark("contributor-pr-reconciliation");
|
|
20994
|
+
try {
|
|
20995
|
+
const reconciled = await reconcileContributorReleasePrs(config2, adapter2, gate.callerUserId, cycleToClose);
|
|
20996
|
+
if (reconciled.merged.length > 0) {
|
|
20997
|
+
const latest = reconciled.merged[0];
|
|
20998
|
+
let caps2 = {};
|
|
20999
|
+
try {
|
|
21000
|
+
const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
|
|
21001
|
+
caps2 = info?.capabilities ?? {};
|
|
21002
|
+
} catch {
|
|
21003
|
+
caps2 = {};
|
|
21004
|
+
}
|
|
21005
|
+
await recordReadinessVerified(tracker);
|
|
21006
|
+
await recordQualityGate(tracker, evaluateReleaseGate(caps2, config2.gateCommand, gateResult), caps2);
|
|
21007
|
+
await completeRelease(tracker, {
|
|
21008
|
+
cycleClosed: latest.cycle,
|
|
21009
|
+
version,
|
|
21010
|
+
caps: caps2,
|
|
21011
|
+
branchMerges: latest.branch ? [{ branch: latest.branch, prUrl: latest.prUrl }] : [],
|
|
21012
|
+
changelogEmitted: false
|
|
21013
|
+
});
|
|
21014
|
+
const localHandoff = isHostedTransport() ? `
|
|
21015
|
+
|
|
21016
|
+
${buildHostedGitHandoff({ version, branch, skipVersion: skipVersion ?? false })}` : "";
|
|
21017
|
+
return textResponse(
|
|
21018
|
+
`## Release ${version} \u2014 contributor PR merged
|
|
21019
|
+
|
|
21020
|
+
PAPI detected that ${latest.prUrl} was merged on GitHub and reconciled the contributor release. GitHub granted the merge permission; your PAPI **editor** role granted the release workflow.
|
|
21021
|
+
|
|
21022
|
+
${latest.cycle ? `Cycle ${latest.cycle}` : "The contributor cycle"} remains **complete**, and the release is now recorded as **released** in PAPI.` + localHandoff + `
|
|
21023
|
+
|
|
21024
|
+
Next: run \`plan\` to start your next cycle.`
|
|
21025
|
+
);
|
|
21026
|
+
}
|
|
21027
|
+
if (reconciled.open.length > 0) {
|
|
21028
|
+
const prLines = reconciled.open.map((p) => `- ${p.prUrl}`).join("\n");
|
|
21029
|
+
return textResponse(
|
|
21030
|
+
`## Release ${version} \u2014 contributor PR awaiting merge
|
|
21031
|
+
|
|
21032
|
+
${prLines}
|
|
21033
|
+
|
|
21034
|
+
Your PAPI **editor** role allowed this release PR. Merge permission is controlled separately by GitHub: if GitHub lets you merge, review and merge it there; otherwise a repository maintainer must merge it.
|
|
21035
|
+
|
|
21036
|
+
After it is merged, run \`release\` again so PAPI can detect the merge and finish the release record.`
|
|
21037
|
+
);
|
|
21038
|
+
}
|
|
21039
|
+
} catch (err) {
|
|
21040
|
+
console.error(`[release] contributor PR reconciliation failed (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
|
|
21041
|
+
}
|
|
20736
21042
|
tracker.mark("contributor-auto-pr");
|
|
20737
21043
|
try {
|
|
20738
21044
|
const cr = await contributorAutoPrRelease(config2, adapter2, version, productionBaseBranch, {
|
|
@@ -20748,9 +21054,9 @@ Releasing to a non-base branch (e.g. dev) is unaffected.` + resolutionNote
|
|
|
20748
21054
|
\u26A0\uFE0F Warnings: ${cr.warnings.join("; ")}
|
|
20749
21055
|
` : "";
|
|
20750
21056
|
return textResponse(
|
|
20751
|
-
`## Release ${version} \u2014 PR opened
|
|
21057
|
+
`## Release ${version} \u2014 contributor PR opened
|
|
20752
21058
|
|
|
20753
|
-
You released as an **editor**, so PAPI opened a pull request to \`${productionBaseBranch}\` instead of merging directly.
|
|
21059
|
+
You released as an **editor**, so PAPI opened a pull request to \`${productionBaseBranch}\` instead of merging directly. GitHub merge permission is separate from your PAPI role: merge it yourself if GitHub allows, otherwise a repository maintainer must merge it.
|
|
20754
21060
|
|
|
20755
21061
|
**Pull request(s):**
|
|
20756
21062
|
${prLines.join("\n")}
|
|
@@ -20760,7 +21066,7 @@ ${failLines.join("\n")}
|
|
|
20760
21066
|
` : "") + warnBlock + `
|
|
20761
21067
|
${cyclePart} is now marked **complete** in PAPI \u2014 your work is shipped from your side. Any changes the owner requests come forward as a fast-follow in your next cycle; your closed cycle is not rewound.
|
|
20762
21068
|
|
|
20763
|
-
|
|
21069
|
+
After the PR is merged, run \`release\` again so PAPI can reconcile it.`
|
|
20764
21070
|
);
|
|
20765
21071
|
} catch (err) {
|
|
20766
21072
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
@@ -23149,8 +23455,19 @@ function isPaidTier(tier) {
|
|
|
23149
23455
|
}
|
|
23150
23456
|
async function enforceProjectCap(adapter2, target) {
|
|
23151
23457
|
if (isSelfHostedDeployment(process.env.PAPI_SELF_HOST)) return null;
|
|
23152
|
-
|
|
23153
|
-
if (
|
|
23458
|
+
let entitlement = null;
|
|
23459
|
+
if (adapterSupports(adapter2, "getMeteredUsage")) {
|
|
23460
|
+
try {
|
|
23461
|
+
entitlement = await adapter2.getMeteredUsage();
|
|
23462
|
+
} catch {
|
|
23463
|
+
return null;
|
|
23464
|
+
}
|
|
23465
|
+
}
|
|
23466
|
+
if (!entitlement) return null;
|
|
23467
|
+
const override = entitlement.projectLimitOverride;
|
|
23468
|
+
if (override === 0) return null;
|
|
23469
|
+
const effectiveLimit = typeof override === "number" && Number.isInteger(override) && override > 0 ? override : FREE_PROJECT_CAP;
|
|
23470
|
+
if (override == null && isPaidTier(entitlement.tier)) return null;
|
|
23154
23471
|
if (!adapterSupports(adapter2, "listUserProjects")) return null;
|
|
23155
23472
|
let projects;
|
|
23156
23473
|
try {
|
|
@@ -23162,17 +23479,20 @@ async function enforceProjectCap(adapter2, target) {
|
|
|
23162
23479
|
(p) => target.papiDir && p.papi_dir && p.papi_dir === target.papiDir || target.name && p.name && p.name.trim().toLowerCase() === target.name.trim().toLowerCase()
|
|
23163
23480
|
);
|
|
23164
23481
|
if (matchesExisting) return null;
|
|
23165
|
-
if (projects.length >=
|
|
23482
|
+
if (projects.length >= effectiveLimit) {
|
|
23483
|
+
return projectCapMessage(projects.length, effectiveLimit, entitlement.tier);
|
|
23484
|
+
}
|
|
23166
23485
|
return null;
|
|
23167
23486
|
}
|
|
23168
23487
|
async function resolveContributorUpsell(adapter2) {
|
|
23169
23488
|
const tier = await resolveTier(adapter2);
|
|
23170
23489
|
return evaluateContributorGate(tier).upsell ?? null;
|
|
23171
23490
|
}
|
|
23172
|
-
function projectCapMessage(currentCount) {
|
|
23173
|
-
|
|
23491
|
+
function projectCapMessage(currentCount, limit = FREE_PROJECT_CAP, tier = "free") {
|
|
23492
|
+
const nextStep = isPaidTier(tier) ? "request more projects" : "upgrade to Pro or request more projects";
|
|
23493
|
+
return `**You've reached your account project limit (${currentCount} of ${limit} projects).**
|
|
23174
23494
|
|
|
23175
|
-
|
|
23495
|
+
Your account currently covers up to ${limit} projects. To run more, ${nextStep} at a flat monthly price (no usage bills): ${PRICING_URL}
|
|
23176
23496
|
|
|
23177
23497
|
Your existing projects are untouched, and you can keep working in any of them.`;
|
|
23178
23498
|
}
|
|
@@ -23351,14 +23671,24 @@ Diagnostic JSON:
|
|
|
23351
23671
|
${JSON.stringify(payload, null, 2)}`
|
|
23352
23672
|
);
|
|
23353
23673
|
}
|
|
23354
|
-
function ensureDocDurable(path7, projectRoot) {
|
|
23674
|
+
function ensureDocDurable(path7, projectRoot, bodyStored = false) {
|
|
23355
23675
|
if (!hasLocalWorkspace() || !projectRoot) return "";
|
|
23356
|
-
const warn = (reason, fix) =>
|
|
23676
|
+
const warn = (reason, fix) => {
|
|
23677
|
+
if (bodyStored) {
|
|
23678
|
+
return `
|
|
23679
|
+
- **Not in git:** ${reason} The body is stored in the registry, so it survives a branch switch or stash; the file on disk is not version-controlled.`;
|
|
23680
|
+
}
|
|
23681
|
+
return `
|
|
23357
23682
|
|
|
23358
23683
|
\u26A0\uFE0F **Registered, but NOT durable.** ${reason}
|
|
23359
23684
|
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.
|
|
23360
23685
|
**Fix:** ${fix}`;
|
|
23686
|
+
};
|
|
23361
23687
|
if (!existsSync9(join13(projectRoot, path7))) {
|
|
23688
|
+
if (bodyStored) {
|
|
23689
|
+
return `
|
|
23690
|
+
- **No file at \`${path7}\`:** the body is stored in the registry, but nothing is on disk at the registered path. Write it there so readers resolve it.`;
|
|
23691
|
+
}
|
|
23362
23692
|
return warn(
|
|
23363
23693
|
`No file exists at \`${path7}\`.`,
|
|
23364
23694
|
`write the doc body to that path, then re-run doc_register.`
|
|
@@ -23375,7 +23705,11 @@ The registry stores metadata and a summary \u2014 not the body. This doc has one
|
|
|
23375
23705
|
if (isPathIgnored(projectRoot, path7)) {
|
|
23376
23706
|
return warn(
|
|
23377
23707
|
`\`${path7}\` is excluded by .gitignore, so it cannot be committed (docs/private/ is ignored by design).`,
|
|
23378
|
-
|
|
23708
|
+
// task-3358: no longer suggests moving the doc to a tracked folder. For anything
|
|
23709
|
+
// under docs/private/ that flips resolveDocVisibility off the private tier and
|
|
23710
|
+
// fails the no-private-artifacts gate — advising a leak to fix a durability gap.
|
|
23711
|
+
// Only reachable now when the body was NOT stored, where a copy is the real fix.
|
|
23712
|
+
`pass \`body\` to doc_register so the registry stores the content, or keep a copy outside the working tree.`
|
|
23379
23713
|
);
|
|
23380
23714
|
}
|
|
23381
23715
|
const result = commitSinglePath(projectRoot, path7, `docs: register ${path7}`);
|
|
@@ -23468,6 +23802,7 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23468
23802
|
visibility
|
|
23469
23803
|
});
|
|
23470
23804
|
let bodyNote = "";
|
|
23805
|
+
let bodyIsStored = false;
|
|
23471
23806
|
try {
|
|
23472
23807
|
const supplied = typeof args.body === "string" ? args.body : void 0;
|
|
23473
23808
|
let body = supplied;
|
|
@@ -23498,6 +23833,7 @@ ${decision.message}`;
|
|
|
23498
23833
|
visibility: entry.visibility ?? visibility,
|
|
23499
23834
|
ownerUserId: entry.ownerUserId
|
|
23500
23835
|
});
|
|
23836
|
+
bodyIsStored = true;
|
|
23501
23837
|
bodyNote = result.stored ? `
|
|
23502
23838
|
- **Body stored:** ${result.byteSize.toLocaleString()} bytes` : "\n- **Body:** unchanged since last registration";
|
|
23503
23839
|
}
|
|
@@ -23508,7 +23844,7 @@ ${decision.message}`;
|
|
|
23508
23844
|
const visibilityLabel = entry.visibility === "contributors" ? "team member" : entry.visibility ?? "private";
|
|
23509
23845
|
let durability = "";
|
|
23510
23846
|
try {
|
|
23511
|
-
durability = ensureDocDurable(entry.path, config2?.projectRoot);
|
|
23847
|
+
durability = ensureDocDurable(entry.path, config2?.projectRoot, bodyIsStored);
|
|
23512
23848
|
} catch {
|
|
23513
23849
|
durability = "";
|
|
23514
23850
|
}
|
|
@@ -24225,19 +24561,106 @@ function formatListItem(task) {
|
|
|
24225
24561
|
}
|
|
24226
24562
|
return base;
|
|
24227
24563
|
}
|
|
24564
|
+
var AD_KEYWORD_STOPWORDS = /* @__PURE__ */ new Set([
|
|
24565
|
+
"the",
|
|
24566
|
+
"and",
|
|
24567
|
+
"for",
|
|
24568
|
+
"but",
|
|
24569
|
+
"not",
|
|
24570
|
+
"you",
|
|
24571
|
+
"all",
|
|
24572
|
+
"any",
|
|
24573
|
+
"can",
|
|
24574
|
+
"has",
|
|
24575
|
+
"had",
|
|
24576
|
+
"was",
|
|
24577
|
+
"are",
|
|
24578
|
+
"its",
|
|
24579
|
+
"our",
|
|
24580
|
+
"out",
|
|
24581
|
+
"off",
|
|
24582
|
+
"own",
|
|
24583
|
+
"via",
|
|
24584
|
+
"per",
|
|
24585
|
+
"use",
|
|
24586
|
+
"get",
|
|
24587
|
+
"set",
|
|
24588
|
+
"new",
|
|
24589
|
+
"old",
|
|
24590
|
+
"one",
|
|
24591
|
+
"two",
|
|
24592
|
+
"now",
|
|
24593
|
+
"why",
|
|
24594
|
+
"how",
|
|
24595
|
+
"who",
|
|
24596
|
+
"let",
|
|
24597
|
+
"may",
|
|
24598
|
+
"yet",
|
|
24599
|
+
"too",
|
|
24600
|
+
"from",
|
|
24601
|
+
"that",
|
|
24602
|
+
"this",
|
|
24603
|
+
"with",
|
|
24604
|
+
"into",
|
|
24605
|
+
"when",
|
|
24606
|
+
"then",
|
|
24607
|
+
"than",
|
|
24608
|
+
"them",
|
|
24609
|
+
"they",
|
|
24610
|
+
"their",
|
|
24611
|
+
"there",
|
|
24612
|
+
"here",
|
|
24613
|
+
"have",
|
|
24614
|
+
"been",
|
|
24615
|
+
"were",
|
|
24616
|
+
"will",
|
|
24617
|
+
"would",
|
|
24618
|
+
"should",
|
|
24619
|
+
"could",
|
|
24620
|
+
"make",
|
|
24621
|
+
"made",
|
|
24622
|
+
"more",
|
|
24623
|
+
"most",
|
|
24624
|
+
"less",
|
|
24625
|
+
"only",
|
|
24626
|
+
"also",
|
|
24627
|
+
"just",
|
|
24628
|
+
"like",
|
|
24629
|
+
"over",
|
|
24630
|
+
"some",
|
|
24631
|
+
"such",
|
|
24632
|
+
"each",
|
|
24633
|
+
"both",
|
|
24634
|
+
"does",
|
|
24635
|
+
"done",
|
|
24636
|
+
"stop",
|
|
24637
|
+
"keep",
|
|
24638
|
+
"take",
|
|
24639
|
+
"give",
|
|
24640
|
+
"work",
|
|
24641
|
+
"thing",
|
|
24642
|
+
"things",
|
|
24643
|
+
"about",
|
|
24644
|
+
"after",
|
|
24645
|
+
"before"
|
|
24646
|
+
]);
|
|
24228
24647
|
function filterRelevantADs(ads, task) {
|
|
24648
|
+
const live = ads.filter((ad) => !ad.superseded);
|
|
24229
24649
|
const keywords = [];
|
|
24230
24650
|
if (task.module) keywords.push(task.module.toLowerCase());
|
|
24231
24651
|
if (task.epic) keywords.push(task.epic.toLowerCase());
|
|
24232
24652
|
if (task.phase) keywords.push(task.phase.toLowerCase());
|
|
24233
|
-
const titleWords = (task.title ?? task.displayId ?? "").toLowerCase().split(
|
|
24653
|
+
const titleWords = (task.title ?? task.displayId ?? "").toLowerCase().split(/[^a-z0-9-]+/).filter((w) => w.length > 2 && !AD_KEYWORD_STOPWORDS.has(w));
|
|
24234
24654
|
keywords.push(...titleWords);
|
|
24235
|
-
|
|
24236
|
-
return ads.filter((ad) => {
|
|
24237
|
-
if (ad.superseded) return false;
|
|
24655
|
+
const matches = keywords.length === 0 ? [] : live.filter((ad) => {
|
|
24238
24656
|
const text = `${ad.title} ${ad.body}`.toLowerCase();
|
|
24239
24657
|
return keywords.some((kw) => text.includes(kw));
|
|
24240
24658
|
});
|
|
24659
|
+
const selected = [...matches];
|
|
24660
|
+
for (const ad of live) {
|
|
24661
|
+
if (ad.confidence === "HIGH" && !selected.includes(ad)) selected.push(ad);
|
|
24662
|
+
}
|
|
24663
|
+
return { ads: selected, matched: matches.length, total: live.length };
|
|
24241
24664
|
}
|
|
24242
24665
|
async function getModuleContext(adapter2, task) {
|
|
24243
24666
|
if (!task.module) return "";
|
|
@@ -24263,9 +24686,18 @@ async function getModuleContext(adapter2, task) {
|
|
|
24263
24686
|
return "";
|
|
24264
24687
|
}
|
|
24265
24688
|
}
|
|
24266
|
-
function formatRelevantADs(ads) {
|
|
24267
|
-
if (ads.length === 0) return "";
|
|
24689
|
+
function formatRelevantADs(ads, stats) {
|
|
24690
|
+
if (ads.length === 0 && !stats) return "";
|
|
24268
24691
|
const lines = ["\n\n---\n\n**ACTIVE DECISIONS (relevant):**"];
|
|
24692
|
+
if (stats) {
|
|
24693
|
+
lines.push(
|
|
24694
|
+
`_${stats.matched} of ${stats.total} active decisions matched this task's keywords${ads.length > stats.matched ? `; HIGH-confidence decisions are always included` : ""}. Keyword matching is approximate \u2014 an absent decision has not been ruled out._`
|
|
24695
|
+
);
|
|
24696
|
+
}
|
|
24697
|
+
if (ads.length === 0) {
|
|
24698
|
+
lines.push("\n_No active decisions matched. This is a filter result, not a statement that none apply._");
|
|
24699
|
+
return lines.join("\n");
|
|
24700
|
+
}
|
|
24269
24701
|
for (const ad of ads) {
|
|
24270
24702
|
const bodyLines = ad.body.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
24271
24703
|
const summary = bodyLines[0]?.trim().slice(0, 120) ?? "";
|
|
@@ -24446,7 +24878,7 @@ If >80% of the scope is already implemented, call \`build_execute\` with complet
|
|
|
24446
24878
|
try {
|
|
24447
24879
|
const allADs = await adapter2.getActiveDecisions();
|
|
24448
24880
|
const relevant = filterRelevantADs(allADs, result.task);
|
|
24449
|
-
adSection = formatRelevantADs(relevant);
|
|
24881
|
+
adSection = formatRelevantADs(relevant.ads, { matched: relevant.matched, total: relevant.total });
|
|
24450
24882
|
} catch {
|
|
24451
24883
|
}
|
|
24452
24884
|
let dogfoodSection = "";
|
|
@@ -25990,6 +26422,21 @@ async function recordAdHoc(adapter2, input) {
|
|
|
25990
26422
|
var VALID_EFFORTS = ["XS", "S", "M", "L", "XL"];
|
|
25991
26423
|
var VALID_PRIORITIES = ["P0 Critical", "P1 High", "P2 Medium", "P3 Low"];
|
|
25992
26424
|
var VALID_TYPES = ["task", "bug", "research", "discovery", "spike", "idea"];
|
|
26425
|
+
var AD_HOC_CONFLICT_GATE_INSTRUCTION = "\n**AD conflict gate:** This work is already done. If any of it cuts against a decision above, say so now \u2014 add a `task_comment` on this task, or pass `proposal` to record the boundary as its own Decision or Convention. A conflict noticed here and not written down is one nobody can find later. A contradiction is not a veto \u2014 surfacing it is the owner's call to make, not yours to skip.";
|
|
26426
|
+
async function adHocDecisionSection(adapter2, task) {
|
|
26427
|
+
try {
|
|
26428
|
+
const allADs = await adapter2.getActiveDecisions();
|
|
26429
|
+
const relevant = filterRelevantADs(allADs, task);
|
|
26430
|
+
if (relevant.total === 0) return "";
|
|
26431
|
+
const section = formatRelevantADs(relevant.ads, {
|
|
26432
|
+
matched: relevant.matched,
|
|
26433
|
+
total: relevant.total
|
|
26434
|
+
});
|
|
26435
|
+
return section.replace(AD_CONFLICT_GATE_INSTRUCTION, AD_HOC_CONFLICT_GATE_INSTRUCTION);
|
|
26436
|
+
} catch {
|
|
26437
|
+
return "";
|
|
26438
|
+
}
|
|
26439
|
+
}
|
|
25993
26440
|
function inferTaskType(description) {
|
|
25994
26441
|
const lower = description.toLowerCase();
|
|
25995
26442
|
if (/\bfix\b|bug|error|crash|broken|regression|defect/.test(lower)) return "bug";
|
|
@@ -26216,12 +26663,12 @@ The task is recorded **In Review** and pinned to **Cycle ${result.task.cycle}**,
|
|
|
26216
26663
|
\`\`\`
|
|
26217
26664
|
2. Leave the branch **unmerged** \u2014 it is picked up by the next cycle's \`release\`.
|
|
26218
26665
|
|
|
26219
|
-
_To correct: board_edit ${result.task.id} with updated fields._` + await recordAdHocProposal(adapter2, args, { cycleNumber: result.task.cycle ?? 0, sourceTaskId: result.task.id })
|
|
26666
|
+
_To correct: board_edit ${result.task.id} with updated fields._` + await recordAdHocProposal(adapter2, args, { cycleNumber: result.task.cycle ?? 0, sourceTaskId: result.task.id }) + await adHocDecisionSection(target, result.task)
|
|
26220
26667
|
);
|
|
26221
26668
|
}
|
|
26222
26669
|
return textResponse(
|
|
26223
26670
|
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule})${overrideNote}.${truncateWarning}${promoNote} Build report attached.
|
|
26224
|
-
_To correct: board_edit ${result.task.id} with updated fields._` + await recordAdHocProposal(adapter2, args, { cycleNumber: result.task.cycle ?? 0, sourceTaskId: result.task.id })
|
|
26671
|
+
_To correct: board_edit ${result.task.id} with updated fields._` + await recordAdHocProposal(adapter2, args, { cycleNumber: result.task.cycle ?? 0, sourceTaskId: result.task.id }) + await adHocDecisionSection(target, result.task)
|
|
26225
26672
|
);
|
|
26226
26673
|
}
|
|
26227
26674
|
|
|
@@ -27595,14 +28042,23 @@ ${overlap}`;
|
|
|
27595
28042
|
const allTasks = await adapter2.queryBoard();
|
|
27596
28043
|
const cycleTasks = allTasks.filter((t) => t.cycle === result.currentCycle);
|
|
27597
28044
|
const gate = await resolveOwnerGate(adapter2, config2);
|
|
27598
|
-
|
|
27599
|
-
|
|
27600
|
-
const
|
|
28045
|
+
let releaseCaps = {};
|
|
28046
|
+
try {
|
|
28047
|
+
const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
|
|
28048
|
+
releaseCaps = info?.capabilities ?? {};
|
|
28049
|
+
} catch {
|
|
28050
|
+
releaseCaps = {};
|
|
28051
|
+
}
|
|
28052
|
+
const cycleGate = isCapabilityEnabled(releaseCaps, "editorRelease") ? await resolveCycleGate(adapter2, gate) : { allowed: !gate.enforced || gate.callerIsOwner, role: gate.callerIsOwner ? "owner" : null };
|
|
28053
|
+
const callerMayRelease = cycleGate.allowed;
|
|
28054
|
+
if (cycleTasks.length > 0 && cycleTasks.every((t) => t.status === "Done") && !callerMayRelease) {
|
|
28055
|
+
const why = cycleGate.role === "viewer" ? `your role on this project is "viewer"` : `your role on this project could not be confirmed`;
|
|
28056
|
+
const resolutionNote = cycleGate.resolutionError ? ` (Role resolution failed: ${cycleGate.resolutionError} \u2014 the gate fails closed.)` : "";
|
|
27601
28057
|
autoReleaseNote = `
|
|
27602
28058
|
|
|
27603
28059
|
---
|
|
27604
28060
|
|
|
27605
|
-
\u2705 Verdict recorded. All cycle tasks are Done, but
|
|
28061
|
+
\u2705 Verdict recorded. All cycle tasks are Done, but no release was cut \u2014 **auto-release needs an owner or editor role**, and ${why}. Push your branch and open a PR, or ask for editor access.${resolutionNote}`;
|
|
27606
28062
|
} else if (cycleTasks.length > 0 && cycleTasks.every((t) => t.status === "Done")) {
|
|
27607
28063
|
let planRunCount = null;
|
|
27608
28064
|
if (adapterSupports(adapter2, "countPlanRunsForCycle")) {
|
|
@@ -27856,7 +28312,7 @@ import { access as access2, readFile as readFile5, writeFile as writeFile3 } fro
|
|
|
27856
28312
|
import path5 from "path";
|
|
27857
28313
|
var initTool = {
|
|
27858
28314
|
name: "init",
|
|
27859
|
-
description: "Write the MCP config file that connects this project to PAPI. Generates .mcp.json
|
|
28315
|
+
description: "Write the MCP config file that connects this project to PAPI. Generates .mcp.json, or the equivalent for whichever MCP client you use \u2014 Claude Code, Cursor, VS Code, Windsurf, OpenCode, Amazon Q, Kilo Code, Gemini CLI, Codex CLI, or Hermes Agent. Config-only \u2014 does not create any project data. Run this first, then run `setup` to create your PAPI project.",
|
|
27860
28316
|
annotations: { title: "Initialise Project", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
27861
28317
|
inputSchema: {
|
|
27862
28318
|
type: "object",
|
|
@@ -29300,6 +29756,13 @@ var orientTool = {
|
|
|
29300
29756
|
full: {
|
|
29301
29757
|
type: "boolean",
|
|
29302
29758
|
description: "Run the heavy enrichment blocks that the lean default path skips: Research Signals (doc search) and npm version-drift. Default false \u2014 the lean path keeps the per-session query count down so orient stays well under the tool timeout on large projects. Implied automatically when deep_housekeeping is true."
|
|
29759
|
+
},
|
|
29760
|
+
// SUP-2026-027: orient stopped multi-project accounts asking which project
|
|
29761
|
+
// to use, while declaring no way to answer — the caller could not pass one
|
|
29762
|
+
// without the schema advertising it. Same contract as board_view / bug.
|
|
29763
|
+
project: {
|
|
29764
|
+
type: "string",
|
|
29765
|
+
description: "Project id (UUID) or slug to orient on, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. Use project_switch to change the session default. PASS THIS WHENEVER YOU KNOW WHICH REPO THE SESSION IS IN: on a multi-project account a connection with no project bound cannot be resolved, and PAPI will stop and ask rather than guess."
|
|
29303
29766
|
}
|
|
29304
29767
|
},
|
|
29305
29768
|
required: []
|
|
@@ -29402,7 +29865,7 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
29402
29865
|
lines.push(`**Connection:** ${statusIcon} ${statusLabel}`);
|
|
29403
29866
|
lines.push("");
|
|
29404
29867
|
}
|
|
29405
|
-
if (projectRoot) {
|
|
29868
|
+
if (projectRoot && hasLocalWorkspace()) {
|
|
29406
29869
|
const modeLine = formatWorkspaceModeLine(resolveWorkspaceMode(projectRoot));
|
|
29407
29870
|
if (modeLine) {
|
|
29408
29871
|
lines.push(modeLine);
|
|
@@ -29768,7 +30231,8 @@ async function computeCohortVisibility(adapter2, config2, contributorsInput) {
|
|
|
29768
30231
|
} catch {
|
|
29769
30232
|
return void 0;
|
|
29770
30233
|
}
|
|
29771
|
-
|
|
30234
|
+
const cycleGate = await resolveCycleGate(adapter2, gate);
|
|
30235
|
+
if (!cycleGate.allowed) return void 0;
|
|
29772
30236
|
let tasks;
|
|
29773
30237
|
try {
|
|
29774
30238
|
tasks = await adapter2.queryBoard({ compact: true });
|
|
@@ -29851,7 +30315,21 @@ function withBoardMemo(adapter2) {
|
|
|
29851
30315
|
});
|
|
29852
30316
|
}
|
|
29853
30317
|
async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVersion = "unknown") {
|
|
29854
|
-
|
|
30318
|
+
let resolvedAdapter = rawAdapter;
|
|
30319
|
+
let projectOverrideNote = "";
|
|
30320
|
+
try {
|
|
30321
|
+
({ adapter: resolvedAdapter, overrideNote: projectOverrideNote } = await resolvePerCallProjectAdapter(
|
|
30322
|
+
rawAdapter,
|
|
30323
|
+
args
|
|
30324
|
+
));
|
|
30325
|
+
} catch (err) {
|
|
30326
|
+
if (err instanceof ProjectResolutionError) return errorResponse(err.message);
|
|
30327
|
+
throw err;
|
|
30328
|
+
}
|
|
30329
|
+
const projectOverrideLine = projectOverrideNote ? `_Project override${projectOverrideNote}_
|
|
30330
|
+
|
|
30331
|
+
` : "";
|
|
30332
|
+
const adapter2 = withBoardMemo(resolvedAdapter);
|
|
29855
30333
|
const environment = normaliseEnvironment(args.environment);
|
|
29856
30334
|
const deepHousekeeping = args.deep_housekeeping === true;
|
|
29857
30335
|
const fullEnrichment = args.full === true || deepHousekeeping;
|
|
@@ -30398,7 +30876,7 @@ ${section}`;
|
|
|
30398
30876
|
const runtimeIdentityNote = `
|
|
30399
30877
|
|
|
30400
30878
|
${formatRuntimeIdentity(getRuntimeIdentity(config2, serverVersion))}`;
|
|
30401
|
-
return { ...textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName, carryForwardRefs) + runtimeIdentityNote + unblockNote + deferredGateNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + onboardingCoachingNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection), _cycleNumber: healthResult.cycleNumber };
|
|
30879
|
+
return { ...textResponse(projectOverrideLine + projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName, carryForwardRefs) + runtimeIdentityNote + unblockNote + deferredGateNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + onboardingCoachingNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection), _cycleNumber: healthResult.cycleNumber };
|
|
30402
30880
|
} catch (err) {
|
|
30403
30881
|
const message = err instanceof Error ? err.message : String(err);
|
|
30404
30882
|
const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
|
|
@@ -31501,7 +31979,6 @@ function readLlmResponse(args) {
|
|
|
31501
31979
|
|
|
31502
31980
|
// src/tools/ad-view.ts
|
|
31503
31981
|
function adColumns(all) {
|
|
31504
|
-
const titles = titleMap(all);
|
|
31505
31982
|
return [
|
|
31506
31983
|
{ name: "id", header: "AD", value: (d) => d.id },
|
|
31507
31984
|
{ name: "title", header: "Title", value: (d) => d.title },
|
|
@@ -31510,7 +31987,7 @@ function adColumns(all) {
|
|
|
31510
31987
|
{
|
|
31511
31988
|
name: "supersededBy",
|
|
31512
31989
|
header: "Superseded By",
|
|
31513
|
-
value: (d) => d.supersededBy ?
|
|
31990
|
+
value: (d) => d.supersededBy ? formatEntityRef(d.supersededBy, all) : "-"
|
|
31514
31991
|
},
|
|
31515
31992
|
{ name: "body", header: "Body", value: (d) => d.body.replace(/\s+/g, " ").trim() }
|
|
31516
31993
|
];
|
|
@@ -31520,7 +31997,7 @@ function supersededNote(d, all) {
|
|
|
31520
31997
|
if (!d.superseded) return "";
|
|
31521
31998
|
const by = d.supersededBy;
|
|
31522
31999
|
if (!by) return " [SUPERSEDED by unknown]";
|
|
31523
|
-
return ` [SUPERSEDED by ${
|
|
32000
|
+
return ` [SUPERSEDED by ${formatEntityRef(by, all)}]`;
|
|
31524
32001
|
}
|
|
31525
32002
|
function formatAdBody(d, all) {
|
|
31526
32003
|
return `## ${d.id}: ${d.title} [${d.confidence}]${supersededNote(d, all)}
|
|
@@ -31737,7 +32214,10 @@ async function handleLearningAction(adapter2, args) {
|
|
|
31737
32214
|
if (!learningId?.trim()) return errorResponse('learning_id is required for mode "mark".');
|
|
31738
32215
|
if (!actionTaken) return errorResponse('action_taken is required for mode "mark".');
|
|
31739
32216
|
try {
|
|
31740
|
-
await adapter2.updateCycleLearningActionRef(learningId.trim(), actionRef?.trim() ?? actionTaken
|
|
32217
|
+
await adapter2.updateCycleLearningActionRef(learningId.trim(), actionRef?.trim() ?? actionTaken, {
|
|
32218
|
+
actionTaken,
|
|
32219
|
+
force: true
|
|
32220
|
+
});
|
|
31741
32221
|
return textResponse(
|
|
31742
32222
|
`Learning \`${learningId}\` marked as **${actionTaken}**${actionRef ? ` \u2192 \`${actionRef}\`` : ""}.`
|
|
31743
32223
|
);
|
|
@@ -32693,7 +33173,7 @@ var inflightWork = /* @__PURE__ */ new Map();
|
|
|
32693
33173
|
var ToolTimeoutError = class extends Error {
|
|
32694
33174
|
constructor(toolName, expectedMs, actualMs, wedged, pendingNote) {
|
|
32695
33175
|
super(
|
|
32696
|
-
wedged ? `Tool '${toolName}' timed out after ${actualMs}ms (budget ${expectedMs}ms). A database operation ran the entire time without returning \u2014 the connection is likely wedged or contended (SUP-2026-012); a second PAPI session open against the same database is a common cause.${pendingNote} The MCP server is restarting; reconnect
|
|
33176
|
+
wedged ? `Tool '${toolName}' timed out after ${actualMs}ms (budget ${expectedMs}ms). A database operation ran the entire time without returning \u2014 the connection is likely wedged or contended (SUP-2026-012); a second PAPI session open against the same database is a common cause.${pendingNote} The MCP server is restarting; reconnect to the PAPI server in your AI client (in some clients that is /mcp; otherwise restart the client), and close any other open PAPI session, before the next call.` : `Tool '${toolName}' timed out after ${actualMs}ms (budget ${expectedMs}ms). No single operation was stuck \u2014 this is cumulative query load exceeding the budget, not a wedged pool, and the database is healthy.${pendingNote} Retry the call; \`orient\` runs a lean path by default (heavy enrichment is opt-in via \`full: true\`).`
|
|
32697
33177
|
);
|
|
32698
33178
|
this.toolName = toolName;
|
|
32699
33179
|
this.expectedMs = expectedMs;
|
|
@@ -33194,7 +33674,12 @@ var FRIENDLY_GET_HTML = `<!doctype html>
|
|
|
33194
33674
|
<code>https://mcp.getpapi.ai/mcp</code>.</p>
|
|
33195
33675
|
<a class="btn" href="https://getpapi.ai/docs/install">See the install guide \u2192</a>
|
|
33196
33676
|
</div></body></html>`;
|
|
33197
|
-
var KNOWN_INSTALL_CLIENTS = /* @__PURE__ */ new Set([
|
|
33677
|
+
var KNOWN_INSTALL_CLIENTS = /* @__PURE__ */ new Set([
|
|
33678
|
+
"claude-code-plugin",
|
|
33679
|
+
"cursor-plugin",
|
|
33680
|
+
"gemini-extension",
|
|
33681
|
+
"codex-plugin"
|
|
33682
|
+
]);
|
|
33198
33683
|
var MAX_BODY_BYTES = 1 * 1024 * 1024;
|
|
33199
33684
|
var SLOW_REQUEST_MS = 5e3;
|
|
33200
33685
|
var CRITICAL_REQUEST_MS = 3e4;
|
|
@@ -33807,7 +34292,7 @@ Options:
|
|
|
33807
34292
|
Getting started:
|
|
33808
34293
|
1. Run "npx @papi-ai/server setup" in any project folder
|
|
33809
34294
|
2. Approve in the browser when it opens
|
|
33810
|
-
3. Open
|
|
34295
|
+
3. Open your AI client in the same folder and say "run setup"
|
|
33811
34296
|
|
|
33812
34297
|
Stuck?
|
|
33813
34298
|
npx @papi-ai/server doctor # diagnose your config
|