@papi-ai/server 0.7.103 → 0.7.105
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 +20 -4
- package/dist/index.js +380 -48
- package/dist/prompts.js +6 -0
- package/package.json +2 -2
|
@@ -201,6 +201,7 @@ __export(git_exports, {
|
|
|
201
201
|
ensureLatestDevelop: () => ensureLatestDevelop,
|
|
202
202
|
ensureTagAtHead: () => ensureTagAtHead,
|
|
203
203
|
fetchBaseBranch: () => fetchBaseBranch,
|
|
204
|
+
findCollidingWork: () => findCollidingWork,
|
|
204
205
|
findContributorReleasePullRequests: () => findContributorReleasePullRequests,
|
|
205
206
|
findTaskCommitsOnBase: () => findTaskCommitsOnBase,
|
|
206
207
|
getBaseDivergence: () => getBaseDivergence,
|
|
@@ -1493,6 +1494,23 @@ function getRemoteBranchFiles(cwd, branch, baseBranch) {
|
|
|
1493
1494
|
return [];
|
|
1494
1495
|
}
|
|
1495
1496
|
}
|
|
1497
|
+
function findCollidingWork(cwd, baseBranch, filesLikelyTouched) {
|
|
1498
|
+
if (filesLikelyTouched.length === 0) return null;
|
|
1499
|
+
const prs = listOpenPullRequests(cwd);
|
|
1500
|
+
if (!prs || prs.length === 0) return null;
|
|
1501
|
+
const wanted = new Set(filesLikelyTouched);
|
|
1502
|
+
for (const pr of prs) {
|
|
1503
|
+
const branchFiles = getRemoteBranchFiles(cwd, pr.headRefName, baseBranch);
|
|
1504
|
+
const overlap = branchFiles.filter((f) => wanted.has(f));
|
|
1505
|
+
if (overlap.length > 0) {
|
|
1506
|
+
return {
|
|
1507
|
+
label: `PR #${pr.number} "${pr.title}" (branch \`${pr.headRefName}\`, opened by ${pr.author})`,
|
|
1508
|
+
files: overlap
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
return null;
|
|
1513
|
+
}
|
|
1496
1514
|
var AUTO_WRITTEN_PATHS, GIT_NETWORK_TIMEOUT_MS, MERGE_RETRY_DELAY_MS, MERGE_MAX_RETRIES, GIT_FETCH_TIMEOUT_MS;
|
|
1497
1515
|
var init_git = __esm({
|
|
1498
1516
|
"src/lib/git.ts"() {
|
|
@@ -1684,7 +1702,6 @@ var init_proxy_adapter = __esm({
|
|
|
1684
1702
|
"getBuildReportsSince",
|
|
1685
1703
|
"getContextHashes",
|
|
1686
1704
|
"getContextUtilisation",
|
|
1687
|
-
"getCostSnapshots",
|
|
1688
1705
|
"getCostSummary",
|
|
1689
1706
|
"getCurrentNorthStar",
|
|
1690
1707
|
"getCycleHealth",
|
|
@@ -2338,9 +2355,8 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
2338
2355
|
getCostSummary(cycleNumber) {
|
|
2339
2356
|
return this.invoke("getCostSummary", [cycleNumber]);
|
|
2340
2357
|
}
|
|
2341
|
-
getCostSnapshots
|
|
2342
|
-
|
|
2343
|
-
}
|
|
2358
|
+
// task-3332: getCostSnapshots retired — see the removal note on CostSnapshot
|
|
2359
|
+
// in packages/shared/src/entities.ts.
|
|
2344
2360
|
appendCycleMetrics(snapshot) {
|
|
2345
2361
|
return this.invoke("appendCycleMetrics", [snapshot]);
|
|
2346
2362
|
}
|
package/dist/index.js
CHANGED
|
@@ -401,6 +401,7 @@ function parseBuildHandoff(markdown) {
|
|
|
401
401
|
taskTitle,
|
|
402
402
|
cycle,
|
|
403
403
|
whyNow,
|
|
404
|
+
relevantDecisions: parseBulletsOnly(sections.get("RELEVANT ACTIVE DECISIONS") ?? ""),
|
|
404
405
|
scope: parseBulletList(sections.get("SCOPE (DO THIS)") ?? ""),
|
|
405
406
|
scopeBoundary: parseBulletList(sections.get("SCOPE BOUNDARY (DO NOT DO THIS)") ?? ""),
|
|
406
407
|
acceptanceCriteria: parseChecklist(sections.get("ACCEPTANCE CRITERIA") ?? ""),
|
|
@@ -424,6 +425,7 @@ function coerceBuildHandoff(fields, taskId) {
|
|
|
424
425
|
taskTitle: str(fields.taskTitle),
|
|
425
426
|
cycle: typeof fields.cycle === "number" ? fields.cycle : 0,
|
|
426
427
|
whyNow: str(fields.whyNow),
|
|
428
|
+
relevantDecisions: ensureArray(fields.relevantDecisions),
|
|
427
429
|
scope,
|
|
428
430
|
scopeBoundary,
|
|
429
431
|
// task-3223: the structured path is where an object criterion (text + an
|
|
@@ -471,6 +473,14 @@ function serializeBuildHandoff(raw) {
|
|
|
471
473
|
lines.push(`Task: ${handoff.taskTitle}`);
|
|
472
474
|
lines.push(`Cycle: ${handoff.cycle}`);
|
|
473
475
|
lines.push(`Why now: ${handoff.whyNow}`);
|
|
476
|
+
const relevantDecisions = ensureArray(handoff.relevantDecisions);
|
|
477
|
+
if (relevantDecisions.length > 0) {
|
|
478
|
+
lines.push("");
|
|
479
|
+
lines.push("RELEVANT ACTIVE DECISIONS");
|
|
480
|
+
for (const item of relevantDecisions) {
|
|
481
|
+
lines.push(`- ${item}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
474
484
|
lines.push("");
|
|
475
485
|
lines.push("SCOPE (DO THIS)");
|
|
476
486
|
for (const item of ensureArray(handoff.scope)) {
|
|
@@ -912,6 +922,7 @@ var init_dist = __esm({
|
|
|
912
922
|
CHECK_VALUE_MAX = 200;
|
|
913
923
|
VALID_EFFORT_SIZES = /* @__PURE__ */ new Set(["XS", "S", "M", "L", "XL"]);
|
|
914
924
|
SECTION_HEADERS = [
|
|
925
|
+
"RELEVANT ACTIVE DECISIONS",
|
|
915
926
|
"SCOPE (DO THIS)",
|
|
916
927
|
"WHY NOT SIMPLER",
|
|
917
928
|
"SCOPE BOUNDARY (DO NOT DO THIS)",
|
|
@@ -960,6 +971,7 @@ __export(git_exports, {
|
|
|
960
971
|
ensureLatestDevelop: () => ensureLatestDevelop,
|
|
961
972
|
ensureTagAtHead: () => ensureTagAtHead,
|
|
962
973
|
fetchBaseBranch: () => fetchBaseBranch,
|
|
974
|
+
findCollidingWork: () => findCollidingWork,
|
|
963
975
|
findContributorReleasePullRequests: () => findContributorReleasePullRequests,
|
|
964
976
|
findTaskCommitsOnBase: () => findTaskCommitsOnBase,
|
|
965
977
|
getBaseDivergence: () => getBaseDivergence,
|
|
@@ -2252,6 +2264,23 @@ function getRemoteBranchFiles(cwd, branch, baseBranch) {
|
|
|
2252
2264
|
return [];
|
|
2253
2265
|
}
|
|
2254
2266
|
}
|
|
2267
|
+
function findCollidingWork(cwd, baseBranch, filesLikelyTouched) {
|
|
2268
|
+
if (filesLikelyTouched.length === 0) return null;
|
|
2269
|
+
const prs = listOpenPullRequests(cwd);
|
|
2270
|
+
if (!prs || prs.length === 0) return null;
|
|
2271
|
+
const wanted = new Set(filesLikelyTouched);
|
|
2272
|
+
for (const pr of prs) {
|
|
2273
|
+
const branchFiles = getRemoteBranchFiles(cwd, pr.headRefName, baseBranch);
|
|
2274
|
+
const overlap = branchFiles.filter((f) => wanted.has(f));
|
|
2275
|
+
if (overlap.length > 0) {
|
|
2276
|
+
return {
|
|
2277
|
+
label: `PR #${pr.number} "${pr.title}" (branch \`${pr.headRefName}\`, opened by ${pr.author})`,
|
|
2278
|
+
files: overlap
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
return null;
|
|
2283
|
+
}
|
|
2255
2284
|
var AUTO_WRITTEN_PATHS, GIT_NETWORK_TIMEOUT_MS, MERGE_RETRY_DELAY_MS, MERGE_MAX_RETRIES, GIT_FETCH_TIMEOUT_MS;
|
|
2256
2285
|
var init_git = __esm({
|
|
2257
2286
|
"src/lib/git.ts"() {
|
|
@@ -2509,7 +2538,6 @@ var init_proxy_adapter = __esm({
|
|
|
2509
2538
|
"getBuildReportsSince",
|
|
2510
2539
|
"getContextHashes",
|
|
2511
2540
|
"getContextUtilisation",
|
|
2512
|
-
"getCostSnapshots",
|
|
2513
2541
|
"getCostSummary",
|
|
2514
2542
|
"getCurrentNorthStar",
|
|
2515
2543
|
"getCycleHealth",
|
|
@@ -3163,9 +3191,8 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
3163
3191
|
getCostSummary(cycleNumber) {
|
|
3164
3192
|
return this.invoke("getCostSummary", [cycleNumber]);
|
|
3165
3193
|
}
|
|
3166
|
-
getCostSnapshots
|
|
3167
|
-
|
|
3168
|
-
}
|
|
3194
|
+
// task-3332: getCostSnapshots retired — see the removal note on CostSnapshot
|
|
3195
|
+
// in packages/shared/src/entities.ts.
|
|
3169
3196
|
appendCycleMetrics(snapshot) {
|
|
3170
3197
|
return this.invoke("appendCycleMetrics", [snapshot]);
|
|
3171
3198
|
}
|
|
@@ -7572,11 +7599,78 @@ init_dist();
|
|
|
7572
7599
|
|
|
7573
7600
|
// src/lib/formatters.ts
|
|
7574
7601
|
init_dist();
|
|
7602
|
+
var CONFIDENCE_RANK = { HIGH: 0, MEDIUM: 1, LOW: 2 };
|
|
7603
|
+
function rankDecisionsForPlan(decisions) {
|
|
7604
|
+
return [...decisions].sort((a, b2) => {
|
|
7605
|
+
const confDiff = (CONFIDENCE_RANK[a.confidence] ?? 1) - (CONFIDENCE_RANK[b2.confidence] ?? 1);
|
|
7606
|
+
if (confDiff !== 0) return confDiff;
|
|
7607
|
+
return (b2.modifiedCycle ?? b2.createdCycle ?? 0) - (a.modifiedCycle ?? a.createdCycle ?? 0);
|
|
7608
|
+
});
|
|
7609
|
+
}
|
|
7575
7610
|
function formatActiveDecisionsForPlan(decisions) {
|
|
7576
7611
|
if (decisions.length === 0) return "No active decisions.";
|
|
7577
|
-
|
|
7612
|
+
const active = decisions.filter((d) => !d.superseded);
|
|
7613
|
+
if (active.length === 0) return "No active decisions.";
|
|
7614
|
+
const softBudget = Number(process.env.PAPI_AD_CONTEXT_BUDGET) || 4e4;
|
|
7615
|
+
const hardBudget = Math.round(softBudget * 1.3);
|
|
7616
|
+
const ranked = rankDecisionsForPlan(active);
|
|
7617
|
+
const fullBlocks = [];
|
|
7618
|
+
const oneLiners = [];
|
|
7619
|
+
let spent = 0;
|
|
7620
|
+
let degradedFrom = -1;
|
|
7621
|
+
for (let i = 0; i < ranked.length; i++) {
|
|
7622
|
+
const d = ranked[i];
|
|
7623
|
+
const fullBlock = `### ${d.id}: ${d.title} [Confidence: ${d.confidence}]
|
|
7624
|
+
|
|
7625
|
+
${d.body}`;
|
|
7626
|
+
const fullCost = Buffer.byteLength(`${fullBlock}
|
|
7578
7627
|
|
|
7579
|
-
|
|
7628
|
+
`, "utf-8");
|
|
7629
|
+
if (spent + fullCost <= softBudget) {
|
|
7630
|
+
fullBlocks.push(fullBlock);
|
|
7631
|
+
spent += fullCost;
|
|
7632
|
+
continue;
|
|
7633
|
+
}
|
|
7634
|
+
degradedFrom = i;
|
|
7635
|
+
break;
|
|
7636
|
+
}
|
|
7637
|
+
const compactedIds = [];
|
|
7638
|
+
const omittedIds = [];
|
|
7639
|
+
if (degradedFrom >= 0) {
|
|
7640
|
+
for (let i = degradedFrom; i < ranked.length; i++) {
|
|
7641
|
+
const d = ranked[i];
|
|
7642
|
+
const line = `- ${d.id} (${d.title}) [${d.confidence}]`;
|
|
7643
|
+
const cost = Buffer.byteLength(`${line}
|
|
7644
|
+
`, "utf-8");
|
|
7645
|
+
if (spent + cost <= hardBudget) {
|
|
7646
|
+
oneLiners.push(line);
|
|
7647
|
+
compactedIds.push(d.id);
|
|
7648
|
+
spent += cost;
|
|
7649
|
+
} else {
|
|
7650
|
+
omittedIds.push(d.id);
|
|
7651
|
+
}
|
|
7652
|
+
}
|
|
7653
|
+
}
|
|
7654
|
+
const parts = [...fullBlocks];
|
|
7655
|
+
if (oneLiners.length > 0) {
|
|
7656
|
+
parts.push(`### Other Active Decisions (compacted for budget)
|
|
7657
|
+
|
|
7658
|
+
${oneLiners.join("\n")}`);
|
|
7659
|
+
}
|
|
7660
|
+
if (compactedIds.length > 0 || omittedIds.length > 0) {
|
|
7661
|
+
const noteLines = [
|
|
7662
|
+
`### Context Budget \u2014 Active Decisions trimmed`,
|
|
7663
|
+
"",
|
|
7664
|
+
`${compactedIds.length + omittedIds.length} of ${ranked.length} Active Decisions were compacted to one-liners to keep this plan payload under ~${Math.round(softBudget / 1024)} KB (lowest confidence/oldest first). Full bodies above are complete and untrimmed.`
|
|
7665
|
+
];
|
|
7666
|
+
if (omittedIds.length > 0) {
|
|
7667
|
+
noteLines.push(
|
|
7668
|
+
`${omittedIds.length} AD one-liner(s) were additionally omitted from the list above for exceeding the hard budget, but are not forgotten: ${omittedIds.join(", ")}. Raise PAPI_AD_CONTEXT_BUDGET in the MCP server env to see their one-liners, or run ad_view for full detail on any specific one.`
|
|
7669
|
+
);
|
|
7670
|
+
}
|
|
7671
|
+
parts.push(noteLines.join("\n"));
|
|
7672
|
+
}
|
|
7673
|
+
return parts.join("\n\n");
|
|
7580
7674
|
}
|
|
7581
7675
|
function formatActiveDecisionsForReview(decisions) {
|
|
7582
7676
|
if (decisions.length === 0) return "No active decisions.";
|
|
@@ -8381,6 +8475,9 @@ Why now: [justification]
|
|
|
8381
8475
|
DEPENDS ON
|
|
8382
8476
|
[Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
|
|
8383
8477
|
|
|
8478
|
+
RELEVANT ACTIVE DECISIONS
|
|
8479
|
+
[Optional \u2014 self-check which Active Decisions actually bind the FILES LIKELY TOUCHED and module below, out of the full AD list already in your context. List the top 3-6 as "AD-N (title) \u2014 one-line reason this AD binds the files/approach". Rank by blast radius (an AD that would be VIOLATED by a naive implementation outranks one that's merely thematically adjacent). This is a judgement call \u2014 no server-side scoring feeds this. Omit this section entirely when nothing in the AD list is genuinely relevant to what this task touches; do not force a match.]
|
|
8480
|
+
|
|
8384
8481
|
SCOPE (DO THIS)
|
|
8385
8482
|
[specific deliverables \u2014 write for the simplest viable path first]
|
|
8386
8483
|
|
|
@@ -9716,6 +9813,9 @@ Why now: [justification]
|
|
|
9716
9813
|
DEPENDS ON
|
|
9717
9814
|
[Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
|
|
9718
9815
|
|
|
9816
|
+
RELEVANT ACTIVE DECISIONS
|
|
9817
|
+
[Optional \u2014 self-check which Active Decisions actually bind the FILES LIKELY TOUCHED and module below, out of the full AD list already in your context. List the top 3-6 as "AD-N (title) \u2014 one-line reason this AD binds the files/approach". Rank by blast radius (an AD that would be VIOLATED by a naive implementation outranks one that's merely thematically adjacent). This is a judgement call \u2014 no server-side scoring feeds this. Omit this section entirely when nothing in the AD list is genuinely relevant to what this task touches; do not force a match.]
|
|
9818
|
+
|
|
9719
9819
|
SCOPE (DO THIS)
|
|
9720
9820
|
[specific deliverables \u2014 write for the simplest viable path first]
|
|
9721
9821
|
|
|
@@ -10870,6 +10970,23 @@ function idMatches(candidate, ref) {
|
|
|
10870
10970
|
if (!candidate) return false;
|
|
10871
10971
|
return candidate.toLowerCase() === ref.toLowerCase();
|
|
10872
10972
|
}
|
|
10973
|
+
function findDecision(ref, decisions) {
|
|
10974
|
+
return decisions.find((d) => idMatches(d.displayId, ref) || idMatches(d.id, ref));
|
|
10975
|
+
}
|
|
10976
|
+
function resolveCurrentDecision(ref, decisions, maxDepth = 10) {
|
|
10977
|
+
let current = findDecision(ref, decisions);
|
|
10978
|
+
if (!current) return void 0;
|
|
10979
|
+
const visited = /* @__PURE__ */ new Set([current.id]);
|
|
10980
|
+
let depth = 0;
|
|
10981
|
+
while (current?.superseded === true && current.supersededBy && depth < maxDepth) {
|
|
10982
|
+
const next = findDecision(current.supersededBy, decisions);
|
|
10983
|
+
if (!next || visited.has(next.id)) break;
|
|
10984
|
+
visited.add(next.id);
|
|
10985
|
+
current = next;
|
|
10986
|
+
depth += 1;
|
|
10987
|
+
}
|
|
10988
|
+
return current;
|
|
10989
|
+
}
|
|
10873
10990
|
function isBlockerResolved(blocker, ctx) {
|
|
10874
10991
|
if (!blocker || !blocker.ref) return false;
|
|
10875
10992
|
switch (blocker.type) {
|
|
@@ -10884,9 +11001,7 @@ function isBlockerResolved(blocker, ctx) {
|
|
|
10884
11001
|
return action != null && action.completed_at != null;
|
|
10885
11002
|
}
|
|
10886
11003
|
case "decision-gate": {
|
|
10887
|
-
const decision = ctx.decisions
|
|
10888
|
-
(d) => idMatches(d.displayId, blocker.ref) || idMatches(d.id, blocker.ref)
|
|
10889
|
-
);
|
|
11004
|
+
const decision = resolveCurrentDecision(blocker.ref, ctx.decisions);
|
|
10890
11005
|
if (decision?.superseded === true) return true;
|
|
10891
11006
|
const resolvedOutcomes = /* @__PURE__ */ new Set(["validated"]);
|
|
10892
11007
|
if (decision?.outcome && resolvedOutcomes.has(decision.outcome)) return true;
|
|
@@ -10902,9 +11017,7 @@ function isBlockerResolved(blocker, ctx) {
|
|
|
10902
11017
|
}
|
|
10903
11018
|
function blockerNeedsRedecision(blocker, ctx) {
|
|
10904
11019
|
if (!blocker || blocker.type !== "decision-gate" || !blocker.ref) return false;
|
|
10905
|
-
const decision = ctx.decisions
|
|
10906
|
-
(d) => idMatches(d.displayId, blocker.ref) || idMatches(d.id, blocker.ref)
|
|
10907
|
-
);
|
|
11020
|
+
const decision = resolveCurrentDecision(blocker.ref, ctx.decisions);
|
|
10908
11021
|
return decision?.resolutionState === "withdrawn";
|
|
10909
11022
|
}
|
|
10910
11023
|
function formatBlockerWaiting(blocker, refTitle) {
|
|
@@ -10927,9 +11040,7 @@ function resolveBlockerTitle(blocker, ctx) {
|
|
|
10927
11040
|
(t) => idMatches(t.displayId, blocker.ref) || idMatches(t.id, blocker.ref)
|
|
10928
11041
|
)?.title;
|
|
10929
11042
|
case "decision-gate":
|
|
10930
|
-
return ctx.decisions
|
|
10931
|
-
(d) => idMatches(d.displayId, blocker.ref) || idMatches(d.id, blocker.ref)
|
|
10932
|
-
)?.title;
|
|
11043
|
+
return resolveCurrentDecision(blocker.ref, ctx.decisions)?.title;
|
|
10933
11044
|
default:
|
|
10934
11045
|
return void 0;
|
|
10935
11046
|
}
|
|
@@ -17761,7 +17872,11 @@ var boardDeprioritiseTool = {
|
|
|
17761
17872
|
},
|
|
17762
17873
|
blocker_ref: {
|
|
17763
17874
|
type: "string",
|
|
17764
|
-
description: "Required when blocker_type is set. The identifier being waited on: a task display-id (depends-on), an AD/decision id (decision-gate), or an owner_action id (owner-action)."
|
|
17875
|
+
description: "Required when blocker_type is set, EXCEPT owner-action with owner_action_name (that mode creates the owner_action and derives the ref itself). The identifier being waited on: a task display-id (depends-on), an AD/decision id (decision-gate), or an existing owner_action id (owner-action, link mode)."
|
|
17876
|
+
},
|
|
17877
|
+
owner_action_name: {
|
|
17878
|
+
type: "string",
|
|
17879
|
+
description: 'Optional, blocker_type="owner-action" only. When set, CREATES a new owner action with this name and unlocks_task_id set to this task in the same insert, instead of linking to an existing one \u2014 collapses the old create-then-link two-step into one call. Omit blocker_ref in this mode; it is derived from the created row.'
|
|
17765
17880
|
},
|
|
17766
17881
|
defer: {
|
|
17767
17882
|
type: "boolean",
|
|
@@ -17779,15 +17894,26 @@ var boardDeprioritiseTool = {
|
|
|
17779
17894
|
required: ["task_id"],
|
|
17780
17895
|
// task-2802: mirror the handler's guards so the agent sees the dependency up
|
|
17781
17896
|
// front. handleBoardDeprioritise requires `reason` for both block and cancel,
|
|
17782
|
-
// and requires `blocker_ref` whenever `blocker_type` is set
|
|
17783
|
-
//
|
|
17897
|
+
// and requires `blocker_ref` whenever `blocker_type` is set — EXCEPT the
|
|
17898
|
+
// task-3451 owner-action create mode (blocker_type='owner-action' +
|
|
17899
|
+
// owner_action_name), which derives its ref from the row it creates.
|
|
17900
|
+
// Keyed on explicit values — an omitted action defaults to "backlog" and
|
|
17901
|
+
// triggers neither.
|
|
17784
17902
|
allOf: [
|
|
17785
17903
|
{
|
|
17786
17904
|
if: { properties: { action: { enum: ["block", "cancel"] } }, required: ["action"] },
|
|
17787
17905
|
then: { required: ["reason"] }
|
|
17788
17906
|
},
|
|
17789
17907
|
{
|
|
17790
|
-
if: {
|
|
17908
|
+
if: {
|
|
17909
|
+
required: ["blocker_type"],
|
|
17910
|
+
not: {
|
|
17911
|
+
allOf: [
|
|
17912
|
+
{ properties: { blocker_type: { const: "owner-action" } }, required: ["blocker_type"] },
|
|
17913
|
+
{ required: ["owner_action_name"] }
|
|
17914
|
+
]
|
|
17915
|
+
}
|
|
17916
|
+
},
|
|
17791
17917
|
then: { required: ["blocker_ref"] }
|
|
17792
17918
|
}
|
|
17793
17919
|
]
|
|
@@ -18092,13 +18218,18 @@ async function handleBoardDeprioritise(adapter2, args) {
|
|
|
18092
18218
|
return errorResponse("reason is required when blocking a task \u2014 explain what external dependency or gate is blocking it.");
|
|
18093
18219
|
}
|
|
18094
18220
|
const blockerType = args.blocker_type;
|
|
18095
|
-
|
|
18221
|
+
let blockerRef = args.blocker_ref;
|
|
18222
|
+
const ownerActionName = args.owner_action_name;
|
|
18223
|
+
const isOwnerActionCreateMode = blockerType === "owner-action" && Boolean(ownerActionName);
|
|
18096
18224
|
const validTypes = /* @__PURE__ */ new Set(["depends-on", "decision-gate", "owner-action"]);
|
|
18097
18225
|
if (blockerType !== void 0 && !validTypes.has(blockerType)) {
|
|
18098
18226
|
return errorResponse(`blocker_type must be one of: depends-on, decision-gate, owner-action (got "${blockerType}").`);
|
|
18099
18227
|
}
|
|
18100
|
-
if (blockerType !== void 0 && !blockerRef) {
|
|
18101
|
-
return errorResponse("blocker_ref is required when blocker_type is set \u2014 provide the task display-id, decision id, or owner_action id being waited on.");
|
|
18228
|
+
if (blockerType !== void 0 && !blockerRef && !isOwnerActionCreateMode) {
|
|
18229
|
+
return errorResponse("blocker_ref is required when blocker_type is set \u2014 provide the task display-id, decision id, or owner_action id being waited on (or owner_action_name to create a new owner action).");
|
|
18230
|
+
}
|
|
18231
|
+
if (ownerActionName && blockerType !== "owner-action") {
|
|
18232
|
+
return errorResponse('owner_action_name only applies when blocker_type is "owner-action".');
|
|
18102
18233
|
}
|
|
18103
18234
|
try {
|
|
18104
18235
|
const task = await adapter2.getTask(taskId);
|
|
@@ -18111,25 +18242,41 @@ async function handleBoardDeprioritise(adapter2, args) {
|
|
|
18111
18242
|
notes: `${existingNotes}BLOCKED: ${reason}`
|
|
18112
18243
|
};
|
|
18113
18244
|
let typedSuffix = "";
|
|
18114
|
-
if (blockerType !== void 0 && blockerRef) {
|
|
18115
|
-
|
|
18116
|
-
const blockedCycle = health?.totalCycles ?? 0;
|
|
18117
|
-
updates.blocker = {
|
|
18118
|
-
type: blockerType,
|
|
18119
|
-
ref: blockerRef,
|
|
18120
|
-
reason,
|
|
18121
|
-
blockedCycle
|
|
18122
|
-
};
|
|
18123
|
-
typedSuffix = `
|
|
18124
|
-
|
|
18125
|
-
Blocker: **${blockerType}** \u2192 ${blockerRef} (auto-unblock scanned at plan/orient).`;
|
|
18126
|
-
if (blockerType === "owner-action" && adapter2.linkOwnerActionToTask && adapter2.getProjectOwnerUserId) {
|
|
18245
|
+
if (blockerType !== void 0 && (blockerRef || isOwnerActionCreateMode)) {
|
|
18246
|
+
if (isOwnerActionCreateMode && adapter2.createOwnerAction && adapter2.getProjectOwnerUserId) {
|
|
18127
18247
|
try {
|
|
18128
18248
|
const ownerUserId = await adapter2.getProjectOwnerUserId();
|
|
18129
|
-
if (ownerUserId)
|
|
18249
|
+
if (ownerUserId) {
|
|
18250
|
+
const created = await adapter2.createOwnerAction(ownerUserId, {
|
|
18251
|
+
name: ownerActionName,
|
|
18252
|
+
dependency: reason,
|
|
18253
|
+
unlocks_task_id: task.uuid
|
|
18254
|
+
});
|
|
18255
|
+
blockerRef = created.id;
|
|
18256
|
+
}
|
|
18130
18257
|
} catch {
|
|
18131
18258
|
}
|
|
18132
18259
|
}
|
|
18260
|
+
if (blockerRef) {
|
|
18261
|
+
const health = await adapter2.getCycleHealth().catch(() => null);
|
|
18262
|
+
const blockedCycle = health?.totalCycles ?? 0;
|
|
18263
|
+
updates.blocker = {
|
|
18264
|
+
type: blockerType,
|
|
18265
|
+
ref: blockerRef,
|
|
18266
|
+
reason,
|
|
18267
|
+
blockedCycle
|
|
18268
|
+
};
|
|
18269
|
+
typedSuffix = `
|
|
18270
|
+
|
|
18271
|
+
Blocker: **${blockerType}** \u2192 ${blockerRef} (auto-unblock scanned at plan/orient).`;
|
|
18272
|
+
if (blockerType === "owner-action" && !isOwnerActionCreateMode && adapter2.linkOwnerActionToTask && adapter2.getProjectOwnerUserId) {
|
|
18273
|
+
try {
|
|
18274
|
+
const ownerUserId = await adapter2.getProjectOwnerUserId();
|
|
18275
|
+
if (ownerUserId) await adapter2.linkOwnerActionToTask(blockerRef, task.uuid, ownerUserId);
|
|
18276
|
+
} catch {
|
|
18277
|
+
}
|
|
18278
|
+
}
|
|
18279
|
+
}
|
|
18133
18280
|
}
|
|
18134
18281
|
await adapter2.updateTask(taskId, updates);
|
|
18135
18282
|
return textResponse(`Blocked **${taskId}** (${task.title}).
|
|
@@ -24777,6 +24924,41 @@ The registry stores metadata and a summary \u2014 not the body. This doc has one
|
|
|
24777
24924
|
return `
|
|
24778
24925
|
- **Durability:** committed \`${path8}\` (was untracked)`;
|
|
24779
24926
|
}
|
|
24927
|
+
var DOC_OVERLAP_COVERAGE_THRESHOLD = 0.6;
|
|
24928
|
+
async function findOverlappingDocs(target, type, title, summary) {
|
|
24929
|
+
if (!target.searchDocs) return [];
|
|
24930
|
+
const newKeywords = extractKeywords(`${title} ${summary}`);
|
|
24931
|
+
if (newKeywords.size < 2) return [];
|
|
24932
|
+
let candidates;
|
|
24933
|
+
try {
|
|
24934
|
+
candidates = await target.searchDocs({ type, status: "active" });
|
|
24935
|
+
} catch {
|
|
24936
|
+
return [];
|
|
24937
|
+
}
|
|
24938
|
+
const matches = [];
|
|
24939
|
+
for (const doc of candidates) {
|
|
24940
|
+
const docKeywords = extractKeywords(`${doc.title} ${doc.summary}`);
|
|
24941
|
+
if (docKeywords.size < 2) continue;
|
|
24942
|
+
let covered = 0;
|
|
24943
|
+
for (const word of newKeywords) if (docKeywords.has(word)) covered++;
|
|
24944
|
+
const coverage = covered / newKeywords.size;
|
|
24945
|
+
if (coverage >= DOC_OVERLAP_COVERAGE_THRESHOLD) {
|
|
24946
|
+
matches.push({ path: doc.path, title: doc.title, coverage });
|
|
24947
|
+
}
|
|
24948
|
+
}
|
|
24949
|
+
return matches.sort((a, b2) => b2.coverage - a.coverage).slice(0, 3);
|
|
24950
|
+
}
|
|
24951
|
+
function overlapReconciliationNote(overlaps) {
|
|
24952
|
+
if (overlaps.length === 0) return "";
|
|
24953
|
+
const named = overlaps.map((o) => `\`${o.path}\` ("${o.title}")`).join(", ");
|
|
24954
|
+
return `
|
|
24955
|
+
|
|
24956
|
+
\u26A0\uFE0F **Possible overlap** with existing active doc(s): ${named} \u2014 verify it is real, then classify:
|
|
24957
|
+
- **AGREES** \u2014 restates the existing doc. Don't duplicate; nothing to register.
|
|
24958
|
+
- **EXTENDS** \u2014 new ground the existing doc doesn't cover. Fine to register as-is.
|
|
24959
|
+
- **CONTRADICTS** \u2014 cuts against the existing doc. Surface to the owner \u2014 never resolve silently (task-3219: a contradiction is not a veto).
|
|
24960
|
+
- **SUPERSEDES** \u2014 this doc should replace the existing one. Re-run doc_register with \`superseded_by_path\` pointing at the overlapping doc's path.`;
|
|
24961
|
+
}
|
|
24780
24962
|
async function handleDocRegister(adapter2, args, config2) {
|
|
24781
24963
|
const adapterType = config2?.adapterType ?? "unknown";
|
|
24782
24964
|
const continueHint = "doc_register is advisory \u2014 your build/plan/review flow is unaffected. Fix the input (or wait for the registry to recover) and re-run doc_register; or just continue without it.";
|
|
@@ -24835,6 +25017,7 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
24835
25017
|
);
|
|
24836
25018
|
}
|
|
24837
25019
|
try {
|
|
25020
|
+
const overlaps = supersededByPath ? [] : await findOverlappingDocs(target, type, title, summary);
|
|
24838
25021
|
let supersededBy;
|
|
24839
25022
|
if (supersededByPath) {
|
|
24840
25023
|
const existing = await target.getDoc?.(supersededByPath);
|
|
@@ -24910,7 +25093,7 @@ ${decision.message}`;
|
|
|
24910
25093
|
- **Visibility:** ${visibilityLabel}
|
|
24911
25094
|
- **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
|
|
24912
25095
|
- **Actions:** ${actions?.length ?? 0} items
|
|
24913
|
-
- **ID:** ${entry.id}` + bodyNote + durability
|
|
25096
|
+
- **ID:** ${entry.id}` + bodyNote + durability + overlapReconciliationNote(overlaps)
|
|
24914
25097
|
);
|
|
24915
25098
|
} catch (err) {
|
|
24916
25099
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -27595,7 +27778,7 @@ function resolveAdHocCycle(cycle, latest, latestComplete) {
|
|
|
27595
27778
|
if (cycle === "current") return latest;
|
|
27596
27779
|
return latestComplete ? latest + 1 : latest;
|
|
27597
27780
|
}
|
|
27598
|
-
async function recordAdHoc(adapter2, input) {
|
|
27781
|
+
async function recordAdHoc(adapter2, config2, input) {
|
|
27599
27782
|
const [health, phases] = await Promise.all([
|
|
27600
27783
|
adapter2.getCycleHealth(),
|
|
27601
27784
|
adapter2.readPhases()
|
|
@@ -27617,9 +27800,16 @@ async function recordAdHoc(adapter2, input) {
|
|
|
27617
27800
|
if (!existing) {
|
|
27618
27801
|
throw new Error(`Task "${input.taskId}" not found on the board. Check the task ID and try again.`);
|
|
27619
27802
|
}
|
|
27620
|
-
if (
|
|
27803
|
+
if (existing.assigneeId) {
|
|
27804
|
+
const gate = await resolveOwnerGate(adapter2, config2);
|
|
27805
|
+
if (gate.enforced && (!gate.callerUserId || gate.callerUserId !== existing.assigneeId)) {
|
|
27806
|
+
throw new Error(
|
|
27807
|
+
`Task "${input.taskId}" (${existing.title}) is claimed by another member \u2014 ad-hoc work cannot be recorded against it. Have the claimer record it, or run \`task_unclaim\` to release it first.`
|
|
27808
|
+
);
|
|
27809
|
+
}
|
|
27810
|
+
} else {
|
|
27621
27811
|
warnings.push(
|
|
27622
|
-
`\u2139\uFE0F ${input.taskId} has no assignee \u2014 recording ad-hoc work against an unclaimed pool task. If someone else might be building it too, check for an open PR/branch on the same files before merging.`
|
|
27812
|
+
`\u2139\uFE0F ${input.taskId} has no assignee \u2014 recording ad-hoc work against an unclaimed pool task. ` + (input.collidingWorkNote ?? `If someone else might be building it too, check for an open PR/branch on the same files before merging.`)
|
|
27623
27813
|
);
|
|
27624
27814
|
}
|
|
27625
27815
|
const updatePayload = {
|
|
@@ -27876,7 +28066,22 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
27876
28066
|
const gitUsable = !overrideNote && isGitAvailable() && isGitRepo(config2.projectRoot);
|
|
27877
28067
|
const currentBranch = gitUsable ? getCurrentBranch(config2.projectRoot) : null;
|
|
27878
28068
|
const baseBranch = gitUsable ? resolveBaseBranch(config2.projectRoot, config2.baseBranch) : null;
|
|
27879
|
-
|
|
28069
|
+
let collidingWorkNote;
|
|
28070
|
+
if (taskId && gitUsable && baseBranch) {
|
|
28071
|
+
try {
|
|
28072
|
+
const existingTask = await target.getTask(taskId);
|
|
28073
|
+
const filesLikelyTouched = existingTask?.buildHandoff?.filesLikelyTouched ?? [];
|
|
28074
|
+
if (existingTask && !existingTask.assigneeId && filesLikelyTouched.length > 0) {
|
|
28075
|
+
const collision = findCollidingWork(config2.projectRoot, baseBranch, filesLikelyTouched);
|
|
28076
|
+
if (collision) {
|
|
28077
|
+
const fileList = collision.files.slice(0, 3).join(", ") + (collision.files.length > 3 ? ", \u2026" : "");
|
|
28078
|
+
collidingWorkNote = `${collision.label} already touches ${collision.files.length === 1 ? "the same file" : `${collision.files.length} of the same files`} (${fileList}) \u2014 check it before merging.`;
|
|
28079
|
+
}
|
|
28080
|
+
}
|
|
28081
|
+
} catch {
|
|
28082
|
+
}
|
|
28083
|
+
}
|
|
28084
|
+
const result = await recordAdHoc(target, config2, {
|
|
27880
28085
|
title: title || "",
|
|
27881
28086
|
taskId,
|
|
27882
28087
|
notes: rawNotes,
|
|
@@ -27891,7 +28096,8 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
27891
28096
|
stage: stageArg,
|
|
27892
28097
|
hold: holdArg,
|
|
27893
28098
|
currentBranch,
|
|
27894
|
-
baseBranch
|
|
28099
|
+
baseBranch,
|
|
28100
|
+
collidingWorkNote
|
|
27895
28101
|
});
|
|
27896
28102
|
if (!holdArg && gitUsable) {
|
|
27897
28103
|
try {
|
|
@@ -28923,6 +29129,27 @@ var reviewSubmitTool = {
|
|
|
28923
29129
|
}
|
|
28924
29130
|
},
|
|
28925
29131
|
required: ["verdict", "summary", "findings"]
|
|
29132
|
+
},
|
|
29133
|
+
proposal: {
|
|
29134
|
+
type: "object",
|
|
29135
|
+
description: 'task-3388, OPTIONAL (build-acceptance + verdict:"accept" only): propose a Decision or a Convention the build or the review itself settled. Same mechanism as build_execute\u2019s `proposal` (task-3273) \u2014 PAPI never mints one for you. A proposal that passes all four admission tests is QUEUED for the owner behind a decision gate, and nothing is written as an Active Decision. YOU apply the four tests (they are stated in the planning prompts); the server routes on your answers and makes no judgement about the content. Fails "arguable today" only and it is recorded as a Convention instead, riding every future build. Fails "constrains future work" and it is returned to you with the reason, unrecorded.',
|
|
29136
|
+
properties: {
|
|
29137
|
+
title: { type: "string", description: "One line stating the stance or rule." },
|
|
29138
|
+
body: { type: "string", description: "What was decided or settled, and why." },
|
|
29139
|
+
module: { type: "string", description: "Optional module scope. Only used when this lands as a Convention." },
|
|
29140
|
+
tests: {
|
|
29141
|
+
type: "object",
|
|
29142
|
+
description: "The four admission tests, each answered explicitly. An omitted test is NOT a failed test \u2014 the proposal is refused rather than routed.",
|
|
29143
|
+
properties: {
|
|
29144
|
+
alternativesWereReal: { type: "boolean", description: "(a) Something else could genuinely have been chosen." },
|
|
29145
|
+
constrainsFutureWork: { type: "boolean", description: "(b) It changes what a task nobody has written yet will do." },
|
|
29146
|
+
arguableToday: { type: "boolean", description: "(c) A competent person could argue the other side right now." },
|
|
29147
|
+
reversalCostsMore: { type: "boolean", description: "(d) Reversing it costs more than making it did." }
|
|
29148
|
+
},
|
|
29149
|
+
required: ["alternativesWereReal", "constrainsFutureWork", "arguableToday", "reversalCostsMore"]
|
|
29150
|
+
}
|
|
29151
|
+
},
|
|
29152
|
+
required: ["title", "body", "tests"]
|
|
28926
29153
|
}
|
|
28927
29154
|
},
|
|
28928
29155
|
required: ["task_id", "stage", "verdict", "comments"],
|
|
@@ -29538,6 +29765,30 @@ Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
|
|
|
29538
29765
|
}
|
|
29539
29766
|
} catch {
|
|
29540
29767
|
}
|
|
29768
|
+
let proposalNote = "";
|
|
29769
|
+
if (args.proposal !== void 0) {
|
|
29770
|
+
if (!(stage === "build-acceptance" && verdict === "accept")) {
|
|
29771
|
+
proposalNote = "\n\n---\n\n**Proposal not recorded.** `proposal` is only accepted on a build-acceptance `accept` \u2014 nothing has settled on a handoff-review or a non-accept verdict.\n\nThe review itself is unaffected \u2014 only the proposal was skipped.";
|
|
29772
|
+
} else {
|
|
29773
|
+
const validated = validateProposal(args.proposal);
|
|
29774
|
+
if ("error" in validated) {
|
|
29775
|
+
proposalNote = `
|
|
29776
|
+
|
|
29777
|
+
---
|
|
29778
|
+
|
|
29779
|
+
**Proposal not recorded.** ${validated.error}
|
|
29780
|
+
|
|
29781
|
+
The review itself is unaffected \u2014 only the proposal failed.`;
|
|
29782
|
+
} else {
|
|
29783
|
+
const routing = routeProposal(validated.proposal);
|
|
29784
|
+
const applied = await applyProposal(adapter2, routing, validated.proposal, {
|
|
29785
|
+
cycleNumber: result.currentCycle,
|
|
29786
|
+
sourceTaskId: result.taskId
|
|
29787
|
+
});
|
|
29788
|
+
proposalNote = formatProposalOutcome(routing, validated.proposal, applied);
|
|
29789
|
+
}
|
|
29790
|
+
}
|
|
29791
|
+
}
|
|
29541
29792
|
tracker.mark("format-response");
|
|
29542
29793
|
return textResponse(
|
|
29543
29794
|
`**${result.stageLabel}** recorded for ${result.taskId}.
|
|
@@ -29545,7 +29796,7 @@ Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
|
|
|
29545
29796
|
- **Verdict:** ${result.verdict}
|
|
29546
29797
|
- **Comments:** ${trimForEcho(result.comments)}
|
|
29547
29798
|
|
|
29548
|
-
${statusNote}${capabilityReviewSkippedNote}${autoReviewNote}${securityNote}${adConflictNote}${unblockNote}${docClosureNote}${regenNote}${mergeNote}${overlapNote}${batchSummaryNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
|
|
29799
|
+
${statusNote}${capabilityReviewSkippedNote}${autoReviewNote}${securityNote}${adConflictNote}${unblockNote}${docClosureNote}${regenNote}${mergeNote}${overlapNote}${batchSummaryNote}${autoReleaseNote}${nextStepNote}${phaseNote}${proposalNote}`
|
|
29549
29800
|
);
|
|
29550
29801
|
} catch (err) {
|
|
29551
29802
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -30579,6 +30830,41 @@ function formatUnblockSection(candidates) {
|
|
|
30579
30830
|
return lines.join("\n");
|
|
30580
30831
|
}
|
|
30581
30832
|
|
|
30833
|
+
// src/lib/decision-drafts.ts
|
|
30834
|
+
async function findPendingDecisionDrafts(adapter2) {
|
|
30835
|
+
let blockedProbe;
|
|
30836
|
+
try {
|
|
30837
|
+
blockedProbe = await adapter2.queryBoard({ status: ["Blocked"], compact: true });
|
|
30838
|
+
} catch {
|
|
30839
|
+
return [];
|
|
30840
|
+
}
|
|
30841
|
+
if (blockedProbe.length === 0) return [];
|
|
30842
|
+
const drafts = [];
|
|
30843
|
+
for (const task of blockedProbe) {
|
|
30844
|
+
const blocker = task.blocker;
|
|
30845
|
+
if (!blocker || blocker.type !== "decision-gate" || blocker.ref !== PROPOSED_DECISION_REF) continue;
|
|
30846
|
+
const evidence = (task.notes ?? "").split("\n")[0]?.trim() ?? "";
|
|
30847
|
+
drafts.push({
|
|
30848
|
+
taskId: task.displayId ?? task.id,
|
|
30849
|
+
taskTitle: task.title,
|
|
30850
|
+
evidence
|
|
30851
|
+
});
|
|
30852
|
+
}
|
|
30853
|
+
return drafts;
|
|
30854
|
+
}
|
|
30855
|
+
function formatPendingDecisionDrafts(drafts) {
|
|
30856
|
+
if (drafts.length === 0) return "";
|
|
30857
|
+
const lines = ["## Decisions Awaiting Your Call"];
|
|
30858
|
+
lines.push(
|
|
30859
|
+
`${drafts.length} decision${drafts.length === 1 ? "" : "s"} proposed during a build, waiting on you \u2014 mint with \`strategy_change\` to make it a real Active Decision, or \`board_edit <task> status:Cancelled\` to dismiss it. Neither is automatic.`
|
|
30860
|
+
);
|
|
30861
|
+
lines.push("");
|
|
30862
|
+
for (const d of drafts) {
|
|
30863
|
+
lines.push(`- **${formatRef(d.taskId, d.taskTitle)}**${d.evidence ? ` \u2014 ${d.evidence}` : ""}`);
|
|
30864
|
+
}
|
|
30865
|
+
return lines.join("\n");
|
|
30866
|
+
}
|
|
30867
|
+
|
|
30582
30868
|
// src/lib/deferred-gate.ts
|
|
30583
30869
|
var GATE_PHRASES = [
|
|
30584
30870
|
"depends on",
|
|
@@ -32121,7 +32407,8 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
|
|
|
32121
32407
|
}
|
|
32122
32408
|
let unrecordedNote2 = "";
|
|
32123
32409
|
try {
|
|
32124
|
-
const
|
|
32410
|
+
const { compareRef } = getBaseDivergence(config2.projectRoot, config2.baseBranch, { timeoutMs: ORIGIN_FETCH_TIMEOUT_MS });
|
|
32411
|
+
const unrecorded = detectUnrecordedCommits(config2.projectRoot, compareRef);
|
|
32125
32412
|
if (unrecorded.length > 0) {
|
|
32126
32413
|
const doneTasks = await adapter2.queryBoard({ status: ["Done"] });
|
|
32127
32414
|
const adHocDoneTasks = doneTasks.filter((t) => t.cycle == null);
|
|
@@ -32250,8 +32537,14 @@ ${versionDrift}` : "";
|
|
|
32250
32537
|
}
|
|
32251
32538
|
tracker.mark("parallel-tail");
|
|
32252
32539
|
const sharedContributorsPromise = adapterSupports(adapter2, "listContributors") ? adapter2.listContributors().catch(() => []) : Promise.resolve([]);
|
|
32253
|
-
const [unblockCandidates, subAgents, teamSummaryLine, releaseHistoryLine, cohortVisibilityLine, carryForwardRefs] = await Promise.all([
|
|
32540
|
+
const [unblockCandidates, pendingDecisionDrafts, subAgents, teamSummaryLine, releaseHistoryLine, cohortVisibilityLine, carryForwardRefs] = await Promise.all([
|
|
32254
32541
|
tracked("unblock-candidates", () => findUnblockCandidates(adapter2, currentCycle2))().catch(() => []),
|
|
32542
|
+
// task-3388: pending decision-proposal drafts (build_execute/review_submit's
|
|
32543
|
+
// `proposal` param, task-3273/3388) — mandatory reader, deliberately
|
|
32544
|
+
// uncapped and separate from the housekeeping-suggestion unblock list above
|
|
32545
|
+
// (see lib/decision-drafts.ts for why). Runs on every orient call, not
|
|
32546
|
+
// gated behind deep_housekeeping — same posture as unblock-candidates.
|
|
32547
|
+
tracked("pending-decision-drafts", () => findPendingDecisionDrafts(adapter2))().catch(() => []),
|
|
32255
32548
|
// task-1866: discover project sub-agents for the orient surface (read-only, never throws).
|
|
32256
32549
|
listAgents(config2.projectRoot),
|
|
32257
32550
|
// task-2071 (MU-3) + task-2072 (MU-5): team summary + release-history — both
|
|
@@ -32270,6 +32563,10 @@ ${versionDrift}` : "";
|
|
|
32270
32563
|
const unblockNote = unblockSection ? `
|
|
32271
32564
|
|
|
32272
32565
|
${unblockSection}` : "";
|
|
32566
|
+
const decisionDraftsSection = formatPendingDecisionDrafts(pendingDecisionDrafts);
|
|
32567
|
+
const decisionDraftsNote = decisionDraftsSection ? `
|
|
32568
|
+
|
|
32569
|
+
${decisionDraftsSection}` : "";
|
|
32273
32570
|
const teamSummary = [teamSummaryLine, releaseHistoryLine, cohortVisibilityLine].filter(Boolean).join("\n") || void 0;
|
|
32274
32571
|
let deferredGateNote = "";
|
|
32275
32572
|
if (deepHousekeeping) {
|
|
@@ -32300,7 +32597,42 @@ ${section}`;
|
|
|
32300
32597
|
const runtimeIdentityNote = `
|
|
32301
32598
|
|
|
32302
32599
|
${formatRuntimeIdentity(getRuntimeIdentity(config2, serverVersion))}`;
|
|
32303
|
-
|
|
32600
|
+
let effDeepHint = deepHint;
|
|
32601
|
+
let effOnboardingCoachingNote = onboardingCoachingNote;
|
|
32602
|
+
let effStaleSkillsNote = staleSkillsNote;
|
|
32603
|
+
let effResearchSignalsNote = researchSignalsNote;
|
|
32604
|
+
const droppedNotes = [];
|
|
32605
|
+
const assembleOrientOutput = () => projectOverrideLine + projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName, carryForwardRefs) + runtimeIdentityNote + unblockNote + decisionDraftsNote + deferredGateNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + effStaleSkillsNote + effResearchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + effOnboardingCoachingNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + effDeepHint + enrichmentFilesSection;
|
|
32606
|
+
const orientBudgetSoft = Number(process.env.PAPI_ORIENT_CONTEXT_BUDGET) || 6e4;
|
|
32607
|
+
let assembled = assembleOrientOutput();
|
|
32608
|
+
if (Buffer.byteLength(assembled, "utf-8") > orientBudgetSoft && effDeepHint) {
|
|
32609
|
+
effDeepHint = "";
|
|
32610
|
+
droppedNotes.push("deep-housekeeping tip");
|
|
32611
|
+
assembled = assembleOrientOutput();
|
|
32612
|
+
}
|
|
32613
|
+
if (Buffer.byteLength(assembled, "utf-8") > orientBudgetSoft && effOnboardingCoachingNote) {
|
|
32614
|
+
effOnboardingCoachingNote = "";
|
|
32615
|
+
droppedNotes.push("onboarding coaching");
|
|
32616
|
+
assembled = assembleOrientOutput();
|
|
32617
|
+
}
|
|
32618
|
+
if (Buffer.byteLength(assembled, "utf-8") > orientBudgetSoft && effStaleSkillsNote) {
|
|
32619
|
+
effStaleSkillsNote = "";
|
|
32620
|
+
droppedNotes.push("stale skills");
|
|
32621
|
+
assembled = assembleOrientOutput();
|
|
32622
|
+
}
|
|
32623
|
+
if (Buffer.byteLength(assembled, "utf-8") > orientBudgetSoft && effResearchSignalsNote) {
|
|
32624
|
+
effResearchSignalsNote = "";
|
|
32625
|
+
droppedNotes.push("research signals");
|
|
32626
|
+
assembled = assembleOrientOutput();
|
|
32627
|
+
}
|
|
32628
|
+
if (droppedNotes.length > 0) {
|
|
32629
|
+
assembled += `
|
|
32630
|
+
|
|
32631
|
+
---
|
|
32632
|
+
|
|
32633
|
+
*Context budget: ${droppedNotes.length} advisory note(s) omitted to keep this orient payload under ~${Math.round(orientBudgetSoft / 1024)} KB \u2014 ${droppedNotes.join(", ")}. Raise \`PAPI_ORIENT_CONTEXT_BUDGET\` in the MCP server env, or re-run without \`deep_housekeeping\`/\`full\` for a smaller payload by default.*`;
|
|
32634
|
+
}
|
|
32635
|
+
return { ...textResponse(assembled), _cycleNumber: healthResult.cycleNumber };
|
|
32304
32636
|
} catch (err) {
|
|
32305
32637
|
const message = err instanceof Error ? err.message : String(err);
|
|
32306
32638
|
const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
|
|
@@ -33561,7 +33893,7 @@ var decisionResolveTool = {
|
|
|
33561
33893
|
required: ["ad_id", "action"]
|
|
33562
33894
|
}
|
|
33563
33895
|
};
|
|
33564
|
-
function
|
|
33896
|
+
function findDecision2(decisions, adId) {
|
|
33565
33897
|
const trimmed = adId.trim();
|
|
33566
33898
|
return decisions.find((d) => d.id === trimmed || d.displayId === trimmed);
|
|
33567
33899
|
}
|
|
@@ -33594,7 +33926,7 @@ async function handleDecisionResolve(adapter2, config2, args) {
|
|
|
33594
33926
|
} catch (err) {
|
|
33595
33927
|
return errorResponse(`Failed to read active decisions: ${err instanceof Error ? err.message : String(err)}`);
|
|
33596
33928
|
}
|
|
33597
|
-
const target =
|
|
33929
|
+
const target = findDecision2(decisions, adId);
|
|
33598
33930
|
if (!target) return errorResponse(`AD not found: ${adId}. Run ad_view to see available decisions.`);
|
|
33599
33931
|
if (target.resolutionState !== "proposed") {
|
|
33600
33932
|
return errorResponse(
|
package/dist/prompts.js
CHANGED
|
@@ -78,6 +78,9 @@ Why now: [justification]
|
|
|
78
78
|
DEPENDS ON
|
|
79
79
|
[Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
|
|
80
80
|
|
|
81
|
+
RELEVANT ACTIVE DECISIONS
|
|
82
|
+
[Optional \u2014 self-check which Active Decisions actually bind the FILES LIKELY TOUCHED and module below, out of the full AD list already in your context. List the top 3-6 as "AD-N (title) \u2014 one-line reason this AD binds the files/approach". Rank by blast radius (an AD that would be VIOLATED by a naive implementation outranks one that's merely thematically adjacent). This is a judgement call \u2014 no server-side scoring feeds this. Omit this section entirely when nothing in the AD list is genuinely relevant to what this task touches; do not force a match.]
|
|
83
|
+
|
|
81
84
|
SCOPE (DO THIS)
|
|
82
85
|
[specific deliverables \u2014 write for the simplest viable path first]
|
|
83
86
|
|
|
@@ -1413,6 +1416,9 @@ Why now: [justification]
|
|
|
1413
1416
|
DEPENDS ON
|
|
1414
1417
|
[Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
|
|
1415
1418
|
|
|
1419
|
+
RELEVANT ACTIVE DECISIONS
|
|
1420
|
+
[Optional \u2014 self-check which Active Decisions actually bind the FILES LIKELY TOUCHED and module below, out of the full AD list already in your context. List the top 3-6 as "AD-N (title) \u2014 one-line reason this AD binds the files/approach". Rank by blast radius (an AD that would be VIOLATED by a naive implementation outranks one that's merely thematically adjacent). This is a judgement call \u2014 no server-side scoring feeds this. Omit this section entirely when nothing in the AD list is genuinely relevant to what this task touches; do not force a match.]
|
|
1421
|
+
|
|
1416
1422
|
SCOPE (DO THIS)
|
|
1417
1423
|
[specific deliverables \u2014 write for the simplest viable path first]
|
|
1418
1424
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "PAPI MCP server
|
|
3
|
+
"version": "0.7.105",
|
|
4
|
+
"description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"mcpName": "io.github.getpapi/papi",
|
|
7
7
|
"type": "module",
|