@papi-ai/server 0.7.67 → 0.7.70
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/index.js +1486 -428
- package/dist/prompts.js +65 -21
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5210,6 +5210,7 @@ function loadConfig() {
|
|
|
5210
5210
|
const baseBranch = process.env.PAPI_BASE_BRANCH ?? "main";
|
|
5211
5211
|
const autoPR = process.env.PAPI_AUTO_PR !== "false";
|
|
5212
5212
|
const lightMode = process.env.PAPI_LIGHT_MODE === "true";
|
|
5213
|
+
const autoWorktree = process.env.PAPI_AUTO_WORKTREE === "true";
|
|
5213
5214
|
const projectOwner = process.env.PAPI_OWNER ?? "Cathal";
|
|
5214
5215
|
const skipProjectSpecificRules = process.env.PAPI_SKIP_PROJECT_RULES === "true";
|
|
5215
5216
|
const userId = process.env.PAPI_USER_ID || void 0;
|
|
@@ -5283,8 +5284,13 @@ Already have an account? Make sure PAPI_USER_ID is set in your .mcp.json env con
|
|
|
5283
5284
|
baseBranch,
|
|
5284
5285
|
autoPR,
|
|
5285
5286
|
adapterType,
|
|
5287
|
+
// Local stdio boot: the server runs on the user's machine, so the client
|
|
5288
|
+
// shares its filesystem. The HTTP transport overrides this to false per
|
|
5289
|
+
// request (task-2906).
|
|
5290
|
+
localFilesystem: true,
|
|
5286
5291
|
papiEndpoint,
|
|
5287
5292
|
lightMode,
|
|
5293
|
+
autoWorktree,
|
|
5288
5294
|
projectOwner,
|
|
5289
5295
|
skipProjectSpecificRules,
|
|
5290
5296
|
userId,
|
|
@@ -9475,11 +9481,16 @@ function buildPlanUserMessage(ctx) {
|
|
|
9475
9481
|
""
|
|
9476
9482
|
);
|
|
9477
9483
|
}
|
|
9478
|
-
|
|
9479
|
-
|
|
9484
|
+
const enrichment = [];
|
|
9485
|
+
const addEnrichment = (label, ...lines) => {
|
|
9486
|
+
enrichment.push({ label, block: lines.join("\n") });
|
|
9487
|
+
};
|
|
9488
|
+
if (ctx.strategyRecommendations) {
|
|
9489
|
+
addEnrichment("Strategy Recommendations (Pending)", "### Strategy Recommendations (Pending)", "", ctx.strategyRecommendations, "");
|
|
9480
9490
|
}
|
|
9481
9491
|
if (ctx.learningPatterns) {
|
|
9482
|
-
|
|
9492
|
+
addEnrichment(
|
|
9493
|
+
"Recurring Learning Patterns",
|
|
9483
9494
|
"### Recurring Learning Patterns (cross-cycle)",
|
|
9484
9495
|
"",
|
|
9485
9496
|
"Tags from cycle learnings (surprises, issues, dead-ends, estimation misses) that keep recurring. Treat a module/theme that recurs across many cycles as a signal: size its tasks up and write a sharper pre-mortem.",
|
|
@@ -9489,7 +9500,8 @@ function buildPlanUserMessage(ctx) {
|
|
|
9489
9500
|
);
|
|
9490
9501
|
}
|
|
9491
9502
|
if (ctx.decisionScorePatterns) {
|
|
9492
|
-
|
|
9503
|
+
addEnrichment(
|
|
9504
|
+
"Decision Risk Scores",
|
|
9493
9505
|
"### Decision Risk Scores (high-risk + movement)",
|
|
9494
9506
|
"",
|
|
9495
9507
|
"Per-decision risk scores (effort+risk+reversibility+scaleCost+lockIn, /25; lower = safer). Decisions flagged \u26A0 high-risk (>15/25) or whose score moved since the last scoring. When a task depends on or advances a \u26A0 high-risk or \u2191 riskier decision, name that decision in the BUILD HANDOFF pre-mortem and lean toward the larger estimate. Do NOT re-score decisions here \u2014 this is read-only input.",
|
|
@@ -9498,38 +9510,39 @@ function buildPlanUserMessage(ctx) {
|
|
|
9498
9510
|
""
|
|
9499
9511
|
);
|
|
9500
9512
|
}
|
|
9513
|
+
if (ctx.estimationCalibration) {
|
|
9514
|
+
addEnrichment("Estimation Calibration", "### Estimation Calibration (Historical)", "", ctx.estimationCalibration, "");
|
|
9515
|
+
}
|
|
9501
9516
|
if (ctx.reviewPatterns) {
|
|
9502
|
-
|
|
9517
|
+
addEnrichment("Review Patterns", "### Review Patterns", "", ctx.reviewPatterns, "");
|
|
9503
9518
|
}
|
|
9504
9519
|
if (ctx.methodologyMetrics) {
|
|
9505
|
-
|
|
9506
|
-
}
|
|
9507
|
-
if (ctx.estimationCalibration) {
|
|
9508
|
-
parts.push("### Estimation Calibration (Historical)", "", ctx.estimationCalibration, "");
|
|
9520
|
+
addEnrichment("Methodology Trends", "### Methodology Trends", "", ctx.methodologyMetrics, "");
|
|
9509
9521
|
}
|
|
9510
9522
|
if (ctx.horizonContext) {
|
|
9511
|
-
|
|
9512
|
-
}
|
|
9513
|
-
if (ctx.strategyRecommendations) {
|
|
9514
|
-
parts.push("### Strategy Recommendations (Pending)", "", ctx.strategyRecommendations, "");
|
|
9523
|
+
addEnrichment("Forward Horizon", "### Forward Horizon", "", ctx.horizonContext, "");
|
|
9515
9524
|
}
|
|
9516
|
-
if (ctx.
|
|
9517
|
-
|
|
9525
|
+
if (ctx.buildPatterns) {
|
|
9526
|
+
addEnrichment("Build Patterns", "### Build Patterns", "", ctx.buildPatterns, "");
|
|
9518
9527
|
}
|
|
9519
9528
|
if (ctx.taskComments) {
|
|
9520
|
-
|
|
9529
|
+
addEnrichment("Task Discussion Threads", "### Task Discussion Threads", "", ctx.taskComments, "");
|
|
9521
9530
|
}
|
|
9522
9531
|
if (ctx.recentReviews) {
|
|
9523
|
-
|
|
9532
|
+
addEnrichment("Human Reviews", "### Human Reviews", "", ctx.recentReviews, "");
|
|
9533
|
+
}
|
|
9534
|
+
if (ctx.dogfoodEntries) {
|
|
9535
|
+
addEnrichment("Dogfood Observations", "### Dogfood Observations (Recent)", "", ctx.dogfoodEntries, "");
|
|
9524
9536
|
}
|
|
9525
9537
|
if (ctx.discoveryCanvas) {
|
|
9526
|
-
|
|
9538
|
+
addEnrichment("Discovery Canvas", "### Discovery Canvas", "", ctx.discoveryCanvas, "");
|
|
9527
9539
|
}
|
|
9528
9540
|
if (ctx.registeredDocs) {
|
|
9529
|
-
|
|
9541
|
+
addEnrichment("Relevant Research Docs", "### Relevant Research Docs", "", ctx.registeredDocs, "");
|
|
9530
9542
|
}
|
|
9531
9543
|
if (ctx.unactionedDocs) {
|
|
9532
|
-
|
|
9544
|
+
addEnrichment(
|
|
9545
|
+
"Unactioned Research",
|
|
9533
9546
|
"### Unactioned Research (soft-warn)",
|
|
9534
9547
|
"",
|
|
9535
9548
|
"These registered docs still have pending actions. Before adding net-new tasks, consider whether one of these unactioned items should be promoted into this cycle via `doc_action_promote`. Surface this in your plan output so the user can decide.",
|
|
@@ -9539,7 +9552,31 @@ function buildPlanUserMessage(ctx) {
|
|
|
9539
9552
|
);
|
|
9540
9553
|
}
|
|
9541
9554
|
if (ctx.carryForwardStaleness) {
|
|
9542
|
-
|
|
9555
|
+
addEnrichment("Carry-Forward Staleness", "### Carry-Forward Staleness", "", ctx.carryForwardStaleness, "");
|
|
9556
|
+
}
|
|
9557
|
+
const contextBudget = Number(process.env.PAPI_PLAN_CONTEXT_BUDGET) || 9e4;
|
|
9558
|
+
const coreBytes = Buffer.byteLength(parts.join("\n"), "utf-8");
|
|
9559
|
+
let enrichmentSpent = 0;
|
|
9560
|
+
const elidedLabels = [];
|
|
9561
|
+
for (const { label, block } of enrichment) {
|
|
9562
|
+
const cost = Buffer.byteLength(`
|
|
9563
|
+
${block}`, "utf-8");
|
|
9564
|
+
if (coreBytes + enrichmentSpent + cost > contextBudget) {
|
|
9565
|
+
elidedLabels.push(label);
|
|
9566
|
+
continue;
|
|
9567
|
+
}
|
|
9568
|
+
parts.push(block);
|
|
9569
|
+
enrichmentSpent += cost;
|
|
9570
|
+
}
|
|
9571
|
+
if (elidedLabels.length > 0) {
|
|
9572
|
+
parts.push(
|
|
9573
|
+
"### Context Budget \u2014 enrichment sections trimmed",
|
|
9574
|
+
"",
|
|
9575
|
+
`${elidedLabels.length} non-essential context section(s) were omitted to keep this plan payload under ${Math.round(contextBudget / 1024)} KB \u2014 a mature board otherwise overflows the client's tool-result ceiling and the plan output is spilled to a file. Trimmed (lowest-value first): ${elidedLabels.join(", ")}.`,
|
|
9576
|
+
"",
|
|
9577
|
+
"These are cross-cycle analytics/enrichment, NOT task-selection inputs \u2014 the Board, Active Decisions, Product Brief, Full Notes, and Recent Build Reports above are complete and untrimmed. Plan normally. If you specifically need one of the trimmed sections, raise `PAPI_PLAN_CONTEXT_BUDGET` in the MCP server env and re-run.",
|
|
9578
|
+
""
|
|
9579
|
+
);
|
|
9543
9580
|
}
|
|
9544
9581
|
}
|
|
9545
9582
|
return parts.join("\n");
|
|
@@ -9928,6 +9965,19 @@ function buildReviewUserMessage(ctx) {
|
|
|
9928
9965
|
} else {
|
|
9929
9966
|
parts.push("(Showing build reports and cycle log entries since last strategy review.)");
|
|
9930
9967
|
}
|
|
9968
|
+
if (ctx.earnedPushback) {
|
|
9969
|
+
parts.push(
|
|
9970
|
+
"",
|
|
9971
|
+
"---",
|
|
9972
|
+
"",
|
|
9973
|
+
"## \u26A0\uFE0F EARNED ADVERSARIAL PUSHBACK",
|
|
9974
|
+
"",
|
|
9975
|
+
"This section appears ONLY because concrete signals crossed a threshold this window. Address each item in your review with a clearly-labelled pushback callout: verify the claim against the actual build reports / code / decisions before accepting it, and push back proportionally to the stakes. If verification shows a signal is benign, say so and move on. Do NOT manufacture additional pushback beyond what these signals support.",
|
|
9976
|
+
"",
|
|
9977
|
+
ctx.earnedPushback,
|
|
9978
|
+
""
|
|
9979
|
+
);
|
|
9980
|
+
}
|
|
9931
9981
|
parts.push(
|
|
9932
9982
|
"",
|
|
9933
9983
|
"---",
|
|
@@ -10679,6 +10729,14 @@ function validateHandoffScope(handoff) {
|
|
|
10679
10729
|
if (!isMeaningful(handoff.scopeBoundary)) invalid.push("scopeBoundary");
|
|
10680
10730
|
return invalid;
|
|
10681
10731
|
}
|
|
10732
|
+
function assertHandoffApplyPayloadNonEmpty(data, cycleNumber) {
|
|
10733
|
+
const handoffCount = data.cycleHandoffs?.length ?? 0;
|
|
10734
|
+
if (handoffCount === 0) {
|
|
10735
|
+
throw new Error(
|
|
10736
|
+
`Handoff apply rejected: trimmed/empty apply payload \u2014 nothing persisted. The parsed output for Cycle ${cycleNumber} carries NO handoffs (cycleHandoffs is empty), but handoff prepare only runs when at least one task needs a handoff \u2014 so the handoffs were dropped. The most likely cause is a truncated apply JSON: the output overflowed the client tool-result ceiling (see task-2905/2906). Re-run handoff_generate and resend the COMPLETE structured output \u2014 ensure the JSON after the <!-- PAPI_STRUCTURED_OUTPUT --> marker includes the full cycleHandoffs array.`
|
|
10737
|
+
);
|
|
10738
|
+
}
|
|
10739
|
+
}
|
|
10682
10740
|
async function prepareHandoffs(adapter2, _config, taskIds, force = false) {
|
|
10683
10741
|
const timer2 = startTimer();
|
|
10684
10742
|
const cycles = await adapter2.readCycles();
|
|
@@ -10735,10 +10793,8 @@ async function applyHandoffs(adapter2, rawLlmOutput, cycleNumber, force = false)
|
|
|
10735
10793
|
if (!data) {
|
|
10736
10794
|
throw new Error("Could not parse structured output. Ensure your output includes <!-- PAPI_STRUCTURED_OUTPUT --> with valid JSON.");
|
|
10737
10795
|
}
|
|
10796
|
+
assertHandoffApplyPayloadNonEmpty(data, cycleNumber);
|
|
10738
10797
|
const handoffs = data.cycleHandoffs ?? [];
|
|
10739
|
-
if (handoffs.length === 0) {
|
|
10740
|
-
throw new Error("No cycleHandoffs found in structured output. Ensure your output includes handoffs in the cycleHandoffs array.");
|
|
10741
|
-
}
|
|
10742
10798
|
const taskIdsToWrite = handoffs.map((h) => h.taskId);
|
|
10743
10799
|
const existingHandoffSet = /* @__PURE__ */ new Set();
|
|
10744
10800
|
try {
|
|
@@ -12794,11 +12850,24 @@ Run \`strategy_review\` first, or pass \`force: true\` to bypass this gate.`
|
|
|
12794
12850
|
}
|
|
12795
12851
|
return { mode, cycleNumber, strategyReviewWarning };
|
|
12796
12852
|
}
|
|
12853
|
+
function assertApplyPayloadNonEmpty(data, cycleNumber) {
|
|
12854
|
+
const handoffCount = data.cycleHandoffs?.length ?? 0;
|
|
12855
|
+
const taskIdCount = data.cycleTaskIds?.length ?? 0;
|
|
12856
|
+
const newTaskCount = data.newTasks?.length ?? 0;
|
|
12857
|
+
if (handoffCount + taskIdCount + newTaskCount === 0) {
|
|
12858
|
+
throw new Error(
|
|
12859
|
+
`Plan apply rejected: trimmed/partial apply payload \u2014 nothing persisted. The parsed plan for Cycle ${cycleNumber + 1} assigns NO tasks to the cycle (cycleHandoffs, cycleTaskIds and newTasks are all empty). Writing it would create a cycle row with zero task membership and no handoffs \u2014 the phantom-empty-cycle failure mode that renders an empty Team view. The most likely cause is a truncated apply JSON: the prepare payload overflowed the client tool-result ceiling (see task-2905/2906). Re-run the plan and resend the COMPLETE structured output \u2014 ensure the JSON after the <!-- PAPI_STRUCTURED_OUTPUT --> marker includes the full cycleHandoffs array (or cycleTaskIds when using skip_handoffs).`
|
|
12860
|
+
);
|
|
12861
|
+
}
|
|
12862
|
+
}
|
|
12797
12863
|
async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber, contextHashes, planRunMeta) {
|
|
12798
12864
|
const applyStartMs = Date.now();
|
|
12799
12865
|
const applyScope = await resolvePlanScope(adapter2, config2);
|
|
12800
12866
|
await assertSingleActiveCycle(adapter2, { allowNumber: cycleNumber + 1, userId: applyScope.callerUserId ?? void 0 });
|
|
12801
12867
|
const { displayText, data } = parseStructuredOutput(rawOutput);
|
|
12868
|
+
if (data) {
|
|
12869
|
+
assertApplyPayloadNonEmpty(data, cycleNumber);
|
|
12870
|
+
}
|
|
12802
12871
|
let resolvedDisplayText = displayText;
|
|
12803
12872
|
let autoCommitNote = "";
|
|
12804
12873
|
let priorityLockNote = "";
|
|
@@ -13276,7 +13345,15 @@ function buildSubagentDispatchPrompt(input) {
|
|
|
13276
13345
|
- "pass" = no blocking issues; "warn" = minor/non-blocking nits; "fail" = blocking issues that should send the build back. \`file\`/\`line\` are optional per finding. Empty \`findings\` is valid when clean.` : `- Read the system prompt and context exactly as the main agent would.
|
|
13277
13346
|
- Produce the full structured output the system prompt requires (Part 1 markdown + Part 2 JSON after \`<!-- PAPI_STRUCTURED_OUTPUT -->\`, or whatever the system prompt specifies).
|
|
13278
13347
|
- Return ONLY that output \u2014 no preamble, no commentary, no closing summary. The dispatching agent will pass your reply verbatim to the apply call.`;
|
|
13279
|
-
const contextBlock = isReview ?
|
|
13348
|
+
const contextBlock = input.contextFilePath ? isReview ? `The review rubric and the full build-under-review context have been written to a local file:
|
|
13349
|
+
|
|
13350
|
+
${input.contextFilePath}
|
|
13351
|
+
|
|
13352
|
+
Read that file IN FULL (it contains the rubric and the build report + diff) before you start. Do not skip it \u2014 it is your only source of context.` : `The system prompt and full planning context have been written to a local file:
|
|
13353
|
+
|
|
13354
|
+
${input.contextFilePath}
|
|
13355
|
+
|
|
13356
|
+
Read that file IN FULL before you produce anything \u2014 it contains the system prompt and all context you need. Do not skip it; it is your only source of context.` : isReview ? `<review_rubric>
|
|
13280
13357
|
${systemPrompt}
|
|
13281
13358
|
</review_rubric>
|
|
13282
13359
|
|
|
@@ -13591,6 +13668,58 @@ async function buildSessionGuidance(callerKey) {
|
|
|
13591
13668
|
return signals.slice(0, 3);
|
|
13592
13669
|
}
|
|
13593
13670
|
|
|
13671
|
+
// src/services/onboarding-coaching.ts
|
|
13672
|
+
var ONBOARDING_EARLY_CYCLE_MAX = 2;
|
|
13673
|
+
var MAX_COACHING_LINES = 4;
|
|
13674
|
+
var COACH_CONNECT_REPO = "No repository is linked to this project yet. Link your repo from the dashboard Settings (or capture it during `setup`) so builds, reviews, and releases attach to the right codebase.";
|
|
13675
|
+
var COACH_ROOT_DIR = "Before building, confirm this session is running from your project root directory, so commits and builds land against the right files.";
|
|
13676
|
+
var COACH_CLICKABLE_TASKS = "Task cards on your dashboard expand on click. Open one to read its full build handoff, comments, and history.";
|
|
13677
|
+
var COACH_OFF_CYCLE = 'When a request falls outside the current cycle, keep it in the loop: `idea "<what you want>"` parks it in the backlog, or promote it into the cycle and generate a build handoff before you build.';
|
|
13678
|
+
var COACH_SESSION_START = 'Start of a session: skim your Active Decisions (the AD summary from `orient`, or `strategy_review`) to reload your project direction, and capture anything new with `idea "<what you want>"`.';
|
|
13679
|
+
var COACH_NO_DECISIONS = 'No Active Decisions recorded yet. Capture your first with `idea "<a direction or constraint>"` so your project starts steering itself.';
|
|
13680
|
+
var COACH_BACKLOG_IMPORT = "Already tracking a backlog elsewhere (Linear, a CSV, a markdown checklist)? Bring it over in one pass with `backlog_import` instead of retyping it.";
|
|
13681
|
+
var ONBOARDING_COACHING_HEADING = "## Getting Started";
|
|
13682
|
+
function buildOnboardingCoaching(state) {
|
|
13683
|
+
const lines = [];
|
|
13684
|
+
const isEarly = state.cycleNumber <= ONBOARDING_EARLY_CYCLE_MAX;
|
|
13685
|
+
const { surface } = state;
|
|
13686
|
+
if (state.repoConnected === false && (surface === "orient" || surface === "plan")) {
|
|
13687
|
+
lines.push(COACH_CONNECT_REPO);
|
|
13688
|
+
}
|
|
13689
|
+
if (state.hasLocalWorkspace && (surface === "setup" || surface === "orient" && isEarly)) {
|
|
13690
|
+
lines.push(COACH_ROOT_DIR);
|
|
13691
|
+
}
|
|
13692
|
+
if ((surface === "orient" || surface === "setup") && isEarly) {
|
|
13693
|
+
lines.push(COACH_CLICKABLE_TASKS);
|
|
13694
|
+
}
|
|
13695
|
+
if ((surface === "orient" || surface === "plan") && state.hasActiveCycle && isEarly) {
|
|
13696
|
+
lines.push(COACH_OFF_CYCLE);
|
|
13697
|
+
}
|
|
13698
|
+
if (surface === "orient" && isEarly) {
|
|
13699
|
+
lines.push(state.hasActiveDecisions === false ? COACH_NO_DECISIONS : COACH_SESSION_START);
|
|
13700
|
+
}
|
|
13701
|
+
if (surface === "plan" && state.bootstrapPlan === true) {
|
|
13702
|
+
lines.push(COACH_BACKLOG_IMPORT);
|
|
13703
|
+
}
|
|
13704
|
+
return lines.slice(0, MAX_COACHING_LINES);
|
|
13705
|
+
}
|
|
13706
|
+
function formatOnboardingCoachingBlock(state) {
|
|
13707
|
+
const lines = buildOnboardingCoaching(state);
|
|
13708
|
+
if (lines.length === 0) return "";
|
|
13709
|
+
return `
|
|
13710
|
+
|
|
13711
|
+
${ONBOARDING_COACHING_HEADING}
|
|
13712
|
+
${lines.map((l) => `- ${l}`).join("\n")}`;
|
|
13713
|
+
}
|
|
13714
|
+
|
|
13715
|
+
// src/lib/hosted-mode.ts
|
|
13716
|
+
function isHostedTransport() {
|
|
13717
|
+
return Boolean(process.env.PORT || process.env.PAPI_HTTP_PORT);
|
|
13718
|
+
}
|
|
13719
|
+
function hasLocalWorkspace() {
|
|
13720
|
+
return !isHostedTransport();
|
|
13721
|
+
}
|
|
13722
|
+
|
|
13594
13723
|
// src/lib/per-caller-cache.ts
|
|
13595
13724
|
var DEFAULT_CALLER_KEY2 = "__default__";
|
|
13596
13725
|
var MAX_PER_CALLER_ENTRIES = 1e3;
|
|
@@ -13682,6 +13811,15 @@ function clearPrepareSpill(projectId, callerKey) {
|
|
|
13682
13811
|
} catch {
|
|
13683
13812
|
}
|
|
13684
13813
|
}
|
|
13814
|
+
function contextPath(projectId, callerKey) {
|
|
13815
|
+
const id = createHash2("sha256").update(`${projectId ?? "no-project"}|${callerKey ?? DEFAULT_CALLER_KEY3}`).digest("hex").slice(0, 16);
|
|
13816
|
+
return join3(tmpdir(), `papi-plan-context-${id}.md`);
|
|
13817
|
+
}
|
|
13818
|
+
function savePrepareContextFile(projectId, callerKey, content) {
|
|
13819
|
+
const path7 = contextPath(projectId, callerKey);
|
|
13820
|
+
writeFileSync2(path7, content, { mode: 384 });
|
|
13821
|
+
return path7;
|
|
13822
|
+
}
|
|
13685
13823
|
|
|
13686
13824
|
// src/tools/plan.ts
|
|
13687
13825
|
var planPrepareCache = new PerCallerCache();
|
|
@@ -13690,6 +13828,7 @@ var planTool = {
|
|
|
13690
13828
|
description: 'Run once per cycle to select tasks and generate BUILD HANDOFFs. Call after setup (first time) or after completing all builds AND running release for the previous cycle. Returns prioritised task recommendations with detailed implementation specs. NEVER call when unbuilt cycle tasks exist \u2014 build and release first. First call returns a planning prompt for you to execute (prepare phase). Then call again with mode "apply" and your output to write results. Use skip_handoffs=true for large backlogs \u2014 handoffs are then generated separately via `handoff_generate`.',
|
|
13691
13829
|
annotations: { title: "Plan Cycle", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
13692
13830
|
inputSchema: {
|
|
13831
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
13693
13832
|
type: "object",
|
|
13694
13833
|
properties: {
|
|
13695
13834
|
mode: {
|
|
@@ -13760,7 +13899,16 @@ var planTool = {
|
|
|
13760
13899
|
description: 'Apply-mode confirmation guard for planner-initiated cancellations (task-1727). When false (default), any boardCorrection with status="Cancelled" is SKIPPED and listed in the response so the user can confirm before destructive writes. Set true to apply the cancellations. board_edit-direct cancellations are unaffected \u2014 this guard is plan-only.'
|
|
13761
13900
|
}
|
|
13762
13901
|
},
|
|
13763
|
-
required: []
|
|
13902
|
+
required: [],
|
|
13903
|
+
// task-2802: mirror resolveLlmResponse — llm_response and llm_response_file are
|
|
13904
|
+
// mutually exclusive, and an apply call must carry exactly one of them.
|
|
13905
|
+
not: { required: ["llm_response", "llm_response_file"] },
|
|
13906
|
+
allOf: [
|
|
13907
|
+
{
|
|
13908
|
+
if: { properties: { mode: { const: "apply" } }, required: ["mode"] },
|
|
13909
|
+
then: { anyOf: [{ required: ["llm_response"] }, { required: ["llm_response_file"] }] }
|
|
13910
|
+
}
|
|
13911
|
+
]
|
|
13764
13912
|
}
|
|
13765
13913
|
};
|
|
13766
13914
|
function formatPlanResult(result) {
|
|
@@ -13815,6 +13963,10 @@ function formatPlanResult(result) {
|
|
|
13815
13963
|
} else {
|
|
13816
13964
|
lines.push("", `Next: run \`build_list\` to see your cycle tasks, then \`build_execute <task_id>\` to start building.`);
|
|
13817
13965
|
}
|
|
13966
|
+
if (result.onboardingCoaching && result.onboardingCoaching.length > 0) {
|
|
13967
|
+
lines.push("", ONBOARDING_COACHING_HEADING);
|
|
13968
|
+
for (const c of result.onboardingCoaching) lines.push(`- ${c}`);
|
|
13969
|
+
}
|
|
13818
13970
|
if (result.contextBytes !== void 0) {
|
|
13819
13971
|
const kb = (result.contextBytes / 1024).toFixed(1);
|
|
13820
13972
|
lines.push(`---`, `Context: ${kb}KB`);
|
|
@@ -13896,7 +14048,15 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13896
14048
|
}, tracker);
|
|
13897
14049
|
const planProjectInfo = adapter2.getProjectInfo ? await adapter2.getProjectInfo().catch(() => null) : null;
|
|
13898
14050
|
const projectBanner = planProjectInfo ? getProjectConnectionBanner(planProjectInfo.name, planProjectInfo.slug) ?? void 0 : void 0;
|
|
13899
|
-
const
|
|
14051
|
+
const onboardingCoaching = buildOnboardingCoaching({
|
|
14052
|
+
surface: "plan",
|
|
14053
|
+
repoConnected: planProjectInfo ? !!planProjectInfo.repo_url : void 0,
|
|
14054
|
+
hasLocalWorkspace: hasLocalWorkspace(),
|
|
14055
|
+
cycleNumber: result.cycleNumber + 1,
|
|
14056
|
+
hasActiveCycle: true,
|
|
14057
|
+
bootstrapPlan: result.mode === "bootstrap"
|
|
14058
|
+
});
|
|
14059
|
+
const response = formatPlanResult({ ...result, contextUtilisation: utilisation, contextBytes, skipHandoffs, projectBanner, onboardingCoaching });
|
|
13900
14060
|
return {
|
|
13901
14061
|
...response,
|
|
13902
14062
|
...contextBytes !== void 0 ? { _contextBytes: contextBytes } : {},
|
|
@@ -13929,6 +14089,27 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13929
14089
|
} else {
|
|
13930
14090
|
dispatch = "inline";
|
|
13931
14091
|
}
|
|
14092
|
+
const modeLabel = result.mode === "bootstrap" ? "Bootstrap" : "Full";
|
|
14093
|
+
const header = result.strategyReviewWarning ? `${result.strategyReviewWarning}
|
|
14094
|
+
` : "";
|
|
14095
|
+
let contextFilePath;
|
|
14096
|
+
if (config2.localFilesystem) {
|
|
14097
|
+
const contextDoc = `### System Prompt
|
|
14098
|
+
|
|
14099
|
+
${result.systemPrompt}
|
|
14100
|
+
|
|
14101
|
+
---
|
|
14102
|
+
|
|
14103
|
+
### Context
|
|
14104
|
+
|
|
14105
|
+
${result.userMessage}
|
|
14106
|
+
`;
|
|
14107
|
+
try {
|
|
14108
|
+
contextFilePath = savePrepareContextFile(adapter2.getProjectId?.(), callerKey, contextDoc);
|
|
14109
|
+
} catch {
|
|
14110
|
+
contextFilePath = void 0;
|
|
14111
|
+
}
|
|
14112
|
+
}
|
|
13932
14113
|
if (dispatch === "subagent") {
|
|
13933
14114
|
const dispatchPrompt = buildSubagentDispatchPrompt({
|
|
13934
14115
|
tool: "plan",
|
|
@@ -13937,16 +14118,34 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13937
14118
|
strategyReviewWarning: result.strategyReviewWarning,
|
|
13938
14119
|
systemPrompt: result.systemPrompt,
|
|
13939
14120
|
userMessage: result.userMessage,
|
|
13940
|
-
contextBytes: result.contextBytes
|
|
14121
|
+
contextBytes: result.contextBytes,
|
|
14122
|
+
contextFilePath
|
|
13941
14123
|
});
|
|
13942
|
-
const
|
|
14124
|
+
const dispatchHeader = result.strategyReviewWarning ? `${result.strategyReviewWarning}
|
|
13943
14125
|
|
|
13944
14126
|
` : "";
|
|
13945
|
-
return { ...textResponse(`${
|
|
14127
|
+
return { ...textResponse(`${dispatchHeader}${dispatchPrompt}`), _contextBytes: result.contextBytes };
|
|
14128
|
+
}
|
|
14129
|
+
if (contextFilePath) {
|
|
14130
|
+
const kb = result.contextBytes !== void 0 ? ` (~${(result.contextBytes / 1024).toFixed(0)} KB)` : "";
|
|
14131
|
+
const pathResponse = textResponse(
|
|
14132
|
+
`${header}## PAPI Cycle Plan \u2014 Prepare Phase (${modeLabel} Mode, Cycle ${result.cycleNumber + 1})
|
|
14133
|
+
|
|
14134
|
+
The full planning brief \u2014 system prompt + all context${kb} \u2014 has been written to a local file to keep it off the tool-result channel (it overflows the client ceiling on a mature board):
|
|
14135
|
+
|
|
14136
|
+
\`${contextFilePath}\`
|
|
14137
|
+
|
|
14138
|
+
**Do this:**
|
|
14139
|
+
1. **Read that file in full** \u2014 it is the system prompt and the entire planning context.
|
|
14140
|
+
2. Produce the complete plan output in TWO parts: Part 1 markdown with BUILD HANDOFF blocks, then after \`<!-- PAPI_STRUCTURED_OUTPUT -->\` a Part 2 JSON block.
|
|
14141
|
+
3. **Write your output to a local file**, then call \`plan\` again with:
|
|
14142
|
+
- \`mode\`: "apply"
|
|
14143
|
+
- \`llm_response_file\`: the absolute path to YOUR output file
|
|
14144
|
+
- \`cycle_number\`: ${result.cycleNumber + 1}
|
|
14145
|
+
- \`strategy_review_warning\`: "${result.strategyReviewWarning.replace(/"/g, '\\"')}"`
|
|
14146
|
+
);
|
|
14147
|
+
return { ...pathResponse, _contextBytes: result.contextBytes };
|
|
13946
14148
|
}
|
|
13947
|
-
const modeLabel = result.mode === "bootstrap" ? "Bootstrap" : "Full";
|
|
13948
|
-
const header = result.strategyReviewWarning ? `${result.strategyReviewWarning}
|
|
13949
|
-
` : "";
|
|
13950
14149
|
const response = textResponse(
|
|
13951
14150
|
`${header}## PAPI Cycle Plan \u2014 Prepare Phase (${modeLabel} Mode, Cycle ${result.cycleNumber + 1})
|
|
13952
14151
|
|
|
@@ -14009,14 +14208,6 @@ import { existsSync as existsSync2, readdirSync, statSync as statSync2 } from "f
|
|
|
14009
14208
|
import { join as join4 } from "path";
|
|
14010
14209
|
import { homedir as homedir2 } from "os";
|
|
14011
14210
|
|
|
14012
|
-
// src/lib/hosted-mode.ts
|
|
14013
|
-
function isHostedTransport() {
|
|
14014
|
-
return Boolean(process.env.PORT || process.env.PAPI_HTTP_PORT);
|
|
14015
|
-
}
|
|
14016
|
-
function hasLocalWorkspace() {
|
|
14017
|
-
return !isHostedTransport();
|
|
14018
|
-
}
|
|
14019
|
-
|
|
14020
14211
|
// src/services/idea.ts
|
|
14021
14212
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
14022
14213
|
var OWNER_ACTION_PATTERNS = [
|
|
@@ -14564,6 +14755,55 @@ function generateValueReport(snapshots) {
|
|
|
14564
14755
|
return lines.join("\n");
|
|
14565
14756
|
}
|
|
14566
14757
|
|
|
14758
|
+
// src/lib/earned-pushback.ts
|
|
14759
|
+
var SIGNAL_MIN_COUNT = 3;
|
|
14760
|
+
var EFFORT_RANK = { XS: 0, S: 1, M: 2, L: 3, XL: 4 };
|
|
14761
|
+
function rankEffort(size2) {
|
|
14762
|
+
if (!size2) return void 0;
|
|
14763
|
+
return EFFORT_RANK[size2];
|
|
14764
|
+
}
|
|
14765
|
+
function computeEarnedPushback(inputs) {
|
|
14766
|
+
const { reports, log: log2, doneTaskIds } = inputs;
|
|
14767
|
+
const drifted = reports.filter(
|
|
14768
|
+
(r) => r.scopeAccuracy && r.scopeAccuracy !== "accurate" || !!r.scopeDriftSignal
|
|
14769
|
+
);
|
|
14770
|
+
const crept = reports.filter((r) => {
|
|
14771
|
+
const actual = rankEffort(r.actualEffort);
|
|
14772
|
+
const estimated = rankEffort(r.estimatedEffort);
|
|
14773
|
+
return actual !== void 0 && estimated !== void 0 && actual > estimated;
|
|
14774
|
+
});
|
|
14775
|
+
const staleness = computeCarryForwardStaleness(log2, doneTaskIds);
|
|
14776
|
+
const driftFires = drifted.length >= SIGNAL_MIN_COUNT;
|
|
14777
|
+
const creepFires = crept.length >= SIGNAL_MIN_COUNT;
|
|
14778
|
+
const deferralFires = staleness !== void 0;
|
|
14779
|
+
if (!driftFires && !creepFires && !deferralFires) return void 0;
|
|
14780
|
+
const sections = [
|
|
14781
|
+
"The following signals crossed a threshold this window and warrant adversarial scrutiny. For each, verify the claim against the actual reports/code before accepting it, and push back proportionally to the stakes \u2014 do NOT wave it through, and do NOT invent pushback beyond what these signals support."
|
|
14782
|
+
];
|
|
14783
|
+
if (driftFires) {
|
|
14784
|
+
const named = drifted.slice(0, 8).map((r) => {
|
|
14785
|
+
const why = r.scopeDriftSignal ? `files diverged from handoff` : `scope ${r.scopeAccuracy}`;
|
|
14786
|
+
return ` - **${r.displayId ?? r.taskId}** (${r.taskName}) \u2014 ${why}`;
|
|
14787
|
+
}).join("\n");
|
|
14788
|
+
sections.push(
|
|
14789
|
+
`**Scope drift (${drifted.length} build${drifted.length === 1 ? "" : "s"}):** handoff scope did not match what was actually built. Verify whether the plan or the execution was wrong, and whether an Active Decision or planning heuristic needs to change.
|
|
14790
|
+
${named}`
|
|
14791
|
+
);
|
|
14792
|
+
}
|
|
14793
|
+
if (creepFires) {
|
|
14794
|
+
const named = crept.slice(0, 8).map((r) => ` - **${r.displayId ?? r.taskId}** (${r.taskName}) \u2014 estimated ${r.estimatedEffort}, actual ${r.actualEffort}`).join("\n");
|
|
14795
|
+
sections.push(
|
|
14796
|
+
`**Scope creep (${crept.length} task${crept.length === 1 ? "" : "s"}):** actual effort exceeded the estimate. Check whether these were under-scoped at plan time or grew mid-build, and whether cycle sizing is drifting.
|
|
14797
|
+
${named}`
|
|
14798
|
+
);
|
|
14799
|
+
}
|
|
14800
|
+
if (deferralFires) {
|
|
14801
|
+
sections.push(`**Repeat deferral (3+ consecutive cycles):**
|
|
14802
|
+
${staleness}`);
|
|
14803
|
+
}
|
|
14804
|
+
return sections.join("\n\n");
|
|
14805
|
+
}
|
|
14806
|
+
|
|
14567
14807
|
// src/services/strategy.ts
|
|
14568
14808
|
var STRATEGY_DUPE_COVERAGE_THRESHOLD = 0.6;
|
|
14569
14809
|
function taskStatusLabel(task) {
|
|
@@ -15071,10 +15311,10 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
|
|
|
15071
15311
|
try {
|
|
15072
15312
|
const comments = await adapter2.getRecentTaskComments?.(50);
|
|
15073
15313
|
if (comments && comments.length > 0) {
|
|
15074
|
-
const
|
|
15314
|
+
const doneTaskIds2 = new Set(recentDoneTasks.map((t) => t.id));
|
|
15075
15315
|
const inReviewTasks = activeTasks.filter((t) => t.status === "In Review");
|
|
15076
15316
|
const reviewWindowTaskIds = /* @__PURE__ */ new Set([
|
|
15077
|
-
...
|
|
15317
|
+
...doneTaskIds2,
|
|
15078
15318
|
...inReviewTasks.map((t) => t.id)
|
|
15079
15319
|
]);
|
|
15080
15320
|
const filtered = comments.filter((c) => reviewWindowTaskIds.has(c.taskId));
|
|
@@ -15104,7 +15344,7 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
|
|
|
15104
15344
|
try {
|
|
15105
15345
|
if (docsWithPendingActions && docsWithPendingActions.length > 0) {
|
|
15106
15346
|
const STALE_THRESHOLD = 20;
|
|
15107
|
-
const
|
|
15347
|
+
const doneTaskIds2 = new Set(recentDoneTasks.map((t) => t.displayId ?? t.id));
|
|
15108
15348
|
const completed = [];
|
|
15109
15349
|
const deferred = [];
|
|
15110
15350
|
const stale = [];
|
|
@@ -15114,7 +15354,7 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
|
|
|
15114
15354
|
const ageInCycles = cycleNumber - (doc.cycleCreated ?? cycleNumber);
|
|
15115
15355
|
for (const action of pendingActions) {
|
|
15116
15356
|
const line = ` - **${doc.title}** (C${doc.cycleCreated ?? "?"}): ${action.description}${action.linkedTaskId ? ` [\u2192${action.linkedTaskId}]` : ""}`;
|
|
15117
|
-
if (action.linkedTaskId &&
|
|
15357
|
+
if (action.linkedTaskId && doneTaskIds2.has(action.linkedTaskId)) {
|
|
15118
15358
|
completed.push(line);
|
|
15119
15359
|
} else if (ageInCycles > STALE_THRESHOLD) {
|
|
15120
15360
|
stale.push(line);
|
|
@@ -15161,6 +15401,10 @@ ${lines.join("\n")}`;
|
|
|
15161
15401
|
{ label: "taskComments", hasData: taskCommentsText !== void 0 },
|
|
15162
15402
|
{ label: "docActionStaleness", hasData: docActionStalenessText !== void 0 }
|
|
15163
15403
|
]);
|
|
15404
|
+
const doneTaskIds = new Set(
|
|
15405
|
+
recentDoneTasks.map((t) => t.displayId ?? t.id).filter((id) => !!id)
|
|
15406
|
+
);
|
|
15407
|
+
const earnedPushback = computeEarnedPushback({ reports, log: recentLog, doneTaskIds });
|
|
15164
15408
|
const context = {
|
|
15165
15409
|
sessionNumber: cycleNumber,
|
|
15166
15410
|
lastReviewCycle: lastReviewCycleNum,
|
|
@@ -15169,6 +15413,7 @@ ${lines.join("\n")}`;
|
|
|
15169
15413
|
allBuildReports: buildReportsText,
|
|
15170
15414
|
sessionLog: formatCycleLog(recentLog),
|
|
15171
15415
|
board: smartBoard,
|
|
15416
|
+
earnedPushback,
|
|
15172
15417
|
humanReviews: formatReviews(reviews),
|
|
15173
15418
|
buildPatterns: buildPatternsText,
|
|
15174
15419
|
reviewPatterns: reviewPatternsText,
|
|
@@ -15419,6 +15664,13 @@ ${cleanContent}`;
|
|
|
15419
15664
|
} catch {
|
|
15420
15665
|
}
|
|
15421
15666
|
}
|
|
15667
|
+
if (data.hierarchyUpdates && data.hierarchyUpdates.length > 0) {
|
|
15668
|
+
try {
|
|
15669
|
+
await applyHierarchyUpdates(adapter2, data.hierarchyUpdates);
|
|
15670
|
+
} catch (err) {
|
|
15671
|
+
console.error("[strategy] applyHierarchyUpdates failed:", err instanceof Error ? err.message : String(err));
|
|
15672
|
+
}
|
|
15673
|
+
}
|
|
15422
15674
|
const compressionThreshold = cycleNumber - 5;
|
|
15423
15675
|
if (compressionThreshold > 0 && data.sessionLogCompressionSummary) {
|
|
15424
15676
|
await adapter2.compressCycleLog(compressionThreshold, data.sessionLogCompressionSummary);
|
|
@@ -15696,6 +15948,79 @@ function buildPhaseLabel(phase) {
|
|
|
15696
15948
|
if (!numMatch) return phase.label;
|
|
15697
15949
|
return `Phase ${numMatch[1]}: ${phase.label}`;
|
|
15698
15950
|
}
|
|
15951
|
+
function nextHierarchySort(existing) {
|
|
15952
|
+
return existing.length === 0 ? 10 : Math.max(...existing.map((e) => e.sortOrder)) + 10;
|
|
15953
|
+
}
|
|
15954
|
+
async function applyHierarchyUpdates(adapter2, updates) {
|
|
15955
|
+
if (!adapter2.readStages || !adapter2.readHorizons) return;
|
|
15956
|
+
const findHorizon = async (ref) => (await adapter2.readHorizons()).find(
|
|
15957
|
+
(h) => h.slug === ref || h.id === ref || h.label.toLowerCase() === ref.toLowerCase()
|
|
15958
|
+
);
|
|
15959
|
+
const findStage = async (ref) => (await adapter2.readStages()).find(
|
|
15960
|
+
(s) => s.slug === ref || s.id === ref || s.label.toLowerCase() === ref.toLowerCase()
|
|
15961
|
+
);
|
|
15962
|
+
for (const u of updates) {
|
|
15963
|
+
if (u.level === "horizon") {
|
|
15964
|
+
if (u.action === "create") {
|
|
15965
|
+
if (!await findHorizon(u.slug) && adapter2.createHorizon) {
|
|
15966
|
+
const horizons = await adapter2.readHorizons();
|
|
15967
|
+
await adapter2.createHorizon({
|
|
15968
|
+
slug: u.slug,
|
|
15969
|
+
label: u.label ?? u.slug,
|
|
15970
|
+
status: u.status ?? "In Progress",
|
|
15971
|
+
sortOrder: u.sortOrder ?? nextHierarchySort(horizons)
|
|
15972
|
+
});
|
|
15973
|
+
}
|
|
15974
|
+
} else if (u.action === "update_status" && u.status && adapter2.updateHorizonStatus) {
|
|
15975
|
+
const h = await findHorizon(u.slug);
|
|
15976
|
+
if (h) await adapter2.updateHorizonStatus(h.id, u.status);
|
|
15977
|
+
}
|
|
15978
|
+
continue;
|
|
15979
|
+
}
|
|
15980
|
+
if (u.action === "update_criterion" && u.criterionId && typeof u.met === "boolean" && adapter2.setCriterionMet) {
|
|
15981
|
+
const s = await findStage(u.slug);
|
|
15982
|
+
if (s) await adapter2.setCriterionMet(s.id, u.criterionId, u.met, u.evidence ?? null);
|
|
15983
|
+
} else if (u.action === "update_status" && u.status && adapter2.updateStageStatus) {
|
|
15984
|
+
const s = await findStage(u.slug);
|
|
15985
|
+
if (s) await adapter2.updateStageStatus(s.id, u.status);
|
|
15986
|
+
} else if (u.action === "create") {
|
|
15987
|
+
await createStageFromUpdate(adapter2, u, findStage, findHorizon);
|
|
15988
|
+
} else if (u.action === "advance" && adapter2.updateStageStatus) {
|
|
15989
|
+
const current = await findStage(u.slug);
|
|
15990
|
+
if (current) await adapter2.updateStageStatus(current.id, "Done");
|
|
15991
|
+
const newRef = u.newStageSlug ?? u.slug;
|
|
15992
|
+
const existingNext = await findStage(newRef);
|
|
15993
|
+
if (existingNext) {
|
|
15994
|
+
await adapter2.updateStageStatus(existingNext.id, "In Progress");
|
|
15995
|
+
} else if (u.newStageLabel) {
|
|
15996
|
+
await createStageFromUpdate(
|
|
15997
|
+
adapter2,
|
|
15998
|
+
{ ...u, slug: newRef, label: u.newStageLabel, status: "In Progress", horizon: u.horizon ?? current?.horizonId },
|
|
15999
|
+
findStage,
|
|
16000
|
+
findHorizon
|
|
16001
|
+
);
|
|
16002
|
+
}
|
|
16003
|
+
}
|
|
16004
|
+
}
|
|
16005
|
+
}
|
|
16006
|
+
async function createStageFromUpdate(adapter2, u, findStage, findHorizon) {
|
|
16007
|
+
if (!adapter2.createStage || !adapter2.readStages || !adapter2.readHorizons) return;
|
|
16008
|
+
if (await findStage(u.slug)) return;
|
|
16009
|
+
const horizons = await adapter2.readHorizons();
|
|
16010
|
+
const parent = u.horizon ? await findHorizon(u.horizon) : horizons.length === 1 ? horizons[0] : horizons.find((h) => h.status === "In Progress");
|
|
16011
|
+
if (!parent) return;
|
|
16012
|
+
const stagesInHorizon = (await adapter2.readStages()).filter((s) => s.horizonId === parent.id);
|
|
16013
|
+
const id = await adapter2.createStage({
|
|
16014
|
+
slug: u.slug,
|
|
16015
|
+
label: u.label ?? u.slug,
|
|
16016
|
+
status: u.status ?? "Not Started",
|
|
16017
|
+
sortOrder: u.sortOrder ?? nextHierarchySort(stagesInHorizon),
|
|
16018
|
+
horizonId: parent.id
|
|
16019
|
+
});
|
|
16020
|
+
if (id && u.exitCriteria?.length && adapter2.updateStageExitCriteria) {
|
|
16021
|
+
await adapter2.updateStageExitCriteria(id, u.exitCriteria);
|
|
16022
|
+
}
|
|
16023
|
+
}
|
|
15699
16024
|
async function applyPhaseUpdates(adapter2, currentPhases, updates) {
|
|
15700
16025
|
const phasesById = new Map(currentPhases.map((p) => [p.id, p]));
|
|
15701
16026
|
const labelMigrations = [];
|
|
@@ -16644,6 +16969,7 @@ var boardViewTool = {
|
|
|
16644
16969
|
description: 'View the Board. To find a SPECIFIC task or subset, FILTER FIRST \u2014 do not dump the whole board: pass task_id for one task (full detail), query="<text>" for a title/notes substring match, or cycle=<n> for one cycle. Combine with status/phase. By default shows active tasks only (excludes Done/Cancelled), sorted by priority, limited to 50; titles are truncated in the table (single-task lookup shows the full title). Use status="all" to see everything, mode="summary" for counts only. Does not call the Anthropic API.',
|
|
16645
16970
|
annotations: { title: "View Board", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
16646
16971
|
inputSchema: {
|
|
16972
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
16647
16973
|
type: "object",
|
|
16648
16974
|
properties: {
|
|
16649
16975
|
task_id: {
|
|
@@ -16668,10 +16994,14 @@ var boardViewTool = {
|
|
|
16668
16994
|
},
|
|
16669
16995
|
limit: {
|
|
16670
16996
|
type: "number",
|
|
16997
|
+
minimum: 1,
|
|
16998
|
+
default: 50,
|
|
16671
16999
|
description: "Max tasks to return (default: 50)."
|
|
16672
17000
|
},
|
|
16673
17001
|
offset: {
|
|
16674
17002
|
type: "number",
|
|
17003
|
+
minimum: 0,
|
|
17004
|
+
default: 0,
|
|
16675
17005
|
description: "Skip first N tasks for pagination (default: 0)."
|
|
16676
17006
|
},
|
|
16677
17007
|
mode: {
|
|
@@ -16688,6 +17018,7 @@ var boardDeprioritiseTool = {
|
|
|
16688
17018
|
description: `Remove a task from the current cycle. Four actions: "backlog" (not now, maybe later \u2014 preserves handoff), "defer" (valid but premature \u2014 hidden from planner), "block" (waiting on external dependency \u2014 visible on board but skipped by planner), "cancel" (don't want this \u2014 permanently closed with reason). When a user rejects a task, ALWAYS ask which action they want. Does not call the Anthropic API.`,
|
|
16689
17019
|
annotations: { title: "Deprioritise Task", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
16690
17020
|
inputSchema: {
|
|
17021
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
16691
17022
|
type: "object",
|
|
16692
17023
|
properties: {
|
|
16693
17024
|
task_id: {
|
|
@@ -16725,7 +17056,21 @@ var boardDeprioritiseTool = {
|
|
|
16725
17056
|
description: 'Optional new phase (only applies to "backlog" and "defer" actions).'
|
|
16726
17057
|
}
|
|
16727
17058
|
},
|
|
16728
|
-
required: ["task_id"]
|
|
17059
|
+
required: ["task_id"],
|
|
17060
|
+
// task-2802: mirror the handler's guards so the agent sees the dependency up
|
|
17061
|
+
// front. handleBoardDeprioritise requires `reason` for both block and cancel,
|
|
17062
|
+
// and requires `blocker_ref` whenever `blocker_type` is set. Keyed on explicit
|
|
17063
|
+
// values — an omitted action defaults to "backlog" and triggers neither.
|
|
17064
|
+
allOf: [
|
|
17065
|
+
{
|
|
17066
|
+
if: { properties: { action: { enum: ["block", "cancel"] } }, required: ["action"] },
|
|
17067
|
+
then: { required: ["reason"] }
|
|
17068
|
+
},
|
|
17069
|
+
{
|
|
17070
|
+
if: { required: ["blocker_type"] },
|
|
17071
|
+
then: { required: ["blocker_ref"] }
|
|
17072
|
+
}
|
|
17073
|
+
]
|
|
16729
17074
|
}
|
|
16730
17075
|
};
|
|
16731
17076
|
var boardArchiveTool = {
|
|
@@ -16752,7 +17097,15 @@ var boardEditTool = {
|
|
|
16752
17097
|
description: "Edit fields on an existing task. Supports title, priority, complexity, module, epic, phase, notes (with notes_mode for append/replace/clear), status, maturity, and cycle (number or null). Pass task_id plus any fields to update. Does not call the Anthropic API.",
|
|
16753
17098
|
annotations: { title: "Edit Task", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
16754
17099
|
inputSchema: {
|
|
17100
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
16755
17101
|
type: "object",
|
|
17102
|
+
// task-2802: shared effort-size enum referenced by estimated_effort/actual_effort.
|
|
17103
|
+
$defs: {
|
|
17104
|
+
effortSize: {
|
|
17105
|
+
type: "string",
|
|
17106
|
+
enum: ["XS", "S", "M", "L", "XL"]
|
|
17107
|
+
}
|
|
17108
|
+
},
|
|
16756
17109
|
properties: {
|
|
16757
17110
|
task_id: {
|
|
16758
17111
|
type: "string",
|
|
@@ -16808,13 +17161,11 @@ var boardEditTool = {
|
|
|
16808
17161
|
description: "Cycle assignment. Pass a cycle number to assign, or null to remove from any cycle. Validated against existing cycles. Replaces the prior workaround of editing cycle_tasks.cycle directly via SQL."
|
|
16809
17162
|
},
|
|
16810
17163
|
estimated_effort: {
|
|
16811
|
-
|
|
16812
|
-
enum: ["XS", "S", "M", "L", "XL"],
|
|
17164
|
+
$ref: "#/$defs/effortSize",
|
|
16813
17165
|
description: "task-2182: correct the estimated effort on this task's LATEST build report (fixes a mis-recorded estimate). Updates the build_reports row, not the task \u2014 feeds the next estimation-accuracy recompute."
|
|
16814
17166
|
},
|
|
16815
17167
|
actual_effort: {
|
|
16816
|
-
|
|
16817
|
-
enum: ["XS", "S", "M", "L", "XL"],
|
|
17168
|
+
$ref: "#/$defs/effortSize",
|
|
16818
17169
|
description: "task-2182: correct the actual effort on this task's LATEST build report (fixes a mis-recorded actual)."
|
|
16819
17170
|
}
|
|
16820
17171
|
},
|
|
@@ -17246,6 +17597,9 @@ var PROJECT_BUNDLE_REL = join6(".agents", "skills", "papi-cycle");
|
|
|
17246
17597
|
function bundleDestRel(rel) {
|
|
17247
17598
|
return rel === "AGENTS.md" ? "AGENTS.md" : join6(PROJECT_BUNDLE_REL, rel);
|
|
17248
17599
|
}
|
|
17600
|
+
function claudeSkillDestRel(rel) {
|
|
17601
|
+
return rel === "AGENTS.md" ? void 0 : join6(".claude", "skills", rel);
|
|
17602
|
+
}
|
|
17249
17603
|
function resolveBundleDir() {
|
|
17250
17604
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
17251
17605
|
for (let i = 0; i < 5; i++) {
|
|
@@ -17274,10 +17628,13 @@ function readBundleFiles(bundleDir = resolveBundleDir()) {
|
|
|
17274
17628
|
function planBundleInstall(projectRoot, projectName, opts = {}) {
|
|
17275
17629
|
const out = {};
|
|
17276
17630
|
for (const f of readBundleFiles()) {
|
|
17277
|
-
const dest = join6(projectRoot, bundleDestRel(f.rel));
|
|
17278
|
-
if (opts.skipExisting && existsSync4(dest) && statSync4(dest).isFile()) continue;
|
|
17279
17631
|
const content = f.rel === "AGENTS.md" ? f.content.replace(/\{\{project_name\}\}/g, projectName) : f.content;
|
|
17280
|
-
|
|
17632
|
+
for (const rel of [bundleDestRel(f.rel), claudeSkillDestRel(f.rel)]) {
|
|
17633
|
+
if (!rel) continue;
|
|
17634
|
+
const dest = join6(projectRoot, rel);
|
|
17635
|
+
if (opts.skipExisting && existsSync4(dest) && statSync4(dest).isFile()) continue;
|
|
17636
|
+
out[dest] = content;
|
|
17637
|
+
}
|
|
17281
17638
|
}
|
|
17282
17639
|
return out;
|
|
17283
17640
|
}
|
|
@@ -17565,7 +17922,7 @@ var CLAUDE_MD_ENRICHMENT_SENTINEL_T1 = "<!-- PAPI_ENRICHMENT_TIER_1 -->";
|
|
|
17565
17922
|
var CLAUDE_MD_ENRICHMENT_SENTINEL_T2 = "<!-- PAPI_ENRICHMENT_TIER_2 -->";
|
|
17566
17923
|
var CLAUDE_MD_STUB = `# {{project_name}}
|
|
17567
17924
|
|
|
17568
|
-
This project is managed with **PAPI**. The agent harness \u2014 session workflow, the plan \u2192 build \u2192 review cycle, branching, and conventions \u2014 lives in **\`AGENTS.md\`** (always loaded) plus lazy-loaded phase skills under \`.agents/skills/papi-cycle
|
|
17925
|
+
This project is managed with **PAPI**. The agent harness \u2014 session workflow, the plan \u2192 build \u2192 review cycle, branching, and conventions \u2014 lives in **\`AGENTS.md\`** (always loaded) plus lazy-loaded phase skills under \`.agents/skills/papi-cycle/\` (Claude Code loads the same phase skills natively from \`.claude/skills/papi-*\`).
|
|
17569
17926
|
|
|
17570
17927
|
**At session start: read \`AGENTS.md\`, then run \`orient\`.** Phase mechanics (planning, building, strategy, ideas) load on demand from the skills bundle. Add project-specific notes below this line \u2014 they will not be overwritten.
|
|
17571
17928
|
|
|
@@ -18896,8 +19253,14 @@ ${result.warnings.map((w) => `- ${w}`).join("\n")}` : "";
|
|
|
18896
19253
|
**Important:** This is a remote PAPI connection, so the server could not write to your project directory. Setup prepared your files (${harnessFiles}, .claude/settings.json, docs/) and returned them in the scaffolding section below \u2014 **write each one to disk, then commit** before running \`build_execute\` (it requires a clean working directory).` : `
|
|
18897
19254
|
|
|
18898
19255
|
**Important:** Setup created/modified files (${harnessFiles}, .claude/settings.json, docs/). Commit these changes before running \`build_execute\` \u2014 it requires a clean working directory.`;
|
|
19256
|
+
const coachingNote = formatOnboardingCoachingBlock({
|
|
19257
|
+
surface: "setup",
|
|
19258
|
+
hasLocalWorkspace: hasLocalWorkspace(),
|
|
19259
|
+
cycleNumber: 0,
|
|
19260
|
+
hasActiveCycle: false
|
|
19261
|
+
});
|
|
18899
19262
|
return textResponse(
|
|
18900
|
-
`${prefix}Product Brief generated and saved.${briefRegenNote}${adNote}${northStarNote}${taskNote}${constraintsHint}${editorNote}${gitignoreNote}${warningsNote}${filesNote}
|
|
19263
|
+
`${prefix}Product Brief generated and saved.${briefRegenNote}${adNote}${northStarNote}${taskNote}${constraintsHint}${editorNote}${gitignoreNote}${warningsNote}${filesNote}${coachingNote}
|
|
18901
19264
|
|
|
18902
19265
|
Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-written brief.
|
|
18903
19266
|
|
|
@@ -19268,8 +19631,8 @@ function buildPapiMetaFramingDirective(caps, inner) {
|
|
|
19268
19631
|
|
|
19269
19632
|
// src/services/build.ts
|
|
19270
19633
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
19271
|
-
import { readdirSync as readdirSync5, existsSync as
|
|
19272
|
-
import { join as
|
|
19634
|
+
import { readdirSync as readdirSync5, existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
19635
|
+
import { join as join12 } from "path";
|
|
19273
19636
|
|
|
19274
19637
|
// src/lib/db-only-notices.ts
|
|
19275
19638
|
var DB_ONLY_START_NOTICE = "No git repo detected \u2014 running this cycle in your project database only. Your work stays in the working tree; run `git init` (and add a remote) to enable branches, commits and PR review.";
|
|
@@ -20187,6 +20550,38 @@ async function postReleaseToX(version, cycleClosed) {
|
|
|
20187
20550
|
return false;
|
|
20188
20551
|
}
|
|
20189
20552
|
}
|
|
20553
|
+
function buildHostedReleaseOutput(params) {
|
|
20554
|
+
const { version, branch, cyclePart, warningsBlock, skipVersion } = params;
|
|
20555
|
+
const tagAnnotation = `Release ${version}`;
|
|
20556
|
+
const titleSuffix = skipVersion ? " (skip version)" : "";
|
|
20557
|
+
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";
|
|
20558
|
+
const commands = skipVersion ? `git checkout ${branch}
|
|
20559
|
+
git pull
|
|
20560
|
+
git push origin ${branch}
|
|
20561
|
+
` : `git checkout ${branch}
|
|
20562
|
+
git pull
|
|
20563
|
+
git tag -a ${version} -m "${tagAnnotation}"
|
|
20564
|
+
git push origin ${branch}
|
|
20565
|
+
git push origin ${version}
|
|
20566
|
+
`;
|
|
20567
|
+
const identityBlock = skipVersion ? "" : `If git reports "Author identity unknown" or "Committer identity unknown" (no global git identity configured), pass your identity inline instead of writing global config:
|
|
20568
|
+
\`\`\`
|
|
20569
|
+
git -c user.name="Your Name" -c user.email="you@example.com" tag -a ${version} -m "${tagAnnotation}"
|
|
20570
|
+
\`\`\`
|
|
20571
|
+
|
|
20572
|
+
`;
|
|
20573
|
+
return `## Release ${version}${titleSuffix} \u2014 cycle closed in the DB
|
|
20574
|
+
|
|
20575
|
+
${cyclePart} is now marked **complete** in PAPI's database, so \`orient\` will no longer flag "Release has not been run."
|
|
20576
|
+
` + warningsBlock + `
|
|
20577
|
+
The hosted remote MCP transport (mcp.getpapi.ai) has no checkout of your project and no git binary, so ${gitHalf}.
|
|
20578
|
+
|
|
20579
|
+
Finish the release locally:
|
|
20580
|
+
\`\`\`
|
|
20581
|
+
` + commands + `\`\`\`
|
|
20582
|
+
|
|
20583
|
+
` + identityBlock + `Next: cycle closed! Run \`plan\` to start your next cycle, or \`idea "<what's next>"\` first if your backlog is thin.`;
|
|
20584
|
+
}
|
|
20190
20585
|
var releaseTool = {
|
|
20191
20586
|
name: "release",
|
|
20192
20587
|
description: "Cut a versioned release \u2014 creates a git tag, generates CHANGELOG.md, and pushes to remote. Pass skipVersion=true to update CHANGELOG and close the cycle without creating a tag or bumping version numbers.",
|
|
@@ -20364,33 +20759,12 @@ Next: run \`plan\` to start your next cycle.`
|
|
|
20364
20759
|
branchMerges: [],
|
|
20365
20760
|
changelogEmitted: false
|
|
20366
20761
|
});
|
|
20367
|
-
const tagAnnotation = `Release ${version}`;
|
|
20368
20762
|
const cyclePart = closed.resolvedCycleNum > 0 ? `Cycle ${closed.resolvedCycleNum}` : "The cycle";
|
|
20369
20763
|
const warningsBlock = closed.warnings.length > 0 ? `
|
|
20370
20764
|
\u26A0\uFE0F Warnings: ${closed.warnings.join("; ")}
|
|
20371
20765
|
` : "";
|
|
20372
20766
|
return textResponse(
|
|
20373
|
-
|
|
20374
|
-
|
|
20375
|
-
${cyclePart} is now marked **complete** in PAPI's database, so \`orient\` will no longer flag "Release has not been run."
|
|
20376
|
-
` + warningsBlock + `
|
|
20377
|
-
The hosted remote MCP transport (mcp.getpapi.ai) has no checkout of your project and no git binary, so the git half of the release (tag, push, CHANGELOG.md) must run on your own machine.
|
|
20378
|
-
|
|
20379
|
-
Finish the release locally:
|
|
20380
|
-
\`\`\`
|
|
20381
|
-
git checkout ${branch}
|
|
20382
|
-
git pull
|
|
20383
|
-
git tag -a ${version} -m "${tagAnnotation}"
|
|
20384
|
-
git push origin ${branch}
|
|
20385
|
-
git push origin ${version}
|
|
20386
|
-
\`\`\`
|
|
20387
|
-
|
|
20388
|
-
If git reports "Author identity unknown" or "Committer identity unknown" (no global git identity configured), pass your identity inline instead of writing global config:
|
|
20389
|
-
\`\`\`
|
|
20390
|
-
git -c user.name="Your Name" -c user.email="you@example.com" tag -a ${version} -m "${tagAnnotation}"
|
|
20391
|
-
\`\`\`
|
|
20392
|
-
|
|
20393
|
-
Next: cycle closed! Run \`plan\` to start your next cycle, or \`idea "<what's next>"\` first if your backlog is thin.`
|
|
20767
|
+
buildHostedReleaseOutput({ version, branch, cyclePart, warningsBlock, skipVersion: skipVersion ?? false })
|
|
20394
20768
|
);
|
|
20395
20769
|
}
|
|
20396
20770
|
tracker.mark("remote-project-guard");
|
|
@@ -20618,59 +20992,174 @@ async function ownsLocalWorkspace(adapter2, cwd) {
|
|
|
20618
20992
|
return storedOwner === remoteOwner;
|
|
20619
20993
|
}
|
|
20620
20994
|
|
|
20995
|
+
// src/lib/worktree-collision.ts
|
|
20996
|
+
var WORKTREE_DIR = ".claude/worktrees";
|
|
20997
|
+
function detectWorktreeCollision(input) {
|
|
20998
|
+
const target = input.targetBranch.trim();
|
|
20999
|
+
if (!target) return null;
|
|
21000
|
+
const conflicts = input.inProgress.filter((b2) => {
|
|
21001
|
+
const branch = b2.branch?.trim();
|
|
21002
|
+
return !!branch && branch !== target && b2.taskId !== input.taskId;
|
|
21003
|
+
});
|
|
21004
|
+
if (conflicts.length === 0) return null;
|
|
21005
|
+
const worktreePath = `${WORKTREE_DIR}/${input.taskId}`;
|
|
21006
|
+
const worktreeCommand = `git worktree add ${worktreePath} ${target}`;
|
|
21007
|
+
const others = conflicts.map((c) => `${c.taskId} (on '${c.branch.trim()}')`).join(", ");
|
|
21008
|
+
const isAre = conflicts.length === 1 ? "is" : "are";
|
|
21009
|
+
const warning = `\u26A0\uFE0F Concurrent build detected: ${others} ${isAre} In Progress on a different branch, but this build targets '${target}'. Switching HEAD on the shared checkout would revert that session's working tree (the task-2346 data-loss incident). Isolate this build into its own worktree instead of switching branches here.`;
|
|
21010
|
+
const setupHint = `Then run \`npm run worktree:setup\` inside ${worktreePath} \u2014 node_modules only exists in the main worktree, so the symlink step is required before tsup/builds work there (task-1419).`;
|
|
21011
|
+
return {
|
|
21012
|
+
warning,
|
|
21013
|
+
worktreeCommand,
|
|
21014
|
+
setupHint,
|
|
21015
|
+
worktreePath,
|
|
21016
|
+
auto: input.autoWorktree,
|
|
21017
|
+
conflicts
|
|
21018
|
+
};
|
|
21019
|
+
}
|
|
21020
|
+
|
|
20621
21021
|
// src/services/build.ts
|
|
20622
21022
|
init_git();
|
|
20623
21023
|
|
|
20624
|
-
// src/lib/
|
|
20625
|
-
import {
|
|
20626
|
-
|
|
20627
|
-
|
|
20628
|
-
|
|
20629
|
-
|
|
20630
|
-
"
|
|
20631
|
-
|
|
20632
|
-
|
|
20633
|
-
|
|
20634
|
-
|
|
20635
|
-
|
|
20636
|
-
"
|
|
20637
|
-
|
|
20638
|
-
|
|
20639
|
-
|
|
20640
|
-
|
|
20641
|
-
|
|
20642
|
-
|
|
20643
|
-
|
|
20644
|
-
|
|
20645
|
-
|
|
20646
|
-
|
|
20647
|
-
const
|
|
20648
|
-
|
|
20649
|
-
|
|
20650
|
-
|
|
20651
|
-
|
|
20652
|
-
|
|
20653
|
-
|
|
20654
|
-
|
|
20655
|
-
|
|
20656
|
-
|
|
20657
|
-
|
|
20658
|
-
|
|
20659
|
-
|
|
20660
|
-
|
|
20661
|
-
|
|
20662
|
-
|
|
20663
|
-
|
|
20664
|
-
|
|
20665
|
-
|
|
20666
|
-
|
|
20667
|
-
|
|
20668
|
-
|
|
20669
|
-
|
|
20670
|
-
|
|
20671
|
-
|
|
20672
|
-
|
|
20673
|
-
|
|
21024
|
+
// src/lib/build-checkpoint.ts
|
|
21025
|
+
import { createHash as createHash4 } from "crypto";
|
|
21026
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
21027
|
+
import { join as join11 } from "path";
|
|
21028
|
+
var BUILD_CHECKPOINT_VERSION = 1;
|
|
21029
|
+
function cwdHash(cwd) {
|
|
21030
|
+
return createHash4("sha256").update(realpathOrSelf(cwd)).digest("hex").slice(0, 12);
|
|
21031
|
+
}
|
|
21032
|
+
function safeTaskId(taskId) {
|
|
21033
|
+
return taskId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
21034
|
+
}
|
|
21035
|
+
function checkpointDir(cwd) {
|
|
21036
|
+
return join11(cwd, ".papi", "state");
|
|
21037
|
+
}
|
|
21038
|
+
function checkpointPath(cwd, taskId) {
|
|
21039
|
+
return join11(checkpointDir(cwd), `build-${safeTaskId(taskId)}.${cwdHash(cwd)}.json`);
|
|
21040
|
+
}
|
|
21041
|
+
function writeBuildCheckpoint(input) {
|
|
21042
|
+
try {
|
|
21043
|
+
const dir = checkpointDir(input.cwd);
|
|
21044
|
+
if (!existsSync7(dir)) {
|
|
21045
|
+
mkdirSync2(dir, { recursive: true });
|
|
21046
|
+
}
|
|
21047
|
+
const checkpoint = {
|
|
21048
|
+
version: BUILD_CHECKPOINT_VERSION,
|
|
21049
|
+
taskId: input.taskId,
|
|
21050
|
+
branch: input.branch,
|
|
21051
|
+
step: input.step,
|
|
21052
|
+
lastCommitSha: input.lastCommitSha,
|
|
21053
|
+
modifiedFiles: input.modifiedFiles,
|
|
21054
|
+
cwd: realpathOrSelf(input.cwd),
|
|
21055
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21056
|
+
};
|
|
21057
|
+
writeFileSync3(checkpointPath(input.cwd, input.taskId), JSON.stringify(checkpoint, null, 2) + "\n", "utf-8");
|
|
21058
|
+
} catch {
|
|
21059
|
+
}
|
|
21060
|
+
}
|
|
21061
|
+
function readBuildCheckpoint(key) {
|
|
21062
|
+
try {
|
|
21063
|
+
const path7 = checkpointPath(key.cwd, key.taskId);
|
|
21064
|
+
if (!existsSync7(path7)) return null;
|
|
21065
|
+
const parsed = JSON.parse(readFileSync6(path7, "utf-8"));
|
|
21066
|
+
if (!parsed || parsed.version !== BUILD_CHECKPOINT_VERSION || parsed.taskId !== key.taskId) {
|
|
21067
|
+
return null;
|
|
21068
|
+
}
|
|
21069
|
+
return {
|
|
21070
|
+
version: BUILD_CHECKPOINT_VERSION,
|
|
21071
|
+
taskId: parsed.taskId,
|
|
21072
|
+
branch: parsed.branch ?? null,
|
|
21073
|
+
step: "branch_ready",
|
|
21074
|
+
lastCommitSha: parsed.lastCommitSha ?? null,
|
|
21075
|
+
modifiedFiles: Array.isArray(parsed.modifiedFiles) ? parsed.modifiedFiles : [],
|
|
21076
|
+
cwd: typeof parsed.cwd === "string" ? parsed.cwd : realpathOrSelf(key.cwd),
|
|
21077
|
+
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : ""
|
|
21078
|
+
};
|
|
21079
|
+
} catch {
|
|
21080
|
+
return null;
|
|
21081
|
+
}
|
|
21082
|
+
}
|
|
21083
|
+
function clearBuildCheckpoint(key) {
|
|
21084
|
+
try {
|
|
21085
|
+
const path7 = checkpointPath(key.cwd, key.taskId);
|
|
21086
|
+
if (existsSync7(path7)) {
|
|
21087
|
+
unlinkSync2(path7);
|
|
21088
|
+
}
|
|
21089
|
+
} catch {
|
|
21090
|
+
}
|
|
21091
|
+
}
|
|
21092
|
+
function formatResumeNote(cp) {
|
|
21093
|
+
const files = cp.modifiedFiles.filter((f) => f && f.trim());
|
|
21094
|
+
const fileList = files.length > 0 ? files.slice(0, 12).map((f) => ` - ${f}`).join("\n") + (files.length > 12 ? `
|
|
21095
|
+
- \u2026+${files.length - 12} more` : "") : " _(none recorded)_";
|
|
21096
|
+
const lines = [
|
|
21097
|
+
"> **\u21BB Resuming from checkpoint** \u2014 this task is already In Progress; you are NOT starting clean.",
|
|
21098
|
+
`> - Branch: \`${cp.branch ?? "unknown"}\``,
|
|
21099
|
+
"> - Last step: branch ready",
|
|
21100
|
+
`> - Last commit: ${cp.lastCommitSha ? cp.lastCommitSha.slice(0, 8) : "none"}`,
|
|
21101
|
+
`> - Checkpoint saved: ${cp.updatedAt || "unknown"}`,
|
|
21102
|
+
">",
|
|
21103
|
+
"> Modified files at last checkpoint:",
|
|
21104
|
+
...fileList.split("\n").map((l) => `> ${l}`),
|
|
21105
|
+
">",
|
|
21106
|
+
"> Review the existing branch and changes above before writing new code \u2014 do not re-do work already committed.",
|
|
21107
|
+
"",
|
|
21108
|
+
""
|
|
21109
|
+
];
|
|
21110
|
+
return lines.join("\n");
|
|
21111
|
+
}
|
|
21112
|
+
|
|
21113
|
+
// src/lib/diff-inspector.ts
|
|
21114
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
21115
|
+
var TRIGGER_SURFACE_GLOBS = [
|
|
21116
|
+
"lib/install-snippets.ts",
|
|
21117
|
+
"packages/server/src/transport-http.ts",
|
|
21118
|
+
"packages/server/src/index.ts",
|
|
21119
|
+
"app/well-known/**",
|
|
21120
|
+
"app/api/auth/oauth/**",
|
|
21121
|
+
"app/proxy.ts",
|
|
21122
|
+
"app/middleware.ts",
|
|
21123
|
+
"lib/auth*",
|
|
21124
|
+
"next.config.ts",
|
|
21125
|
+
"vercel.json",
|
|
21126
|
+
"**/railway.toml",
|
|
21127
|
+
"**/Dockerfile",
|
|
21128
|
+
"**/Procfile",
|
|
21129
|
+
"supabase/functions/**",
|
|
21130
|
+
"lib/env*.ts"
|
|
21131
|
+
];
|
|
21132
|
+
function globToRegex(glob) {
|
|
21133
|
+
let out = "";
|
|
21134
|
+
let i = 0;
|
|
21135
|
+
while (i < glob.length) {
|
|
21136
|
+
const ch = glob[i];
|
|
21137
|
+
if (ch === "*") {
|
|
21138
|
+
if (glob[i + 1] === "*") {
|
|
21139
|
+
if (glob[i + 2] === "/") {
|
|
21140
|
+
out += "(?:.*/)?";
|
|
21141
|
+
i += 3;
|
|
21142
|
+
} else {
|
|
21143
|
+
out += ".*";
|
|
21144
|
+
i += 2;
|
|
21145
|
+
}
|
|
21146
|
+
} else {
|
|
21147
|
+
out += "[^/]*";
|
|
21148
|
+
i += 1;
|
|
21149
|
+
}
|
|
21150
|
+
} else if (ch === "." || ch === "+" || ch === "?" || ch === "(" || ch === ")" || ch === "|" || ch === "[" || ch === "]" || ch === "{" || ch === "}" || ch === "^" || ch === "$" || ch === "\\") {
|
|
21151
|
+
out += "\\" + ch;
|
|
21152
|
+
i += 1;
|
|
21153
|
+
} else {
|
|
21154
|
+
out += ch;
|
|
21155
|
+
i += 1;
|
|
21156
|
+
}
|
|
21157
|
+
}
|
|
21158
|
+
return new RegExp("^" + out + "$");
|
|
21159
|
+
}
|
|
21160
|
+
function isTriggerSurfacePath(path7, globs = TRIGGER_SURFACE_GLOBS) {
|
|
21161
|
+
for (const g of globs) {
|
|
21162
|
+
if (globToRegex(g).test(path7)) return true;
|
|
20674
21163
|
}
|
|
20675
21164
|
return false;
|
|
20676
21165
|
}
|
|
@@ -20856,51 +21345,22 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
20856
21345
|
if (staged.length > 0) {
|
|
20857
21346
|
return safeRun(() => commitStagedOnly(cwd, message)) + ` (selective staging respected: ${staged.length} file(s)).`;
|
|
20858
21347
|
}
|
|
21348
|
+
const modified = getModifiedFiles(cwd);
|
|
21349
|
+
if (modified.length === 0) {
|
|
21350
|
+
return "Auto-commit: skipped (no working-tree changes).";
|
|
21351
|
+
}
|
|
21352
|
+
const commitResult = safeRun(() => stageAllAndCommit(cwd, message));
|
|
20859
21353
|
if (predictedFiles && predictedFiles.length > 0) {
|
|
20860
|
-
const modified = getModifiedFiles(cwd);
|
|
20861
|
-
if (modified.length === 0) {
|
|
20862
|
-
return "Auto-commit: skipped (no working-tree changes).";
|
|
20863
|
-
}
|
|
20864
|
-
const dirname7 = (p) => {
|
|
20865
|
-
const idx = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
|
|
20866
|
-
return idx > 0 ? p.slice(0, idx) : "";
|
|
20867
|
-
};
|
|
20868
21354
|
const cleanedPredicted = sanitisePredictedFiles(predictedFiles);
|
|
20869
|
-
const
|
|
20870
|
-
|
|
20871
|
-
|
|
20872
|
-
|
|
20873
|
-
(
|
|
20874
|
-
);
|
|
20875
|
-
if (scoped.length === 0 && untrackedInScope.length === 0) {
|
|
20876
|
-
const modSample = modified.slice(0, 5).join(", ");
|
|
20877
|
-
const predSample = cleanedPredicted.slice(0, 5).join(", ");
|
|
20878
|
-
return `Auto-commit: refused \u2014 none of the ${modified.length} modified file(s) intersect FILES LIKELY TOUCHED. Modified: ${modSample}. Expected: ${predSample}. Stage the intended files manually (\`git add <paths>\`) then re-run, or set PAPI_AUTO_COMMIT=false.`;
|
|
20879
|
-
}
|
|
20880
|
-
const scopedDirs = [...new Set(scoped.map(dirname7).filter((d) => d.length > 0))];
|
|
20881
|
-
const isUnderScopedDir = (p) => scopedDirs.some((d) => p === d || p.startsWith(`${d}/`) || p.startsWith(`${d}\\`));
|
|
20882
|
-
const inScopeSet = /* @__PURE__ */ new Set([...scoped, ...untrackedInScope]);
|
|
20883
|
-
const adjacentUntracked = untracked.filter(
|
|
20884
|
-
(p) => !inScopeSet.has(p) && isUnderScopedDir(p)
|
|
20885
|
-
);
|
|
20886
|
-
const toStage = [...scoped, ...untrackedInScope, ...adjacentUntracked];
|
|
20887
|
-
const toStageSet = new Set(toStage);
|
|
20888
|
-
const droppedUntracked = untracked.filter((p) => !toStageSet.has(p));
|
|
20889
|
-
const droppedModified = modified.filter((p) => !scopedSet.has(p));
|
|
20890
|
-
let line = safeRun(() => stagePathsAndCommit(cwd, toStage, message)) + ` (scoped to ${scoped.length}/${modified.length} files via FILES LIKELY TOUCHED` + (untrackedInScope.length > 0 ? ` + ${untrackedInScope.length} new file(s) named in the handoff` : "") + (adjacentUntracked.length > 0 ? ` + ${adjacentUntracked.length} untracked under scoped dir(s)` : "") + `).`;
|
|
20891
|
-
if (droppedModified.length > 0) {
|
|
20892
|
-
const sample = droppedModified.slice(0, 10).join(", ");
|
|
20893
|
-
const more = droppedModified.length > 10 ? ` (+${droppedModified.length - 10} more)` : "";
|
|
20894
|
-
line += ` \u26A0\uFE0F ${droppedModified.length} modified file(s) outside FILES LIKELY TOUCHED were NOT staged: ${sample}${more}. If they belong to this task, stage them manually (\`git add <paths>\`) and re-run, or set PAPI_AUTO_COMMIT=false.`;
|
|
20895
|
-
}
|
|
20896
|
-
if (droppedUntracked.length > 0) {
|
|
20897
|
-
const sample = droppedUntracked.slice(0, 10).join(", ");
|
|
20898
|
-
const more = droppedUntracked.length > 10 ? ` (+${droppedUntracked.length - 10} more)` : "";
|
|
20899
|
-
line += ` \u26A0\uFE0F ${droppedUntracked.length} untracked file(s) were NOT committed: ${sample}${more}. If they belong to this task, run \`git add <paths> && git commit --amend --no-edit\` before pushing \u2014 otherwise the committed tree may not build on checkout/CI.`;
|
|
21355
|
+
const outOfScope = modified.filter((p) => !isPathInPredictedScope(p, cleanedPredicted));
|
|
21356
|
+
if (outOfScope.length > 0) {
|
|
21357
|
+
const sample = outOfScope.slice(0, 10).join(", ");
|
|
21358
|
+
const more = outOfScope.length > 10 ? ` (+${outOfScope.length - 10} more)` : "";
|
|
21359
|
+
return `${commitResult} (staged all ${modified.length} changed file(s)). \u2139\uFE0F Scope drift: ${outOfScope.length} committed file(s) were outside the handoff's FILES LIKELY TOUCHED \u2014 handoff under-predicted: ${sample}${more}.`;
|
|
20900
21360
|
}
|
|
20901
|
-
return
|
|
21361
|
+
return `${commitResult} (staged all ${modified.length} changed file(s), all within FILES LIKELY TOUCHED).`;
|
|
20902
21362
|
}
|
|
20903
|
-
return
|
|
21363
|
+
return `${commitResult} (staged all ${modified.length} changed file(s)).`;
|
|
20904
21364
|
}
|
|
20905
21365
|
function pushAndCreatePR(config2, taskId, taskTitle, clientName, module, cycleNumber) {
|
|
20906
21366
|
const lines = [];
|
|
@@ -21221,7 +21681,46 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
21221
21681
|
}
|
|
21222
21682
|
taskBranchMap.set(taskId, featureBranch);
|
|
21223
21683
|
const currentBranch = getCurrentBranch(config2.projectRoot);
|
|
21224
|
-
|
|
21684
|
+
let isolatedIntoWorktree = false;
|
|
21685
|
+
if (currentBranch !== featureBranch) {
|
|
21686
|
+
const otherInProgress = allTasks.filter((t) => t.status === "In Progress" && t.id !== taskId && t.displayId !== taskId).map((t) => ({
|
|
21687
|
+
taskId: t.displayId || t.id,
|
|
21688
|
+
branch: (t.branchName ?? taskBranchMap.get(t.id) ?? taskBranchMap.get(t.displayId) ?? "").trim()
|
|
21689
|
+
})).filter((t) => t.branch.length > 0);
|
|
21690
|
+
const collision = detectWorktreeCollision({
|
|
21691
|
+
taskId,
|
|
21692
|
+
targetBranch: featureBranch,
|
|
21693
|
+
inProgress: otherInProgress,
|
|
21694
|
+
autoWorktree: config2.autoWorktree
|
|
21695
|
+
});
|
|
21696
|
+
if (collision) {
|
|
21697
|
+
branchLines.push(collision.warning);
|
|
21698
|
+
if (collision.auto) {
|
|
21699
|
+
try {
|
|
21700
|
+
const { execFileSync: execFileSync7 } = await import("child_process");
|
|
21701
|
+
const baseForWorktree = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
21702
|
+
const wtArgs = branchExists(config2.projectRoot, featureBranch) ? ["worktree", "add", collision.worktreePath, featureBranch] : ["worktree", "add", "-b", featureBranch, collision.worktreePath, baseForWorktree];
|
|
21703
|
+
execFileSync7("git", wtArgs, { cwd: config2.projectRoot, encoding: "utf-8" });
|
|
21704
|
+
isolatedIntoWorktree = true;
|
|
21705
|
+
branchLines.push(
|
|
21706
|
+
`Auto-isolated (PAPI_AUTO_WORKTREE): created worktree at ${collision.worktreePath} on '${featureBranch}'. The shared checkout stays on '${currentBranch}' \u2014 continue this build from inside the worktree.`
|
|
21707
|
+
);
|
|
21708
|
+
branchLines.push(collision.setupHint);
|
|
21709
|
+
} catch (err) {
|
|
21710
|
+
branchLines.push(
|
|
21711
|
+
`Auto-worktree attempt failed (${err instanceof Error ? err.message : String(err)}) \u2014 isolate manually instead:`
|
|
21712
|
+
);
|
|
21713
|
+
branchLines.push(` ${collision.worktreeCommand}`);
|
|
21714
|
+
branchLines.push(collision.setupHint);
|
|
21715
|
+
}
|
|
21716
|
+
} else {
|
|
21717
|
+
branchLines.push(` ${collision.worktreeCommand}`);
|
|
21718
|
+
branchLines.push(collision.setupHint);
|
|
21719
|
+
}
|
|
21720
|
+
}
|
|
21721
|
+
}
|
|
21722
|
+
if (isolatedIntoWorktree) {
|
|
21723
|
+
} else if (currentBranch === featureBranch) {
|
|
21225
21724
|
branchLines.push(`Already on branch '${featureBranch}'.`);
|
|
21226
21725
|
if (useSharedBranch) {
|
|
21227
21726
|
branchLines.push(`Reusing shared cycle branch for ${task.complexity} ${task.module} task.`);
|
|
@@ -21338,8 +21837,9 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
21338
21837
|
await persistBranchName(adapter2, taskId, startedBranch);
|
|
21339
21838
|
}
|
|
21340
21839
|
buildStartTimes.set(taskId, (/* @__PURE__ */ new Date()).toISOString());
|
|
21840
|
+
let startSha = null;
|
|
21341
21841
|
if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
21342
|
-
|
|
21842
|
+
startSha = getHeadCommitSha(config2.projectRoot);
|
|
21343
21843
|
if (startSha) taskStartShaMap.set(taskId, startSha);
|
|
21344
21844
|
}
|
|
21345
21845
|
let phaseChanges = [];
|
|
@@ -21358,6 +21858,14 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
21358
21858
|
);
|
|
21359
21859
|
} catch {
|
|
21360
21860
|
}
|
|
21861
|
+
writeBuildCheckpoint({
|
|
21862
|
+
cwd: config2.projectRoot,
|
|
21863
|
+
taskId,
|
|
21864
|
+
branch: getCurrentBranch(config2.projectRoot),
|
|
21865
|
+
step: "branch_ready",
|
|
21866
|
+
lastCommitSha: startSha,
|
|
21867
|
+
modifiedFiles: getModifiedFiles(config2.projectRoot)
|
|
21868
|
+
});
|
|
21361
21869
|
return {
|
|
21362
21870
|
task,
|
|
21363
21871
|
branchLines,
|
|
@@ -21372,17 +21880,17 @@ function writeActiveTaskScope(projectRoot, taskId, filesLikelyTouched, adapterTy
|
|
|
21372
21880
|
collector.add({ path: ".papi/active-task-scope.txt", content, mode: "overwrite" });
|
|
21373
21881
|
return;
|
|
21374
21882
|
}
|
|
21375
|
-
const papiDir =
|
|
21376
|
-
if (!
|
|
21377
|
-
|
|
21883
|
+
const papiDir = join12(projectRoot, ".papi");
|
|
21884
|
+
if (!existsSync8(papiDir)) {
|
|
21885
|
+
mkdirSync3(papiDir, { recursive: true });
|
|
21378
21886
|
}
|
|
21379
|
-
const scopePath =
|
|
21380
|
-
|
|
21887
|
+
const scopePath = join12(papiDir, "active-task-scope.txt");
|
|
21888
|
+
writeFileSync4(scopePath, content, "utf-8");
|
|
21381
21889
|
}
|
|
21382
21890
|
function clearActiveTaskScope(projectRoot) {
|
|
21383
|
-
const scopePath =
|
|
21384
|
-
if (
|
|
21385
|
-
|
|
21891
|
+
const scopePath = join12(projectRoot, ".papi", "active-task-scope.txt");
|
|
21892
|
+
if (existsSync8(scopePath)) {
|
|
21893
|
+
unlinkSync3(scopePath);
|
|
21386
21894
|
}
|
|
21387
21895
|
}
|
|
21388
21896
|
function sanitiseResponseExcerpt(raw) {
|
|
@@ -21401,7 +21909,7 @@ function extractDocMeta(absolutePath, relativePath, cycleNumber) {
|
|
|
21401
21909
|
else if (relativePath.startsWith("docs/architecture/")) type = "architecture";
|
|
21402
21910
|
else if (relativePath.startsWith("docs/audits/")) type = "audit";
|
|
21403
21911
|
try {
|
|
21404
|
-
const content =
|
|
21912
|
+
const content = readFileSync7(absolutePath, "utf-8").slice(0, 2e3);
|
|
21405
21913
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
21406
21914
|
if (fmMatch) {
|
|
21407
21915
|
const fm = fmMatch[1];
|
|
@@ -21798,14 +22306,14 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
21798
22306
|
let docWarning;
|
|
21799
22307
|
try {
|
|
21800
22308
|
if (adapter2.searchDocs && hasLocalWorkspace() && await ownsLocalWorkspace(adapter2, config2.projectRoot)) {
|
|
21801
|
-
const docsDir =
|
|
21802
|
-
if (
|
|
22309
|
+
const docsDir = join12(config2.projectRoot, "docs");
|
|
22310
|
+
if (existsSync8(docsDir)) {
|
|
21803
22311
|
const scanDir = (dir, depth = 0) => {
|
|
21804
22312
|
if (depth > 8) return [];
|
|
21805
22313
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
21806
22314
|
const files = [];
|
|
21807
22315
|
for (const e of entries) {
|
|
21808
|
-
const full =
|
|
22316
|
+
const full = join12(dir, e.name);
|
|
21809
22317
|
if (e.isDirectory() && !e.isSymbolicLink()) files.push(...scanDir(full, depth + 1));
|
|
21810
22318
|
else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
|
|
21811
22319
|
}
|
|
@@ -21820,7 +22328,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
21820
22328
|
const failed = [];
|
|
21821
22329
|
for (const docPath of unregistered) {
|
|
21822
22330
|
try {
|
|
21823
|
-
const meta = extractDocMeta(
|
|
22331
|
+
const meta = extractDocMeta(join12(config2.projectRoot, docPath), docPath, cycleNumber);
|
|
21824
22332
|
await adapter2.registerDoc({
|
|
21825
22333
|
title: meta.title,
|
|
21826
22334
|
type: meta.type,
|
|
@@ -21858,6 +22366,10 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
21858
22366
|
clearActiveTaskScope(config2.projectRoot);
|
|
21859
22367
|
} catch {
|
|
21860
22368
|
}
|
|
22369
|
+
try {
|
|
22370
|
+
clearBuildCheckpoint({ cwd: config2.projectRoot, taskId });
|
|
22371
|
+
} catch {
|
|
22372
|
+
}
|
|
21861
22373
|
return {
|
|
21862
22374
|
task,
|
|
21863
22375
|
report,
|
|
@@ -21972,8 +22484,8 @@ ${instructions}`;
|
|
|
21972
22484
|
}
|
|
21973
22485
|
|
|
21974
22486
|
// src/tools/doc-registry.ts
|
|
21975
|
-
import { readdirSync as readdirSync6, existsSync as
|
|
21976
|
-
import { join as
|
|
22487
|
+
import { readdirSync as readdirSync6, existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
22488
|
+
import { join as join13, relative } from "path";
|
|
21977
22489
|
import { homedir as homedir3 } from "os";
|
|
21978
22490
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
21979
22491
|
import { docDeletionBlockMessage } from "@papi-ai/shared";
|
|
@@ -22171,7 +22683,7 @@ async function handleDocSearch(adapter2, args, config2) {
|
|
|
22171
22683
|
const lines = docs.map((d) => {
|
|
22172
22684
|
const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
|
|
22173
22685
|
const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
|
|
22174
|
-
const missingNote = root && d.path && !
|
|
22686
|
+
const missingNote = root && d.path && !existsSync9(join13(root, d.path)) ? `
|
|
22175
22687
|
> \u26A0\uFE0F **File missing on disk** \u2014 the registry points at \`${d.path}\` but nothing is there. Check \`git stash list\` for a papi-autostash entry, or re-create/deregister the doc.` : "";
|
|
22176
22688
|
return `### ${d.title}
|
|
22177
22689
|
**Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
|
|
@@ -22185,12 +22697,12 @@ ${d.summary}
|
|
|
22185
22697
|
${lines.join("\n---\n\n")}`);
|
|
22186
22698
|
}
|
|
22187
22699
|
function scanMdFiles(dir, rootDir) {
|
|
22188
|
-
if (!
|
|
22700
|
+
if (!existsSync9(dir)) return [];
|
|
22189
22701
|
const files = [];
|
|
22190
22702
|
try {
|
|
22191
22703
|
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
22192
22704
|
for (const entry of entries) {
|
|
22193
|
-
const full =
|
|
22705
|
+
const full = join13(dir, entry.name);
|
|
22194
22706
|
if (entry.isDirectory()) {
|
|
22195
22707
|
files.push(...scanMdFiles(full, rootDir));
|
|
22196
22708
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -22203,7 +22715,7 @@ function scanMdFiles(dir, rootDir) {
|
|
|
22203
22715
|
}
|
|
22204
22716
|
function extractTitle(filePath) {
|
|
22205
22717
|
try {
|
|
22206
|
-
const content =
|
|
22718
|
+
const content = readFileSync8(filePath, "utf-8").slice(0, 1e3);
|
|
22207
22719
|
const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
|
|
22208
22720
|
if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
22209
22721
|
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
@@ -22215,7 +22727,7 @@ function extractTitle(filePath) {
|
|
|
22215
22727
|
async function detectUnregisteredDocsNote(adapter2, config2) {
|
|
22216
22728
|
try {
|
|
22217
22729
|
if (!adapter2.searchDocs || !hasLocalWorkspace()) return "";
|
|
22218
|
-
const docsDir =
|
|
22730
|
+
const docsDir = join13(config2.projectRoot, "docs");
|
|
22219
22731
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
22220
22732
|
if (docsFiles.length === 0) return "";
|
|
22221
22733
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
@@ -22241,17 +22753,17 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
22241
22753
|
const includePlans = args.include_plans ?? false;
|
|
22242
22754
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
22243
22755
|
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
22244
|
-
const docsDir =
|
|
22756
|
+
const docsDir = join13(config2.projectRoot, "docs");
|
|
22245
22757
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
22246
22758
|
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
22247
22759
|
let unregisteredPlans = [];
|
|
22248
22760
|
if (includePlans) {
|
|
22249
|
-
const plansDir =
|
|
22250
|
-
if (
|
|
22761
|
+
const plansDir = join13(homedir3(), ".claude", "plans");
|
|
22762
|
+
if (existsSync9(plansDir)) {
|
|
22251
22763
|
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
22252
22764
|
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
22253
22765
|
path: f,
|
|
22254
|
-
title: extractTitle(
|
|
22766
|
+
title: extractTitle(join13(plansDir, f.replace("plans/", "")))
|
|
22255
22767
|
}));
|
|
22256
22768
|
}
|
|
22257
22769
|
}
|
|
@@ -22262,7 +22774,7 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
22262
22774
|
if (unregisteredDocs.length > 0) {
|
|
22263
22775
|
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
22264
22776
|
for (const f of unregisteredDocs) {
|
|
22265
|
-
const title = extractTitle(
|
|
22777
|
+
const title = extractTitle(join13(config2.projectRoot, f));
|
|
22266
22778
|
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
22267
22779
|
}
|
|
22268
22780
|
}
|
|
@@ -22435,97 +22947,6 @@ async function handleDocReorder(adapter2, args) {
|
|
|
22435
22947
|
|
|
22436
22948
|
// src/tools/build.ts
|
|
22437
22949
|
init_git();
|
|
22438
|
-
|
|
22439
|
-
// src/lib/build-checkpoint.ts
|
|
22440
|
-
import { createHash as createHash4 } from "crypto";
|
|
22441
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
22442
|
-
import { join as join13 } from "path";
|
|
22443
|
-
var BUILD_CHECKPOINT_VERSION = 1;
|
|
22444
|
-
function cwdHash(cwd) {
|
|
22445
|
-
return createHash4("sha256").update(realpathOrSelf(cwd)).digest("hex").slice(0, 12);
|
|
22446
|
-
}
|
|
22447
|
-
function safeTaskId(taskId) {
|
|
22448
|
-
return taskId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
22449
|
-
}
|
|
22450
|
-
function checkpointDir(cwd) {
|
|
22451
|
-
return join13(cwd, ".papi", "state");
|
|
22452
|
-
}
|
|
22453
|
-
function checkpointPath(cwd, taskId) {
|
|
22454
|
-
return join13(checkpointDir(cwd), `build-${safeTaskId(taskId)}.${cwdHash(cwd)}.json`);
|
|
22455
|
-
}
|
|
22456
|
-
function writeBuildCheckpoint(input) {
|
|
22457
|
-
try {
|
|
22458
|
-
const dir = checkpointDir(input.cwd);
|
|
22459
|
-
if (!existsSync9(dir)) {
|
|
22460
|
-
mkdirSync3(dir, { recursive: true });
|
|
22461
|
-
}
|
|
22462
|
-
const checkpoint = {
|
|
22463
|
-
version: BUILD_CHECKPOINT_VERSION,
|
|
22464
|
-
taskId: input.taskId,
|
|
22465
|
-
branch: input.branch,
|
|
22466
|
-
step: input.step,
|
|
22467
|
-
lastCommitSha: input.lastCommitSha,
|
|
22468
|
-
modifiedFiles: input.modifiedFiles,
|
|
22469
|
-
cwd: realpathOrSelf(input.cwd),
|
|
22470
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22471
|
-
};
|
|
22472
|
-
writeFileSync4(checkpointPath(input.cwd, input.taskId), JSON.stringify(checkpoint, null, 2) + "\n", "utf-8");
|
|
22473
|
-
} catch {
|
|
22474
|
-
}
|
|
22475
|
-
}
|
|
22476
|
-
function readBuildCheckpoint(key) {
|
|
22477
|
-
try {
|
|
22478
|
-
const path7 = checkpointPath(key.cwd, key.taskId);
|
|
22479
|
-
if (!existsSync9(path7)) return null;
|
|
22480
|
-
const parsed = JSON.parse(readFileSync8(path7, "utf-8"));
|
|
22481
|
-
if (!parsed || parsed.version !== BUILD_CHECKPOINT_VERSION || parsed.taskId !== key.taskId) {
|
|
22482
|
-
return null;
|
|
22483
|
-
}
|
|
22484
|
-
return {
|
|
22485
|
-
version: BUILD_CHECKPOINT_VERSION,
|
|
22486
|
-
taskId: parsed.taskId,
|
|
22487
|
-
branch: parsed.branch ?? null,
|
|
22488
|
-
step: "branch_ready",
|
|
22489
|
-
lastCommitSha: parsed.lastCommitSha ?? null,
|
|
22490
|
-
modifiedFiles: Array.isArray(parsed.modifiedFiles) ? parsed.modifiedFiles : [],
|
|
22491
|
-
cwd: typeof parsed.cwd === "string" ? parsed.cwd : realpathOrSelf(key.cwd),
|
|
22492
|
-
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : ""
|
|
22493
|
-
};
|
|
22494
|
-
} catch {
|
|
22495
|
-
return null;
|
|
22496
|
-
}
|
|
22497
|
-
}
|
|
22498
|
-
function clearBuildCheckpoint(key) {
|
|
22499
|
-
try {
|
|
22500
|
-
const path7 = checkpointPath(key.cwd, key.taskId);
|
|
22501
|
-
if (existsSync9(path7)) {
|
|
22502
|
-
unlinkSync3(path7);
|
|
22503
|
-
}
|
|
22504
|
-
} catch {
|
|
22505
|
-
}
|
|
22506
|
-
}
|
|
22507
|
-
function formatResumeNote(cp) {
|
|
22508
|
-
const files = cp.modifiedFiles.filter((f) => f && f.trim());
|
|
22509
|
-
const fileList = files.length > 0 ? files.slice(0, 12).map((f) => ` - ${f}`).join("\n") + (files.length > 12 ? `
|
|
22510
|
-
- \u2026+${files.length - 12} more` : "") : " _(none recorded)_";
|
|
22511
|
-
const lines = [
|
|
22512
|
-
"> **\u21BB Resuming from checkpoint** \u2014 this task is already In Progress; you are NOT starting clean.",
|
|
22513
|
-
`> - Branch: \`${cp.branch ?? "unknown"}\``,
|
|
22514
|
-
"> - Last step: branch ready",
|
|
22515
|
-
`> - Last commit: ${cp.lastCommitSha ? cp.lastCommitSha.slice(0, 8) : "none"}`,
|
|
22516
|
-
`> - Checkpoint saved: ${cp.updatedAt || "unknown"}`,
|
|
22517
|
-
">",
|
|
22518
|
-
"> Modified files at last checkpoint:",
|
|
22519
|
-
...fileList.split("\n").map((l) => `> ${l}`),
|
|
22520
|
-
">",
|
|
22521
|
-
"> Review the existing branch and changes above before writing new code \u2014 do not re-do work already committed.",
|
|
22522
|
-
"",
|
|
22523
|
-
""
|
|
22524
|
-
];
|
|
22525
|
-
return lines.join("\n");
|
|
22526
|
-
}
|
|
22527
|
-
|
|
22528
|
-
// src/tools/build.ts
|
|
22529
22950
|
var buildListTool = {
|
|
22530
22951
|
name: "build_list",
|
|
22531
22952
|
description: "List cycle tasks that have BUILD HANDOFFs ready for execution. Shows task ID, title, status, priority, and complexity. In Progress tasks appear first, then Backlog. Does not call the Anthropic API.",
|
|
@@ -22561,7 +22982,17 @@ var buildExecuteTool = {
|
|
|
22561
22982
|
description: "Start or complete a build task. Call with just task_id to start (returns BUILD HANDOFF, creates feature branch, marks In Progress). After implementing the task, you MUST call build_execute again with all report fields (completed, effort, estimated_effort, surprises, discovered_issues, architecture_notes) to finish \u2014 do not wait for user confirmation between start and complete. Never call on tasks that are already In Review or Done. Does not call the Anthropic API. Set light=true to skip branch/PR creation (commits to current branch). Set PAPI_LIGHT_MODE=true in env to default all builds to light mode.",
|
|
22562
22983
|
annotations: { title: "Run Build", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
22563
22984
|
inputSchema: {
|
|
22985
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
22564
22986
|
type: "object",
|
|
22987
|
+
// task-2802: JSON Schema 2020-12. Shared effort-size enum lives in $defs and is
|
|
22988
|
+
// referenced from effort/estimated_effort so the two stay in lockstep; the
|
|
22989
|
+
// start-vs-complete contract is expressed as a conditional (see allOf below).
|
|
22990
|
+
$defs: {
|
|
22991
|
+
effortSize: {
|
|
22992
|
+
type: "string",
|
|
22993
|
+
enum: ["XS", "S", "M", "L", "XL"]
|
|
22994
|
+
}
|
|
22995
|
+
},
|
|
22565
22996
|
properties: {
|
|
22566
22997
|
task_id: {
|
|
22567
22998
|
type: "string",
|
|
@@ -22581,13 +23012,11 @@ var buildExecuteTool = {
|
|
|
22581
23012
|
description: `task-2833: set true to assert every acceptance criterion in the task's BUILD HANDOFF was met. Required to record a completed:"yes" build when the handoff lists acceptance criteria \u2014 without it, build_execute returns the criteria checklist and does NOT mark the task Done (the report is not discarded; re-send with acceptance_confirmed:true). Tasks with no acceptance criteria, and completed:"partial"/"no", are unaffected.`
|
|
22582
23013
|
},
|
|
22583
23014
|
effort: {
|
|
22584
|
-
|
|
22585
|
-
enum: ["XS", "S", "M", "L", "XL"],
|
|
23015
|
+
$ref: "#/$defs/effortSize",
|
|
22586
23016
|
description: "Actual effort: XS, S, M, L, or XL. Required for complete."
|
|
22587
23017
|
},
|
|
22588
23018
|
estimated_effort: {
|
|
22589
|
-
|
|
22590
|
-
enum: ["XS", "S", "M", "L", "XL"],
|
|
23019
|
+
$ref: "#/$defs/effortSize",
|
|
22591
23020
|
description: "Estimated effort from the BUILD HANDOFF. Required for complete."
|
|
22592
23021
|
},
|
|
22593
23022
|
model: {
|
|
@@ -22631,6 +23060,8 @@ var buildExecuteTool = {
|
|
|
22631
23060
|
},
|
|
22632
23061
|
corrections_count: {
|
|
22633
23062
|
type: "integer",
|
|
23063
|
+
minimum: 0,
|
|
23064
|
+
default: 0,
|
|
22634
23065
|
description: "Number of times the user corrected or redirected the build during implementation. Captures informal pushback that bypasses review_submit. Default 0."
|
|
22635
23066
|
},
|
|
22636
23067
|
dead_ends: {
|
|
@@ -22681,7 +23112,29 @@ var buildExecuteTool = {
|
|
|
22681
23112
|
}
|
|
22682
23113
|
}
|
|
22683
23114
|
},
|
|
22684
|
-
required: ["task_id"]
|
|
23115
|
+
required: ["task_id"],
|
|
23116
|
+
// task-2802: a COMPLETE call is any call carrying report data (mirrors the
|
|
23117
|
+
// handler's isCompleteCall at build.ts). When any of the six report fields is
|
|
23118
|
+
// present, all six are required — exactly what completeBuild enforces, so this
|
|
23119
|
+
// never rejects a call the handler would accept; it just steers the agent to
|
|
23120
|
+
// send the full report in one shot instead of round-tripping on a missing field.
|
|
23121
|
+
allOf: [
|
|
23122
|
+
{
|
|
23123
|
+
if: {
|
|
23124
|
+
anyOf: [
|
|
23125
|
+
{ required: ["completed"] },
|
|
23126
|
+
{ required: ["effort"] },
|
|
23127
|
+
{ required: ["estimated_effort"] },
|
|
23128
|
+
{ required: ["surprises"] },
|
|
23129
|
+
{ required: ["discovered_issues"] },
|
|
23130
|
+
{ required: ["architecture_notes"] }
|
|
23131
|
+
]
|
|
23132
|
+
},
|
|
23133
|
+
then: {
|
|
23134
|
+
required: ["completed", "effort", "estimated_effort", "surprises", "discovered_issues", "architecture_notes"]
|
|
23135
|
+
}
|
|
23136
|
+
}
|
|
23137
|
+
]
|
|
22685
23138
|
}
|
|
22686
23139
|
};
|
|
22687
23140
|
var buildCancelTool = {
|
|
@@ -22872,14 +23325,6 @@ async function handleBuildExecute(adapter2, config2, args, clientName) {
|
|
|
22872
23325
|
const result = await startBuild(adapter2, config2, taskId, { light }, clientName);
|
|
22873
23326
|
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.task.cycle ?? null });
|
|
22874
23327
|
await tracker.recordStep("branch_ready");
|
|
22875
|
-
writeBuildCheckpoint({
|
|
22876
|
-
cwd: config2.projectRoot,
|
|
22877
|
-
taskId,
|
|
22878
|
-
branch: getCurrentBranch(config2.projectRoot),
|
|
22879
|
-
step: "branch_ready",
|
|
22880
|
-
lastCommitSha: getHeadCommitSha(config2.projectRoot),
|
|
22881
|
-
modifiedFiles: getModifiedFiles(config2.projectRoot)
|
|
22882
|
-
});
|
|
22883
23328
|
tracker.mark("start_decorate_handoff");
|
|
22884
23329
|
const branchInfo = result.branchLines.length > 0 ? result.branchLines.map((l) => `> ${l}`).join("\n") + "\n\n" : "";
|
|
22885
23330
|
const phaseNote = result.phaseChanges.length > 0 ? "\n\n" + result.phaseChanges.map((c) => `Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`).join("\n") : "";
|
|
@@ -23098,7 +23543,6 @@ Your report was NOT discarded and the task is NOT yet Done \u2014 re-send with \
|
|
|
23098
23543
|
preview
|
|
23099
23544
|
}, { light }, clientName);
|
|
23100
23545
|
tracker.mark("complete_format");
|
|
23101
|
-
clearBuildCheckpoint({ cwd: config2.projectRoot, taskId });
|
|
23102
23546
|
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.cycleNumber });
|
|
23103
23547
|
await tracker.recordStep("report_written");
|
|
23104
23548
|
if ((result.autoTriagedCount ?? 0) > 0) {
|
|
@@ -23456,12 +23900,460 @@ Re-submit with \`notes: "... Reference: <path>"\` to link one, or ignore if none
|
|
|
23456
23900
|
return textResponse(`${result.message}${overrideNote}`);
|
|
23457
23901
|
}
|
|
23458
23902
|
|
|
23903
|
+
// src/services/import.ts
|
|
23904
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
23905
|
+
var IMPLEMENTED_SOURCES = ["csv", "markdown", "linear"];
|
|
23906
|
+
var MAX_TITLE_LEN = 200;
|
|
23907
|
+
var MAX_NOTES_LEN = 2e3;
|
|
23908
|
+
function sanitiseText(value, maxLen, keepNewlines = false) {
|
|
23909
|
+
let str;
|
|
23910
|
+
if (typeof value === "string") str = value;
|
|
23911
|
+
else if (typeof value === "number" || typeof value === "boolean") str = String(value);
|
|
23912
|
+
else if (value == null) str = "";
|
|
23913
|
+
else {
|
|
23914
|
+
try {
|
|
23915
|
+
str = String(value);
|
|
23916
|
+
} catch {
|
|
23917
|
+
str = "";
|
|
23918
|
+
}
|
|
23919
|
+
}
|
|
23920
|
+
if (keepNewlines) {
|
|
23921
|
+
str = str.replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/g, " ");
|
|
23922
|
+
str = str.replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n");
|
|
23923
|
+
} else {
|
|
23924
|
+
str = str.replace(/[\x00-\x1F\x7F-\x9F]/g, " ");
|
|
23925
|
+
str = str.replace(/\s+/g, " ");
|
|
23926
|
+
}
|
|
23927
|
+
str = str.trim();
|
|
23928
|
+
if (str.length > maxLen) str = str.slice(0, maxLen).trim();
|
|
23929
|
+
return str;
|
|
23930
|
+
}
|
|
23931
|
+
function mapPriorityString(value) {
|
|
23932
|
+
const v = String(value ?? "").trim().toLowerCase();
|
|
23933
|
+
if (/\b(p0|urgent|critical|highest|blocker)\b/.test(v)) return "P0 Critical";
|
|
23934
|
+
if (/\b(p1|high)\b/.test(v)) return "P1 High";
|
|
23935
|
+
if (/\b(p3|low|lowest|minor)\b/.test(v)) return "P3 Low";
|
|
23936
|
+
return "P2 Medium";
|
|
23937
|
+
}
|
|
23938
|
+
function mapPriorityNumber(value) {
|
|
23939
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
23940
|
+
switch (n) {
|
|
23941
|
+
case 1:
|
|
23942
|
+
return "P0 Critical";
|
|
23943
|
+
case 2:
|
|
23944
|
+
return "P1 High";
|
|
23945
|
+
case 4:
|
|
23946
|
+
return "P3 Low";
|
|
23947
|
+
case 3:
|
|
23948
|
+
case 0:
|
|
23949
|
+
default:
|
|
23950
|
+
return "P2 Medium";
|
|
23951
|
+
}
|
|
23952
|
+
}
|
|
23953
|
+
function mapStatusString(value) {
|
|
23954
|
+
const v = String(value ?? "").trim().toLowerCase();
|
|
23955
|
+
if (/\b(done|complete|completed|closed|merged|shipped|resolved)\b/.test(v)) return "Done";
|
|
23956
|
+
if (/\b(cancel|cancelled|canceled|wont.?fix|duplicate|abandoned)\b/.test(v)) return "Cancelled";
|
|
23957
|
+
if (/\b(in.?review|review|qa|verifying)\b/.test(v)) return "In Review";
|
|
23958
|
+
if (/\b(in.?progress|started|doing|active|wip)\b/.test(v)) return "In Progress";
|
|
23959
|
+
if (/\b(blocked|waiting|on.?hold)\b/.test(v)) return "Blocked";
|
|
23960
|
+
return "Backlog";
|
|
23961
|
+
}
|
|
23962
|
+
function estimateToComplexity(value) {
|
|
23963
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
23964
|
+
if (!Number.isFinite(n) || n <= 0) return "Small";
|
|
23965
|
+
if (n <= 1) return "XS";
|
|
23966
|
+
if (n <= 2) return "Small";
|
|
23967
|
+
if (n <= 3) return "Medium";
|
|
23968
|
+
if (n <= 5) return "Large";
|
|
23969
|
+
return "XL";
|
|
23970
|
+
}
|
|
23971
|
+
function mapComplexityField(value) {
|
|
23972
|
+
const v = String(value ?? "").trim();
|
|
23973
|
+
if (v !== "" && /^\d+(\.\d+)?$/.test(v)) return estimateToComplexity(v);
|
|
23974
|
+
return normalizeComplexity(v);
|
|
23975
|
+
}
|
|
23976
|
+
function parseCsv(text) {
|
|
23977
|
+
const rows = [];
|
|
23978
|
+
let field = "";
|
|
23979
|
+
let row = [];
|
|
23980
|
+
let inQuotes = false;
|
|
23981
|
+
for (let i = 0; i < text.length; i++) {
|
|
23982
|
+
const c = text[i];
|
|
23983
|
+
if (inQuotes) {
|
|
23984
|
+
if (c === '"') {
|
|
23985
|
+
if (text[i + 1] === '"') {
|
|
23986
|
+
field += '"';
|
|
23987
|
+
i++;
|
|
23988
|
+
} else {
|
|
23989
|
+
inQuotes = false;
|
|
23990
|
+
}
|
|
23991
|
+
} else {
|
|
23992
|
+
field += c;
|
|
23993
|
+
}
|
|
23994
|
+
continue;
|
|
23995
|
+
}
|
|
23996
|
+
if (c === '"') {
|
|
23997
|
+
inQuotes = true;
|
|
23998
|
+
} else if (c === ",") {
|
|
23999
|
+
row.push(field);
|
|
24000
|
+
field = "";
|
|
24001
|
+
} else if (c === "\n" || c === "\r") {
|
|
24002
|
+
if (c === "\r" && text[i + 1] === "\n") i++;
|
|
24003
|
+
row.push(field);
|
|
24004
|
+
field = "";
|
|
24005
|
+
if (row.some((f) => f.trim() !== "")) rows.push(row);
|
|
24006
|
+
row = [];
|
|
24007
|
+
} else {
|
|
24008
|
+
field += c;
|
|
24009
|
+
}
|
|
24010
|
+
}
|
|
24011
|
+
if (field !== "" || row.length > 0) {
|
|
24012
|
+
row.push(field);
|
|
24013
|
+
if (row.some((f) => f.trim() !== "")) rows.push(row);
|
|
24014
|
+
}
|
|
24015
|
+
return rows;
|
|
24016
|
+
}
|
|
24017
|
+
function findColumn(header, candidates) {
|
|
24018
|
+
const lower = header.map((h) => h.trim().toLowerCase());
|
|
24019
|
+
for (const cand of candidates) {
|
|
24020
|
+
const idx = lower.indexOf(cand);
|
|
24021
|
+
if (idx !== -1) return idx;
|
|
24022
|
+
}
|
|
24023
|
+
return -1;
|
|
24024
|
+
}
|
|
24025
|
+
var csvNormaliser = ({ raw }) => {
|
|
24026
|
+
if (!raw || !raw.trim()) return [];
|
|
24027
|
+
const rows = parseCsv(raw);
|
|
24028
|
+
if (rows.length < 2) return [];
|
|
24029
|
+
const header = rows[0];
|
|
24030
|
+
const titleCol = findColumn(header, ["title", "name", "task", "summary", "subject"]);
|
|
24031
|
+
if (titleCol === -1) {
|
|
24032
|
+
throw new Error(
|
|
24033
|
+
"CSV import: no recognisable title column. Expected a header row with one of: title, name, task, summary, subject."
|
|
24034
|
+
);
|
|
24035
|
+
}
|
|
24036
|
+
const notesCol = findColumn(header, ["notes", "description", "details", "body"]);
|
|
24037
|
+
const statusCol = findColumn(header, ["status", "state"]);
|
|
24038
|
+
const priorityCol = findColumn(header, ["priority", "importance"]);
|
|
24039
|
+
const complexityCol = findColumn(header, ["complexity", "estimate", "size", "points", "effort"]);
|
|
24040
|
+
const idCol = findColumn(header, ["id", "key", "identifier", "ref"]);
|
|
24041
|
+
const out = [];
|
|
24042
|
+
for (let r = 1; r < rows.length; r++) {
|
|
24043
|
+
const cells = rows[r];
|
|
24044
|
+
const title = sanitiseText(cells[titleCol], MAX_TITLE_LEN);
|
|
24045
|
+
if (!title) continue;
|
|
24046
|
+
out.push({
|
|
24047
|
+
title,
|
|
24048
|
+
notes: notesCol === -1 ? "" : sanitiseText(cells[notesCol], MAX_NOTES_LEN, true),
|
|
24049
|
+
status: statusCol === -1 ? "Backlog" : mapStatusString(cells[statusCol]),
|
|
24050
|
+
priority: priorityCol === -1 ? "P2 Medium" : mapPriorityString(cells[priorityCol]),
|
|
24051
|
+
complexity: complexityCol === -1 ? "Small" : mapComplexityField(cells[complexityCol]),
|
|
24052
|
+
sourceId: idCol === -1 ? void 0 : sanitiseText(cells[idCol], 80) || void 0
|
|
24053
|
+
});
|
|
24054
|
+
}
|
|
24055
|
+
return out;
|
|
24056
|
+
};
|
|
24057
|
+
var markdownNormaliser = ({ raw }) => {
|
|
24058
|
+
if (!raw || !raw.trim()) return [];
|
|
24059
|
+
const lines = raw.split(/\r?\n/);
|
|
24060
|
+
const tableHeaderIdx = lines.findIndex(
|
|
24061
|
+
(l, i) => /\|/.test(l) && lines[i + 1] !== void 0 && /^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i + 1]) && /-/.test(lines[i + 1])
|
|
24062
|
+
);
|
|
24063
|
+
if (tableHeaderIdx !== -1) {
|
|
24064
|
+
const splitRow = (l) => l.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim());
|
|
24065
|
+
const header = splitRow(lines[tableHeaderIdx]);
|
|
24066
|
+
const titleCol = findColumn(header, ["title", "name", "task", "summary", "subject"]);
|
|
24067
|
+
if (titleCol === -1) {
|
|
24068
|
+
throw new Error(
|
|
24069
|
+
"Markdown table import: no recognisable title column. Expected a header cell of: title, name, task, summary, subject."
|
|
24070
|
+
);
|
|
24071
|
+
}
|
|
24072
|
+
const notesCol = findColumn(header, ["notes", "description", "details", "body"]);
|
|
24073
|
+
const statusCol = findColumn(header, ["status", "state"]);
|
|
24074
|
+
const priorityCol = findColumn(header, ["priority", "importance"]);
|
|
24075
|
+
const complexityCol = findColumn(header, ["complexity", "estimate", "size", "points", "effort"]);
|
|
24076
|
+
const idCol = findColumn(header, ["id", "key", "identifier", "ref"]);
|
|
24077
|
+
const out2 = [];
|
|
24078
|
+
for (let i = tableHeaderIdx + 2; i < lines.length; i++) {
|
|
24079
|
+
const l = lines[i];
|
|
24080
|
+
if (!/\|/.test(l) || l.trim() === "") break;
|
|
24081
|
+
const cells = splitRow(l);
|
|
24082
|
+
const title = sanitiseText(cells[titleCol], MAX_TITLE_LEN);
|
|
24083
|
+
if (!title) continue;
|
|
24084
|
+
out2.push({
|
|
24085
|
+
title,
|
|
24086
|
+
notes: notesCol === -1 ? "" : sanitiseText(cells[notesCol], MAX_NOTES_LEN, true),
|
|
24087
|
+
status: statusCol === -1 ? "Backlog" : mapStatusString(cells[statusCol]),
|
|
24088
|
+
priority: priorityCol === -1 ? "P2 Medium" : mapPriorityString(cells[priorityCol]),
|
|
24089
|
+
complexity: complexityCol === -1 ? "Small" : mapComplexityField(cells[complexityCol]),
|
|
24090
|
+
sourceId: idCol === -1 ? void 0 : sanitiseText(cells[idCol], 80) || void 0
|
|
24091
|
+
});
|
|
24092
|
+
}
|
|
24093
|
+
return out2;
|
|
24094
|
+
}
|
|
24095
|
+
const out = [];
|
|
24096
|
+
const checklistRe = /^\s*(?:[-*+]|\d+\.)\s+(?:\[( |x|X)\]\s+)?(.*\S)\s*$/;
|
|
24097
|
+
for (const line of lines) {
|
|
24098
|
+
const m = line.match(checklistRe);
|
|
24099
|
+
if (!m) continue;
|
|
24100
|
+
const checked = m[1];
|
|
24101
|
+
const title = sanitiseText(m[2], MAX_TITLE_LEN);
|
|
24102
|
+
if (!title) continue;
|
|
24103
|
+
out.push({
|
|
24104
|
+
title,
|
|
24105
|
+
notes: "",
|
|
24106
|
+
status: checked && checked.toLowerCase() === "x" ? "Done" : "Backlog",
|
|
24107
|
+
priority: "P2 Medium",
|
|
24108
|
+
complexity: "Small"
|
|
24109
|
+
});
|
|
24110
|
+
}
|
|
24111
|
+
return out;
|
|
24112
|
+
};
|
|
24113
|
+
function isRecord(v) {
|
|
24114
|
+
return typeof v === "object" && v !== null;
|
|
24115
|
+
}
|
|
24116
|
+
var linearNormaliser = ({ rows }) => {
|
|
24117
|
+
if (!rows || rows.length === 0) return [];
|
|
24118
|
+
const out = [];
|
|
24119
|
+
for (const row of rows) {
|
|
24120
|
+
if (!isRecord(row)) continue;
|
|
24121
|
+
const issue = row;
|
|
24122
|
+
const title = sanitiseText(issue.title, MAX_TITLE_LEN);
|
|
24123
|
+
if (!title) continue;
|
|
24124
|
+
const stateName = isRecord(issue.state) ? issue.state.name ?? issue.state.type : issue.state;
|
|
24125
|
+
const identifier = sanitiseText(issue.identifier, 80) || sanitiseText(issue.id, 80) || void 0;
|
|
24126
|
+
const url = sanitiseText(issue.url, 300);
|
|
24127
|
+
const description = sanitiseText(issue.description, MAX_NOTES_LEN, true);
|
|
24128
|
+
const notesParts = [];
|
|
24129
|
+
if (description) notesParts.push(description);
|
|
24130
|
+
if (url) notesParts.push(`Reference: ${url}`);
|
|
24131
|
+
out.push({
|
|
24132
|
+
title,
|
|
24133
|
+
notes: sanitiseText(notesParts.join("\n"), MAX_NOTES_LEN, true),
|
|
24134
|
+
status: mapStatusString(stateName),
|
|
24135
|
+
priority: mapPriorityNumber(issue.priority),
|
|
24136
|
+
complexity: issue.estimate == null ? "Small" : estimateToComplexity(issue.estimate),
|
|
24137
|
+
sourceId: identifier
|
|
24138
|
+
});
|
|
24139
|
+
}
|
|
24140
|
+
return out;
|
|
24141
|
+
};
|
|
24142
|
+
function notImplemented(source) {
|
|
24143
|
+
return () => {
|
|
24144
|
+
throw new Error(
|
|
24145
|
+
`Import source "${source}" is not implemented yet. Shipped sources: ${IMPLEMENTED_SOURCES.join(", ")}. To add "${source}", implement a normaliser in packages/server/src/services/import.ts (see the "EXTENDING THE IMPORTER" header comment) \u2014 the host reads "${source}" via its own MCP tool and passes rows here; PAPI never connects to it directly.`
|
|
24146
|
+
);
|
|
24147
|
+
};
|
|
24148
|
+
}
|
|
24149
|
+
var SOURCE_NORMALISERS = {
|
|
24150
|
+
csv: csvNormaliser,
|
|
24151
|
+
markdown: markdownNormaliser,
|
|
24152
|
+
linear: linearNormaliser,
|
|
24153
|
+
// Extension points — build these by replacing notImplemented() with a real normaliser.
|
|
24154
|
+
trello: notImplemented("trello"),
|
|
24155
|
+
todoist: notImplemented("todoist"),
|
|
24156
|
+
notion: notImplemented("notion")
|
|
24157
|
+
};
|
|
24158
|
+
function importMarker(source, sourceId) {
|
|
24159
|
+
return `[papi-import:${source}:${sourceId}]`;
|
|
24160
|
+
}
|
|
24161
|
+
function dedupTitleKey(title) {
|
|
24162
|
+
return title.toLowerCase().replace(/[^a-z0-9\s]/g, "").replace(/\s+/g, " ").trim();
|
|
24163
|
+
}
|
|
24164
|
+
async function importBacklog(adapter2, input) {
|
|
24165
|
+
const normaliser = SOURCE_NORMALISERS[input.source];
|
|
24166
|
+
if (!normaliser) {
|
|
24167
|
+
throw new Error(
|
|
24168
|
+
`Unknown import source "${input.source}". Valid sources: ${Object.keys(SOURCE_NORMALISERS).join(", ")}.`
|
|
24169
|
+
);
|
|
24170
|
+
}
|
|
24171
|
+
const normalised = normaliser({ raw: input.raw, rows: input.rows });
|
|
24172
|
+
const existing = await adapter2.queryBoard({});
|
|
24173
|
+
const existingTitleKeys = /* @__PURE__ */ new Set();
|
|
24174
|
+
const existingMarkers = /* @__PURE__ */ new Set();
|
|
24175
|
+
for (const t of existing) {
|
|
24176
|
+
existingTitleKeys.add(dedupTitleKey(t.title));
|
|
24177
|
+
const notes = t.notes ?? "";
|
|
24178
|
+
const markerMatches = notes.match(/\[papi-import:[^\]]+\]/g);
|
|
24179
|
+
if (markerMatches) for (const mk of markerMatches) existingMarkers.add(mk);
|
|
24180
|
+
}
|
|
24181
|
+
const health = await adapter2.getCycleHealth();
|
|
24182
|
+
warnIfEmpty("getCycleHealth (import)", health);
|
|
24183
|
+
const createdCycle = health.totalCycles;
|
|
24184
|
+
const targetModule = input.module && input.module.trim() || "Core";
|
|
24185
|
+
const createdTasks = [];
|
|
24186
|
+
const skippedTitles = [];
|
|
24187
|
+
const seenThisBatch = /* @__PURE__ */ new Set();
|
|
24188
|
+
for (const task of normalised) {
|
|
24189
|
+
const titleKey = dedupTitleKey(task.title);
|
|
24190
|
+
const marker = task.sourceId ? importMarker(input.source, task.sourceId) : null;
|
|
24191
|
+
const isDup = marker !== null && existingMarkers.has(marker) || existingTitleKeys.has(titleKey) || seenThisBatch.has(marker ?? titleKey);
|
|
24192
|
+
if (isDup) {
|
|
24193
|
+
skippedTitles.push(task.title);
|
|
24194
|
+
continue;
|
|
24195
|
+
}
|
|
24196
|
+
seenThisBatch.add(marker ?? titleKey);
|
|
24197
|
+
if (input.dryRun) {
|
|
24198
|
+
createdTasks.push({ id: "(dry-run)", title: task.title });
|
|
24199
|
+
continue;
|
|
24200
|
+
}
|
|
24201
|
+
const notes = marker ? `${task.notes ? `${task.notes}
|
|
24202
|
+
|
|
24203
|
+
` : ""}${marker}`.trim() : task.notes;
|
|
24204
|
+
const created = await adapter2.createTask({
|
|
24205
|
+
uuid: randomUUID13(),
|
|
24206
|
+
displayId: "",
|
|
24207
|
+
title: task.title,
|
|
24208
|
+
status: task.status,
|
|
24209
|
+
priority: task.priority,
|
|
24210
|
+
complexity: task.complexity,
|
|
24211
|
+
module: targetModule,
|
|
24212
|
+
epic: "Platform",
|
|
24213
|
+
phase: "Unscoped",
|
|
24214
|
+
owner: "TBD",
|
|
24215
|
+
reviewed: false,
|
|
24216
|
+
createdCycle,
|
|
24217
|
+
notes,
|
|
24218
|
+
taskType: "task",
|
|
24219
|
+
maturity: "raw",
|
|
24220
|
+
source: `import:${input.source}`
|
|
24221
|
+
});
|
|
24222
|
+
createdTasks.push({ id: created.id, title: created.title });
|
|
24223
|
+
existingTitleKeys.add(titleKey);
|
|
24224
|
+
if (marker) existingMarkers.add(marker);
|
|
24225
|
+
}
|
|
24226
|
+
return {
|
|
24227
|
+
source: input.source,
|
|
24228
|
+
normalised: normalised.length,
|
|
24229
|
+
imported: input.dryRun ? 0 : createdTasks.length,
|
|
24230
|
+
skipped: skippedTitles.length,
|
|
24231
|
+
skippedTitles,
|
|
24232
|
+
createdTasks,
|
|
24233
|
+
dryRun: input.dryRun === true
|
|
24234
|
+
};
|
|
24235
|
+
}
|
|
24236
|
+
|
|
24237
|
+
// src/tools/import.ts
|
|
24238
|
+
var ALL_SOURCES = ["csv", "markdown", "linear", "trello", "todoist", "notion"];
|
|
24239
|
+
var backlogImportTool = {
|
|
24240
|
+
name: "backlog_import",
|
|
24241
|
+
annotations: { title: "Import Backlog", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
24242
|
+
description: `Import an existing backlog from another tool into PAPI as Backlog tasks, removing the "start from an empty board" wall. PAPI never connects to the source tool \u2014 YOU (the host) read the export and pass it here. Flat-file sources (csv, markdown) take a \`raw\` string; structured sources (linear) take a \`rows\` array you read via that tool's MCP. Import is one-way and idempotent: re-running the same import does NOT create duplicates. Shipped sources: ${IMPLEMENTED_SOURCES.join(", ")}. trello/todoist/notion are reserved but not implemented yet. Does not call the Anthropic API.`,
|
|
24243
|
+
inputSchema: {
|
|
24244
|
+
type: "object",
|
|
24245
|
+
properties: {
|
|
24246
|
+
source: {
|
|
24247
|
+
type: "string",
|
|
24248
|
+
enum: ALL_SOURCES,
|
|
24249
|
+
description: "The tool the backlog came from. csv/markdown read a `raw` string; linear reads a `rows` array of issues. trello/todoist/notion are reserved extension points (not implemented yet)."
|
|
24250
|
+
},
|
|
24251
|
+
raw: {
|
|
24252
|
+
type: "string",
|
|
24253
|
+
description: "Raw export text for flat-file sources. CSV: a header row + data rows (title/name column required; optional notes/description, status/state, priority, complexity/estimate, id). Markdown: a checklist (`- [ ] Task`) or a GitHub-style table."
|
|
24254
|
+
},
|
|
24255
|
+
rows: {
|
|
24256
|
+
type: "array",
|
|
24257
|
+
description: "Pre-read rows for structured sources. For linear: an array of issues you read via the Linear MCP, each like { identifier, title, description, state: { name }, priority (0-4), estimate, url }.",
|
|
24258
|
+
items: { type: "object" }
|
|
24259
|
+
},
|
|
24260
|
+
module: {
|
|
24261
|
+
type: "string",
|
|
24262
|
+
description: 'Module to file imported tasks under (default "Core").'
|
|
24263
|
+
},
|
|
24264
|
+
dry_run: {
|
|
24265
|
+
type: "boolean",
|
|
24266
|
+
description: "When true, normalise and dedup-check without writing anything. Use this to preview what would be imported. Default: false."
|
|
24267
|
+
},
|
|
24268
|
+
project: {
|
|
24269
|
+
type: "string",
|
|
24270
|
+
description: "Project id (UUID) or slug to import into, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise."
|
|
24271
|
+
}
|
|
24272
|
+
},
|
|
24273
|
+
required: ["source"]
|
|
24274
|
+
}
|
|
24275
|
+
};
|
|
24276
|
+
function diagnostic(lastStep, error, hint) {
|
|
24277
|
+
return errorResponse(JSON.stringify({ tool: "backlog_import", lastStep, error, hint }, null, 2));
|
|
24278
|
+
}
|
|
24279
|
+
function formatResult(result, overrideNote) {
|
|
24280
|
+
const lines = [];
|
|
24281
|
+
const verb = result.dryRun ? "Would import" : "Imported";
|
|
24282
|
+
lines.push(
|
|
24283
|
+
`${verb} ${result.dryRun ? result.normalised - result.skipped : result.imported} task(s) from ${result.source}${overrideNote}.`
|
|
24284
|
+
);
|
|
24285
|
+
lines.push(` normalised: ${result.normalised} \xB7 ${result.dryRun ? "would-write" : "written"}: ${result.dryRun ? result.normalised - result.skipped : result.imported} \xB7 skipped (duplicates): ${result.skipped}`);
|
|
24286
|
+
if (result.createdTasks.length > 0 && !result.dryRun) {
|
|
24287
|
+
const preview = result.createdTasks.slice(0, 10).map((t) => ` - ${t.id}: ${t.title}`);
|
|
24288
|
+
lines.push("Created:");
|
|
24289
|
+
lines.push(...preview);
|
|
24290
|
+
if (result.createdTasks.length > 10) lines.push(` \u2026and ${result.createdTasks.length - 10} more`);
|
|
24291
|
+
}
|
|
24292
|
+
if (result.skipped > 0) {
|
|
24293
|
+
const dupPreview = result.skippedTitles.slice(0, 5).map((t) => ` - ${t}`);
|
|
24294
|
+
lines.push(`Skipped ${result.skipped} duplicate(s) (already on the board or repeated in this import):`);
|
|
24295
|
+
lines.push(...dupPreview);
|
|
24296
|
+
if (result.skippedTitles.length > 5) lines.push(` \u2026and ${result.skippedTitles.length - 5} more`);
|
|
24297
|
+
}
|
|
24298
|
+
if (!result.dryRun && result.imported > 0) {
|
|
24299
|
+
lines.push("\nRun `plan` to triage and scope the imported backlog into a cycle.");
|
|
24300
|
+
}
|
|
24301
|
+
return lines.join("\n");
|
|
24302
|
+
}
|
|
24303
|
+
async function handleBacklogImport(adapter2, args) {
|
|
24304
|
+
const source = args.source?.trim();
|
|
24305
|
+
if (!source) {
|
|
24306
|
+
return errorResponse(`source is required \u2014 one of: ${ALL_SOURCES.join(", ")}.`);
|
|
24307
|
+
}
|
|
24308
|
+
if (!ALL_SOURCES.includes(source)) {
|
|
24309
|
+
return errorResponse(`Unknown source "${source}". Valid: ${ALL_SOURCES.join(", ")}.`);
|
|
24310
|
+
}
|
|
24311
|
+
const raw = args.raw;
|
|
24312
|
+
const rawRows = args.rows;
|
|
24313
|
+
const rows = Array.isArray(rawRows) ? rawRows : void 0;
|
|
24314
|
+
const isFlatFile = source === "csv" || source === "markdown";
|
|
24315
|
+
if (isFlatFile && (!raw || !raw.trim())) {
|
|
24316
|
+
return errorResponse(`Source "${source}" needs a \`raw\` string (the exported ${source} text).`);
|
|
24317
|
+
}
|
|
24318
|
+
if (!isFlatFile && IMPLEMENTED_SOURCES.includes(source) && (!rows || rows.length === 0)) {
|
|
24319
|
+
return errorResponse(`Source "${source}" needs a \`rows\` array of issues you read via that tool's MCP.`);
|
|
24320
|
+
}
|
|
24321
|
+
let target = adapter2;
|
|
24322
|
+
let overrideNote = "";
|
|
24323
|
+
try {
|
|
24324
|
+
({ adapter: target, overrideNote } = await resolvePerCallProjectAdapter(adapter2, args));
|
|
24325
|
+
} catch (err) {
|
|
24326
|
+
if (err instanceof ProjectResolutionError) return errorResponse(err.message);
|
|
24327
|
+
throw err;
|
|
24328
|
+
}
|
|
24329
|
+
try {
|
|
24330
|
+
const result = await importBacklog(target, {
|
|
24331
|
+
source,
|
|
24332
|
+
raw,
|
|
24333
|
+
rows,
|
|
24334
|
+
module: args.module,
|
|
24335
|
+
dryRun: args.dry_run === true
|
|
24336
|
+
});
|
|
24337
|
+
return textResponse(formatResult(result, overrideNote));
|
|
24338
|
+
} catch (err) {
|
|
24339
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24340
|
+
if (/not implemented yet|no recognisable title column/i.test(message)) {
|
|
24341
|
+
return errorResponse(message);
|
|
24342
|
+
}
|
|
24343
|
+
return diagnostic(
|
|
24344
|
+
"importBacklog",
|
|
24345
|
+
message,
|
|
24346
|
+
"Check the export shape matches the source (csv/markdown need `raw`; linear needs `rows`). Re-run with dry_run:true to preview."
|
|
24347
|
+
);
|
|
24348
|
+
}
|
|
24349
|
+
}
|
|
24350
|
+
|
|
23459
24351
|
// src/tools/bug.ts
|
|
23460
24352
|
import os from "os";
|
|
23461
24353
|
init_git();
|
|
23462
24354
|
|
|
23463
24355
|
// src/services/bug.ts
|
|
23464
|
-
import { randomUUID as
|
|
24356
|
+
import { randomUUID as randomUUID14 } from "crypto";
|
|
23465
24357
|
function resolveCurrentPhase2(phases) {
|
|
23466
24358
|
if (phases.length === 0) return "Unscoped";
|
|
23467
24359
|
const inProgress = phases.find((p) => p.status === "In Progress");
|
|
@@ -23487,7 +24379,7 @@ async function captureBug(adapter2, input) {
|
|
|
23487
24379
|
warnIfEmpty("getCycleHealth (bug)", health);
|
|
23488
24380
|
const phase = input.phase || resolveCurrentPhase2(phases);
|
|
23489
24381
|
return adapter2.createTask({
|
|
23490
|
-
uuid:
|
|
24382
|
+
uuid: randomUUID14(),
|
|
23491
24383
|
displayId: "",
|
|
23492
24384
|
title: input.text,
|
|
23493
24385
|
status: "Backlog",
|
|
@@ -23741,7 +24633,7 @@ ${lines.join("\n")}`
|
|
|
23741
24633
|
init_git();
|
|
23742
24634
|
|
|
23743
24635
|
// src/services/ad-hoc.ts
|
|
23744
|
-
import { randomUUID as
|
|
24636
|
+
import { randomUUID as randomUUID15 } from "crypto";
|
|
23745
24637
|
function resolveAdHocCycle(cycle, latest, latestComplete) {
|
|
23746
24638
|
if (cycle === void 0) return null;
|
|
23747
24639
|
if (typeof cycle === "number") return cycle;
|
|
@@ -23781,7 +24673,7 @@ async function recordAdHoc(adapter2, input) {
|
|
|
23781
24673
|
} else {
|
|
23782
24674
|
const phase = resolveCurrentPhase(phases);
|
|
23783
24675
|
task = await adapter2.createTask({
|
|
23784
|
-
uuid:
|
|
24676
|
+
uuid: randomUUID15(),
|
|
23785
24677
|
displayId: "",
|
|
23786
24678
|
title: input.title,
|
|
23787
24679
|
status: landInReview ? "In Review" : "Done",
|
|
@@ -23800,7 +24692,7 @@ async function recordAdHoc(adapter2, input) {
|
|
|
23800
24692
|
});
|
|
23801
24693
|
}
|
|
23802
24694
|
const report = {
|
|
23803
|
-
uuid:
|
|
24695
|
+
uuid: randomUUID15(),
|
|
23804
24696
|
createdAt: now.toISOString(),
|
|
23805
24697
|
taskId: task.id,
|
|
23806
24698
|
taskName: task.title ?? task.displayId ?? "untitled",
|
|
@@ -23954,13 +24846,38 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
23954
24846
|
const promoNote = result.task.cycle != null ? ` Promoted into Cycle ${result.task.cycle} as injected work${result.task.status === "In Review" ? " (In Review \u2014 will release with the cycle)" : ""}.` : "";
|
|
23955
24847
|
if (holdArg) {
|
|
23956
24848
|
const branch = `feat/${result.task.id}`;
|
|
24849
|
+
let collisionBlock = "";
|
|
24850
|
+
try {
|
|
24851
|
+
const board = await adapter2.queryBoard({ status: ["In Progress"] });
|
|
24852
|
+
const otherInProgress = board.filter((t) => t.id !== result.task.id && t.displayId !== result.task.id).map((t) => ({ taskId: t.displayId || t.id, branch: (t.branchName ?? "").trim() })).filter((t) => t.branch.length > 0);
|
|
24853
|
+
const collision = detectWorktreeCollision({
|
|
24854
|
+
taskId: result.task.id,
|
|
24855
|
+
targetBranch: branch,
|
|
24856
|
+
inProgress: otherInProgress,
|
|
24857
|
+
// Held work never runs git server-side — treat as suggest-only.
|
|
24858
|
+
autoWorktree: false
|
|
24859
|
+
});
|
|
24860
|
+
if (collision) {
|
|
24861
|
+
collisionBlock = `
|
|
24862
|
+
> ${collision.warning}
|
|
24863
|
+
> Instead of \`git switch -c\`, isolate into a worktree:
|
|
24864
|
+
> \`\`\`
|
|
24865
|
+
> git worktree add -b ${branch} ${collision.worktreePath}
|
|
24866
|
+
> cd ${collision.worktreePath} && npm run worktree:setup
|
|
24867
|
+
> \`\`\`
|
|
24868
|
+
> ${collision.setupHint}
|
|
24869
|
+
|
|
24870
|
+
`;
|
|
24871
|
+
}
|
|
24872
|
+
} catch {
|
|
24873
|
+
}
|
|
23957
24874
|
return textResponse(
|
|
23958
24875
|
`**${result.task.id}:** "${result.task.title}" held for review (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule}).${truncateWarning}${promoNote} Build report attached.
|
|
23959
24876
|
|
|
23960
24877
|
## Held for the next cycle \u2014 branch + commit, do NOT merge
|
|
23961
24878
|
The task is recorded **In Review** and pinned to **Cycle ${result.task.cycle}**, so the planner won't re-plan it and it rides that cycle's review \u2192 release bundled with planned work.
|
|
23962
24879
|
|
|
23963
|
-
1. Create a branch and commit your code there (never \`main\`):
|
|
24880
|
+
` + collisionBlock + `1. Create a branch and commit your code there (never \`main\`):
|
|
23964
24881
|
\`\`\`
|
|
23965
24882
|
git switch -c ${branch}
|
|
23966
24883
|
git add -- <your changed files>
|
|
@@ -24568,7 +25485,7 @@ import { join as join15 } from "path";
|
|
|
24568
25485
|
init_git();
|
|
24569
25486
|
|
|
24570
25487
|
// src/services/review.ts
|
|
24571
|
-
import { randomUUID as
|
|
25488
|
+
import { randomUUID as randomUUID16 } from "crypto";
|
|
24572
25489
|
function isValidVerdict(stage, verdict) {
|
|
24573
25490
|
if (stage === "handoff-review") {
|
|
24574
25491
|
return verdict === "approve" || verdict === "request-changes" || verdict === "reject";
|
|
@@ -24625,7 +25542,7 @@ async function submitReview(adapter2, input) {
|
|
|
24625
25542
|
}
|
|
24626
25543
|
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
24627
25544
|
const review = {
|
|
24628
|
-
uuid:
|
|
25545
|
+
uuid: randomUUID16(),
|
|
24629
25546
|
taskId: input.taskId,
|
|
24630
25547
|
stage: input.stage,
|
|
24631
25548
|
reviewer: input.reviewer,
|
|
@@ -24840,6 +25757,7 @@ var reviewSubmitTool = {
|
|
|
24840
25757
|
description: "Record a review verdict on a completed build (build-acceptance) or task plan (handoff-review). ALWAYS ask the human for their verdict before calling \u2014 never auto-submit without human input. Accept moves the task to Done, request-changes sends it back for rework, reject discards the build. Updates task status based on the verdict. On handoff-review with suggested changes, returns a prompt to revise the BUILD HANDOFF.\n\nDO NOT use this tool as a substitute for review_list. If you need to see what is pending review, call review_list first. If review_list is unavailable in your tool set, STOP and tell the human their MCP integration is incomplete rather than guessing at the next pending task. (SUP-2026-010.)",
|
|
24841
25758
|
annotations: { title: "Submit Review", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
24842
25759
|
inputSchema: {
|
|
25760
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
24843
25761
|
type: "object",
|
|
24844
25762
|
properties: {
|
|
24845
25763
|
task_id: {
|
|
@@ -24906,7 +25824,21 @@ var reviewSubmitTool = {
|
|
|
24906
25824
|
required: ["verdict", "summary", "findings"]
|
|
24907
25825
|
}
|
|
24908
25826
|
},
|
|
24909
|
-
required: ["task_id", "stage", "verdict", "comments"]
|
|
25827
|
+
required: ["task_id", "stage", "verdict", "comments"],
|
|
25828
|
+
// task-2802: mirror isValidVerdict — the legal verdict set is stage-dependent.
|
|
25829
|
+
// handoff-review takes approve/request-changes/reject; build-acceptance takes
|
|
25830
|
+
// accept/request-changes/reject. The flat enum above lists the union; these
|
|
25831
|
+
// conditionals narrow it per stage so the agent can't pair an impossible verdict.
|
|
25832
|
+
allOf: [
|
|
25833
|
+
{
|
|
25834
|
+
if: { properties: { stage: { const: "handoff-review" } }, required: ["stage"] },
|
|
25835
|
+
then: { properties: { verdict: { enum: ["approve", "request-changes", "reject"] } } }
|
|
25836
|
+
},
|
|
25837
|
+
{
|
|
25838
|
+
if: { properties: { stage: { const: "build-acceptance" } }, required: ["stage"] },
|
|
25839
|
+
then: { properties: { verdict: { enum: ["accept", "request-changes", "reject"] } } }
|
|
25840
|
+
}
|
|
25841
|
+
]
|
|
24910
25842
|
}
|
|
24911
25843
|
};
|
|
24912
25844
|
function formatReviewList(pendingBuilds) {
|
|
@@ -25525,7 +26457,7 @@ async function handleReviewClaim(adapter2, config2, args) {
|
|
|
25525
26457
|
}
|
|
25526
26458
|
|
|
25527
26459
|
// src/tools/init.ts
|
|
25528
|
-
import { randomUUID as
|
|
26460
|
+
import { randomUUID as randomUUID17 } from "crypto";
|
|
25529
26461
|
import { access as access3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
25530
26462
|
import path5 from "path";
|
|
25531
26463
|
var initTool = {
|
|
@@ -25958,7 +26890,7 @@ ${writeNote}
|
|
|
25958
26890
|
);
|
|
25959
26891
|
}
|
|
25960
26892
|
if (isDatabaseUser) {
|
|
25961
|
-
const projectId =
|
|
26893
|
+
const projectId = randomUUID17();
|
|
25962
26894
|
const envVars = {
|
|
25963
26895
|
PAPI_PROJECT_DIR: projectRoot,
|
|
25964
26896
|
PAPI_ADAPTER: "pg",
|
|
@@ -26333,9 +27265,15 @@ var TASK_REF = /\btask-\d+\b/gi;
|
|
|
26333
27265
|
var RECENT_CYCLE_WINDOW = 5;
|
|
26334
27266
|
var MAX_CANDIDATES = 5;
|
|
26335
27267
|
async function findUnblockCandidates(adapter2, currentCycle) {
|
|
27268
|
+
try {
|
|
27269
|
+
const blockedProbe = await adapter2.queryBoard({ status: ["Blocked"], compact: true });
|
|
27270
|
+
if (blockedProbe.length === 0) return [];
|
|
27271
|
+
} catch {
|
|
27272
|
+
return [];
|
|
27273
|
+
}
|
|
26336
27274
|
let allTasks = [];
|
|
26337
27275
|
try {
|
|
26338
|
-
allTasks = await adapter2.queryBoard();
|
|
27276
|
+
allTasks = await adapter2.queryBoard({ compact: true });
|
|
26339
27277
|
} catch {
|
|
26340
27278
|
return [];
|
|
26341
27279
|
}
|
|
@@ -26761,6 +27699,7 @@ var orientTool = {
|
|
|
26761
27699
|
description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files. PAPI detects build capability from the connecting harness (clientInfo); pass `environment` only to override that detection for git-dependent recommendations (build_execute, release, review_submit).",
|
|
26762
27700
|
annotations: { title: "Orient Session", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
26763
27701
|
inputSchema: {
|
|
27702
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
26764
27703
|
type: "object",
|
|
26765
27704
|
properties: {
|
|
26766
27705
|
environment: {
|
|
@@ -26920,7 +27859,13 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
26920
27859
|
lines.push(`**Nearing Closure:** ${hierarchy.phasesNearingClosure.join(", ")}`);
|
|
26921
27860
|
}
|
|
26922
27861
|
if (hierarchy.stageExitCriteria && hierarchy.stageExitCriteria.length > 0) {
|
|
26923
|
-
|
|
27862
|
+
const crit = hierarchy.stageExitCriteria;
|
|
27863
|
+
const met = crit.filter((c) => c.met).length;
|
|
27864
|
+
const total = crit.length;
|
|
27865
|
+
lines.push(`**Stage Exit Criteria [${met}/${total} met]:** ${crit.map((c) => `${c.met ? "[x]" : "[ ]"} ${c.text}`).join(" | ")}`);
|
|
27866
|
+
if (met === total) {
|
|
27867
|
+
lines.push(" \u21B3 All exit criteria met \u2014 run `strategy_review` to propose advancing the stage.");
|
|
27868
|
+
}
|
|
26924
27869
|
}
|
|
26925
27870
|
lines.push("");
|
|
26926
27871
|
}
|
|
@@ -27125,9 +28070,9 @@ function formatDiscoveredIssuesBlocks(candidateLearnings, closedTaskIds) {
|
|
|
27125
28070
|
for (const issue of alerts) {
|
|
27126
28071
|
const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
|
|
27127
28072
|
lines.push(`- **${issue.severity}**: ${desc}`);
|
|
27128
|
-
lines.push(` \u21B3 discovered during ${issue.taskId} (C${issue.cycleNumber})`);
|
|
28073
|
+
lines.push(` \u21B3 discovered during ${issue.taskId} (C${issue.cycleNumber})${issue.id ? ` \xB7 id \`${issue.id}\`` : ""}`);
|
|
27129
28074
|
}
|
|
27130
|
-
lines.push("
|
|
28075
|
+
lines.push("_Already fixed? `discovered_issue_resolve <id>` clears it for good (won't reappear). Not yet? `idea` with P1 priority to log it as a backlog task._");
|
|
27131
28076
|
alertsNote = lines.join("\n");
|
|
27132
28077
|
}
|
|
27133
28078
|
if (allLowSev.length > 0) {
|
|
@@ -27137,18 +28082,18 @@ function formatDiscoveredIssuesBlocks(candidateLearnings, closedTaskIds) {
|
|
|
27137
28082
|
for (const issue of unactioned) {
|
|
27138
28083
|
const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
|
|
27139
28084
|
lines.push(`- **${issue.severity}**: ${desc}`);
|
|
27140
|
-
lines.push(` \u21B3 discovered during ${issue.taskId} (C${issue.cycleNumber})`);
|
|
28085
|
+
lines.push(` \u21B3 discovered during ${issue.taskId} (C${issue.cycleNumber})${issue.id ? ` \xB7 id \`${issue.id}\`` : ""}`);
|
|
27141
28086
|
}
|
|
27142
|
-
lines.push("
|
|
28087
|
+
lines.push("_Already fixed? `discovered_issue_resolve <id>` clears it. Otherwise `idea` to log as a backlog task._");
|
|
27143
28088
|
unactionedIssuesNote = lines.join("\n");
|
|
27144
28089
|
}
|
|
27145
28090
|
return { alertsNote, unactionedIssuesNote };
|
|
27146
28091
|
}
|
|
27147
|
-
async function computeTeamSummary(adapter2) {
|
|
28092
|
+
async function computeTeamSummary(adapter2, contributorsInput) {
|
|
27148
28093
|
if (typeof adapter2.listContributors !== "function") return void 0;
|
|
27149
28094
|
let members;
|
|
27150
28095
|
try {
|
|
27151
|
-
members = (await adapter2.listContributors()).length;
|
|
28096
|
+
members = (await (contributorsInput ?? adapter2.listContributors())).length;
|
|
27152
28097
|
} catch {
|
|
27153
28098
|
return void 0;
|
|
27154
28099
|
}
|
|
@@ -27165,11 +28110,11 @@ async function computeTeamSummary(adapter2) {
|
|
|
27165
28110
|
const reviewQueue = tasks.filter((t) => t.status === "In Review").length;
|
|
27166
28111
|
return `**Team:** ${members} members \xB7 ${pool} in pool \xB7 ${inFlight} in flight \xB7 ${reviewQueue} in review`;
|
|
27167
28112
|
}
|
|
27168
|
-
async function computeReleaseHistory(adapter2) {
|
|
28113
|
+
async function computeReleaseHistory(adapter2, contributorsInput) {
|
|
27169
28114
|
if (typeof adapter2.listContributors !== "function") return void 0;
|
|
27170
28115
|
let contributors;
|
|
27171
28116
|
try {
|
|
27172
|
-
contributors = await adapter2.listContributors();
|
|
28117
|
+
contributors = await (contributorsInput ?? adapter2.listContributors());
|
|
27173
28118
|
} catch {
|
|
27174
28119
|
return void 0;
|
|
27175
28120
|
}
|
|
@@ -27500,13 +28445,13 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
27500
28445
|
// "writing to the wrong project" for multi-project users on a shared key / stateless
|
|
27501
28446
|
// HTTP transport where a once-per-session gate can't work.
|
|
27502
28447
|
tracked("project-banner", async () => {
|
|
27503
|
-
if (!adapter2.getProjectInfo) return { banner: "", name: void 0 };
|
|
28448
|
+
if (!adapter2.getProjectInfo) return { banner: "", name: void 0, repoConnected: void 0 };
|
|
27504
28449
|
const projectInfo = await adapter2.getProjectInfo();
|
|
27505
|
-
if (!projectInfo) return { banner: "", name: void 0 };
|
|
28450
|
+
if (!projectInfo) return { banner: "", name: void 0, repoConnected: void 0 };
|
|
27506
28451
|
const banner = getProjectConnectionBanner(projectInfo.name, projectInfo.slug);
|
|
27507
28452
|
return { banner: banner ? `
|
|
27508
28453
|
> ${banner}
|
|
27509
|
-
` : "", name: projectInfo.name };
|
|
28454
|
+
` : "", name: projectInfo.name, repoConnected: !!projectInfo.repo_url };
|
|
27510
28455
|
}),
|
|
27511
28456
|
// Session guidance — proactive nudges (doc_register, context bloat, mode switch)
|
|
27512
28457
|
tracked("session-guidance", async () => {
|
|
@@ -27644,7 +28589,7 @@ ${versionDrift}` : "";
|
|
|
27644
28589
|
const patternsNote = patternsOutcome.status === "fulfilled" ? patternsOutcome.value : "";
|
|
27645
28590
|
const { alertsNote, unactionedIssuesNote } = discoveredIssuesOutcome.status === "fulfilled" ? discoveredIssuesOutcome.value : { alertsNote: "", unactionedIssuesNote: "" };
|
|
27646
28591
|
const skillProposalsNote = skillScanOutcome.status === "fulfilled" ? skillScanOutcome.value : "";
|
|
27647
|
-
const projectBannerResult = projectBannerOutcome.status === "fulfilled" ? projectBannerOutcome.value : { banner: "", name: void 0 };
|
|
28592
|
+
const projectBannerResult = projectBannerOutcome.status === "fulfilled" ? projectBannerOutcome.value : { banner: "", name: void 0, repoConnected: void 0 };
|
|
27648
28593
|
const projectBannerNote = projectBannerResult.banner;
|
|
27649
28594
|
const projectName = projectBannerResult.name;
|
|
27650
28595
|
const sessionGuidanceNote = sessionGuidanceOutcome.status === "fulfilled" ? sessionGuidanceOutcome.value : "";
|
|
@@ -27685,16 +28630,27 @@ ${versionDrift}` : "";
|
|
|
27685
28630
|
preBuildCheckNote = lines.join("\n");
|
|
27686
28631
|
}
|
|
27687
28632
|
}
|
|
27688
|
-
tracker.mark("
|
|
27689
|
-
|
|
27690
|
-
|
|
27691
|
-
|
|
27692
|
-
|
|
27693
|
-
|
|
28633
|
+
tracker.mark("parallel-tail");
|
|
28634
|
+
const sharedContributorsPromise = typeof adapter2.listContributors === "function" ? adapter2.listContributors().catch(() => []) : Promise.resolve([]);
|
|
28635
|
+
const [unblockCandidates, subAgents, teamSummaryLine, releaseHistoryLine, carryForwardRefs] = await Promise.all([
|
|
28636
|
+
tracked("unblock-candidates", () => findUnblockCandidates(adapter2, currentCycle))().catch(() => []),
|
|
28637
|
+
// task-1866: discover project sub-agents for the orient surface (read-only, never throws).
|
|
28638
|
+
listAgents(config2.projectRoot),
|
|
28639
|
+
// task-2071 (MU-3) + task-2072 (MU-5): team summary + release-history — both
|
|
28640
|
+
// multi-member only; solo projects get undefined from both, so orient stays
|
|
28641
|
+
// byte-identical there.
|
|
28642
|
+
tracked("team-summary", () => computeTeamSummary(adapter2, sharedContributorsPromise))().catch(() => void 0),
|
|
28643
|
+
tracked("release-history", () => computeReleaseHistory(adapter2, sharedContributorsPromise))().catch(() => void 0),
|
|
28644
|
+
// task-2751 (C332): resolve every task-NNNN mentioned in the Carry-Forward
|
|
28645
|
+
// prose to its title so orient can name it inline. Built from the board
|
|
28646
|
+
// already in hand — no extra query.
|
|
28647
|
+
resolveCarryForwardRefs(healthResult.carryForward, allTasks, adapter2)
|
|
28648
|
+
]);
|
|
28649
|
+
const unblockSection = formatUnblockSection(unblockCandidates);
|
|
28650
|
+
const unblockNote = unblockSection ? `
|
|
27694
28651
|
|
|
27695
|
-
${
|
|
27696
|
-
|
|
27697
|
-
}
|
|
28652
|
+
${unblockSection}` : "";
|
|
28653
|
+
const teamSummary = [teamSummaryLine, releaseHistoryLine].filter(Boolean).join("\n") || void 0;
|
|
27698
28654
|
let deferredGateNote = "";
|
|
27699
28655
|
if (deepHousekeeping) {
|
|
27700
28656
|
try {
|
|
@@ -27706,16 +28662,22 @@ ${section}`;
|
|
|
27706
28662
|
} catch {
|
|
27707
28663
|
}
|
|
27708
28664
|
}
|
|
28665
|
+
let onboardingCoachingNote = "";
|
|
28666
|
+
try {
|
|
28667
|
+
const adsForCoaching = await sharedActiveDecisionsPromise;
|
|
28668
|
+
onboardingCoachingNote = formatOnboardingCoachingBlock({
|
|
28669
|
+
surface: "orient",
|
|
28670
|
+
repoConnected: projectBannerResult.repoConnected,
|
|
28671
|
+
hasLocalWorkspace: hasLocalWorkspace(),
|
|
28672
|
+
hasActiveDecisions: adsForCoaching.length > 0,
|
|
28673
|
+
cycleNumber: currentCycle,
|
|
28674
|
+
hasActiveCycle: currentCycle > 0 && !cycleIsComplete
|
|
28675
|
+
});
|
|
28676
|
+
} catch {
|
|
28677
|
+
}
|
|
27709
28678
|
tracker.mark("format-summary");
|
|
27710
|
-
const subAgents = await listAgents(config2.projectRoot);
|
|
27711
|
-
const [teamSummaryLine, releaseHistoryLine] = await Promise.all([
|
|
27712
|
-
tracked("team-summary", () => computeTeamSummary(adapter2))().catch(() => void 0),
|
|
27713
|
-
tracked("release-history", () => computeReleaseHistory(adapter2))().catch(() => void 0)
|
|
27714
|
-
]);
|
|
27715
|
-
const teamSummary = [teamSummaryLine, releaseHistoryLine].filter(Boolean).join("\n") || void 0;
|
|
27716
28679
|
const deepHint = deepHousekeeping ? "" : "\n\n*Tip: pass `full: true` for Research Signals + version-drift, or `deep_housekeeping: true` to also check orphaned branches, merged-but-In-Progress tasks, unrecorded commits, unregistered docs, and stale skill forks (implies `full`).*";
|
|
27717
|
-
|
|
27718
|
-
return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName, carryForwardRefs) + unblockNote + deferredGateNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
|
|
28680
|
+
return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName, carryForwardRefs) + unblockNote + deferredGateNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + onboardingCoachingNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
|
|
27719
28681
|
} catch (err) {
|
|
27720
28682
|
const message = err instanceof Error ? err.message : String(err);
|
|
27721
28683
|
const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
|
|
@@ -27770,7 +28732,7 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
|
|
|
27770
28732
|
// src/tools/hierarchy.ts
|
|
27771
28733
|
var hierarchyUpdateTool = {
|
|
27772
28734
|
name: "hierarchy_update",
|
|
27773
|
-
description: "
|
|
28735
|
+
description: "Create or update a horizon, stage, or phase in the project hierarchy (AD-14). For stages and horizons this UPSERTS: if the named entity exists it is updated, otherwise it is CREATED (pass a label). Phases are update-only (they evolve via plan/strategy). For stages you can set exit_criteria (a checklist), or flip a single criterion with set_criterion_met. NEVER auto-advances \u2014 progression is human-in-loop via strategy_review. Does not call the Anthropic API.",
|
|
27774
28736
|
annotations: { title: "Update Hierarchy", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
27775
28737
|
inputSchema: {
|
|
27776
28738
|
type: "object",
|
|
@@ -27778,129 +28740,218 @@ var hierarchyUpdateTool = {
|
|
|
27778
28740
|
level: {
|
|
27779
28741
|
type: "string",
|
|
27780
28742
|
enum: ["phase", "stage", "horizon"],
|
|
27781
|
-
description: "Which hierarchy level to update."
|
|
28743
|
+
description: "Which hierarchy level to create/update."
|
|
27782
28744
|
},
|
|
27783
28745
|
name: {
|
|
27784
28746
|
type: "string",
|
|
27785
|
-
description: "The label or ID of the
|
|
28747
|
+
description: "The label, slug, or ID of the entity. On create, becomes the label if `label` is omitted."
|
|
28748
|
+
},
|
|
28749
|
+
label: {
|
|
28750
|
+
type: "string",
|
|
28751
|
+
description: 'Display label. Required to CREATE a new stage/horizon when `name` does not match an existing one (e.g. "S2: Alpha Cohort").'
|
|
28752
|
+
},
|
|
28753
|
+
slug: {
|
|
28754
|
+
type: "string",
|
|
28755
|
+
description: "Explicit slug for a new entity (stage/horizon). Auto-derived from the label when omitted."
|
|
28756
|
+
},
|
|
28757
|
+
description: {
|
|
28758
|
+
type: "string",
|
|
28759
|
+
description: "Optional longer description (stage/horizon)."
|
|
27786
28760
|
},
|
|
27787
28761
|
status: {
|
|
27788
28762
|
type: "string",
|
|
27789
28763
|
enum: ["Not Started", "In Progress", "Done", "Deferred"],
|
|
27790
|
-
description:
|
|
28764
|
+
description: 'The status to set. On create, defaults to "Not Started".'
|
|
28765
|
+
},
|
|
28766
|
+
sort_order: {
|
|
28767
|
+
type: "number",
|
|
28768
|
+
description: "Display order for a new entity. Auto-computed (max existing + 10) when omitted."
|
|
28769
|
+
},
|
|
28770
|
+
horizon: {
|
|
28771
|
+
type: "string",
|
|
28772
|
+
description: "Parent horizon (name/slug/id) when CREATING a stage. Defaults to the sole horizon if only one exists."
|
|
27791
28773
|
},
|
|
27792
28774
|
exit_criteria: {
|
|
27793
28775
|
type: "array",
|
|
27794
28776
|
items: { type: "string" },
|
|
27795
|
-
description:
|
|
28777
|
+
description: "Checklist defining when a STAGE is done. Each item is a completion condition. REPLACES existing criteria (resets met state \u2014 use set_criterion_met to flip one)."
|
|
28778
|
+
},
|
|
28779
|
+
set_criterion_met: {
|
|
28780
|
+
type: "object",
|
|
28781
|
+
description: "Flip a single stage exit criterion (task-1625). { criterion_id, met, evidence? }.",
|
|
28782
|
+
properties: {
|
|
28783
|
+
criterion_id: { type: "string", description: "The ExitCriterion id to flip." },
|
|
28784
|
+
met: { type: "boolean", description: "true = met, false = unmet." },
|
|
28785
|
+
evidence: { type: "string", description: 'Optional evidence for a met criterion (e.g. "task-1234 shipped").' }
|
|
28786
|
+
},
|
|
28787
|
+
required: ["criterion_id", "met"]
|
|
27796
28788
|
}
|
|
27797
28789
|
},
|
|
27798
28790
|
required: ["level", "name"]
|
|
27799
28791
|
}
|
|
27800
28792
|
};
|
|
27801
28793
|
var VALID_STATUSES3 = /* @__PURE__ */ new Set(["Not Started", "In Progress", "Done", "Deferred"]);
|
|
28794
|
+
function slugify(input) {
|
|
28795
|
+
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "item";
|
|
28796
|
+
}
|
|
28797
|
+
function nextSortOrder(existing) {
|
|
28798
|
+
if (existing.length === 0) return 10;
|
|
28799
|
+
return Math.max(...existing.map((e) => e.sortOrder)) + 10;
|
|
28800
|
+
}
|
|
27802
28801
|
async function handleHierarchyUpdate(adapter2, args) {
|
|
27803
28802
|
const level = args.level;
|
|
27804
28803
|
const name = args.name;
|
|
28804
|
+
const label = args.label;
|
|
28805
|
+
const slug = args.slug;
|
|
28806
|
+
const description = args.description;
|
|
27805
28807
|
const status = args.status;
|
|
28808
|
+
const sortOrder = typeof args.sort_order === "number" ? args.sort_order : void 0;
|
|
28809
|
+
const horizonRef = args.horizon;
|
|
27806
28810
|
const exitCriteria = args.exit_criteria;
|
|
28811
|
+
const setCriterion = args.set_criterion_met;
|
|
27807
28812
|
if (!level || !name) {
|
|
27808
28813
|
return errorResponse("Missing required parameters: level, name.");
|
|
27809
28814
|
}
|
|
27810
|
-
if (!status && !exitCriteria) {
|
|
27811
|
-
return errorResponse("Nothing to update. Provide at least one of: status, exit_criteria.");
|
|
27812
|
-
}
|
|
27813
28815
|
if (level !== "phase" && level !== "stage" && level !== "horizon") {
|
|
27814
28816
|
return errorResponse(`Invalid level "${level}". Must be "phase", "stage", or "horizon".`);
|
|
27815
28817
|
}
|
|
27816
28818
|
if (status && !VALID_STATUSES3.has(status)) {
|
|
27817
28819
|
return errorResponse(`Invalid status "${status}". Must be one of: Not Started, In Progress, Done, Deferred.`);
|
|
27818
28820
|
}
|
|
27819
|
-
if (exitCriteria !== void 0 && level !== "stage") {
|
|
27820
|
-
return errorResponse("exit_criteria can only be
|
|
28821
|
+
if ((exitCriteria !== void 0 || setCriterion !== void 0) && level !== "stage") {
|
|
28822
|
+
return errorResponse("exit_criteria and set_criterion_met can only be used on stages.");
|
|
28823
|
+
}
|
|
28824
|
+
if (!status && exitCriteria === void 0 && setCriterion === void 0 && !label) {
|
|
28825
|
+
return errorResponse("Nothing to do. Provide at least one of: status, exit_criteria, set_criterion_met, or label (to create).");
|
|
27821
28826
|
}
|
|
27822
28827
|
try {
|
|
27823
28828
|
if (level === "phase") {
|
|
27824
|
-
|
|
27825
|
-
return errorResponse("Phase management is not supported by the current adapter.");
|
|
27826
|
-
}
|
|
27827
|
-
const phases = await adapter2.readPhases();
|
|
27828
|
-
const phase = phases.find(
|
|
27829
|
-
(p) => p.label.toLowerCase() === name.toLowerCase() || p.id === name || p.slug === name
|
|
27830
|
-
);
|
|
27831
|
-
if (!phase) {
|
|
27832
|
-
const available = phases.map((p) => p.label).join(", ");
|
|
27833
|
-
return errorResponse(`Phase "${name}" not found. Available phases: ${available || "none"}`);
|
|
27834
|
-
}
|
|
27835
|
-
if (!status) {
|
|
27836
|
-
return errorResponse("status is required for phase updates.");
|
|
27837
|
-
}
|
|
27838
|
-
if (phase.status === status) {
|
|
27839
|
-
return textResponse(`Phase "${phase.label}" is already "${status}". No change made.`);
|
|
27840
|
-
}
|
|
27841
|
-
const oldStatus2 = phase.status;
|
|
27842
|
-
await adapter2.updatePhaseStatus(phase.id, status);
|
|
27843
|
-
return textResponse(`Phase updated: **${phase.label}** ${oldStatus2} \u2192 ${status}`);
|
|
28829
|
+
return await handlePhase(adapter2, name, status);
|
|
27844
28830
|
}
|
|
27845
28831
|
if (level === "stage") {
|
|
27846
|
-
|
|
27847
|
-
return errorResponse("Stage management is not supported by the current adapter.");
|
|
27848
|
-
}
|
|
27849
|
-
const stages = await adapter2.readStages();
|
|
27850
|
-
const stage = stages.find(
|
|
27851
|
-
(s) => s.label.toLowerCase() === name.toLowerCase() || s.id === name || s.slug === name
|
|
27852
|
-
);
|
|
27853
|
-
if (!stage) {
|
|
27854
|
-
const available = stages.map((s) => s.label).join(", ");
|
|
27855
|
-
return errorResponse(`Stage "${name}" not found. Available stages: ${available || "none"}`);
|
|
27856
|
-
}
|
|
27857
|
-
const resultLines = [];
|
|
27858
|
-
if (status) {
|
|
27859
|
-
if (!adapter2.updateStageStatus) {
|
|
27860
|
-
return errorResponse("Stage status updates are not supported by the current adapter.");
|
|
27861
|
-
}
|
|
27862
|
-
if (stage.status === status) {
|
|
27863
|
-
resultLines.push(`Stage "${stage.label}" is already "${status}".`);
|
|
27864
|
-
} else {
|
|
27865
|
-
const oldStatus2 = stage.status;
|
|
27866
|
-
await adapter2.updateStageStatus(stage.id, status);
|
|
27867
|
-
resultLines.push(`Stage updated: **${stage.label}** ${oldStatus2} \u2192 ${status}`);
|
|
27868
|
-
}
|
|
27869
|
-
}
|
|
27870
|
-
if (exitCriteria !== void 0) {
|
|
27871
|
-
if (!adapter2.updateStageExitCriteria) {
|
|
27872
|
-
return errorResponse("Exit criteria updates are not supported by the current adapter.");
|
|
27873
|
-
}
|
|
27874
|
-
await adapter2.updateStageExitCriteria(stage.id, exitCriteria);
|
|
27875
|
-
resultLines.push(`Exit criteria set (${exitCriteria.length} item${exitCriteria.length !== 1 ? "s" : ""}):`);
|
|
27876
|
-
exitCriteria.forEach((c) => resultLines.push(` - ${c}`));
|
|
27877
|
-
}
|
|
27878
|
-
return textResponse(resultLines.join("\n"));
|
|
27879
|
-
}
|
|
27880
|
-
if (!adapter2.readHorizons || !adapter2.updateHorizonStatus) {
|
|
27881
|
-
return errorResponse("Horizon management is not supported by the current adapter.");
|
|
27882
|
-
}
|
|
27883
|
-
const horizons = await adapter2.readHorizons();
|
|
27884
|
-
const horizon = horizons.find(
|
|
27885
|
-
(h) => h.label.toLowerCase() === name.toLowerCase() || h.id === name || h.slug === name
|
|
27886
|
-
);
|
|
27887
|
-
if (!horizon) {
|
|
27888
|
-
const available = horizons.map((h) => h.label).join(", ");
|
|
27889
|
-
return errorResponse(`Horizon "${name}" not found. Available horizons: ${available || "none"}`);
|
|
27890
|
-
}
|
|
27891
|
-
if (!status) {
|
|
27892
|
-
return errorResponse("status is required for horizon updates.");
|
|
27893
|
-
}
|
|
27894
|
-
if (horizon.status === status) {
|
|
27895
|
-
return textResponse(`Horizon "${horizon.label}" is already "${status}". No change made.`);
|
|
28832
|
+
return await handleStage(adapter2, { name, label, slug, description, status, sortOrder, horizonRef, exitCriteria, setCriterion });
|
|
27896
28833
|
}
|
|
27897
|
-
|
|
27898
|
-
await adapter2.updateHorizonStatus(horizon.id, status);
|
|
27899
|
-
return textResponse(`Horizon updated: **${horizon.label}** ${oldStatus} \u2192 ${status}`);
|
|
28834
|
+
return await handleHorizon(adapter2, { name, label, slug, description, status, sortOrder });
|
|
27900
28835
|
} catch (err) {
|
|
27901
28836
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
27902
28837
|
}
|
|
27903
28838
|
}
|
|
28839
|
+
async function handlePhase(adapter2, name, status) {
|
|
28840
|
+
if (!adapter2.readPhases || !adapter2.updatePhaseStatus) {
|
|
28841
|
+
return errorResponse("Phase management is not supported by the current adapter.");
|
|
28842
|
+
}
|
|
28843
|
+
if (!status) return errorResponse("status is required for phase updates.");
|
|
28844
|
+
const phases = await adapter2.readPhases();
|
|
28845
|
+
const phase = phases.find((p) => p.label.toLowerCase() === name.toLowerCase() || p.id === name || p.slug === name);
|
|
28846
|
+
if (!phase) {
|
|
28847
|
+
const available = phases.map((p) => p.label).join(", ");
|
|
28848
|
+
return errorResponse(`Phase "${name}" not found. Available phases: ${available || "none"} (phases are update-only \u2014 create them via plan/setup).`);
|
|
28849
|
+
}
|
|
28850
|
+
if (phase.status === status) return textResponse(`Phase "${phase.label}" is already "${status}". No change made.`);
|
|
28851
|
+
const oldStatus = phase.status;
|
|
28852
|
+
await adapter2.updatePhaseStatus(phase.id, status);
|
|
28853
|
+
return textResponse(`Phase updated: **${phase.label}** ${oldStatus} \u2192 ${status}`);
|
|
28854
|
+
}
|
|
28855
|
+
async function handleStage(adapter2, a) {
|
|
28856
|
+
if (!adapter2.readStages) {
|
|
28857
|
+
return errorResponse("Stage management is not supported by the current adapter.");
|
|
28858
|
+
}
|
|
28859
|
+
const stages = await adapter2.readStages();
|
|
28860
|
+
let stage = stages.find(
|
|
28861
|
+
(s) => s.label.toLowerCase() === a.name.toLowerCase() || s.id === a.name || s.slug === a.name
|
|
28862
|
+
);
|
|
28863
|
+
const resultLines = [];
|
|
28864
|
+
let created = false;
|
|
28865
|
+
if (!stage) {
|
|
28866
|
+
if (!adapter2.createHorizon || !adapter2.createStage || !adapter2.readHorizons) {
|
|
28867
|
+
return errorResponse("Stage creation is not supported by the current adapter.");
|
|
28868
|
+
}
|
|
28869
|
+
const createLabel = a.label ?? a.name;
|
|
28870
|
+
const horizons = await adapter2.readHorizons();
|
|
28871
|
+
let parent;
|
|
28872
|
+
if (a.horizonRef) {
|
|
28873
|
+
parent = horizons.find((h) => h.label.toLowerCase() === a.horizonRef.toLowerCase() || h.id === a.horizonRef || h.slug === a.horizonRef);
|
|
28874
|
+
if (!parent) {
|
|
28875
|
+
const available = horizons.map((h) => h.label).join(", ");
|
|
28876
|
+
return errorResponse(`Parent horizon "${a.horizonRef}" not found. Available horizons: ${available || "none"}.`);
|
|
28877
|
+
}
|
|
28878
|
+
} else if (horizons.length === 1) {
|
|
28879
|
+
parent = horizons[0];
|
|
28880
|
+
} else if (horizons.length === 0) {
|
|
28881
|
+
return errorResponse('No horizon exists to attach the stage to. Create a horizon first (level:"horizon", label:"H1: ...").');
|
|
28882
|
+
} else {
|
|
28883
|
+
return errorResponse(`Multiple horizons exist \u2014 pass \`horizon\` to say which one the stage belongs to: ${horizons.map((h) => h.label).join(", ")}.`);
|
|
28884
|
+
}
|
|
28885
|
+
const newId = await adapter2.createStage({
|
|
28886
|
+
slug: a.slug ?? slugify(createLabel),
|
|
28887
|
+
label: createLabel,
|
|
28888
|
+
description: a.description,
|
|
28889
|
+
status: a.status ?? "Not Started",
|
|
28890
|
+
sortOrder: a.sortOrder ?? nextSortOrder(stages.filter((s) => s.horizonId === parent.id)),
|
|
28891
|
+
horizonId: parent.id
|
|
28892
|
+
});
|
|
28893
|
+
const refreshed = await adapter2.readStages();
|
|
28894
|
+
stage = refreshed.find((s) => s.id === newId);
|
|
28895
|
+
if (!stage) return errorResponse("Stage was created but could not be re-read.");
|
|
28896
|
+
created = true;
|
|
28897
|
+
resultLines.push(`Stage created: **${stage.label}** (under ${parent.label}, status ${stage.status})`);
|
|
28898
|
+
}
|
|
28899
|
+
if (a.status && !created) {
|
|
28900
|
+
if (!adapter2.updateStageStatus) return errorResponse("Stage status updates are not supported by the current adapter.");
|
|
28901
|
+
if (stage.status === a.status) {
|
|
28902
|
+
resultLines.push(`Stage "${stage.label}" is already "${a.status}".`);
|
|
28903
|
+
} else {
|
|
28904
|
+
const oldStatus = stage.status;
|
|
28905
|
+
await adapter2.updateStageStatus(stage.id, a.status);
|
|
28906
|
+
resultLines.push(`Stage updated: **${stage.label}** ${oldStatus} \u2192 ${a.status}`);
|
|
28907
|
+
}
|
|
28908
|
+
}
|
|
28909
|
+
if (a.exitCriteria !== void 0) {
|
|
28910
|
+
if (!adapter2.updateStageExitCriteria) return errorResponse("Exit criteria updates are not supported by the current adapter.");
|
|
28911
|
+
await adapter2.updateStageExitCriteria(stage.id, a.exitCriteria);
|
|
28912
|
+
resultLines.push(`Exit criteria set (${a.exitCriteria.length} item${a.exitCriteria.length !== 1 ? "s" : ""}, all unmet):`);
|
|
28913
|
+
a.exitCriteria.forEach((c) => resultLines.push(` - ${c}`));
|
|
28914
|
+
}
|
|
28915
|
+
if (a.setCriterion !== void 0) {
|
|
28916
|
+
if (!adapter2.setCriterionMet) return errorResponse("set_criterion_met is not supported by the current adapter.");
|
|
28917
|
+
await adapter2.setCriterionMet(stage.id, a.setCriterion.criterion_id, a.setCriterion.met, a.setCriterion.evidence ?? null);
|
|
28918
|
+
const after = (await adapter2.readStages()).find((s) => s.id === stage.id);
|
|
28919
|
+
const crit = after?.exitCriteria?.find((c) => c.id === a.setCriterion.criterion_id);
|
|
28920
|
+
if (!crit) {
|
|
28921
|
+
resultLines.push(`\u26A0\uFE0F Criterion id "${a.setCriterion.criterion_id}" not found on this stage \u2014 no change.`);
|
|
28922
|
+
} else {
|
|
28923
|
+
const met = after?.exitCriteria?.filter((c) => c.met).length ?? 0;
|
|
28924
|
+
const total = after?.exitCriteria?.length ?? 0;
|
|
28925
|
+
resultLines.push(`Criterion "${crit.text}" \u2192 ${crit.met ? "met" : "unmet"}. [${met}/${total} criteria met]`);
|
|
28926
|
+
}
|
|
28927
|
+
}
|
|
28928
|
+
return textResponse(resultLines.join("\n"));
|
|
28929
|
+
}
|
|
28930
|
+
async function handleHorizon(adapter2, a) {
|
|
28931
|
+
if (!adapter2.readHorizons) {
|
|
28932
|
+
return errorResponse("Horizon management is not supported by the current adapter.");
|
|
28933
|
+
}
|
|
28934
|
+
const horizons = await adapter2.readHorizons();
|
|
28935
|
+
const horizon = horizons.find((h) => h.label.toLowerCase() === a.name.toLowerCase() || h.id === a.name || h.slug === a.name);
|
|
28936
|
+
if (!horizon) {
|
|
28937
|
+
if (!adapter2.createHorizon) return errorResponse("Horizon creation is not supported by the current adapter.");
|
|
28938
|
+
const createLabel = a.label ?? a.name;
|
|
28939
|
+
const newId = await adapter2.createHorizon({
|
|
28940
|
+
slug: a.slug ?? slugify(createLabel),
|
|
28941
|
+
label: createLabel,
|
|
28942
|
+
description: a.description,
|
|
28943
|
+
status: a.status ?? "Not Started",
|
|
28944
|
+
sortOrder: a.sortOrder ?? nextSortOrder(horizons)
|
|
28945
|
+
});
|
|
28946
|
+
return textResponse(`Horizon created: **${createLabel}** (id ${newId}, status ${a.status ?? "Not Started"})`);
|
|
28947
|
+
}
|
|
28948
|
+
if (!a.status) return errorResponse("status is required to update an existing horizon (or pass a new name to create one).");
|
|
28949
|
+
if (horizon.status === a.status) return textResponse(`Horizon "${horizon.label}" is already "${a.status}". No change made.`);
|
|
28950
|
+
if (!adapter2.updateHorizonStatus) return errorResponse("Horizon status updates are not supported by the current adapter.");
|
|
28951
|
+
const oldStatus = horizon.status;
|
|
28952
|
+
await adapter2.updateHorizonStatus(horizon.id, a.status);
|
|
28953
|
+
return textResponse(`Horizon updated: **${horizon.label}** ${oldStatus} \u2192 ${a.status}`);
|
|
28954
|
+
}
|
|
27904
28955
|
|
|
27905
28956
|
// src/services/zoom-out.ts
|
|
27906
28957
|
var BUDGET_SOFT = 12e4;
|
|
@@ -29906,6 +30957,7 @@ var PAPI_TOOLS = [
|
|
|
29906
30957
|
buildExecuteTool,
|
|
29907
30958
|
buildCancelTool,
|
|
29908
30959
|
ideaTool,
|
|
30960
|
+
backlogImportTool,
|
|
29909
30961
|
bugTool,
|
|
29910
30962
|
bugListTool,
|
|
29911
30963
|
adHocTool,
|
|
@@ -30088,6 +31140,8 @@ function createServer(adapter2, config2) {
|
|
|
30088
31140
|
return handleBuildCancel(adapter2, safeArgs);
|
|
30089
31141
|
case "idea":
|
|
30090
31142
|
return handleIdea(adapter2, config2, safeArgs);
|
|
31143
|
+
case "backlog_import":
|
|
31144
|
+
return handleBacklogImport(adapter2, safeArgs);
|
|
30091
31145
|
case "bug":
|
|
30092
31146
|
return handleBug(adapter2, config2, safeArgs);
|
|
30093
31147
|
case "bug_list":
|
|
@@ -30664,7 +31718,11 @@ async function dispatchRequest(args) {
|
|
|
30664
31718
|
const requestConfig = {
|
|
30665
31719
|
...baseConfig,
|
|
30666
31720
|
adapterType: "proxy",
|
|
30667
|
-
projectId: effectiveProjectId
|
|
31721
|
+
projectId: effectiveProjectId,
|
|
31722
|
+
// Remote transport: the client cannot read files this server writes, so
|
|
31723
|
+
// prepare must keep returning inline blobs (2905's budget bounds them),
|
|
31724
|
+
// never a server-side file path (task-2906).
|
|
31725
|
+
localFilesystem: false
|
|
30668
31726
|
};
|
|
30669
31727
|
const server2 = createServer(adapter2, requestConfig);
|
|
30670
31728
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|