@papi-ai/server 0.7.36 → 0.7.38
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 +281 -16
- package/dist/prompts.js +37 -5
- 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);
|
|
@@ -7087,6 +7114,9 @@ ${footer}`);
|
|
|
7087
7114
|
await this.write("PLANNING_LOG.md", updated);
|
|
7088
7115
|
}
|
|
7089
7116
|
};
|
|
7117
|
+
function newTaskJoinKey(task, index) {
|
|
7118
|
+
return task.tempId ?? `new-${index + 1}`;
|
|
7119
|
+
}
|
|
7090
7120
|
var NONE_PATTERN2 = /^none\b/i;
|
|
7091
7121
|
function normalizeText2(text) {
|
|
7092
7122
|
return text.trim().toLowerCase().replace(/[.,;:!]+$/, "").replace(/\s+/g, " ");
|
|
@@ -7981,6 +8011,39 @@ function formatDerivedMetrics(snapshots, backlogTasks) {
|
|
|
7981
8011
|
}
|
|
7982
8012
|
return lines.join("\n");
|
|
7983
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
|
+
}
|
|
7984
8047
|
function formatBuildPatterns(patterns) {
|
|
7985
8048
|
const sections = [];
|
|
7986
8049
|
if (patterns.recurringSurprises.length > 0) {
|
|
@@ -8253,7 +8316,7 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
8253
8316
|
"boardHealth": "string \u2014 e.g. 5 tasks (3 backlog, 2 done)",
|
|
8254
8317
|
"strategicDirection": "string \u2014 one sentence about current phase/direction",
|
|
8255
8318
|
"recommendedTaskId": "string or null \u2014 task ID to set In Progress",
|
|
8256
|
-
"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"}],
|
|
8257
8320
|
"newTasks": [],
|
|
8258
8321
|
"boardCorrections": [],
|
|
8259
8322
|
"productBrief": null,
|
|
@@ -8298,7 +8361,7 @@ Everything in Part 1 (natural language) is **display-only**. Part 2 (structured
|
|
|
8298
8361
|
**Example with populated fields (DO NOT copy literally \u2014 adapt to your actual analysis):**
|
|
8299
8362
|
\`\`\`json
|
|
8300
8363
|
{
|
|
8301
|
-
"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"}],
|
|
8302
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."}],
|
|
8303
8366
|
"boardCorrections": [{"taskId": "task-005", "updates": {"priority": "P1 High", "reviewed": true}}, {"taskId": "task-009", "updates": {"status": "Cancelled", "closureReason": "Duplicates task-003"}}],
|
|
8304
8367
|
"cycleHandoffs": [{"taskId": "task-002", "buildHandoff": "BUILD HANDOFF \u2014 task-002\\nTask: ..."}]
|
|
@@ -8336,7 +8399,7 @@ This is Cycle 0 \u2014 the first planning cycle for a brand-new project.
|
|
|
8336
8399
|
|
|
8337
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.
|
|
8338
8401
|
|
|
8339
|
-
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.
|
|
8340
8403
|
|
|
8341
8404
|
### Structured output for Bootstrap:
|
|
8342
8405
|
In the JSON block, you MUST include:
|
|
@@ -8544,7 +8607,7 @@ ${AD_REJECTION_RULES}
|
|
|
8544
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.
|
|
8545
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.
|
|
8546
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.
|
|
8547
|
-
**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.
|
|
8548
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.
|
|
8549
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.
|
|
8550
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.
|
|
@@ -8568,7 +8631,7 @@ ${AD_REJECTION_RULES}
|
|
|
8568
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.
|
|
8569
8632
|
- **Strategy gaps:** If an Active Decision has no board tasks supporting it, propose one.
|
|
8570
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.
|
|
8571
|
-
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.
|
|
8572
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.
|
|
8573
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.`);
|
|
8574
8637
|
parts.push(PLAN_FRAGMENT_PRODUCT_BRIEF);
|
|
@@ -8688,6 +8751,26 @@ function buildPlanUserMessage(ctx) {
|
|
|
8688
8751
|
if (ctx.buildPatterns) {
|
|
8689
8752
|
parts.push("### Build Patterns", "", ctx.buildPatterns, "");
|
|
8690
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
|
+
}
|
|
8691
8774
|
if (ctx.reviewPatterns) {
|
|
8692
8775
|
parts.push("### Review Patterns", "", ctx.reviewPatterns, "");
|
|
8693
8776
|
}
|
|
@@ -8824,6 +8907,8 @@ function coerceStructuredOutput(parsed) {
|
|
|
8824
8907
|
buildHandoff: coerceToString(h.buildHandoff)
|
|
8825
8908
|
})) : [];
|
|
8826
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,
|
|
8827
8912
|
title: coerceToString(t.title),
|
|
8828
8913
|
status: coerceToString(t.status),
|
|
8829
8914
|
priority: coerceToString(t.priority),
|
|
@@ -9152,6 +9237,16 @@ function buildReviewUserMessage(ctx) {
|
|
|
9152
9237
|
if (ctx.decisionUsage) {
|
|
9153
9238
|
parts.push("### Decision Usage (Reference Frequency)", "", ctx.decisionUsage, "");
|
|
9154
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
|
+
}
|
|
9155
9250
|
if (ctx.recommendationEffectiveness) {
|
|
9156
9251
|
parts.push("### Recommendation Follow-Through", "", ctx.recommendationEffectiveness, "");
|
|
9157
9252
|
}
|
|
@@ -10290,6 +10385,8 @@ function applyContextTier(ctx, cycleCount) {
|
|
|
10290
10385
|
ctx.discoveryCanvas = void 0;
|
|
10291
10386
|
ctx.estimationCalibration = void 0;
|
|
10292
10387
|
ctx.buildPatterns = void 0;
|
|
10388
|
+
ctx.learningPatterns = void 0;
|
|
10389
|
+
ctx.decisionScorePatterns = void 0;
|
|
10293
10390
|
ctx.reviewPatterns = void 0;
|
|
10294
10391
|
ctx.horizonContext = void 0;
|
|
10295
10392
|
ctx.registeredDocs = void 0;
|
|
@@ -10792,7 +10889,9 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
10792
10889
|
preAssignedResult,
|
|
10793
10890
|
reportsForCapsResult,
|
|
10794
10891
|
contextHashesResult,
|
|
10795
|
-
doneTasksResult
|
|
10892
|
+
doneTasksResult,
|
|
10893
|
+
learningPatternsResult,
|
|
10894
|
+
decisionScoresResult
|
|
10796
10895
|
] = await Promise.allSettled([
|
|
10797
10896
|
detectReviewPatterns(reviews2, health.totalCycles, 5),
|
|
10798
10897
|
adapter2.getPendingRecommendations(),
|
|
@@ -10807,7 +10906,11 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
10807
10906
|
adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready"], compact: true }),
|
|
10808
10907
|
Promise.resolve(leanBuildReports.slice(0, 10)),
|
|
10809
10908
|
adapter2.getContextHashes?.(health.totalCycles) ?? Promise.resolve(null),
|
|
10810
|
-
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([])
|
|
10811
10914
|
]);
|
|
10812
10915
|
timings["parallelReads"] = t();
|
|
10813
10916
|
let reviewPatternsText2;
|
|
@@ -10837,6 +10940,8 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
10837
10940
|
estimationCalibrationText = (estimationCalibrationText ?? "") + moduleLines;
|
|
10838
10941
|
}
|
|
10839
10942
|
}
|
|
10943
|
+
const learningPatternsText = learningPatternsResult.status === "fulfilled" ? formatLearningPatterns(learningPatternsResult.value) : void 0;
|
|
10944
|
+
const decisionScorePatternsText = decisionScoresResult.status === "fulfilled" ? formatDecisionScorePatterns(decisionScoresResult.value) : void 0;
|
|
10840
10945
|
let registeredDocsText;
|
|
10841
10946
|
let unactionedDocsTextLean;
|
|
10842
10947
|
if (docsResult.status === "fulfilled" && docsResult.value && docsResult.value.length > 0) {
|
|
@@ -10897,6 +11002,8 @@ ${lines.join("\n")}`;
|
|
|
10897
11002
|
taskComments: taskCommentsText,
|
|
10898
11003
|
discoveryCanvas: discoveryCanvasText,
|
|
10899
11004
|
estimationCalibration: estimationCalibrationText,
|
|
11005
|
+
learningPatterns: learningPatternsText,
|
|
11006
|
+
decisionScorePatterns: decisionScorePatternsText,
|
|
10900
11007
|
registeredDocs: registeredDocsText,
|
|
10901
11008
|
unactionedDocs: unactionedDocsTextLean,
|
|
10902
11009
|
focus,
|
|
@@ -10948,7 +11055,9 @@ ${lines.join("\n")}`;
|
|
|
10948
11055
|
taskCommentsResultFull,
|
|
10949
11056
|
docsResultFull,
|
|
10950
11057
|
contextHashesResultFull,
|
|
10951
|
-
doneTasksResultFull
|
|
11058
|
+
doneTasksResultFull,
|
|
11059
|
+
learningPatternsResultFull,
|
|
11060
|
+
decisionScoresResultFull
|
|
10952
11061
|
] = await Promise.allSettled([
|
|
10953
11062
|
Promise.resolve(allBuildReports),
|
|
10954
11063
|
detectReviewPatterns(reviews, health.totalCycles, 5),
|
|
@@ -10957,7 +11066,11 @@ ${lines.join("\n")}`;
|
|
|
10957
11066
|
assembleTaskComments(adapter2),
|
|
10958
11067
|
adapter2.searchDocs?.({ status: "active", limit: 15 }),
|
|
10959
11068
|
adapter2.getContextHashes?.(health.totalCycles) ?? Promise.resolve(null),
|
|
10960
|
-
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([])
|
|
10961
11074
|
]);
|
|
10962
11075
|
timings["parallelAdvisory"] = t();
|
|
10963
11076
|
let buildPatternsText;
|
|
@@ -10970,6 +11083,8 @@ ${lines.join("\n")}`;
|
|
|
10970
11083
|
} catch {
|
|
10971
11084
|
}
|
|
10972
11085
|
}
|
|
11086
|
+
const learningPatternsTextFull = learningPatternsResultFull.status === "fulfilled" ? formatLearningPatterns(learningPatternsResultFull.value) : void 0;
|
|
11087
|
+
const decisionScorePatternsTextFull = decisionScoresResultFull.status === "fulfilled" ? formatDecisionScorePatterns(decisionScoresResultFull.value) : void 0;
|
|
10973
11088
|
let reviewPatternsText;
|
|
10974
11089
|
if (reviewPatternsResultFull.status === "fulfilled") {
|
|
10975
11090
|
reviewPatternsText = hasReviewPatterns(reviewPatternsResultFull.value) ? formatReviewPatterns(reviewPatternsResultFull.value) : void 0;
|
|
@@ -11073,6 +11188,8 @@ ${logLines}`);
|
|
|
11073
11188
|
methodologyMetrics: formatCycleMetrics(metricsSnapshots),
|
|
11074
11189
|
recentReviews: formatReviews(reviews),
|
|
11075
11190
|
buildPatterns: buildPatternsText,
|
|
11191
|
+
learningPatterns: learningPatternsTextFull,
|
|
11192
|
+
decisionScorePatterns: decisionScorePatternsTextFull,
|
|
11076
11193
|
reviewPatterns: reviewPatternsText,
|
|
11077
11194
|
horizonContext: horizonCtx,
|
|
11078
11195
|
strategyRecommendations: strategyRecommendationsText,
|
|
@@ -11204,6 +11321,8 @@ ${cleanContent}`;
|
|
|
11204
11321
|
strategicDirection: data.strategicDirection
|
|
11205
11322
|
},
|
|
11206
11323
|
newTasks: dedupedNewTasks.map((t) => ({
|
|
11324
|
+
tempId: t.tempId,
|
|
11325
|
+
// task-2242: stable handoff join key
|
|
11207
11326
|
title: t.title,
|
|
11208
11327
|
status: t.status || "Backlog",
|
|
11209
11328
|
priority: t.priority || "P1 High",
|
|
@@ -11353,7 +11472,7 @@ ${cleanContent}`;
|
|
|
11353
11472
|
visibility,
|
|
11354
11473
|
ownerUserId: inheritedVis.ownerUserId
|
|
11355
11474
|
});
|
|
11356
|
-
newTaskIdMap.set(
|
|
11475
|
+
newTaskIdMap.set(newTaskJoinKey(task, i), created.id);
|
|
11357
11476
|
if (adapter2.updateCycleLearningActionRef && task.notes) {
|
|
11358
11477
|
const learningRefs = task.notes.match(/learning:([a-f0-9-]+)/gi);
|
|
11359
11478
|
if (learningRefs) {
|
|
@@ -11512,6 +11631,13 @@ ${cleanContent}`;
|
|
|
11512
11631
|
}
|
|
11513
11632
|
for (const handoff of data.cycleHandoffs) {
|
|
11514
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
|
+
}
|
|
11515
11641
|
try {
|
|
11516
11642
|
if (existingHandoffSet.has(resolvedId)) {
|
|
11517
11643
|
console.error(`[plan] skipping handoff write for ${resolvedId} \u2014 already has one`);
|
|
@@ -13469,7 +13595,8 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
13469
13595
|
recData,
|
|
13470
13596
|
pendingRecs,
|
|
13471
13597
|
registeredDocs,
|
|
13472
|
-
docsWithPendingActions
|
|
13598
|
+
docsWithPendingActions,
|
|
13599
|
+
decisionScores
|
|
13473
13600
|
] = await Promise.all([
|
|
13474
13601
|
adapter2.readProductBrief(),
|
|
13475
13602
|
// Strategy review needs to see retired ADs to triage/restore them as needed.
|
|
@@ -13496,7 +13623,9 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
13496
13623
|
// Doc registry — summaries for strategy review context
|
|
13497
13624
|
adapter2.searchDocs?.({ status: "active", limit: 10 })?.catch(() => []) ?? Promise.resolve([]),
|
|
13498
13625
|
// Doc registry — docs with pending actions for staleness audit
|
|
13499
|
-
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([])
|
|
13500
13629
|
]);
|
|
13501
13630
|
const pendingAgendaTopics = await (adapter2.getPendingAgendaTopics?.().catch(() => []) ?? Promise.resolve([]));
|
|
13502
13631
|
const tasks = [...activeTasks, ...recentDoneTasks];
|
|
@@ -13777,6 +13906,7 @@ ${lines.join("\n")}`;
|
|
|
13777
13906
|
previousReviews: previousReviewsText,
|
|
13778
13907
|
phases: phasesText,
|
|
13779
13908
|
decisionUsage: decisionUsageText,
|
|
13909
|
+
decisionScoreTrajectory: formatDecisionScorePatterns(decisionScores),
|
|
13780
13910
|
northStar: currentNorthStar ?? void 0,
|
|
13781
13911
|
recommendationEffectiveness: recEffectivenessText,
|
|
13782
13912
|
adHocCommits: adHocCommitsText,
|
|
@@ -16874,6 +17004,10 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
16874
17004
|
priority: task.priority || "P2 Medium",
|
|
16875
17005
|
complexity: task.complexity || "Small",
|
|
16876
17006
|
module: task.module || "Core",
|
|
17007
|
+
// epic is NOT NULL in cycle_tasks; default starter tasks to the
|
|
17008
|
+
// 'Platform' epic (matches the idea tool) rather than leaving it
|
|
17009
|
+
// unset (SUP-2026-020 — omitting it tripped a 23502 on every seed).
|
|
17010
|
+
epic: task.epic || "Platform",
|
|
16877
17011
|
phase: task.phase || "Phase 1",
|
|
16878
17012
|
owner: "TBD",
|
|
16879
17013
|
reviewed: false,
|
|
@@ -17566,12 +17700,60 @@ ${existing}`;
|
|
|
17566
17700
|
}
|
|
17567
17701
|
await writeFile3(changelogPath, body, "utf-8");
|
|
17568
17702
|
}
|
|
17569
|
-
function
|
|
17703
|
+
async function allCancelledBranches(adapter2, cycleNum) {
|
|
17704
|
+
const skip = /* @__PURE__ */ new Set();
|
|
17705
|
+
try {
|
|
17706
|
+
const ALL_STATUSES = [
|
|
17707
|
+
"Backlog",
|
|
17708
|
+
"In Cycle",
|
|
17709
|
+
"Ready",
|
|
17710
|
+
"In Progress",
|
|
17711
|
+
"In Review",
|
|
17712
|
+
"Done",
|
|
17713
|
+
"Blocked",
|
|
17714
|
+
"Cancelled",
|
|
17715
|
+
"Deferred"
|
|
17716
|
+
];
|
|
17717
|
+
const tasks = await adapter2.queryBoard({ cycleSince: cycleNum, status: ALL_STATUSES, compact: true });
|
|
17718
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
17719
|
+
for (const t of tasks) {
|
|
17720
|
+
if (t.cycle !== cycleNum) continue;
|
|
17721
|
+
const moduleName = (t.module ?? "").trim();
|
|
17722
|
+
if (!moduleName) continue;
|
|
17723
|
+
const entry = byModule.get(moduleName) ?? { total: 0, cancelled: 0 };
|
|
17724
|
+
entry.total++;
|
|
17725
|
+
if (t.status === "Cancelled") entry.cancelled++;
|
|
17726
|
+
byModule.set(moduleName, entry);
|
|
17727
|
+
}
|
|
17728
|
+
for (const [moduleName, entry] of byModule) {
|
|
17729
|
+
if (entry.total > 0 && entry.cancelled === entry.total) {
|
|
17730
|
+
skip.add(cycleBranchName(cycleNum, moduleName));
|
|
17731
|
+
}
|
|
17732
|
+
}
|
|
17733
|
+
} catch {
|
|
17734
|
+
}
|
|
17735
|
+
return skip;
|
|
17736
|
+
}
|
|
17737
|
+
function mergeGroupedCycleBranches(config2, cycleNum, baseBranch, skipBranches = /* @__PURE__ */ new Set()) {
|
|
17570
17738
|
const branches = listGroupedCycleBranches(config2.projectRoot, cycleNum, baseBranch);
|
|
17571
17739
|
if (branches.length === 0) return [];
|
|
17572
17740
|
const ghAvailable = isGhAvailable();
|
|
17573
17741
|
const results = [];
|
|
17574
17742
|
for (const branch of branches) {
|
|
17743
|
+
if (skipBranches.has(branch)) {
|
|
17744
|
+
results.push({ branch, prUrl: null, status: "skipped", message: "all cycle tasks Cancelled \u2014 branch not merged (task-2253)" });
|
|
17745
|
+
continue;
|
|
17746
|
+
}
|
|
17747
|
+
if (isBranchContentAlreadyInBase(config2.projectRoot, branch, baseBranch)) {
|
|
17748
|
+
deleteLocalBranch(config2.projectRoot, branch);
|
|
17749
|
+
results.push({
|
|
17750
|
+
branch,
|
|
17751
|
+
prUrl: null,
|
|
17752
|
+
status: "skipped",
|
|
17753
|
+
message: `content already on '${baseBranch}' (tree-equal \u2014 squash-merged); lingering branch cleaned, no re-merge`
|
|
17754
|
+
});
|
|
17755
|
+
continue;
|
|
17756
|
+
}
|
|
17575
17757
|
if (branchExists(config2.projectRoot, branch) && hasRemote(config2.projectRoot)) {
|
|
17576
17758
|
const push = gitPush(config2.projectRoot, branch);
|
|
17577
17759
|
if (!push.success) {
|
|
@@ -17794,7 +17976,8 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
|
|
|
17794
17976
|
}
|
|
17795
17977
|
let groupedBranchMerges;
|
|
17796
17978
|
if (resolvedCycleNum > 0) {
|
|
17797
|
-
const
|
|
17979
|
+
const skipBranches = adapter2 ? await allCancelledBranches(adapter2, resolvedCycleNum) : /* @__PURE__ */ new Set();
|
|
17980
|
+
const mergeResults = mergeGroupedCycleBranches(config2, resolvedCycleNum, branch, skipBranches);
|
|
17798
17981
|
if (mergeResults.length > 0) {
|
|
17799
17982
|
groupedBranchMerges = mergeResults;
|
|
17800
17983
|
const failures = mergeResults.filter((r) => r.status === "failed");
|
|
@@ -18598,6 +18781,23 @@ If you intend to discard the work, run build_cancel instead.`
|
|
|
18598
18781
|
err.gitStderr = scrubbed;
|
|
18599
18782
|
return err;
|
|
18600
18783
|
}
|
|
18784
|
+
var UI_FILE_RE = /^(app\/.+\.(tsx|css)|components\/.+\.tsx)$/;
|
|
18785
|
+
var NON_UI_RE = /(^app\/api\/|\.test\.|\.spec\.|\/__tests__\/)/;
|
|
18786
|
+
function inferRouteFromPage(file) {
|
|
18787
|
+
const m = file.match(/^app\/(.*)\/page\.tsx$/);
|
|
18788
|
+
if (!m) return null;
|
|
18789
|
+
const segments = m[1].split("/").filter((s) => s.length > 0 && !(s.startsWith("(") && s.endsWith(")")));
|
|
18790
|
+
return "/" + segments.join("/");
|
|
18791
|
+
}
|
|
18792
|
+
function computeLocalPreview(filesChanged, provided) {
|
|
18793
|
+
const uiFiles = (filesChanged ?? []).filter((f) => UI_FILE_RE.test(f) && !NON_UI_RE.test(f));
|
|
18794
|
+
const hasProvided = !!(provided && (provided.urls?.length || provided.screenshotPath || provided.notes));
|
|
18795
|
+
if (uiFiles.length === 0 && !hasProvided) return void 0;
|
|
18796
|
+
const routes = Array.from(
|
|
18797
|
+
new Set(uiFiles.map(inferRouteFromPage).filter((r) => r !== null))
|
|
18798
|
+
);
|
|
18799
|
+
return { uiFiles, routes, provided: hasProvided ? provided : void 0 };
|
|
18800
|
+
}
|
|
18601
18801
|
var AMBIENT_DRIFT_PATTERNS = [
|
|
18602
18802
|
/^\.next\//,
|
|
18603
18803
|
/^\.next$/,
|
|
@@ -19274,6 +19474,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
19274
19474
|
const drift = computeScopeDriftSignal(task.buildHandoff?.filesLikelyTouched, driftChanged);
|
|
19275
19475
|
if (drift) report.scopeDriftSignal = drift;
|
|
19276
19476
|
}
|
|
19477
|
+
const localPreview = computeLocalPreview(report.filesChanged, input.preview);
|
|
19277
19478
|
let prLines = [];
|
|
19278
19479
|
if (options.light) {
|
|
19279
19480
|
prLines.push("Light mode: skipping push and PR creation.");
|
|
@@ -19376,7 +19577,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
19376
19577
|
autoTriagedCount: autoTriagedCount > 0 ? autoTriagedCount : void 0,
|
|
19377
19578
|
autoTriagedIds: autoTriagedIds.length > 0 ? autoTriagedIds : void 0,
|
|
19378
19579
|
autoTriagedDupes: autoTriagedDupes.length > 0 ? autoTriagedDupes : void 0,
|
|
19379
|
-
reportWriteVerified
|
|
19580
|
+
reportWriteVerified,
|
|
19581
|
+
localPreview
|
|
19380
19582
|
};
|
|
19381
19583
|
}
|
|
19382
19584
|
async function cancelBuild(adapter2, taskId, reason) {
|
|
@@ -19596,6 +19798,15 @@ var buildExecuteTool = {
|
|
|
19596
19798
|
verified_at: { type: "string", description: "ISO 8601 timestamp." }
|
|
19597
19799
|
},
|
|
19598
19800
|
required: ["urls", "curl_command", "http_status", "response_excerpt", "verified_at"]
|
|
19801
|
+
},
|
|
19802
|
+
preview: {
|
|
19803
|
+
type: "object",
|
|
19804
|
+
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.`,
|
|
19805
|
+
properties: {
|
|
19806
|
+
urls: { type: "array", items: { type: "string" }, description: "Localhost route(s) to open to view the work." },
|
|
19807
|
+
notes: { type: "string", description: "What the owner should look at / verify." },
|
|
19808
|
+
screenshot_path: { type: "string", description: "Path to a captured screenshot of the rendered result." }
|
|
19809
|
+
}
|
|
19599
19810
|
}
|
|
19600
19811
|
},
|
|
19601
19812
|
required: ["task_id"]
|
|
@@ -19873,6 +20084,12 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
19873
20084
|
const deadEnds = args.dead_ends;
|
|
19874
20085
|
const rawBriefImplications = args.brief_implications;
|
|
19875
20086
|
const resolvesLearnings = Array.isArray(args.resolves_learnings) ? args.resolves_learnings : void 0;
|
|
20087
|
+
const rawPreview = args.preview;
|
|
20088
|
+
const preview = rawPreview ? {
|
|
20089
|
+
urls: Array.isArray(rawPreview.urls) ? rawPreview.urls.filter((u) => typeof u === "string") : void 0,
|
|
20090
|
+
notes: typeof rawPreview.notes === "string" ? rawPreview.notes : void 0,
|
|
20091
|
+
screenshotPath: typeof rawPreview.screenshot_path === "string" ? rawPreview.screenshot_path : void 0
|
|
20092
|
+
} : void 0;
|
|
19876
20093
|
const rawProductionVerification = args.production_verification;
|
|
19877
20094
|
const productionVerification = rawProductionVerification ? {
|
|
19878
20095
|
urls: Array.isArray(rawProductionVerification.urls) ? rawProductionVerification.urls.filter((u) => typeof u === "string") : [],
|
|
@@ -19929,7 +20146,8 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
19929
20146
|
type: bi.type ?? "new",
|
|
19930
20147
|
detail: bi.detail ?? ""
|
|
19931
20148
|
})),
|
|
19932
|
-
productionVerification
|
|
20149
|
+
productionVerification,
|
|
20150
|
+
preview
|
|
19933
20151
|
}, { light });
|
|
19934
20152
|
tracker.mark("complete_format");
|
|
19935
20153
|
if (resolvesLearnings && resolvesLearnings.length > 0 && adapter2.updateCycleLearningActionRef) {
|
|
@@ -19986,6 +20204,26 @@ function formatCompleteResult(result) {
|
|
|
19986
20204
|
if (result.cycleProgress.total > 0) {
|
|
19987
20205
|
lines.push("", `Cycle progress: ${result.cycleProgress.completed} of ${result.cycleProgress.total} cycle tasks complete.`);
|
|
19988
20206
|
}
|
|
20207
|
+
if (result.localPreview) {
|
|
20208
|
+
const { routes, provided, uiFiles } = result.localPreview;
|
|
20209
|
+
const previewLines = ["", "### \u{1F441} Preview the work"];
|
|
20210
|
+
if (provided?.urls?.length) {
|
|
20211
|
+
previewLines.push(`Open: ${provided.urls.join(" \xB7 ")}`);
|
|
20212
|
+
} else if (routes.length > 0) {
|
|
20213
|
+
const urls = routes.map((r) => `http://localhost:3000${r}`);
|
|
20214
|
+
previewLines.push(`Run \`npm run dev\` and open: ${urls.join(" \xB7 ")}`);
|
|
20215
|
+
} else {
|
|
20216
|
+
previewLines.push(
|
|
20217
|
+
`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.`
|
|
20218
|
+
);
|
|
20219
|
+
}
|
|
20220
|
+
if (provided?.screenshotPath) previewLines.push(`Screenshot: ${provided.screenshotPath}`);
|
|
20221
|
+
if (provided?.notes) previewLines.push(`Look at: ${provided.notes}`);
|
|
20222
|
+
if (!provided) {
|
|
20223
|
+
previewLines.push("_Tip: pass `preview` (the route you viewed / a screenshot) on complete so the result travels with the build._");
|
|
20224
|
+
}
|
|
20225
|
+
lines.push(...previewLines);
|
|
20226
|
+
}
|
|
19989
20227
|
if (result.phaseChanges.length > 0) {
|
|
19990
20228
|
for (const c of result.phaseChanges) {
|
|
19991
20229
|
lines.push(`Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`);
|
|
@@ -26652,6 +26890,8 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
26652
26890
|
var BEARER_PREFIX = "papi_";
|
|
26653
26891
|
var BEARER_REGEX = /^(papi_|papi_oauth_)[a-f0-9]{64}$/;
|
|
26654
26892
|
var RESOURCE_METADATA_URL = process.env["PAPI_RESOURCE_METADATA_URL"] ?? "https://getpapi.ai/.well-known/oauth-protected-resource";
|
|
26893
|
+
var DASHBOARD_ORIGIN = process.env["PAPI_DASHBOARD_URL"] ?? "https://getpapi.ai";
|
|
26894
|
+
var MCP_RESOURCE_URL = process.env["NEXT_PUBLIC_MCP_URL"] ?? "https://mcp.getpapi.ai";
|
|
26655
26895
|
var FRIENDLY_GET_HTML = `<!doctype html>
|
|
26656
26896
|
<html lang="en"><head><meta charset="utf-8">
|
|
26657
26897
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -26779,6 +27019,31 @@ function startHttpTransport(opts) {
|
|
|
26779
27019
|
res.end("ok");
|
|
26780
27020
|
return;
|
|
26781
27021
|
}
|
|
27022
|
+
if (req.method === "GET" && req.url === "/.well-known/oauth-protected-resource") {
|
|
27023
|
+
res.writeHead(200, {
|
|
27024
|
+
"Content-Type": "application/json",
|
|
27025
|
+
"Cache-Control": "public, max-age=3600",
|
|
27026
|
+
...cors
|
|
27027
|
+
});
|
|
27028
|
+
res.end(
|
|
27029
|
+
JSON.stringify({
|
|
27030
|
+
resource: MCP_RESOURCE_URL,
|
|
27031
|
+
authorization_servers: [DASHBOARD_ORIGIN],
|
|
27032
|
+
scopes_supported: ["mcp:full"],
|
|
27033
|
+
bearer_methods_supported: ["header"]
|
|
27034
|
+
})
|
|
27035
|
+
);
|
|
27036
|
+
return;
|
|
27037
|
+
}
|
|
27038
|
+
if (req.method === "GET" && req.url === "/.well-known/oauth-authorization-server") {
|
|
27039
|
+
res.writeHead(302, {
|
|
27040
|
+
Location: `${DASHBOARD_ORIGIN}/.well-known/oauth-authorization-server`,
|
|
27041
|
+
"Cache-Control": "public, max-age=3600",
|
|
27042
|
+
...cors
|
|
27043
|
+
});
|
|
27044
|
+
res.end();
|
|
27045
|
+
return;
|
|
27046
|
+
}
|
|
26782
27047
|
if (req.url !== "/mcp" && req.url !== "/sse") {
|
|
26783
27048
|
sendError(res, { status: 404, body: { error: "Not found" } }, cors);
|
|
26784
27049
|
return;
|
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:
|
|
@@ -380,7 +380,7 @@ ${AD_REJECTION_RULES}
|
|
|
380
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.
|
|
381
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.
|
|
382
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.
|
|
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. 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.
|
|
384
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.
|
|
385
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.
|
|
386
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.
|
|
@@ -404,7 +404,7 @@ ${AD_REJECTION_RULES}
|
|
|
404
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.
|
|
405
405
|
- **Strategy gaps:** If an Active Decision has no board tasks supporting it, propose one.
|
|
406
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.
|
|
407
|
-
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.
|
|
408
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.
|
|
409
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.`);
|
|
410
410
|
parts.push(PLAN_FRAGMENT_PRODUCT_BRIEF);
|
|
@@ -524,6 +524,26 @@ function buildPlanUserMessage(ctx) {
|
|
|
524
524
|
if (ctx.buildPatterns) {
|
|
525
525
|
parts.push("### Build Patterns", "", ctx.buildPatterns, "");
|
|
526
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
|
+
}
|
|
527
547
|
if (ctx.reviewPatterns) {
|
|
528
548
|
parts.push("### Review Patterns", "", ctx.reviewPatterns, "");
|
|
529
549
|
}
|
|
@@ -660,6 +680,8 @@ function coerceStructuredOutput(parsed) {
|
|
|
660
680
|
buildHandoff: coerceToString(h.buildHandoff)
|
|
661
681
|
})) : [];
|
|
662
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,
|
|
663
685
|
title: coerceToString(t.title),
|
|
664
686
|
status: coerceToString(t.status),
|
|
665
687
|
priority: coerceToString(t.priority),
|
|
@@ -988,6 +1010,16 @@ function buildReviewUserMessage(ctx) {
|
|
|
988
1010
|
if (ctx.decisionUsage) {
|
|
989
1011
|
parts.push("### Decision Usage (Reference Frequency)", "", ctx.decisionUsage, "");
|
|
990
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
|
+
}
|
|
991
1023
|
if (ctx.recommendationEffectiveness) {
|
|
992
1024
|
parts.push("### Recommendation Follow-Through", "", ctx.recommendationEffectiveness, "");
|
|
993
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.38",
|
|
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",
|