@papi-ai/server 0.7.35 → 0.7.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backfill-cycle-metrics.js +27 -0
- package/dist/index.js +352 -27
- package/dist/prompts.js +39 -6
- package/package.json +1 -1
|
@@ -50,6 +50,7 @@ __export(git_exports, {
|
|
|
50
50
|
hasRemote: () => hasRemote,
|
|
51
51
|
hasUncommittedChanges: () => hasUncommittedChanges,
|
|
52
52
|
hasUnpushedCommits: () => hasUnpushedCommits,
|
|
53
|
+
isBranchContentAlreadyInBase: () => isBranchContentAlreadyInBase,
|
|
53
54
|
isBranchMergedInto: () => isBranchMergedInto,
|
|
54
55
|
isGhAvailable: () => isGhAvailable,
|
|
55
56
|
isGitAvailable: () => isGitAvailable,
|
|
@@ -807,6 +808,32 @@ function isBranchMergedInto(cwd, branch, baseBranch) {
|
|
|
807
808
|
return false;
|
|
808
809
|
}
|
|
809
810
|
}
|
|
811
|
+
function isBranchContentAlreadyInBase(cwd, branch, baseBranch) {
|
|
812
|
+
const resolveCommit = (ref) => {
|
|
813
|
+
try {
|
|
814
|
+
return execFileSync("git", ["rev-parse", "--verify", `${ref}^{commit}`], { cwd, encoding: "utf-8" }).trim();
|
|
815
|
+
} catch {
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
const branchRef = resolveCommit(branch) ? branch : resolveCommit(`origin/${branch}`) ? `origin/${branch}` : null;
|
|
820
|
+
if (!branchRef) return false;
|
|
821
|
+
try {
|
|
822
|
+
const out = execFileSync("git", ["merge-tree", "--write-tree", baseBranch, branchRef], {
|
|
823
|
+
cwd,
|
|
824
|
+
encoding: "utf-8"
|
|
825
|
+
}).trim();
|
|
826
|
+
const mergedTree = out.split("\n")[0]?.trim();
|
|
827
|
+
if (!mergedTree) return false;
|
|
828
|
+
const baseTree = execFileSync("git", ["rev-parse", `${baseBranch}^{tree}`], {
|
|
829
|
+
cwd,
|
|
830
|
+
encoding: "utf-8"
|
|
831
|
+
}).trim();
|
|
832
|
+
return mergedTree === baseTree;
|
|
833
|
+
} catch {
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
810
837
|
function pickModuleCycleBranch(candidates, cycleNumber, module) {
|
|
811
838
|
if (candidates.length === 0) return void 0;
|
|
812
839
|
const expected = cycleBranchName(cycleNumber, module);
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,7 @@ __export(git_exports, {
|
|
|
51
51
|
hasRemote: () => hasRemote,
|
|
52
52
|
hasUncommittedChanges: () => hasUncommittedChanges,
|
|
53
53
|
hasUnpushedCommits: () => hasUnpushedCommits,
|
|
54
|
+
isBranchContentAlreadyInBase: () => isBranchContentAlreadyInBase,
|
|
54
55
|
isBranchMergedInto: () => isBranchMergedInto,
|
|
55
56
|
isGhAvailable: () => isGhAvailable,
|
|
56
57
|
isGitAvailable: () => isGitAvailable,
|
|
@@ -808,6 +809,32 @@ function isBranchMergedInto(cwd, branch, baseBranch) {
|
|
|
808
809
|
return false;
|
|
809
810
|
}
|
|
810
811
|
}
|
|
812
|
+
function isBranchContentAlreadyInBase(cwd, branch, baseBranch) {
|
|
813
|
+
const resolveCommit = (ref) => {
|
|
814
|
+
try {
|
|
815
|
+
return execFileSync("git", ["rev-parse", "--verify", `${ref}^{commit}`], { cwd, encoding: "utf-8" }).trim();
|
|
816
|
+
} catch {
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
const branchRef = resolveCommit(branch) ? branch : resolveCommit(`origin/${branch}`) ? `origin/${branch}` : null;
|
|
821
|
+
if (!branchRef) return false;
|
|
822
|
+
try {
|
|
823
|
+
const out = execFileSync("git", ["merge-tree", "--write-tree", baseBranch, branchRef], {
|
|
824
|
+
cwd,
|
|
825
|
+
encoding: "utf-8"
|
|
826
|
+
}).trim();
|
|
827
|
+
const mergedTree = out.split("\n")[0]?.trim();
|
|
828
|
+
if (!mergedTree) return false;
|
|
829
|
+
const baseTree = execFileSync("git", ["rev-parse", `${baseBranch}^{tree}`], {
|
|
830
|
+
cwd,
|
|
831
|
+
encoding: "utf-8"
|
|
832
|
+
}).trim();
|
|
833
|
+
return mergedTree === baseTree;
|
|
834
|
+
} catch {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
811
838
|
function pickModuleCycleBranch(candidates, cycleNumber, module) {
|
|
812
839
|
if (candidates.length === 0) return void 0;
|
|
813
840
|
const expected = cycleBranchName(cycleNumber, module);
|
|
@@ -4639,13 +4666,35 @@ import {
|
|
|
4639
4666
|
|
|
4640
4667
|
// src/config.ts
|
|
4641
4668
|
import path from "path";
|
|
4669
|
+
|
|
4670
|
+
// src/lib/help-text.ts
|
|
4671
|
+
var PAPI_DISCORD = "https://discord.gg/papi";
|
|
4672
|
+
var PAPI_DOCS = "https://getpapi.ai/docs/install";
|
|
4673
|
+
var HELP_FOOTER = `
|
|
4674
|
+
Need a hand?
|
|
4675
|
+
\u2022 Make sure you're in your project's root folder (where your code lives), then say "run setup".
|
|
4676
|
+
\u2022 Diagnose your setup: npx @papi-ai/server doctor
|
|
4677
|
+
\u2022 Hit a PAPI bug or have an idea? Say "report a bug to PAPI" \u2014 it goes straight to the
|
|
4678
|
+
PAPI team (not your project backlog), with diagnostics attached.
|
|
4679
|
+
\u2022 Get help: ${PAPI_DISCORD} \u2022 Docs: ${PAPI_DOCS}
|
|
4680
|
+
`;
|
|
4681
|
+
var HELP_FOOTER_MD = `
|
|
4682
|
+
|
|
4683
|
+
## Need a hand?
|
|
4684
|
+
- Make sure you're in your project's **root folder** (where your code lives), then say "run setup".
|
|
4685
|
+
- Diagnose your setup: \`npx @papi-ai/server doctor\`
|
|
4686
|
+
- Hit a PAPI bug or have an idea? Say **"report a bug to PAPI"** \u2014 it goes straight to the PAPI team (not your project backlog), with diagnostics attached.
|
|
4687
|
+
- Get help: [Discord](${PAPI_DISCORD}) \xB7 [Docs](${PAPI_DOCS})
|
|
4688
|
+
`;
|
|
4689
|
+
|
|
4690
|
+
// src/config.ts
|
|
4642
4691
|
function loadConfig() {
|
|
4643
4692
|
const projectArgIdx = process.argv.indexOf("--project");
|
|
4644
4693
|
const configuredRoot = projectArgIdx !== -1 ? process.argv[projectArgIdx + 1] : process.env.PAPI_PROJECT_DIR;
|
|
4645
4694
|
const projectRoot = configuredRoot ?? process.cwd();
|
|
4646
4695
|
if (!configuredRoot) {
|
|
4647
4696
|
process.stderr.write(
|
|
4648
|
-
|
|
4697
|
+
"\nPAPI is running, but no project is configured here.\n" + HELP_FOOTER + "\n"
|
|
4649
4698
|
);
|
|
4650
4699
|
}
|
|
4651
4700
|
const anthropicApiKey = process.env.PAPI_API_KEY ?? "";
|
|
@@ -4693,7 +4742,7 @@ If you intentionally self-host, set PAPI_SELF_HOST=1 in your environment to bypa
|
|
|
4693
4742
|
}
|
|
4694
4743
|
if (databaseUrl && !papiEndpoint && !explicitAdapter && !selfHostOptIn && !dataApiKey) {
|
|
4695
4744
|
throw new Error(
|
|
4696
|
-
'DATABASE_URL is set but PAPI_ADAPTER is not specified.\n\nIf you want to self-host PAPI against your own database, add to your .mcp.json env:\n "PAPI_ADAPTER": "pg"\n\nIf you are an external user with a PAPI account, remove DATABASE_URL from your config.\nExternal users authenticate via PAPI_DATA_API_KEY \u2014 no DATABASE_URL needed.\n
|
|
4745
|
+
'DATABASE_URL is set but PAPI_ADAPTER is not specified.\n\nIf you want to self-host PAPI against your own database, add to your .mcp.json env:\n "PAPI_ADAPTER": "pg"\n\nIf you are an external user with a PAPI account, remove DATABASE_URL from your config.\nExternal users authenticate via PAPI_DATA_API_KEY \u2014 no DATABASE_URL needed.\n' + HELP_FOOTER
|
|
4697
4746
|
);
|
|
4698
4747
|
}
|
|
4699
4748
|
let adapterType = papiEndpoint ? "pg" : databaseUrl && (explicitAdapter === "pg" || selfHostOptIn) ? "pg" : dataEndpoint ? "proxy" : explicitAdapter ? explicitAdapter : "proxy";
|
|
@@ -4712,7 +4761,8 @@ and unlocks dashboard features when you're ready.
|
|
|
4712
4761
|
After signing up, add this to your .mcp.json env config:
|
|
4713
4762
|
"PAPI_USER_ID": "your-email@example.com"
|
|
4714
4763
|
|
|
4715
|
-
Already have an account? Make sure PAPI_USER_ID is set in your .mcp.json env config
|
|
4764
|
+
Already have an account? Make sure PAPI_USER_ID is set in your .mcp.json env config.
|
|
4765
|
+
` + HELP_FOOTER
|
|
4716
4766
|
);
|
|
4717
4767
|
}
|
|
4718
4768
|
return {
|
|
@@ -7064,6 +7114,9 @@ ${footer}`);
|
|
|
7064
7114
|
await this.write("PLANNING_LOG.md", updated);
|
|
7065
7115
|
}
|
|
7066
7116
|
};
|
|
7117
|
+
function newTaskJoinKey(task, index) {
|
|
7118
|
+
return task.tempId ?? `new-${index + 1}`;
|
|
7119
|
+
}
|
|
7067
7120
|
var NONE_PATTERN2 = /^none\b/i;
|
|
7068
7121
|
function normalizeText2(text) {
|
|
7069
7122
|
return text.trim().toLowerCase().replace(/[.,;:!]+$/, "").replace(/\s+/g, " ");
|
|
@@ -7958,6 +8011,39 @@ function formatDerivedMetrics(snapshots, backlogTasks) {
|
|
|
7958
8011
|
}
|
|
7959
8012
|
return lines.join("\n");
|
|
7960
8013
|
}
|
|
8014
|
+
function formatLearningPatterns(patterns) {
|
|
8015
|
+
if (!patterns || patterns.length === 0) return void 0;
|
|
8016
|
+
const ranked = [...patterns].sort(
|
|
8017
|
+
(a, b2) => b2.cycles.length - a.cycles.length || b2.frequency - a.frequency
|
|
8018
|
+
);
|
|
8019
|
+
const strong = ranked.filter((p) => p.cycles.length >= 3);
|
|
8020
|
+
const shown = (strong.length > 0 ? strong : ranked).slice(0, 12);
|
|
8021
|
+
if (shown.length === 0) return void 0;
|
|
8022
|
+
const lines = shown.map(
|
|
8023
|
+
(p) => `- \`${p.tag}\` \u2014 recurred in ${p.cycles.length} cycles (${p.frequency}\xD7), most recent C${Math.max(...p.cycles)}`
|
|
8024
|
+
);
|
|
8025
|
+
return lines.join("\n");
|
|
8026
|
+
}
|
|
8027
|
+
function formatDecisionScorePatterns(patterns) {
|
|
8028
|
+
if (!patterns || patterns.length === 0) return void 0;
|
|
8029
|
+
const HIGH_RISK = 15;
|
|
8030
|
+
const moved = (p) => p.previousScore != null && p.previousScore !== p.totalScore;
|
|
8031
|
+
const relevant = patterns.filter((p) => p.totalScore > HIGH_RISK || moved(p));
|
|
8032
|
+
if (relevant.length === 0) return void 0;
|
|
8033
|
+
const ranked = [...relevant].sort(
|
|
8034
|
+
(a, b2) => b2.totalScore - a.totalScore || Math.abs((b2.previousScore ?? b2.totalScore) - b2.totalScore) - Math.abs((a.previousScore ?? a.totalScore) - a.totalScore)
|
|
8035
|
+
);
|
|
8036
|
+
const lines = ranked.slice(0, 10).map((p) => {
|
|
8037
|
+
let line = `- ${p.decisionId}: ${p.title} [${p.confidence}] \u2014 score ${p.totalScore}/25`;
|
|
8038
|
+
if (p.previousScore != null && p.previousScore !== p.totalScore) {
|
|
8039
|
+
const arrow = p.totalScore > p.previousScore ? "\u2191 riskier" : "\u2193 safer";
|
|
8040
|
+
line += ` (${arrow} from ${p.previousScore}/25${p.previousCycle != null ? ` at C${p.previousCycle}` : ""})`;
|
|
8041
|
+
}
|
|
8042
|
+
if (p.totalScore > HIGH_RISK) line += " \u26A0 high-risk";
|
|
8043
|
+
return line;
|
|
8044
|
+
});
|
|
8045
|
+
return lines.join("\n");
|
|
8046
|
+
}
|
|
7961
8047
|
function formatBuildPatterns(patterns) {
|
|
7962
8048
|
const sections = [];
|
|
7963
8049
|
if (patterns.recurringSurprises.length > 0) {
|
|
@@ -8230,7 +8316,7 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
8230
8316
|
"boardHealth": "string \u2014 e.g. 5 tasks (3 backlog, 2 done)",
|
|
8231
8317
|
"strategicDirection": "string \u2014 one sentence about current phase/direction",
|
|
8232
8318
|
"recommendedTaskId": "string or null \u2014 task ID to set In Progress",
|
|
8233
|
-
"cycleHandoffs": [{"taskId": "string \u2014 existing task ID or new-
|
|
8319
|
+
"cycleHandoffs": [{"taskId": "string \u2014 an existing task ID, or the EXACT tempId of a newTasks entry (e.g. "new-1") it provides the handoff for", "buildHandoff": "string \u2014 full BUILD HANDOFF text"}],
|
|
8234
8320
|
"newTasks": [],
|
|
8235
8321
|
"boardCorrections": [],
|
|
8236
8322
|
"productBrief": null,
|
|
@@ -8275,7 +8361,7 @@ Everything in Part 1 (natural language) is **display-only**. Part 2 (structured
|
|
|
8275
8361
|
**Example with populated fields (DO NOT copy literally \u2014 adapt to your actual analysis):**
|
|
8276
8362
|
\`\`\`json
|
|
8277
8363
|
{
|
|
8278
|
-
"newTasks": [{"title": "Example task", "status": "Backlog", "priority": "P2 Medium", "complexity": "Small", "module": "Core", "epic": "Platform", "phase": "Phase 1", "owner": "TBD", "notes": "Created during triage"}],
|
|
8364
|
+
"newTasks": [{"tempId": "new-1", "title": "Example task", "status": "Backlog", "priority": "P2 Medium", "complexity": "Small", "module": "Core", "epic": "Platform", "phase": "Phase 1", "owner": "TBD", "notes": "Created during triage"}],
|
|
8279
8365
|
"activeDecisions": [{"id": "AD-3", "body": "### AD-3: Use REST over GraphQL [Confidence: HIGH]\\n\\n- **Decision:** REST API for v1.\\n- **Evidence:** Simpler for current scope."}],
|
|
8280
8366
|
"boardCorrections": [{"taskId": "task-005", "updates": {"priority": "P1 High", "reviewed": true}}, {"taskId": "task-009", "updates": {"status": "Cancelled", "closureReason": "Duplicates task-003"}}],
|
|
8281
8367
|
"cycleHandoffs": [{"taskId": "task-002", "buildHandoff": "BUILD HANDOFF \u2014 task-002\\nTask: ..."}]
|
|
@@ -8313,7 +8399,7 @@ This is Cycle 0 \u2014 the first planning cycle for a brand-new project.
|
|
|
8313
8399
|
|
|
8314
8400
|
4. **First Active Decision** \u2014 If the description implies a clear architectural choice, create AD-1 with Confidence: MEDIUM. If no clear choice, skip this.
|
|
8315
8401
|
|
|
8316
|
-
5. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for EVERY task created in step 3 (all 3-5 tasks). Include each in the \`cycleHandoffs\` array.
|
|
8402
|
+
5. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for EVERY task created in step 3 (all 3-5 tasks). Include each in the \`cycleHandoffs\` array. **tempId join (REQUIRED \u2014 mismatches silently scramble handoffs):** give EVERY \`newTasks\` entry a unique \`tempId\` (\`"new-1"\`, \`"new-2"\`, \u2026), and set each \`cycleHandoffs\` \`taskId\` to the EXACT \`tempId\` of the newTask it belongs to. Do NOT rely on array order, and do NOT reuse a tempId. The builder needs handoffs to run \`build_execute\` \u2014 without them, tasks must be completed via \`ad_hoc\`, which breaks the normal flow.
|
|
8317
8403
|
|
|
8318
8404
|
### Structured output for Bootstrap:
|
|
8319
8405
|
In the JSON block, you MUST include:
|
|
@@ -8484,7 +8570,8 @@ Standard planning cycle with full board review.
|
|
|
8484
8570
|
- **P1 High** \u2014 Strategically aligned: directly advances the current horizon, phase, or Active Decision goals.
|
|
8485
8571
|
- **P2 Medium** \u2014 Valuable but not strategically urgent: quality improvements, efficiency, polish, infra.
|
|
8486
8572
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
8487
|
-
Within the same priority level, prefer tasks with the highest **impact-to-effort ratio**. Impact is measured by: (a) strategic alignment \u2014 does it advance the current horizon/phase? (b) unlocks other work \u2014 are tasks blocked by this? (c) user-facing \u2014 does it change what users see? (d) compounds over time \u2014 does it make future cycles faster? A high-impact Medium task beats a low-impact Small task at the same priority level. Justify in 2-3 sentences.
|
|
8573
|
+
Within the same priority level, prefer tasks with the highest **impact-to-effort ratio**. Impact is measured by: (a) strategic alignment \u2014 does it advance the current horizon/phase? (b) unlocks other work \u2014 are tasks blocked by this? (c) user-facing \u2014 does it change what users see? (d) compounds over time \u2014 does it make future cycles faster? A high-impact Medium task beats a low-impact Small task at the same priority level. **Capacity-fit (smaller effort) breaks ties between comparable-impact tasks \u2014 it does NOT outrank impact.** Do NOT let a task's smaller size be the deciding factor that repeatedly defers a higher-impact larger task; that is the exact failure mode this step guards against. Justify in 2-3 sentences.
|
|
8574
|
+
**\u26A0\uFE0F Large-task inclusion guarantee (task-2227, structural rule \u2014 not advisory):** Impact weighting alone has historically lost to capacity-fit \u2014 the planner repeatedly filled cycles with XS/S work and deferred high-impact L/XL P1s (9 P1s stalled 6-21 cycles as of C299; C297 named this the "sequencing inversion"). Enforce a HARD rule: **every cycle MUST contain at least one Large or XL strategically-aligned P1 task, UNLESS no unblocked L/XL P1 exists on the board.** Before finalising the selection, scan the candidate pool: if the selected cycle contains zero L/XL tasks AND \u22651 unblocked L/XL P1 exists in the backlog, you MUST force-include the highest-impact such task \u2014 displacing the lowest-impact small task if the budget requires it (respect the single-theme vs mixed-theme budget from the Cycle-sizing rule below rather than always adding on top of an already-full cycle). Record the outcome in a "Large-task guarantee:" line in \`cycleLogNotes\`: name the task the guarantee pulled in, OR "satisfied \u2014 cycle already contains an L/XL task", OR "no unblocked L/XL P1 on the board". Do NOT skip this because the small tasks look individually attractive \u2014 that attractiveness IS the bias.
|
|
8488
8575
|
**Blocked tasks:** Tasks with status "Blocked" MUST be skipped during task selection \u2014 they are waiting on external dependencies or gates and cannot be built. Do NOT generate BUILD HANDOFFs for blocked tasks. Do NOT recommend blocked tasks. If a blocked task's gate has been resolved (check the notes and recent build reports), emit a \`boardCorrections\` entry to move it back to Backlog. Report blocked task count in the cycle log.
|
|
8489
8576
|
**Cycle sizing:** Size the cycle based on what the selected tasks actually require \u2014 not a fixed budget. Select the highest-priority unblocked tasks, estimate each one's effort from its scope, and let the total emerge. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 6-10 tasks. Cycles with fewer than 5 tasks require explicit justification in the cycle log \u2014 explain why more tasks could not be included. When the backlog has 10+ tasks, the cycle SHOULD have 6+ tasks \u2014 undersized cycles waste planning overhead relative to the available work. If fewer than 5 tasks qualify after filtering (blocked, deferred, raw), check Deferred tasks \u2014 some may be ready to un-defer via a \`boardCorrections\` entry. A 1-task cycle is almost never correct. Prefer grouping tasks by module or similarity \u2014 reduces context switching and enables shared branches during the build phase.
|
|
8490
8577
|
**Theme-driven sizing:** Single-theme cycles (all tasks in the same module or epic) can absorb 25-30 effort points because builders maintain context across tasks. Mixed-theme cycles should stay at 15-20 effort points to limit context switching. Use the theme to determine the budget, not a fixed number.
|
|
@@ -8520,7 +8607,7 @@ ${AD_REJECTION_RULES}
|
|
|
8520
8607
|
**Reference docs:** If a task's notes include a \`Reference:\` path (e.g. \`Reference: docs/architecture/papi-brain-v1.md\`), include a REFERENCE DOCS section in the BUILD HANDOFF with those paths. This tells the builder to read the referenced doc for background context before implementing. Do NOT omit or summarise the reference \u2014 pass it through so the builder can access the full document. Only tasks with explicit \`Reference:\` paths in their notes should have this section.
|
|
8521
8608
|
**Full notes lookup:** Notes in the Board section are truncated to 300 chars for concise task selection. When generating a BUILD HANDOFF for a task, check the "Full Notes for Candidate Tasks" section (if present in context) for that task's complete untruncated notes before writing SCOPE, SCOPE BOUNDARY, and PRE-MORTEM. Submitter context, constraints, and reasoning often live past the 300-char cutoff and must not be dropped from the handoff.
|
|
8522
8609
|
**Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
|
|
8523
|
-
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
|
|
8610
|
+
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. **If a "Recurring Learning Patterns (cross-cycle)" section is provided in the context, treat a module or theme that recurs across many cycles as a strong signal \u2014 size such tasks up (lean toward the larger estimate) and name the recurring failure mode explicitly in the pre-mortem.** Omit this section entirely for projects with fewer than 10 cycles.
|
|
8524
8611
|
**Intra-cycle dependency detection:** After selecting cycle tasks, check every pair for build-order dependencies. Two tasks A and B have an intra-cycle dependency when A must be built before B because B consumes an artifact A creates \u2014 e.g. A adds a new adapter method that B calls, A creates a DB migration B depends on, A introduces a new shared type B imports, A refactors a utility B modifies. Signals: same module + adjacent scope (one is "add X", another is "use X"), or notes explicitly reference the other task. For each dependency detected: (a) populate the DEPENDS ON section in the dependent task's BUILD HANDOFF with the upstream task ID(s); (b) add a \`boardCorrections\` entry for the dependent task with \`updates.dependsOn\` set to the comma-separated upstream IDs \u2014 this persists the dependency so the builder's runtime can reuse the upstream branch; (c) keep SCOPE sections independent but note the ordering in "Why now". Do NOT invent dependencies where tasks merely share a module \u2014 only real build-order coupling counts. Linear chains only \u2014 no multi-level graph resolution. When in doubt, omit.
|
|
8525
8612
|
**Dependency Chain section (Part 1 markdown):** When intra-cycle dependencies are detected, include a visible **## Dependency Chain** section in Part 1 markdown immediately before the first BUILD HANDOFF block. List each dependency as an arrow chain with a brief reason: \`task-A \u2192 task-B (B calls the adapter method A creates)\`. Then show the full recommended build sequence for all cycle tasks, including standalone tasks: e.g. \`Build order: task-A \u2192 task-B; task-C standalone; task-D standalone\`. Flag circular dependencies with \u26A0\uFE0F and a note. Omit this section entirely when no intra-cycle dependencies exist \u2014 do not include an empty section.
|
|
8526
8613
|
**Build order in cycle log:** If intra-cycle dependencies were detected, include a "Build order:" line in \`cycleLogNotes\` showing the recommended sequence as arrow chains (e.g. "Build order: task-123 \u2192 task-124; task-130 standalone"). Skip when no dependencies exist.
|
|
@@ -8544,7 +8631,7 @@ ${AD_REJECTION_RULES}
|
|
|
8544
8631
|
- **Architecture Notes:** If a pattern was established that needs follow-up (e.g. "shared service layer created, MCP migration needed"), propose the follow-up.
|
|
8545
8632
|
- **Strategy gaps:** If an Active Decision has no board tasks supporting it, propose one.
|
|
8546
8633
|
- **Dogfood observations:** Unactioned dogfood entries span FOUR categories (friction, methodology, signal, commercial) \u2014 consider ALL of them, not just friction. If an entry (with ID) maps to no existing task, propose one, matching task type to category: friction/signal \u2192 fix or improvement; methodology \u2192 process/tooling change; commercial \u2192 GTM/positioning. **CRITICAL: Include \`dogfood:<uuid>\` in the new task's \`notes\` field** (e.g. \`"notes": "dogfood:abc12345-..."\`). This links the task to the observation so the pipeline can track what was actioned. Without this annotation, the observation stays unactioned forever.
|
|
8547
|
-
Create new tasks via the \`newTasks\` array in Part 2.
|
|
8634
|
+
Create new tasks via the \`newTasks\` array in Part 2. Give each a unique \`tempId\` (\`"new-1"\`, \`"new-2"\`, \u2026) and set the matching \`cycleHandoffs\` \`taskId\` to that exact tempId (the apply step joins on tempId \u2014 array order is NOT used). **New-task cap \u2014 scale it to backlog depth, do NOT hard-cap at 3:** when 5+ unblocked backlog candidates already exist, limit new tasks to 3 (there is plenty to select from, so prevent backlog bloat). When the backlog is thin (fewer than 5 unblocked candidates), create as many as needed to bring the cycle into the healthy 5-8 range \u2014 roughly \`6 \u2212 unblocked_candidates\` additional new tasks, derived from the brief, roadmap phases, and recent build reports \u2014 up to an absolute ceiling of **8 new tasks**. This removes the contradiction with the Cycle-sizing rule above (a healthy cycle is 6-10 tasks; <5 needs justification), which the old flat cap of 3 violated whenever the backlog was thin. Never invent filler to hit a number: every new task must trace to a real discovered issue, dogfood entry, roadmap phase, or brief item.
|
|
8548
8635
|
**\u26A0\uFE0F DUPLICATE CHECK:** Before adding a task to \`newTasks\`, scan the Cycle Board above for any existing task with the same or very similar title/scope. If a matching task already exists (even with slightly different wording), do NOT create a duplicate \u2014 reference the existing task ID instead. The board already contains all active tasks; re-creating them wastes IDs and bloats the board.
|
|
8549
8636
|
**\u26A0\uFE0F ALREADY-BUILT CHECK:** Before creating a task, check the recent build reports and cycle log for evidence that this capability was already shipped. If a recent build report shows this feature was completed (even under a different task name), do NOT create a new task for it. This is especially important for UI features, data models, and integrations that may already exist.`);
|
|
8550
8637
|
parts.push(PLAN_FRAGMENT_PRODUCT_BRIEF);
|
|
@@ -8664,6 +8751,26 @@ function buildPlanUserMessage(ctx) {
|
|
|
8664
8751
|
if (ctx.buildPatterns) {
|
|
8665
8752
|
parts.push("### Build Patterns", "", ctx.buildPatterns, "");
|
|
8666
8753
|
}
|
|
8754
|
+
if (ctx.learningPatterns) {
|
|
8755
|
+
parts.push(
|
|
8756
|
+
"### Recurring Learning Patterns (cross-cycle)",
|
|
8757
|
+
"",
|
|
8758
|
+
"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.",
|
|
8759
|
+
"",
|
|
8760
|
+
ctx.learningPatterns,
|
|
8761
|
+
""
|
|
8762
|
+
);
|
|
8763
|
+
}
|
|
8764
|
+
if (ctx.decisionScorePatterns) {
|
|
8765
|
+
parts.push(
|
|
8766
|
+
"### Decision Risk Scores (high-risk + movement)",
|
|
8767
|
+
"",
|
|
8768
|
+
"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.",
|
|
8769
|
+
"",
|
|
8770
|
+
ctx.decisionScorePatterns,
|
|
8771
|
+
""
|
|
8772
|
+
);
|
|
8773
|
+
}
|
|
8667
8774
|
if (ctx.reviewPatterns) {
|
|
8668
8775
|
parts.push("### Review Patterns", "", ctx.reviewPatterns, "");
|
|
8669
8776
|
}
|
|
@@ -8800,6 +8907,8 @@ function coerceStructuredOutput(parsed) {
|
|
|
8800
8907
|
buildHandoff: coerceToString(h.buildHandoff)
|
|
8801
8908
|
})) : [];
|
|
8802
8909
|
const newTasks = Array.isArray(parsed.newTasks) ? parsed.newTasks.map((t) => ({
|
|
8910
|
+
// task-2242: stable join key (optional — undefined falls back to index).
|
|
8911
|
+
tempId: t.tempId !== void 0 && t.tempId !== null ? coerceToString(t.tempId) : void 0,
|
|
8803
8912
|
title: coerceToString(t.title),
|
|
8804
8913
|
status: coerceToString(t.status),
|
|
8805
8914
|
priority: coerceToString(t.priority),
|
|
@@ -9128,6 +9237,16 @@ function buildReviewUserMessage(ctx) {
|
|
|
9128
9237
|
if (ctx.decisionUsage) {
|
|
9129
9238
|
parts.push("### Decision Usage (Reference Frequency)", "", ctx.decisionUsage, "");
|
|
9130
9239
|
}
|
|
9240
|
+
if (ctx.decisionScoreTrajectory) {
|
|
9241
|
+
parts.push(
|
|
9242
|
+
"### Decision Risk Trajectory (score movement)",
|
|
9243
|
+
"",
|
|
9244
|
+
"Decision risk scores (/25, lower = safer) that are high-risk (>15) or have moved since they were last scored. Call out which decisions got riskier (\u2191) and whether their confidence still holds; consider re-scoring or revisiting a decision whose risk is rising.",
|
|
9245
|
+
"",
|
|
9246
|
+
ctx.decisionScoreTrajectory,
|
|
9247
|
+
""
|
|
9248
|
+
);
|
|
9249
|
+
}
|
|
9131
9250
|
if (ctx.recommendationEffectiveness) {
|
|
9132
9251
|
parts.push("### Recommendation Follow-Through", "", ctx.recommendationEffectiveness, "");
|
|
9133
9252
|
}
|
|
@@ -10266,6 +10385,8 @@ function applyContextTier(ctx, cycleCount) {
|
|
|
10266
10385
|
ctx.discoveryCanvas = void 0;
|
|
10267
10386
|
ctx.estimationCalibration = void 0;
|
|
10268
10387
|
ctx.buildPatterns = void 0;
|
|
10388
|
+
ctx.learningPatterns = void 0;
|
|
10389
|
+
ctx.decisionScorePatterns = void 0;
|
|
10269
10390
|
ctx.reviewPatterns = void 0;
|
|
10270
10391
|
ctx.horizonContext = void 0;
|
|
10271
10392
|
ctx.registeredDocs = void 0;
|
|
@@ -10768,7 +10889,9 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
10768
10889
|
preAssignedResult,
|
|
10769
10890
|
reportsForCapsResult,
|
|
10770
10891
|
contextHashesResult,
|
|
10771
|
-
doneTasksResult
|
|
10892
|
+
doneTasksResult,
|
|
10893
|
+
learningPatternsResult,
|
|
10894
|
+
decisionScoresResult
|
|
10772
10895
|
] = await Promise.allSettled([
|
|
10773
10896
|
detectReviewPatterns(reviews2, health.totalCycles, 5),
|
|
10774
10897
|
adapter2.getPendingRecommendations(),
|
|
@@ -10783,7 +10906,11 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
10783
10906
|
adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready"], compact: true }),
|
|
10784
10907
|
Promise.resolve(leanBuildReports.slice(0, 10)),
|
|
10785
10908
|
adapter2.getContextHashes?.(health.totalCycles) ?? Promise.resolve(null),
|
|
10786
|
-
adapter2.queryBoard({ status: ["Done", "Cancelled"], compact: true })
|
|
10909
|
+
adapter2.queryBoard({ status: ["Done", "Cancelled"], compact: true }),
|
|
10910
|
+
// task-2241: cross-cycle recurring learning patterns (the compounding-intelligence loop).
|
|
10911
|
+
adapter2.getCycleLearningPatterns?.() ?? Promise.resolve([]),
|
|
10912
|
+
// task-2264: per-decision risk scores + movement (second compounding-intelligence signal).
|
|
10913
|
+
adapter2.getDecisionScorePatterns?.() ?? Promise.resolve([])
|
|
10787
10914
|
]);
|
|
10788
10915
|
timings["parallelReads"] = t();
|
|
10789
10916
|
let reviewPatternsText2;
|
|
@@ -10813,6 +10940,8 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
10813
10940
|
estimationCalibrationText = (estimationCalibrationText ?? "") + moduleLines;
|
|
10814
10941
|
}
|
|
10815
10942
|
}
|
|
10943
|
+
const learningPatternsText = learningPatternsResult.status === "fulfilled" ? formatLearningPatterns(learningPatternsResult.value) : void 0;
|
|
10944
|
+
const decisionScorePatternsText = decisionScoresResult.status === "fulfilled" ? formatDecisionScorePatterns(decisionScoresResult.value) : void 0;
|
|
10816
10945
|
let registeredDocsText;
|
|
10817
10946
|
let unactionedDocsTextLean;
|
|
10818
10947
|
if (docsResult.status === "fulfilled" && docsResult.value && docsResult.value.length > 0) {
|
|
@@ -10873,6 +11002,8 @@ ${lines.join("\n")}`;
|
|
|
10873
11002
|
taskComments: taskCommentsText,
|
|
10874
11003
|
discoveryCanvas: discoveryCanvasText,
|
|
10875
11004
|
estimationCalibration: estimationCalibrationText,
|
|
11005
|
+
learningPatterns: learningPatternsText,
|
|
11006
|
+
decisionScorePatterns: decisionScorePatternsText,
|
|
10876
11007
|
registeredDocs: registeredDocsText,
|
|
10877
11008
|
unactionedDocs: unactionedDocsTextLean,
|
|
10878
11009
|
focus,
|
|
@@ -10924,7 +11055,9 @@ ${lines.join("\n")}`;
|
|
|
10924
11055
|
taskCommentsResultFull,
|
|
10925
11056
|
docsResultFull,
|
|
10926
11057
|
contextHashesResultFull,
|
|
10927
|
-
doneTasksResultFull
|
|
11058
|
+
doneTasksResultFull,
|
|
11059
|
+
learningPatternsResultFull,
|
|
11060
|
+
decisionScoresResultFull
|
|
10928
11061
|
] = await Promise.allSettled([
|
|
10929
11062
|
Promise.resolve(allBuildReports),
|
|
10930
11063
|
detectReviewPatterns(reviews, health.totalCycles, 5),
|
|
@@ -10933,7 +11066,11 @@ ${lines.join("\n")}`;
|
|
|
10933
11066
|
assembleTaskComments(adapter2),
|
|
10934
11067
|
adapter2.searchDocs?.({ status: "active", limit: 15 }),
|
|
10935
11068
|
adapter2.getContextHashes?.(health.totalCycles) ?? Promise.resolve(null),
|
|
10936
|
-
adapter2.queryBoard({ status: ["Done", "Cancelled"], compact: true })
|
|
11069
|
+
adapter2.queryBoard({ status: ["Done", "Cancelled"], compact: true }),
|
|
11070
|
+
// task-2241: cross-cycle recurring learning patterns (the compounding-intelligence loop).
|
|
11071
|
+
adapter2.getCycleLearningPatterns?.() ?? Promise.resolve([]),
|
|
11072
|
+
// task-2264: per-decision risk scores + movement (second compounding-intelligence signal).
|
|
11073
|
+
adapter2.getDecisionScorePatterns?.() ?? Promise.resolve([])
|
|
10937
11074
|
]);
|
|
10938
11075
|
timings["parallelAdvisory"] = t();
|
|
10939
11076
|
let buildPatternsText;
|
|
@@ -10946,6 +11083,8 @@ ${lines.join("\n")}`;
|
|
|
10946
11083
|
} catch {
|
|
10947
11084
|
}
|
|
10948
11085
|
}
|
|
11086
|
+
const learningPatternsTextFull = learningPatternsResultFull.status === "fulfilled" ? formatLearningPatterns(learningPatternsResultFull.value) : void 0;
|
|
11087
|
+
const decisionScorePatternsTextFull = decisionScoresResultFull.status === "fulfilled" ? formatDecisionScorePatterns(decisionScoresResultFull.value) : void 0;
|
|
10949
11088
|
let reviewPatternsText;
|
|
10950
11089
|
if (reviewPatternsResultFull.status === "fulfilled") {
|
|
10951
11090
|
reviewPatternsText = hasReviewPatterns(reviewPatternsResultFull.value) ? formatReviewPatterns(reviewPatternsResultFull.value) : void 0;
|
|
@@ -11049,6 +11188,8 @@ ${logLines}`);
|
|
|
11049
11188
|
methodologyMetrics: formatCycleMetrics(metricsSnapshots),
|
|
11050
11189
|
recentReviews: formatReviews(reviews),
|
|
11051
11190
|
buildPatterns: buildPatternsText,
|
|
11191
|
+
learningPatterns: learningPatternsTextFull,
|
|
11192
|
+
decisionScorePatterns: decisionScorePatternsTextFull,
|
|
11052
11193
|
reviewPatterns: reviewPatternsText,
|
|
11053
11194
|
horizonContext: horizonCtx,
|
|
11054
11195
|
strategyRecommendations: strategyRecommendationsText,
|
|
@@ -11180,6 +11321,8 @@ ${cleanContent}`;
|
|
|
11180
11321
|
strategicDirection: data.strategicDirection
|
|
11181
11322
|
},
|
|
11182
11323
|
newTasks: dedupedNewTasks.map((t) => ({
|
|
11324
|
+
tempId: t.tempId,
|
|
11325
|
+
// task-2242: stable handoff join key
|
|
11183
11326
|
title: t.title,
|
|
11184
11327
|
status: t.status || "Backlog",
|
|
11185
11328
|
priority: t.priority || "P1 High",
|
|
@@ -11329,7 +11472,7 @@ ${cleanContent}`;
|
|
|
11329
11472
|
visibility,
|
|
11330
11473
|
ownerUserId: inheritedVis.ownerUserId
|
|
11331
11474
|
});
|
|
11332
|
-
newTaskIdMap.set(
|
|
11475
|
+
newTaskIdMap.set(newTaskJoinKey(task, i), created.id);
|
|
11333
11476
|
if (adapter2.updateCycleLearningActionRef && task.notes) {
|
|
11334
11477
|
const learningRefs = task.notes.match(/learning:([a-f0-9-]+)/gi);
|
|
11335
11478
|
if (learningRefs) {
|
|
@@ -11488,6 +11631,13 @@ ${cleanContent}`;
|
|
|
11488
11631
|
}
|
|
11489
11632
|
for (const handoff of data.cycleHandoffs) {
|
|
11490
11633
|
const resolvedId = newTaskIdMap.get(handoff.taskId) ?? handoff.taskId;
|
|
11634
|
+
if (resolvedId === handoff.taskId && /^new-\d+$/i.test(handoff.taskId)) {
|
|
11635
|
+
const titleLine = handoff.buildHandoff.match(/^Task:\s*(.+)/m)?.[1] ?? "(unknown)";
|
|
11636
|
+
console.error(
|
|
11637
|
+
`[plan] UNRESOLVED handoff reference "${handoff.taskId}" (task: ${titleLine.slice(0, 80)}) \u2014 no newTask carries this tempId/index. Handoff NOT written. Ensure each newTasks entry has a tempId matching its cycleHandoffs taskId (task-2242).`
|
|
11638
|
+
);
|
|
11639
|
+
continue;
|
|
11640
|
+
}
|
|
11491
11641
|
try {
|
|
11492
11642
|
if (existingHandoffSet.has(resolvedId)) {
|
|
11493
11643
|
console.error(`[plan] skipping handoff write for ${resolvedId} \u2014 already has one`);
|
|
@@ -12016,6 +12166,7 @@ function defaultHint(tool) {
|
|
|
12016
12166
|
"plan",
|
|
12017
12167
|
"strategy_review",
|
|
12018
12168
|
"orient",
|
|
12169
|
+
"papi",
|
|
12019
12170
|
"build_execute",
|
|
12020
12171
|
"review_list",
|
|
12021
12172
|
"review_submit",
|
|
@@ -13444,7 +13595,8 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
13444
13595
|
recData,
|
|
13445
13596
|
pendingRecs,
|
|
13446
13597
|
registeredDocs,
|
|
13447
|
-
docsWithPendingActions
|
|
13598
|
+
docsWithPendingActions,
|
|
13599
|
+
decisionScores
|
|
13448
13600
|
] = await Promise.all([
|
|
13449
13601
|
adapter2.readProductBrief(),
|
|
13450
13602
|
// Strategy review needs to see retired ADs to triage/restore them as needed.
|
|
@@ -13471,7 +13623,9 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
13471
13623
|
// Doc registry — summaries for strategy review context
|
|
13472
13624
|
adapter2.searchDocs?.({ status: "active", limit: 10 })?.catch(() => []) ?? Promise.resolve([]),
|
|
13473
13625
|
// Doc registry — docs with pending actions for staleness audit
|
|
13474
|
-
adapter2.searchDocs?.({ hasPendingActions: true, limit: 20 })?.catch(() => []) ?? Promise.resolve([])
|
|
13626
|
+
adapter2.searchDocs?.({ hasPendingActions: true, limit: 20 })?.catch(() => []) ?? Promise.resolve([]),
|
|
13627
|
+
// task-2264: decision risk scores + movement, so the review can show trajectory.
|
|
13628
|
+
adapter2.getDecisionScorePatterns?.()?.catch(() => []) ?? Promise.resolve([])
|
|
13475
13629
|
]);
|
|
13476
13630
|
const pendingAgendaTopics = await (adapter2.getPendingAgendaTopics?.().catch(() => []) ?? Promise.resolve([]));
|
|
13477
13631
|
const tasks = [...activeTasks, ...recentDoneTasks];
|
|
@@ -13752,6 +13906,7 @@ ${lines.join("\n")}`;
|
|
|
13752
13906
|
previousReviews: previousReviewsText,
|
|
13753
13907
|
phases: phasesText,
|
|
13754
13908
|
decisionUsage: decisionUsageText,
|
|
13909
|
+
decisionScoreTrajectory: formatDecisionScorePatterns(decisionScores),
|
|
13755
13910
|
northStar: currentNorthStar ?? void 0,
|
|
13756
13911
|
recommendationEffectiveness: recEffectivenessText,
|
|
13757
13912
|
adHocCommits: adHocCommitsText,
|
|
@@ -17541,12 +17696,60 @@ ${existing}`;
|
|
|
17541
17696
|
}
|
|
17542
17697
|
await writeFile3(changelogPath, body, "utf-8");
|
|
17543
17698
|
}
|
|
17544
|
-
function
|
|
17699
|
+
async function allCancelledBranches(adapter2, cycleNum) {
|
|
17700
|
+
const skip = /* @__PURE__ */ new Set();
|
|
17701
|
+
try {
|
|
17702
|
+
const ALL_STATUSES = [
|
|
17703
|
+
"Backlog",
|
|
17704
|
+
"In Cycle",
|
|
17705
|
+
"Ready",
|
|
17706
|
+
"In Progress",
|
|
17707
|
+
"In Review",
|
|
17708
|
+
"Done",
|
|
17709
|
+
"Blocked",
|
|
17710
|
+
"Cancelled",
|
|
17711
|
+
"Deferred"
|
|
17712
|
+
];
|
|
17713
|
+
const tasks = await adapter2.queryBoard({ cycleSince: cycleNum, status: ALL_STATUSES, compact: true });
|
|
17714
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
17715
|
+
for (const t of tasks) {
|
|
17716
|
+
if (t.cycle !== cycleNum) continue;
|
|
17717
|
+
const moduleName = (t.module ?? "").trim();
|
|
17718
|
+
if (!moduleName) continue;
|
|
17719
|
+
const entry = byModule.get(moduleName) ?? { total: 0, cancelled: 0 };
|
|
17720
|
+
entry.total++;
|
|
17721
|
+
if (t.status === "Cancelled") entry.cancelled++;
|
|
17722
|
+
byModule.set(moduleName, entry);
|
|
17723
|
+
}
|
|
17724
|
+
for (const [moduleName, entry] of byModule) {
|
|
17725
|
+
if (entry.total > 0 && entry.cancelled === entry.total) {
|
|
17726
|
+
skip.add(cycleBranchName(cycleNum, moduleName));
|
|
17727
|
+
}
|
|
17728
|
+
}
|
|
17729
|
+
} catch {
|
|
17730
|
+
}
|
|
17731
|
+
return skip;
|
|
17732
|
+
}
|
|
17733
|
+
function mergeGroupedCycleBranches(config2, cycleNum, baseBranch, skipBranches = /* @__PURE__ */ new Set()) {
|
|
17545
17734
|
const branches = listGroupedCycleBranches(config2.projectRoot, cycleNum, baseBranch);
|
|
17546
17735
|
if (branches.length === 0) return [];
|
|
17547
17736
|
const ghAvailable = isGhAvailable();
|
|
17548
17737
|
const results = [];
|
|
17549
17738
|
for (const branch of branches) {
|
|
17739
|
+
if (skipBranches.has(branch)) {
|
|
17740
|
+
results.push({ branch, prUrl: null, status: "skipped", message: "all cycle tasks Cancelled \u2014 branch not merged (task-2253)" });
|
|
17741
|
+
continue;
|
|
17742
|
+
}
|
|
17743
|
+
if (isBranchContentAlreadyInBase(config2.projectRoot, branch, baseBranch)) {
|
|
17744
|
+
deleteLocalBranch(config2.projectRoot, branch);
|
|
17745
|
+
results.push({
|
|
17746
|
+
branch,
|
|
17747
|
+
prUrl: null,
|
|
17748
|
+
status: "skipped",
|
|
17749
|
+
message: `content already on '${baseBranch}' (tree-equal \u2014 squash-merged); lingering branch cleaned, no re-merge`
|
|
17750
|
+
});
|
|
17751
|
+
continue;
|
|
17752
|
+
}
|
|
17550
17753
|
if (branchExists(config2.projectRoot, branch) && hasRemote(config2.projectRoot)) {
|
|
17551
17754
|
const push = gitPush(config2.projectRoot, branch);
|
|
17552
17755
|
if (!push.success) {
|
|
@@ -17769,7 +17972,8 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
|
|
|
17769
17972
|
}
|
|
17770
17973
|
let groupedBranchMerges;
|
|
17771
17974
|
if (resolvedCycleNum > 0) {
|
|
17772
|
-
const
|
|
17975
|
+
const skipBranches = adapter2 ? await allCancelledBranches(adapter2, resolvedCycleNum) : /* @__PURE__ */ new Set();
|
|
17976
|
+
const mergeResults = mergeGroupedCycleBranches(config2, resolvedCycleNum, branch, skipBranches);
|
|
17773
17977
|
if (mergeResults.length > 0) {
|
|
17774
17978
|
groupedBranchMerges = mergeResults;
|
|
17775
17979
|
const failures = mergeResults.filter((r) => r.status === "failed");
|
|
@@ -18573,6 +18777,23 @@ If you intend to discard the work, run build_cancel instead.`
|
|
|
18573
18777
|
err.gitStderr = scrubbed;
|
|
18574
18778
|
return err;
|
|
18575
18779
|
}
|
|
18780
|
+
var UI_FILE_RE = /^(app\/.+\.(tsx|css)|components\/.+\.tsx)$/;
|
|
18781
|
+
var NON_UI_RE = /(^app\/api\/|\.test\.|\.spec\.|\/__tests__\/)/;
|
|
18782
|
+
function inferRouteFromPage(file) {
|
|
18783
|
+
const m = file.match(/^app\/(.*)\/page\.tsx$/);
|
|
18784
|
+
if (!m) return null;
|
|
18785
|
+
const segments = m[1].split("/").filter((s) => s.length > 0 && !(s.startsWith("(") && s.endsWith(")")));
|
|
18786
|
+
return "/" + segments.join("/");
|
|
18787
|
+
}
|
|
18788
|
+
function computeLocalPreview(filesChanged, provided) {
|
|
18789
|
+
const uiFiles = (filesChanged ?? []).filter((f) => UI_FILE_RE.test(f) && !NON_UI_RE.test(f));
|
|
18790
|
+
const hasProvided = !!(provided && (provided.urls?.length || provided.screenshotPath || provided.notes));
|
|
18791
|
+
if (uiFiles.length === 0 && !hasProvided) return void 0;
|
|
18792
|
+
const routes = Array.from(
|
|
18793
|
+
new Set(uiFiles.map(inferRouteFromPage).filter((r) => r !== null))
|
|
18794
|
+
);
|
|
18795
|
+
return { uiFiles, routes, provided: hasProvided ? provided : void 0 };
|
|
18796
|
+
}
|
|
18576
18797
|
var AMBIENT_DRIFT_PATTERNS = [
|
|
18577
18798
|
/^\.next\//,
|
|
18578
18799
|
/^\.next$/,
|
|
@@ -19249,6 +19470,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
19249
19470
|
const drift = computeScopeDriftSignal(task.buildHandoff?.filesLikelyTouched, driftChanged);
|
|
19250
19471
|
if (drift) report.scopeDriftSignal = drift;
|
|
19251
19472
|
}
|
|
19473
|
+
const localPreview = computeLocalPreview(report.filesChanged, input.preview);
|
|
19252
19474
|
let prLines = [];
|
|
19253
19475
|
if (options.light) {
|
|
19254
19476
|
prLines.push("Light mode: skipping push and PR creation.");
|
|
@@ -19351,7 +19573,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
19351
19573
|
autoTriagedCount: autoTriagedCount > 0 ? autoTriagedCount : void 0,
|
|
19352
19574
|
autoTriagedIds: autoTriagedIds.length > 0 ? autoTriagedIds : void 0,
|
|
19353
19575
|
autoTriagedDupes: autoTriagedDupes.length > 0 ? autoTriagedDupes : void 0,
|
|
19354
|
-
reportWriteVerified
|
|
19576
|
+
reportWriteVerified,
|
|
19577
|
+
localPreview
|
|
19355
19578
|
};
|
|
19356
19579
|
}
|
|
19357
19580
|
async function cancelBuild(adapter2, taskId, reason) {
|
|
@@ -19571,6 +19794,15 @@ var buildExecuteTool = {
|
|
|
19571
19794
|
verified_at: { type: "string", description: "ISO 8601 timestamp." }
|
|
19572
19795
|
},
|
|
19573
19796
|
required: ["urls", "curl_command", "http_status", "response_excerpt", "verified_at"]
|
|
19797
|
+
},
|
|
19798
|
+
preview: {
|
|
19799
|
+
type: "object",
|
|
19800
|
+
description: `Optional. For a build that touched USER-FACING UI: tell the owner how to SEE the result locally so reviewing it doesn't mean reading code or asking "show me". urls = the localhost route(s) to open (e.g. ["http://localhost:3000/hub"]); notes = what to look at; screenshot_path = a path to an image you captured. Surfaced verbatim in the completion output. When the diff touched UI and you omit this, the output nudges you to provide it.`,
|
|
19801
|
+
properties: {
|
|
19802
|
+
urls: { type: "array", items: { type: "string" }, description: "Localhost route(s) to open to view the work." },
|
|
19803
|
+
notes: { type: "string", description: "What the owner should look at / verify." },
|
|
19804
|
+
screenshot_path: { type: "string", description: "Path to a captured screenshot of the rendered result." }
|
|
19805
|
+
}
|
|
19574
19806
|
}
|
|
19575
19807
|
},
|
|
19576
19808
|
required: ["task_id"]
|
|
@@ -19848,6 +20080,12 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
19848
20080
|
const deadEnds = args.dead_ends;
|
|
19849
20081
|
const rawBriefImplications = args.brief_implications;
|
|
19850
20082
|
const resolvesLearnings = Array.isArray(args.resolves_learnings) ? args.resolves_learnings : void 0;
|
|
20083
|
+
const rawPreview = args.preview;
|
|
20084
|
+
const preview = rawPreview ? {
|
|
20085
|
+
urls: Array.isArray(rawPreview.urls) ? rawPreview.urls.filter((u) => typeof u === "string") : void 0,
|
|
20086
|
+
notes: typeof rawPreview.notes === "string" ? rawPreview.notes : void 0,
|
|
20087
|
+
screenshotPath: typeof rawPreview.screenshot_path === "string" ? rawPreview.screenshot_path : void 0
|
|
20088
|
+
} : void 0;
|
|
19851
20089
|
const rawProductionVerification = args.production_verification;
|
|
19852
20090
|
const productionVerification = rawProductionVerification ? {
|
|
19853
20091
|
urls: Array.isArray(rawProductionVerification.urls) ? rawProductionVerification.urls.filter((u) => typeof u === "string") : [],
|
|
@@ -19904,7 +20142,8 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
19904
20142
|
type: bi.type ?? "new",
|
|
19905
20143
|
detail: bi.detail ?? ""
|
|
19906
20144
|
})),
|
|
19907
|
-
productionVerification
|
|
20145
|
+
productionVerification,
|
|
20146
|
+
preview
|
|
19908
20147
|
}, { light });
|
|
19909
20148
|
tracker.mark("complete_format");
|
|
19910
20149
|
if (resolvesLearnings && resolvesLearnings.length > 0 && adapter2.updateCycleLearningActionRef) {
|
|
@@ -19961,6 +20200,26 @@ function formatCompleteResult(result) {
|
|
|
19961
20200
|
if (result.cycleProgress.total > 0) {
|
|
19962
20201
|
lines.push("", `Cycle progress: ${result.cycleProgress.completed} of ${result.cycleProgress.total} cycle tasks complete.`);
|
|
19963
20202
|
}
|
|
20203
|
+
if (result.localPreview) {
|
|
20204
|
+
const { routes, provided, uiFiles } = result.localPreview;
|
|
20205
|
+
const previewLines = ["", "### \u{1F441} Preview the work"];
|
|
20206
|
+
if (provided?.urls?.length) {
|
|
20207
|
+
previewLines.push(`Open: ${provided.urls.join(" \xB7 ")}`);
|
|
20208
|
+
} else if (routes.length > 0) {
|
|
20209
|
+
const urls = routes.map((r) => `http://localhost:3000${r}`);
|
|
20210
|
+
previewLines.push(`Run \`npm run dev\` and open: ${urls.join(" \xB7 ")}`);
|
|
20211
|
+
} else {
|
|
20212
|
+
previewLines.push(
|
|
20213
|
+
`This build changed UI (${uiFiles.slice(0, 4).join(", ")}${uiFiles.length > 4 ? `, +${uiFiles.length - 4} more` : ""}). Run \`npm run dev\` and open the page that renders it.`
|
|
20214
|
+
);
|
|
20215
|
+
}
|
|
20216
|
+
if (provided?.screenshotPath) previewLines.push(`Screenshot: ${provided.screenshotPath}`);
|
|
20217
|
+
if (provided?.notes) previewLines.push(`Look at: ${provided.notes}`);
|
|
20218
|
+
if (!provided) {
|
|
20219
|
+
previewLines.push("_Tip: pass `preview` (the route you viewed / a screenshot) on complete so the result travels with the build._");
|
|
20220
|
+
}
|
|
20221
|
+
lines.push(...previewLines);
|
|
20222
|
+
}
|
|
19964
20223
|
if (result.phaseChanges.length > 0) {
|
|
19965
20224
|
for (const c of result.phaseChanges) {
|
|
19966
20225
|
lines.push(`Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`);
|
|
@@ -20239,6 +20498,20 @@ async function captureBug(adapter2, input) {
|
|
|
20239
20498
|
}
|
|
20240
20499
|
|
|
20241
20500
|
// src/tools/bug.ts
|
|
20501
|
+
var PAPI_SIGNAL_PATTERNS = [
|
|
20502
|
+
/\bpapi\b/i,
|
|
20503
|
+
/\bmcp\b/i,
|
|
20504
|
+
// Distinctive PAPI MCP tool names (whole-word) — these don't appear in an
|
|
20505
|
+
// everyday project-domain bug report.
|
|
20506
|
+
/\b(orient|build_execute|build_list|build_describe|build_cancel|review_submit|review_list|strategy_review|strategy_change|board_view|board_edit|board_deprioritise|board_reconcile|handoff_generate|doc_register|doc_search|ad_hoc|inventory_sync|scope_brief)\b/i,
|
|
20507
|
+
/\bbuild handoff\b/i,
|
|
20508
|
+
/\bremote connector\b/i
|
|
20509
|
+
];
|
|
20510
|
+
function looksLikePapiBug(text, notes) {
|
|
20511
|
+
const haystack = `${text}
|
|
20512
|
+
${notes ?? ""}`;
|
|
20513
|
+
return PAPI_SIGNAL_PATTERNS.some((re) => re.test(haystack));
|
|
20514
|
+
}
|
|
20242
20515
|
function collectDiagnostics(config2) {
|
|
20243
20516
|
return {
|
|
20244
20517
|
nodeVersion: process.version,
|
|
@@ -20252,7 +20525,7 @@ function collectDiagnostics(config2) {
|
|
|
20252
20525
|
}
|
|
20253
20526
|
var bugTool = {
|
|
20254
20527
|
name: "bug",
|
|
20255
|
-
description:
|
|
20528
|
+
description: 'Report a bug OR submit an idea. Routing: a bug about PAPI itself (a PAPI tool/MCP error, the connector, a handoff/cycle problem) auto-submits UPSTREAM to PAPI maintainers with diagnostics \u2014 you do NOT need to set report=true for these. A bug in the user\'s OWN project auto-files as a Backlog task on their board. Override the routing explicitly: report=true forces upstream, report=false forces the user\'s own board. Set `type` ("bug"/"idea") and optional notify-when-fixed / contact-ok consent for upstream submissions. Does not call the Anthropic API.',
|
|
20256
20529
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
20257
20530
|
inputSchema: {
|
|
20258
20531
|
type: "object",
|
|
@@ -20284,7 +20557,7 @@ var bugTool = {
|
|
|
20284
20557
|
},
|
|
20285
20558
|
report: {
|
|
20286
20559
|
type: "boolean",
|
|
20287
|
-
description: "
|
|
20560
|
+
description: "Routing override. Leave UNSET to auto-route: PAPI-product bugs go upstream to maintainers, project-domain bugs go to the user's board. Set true to force an upstream diagnostic submission; set false to force a task on the user's own board (use false when an auto-detected PAPI bug is actually about the user's project)."
|
|
20288
20561
|
},
|
|
20289
20562
|
type: {
|
|
20290
20563
|
type: "string",
|
|
@@ -20312,8 +20585,18 @@ async function handleBug(adapter2, config2, args) {
|
|
|
20312
20585
|
if (!text) {
|
|
20313
20586
|
return errorResponse("text is required \u2014 describe the bug you want to report.");
|
|
20314
20587
|
}
|
|
20315
|
-
|
|
20588
|
+
const notesArg = args.notes;
|
|
20589
|
+
const explicitReport = typeof args.report === "boolean" ? args.report : void 0;
|
|
20590
|
+
const papiBug = looksLikePapiBug(text, notesArg);
|
|
20591
|
+
const autoRoutedUpstream = explicitReport === void 0 && papiBug;
|
|
20592
|
+
const shouldReportUpstream = explicitReport === true || autoRoutedUpstream;
|
|
20593
|
+
if (shouldReportUpstream) {
|
|
20316
20594
|
if (!adapter2.submitBugReport) {
|
|
20595
|
+
if (autoRoutedUpstream) {
|
|
20596
|
+
return errorResponse(
|
|
20597
|
+
`This looks like a PAPI bug, which belongs upstream with the PAPI maintainers \u2014 but upstream reporting needs a database adapter (pg or proxy) and this session uses the md adapter, which can't submit it. Two options: (1) switch to the hosted/pg setup to report it upstream, or (2) re-run with report=false to file it on your own project board instead.`
|
|
20598
|
+
);
|
|
20599
|
+
}
|
|
20317
20600
|
return errorResponse("Bug reports require a database adapter (pg or proxy). The md adapter does not support bug reports.");
|
|
20318
20601
|
}
|
|
20319
20602
|
const type = args.type === "idea" ? "idea" : "bug";
|
|
@@ -20348,13 +20631,16 @@ async function handleBug(adapter2, config2, args) {
|
|
|
20348
20631
|
const followUpLine = followUps.length ? `
|
|
20349
20632
|
|
|
20350
20633
|
Noted: ${followUps.join("; ")}.` : "";
|
|
20634
|
+
const autoRouteLine = autoRoutedUpstream ? `
|
|
20635
|
+
|
|
20636
|
+
_Detected this as a PAPI bug and sent it upstream rather than adding it to your own board. If it's actually about your project, re-run with \`report=false\` to file it on your board instead._` : "";
|
|
20351
20637
|
return textResponse(
|
|
20352
20638
|
`**${kindLabel} submitted** \u2014 ID: \`${report.id}\`
|
|
20353
20639
|
|
|
20354
20640
|
${type === "idea" ? "Idea" : "Description"}: ${text}
|
|
20355
20641
|
Diagnostics collected: Node ${diagnostics.nodeVersion}, ${diagnostics.platform}/${diagnostics.arch}, adapter=${diagnostics.adapterType}
|
|
20356
20642
|
|
|
20357
|
-
This ${type} is visible to PAPI maintainers.${followUpLine}`
|
|
20643
|
+
This ${type} is visible to PAPI maintainers.${followUpLine}${autoRouteLine}`
|
|
20358
20644
|
);
|
|
20359
20645
|
}
|
|
20360
20646
|
let target = adapter2;
|
|
@@ -22745,7 +23031,7 @@ ${lines.join("\n")}`;
|
|
|
22745
23031
|
|
|
22746
23032
|
// src/lib/version-handshake.ts
|
|
22747
23033
|
var MIN_PROXY_VERSION = "1.0.0";
|
|
22748
|
-
var SERVER_VERSION = "0.7.
|
|
23034
|
+
var SERVER_VERSION = "0.7.36";
|
|
22749
23035
|
function meetsMinVersion(actual, required) {
|
|
22750
23036
|
const actualParts = actual.split(".").map((n) => parseInt(n, 10));
|
|
22751
23037
|
const requiredParts = required.split(".").map((n) => parseInt(n, 10));
|
|
@@ -23564,6 +23850,11 @@ var orientTool = {
|
|
|
23564
23850
|
required: []
|
|
23565
23851
|
}
|
|
23566
23852
|
};
|
|
23853
|
+
var papiTool = {
|
|
23854
|
+
...orientTool,
|
|
23855
|
+
name: "papi",
|
|
23856
|
+
description: 'Say "papi" to check in with Papi \u2014 an alias for `orient`. Run this FIRST at session start: it returns your cycle number, task counts, in-progress/in-review work, strategy-review cadence, a velocity snapshot, and the recommended next action. Identical to `orient` (same inputs, same output); use whichever name you prefer. Read-only.'
|
|
23857
|
+
};
|
|
23567
23858
|
function countStalledP1(warnings) {
|
|
23568
23859
|
const stallLine = warnings.find((w) => w.includes("P1 tasks stalled"));
|
|
23569
23860
|
if (!stallLine) return 0;
|
|
@@ -26093,6 +26384,8 @@ var ABUSE_CEILINGS = {
|
|
|
26093
26384
|
var DEFAULT_CEILING = ABUSE_CEILINGS.free;
|
|
26094
26385
|
var ALWAYS_FREE_TOOLS = /* @__PURE__ */ new Set([
|
|
26095
26386
|
"orient",
|
|
26387
|
+
"papi",
|
|
26388
|
+
// alias for orient (task-2223) — must share its free treatment
|
|
26096
26389
|
"board_view",
|
|
26097
26390
|
"build_list",
|
|
26098
26391
|
"build_describe",
|
|
@@ -26242,6 +26535,7 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
|
|
|
26242
26535
|
"review_list",
|
|
26243
26536
|
"review_submit",
|
|
26244
26537
|
"orient",
|
|
26538
|
+
"papi",
|
|
26245
26539
|
"hierarchy_update",
|
|
26246
26540
|
"zoom_out",
|
|
26247
26541
|
"handoff_generate",
|
|
@@ -26273,6 +26567,7 @@ var PAPI_TOOLS = [
|
|
|
26273
26567
|
reviewClaimTool,
|
|
26274
26568
|
initTool,
|
|
26275
26569
|
orientTool,
|
|
26570
|
+
papiTool,
|
|
26276
26571
|
hierarchyUpdateTool,
|
|
26277
26572
|
zoomOutTool,
|
|
26278
26573
|
docRegisterTool,
|
|
@@ -26447,6 +26742,9 @@ function createServer(adapter2, config2) {
|
|
|
26447
26742
|
return handleInit(config2, safeArgs);
|
|
26448
26743
|
case "orient":
|
|
26449
26744
|
return handleOrient(adapter2, config2, safeArgs);
|
|
26745
|
+
case "papi":
|
|
26746
|
+
return handleOrient(adapter2, config2, safeArgs);
|
|
26747
|
+
// alias (task-2223)
|
|
26450
26748
|
case "hierarchy_update":
|
|
26451
26749
|
return handleHierarchyUpdate(adapter2, safeArgs);
|
|
26452
26750
|
case "zoom_out":
|
|
@@ -26518,7 +26816,7 @@ function createServer(adapter2, config2) {
|
|
|
26518
26816
|
}
|
|
26519
26817
|
throw err;
|
|
26520
26818
|
}
|
|
26521
|
-
if (name === "orient" && adapter2.getMeteredUsage && !result.content.some((c) => c.text.startsWith("Error:"))) {
|
|
26819
|
+
if ((name === "orient" || name === "papi") && adapter2.getMeteredUsage && !result.content.some((c) => c.text.startsWith("Error:"))) {
|
|
26522
26820
|
try {
|
|
26523
26821
|
const decision = await checkMeter(adapter2, "plan", config2.projectId ?? "session");
|
|
26524
26822
|
if (decision.usage) {
|
|
@@ -26588,6 +26886,8 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
26588
26886
|
var BEARER_PREFIX = "papi_";
|
|
26589
26887
|
var BEARER_REGEX = /^(papi_|papi_oauth_)[a-f0-9]{64}$/;
|
|
26590
26888
|
var RESOURCE_METADATA_URL = process.env["PAPI_RESOURCE_METADATA_URL"] ?? "https://getpapi.ai/.well-known/oauth-protected-resource";
|
|
26889
|
+
var DASHBOARD_ORIGIN = process.env["PAPI_DASHBOARD_URL"] ?? "https://getpapi.ai";
|
|
26890
|
+
var MCP_RESOURCE_URL = process.env["NEXT_PUBLIC_MCP_URL"] ?? "https://mcp.getpapi.ai";
|
|
26591
26891
|
var FRIENDLY_GET_HTML = `<!doctype html>
|
|
26592
26892
|
<html lang="en"><head><meta charset="utf-8">
|
|
26593
26893
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -26715,6 +27015,31 @@ function startHttpTransport(opts) {
|
|
|
26715
27015
|
res.end("ok");
|
|
26716
27016
|
return;
|
|
26717
27017
|
}
|
|
27018
|
+
if (req.method === "GET" && req.url === "/.well-known/oauth-protected-resource") {
|
|
27019
|
+
res.writeHead(200, {
|
|
27020
|
+
"Content-Type": "application/json",
|
|
27021
|
+
"Cache-Control": "public, max-age=3600",
|
|
27022
|
+
...cors
|
|
27023
|
+
});
|
|
27024
|
+
res.end(
|
|
27025
|
+
JSON.stringify({
|
|
27026
|
+
resource: MCP_RESOURCE_URL,
|
|
27027
|
+
authorization_servers: [DASHBOARD_ORIGIN],
|
|
27028
|
+
scopes_supported: ["mcp:full"],
|
|
27029
|
+
bearer_methods_supported: ["header"]
|
|
27030
|
+
})
|
|
27031
|
+
);
|
|
27032
|
+
return;
|
|
27033
|
+
}
|
|
27034
|
+
if (req.method === "GET" && req.url === "/.well-known/oauth-authorization-server") {
|
|
27035
|
+
res.writeHead(302, {
|
|
27036
|
+
Location: `${DASHBOARD_ORIGIN}/.well-known/oauth-authorization-server`,
|
|
27037
|
+
"Cache-Control": "public, max-age=3600",
|
|
27038
|
+
...cors
|
|
27039
|
+
});
|
|
27040
|
+
res.end();
|
|
27041
|
+
return;
|
|
27042
|
+
}
|
|
26718
27043
|
if (req.url !== "/mcp" && req.url !== "/sse") {
|
|
26719
27044
|
sendError(res, { status: 404, body: { error: "Not found" } }, cors);
|
|
26720
27045
|
return;
|
|
@@ -27108,7 +27433,7 @@ If you haven't set up PAPI yet:
|
|
|
27108
27433
|
2. Complete the onboarding wizard \u2014 it generates your config
|
|
27109
27434
|
3. Copy the config to your project and restart your AI tool
|
|
27110
27435
|
|
|
27111
|
-
If you already have an account, check that both **PAPI_PROJECT_ID** and **PAPI_DATA_API_KEY** are set in your .mcp.json env config.`
|
|
27436
|
+
If you already have an account, check that both **PAPI_PROJECT_ID** and **PAPI_DATA_API_KEY** are set in your .mcp.json env config.` + HELP_FOOTER_MD
|
|
27112
27437
|
}]
|
|
27113
27438
|
}));
|
|
27114
27439
|
}
|
package/dist/prompts.js
CHANGED
|
@@ -89,7 +89,7 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
89
89
|
"boardHealth": "string \u2014 e.g. 5 tasks (3 backlog, 2 done)",
|
|
90
90
|
"strategicDirection": "string \u2014 one sentence about current phase/direction",
|
|
91
91
|
"recommendedTaskId": "string or null \u2014 task ID to set In Progress",
|
|
92
|
-
"cycleHandoffs": [{"taskId": "string \u2014 existing task ID or new-
|
|
92
|
+
"cycleHandoffs": [{"taskId": "string \u2014 an existing task ID, or the EXACT tempId of a newTasks entry (e.g. "new-1") it provides the handoff for", "buildHandoff": "string \u2014 full BUILD HANDOFF text"}],
|
|
93
93
|
"newTasks": [],
|
|
94
94
|
"boardCorrections": [],
|
|
95
95
|
"productBrief": null,
|
|
@@ -134,7 +134,7 @@ Everything in Part 1 (natural language) is **display-only**. Part 2 (structured
|
|
|
134
134
|
**Example with populated fields (DO NOT copy literally \u2014 adapt to your actual analysis):**
|
|
135
135
|
\`\`\`json
|
|
136
136
|
{
|
|
137
|
-
"newTasks": [{"title": "Example task", "status": "Backlog", "priority": "P2 Medium", "complexity": "Small", "module": "Core", "epic": "Platform", "phase": "Phase 1", "owner": "TBD", "notes": "Created during triage"}],
|
|
137
|
+
"newTasks": [{"tempId": "new-1", "title": "Example task", "status": "Backlog", "priority": "P2 Medium", "complexity": "Small", "module": "Core", "epic": "Platform", "phase": "Phase 1", "owner": "TBD", "notes": "Created during triage"}],
|
|
138
138
|
"activeDecisions": [{"id": "AD-3", "body": "### AD-3: Use REST over GraphQL [Confidence: HIGH]\\n\\n- **Decision:** REST API for v1.\\n- **Evidence:** Simpler for current scope."}],
|
|
139
139
|
"boardCorrections": [{"taskId": "task-005", "updates": {"priority": "P1 High", "reviewed": true}}, {"taskId": "task-009", "updates": {"status": "Cancelled", "closureReason": "Duplicates task-003"}}],
|
|
140
140
|
"cycleHandoffs": [{"taskId": "task-002", "buildHandoff": "BUILD HANDOFF \u2014 task-002\\nTask: ..."}]
|
|
@@ -172,7 +172,7 @@ This is Cycle 0 \u2014 the first planning cycle for a brand-new project.
|
|
|
172
172
|
|
|
173
173
|
4. **First Active Decision** \u2014 If the description implies a clear architectural choice, create AD-1 with Confidence: MEDIUM. If no clear choice, skip this.
|
|
174
174
|
|
|
175
|
-
5. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for EVERY task created in step 3 (all 3-5 tasks). Include each in the \`cycleHandoffs\` array.
|
|
175
|
+
5. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for EVERY task created in step 3 (all 3-5 tasks). Include each in the \`cycleHandoffs\` array. **tempId join (REQUIRED \u2014 mismatches silently scramble handoffs):** give EVERY \`newTasks\` entry a unique \`tempId\` (\`"new-1"\`, \`"new-2"\`, \u2026), and set each \`cycleHandoffs\` \`taskId\` to the EXACT \`tempId\` of the newTask it belongs to. Do NOT rely on array order, and do NOT reuse a tempId. The builder needs handoffs to run \`build_execute\` \u2014 without them, tasks must be completed via \`ad_hoc\`, which breaks the normal flow.
|
|
176
176
|
|
|
177
177
|
### Structured output for Bootstrap:
|
|
178
178
|
In the JSON block, you MUST include:
|
|
@@ -343,7 +343,8 @@ Standard planning cycle with full board review.
|
|
|
343
343
|
- **P1 High** \u2014 Strategically aligned: directly advances the current horizon, phase, or Active Decision goals.
|
|
344
344
|
- **P2 Medium** \u2014 Valuable but not strategically urgent: quality improvements, efficiency, polish, infra.
|
|
345
345
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
346
|
-
Within the same priority level, prefer tasks with the highest **impact-to-effort ratio**. Impact is measured by: (a) strategic alignment \u2014 does it advance the current horizon/phase? (b) unlocks other work \u2014 are tasks blocked by this? (c) user-facing \u2014 does it change what users see? (d) compounds over time \u2014 does it make future cycles faster? A high-impact Medium task beats a low-impact Small task at the same priority level. Justify in 2-3 sentences.
|
|
346
|
+
Within the same priority level, prefer tasks with the highest **impact-to-effort ratio**. Impact is measured by: (a) strategic alignment \u2014 does it advance the current horizon/phase? (b) unlocks other work \u2014 are tasks blocked by this? (c) user-facing \u2014 does it change what users see? (d) compounds over time \u2014 does it make future cycles faster? A high-impact Medium task beats a low-impact Small task at the same priority level. **Capacity-fit (smaller effort) breaks ties between comparable-impact tasks \u2014 it does NOT outrank impact.** Do NOT let a task's smaller size be the deciding factor that repeatedly defers a higher-impact larger task; that is the exact failure mode this step guards against. Justify in 2-3 sentences.
|
|
347
|
+
**\u26A0\uFE0F Large-task inclusion guarantee (task-2227, structural rule \u2014 not advisory):** Impact weighting alone has historically lost to capacity-fit \u2014 the planner repeatedly filled cycles with XS/S work and deferred high-impact L/XL P1s (9 P1s stalled 6-21 cycles as of C299; C297 named this the "sequencing inversion"). Enforce a HARD rule: **every cycle MUST contain at least one Large or XL strategically-aligned P1 task, UNLESS no unblocked L/XL P1 exists on the board.** Before finalising the selection, scan the candidate pool: if the selected cycle contains zero L/XL tasks AND \u22651 unblocked L/XL P1 exists in the backlog, you MUST force-include the highest-impact such task \u2014 displacing the lowest-impact small task if the budget requires it (respect the single-theme vs mixed-theme budget from the Cycle-sizing rule below rather than always adding on top of an already-full cycle). Record the outcome in a "Large-task guarantee:" line in \`cycleLogNotes\`: name the task the guarantee pulled in, OR "satisfied \u2014 cycle already contains an L/XL task", OR "no unblocked L/XL P1 on the board". Do NOT skip this because the small tasks look individually attractive \u2014 that attractiveness IS the bias.
|
|
347
348
|
**Blocked tasks:** Tasks with status "Blocked" MUST be skipped during task selection \u2014 they are waiting on external dependencies or gates and cannot be built. Do NOT generate BUILD HANDOFFs for blocked tasks. Do NOT recommend blocked tasks. If a blocked task's gate has been resolved (check the notes and recent build reports), emit a \`boardCorrections\` entry to move it back to Backlog. Report blocked task count in the cycle log.
|
|
348
349
|
**Cycle sizing:** Size the cycle based on what the selected tasks actually require \u2014 not a fixed budget. Select the highest-priority unblocked tasks, estimate each one's effort from its scope, and let the total emerge. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 6-10 tasks. Cycles with fewer than 5 tasks require explicit justification in the cycle log \u2014 explain why more tasks could not be included. When the backlog has 10+ tasks, the cycle SHOULD have 6+ tasks \u2014 undersized cycles waste planning overhead relative to the available work. If fewer than 5 tasks qualify after filtering (blocked, deferred, raw), check Deferred tasks \u2014 some may be ready to un-defer via a \`boardCorrections\` entry. A 1-task cycle is almost never correct. Prefer grouping tasks by module or similarity \u2014 reduces context switching and enables shared branches during the build phase.
|
|
349
350
|
**Theme-driven sizing:** Single-theme cycles (all tasks in the same module or epic) can absorb 25-30 effort points because builders maintain context across tasks. Mixed-theme cycles should stay at 15-20 effort points to limit context switching. Use the theme to determine the budget, not a fixed number.
|
|
@@ -379,7 +380,7 @@ ${AD_REJECTION_RULES}
|
|
|
379
380
|
**Reference docs:** If a task's notes include a \`Reference:\` path (e.g. \`Reference: docs/architecture/papi-brain-v1.md\`), include a REFERENCE DOCS section in the BUILD HANDOFF with those paths. This tells the builder to read the referenced doc for background context before implementing. Do NOT omit or summarise the reference \u2014 pass it through so the builder can access the full document. Only tasks with explicit \`Reference:\` paths in their notes should have this section.
|
|
380
381
|
**Full notes lookup:** Notes in the Board section are truncated to 300 chars for concise task selection. When generating a BUILD HANDOFF for a task, check the "Full Notes for Candidate Tasks" section (if present in context) for that task's complete untruncated notes before writing SCOPE, SCOPE BOUNDARY, and PRE-MORTEM. Submitter context, constraints, and reasoning often live past the 300-char cutoff and must not be dropped from the handoff.
|
|
381
382
|
**Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
|
|
382
|
-
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
|
|
383
|
+
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. **If a "Recurring Learning Patterns (cross-cycle)" section is provided in the context, treat a module or theme that recurs across many cycles as a strong signal \u2014 size such tasks up (lean toward the larger estimate) and name the recurring failure mode explicitly in the pre-mortem.** Omit this section entirely for projects with fewer than 10 cycles.
|
|
383
384
|
**Intra-cycle dependency detection:** After selecting cycle tasks, check every pair for build-order dependencies. Two tasks A and B have an intra-cycle dependency when A must be built before B because B consumes an artifact A creates \u2014 e.g. A adds a new adapter method that B calls, A creates a DB migration B depends on, A introduces a new shared type B imports, A refactors a utility B modifies. Signals: same module + adjacent scope (one is "add X", another is "use X"), or notes explicitly reference the other task. For each dependency detected: (a) populate the DEPENDS ON section in the dependent task's BUILD HANDOFF with the upstream task ID(s); (b) add a \`boardCorrections\` entry for the dependent task with \`updates.dependsOn\` set to the comma-separated upstream IDs \u2014 this persists the dependency so the builder's runtime can reuse the upstream branch; (c) keep SCOPE sections independent but note the ordering in "Why now". Do NOT invent dependencies where tasks merely share a module \u2014 only real build-order coupling counts. Linear chains only \u2014 no multi-level graph resolution. When in doubt, omit.
|
|
384
385
|
**Dependency Chain section (Part 1 markdown):** When intra-cycle dependencies are detected, include a visible **## Dependency Chain** section in Part 1 markdown immediately before the first BUILD HANDOFF block. List each dependency as an arrow chain with a brief reason: \`task-A \u2192 task-B (B calls the adapter method A creates)\`. Then show the full recommended build sequence for all cycle tasks, including standalone tasks: e.g. \`Build order: task-A \u2192 task-B; task-C standalone; task-D standalone\`. Flag circular dependencies with \u26A0\uFE0F and a note. Omit this section entirely when no intra-cycle dependencies exist \u2014 do not include an empty section.
|
|
385
386
|
**Build order in cycle log:** If intra-cycle dependencies were detected, include a "Build order:" line in \`cycleLogNotes\` showing the recommended sequence as arrow chains (e.g. "Build order: task-123 \u2192 task-124; task-130 standalone"). Skip when no dependencies exist.
|
|
@@ -403,7 +404,7 @@ ${AD_REJECTION_RULES}
|
|
|
403
404
|
- **Architecture Notes:** If a pattern was established that needs follow-up (e.g. "shared service layer created, MCP migration needed"), propose the follow-up.
|
|
404
405
|
- **Strategy gaps:** If an Active Decision has no board tasks supporting it, propose one.
|
|
405
406
|
- **Dogfood observations:** Unactioned dogfood entries span FOUR categories (friction, methodology, signal, commercial) \u2014 consider ALL of them, not just friction. If an entry (with ID) maps to no existing task, propose one, matching task type to category: friction/signal \u2192 fix or improvement; methodology \u2192 process/tooling change; commercial \u2192 GTM/positioning. **CRITICAL: Include \`dogfood:<uuid>\` in the new task's \`notes\` field** (e.g. \`"notes": "dogfood:abc12345-..."\`). This links the task to the observation so the pipeline can track what was actioned. Without this annotation, the observation stays unactioned forever.
|
|
406
|
-
Create new tasks via the \`newTasks\` array in Part 2.
|
|
407
|
+
Create new tasks via the \`newTasks\` array in Part 2. Give each a unique \`tempId\` (\`"new-1"\`, \`"new-2"\`, \u2026) and set the matching \`cycleHandoffs\` \`taskId\` to that exact tempId (the apply step joins on tempId \u2014 array order is NOT used). **New-task cap \u2014 scale it to backlog depth, do NOT hard-cap at 3:** when 5+ unblocked backlog candidates already exist, limit new tasks to 3 (there is plenty to select from, so prevent backlog bloat). When the backlog is thin (fewer than 5 unblocked candidates), create as many as needed to bring the cycle into the healthy 5-8 range \u2014 roughly \`6 \u2212 unblocked_candidates\` additional new tasks, derived from the brief, roadmap phases, and recent build reports \u2014 up to an absolute ceiling of **8 new tasks**. This removes the contradiction with the Cycle-sizing rule above (a healthy cycle is 6-10 tasks; <5 needs justification), which the old flat cap of 3 violated whenever the backlog was thin. Never invent filler to hit a number: every new task must trace to a real discovered issue, dogfood entry, roadmap phase, or brief item.
|
|
407
408
|
**\u26A0\uFE0F DUPLICATE CHECK:** Before adding a task to \`newTasks\`, scan the Cycle Board above for any existing task with the same or very similar title/scope. If a matching task already exists (even with slightly different wording), do NOT create a duplicate \u2014 reference the existing task ID instead. The board already contains all active tasks; re-creating them wastes IDs and bloats the board.
|
|
408
409
|
**\u26A0\uFE0F ALREADY-BUILT CHECK:** Before creating a task, check the recent build reports and cycle log for evidence that this capability was already shipped. If a recent build report shows this feature was completed (even under a different task name), do NOT create a new task for it. This is especially important for UI features, data models, and integrations that may already exist.`);
|
|
409
410
|
parts.push(PLAN_FRAGMENT_PRODUCT_BRIEF);
|
|
@@ -523,6 +524,26 @@ function buildPlanUserMessage(ctx) {
|
|
|
523
524
|
if (ctx.buildPatterns) {
|
|
524
525
|
parts.push("### Build Patterns", "", ctx.buildPatterns, "");
|
|
525
526
|
}
|
|
527
|
+
if (ctx.learningPatterns) {
|
|
528
|
+
parts.push(
|
|
529
|
+
"### Recurring Learning Patterns (cross-cycle)",
|
|
530
|
+
"",
|
|
531
|
+
"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.",
|
|
532
|
+
"",
|
|
533
|
+
ctx.learningPatterns,
|
|
534
|
+
""
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
if (ctx.decisionScorePatterns) {
|
|
538
|
+
parts.push(
|
|
539
|
+
"### Decision Risk Scores (high-risk + movement)",
|
|
540
|
+
"",
|
|
541
|
+
"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.",
|
|
542
|
+
"",
|
|
543
|
+
ctx.decisionScorePatterns,
|
|
544
|
+
""
|
|
545
|
+
);
|
|
546
|
+
}
|
|
526
547
|
if (ctx.reviewPatterns) {
|
|
527
548
|
parts.push("### Review Patterns", "", ctx.reviewPatterns, "");
|
|
528
549
|
}
|
|
@@ -659,6 +680,8 @@ function coerceStructuredOutput(parsed) {
|
|
|
659
680
|
buildHandoff: coerceToString(h.buildHandoff)
|
|
660
681
|
})) : [];
|
|
661
682
|
const newTasks = Array.isArray(parsed.newTasks) ? parsed.newTasks.map((t) => ({
|
|
683
|
+
// task-2242: stable join key (optional — undefined falls back to index).
|
|
684
|
+
tempId: t.tempId !== void 0 && t.tempId !== null ? coerceToString(t.tempId) : void 0,
|
|
662
685
|
title: coerceToString(t.title),
|
|
663
686
|
status: coerceToString(t.status),
|
|
664
687
|
priority: coerceToString(t.priority),
|
|
@@ -987,6 +1010,16 @@ function buildReviewUserMessage(ctx) {
|
|
|
987
1010
|
if (ctx.decisionUsage) {
|
|
988
1011
|
parts.push("### Decision Usage (Reference Frequency)", "", ctx.decisionUsage, "");
|
|
989
1012
|
}
|
|
1013
|
+
if (ctx.decisionScoreTrajectory) {
|
|
1014
|
+
parts.push(
|
|
1015
|
+
"### Decision Risk Trajectory (score movement)",
|
|
1016
|
+
"",
|
|
1017
|
+
"Decision risk scores (/25, lower = safer) that are high-risk (>15) or have moved since they were last scored. Call out which decisions got riskier (\u2191) and whether their confidence still holds; consider re-scoring or revisiting a decision whose risk is rising.",
|
|
1018
|
+
"",
|
|
1019
|
+
ctx.decisionScoreTrajectory,
|
|
1020
|
+
""
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
990
1023
|
if (ctx.recommendationEffectiveness) {
|
|
991
1024
|
parts.push("### Recommendation Follow-Through", "", ctx.recommendationEffectiveness, "");
|
|
992
1025
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.37",
|
|
4
4
|
"description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"mcpName": "io.github.cathalos92/papi",
|