@papi-ai/server 0.7.83 → 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 +98 -15
- package/dist/index.js +1208 -241
- 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, 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,9 +809,24 @@ 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);
|
|
829
|
+
ASSIGNABLE_CONTRIBUTOR_ROLES = ["editor", "viewer"];
|
|
806
830
|
SENSITIVE_CHANGELOG_PATTERNS = [
|
|
807
831
|
// ── Test-user machinery ────────────────────────────────────────────────
|
|
808
832
|
/@test\.papi\.dev/i,
|
|
@@ -851,6 +875,8 @@ var init_dist = __esm({
|
|
|
851
875
|
};
|
|
852
876
|
NONE_RE = /^(none|n\/a|no(ne)? (found|discovered))\.?$/i;
|
|
853
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;
|
|
854
880
|
MIN_REASON_LENGTH = 12;
|
|
855
881
|
LOW_SEVERITIES = /* @__PURE__ */ new Set(["P2", "P3"]);
|
|
856
882
|
WAB_WINDOW_DAYS = 7;
|
|
@@ -906,6 +932,7 @@ __export(git_exports, {
|
|
|
906
932
|
detectUnrecordedCommits: () => detectUnrecordedCommits,
|
|
907
933
|
ensureLatestDevelop: () => ensureLatestDevelop,
|
|
908
934
|
ensureTagAtHead: () => ensureTagAtHead,
|
|
935
|
+
findContributorReleasePullRequests: () => findContributorReleasePullRequests,
|
|
909
936
|
findTaskCommitsOnBase: () => findTaskCommitsOnBase,
|
|
910
937
|
getBranchDiff: () => getBranchDiff,
|
|
911
938
|
getCommitFiles: () => getCommitFiles,
|
|
@@ -923,6 +950,7 @@ __export(git_exports, {
|
|
|
923
950
|
getOriginRepoSlug: () => getOriginRepoSlug,
|
|
924
951
|
getOriginUrl: () => getOriginUrl,
|
|
925
952
|
getPathsDifferingFrom: () => getPathsDifferingFrom,
|
|
953
|
+
getPullRequestState: () => getPullRequestState,
|
|
926
954
|
getPullRequestUrl: () => getPullRequestUrl,
|
|
927
955
|
getRemoteBranchFiles: () => getRemoteBranchFiles,
|
|
928
956
|
getRootCommitHash: () => getRootCommitHash,
|
|
@@ -949,6 +977,7 @@ __export(git_exports, {
|
|
|
949
977
|
listGroupedCycleBranches: () => listGroupedCycleBranches,
|
|
950
978
|
listOpenPullRequests: () => listOpenPullRequests,
|
|
951
979
|
listOrphanFeatBranches: () => listOrphanFeatBranches,
|
|
980
|
+
memberBranchSlug: () => memberBranchSlug,
|
|
952
981
|
mergePullRequest: () => mergePullRequest,
|
|
953
982
|
normalizeGitUrl: () => normalizeGitUrl,
|
|
954
983
|
pickModuleCycleBranch: () => pickModuleCycleBranch,
|
|
@@ -1841,9 +1870,20 @@ function detectUnrecordedCommits(cwd, baseBranch) {
|
|
|
1841
1870
|
function taskBranchName(taskId) {
|
|
1842
1871
|
return `feat/${taskId}`;
|
|
1843
1872
|
}
|
|
1844
|
-
function cycleBranchName(cycleNumber, module) {
|
|
1873
|
+
function cycleBranchName(cycleNumber, module, memberSlug) {
|
|
1845
1874
|
const slug = module.toLowerCase().replace(/&/g, "and").replace(/&/g, "and").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1846
|
-
|
|
1875
|
+
const member = memberSlug ? `-${memberSlug}` : "";
|
|
1876
|
+
return `feat/cycle-${cycleNumber}-${slug}${member}`;
|
|
1877
|
+
}
|
|
1878
|
+
function memberBranchSlug(member) {
|
|
1879
|
+
const sanitise = (raw) => raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20).replace(/-+$/g, "");
|
|
1880
|
+
const fromName = member.displayName ? sanitise(member.displayName) : "";
|
|
1881
|
+
if (fromName) return fromName;
|
|
1882
|
+
const localPart = member.email ? member.email.split("@")[0] : "";
|
|
1883
|
+
const fromEmail = localPart ? sanitise(localPart) : "";
|
|
1884
|
+
if (fromEmail) return fromEmail;
|
|
1885
|
+
const fromId = member.userId ? sanitise(member.userId).slice(0, 8) : "";
|
|
1886
|
+
return fromId || void 0;
|
|
1847
1887
|
}
|
|
1848
1888
|
function getHeadCommitSha(cwd) {
|
|
1849
1889
|
try {
|
|
@@ -1874,6 +1914,56 @@ function getPullRequestUrl(cwd, branch) {
|
|
|
1874
1914
|
return null;
|
|
1875
1915
|
}
|
|
1876
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
|
+
}
|
|
1877
1967
|
function squashMergePullRequest(cwd, branch) {
|
|
1878
1968
|
const repo = getOriginRepoSlug(cwd);
|
|
1879
1969
|
const baseArgs = ["pr", "merge", branch, "--squash", "--delete-branch"];
|
|
@@ -1966,9 +2056,9 @@ function isBranchContentAlreadyInBase(cwd, branch, baseBranch) {
|
|
|
1966
2056
|
return false;
|
|
1967
2057
|
}
|
|
1968
2058
|
}
|
|
1969
|
-
function pickModuleCycleBranch(candidates, cycleNumber, module) {
|
|
2059
|
+
function pickModuleCycleBranch(candidates, cycleNumber, module, memberSlug) {
|
|
1970
2060
|
if (candidates.length === 0) return void 0;
|
|
1971
|
-
const expected = cycleBranchName(cycleNumber, module);
|
|
2061
|
+
const expected = cycleBranchName(cycleNumber, module, memberSlug);
|
|
1972
2062
|
return candidates.find((b2) => b2 === expected);
|
|
1973
2063
|
}
|
|
1974
2064
|
function listOrphanFeatBranches(cwd, baseBranch) {
|
|
@@ -2307,6 +2397,10 @@ var init_proxy_adapter = __esm({
|
|
|
2307
2397
|
"dismissRecommendation",
|
|
2308
2398
|
"findPendingDocActionsForTask",
|
|
2309
2399
|
"getActiveDecisions",
|
|
2400
|
+
"getContributorRole",
|
|
2401
|
+
"listContributorReleasePrs",
|
|
2402
|
+
"recordContributorReleasePr",
|
|
2403
|
+
"setContributorReleasePrStatus",
|
|
2310
2404
|
"getActiveStage",
|
|
2311
2405
|
"getBuildReportCountForTask",
|
|
2312
2406
|
"getBuildReportsSince",
|
|
@@ -2446,7 +2540,7 @@ var init_proxy_adapter = __esm({
|
|
|
2446
2540
|
"applyActiveDecisionUpdates",
|
|
2447
2541
|
// getToolCallCount + updatePhaseStatus are ABSENT: they now have edge handlers and
|
|
2448
2542
|
// are served through the forwarder (getToolCallCount = SUP-2026-026 handler #1).
|
|
2449
|
-
|
|
2543
|
+
// getContributorRole is edge-wired and binds identity from the bearer.
|
|
2450
2544
|
// task-3018 (C356): storeDocBody + getDocBodyUsage are now WIRED — edge case
|
|
2451
2545
|
// handlers + ALLOWED_METHODS/WRITE_METHODS entries exist, so they forward and
|
|
2452
2546
|
// hosted callers get body storage. Removed from this list, as task-3017 required.
|
|
@@ -2454,9 +2548,6 @@ var init_proxy_adapter = __esm({
|
|
|
2454
2548
|
// of the owner-action queue. Six readers were wired C329 (task-2412) but the
|
|
2455
2549
|
// producer stayed here, so the hosted Owner Action Queue was structurally empty
|
|
2456
2550
|
// (AD-74 inverted). Now forwards; the edge binds user_id to the bearer ([C]).
|
|
2457
|
-
"recordContributorReleasePr",
|
|
2458
|
-
"setContributorReleasePrStatus",
|
|
2459
|
-
"listContributorReleasePrs",
|
|
2460
2551
|
"claimReview",
|
|
2461
2552
|
"getSiblingAds",
|
|
2462
2553
|
"getSiblingRepoTasks",
|
|
@@ -3172,8 +3263,8 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
3172
3263
|
getCycleLearningPatterns() {
|
|
3173
3264
|
return this.invoke("getCycleLearningPatterns", []);
|
|
3174
3265
|
}
|
|
3175
|
-
updateCycleLearningActionRef(learningId, taskDisplayId) {
|
|
3176
|
-
return this.invoke("updateCycleLearningActionRef", [learningId, taskDisplayId]);
|
|
3266
|
+
updateCycleLearningActionRef(learningId, taskDisplayId, opts) {
|
|
3267
|
+
return this.invoke("updateCycleLearningActionRef", [learningId, taskDisplayId, opts ?? {}]);
|
|
3177
3268
|
}
|
|
3178
3269
|
// --- Strategy Review Drafts ---
|
|
3179
3270
|
savePendingReviewResponse(cycleNumber, rawResponse) {
|
|
@@ -3232,8 +3323,8 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
3232
3323
|
async listContributors() {
|
|
3233
3324
|
return this.invoke("listContributors", []);
|
|
3234
3325
|
}
|
|
3235
|
-
async addContributorByEmail(email) {
|
|
3236
|
-
return this.invoke("addContributorByEmail", [email]);
|
|
3326
|
+
async addContributorByEmail(email, role) {
|
|
3327
|
+
return this.invoke("addContributorByEmail", [email, role]);
|
|
3237
3328
|
}
|
|
3238
3329
|
async removeContributorByEmail(email) {
|
|
3239
3330
|
return this.invoke("removeContributorByEmail", [email]);
|
|
@@ -3279,7 +3370,11 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
3279
3370
|
*/
|
|
3280
3371
|
async getMeteredUsage() {
|
|
3281
3372
|
const body = await this.postRoute("metering", {});
|
|
3282
|
-
return {
|
|
3373
|
+
return {
|
|
3374
|
+
tier: body.tier ?? "free",
|
|
3375
|
+
monthlyToolCalls: body.monthlyToolCalls ?? 0,
|
|
3376
|
+
projectLimitOverride: body.projectLimitOverride ?? null
|
|
3377
|
+
};
|
|
3283
3378
|
}
|
|
3284
3379
|
async listUserProjects() {
|
|
3285
3380
|
const body = await this.postRoute("project-list", {});
|
|
@@ -6391,7 +6486,7 @@ async function runSetup() {
|
|
|
6391
6486
|
log(`Project: ${result.project_slug}`);
|
|
6392
6487
|
}
|
|
6393
6488
|
log("");
|
|
6394
|
-
log(`Done. Open
|
|
6489
|
+
log(`Done. Open your AI client in this folder and say "run setup" to scaffold your first plan.`);
|
|
6395
6490
|
return 0;
|
|
6396
6491
|
}
|
|
6397
6492
|
var DEFAULT_BASE_URL, MAX_POLL_SECONDS, NETWORK_RETRY_LIMIT;
|
|
@@ -6499,7 +6594,7 @@ function loadConfig() {
|
|
|
6499
6594
|
${conflicting} is set in your environment, but PAPI_DATA_API_KEY is also set \u2014
|
|
6500
6595
|
you have a hosted PAPI account, so the database URL is being ignored anyway.
|
|
6501
6596
|
|
|
6502
|
-
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,
|
|
6503
6598
|
or remove it from your shell rc file (~/.zshrc, ~/.bashrc).
|
|
6504
6599
|
|
|
6505
6600
|
If you intentionally self-host, set PAPI_SELF_HOST=1 in your environment to bypass this guard.`
|
|
@@ -6944,7 +7039,7 @@ Fix the PAPI_PROJECT_ID in your .mcp.json (or unset it and let PAPI resolve the
|
|
|
6944
7039
|
Get started in 3 steps:
|
|
6945
7040
|
1. Sign up at ${dashboardUrl ? `${dashboardUrl}/login` : "your configured PAPI dashboard"}
|
|
6946
7041
|
2. Complete the onboarding wizard \u2014 it generates your .mcp.json config
|
|
6947
|
-
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
|
|
6948
7043
|
|
|
6949
7044
|
Already have an account? Make sure PAPI_DATA_API_KEY is set in your .mcp.json env config.`
|
|
6950
7045
|
);
|
|
@@ -7870,7 +7965,11 @@ The guard above says what to reject. This says what to propose. Rejecting is not
|
|
|
7870
7965
|
|
|
7871
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.
|
|
7872
7967
|
|
|
7873
|
-
**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.`;
|
|
7874
7973
|
var AD_CONFLICT_SURFACING_RULES = `**A contradiction is NOT a veto \u2014 surface it, never silently shelve it.**
|
|
7875
7974
|
|
|
7876
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.
|
|
@@ -8857,7 +8956,7 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
8857
8956
|
"activeDecisionUpdates": [
|
|
8858
8957
|
{
|
|
8859
8958
|
"id": "string \u2014 AD-N (existing) or new AD-N (for new decisions)",
|
|
8860
|
-
"action": "confidence_change | modify | resolve | supersede | new | delete",
|
|
8959
|
+
"action": "confidence_change | modify | resolve | supersede | new | delete | demote",
|
|
8861
8960
|
"body": "string \u2014 full AD block including ### heading, confidence tag, and body text (empty string for delete)",
|
|
8862
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.",
|
|
8863
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."
|
|
@@ -9109,7 +9208,7 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
9109
9208
|
"activeDecisionUpdates": [
|
|
9110
9209
|
{
|
|
9111
9210
|
"id": "string \u2014 AD-N (existing) or new AD-N (for new decisions)",
|
|
9112
|
-
"action": "confidence_change | modify | resolve | supersede | new | delete",
|
|
9211
|
+
"action": "confidence_change | modify | resolve | supersede | new | delete | demote",
|
|
9113
9212
|
"body": "string \u2014 full AD block including ### heading, confidence tag, and body text (empty string for delete)",
|
|
9114
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.",
|
|
9115
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."
|
|
@@ -9620,13 +9719,20 @@ async function getPrompt(name) {
|
|
|
9620
9719
|
}
|
|
9621
9720
|
|
|
9622
9721
|
// src/lib/foundation.ts
|
|
9623
|
-
var cache2 =
|
|
9722
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
9624
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
|
+
}
|
|
9625
9729
|
var MAX_TOKENS = 800;
|
|
9626
9730
|
var MAX_CHARS = MAX_TOKENS * 4;
|
|
9627
9731
|
async function buildProjectFoundation(adapter2) {
|
|
9628
9732
|
const now = Date.now();
|
|
9629
|
-
|
|
9733
|
+
const cacheKey = cacheKeyFor(adapter2);
|
|
9734
|
+
const hit = cache2.get(cacheKey);
|
|
9735
|
+
if (hit && hit.expiresAt > now) return hit.content;
|
|
9630
9736
|
const [briefRaw, decisions, northStar, horizons, stages] = await Promise.all([
|
|
9631
9737
|
safe(() => adapter2.readProductBrief(), ""),
|
|
9632
9738
|
safe(() => adapter2.getActiveDecisions(), []),
|
|
@@ -9648,7 +9754,7 @@ async function buildProjectFoundation(adapter2) {
|
|
|
9648
9754
|
const userType = extractUserType(briefRaw);
|
|
9649
9755
|
if (userType) lines.push(`Primary user: ${userType}`);
|
|
9650
9756
|
if (lines.length === 0) {
|
|
9651
|
-
cache2
|
|
9757
|
+
cache2.set(cacheKey, { content: "", expiresAt: now + CACHE_TTL_MS2 });
|
|
9652
9758
|
return "";
|
|
9653
9759
|
}
|
|
9654
9760
|
const body = lines.join("\n");
|
|
@@ -9661,7 +9767,7 @@ ${body}
|
|
|
9661
9767
|
[/project_foundation]
|
|
9662
9768
|
|
|
9663
9769
|
` : block;
|
|
9664
|
-
cache2
|
|
9770
|
+
cache2.set(cacheKey, { content: final, expiresAt: now + CACHE_TTL_MS2 });
|
|
9665
9771
|
return final;
|
|
9666
9772
|
}
|
|
9667
9773
|
async function safe(fn, fallback) {
|
|
@@ -10155,6 +10261,46 @@ function computeCarryForwardStaleness(log2, doneTaskIds) {
|
|
|
10155
10261
|
].join("\n");
|
|
10156
10262
|
}
|
|
10157
10263
|
|
|
10264
|
+
// src/lib/refs.ts
|
|
10265
|
+
var MAX_LABEL = 55;
|
|
10266
|
+
function truncateLabel(title, max = MAX_LABEL) {
|
|
10267
|
+
if (title.length <= max) return title;
|
|
10268
|
+
return `${title.slice(0, max - 1).trimEnd()}\u2026`;
|
|
10269
|
+
}
|
|
10270
|
+
function formatRef(id, title) {
|
|
10271
|
+
const label = title?.trim();
|
|
10272
|
+
if (!label) return id;
|
|
10273
|
+
if (label.toLowerCase() === id.toLowerCase()) return id;
|
|
10274
|
+
return `${id} (${truncateLabel(label)})`;
|
|
10275
|
+
}
|
|
10276
|
+
function formatRefList(ids, titles) {
|
|
10277
|
+
return ids.map((id) => formatRef(id, lookupTitle(id, titles))).join(", ");
|
|
10278
|
+
}
|
|
10279
|
+
function lookupTitle(id, titles) {
|
|
10280
|
+
if (!titles) return void 0;
|
|
10281
|
+
return titles.get(id) ?? titles.get(id.toLowerCase()) ?? titles.get(id.toUpperCase());
|
|
10282
|
+
}
|
|
10283
|
+
function titleMap(entities) {
|
|
10284
|
+
const map = /* @__PURE__ */ new Map();
|
|
10285
|
+
for (const e of entities) {
|
|
10286
|
+
if (!e.title) continue;
|
|
10287
|
+
for (const key of [e.id, e.displayId, e.uuid]) {
|
|
10288
|
+
if (!key) continue;
|
|
10289
|
+
map.set(key, e.title);
|
|
10290
|
+
map.set(key.toLowerCase(), e.title);
|
|
10291
|
+
}
|
|
10292
|
+
}
|
|
10293
|
+
return map;
|
|
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
|
+
}
|
|
10303
|
+
|
|
10158
10304
|
// src/lib/blocker.ts
|
|
10159
10305
|
function idMatches(candidate, ref) {
|
|
10160
10306
|
if (!candidate) return false;
|
|
@@ -10188,16 +10334,31 @@ function isBlockerResolved(blocker, ctx) {
|
|
|
10188
10334
|
return false;
|
|
10189
10335
|
}
|
|
10190
10336
|
}
|
|
10191
|
-
function formatBlockerWaiting(blocker) {
|
|
10337
|
+
function formatBlockerWaiting(blocker, refTitle) {
|
|
10338
|
+
const ref = formatRef(blocker.ref, refTitle);
|
|
10192
10339
|
switch (blocker.type) {
|
|
10193
10340
|
case "depends-on":
|
|
10194
|
-
return `waiting on ${
|
|
10341
|
+
return `waiting on ${ref} \u2014 clears when that task is Done`;
|
|
10195
10342
|
case "owner-action":
|
|
10196
|
-
return `waiting on owner action ${
|
|
10343
|
+
return `waiting on owner action ${ref} \u2014 clears when you complete it`;
|
|
10344
|
+
case "decision-gate":
|
|
10345
|
+
return `waiting on decision ${ref} \u2014 clears when that decision is resolved`;
|
|
10346
|
+
default:
|
|
10347
|
+
return `waiting on ${ref}`;
|
|
10348
|
+
}
|
|
10349
|
+
}
|
|
10350
|
+
function resolveBlockerTitle(blocker, ctx) {
|
|
10351
|
+
switch (blocker.type) {
|
|
10352
|
+
case "depends-on":
|
|
10353
|
+
return ctx.tasks.find(
|
|
10354
|
+
(t) => idMatches(t.displayId, blocker.ref) || idMatches(t.id, blocker.ref)
|
|
10355
|
+
)?.title;
|
|
10197
10356
|
case "decision-gate":
|
|
10198
|
-
return
|
|
10357
|
+
return ctx.decisions.find(
|
|
10358
|
+
(d) => idMatches(d.displayId, blocker.ref) || idMatches(d.id, blocker.ref)
|
|
10359
|
+
)?.title;
|
|
10199
10360
|
default:
|
|
10200
|
-
return
|
|
10361
|
+
return void 0;
|
|
10201
10362
|
}
|
|
10202
10363
|
}
|
|
10203
10364
|
|
|
@@ -10303,6 +10464,31 @@ function isProjectOwner(callerUserId, ownerUserId) {
|
|
|
10303
10464
|
if (caller.length === 0 || owner.length === 0) return false;
|
|
10304
10465
|
return caller === owner;
|
|
10305
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
|
+
}
|
|
10306
10492
|
async function resolveOwnerGate(adapter2, config2) {
|
|
10307
10493
|
if (adapterSupports(adapter2, "getOwnerIdentity")) {
|
|
10308
10494
|
try {
|
|
@@ -10368,6 +10554,48 @@ function normalizeComplexity(value) {
|
|
|
10368
10554
|
const key = (value ?? "").trim().toUpperCase();
|
|
10369
10555
|
return COMPLEXITY_ALIASES[key] ?? "Small";
|
|
10370
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
|
+
}
|
|
10371
10599
|
var PLAN_BUILD_REPORT_BUDGET = { maxReports: 12, fieldBudget: 280 };
|
|
10372
10600
|
function leadChainWithRecommended(chain, recommendedTaskId) {
|
|
10373
10601
|
const rec = recommendedTaskId?.trim();
|
|
@@ -10384,10 +10612,7 @@ async function resolvePlanScope(adapter2, config2) {
|
|
|
10384
10612
|
}
|
|
10385
10613
|
function filterToPersonalBacklog(tasks, scope) {
|
|
10386
10614
|
if (!scope.callerUserId) return tasks;
|
|
10387
|
-
|
|
10388
|
-
return tasks.filter((t) => !t.assigneeId || t.assigneeId === scope.callerUserId);
|
|
10389
|
-
}
|
|
10390
|
-
return tasks.filter((t) => t.assigneeId === scope.callerUserId);
|
|
10615
|
+
return tasks.filter((t) => !t.assigneeId || t.assigneeId === scope.callerUserId);
|
|
10391
10616
|
}
|
|
10392
10617
|
function determineContextTier(cycleCount) {
|
|
10393
10618
|
if (cycleCount <= 5) return 1;
|
|
@@ -10428,11 +10653,22 @@ async function parkDecisionConflicts(adapter2, conflicts, newTaskIdMap, blockedC
|
|
|
10428
10653
|
if (conflicts.length === 0) return [];
|
|
10429
10654
|
const scheduled = new Set(scheduledTaskIds.map((id) => id.toLowerCase()));
|
|
10430
10655
|
const results = [];
|
|
10656
|
+
let adTitles = /* @__PURE__ */ new Map();
|
|
10657
|
+
try {
|
|
10658
|
+
if (adapter2.getActiveDecisions) adTitles = titleMap(await adapter2.getActiveDecisions());
|
|
10659
|
+
} catch {
|
|
10660
|
+
}
|
|
10431
10661
|
for (const conflict of conflicts) {
|
|
10432
10662
|
const resolvedTaskId = newTaskIdMap.get(conflict.taskId) ?? conflict.taskId;
|
|
10433
|
-
const
|
|
10663
|
+
const adTitle = lookupTitle(conflict.adId, adTitles);
|
|
10664
|
+
const base = { ...conflict, resolvedTaskId, parked: false, adTitle };
|
|
10434
10665
|
if (scheduled.has(resolvedTaskId.toLowerCase())) {
|
|
10435
|
-
|
|
10666
|
+
const [scheduledTask] = await adapter2.getTasks([resolvedTaskId]).catch(() => []);
|
|
10667
|
+
results.push({
|
|
10668
|
+
...base,
|
|
10669
|
+
taskTitle: scheduledTask?.title,
|
|
10670
|
+
skipReason: "scheduled in this cycle \u2014 left in the cycle, conflict still surfaced"
|
|
10671
|
+
});
|
|
10436
10672
|
continue;
|
|
10437
10673
|
}
|
|
10438
10674
|
try {
|
|
@@ -10441,6 +10677,7 @@ async function parkDecisionConflicts(adapter2, conflicts, newTaskIdMap, blockedC
|
|
|
10441
10677
|
results.push({ ...base, skipReason: "task not found" });
|
|
10442
10678
|
continue;
|
|
10443
10679
|
}
|
|
10680
|
+
base.taskTitle = task.title;
|
|
10444
10681
|
if (!PARKABLE_STATUSES.has(task.status)) {
|
|
10445
10682
|
results.push({ ...base, skipReason: `status is ${task.status} \u2014 left as-is` });
|
|
10446
10683
|
continue;
|
|
@@ -10453,7 +10690,7 @@ async function parkDecisionConflicts(adapter2, conflicts, newTaskIdMap, blockedC
|
|
|
10453
10690
|
blocker: {
|
|
10454
10691
|
type: "decision-gate",
|
|
10455
10692
|
ref: conflict.adId,
|
|
10456
|
-
reason: conflict.conflict || `Contradicts ${conflict.adId} \u2014 awaiting the owner's decision.`,
|
|
10693
|
+
reason: conflict.conflict || `Contradicts ${formatRef(conflict.adId, adTitle)} \u2014 awaiting the owner's decision.`,
|
|
10457
10694
|
blockedCycle
|
|
10458
10695
|
}
|
|
10459
10696
|
});
|
|
@@ -10780,7 +11017,8 @@ async function computeResurfacedBlockers(adapter2, tasks, decisions, currentCycl
|
|
|
10780
11017
|
const resolvedLines = [];
|
|
10781
11018
|
for (const t of blocked) {
|
|
10782
11019
|
if (t.blocker && isBlockerResolved(t.blocker, ctx)) {
|
|
10783
|
-
|
|
11020
|
+
const waiting = formatBlockerWaiting(t.blocker, resolveBlockerTitle(t.blocker, ctx));
|
|
11021
|
+
resolvedLines.push(`- **${t.displayId}** (${t.title}) \u2014 ${waiting} \u2192 now RESOLVED, ready to unblock`);
|
|
10784
11022
|
}
|
|
10785
11023
|
}
|
|
10786
11024
|
return resolvedLines.length > 0 ? resolvedLines.join("\n") : void 0;
|
|
@@ -11338,6 +11576,14 @@ function applyCancellationGuard(corrections, confirmCancellations) {
|
|
|
11338
11576
|
async function transactionalWriteBack(adapter2, cycleNumber, data, contextHashes, options = {}) {
|
|
11339
11577
|
const writeBackTimer = startTimer();
|
|
11340
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
|
+
}
|
|
11341
11587
|
const skippedCancellations = [];
|
|
11342
11588
|
const cleanTitle = data.cycleLogTitle.replace(/^(?:Cycle|Session)\s+\d+\s*—\s*/i, "").trim();
|
|
11343
11589
|
const cleanContent = data.cycleLogContent.replace(/^#{1,3}\s+(?:Cycle|Session)\s+\d+\s*—[^\n]*\n*/i, "").trim();
|
|
@@ -11444,7 +11690,7 @@ ${cleanContent}`;
|
|
|
11444
11690
|
// task-2242: stable handoff join key
|
|
11445
11691
|
title: t.title,
|
|
11446
11692
|
status: t.status || "Backlog",
|
|
11447
|
-
priority: t.priority
|
|
11693
|
+
priority: normalizePriority(t.priority),
|
|
11448
11694
|
complexity: normalizeComplexity(t.complexity),
|
|
11449
11695
|
module: t.module || "Core",
|
|
11450
11696
|
epic: t.epic || "Platform",
|
|
@@ -11592,7 +11838,7 @@ ${cleanContent}`;
|
|
|
11592
11838
|
displayId: "",
|
|
11593
11839
|
title: task.title,
|
|
11594
11840
|
status: task.status || "Backlog",
|
|
11595
|
-
priority: task.priority
|
|
11841
|
+
priority: normalizePriority(task.priority),
|
|
11596
11842
|
complexity: normalizeComplexity(task.complexity),
|
|
11597
11843
|
module: task.module || "Core",
|
|
11598
11844
|
epic: task.epic || "Platform",
|
|
@@ -11894,7 +12140,7 @@ async function validateAndPrepare(adapter2, force, callerUserId, adapterType) {
|
|
|
11894
12140
|
try {
|
|
11895
12141
|
const health = await adapter2.getCycleHealth();
|
|
11896
12142
|
cycleNumber = health.totalCycles;
|
|
11897
|
-
mode = determineMode(health.totalCycles);
|
|
12143
|
+
mode = determineMode(health.projectCycleCount ?? health.totalCycles);
|
|
11898
12144
|
const blockingCycle = callerUserId ? await resolveCallerLatestCycle(adapter2, callerUserId) : void 0;
|
|
11899
12145
|
const latestStatus = callerUserId ? blockingCycle?.status : health.latestCycleStatus;
|
|
11900
12146
|
const blockingNumber = blockingCycle?.number ?? cycleNumber;
|
|
@@ -13085,6 +13331,22 @@ var planTool = {
|
|
|
13085
13331
|
]
|
|
13086
13332
|
}
|
|
13087
13333
|
};
|
|
13334
|
+
function formatDecisionConflicts(conflicts) {
|
|
13335
|
+
if (conflicts.length === 0) return [];
|
|
13336
|
+
const lines = [""];
|
|
13337
|
+
lines.push(`\u{1F500} **${conflicts.length} decision(s) need your call \u2014 work was NOT dropped.**`);
|
|
13338
|
+
lines.push("These tasks cut against a live Active Decision. An AD is *active* \u2014 it can be superseded, so the planner parked the work instead of shelving it:");
|
|
13339
|
+
for (const c of conflicts) {
|
|
13340
|
+
const ad = formatRef(c.adId, c.adTitle);
|
|
13341
|
+
const state = c.parked ? `Blocked behind ${ad}` : `still ${c.skipReason ?? "unparked"}`;
|
|
13342
|
+
lines.push(`- **${formatRef(c.resolvedTaskId, c.taskTitle)} vs ${ad}** \u2014 ${c.conflict} _(${state})_`);
|
|
13343
|
+
if (c.options.length > 0) lines.push(` Options: ${c.options.join(" | ")}`);
|
|
13344
|
+
if (c.recommendation) lines.push(` Planner recommends: ${c.recommendation}`);
|
|
13345
|
+
}
|
|
13346
|
+
lines.push("");
|
|
13347
|
+
lines.push('Decide: `strategy_change` with `mode: "capture"` to supersede or modify the AD (parked tasks auto-unblock once it moves), or `board_edit` to cancel the task if the AD stands.');
|
|
13348
|
+
return lines;
|
|
13349
|
+
}
|
|
13088
13350
|
function formatPlanResult(result) {
|
|
13089
13351
|
const modeLabel = result.mode === "bootstrap" ? "Bootstrap" : "Full";
|
|
13090
13352
|
const cycleLabel = `Cycle ${result.cycleNumber + 1}`;
|
|
@@ -13131,20 +13393,7 @@ function formatPlanResult(result) {
|
|
|
13131
13393
|
lines.push('To apply these cancellations, re-run `plan` with `mode: "apply"` and `confirm_cancellations: true`.');
|
|
13132
13394
|
lines.push("To keep these tasks, do nothing \u2014 they remain on the board.");
|
|
13133
13395
|
}
|
|
13134
|
-
|
|
13135
|
-
if (conflicts.length > 0) {
|
|
13136
|
-
lines.push("");
|
|
13137
|
-
lines.push(`\u{1F500} **${conflicts.length} decision(s) need your call \u2014 work was NOT dropped.**`);
|
|
13138
|
-
lines.push("These tasks cut against a live Active Decision. An AD is *active* \u2014 it can be superseded, so the planner parked the work instead of shelving it:");
|
|
13139
|
-
for (const c of conflicts) {
|
|
13140
|
-
const state = c.parked ? `Blocked behind ${c.adId}` : `still ${c.skipReason ?? "unparked"}`;
|
|
13141
|
-
lines.push(`- **${c.resolvedTaskId} vs ${c.adId}** \u2014 ${c.conflict} _(${state})_`);
|
|
13142
|
-
if (c.options.length > 0) lines.push(` Options: ${c.options.join(" | ")}`);
|
|
13143
|
-
if (c.recommendation) lines.push(` Planner recommends: ${c.recommendation}`);
|
|
13144
|
-
}
|
|
13145
|
-
lines.push("");
|
|
13146
|
-
lines.push('Decide: `strategy_change` with `mode: "capture"` to supersede or modify the AD (parked tasks auto-unblock once it moves), or `board_edit` to cancel the task if the AD stands.');
|
|
13147
|
-
}
|
|
13396
|
+
lines.push(...formatDecisionConflicts(result.decisionConflicts ?? []));
|
|
13148
13397
|
if (result.skipHandoffs) {
|
|
13149
13398
|
const taskCount = result.writeSummary?.taskIds.length ?? 0;
|
|
13150
13399
|
lines.push("", `Next: run \`handoff_generate\` to create BUILD HANDOFFs for your ${taskCount} cycle task(s), then \`build_list\` to start building.`);
|
|
@@ -13381,6 +13630,85 @@ ${result.userMessage}
|
|
|
13381
13630
|
}
|
|
13382
13631
|
}
|
|
13383
13632
|
|
|
13633
|
+
// src/lib/capabilities.ts
|
|
13634
|
+
init_dist();
|
|
13635
|
+
|
|
13636
|
+
// src/lib/directive-builders.ts
|
|
13637
|
+
function buildPrReviewerDirective(caps) {
|
|
13638
|
+
if (!isCapabilityEnabled(caps, "prReviewer")) return null;
|
|
13639
|
+
return `
|
|
13640
|
+
|
|
13641
|
+
**Quality Gate:** no auto-review attached. PAPI's standard pre-accept step is a code review of the branch diff \u2014 run \`review_submit\` with \`dispatch:"subagent"\` to auto-review, or attach \`auto_review\` findings. Risk-tier work (auth, data, migrations, CI) should always carry one.`;
|
|
13642
|
+
}
|
|
13643
|
+
function buildChangelogDirective(caps, inner) {
|
|
13644
|
+
if (!isCapabilityEnabled(caps, "changelog")) return null;
|
|
13645
|
+
return inner;
|
|
13646
|
+
}
|
|
13647
|
+
function buildDiscoveredIssuesDirective(caps, issueLines) {
|
|
13648
|
+
if (!isCapabilityEnabled(caps, "discoveredIssues")) return null;
|
|
13649
|
+
if (issueLines.length === 0) return null;
|
|
13650
|
+
return [
|
|
13651
|
+
"",
|
|
13652
|
+
"---",
|
|
13653
|
+
"",
|
|
13654
|
+
`## Discovered Issues (${issueLines.length})`,
|
|
13655
|
+
"",
|
|
13656
|
+
...issueLines,
|
|
13657
|
+
"",
|
|
13658
|
+
"*These issues were logged during builds \u2014 triage them in the next plan.*"
|
|
13659
|
+
].join("\n");
|
|
13660
|
+
}
|
|
13661
|
+
function buildModelRecommendationDirective(caps, tierBlock) {
|
|
13662
|
+
if (!isCapabilityEnabled(caps, "modelRecommendation")) return null;
|
|
13663
|
+
return tierBlock;
|
|
13664
|
+
}
|
|
13665
|
+
function buildVerifyHealthCheckDirective(caps) {
|
|
13666
|
+
if (!isCapabilityEnabled(caps, "verifyHealthCheck")) return null;
|
|
13667
|
+
return `
|
|
13668
|
+
|
|
13669
|
+
**Health check:** before you consider the cycle shipped, verify cycle state (plan validity, review coverage, branch hygiene) \u2014 run the \`papi-verify\` skill or a quick \`board_view\` pass.`;
|
|
13670
|
+
}
|
|
13671
|
+
function buildGestaltPreBuildDirective(caps) {
|
|
13672
|
+
if (!isCapabilityEnabled(caps, "gestaltPreBuild")) return null;
|
|
13673
|
+
return `
|
|
13674
|
+
|
|
13675
|
+
**Gestalt pre-build:** on the first task of a multi-task cycle, read every task's BUILD HANDOFF together (shared files, sequencing, module split) before building \u2014 one-time check at cycle start.`;
|
|
13676
|
+
}
|
|
13677
|
+
function buildBatchBuildRollupDirective(caps) {
|
|
13678
|
+
if (!isCapabilityEnabled(caps, "batchBuildRollup")) return null;
|
|
13679
|
+
return `
|
|
13680
|
+
|
|
13681
|
+
**Batch rollup:** after the final task of a batch, emit a cycle-level rollup \u2014 build summary table + discovered issues grouped by severity + next action.`;
|
|
13682
|
+
}
|
|
13683
|
+
function buildSecurityScanDirective(caps) {
|
|
13684
|
+
if (!isCapabilityEnabled(caps, "securityScan")) return null;
|
|
13685
|
+
return `
|
|
13686
|
+
|
|
13687
|
+
**Security scan:** risk-tier changes (auth, data, migrations, secrets, endpoints) should carry a security pass \u2014 OWASP top-10 + secret-exposure check on the changed surface before merge.`;
|
|
13688
|
+
}
|
|
13689
|
+
function buildDeployHookDirective(caps, deployCommand) {
|
|
13690
|
+
if (!isCapabilityEnabled(caps, "deployHook")) return null;
|
|
13691
|
+
const command = deployCommand?.trim() || void 0;
|
|
13692
|
+
if (!command) return null;
|
|
13693
|
+
return `
|
|
13694
|
+
|
|
13695
|
+
---
|
|
13696
|
+
|
|
13697
|
+
## Deploy \u2014 run after this release
|
|
13698
|
+
|
|
13699
|
+
The post-release deploy hook is on and a deploy command is configured, so ship the merged release now.
|
|
13700
|
+
|
|
13701
|
+
Run your deploy command yourself:
|
|
13702
|
+
\`\`\`
|
|
13703
|
+
${command}
|
|
13704
|
+
\`\`\`
|
|
13705
|
+
PAPI never runs this command itself (AD-58) \u2014 you run it in your own environment. Turn the "Post-release deploy" capability off in the dashboard, or unset PAPI_DEPLOY, to stop this reminder.`;
|
|
13706
|
+
}
|
|
13707
|
+
function buildPapiMetaFramingDirective(caps, inner) {
|
|
13708
|
+
if (!isCapabilityEnabled(caps, "papiMetaFraming")) return null;
|
|
13709
|
+
return inner;
|
|
13710
|
+
}
|
|
13711
|
+
|
|
13384
13712
|
// src/services/strategy.ts
|
|
13385
13713
|
import { randomUUID as randomUUID5, createHash as createHash4 } from "crypto";
|
|
13386
13714
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -13737,8 +14065,10 @@ ${lines.join("\n")}
|
|
|
13737
14065
|
}
|
|
13738
14066
|
let notesWithAlignment = input.notes || "";
|
|
13739
14067
|
let detectedAdIds = [];
|
|
14068
|
+
let adTitles = /* @__PURE__ */ new Map();
|
|
13740
14069
|
try {
|
|
13741
14070
|
const ads = await adapter2.getActiveDecisions();
|
|
14071
|
+
adTitles = titleMap(ads);
|
|
13742
14072
|
const ideaCombined = `${input.text} ${input.notes ?? ""}`;
|
|
13743
14073
|
const alignmentMatches = findAdAlignmentMatches(ideaCombined, ads);
|
|
13744
14074
|
if (alignmentMatches.length > 0) {
|
|
@@ -13828,7 +14158,7 @@ AD-CONFLICT: ${conflictsWithAd} \u2014 submitter flagged this as cutting against
|
|
|
13828
14158
|
blocker: {
|
|
13829
14159
|
type: "decision-gate",
|
|
13830
14160
|
ref: conflictsWithAd,
|
|
13831
|
-
reason: `Submitted as cutting against ${conflictsWithAd}. Awaiting the owner's decision.`,
|
|
14161
|
+
reason: `Submitted as cutting against ${formatRef(conflictsWithAd, lookupTitle(conflictsWithAd, adTitles))}. Awaiting the owner's decision.`,
|
|
13832
14162
|
blockedCycle: health.totalCycles
|
|
13833
14163
|
}
|
|
13834
14164
|
});
|
|
@@ -13847,21 +14177,36 @@ AD-CONFLICT: ${conflictsWithAd} \u2014 submitter flagged this as cutting against
|
|
|
13847
14177
|
routing: "task",
|
|
13848
14178
|
task,
|
|
13849
14179
|
message: `${task.id}: "${task.title}" \u2014 ${landing}${typeNote}${visibilityNote}${mismatchNote}`,
|
|
13850
|
-
...adIds.length > 0 ? {
|
|
14180
|
+
...adIds.length > 0 ? {
|
|
14181
|
+
adConflicts: {
|
|
14182
|
+
adIds,
|
|
14183
|
+
explicit: Boolean(conflictsWithAd),
|
|
14184
|
+
gated,
|
|
14185
|
+
// task-3347: id → title for exactly the ADs named above, so the tool
|
|
14186
|
+
// layer renders `AD-12 (Single board view)` without a second read.
|
|
14187
|
+
adTitles: Object.fromEntries(
|
|
14188
|
+
adIds.flatMap((id) => {
|
|
14189
|
+
const title = lookupTitle(id, adTitles);
|
|
14190
|
+
return title ? [[id, title]] : [];
|
|
14191
|
+
})
|
|
14192
|
+
)
|
|
14193
|
+
}
|
|
14194
|
+
} : {}
|
|
13851
14195
|
};
|
|
13852
14196
|
}
|
|
13853
|
-
function buildAdConflictNote(conflictsWithAd, detectedAdIds, gated) {
|
|
14197
|
+
function buildAdConflictNote(conflictsWithAd, detectedAdIds, gated, adTitles) {
|
|
13854
14198
|
if (conflictsWithAd) {
|
|
13855
|
-
const
|
|
14199
|
+
const ref = formatRef(conflictsWithAd, lookupTitle(conflictsWithAd, adTitles));
|
|
14200
|
+
const state = gated ? `Parked as **Blocked** behind a decision gate on ${ref} \u2014 it auto-unblocks once that AD is superseded, modified, or reaffirmed.` : `Recorded against ${ref} (gate could not be applied \u2014 the AD-CONFLICT note is on the task).`;
|
|
13856
14201
|
return `
|
|
13857
14202
|
|
|
13858
|
-
\u{1F500} **Conflicts with ${
|
|
14203
|
+
\u{1F500} **Conflicts with ${ref}.** ${state}
|
|
13859
14204
|
**Tell the user now** \u2014 an AD is *active*, so this is their call to make, not yours. Offer both paths: supersede the AD via \`strategy_change\`, or leave the AD standing and drop the task.`;
|
|
13860
14205
|
}
|
|
13861
14206
|
if (detectedAdIds.length > 0) {
|
|
13862
14207
|
return `
|
|
13863
14208
|
|
|
13864
|
-
\u{1F500} **Possible conflict with ${detectedAdIds
|
|
14209
|
+
\u{1F500} **Possible conflict with ${formatRefList(detectedAdIds, adTitles)}** (keyword match \u2014 verify it is real).
|
|
13865
14210
|
If it IS a real contradiction, **do not shelve the idea on those grounds**: surface it to the user and re-submit with \`conflicts_with_ad\` so the task is gated on the decision instead of quietly competing in the backlog.`;
|
|
13866
14211
|
}
|
|
13867
14212
|
return "";
|
|
@@ -14332,7 +14677,8 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
14332
14677
|
pendingRecs,
|
|
14333
14678
|
registeredDocs,
|
|
14334
14679
|
docsWithPendingActions,
|
|
14335
|
-
decisionScores
|
|
14680
|
+
decisionScores,
|
|
14681
|
+
allClosedTasks
|
|
14336
14682
|
] = await Promise.all([
|
|
14337
14683
|
adapter2.readProductBrief(),
|
|
14338
14684
|
// Strategy review needs to see retired ADs to triage/restore them as needed.
|
|
@@ -14361,7 +14707,15 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
14361
14707
|
// Doc registry — docs with pending actions for staleness audit
|
|
14362
14708
|
adapter2.searchDocs?.({ hasPendingActions: true, limit: 20 })?.catch(() => []) ?? Promise.resolve([]),
|
|
14363
14709
|
// task-2264: decision risk scores + movement, so the review can show trajectory.
|
|
14364
|
-
adapter2.getDecisionScorePatterns?.()?.catch(() => []) ?? Promise.resolve([])
|
|
14710
|
+
adapter2.getDecisionScorePatterns?.()?.catch(() => []) ?? Promise.resolve([]),
|
|
14711
|
+
// task-3331 (C363): UNWINDOWED closed-task ids, purely to exclude already-closed
|
|
14712
|
+
// tasks from the repeat-deferral detector. Deliberately NOT reusing
|
|
14713
|
+
// recentDoneTasks: that set is windowed with `cycleSince: lastReviewCycleNum`
|
|
14714
|
+
// because the surfaces it feeds (review-window comments) genuinely want the
|
|
14715
|
+
// window. Carry-forward history spans many cycles, so a task that shipped BEFORE
|
|
14716
|
+
// the window was invisible to the exclusion and kept being flagged. Compact +
|
|
14717
|
+
// Cancelled, matching what services/plan.ts already does correctly.
|
|
14718
|
+
adapter2.queryBoard({ status: ["Done", "Cancelled"], compact: true }).catch(() => [])
|
|
14365
14719
|
]);
|
|
14366
14720
|
const pendingAgendaTopics = await (adapter2.getPendingAgendaTopics?.().catch(() => []) ?? Promise.resolve([]));
|
|
14367
14721
|
const tasks = [...activeTasks, ...recentDoneTasks];
|
|
@@ -14643,7 +14997,7 @@ ${lines.join("\n")}`;
|
|
|
14643
14997
|
{ label: "docActionStaleness", hasData: docActionStalenessText !== void 0 }
|
|
14644
14998
|
]);
|
|
14645
14999
|
const doneTaskIds = new Set(
|
|
14646
|
-
|
|
15000
|
+
allClosedTasks.map((t) => t.displayId ?? t.id).filter((id) => !!id)
|
|
14647
15001
|
);
|
|
14648
15002
|
const earnedPushback = computeEarnedPushback({ reports, log: recentLog, doneTaskIds });
|
|
14649
15003
|
const context = {
|
|
@@ -14796,6 +15150,29 @@ function asDecisionBatchApplier(adapter2) {
|
|
|
14796
15150
|
const candidate = adapter2;
|
|
14797
15151
|
return typeof candidate.applyActiveDecisionUpdates === "function" ? candidate : void 0;
|
|
14798
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
|
+
}
|
|
14799
15176
|
function routeDecisionUpdate(ad, adapter2, cycleNumber, warnings) {
|
|
14800
15177
|
const action = ad.action;
|
|
14801
15178
|
let route;
|
|
@@ -15542,7 +15919,10 @@ ${cleanContent}`;
|
|
|
15542
15919
|
});
|
|
15543
15920
|
if (data.activeDecisionUpdates && data.activeDecisionUpdates.length > 0) {
|
|
15544
15921
|
for (const ad of data.activeDecisionUpdates) {
|
|
15545
|
-
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) {
|
|
15546
15926
|
await adapter2.deleteActiveDecision(ad.id);
|
|
15547
15927
|
} else if (ad.action === "new" && adapter2.upsertActiveDecision) {
|
|
15548
15928
|
const titleMatch = ad.body.match(/^###\s+\S+:\s*([^\n[]+?)(?:\s*\[|$)/m);
|
|
@@ -15792,6 +16172,35 @@ function toDateLabel(value) {
|
|
|
15792
16172
|
const d = value instanceof Date ? value : new Date(value);
|
|
15793
16173
|
return Number.isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10);
|
|
15794
16174
|
}
|
|
16175
|
+
var STRATEGY_REVIEW_CHANNEL_ENV = "DISCORD_STRATEGY_REVIEW_CHANNEL_ID";
|
|
16176
|
+
function buildStrategyReviewPostDirective(cycleNumber) {
|
|
16177
|
+
const channelId = process.env[STRATEGY_REVIEW_CHANNEL_ENV]?.trim();
|
|
16178
|
+
if (!channelId) return null;
|
|
16179
|
+
return [
|
|
16180
|
+
"",
|
|
16181
|
+
"---",
|
|
16182
|
+
"## \u{1F4E3} Post the patch notes to Discord #papi-strategy-review",
|
|
16183
|
+
"",
|
|
16184
|
+
`This review closed **Cycle ${cycleNumber}**. Post a curated, safety-gated **rich embed** (NOT plain content, so links do not unfurl) to #papi-strategy-review (id \`${channelId}\`) via \`mcp__discord__send_embed\`. Run \`/patch-notes\` \u2014 it owns the mechanics.`,
|
|
16185
|
+
"",
|
|
16186
|
+
"**LOCKED TEMPLATE \u2014 every patch note uses this exact shape (do NOT improvise the structure):**",
|
|
16187
|
+
"- `title`: `PAPI Patch Notes v<version>`",
|
|
16188
|
+
"- `description`: `Covering Cycles <start>\u2013<end>`",
|
|
16189
|
+
"- `color`: `#6C5CE7` (PAPI purple)",
|
|
16190
|
+
"- `fields`: one per non-empty category, `inline: false`, `\u2022 ` bullets \u2014 \u{1F680} New Features, \u{1F6E0} Improvements, \u{1F41B} Bug Fixes, \u2699\uFE0F Under the Hood, \u{1F52E} What's Coming (always included)",
|
|
16191
|
+
"- `footer`: `PAPI v<version> \xB7 <date>`",
|
|
16192
|
+
"",
|
|
16193
|
+
"This is the DEEP multi-cycle note covering the whole review window, not a single release. Aim for 5-7 items in the strong sections. Strip task-IDs, branch names and internal framing; only changes an existing user would notice.",
|
|
16194
|
+
"",
|
|
16195
|
+
"Then:",
|
|
16196
|
+
"1. Run the build-in-public safety gate: NO external usernames, NO contributor or private work, NO owner cost/commercial data, NO failure-rate or funnel percentages.",
|
|
16197
|
+
"2. Show the draft embed for approval BEFORE posting.",
|
|
16198
|
+
`3. On approval, send the embed to channel \`${channelId}\`.`,
|
|
16199
|
+
"4. Then run `/content-pack --source=strategy_review` \u2014 a review is the richest content source PAPI produces, and it is the cadence the Blog and Substack lanes exist for.",
|
|
16200
|
+
"",
|
|
16201
|
+
"Skip only if nothing user-facing shipped in the whole window, and say so explicitly rather than moving on silently."
|
|
16202
|
+
].join("\n");
|
|
16203
|
+
}
|
|
15795
16204
|
var reviewPrepareCache = new PerCallerCache();
|
|
15796
16205
|
var strategyReviewTool = {
|
|
15797
16206
|
name: "strategy_review",
|
|
@@ -15973,6 +16382,19 @@ ${recLines.join("\n")}
|
|
|
15973
16382
|
}
|
|
15974
16383
|
} catch {
|
|
15975
16384
|
}
|
|
16385
|
+
let caps = {};
|
|
16386
|
+
try {
|
|
16387
|
+
const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
|
|
16388
|
+
caps = info?.capabilities ?? {};
|
|
16389
|
+
} catch {
|
|
16390
|
+
caps = {};
|
|
16391
|
+
}
|
|
16392
|
+
const postDirective = buildChangelogDirective(
|
|
16393
|
+
caps,
|
|
16394
|
+
buildStrategyReviewPostDirective(result.cycleNumber)
|
|
16395
|
+
);
|
|
16396
|
+
const postStep = postDirective ? `4. Post the patch notes to Discord \u2014 see the directive at the end of this output.
|
|
16397
|
+
5. Then run \`plan\` for the next cycle.` : `4. Then run \`plan\` for the next cycle.`;
|
|
15976
16398
|
const actionReminder = `
|
|
15977
16399
|
|
|
15978
16400
|
---
|
|
@@ -15981,8 +16403,10 @@ ${recLines.join("\n")}
|
|
|
15981
16403
|
1. Review the AD changes above \u2014 are any resolved/modified ADs still referenced by active tasks?
|
|
15982
16404
|
2. Act on strategic recommendations \u2014 cancel, defer, or reprioritise tasks as suggested.
|
|
15983
16405
|
3. Run \`strategy_change\` if any recommendation requires an AD update not already captured.
|
|
15984
|
-
|
|
15985
|
-
const response = textResponse(
|
|
16406
|
+
` + postStep;
|
|
16407
|
+
const response = textResponse(
|
|
16408
|
+
header + result.displayText + slackNote + recsCloseOut + actionReminder + (postDirective ?? "")
|
|
16409
|
+
);
|
|
15986
16410
|
return {
|
|
15987
16411
|
...response,
|
|
15988
16412
|
...contextBytes !== void 0 ? { _contextBytes: contextBytes } : {},
|
|
@@ -16386,7 +16810,17 @@ var BOARD_COLUMNS = [
|
|
|
16386
16810
|
{ name: "createdAt", header: "Created", value: (t) => t.createdAt ?? "-" },
|
|
16387
16811
|
{ name: "source", header: "Source", value: (t) => t.source ?? "-" }
|
|
16388
16812
|
];
|
|
16389
|
-
var
|
|
16813
|
+
var ASSIGNEE_FIELD = "assignee";
|
|
16814
|
+
function assigneeColumn(nameOf) {
|
|
16815
|
+
return {
|
|
16816
|
+
name: ASSIGNEE_FIELD,
|
|
16817
|
+
header: "Assignee",
|
|
16818
|
+
// An unresolved id still renders as a short id rather than blank: "someone holds
|
|
16819
|
+
// this" is the fact that matters, and a blank cell reads as unclaimed.
|
|
16820
|
+
value: (t) => t.assigneeId ? nameOf.get(t.assigneeId) ?? t.assigneeId.slice(0, 8) : "\u2014"
|
|
16821
|
+
};
|
|
16822
|
+
}
|
|
16823
|
+
var BOARD_FIELDS = [...BOARD_COLUMNS.map((c) => c.name), ASSIGNEE_FIELD];
|
|
16390
16824
|
var BOARD_SINGLE_LABELS = {
|
|
16391
16825
|
id: "Task",
|
|
16392
16826
|
title: "Title",
|
|
@@ -16445,6 +16879,11 @@ var boardViewTool = {
|
|
|
16445
16879
|
enum: ["full", "summary"],
|
|
16446
16880
|
description: 'Output mode: "full" (default) shows task table, "summary" shows counts only.'
|
|
16447
16881
|
},
|
|
16882
|
+
assignee: {
|
|
16883
|
+
type: "string",
|
|
16884
|
+
enum: ["me", "unclaimed"],
|
|
16885
|
+
description: 'Filter by who holds the task on a shared project. "me" shows only your own claims; "unclaimed" shows the open pool nobody has taken. Omit to see everything. On a multi-member project the board also gains an Assignee column showing who holds each task.'
|
|
16886
|
+
},
|
|
16448
16887
|
fields: {
|
|
16449
16888
|
type: "string",
|
|
16450
16889
|
description: `Optional sparse-field projection \u2014 comma-separated column names, e.g. "title,status". Returns ONLY those columns (id is always included so rows stay addressable) which is far cheaper on session context than the full table. Allowed: ${BOARD_FIELDS.join(", ")}. An unknown name is an error.`
|
|
@@ -16657,11 +17096,16 @@ function formatSingleTask(t, fields) {
|
|
|
16657
17096
|
if (t.why?.trim()) lines.push(`- **Why:** ${t.why.trim()}`);
|
|
16658
17097
|
return lines.join("\n");
|
|
16659
17098
|
}
|
|
16660
|
-
function formatBoard(result, fields) {
|
|
17099
|
+
function formatBoard(result, fields, assignee) {
|
|
16661
17100
|
if (result.tasks.length === 0) {
|
|
16662
17101
|
return "No tasks found.";
|
|
16663
17102
|
}
|
|
16664
|
-
const
|
|
17103
|
+
const available = assignee ? [
|
|
17104
|
+
...BOARD_COLUMNS.slice(0, 4),
|
|
17105
|
+
assignee,
|
|
17106
|
+
...BOARD_COLUMNS.slice(4)
|
|
17107
|
+
] : BOARD_COLUMNS;
|
|
17108
|
+
const columns = selectColumns(available, fields);
|
|
16665
17109
|
const headers = columns.map((c) => c.header);
|
|
16666
17110
|
const rows = result.tasks.map((t) => columns.map((c) => c.value(t)));
|
|
16667
17111
|
const lines = [];
|
|
@@ -16690,6 +17134,38 @@ function formatSummary(summary) {
|
|
|
16690
17134
|
}
|
|
16691
17135
|
return lines.join("\n");
|
|
16692
17136
|
}
|
|
17137
|
+
async function resolveBoardMembership(adapter2) {
|
|
17138
|
+
const empty = { isMultiMember: false, nameOf: /* @__PURE__ */ new Map(), callerUserId: null };
|
|
17139
|
+
if (!adapterSupports(adapter2, "listContributors")) return empty;
|
|
17140
|
+
let contributors;
|
|
17141
|
+
try {
|
|
17142
|
+
contributors = await adapter2.listContributors();
|
|
17143
|
+
} catch {
|
|
17144
|
+
return empty;
|
|
17145
|
+
}
|
|
17146
|
+
if (contributors.length <= 1) return empty;
|
|
17147
|
+
const nameOf = new Map(
|
|
17148
|
+
contributors.map((c) => [c.userId, c.displayName || c.email || c.userId.slice(0, 8)])
|
|
17149
|
+
);
|
|
17150
|
+
let callerUserId = null;
|
|
17151
|
+
try {
|
|
17152
|
+
if (adapterSupports(adapter2, "getOwnerIdentity")) {
|
|
17153
|
+
callerUserId = (await adapter2.getOwnerIdentity()).callerUserId ?? null;
|
|
17154
|
+
} else if (adapterSupports(adapter2, "getProjectOwnerUserId")) {
|
|
17155
|
+
callerUserId = await adapter2.getProjectOwnerUserId();
|
|
17156
|
+
}
|
|
17157
|
+
} catch {
|
|
17158
|
+
callerUserId = null;
|
|
17159
|
+
}
|
|
17160
|
+
return { isMultiMember: true, nameOf, callerUserId };
|
|
17161
|
+
}
|
|
17162
|
+
function applyAssigneeFilter(result, filter, callerUserId) {
|
|
17163
|
+
if (!filter) return result;
|
|
17164
|
+
const tasks = result.tasks.filter(
|
|
17165
|
+
(t) => filter === "unclaimed" ? !t.assigneeId : t.assigneeId === callerUserId
|
|
17166
|
+
);
|
|
17167
|
+
return { ...result, tasks, returned: tasks.length, total: tasks.length, hasMore: false };
|
|
17168
|
+
}
|
|
16693
17169
|
async function handleBoardView(adapter2, args) {
|
|
16694
17170
|
const mode = args.mode;
|
|
16695
17171
|
const projection = resolveFields(args.fields, BOARD_FIELDS, "board_view");
|
|
@@ -16708,6 +17184,18 @@ async function handleBoardView(adapter2, args) {
|
|
|
16708
17184
|
const meta2 = emitMeta ? { total: 1, filtered: 1, returned: 1, limit: null, offset: 0 } : null;
|
|
16709
17185
|
return textResponse(withMeta(formatSingleTask(task, fields), meta2));
|
|
16710
17186
|
}
|
|
17187
|
+
const membership = await resolveBoardMembership(adapter2);
|
|
17188
|
+
const assigneeFilterRaw = typeof args.assignee === "string" ? args.assignee.trim().toLowerCase() : void 0;
|
|
17189
|
+
if (assigneeFilterRaw !== void 0 && assigneeFilterRaw !== "me" && assigneeFilterRaw !== "unclaimed") {
|
|
17190
|
+
return errorResponse(
|
|
17191
|
+
`Unknown assignee filter "${args.assignee}". Use assignee="me" for your own claims, or assignee="unclaimed" for the open pool.`
|
|
17192
|
+
);
|
|
17193
|
+
}
|
|
17194
|
+
if (assigneeFilterRaw === "me" && !membership.callerUserId) {
|
|
17195
|
+
return errorResponse(
|
|
17196
|
+
'assignee="me" needs an identified caller, and this connection has none. Use assignee="unclaimed" to see the open pool.'
|
|
17197
|
+
);
|
|
17198
|
+
}
|
|
16711
17199
|
const result = await viewBoard(adapter2, void 0, {
|
|
16712
17200
|
phase: args.phase,
|
|
16713
17201
|
status: args.status,
|
|
@@ -16716,7 +17204,12 @@ async function handleBoardView(adapter2, args) {
|
|
|
16716
17204
|
query: args.query,
|
|
16717
17205
|
cycle: args.cycle
|
|
16718
17206
|
});
|
|
16719
|
-
|
|
17207
|
+
const filtered = applyAssigneeFilter(result, assigneeFilterRaw, membership.callerUserId);
|
|
17208
|
+
let output = formatBoard(
|
|
17209
|
+
filtered,
|
|
17210
|
+
fields,
|
|
17211
|
+
membership.isMultiMember ? assigneeColumn(membership.nameOf) : null
|
|
17212
|
+
);
|
|
16720
17213
|
if (!fields) {
|
|
16721
17214
|
try {
|
|
16722
17215
|
const comments = await adapter2.getRecentTaskComments?.(30);
|
|
@@ -16943,11 +17436,13 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
16943
17436
|
} else {
|
|
16944
17437
|
const health = await target.getCycleHealth().catch(() => null);
|
|
16945
17438
|
const activeCycle = health?.totalCycles ?? null;
|
|
16946
|
-
const
|
|
16947
|
-
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]`;
|
|
16948
17441
|
const entry = `${stamp} ${trimmed}`;
|
|
16949
17442
|
updates.notes = existing.trim().length > 0 ? `${entry}
|
|
16950
17443
|
|
|
17444
|
+
---
|
|
17445
|
+
|
|
16951
17446
|
${existing}` : entry;
|
|
16952
17447
|
}
|
|
16953
17448
|
}
|
|
@@ -18658,6 +19153,118 @@ async function ensureMcpJsonGitignored(projectRoot) {
|
|
|
18658
19153
|
}
|
|
18659
19154
|
}
|
|
18660
19155
|
|
|
19156
|
+
// src/tools/join.ts
|
|
19157
|
+
var joinTool = {
|
|
19158
|
+
name: "join",
|
|
19159
|
+
description: "Join a project that already exists, as a new team member. Run this instead of `setup` when someone else already set the project up \u2014 setup is for creating a project, and running it as a joiner writes scaffolding into a repo that already has it. `join` confirms which project you are bound to, shows the Active Decisions and canon docs you are expected to read, and shows the task pool you can claim from. It writes NOTHING to your repo. Does not call the Anthropic API.",
|
|
19160
|
+
annotations: { title: "Join Project", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
19161
|
+
inputSchema: {
|
|
19162
|
+
type: "object",
|
|
19163
|
+
properties: {
|
|
19164
|
+
limit: {
|
|
19165
|
+
type: "integer",
|
|
19166
|
+
description: "Maximum claimable tasks to list (default 10)."
|
|
19167
|
+
}
|
|
19168
|
+
},
|
|
19169
|
+
required: []
|
|
19170
|
+
}
|
|
19171
|
+
};
|
|
19172
|
+
function formatDecisions(decisions) {
|
|
19173
|
+
if (decisions.length === 0) {
|
|
19174
|
+
return "_No Active Decisions recorded yet._";
|
|
19175
|
+
}
|
|
19176
|
+
const shown = decisions.slice(0, 15);
|
|
19177
|
+
const lines = shown.map((d) => `- **${d.id}** \u2014 ${d.title ?? "(untitled)"}${d.confidence ? ` _(${d.confidence})_` : ""}`);
|
|
19178
|
+
const more = decisions.length > shown.length ? `
|
|
19179
|
+
- _\u2026and ${decisions.length - shown.length} more. Run \`ad_view\` for the full set._` : "";
|
|
19180
|
+
return lines.join("\n") + more;
|
|
19181
|
+
}
|
|
19182
|
+
function formatDocs(docs) {
|
|
19183
|
+
if (docs.length === 0) {
|
|
19184
|
+
return "_No registered canon docs. Ask the project owner what to read._";
|
|
19185
|
+
}
|
|
19186
|
+
return docs.slice(0, 12).map((d) => `- \`${d.path}\`${d.title ? ` \u2014 ${d.title}` : ""}`).join("\n");
|
|
19187
|
+
}
|
|
19188
|
+
function formatPool(tasks, limit) {
|
|
19189
|
+
if (tasks.length === 0) {
|
|
19190
|
+
return "_The shared backlog is empty right now \u2014 ask the owner what to pick up._";
|
|
19191
|
+
}
|
|
19192
|
+
const rows = tasks.slice(0, limit).map(
|
|
19193
|
+
(t) => `| ${t.id} | ${t.title} | ${t.priority ?? "-"} | ${t.complexity ?? "-"} |`
|
|
19194
|
+
);
|
|
19195
|
+
const more = tasks.length > limit ? `
|
|
19196
|
+
|
|
19197
|
+
_\u2026and ${tasks.length - limit} more in the pool._` : "";
|
|
19198
|
+
return `| Task | Summary | Priority | Effort |
|
|
19199
|
+
|---|---|---|---|
|
|
19200
|
+
${rows.join("\n")}${more}`;
|
|
19201
|
+
}
|
|
19202
|
+
var JOIN_POINTER = "Run `join` instead \u2014 it shows you the project, its Active Decisions, the docs to read, and the tasks you can claim, without writing anything to the repo.";
|
|
19203
|
+
async function handleJoin(adapter2, config2, args) {
|
|
19204
|
+
const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 10;
|
|
19205
|
+
let projectLabel = "this project";
|
|
19206
|
+
try {
|
|
19207
|
+
const brief = adapterSupports(adapter2, "readProductBrief") ? await adapter2.readProductBrief() : "";
|
|
19208
|
+
const heading = brief.match(/^#\s+(.+)$/m)?.[1]?.trim();
|
|
19209
|
+
if (heading) projectLabel = heading;
|
|
19210
|
+
} catch {
|
|
19211
|
+
}
|
|
19212
|
+
const gate = await resolveOwnerGate(adapter2, config2);
|
|
19213
|
+
const [decisions, docs, pool] = await Promise.all([
|
|
19214
|
+
adapter2.getActiveDecisions().catch(() => []),
|
|
19215
|
+
// SECURITY: visibility scoping is delegated to searchDocs' requesterUserId
|
|
19216
|
+
// (task-1977) rather than reading rows directly — a contributors-tier or
|
|
19217
|
+
// private doc must not become readable just because someone joined. Passing
|
|
19218
|
+
// the caller means a non-owner sees public docs plus the contributors-tier
|
|
19219
|
+
// docs they are actually a cohort member of; private is excluded fail-closed.
|
|
19220
|
+
adapterSupports(adapter2, "searchDocs") ? adapter2.searchDocs({ requesterUserId: gate.callerUserId ?? void 0, limit: 12 }).catch(() => []) : Promise.resolve([]),
|
|
19221
|
+
// The claimable pool is the shared backlog: unassigned AND not yet pulled into
|
|
19222
|
+
// anyone's cycle. Same invariant task_claim enforces (assigneeId == null &&
|
|
19223
|
+
// cycle == null), so nothing is listed here that a claim would then refuse.
|
|
19224
|
+
adapter2.queryBoard({ status: ["Backlog"] }).catch(() => [])
|
|
19225
|
+
]);
|
|
19226
|
+
const claimable = pool.filter((t) => !t.assigneeId && t.cycle == null);
|
|
19227
|
+
let conventionLine = "";
|
|
19228
|
+
try {
|
|
19229
|
+
if (adapterSupports(adapter2, "listConventions")) {
|
|
19230
|
+
const conventions = await adapter2.listConventions();
|
|
19231
|
+
if (conventions.length > 0) {
|
|
19232
|
+
conventionLine = `
|
|
19233
|
+
|
|
19234
|
+
## Conventions that ride every build
|
|
19235
|
+
` + conventions.slice(0, 8).map((c) => `- ${c.rule}`).join("\n");
|
|
19236
|
+
}
|
|
19237
|
+
}
|
|
19238
|
+
} catch {
|
|
19239
|
+
}
|
|
19240
|
+
const roleLine = gate.callerIsOwner ? `You are the **owner** of this project.` : `You are a **member** of this project.`;
|
|
19241
|
+
return textResponse(
|
|
19242
|
+
`# Joined \u2014 ${projectLabel}
|
|
19243
|
+
|
|
19244
|
+
${roleLine} Nothing has been written to your repo \u2014 \`join\` is read-only, and this project's harness files are already committed by whoever set it up.
|
|
19245
|
+
|
|
19246
|
+
**If that is not the project you expected, stop here.** Your connection is bound to the wrong project; fix \`PAPI_PROJECT_ID\` (local) or the \`x-papi-project-id\` header (remote), or run \`project_switch\`, before writing anything.
|
|
19247
|
+
|
|
19248
|
+
## Decisions already made
|
|
19249
|
+
These are settled stances, not suggestions. Read them before proposing a different approach \u2014 if you disagree with one, say so rather than working around it.
|
|
19250
|
+
|
|
19251
|
+
${formatDecisions(decisions)}
|
|
19252
|
+
|
|
19253
|
+
## Canon docs to read
|
|
19254
|
+
${formatDocs(docs)}` + conventionLine + `
|
|
19255
|
+
|
|
19256
|
+
## Work you can claim
|
|
19257
|
+
${claimable.length} task(s) in the shared backlog. Claim one with \`task_claim <task-id>\` \u2014 it becomes yours and shows against your name on the board.
|
|
19258
|
+
|
|
19259
|
+
${formatPool(claimable, limit)}
|
|
19260
|
+
|
|
19261
|
+
## Your next move
|
|
19262
|
+
Run \`orient\` to see where the project stands and what your own cycle looks like. You get your OWN cycle numbering \u2014 your cycle 1 is independent of everyone else's.
|
|
19263
|
+
|
|
19264
|
+
Do NOT run \`setup\`. It is for creating a project, and this one already exists.`
|
|
19265
|
+
);
|
|
19266
|
+
}
|
|
19267
|
+
|
|
18661
19268
|
// src/tools/setup.ts
|
|
18662
19269
|
var setupTool = {
|
|
18663
19270
|
name: "setup",
|
|
@@ -18875,8 +19482,36 @@ function formatSetupNarration(result) {
|
|
|
18875
19482
|
...steps.map((s, i) => `${i + 1}. ${s}`)
|
|
18876
19483
|
];
|
|
18877
19484
|
}
|
|
19485
|
+
async function refuseIfJoiner(adapter2, config2) {
|
|
19486
|
+
let gate;
|
|
19487
|
+
try {
|
|
19488
|
+
gate = await resolveOwnerGate(adapter2, config2);
|
|
19489
|
+
} catch {
|
|
19490
|
+
return null;
|
|
19491
|
+
}
|
|
19492
|
+
if (!gate.enforced || gate.callerIsOwner) return null;
|
|
19493
|
+
if (gate.resolutionError || !gate.callerUserId) return null;
|
|
19494
|
+
let alreadySetUp = false;
|
|
19495
|
+
try {
|
|
19496
|
+
if (adapterSupports(adapter2, "readProductBrief")) {
|
|
19497
|
+
alreadySetUp = (await adapter2.readProductBrief()).trim().length > 0;
|
|
19498
|
+
}
|
|
19499
|
+
} catch {
|
|
19500
|
+
return null;
|
|
19501
|
+
}
|
|
19502
|
+
if (!alreadySetUp) return null;
|
|
19503
|
+
return `This project is already set up, and you are joining it rather than creating it.
|
|
19504
|
+
|
|
19505
|
+
\`setup\` writes the project harness \u2014 AGENTS.md, CLAUDE.md, .claude/settings.json, docs/ \u2014 into your repo. Those files are already here and committed by whoever set the project up, so running setup would rewrite shared files and record "first" Active Decisions into a registry that already has them.
|
|
19506
|
+
|
|
19507
|
+
${JOIN_POINTER}
|
|
19508
|
+
|
|
19509
|
+
If you genuinely need to re-scaffold this project, ask its owner to run setup.`;
|
|
19510
|
+
}
|
|
18878
19511
|
async function handleSetup(adapter2, config2, args, clientName) {
|
|
18879
19512
|
const toolMode = args.mode;
|
|
19513
|
+
const joinerRefusal = await refuseIfJoiner(adapter2, config2);
|
|
19514
|
+
if (joinerRefusal) return errorResponse(joinerRefusal);
|
|
18880
19515
|
const REQUIRED_FIELDS = ["project_name"];
|
|
18881
19516
|
const missing = REQUIRED_FIELDS.filter((f) => !args[f] || typeof args[f] === "string" && !args[f].trim());
|
|
18882
19517
|
if (missing.length > 0) {
|
|
@@ -19154,89 +19789,10 @@ Examples (use whichever provider you have): ${spec.examples}.
|
|
|
19154
19789
|
_Recommendation only \u2014 PAPI never selects or runs a model. Policy: XS/S \u2192 cheap, M/L \u2192 capable, XL \u2192 frontier._`;
|
|
19155
19790
|
}
|
|
19156
19791
|
|
|
19157
|
-
// src/lib/capabilities.ts
|
|
19158
|
-
init_dist();
|
|
19159
|
-
|
|
19160
19792
|
// src/tools/build.ts
|
|
19161
19793
|
init_dist();
|
|
19162
19794
|
init_dist();
|
|
19163
19795
|
|
|
19164
|
-
// src/lib/directive-builders.ts
|
|
19165
|
-
function buildPrReviewerDirective(caps) {
|
|
19166
|
-
if (!isCapabilityEnabled(caps, "prReviewer")) return null;
|
|
19167
|
-
return `
|
|
19168
|
-
|
|
19169
|
-
**Quality Gate:** no auto-review attached. PAPI's standard pre-accept step is a code review of the branch diff \u2014 run \`review_submit\` with \`dispatch:"subagent"\` to auto-review, or attach \`auto_review\` findings. Risk-tier work (auth, data, migrations, CI) should always carry one.`;
|
|
19170
|
-
}
|
|
19171
|
-
function buildChangelogDirective(caps, inner) {
|
|
19172
|
-
if (!isCapabilityEnabled(caps, "changelog")) return null;
|
|
19173
|
-
return inner;
|
|
19174
|
-
}
|
|
19175
|
-
function buildDiscoveredIssuesDirective(caps, issueLines) {
|
|
19176
|
-
if (!isCapabilityEnabled(caps, "discoveredIssues")) return null;
|
|
19177
|
-
if (issueLines.length === 0) return null;
|
|
19178
|
-
return [
|
|
19179
|
-
"",
|
|
19180
|
-
"---",
|
|
19181
|
-
"",
|
|
19182
|
-
`## Discovered Issues (${issueLines.length})`,
|
|
19183
|
-
"",
|
|
19184
|
-
...issueLines,
|
|
19185
|
-
"",
|
|
19186
|
-
"*These issues were logged during builds \u2014 triage them in the next plan.*"
|
|
19187
|
-
].join("\n");
|
|
19188
|
-
}
|
|
19189
|
-
function buildModelRecommendationDirective(caps, tierBlock) {
|
|
19190
|
-
if (!isCapabilityEnabled(caps, "modelRecommendation")) return null;
|
|
19191
|
-
return tierBlock;
|
|
19192
|
-
}
|
|
19193
|
-
function buildVerifyHealthCheckDirective(caps) {
|
|
19194
|
-
if (!isCapabilityEnabled(caps, "verifyHealthCheck")) return null;
|
|
19195
|
-
return `
|
|
19196
|
-
|
|
19197
|
-
**Health check:** before you consider the cycle shipped, verify cycle state (plan validity, review coverage, branch hygiene) \u2014 run the \`papi-verify\` skill or a quick \`board_view\` pass.`;
|
|
19198
|
-
}
|
|
19199
|
-
function buildGestaltPreBuildDirective(caps) {
|
|
19200
|
-
if (!isCapabilityEnabled(caps, "gestaltPreBuild")) return null;
|
|
19201
|
-
return `
|
|
19202
|
-
|
|
19203
|
-
**Gestalt pre-build:** on the first task of a multi-task cycle, read every task's BUILD HANDOFF together (shared files, sequencing, module split) before building \u2014 one-time check at cycle start.`;
|
|
19204
|
-
}
|
|
19205
|
-
function buildBatchBuildRollupDirective(caps) {
|
|
19206
|
-
if (!isCapabilityEnabled(caps, "batchBuildRollup")) return null;
|
|
19207
|
-
return `
|
|
19208
|
-
|
|
19209
|
-
**Batch rollup:** after the final task of a batch, emit a cycle-level rollup \u2014 build summary table + discovered issues grouped by severity + next action.`;
|
|
19210
|
-
}
|
|
19211
|
-
function buildSecurityScanDirective(caps) {
|
|
19212
|
-
if (!isCapabilityEnabled(caps, "securityScan")) return null;
|
|
19213
|
-
return `
|
|
19214
|
-
|
|
19215
|
-
**Security scan:** risk-tier changes (auth, data, migrations, secrets, endpoints) should carry a security pass \u2014 OWASP top-10 + secret-exposure check on the changed surface before merge.`;
|
|
19216
|
-
}
|
|
19217
|
-
function buildDeployHookDirective(caps, deployCommand) {
|
|
19218
|
-
if (!isCapabilityEnabled(caps, "deployHook")) return null;
|
|
19219
|
-
const command = deployCommand?.trim() || void 0;
|
|
19220
|
-
if (!command) return null;
|
|
19221
|
-
return `
|
|
19222
|
-
|
|
19223
|
-
---
|
|
19224
|
-
|
|
19225
|
-
## Deploy \u2014 run after this release
|
|
19226
|
-
|
|
19227
|
-
The post-release deploy hook is on and a deploy command is configured, so ship the merged release now.
|
|
19228
|
-
|
|
19229
|
-
Run your deploy command yourself:
|
|
19230
|
-
\`\`\`
|
|
19231
|
-
${command}
|
|
19232
|
-
\`\`\`
|
|
19233
|
-
PAPI never runs this command itself (AD-58) \u2014 you run it in your own environment. Turn the "Post-release deploy" capability off in the dashboard, or unset PAPI_DEPLOY, to stop this reminder.`;
|
|
19234
|
-
}
|
|
19235
|
-
function buildPapiMetaFramingDirective(caps, inner) {
|
|
19236
|
-
if (!isCapabilityEnabled(caps, "papiMetaFraming")) return null;
|
|
19237
|
-
return inner;
|
|
19238
|
-
}
|
|
19239
|
-
|
|
19240
19796
|
// src/services/build.ts
|
|
19241
19797
|
init_dist();
|
|
19242
19798
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
@@ -19716,6 +20272,47 @@ To override, pass force=true (emits a telemetry warning).`
|
|
|
19716
20272
|
}
|
|
19717
20273
|
return { resolvedCycleNum, warnings, force, skipVersion };
|
|
19718
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
|
+
}
|
|
19719
20316
|
async function contributorAutoPrRelease(config2, adapter2, version, productionBaseBranch, options) {
|
|
19720
20317
|
if (!isGhAvailable()) {
|
|
19721
20318
|
throw new Error(
|
|
@@ -20234,8 +20831,18 @@ async function postReleaseToX(version, cycleClosed) {
|
|
|
20234
20831
|
}
|
|
20235
20832
|
function buildHostedReleaseOutput(params) {
|
|
20236
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;
|
|
20237
20845
|
const tagAnnotation = `Release ${version}`;
|
|
20238
|
-
const titleSuffix = skipVersion ? " (skip version)" : "";
|
|
20239
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";
|
|
20240
20847
|
const commands = skipVersion ? `git checkout ${branch}
|
|
20241
20848
|
git pull
|
|
@@ -20252,17 +20859,13 @@ git -c user.name="Your Name" -c user.email="you@example.com" tag -a ${version} -
|
|
|
20252
20859
|
\`\`\`
|
|
20253
20860
|
|
|
20254
20861
|
`;
|
|
20255
|
-
return
|
|
20256
|
-
|
|
20257
|
-
${cyclePart} is now marked **complete** in PAPI's database, so \`orient\` will no longer flag "Release has not been run."
|
|
20258
|
-
` + warningsBlock + `
|
|
20259
|
-
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}.
|
|
20260
20863
|
|
|
20261
20864
|
Finish the release locally:
|
|
20262
20865
|
\`\`\`
|
|
20263
20866
|
` + commands + `\`\`\`
|
|
20264
20867
|
|
|
20265
|
-
` + identityBlock
|
|
20868
|
+
` + identityBlock.trimEnd();
|
|
20266
20869
|
}
|
|
20267
20870
|
var releaseTool = {
|
|
20268
20871
|
name: "release",
|
|
@@ -20350,7 +20953,16 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
20350
20953
|
await beginRelease(tracker, cycleToClose);
|
|
20351
20954
|
const productionBaseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
20352
20955
|
if (branch === productionBaseBranch) {
|
|
20353
|
-
|
|
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) {
|
|
20354
20966
|
const resolutionNote = gate.resolutionError ? `
|
|
20355
20967
|
|
|
20356
20968
|
Identity resolution failed (${gate.resolutionError}) \u2014 the gate fails closed. Retry once connectivity is restored.` : "";
|
|
@@ -20371,13 +20983,62 @@ Then reconnect your AI tool and run release again. (Direct/pg setups read identi
|
|
|
20371
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.`;
|
|
20372
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;
|
|
20373
20985
|
return errorResponse(
|
|
20374
|
-
`Release to ${productionBaseBranch}
|
|
20986
|
+
`Release to ${productionBaseBranch} needs an owner or editor role.
|
|
20375
20987
|
|
|
20376
20988
|
` + roleNote + `
|
|
20377
20989
|
|
|
20378
20990
|
Releasing to a non-base branch (e.g. dev) is unaffected.` + resolutionNote
|
|
20379
20991
|
);
|
|
20380
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
|
+
}
|
|
20381
21042
|
tracker.mark("contributor-auto-pr");
|
|
20382
21043
|
try {
|
|
20383
21044
|
const cr = await contributorAutoPrRelease(config2, adapter2, version, productionBaseBranch, {
|
|
@@ -20393,9 +21054,9 @@ Releasing to a non-base branch (e.g. dev) is unaffected.` + resolutionNote
|
|
|
20393
21054
|
\u26A0\uFE0F Warnings: ${cr.warnings.join("; ")}
|
|
20394
21055
|
` : "";
|
|
20395
21056
|
return textResponse(
|
|
20396
|
-
`## Release ${version} \u2014 PR opened
|
|
21057
|
+
`## Release ${version} \u2014 contributor PR opened
|
|
20397
21058
|
|
|
20398
|
-
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.
|
|
20399
21060
|
|
|
20400
21061
|
**Pull request(s):**
|
|
20401
21062
|
${prLines.join("\n")}
|
|
@@ -20405,7 +21066,7 @@ ${failLines.join("\n")}
|
|
|
20405
21066
|
` : "") + warnBlock + `
|
|
20406
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.
|
|
20407
21068
|
|
|
20408
|
-
|
|
21069
|
+
After the PR is merged, run \`release\` again so PAPI can reconcile it.`
|
|
20409
21070
|
);
|
|
20410
21071
|
} catch (err) {
|
|
20411
21072
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
@@ -21335,6 +21996,20 @@ async function safeUpdateBlocker(adapter2, taskId, blocker) {
|
|
|
21335
21996
|
} catch {
|
|
21336
21997
|
}
|
|
21337
21998
|
}
|
|
21999
|
+
async function resolveMemberBranchSlug(adapter2) {
|
|
22000
|
+
if (!adapterSupports(adapter2, "listContributors")) return void 0;
|
|
22001
|
+
try {
|
|
22002
|
+
const members = await adapter2.listContributors();
|
|
22003
|
+
if (members.length <= 1) return void 0;
|
|
22004
|
+
const callerUserId = adapterSupports(adapter2, "getOwnerIdentity") ? (await adapter2.getOwnerIdentity()).callerUserId ?? null : adapterSupports(adapter2, "getProjectOwnerUserId") ? await adapter2.getProjectOwnerUserId() : null;
|
|
22005
|
+
if (!callerUserId) return void 0;
|
|
22006
|
+
const me = members.find((m) => m.userId === callerUserId);
|
|
22007
|
+
if (!me) return void 0;
|
|
22008
|
+
return memberBranchSlug({ displayName: me.displayName, email: me.email, userId: me.userId });
|
|
22009
|
+
} catch {
|
|
22010
|
+
return void 0;
|
|
22011
|
+
}
|
|
22012
|
+
}
|
|
21338
22013
|
async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
21339
22014
|
const task = await adapter2.getTask(taskId);
|
|
21340
22015
|
if (!task) {
|
|
@@ -21437,13 +22112,14 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
21437
22112
|
}
|
|
21438
22113
|
const inFlightTaskBranch = !depBranchReuse && branchExists(config2.projectRoot, taskBranchName(taskId)) ? taskBranchName(taskId) : void 0;
|
|
21439
22114
|
const useSharedBranch = !depBranchReuse && !inFlightTaskBranch && !!task.module && cycleNumber > 0;
|
|
22115
|
+
const memberSlug = useSharedBranch ? await resolveMemberBranchSlug(adapter2) : void 0;
|
|
21440
22116
|
let originCycleBranch;
|
|
21441
22117
|
if (useSharedBranch && hasRemote(config2.projectRoot)) {
|
|
21442
22118
|
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
21443
22119
|
const remoteCycleBranches = listGroupedCycleBranches(config2.projectRoot, cycleNumber, baseBranch);
|
|
21444
|
-
originCycleBranch = pickModuleCycleBranch(remoteCycleBranches, cycleNumber, task.module);
|
|
22120
|
+
originCycleBranch = pickModuleCycleBranch(remoteCycleBranches, cycleNumber, task.module, memberSlug);
|
|
21445
22121
|
}
|
|
21446
|
-
const featureBranch = depBranchReuse ? depBranchReuse.branch : inFlightTaskBranch ? inFlightTaskBranch : useSharedBranch ? originCycleBranch ?? cycleBranchName(cycleNumber, task.module) : taskBranchName(taskId);
|
|
22122
|
+
const featureBranch = depBranchReuse ? depBranchReuse.branch : inFlightTaskBranch ? inFlightTaskBranch : useSharedBranch ? originCycleBranch ?? cycleBranchName(cycleNumber, task.module, memberSlug) : taskBranchName(taskId);
|
|
21447
22123
|
if (depBranchReuse) {
|
|
21448
22124
|
branchLines.push(
|
|
21449
22125
|
`Reusing branch '${depBranchReuse.branch}' from dependency ${depBranchReuse.upstreamId} \u2014 commits will stack for a single PR.`
|
|
@@ -22779,8 +23455,19 @@ function isPaidTier(tier) {
|
|
|
22779
23455
|
}
|
|
22780
23456
|
async function enforceProjectCap(adapter2, target) {
|
|
22781
23457
|
if (isSelfHostedDeployment(process.env.PAPI_SELF_HOST)) return null;
|
|
22782
|
-
|
|
22783
|
-
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;
|
|
22784
23471
|
if (!adapterSupports(adapter2, "listUserProjects")) return null;
|
|
22785
23472
|
let projects;
|
|
22786
23473
|
try {
|
|
@@ -22792,17 +23479,20 @@ async function enforceProjectCap(adapter2, target) {
|
|
|
22792
23479
|
(p) => target.papiDir && p.papi_dir && p.papi_dir === target.papiDir || target.name && p.name && p.name.trim().toLowerCase() === target.name.trim().toLowerCase()
|
|
22793
23480
|
);
|
|
22794
23481
|
if (matchesExisting) return null;
|
|
22795
|
-
if (projects.length >=
|
|
23482
|
+
if (projects.length >= effectiveLimit) {
|
|
23483
|
+
return projectCapMessage(projects.length, effectiveLimit, entitlement.tier);
|
|
23484
|
+
}
|
|
22796
23485
|
return null;
|
|
22797
23486
|
}
|
|
22798
23487
|
async function resolveContributorUpsell(adapter2) {
|
|
22799
23488
|
const tier = await resolveTier(adapter2);
|
|
22800
23489
|
return evaluateContributorGate(tier).upsell ?? null;
|
|
22801
23490
|
}
|
|
22802
|
-
function projectCapMessage(currentCount) {
|
|
22803
|
-
|
|
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).**
|
|
22804
23494
|
|
|
22805
|
-
|
|
23495
|
+
Your account currently covers up to ${limit} projects. To run more, ${nextStep} at a flat monthly price (no usage bills): ${PRICING_URL}
|
|
22806
23496
|
|
|
22807
23497
|
Your existing projects are untouched, and you can keep working in any of them.`;
|
|
22808
23498
|
}
|
|
@@ -22981,14 +23671,24 @@ Diagnostic JSON:
|
|
|
22981
23671
|
${JSON.stringify(payload, null, 2)}`
|
|
22982
23672
|
);
|
|
22983
23673
|
}
|
|
22984
|
-
function ensureDocDurable(path7, projectRoot) {
|
|
23674
|
+
function ensureDocDurable(path7, projectRoot, bodyStored = false) {
|
|
22985
23675
|
if (!hasLocalWorkspace() || !projectRoot) return "";
|
|
22986
|
-
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 `
|
|
22987
23682
|
|
|
22988
23683
|
\u26A0\uFE0F **Registered, but NOT durable.** ${reason}
|
|
22989
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.
|
|
22990
23685
|
**Fix:** ${fix}`;
|
|
23686
|
+
};
|
|
22991
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
|
+
}
|
|
22992
23692
|
return warn(
|
|
22993
23693
|
`No file exists at \`${path7}\`.`,
|
|
22994
23694
|
`write the doc body to that path, then re-run doc_register.`
|
|
@@ -23005,7 +23705,11 @@ The registry stores metadata and a summary \u2014 not the body. This doc has one
|
|
|
23005
23705
|
if (isPathIgnored(projectRoot, path7)) {
|
|
23006
23706
|
return warn(
|
|
23007
23707
|
`\`${path7}\` is excluded by .gitignore, so it cannot be committed (docs/private/ is ignored by design).`,
|
|
23008
|
-
|
|
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.`
|
|
23009
23713
|
);
|
|
23010
23714
|
}
|
|
23011
23715
|
const result = commitSinglePath(projectRoot, path7, `docs: register ${path7}`);
|
|
@@ -23098,6 +23802,7 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23098
23802
|
visibility
|
|
23099
23803
|
});
|
|
23100
23804
|
let bodyNote = "";
|
|
23805
|
+
let bodyIsStored = false;
|
|
23101
23806
|
try {
|
|
23102
23807
|
const supplied = typeof args.body === "string" ? args.body : void 0;
|
|
23103
23808
|
let body = supplied;
|
|
@@ -23128,6 +23833,7 @@ ${decision.message}`;
|
|
|
23128
23833
|
visibility: entry.visibility ?? visibility,
|
|
23129
23834
|
ownerUserId: entry.ownerUserId
|
|
23130
23835
|
});
|
|
23836
|
+
bodyIsStored = true;
|
|
23131
23837
|
bodyNote = result.stored ? `
|
|
23132
23838
|
- **Body stored:** ${result.byteSize.toLocaleString()} bytes` : "\n- **Body:** unchanged since last registration";
|
|
23133
23839
|
}
|
|
@@ -23138,7 +23844,7 @@ ${decision.message}`;
|
|
|
23138
23844
|
const visibilityLabel = entry.visibility === "contributors" ? "team member" : entry.visibility ?? "private";
|
|
23139
23845
|
let durability = "";
|
|
23140
23846
|
try {
|
|
23141
|
-
durability = ensureDocDurable(entry.path, config2?.projectRoot);
|
|
23847
|
+
durability = ensureDocDurable(entry.path, config2?.projectRoot, bodyIsStored);
|
|
23142
23848
|
} catch {
|
|
23143
23849
|
durability = "";
|
|
23144
23850
|
}
|
|
@@ -23855,19 +24561,106 @@ function formatListItem(task) {
|
|
|
23855
24561
|
}
|
|
23856
24562
|
return base;
|
|
23857
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
|
+
]);
|
|
23858
24647
|
function filterRelevantADs(ads, task) {
|
|
24648
|
+
const live = ads.filter((ad) => !ad.superseded);
|
|
23859
24649
|
const keywords = [];
|
|
23860
24650
|
if (task.module) keywords.push(task.module.toLowerCase());
|
|
23861
24651
|
if (task.epic) keywords.push(task.epic.toLowerCase());
|
|
23862
24652
|
if (task.phase) keywords.push(task.phase.toLowerCase());
|
|
23863
|
-
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));
|
|
23864
24654
|
keywords.push(...titleWords);
|
|
23865
|
-
|
|
23866
|
-
return ads.filter((ad) => {
|
|
23867
|
-
if (ad.superseded) return false;
|
|
24655
|
+
const matches = keywords.length === 0 ? [] : live.filter((ad) => {
|
|
23868
24656
|
const text = `${ad.title} ${ad.body}`.toLowerCase();
|
|
23869
24657
|
return keywords.some((kw) => text.includes(kw));
|
|
23870
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 };
|
|
23871
24664
|
}
|
|
23872
24665
|
async function getModuleContext(adapter2, task) {
|
|
23873
24666
|
if (!task.module) return "";
|
|
@@ -23893,9 +24686,18 @@ async function getModuleContext(adapter2, task) {
|
|
|
23893
24686
|
return "";
|
|
23894
24687
|
}
|
|
23895
24688
|
}
|
|
23896
|
-
function formatRelevantADs(ads) {
|
|
23897
|
-
if (ads.length === 0) return "";
|
|
24689
|
+
function formatRelevantADs(ads, stats) {
|
|
24690
|
+
if (ads.length === 0 && !stats) return "";
|
|
23898
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
|
+
}
|
|
23899
24701
|
for (const ad of ads) {
|
|
23900
24702
|
const bodyLines = ad.body.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
23901
24703
|
const summary = bodyLines[0]?.trim().slice(0, 120) ?? "";
|
|
@@ -24076,7 +24878,7 @@ If >80% of the scope is already implemented, call \`build_execute\` with complet
|
|
|
24076
24878
|
try {
|
|
24077
24879
|
const allADs = await adapter2.getActiveDecisions();
|
|
24078
24880
|
const relevant = filterRelevantADs(allADs, result.task);
|
|
24079
|
-
adSection = formatRelevantADs(relevant);
|
|
24881
|
+
adSection = formatRelevantADs(relevant.ads, { matched: relevant.matched, total: relevant.total });
|
|
24080
24882
|
} catch {
|
|
24081
24883
|
}
|
|
24082
24884
|
let dogfoodSection = "";
|
|
@@ -24749,7 +25551,10 @@ Re-submit with \`notes: "... Reference: <path>"\` to link one, or ignore if none
|
|
|
24749
25551
|
const conflictNote = result.adConflicts ? buildAdConflictNote(
|
|
24750
25552
|
result.adConflicts.explicit ? result.adConflicts.adIds[0] : void 0,
|
|
24751
25553
|
result.adConflicts.explicit ? [] : result.adConflicts.adIds,
|
|
24752
|
-
result.adConflicts.gated
|
|
25554
|
+
result.adConflicts.gated,
|
|
25555
|
+
// task-3347: resolved alongside the ids, so the directive names the
|
|
25556
|
+
// decision the user is being asked to rule on.
|
|
25557
|
+
new Map(Object.entries(result.adConflicts.adTitles ?? {}))
|
|
24753
25558
|
) : "";
|
|
24754
25559
|
return textResponse(`${result.message}${overrideNote}${branchNote}${truncateWarning}${refNudge}${conflictNote}`);
|
|
24755
25560
|
}
|
|
@@ -25617,6 +26422,21 @@ async function recordAdHoc(adapter2, input) {
|
|
|
25617
26422
|
var VALID_EFFORTS = ["XS", "S", "M", "L", "XL"];
|
|
25618
26423
|
var VALID_PRIORITIES = ["P0 Critical", "P1 High", "P2 Medium", "P3 Low"];
|
|
25619
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
|
+
}
|
|
25620
26440
|
function inferTaskType(description) {
|
|
25621
26441
|
const lower = description.toLowerCase();
|
|
25622
26442
|
if (/\bfix\b|bug|error|crash|broken|regression|defect/.test(lower)) return "bug";
|
|
@@ -25843,12 +26663,12 @@ The task is recorded **In Review** and pinned to **Cycle ${result.task.cycle}**,
|
|
|
25843
26663
|
\`\`\`
|
|
25844
26664
|
2. Leave the branch **unmerged** \u2014 it is picked up by the next cycle's \`release\`.
|
|
25845
26665
|
|
|
25846
|
-
_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)
|
|
25847
26667
|
);
|
|
25848
26668
|
}
|
|
25849
26669
|
return textResponse(
|
|
25850
26670
|
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule})${overrideNote}.${truncateWarning}${promoNote} Build report attached.
|
|
25851
|
-
_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)
|
|
25852
26672
|
);
|
|
25853
26673
|
}
|
|
25854
26674
|
|
|
@@ -27222,14 +28042,23 @@ ${overlap}`;
|
|
|
27222
28042
|
const allTasks = await adapter2.queryBoard();
|
|
27223
28043
|
const cycleTasks = allTasks.filter((t) => t.cycle === result.currentCycle);
|
|
27224
28044
|
const gate = await resolveOwnerGate(adapter2, config2);
|
|
27225
|
-
|
|
27226
|
-
|
|
27227
|
-
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.)` : "";
|
|
27228
28057
|
autoReleaseNote = `
|
|
27229
28058
|
|
|
27230
28059
|
---
|
|
27231
28060
|
|
|
27232
|
-
\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}`;
|
|
27233
28062
|
} else if (cycleTasks.length > 0 && cycleTasks.every((t) => t.status === "Done")) {
|
|
27234
28063
|
let planRunCount = null;
|
|
27235
28064
|
if (adapterSupports(adapter2, "countPlanRunsForCycle")) {
|
|
@@ -27483,7 +28312,7 @@ import { access as access2, readFile as readFile5, writeFile as writeFile3 } fro
|
|
|
27483
28312
|
import path5 from "path";
|
|
27484
28313
|
var initTool = {
|
|
27485
28314
|
name: "init",
|
|
27486
|
-
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.",
|
|
27487
28316
|
annotations: { title: "Initialise Project", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
27488
28317
|
inputSchema: {
|
|
27489
28318
|
type: "object",
|
|
@@ -27972,6 +28801,29 @@ ${writeNote}
|
|
|
27972
28801
|
return textResponse(output);
|
|
27973
28802
|
}
|
|
27974
28803
|
|
|
28804
|
+
// src/lib/workspace-mode.ts
|
|
28805
|
+
init_git();
|
|
28806
|
+
var cache3 = /* @__PURE__ */ new Map();
|
|
28807
|
+
function resolveWorkspaceMode(projectRoot) {
|
|
28808
|
+
const cached2 = cache3.get(projectRoot);
|
|
28809
|
+
if (cached2) return cached2;
|
|
28810
|
+
let result;
|
|
28811
|
+
if (!isGitAvailable()) {
|
|
28812
|
+
result = { mode: "no-git", gitBacked: false, reason: "git-not-installed" };
|
|
28813
|
+
} else if (!isGitRepo(projectRoot)) {
|
|
28814
|
+
result = { mode: "no-git", gitBacked: false, reason: "not-a-repo" };
|
|
28815
|
+
} else {
|
|
28816
|
+
result = { mode: "git", gitBacked: true, reason: null };
|
|
28817
|
+
}
|
|
28818
|
+
cache3.set(projectRoot, result);
|
|
28819
|
+
return result;
|
|
28820
|
+
}
|
|
28821
|
+
function formatWorkspaceModeLine(result) {
|
|
28822
|
+
if (result.gitBacked) return void 0;
|
|
28823
|
+
const because = result.reason === "git-not-installed" ? "no version control was detected on this machine" : "this folder is not under version control";
|
|
28824
|
+
return `**Mode:** running without version control \u2014 ${because}. The full loop works: plan, build, review and release all run normally, and your work is saved to PAPI either way. Version control would add branch-per-task, change history and pull-request review on top.`;
|
|
28825
|
+
}
|
|
28826
|
+
|
|
27975
28827
|
// src/services/health.ts
|
|
27976
28828
|
function computeZoomOutWarning(cycleNumber, lastZoomOutCycle) {
|
|
27977
28829
|
if (cycleNumber <= 0) return "";
|
|
@@ -28309,15 +29161,16 @@ async function findUnblockCandidates(adapter2, currentCycle) {
|
|
|
28309
29161
|
const blocker = blocked.blocker;
|
|
28310
29162
|
if (!blocker) continue;
|
|
28311
29163
|
const resolved = isBlockerResolved(blocker, ctx);
|
|
29164
|
+
const blockerTitle = resolveBlockerTitle(blocker, ctx) ?? "";
|
|
28312
29165
|
candidates.push({
|
|
28313
29166
|
blockedId: blocked.displayId ?? blocked.id,
|
|
28314
29167
|
blockedTitle: blocked.title,
|
|
28315
29168
|
blockerId: blocker.ref,
|
|
28316
|
-
blockerTitle
|
|
29169
|
+
blockerTitle,
|
|
28317
29170
|
blockerCycle: blocker.blockedCycle,
|
|
28318
29171
|
typed: {
|
|
28319
29172
|
kind: blocker.type,
|
|
28320
|
-
waiting: formatBlockerWaiting(blocker),
|
|
29173
|
+
waiting: formatBlockerWaiting(blocker, blockerTitle),
|
|
28321
29174
|
resolved
|
|
28322
29175
|
}
|
|
28323
29176
|
});
|
|
@@ -28383,9 +29236,9 @@ function formatUnblockSection(candidates) {
|
|
|
28383
29236
|
lines.push("");
|
|
28384
29237
|
for (const c of resolved) {
|
|
28385
29238
|
if (c.typed) {
|
|
28386
|
-
lines.push(`- **${c.blockedId}** (${c.blockedTitle}) \u2014 ${c.typed.kind}: ${c.blockerId} now resolved \u2705`);
|
|
29239
|
+
lines.push(`- **${c.blockedId}** (${c.blockedTitle}) \u2014 ${c.typed.kind}: ${formatRef(c.blockerId, c.blockerTitle)} now resolved \u2705`);
|
|
28387
29240
|
} else {
|
|
28388
|
-
lines.push(`- **${c.blockedId}** (${c.blockedTitle}) \u2014 blocker **${c.blockerId}** Done in C${c.blockerCycle}`);
|
|
29241
|
+
lines.push(`- **${c.blockedId}** (${c.blockedTitle}) \u2014 blocker **${formatRef(c.blockerId, c.blockerTitle)}** Done in C${c.blockerCycle}`);
|
|
28389
29242
|
}
|
|
28390
29243
|
}
|
|
28391
29244
|
}
|
|
@@ -28903,6 +29756,13 @@ var orientTool = {
|
|
|
28903
29756
|
full: {
|
|
28904
29757
|
type: "boolean",
|
|
28905
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."
|
|
28906
29766
|
}
|
|
28907
29767
|
},
|
|
28908
29768
|
required: []
|
|
@@ -29005,6 +29865,13 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
29005
29865
|
lines.push(`**Connection:** ${statusIcon} ${statusLabel}`);
|
|
29006
29866
|
lines.push("");
|
|
29007
29867
|
}
|
|
29868
|
+
if (projectRoot && hasLocalWorkspace()) {
|
|
29869
|
+
const modeLine = formatWorkspaceModeLine(resolveWorkspaceMode(projectRoot));
|
|
29870
|
+
if (modeLine) {
|
|
29871
|
+
lines.push(modeLine);
|
|
29872
|
+
lines.push("");
|
|
29873
|
+
}
|
|
29874
|
+
}
|
|
29008
29875
|
const harness = detectHarness(clientName);
|
|
29009
29876
|
const roleNote = connectOnlyRoleNote(harness);
|
|
29010
29877
|
if (roleNote) {
|
|
@@ -29349,6 +30216,42 @@ async function computeTeamSummary(adapter2, contributorsInput) {
|
|
|
29349
30216
|
const reviewQueue = tasks.filter((t) => t.status === "In Review").length;
|
|
29350
30217
|
return `**Team:** ${members} members \xB7 ${pool} in pool \xB7 ${inFlight} in flight \xB7 ${reviewQueue} in review`;
|
|
29351
30218
|
}
|
|
30219
|
+
async function computeCohortVisibility(adapter2, config2, contributorsInput) {
|
|
30220
|
+
if (!adapterSupports(adapter2, "listContributors")) return void 0;
|
|
30221
|
+
let members;
|
|
30222
|
+
try {
|
|
30223
|
+
members = (await (contributorsInput ?? adapter2.listContributors())).length;
|
|
30224
|
+
} catch {
|
|
30225
|
+
return void 0;
|
|
30226
|
+
}
|
|
30227
|
+
if (members <= 1) return void 0;
|
|
30228
|
+
let gate;
|
|
30229
|
+
try {
|
|
30230
|
+
gate = await resolveOwnerGate(adapter2, config2);
|
|
30231
|
+
} catch {
|
|
30232
|
+
return void 0;
|
|
30233
|
+
}
|
|
30234
|
+
const cycleGate = await resolveCycleGate(adapter2, gate);
|
|
30235
|
+
if (!cycleGate.allowed) return void 0;
|
|
30236
|
+
let tasks;
|
|
30237
|
+
try {
|
|
30238
|
+
tasks = await adapter2.queryBoard({ compact: true });
|
|
30239
|
+
} catch {
|
|
30240
|
+
return void 0;
|
|
30241
|
+
}
|
|
30242
|
+
const open = tasks.filter(
|
|
30243
|
+
(t) => t.status !== "Done" && t.status !== "Cancelled" && t.status !== "Archived"
|
|
30244
|
+
);
|
|
30245
|
+
if (open.length === 0) return void 0;
|
|
30246
|
+
const publicCount = open.filter((t) => !t.visibility || t.visibility === "public").length;
|
|
30247
|
+
const contributorCount = publicCount + open.filter((t) => t.visibility === "contributors").length;
|
|
30248
|
+
const hiddenFromContributors = open.length - contributorCount;
|
|
30249
|
+
if (hiddenFromContributors === 0) {
|
|
30250
|
+
return `**Cohort visibility:** your ${members - 1} teammate(s) can see the whole active board (${open.length} tasks).`;
|
|
30251
|
+
}
|
|
30252
|
+
const pct = Math.round(hiddenFromContributors / open.length * 100);
|
|
30253
|
+
return `**Cohort visibility:** of ${open.length} active tasks, a contributor sees ${contributorCount} and a public-tier viewer sees ${publicCount} \u2014 **${hiddenFromContributors} (${pct}%) are private and invisible to your team.**`;
|
|
30254
|
+
}
|
|
29352
30255
|
async function computeReleaseHistory(adapter2, contributorsInput) {
|
|
29353
30256
|
if (!adapterSupports(adapter2, "listContributors")) return void 0;
|
|
29354
30257
|
let contributors;
|
|
@@ -29412,7 +30315,21 @@ function withBoardMemo(adapter2) {
|
|
|
29412
30315
|
});
|
|
29413
30316
|
}
|
|
29414
30317
|
async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVersion = "unknown") {
|
|
29415
|
-
|
|
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);
|
|
29416
30333
|
const environment = normaliseEnvironment(args.environment);
|
|
29417
30334
|
const deepHousekeeping = args.deep_housekeeping === true;
|
|
29418
30335
|
const fullEnrichment = args.full === true || deepHousekeeping;
|
|
@@ -29654,7 +30571,7 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
|
|
|
29654
30571
|
return adWords.some((w) => docText.includes(w));
|
|
29655
30572
|
});
|
|
29656
30573
|
const cycleLabel = `C${doc.cycleUpdated ?? doc.cycleCreated}`;
|
|
29657
|
-
const adRef = relatedAds.length > 0 ? ` \u2014 may relate to ${relatedAds.map((a) => a.displayId).join(", ")}` : "";
|
|
30574
|
+
const adRef = relatedAds.length > 0 ? ` \u2014 may relate to ${relatedAds.map((a) => formatRef(a.displayId, a.title)).join(", ")}` : "";
|
|
29658
30575
|
const pendingActions = doc.actions?.filter((a) => a.status === "pending") ?? [];
|
|
29659
30576
|
lines.push(`- **${doc.title}** [${cycleLabel}${adRef}]`);
|
|
29660
30577
|
for (const action of pendingActions.slice(0, 2)) {
|
|
@@ -29761,8 +30678,8 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
|
|
|
29761
30678
|
const mismatches = detectBoardMismatches(config2.projectRoot, allTasks);
|
|
29762
30679
|
if (mismatches.codeAhead.length > 0 || mismatches.staleInProgress.length > 0) {
|
|
29763
30680
|
const lines = ["\n\n## Reconciliation"];
|
|
29764
|
-
for (const m of mismatches.codeAhead) lines.push(`\u26A0\uFE0F **${m.displayId}** \u2014 branch \`${m.branch}\` merged to main but task is still Backlog. Run \`board_edit\` to mark Done.`);
|
|
29765
|
-
for (const m of mismatches.staleInProgress) lines.push(`\u26A0\uFE0F **${m.displayId}** \u2014 In Progress but no feature branch found. May be stale or built without \`build_execute\`.`);
|
|
30681
|
+
for (const m of mismatches.codeAhead) lines.push(`\u26A0\uFE0F **${formatRef(m.displayId, m.title)}** \u2014 branch \`${m.branch}\` merged to main but task is still Backlog. Run \`board_edit\` to mark Done.`);
|
|
30682
|
+
for (const m of mismatches.staleInProgress) lines.push(`\u26A0\uFE0F **${formatRef(m.displayId, m.title)}** \u2014 In Progress but no feature branch found. May be stale or built without \`build_execute\`.`);
|
|
29766
30683
|
reconciliationNote2 = lines.join("\n");
|
|
29767
30684
|
}
|
|
29768
30685
|
} catch {
|
|
@@ -29776,7 +30693,7 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
|
|
|
29776
30693
|
lines.push(`${merged.length} In-Progress task(s) already merged to the default branch \u2014 close them out with \`build_execute\` complete (or \`board_edit\` to mark Done):`);
|
|
29777
30694
|
for (const m of merged) {
|
|
29778
30695
|
const ref = m.pr ? `${m.pr} (\`${m.commit}\`)` : `\`${m.commit}\``;
|
|
29779
|
-
lines.push(`- \u26A0\uFE0F **${m.displayId}** \u2014 merged via ${ref}: ${m.subject}`);
|
|
30696
|
+
lines.push(`- \u26A0\uFE0F **${formatRef(m.displayId, m.title)}** \u2014 merged via ${ref}: ${m.subject}`);
|
|
29780
30697
|
}
|
|
29781
30698
|
mergedInProgressNote2 = lines.join("\n");
|
|
29782
30699
|
}
|
|
@@ -29909,7 +30826,7 @@ ${versionDrift}` : "";
|
|
|
29909
30826
|
}
|
|
29910
30827
|
tracker.mark("parallel-tail");
|
|
29911
30828
|
const sharedContributorsPromise = adapterSupports(adapter2, "listContributors") ? adapter2.listContributors().catch(() => []) : Promise.resolve([]);
|
|
29912
|
-
const [unblockCandidates, subAgents, teamSummaryLine, releaseHistoryLine, carryForwardRefs] = await Promise.all([
|
|
30829
|
+
const [unblockCandidates, subAgents, teamSummaryLine, releaseHistoryLine, cohortVisibilityLine, carryForwardRefs] = await Promise.all([
|
|
29913
30830
|
tracked("unblock-candidates", () => findUnblockCandidates(adapter2, currentCycle))().catch(() => []),
|
|
29914
30831
|
// task-1866: discover project sub-agents for the orient surface (read-only, never throws).
|
|
29915
30832
|
listAgents(config2.projectRoot),
|
|
@@ -29918,6 +30835,8 @@ ${versionDrift}` : "";
|
|
|
29918
30835
|
// byte-identical there.
|
|
29919
30836
|
tracked("team-summary", () => computeTeamSummary(adapter2, sharedContributorsPromise))().catch(() => void 0),
|
|
29920
30837
|
tracked("release-history", () => computeReleaseHistory(adapter2, sharedContributorsPromise))().catch(() => void 0),
|
|
30838
|
+
// task-3339 (C363): owner-only readout of how much of the board the cohort cannot see.
|
|
30839
|
+
tracked("cohort-visibility", () => computeCohortVisibility(adapter2, config2, sharedContributorsPromise))().catch(() => void 0),
|
|
29921
30840
|
// task-2751 (C332): resolve every task-NNNN mentioned in the Carry-Forward
|
|
29922
30841
|
// prose to its title so orient can name it inline. Built from the board
|
|
29923
30842
|
// already in hand — no extra query.
|
|
@@ -29927,7 +30846,7 @@ ${versionDrift}` : "";
|
|
|
29927
30846
|
const unblockNote = unblockSection ? `
|
|
29928
30847
|
|
|
29929
30848
|
${unblockSection}` : "";
|
|
29930
|
-
const teamSummary = [teamSummaryLine, releaseHistoryLine].filter(Boolean).join("\n") || void 0;
|
|
30849
|
+
const teamSummary = [teamSummaryLine, releaseHistoryLine, cohortVisibilityLine].filter(Boolean).join("\n") || void 0;
|
|
29931
30850
|
let deferredGateNote = "";
|
|
29932
30851
|
if (deepHousekeeping) {
|
|
29933
30852
|
try {
|
|
@@ -29957,7 +30876,7 @@ ${section}`;
|
|
|
29957
30876
|
const runtimeIdentityNote = `
|
|
29958
30877
|
|
|
29959
30878
|
${formatRuntimeIdentity(getRuntimeIdentity(config2, serverVersion))}`;
|
|
29960
|
-
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 };
|
|
29961
30880
|
} catch (err) {
|
|
29962
30881
|
const message = err instanceof Error ? err.message : String(err);
|
|
29963
30882
|
const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
|
|
@@ -31059,15 +31978,35 @@ function readLlmResponse(args) {
|
|
|
31059
31978
|
}
|
|
31060
31979
|
|
|
31061
31980
|
// src/tools/ad-view.ts
|
|
31062
|
-
|
|
31063
|
-
|
|
31064
|
-
|
|
31065
|
-
|
|
31066
|
-
|
|
31067
|
-
|
|
31068
|
-
|
|
31069
|
-
|
|
31070
|
-
|
|
31981
|
+
function adColumns(all) {
|
|
31982
|
+
return [
|
|
31983
|
+
{ name: "id", header: "AD", value: (d) => d.id },
|
|
31984
|
+
{ name: "title", header: "Title", value: (d) => d.title },
|
|
31985
|
+
{ name: "confidence", header: "Confidence", value: (d) => d.confidence },
|
|
31986
|
+
{ name: "superseded", header: "Superseded", value: (d) => d.superseded ? "yes" : "no" },
|
|
31987
|
+
{
|
|
31988
|
+
name: "supersededBy",
|
|
31989
|
+
header: "Superseded By",
|
|
31990
|
+
value: (d) => d.supersededBy ? formatEntityRef(d.supersededBy, all) : "-"
|
|
31991
|
+
},
|
|
31992
|
+
{ name: "body", header: "Body", value: (d) => d.body.replace(/\s+/g, " ").trim() }
|
|
31993
|
+
];
|
|
31994
|
+
}
|
|
31995
|
+
var AD_FIELDS = adColumns([]).map((c) => c.name);
|
|
31996
|
+
function supersededNote(d, all) {
|
|
31997
|
+
if (!d.superseded) return "";
|
|
31998
|
+
const by = d.supersededBy;
|
|
31999
|
+
if (!by) return " [SUPERSEDED by unknown]";
|
|
32000
|
+
return ` [SUPERSEDED by ${formatEntityRef(by, all)}]`;
|
|
32001
|
+
}
|
|
32002
|
+
function formatAdBody(d, all) {
|
|
32003
|
+
return `## ${d.id}: ${d.title} [${d.confidence}]${supersededNote(d, all)}
|
|
32004
|
+
|
|
32005
|
+
${d.body}`;
|
|
32006
|
+
}
|
|
32007
|
+
function formatAdList(decisions, all) {
|
|
32008
|
+
return decisions.map((d) => formatAdBody(d, all)).join("\n\n---\n\n");
|
|
32009
|
+
}
|
|
31071
32010
|
var adViewTool = {
|
|
31072
32011
|
name: "ad_view",
|
|
31073
32012
|
description: "View one or all Active Decisions with full bodies. Use when you need to read the complete reasoning and evidence behind a specific AD before running strategy_change.",
|
|
@@ -31130,16 +32069,11 @@ async function handleAdView(adapter2, args) {
|
|
|
31130
32069
|
}
|
|
31131
32070
|
const meta2 = emitMeta ? { total: decisions.length, filtered: 1, returned: 1, limit: null, offset: 0 } : null;
|
|
31132
32071
|
if (fields) {
|
|
31133
|
-
const columns = selectColumns(
|
|
32072
|
+
const columns = selectColumns(adColumns(decisions), fields);
|
|
31134
32073
|
const body = columns.map((c) => `- **${c.header}:** ${c.value(target)}`).join("\n");
|
|
31135
32074
|
return textResponse(withMeta(body, meta2));
|
|
31136
32075
|
}
|
|
31137
|
-
|
|
31138
|
-
return textResponse(
|
|
31139
|
-
withMeta(`## ${target.id}: ${target.title} [${target.confidence}]${supersededNote}
|
|
31140
|
-
|
|
31141
|
-
${target.body}`, meta2)
|
|
31142
|
-
);
|
|
32076
|
+
return textResponse(withMeta(formatAdBody(target, decisions), meta2));
|
|
31143
32077
|
}
|
|
31144
32078
|
const filtered = includeSuperseded ? decisions : decisions.filter((d) => !d.superseded);
|
|
31145
32079
|
const paged = limit === null && offset === 0 ? filtered : filtered.slice(offset, limit === null ? void 0 : offset + limit);
|
|
@@ -31148,16 +32082,11 @@ ${target.body}`, meta2)
|
|
|
31148
32082
|
return textResponse(withMeta("No active decisions found.", meta));
|
|
31149
32083
|
}
|
|
31150
32084
|
if (fields) {
|
|
31151
|
-
const columns = selectColumns(
|
|
32085
|
+
const columns = selectColumns(adColumns(decisions), fields);
|
|
31152
32086
|
const rows = paged.map((d) => columns.map((c) => c.value(d)));
|
|
31153
32087
|
return textResponse(withMeta(renderTable(columns.map((c) => c.header), rows), meta));
|
|
31154
32088
|
}
|
|
31155
|
-
const formatted = paged
|
|
31156
|
-
const supersededNote = d.superseded ? ` [SUPERSEDED by ${d.supersededBy}]` : "";
|
|
31157
|
-
return `## ${d.id}: ${d.title} [${d.confidence}]${supersededNote}
|
|
31158
|
-
|
|
31159
|
-
${d.body}`;
|
|
31160
|
-
}).join("\n\n---\n\n");
|
|
32089
|
+
const formatted = formatAdList(paged, decisions);
|
|
31161
32090
|
return textResponse(withMeta(`# Active Decisions (${paged.length})
|
|
31162
32091
|
|
|
31163
32092
|
${formatted}`, meta));
|
|
@@ -31285,7 +32214,10 @@ async function handleLearningAction(adapter2, args) {
|
|
|
31285
32214
|
if (!learningId?.trim()) return errorResponse('learning_id is required for mode "mark".');
|
|
31286
32215
|
if (!actionTaken) return errorResponse('action_taken is required for mode "mark".');
|
|
31287
32216
|
try {
|
|
31288
|
-
await adapter2.updateCycleLearningActionRef(learningId.trim(), actionRef?.trim() ?? actionTaken
|
|
32217
|
+
await adapter2.updateCycleLearningActionRef(learningId.trim(), actionRef?.trim() ?? actionTaken, {
|
|
32218
|
+
actionTaken,
|
|
32219
|
+
force: true
|
|
32220
|
+
});
|
|
31289
32221
|
return textResponse(
|
|
31290
32222
|
`Learning \`${learningId}\` marked as **${actionTaken}**${actionRef ? ` \u2192 \`${actionRef}\`` : ""}.`
|
|
31291
32223
|
);
|
|
@@ -31478,14 +32410,24 @@ On hosted/remote sessions this is now your DEFAULT project \u2014 calls without
|
|
|
31478
32410
|
}
|
|
31479
32411
|
|
|
31480
32412
|
// src/tools/contributor.ts
|
|
32413
|
+
init_dist();
|
|
31481
32414
|
var contributorAddTool = {
|
|
31482
32415
|
name: "contributor_add",
|
|
31483
|
-
description:
|
|
32416
|
+
description: `Add a contributor to a project by email, or change an existing member's role (owner-only). The person must already have a PAPI account. Grants membership on project_contributors. Pass role="editor" for a working teammate who needs to run the full cycle \u2014 the default, "viewer", cannot release. Calling it again with a different role promotes or demotes that member.`,
|
|
31484
32417
|
annotations: { title: "Add Contributor", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
31485
32418
|
inputSchema: {
|
|
31486
32419
|
type: "object",
|
|
31487
32420
|
properties: {
|
|
31488
|
-
email: { type: "string", description: 'Email of the PAPI account to add (e.g. "wes@example.com").' }
|
|
32421
|
+
email: { type: "string", description: 'Email of the PAPI account to add (e.g. "wes@example.com").' },
|
|
32422
|
+
role: {
|
|
32423
|
+
type: "string",
|
|
32424
|
+
enum: [...ASSIGNABLE_CONTRIBUTOR_ROLES],
|
|
32425
|
+
description: `Membership role. "editor" can run the full build/review/release cycle; "viewer" has read-only visibility and is REFUSED by the release gate. Defaults to "viewer" for a new member; omit it when changing nothing to leave an existing member's role untouched. Project ownership cannot be granted here.`
|
|
32426
|
+
},
|
|
32427
|
+
project: {
|
|
32428
|
+
type: "string",
|
|
32429
|
+
description: "Project id (UUID) or slug to add this contributor to, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. Does NOT change your default project."
|
|
32430
|
+
}
|
|
31489
32431
|
},
|
|
31490
32432
|
required: ["email"]
|
|
31491
32433
|
}
|
|
@@ -31550,21 +32492,37 @@ function requireEmail(args) {
|
|
|
31550
32492
|
return EMAIL_SHAPE.test(email) ? email : null;
|
|
31551
32493
|
}
|
|
31552
32494
|
async function handleContributorAdd(adapter2, config2, args) {
|
|
31553
|
-
const denied = await denyUnlessOwner(adapter2, config2);
|
|
31554
|
-
if (denied) return errorResponse(denied);
|
|
31555
32495
|
const email = requireEmail(args);
|
|
31556
32496
|
if (!email) return errorResponse('A valid email is required. Example: contributor_add email="wes@example.com"');
|
|
32497
|
+
const rawRole = typeof args.role === "string" ? args.role.trim().toLowerCase() : void 0;
|
|
32498
|
+
if (rawRole !== void 0 && !ASSIGNABLE_CONTRIBUTOR_ROLES.includes(rawRole)) {
|
|
32499
|
+
return errorResponse(
|
|
32500
|
+
`Invalid role "${args.role}". Assignable roles: ${ASSIGNABLE_CONTRIBUTOR_ROLES.join(", ")}. Project ownership cannot be granted through contributor_add.`
|
|
32501
|
+
);
|
|
32502
|
+
}
|
|
32503
|
+
let target = adapter2;
|
|
32504
|
+
let overrideNote = "";
|
|
31557
32505
|
try {
|
|
31558
|
-
|
|
32506
|
+
({ adapter: target, overrideNote } = await resolvePerCallProjectAdapter(adapter2, args));
|
|
32507
|
+
} catch (err) {
|
|
32508
|
+
if (err instanceof ProjectResolutionError) return errorResponse(err.message);
|
|
32509
|
+
throw err;
|
|
32510
|
+
}
|
|
32511
|
+
const denied = await denyUnlessOwner(target, config2);
|
|
32512
|
+
if (denied) return errorResponse(denied);
|
|
32513
|
+
try {
|
|
32514
|
+
const entry = await target.addContributorByEmail(email, rawRole);
|
|
31559
32515
|
const name = entry.displayName ? ` (${entry.displayName})` : "";
|
|
31560
|
-
const upsell = await resolveContributorUpsell(
|
|
32516
|
+
const upsell = await resolveContributorUpsell(target);
|
|
31561
32517
|
const upsellSuffix = upsell ? `
|
|
31562
32518
|
|
|
31563
32519
|
${upsell}` : "";
|
|
32520
|
+
const where = overrideNote ? ` ${overrideNote}` : " on this project";
|
|
32521
|
+
const roleLine = entry.role === "editor" ? `They can run the full cycle \u2014 plan, build, review and release.` : `**They are a "${entry.role}" and cannot run \`release\`.** Re-run with role="editor" if they are a working teammate rather than an observer.`;
|
|
31564
32522
|
return textResponse(
|
|
31565
|
-
`\u2705
|
|
32523
|
+
`\u2705 **${entry.email ?? email}**${name} is now a **${entry.role}**${where}.
|
|
31566
32524
|
|
|
31567
|
-
|
|
32525
|
+
` + roleLine + upsellSuffix
|
|
31568
32526
|
);
|
|
31569
32527
|
} catch (err) {
|
|
31570
32528
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
@@ -32069,12 +33027,13 @@ async function readPapiResource(adapter2, uri) {
|
|
|
32069
33027
|
const ads = await adapter2.getActiveDecisions({ includeRetired: true });
|
|
32070
33028
|
const target = ads.find((d) => d.id === adId);
|
|
32071
33029
|
if (!target) throw new Error(`Active Decision not found in this project: ${adId}`);
|
|
32072
|
-
const
|
|
33030
|
+
const titles = titleMap(ads);
|
|
33031
|
+
const supersededNote2 = target.superseded ? ` [SUPERSEDED by ${target.supersededBy ? formatRef(target.supersededBy, lookupTitle(target.supersededBy, titles)) : "unknown"}]` : "";
|
|
32073
33032
|
const supersedes = ads.filter((d) => d.supersededBy === target.id).map((d) => d.id);
|
|
32074
33033
|
const chain = supersedes.length > 0 ? `
|
|
32075
33034
|
|
|
32076
|
-
_Supersedes: ${supersedes
|
|
32077
|
-
return md(`## ${target.id}: ${target.title} [${target.confidence}]${
|
|
33035
|
+
_Supersedes: ${formatRefList(supersedes, titles)}_` : "";
|
|
33036
|
+
return md(`## ${target.id}: ${target.title} [${target.confidence}]${supersededNote2}
|
|
32078
33037
|
|
|
32079
33038
|
${target.body}${chain}`);
|
|
32080
33039
|
}
|
|
@@ -32214,7 +33173,7 @@ var inflightWork = /* @__PURE__ */ new Map();
|
|
|
32214
33173
|
var ToolTimeoutError = class extends Error {
|
|
32215
33174
|
constructor(toolName, expectedMs, actualMs, wedged, pendingNote) {
|
|
32216
33175
|
super(
|
|
32217
|
-
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\`).`
|
|
32218
33177
|
);
|
|
32219
33178
|
this.toolName = toolName;
|
|
32220
33179
|
this.expectedMs = expectedMs;
|
|
@@ -32302,6 +33261,7 @@ var PAPI_TOOLS = [
|
|
|
32302
33261
|
boardArchiveTool,
|
|
32303
33262
|
boardEditTool,
|
|
32304
33263
|
setupTool,
|
|
33264
|
+
joinTool,
|
|
32305
33265
|
buildListTool,
|
|
32306
33266
|
buildDescribeTool,
|
|
32307
33267
|
buildExecuteTool,
|
|
@@ -32467,6 +33427,8 @@ function createServer(adapter2, config2, observation) {
|
|
|
32467
33427
|
return handleBoardEdit(adapter2, safeArgs);
|
|
32468
33428
|
case "setup":
|
|
32469
33429
|
return handleSetup(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
|
|
33430
|
+
case "join":
|
|
33431
|
+
return handleJoin(adapter2, config2, safeArgs);
|
|
32470
33432
|
case "build_list":
|
|
32471
33433
|
return handleBuildList(adapter2, config2, safeArgs);
|
|
32472
33434
|
case "build_describe":
|
|
@@ -32712,7 +33674,12 @@ var FRIENDLY_GET_HTML = `<!doctype html>
|
|
|
32712
33674
|
<code>https://mcp.getpapi.ai/mcp</code>.</p>
|
|
32713
33675
|
<a class="btn" href="https://getpapi.ai/docs/install">See the install guide \u2192</a>
|
|
32714
33676
|
</div></body></html>`;
|
|
32715
|
-
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
|
+
]);
|
|
32716
33683
|
var MAX_BODY_BYTES = 1 * 1024 * 1024;
|
|
32717
33684
|
var SLOW_REQUEST_MS = 5e3;
|
|
32718
33685
|
var CRITICAL_REQUEST_MS = 3e4;
|
|
@@ -33325,7 +34292,7 @@ Options:
|
|
|
33325
34292
|
Getting started:
|
|
33326
34293
|
1. Run "npx @papi-ai/server setup" in any project folder
|
|
33327
34294
|
2. Approve in the browser when it opens
|
|
33328
|
-
3. Open
|
|
34295
|
+
3. Open your AI client in the same folder and say "run setup"
|
|
33329
34296
|
|
|
33330
34297
|
Stuck?
|
|
33331
34298
|
npx @papi-ai/server doctor # diagnose your config
|